ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OS85.【Linux】分析glibc-2.43 x86_64版本的自旋锁的底层原理

OS85.【Linux】分析glibc-2.43 x86_64版本的自旋锁的底层原理 目录1.知识回顾: glibc-2.43 i386版本的自旋锁2.glibc-2.43 x86_64版本的自旋锁的参考代码3.前置知识几个调用约定lock前缀、pthread_spinlock_t的类型4.分析pthread_spin_init.cpthread_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 alignment1.知识回顾: 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.S3.前置知识几个调用约定参见127.【C语言】面试题:函数的调用约定文章复习x86_64(64位)和i386(32位)下的linux的函数调用约定lock前缀、pthread_spinlock_t的类型两个都参见OS85.【Linux】各种锁(重点分析glibc i386版本的自旋锁的底层原理)文章4.分析pthread_spin_init.cx86_64版的pthread_spin_init.c和之前文章讲的i386版的一样pthread_spin_init.c就一个include语句:#include sysdeps/i386/nptl/pthread_spin_init.csysdeps/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 doesnt 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 [raxrax0x0] label: pause cmp [rdi],eax jng label lock dec dword [rdi] jnz label ret逐条指令分析:xor eax,eax: 表示pthread_spin_lock函数总是返回0lock dec dword [rdi]: 锁值原子的-1jnz label:如果锁值变成0,说明线程成功申请锁,那么执行ret返回,nop dword [raxrax0x0]是空操作,用于对齐,不会执行如果锁值没变成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适用于有符号数的比较,要求: ZF1(两个数相同,相减的结果为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: 清零ecxxchg 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 Thing1.非对齐访问可能导致跨页访问,而操作系统可能不允许访问跨的页,跨页会触发缺页异常,如果缺页异常处理程序会判定该地址非法,会引发段错误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. )
返回列表