pthread_rwlock系列函数是pthread库的读写锁函数。随着版本的不同,它的实现也不同。
本篇的组织是:
- 前置知识
- GCC的扩展内联汇编
- Futex
- 2.17和2.30两个版本的lowlevellock的实现
- 2.17和2.30两个版本的pthread_rwlock的实现
我们的结论是:
旧版GLIBC使用lowlevellock实现pthread_rwlock;
新版GLIBC使用原子操作实现pthread_rwlock。
因此具有不同的ABI
扩展形式的GCC汇编介绍
在扩展形式的GCC汇编中,可以访问C语言中定义的变量,或者跳转到C语言定义的标签处。此外,GCC扩展汇编中可以用:
去delimit各个operand parameter。
对于要访问的寄存器,并不一定要要显式指明,也可以留给GCC自己去选择,这可能让GCC更好去优化代码。
GCC扩展内联汇编格式如下:
1 | asm asm-qualifiers ( AssemblerTemplate |
OutputOperands、InputOperands等列表里面的项目会从0开始标号,并且可以使用%0
、%1
的方法去表示,例如对下面的代码而言,%0
是old
,%1
是*Base
,%2
是Offset
。
1 | bool old; |
我们看看这个语法中的几个组成部分:
- AssemblerTemplate
是ASM代码的模板。 - OutputOperands
一个逗号分隔的列表,表示所有被这段代码修改的变量。
其中+rm
、=r
等表示Constraints,将在稍后介绍。
在括号中的应该是一个C语言左值。 - InputOperands
表示所有被这段代码访问的表达式。 - Clobbers
除了OutputOperands之外,还被这段代码修改的内容,例如cc
:eflags寄存器memory
:内存
例如我们常见的asm volatile("mfence" ::: "memory")
用法。
- GotoLabels
这段代码可能跳转到的C语言的标签。
Constraints
Simple Constraints
Simple Constraints都是字母或数字,表示允许其中的某一种类的操作。
r
表示允许寄存器操作;m
表示允许内存操作。这两个是很泛化的内存和寄存器操作。a
表示地址寄存器,f
表示浮点寄存器,d
表示数据寄存器。这些都对特定的处理器适用。- 数字表示matching constraint
Modifiers
Modifiers用来描述operands的读写性。
+
表示这个值在操作中会被读写。=
表示这个值只会被写。&
%
Futex介绍
Futex是一个系统调用。在使用Futex时,大部分的工作是在用户态完成的,只有当程序很有可能阻塞较长时间时,才会真的去使用这个系统调用。
Futex值的加载、Futex值和传入的expected value的比较以及实际的阻塞都是原子的。并且,不同线程对同一个Futex值的并发操作是Total order的。
Futex调用如下所示
1 | long futex(uint32_t *uaddr, int futex_op, uint32_t val, |
它的实现类似下面
1 | static int |
其中uaddr
指向一个Futex值,它在32位或者64位系统上都是一个32位的值。futex_op
包含两部分,要执行的操作(operation),以及操作的option。
option包含下面几个常量:
FUTEX_PRIVATE_FLAG
:128
【Since 2.6.22】
这个option表示这个Futex是否是被跨进程共享的。指定这个flag能够允许内核做一系列优化。
对于所有要执行的操作,提供了带_PRIVATE
版本后缀的宏,其作用相当于or上了这个FUTEX_PRIVATE_FLAG
。
因为这个宏在22版本之后才有,所以我们看到nptl的一些实现上会使用__ASSUME_PRIVATE_FUTEX
判定Linux是否支持Private Futex这个功能。我们可以认为在22版本之后的Linux下这个宏始终是1。1
2
3
4
5// kernel-features.h
/* Support for private futexes was added in 2.6.22. */FUTEX_CLOCK_REALTIME
:256
【Since 2.6.28】
这个option指定timeout的计算方式
操作部分包含下面几个常量:
FUTEX_WAIT
:0
这个操作检查uaddr
指向的值是否等于val
。
如果是,那么就会睡在这个Futex上面,直到另一个FUTEXT_WAKE
被调用。这个读取、比较和开始睡眠的过程是原子的。
如果不是,那么就会立即返回EAGAIN
。
之所以要在这个操作里面再比较一次val
,而不是直接加锁,其原因是为了防止丢失wake up事件。例如,如果在本线程准备阻塞之后,另一个线程修改了Futex的值,并且遵循了下面的时序:- 对方线程先修改Futex值
- 对方线程执行
FUTEX_WAKE
- 本线程执行
FUTEX_WAIT
那么在执行FUTEX_WAIT
时同步检查val
的方案就能发现Futex值变化了,并且不进入睡眠。
FUTEX_WAKE
:1
这个操作唤醒最多val
个睡在这个Futex上面的线程。指定INT_MAX
表示唤起所有线程。FUTEX_FD
:2
【已被移除】
表示为当前Futex创建一个fd,当这个Futex发生FUTEX_WAKE
调用时,这个fd被select、poll和epoll等可读。FUTEX_REQUEUE
:3
是FUTEX_CMP_REQUEUE
的不带check的简化版。FUTEX_CMP_REQUEUE
:4
TODOFUTEX_WAKE_OP
:5
这个操作用来支持在用户态中对多个Futex同时操作的情形。例如pthread_cond_signal
的实现中需要用到两个Futex,一个用来实现Mutex,一个用来管理和CV相连的Wait queue。通过这个操作可以避免一些contention或者上下文切换。
这个op的主要过程是:- 保存uaddr2的旧值到oldval,并且对uaddr2执行操作。这个操作是原子的。
- 唤醒uaddr上的val个线程。
- 如果oldval的值满足一定条件,则唤起val2个线程。
FUTEX_LOCK_PI
:6
这里的PI表示”priority inheritance”,用来处理所谓的优先级倒置问题。
它的解决方案是,当一个高优先级的任务被一个低优先级任务持有的锁阻塞时,会暂时提高这个低优先级任务的优先级为高优先级,这样它就不会被对方抢占。
注意,这个”priority inheritance”的实现也必须是可传递的。也就是当这个低优先级任务在等待另一个中等优先级任务的锁的时候,所有的任务都要被提升为高优先级。FUTEX_UNLOCK_PI
:7FUTEX_TRYLOCK_PI
:8FUTEX_WAIT_BITSET
:9FUTEX_WAKE_BITSET
:10FUTEX_WAIT_REQUEUE_PI
:11FUTEX_CMP_REQUEUE_PI
:12
下面我们讨论futex调用的返回值。首先如果发生错误,就按照通常规矩,返回-1,并且设置errno。对于成功的情况,需要根据op讨论:
FUTEX_WAIT
返回0表示caller被唤醒了。需要注意的是,这个0有可能是spurious wake-up,所以在这之后,仍然需要根据Futex值的具体值去判断是否继续block。我认为这也是为什么__lll_lock_wait_private
的实现中有一个while循环的原因。FUTEX_WAKE
返回唤醒了多少个waiter。
lll的GLIBC2.17版本
在这个版本中,lll的实现是FUTEX。
我们只看PRIVATE的部分
1 | // nptl/sysdeps/unix/sysv/linux/x86_64 |
查看__lll_lock_wait_private
函数的调用。下面两个lll_futex_wait
的含义是如果futex
的值是2,那么就会进行等待。
那么为什么要写成两个呢?如果我们已经观察到futex
是2,即已经被加锁了,那么我们就直接去wait了,否则我们可以尝试加锁。
1 | void |
但是加锁的过程也未必成功,可能有两个线程同时过了上面的检验,因此我们还需要进行一次判断。加锁操作是由atomic_exchange_acq
调用,它会尝试将futex设置为2,并且返回原值。在while循环中,我们会判断返回的原值是否为0,如果不是0,那么说明这个锁已经被另一个线程加了,所以我们直接wait到这个锁被释放,然后在重新while一次尝试加锁。
1 | ... |
辅助函数解释
lll_futex_wait
查看lll_futex_wait
函数
1 |
|
解释一下:
__status
这个变量在一个地址寄存器上,是这个汇编段的Output。SYS_futex
TODOfutex
__lll_private_flag
根据传入的两个参数决定是否产生private的futex。需要注意libc和libdl中的所有的futex都应该是private的。
检查这个函数的实现,有一句很神奇的代码。这个代码的结果是,如果private
设置了FUTEX_PRIVATE_FLAG
位,那么就将这个位清空。要解释清楚这个问题,需要结合后面rwlock的初始化过程来看。1
2
(((fl) | FUTEX_PRIVATE_FLAG) ^ (private))cc
表示eflags寄存器也会被修改。
1 | ... |
atomic_exchange_acq
lll的GLIBC2.30版本
1 | // lowlevellock.h |
rwlock的GLIBC2.17实现
INIT
1 | // pthread_rwlock_init.c |
__ASSUME_PRIVATE_FUTEX
这个宏表示是否支持Private Futex,在2.6.22之前是不支持的,因此我们看到在对应分支没有使用FUTEX_PRIVATE_FLAG
这个宏,而是借助了THREAD_GETMEM
的实现。FUTEX_PRIVATE_FLAG
的值表示这个Futex是否是Private的,在之前已经介绍过。
__SHARED
这个字段我觉得有点奇怪了,我不是很明白为啥Futex用Private,而pthread用Shared,正好这两个值是相反的。源码在注释里面给了下面这个转换表,容易看出来在不支持FUTEX_PRIVATE_FLAG
的时候就都是0。
1 | | pshared | result |
不管怎样吧,下面我们设置_shared
的值,正好和Private相反。也就是如果我们希望我们的rwlock是Private的,那么我们就清空FUTEX_PRIVATE_FLAG
位;如果我们希望它是Shared,那么就设置FUTEX_PRIVATE_FLAG
。这样我们xor一下就能得到Private的实际值,这实际上对应了我们先前在lll_futex_timed_wait
这个函数上的困惑。
1 | ... |
由于
1 | ... |
LOCK
1 | // pthread_rwlock_wrlock.c |
这个LIBC_PROBE
定义在include/stap-probe.h
里面,实际上是一个systemtap静态检查点的功能。
1 | ... |
下面调用lll_lock
去加lowlevellock锁
1 | ... |
辅助函数详解
THREAD_GETMEM
1 | /* Read member of the thread descriptor directly. */ |
rwlock的GLIBC2.30实现
INIT
1 | // pthread_rwlock_init.c |
strong_alias
是C的一个别名机制,定义pthread_rwlock_init
是__pthread_rwlock_init
的别名。别名包括strong和weak的。
1 | strong_alias (__pthread_rwlock_init, pthread_rwlock_init) |
它的实现在libc-symbols.h
中。其中,__typeof (name) aliasname
定义了一个aliasname
,它的类型是name
的类型。
1 | /* Define ALIASNAME as a strong alias for NAME. */ |
LOCK
1 | // pthread_rwlock_wrlock.c |
__pthread_rwlock_wrlock_full
使用原子操作实现的
1 | // pthread_rwlock_common.c |
Reference
- https://man7.org/linux/man-pages/man2/futex.2.html
附注一下,Linux的man page后面的数字序号有下面的含义:- 普通命令
- 系统调用
- 库函数
- 特殊文件,也就是/dev下的各种设备文件
- 文件的格式
- 游戏留的
- 附件,或者一些全局变量
- 是root专用的命令
- https://man7.org/linux/man-pages/man7/futex.7.html
- https://www.cnblogs.com/pslydff/p/7041444.html
- https://gohalo.me/post/program-c-gdb-deadlock-analyze-introduce.html