1. 缓存一致性基础

在嵌入式 Linux 系统与多核 SoC 中,CPU 缓存(Cache)是提升内存访问性能的核心硬件。然而,缓存带来的数据副本问题,使得驱动开发者必须深入理解缓存一致性协议,否则将导致数据错乱、系统崩溃等难以调试的 Bug。本节从 CPU 缓存架构出发,逐步拆解 Cache Line、MESI 协议,并阐明驱动为何必须关注缓存一致性。

1.1 CPU 缓存架构概述

现代 CPU 采用多级缓存(L1/L2/L3)来弥合处理器与主存(DRAM)之间的速度鸿沟。L1 缓存通常分为指令缓存(L1i)和数据缓存(L1d),与核心紧耦合;L2 缓存为核心私有或共享;L3 缓存为所有核心共享。缓存以 Cache Line 为基本单位与主存交换数据,典型大小为 32 字节、64 字节或 128 字节。

关键概念: 缓存命中(Cache Hit)与缓存缺失(Cache Miss)直接决定内存访问延迟。L1 命中延迟约 1-4 个 CPU 周期,而主存访问延迟可达 100-300 个周期。

下图展示了一个典型四核 SoC 的缓存层次结构,包含私有 L1/L2 与共享 L3 缓存:

多核 SoC 缓存层次架构 Core 0 L1i / L1d L2 Cache (私有) Core 1 L1i / L1d L2 Cache (私有) Core 2 L1i / L1d L2 Cache (私有) Core 3 L1i / L1d L2 Cache (私有) 片上互联总线 (CCI / Ring Bus) L3 Cache (共享,所有核心可见) 内存控制器 (DDR Controller) 主存 (DRAM)
驱动视角: 在 DMA 传输或设备共享内存时,数据可能被 CPU 缓存在 L1/L2 中,而设备直接访问主存。此时若未执行缓存刷新(Cache Flush)或失效(Invalidate),CPU 与设备将看到不一致的数据副本。

1.2 Cache Line 与 MESI 协议简介

Cache Line 是缓存与主存之间数据传输的最小单元。每个 Cache Line 除存储数据外,还包含标签(Tag)、有效位(Valid Bit)和一致性状态位。多核系统中,同一内存地址的数据可能同时存在于多个核心的私有缓存中,必须通过缓存一致性协议来保证所有副本的一致。

MESI 协议 是最经典的缓存一致性协议,为每个 Cache Line 定义四种状态:

状态 全称 含义 该副本是否唯一 数据是否与主存一致
M Modified 该 Cache Line 已被修改,数据仅存在于当前核心的缓存中 是 否(脏数据,需写回主存)
E Exclusive 该 Cache Line 未被修改,且仅存在于当前核心的缓存中 是 是
S Shared 该 Cache Line 未被修改,且可能存在于多个核心的缓存中 否 是
I Invalid 该 Cache Line 无效,不包含有效数据 — —

MESI 协议通过总线嗅探(Bus Snooping)或目录协议(Directory-based)来监听其他核心的读写请求,并自动切换 Cache Line 状态。例如,当核心 A 要写入一个处于 S 状态的 Cache Line 时,协议会向其他核心发送“失效”消息,将它们的副本置为 I 状态,然后将本地状态升级为 M。

下图展示了 MESI 协议的状态转换流程:

MESI 协议状态转换图 M E S I 其他核心读请求 (写回主存) 本地写 本地写 其他核心读 其他核心写 其他核心写 唯一读 (其他核心失效) 本地读 (未命中) 本地读 (其他核心有副本)
常见误区: MESI 协议仅保证 CPU 核心之间的缓存一致性,不保证 CPU 缓存与外部设备(如 DMA 控制器、GPU、网卡)之间的数据一致性。设备直接访问主存时,CPU 缓存中的脏数据可能尚未写回,导致设备读到旧数据。

1.3 为什么驱动需要关心缓存一致性

驱动运行在操作系统内核空间,直接管理硬件设备。当设备通过 DMA 读写主存,而 CPU 同时对该内存区域进行缓存访问时,就会出现缓存一致性问题。典型场景包括:

  • DMA 接收数据: 设备将数据写入主存缓冲区,CPU 读取该缓冲区时,若 L1/L2 缓存中仍保留旧副本,则 CPU 读到的是过期数据。
  • DMA 发送数据: CPU 构造数据到缓冲区,然后通知设备 DMA 读取。若 CPU 写入的数据仍停留在缓存中(脏数据),设备将读到不完整或错误的数据。
  • 共享内存通信: 多核之间或 CPU 与协处理器(如 DSP、GPU)共享内存时,缺乏一致性协议会导致数据错乱。

下表总结了驱动中常见的缓存一致性风险场景及后果:

场景 操作方向 风险描述 典型后果
DMA 接收 设备 → 主存 → CPU CPU 缓存中保留旧数据,未失效 数据丢包、校验错误、协议栈崩溃
DMA 发送 CPU → 主存 → 设备 CPU 写入的数据仍在缓存中,未写回主存 设备发送错误数据、硬件超时
多核共享变量 Core A 写 / Core B 读 Core B 读到过期的缓存副本 竞态条件、死锁、数据损坏
MMIO 与缓存混用 CPU 读写设备寄存器 寄存器映射内存被缓存,导致写操作延迟 设备状态错误、中断丢失

Linux 内核提供了一系列 API 来显式控制缓存一致性,驱动开发者必须掌握这些接口的使用时机:

/* 缓存刷新(写回):将脏数据从缓存写回主存 */
void dma_sync_single_for_device(struct device *dev, dma_addr_t dma_handle,
                                size_t size, enum dma_data_direction dir);

/* 缓存失效:使缓存中的副本失效,下次读取从主存加载 */
void dma_sync_single_for_cpu(struct device *dev, dma_addr_t dma_handle,
                             size_t size, enum dma_data_direction dir);

/* 分配一致性内存(非缓存或硬件一致性) */
dma_addr_t dma_alloc_coherent(struct device *dev, size_t size,
                              dma_addr_t *dma_handle, gfp_t flag);
核心原则: 在设备访问内存之前,驱动必须执行缓存刷新(Flush);在 CPU 读取设备写入的数据之前,驱动必须执行缓存失效(Invalidate)。使用 dma_alloc_coherent 分配的内存默认是非缓存的,或由硬件保证一致性,可避免手动同步。

下图展示了驱动在 DMA 发送流程中必须执行的缓存同步操作:

DMA 发送流程中的缓存同步 1. CPU 写入数据到缓冲区 数据可能仅存在于 L1/L2 缓存 2. 驱动调用缓存刷新 dma_sync_single_for_device() 3. 数据已写回主存 设备可安全读取 4. 通知设备启动 DMA 写寄存器或 doorbell ⚠ 若跳过步骤 2,设备可能读到缓存中的旧数据或部分数据 导致 CRC 错误、协议异常或系统挂死
调试建议: 当遇到 DMA 数据异常时,首先检查驱动是否在正确的时机调用了缓存同步 API。使用 dmam_alloc_coherent 或 dma_alloc_coherent 分配缓冲区可以彻底避免手动同步,但需注意一致性内存的分配开销较大,不适合高频小数据场景。