3、一致性问题的根源:多核共享内存、DMA访问、非一致性内存访问(NUMA)
好,咱们进入正题。
缓存一致性,说白了就是个“谁看到了什么”的问题。你想想看,一个数据在内存里待得好好的,结果CPU核A把它读到缓存里改了,核B不知道,还拿着老数据做计算——这不就乱套了吗?
我个人习惯把一致性问题的根源归结为三类场景。搞懂了这三类,你就抓住了缓存一致性的命门。
3.1 多核共享内存:最经典的“打架”现场
多核CPU共享同一块物理内存,这是最常见的场景。每个核都有自己的L1、L2缓存,大家各自为政。
我在项目中遇到过这样一个坑:两个核同时对一个全局变量做自增操作。代码写的是 counter++,看起来人畜无害对吧?
// 核A执行
LOAD R1, [counter] // 从内存读到R1
ADD R1, R1, #1 // 加1
STORE [counter], R1 // 写回内存
// 核B执行
LOAD R1, [counter] // 从内存读到R1
ADD R1, R1, #1 // 加1
STORE [counter], R1 // 写回内存
结果呢?两个核同时读到counter=0,各自加1,最后写回的是1。本该是2,却丢了1次更新。
为什么会这样?因为每个核的缓存里都存了一份counter的副本。核A改了,核B不知道。这就是一致性问题最原始的形态——写后读不一致。
核心矛盾:每个核都想用自己的缓存提速,但共享数据不允许有多个“版本”同时存在。
嗯,这里要注意:不是所有共享数据都会出问题。只有那些被多个核同时读写的数据才需要一致性协议来保护。只读数据?随便缓存,没人管你。
3.2 DMA访问:外设这个“不速之客”
DMA(直接内存访问)是另一个让人头疼的根源。外设通过DMA控制器直接读写内存,完全不经过CPU缓存。
我曾经在一个网络加速器项目里吃过这个亏。网卡通过DMA把数据包写到了内存地址0x1000,CPU核C的缓存里恰好也缓存了这个地址的数据。结果核C读到的还是老数据,根本不知道新数据包已经到了。
// 场景:网卡DMA写入数据包
// 内存地址0x1000 被DMA更新
// 但核C的缓存行里还存着旧版本
// 核C读到的数据:
LOAD R1, [0x1000] // 从缓存读!不是从内存读!
// R1 = 旧数据,新数据包丢了
说白了,DMA这个“不速之客”绕过了缓存一致性协议。CPU核之间可以通过MESI之类的协议互相通知,但DMA不跟你玩这套。它直接写内存,缓存里的数据就成了“脏数据”。
避坑指南:我曾经在调试一个视频处理系统时,发现图像花屏。查了两天才定位到——DMA写完了帧缓冲,但GPU的缓存没刷新。解决方案是在DMA完成后,手动执行一次缓存刷新指令(比如ARM的DC CVAC)。
解决DMA一致性问题,通常有两种思路:
- 软件方案:在DMA操作前后,手动刷新或失效缓存行。简单粗暴,但容易漏掉。
- 硬件方案:使用支持缓存一致性的互连总线(如ACE、CHI),让DMA也参与一致性协议。代价是硬件复杂度上升。
3.3 非一致性内存访问(NUMA):距离产生“偏见”
NUMA,这个名字听起来高大上,其实核心就一句话:不同CPU访问不同内存块的速度不一样。
你想想看,一个多路服务器系统,CPU0和CPU1各自有本地内存。CPU0访问自己的本地内存,延迟可能只有80ns;但访问CPU1的远端内存,延迟可能飙到200ns以上。这就是“非一致性”的由来。
| 访问类型 | 延迟(典型值) | 带宽 |
|---|---|---|
| CPU0 → 本地内存 | ~80 ns | 高 |
| CPU0 → CPU1的远端内存 | ~200 ns | 中 |
| CPU0 → 远端内存(跨Socket) | ~300 ns | 低 |
NUMA对缓存一致性的影响在哪?我举个例子你就明白了。
假设CPU0和CPU1共享一个数据结构,数据物理上放在CPU1的本地内存里。CPU0要读这个数据,得先发一个远程请求到CPU1的内存控制器。如果CPU1的缓存里恰好有这份数据的修改版本,那还得先等CPU1把数据写回内存,或者直接转发给CPU0。
这个过程涉及跨Socket的通信,延迟高得吓人。我在一个数据库服务器项目里遇到过,因为NUMA感知没做好,性能直接掉了30%。
我的建议:在NUMA系统上写高性能程序,一定要做“亲和性绑定”。让线程运行在离它所用内存最近的CPU上。Linux下用 numactl 或者 sched_setaffinity() 都可以实现。
3.4 一张图看懂一致性问题的根源
下面这张SVG图,我把三类根源画在了一起。你看完应该能有个全局印象。
你看这张图,左边是多核共享内存——多个CPU核各自缓存同一份数据;中间是DMA——外设直接写内存,不通知缓存;右边是NUMA——不同CPU访问内存的速度不一样,导致数据同步的延迟差异巨大。
这三类问题,本质上都是“多个数据视图”的问题。缓存一致性协议要做的,就是让所有参与者看到同一个数据的最新版本。
一句话总结:一致性问题的根源,在于系统中存在多个独立的数据通路(缓存、内存、DMA),它们之间缺乏统一的“版本管理”机制。
嗯,讲到这里,你应该对“为什么需要缓存一致性”有了直观的理解。下一节我们会深入具体的协议实现,看看硬件是怎么解决这些问题的。
公众号:蓝海资料掘金营,微信deep3321