6.1810 / 6.1810 · 第 11 课 · rtm

设备驱动与中断

CPU 需要外设:存储、通信、显示。操作系统设备驱动程序控制这些设备,而中断是硬件通知软件"我需要关注"的核心机制。本节课通过 UART 驱动详解中断、DMA、轮询等概念。

授课教师:rtm

日期:2025 年春季

难度:中等

阅读:xv6 源码 kernel/uart.c, kernel/trap.c

设备驱动概述

编程设备:内存映射 I/O

示例设备:UART

UART(Universal Asynchronous Receiver/Transmitter)是串行接口,QEMU 模拟常见的 16550 UART 芯片。

UART 结构

组件作用
RX wire / TX wire串行输入/输出线路
Receive Shift Register / FIFO接收数据(16 字节 FIFO)
Transmit FIFO / Shift Register发送数据(16 字节 FIFO)

内存映射寄存器(UART0 = 0x10000000)

偏移名称作用
0RHR / THR接收/发送保持寄存器
1IER中断使能寄存器(0x1=接收,0x2=发送)
5LSR线路状态寄存器(0x1=接收数据就绪)

驱动如何使用寄存器

// uartgetc() in kernel/uart.c
ReadReg(RHR) → *(char*)(0x10000000 + 0)

为什么 UART 需要 FIFO?

FIFO 缓冲允许设备在 CPU 来不及处理时暂存数据,避免丢失。16 字节 FIFO 意味着 CPU 最多可以每 16 字节处理一次,而非每字节一次。


中断:设备通知 CPU 的机制

为什么不用忙等待?

// 忙等待示例
while ((LSR & 1) == 0)
  ;
return RHR;

如果输入马上就到,忙等待可以。但控制台经常没有输入,忙等待浪费大量 CPU。

中断方案

当设备需要关注时,发出中断:

中断路径

设备 → PLIC → CPU → trap → usertrap()/kerneltrap() → devintr()

中断只是提示

中断只是提示设备状态可能已改变。真正的真相在设备的状态寄存器中。驱动必须读取它们决定如何行动。一个中断可能表示多个操作需要处理。


GDB 实战:跟踪 UART 中断

以 shell 从控制台读取输入为例:

  1. sys_read() → fileread() → consoleread()
  2. consoleread() 看到缓冲区为空,调用 sleep()
  3. UART → PLIC → stvec → kernelvec(内核中,使用调度器栈)
  4. kerneltrap() → devintr()
  5. plic_claim() 获取 IRQ = 10
  6. uartintr() → uartgetc() → 读 LSR → 读 RHR
  7. consoleintr() 处理字符 → wakeup()
  8. 调度器恢复 shell 的 read()

通用驱动模式:Bottom-half 与 Top-half

部分调用者职责
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 上发生。完整解决方案需要锁——下一讲的主题。

SSTATUS_SIE = 0 时中断去哪了?

PLIC/CPU 记住未决中断,在内核重新使能中断时投递。


中断的开销与优化

中断开销

约 1 微秒(不包括有用的设备驱动工作)。包括:CPU trap、保存寄存器、可能切换页表、决定哪个设备、之后恢复一切并返回。流水线、大寄存器集、cache/TLB 缺失、慢 RAM 都增加开销。

高中断率问题

以太网每秒可交付数百万包。在此速率下,大部分 CPU 时间花在中断开销上。

轮询(Polling)

DMA(直接内存访问)

中断演进

年代开销策略
几十年前几个周期简单硬件、智能软件、大量中断
现在数千周期智能硬件、每个中断做更多工作

发展方向

  • 专用快速中断路径(也用于缺页、系统调用)
  • 跨 CPU 分配设备工作
  • 用户态设备驱动——完全绕过内核

测验

Q1:内存映射 I/O 是什么意思?

Q2:中断处理程序必须读取设备状态寄存器的原因是什么?

Q3:为什么 bottom half 和 top half 的交互必须通过 arms-length 方式?

Q4:高中断率下,轮询比中断更高效的原因是什么?

Q5:仅禁用中断(intr_off)为什么不足以保护并发?

要点总结

内存映射 I/O

设备寄存器位于物理地址,通过加载/存储访问,统一了 CPU 与设备的编程模型。

中断驱动

中断让设备主动通知 CPU,避免浪费的忙等待。但中断有开销,高负载下需轮询。

Top/Bottom 分离

系统调用(bottom half)与中断处理(top half)通过缓冲区 + sleep/wakeup 交互。

并发挑战

中断可能发生在任意指令间,需要禁用中断 + 锁来保证正确性。

延伸阅读