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级别的原子性来保证的