pthread互斥信号量使用总结


glibc提供的pthread互斥信号量可以用在进程内部,也可以用在进程间,可以在初始化时通过pthread_mutexattr_setpshared接口设置该信号量属性,表示是进程内还是进程间。进程内的使用较为简单,本文的总结主要是针对进程间的,进程内的也可以参考,其代码实现原理是类似的。

一、实现原理
pthread mutex的实现是非常轻量级的,采用原子操作+futex系统调用。
在没有竞争的情况下,即锁空闲时,任务获取信号量只需要通过原子操作锁的状态值,把值置为占有,再记录其他一些俄信息(owner,计数,如果使能回收功能则串入任务的信号量回收链表等),然后就返回了。
如果在获取锁时发现被占用了,如果调用者需要睡眠等待,这时候会触发futex系统调用,由内核继续处理,内核会让调用任务睡眠,并在适当时候唤醒(超时或者锁状态为可用)。
占用锁的任务释放锁时,如果没有任务等待这把锁,只需要把锁状态置为空闲即可。如果发现有其他任务在等待此锁,则触发futex系统调用,由内核唤醒等待任务。

由此可见,在没有竞争的情况下,mutex只需要在用户态操作锁状态值,无须陷入内核,是非常高效的。

获取到锁的任务没有陷入内核,那么当锁支持优先级翻转时,高优先级任务等待这把锁,正常处理必须提升占用锁的任务优先级。内核又是怎么知道是哪个任务占用了锁呢?实现上,复用了锁的状态值,该值在空闲态时为0,非空闲态则保存了锁的持有者ID,即PID,内核态通过PID就知道是那个任务了。

二、内核对锁的管理
内核维护了一个hash链表,每把锁都被插入到hash链表中去,hash值的计算如下(参考get_futex_key):1,如果是进程内的锁,则通
过锁的虚拟地址+任务mm指针值+锁在页内偏移;2,如果是进程间的锁,则会获取锁虚拟地址对应物理地址的page描述符,由page描述符构造
hash值。
这样计算的原因是进程间的锁在各个进程内虚拟地址可能是不同的,但都映射到同一个物理地址,对应同一个page描述符。所以,内
核使用它来定位是否同一个锁。

这里对进程间互斥锁计算hash值的方法,给进程间共享锁的使用设置了一个隐患条件。下面描述这个问题。


三、进程间互斥信号量的使用限制:必须在系统管理的内存上定义mutex结构,而不能在用户reserved的共享内存上定义mutex结构。
锁要实现进程间互斥,必须各个进程都能看到这个锁,因此,锁结构必须放在共享内存上。
获取系统的共享内存通过System V的API接口创建:shmget, shmat,shmdt。但是shmget的参数需要一个id值,各进程映射同一块共享内存需要同样的ID值。如果各个进程需要共享的共享内存比较多,如几千上万个,ID值如果管理?shmget的man帮助和一些示例代码给出的是通过ftok函数把一个文件转为ID值(实际就是把文件对应的INODE转为ID值),但实际应用中,如果需要的共享内存个数较多,难道创建成千上万个文件来使用?而且怎么保证文件在进程的生命周期内不会被删除或者重建?
当时开发的系统还存在另外一种共享内存,就是我们通过remap_pfn_range实现的,自己管理了这块内存的申请释放。申请接口参数为字符串,相同的字符串表示同一块内存。因此,倾向于使用自己管理的共享内存存放mutex结构。但在使用中,发现这种方法达不到互斥的效果。为什么?
原因是自己管理的共享内存在内核是通过remap_pfn_range实现的,内核会把这块内存置为reserved,表示非内核管理,获取锁的HASH值时,查找不到page结构,返回失败了。最后的解决方法还是通过shmget申请共享内存,但不是通过ftok获取ID,而是通过字符串转为ID值并处理冲突。


四、进程间互斥信号量回收问题。
假设进程P1获取了进程间信号量,异常退出了,还没有释放信号量,这时候其他进程想来获取信号量,能获取的到吗?
或者进程P1获取了信号量后,其他进程获取不到进入了睡眠后,P1异常退出了,谁来负责唤醒睡眠的进程?
好在系统设计上已经考虑了这一点。
只要在信号量初始化时调用pthread_mutexattr_setrobust_np设置支持信号量回收机制,然后,在获取信号量时,如果原来占有信号量的进程退出了,系统将会返回EOWNERDEAD,判断是这个返回值后,调用pthread_mutex_consistent_np完成信号量owner的切换工作即可。
其原理如下:
任务创建时,会注册一个robust list(用户态链表)到内核的任务控制块TCB中期,获取了信号量时,会把信号量挂入链表。进程复位时,内核会遍历此链表(内核必须非常小心,因为此时的链表信息可能不可靠了,可不能影响到内核),置上ownerdead的标志到锁状态,并唤醒等待在此信号量链表上的进程。

五、pthread接口使用说明
pthread_mutex_init:    根据指定的属性初始化一个mutex,状态为空闲。
pthread_mutex_destroy: 删除一个mutex
pthread_mutex_lock/trylock/timedlock/unlock: 获取锁、释放锁。没有竞争关系的情况下在用户态只需要置下锁的状态值即返回了,无须陷入内核。但是timedlock的入参为超时时间,一般需要调用系统API获取,会导致陷入内核,性能较差,实现上,可先trylock,失败了再timedlock。

pthread_mutexattr_init:配置初始化
pthread_mutexattr_destroy:删除配置初始化接口申请的资源
pthread_mutexattr_setpshared:设置mutex是否进程间共享
pthread_mutexattr_settype:设置类型,如递归调用,错误检测等。
pthread_mutexattr_setprotocol:设置是否支持优先级翻转
pthread_mutexattr_setprioceiling:设置获取信号量的任务运行在最高优先级。
每个set接口都有对应的get接口。


六、pthread结构变量说明


struct __pthread_mutex_s
{
int __lock; ----31bit:这个锁是否有等待者;30bit:这个锁的owner是否已经挂掉了。其他bit位:0锁状态空闲,非0为持有锁的任务PID;
unsigned int __count; ----获取锁的次数,支持嵌套调用,每次获取到锁值加1,释放减1。
int __owner; ----锁的owner
unsigned int __nusers; ----使用锁的任务个数,通常为1(被占用)或0(空闲)
int __kind;----锁的属性,如递归调用,优先级翻转等。
int __spins; ----SMP下,尝试获取锁的次数,尽量不进入内核。
__pthread_list_t __list; ----把锁插入回收链表,如果支持回收功能,每次获取锁时要插入任务控制块的回收链表。
}__data;

相关内容