目录
1.知识回顾: glibc-2.43 i386版本的自旋锁
2.glibc-2.43 x86_64版本的自旋锁的参考代码
3.前置知识
几个调用约定
lock前缀、pthread_spinlock_t的类型
4.分析
pthread_spin_init.c
pthread_spin_unlock.S(x86_64)
pthread_spin_lock.S(x86_64)
pthread_spin_trylock.S(x86_64)
从汇编指令看pthread_spin_lock.S和pthread_spin_trylock.S的区别
5.知识点摘录: Raymond Chen The Intel 80386, part 10: Atomic operations and memory alignment
1.知识回顾: glibc-2.43 i386版本的自旋锁
参见OS85.【Linux】各种锁(重点分析glibc i386版本的自旋锁的底层原理)文章
2.glibc-2.43 x86_64版本的自旋锁的参考代码
glibc-2.43的以下文件:
/sysdeps/x86_64/nptl/pthread_spin_init.c
/sysdeps/x86_64/nptl/pthread_spin_lock.S
/sysdeps/x86_64/nptl/pthread_spin_trylock.S
/sysdeps/x86_64/nptl/thread_spin_unlock.S
3.前置知识
几个调用约定
参见127.【C语言】面试题:函数的调用约定文章复习x86_64(64位)和i386(32位)下的linux的函数调用约定
lock前缀、pthread_spinlock_t的类型
两个都参见OS85.【Linux】各种锁(重点分析glibc i386版本的自旋锁的底层原理)文章
4.分析
pthread_spin_init.c
x86_64版的pthread_spin_init.c和之前文章讲的i386版的一样
pthread_spin_init.c就一个include语句:
#include <sysdeps/i386/nptl/pthread_spin_init.c>sysdeps/i386/nptl/pthread_spin_init.c中的pthread_spin_init函数又是i386版pthread_spin_unlock的别名
也就是说: x86_64和i386版的pthread_spin_init函数的实现都是i386版pthread_spin_unlock汇编版本
pthread_spin_unlock使用栈传递锁的指针,在x86_64下也是兼容i386的
pthread_spin_unlock.S(x86_64)
#include <sysdep.h> #include <shlib-compat.h> ENTRY(__pthread_spin_unlock) movl $1, (%rdi) xorl %eax, %eax retq END(__pthread_spin_unlock) versioned_symbol (libc, __pthread_spin_unlock, pthread_spin_unlock, GLIBC_2_34) /* The implementation of pthread_spin_init is identical. */ versioned_symbol (libc, __pthread_spin_unlock, pthread_spin_init, GLIBC_2_34) #if OTHER_SHLIB_COMPAT (libpthread, GLIBC_2_2, GLIBC_2_34) compat_symbol (libpthread, __pthread_spin_unlock, pthread_spin_unlock, GLIBC_2_2) compat_symbol (libpthread, __pthread_spin_unlock, pthread_spin_init, GLIBC_2_2) #endif转换为intel语法:
mov dword ptr [rdi], 1 xor eax, eax ret由于Linux上x86_64的调用约定和i386不一样,根据System V AMD64 ABI的调用约定,第一个整数参数使用rdi寄存器
和OS85.【Linux】各种锁(重点分析glibc i386版本的自旋锁的底层原理)文章的分析一样,锁在内存中的值为1,代表锁未被线程获取
pthread_spin_lock.S(x86_64)
#include <sysdep.h> #include <shlib-compat.h> ENTRY(__pthread_spin_lock) /* Always return zero. */ xor %eax, %eax LOCK decl 0(%rdi) jne 1f ret .align 16 1: /* `rep nop` == `pause`. */ rep nop cmpl %eax, 0(%rdi) jle 1b /* Just repeat the `lock decl` logic here. The code size save of jumping back to entry doesn't change how many 16-byte chunks (default function alignment) that the code fits in. */ LOCK decl 0(%rdi) jne 1b ret END(__pthread_spin_lock) versioned_symbol (libc, __pthread_spin_lock, pthread_spin_lock, GLIBC_2_34) #if OTHER_SHLIB_COMPAT (libpthread, GLIBC_2_2, GLIBC_2_34) compat_symbol (libpthread, __pthread_spin_lock, pthread_spin_lock, GLIBC_2_2) #endif转换为intel语法:
xor eax,eax lock dec dword [rdi] jnz label ret nop dword [rax+rax+0x0] label: pause cmp [rdi],eax jng label lock dec dword [rdi] jnz label ret逐条指令分析:
xor eax,eax: 表示pthread_spin_lock函数总是返回0
lock dec dword [rdi]: 锁值原子的-1
jnz label:
如果锁值变成0,说明线程成功申请锁,那么执行ret返回,nop dword [rax+rax+0x0]是空操作,用于对齐,不会执行
如果锁值没变成0(比如锁被其它线程抢走了,这个线程再执行锁值-1操作会导致锁值变成-1),说明线程未成功申请锁,执行pause
(pause的作用在 OS85.【Linux】各种锁(重点分析glibc i386版本的自旋锁的底层原理)文章讲过了)
cmp [rdi],eax,将锁值[rdi]和0比较(此时eax寄存器的值就是0)
jng label:
根据李忠老师的《x86汇编语言:从实模式到保护模式 第二版》的第7章
相同的功能,不同的代码 → 7.9 其他标志位和条件转移指令 → 7.9.5 条件转移指令 → 表7-1 各种比较结果和相应的条件转移指令:
jng的ng是not greater(不大于
),jg适用于有符号数的比较,要求: ZF=1(两个数相同,相减的结果为0,或者SF≠OF(如果相减后溢出,则结果必须是正数,说明源操作数大;如果相减后未溢出,则结果必须是负数,同样表明源操作数大些)
如果锁变量0,则跳到label的pause,继续自旋等待
如果锁变量>0,则执行下一个lock dec dword [rdi],获取锁
jnz label,如果获取锁失败,则跳到label的pause,继续自旋等待,否则执行ret返回
jng label lock dec dword [rdi] jnz label使用jng label和jnz label的原因: 意两条指令之间都可能发生线程的的切换,线程切换后,其它线程可能会抢走锁
代码充分说明pthread_spin_lock申请自旋锁失败会一直申请
pthread_spin_trylock.S(x86_64)
#include <sysdep.h> #include <errno.h> #include <shlib-compat.h> ENTRY(__pthread_spin_trylock) xorl %ecx, %ecx /* xchg has implicit LOCK prefix. */ xchgl %ecx, (%rdi) /* Branch on result. Expectation is the use of trylock will be branching on success/failure so this branch can be used to to predict the coming branch. It has the benefit of breaking the likely expensive memory dependency on (%rdi). */ cmpl $1, %ecx jnz 1f xorl %eax, %eax ret 1: movl $EBUSY, %eax ret END(__pthread_spin_trylock) versioned_symbol (libc, __pthread_spin_trylock, pthread_spin_trylock, GLIBC_2_34) #if OTHER_SHLIB_COMPAT (libpthread, GLIBC_2_2, GLIBC_2_34) compat_symbol (libpthread, __pthread_spin_trylock, pthread_spin_trylock, GLIBC_2_2) #endif转换为intel语法:
xor ecx,ecx xchg ecx,[rdi] cmp ecx, 0x1 jnz label xor eax,eax ret label: mov eax,$EBUSY ret逐条指令分析:
xor ecx,ecx: 清零ecx
xchg ecx,[rdi]: 交换ecx和[rdi]的值
cmp ecx, 0x1: 比较ecx和1的值,设置状态寄存器
jnz label: 根据cmp ecx, 0x1返回的结果,如果ecx值为1,说明取到锁了,继续执行下一条指令xor eax,eax,接着执行ret返回; 如果ecx值不为1,说明没有取到锁,跳到label编号的mov eax,$EBUSY,接着执行ret返回
从汇编指令看pthread_spin_lock.S和pthread_spin_trylock.S的区别
从以上分析可以看出: pthread_spin_lock有循环逻辑,因为需要等待锁,pthread_spin_trylock没有循环逻辑,不需要等待锁
5.知识点摘录: Raymond Chen The Intel 80386, part 10: Atomic operations and memory alignment
链接: The Intel 80386, part 10: Atomic operations and memory alignment - The Old New Thing
1.非对齐访问可能导致跨页访问,而操作系统可能不允许访问跨的页,跨页会触发缺页异常,如果缺页异常处理程序会判定该地址非法,会引发段错误
2."读、改、写"默认是非原子的,比如inc [value],如果需要改成原子操作,那么应该写成lock inc [value]
3.lock前缀也保证非对齐的内存访问也是原子的
4.执行交换指令会自动锁定总线,比如x86架构的XCHG指令
查xchg指令在上的描述:
从手册中了解到: xchg可以在寄存器之间、寄存器与内存之间交换数据
如果是在寄存器与内存之间交换数据,那么处理器会自动实施其锁定协议,无论xchg是否带lock前缀,也不管IOPL的值是多少("If a memory operand is referenced, the processor’s locking protocol is automatically implemented for the duration of the exchange operation, regardless of the presence or absence of the LOCK prefix or of the value of the IOPL. ")