文章目录
-
- I/O 数据传送控制方式
-
- 三种方式全景对比
- 程序直接控制(Programmed I/O)
-
- 无条件传送(同步传送)
- 条件传送(程序查询方式)—— 详细过程
- 定时查询 vs 独占查询(深度对比)
- 中断控制 I/O
-
- 中断控制的完整生命流程
- 中断 vs 异常 —— 同步性差异
- 中断系统核心寄存器组
- 中断响应三阶段(硬件视角)
- 中断嵌套与屏蔽字机制
- 中断源识别方法(软件 vs 硬件——深度对比)
- 直接内存访问(DMA)
-
- DMA 的本质:绕开 CPU 的数据高速公路
- DMA 控制器内部结构
- DMA 操作详细时序
- DMA 三种工作方式(详解 + 场景适配)
- DMA 与虚拟内存 / Cache 的冲突
I/O 数据传送控制方式
三种方式全景对比
I/O 控制方式的演进史,本质上是一部**“解放 CPU”**的历史——从 CPU 全程包办一切,到外设自主完成数据传输。
| CPU 参与度 | 全程参与,逐个数据字 | 每个数据字触发一次中断 | 仅在整个数据块传输的开始和结束各参与一次 |
| 数据传送触发者 | CPU(主动反复查询) | 外设(发中断信号通知 CPU) | DMA 控制器(接管总线自主搬运) |
| CPU 与外设并行 | ❌ 不能并行 | ✅ 能并行(CPU 和外设同时工作) | ✅ 完全并行(CPU 甚至感知不到传输) |
| 额外硬件成本 | 极低(基本 I/O 接口即可) | 中等(需要中断控制器 PIC) | 较高(需要 DMA 控制器 + 总线仲裁逻辑) |
| 适用设备 | 极低速(开关/传感器) | 中低速随机设备(键盘/鼠标/打印机) | 高速块设备(磁盘/SSD/万兆网卡/GPU DMA) |
| CPU 利用率 | 极低(大量时间空转轮询) | 中(上下文切换开销) | 极高(整个传输期间 CPU 可执行其他进程) |
| 最大传输速率 | 受 CPU 轮询频率限制 | 受中断处理频率限制(10<sup>4105 次/秒) | 仅受总线和设备带宽限制(GB/s 级) |
I/O 控制方式的演进路线 (由 CPU 参与多 → 少):
程序直接控制 中断控制I/O DMA控制 I/O 通道/IOP
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ CPU 全程 │ ───► │ CPU 仅 │ ───► │ 硬件自主 │ ───► │ 专用I/O │
│ 主动轮询 │ │ 响应中断 │ │ 直接传送 │ │ 处理器 │
│ 不能并行 │ │ 可与外设 │ │ CPU 几乎 │ │ CPU 彻底 │
│ │ │ 并行工作 │ │ 不参与 │ │ 解放 │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
▲ ▲ ▲ ▲
│ │ │ │
极低速设备 中低速设备 高速块设备 大型机/服务器
开关/传感器 键盘/打印机 磁盘/网卡/SSD 通道子系统
程序直接控制(Programmed I/O)
无条件传送(同步传送)
无条件传送的硬件接口(极简版):
系统总线
数据总线 ◄────────────────────────────►
│
┌────┴────┐
│ 三态缓冲器│ ← 当 CE 有效 + RD 有效时, 将输入数据放上总线
└────┬────┘
│
┌────┴────┐
│ 锁存器 │ ← 当 CE 有效 + WR 有效时, 锁存总线数据
└────┬────┘
│
┌────────┴────────┐
▼ ▼
输入设备 输出设备
(开关/传感器) (LED/继电器/7段显示器)
接口极简:有时甚至不需要状态寄存器——CPU 假设设备"永远就绪"。
无条件传送的核心假设:外设的状态变化足够慢,以至于 CPU 在任何时刻访问它,它都处于"准备好"的状态。
| ① 设备状态变化极慢(秒级或更长) | 任何速度超过 CPU 轮询频率的设备 |
| ② CPU 有固定周期访问该设备(如 PLC 扫描周期) | 设备状态变化不可预测(如键盘用户随时按键) |
| ③ 错过一次读/写不会造成严重后果 | 需要确保每一次数据都被捕获 |
典型应用:
- 工业控制中的巡回采样(PLC 读取各传感器值)
- LED 数码管刷新(每几 ms 更新一次显示)
- 开关量输入/继电器输出
条件传送(程序查询方式)—— 详细过程
条件传送的核心循环:
启动外设
│
▼
┌────────────────────────────────────────────┐
│ 状态查询循环 │
│ │
│ ┌──────────────┐ Busy/Not Ready │
│ │ 读状态寄存器 │──────────────────────. │
│ └──────┬───────┘ . │
│ │ Ready/Done . │
│ ▼ . │
│ ┌──────────────┐ . │
│ │ 传送 1 字节 │ . │
│ └──────┬───────┘ . │
│ │ . │
│ ▼ . │
│ 所有字节传完? ──── 否 ────┐ . │
│ │ │ . │
│ │ 是 ▼ . │
│ ▼ ┌──────────┐ . │
│ 传输完成 │ 继续查询 │◄──────┘ │
│ └──────────┘ │
└────────────────────────────────────────────┘
六步骤详解(以打印机为例):
步骤1: CPU 执行 OUT 指令, 向控制寄存器写入"启动"命令字
→ 接口内部: 命令触发器 B ← 1, 状态触发器 D ← 0
步骤2: I/O 接口解码命令, 向打印机发出"启动"信号
→ 打印机开始接收数据准备打印
步骤3: 打印机逐字处理数据, 完成后将结果送入接口的数据缓冲寄存器
步骤4: 打印机向接口发"工作结束"信号
→ 接口内部: D ← 1 (Done), B ← 0 (命令执行完毕)
步骤5: CPU 循环执行 IN 指令读取状态寄存器, 检查 D 位
→ 如果 D=0 (未完成): 继续循环 (可能执行数千至数百万次循环!)
→ 如果 D=1 (已完成): 跳出循环, 进入步骤6
步骤6: CPU 执行 IN 指令从数据缓冲寄存器读取数据 (或执行 OUT 写入下一数据)
→ 一次数据交换完成, 如需继续则回到步骤1
条件传送的 CPU 时间浪费量化分析:
假设:
· CPU 主频 2 GHz (周期 0.5 ns)
· 打印机每字符处理时间 5 ms
· 状态查询循环: 读状态寄存器 + 判断 + 跳转 ≈ 10 条指令 ≈ 5 ns (现代 CPU 流水线)
每个字符等待期间:
CPU 可执行 5 ms ÷ 5 ns ≈ 1,000,000 次状态查询!
→ 其中 999,999 次查询结果都是"还没好"
→ CPU 利用率 ≈ 0.0001%
如果改为中断方式:
CPU 启动打印机后就去执行其他任务
打印机完成时发中断 → CPU 回来处理 (中断开销 ~1 µs)
→ CPU 利用率接近 100%
定时查询 vs 独占查询(深度对比)
定时查询:
时间轴: ──┬──────┬──────┬──────┬──────┬──────►
查询 查询 查询 查询 查询
↑ ↑ ↑ ↑ ↑
固定间隔 (如每 10ms 一次)
设备未就绪时: 浪费一次查询
设备刚就绪时: 最多延迟一个间隔
独占查询:
时间轴: ──┬──────────────────┬──────────────►
查询 查询
↑ ↑
设备状态变化才触发
设备未就绪时: CPU 做其他事, 完全不查询
设备刚就绪时: 立即查询, 延迟最小
| 触发条件 | 定时器到期即查询 | 设备状态变化信号触发 |
| 实现复杂度 | 简单——只需一个定时中断 | 较复杂——需设备支持状态变化通知 |
| CPU 占用 | 按固定频率消耗 CPU | 按需消耗,设备不活跃时几乎为零 |
| 最坏延迟 | 一个查询间隔 (可调) | 几乎为零 (设备一变就查) |
| 适用场景 | 低速、定期更新的传感器 | 高速、实时性要求高的设备 |
| 典型实现 | 内核定时器 + 轮询函数 | Linux NAPI (网卡中断 + 轮询混合)、epoll |
中断控制 I/O
中断控制的完整生命流程
时间线视角 (以磁盘读取 4KB 数据块为例):
t0: 进程 R 发出 read() 系统调用
│
▼
t1: CPU 配置磁盘控制器 → 启动读操作
进程 R 被 OS 标记为阻塞态, 移入等待队列
OS 调度进程 P2 在 CPU 上运行
│
│ ╔═══════════════════════════════════════╗
│ ║ 并行窗口: ║
│ ║ · 磁盘: 寻道 + 旋转延迟 + 数据读取 ║
│ ║ · CPU: 执行进程 P2 (计算、内存访问等) ║
│ ║ · 两者互不干扰, 真正并行 ║
│ ╚═══════════════════════════════════════╝
│
▼
t2: 磁盘控制器将第一个扇区数据读入内部缓冲区
磁盘控制器 → 中断控制器 (PIC/APIC): 拉高 IRQ 线
│
▼
t3: CPU 检测到 INTR 引脚有效
(在当前指令执行完毕后检查)
├─→ CPU 向 PIC 发 INTA (中断确认)
├─→ PIC 将中断向量号送上数据总线
├─→ CPU 保存当前上下文 (PC, PSW, 关键寄存器)
├─→ CPU 从中断向量表取出 ISR 入口地址
└─→ CPU 跳转到磁盘 ISR 开始执行
│
▼
t4: 磁盘 ISR 执行:
├─→ 读取磁盘控制器的数据寄存器 → 拷入内核缓冲区
├─→ 如果需要继续传输, 再次配置磁盘控制器 (启动下一个扇区)
├─→ 如果全部传输完成, 唤醒等待的进程 R
└─→ 发出 EOI (End Of Interrupt) 给中断控制器
│
▼
t5: 执行 IRET 指令 → 恢复被中断进程 P2 的上下文 → P2 继续执行
t6: 当进程 R 被再次调度到 CPU 上时, 它的 read() 调用返回, 数据已在用户缓冲区中
中断 vs 异常 —— 同步性差异
外部中断 (异步): 内部异常 (同步):
指令1 ████████████ 指令1 ████████████
指令2 ████████████ 指令2 ████████████
指令3 ██████░░░░░░ ← 中断在此 指令3 ████💥 ← 异常发生在本指令内部
░░ 响应中断 ░░ 跳转到异常处理
░░ ░░
指令4 ████████████ ← ISR返回后继续 指令4 ──跳过── (可能不返回)
指令5 ████████████ 指令5 ──跳过──
中断: 与指令流异步, 指令3正常执行 异常: 与指令3同步, 指令3无法完成
完毕后才响应中断 指令3 本身是异常的"案发现场"
| 发生时机 | 任意时刻, 与当前指令无关 | 某条特定指令执行期间 |
| 与指令的关系 | 异步 | 同步 |
| 触发来源 | I/O 设备、定时器、其他硬件 | CPU 本身检测 (除零、缺页、非法指令) |
| 可屏蔽性 | 可屏蔽 (通过 IF 标志 / IMR) | 不可屏蔽 (必须立即处理) |
| 返回行为 | 总是返回到被中断指令的下一条指令 | 视情况而定:缺页→重执行该指令;除零→可能终止进程 |
| 处理程序 | 中断服务程序 (ISR) | 异常处理程序 (Exception Handler) |
| 典型例子 | 键盘按键、磁盘 I/O 完成、网卡收包 | 除零错误 (#DE)、缺页 (#PF)、非法指令 (#UD)、断点 (#BP) |
📌 系统调用 (syscall) 在 x86 上通过 INT 0x80 或 SYSCALL 指令触发——它属于软件中断/陷阱,是同步的(由特定指令主动发起),但处理机制和中断类似。
中断系统核心寄存器组
┌──────────────────────────────────────────────────────────┐
│ 中断控制器 (PIC / APIC) │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ IRR │ │ IMR │ │ IPR │ │
│ │中断请求寄存器│ │中断屏蔽寄存器│ │中断优先级寄存器│ │
│ ├────────────┤ ├────────────┤ ├────────────┤ │
│ │每 bit 对应 │ │每 bit 对应 │ │记录当前正在 │ │
│ │一个中断源 │ │一个中断源 │ │服务的中断 │ │
│ │ │ │ │ │ │ │
│ │bit=1: │ │bit=1: │ │用于中断嵌套 │ │
│ │有中断请求 │ │该中断被屏蔽 │ │判优 │ │
│ │bit=0: │ │bit=0: │ │ │ │
│ │无请求 │ │该中断允许 │ │ │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │
│ 判优逻辑: IRR & (~IMR) → 选出未屏蔽的最高优先级 → 通知CPU│
└──────────────────────────────────────────────────────────┘
中断响应三阶段(硬件视角)
阶段一: 中断响应 (纯硬件, CPU 自动完成)
┌─────────────────────────────────────────────────┐
│ 条件检查: │
│ ① CPU 处于开中断状态 (EFLAGS.IF = 1)? │
│ ② 有未被屏蔽的中断请求 (IRR & ~IMR ≠ 0)? │
│ ③ 当前指令已执行完毕? │
│ ↓ 三个条件都满足 │
│ ④ 关中断 (IF ← 0) │
│ ⑤ 保存断点: │
│ · CS (代码段寄存器) + EIP (指令指针) ↦ 压栈 │
│ · EFLAGS (标志寄存器) ↦ 压栈 │
│ ⑥ 向 PIC 发 INTA (中断确认) │
│ ⑦ PIC 返回中断向量号 n │
│ ⑧ n × 4 = 中断向量表索引 → 加载 ISR 入口地址 │
│ · CS ← 向量表[n].段基址 │
│ · EIP ← 向量表[n].偏移量 │
│ ⑨ 开始执行 ISR 的第一条指令 │
└─────────────────────────────────────────────────┘
│
▼
阶段二: 中断处理 (软件, ISR 代码执行)
┌─────────────────────────────────────────────────┐
│ ISR 开头 (通常用汇编编写): │
│ ① PUSH 所有将被修改的通用寄存器 (保存现场) │
│ ② 设置 DS/ES 等段寄存器指向内核数据段 │
│ ③ 开中断 (STI 指令) —— 允许更高优先级中断嵌套 │
│ │
│ ISR 主体 (C 语言): │
│ ④ 读取设备状态/数据寄存器 │
│ ⑤ 处理中断事件 (将数据拷入缓冲区, 唤醒等待进程等) │
│ ⑥ 如有必要, 配置设备开始下一次 I/O │
│ │
│ ISR 结尾: │
│ ⑦ 关中断 (CLI 指令) —— 准备恢复 │
│ ⑧ 向 PIC/APIC 发 EOI (中断结束通知) │
│ ⑨ POP 所有通用寄存器 (恢复现场) │
│ ⑩ IRET 指令 │
└─────────────────────────────────────────────────┘
│
▼
阶段三: 中断返回 (IRET 指令, CPU 自动完成)
┌─────────────────────────────────────────────────┐
│ IRET 指令自动完成: │
│ ① 从栈中弹出 EFLAGS ← 恢复中断前的标志状态 │
│ ② 从栈中弹出 CS ← 恢复代码段 │
│ ③ 从栈中弹出 EIP ← 恢复指令指针 │
│ ④ 从此地址继续执行——仿佛什么都没发生过 │
└─────────────────────────────────────────────────┘
中断嵌套与屏蔽字机制
中断屏蔽字示例 (假设8个中断源, 1=允许响应):
中断源 屏蔽字 (二进制) 含义
──────────────────────────────────────────
交通事故 (最高) 1111_1111 可被任何中断打断 (但无更高级别可打断它)
车辆拥堵 0111_1111 可被交通事故打断, 不能被更低级中断打断
信号灯故障 (最低) 0100_1111 只允许被交通事故和车辆拥堵打断
…
嵌套执行示例:
主程序 ──────────────────────────────────────────────►
│ │ │
│ 信号灯故障中断 │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ 信号灯 │─────►│车辆拥堵│─────────►│交通事故│
│ 故障 │ 打断 │ ISR │ 打断 │ ISR │
│ ISR │◄─────│ │◄─────────│ │
└────────┘ 返回 └────────┘ 返回 └────────┘
│
处理完毕
│
依次返回: 交通事故→车辆拥堵→信号灯故障→主程序
中断源识别方法(软件 vs 硬件——深度对比)
软件查询法:
ISR 入口处:
读 PIC 状态 → 有中断?
├─ 是 PIC 的 IRQ0 (定时器)? → 调用 timer_isr()
├─ 是 PIC 的 IRQ1 (键盘)? → 调用 keyboard_isr()
├─ 是 PIC 的 IRQ14 (主硬盘)? → 调用 primary_disk_isr()
└─ 是 PIC 的 IRQ15 (从硬盘)? → 调用 secondary_disk_isr()
优点: 查询顺序 = 优先级, 改代码即可改优先级
缺点: 每个中断都要遍历查询链, O(n) 时间复杂度, 设备多了就慢
硬件判优法(菊花链 / 向量中断):
菊花链 (Daisy Chain):
中断确认信号 INTA ──► 设备A ──► 设备B ──► 设备C ──► CPU
(优先级最高) (优先级最低)
离 CPU 越近的设备优先级越高。
INTA 信号沿链路传递,第一个有中断请求的设备截获 INTA,
将其向量号放上数据总线,并阻断 INTA 继续向后传递。
| 软件查询 | 慢 (O(n)) | 高 | 极低 | 可以 (查询顺序即优先级) |
| 硬件判优 (PIC) | 快 (O(1)) | 中 (通过编程 PIC 寄存器调整) | 中 | 可以 |
| 硬件判优 (APIC) | 极快 | 高 (支持动态重定向) | 高 | 可以, 且支持多核间中断 (IPI) |
直接内存访问(DMA)
DMA 的本质:绕开 CPU 的数据高速公路
三种数据传送路径对比:
① 程序查询/中断方式 (PIO):
┌──────┐ ┌──────┐ ┌──────┐
│ 外设 │ ──► │ CPU │ ──► │ 内存 │ CPU 是数据必经之路
└──────┘ └──────┘ └──────┘ 每次传 1~4 字节, CPU 全程参与
② DMA 方式:
┌──────┐ ┌──────┐
│ 外设 │ ════════════► │ 内存 │ 数据直接传输, 不经过 CPU
└──────┘ DMA 控制器 └──────┘ CPU 仅在开始配置和结束时收通知
▲
接管总线
CPU 暂时让出总线控制权
DMA 控制器内部结构
┌──────────────────────────────────────────────────┐
│ DMA 控制器 │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 源地址寄存器 │ │目标地址寄存器│ │
│ │ (SAR) │ │ (DAR) │ │
│ └──────┬──────┘ └──────┬──────┘ │
│ └────────┬───────┘ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ 计数寄存器 (TCR) │ │
│ │ 剩余传输字节数, 每次传输减1 │ │
│ │ 减到 0 → 传输完成 → 发中断 │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 控制/状态寄存器│ │ 模式寄存器 │ │
│ │ Start/Stop │ │ 单次/块/链 │ │
│ │ Done/Error │ │ 读/写/校验 │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ 总线接口逻辑 │
│ (HRQ/HLDA 与 CPU 握手, DREQ/DACK 与外设握手) │
└──────────────────────────────────────────────────┘
DMA 操作详细时序
DMA 传输一次数据块 (如磁盘读取 4KB → 内存):
预备阶段 (CPU 执行):
┌────────────────────────────────────────────┐
│ 1. CPU 写 DMA 控制器的 SAR ← 磁盘数据端口地址 │
│ 2. CPU 写 DMA 控制器的 DAR ← 内存目标缓冲区地址│
│ 3. CPU 写 DMA 控制器的 TCR ← 4096 (4KB) │
│ 4. CPU 写 DMA 控制器的模式寄存器 ← 块传输+地址增│
│ 5. CPU 写 DMA 控制器的控制寄存器 ← "启动" │
│ 6. CPU 向磁盘控制器发"读扇区"命令 │
│ 7. CPU 返回执行其他进程 (进程调度) │
└────────────────────────────────────────────┘
│
▼
传输阶段 (纯硬件, DMA 控制器自主完成):
┌────────────────────────────────────────────┐
│ 循环 4096 次: │
│ a. 磁盘控制器 → DREQ (DMA请求)有效 │
│ b. DMA 控制器 → HRQ (总线请求) → CPU │
│ c. CPU 完成当前总线周期 → HLDA (总线允许) │
│ d. DMA 控制器接管总线: │
│ · 地址总线 ← SAR 的值 │
│ · 控制总线 ← IOR# (I/O读)有效 │
│ · 数据从磁盘控制器流入 DMA 控制器暂存 │
│ · 地址总线 ← DAR 的值 │
│ · 控制总线 ← MEMW# (内存写)有效 │
│ · 数据从 DMA 控制器写入内存 │
│ e. SAR++, DAR++, TCR– │
│ f. 释放总线 (HRQ 无效) │
│ g. 如果 TCR ≠ 0, 回到步骤a │
│ h. 如果 TCR = 0, 跳转到完成阶段 │
└────────────────────────────────────────────┘
│
▼
完成阶段:
┌────────────────────────────────────────────┐
│ 8. DMA 控制器 → IRQ 线 → CPU (发中断) │
│ 9. CPU 执行 DMA ISR: 检查传输状态, │
│ 唤醒等待 I/O 完成的进程 │
│ 10. 进程读取内存缓冲区 → 数据已就位! │
└────────────────────────────────────────────┘
DMA 三种工作方式(详解 + 场景适配)
方式一: CPU 停止法 (Burst Mode / Block Mode)
CPU ████████████████░░░░░░░░░░░░░░░░░░░░░░░░████████████
│ │
└── DMA传输整个块 ──┘
CPU 完全停止
方式二: 周期挪用法 (Cycle Stealing)
CPU ██░██░████░░███░█░█████░░██░████░██░░███░█████░██
│ │ │ │ │ │ │ │ │
└──┴──────┴──┴───────┴──┴─────┴─────┴───────┘
每次DMA只挪用1个总线周期, CPU仅在该周期等待
方式三: 交替分时法 (Transparent Mode / Time-Sliced)
┌───┐┌───┐┌───┐┌───┐┌───┐┌───┐┌───┐┌───┐
│ C ││ D ││ C ││ D ││ C ││ D ││ C ││ D │ C=CPU D=DMA
│ P ││ M ││ P ││ M ││ P ││ M ││ P ││ M │ 每个存储周期
│ U ││ A ││ U ││ A ││ U ││ A ││ U ││ A │ 严格交替
└───┘└───┘└───┘└───┘└───┘└───┘└───┘└───┘
需要总线时钟频率 ≥ 2× CPU 需求频率
| CPU 停止法 | 长时间完全停止 | DMA 期间 100% 归 DMA | 最简单 | 大量连续数据传输(老式磁盘整道读写) |
| 周期挪用法 | 每字节仅停顿 1 个总线周期 | DMA 和 CPU 交替使用 | 中等 | 数据传输量适中,CPU 不能长时间停顿 |
| 交替分时法 | 几乎不感知 | 50/50 轮转 | 最复杂 | 高吞吐场景(现代高速 DMA 常用此法) |
📌 现代 DMA 控制器(如 Intel I/OAT)还支持分散-聚集(Scatter-Gather)DMA:不要求数据在物理内存中连续,DMA 控制器通过一个描述符链表(每个描述符 = 基址 + 长度),自动跳转完成不连续物理页的传输。这正是现代操作系统零拷贝(Zero-Copy)网络栈的基础。
DMA 与虚拟内存 / Cache 的冲突
在引入 DMA 之后,内存有了两个独立的访问者——CPU(通过 MMU 和 Cache)和 DMA 控制器(直接访问物理内存)。这带来了两个经典问题。
问题一:虚拟地址 vs 物理地址
CPU 视角: DMA 视角:
┌──────┐ ┌──────────┐
│ CPU │ │ DMA │
└──┬───┘ └────┬─────┘
│ 虚拟地址 │ 物理地址
▼ ▼
┌──────┐ ┌──────────┐
│ MMU │ │ 物理内存 │
│翻译 │ └──────────┘
└──┬───┘
│ 物理地址
▼
┌──────────┐
│ 物理内存 │
└──────────┘
问题: 用户进程的缓冲区在虚拟地址空间中是连续的 4KB,
但映射到的物理页框可能分散在 2 个不连续的 4KB 物理页中。
DMA 控制器只知道物理地址 → 无法直接完成跨页传输。
解决方案:
- 操作系统在发起 DMA 之前,检查用户缓冲区是否跨物理页
- 如果跨页,将传输拆分为多次 DMA(每次限于一个物理页内)
- 或者使用 I/O MMU(IOMMU),为 DMA 设备提供虚拟→物理地址翻译
问题二:Cache 一致性
场景 A: DMA 写入主存, CPU 从 Cache 读到旧数据
┌──────┐ ┌───────────┐ ┌──────────┐
│ CPU │◄──│ Cache │ │ 主存 │ ← DMA 写入新数据
└──────┘ │ v=100 (旧)│ │ v=200 │
└───────────┘ └──────────┘
CPU 读 v → 从 Cache 取到 100 → 错误! 应该是 200
场景 B: CPU 写入 Cache (写回模式), DMA 从主存读到旧数据
┌──────┐ ┌───────────┐ ┌──────────┐
│ CPU │──►│ Cache │ │ 主存 │ ← DMA 读取
└──────┘ │ v=200 (新)│ │ v=100 │
│ (未写回) │ │ (旧值) │
└───────────┘ └──────────┘
DMA 读 v → 从主存取到 100 → 错误! 应该是 200
解决方案汇总:
| MESI 协议 | 硬件自动维护 Cache 和主存一致性;当 DMA 访问内存时,一致性引擎自动使相关 Cache 行失效或刷新 | 硬件支持缓存一致性的系统 (x86 多数情况) |
| 软件 Cache Flush | DMA 写之前:flush_cache() 将 CPU Cache 中的脏数据写回主存;DMA 读之后:invalidate_cache() 使对应 Cache 行失效 | 无硬件一致性支持的嵌入式系统 |
| Uncached 内存区 | 将 DMA 缓冲区所在内存页标记为不可缓存 (Uncacheable),CPU 每次直接访问主存 | 简单粗暴,但 CPU 访问变慢 |
| DMA 专用内存区 | 在内存中划分一块"DMA 专用区",CPU 不使用 Cache 访问该区域 | 需要 OS 和驱动程序协同管理 |
| IOMMU + 一致性 | IOMMU 不仅做地址翻译,还参与 Cache 一致性协议(如 Intel VT-d) | 虚拟化环境、现代服务器 |


