xv6驱动开发实战:2个核心驱动(IDE磁盘 + UART串口)逐行精讲

发布时间:2026/9/19 22:19:37
xv6驱动开发实战:2个核心驱动(IDE磁盘 + UART串口)逐行精讲
xv6驱动开发实战2个核心驱动IDE磁盘 UART串口逐行精讲【免费下载链接】xv6-publicxv6 OS项目地址: https://gitcode.com/gh_mirrors/xv/xv6-publicxv6 是一个为教学而生的简化的 x86 操作系统。本文带你做 xv6 驱动开发实战逐行拆解它的 2 个最经典的内核驱动IDE 磁盘驱动与 UART 串口驱动。你将理解操作系统如何轮询寄存器、处理中断、用锁与睡眠队列协调 CPU 和硬件——这是内核开发者绕不开的基本功。先搞懂 xv6 的驱动模型 xv6 的硬件驱动遵循一套清晰的套路两个驱动都完全符合初始化main()中按顺序调用uartinit()、ideinit()配置硬件并注册中断源见 main.c#L28-L33端口 I/O所有读写通过inb/outb/insl/outsl宏函数完成底层是in/out/rep insl汇编指令x86.h#L3-L40中断分发硬件触发 IRQ →trap()统一分发到驱动的中断处理函数trap.c#L59-L71阻塞等待CPU 请求 I/O 后挂起睡眠硬件完成后中断唤醒避免忙等浪费组件磁盘路径串口路径初始化ideinit()ide.c#L50-L70uartinit()uart.c#L19-L49中断处理ideintr()← IRQ 14uartintr()← IRQ 4调用入口bread()/bwrite()cprintf()/ 控制台读入中断号定义IRQ_IDE 14IRQ_COM1 4中断号定义在 traps.h#L34-L35注册到 IOAPIC 的动作在各自的init函数中。一、IDE 磁盘驱动从块缓存到磁盘扇区磁盘是怎么被喂数据的调用链非常短文件系统层通过块缓存buffer cache发起读写 →iderw()把缓冲块挂进磁盘队列 → 硬件完成后中断唤醒调用者。块缓存bread()/bwrite()拿到缓冲块后才决定是否真正碰硬件bio.c#L96-L116缓冲块结构struct buf带B_VALID已读入与B_DIRTY需写盘两个状态位外加磁盘队列指针qnextbuf.h读懂 IDE 的寄存器与命令xv6 使用PIO程序控模式的 IDE 接口不启用 DMA。核心常量一眼看懂ide.c#L16-L25端口用途说明0x1f0数据口insl/outsl按 32 位批量搬运 512 字节扇区0x1f2~0x1f6扇区数 / LBA 地址LBA 32 位分 4 个字节口写入0x1f7命令/状态口写发命令读查状态IDE_BSY、IDE_DRDY、IDE_ERR命令只有 4 个单扇区读0x20、单扇区写0x30、多扇区读0xc4、多扇区写0xc5。idestart()如何下达一次磁盘请求idestart()是驱动的心脏ide.c#L73-L100逐行看它的逻辑idewait(0); // 1. 等磁盘就绪 outb(0x3f6, 0); // 2. 清中断状态位避免陈旧中断 outb(0x1f2, sector_per_block); // 3. 请求的扇区数 outb(0x1f3, sector 0xff); // 4~6. LBA 28 位逐字节写入 outb(0x1f4, (sector 8) 0xff); outb(0x1f5, (sector 16) 0xff); outb(0x1f6, 0xe0 | ((b-dev1)4) | ((sector24)0x0f)); // 7. 盘选择 LBA高4位 if(b-flags B_DIRTY){ outb(0x1f7, write_cmd); // 8. 先写命令…… outsl(0x1f0, b-data, BSIZE/4); // 9. 立刻把 512 字节数据一口气推给磁盘 } else { outb(0x1f7, read_cmd); // 8. 读命令发下后数据稍后在中断里取回 }两个精妙设计块大小BSIZE512时发单扇区命令块更大时自动切换成多扇区命令ide.c#L80-L85写时立刻灌数据读时延迟取数据——写数据的outsl必须在发命令之后马上执行而读数据则放到中断处理函数里做中断驱动的睡眠等待iderw() 与 ideintr()这是 xv6 驱动开发最值得学的一段——用锁 睡眠 唤醒替代忙等// iderw()把 b 追加到队列若是队头则启动磁盘然后睡眠ide.c#L151-L164 b-qnext 0; for(ppidequeue; *pp; pp(*pp)-qnext) // 遍历到队尾 ; *pp b; if(idequeue b) idestart(b); // 队列原本为空立即启动 while((b-flags (B_VALID|B_DIRTY)) ! B_VALID){ sleep(b, idelock); // 带着自旋锁睡眠CPU 让给别的进程 }// ideintr()硬件完成后被 trap() 调用ide.c#L104-L131 idequeue b-qnext; // 出队 if(!(b-flags B_DIRTY) idewait(1) 0) insl(0x1f0, b-data, BSIZE/4); // 读请求把 512 字节从磁盘搬回内存 b-flags | B_VALID; b-flags ~B_DIRTY; wakeup(b); // 唤醒 iderw() 中睡眠的进程 if(idequeue ! 0) idestart(idequeue); // 关键立刻启动队列中的下一个请求队列的意义wakeup之后不等等待者醒来而是直接idestart()下一个缓冲块——磁盘全程保持忙碌多个 I/O 请求流水线式执行。这就是为什么磁盘队列这个不起眼的设计能显著提升文件系统吞吐。初始化小细节ideinit()还负责探测第二块盘是否存在向盘选择寄存器写入 disk1 的选择位反复读状态口 1000 次若非 0 说明有第二块盘ide.c#L59-L69。这个探测寄存器的手法在很多驱动里都常见。二、UART 串口驱动屏幕之外的那个控制台为什么内核要接一根串口线串口COM1基址0x3f8是主机的远程调试控制台内核启动打印xv6...、用户cat的文件内容都会同时输出到串口远程终端即可看到整个系统的运行过程uart.c#L46-L48。uartinit()波特率是怎么配出来的串口通信靠波特率对齐收发双方的时钟。x86 的 16550 系列芯片基准晶振是 115200Hz要得到 9600 波特率需写入除数 115200/9600 12uart.c#L24-L48寄存器写入值含义COM120关闭 FIFO使用最简模式COM130x80先解锁除数锁存器COM1012除数低 8 位COM130x03锁定除数配置为 8 数据位 1 停止位COM110x01使能接收中断——收到字符就通知 CPU 两个新手容易忽略的细节若读线路状态寄存器COM15得到0xFF说明机器根本没有串口驱动直接放弃uart.c#L35-L38——先探测硬件存在性再配置是好驱动的习惯初始化前先读一次COM12和COM10是为了确认并清掉遗留的中断请求防止刚使能中断就收到假中断收发数据轮询状态位串口很慢9600 波特率下每秒才 960 字符CPU 必须等硬件的寄存器信号// 发送等发送寄存器空第5位再写数据uart.c#L52-L61 for(i 0; i 128 !(inb(COM15) 0x20); i) microdelay(10); // 每次微延迟 10us最多等 ~1.2ms outb(COM10, c); // 接收先看接收 FIFO 非空第0位有数据才读uart.c#L63-L71 if(!(inb(COM15) 0x01)) return -1; return inb(COM10);状态寄存器COM15是两个方向共用的信号灯位 50x20 可以发送位 00x01 有数据可读。数据流闭环从按键到屏幕输出cprintf()console.c#L54-L100把格式化后的字符逐个交给consputc()同时刷到 VGA 屏幕和uartputc()输入硬件中断 →uartintr()→consoleintr()把字符存入环形输入缓冲区并wakeup等待者uart.c#L73-L77、console.c#L235-L271这样无论串口上敲什么consoleread()中的进程都能被正确唤醒读到——一个完整的中断 → 环形缓冲 → 睡眠/唤醒输入模型。三、总结xv6 驱动开发的 3 个通用技巧绝不忙等硬件磁盘用sleep()/wakeup()挂起进程串口发送只允许微秒级短等待uart.c#L58-L59中断里干完所有活再走ideintr()中既取回数据、又启动下一个请求让硬件流水线不断流先探测、再配置、最后使能中断ideinit()探测第二块盘、uartinit()用0xFF探测串口都遵循这个安全顺序如果你想继续深入建议按这个顺序读源码main.c 的初始化顺序 → x86.h 的端口 I/O 原语 → ide.c 与 bio.c 的磁盘数据通路 → uart.c 与 console.c 的串口控制台通路。把这两条链路在脑子里串成闭环你就掌握了 xv6 驱动开发的核心骨架。【免费下载链接】xv6-publicxv6 OS项目地址: https://gitcode.com/gh_mirrors/xv/xv6-public创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考