多线程可见性深度解读
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去找,然后转发过去),loadstore也不会出现
- 写不会重排序到后面的写操作(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 <store> <store> lwsync
// <store>
//
// release_store_fence xchg <store> lwsync
// membar #StoreLoad <store>
// sync
//
//
// load_acquire <load> <load> <load>
// lwsync
inline void OrderAccess::loadload() { acquire(); }
inline void OrderAccess::storestore() { release(); }
inline void OrderAccess::loadstore() { acquire(); }
inline void OrderAccess::storeload() { fence(); }
// 很明显loadload 和loadstore的实现是直接调用acquire方法通过前面内存访问顺序的分析,可知,指令执行层面不会出现loadload和loadstore的乱序, 这里只需要保证编译器不优化和禁止指令对CPU重排序即可
inline void OrderAccess::acquire() {
volatile intptr_t local_dummy;
#ifdef AMD64
__asm__ volatile ("movq 0(%%rsp), %0" : "=r" (local_dummy) : : "memory");
#else
__asm__ volatile ("movl 0(%%esp),%0" : "=r" (local_dummy) : : "memory");
#endif // AMD64}
//同理禁止编译器优化inline void OrderAccess::release() { volatile jint local_dummy = 0;}
// 注意这里,如果是单核CPU不会有问题,storebuffer是同一个, 如果是多核,storebuffer会有多个,出现了在缓存中没有命中的情况下,会先写到storebuffer中,读请求从缓存中读,实际是从loadbuffer中读取,没办法做到forewarding, 可能还是老的值, 所以要实现语义,需要加lock前缀
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
}
}
5. MESI协议
5.1 架构
几个关键概念:
WB:写缓存行,在某个时刻同步到内存或者L3缓存
WT:不写缓存行,只写内存或者L3缓存
WC:写缓存行,同时写内存或者L3缓存
5.2 关键状态流转
- Exclusive
- share
- Modify & Invalid
MESI必须存在,否则是一个失败的CPU设计,同时MESI不能被干预,默认工作,与其他一切毫无关系
6. JAVA语言volatile的实现
6.1 Java层面
一个java单例模式的例子
public class Demo {
public static volatile Demo demo;
private Demo() {
}
public static void getInstance() {
if (demo == null) {
synchronized (Demo.class) {
if (demo == null) {
demo = new Demo();
}
}
}
}
}
对应的字节码
// 这里只列出了demo属性,注意flags标记ACC_VOLATILE
public static volatile com.example.demo.Demo demo;
descriptor: Lcom/example/demo/Demo;
flags: (0x0049) ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE
6.2 JVM层面
对于变量volatile的实现都是通过putstatic和getstatic来处理的
putstatic,putfield
CASE(_putfield):
CASE(_putstatic):
{
// 获取常量池索引&解析常量池
u2 index = Bytes::get_native_u2(pc+1);
ConstantPoolCacheEntry* cache = cp->entry_at(index);
if (!cache->is_resolved((Bytecodes::Code)opcode)) {
CALL_VM(InterpreterRuntime::resolve_get_put(THREAD, (Bytecodes::Code)opcode),
handle_exception);
cache = cp->entry_at(index);
}
// 省略了部分JVMTI的实现
oop obj;
int count;
TosState tos_type = cache->flag_state();
// 从栈帧中取几个
count = -1;
// long和double需要2个slot
if (tos_type == ltos || tos_type == dtos) {
--count;
}
// static变量不保存对象中,mirrorKlass中的
if ((Bytecodes::Code)opcode == Bytecodes::_putstatic) {
Klass* k = cache->f1_as_klass();
obj = k->java_mirror();
} else {
--count;
obj = (oop) STACK_OBJECT(count);
CHECK_NULL(obj);
}
// 取出属性的偏移量
int field_offset = cache->f2_as_index();
// 注意这里,如果是volatile使用的是release_**方法
if (cache->is_volatile()) {
if (tos_type == itos) {
// int
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));
OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
// bool类型
} else if (tos_type == btos) {
obj->release_byte_field_put(field_offset, STACK_INT(-1));
// long
} else if (tos_type == ltos) {
obj->release_long_field_put(field_offset, STACK_LONG(-1));
// char
} else if (tos_type == ctos) {
obj->release_char_field_put(field_offset, STACK_INT(-1));
// string
} else if (tos_type == stos) {
obj->release_short_field_put(field_offset, STACK_INT(-1));
// float
} else if (tos_type == ftos) {
obj->release_float_field_put(field_offset, STACK_FLOAT(-1));
} else {
// double
obj->release_double_field_put(field_offset, STACK_DOUBLE(-1));
}
// 写完之后使用的storeload
OrderAccess::storeload();
} else {
if (tos_type == itos) {
obj->int_field_put(field_offset, STACK_INT(-1));
} else if (tos_type == atos) {
VERIFY_OOP(STACK_OBJECT(-1));
obj->obj_field_put(field_offset, STACK_OBJECT(-1));
OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
} else if (tos_type == btos) {
obj->byte_field_put(field_offset, STACK_INT(-1));
} else if (tos_type == ltos) {
obj->long_field_put(field_offset, STACK_LONG(-1));
} else if (tos_type == ctos) {
obj->char_field_put(field_offset, STACK_INT(-1));
} else if (tos_type == stos) {
obj->short_field_put(field_offset, STACK_INT(-1));
} else if (tos_type == ftos) {
obj->float_field_put(field_offset, STACK_FLOAT(-1));
} else {
obj->double_field_put(field_offset, STACK_DOUBLE(-1));
}
}
UPDATE_PC_AND_TOS_AND_CONTINUE(3, count);
}
// 在写之前调用的是OrderAccess::release_store
inline void oopDesc::release_char_field_put(int offset, jchar contents) { OrderAccess::release_store(char_field_addr(offset), contents); }
// 在写之后调用的 OrderAccess::storeload();
inline void OrderAccess::storeload() { fence(); }
从上面的代码可知,修改值的时候,在修改之前就调用的是release_store方法,在修改之后添加的是storeload屏障
// 以X86平台为例, 这里只添加了volatile,只需要保证编译器不优化,我们的X86平台是不会存在storestore平台的
inline void OrderAccess::release_store(volatile jbyte* p, jbyte v) { *p = v; }
// X86平台并不能说明问题,我们查看zero的实现(0汇编)
inline void OrderAccess::release_store(volatile jbyte* p, jbyte v) { release(); *p = v; }
inline void OrderAccess::release() {
WRITE_MEM_BARRIER;
}
// 这里告诉编译器,这里不允许优化
#define WRITE_MEM_BARRIER __asm __volatile ("":::"memory")
// 全屏障
inline void OrderAccess::storeload() { fence(); }
// 使用lock前缀,这里至少保证锁缓存
inline void OrderAccess::fence() {
// 多核处理器,才需要处理,单核是不会存在重排序问题的
if (os::is_MP()) {
#ifdef AMD64
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
}
}
getstatic,getfield
CASE(_getfield):
CASE(_getstatic):
{
// 同样先解析常量池
u2 index;
ConstantPoolCacheEntry* cache;
index = Bytes::get_native_u2(pc+1);
cache = cp->entry_at(index);
if (!cache->is_resolved((Bytecodes::Code)opcode)) {
CALL_VM(InterpreterRuntime::resolve_get_put(THREAD, (Bytecodes::Code)opcode),
handle_exception);
cache = cp->entry_at(index);
}
// 省略JVMTI的处理,我们此处不关心
oop obj;
if ((Bytecodes::Code)opcode == Bytecodes::_getstatic) {
Klass* k = cache->f1_as_klass();
obj = k->java_mirror();
MORE_STACK(1); // Assume single slot push
} else {
obj = (oop) STACK_OBJECT(-1);
CHECK_NULL(obj);
}
TosState tos_type = cache->flag_state();
int field_offset = cache->f2_as_index();
// volatile的处理
if (cache->is_volatile()) {
if (tos_type == atos) {
// 关键是**field_acquire
VERIFY_OOP(obj->obj_field_acquire(field_offset));
SET_STACK_OBJECT(obj->obj_field_acquire(field_offset), -1);
} else if (tos_type == itos) {
SET_STACK_INT(obj->int_field_acquire(field_offset), -1);
} else if (tos_type == ltos) {
SET_STACK_LONG(obj->long_field_acquire(field_offset), 0);
MORE_STACK(1);
} else if (tos_type == btos) {
SET_STACK_INT(obj->byte_field_acquire(field_offset), -1);
} else if (tos_type == ctos) {
SET_STACK_INT(obj->char_field_acquire(field_offset), -1);
} else if (tos_type == stos) {
SET_STACK_INT(obj->short_field_acquire(field_offset), -1);
} else if (tos_type == ftos) {
SET_STACK_FLOAT(obj->float_field_acquire(field_offset), -1);
} else {
SET_STACK_DOUBLE(obj->double_field_acquire(field_offset), 0);
MORE_STACK(1);
}
} else {
if (tos_type == atos) {
VERIFY_OOP(obj->obj_field(field_offset));
SET_STACK_OBJECT(obj->obj_field(field_offset), -1);
} else if (tos_type == itos) {
SET_STACK_INT(obj->int_field(field_offset), -1);
} else if (tos_type == ltos) {
SET_STACK_LONG(obj->long_field(field_offset), 0);
MORE_STACK(1);
} else if (tos_type == btos) {
SET_STACK_INT(obj->byte_field(field_offset), -1);
} else if (tos_type == ctos) {
SET_STACK_INT(obj->char_field(field_offset), -1);
} else if (tos_type == stos) {
SET_STACK_INT(obj->short_field(field_offset), -1);
} else if (tos_type == ftos) {
SET_STACK_FLOAT(obj->float_field(field_offset), -1);
} else {
SET_STACK_DOUBLE(obj->double_field(field_offset), 0);
MORE_STACK(1);
}
}
UPDATE_PC_AND_CONTINUE(3);
}
// 以obj_field_acquire的实现为例
inline oop oopDesc::obj_field_acquire(int offset) const {
return UseCompressedOops ?
decode_heap_oop((narrowOop)
OrderAccess::load_acquire(obj_field_addr<narrowOop>(offset)))
: decode_heap_oop((oop)
OrderAccess::load_ptr_acquire(obj_field_addr<oop>(offset)));
}
// X86实现为例, 只保证编译器不优化即可,由于loadbuffer的存在,FIFO,不存在乱序
inline jbyte OrderAccess::load_acquire(volatile jbyte* p) { return *p; }
// zero的实现,可知只加了一个读屏障readread
inline jbyte OrderAccess::load_acquire(volatile jbyte* p) { jbyte data = *p; acquire(); return data; }
inline void OrderAccess::acquire() {
READ_MEM_BARRIER;
}
#define READ_MEM_BARRIER __asm __volatile ("":::"memory")
总结一下,volatile的属性,的处理方式如下:
- 写:storestore;写逻辑;storeload, x86平台下storestore不存在重排序,只需要保证编译器不优化即可,而storeload是有可能重排序的,加lock前缀保证,兼具防止编译器优化的能力
- 读:其实只加了一个读屏障,赋值之后加读屏障。但是X86平台不存在,读读乱序,只需要避免编译器优化即可
6.3 CPU层面
前面第4节,详细阐述了CPU访问内存的顺序。回过头再看看。
7. Linux 锁实现
7.1 基础知识
7.1.1 C语言中的volatile
查阅linux内核对volatile的说明,
C程序员使用volatile表示变量不会在当前线程以外被改变;这就导致某些时候,在内核中的同步结构中voaltile被使用。换句话说,他们把volatile当做是一种简单的原子变量,事实上不是。volatile在内核中的使用几乎不会正确。这篇文章说明了为什么。
关于volatile,关键点是需要理解volatile的目的是解优化(不想编译器优化代码)。
考虑如下代码:
// C语言
#include "stdio.h"
int main(){
int a = 1;
a++;
int b = 1;
b++;
return a+b;
}
## gcc -S *.c, 移除部分开辟栈帧的代码
main:
movl $1, -4(%rbp)
addl $1, -4(%rbp)
movl $1, -8(%rbp)
addl $1, -8(%rbp)
movl -4(%rbp), %edx
movl -8(%rbp), %eax
addl %edx, %eax
ret
##gcc -S -O4 *.c, 采取O4级别编译优化
main:
movl $4, %eax
ret
添加volatile
#include "stdio.h"
int main(){
int a = 1;
a++;
// 添加volatile,禁止优化
volatile int b = 1;
b++;
return a+b;
}
main:
movl $1, -4(%rsp)
movl -4(%rsp), %eax # int b =1;
addl $1, %eax # 注意这里只有一个+1,b++
movl %eax, -4(%rsp)
movl -4(%rsp), %eax
addl $2, %eax # 直接+2
ret
在内核中,必须避免共享数据结构的并发访问,这是一项完全不同的任务。防止不必要的并发过程也将以更有效的方式避免几乎所有的优化相关的问题。像volatile一样,内核提供了原语来实现并发结构的安全访问(spinlocks, mutexes, memory barriers等),这些结构也能避免不必要的优化。如果他们被合适的使用,将不需要使用volatile。如果volatile仍然是必要的,几乎可以确定代码的某些地方有bug。在正确编写的内核代码中,volatile只能起到减慢速度的作用。
考虑一个典型的内核代码块:
spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);
如果满足加锁规则,当持有锁,共享数据结构不会被改变成一个不想要的值。其他想要修改数据的代码将会被阻塞。自旋锁扮演者内存屏障的角色。明确要求遵循下面的规则:
在加锁代码块内的数据访问不会被优化。编译器可能会认为存在共享数据中,但是由于spin_lock()的调用,所以它扮演者内存屏障的角色,将会迫使编译器忘记所有事。在访问那个数据结构的时候将不会产生优化问题。
如果共享数据结构定义为volatile, 加锁操作仍然是必须的。但是编译器也会避免优化共享数据结构,即使我们知道没有其他线程会执行。持有锁的时候,共享数据不需要volatile。当处理共享数据的时候,合适的锁使得volatile是没必要的,甚至可能是有害的。
7.2 自旋锁
在单核CPU其实是不需要自旋锁,单核CPU在同一时刻只有一个线程运行。在SMP对称多处理器中才需要使用它。
typedef struct {
// volatile禁止编译器优化,整型变量保存锁
volatile unsigned int lock;
} spinlock_t;
#define SPIN_LOCK_UNLOCKED (spinlock_t) { 1 SPINLOCK_MAGIC_INIT }
// 自旋锁初始化为1
#define spin_lock_init(x) do { *(x) = SPIN_LOCK_UNLOCKED; } while(0)
// 判断是否加锁,判断lock的值<=0
#define spin_is_locked(x) (*(volatile signed char *)(&(x)->lock) <= 0)
// 等待锁释放,只要没释放,会一直轮循检测
#define spin_unlock_wait(x) do { barrier(); } while(spin_is_locked(x))
// 内存屏障,禁止编译器优化
#define barrier() __asm__ __volatile__("": : :"memory")
// tryLock 的实现
static inline int _raw_spin_trylock(spinlock_t *lock)
{
char oldval;
__asm__ __volatile__(
"xchgb %b0,%1"
:"=q" (oldval), "=m" (lock->lock)
:"0" (0) : "memory");
return oldval > 0;
}
// 调用spin_lock_string, 输出到lock-lock
static inline void _raw_spin_lock(spinlock_t *lock)
{
__asm__ __volatile__(
spin_lock_string
:"=m" (lock->lock) : : "memory");
}
#define spin_lock_string
"1:"
"lock ; decb %0" // lock前缀加锁(锁缓存行或者总线),减1
"jns 3f" // >= 0跳转到3,直接返回了
"2:"
"rep;nop" // 检测0标志位的时候停止,其他循环进行空操作,
// 后面其实是进行了二次判断,多CPU的场景下,有可能被其他CPU改变的
"cmpb $0,%0" // 判断lock和0
"jle 2b" // 如果lock > 0, 回到2重新执行
"jmp 1b" // 如果=0, 开始跳到1处执行
"3:"
static inline void _raw_spin_unlock(spinlock_t *lock)
{
__asm__ __volatile__(
spin_unlock_string
);
}
// lock 恢复成1
#define spin_unlock_string
"movb $1,%0"
:"=m" (lock->lock) : : "memory"
7.3 读写锁-基于自旋锁实现
读写自旋锁,允许多个读但是只允许一个写。注意:通常有读请求是可以被中断的,但是写实不允许中断。
// 跟读锁一样,lock是无符号int表示
typedef struct {
volatile unsigned int lock;
} rwlock_t;
// 注意,这里用第7位作为写锁的标志位,32位表示
#define RW_LOCK_BIAS 0x01000000
#define RW_LOCK_BIAS_STR "0x01000000"
#define RW_LOCK_UNLOCKED (rwlock_t) { RW_LOCK_BIAS RWLOCK_MAGIC_INIT }
// 初始化操作
#define rwlock_init(x) do { *(x) = RW_LOCK_UNLOCKED; } while(0)
// lock != 初始值,实际是判断写锁
#define rwlock_is_locked(x) ((x)->lock != RW_LOCK_BIAS)
// 获取读锁
static inline void _raw_read_lock(rwlock_t *rw)
{
__build_read_lock(rw, "__read_lock_failed");
}
#define __build_read_lock(rw, helper) do {
if (__builtin_constant_p(rw))
__build_read_lock_const(rw, helper);
else
__build_read_lock_ptr(rw, helper);
} while (0)
#define __build_read_lock_ptr(rw, helper)
asm volatile(LOCK "subl $1,(%0)" // 减1
"jns 1f" // 非负数跳转, 只有可能之前获取了写锁,才有可能变成0, 减1就是负数
"call " helper "" // 失败调用__read_lock_failed
"1:"
::"a" (rw) : "memory") // 不输出,即减1操作不会影响rw的锁,已经将rw放入eax寄存器中
asm(
"__read_lock_failed:"
LOCK "incl (%eax)" // 上面做了减1,这里加1恢复一下
"1: rep; nop"
"cmpl $1,(%eax)" // 查看lock是否>=1,即写锁释放
"js 1b" // 小于1,跳转到1处执行
LOCK "decl (%eax)" // 写锁释放后,可以加读锁
"js __read_lock_failed", //小于0,继续执行__read_lock_failed
"ret"
);
// 释放锁,lock前缀指令,+1
#define _raw_read_unlock(rw) asm volatile("lock ; incl %0" :"=m" ((rw)->lock) : : "memory")
// 写锁
static inline void _raw_write_lock(rwlock_t *rw)
{
__build_write_lock(rw, "__write_lock_failed");
}
#define __build_write_lock(rw, helper) do {
if (__builtin_constant_p(rw))
__build_write_lock_const(rw, helper);
else
__build_write_lock_ptr(rw, helper);
} while (0)
#define __build_write_lock_ptr(rw, helper)
asm volatile(LOCK "subl $" RW_LOCK_BIAS_STR ",(%0)" // 减去0x01000000
"jz 1f\n" // =0, 说明加锁成功
"call " helper "\n\t"
"1:\n" \
::"a" (rw) : "memory")
asm(
"__write_lock_failed:"
LOCK "addl $" RW_LOCK_BIAS_STR ",(%eax)" // 恢复一下
"1: rep; nop"
"cmpl $" RW_LOCK_BIAS_STR ",(%eax)" // 是否和0x01000000相等
"jne 1b" // 不相等,跳到1轮循
LOCK "subl $" RW_LOCK_BIAS_STR ",(%eax)" // 已经相等了,可以加锁了
"jnz __write_lock_failed" // 又不等于0了,被其他CPU给抢了,再重复执行
"ret"
);
7.4 读写锁-基于信号量实现
- MSW: 高有效字
- LSW:低有效字
简述该读写锁的实现:
count的高16位是负数,表示的是活动的写锁的持有数量和等待锁的数量,低16位活动锁的总数。
count初始化为0(没有活动锁和等待锁的线程)
当writer减去WRITE_BIAS(-0x00010000 + 0x00000001 = 0xffff0000 + 0x00000001 = 0xffff0001)将会得到0xffff0001,代表获得了一个非竞争的锁。因为XADD会返回老的值,所以可以用来检测。
当没有竞争的时候,Reader+1,会得到一个整数, 当存在writers或者读者在等待,将会得到负数
这个时间是公平的-如果有进程在等待,一个进程想要获取锁,将会被添加吊队列中。当当前活动的锁被释放,如果有写进程在对头,只有这个写进程会被唤醒,如果是读进程在对头,在对头的所有连续的读进程都会被唤醒,但是其他进程不会被唤醒。
实现
struct rw_semaphore {
// 32为系统,long为32位,64位系统,用int就可以表示,看见long的定义,可以分为高16位和低16位(MSW,LSW)
signed long count;
#define RWSEM_UNLOCKED_VALUE 0x00000000 // 无锁状态
#define RWSEM_ACTIVE_BIAS 0x00000001 // 锁活动偏移
#define RWSEM_ACTIVE_MASK 0x0000ffff
#define RWSEM_WAITING_BIAS (-0x00010000) // 锁等待偏移
#define RWSEM_ACTIVE_READ_BIAS RWSEM_ACTIVE_BIAS // 读锁偏移
#define RWSEM_ACTIVE_WRITE_BIAS (RWSEM_WAITING_BIAS + RWSEM_ACTIVE_BIAS) //写锁偏移
spinlock_t wait_lock; // 保护等待链表
struct list_head wait_list;// 等待链表
};
// 读写锁信号量初始化
static inline void init_rwsem(struct rw_semaphore *sem)
{
// 初始化为0, 即无锁状态
sem->count = RWSEM_UNLOCKED_VALUE;
// 自旋锁初始化
spin_lock_init(&sem->wait_lock);
// 初始化等待吗队列
INIT_LIST_HEAD(&sem->wait_list);
}
// 双向链表的实现
#define INIT_LIST_HEAD(ptr) do {
(ptr)->next = (ptr); (ptr)->prev = (ptr);
} while (0)
/*
* 获取读锁
*/
static inline void __down_read(struct rw_semaphore *sem)
{
// 内联汇编,禁止编译器优化
__asm__ __volatile__(
LOCK_PREFIX " incl (%%eax)" /* 加 0x00000001, 返回旧值 */
" js 2f" /* 负数跳转,负数说明之前有写锁存在,加锁失败,跳转到标号2 */
"1:\n\t"
LOCK_SECTION_START("")
"2:"
" pushl %%ecx"
" pushl %%edx"
" call rwsem_down_read_failed" // 调用rwsem_down_read_failed
" popl %%edx"
" popl %%ecx"
" jmp 1b"
LOCK_SECTION_END
"# ending down_read"
: "=m"(sem->count)
: "a"(sem), "m"(sem->count)
: "memory", "cc");
}
#define RWSEM_WAITING_FOR_READ 0x00000001
rwsem_down_read_failed(struct rw_semaphore *sem)
{
struct rwsem_waiter waiter;
waiter.flags = RWSEM_WAITING_FOR_READ;
rwsem_down_failed_common(sem, &waiter,
RWSEM_WAITING_BIAS - RWSEM_ACTIVE_BIAS); // fffeffff, 高16位-1
rwsemtrace(sem, "Leaving rwsem_down_read_failed");
return sem;
}
static inline struct rw_semaphore *
rwsem_down_failed_common(struct rw_semaphore *sem,
struct rwsem_waiter *waiter, signed long adjustment)
{
struct task_struct *tsk = current; // 当前任务PCB
signed long count;
set_task_state(tsk, TASK_UNINTERRUPTIBLE); // 设置当前任务不可中断
//获取自旋锁
spin_lock(&sem->wait_lock);
// PCB和等待节点关联
waiter->task = tsk;
get_task_struct(tsk);
// 插入队列末尾
list_add_tail(&waiter->list, &sem->wait_list);
/* 原子更新count */
count = rwsem_atomic_update(adjustment, sem);
// 读锁和写锁,一样,低16位=0的情况下,是没有获取到锁的
/* 如果没有活动的锁,直接唤醒队列中的任务,只存在当前读锁情况,count的低16位为0 */
if (!(count & RWSEM_ACTIVE_MASK))
// 这个时候说明没有其他进程拿到锁了,可以直接唤醒一个线程
sem = __rwsem_do_wake(sem, 0);
// 解锁
spin_unlock(&sem->wait_lock);
/* wait to be given the lock */
for (;;) {
// 等待标志位为0,直接退出
if (!waiter->task)
break;
//调度其他任务
schedule();
// 不可中断等待
set_task_state(tsk, TASK_UNINTERRUPTIBLE);
}
tsk->state = TASK_RUNNING;
return sem;
}
// delta为全1,高16位减去1
static inline int rwsem_atomic_update(int delta, struct rw_semaphore *sem)
{
int tmp = delta;
__asm__ __volatile__(
LOCK_PREFIX "xadd %0,(%2)"
: "+r"(tmp), "=m"(sem->count)
: "r"(sem), "m"(sem->count)
: "memory");
return tmp+delta;
}
static inline void __down_write(struct rw_semaphore *sem)
{
int tmp;
tmp = RWSEM_ACTIVE_WRITE_BIAS;
__asm__ __volatile__(
"# beginning down_write\n\t"
LOCK_PREFIX " xadd %%edx,(%%eax)" /* 加锁成功将会得到,0xffff0001, 高16位和低16位都会变,高16位为负数,低16位+1*/
" testl %%edx,%%edx" /* was the count 0 before? */
" jnz 2f\" /* 之前!=0, 说明有读锁或者写锁 */
"1:"
LOCK_SECTION_START("")
"2:"
" pushl %%ecx\n\t"
" call rwsem_down_write_failed\n\t"
" popl %%ecx\n\t"
" jmp 1b\n"
LOCK_SECTION_END
"# ending down_write"
: "=m"(sem->count), "=d"(tmp)
: "a"(sem), "1"(tmp), "m"(sem->count)
: "memory", "cc");
}
struct rw_semaphore fastcall __sched *
rwsem_down_write_failed(struct rw_semaphore *sem)
{
struct rwsem_waiter waiter;
waiter.flags = RWSEM_WAITING_FOR_WRITE;
rwsem_down_failed_common(sem, &waiter, -RWSEM_ACTIVE_BIAS);// -RWSEM_ACTIVE_BIAS,全1,
rwsemtrace(sem, "Leaving rwsem_down_write_failed");
return sem;
}
7.5 seq锁的实现
为什么叫seq锁呢?就是对一个sequence变量执行操作?前面提到的读写锁,当读锁持有是,写被阻塞,持有写锁时,读和写都被阻塞,这可能会导致写锁长时间不能加锁,饿死现象。而seq锁可以实现持有读锁时,还能写,即读写互不影响,如何做到的呢?
typedef struct {
// 持有锁的变量
unsigned sequence;
spinlock_t lock;
} seqlock_t;
// sequence初始化为0,
#define SEQLOCK_UNLOCKED { 0, SPIN_LOCK_UNLOCKED }
#define seqlock_init(x) do { *(x) = (seqlock_t) SEQLOCK_UNLOCKED; } while (0)
/* Lock out other writers and update the count.
* Acts like a normal spin_lock/unlock.
* Don't need preempt_disable() because that is in the spin_lock already.
* 获取写锁
*/
static inline void write_seqlock(seqlock_t *sl)
{
// 自旋锁
spin_lock(&sl->lock);
// sequence+1
++sl->sequence;
/*
* 写屏障,保证指令有序性,不需要禁止抢占,因为加了spin_lock
**/
smp_wmb();
}
// 写,顺序释放锁
static inline void write_sequnlock(seqlock_t *sl)
{
/*
* 写屏障,保证指令有序性,不需要禁止抢占,因为加了spin_lock
**/
smp_wmb();
sl->sequence++;
spin_unlock(&sl->lock);
}
/* 读锁?实际并没有加锁 */
static inline unsigned read_seqbegin(const seqlock_t *sl)
{
unsigned ret = sl->sequence;
// 加了读屏障,保证不会重排序后面的读操作,如果返回值跟之前保存的值不相等,说明在读期间发生了写,需要重试
smp_rmb();
return ret;
}
/* 用于判断读期间有没有写进程进行了修改.
* 如果初始值是偶数,
*/
static inline int read_seqretry(const seqlock_t *sl, unsigned iv)
{
smp_rmb();
return (iv & 1) | (sl->sequence ^ iv);
}
一言以蔽之,写锁,通过自旋锁来保证,读锁实际就是通过屏障指令来保证,并没有真实加锁,只是保证可见性。
8. 总结
一切应用层的锁机制,均是以CPU级别的原子性来保证的