1. 从一颗芯片的“沟通问题”说起
如果你最近在折腾 RISC-V 开发板,尤其是那种四核、八核的板子,大概率会遇到一个很隐蔽的坑:两个核心同时读写同一块内存,程序跑着跑着结果就不对了。单核时代那种“代码怎么写就怎么跑”的直觉,在多核上完全失效。这背后的核心问题,就是 缓存一致性 。
RISC-V 作为一个开放指令集架构,它的多核缓存一致性机制和 ARM、x86 有相似之处,但细节上又有很多自己的设计取舍。MESI 协议族是绕不开的基础,CMO(Cache Management Operations)扩展是 RISC-V 特有的“手动挡”,而 Linux 内核里的同步原语则是把这些硬件机制用起来的“上层建筑”。这三者串起来,才构成一个完整的多核内存模型。
这篇文章适合谁看?如果你正在做 RISC-V 平台的嵌入式开发、内核移植、驱动编写,或者单纯想搞明白“为什么多核程序需要内存屏障”这类问题,那接下来的内容应该能帮你省下不少翻手册的时间。我会从硬件协议讲到内核实现,再落到实际调试中踩过的坑,尽量把这条链路讲透。
2. 缓存一致性的底层逻辑与 MESI 协议族拆解
2.1 为什么多核必须解决缓存一致性问题
先打个比方。假设一个办公室里有四个同事,每个人手头都有一份公司通讯录的复印件。某天行政更新了某个人的电话号码,只改了自己手里那份,其他三个人的复印件还是旧的。这时候如果有人按旧号码打电话,就会找不到人。多核 CPU 的缓存就是这些“复印件”,主存是“行政的原件”。
每个核心都有自己的 L1 缓存,甚至 L2 也是私有的,只有 L3 和主存是共享的。当一个核心修改了某块数据,其他核心的缓存里可能还留着旧值。如果不加干预,程序读到的就是脏数据。缓存一致性协议要做的,就是保证所有核心看到的内存视图是一致的。
这里有个关键概念叫 缓存行 (Cache Line),通常是 64 字节。CPU 不是按字节去同步的,而是按缓存行来管理状态。你改了一个字节,整行缓存的状态都会变化。理解这一点很重要,后面讲伪共享(False Sharing)的时候会用到。
2.2 MESI 四种状态到底在描述什么
MESI 是四个状态的首字母缩写,分别是 Modified、Exclusive、Shared、Invalid。每个缓存行在任何时刻都处于这四种状态之一。
- Modified(M) :这行数据被当前核心修改过,和主存不一致,且只有当前核心有这份数据。当前核心是这个缓存行的唯一拥有者,其他核心如果要用,必须先从它这里拿到最新值。
- Exclusive(E) :这行数据和主存一致,且只有当前核心缓存了它。这时候如果当前核心要写,可以直接改成 M 状态,不需要通知其他核心,因为没有别人有这份数据。
- Shared(S) :这行数据和主存一致,可能有多个核心都缓存了它。任何核心要写这行数据,都必须先让其他核心的副本失效。
- Invalid(I) :这行数据无效,等同于没有缓存。读到 I 状态的行会触发缓存未命中,需要从其他核心或主存重新加载。
状态之间的迁移由总线上的嗅探(Snooping)机制驱动。每个核心都在监听总线上的事务,当发现自己缓存的行被别人动了,就相应地更新自己的状态。这套机制在总线型架构上工作得很好,但核心一多,总线就成了瓶颈。
2.3 MESI 的变体:MOESI 和 MESIF 解决了什么问题
MESI 有个明显的短板:当一个核心要把 M 状态的数据换出时,它必须先写回主存,再让其他核心去读。这一来一回延迟很高。MOESI 加了一个 Owned(O) 状态,表示这行数据被修改过、和主存不一致,但允许其他核心共享这份“脏”数据。拥有 O 状态的核心负责在必要时写回主存,其他核心可以直接从它那里读,省去了写回再读的往返。
MESIF 则是 Intel 的变体,加了一个 Forward(F) 状态。当多个核心共享同一行数据时,只有一个核心处于 F 状态,其他处于 S 状态。当有新的核心请求这行数据时,由 F 状态的核心负责响应,避免了多个核心同时抢答导致的冲突。这个设计在环形总线上特别有用。
RISC-V 并没有强制规定必须用哪种协议。规范只定义了内存模型和必要的同步指令,具体的一致性协议由实现者决定。不过在实际的 RISC-V 多核芯片中,MOESI 和 MESIF 都有采用,也有用目录式(Directory-based)协议的。目录式协议通过一个集中的目录来记录每个缓存行的共享情况
246



被折叠的 条评论
为什么被折叠?



