device - driver - interrupt
07. 设备、驱动与中断
0. 本章先解决什么问题
操作系统不仅管理 CPU 和内存,还要管理各种设备:
- 磁盘。
- 网卡。
- 键盘。
- 显示器。
- USB 设备。
- 传感器。
设备速度不同、协议不同、控制方式不同。OS 通过驱动和中断机制把它们纳入统一管理。
本章要讲:
- 设备控制器和驱动是什么?
- 中断如何让设备通知 CPU?
- DMA 如何减少 CPU 搬运数据?
- 为什么中断太多也会影响性能?
- 设备队列和缓冲区如何影响 IO 延迟?
先看设备 IO 的路径:应用请求内核,驱动提交命令,设备通过 DMA 和中断把结果带回来。
这张图怎么读
从应用发起 IO 开始看:系统调用把请求交给内核,驱动把通用请求翻译成设备控制器能理解的命令,设备执行后可能用 DMA 直接把数据搬进内存,再用中断通知 CPU。
图里的关键边界有两个:CPU 不应该为大块数据做逐字节搬运,驱动也不能在中断里做太久的重活。
设备 IO 的延迟通常来自队列、控制器、DMA、缓存一致性和中断处理共同叠加。
1. 设备控制器
真实设备通常通过控制器接入系统。
控制器暴露:
- 控制寄存器。
- 状态寄存器。
- 数据缓冲区。
- DMA 配置入口。
CPU 不直接理解每种设备内部细节,而是通过控制器读写寄存器和缓冲区。
2. 驱动程序
驱动是 OS 中负责管理具体设备的软件。
驱动负责:
- 初始化设备。
- 配置设备寄存器。
- 提交 IO 请求。
- 处理中断。
- 管理设备队列和缓冲区。
- 向上提供统一接口。
应用程序不直接和设备控制器打交道。它通过文件、Socket、设备文件等抽象调用内核,内核再通过驱动操作设备。
3. 轮询
最简单的设备等待方式是轮询:
CPU 发出命令
while 设备未完成:
读取状态寄存器
优点:
- 简单。
- 适合非常短的等待。
缺点:
- CPU 忙等。
- 设备慢时浪费严重。
轮询不是完全没用。在某些高性能场景,系统会用轮询减少中断开销,但前提是设备事件非常频繁,且 CPU 资源可接受。
4. 中断
中断让设备主动通知 CPU:
CPU 启动 IO
CPU 去做别的事
设备完成
设备发中断
CPU 进入中断处理程序
流程:
设备产生事件
-> 中断控制器通知 CPU
-> CPU 保存现场
-> 进入中断处理程序
-> 驱动处理事件
-> 恢复现场
中断减少忙等,但中断处理本身也有成本。
机制深挖:中断延迟从哪里来
设备发出中断后,CPU 也不是“瞬间处理完”。中断路径里有几个时间段:
| 阶段 | 发生什么 | 延迟来源 |
|---|---|---|
| 设备产生事件 | 数据到达、命令完成、错误发生 | 设备内部队列和控制器状态 |
| 中断控制器分发 | 把事件路由到某个 CPU | 中断屏蔽、优先级、CPU 亲和性 |
| CPU 响应中断 | 保存现场,进入内核处理 | 当前临界区、关中断窗口 |
| 驱动确认事件 | 读状态寄存器,确认完成项 | 寄存器访问、设备总线延迟 |
| 延后处理 | 把数据交给协议栈或唤醒等待者 | 队列积压、软中断/工作队列 |
| 应用被唤醒 | 等待线程重新获得 CPU | 调度延迟、锁竞争、运行队列长度 |
因此“设备已经完成”和“应用已经拿到结果”之间还有距离。高吞吐场景里,系统常用中断合并和批处理减少中断次数:
少量事件:
立即中断,降低单次延迟
大量事件:
合并一批再通知,降低 CPU 中断开销
这是一组 trade-off:低延迟和高吞吐经常不能同时最大化。
5. 中断处理为什么要快
中断处理程序通常应该尽量短。
原因:
- 中断处理会打断当前执行流。
- 处理太久会影响系统响应。
- 中断期间可能限制其他中断。
- 高优先级事件可能被延迟。
常见做法:
中断上半部快速确认事件
把耗时工作推迟到后续上下文处理
不同系统命名不同,但核心思想是:
紧急部分立即做,耗时部分延后做。
6. DMA
DMA 让设备和内存之间直接传输大块数据。
CPU 只负责:
设置源地址
设置目标地址
设置长度
启动传输
等待完成通知
数据路径:
设备 <-> DMA <-> 内存
完成后设备发中断通知 CPU。
DMA 减少 CPU 逐字节搬运成本,但也带来 Cache 一致性和内存可见性问题。
DMA 的关键细节:缓冲区所有权
DMA 不是“设备随便往内存写”。系统必须约定一块缓冲区当前归谁使用:
CPU 准备缓冲区
-> 把描述符交给设备
-> 设备拥有这段缓冲区
-> 设备 DMA 写入或读取
-> 设备完成并通知
-> CPU 重新取得所有权
如果所有权边界处理错,会出现非常难查的问题:
| 错误 | 可能后果 |
|---|---|
| CPU 在设备写入时读取缓冲区 | 读到旧数据或半写入数据 |
| 设备还没完成就复用缓冲区 | 数据被覆盖 |
| Cache 未刷新或未失效 | CPU 和设备看到的数据不一致 |
| 描述符状态更新顺序不对 | 设备漏处理或重复处理请求 |
所以很多高性能 IO 结构都会维护 ring buffer / descriptor queue。它们本质上是在解决“CPU 和设备之间如何批量交换缓冲区所有权”。
7. 设备队列
设备通常处理速度有限。多个请求会排队。
请求进入队列
设备一个个处理
完成后通知
队列影响:
- 吞吐。
- 延迟。
- 公平性。
- 请求合并。
- 优先级。
如果请求到达速度长期大于设备处理速度,队列会增长,延迟会上升。
8. 中断风暴
中断太频繁,CPU 可能大部分时间都在处理中断。
原因:
- 网络包太多。
- 设备异常。
- 中断合并策略不合理。
- 驱动问题。
- 应用消费太慢导致事件堆积。
优化方向:
- 中断合并。
- 批量处理。
- 轮询和中断混合。
- 多队列分散到多个 CPU。
- 减少包或请求数量。
9. 设备抽象成文件
很多 OS 会把设备也抽象成类似文件的对象。
应用可以通过:
open/read/write/ioctl
和设备交互。
这体现了 OS 的抽象思想:
不同设备细节不同
但尽量向应用提供统一接口
不过设备并不是真的普通文件。读写设备可能触发真实硬件行为,可能阻塞,也可能返回特殊错误。
10. 联系实际:磁盘或网卡为什么让 CPU 忙
设备 IO 高时,CPU 可能忙在:
- 中断处理。
- 驱动逻辑。
- 数据复制。
- 协议栈处理。
- 队列管理。
- 唤醒等待线程。
- DMA 缓冲区维护。
所以看到 CPU 高,不一定是应用层计算。
可能是内核和设备路径在忙。
手推:一次网卡收包为什么会消耗 CPU
很多人以为网卡收包是“硬件自己做完”,CPU 不该忙。实际路径更像:
网卡收到帧
-> DMA 写入接收缓冲区
-> 更新描述符状态
-> 触发中断或等待批量轮询
-> 驱动确认哪些 buffer 有新数据
-> 协议栈解析链路层/IP/传输层头部
-> 找到对应 socket 或丢弃
-> 唤醒等待的应用线程
-> 应用从 socket buffer 读取
CPU 不一定在搬运每个 byte,但它仍然参与:
| CPU 工作 | 说明 |
|---|---|
| 中断响应 | 保存现场,进入内核路径 |
| 驱动处理 | 读描述符、回收/补充 buffer |
| 协议栈处理 | 校验、拆头、路由到 socket |
| 队列管理 | 把数据放入合适的接收队列 |
| 唤醒调度 | 让等待数据的线程重新 runnable |
如果小包很多,CPU 可能忙在“每个包的固定处理成本”,而不是业务计算。于是会出现:
代码块收起展开
带宽还没打满;
CPU 已经很高;
延迟开始上升;丢包或队列溢出增加。
这类问题的优化方向通常不是“让网卡更快”,而是减少每个事件的固定成本:批量处理、中断合并、多队列、减少小包、降低不必要的唤醒和复制。
边界条件:DMA 完成不等于应用已经处理完
DMA 完成只说明设备和内存之间的数据移动完成了。后面还有:
驱动看到完成
协议栈处理
数据进入内核缓冲区
应用线程被唤醒
应用真正读取和处理
如果应用消费慢,设备层可能已经很快,但上层仍然堆积:
| 层 | 可能堆积 |
|---|---|
| 设备 ring | 驱动来不及回收描述符 |
| 内核队列 | 协议栈或 socket buffer 堆积 |
| 应用队列 | 应用读到了但处理不过来 |
| 存储/网络下游 | 后续写出或转发阻塞 |
所以“DMA 已经降低 CPU 拷贝”不代表 IO 路径没有瓶颈。排障要问:
数据卡在设备队列、内核协议栈、socket buffer,还是应用自己的队列?
定位到具体队列,才能决定是调中断、调队列、优化协议栈路径、限流,还是优化应用消费。
排障卡:设备让 CPU 忙时先分清忙在哪里
设备相关 CPU 高,不一定是应用计算多。
| 忙的位置 | 可能原因 | 观察方向 |
|---|---|---|
| 用户态忙 | 应用处理数据太重 | 调用栈、处理函数、解析/拷贝 |
| 内核态忙 | 系统调用、协议栈、驱动工作多 | 内核时间、系统调用频率 |
| 中断忙 | 设备事件太频繁 | 中断计数、网卡/磁盘事件 |
| 软中断/延后处理忙 | 中断后半部积压 | 包处理、队列、批量处理 |
| DMA 后仍慢 | 消费者跟不上或复制多 | 缓冲区、内存带宽、队列长度 |
练习:画出“设备产生数据 -> 中断通知 -> 驱动搬运/登记 -> 应用读取”的路径。每个箭头旁边写一个可能排队的位置,你就能解释为什么设备很快,程序仍然慢。
11. 学完本章你能解决什么问题
学完这一章,你应该能解决或开始分析这些问题:
- 设备控制器为什么存在?
- 驱动程序负责什么?
- 轮询和中断各有什么取舍?
- 中断处理为什么不能太慢?
- DMA 如何减少 CPU 搬运数据?
- 设备队列为什么会造成 IO 延迟?
- 中断风暴为什么会让 CPU 忙?
- 为什么设备也能被抽象成文件,但又和普通文件不同?
设备、驱动和中断是 OS 连接硬件世界的桥。理解它们,才能解释很多 IO 和系统负载问题。