4. DMA传输与CPU轮询:性能差异的底层逻辑

面试里被问到DMA和轮询的区别,很多人能背出定义。但真的上手调过SDIO的人会知道,这里面坑很多。我个人习惯把这两种方式看作两种截然不同的人——一个是你亲自盯着仪表盘,另一个是你雇了个自动记录的机器人。

根本区别:谁在做数据搬运

说白了,CPU轮询就是让CPU不断检查状态寄存器,判断数据是否准备好了。一旦准备好了,CPU就把数据从FIFO搬到内存里。这个过程里,CPU全程参与每个字节的传输。

DMA呢?它是个独立的硬件搬运工,你给它设定好源地址、目的地址、长度,它就自己埋头干活,搬完了才给你一个中断信号。整个过程CPU完全可以去跑别的任务。

直接看结果?我来给你列个表格,这样更直观:

对比项 CPU轮询 DMA传输
数据搬运者 CPU核心 DMA控制器
CPU占用率 接近100%(搬运期间) 仅设置和中断时占用
单次传输开销 每条指令需取指、译码、执行 一次配置后硬件自动完成
适用场景 小数据、实时性要求极低 大数据块、高吞吐要求
中断频率 无中断,但忙等待 传输完成产生一次中断
对Cache的影响 频繁写内存导致Cache颠簸 直接内存访问,影响小

核心结论:SDIO单次数据传输通常在512字节~4KB之间。用CPU轮询来搬这些数据,CPU得耗时几百微秒。用DMA,这个时间可以被系统调度器用来跑其他任务。

数据搬运效率对比

先看一个实际场景。假设SDIO工作在50MHz的4-bit模式,理论带宽约200Mbps。我们来算一下搬运一个4KB块需要多少CPU时间。

CPU轮询的典型流程:

  1. 读状态寄存器——30个CPU周期
  2. 判断读完成位——2个周期
  3. 如果完成,从FIFO读数据——50个周期
  4. 写数据到内存——20个周期

每个4字节的传输需要约100个CPU周期。4KB需要1000次传输,总共10万周期。假设CPU跑在500MHz,那就是200微秒。这200微秒里,CPU不能干任何其他事。

而DMA呢?

  • 配置DMA通道:约1000个周期
  • 等待DMA完成:0周期(CPU退出)
  • 中断处理:约500个周期

总开销就1500个周期,3微秒。其他197微秒都还给系统了。

实际提效点:在多任务系统里,DMA省下的那197微秒可以被调度器分配给实时性任务。我曾在RTOS里做过测试,一个任务用DMA传数据,另一个优先级更高的控制任务响应时间从平均300μs降到了50μs。

CPU能不能同时干别的?

这个问题的关键在于:中断和忙等待对CPU的影响有多大。

CPU轮询时,CPU处于忙等待状态。你以为它在检查状态,但操作系统并不知道它在等IO。如果被高优先级任务抢占,轮询任务就会延迟,导致数据丢失。如果不被抢占,那整个CPU就被这个轮询任务占死了。

DMA不一样。DMA传输启动后,CPU可以执行完全不相干的任务。操作系统也不关心DMA在做什么,只会在传输完成后收到一个硬件中断。

我看到过一个场景:某同事在项目中用CPU轮询做SDIO读取,跑实时采集,结果音频采集任务老是丢给SDIO的轮询。换成DMA后,问题直接解决了。原因很简单——DMA不会阻塞其他任务的中断响应。

什么场景该用谁?

有些面试官喜欢问:能不能给个明确的场景选择?我的习惯是这样分的:

  • 高吞吐场景(SDIO/UFS/eMMC):毫不犹豫选DMA。单次传输超过64字节,DMA就赢了。
  • 超大传输(>1MB):必须用DMA。CPU搬1MB数据,基本上所有实时性都崩了。
  • 低延迟控制信号:极短数据(4-8字节),用CPU直接读写寄存器反而更快,因为DMA配置延迟超过传输本身。
  • 小数据量但频繁:比如每100μs就传几个字节,用DMA不一定划算。DMA描述符设置的开销可能超过节省的时间。
