Cache 的一致性问题

前面四节讲的全是”读”。一旦允许写,Cache 里就出现了主存内容的副本,而副本一被修改,两份数据就不一致了。

一致性问题有三个来源,教材按这个顺序展开:

  1. CPU 自己写 Cache → Cache 与主存不一致 → 用写策略解决
  2. 多个处理器各有自己的 Cache → 各 Cache 之间不一致 → 用一致性协议解决
  3. I/O 设备(DMA)绕过 Cache 直接读写主存 → 主存与 Cache 不一致 → 用不可缓存区或强制刷新解决

写策略是必考的,后两个只考概念。

机制

写命中:写直达与写回

写直达法(全写法,Write-through):同时写 Cache 和主存。

  • 任何时刻主存都是最新的,一致性天然保持
  • 不需要脏位
  • 每次写都要访问主存,写开销大
  • 缓解办法:加写缓冲(Write Buffer)——CPU 把数据写进缓冲区就继续执行,由缓冲区异步写入主存。写缓冲满时仍会阻塞。

写回法(Write-back):只写 Cache,并置脏位;直到该行被替换时才写回主存。

  • 大幅减少访存次数,对写密集的程序优势明显
  • 需要脏位(见 3.5.2)
  • 替换时若脏位为 1,必须先写回再装入新块——这使缺失代价变成两次主存访问
  • 主存中的数据可能是过时的,这正是多核和 DMA 一致性问题的根源

写不命中:写分配与非写分配

写分配(Write-allocate):先把该块调入 Cache,再在 Cache 中写。

  • 押注”写过的地方马上还会再访问”(时间局部性)
  • 一次写不命中要付一次块调入的代价

非写分配(No-write-allocate):直接写主存,不调入 Cache。

  • 不占用 Cache 空间,适合”写一次就不再用”的数据

四种组合,两种常见搭配

写命中写不命中是否常见理由
写回法写分配常见两者都押注局部性;既然要反复写,就该把块留在 Cache 里
写直达法非写分配常见两者都不押注局部性;反正每次都要写主存,调入块没有收益
写回法非写分配少见自相矛盾
写直达法写分配少见调入了块,写时却还要同时写主存,收益有限

“写回配写分配、写直达配非写分配”这两条搭配是考点,理由是两者押注的是不是同一件事。

flowchart TD
    W["CPU 执行写操作"] --> H{"写命中?"}
    H -->|"命中"| WP{"写策略"}
    WP -->|"写直达"| WT["同时写 Cache 和主存<br/>(可经写缓冲)"]
    WP -->|"写回"| WB["只写 Cache,置脏位<br/>替换时若脏则写回"]
    H -->|"不命中"| MP{"写不命中策略"}
    MP -->|"写分配"| WA["先调入块,再按写命中处理"]
    MP -->|"非写分配"| NWA["直接写主存,不调入"]

    classDef a fill:#d5f5e3,stroke:#27ae60
    classDef b fill:#fdebd0,stroke:#e67e22
    class WB,WA a
    class WT,NWA b

多处理器的 Cache 一致性

多个核各有私有 Cache 时,同一个主存块可能在多个 Cache 里都有副本。一个核改了自己的副本,其他核的副本就过期了。

两类解决思路:

监听(嗅探)协议 Snooping:所有 Cache 控制器监听总线上的事务,发现别人写了自己也缓存着的块时作出反应。

  • 写直达式(写更新):把新值广播出去,其他副本一起更新
  • 写回式(写无效):把其他副本置为无效,下次访问时重新取。这是主流,MESI 协议属于此类
  • 依赖广播,适合总线互连的小规模系统

目录协议 Directory:用一个集中的目录记录每个主存块被哪些 Cache 缓存着,写时只通知相关的那几个。

  • 不需要广播,适合大规模多处理器
  • 目录本身有存储和查询开销

I/O 引起的一致性

DMA 直接在主存与设备之间搬数据,不经过 Cache(见 第 7 章),于是:

  • DMA 写主存:Cache 里的副本过期
  • DMA 读主存:若 Cache 用写回法且该块是脏的,主存里的数据是旧的,DMA 读到错误数据

三种对策:把 DMA 缓冲区设为不可缓存(non-cacheable);让 DMA 也经过 Cache;由软件在 DMA 前后显式刷新/无效化 Cache。

这正是操作系统的驱动程序要做的事之一,见 OS 5.2.5 设备驱动程序接口。

边界

“Cache 一致性”和”存储一致性模型”不是一回事。 一致性(Coherence)保证的是对同一个地址,所有核看到的写顺序一致;存储一致性模型(Consistency)规定的是不同地址之间的读写能否被重排。408 只考前者。

写直达法不需要脏位,写回法必须有。 一道题问”采用写回法的 Cache 每行需要哪些控制位”,脏位不能漏。

写回法下主存不一定是最新的。 这句话本身是写回法的定义,不是缺点描述;它的缺点是替换时可能要多一次写回,且多核/DMA 一致性更复杂。

写缓冲不改变写直达的语义。 它只是把”写主存”变成异步的,一致性仍然由写直达保证;写缓冲满时 CPU 照样阻塞。

写分配 / 非写分配只在”写不命中”时才有区别。 写命中时两者行为完全相同,由写命中策略决定。

Cache 一致性问题在单核单进程、无 DMA 的模型里不存在。 教材先讲写策略再讲多核,顺序不能颠倒:写策略解决的是 Cache 与主存的不一致,多核协议解决的是 Cache 之间的不一致。

对照速查

写直达(全写法)写回法
写命中时同时写 Cache 和主存只写 Cache,置脏位
脏位不需要需要
主存是否最新总是最新可能过时
访存次数多少
替换开销直接覆盖脏行需先写回
常搭配非写分配写分配
一致性维护简单复杂
一致性来源解决手段
CPU 写 Cache写策略(写直达 / 写回)
多核私有 Cache监听协议 / 目录协议
DMA 绕过 Cache不可缓存区 / 软件刷新

考点

  • 写命中:写直达 vs 写回;写不命中:写分配 vs 非写分配,两组正交
  • 常见搭配是”写回 + 写分配”和”写直达 + 非写分配”,因为两者押注的是同一件事
  • 只有写回法需要脏位
  • 写缓冲缓解写直达的开销,但不改变其语义
  • 写回法下替换脏行要多一次主存写
  • 监听协议靠广播(写无效为主流),目录协议靠集中记录
  • DMA 绕过 Cache 会造成不一致,靠不可缓存区或软件刷新解决
  • Cache 一致性 ≠ 存储一致性模型,408 只考前者

链接