xv6 设备驱动、console、UART 与磁盘 / xv6 Device Driver Console UART And Disk
1. 这篇文章解决什么问题
用户程序调用 read() 和 write()。
有时读写的是普通文件。
有时读写的是终端。
有时最终会访问磁盘。
这些动作看起来都像文件 I/O。
但内核底下可能走完全不同的路径。
例如:
write(1, "hi\n", 3)
|
v
consolewrite
|
v
uartwrite
|
v
UART MMIO register
read(file_fd, buf, n)
|
v
readi
|
v
bread
|
v
virtio_disk_rw
|
v
VirtIO block device本篇关注设备驱动。
重点包括:
- 什么是设备。
- 什么是驱动。
- 什么是 MMIO。
- 什么是中断。
- PLIC 在 RISC-V 里做什么。
- console 输入输出如何走到 UART。
- 磁盘块读写如何走到 virtio。
- 为什么驱动经常要 sleep/wakeup。
本文基于标准 MIT xv6-riscv 的设备驱动结构。
本地 E:\Github\xv6-riscv 可读,本文列出的文件来自本地标准仓库。
1.1 本文基础词小注释
设备 device:设备是 CPU 外面的硬件或虚拟硬件,比如键盘、串口、磁盘、时钟。在 QEMU 里很多设备是虚拟出来的,但对 xv6 来说仍然像真实硬件一样访问。
驱动 driver:驱动是内核里专门和某类设备打交道的代码。它把复杂的寄存器、队列、中断细节,包装成内核其他部分能理解的函数。
kernel generic code
|
v
device driver
|
v
hardware registers / queuesUART:串口设备。xv6 用它做 console 的底层输入输出,所以你在 QEMU 终端里看到的字符,背后会经过 UART 驱动。
console:控制台。它是用户和系统交互的文本入口。console 负责把用户输入整理成行,也负责把内核或用户程序输出的字符送出去。
MMIO:Memory-Mapped I/O,内存映射 I/O。意思是某些物理地址不是普通内存,而是设备寄存器。CPU 对这些地址读写,效果就是和设备通信。
PLIC:Platform-Level Interrupt Controller,平台级中断控制器。外部设备发来的中断会先到 PLIC,再由内核判断是哪个设备需要处理。
VirtIO:一种虚拟设备标准。QEMU 通过 VirtIO 提供虚拟磁盘,xv6 的 virtio_disk.c 负责和它交互。
2. 最简单心智模型
设备驱动是内核和硬件之间的翻译层。
用户程序不应该知道 UART 寄存器地址。
文件系统不应该知道 virtio 队列细节。
所以内核把这些细节封装在驱动里。
图:
user program
|
v
system call
|
v
kernel abstraction
|
+-- console device -> UART driver
|
+-- block cache -> virtio disk driver
|
v
hardware / qemu device一句话:
驱动把内核中的通用请求翻译成设备能理解的寄存器、队列和中断协议。3. 涉及的 xv6 源码文件
本篇主要涉及:
kernel/console.c
kernel/uart.c
kernel/plic.c
kernel/virtio_disk.c
kernel/virtio.h
kernel/trap.c
kernel/file.c
kernel/file.h
kernel/bio.c
kernel/buf.h
kernel/memlayout.h
kernel/riscv.h
kernel/defs.h
kernel/param.hkernel/console.c
实现 console 设备的输入输出逻辑。
处理行缓冲、退格、Ctrl-D、Ctrl-U、Ctrl-P。
kernel/uart.c
实现 16550a UART 的低层驱动。
通过 MMIO 读写 UART 寄存器。
kernel/plic.c
实现 RISC-V PLIC 的初始化、claim 和 complete。
PLIC 负责把外部设备中断分发给 CPU。
kernel/virtio_disk.c
实现 QEMU virtio block 磁盘驱动。
构造 descriptor chain,把请求交给设备,等待中断完成。
kernel/virtio.h
定义 virtio MMIO 寄存器、状态位、descriptor、avail ring、used ring。
kernel/trap.c
处理 trap 和设备中断分发。
kernel/file.c 和 kernel/file.h
把设备注册到 devsw,让 read/write 可以分发到设备。
kernel/bio.c 和 kernel/buf.h
把磁盘块请求转成 virtio_disk_rw()。
kernel/memlayout.h
定义 UART、VIRTIO、PLIC 等 MMIO 地址。
4. 什么是设备
设备是 CPU 外部的 I/O 对象。
例如:
- 串口。
- 磁盘。
- 网卡。
- 键盘。
- 显示器。
xv6 在 QEMU 中运行。
所以它面对的是 QEMU 模拟出来的设备。
这不影响理解。
因为真实硬件和模拟设备都需要类似机制:
driver writes command
device performs work
device raises interrupt
driver handles completion设备的关键特点是:
CPU 不直接“调用”设备函数。
CPU 通过寄存器、内存队列和中断与设备通信。
5. 什么是驱动
驱动是内核里理解某种设备协议的代码。
它的职责包括:
- 初始化设备。
- 配置设备寄存器。
- 把内核请求转成设备命令。
- 处理中断。
- 唤醒等待设备完成的进程。
- 隐藏设备细节。
图:
kernel wants:
read block 100
driver translates:
descriptor chain
sector number
device notify register
device returns:
interrupt + status byte没有驱动,上层只能自己操作寄存器。
那会让文件系统、console、系统调用全部混在一起。
6. 什么是 MMIO
MMIO 是 memory-mapped I/O。
中文可以说“内存映射 I/O”。
意思是:
设备寄存器被映射到物理地址空间中。
CPU 用普通的 load/store 指令访问这些地址。
但读写的不是普通内存。
而是设备寄存器。
图:
CPU store to address 0x10000000
|
v
not RAM
|
v
UART register changesxv6 的 memlayout.h 定义了这些地址。
例如 UART、VIRTIO 和 PLIC。
驱动中常见这种宏:
Reg(offset) = UART0 + offset
R(offset) = VIRTIO0 + offset这些宏让代码像访问内存一样访问设备。
7. 什么是中断
中断是设备通知 CPU 的机制。
设备不能直接调用内核函数。
它只能发出中断信号。
CPU 收到中断后进入内核 trap 处理路径。
然后内核查询是哪个设备打断了它。
最后调用对应驱动的 interrupt handler。
图:
device finishes work
|
v
raises interrupt
|
v
CPU enters trap
|
v
devintr
|
+-- UART IRQ -> uartintr
|
+-- VIRTIO IRQ -> virtio_disk_intr中断让设备 I/O 可以异步完成。
发起请求的进程可以睡眠。
设备完成时再唤醒它。
8. PLIC 的角色
PLIC 是 Platform Level Interrupt Controller。
中文可以理解为平台级中断控制器。
在 RISC-V 中,外部设备中断通常通过 PLIC 分发。
PLIC 做几件事:
- 给设备中断设置优先级。
- 给每个 hart 启用某些中断源。
- 让内核 claim 当前要处理的 IRQ。
- 在处理完后 complete 这个 IRQ。
plic.c 中核心函数:
plicinit
plicinithart
plic_claim
plic_complete图:
UART / VIRTIO
|
v
PLIC
|
v
hart external interrupt
|
v
kernel trapPLIC 不处理设备数据。
它只处理“哪个设备中断了”的路由问题。
9. console 是什么
console 是用户和系统交互的终端抽象。
在 xv6 中,console 主要通过 UART 输入输出字符。
用户看到的是:
read from console
write to console内核里分成两层:
console.c
|
+-- line discipline
+-- editing keys
+-- read buffer
+-- devsw registration
uart.c
|
+-- UART register access
+-- transmit / receive
+-- UART interrupt handlerconsole.c 更接近“终端语义”。
uart.c 更接近“硬件协议”。
10. console 注册到 devsw
设备文件能通过 read/write 使用,是因为 devsw。
devsw 是设备分发表。
它在 file.h 中定义概念。
consoleinit() 会做:
devsw[CONSOLE].read = consoleread
devsw[CONSOLE].write = consolewrite之后普通 write(fd, ...) 如果 fd 对应 FD_DEVICE 且 major 是 CONSOLE,就会调用 consolewrite()。
图:
write(fd)
|
v
filewrite
|
v
FD_DEVICE
|
v
devsw[major].write
|
v
consolewrite这就是 Unix “设备也是文件”的实现骨架。
11. console 输出路径
用户程序写 console 时,路径大致是:
user write
|
v
sys_write
|
v
filewrite
|
v
consolewrite
|
v
uartwrite
|
v
UART transmit registerconsolewrite() 负责从用户地址空间拷贝数据。
它会用一个小缓冲区分批拷贝。
然后调用 uartwrite()。
uartwrite() 会等待 UART 发送寄存器空闲。
当 UART 忙时,发送线程可以睡眠。
当 UART 发送完成,中断处理会唤醒发送线程。
这就是异步设备和 sleep/wakeup 的配合。
12. uart 输出细节
UART 是 Universal Asynchronous Receiver/Transmitter。
中文常译为通用异步收发器。
xv6 使用的是 16550a UART。
关键寄存器包括:
RHR:receive holding register,接收寄存器。THR:transmit holding register,发送寄存器。IER:interrupt enable register,中断使能。LSR:line status register,线路状态。
输出时最关心的是 LSR_TX_IDLE。
它表示发送寄存器可以接收下一个字符。
简化流程:
uartwrite
|
+-- acquire tx_lock
+-- wait until LSR_TX_IDLE
+-- write char to THR
+-- maybe sleep waiting for next ready event
+-- release tx_locktx_lock 是 sleeplock。
它让多个写 console 的进程串行发送。
否则字符可能互相交错。
13. uartputc_sync
uartputc_sync() 是同步输出单字符。
它不用中断。
它会自旋等待 UART 空闲。
它常用于 printk() 和输入回显。
为什么需要它?
因为有些场景不能睡眠。
例如 panic 或中断上下文。
这时用 uartwrite() 这类可能睡眠的函数不合适。
图:
uartputc_sync
|
+-- spin until TX idle
+-- write one byte它简单但会占 CPU。
所以适合紧急或短输出。
不适合大量普通用户输出。
14. console 输入路径
输入和输出不同。
输入通常由中断驱动。
用户敲键。
UART 收到字符。
UART 产生中断。
内核进入 uartintr()。
uartintr() 读出字符并交给 consoleintr()。
consoleintr() 把字符放进 console 缓冲区。
如果形成一行,就唤醒等待 consoleread() 的进程。
图:
keyboard input in QEMU
|
v
UART receive register
|
v
UART interrupt
|
v
uartintr
|
v
consoleintr
|
v
cons.buf
|
v
wakeup readers15. consoleread 的行缓冲
consoleread() 不是每按一个字符就返回。
它按行读取。
也就是说,通常要等到换行。
console 维护一个输入环形缓冲区。
字段:
cons.buf
cons.r
cons.w
cons.er 是读位置。
w 是已经提交给读者的位置。
e 是编辑位置。
图:
cons.buf circular buffer
r ---- readable start
w ---- committed end
e ---- edit end当用户输入普通字符时,e 前进。
当输入换行或 Ctrl-D 时,w 更新到 e。
等待读的进程被唤醒。
这样用户程序一次读到一行。
16. console 特殊字符
consoleintr() 处理几个特殊输入。
Ctrl-H 或 Delete 表示退格。
Ctrl-U 表示删除当前行。
Ctrl-D 表示文件结束。
Ctrl-P 表示打印进程列表。
这些功能属于 console 层。
不是 UART 层。
UART 只知道字节。
console 才知道这些字节代表什么终端编辑语义。
图:
raw byte from UART
|
v
consoleintr
|
+-- normal char -> append buffer
+-- backspace -> edit buffer
+-- Ctrl-U -> kill line
+-- Ctrl-D -> EOF marker
+-- Ctrl-P -> procdump这就是分层的价值。
低层驱动不懂用户交互。
高层 console 不直接碰寄存器。
17. 为什么 console read 会 sleep
如果用户程序调用 read(0, buf, n)。
但当前还没有完整输入行。
进程不能一直占用 CPU 等键盘。
它应该睡眠。
consoleread() 会在缓冲区为空时睡眠。
当 consoleintr() 收到完整行后,会 wakeup()。
流程:
reader:
cons.r == cons.w
sleep on &cons.r
interrupt:
store input char
cons.w = cons.e
wakeup(&cons.r)这是设备输入和进程调度的经典配合。
设备中断改变条件。
等待进程醒来继续执行。
18. 磁盘驱动路径总览
磁盘不像 console 那样以字符为单位。
文件系统以块为单位读写磁盘。
路径通常从 buffer cache 开始:
file system
|
v
bread / bwrite
|
v
virtio_disk_rw
|
v
virtio descriptor chain
|
v
virtio block device用户的文件读写要经过很多层:
read(fd)
|
v
fileread
|
v
readi
|
v
bread
|
v
virtio_disk_rw这条路径非常重要。
它把文件抽象、文件系统、缓存和驱动连到一起。
19. virtio 是什么
VirtIO 是一种虚拟化设备标准。
它让客户机操作系统和虚拟机管理器之间用标准协议通信。
xv6 在 QEMU 中使用 virtio block 设备作为磁盘。
这意味着:
xv6 kernel
|
v
virtio MMIO protocol
|
v
QEMU virtual block device
|
v
fs.imgvirtio.h 定义了协议结构。
virtio_disk.c 实现了 xv6 对这个协议的使用。
20. virtio 三个核心结构
virtio 队列里有三个关键区域。
第一是 descriptor table。
它描述内存中的一段 buffer。
第二是 avail ring。
驱动把要交给设备处理的 descriptor chain 头放在这里。
第三是 used ring。
设备完成后把结果放在这里。
图:
driver-owned memory
+-------------------+
| descriptor table |
+-------------------+
^
|
+-------------------+
| avail ring | driver -> device
+-------------------+
+-------------------+
| used ring | device -> driver
+-------------------+descriptor 描述“哪里有数据,长度是多少,设备读还是写”。
ring 描述“哪些 descriptor chain 可用或已完成”。
21. virtio 磁盘初始化
virtio_disk_init() 做很多硬件协商。
大致流程:
- 检查 magic、version、device id、vendor id。
- reset device。
- 设置 ACKNOWLEDGE。
- 设置 DRIVER。
- 读取并筛掉不使用的 feature。
- 设置 FEATURES_OK。
- 初始化 queue 0。
- 分配 descriptor、avail、used 三块内存。
- 把这些内存地址写给设备。
- 设置 QUEUE_READY。
- 标记所有 descriptor 为空闲。
- 设置 DRIVER_OK。
图:
reset
|
v
acknowledge
|
v
negotiate features
|
v
allocate queue memory
|
v
publish queue addresses
|
v
driver ok这一步完成后,设备和驱动才有共同的队列可以通信。
22. virtio_disk_rw 的职责
virtio_disk_rw(struct buf *b, int write) 读写一个 buffer 对应的磁盘块。
参数 b 是 buffer cache 的块。
参数 write 表示方向。
write == 0 表示从磁盘读到 b->data。
write == 1 表示把 b->data 写到磁盘。
简化流程:
virtio_disk_rw
|
+-- acquire disk lock
+-- allocate 3 descriptors
+-- fill request header
+-- point descriptor to data buffer
+-- point descriptor to status byte
+-- put head descriptor into avail ring
+-- notify device
+-- sleep until interrupt marks done
+-- free descriptors
+-- release disk lock这里的核心是 descriptor chain。
一次磁盘请求需要三段描述。
23. 一次磁盘请求的三个 descriptor
virtio block 请求使用三个 descriptor。
第一个 descriptor 是请求头。
它告诉设备:
- 读还是写。
- 哪个 sector。
第二个 descriptor 是数据缓冲区。
读请求时,设备往这里写数据。
写请求时,设备从这里读数据。
第三个 descriptor 是状态字节。
设备完成后写入状态。
图:
descriptor 0
|
+-- virtio_blk_req { type, sector }
|
v
descriptor 1
|
+-- b->data[BSIZE]
|
v
descriptor 2
|
+-- status bytedescriptor 之间用 NEXT 标志串起来。
这叫 descriptor chain。
24. sector 和 block 的关系
xv6 文件系统块大小是 1024 字节。
virtio block 设备通常按 512 字节 sector 工作。
所以 virtio_disk_rw() 会把文件系统块号转成 sector:
sector = blockno * (BSIZE / 512)由于 BSIZE 是 1024。
一个 xv6 block 对应两个 512 字节 sector。
这个换算很小。
但它是文件系统块和设备扇区之间的边界。
25. 磁盘请求为什么会 sleep
磁盘 I/O 比 CPU 慢很多。
发起请求后,CPU 不应该一直忙等。
virtio_disk_rw() 把请求放进 avail ring 并通知设备后,会等待完成。
等待条件是 b->disk == 1。
设备完成后,中断处理函数会把它改为 0。
然后唤醒睡眠进程。
图:
request thread
|
+-- b->disk = 1
+-- notify device
+-- sleep on b
interrupt handler
|
+-- b->disk = 0
+-- wakeup(b)这和 console 输入的模式类似。
等待者睡眠。
中断唤醒。
26. virtio_disk_intr
磁盘中断处理函数是 virtio_disk_intr()。
它会:
- 获取磁盘锁。
- 确认设备中断。
- 查看 used ring。
- 对每个完成请求检查 status。
- 找到对应
struct buf。 - 把
b->disk设为 0。 wakeup(b)。- 前进
used_idx。 - 释放锁。
图:
virtio interrupt
|
v
virtio_disk_intr
|
+-- read used ring
+-- find completed buf
+-- mark done
+-- wake request thread中断处理函数不继续执行文件系统逻辑。
它只完成设备层状态转换。
真正等待的进程被唤醒后会继续从 virtio_disk_rw() 返回。
27. trap.c 与设备中断分发
虽然本篇重点不是 trap,但设备中断离不开 trap 路径。
设备中断到来后,CPU 进入 trap。
devintr() 会判断中断类型。
如果是外部中断,就通过 PLIC claim 得到 IRQ。
然后分发:
if irq == UART0_IRQ:
uartintr()
if irq == VIRTIO0_IRQ:
virtio_disk_intr()处理完后调用 plic_complete(irq)。
图:
external interrupt
|
v
devintr
|
v
plic_claim
|
+-- UART0_IRQ -> uartintr
+-- VIRTIO0_IRQ -> virtio_disk_intr
|
v
plic_complete这就是 PLIC、trap 和驱动的连接点。
28. console 和磁盘的共同模式
console 和磁盘看起来完全不同。
但它们有共同模式:
process issues request
|
v
driver talks to device
|
v
process may sleep
|
v
device interrupt arrives
|
v
interrupt handler updates state
|
v
wakeup waiting processconsole read:
consoleread sleeps
consoleintr receives line
wakeup readerdisk read:
virtio_disk_rw sleeps
virtio_disk_intr sees used ring
wakeup requester这是 I/O 驱动的基本模型。
29. 锁在驱动中的作用
驱动处理共享状态。
例如 console 的输入缓冲区。
例如 UART 发送锁。
例如 virtio 的 descriptor 空闲表和 used ring 位置。
这些都需要锁。
console 使用 cons.lock。
UART 输出使用 tx_lock。
virtio disk 使用 vdisk_lock。
图:
console input buffer -> cons.lock
uart writer ordering -> tx_lock
virtio queue state -> vdisk_lock
buffer contents -> buf sleeplock锁的目标仍然是保护不变量。
例如 descriptor 不能被两个请求同时使用。
used ring 不能被两个 CPU 同时消费乱。
console 缓冲区的 r/w/e 不能互相矛盾。
30. 中断上下文里不能做太多
中断处理函数应该尽量短。
原因:
- 中断打断了当前执行流。
- 持锁太久会影响系统响应。
- 复杂逻辑更容易引入死锁。
- 某些上下文不能睡眠。
xv6 的中断处理通常做最必要的事:
read/ack device state
move minimal data
update completion flags
wakeup waiters例如 virtio_disk_intr() 不解析文件系统。
它只标记 buffer 完成。
例如 uartintr() 读出字符并交给 console。
它不执行用户程序逻辑。
31. console 输出和 printk 的差异
用户 write() 到 console 会走 consolewrite() 和 uartwrite()。
它可能睡眠。
内核 printk() 通常使用同步输出路径。
这更适合调试和 panic。
区别:
user console write
|
+-- can sleep
+-- uses UART interrupts
+-- serialized by tx_lock
kernel printk path
|
+-- synchronous
+-- spins waiting for UART
+-- usable in tighter contexts这解释了为什么同样是输出字符,内核有不同函数。
上下文不同,允许的行为也不同。
32. buffer cache 到磁盘驱动的边界
bio.c 不懂 virtio descriptor。
它只知道要读写某个 (dev, blockno)。
virtio_disk.c 不懂 inode。
它只知道要读写 struct buf 对应的数据块。
图:
fs.c
|
| wants inode block or data block
v
bio.c
|
| wants block cache buffer
v
virtio_disk.c
|
| wants device operation
v
virtio device这个边界非常漂亮。
上层表达“我要这个块”。
下层负责“如何让设备把这个块搬进内存”。
33. 常见误解
误解一:设备驱动就是读写文件。
不是。
驱动是把内核请求翻译成设备协议。
误解二:console 和 UART 是同一层。
不是。
console 管终端语义。
UART 管寄存器和字节收发。
误解三:PLIC 负责读取设备数据。
不是。
PLIC 只负责中断路由。
误解四:中断一到,请求函数就自动继续执行。
不是。
中断处理函数通常 wakeup() 等待者。
之后调度器让等待进程继续运行。
误解五:virtio descriptor 里存文件内容。
不准确。
descriptor 描述内存区域。
真实数据在 b->data 或请求头结构中。
误解六:磁盘块号就是 sector 号。
不是。
xv6 block 是 1024 字节,virtio sector 通常是 512 字节。
两者需要换算。
误解七:所有输出都能 sleep。
不是。
中断上下文、panic 路径等场景不能随便睡眠。
34. 调试和阅读方法
阅读驱动代码时,不要只看函数名。
要看状态机。
可以按这个模板:
device:
request object:
shared state:
start operation:
sleep condition:
interrupt completion:
wakeup channel:
locks:例如 virtio disk:
device:
virtio block
request object:
descriptor chain + struct buf
shared state:
descriptor free table, avail ring, used ring
start operation:
virtio_disk_rw
sleep condition:
b->disk == 1
interrupt completion:
virtio_disk_intr
wakeup channel:
b
locks:
disk.vdisk_lock这个模板能帮你避免陷在寄存器细节里。
35. 小实验一:追踪 console 输出
在 xv6 中写用户程序:
write(1, "hello\n", 6);然后在内核里临时打印路径。
推荐位置:
filewrite
consolewrite
uartwrite观察:
filewrite: FD_DEVICE
consolewrite: n=6
uartwrite: chars sent不要在每个字符都打印太多。
否则输出会互相干扰。
36. 小实验二:观察 console 输入唤醒
在 consoleread() 睡眠前加一条打印。
在 consoleintr() 形成完整行并唤醒前加一条打印。
然后运行:
cat键入一行。
观察:
reader sleeps
input arrives
reader wakes重点不是打印本身。
重点是理解:
用户进程等待输入时不占 CPU。
输入中断到来后才被唤醒。
37. 小实验三:追踪磁盘读
在 bread() 和 virtio_disk_rw() 附近加少量打印。
执行:
cat README观察:
bread dev blockno
virtio read sector
virtio interrupt complete第一次读取可能走磁盘。
后续读取同一块可能命中 buffer cache。
这能帮助你区分缓存命中和真实设备 I/O。
38. 小实验四:观察 descriptor 耗尽
virtio descriptor 数量有限。
xv6 中 NUM 很小。
可以阅读 alloc3_desc() 和等待逻辑。
不一定要强行构造耗尽。
只要理解:
one request needs 3 descriptors
NUM descriptors total
if not enough descriptors -> sleep
free_desc wakes waiters这说明驱动内部也有资源管理。
不是只有进程和文件系统有资源限制。
39. 小实验五:区分 block 和 sector
在 virtio_disk_rw() 打印:
blockno
sector读几个文件。
你会看到:
sector = blockno * 2因为 xv6 block 是 1024 字节。
virtio sector 是 512 字节。
这个实验很小。
但能避免把文件系统块和设备扇区混为一谈。
40. 工程取舍
xv6 驱动层也做了教学化简。
例如:
- 只支持少数设备。
- 没有复杂 DMA 映射层。
- 没有通用驱动模型。
- 没有热插拔。
- 没有电源管理。
- 没有多队列高性能磁盘 I/O。
- 没有复杂终端模式。
但保留了关键机制:
- MMIO。
- 中断。
- PLIC。
- 设备分发表。
- buffer cache 到块设备驱动。
- sleep/wakeup 等待 I/O 完成。
这些机制在真实内核里仍然存在。
只是规模更大。
41. 和前两篇的连接
第 12 篇讲 fd 到 inode。
第 13 篇讲 inode 到磁盘块。
本篇讲磁盘块和设备。
三篇连起来就是:
fd
|
v
struct file
|
v
inode / device / pipe
|
v
file system blocks
|
v
buffer cache
|
v
virtio disk driver
|
v
hardware or QEMU deviceconsole 则走另一条设备路径:
fd
|
v
FD_DEVICE
|
v
devsw[CONSOLE]
|
v
console.c
|
v
uart.c
|
v
UART MMIO这就是 xv6 I/O 子系统的大图。
42. 本篇总结
设备驱动是内核和硬件之间的翻译层。
console 把设备文件抽象接到终端语义。
UART 驱动把终端字节接到 MMIO 寄存器。
PLIC 把外部中断路由到 CPU。
virtio disk 驱动把块缓存请求接到 virtio 队列。
中断处理函数负责确认设备完成并唤醒等待者。
核心模型:
request
|
v
driver starts device operation
|
v
sleep if operation is slow
|
v
device interrupt
|
v
driver marks completion
|
v
wakeup requester学完这三篇后,xv6 的 I/O 主线已经连起来:
file descriptor
-> struct file
-> inode / pipe / device
-> file system / console
-> buffer cache / UART
-> disk / terminal hardware下一步可以回到源码里做路径追踪。
从一个 write() 开始。
从一个 read() 开始。
从一个磁盘块缺页式的 bread() 开始。
把调用链画出来,xv6 的 I/O 就不再是一堆散文件,而是一套可解释的系统。