interrupt - dma
09. 中断与 DMA
0. 本章先解决什么问题
上一章已经看到,中断和 DMA 是高效 IO 的核心。本章单独展开,因为它们连接了组成原理和操作系统:
设备事件如何打断 CPU?
CPU 如何保存现场再回来?
大块数据为什么不用 CPU 手动搬?
本章要解决:
- 中断是什么,为什么需要它?
- 中断处理程序如何工作?
- 异常、中断、陷入有什么区别?
- 中断优先级和屏蔽是什么?
- DMA 的完整流程是什么?
- 中断和 DMA 会带来哪些性能和一致性问题?
这张图给出一个底层 IO 的核心分工:CPU 负责配置和收尾,设备/DMA 控制器负责大块数据搬运,完成后用中断通知 CPU。这样 CPU 不必一直轮询,也不必逐字节搬数据。
这张图怎么读
读这张图时,把 CPU 从“搬运工”改看成“调度者”:
CPU 配置 DMA
设备和内存之间搬数据
中断通知 CPU 完成
CPU 做收尾和错误处理
图里最重要的两个边界:
| 边界 | 风险 |
|---|---|
| 中断边界 | 控制流被打断,必须保存/恢复现场 |
| DMA 边界 | 设备绕过 CPU cache 读写内存,必须处理可见性和一致性 |
中断和 DMA 提高 IO 效率,但也把问题从“CPU 忙等”变成了“通知频率、缓冲管理和缓存一致性”。
1. 为什么不能只靠轮询
轮询方式:
while 设备未完成:
CPU 一直检查状态
如果设备很快,轮询还可以接受。
如果设备很慢,CPU 会浪费大量时间。
中断的目标是:
设备没事时,CPU 做别的事。
设备有事时,再通知 CPU。
这让 CPU 和设备可以更好地并发工作。
2. 中断是什么
中断是硬件或系统事件请求 CPU 暂停当前执行流,转去处理特定事件。
简化流程:
CPU 正在执行程序
设备发出中断请求
CPU 在合适时机响应
保存当前现场
跳到中断处理程序
处理中断
恢复现场
继续原来的程序
关键点:
中断改变控制流,但不应该让原程序无故丢失现场。
3. 中断向量和中断处理程序
不同中断需要不同处理逻辑。
系统会有中断向量表:
中断编号 -> 处理程序入口地址
例如:
| 中断来源 | 处理方向 |
|---|---|
| 时钟 | 更新系统时间、触发调度 |
| 键盘 | 读取输入 |
| 网卡 | 处理收到的数据包 |
| 磁盘 | 处理 IO 完成 |
| 错误异常 | 处理非法访问、除零等 |
CPU 收到中断编号后,根据向量表找到处理程序。
4. 保存现场和恢复现场
中断发生时,CPU 原来正在执行某个程序。
为了处理完中断还能回来,必须保存现场:
- 程序计数器。
- 状态寄存器。
- 某些通用寄存器。
- 必要的栈信息。
处理完后再恢复:
保存现场
-> 处理中断
-> 恢复现场
-> 回到原位置继续
如果现场保存错误,程序会表现得像“莫名其妙被改了状态”。
5. 异常、中断、陷入
这些概念都可能改变控制流,但来源不同。
| 概念 | 来源 | 例子 |
|---|---|---|
| 外部中断 | 外部设备 | 网卡、磁盘、键盘、时钟 |
| 异常 | 当前指令执行导致 | 除零、非法地址、缺页 |
| 陷入 trap | 程序主动请求进入内核 | 系统调用 |
共同点:
都让 CPU 从普通执行路径转入特殊处理路径。
区别在于触发原因和处理语义。
操作系统大量依赖这些机制:
- 时钟中断用于调度。
- 缺页异常用于虚拟内存。
- trap 用于系统调用。
- IO 中断用于设备完成通知。
6. 中断优先级和屏蔽
多个中断可能同时发生,需要决定先处理谁。
中断优先级用于排序:
高优先级中断可以先处理
低优先级中断等待
中断屏蔽用于临时禁止某些中断:
进入关键区域
-> 屏蔽部分中断
-> 完成关键操作
-> 恢复中断
但屏蔽中断不能滥用。屏蔽太久会导致:
- 设备响应延迟。
- 时钟不准。
- 系统交互卡顿。
- 丢事件风险增加。
7. 中断风暴
如果设备频繁产生中断,CPU 可能大部分时间都在处理中断。
这叫中断风暴。
可能原因:
- 网络包太多。
- 设备故障。
- 中断合并策略不合理。
- 驱动 bug。
- 系统负载过高。
解决方向:
- 中断合并。
- 批量处理。
- 降低中断频率。
- 使用轮询和中断混合策略。
- 优化驱动和缓冲区。
这解释了一个现象:
中断减少了空等,但中断太多也会成为负担。
8. DMA 的基本流程
DMA 让设备或 DMA 控制器直接和内存传输数据,CPU 不负责逐字节搬运。
流程:
CPU 设置 DMA 参数:
源地址
目标地址
数据长度
传输方向
CPU 启动设备
DMA 控制器搬运数据
搬运完成
设备发中断通知 CPU
CPU 做收尾处理
简化图:
设备 <---- DMA ----> 内存
\ /
—中断通知 CPU—/
CPU 从搬运工变成调度者。
手推:一次磁盘读如何经过中断和 DMA
把“读一个磁盘块”拆成硬件动作:
- CPU/内核准备一块内存缓冲区
- 驱动把缓冲区地址、长度、读命令写给设备控制器
- 设备开始从介质读取数据
- DMA 把数据写入指定内存区域
- 设备完成后发中断
- CPU 响应中断,进入处理程序
- 驱动确认完成状态,处理错误或成功
- 内核标记缓冲区可用,唤醒等待的执行流
这条路径里,CPU 做的是配置、检查和调度,不负责逐字节搬运数据。慢点也不只可能在设备:
| 阶段 | 可能慢点 |
|---|---|
| 提交命令 | 设备队列已满、驱动锁竞争 |
| 设备读取 | 介质延迟、控制器排队 |
| DMA 写内存 | 总线竞争、内存带宽 |
| 中断通知 | 中断合并、CPU 忙、优先级 |
| 唤醒应用 | 调度队列、锁、上层缓冲区 |
所以“磁盘读慢”在组成原理视角下,不只是磁盘本身慢,而是设备、总线、内存、中断、OS 调度共同组成的路径慢。
9. DMA 和 Cache 一致性
DMA 绕过 CPU 直接读写内存,会带来一致性问题。
场景 1:设备从内存读数据
CPU 修改了缓冲区
数据还在 Cache,没写回内存
设备 DMA 从内存读
设备读到旧数据
场景 2:设备写入内存
设备 DMA 把新数据写入内存
CPU Cache 里还有旧副本
CPU 继续读旧数据
所以系统需要 Cache flush、invalidate 或一致性协议来保证 DMA 缓冲区正确。
学习阶段先记住:
DMA 提高性能,但会让缓存一致性和内存可见性更复杂。
10. 零拷贝和 DMA 的关系
普通数据发送可能涉及多次复制:
磁盘 -> 内核缓冲区 -> 用户缓冲区 -> 内核 socket 缓冲区 -> 网卡
零拷贝优化会尽量减少用户态和内核态之间的复制,让数据路径更短:
磁盘/页缓存 -> 网卡
DMA 在其中负责设备和内存之间的高效传输。
核心思想:
如果程序不需要修改数据,就不要让数据来回穿过程序内存。
11. 中断和调度
时钟中断是操作系统调度的重要基础。
没有时钟中断,一个程序如果一直运行,系统很难定期夺回 CPU。
时钟中断让系统有机会:
- 更新时间。
- 检查当前任务运行时间。
- 决定是否切换任务。
- 处理定时器。
这连接到操作系统里的:
时间片
进程调度
上下文切换
定时器
12. 联系实际:为什么网络高负载会让 CPU 忙
高网络流量下,CPU 可能忙在:
- 网卡频繁产生中断。
- 操作系统处理大量包。
- DMA 缓冲区管理。
- 协议栈解析。
- 数据复制。
- 应用读取不及时导致队列堆积。
优化方向可能是:
- 中断合并。
- 批量收包。
- 增大或调整缓冲区。
- 减少复制。
- 分散到多个核心。
- 应用更快消费。
这说明 IO 性能不是“网速”一个指标决定的,它也依赖 CPU、中断、DMA、缓存和内存带宽。
排障卡:CPU 忙在 IO 上时看中断、复制和消费速度
网络或磁盘负载高时,CPU 忙不一定是在跑业务计算。可以按这张表拆:
| 现象 | 可能机制 | 方向 |
|---|---|---|
| 中断计数暴涨 | 设备频繁通知 CPU | 中断合并、批量处理、分散到多核 |
| CPU 花在内核态 | 协议栈、驱动、缓冲区管理 | 看包量、系统调用、内核队列 |
| 内存带宽压力高 | 大量数据复制 | 减少复制、批量、零拷贝路径 |
| 应用读不及时 | 内核队列堆积 | 提高消费速度、背压、线程/事件模型 |
| DMA 后数据异常 | Cache 可见性问题 | flush/invalidate、一致性边界 |
一个很实用的判断是:
如果吞吐上升时,业务 CPU 没明显增加,但系统 CPU 和中断处理明显增加,
问题可能在设备通知、内核协议栈、拷贝和缓冲区管理,而不是业务算法。
中断和 DMA 是性能工具,也会引入新的成本:通知太频繁会拖垮 CPU,绕过 Cache 的数据路径要处理一致性。
机制深挖:中断风暴不是“事件多”这么简单
中断风暴的核心是:
CPU 被设备通知路径占用太多,
真正留给应用和协议处理的时间变少。
一次事件的固定成本包括:
保存现场
跳到中断处理路径
读取设备状态
确认或清除中断
调度后续处理
恢复现场
如果每个包、每个小 IO、每个设备状态变化都触发一次通知,CPU 会不断进入这些固定路径。于是系统可能出现:
| 现象 | 解释 |
|---|---|
| 系统 CPU 高 | 大量时间花在内核/中断路径 |
| 应用吞吐不上去 | 应用拿到 CPU 的机会减少 |
| 延迟长尾升高 | 包或请求在后续队列里等待 |
| 丢包或设备队列满 | CPU 来不及消费通知后的工作 |
这也是为什么高性能 IO 会使用:
中断合并
批量轮询
多队列
每核队列
减少小包
它们的共同目标是摊薄“每次通知”的固定成本。
边界条件:屏蔽中断能保护临界区,也可能放大延迟
某些底层路径会短时间屏蔽中断,防止关键状态更新被打断。但屏蔽时间过长会带来副作用:
代码块收起展开
设备事件已经发生;
CPU 暂时不响应;
队列继续积压;响应延迟上升。
所以中断控制有一个硬约束:
需要原子保护的区域越短越好。
如果为了简单粗暴地避免并发问题而长时间屏蔽中断,就可能把正确性问题变成实时性和吞吐问题。
这和锁很像:
| 机制 | 保护什么 | 过度使用的代价 |
|---|---|---|
| 锁 | 共享数据不变量 | 阻塞、死锁、争用 |
| 屏蔽中断 | 关键硬件/内核状态 | 响应延迟、事件积压 |
底层系统设计的难点就在这里:既要保护状态一致性,又不能让外部事件等待太久。
13. 学完本章你能解决什么问题
学完这一章,你应该能解决或开始分析这些问题:
- 为什么中断比单纯轮询更高效?
- 中断发生时 CPU 如何保存和恢复现场?
- 外部中断、异常、trap 有什么区别?
- 中断优先级和屏蔽为什么必要?
- 中断风暴为什么会拖垮系统?
- DMA 如何减少 CPU 搬运大块数据的负担?
- DMA 为什么会带来 Cache 一致性问题?
- 零拷贝为什么能提升 IO 性能?
- 时钟中断为什么和操作系统调度有关?
中断让设备能通知 CPU,DMA 让设备能高效搬数据。它们是硬件 IO 和操作系统之间最重要的桥。