内存映射 I/O
设备寄存器位于物理地址,通过加载/存储访问,统一了 CPU 与设备的编程模型。
6.1810 / 6.1810 · 第 11 课 · rtm
CPU 需要外设:存储、通信、显示。操作系统设备驱动程序控制这些设备,而中断是硬件通知软件"我需要关注"的核心机制。本节课通过 UART 驱动详解中断、DMA、轮询等概念。
UART(Universal Asynchronous Receiver/Transmitter)是串行接口,QEMU 模拟常见的 16550 UART 芯片。
| 组件 | 作用 |
|---|---|
| RX wire / TX wire | 串行输入/输出线路 |
| Receive Shift Register / FIFO | 接收数据(16 字节 FIFO) |
| Transmit FIFO / Shift Register | 发送数据(16 字节 FIFO) |
| 偏移 | 名称 | 作用 |
|---|---|---|
| 0 | RHR / THR | 接收/发送保持寄存器 |
| 1 | IER | 中断使能寄存器(0x1=接收,0x2=发送) |
| 5 | LSR | 线路状态寄存器(0x1=接收数据就绪) |
// uartgetc() in kernel/uart.c
ReadReg(RHR) → *(char*)(0x10000000 + 0)
FIFO 缓冲允许设备在 CPU 来不及处理时暂存数据,避免丢失。16 字节 FIFO 意味着 CPU 最多可以每 16 字节处理一次,而非每字节一次。
// 忙等待示例
while ((LSR & 1) == 0)
;
return RHR;
如果输入马上就到,忙等待可以。但控制台经常没有输入,忙等待浪费大量 CPU。
当设备需要关注时,发出中断:
设备 → PLIC → CPU → trap → usertrap()/kerneltrap() → devintr()
scause 高位表示这是设备中断中断只是提示设备状态可能已改变。真正的真相在设备的状态寄存器中。驱动必须读取它们决定如何行动。一个中断可能表示多个操作需要处理。
以 shell 从控制台读取输入为例:
sys_read() → fileread() → consoleread()consoleread() 看到缓冲区为空,调用 sleep()stvec → kernelvec(内核中,使用调度器栈)kerneltrap() → devintr()plic_claim() 获取 IRQ = 10uartintr() → uartgetc() → 读 LSR → 读 RHRconsoleintr() 处理字符 → wakeup()read()| 部分 | 调用者 | 职责 |
|---|---|---|
| Bottom half | 进程的系统调用(read/write) | 告诉设备开始输入/输出;等待输入就绪或输出完成 |
| Top half | 中断处理程序 | 从设备读取输入/发送更多输出;与 bottom half 交互(通过 buffer + sleep/wakeup) |
Top half 不在 bottom half 进程的上下文中运行(可能在另一个核心,或中断其他进程),所以交互必须通过 arms-length 方式——缓冲区 + sleep()/wakeup()。
PLIC 将中断分配到不同核心,不同中断可并行处理。每个中断由第一个调用 plic_claim() 的核心认领。每个设备最多只有一个中断在进行中(通过 plic_complete() 通知完成)。
// bottom half: n = n + 1
// interrupt top: n = n + 1
// 机器码:
lw a4, n
add a4, a4, 1
sw a4, n
如果中断发生在 lw 和 add 之间,两个 n = n + 1 可能只增加一次!
intr_off();
n = n + 1;
intr_on();
中断可能在另一个 CPU 上发生。完整解决方案需要锁——下一讲的主题。
PLIC/CPU 记住未决中断,在内核重新使能中断时投递。
约 1 微秒(不包括有用的设备驱动工作)。包括:CPU trap、保存寄存器、可能切换页表、决定哪个设备、之后恢复一切并返回。流水线、大寄存器集、cache/TLB 缺失、慢 RAM 都增加开销。
以太网每秒可交付数百万包。在此速率下,大部分 CPU 时间花在中断开销上。
scheduler() 或定时器中断中)| 年代 | 开销 | 策略 |
|---|---|---|
| 几十年前 | 几个周期 | 简单硬件、智能软件、大量中断 |
| 现在 | 数千周期 | 智能硬件、每个中断做更多工作 |
Q1:内存映射 I/O 是什么意思?
Q2:中断处理程序必须读取设备状态寄存器的原因是什么?
Q3:为什么 bottom half 和 top half 的交互必须通过 arms-length 方式?
Q4:高中断率下,轮询比中断更高效的原因是什么?
Q5:仅禁用中断(intr_off)为什么不足以保护并发?
设备寄存器位于物理地址,通过加载/存储访问,统一了 CPU 与设备的编程模型。
中断让设备主动通知 CPU,避免浪费的忙等待。但中断有开销,高负载下需轮询。
系统调用(bottom half)与中断处理(top half)通过缓冲区 + sleep/wakeup 交互。
中断可能发生在任意指令间,需要禁用中断 + 锁来保证正确性。