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轮询的典型流程:
- 读状态寄存器——30个CPU周期
- 判断读完成位——2个周期
- 如果完成,从FIFO读数据——50个周期
- 写数据到内存——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轮询的选择,本质上是CPU时间片和传输效率的博弈。我把自己的结论概括成一句话:
能用DMA的场合,坚决不用轮询。只有在传输量极小、配置开销超过传输开销的临界点上,才保留CPU轮询作为备选。
而且,这个临界点通常在64字节以下。超过了,DMA的收益就很明显了。