2. 嵌入式系统基础回顾
大家好,我是你们的嵌入式系统讲师。今天我们来聊聊 Cache——这个在性能调优里绕不开的话题。很多同学觉得 Cache 就是“快一点的内存”,其实没那么简单。我刚开始做嵌入式开发时,也踩过 Cache 的坑,有一次 DMA 传完数据,CPU 读到的全是旧值,排查了整整两天……嗯,从那以后我彻底搞懂了 Cache 一致性。
Cache 工作原理:为什么它能提速?
说白了,Cache 就是 CPU 和主存之间的“小抄本”。CPU 要读数据时,先翻翻小抄本,有就直接用(Cache Hit),没有才去主存拿(Cache Miss)。
为什么能提速?因为局部性原理:
- 时间局部性:刚用过的数据,很快还会再用(比如循环变量)
- 空间局部性:刚用过的数据附近的数据,很快也会被用到(比如数组遍历)
我习惯把 Cache 比作“工作台”——你把常用的工具放台上,不用每次都去仓库翻。但工作台空间有限,放不下所有东西,这就引出了替换策略和写策略的问题。
写通(Write-Through)与写回(Write-Back)策略
这是 Cache 设计里最核心的决策之一。两种策略各有千秋,我分别说说。
| 策略 | 写入方式 | 优点 | 缺点 | 典型场景 |
|---|---|---|---|---|
| 写通(Write-Through) | 同时写入 Cache 和主存 | 一致性好,实现简单 | 写操作慢,带宽浪费 | 对一致性要求高的系统 |
| 写回(Write-Back) | 只写 Cache,标记脏位,替换时写回主存 | 写操作快,带宽节省 | 一致性复杂,需要脏位管理 | 高性能嵌入式系统 |
关键点:写通策略下,每次写操作都要等主存完成,性能损失明显。写回策略虽然快,但脏数据留在 Cache 里,一旦 DMA 直接访问主存,就会读到旧值——这就是一致性问题的根源。
我在一个视频处理项目里用过写回策略。当时图像数据量很大,写通策略导致帧率掉了一半。换成写回后,性能上来了,但 DMA 和 CPU 共享数据时,必须手动做 Cache 刷新操作。你想想看,如果不做,画面就会出现“撕裂”——一半是新数据,一半是旧数据。
Cache 一致性问题根源:谁动了我的数据?
为什么会有一致性问题?根本原因是:多个“观察者”看到的数据不同步。
在嵌入式系统里,常见的“观察者”有:
- CPU:通过 Cache 读写数据
- DMA:直接访问主存,不经过 Cache
- 其他总线主控:如 GPU、网络控制器
举个例子:CPU 把数据写到了 Cache 里(写回策略),但主存里还是旧值。这时 DMA 启动,从主存读取数据——它读到的就是旧值。反过来,DMA 往主存写入了新数据,但 CPU 的 Cache 里还存着旧值,CPU 再读时,拿到的还是旧数据。
避坑指南:我曾经在一个音频处理项目里,DMA 采集完数据后,CPU 处理时总出现杂音。排查到最后,发现是 Cache 没刷新,CPU 一直在处理“过期”的音频样本。从那以后,我养成了习惯:DMA 传输完成后,第一时间做 Cache 无效化操作。
Cache 一致性问题场景分类
我把常见场景整理成了一张表,方便你对照排查:
| 场景 | 数据流向 | 问题描述 | 解决方案 |
|---|---|---|---|
| CPU 写,DMA 读 | CPU → Cache → (脏) → DMA 读主存 | DMA 读到旧数据 | 写回前做 Cache Clean |
| DMA 写,CPU 读 | DMA → 主存 → CPU 读 Cache | CPU 读到旧数据 | 读前做 Cache Invalidate |
| CPU 写,CPU 读(多核) | Core0 Cache → Core1 Cache | 核间数据不一致 | 硬件一致性协议(如 MESI) |
小技巧:我习惯在 DMA 描述符里加一个“完成标志位”。DMA 传输完成后,先写标志位,CPU 轮询到标志位后,先做 Cache Invalidate,再读取数据。这样能保证数据一致性,而且代码逻辑清晰。
Cache 一致性问题的本质
说白了,就是数据在 Cache 和主存之间“不同步”。写回策略下,Cache 是“主人”,主存是“备份”;写通策略下,主存是“主人”,Cache 是“副本”。但无论哪种策略,只要存在多个数据通路,就可能有“谁的数据才是最新的”这个问题。
我见过很多工程师在调试时,遇到奇怪的数据错误,第一反应是怀疑算法逻辑。其实,很多时候就是 Cache 一致性问题在作祟。你想想看,如果数据在 Cache 里是 0xAA,在主存里是 0x55,CPU 和 DMA 各读各的,结果能一样吗?
SVG:Cache 一致性问题根源示意图
如何应对 Cache 一致性问题?
在实际项目中,我总结了几条经验:
- 明确数据归属:哪些数据由 CPU 独占,哪些由 DMA 独占,哪些是共享的。共享数据必须做一致性处理。
- 使用 Cache 操作指令:ARM 的
DCache_Clean、DCache_Invalidate,RISC-V 的fence.i等,要熟练使用。 - DMA 缓冲区对齐:确保 DMA 缓冲区地址和大小都是 Cache Line 的整数倍(通常是 32 或 64 字节),否则操作会出问题。
- 避免频繁切换:如果数据在 CPU 和 DMA 之间频繁交换,考虑使用非 Cache 的内存区域(如
mmap时设置MAP_UNCACHED)。
个人习惯:我在设计 DMA 驱动时,会专门分配一块“一致性内存”(Coherent Memory),这块内存不会被 Cache 缓存。这样 CPU 和 DMA 访问的都是同一份数据,彻底避免一致性问题。代价是访问速度稍慢,但换来的是代码简单可靠。
小结
Cache 一致性问题是嵌入式性能调优里的“隐形杀手”。你写代码时感觉一切正常,跑起来就是各种诡异 bug。理解了 Cache 的工作原理、写通与写回策略的区别,以及一致性问题的根源,你就能在设计和调试时有的放矢。