device - driver - interrupt

07. 设备、驱动与中断

0. 本章先解决什么问题

操作系统不仅管理 CPU 和内存,还要管理各种设备:

  • 磁盘。
  • 网卡。
  • 键盘。
  • 显示器。
  • USB 设备。
  • 传感器。

设备速度不同、协议不同、控制方式不同。OS 通过驱动和中断机制把它们纳入统一管理。

本章要讲:

  • 设备控制器和驱动是什么?
  • 中断如何让设备通知 CPU?
  • DMA 如何减少 CPU 搬运数据?
  • 为什么中断太多也会影响性能?
  • 设备队列和缓冲区如何影响 IO 延迟?

先看设备 IO 的路径:应用请求内核,驱动提交命令,设备通过 DMA 和中断把结果带回来。

设备、驱动、中断与 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 可能忙在“每个包的固定处理成本”,而不是业务计算。于是会出现:

代码块JAVA · 3 行收起展开
带宽还没打满;
CPU 已经很高;
延迟开始上升;

丢包或队列溢出增加。

这类问题的优化方向通常不是“让网卡更快”,而是减少每个事件的固定成本:批量处理、中断合并、多队列、减少小包、降低不必要的唤醒和复制。

边界条件:DMA 完成不等于应用已经处理完

DMA 完成只说明设备和内存之间的数据移动完成了。后面还有:

驱动看到完成
协议栈处理
数据进入内核缓冲区
应用线程被唤醒
应用真正读取和处理

如果应用消费慢,设备层可能已经很快,但上层仍然堆积:

可能堆积
设备 ring驱动来不及回收描述符
内核队列协议栈或 socket buffer 堆积
应用队列应用读到了但处理不过来
存储/网络下游后续写出或转发阻塞

所以“DMA 已经降低 CPU 拷贝”不代表 IO 路径没有瓶颈。排障要问:

数据卡在设备队列、内核协议栈、socket buffer,还是应用自己的队列?

定位到具体队列,才能决定是调中断、调队列、优化协议栈路径、限流,还是优化应用消费。

排障卡:设备让 CPU 忙时先分清忙在哪里

设备相关 CPU 高,不一定是应用计算多。

忙的位置可能原因观察方向
用户态忙应用处理数据太重调用栈、处理函数、解析/拷贝
内核态忙系统调用、协议栈、驱动工作多内核时间、系统调用频率
中断忙设备事件太频繁中断计数、网卡/磁盘事件
软中断/延后处理忙中断后半部积压包处理、队列、批量处理
DMA 后仍慢消费者跟不上或复制多缓冲区、内存带宽、队列长度

练习:画出“设备产生数据 -> 中断通知 -> 驱动搬运/登记 -> 应用读取”的路径。每个箭头旁边写一个可能排队的位置,你就能解释为什么设备很快,程序仍然慢。

11. 学完本章你能解决什么问题

学完这一章,你应该能解决或开始分析这些问题:

  1. 设备控制器为什么存在?
  2. 驱动程序负责什么?
  3. 轮询和中断各有什么取舍?
  4. 中断处理为什么不能太慢?
  5. DMA 如何减少 CPU 搬运数据?
  6. 设备队列为什么会造成 IO 延迟?
  7. 中断风暴为什么会让 CPU 忙?
  8. 为什么设备也能被抽象成文件,但又和普通文件不同?

设备、驱动和中断是 OS 连接硬件世界的桥。理解它们,才能解释很多 IO 和系统负载问题。

延伸阅读