一、引言:被混为一谈的两个概念
多核开发中,"一致性"一词常被笼统使用,但它实际指向两个不同层次的问题:
缓存一致性(cache coherence) 关注单个内存位置,保证对同一地址的读操作不会在写操作完成之后仍返回陈旧值,约束的是"值"。
内存一致性(memory consistency) 关注多个内存位置之间的可见顺序,规定不同 Hart 观察到的跨地址操作序是否允许不一致,约束的是"顺序"。
硬件一致性协议解决前者,对后者无能为力------前文讨论的 RVWMO 保序规则与 FENCE 指令属于后一个范畴。这条分界决定了软件栈的责任划分:值的新鲜度由硬件透明提供,操作顺序必须由软件通过屏障与原子指令显式建立。
RISC-V 在这一层面的立场值得单独指出:ISA 规范不规定任何一致性协议,手册只要求通过一致性内存进行的访问其可观测行为符合 RVWMO;至于采用监听还是目录、MESI 还是 MOESI、一致性域覆盖多少核,全部交由实现决定。其工程后果是,同一份内核镜像可以运行在不同的一致性实现之上,而性能特征与调优手段并不通用。
二、一致性协议的实现路线
2.1 MESI 族的状态划分
工业生产中的一致性协议普遍基于 MESI 族状态机,每个缓存行在每级缓存中处于以下状态之一:
| 状态 | 语义 | 是否独占 | 与内存是否一致 |
|---|---|---|---|
| M(Modified) | 已修改,系统中无其他有效副本 | 是 | 否(脏行) |
| E(Exclusive) | 未修改且独占 | 是 | 是 |
| S(Shared) | 未修改,可能存在多份副本 | 否 | 是 |
| I(Invalid) | 副本无效 | --- | --- |
| O(Owned) | 已修改但允许共享,由本缓存负责回写 | 否 | 否(脏行) |
MESI 与 MOESI 的差异集中在 O 态:MESI 下被修改的缓存行若需与他人共享,必须先回写内存再降级为 S;MOESI 允许保留 O 态直接共享,由持有者承担回写责任,省去一次写回与一次后续读取。软件无法干预协议选择,但评估平台性能特征时需将其纳入考虑。
2.2 一致性域的规模约束
监听协议要求每个缓存行请求被广播至一致性域内所有参与者,流量开销随参与者数量上升,一致性域因而无法无限扩大。工业界的通行做法是分层:簇内采用监听以维持低延迟,跨簇通过目录点收敛请求。
以玄铁 C950 为例,其多核模式依托 XT-Link 一致性互联组成最高 8 核 Cluster,核侧通过 AMBA CHI.E / CHI.F 接口直连,每核配置 256KB 至 3MB 私有 L2,可选共享 L3 最大 8MB(具体规格见玄铁 C950 产品页)。这一结构正是"簇内监听 + 簇间目录"的具体形态:L2 归属单核,L3 作为跨核共享点,CHI 承担跨一致性域的请求通道。
对软件而言,这段规格给出的可操作信息是:跨核数据交换的成本不是常量------同步代价取决于两个 Hart 的物理位置,以及数据是否驻留在共享 L3 中。把同步密集的线程绑定到同簇,是可预期的优化手段。
三、真共享与伪共享
缓存的分配与失效以 cache block 为粒度,而非以变量为粒度,由此产生两类共享的性能差异:
- 真共享(true sharing):多个 Hart 访问同一地址,失效是必要的,代价源于数据需在核间传递。
- 伪共享(false sharing):多个 Hart 访问同一 cache block 内的不同地址,失效并非必要,却由布局引发。
伪共享的典型形态是并发数据结构中的相邻字段,以下结构在高并发下会产生严重的一致性流量:
c
struct counter {
atomic_long_t hits; /* Hart 0 频繁更新 */
atomic_long_t misses; /* Hart 1 频繁更新 */
};
两个计数器位于同一 cache block 内,任一 Hart 的写入都会使对方的副本失效,导致该 cache block 在缓存之间反复迁移。规避方式是让并发更新的字段各自独占 cache block:
c
struct counter {
atomic_long_t hits;
char pad0[CACHE_LINE_SIZE - sizeof(atomic_long_t)];
atomic_long_t misses;
char pad1[CACHE_LINE_SIZE - sizeof(atomic_long_t)];
} ____cacheline_aligned;
____cacheline_aligned 由内核在 include/linux/cache.h 中提供。填充宽度应取自平台实际 cache block 大小------在支持 Zicbom 的平台上,该值可由设备树的 riscv,cbom-block-size 属性获得(见第五节),硬编码 64 字节在部分实现上并不成立。
四、非一致场景与 CMO 扩展
硬件一致性协议只覆盖参与该协议的 agent。DMA 控制器、独立加速器以及部分 I/O 设备可能位于一致性域之外,其内存视图与 CPU 不一致,必须由软件显式维护。RISC-V 为此定义了 CMO(Cache Management Operation)扩展。
4.1 指令语义
| 指令 | 所属扩展 | 操作 | 典型方向 |
|---|---|---|---|
cbo.clean |
Zicbom | 脏数据回写,保留有效副本 | CPU → 设备(设备即将读取) |
cbo.flush |
Zicbom | 回写并失效,两步原子完成 | 双向,最通用 |
cbo.inval |
Zicbom | 丢弃本地副本,不回写 | 设备 → CPU(设备已写入) |
cbo.zero |
Zicboz | 整块置零 | 页初始化 |
prefetch.i/r/w |
Zicbop | 预取提示(HINT 语义) | 遍历优化 |
数据即将离开 CPU(设备要读)用 clean;即将进入 CPU(设备已写)用 inval 或 flush;方向不确定时用 flush。
4.2 两个容易被忽略的语义点
其一,cbo.inval 可能被硬件降级。 控制位 menvcfg.CBIE[5:4] 决定其行为:00 禁用(触发非法指令异常),01 降级为 flush,11 为原生失效。降级模式以安全优先,代价是额外的回写流量。因此在依赖 cbo.inval 的性能收益之前,必须先确认 CBIE 的实际取值。
其二,CMO 忽略 cacheability 属性。 规范明确说明,cache-block management 指令不受 PMA 中 cacheable 属性的影响,也不受 PBMT 降级的影响,始终作用于对应的 cache block,因此无法通过把页面标记为非缓存来规避其作用范围。
4.3 权限控制位
三组控制位分别管制三类指令,且存在 M 模式对 S 模式的权限上限关系:
asm
/* M 模式:放开权限上限 */
csrr t0, menvcfg
li t1, (1 << 7) | (1 << 6) | (3 << 4) # CBZE | CBCFE | CBIE=11
or t0, t0, t1
csrw menvcfg, t0
/* S 模式:在内核中实际启用 */
csrr t0, senvcfg
li t1, (1 << 7) | (1 << 6) | (3 << 4)
or t0, t0, t1
csrw senvcfg, t0
CBIE 位于 [5:4],管制 cbo.inval;CBCFE 位于第 6 位,管制 cbo.clean 与 cbo.flush;CBZE 位于第 7 位,管制 cbo.zero。S 模式 senvcfg 中的对应字段取值不得超过 M 模式 menvcfg 的设定值------这是排查"senvcfg 写入后指令仍报异常"的第一检查点。
五、Linux 侧的落地
5.1 设备树声明
CMO 相关的硬件参数由固件通过设备树传递给内核,其中最基础的一项是 cache block 大小:
dts
cpus {
#address-cells = <1>;
#size-cells = <0>;
riscv,cbom-block-size = <64>; /* 与 cbo.clean/flush/inval 相关 */
cpu@0 {
device_type = "cpu";
compatible = "riscv";
/* ... */
};
};
内核据此设定 ARCH_DMA_MINALIGN 并校验其不小于硬件实际 block 大小。非一致设备则在各自节点中标记 dma-noncoherent------内核默认假定所有设备是一致的,只有显式标记的设备才走同步路径。这一默认值的含义是:漏标会产生难以定位的数据错误,而不会产生编译期或启动期告警。
5.2 内核实现路径
Zicbom 支持由 CONFIG_RISCV_ISA_ZICBOM 开启,实现在 arch/riscv/mm/dma-noncoherent.c,核心是向 DMA 框架注册一组区间操作:
c
struct riscv_cache_ops {
void (*clean_range)(unsigned long addr, unsigned long size);
void (*inv_range)(unsigned long addr, unsigned long size);
void (*flush_range)(unsigned long addr, unsigned long size);
};
内核在 arch_sync_dma_for_device 与 arch_sync_dma_for_cpu 两个钩子上按传输方向选择操作。其中一处取舍值得注意:arch_sync_dma_for_device 在 DMA_FROM_DEVICE 方向采用 clean 而非 inval,以避免 CPU 侧意外持有脏副本时丢失数据。平台若实现了非标准的一致性维护指令序列,可通过 riscv_noncoherent_register_cache_ops() 注册自有实现覆盖默认路径。上层驱动无需感知平台差异,但平台是否真正提供了可用的 CMO 路径,是板卡 bring-up 阶段需要逐项确认的事项。
六、实战:QEMU 上的验证边界
QEMU 对 CMO 扩展提供了基本建模,但验证范围需要明确界定。启动参数可显式配置扩展与块大小:
bash
qemu-system-riscv64 \
-machine virt -smp 4 -nographic \
-cpu rv64,zicbom=true,zicboz=true,cbom_blocksize=64,cboz_blocksize=64 \
-kernel Image -append "root=/dev/vda rw console=ttyS0"
cbo.zero 是其中少数具有实际语义的 CMO 指令,其效果可被验证:
c
/* 将一页内存整块置零,按 cache block 大小推进 */
static void page_zero_cbo(void *page, size_t block_size)
{
unsigned long addr = (unsigned long)page;
unsigned long end = addr + PAGE_SIZE;
for (; addr < end; addr += block_size)
asm volatile ("cbo.zero (%0)" :: "r"(addr) : "memory");
}
与之相对,cbo.clean、cbo.flush、cbo.inval 只做权限检查与异常判定,不产生任何缓存层次的实际动作------QEMU 并未对内存层次建模。验证边界可概括为:
| 验证对象 | QEMU 是否可验 | 说明 |
|---|---|---|
| 扩展可见性与 CSR 权限位 | 可 | 非法指令异常、字段上限关系均可复现 |
cbo.zero 的清零效果 |
可 | 有明确的数据语义 |
| CMO 指令不触发异常 | 可 | 权限配置错误会在此暴露 |
| 缓存层次的失效与回写行为 | 不可 | 内存层次未建模 |
| 一致性协议时序与竞争 | 不可 | 无协议状态机 |
| 伪共享的性能影响 | 不可 | 无缓存模型,无一致性流量 |
最后三行是本文主题的核心:一致性与缓存行为相关的缺陷无法在 QEMU 上观测。缺失一致性维护的代码在 QEMU 中通常表现完美,真机验证在此不可替代。
七、从硬件一致性到软件同步原语
硬件一致性协议解决"单个地址的值是否新鲜",并不提供原子性与跨地址顺序,Linux 的同步原语正建立在这两项缺口之上:
| 层次 | 典型机制 | 依赖的硬件能力 |
|---|---|---|
| 原子操作 | atomic_*、cmpxchg |
A 扩展的 LR/SC 与 AMO |
| 单向屏障 | smp_store_release / smp_load_acquire |
带 .rl / .aq 注释的 AMO |
| 双向屏障 | smp_mb() |
fence rw, rw |
| 锁原语 | spinlock / mutex / rwlock |
原子操作 + 屏障 |
| 无锁结构 | seqlock、RCU |
上述全部 + 数据结构设计 |
这条链条揭示了一个容易被忽略的因果:一致性协议让锁能够工作,而锁的性能问题几乎都源于一致性流量的代价。自旋锁竞争表现为 cache block 在核间反复迁移,伪共享是同一现象在无锁代码中的表现。因此优化同步开销的着力点通常不在屏障指令本身,而在数据布局与访问模式:把频繁同步的数据集中到同一 cache block,把独立更新的数据分散到不同 cache block,把同步密集的线程约束在同一一致性域内。
八、常见坑位与总结
坑位一:把缓存一致性与内存顺序混为一谈。 一致性保证值的可见性,不保证跨地址操作的顺序;缺少屏障的代码在一致性正确的硬件上仍可能出错。
坑位二:假定 DMA 缓冲区必然一致。 非一致设备必须在设备树中标记 dma-noncoherent,默认值是"一致",漏标不产生任何告警。
坑位三:依赖 cbo.inval 的性能收益而未检查 CBIE。 降级模式下该指令等价于 flush,回写流量照旧产生。
坑位四:把伪共享误判为锁竞争。 若吞吐下降在数据布局调整后消失,原因通常是伪共享,此时增加锁粒度或改用无锁结构均无效果。
坑位五:假定一致性域覆盖全系统,或用 QEMU 验证缓存与一致性行为。 前者的遗漏设备必须由软件维护,后者的内存层次未建模,跑通不构成证据。
三个关键结论:
其一,RISC-V 规范只约束可观测行为,不约束实现路径。 "符合 RVWMO"是语义要求,MESI 或 MOESI、监听或目录都属于实现自由;正确性可基于规范推导,性能判断必须回到具体平台。
其二,一致性协议覆盖范围之外的部分由软件接管。 CMO 扩展与设备树中的非一致标记共同构成这条边界的管理手段。
其三,同步性能的瓶颈大多在数据布局而非指令选择。 原子指令与屏障的开销相对固定,跨核失效的次数才是可变量。
延伸方向有两个:其一是用 HPM 计数器量化跨核流量与缓存未命中,把上述判断从经验转为数据;其二是内存属性配置机制(Svpbmt)与 CMO 的配合,两者分别控制页面缓存属性与显式维护动作,在异构集成场景中需协同使用。