CPU如何通过LOCK前缀实现缓存锁定并保证内存一致性?
背景
在Java中,为变量添加volatile关键字可保证内存一致性(或可见性)。
在x86平台上,Hotspot虚拟机通过添加LOCK前缀指令来实现volatile变量的内存一致性,例如:lock addl $0x0,(%esp);
根据《Intel® 64和IA-32架构软件开发手册》:
对于Intel486和Pentium处理器,LOCK操作期间总会在总线上断言LOCK#信号,即使被锁定的内存区域已被处理器缓存。
对于P6及后续处理器系列,如果LOCK操作期间被锁定的内存区域在执行该操作的处理器中以回写内存方式缓存且完全包含在一个缓存行中,处理器可能不会在总线上断言LOCK#信号。而是会在内部修改内存位置,并通过缓存一致性机制确保操作原子性,这种操作称为“缓存锁定”。缓存一致性机制会自动防止两个或多个缓存了同一内存区域的处理器同时修改该区域的数据。
我认为“LOCK操作期间被锁定的内存区域已被处理器缓存”指缓存行状态为S、E或M(可能是E或M?)。
查阅很多资料后得知缓存锁定通过MESI协议实现,但阅读MESI协议后仍有两个核心疑问:
- MESI能否实现缓存锁定?
- LOCK指令如何实现内存一致性?
Hotspot中Volatile写的相关代码
// volatile write if (cache->is_volatile()) { // if (tos_type == itos) { obj->release_int_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == atos) { VERIFY_OOP(STACK_OBJECT(-1)); obj->release_obj_field_put(field_offset, STACK_OBJECT(-1)); } else if (tos_type == btos) { obj->release_byte_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == ztos) { int bool_field = STACK_INT(-1); // only store LSB obj->release_byte_field_put(field_offset, (bool_field & 1)); } else if (tos_type == ltos) { obj->release_long_field_put(field_offset, STACK_LONG(-1)); } else if (tos_type == ctos) { obj->release_char_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == stos) { obj->release_short_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == ftos) { obj->release_float_field_put(field_offset, STACK_FLOAT(-1)); } else { obj->release_double_field_put(field_offset, STACK_DOUBLE(-1)); } // after volatile write,insert storeload memory barrier OrderAccess::storeload(); } ================================ inline void OrderAccess::storeload() { fence(); } ================================ inline void OrderAccess::fence() { if (os::is_MP()) { // always use locked addl since mfence is sometimes expensive #ifdef AMD64 __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory"); #else __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory"); #endif } }
(令我困惑的是,这个LOCK前缀在volatile写之后执行,此时已经完成写操作,它能锁定什么?)
Store Buffer与MESI的一致性问题
我认为引入Store Buffer和Invalidate队列后,MESI无法保证内存一致性。

我认为release_int_field_put是将值写入Store Buffer?
那么,__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");如何保证内存一致性?
假设有一个volatile变量被3个CPU(CPU1、CPU2、CPU3)加载到缓存中,它们的缓存行状态均为S。

根据MESI协议,CPU3开始写入该volatile变量:由于CPU3的缓存行状态为S,它会将修改写入Store Buffer,同时向地址总线发送Invalidate消息使其他CPU的缓存行失效(RFO)。CPU1和CPU2收到Invalidate消息后发送Invalidate ACK,并将自身缓存行状态改为I。正常情况下,CPU3收到所有其他CPU的ACK后,会将自身缓存行状态改为E,再将Store Buffer中的内容刷入缓存行,最后改为M状态。此时若CPU1发起本地读取,因缓存行状态为I,会向总线发起读请求,总线广播该请求;CPU3收到后,因缓存行状态为M,需先写回内存,CPU1再从内存读取,确保读到最新数据。
可能出现的一致性漏洞?
若CPU1已发送ACK,但CPU2繁忙未发送,CPU3需等收到所有处理器的ACK后才会将Store Buffer刷入缓存行。此时Store Buffer中是新值,但CPU3的缓存行状态仍为S,内部还是旧值,内存也存着旧值。而此时CPU1发起本地读取,发现缓存行状态为I,向总线发起读请求,因CPU3缓存行状态仍为S,最终从内存读到旧值。这就是引入Store Buffer后MESI无法保证内存一致性的原因。
上述过程如下:
- CPU1 读取A

- CPU2读取A

- CPU3读取A

- CPU3修改寄存器中的A内容并写入Store Buffer。

核心机制疑问
必须有一种机制保证CPU3写入缓存行时,其他处理器无法读写该缓存行,使CPU能完成完整写入、将值写入缓存并将缓存行状态改为M。这种机制由谁提供?是MESI协议吗?我理解MESI似乎不提供该机制。是LOCK前缀指令吗?但根据Intel文档,若内存已被缓存,不会锁定总线而是锁定缓存,且有人说这种缓存锁由MESI实现,因此我感到困惑。
我知道禁用重排序需要内存屏障,但对于缓存一致性,当无需向总线发送LOCK#信号时,LOCK是否相当于空操作?锁是通过总线仲裁实现的吗?
我甚至开始怀疑volatile保证每次读取最新值的说法是否正确,认为每次都能获取最新值的前提是每个volatile写操作都以原子方式写入内存(或处于M状态的缓存行),但根据我目前的理解这无法实现。我认为它仅保证每次读取都必须来自主内存。
关于缓存锁实现的猜想
我认为缓存锁的实现不应仅依赖MESI,而是这样的:
实现基于缓存的锁定机制时,锁控制单元通常由缓存控制器和锁状态字组成:
- 当CPU需要访问共享资源时,向缓存发送请求并在锁状态字上设置锁标志。
- 缓存控制器会检查是否有其他CPU已获取锁,在当前CPU释放锁前阻止其他CPU访问该共享资源。
内容的提问来源于stack exchange,提问作者Triassic