场景 推荐方式 原因
SDIO批量写入 DMA 数据块大,CPU开销明显
寄存器配置 CPU直接写 传输量极低,DMA配置不划算
高实时任务共存 DMA 释放CPU给关键任务
极低功耗模式 DMA CPU可以睡眠,DMA独立运行

DMA的关键参数调整

光选DMA还不够,有些参数不调好,吞吐量照样上不去。

1. Burst Size(突发长度)

DMA控制器每次请求总线时,一次传多少个数据单元。建议设置为SDIO FIFO深度的一半。比如FIFO是16个字,burst设成8。设太小了总线利用率低,设太大可能等FIFO填满导致DMA空等。

2. 描述符链

单个DMA传输完成会触发中断。但如果你有连续大批量传输,用描述符链可以让DMA自动加载下一个任务。CPU只需要配置一次描述符链表,DMA就一直跑下去。

代码示例(伪代码,描述符链配置):

/* 描述符结构 */
struct dma_desc {
    uint32 *src;
    uint32 *dst;
    uint32  length;
    struct dma_desc *next;
};

/* 配置链条 */
struct dma_desc desc1, desc2, desc3;

desc1.src = buf1;
desc1.dst = sdio_fifo;
desc1.length = 1024;
desc1.next = &desc2;    /* 自动加载下一个 */

desc2.src = buf2;
desc2.dst = sdio_fifo;
desc2.length = 1024;
desc2.next = &desc3;

desc3.src = buf3;
desc3.dst = sdio_fifo;
desc3.length = 1024;
desc3.next = NULL;      /* 最后一个 */

dma_start_chain(&desc1); /* 启动链条,不再中断直到全部完成 */

注意处理描述符的回写状态:DMA完成后,描述符里的状态字段会被硬件更新。如果中断处理里重新使用这个描述符,一定要检查硬件回写是否完成。我曾经踩过这个坑——误以为中断时描述符已经安全,结果直接改了还在被DMA读取的描述符。

3. 数据对齐

DMA对地址对齐要求很高。很多DMA控制器要求源地址和目的地址按4字节(甚至8字节、16字节)对齐。如果你给了非对齐地址,要么DMA自动做对齐处理(性能打折),要么直接报错。

CPU轮询也不能全盘否定

坦白讲,有些极端场景下CPU轮询反而比DMA好。我之前做的一个项目,系统里只有一个任务,没有其他进程抢占。SDIO偶尔传几十字节的调试日志。用CPU轮询总共就花几微秒,觉得没必要折腾DMA。

但也要小心:裸系统里轮询的代码如果写得不好,比如循环里加了很多延迟,反而比DMA慢。

我一般如何考察候选人对这个问题的理解

面试时如果有候选人能说出以下两点,我就觉得他确实自己调过:

  • “CPU轮询的延迟不是问题,问题在于它占用了CPU的时间片,这个时间片本来可以干其他事。”
  • “DMA配置的开销是固定的,所以传输越大越划算。小数据块需要评估阈值。”

嗯,我判断的标准很清楚:能不能把“CPU利用率”和“系统吞吐量”这两个概念区分开。很多人会混淆这两个东西。

总结

DMA与CPU轮询性能差异总结图 DMA传输 vs CPU轮询:性能差异总结 CPU轮询 特点 • 每一字节都需要CPU调度 • 忙等待期间不能执行其他任务 • 单次传输开销:约100 CPU周期/4字节 适用 • 小数据传输(<64字节) • 无多任务抢占的裸系统 DMA传输 特点 • 硬件独立完成数据搬运 • CPU可执行其他任务或休眠 • 单次传输开销:约1000周期/次配置 适用 • 大批量传输(SDIO/UFS/eMMC) • 有实时性要求的多任务系统 关键性能指标对比 指标 CPU轮询 DMA CPU使用率 ≈100%(搬运期间) ≈1%(配置+中断) 传输4KB耗时 ~200μs(500MHz) ~3μs开销 多任务友好 低(阻塞其他任务) 高(可并行执行) VS

DMA传输和CPU轮询的选择,本质上是CPU时间片和传输效率的博弈。我把自己的结论概括成一句话:

能用DMA的场合,坚决不用轮询。只有在传输量极小、配置开销超过传输开销的临界点上,才保留CPU轮询作为备选。

而且,这个临界点通常在64字节以下。超过了,DMA的收益就很明显了。