在Intel® 64 and IA-32 Architectures Software Developer's Manual中的章节LOCK-Assert LOCK$ Signal Prefix中给出LOCK指令的详细解释
LOCK是一个指令前缀,也就是说LOCK会使紧跟在其后面的指令变成原子指令(atomic instruction)。
LOCK指令前缀只能加在以下这些指令前面ADD,ADC,AND,BTC,BTR,BTS,CMPXCHG,CMPXCH8B,CMPXCHG16B,DEC,INC,NEG,NOT,OR,SBB,SUB,XOR,XADD,XCHG
总线锁
在多处理器环境中,CPU提供了在指令执行期间对总线加锁的手段。CPU芯片上有一条引线LOCK,如果汇编语言的程序中在一条指令前面加上前缀“LOCK”,经过汇编以后的机器代码就是CPU在执行这条指令的时候把引线LOCK的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。
总线锁这种做法锁定的范围太大了,导致CPU利用率急剧下降,因为使用LOCK#是把CPU和内存之间的通信锁住了,这使得锁定期间其他处理器不能操作其内存地址的数据,所以总线锁的开销比较大
缓存锁
从P6系列处理器开始,如果访问的内存区域已经缓存在处理器的缓存行中,LOCK#信号不会被发送。它会对CPU缓存中的缓存行进行锁定,在锁定期间,其他CPU不能同时缓存此数据。在修改之后通过缓存一致性协议来保证修改的原子性。这个操作被称为“缓存锁”
什么情况下使用总线锁
当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,也会使用总线锁
因为从P6系列处理器开始才有缓存锁,所以对于早些处理器是不支持缓存锁定的,也会使用总线锁。
x86中,有些指令是自带 lock 语义的,比如 XCHG;另外一些指令可以手动加上lock前缀来实现lock语义,比如BTS, BTR,CMPXCHG指令。在这些指令中,最核心的是 CAS(Compare And Swap) 指令,它是实现各种锁语义的核心指令。不同于自带原子语义的XCHG,CAS操作要通过"lock CMPXCHG"这样的形式来实现。一般而言,原子操作的数据长度不会超过8个字节。简单来说,自旋锁和读写锁的核心都是利用原子指令来CAS操纵一个32位/64位的值
1,x86上 atomic_add
/**
* atomic_add - add integer to atomic variable
* @i: integer value to add
* @v: pointer of type atomic_t
*
* Atomically adds @i to @v.
*/
static inline void atomic_add(int i, atomic_t *v)
{
asm volatile(LOCK_PREFIX "addl %1,%0"
: "+m" (v->counter)
: "ir" (i));
}
2,__raw_cmpxchg
/*
* Atomic compare and exchange. Compare OLD with MEM, if identical,
* store NEW in MEM. Return the initial value in MEM. Success is
* indicated by comparing RETURN with OLD.
*/
#define __raw_cmpxchg(ptr, old, new, size, lock) \
({ \
__typeof__(*(ptr)) __ret; \
__typeof__(*(ptr)) __old = (old); \
__typeof__(*(ptr)) __new = (new); \
switch (size) { \
case __X86_CASE_B: \
{ \
volatile u8 *__ptr = (volatile u8 *)(ptr); \
asm volatile(lock "cmpxchgb %2,%1" \
: "=a" (__ret), "+m" (*__ptr) \
: "q" (__new), "0" (__old) \
: "memory"); \
break; \
}
3,__down_read
/*
* lock for reading
*/
static inline void __down_read(struct rw_semaphore *sem)
{
asm volatile("# beginning down_read\n\t"
LOCK_PREFIX _ASM_INC "(%1)\n\t"
/* adds 0x00000001 */
" jns 1f\n"
" call call_rwsem_down_read_failed\n"
"1:\n\t"
"# ending down_read\n\t"
: "+m" (sem->count)
: "a" (sem)
: "memory", "cc");
}
4,volatile如何保证可见性
加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障,它有三个功能:
-
确保指令重排序时不会把其后面的指令重排到内存屏障之前的位置,也不会把前面的指令排到内存屏障后面,即在执行到内存屏障这句指令时,前面的操作已经全部完成;
-
将当前处理器缓存行的数据立即写回系统内存(由volatile先行发生原则保证);
-
这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。写回操作时要经过总线传播数据,而每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器要对这个值进行修改的时候,会强制重新从系统内存里把数据读到处理器缓存(也是由volatile先行发生原则保证);
Intel 为了优化总线锁导致的性能问题,在 P6 后的处理器上,引入了缓存锁(cache locking)机制:通过缓存一致性协议保证多个 CPU 核访问跨 cache line 的内存地址的多次访问的原子性与一致性,而不需要锁内存总线。(缓存锁是依赖缓存一致性协议来保证内存访问的原子性,因为缓存一致性协议会阻止被多个 CPU 缓存的内存地址被多个 CPU 同时修改;如果要访问的内存区域已经在当前cpu的cache中了,就会利用cache一致性协议来实现原子操作,否则会锁总线;缓存锁是如何基于 MESI 协议实现内存读写的原子性:深入剖析 split locks,i++ 可能导致的灾难 - 知乎)
缓存一致性协议有多种,但是日常处理的大多数计算机设备都属于”嗅探(snooping)”机制,它的基本思想是:
所有内存的传输都发生在一条共享的总线上,而所有的处理器都能看到这条总线:缓存本身是独立的,但是内存是共享资源,所有的内存访问都要经过仲裁(同一个指令周期中,只有一个CPU缓存可以读写内存)。
CPU缓存不仅仅在做内存传输的时候才与总线打交道,而是不停在嗅探总线上发生的数据交换,跟踪其他缓存在做什么。所以当一个缓存代表它所属的处理器去读写内存时,其它处理器都会得到通知,它们以此来使自己的缓存保持同步。只要某个处理器一写内存,其它处理器马上知道这块内存在它们的缓存段中已失效。
可以得出lock指令的几个作用:
1、锁总线,其它CPU对内存的读写请求都会被阻塞,直到锁释放,不过实际后来的处理器都采用锁缓存替代锁总线,因为锁总线的开销比较大,锁总线期间其他CPU没法访问内存
2、lock后的写操作会回写已修改的数据,同时让其它CPU相关缓存行失效,从而重新从主存中加载最新的数据(发出的 LOCK# 指令锁总线或锁缓存行,同时让其他高速缓存中的缓存行内容失效,这种场景下多缓存的数据一致是通过缓存一致性协议来保证的);
3、不是内存屏障却能完成类似内存屏障的功能,阻止屏障两遍的指令重排序
由于效率问题,实际后来的处理器都采用锁缓存来替代锁总线,这种场景下多缓存的数据一致是通过缓存一致性协议来保证的 。
split lock
由于缓存一致性协议的粒度是一个 cache line,当原子操作的数据跨 cache line 时(x86 架构存在,arm 与 riscv 架构不允许),依赖缓存锁机制无法保证数据一致性,会退化为总线锁来保证一致性,这种情况就是 split lock,split 也可以理解为访存的 cache 被 split 为两个 line。
split lock 两个特征:1,原子操作,即汇编代码指令包含 Lock 前缀;2,操作的数据地址不对齐,跨越两个 cache line;
比如有如下数据结构:
struct Data {
char padding[62]; // 62字节
int32_t value; // 4字节
} __attribute__((packed)) // 按实际字节对齐
被缓存到 cache line 大小为 64 字节的 cache 中时,value 成员会跨 cache line。
此时如果想要通过LOCK ADD
指令操作 Data 结构中的 value 成员,就无法通过缓存锁解决,只能走老路,锁总线来保证数据一致性。而锁总线会引起严重的性能下降,访存延迟增加百倍左右,如果是内存密集型业务,性能会下降 2 个数量级。所以在现代 X86 处理器中,要避免写出会产生 split lock 的代码,并有能力检测出 Split lock 的产生。
https://www.cnblogs.com/badboys/p/12695183.html