高级 #os#dma#io

DMA(直接存储器访问)

DMA(Direct Memory Access)让硬件设备可以直接读写内存,不需要 CPU 参与每字节的数据搬运——是现代高速 I/O(网卡、磁盘、GPU)性能的基石

搬运数据——CPU 该不该亲自干?

没有 DMA 时,硬盘读数据到内存要走以下流程:

程序调用 read()

CPU 发命令给磁盘控制器:从扇区 N 开始读

CPU 忙等:循环检查状态寄存器(PIO 方式)
或者 CPU 去做其他事,等磁盘中断

磁盘准备好数据

CPU 逐字从设备读入内存:
  mov rax, [设备数据寄存器]
  mov [内存缓冲区], rax
  重复 256 次…

数据读取完成 → 程序继续

问题:一个 4KB 的扇区,CPU 需要执行上千条指令来搬运数据——如果系统正在大量读写磁盘或网络,CPU 大部分时间都在做”搬运工”而不是真正的计算。

🏫 类比:快递搬运 你(CPU)在写论文(计算任务),快递员(硬盘)送来一个大箱子(数据)。没有 DMA:你必须自己把箱子搬进书房、拆箱、把东西放好——论文写不下去了。有 DMA:快递员直接把箱子搬进书房放好,然后通知你”东西到了”——你可以写完手头这页再去整理。

DMA 的工作原理

DMA 控制器(DMAC) 是一个独立的硬件模块,可以在不打扰 CPU 的情况下在设备和内存之间传输数据。

没有 DMA:                        有 DMA:
CPU ─→ 设备数据 ─→ CPU ─→ 内存    CPU ─→ 配置 DMA → 其他工作

                              DMA 控制器
                              ─→ 设备数据 ─→ 内存(或反向)

                                    完成 → 中断通知 CPU

传输流程

步骤 1:CPU 配置 DMA 控制器
  DMA 控制器有三个寄存器:
  • 源地址(设备或内存的地址)
  • 目的地址(内存或设备的地址)
  • 传输长度

步骤 2:CPU 启动 DMA 传输
  CPU 写控制寄存器 → DMA 开始工作

步骤 3:DMA 独立传输
  DMA 控制器在总线上直接读写内存和设备
  CPU 可以继续执行程序(缓存在 CPU 缓存中命中时不受影响)

步骤 4:传输完成
  DMA 控制器发中断通知 CPU
  CPU 在中断处理程序中知道数据已就绪

三种 DMA 传输方式

方式流程总线占用
突发模式(Burst)DMAC 独占总线,传完一个块才释放全程占用
周期窃取(Cycle Stealing)DMAC 每传一个字节释放一次总线一个周期
透明模式(Transparent)只在 CPU 不用总线时传输零影响

💡 现代系统大多用总线主控 DMA(Bus-Mastering DMA)——设备自己就是”总线主控”,可以直接读写内存,不需要专门的 DMA 控制器芯片。

操作系统中 DMA 的使用

// Linux 内核中 DMA 操作的简化流程

// 1. 分配 DMA 缓冲区(物理连续)
dma_addr_t dma_handle;
void* cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);

// 2. 告诉设备 DMA 缓冲区的物理地址
//    设备通过 PCIe BAR 空间得到这个地址
writel(dma_handle, dev->regs + DMA_ADDR_REG);
writel(size, dev->regs + DMA_SIZE_REG);
writel(CMD_DMA_START, dev->regs + DMA_CMD_REG);
//    设备开始 DMA 传输,CPU 可以继续执行

// 3. 设备完成 DMA 后发中断
//    中断处理程序中:
irqreturn_t dma_done_handler(int irq, void* dev_id) {
    // DMA 已完成,数据在 cpu_addr 指向的内存中
    process_data(cpu_addr);
    
    // 启动下一次 DMA 传输(如果需要)
    return IRQ_HANDLED;
}

DMA 的类型

1. 块设备 DMA(磁盘)

