前言
在理解mmap的"零拷贝"原理时,我们反复提到了DMA(Direct Memory Access,直接内存访问)。DMA是计算机系统中一项至关重要的技术,它让数据在内存和设备之间直接传输,而无需CPU的持续介入。
本文将深入剖析DMA的本质、工作原理,以及它如何与mmap配合实现高性能数据通路。
一、没有DMA的时代:CPU做苦力
1.1 传统PIO模式
在DMA出现之前,CPU与外设的数据传输使用**PIO(Programmed I/O,程序控制I/O)**模式:
// PIO模式下,CPU逐字节搬运数据
for (int i = 0; i < len; i++) {
data[i] = read_from_device(); // CPU读取设备寄存器
// 每个字节都要CPU执行指令
}
CPU 磁盘控制器 内存
│ │ │
│ 读数据命令 │ │
│───────────────────▶│ │
│ │ │
│ 读取1字节 │ │
│◀───────────────────│ 数据(1字节) │
│ │ │
│ 写入1字节 │ │
│────────────────────────────────────────▶│ 数据(1字节)
│ │ │
│ 读取1字节 │ │
│◀───────────────────│ 数据(1字节) │
│ │ │
│ 写入1字节 │ │
│────────────────────────────────────────▶│ 数据(1字节)
│ │ │
│ ... 重复N次 │ │
严重问题:
- CPU被完全占用,无法处理其他任务
- 传输速度受限于CPU指令执行速度
- 每个字节都要经过CPU寄存器
1.2 典型场景:40MB/s的SCSI磁盘
CPU速度:100MHz → 每秒1亿条指令
磁盘速度:40MB/s → 每秒传输4000万字节
如果每个字节需要10条指令:
4000万 × 10 = 4亿条指令/秒
需要400MHz的CPU才能满足!
→ CPU完全被IO任务占满,无法做其他工作
这就是DMA诞生的原因 :让CPU从数据搬运中解放出来。
二、DMA的本质与工作原理
2.1 DMA的定义
DMA(Direct Memory Access):一种硬件机制,允许外设直接读写系统内存,无需CPU介入。
核心思想:
CPU发号施令(指挥) → DMA控制器执行(搬运)
而不是 CPU既指挥又搬运
2.2 DMA控制器
DMA控制器是一个独立的硬件单元,专门负责数据搬运:
┌──────────────────┐
│ DMA控制器 │
│ ┌────────────┐ │
CPU │ │ 控制寄存器 │ │
│ │ │ - 源地址 │ │
│ 配置DMA传输 │ │ - 目标地址 │ │
│─────────────────▶│ │ - 传输长度 │ │
│ │ │ - 传输模式 │ │
│ 开始传输命令 │ └────────────┘ │
│─────────────────▶│ │ │
│ │ ┌────┴────┐ │
│ 其他任务... │ │ DMA引擎 │ │
│ 继续执行 │ └────┬────┘ │
│ │ │ │
│ ◀─ DMA完成中断 │ │ │
│──────────────────│ │ │
└─────────┼─────────┘
│
┌─────────┴─────────┐
│ 数据传输总线 │
│ (不经过CPU) │
└─────────┬─────────┘
│
┌─────────┴─────────┐
│ │
磁盘控制器 内存
2.3 DMA工作流程
1. CPU配置DMA控制器
├── 源地址:磁盘控制器的数据寄存器
├── 目标地址:内存缓冲区地址
├── 传输长度:4KB
└── 传输模式:读取
2. CPU启动DMA传输
└── 写控制寄存器启动位
3. DMA控制器独立工作
├── 从磁盘控制器读取数据
├── 直接写入内存缓冲区
└── 重复直到传输完成
4. DMA传输完成
├── 向CPU发送中断信号
└── CPU响应中断,处理数据
5. CPU继续执行其他任务
└── 传输期间CPU完全自由
2.4 一个完整的DMA传输示例
// 伪代码:从磁盘读取4KB数据到内存
void disk_read_dma(void *buffer, uint32_t sector, uint32_t count) {
// 1. 配置DMA控制器
dma_set_source(DISK_CONTROLLER_DATA_REG);
dma_set_destination(buffer);
dma_set_length(count * 512); // 扇区大小
dma_set_mode(DMA_READ);
// 2. 启动DMA传输
dma_start();
// 3. CPU可以干其他事
do_other_work();
// 4. 等待DMA完成(中断方式)
wait_for_dma_interrupt();
// 5. 数据已在buffer中
}
三、DMA与CPU的协作模式
3.1 三种传输模式
| 模式 | CPU占用 | 数据流向 | 适用场景 |
|---|---|---|---|
| 轮询模式 | 高(持续检查) | 外设↔内存 | 小数据量、实时性要求高 |
| 中断模式 | 低(仅在完成时中断) | 外设↔内存 | 大数据量、块设备 |
| 周期挪用 | 中(窃取CPU周期) | 外设↔内存 | 多媒体、实时数据流 |
3.2 DMA vs CPU直接控制
DMA方式:
CPU: [配置DMA] → [执行其他任务] ← [中断通知完成]
DMA: [数据搬运中...] → [完成]
CPU占用率:5%(配置+中断处理)
PIO方式:
CPU: [读1字节] → [写1字节] → [读1字节] → [写1字节] ...
CPU: 100%占用,无法执行其他任务
四、DMA在mmap中的作用
4.1 mmap + DMA = 零拷贝
现在我们可以完整理解mmap的"零拷贝"数据流:
步骤1:用户程序调用mmap
└── 内核建立VMA映射(仅建映射,不读取数据)
步骤2:用户访问mmap地址(如data[0])
└── MMU缺页中断 → 内核发起IO
步骤3:内核启动DMA传输
├── CPU配置DMA控制器
│ ├── 源:磁盘扇区X
│ └── 目标:页缓存物理页Y
├── CPU启动DMA
└── CPU继续执行(或被中断)
步骤4:DMA独立工作
├── 磁盘 → DMA → 页缓存(物理内存)
└── 无需CPU参与数据搬运
步骤5:DMA传输完成
├── 发送中断给CPU
└── 内核建立页表映射
步骤6:用户程序继续执行
└── 直接访问页缓存(虚拟地址→物理地址由MMU转换)
关键:
- 数据拷贝次数:1次(磁盘→页缓存,由DMA完成)
- CPU参与数据搬运:0次(只参与配置和中断处理)
- 系统调用次数:1次(mmap本身)
4.2 数据流完整图示
用户态 内核态 硬件层
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 进程 │ │ 页表 │ │ DMA │
│ 虚拟地址│────────▶│ 映射 │─────▶│ 控制器 │
│ 0x1000 │ │ 到物理页│ │ │
└──────────┘ └──────────┘ └────┬─────┘
│ │ │
│ 直接访问 │ │ DMA传输
│ (无系统调用) │ │ (无CPU参与)
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 用户程序│ │ 页缓存 │◀─────│ 磁盘 │
│ 读取数据│ │ (物理页)│ │ 文件 │
│ 从内存 │ │ │ │ │
└──────────┘ └──────────┘ └──────────┘
│ ▲ │
│ CPU参与 │ DMA完成 │ CPU只发命令
│ 只管理映射 │ 中断通知 │ DMA执行搬运
└─────────────────────┘ │
│
CPU发命令启动DMA
五、现代DMA的高级特性
5.1 分散-聚集DMA(Scatter-Gather DMA)
传统DMA要求内存连续,现代DMA支持不连续的内存:
// 分散-聚集列表
struct sg_list {
{addr: 0x1000, len: 4096},
{addr: 0x2000, len: 4096},
{addr: 0x3000, len: 4096},
};
// DMA将数据分别写入不连续的内存块
dma_set_sg_list(sg_list, 3);
dma_start();
// 数据分散到多个内存区域,无需CPU重组
5.2 环形缓冲区DMA
网卡驱动常用:
// 预先分配环形缓冲区
struct ring_buffer {
void *buffers[64]; // 64个缓冲区
int head;
int tail;
};
// DMA直接写入环形缓冲区
dma_set_ring_buffer(ring_buffer);
dma_start_continuous();
// 驱动处理时直接读取DMA写入的数据
5.3 RDMA(Remote DMA)
跨网络直接内存访问:
本地机器 网络 远程机器
┌─────────┐ ┌─────────┐
│ RDMA │───── 直接访问远程内存 ────▶│ RDMA │
│ 网卡 │ │ 网卡 │
└─────────┘ └─────────┘
│ │
│ │
应用程序 应用程序内存
内存 (被直接读写)
六、实际性能对比
6.1 不同方式读取1GB文件
| 方式 | CPU占用 | 耗时 | 上下文切换 |
|---|---|---|---|
| PIO(无DMA) | 100% | ~10秒 | 无(但CPU忙死) |
| 传统read(DMA) | 10% | ~500ms | 262,144次系统调用 |
| mmap(DMA+页表) | 5% | ~50ms | 3次系统调用 |
6.2 DMA解放CPU的效果
没有DMA:
CPU: [读取1字节] [写入内存] [读取1字节] [写入内存] ...
100%用于IO,无法运行应用程序
有DMA:
CPU: [配置DMA] [运行应用程序] [响应中断] [运行应用程序] ...
95%用于应用程序,5%用于IO管理
七、总结
DMA的本质
DMA = 让硬件直接搬运数据,CPU只做指挥
传统方式:CPU = 搬运工 + 指挥官
DMA方式: CPU = 指挥官(DMA = 搬运工)
DMA的核心价值
- 解放CPU:CPU从数据搬运中解放出来,专注于计算和控制
- 提高吞吐量:硬件搬运速度快于CPU逐字节搬运
- 降低延迟:减少CPU处理路径,数据更快到达
- 支持并行:CPU和DMA可以同时工作
DMA与mmap的关系
mmap提供了"不拷贝"的访问路径
DMA提供了"不占CPU"的搬运方式
两者结合 = 零拷贝 + 零CPU开销
适用场景
✅ 适合DMA的场景:
- 块设备(磁盘、SSD)
- 网络设备(网卡)
- 多媒体(音频、视频)
- 大数据量传输
❌ 不适合DMA的场景:
- 小数据量(DMA配置开销大于收益)
- 简单设备(无需复杂控制)
DMA是现代计算机系统中不可或缺的技术。它与mmap配合,实现了"数据从磁盘直接到用户程序虚拟地址,全程无需CPU参与"的高性能数据通路。理解DMA,是理解整个IO子系统性能的关键。