Intel 锁机制


1. CPU 架构

1.1 UMA

SMP称为共享存储型多处理机(Shared Memory mulptiProcessors), 也称为对称型多处理机(Symmetry MultiProcessors)。
  共享存储型多处理机有三种模型:UMA模型、NUMA模型,区别在于存储器和外围资源如何共享或分布。
  UMA多处理机模型图所示。图中,物理存储器被所有处理机均匀共享。所有处理机对所有存储字相同。每台处理机可以有私用高速缓存,外围设备也以一定形式共享。

1.2 NUMA

NUMA多处理机模型的访问时间随存储字的位置不同而变化。其共享存储器物理上是分布在所有处理机的本地存储器上。所有本地存储器的集合组成了全局地址空间,可被所有的处理机访问。处理机访问本地存储器是比较快的,但访问属于另一台处理机的远程存储器则比较慢,因为通过互连会产生附加时延。

2. 常用指令

2.1 xchg

交换第一个操作数(寄存器/内存)与第二个操作数(寄存器)中的值。操作数可以是两个寄存器,也可以是一个寄存器和一个内存。如果其中一个操作数内存中的值,不管指令有没有添加lock前缀,处理器会自动添加lock#前缀

这个指令在实现信号量或者进程同步结构非常有用。

2.2 Lock#

在多核处理环境下,Lock#前缀可以确保访问共享内存是排它(exclusive)的。lock前缀只能出现在这些指令中:ADD,ADC,AND,BTC,BTR,BTS,CMPXCHG,CMPCH8B,CMPXCHG16B,DEC,INC,NEG,NOT,OR,SBB,SUB,XOR,XADD,XCHG。XCHG总是会自动添加LOCK前缀,不管是不是声明了。

P6处理器之后,如果一个指令声明了Lock前缀,但是要访问的内存数据已经缓存在处理器内部(缓存行),只有处理器的缓存会被加锁。缓存一致性协议确保内存的原子性访问。

3. 总线锁(Bus Locking)

Lock# 可以在硬件层面保证内存的原子性访问,通过锁总线的方式实现。Intel386,Intel486,奔腾处理器,Lock#前缀会导致锁总线。

从P6处理器之后,如果要访问的内存已经被缓存,Lock#不会生效,只会在CPU内部的缓存上加锁。MESI协议默认就会保证内存的原子性访问。

自动加锁的场景:

  • 执行XCHG指令的时候,自动添加Lock#前缀
  • 在TSS描述符中设置B(Busy)标记,切换任务的时候,避免多个处理器,同时切换执行同一个任务,处理器在testing和setting标记的时候会自动加锁
  • 更新段描述符
  • 更新页目录和页表
  • 众所周知的中断

4. CPU访问内存顺序

Intel 64 和IA-32架构支持多种内存序列模型:Intel386处理器保证程序执行序列,奔腾处理和Intel486处理器的处理器序列模型是强顺序,读和写大部分场景是强顺序(例外情况,如果读在缓存行没有命中,需要从系统总线去读取数据,这时候读会优先于写)

为了支持指令执行优化,处理器的执行序列可能跟指令的顺序不同(重排序)例如下面的例子:

int a = 1;
a++;
int b = 1;
b++;

真实的执行顺序可能被重排:

int a = 1;
int b = 1;
b++;
a++;

指令重排的前提是没有依赖关系, 如果有依赖关系,那是程序错误了。

在P6和最近的处理器执行序列模型支持写序列支持store-buffer转发(write ordered with store-buffer forwarding)

在单核处理器访问内存域定义了write-back cacheable(写的时候写入缓存,选择合适的时机回写到内存, write through:只写内存),内存序列模型遵守以下的原则:(内存序列模型,单核和多核的处理方式差不多):

  • 读不会重排序到后面的读操作(由于存在loadbuffer,类似队列,FIFO保证)---loadload不会出现
  • 写不会重排序到后面的读操作(写是写到store buffer的,支持forwarding机制,写入之后,如果有指令正在读,会先从store buffer去找,然后转发过去),storeload也不会出现
  • 写不会重排序到后面的写操作(storebuffer也是一个对列,保证不会重排序)--storestore不存在
  • 读操作可能重排序后面的写操作,前提是操作的是不同的内存(数据无关,Icache,Dcache提前预读)
  • 读和写不会重排序在IO指令,lock前缀或者序列化指令
  • 读不会跨越LFENCE和MFENCE指令

在多核处理器访问内存域遵循以下原则:

  • 每一个CPU遵循单核CPU的规则
  • 一个CPU的写会被其他CPU观测到
  • 从一个CPU的写操作不会重排序到其他CPU后面的写操作(MESI保证)
  • 从StoreBuffer写出去,其他CPU立马可见(MESI保证)
  • StoreBuffer是一起刷出去的,其他CPU马上可见
  • Locked指令一定全局有序

这就不难解释,JVM层面的屏障的实现

//x86下面,loadload和storeload不存在,storestore为不加锁实现,LoadStore采用lock指令保证语义
// The following table shows the implementations on some architectures:
//
//                       Constraint     x86          sparc TSO          ppc
// ---------------------------------------------------------------------------
// fence                 LoadStore  |   lock         membar #StoreLoad  sync
//                       StoreStore |   addl 0,(sp)
//                       LoadLoad   |
//                       StoreLoad
//
// release               LoadStore  |                                   lwsync
//                       StoreStore
//
// acquire               LoadLoad   |                                   lwsync
//                       LoadStore
//
// release_store                                          lwsync
//                                                                      
//
// release_store_fence                  xchg                     lwsync
//                                                   membar #StoreLoad  
//                                                                      sync
//
//
// load_acquire                                             
//                                                                      lwsync

5. MESI协议

5.1 架构

几个关键概念:

WB:写缓存行,在某个时刻同步到内存或者L3缓存

WT:不写缓存行,只写内存或者L3缓存

WC:写缓存行,同时写内存或者L3缓存

5.2 关键状态流转

  • Exclusive

  • share

  • Modify & Invalid

MESI必须存在,否则是一个失败的CPU设计,同时MESI不能被干预,默认工作,与其他一切毫无关系

6. 总结

一切应用层的锁机制,均是以CPU级别的原子性来保证的

JVM