// 磁盘读操作的 DMA 流程
void disk_read(struct disk* disk, void* buf, int sector, int count) {
    // 1. 分配 DMA 缓冲区
    dma_addr_t dma_buf = dma_map_single(disk->dev, buf, count * 512, DMA_FROM_DEVICE);
    
    // 2. 配置 DMA
    write_reg(disk->regs, SECTOR_REG, sector);
    write_reg(disk->regs, COUNT_REG, count);
    write_reg(disk->regs, DMA_ADDR_REG, dma_buf);
    write_reg(disk->regs, CMD_REG, CMD_READ_DMA);
    // CPU 返回,做别的事
    
    // 3. 传输完成 → 中断 → 数据已在 buf 中
}

2. 网络 DMA

网卡收到的数据包直接通过 DMA 写入内存——CPU 不碰数据。

网卡收到网络包 ──→ 网卡通过 DMA 写入内存中的环形缓冲区

                      DMA 完成

                 网卡发中断通知 CPU

           CPU 从缓冲区取出数据发给协议栈

零拷贝网络(Zero-Copy Networking):数据从网卡到应用程序全程不走 CPU——DMA 直接传到用户空间缓冲区。

# 一些网卡支持零拷贝(如 Intel DPDK)
# 应用程序直接操作 DMA 缓冲区,不需要 read() 系统调用

3. GPU DMA

// GPU 通过 DMA 从 CPU 内存读取数据
// (典型的游戏渲染数据流)
cudaMemcpy(gpu_buffer, cpu_buffer, size, cudaMemcpyHostToDevice);
//          ↑ GPU 显存    ↑ CPU 内存
// 背后:CPU 启动 DMA 传输 → DMA 将数据从内存复制到 GPU 显存 → CPU 继续执行

DMA 与缓存一致性(Cache Coherency)

DMA 的数据在内存中,CPU 的数据在缓存中——它们会不一致!

问题场景:
1. CPU 写变量 x = 42(x 在缓存中,还没写回内存)
2. 设备通过 DMA 从内存读 x → 读到旧值!
                    或者
1. 设备通过 DMA 写内存(更新了 x)
2. CPU 读 x → 缓存中是旧值!

解决方案

// 方案 1:一致性 DMA 缓冲区(dma_alloc_coherent)
// 这块内存不会被 CPU 缓存(或硬件保证缓存一致性)
void* buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);

// 方案 2:流式 DMA 映射(dma_map_single)
// 使用前需要 CPU 主动刷新缓存
dma_addr_t addr = dma_map_single(dev, cpu_buf, size, DMA_FROM_DEVICE);
// CPU 不能读写 cpu_buf,直到 dma_unmap_single
dma_unmap_single(dev, addr, size, DMA_FROM_DEVICE);
// 这里保证了数据一致性

💡 现代 x86 CPU 使用 MESI 协议 维护缓存一致性——设备写入内存后,CPU 的缓存行自动失效。但有些架构(如 ARM)需要软件显式处理。

DMA 的性能优势

场景:读取 1MB 数据从磁盘到内存

没有 DMA(PIO 方式):
  • CPU 逐字读取:1MB / 8 字节 × 每次 ~10 个周期 ≈ 1.3M 条指令
  • CPU 利用率:100%(啥也干不了)
  • 传输时间:~10ms(受 CPU 限制)

有 DMA:
  • CPU 启动 DMA:几十条指令(微秒级)
  • CPU 利用率:接近 0%(可以继续计算)
  • 传输时间:~5ms(受磁盘/总线限制)

DMA 让 CPU 从”数据搬运工”变成了”任务管理者”——告诉硬件做什么,然后去做更重要的事。

小结

概念要点
DMA 定义硬件设备直接读写内存,无需 CPU 逐字拷贝
工作流程CPU 配置 DMA → DMA 独立传输 → 中断通知完成
总线主控现代设备自带的 DMA 能力(非专用 DMAC)
缓存一致性DMA 内存和 CPU 缓存之间的数据同步问题
零拷贝数据从设备到应用程序全程不经 CPU
性能收益CPU 从数据搬运中解放,专注计算任务

为什么先学这个? DMA 是计算机系统 I/O 性能的终极解决方案。至此,你已经学完了操作系统板块的全部核心知识——从进程管理、并发同步、内存管理、文件系统到 I/O 系统。接下来你可以进入计算机网络或编译原理板块继续学习。