① linux 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;
② Linux信号量
信号量是包含一个非负整数型的变量,并且带有两个原子操作wait和signal。Wait还可以被称为down、P或lock,signal还可以被称为up、V、unlock或post。在UNIX的API中(POSIX标准)用的是wait和post。
对于wait操作,如果信号量的非负整形变量S大于0,wait就将其减1,如果S等于0,wait就将调用线程阻塞;对于post操作,如果有线程在信号量上阻塞(此时S等于0),post就会解除对某个等待线程的阻塞,使其从wait中返回,如果没有线程阻塞在信号量上,post就将S加1.
由此可见,S可以被理解为一种资源的数量,信号量即是通过控制这种资源的分配来实现互斥和同步的。如果把S设为1,那么信号量即可使多线程并发运行。另外,信号量不仅允许使用者申请和释放资源,而且还允许使用者创造资源,这就赋予了信号量实现同步的功能。可见信号量的功能要比互斥量丰富许多。
POSIX信号量是一个sem_t类型的变量,但POSIX有两种信号量的实现机制: 无名信号量 和 命名信号量 。无名信号量只可以在共享内存的情况下,比如实现进程中各个线程之间的互斥和同步,因此无名信号量也被称作基于内存的信号量;命名信号量通常用于不共享内存的情况下,比如进程间通信。
同时,在创建信号量时,根据信号量取值的不同,POSIX信号量还可以分为:
下面是POSIX信号量函数接口:
信号量的函数都以sem_开头,线程中使用的基本信号函数有4个,他们都声明在头文件semaphore.h中,该头文件定义了用于信号量操作的sem_t类型:
【sem_init函数】:
该函数用于创建信号量,原型如下:
该函数初始化由sem指向的信号对象,设置它的共享选项,并给它一个初始的整数值。pshared控制信号量的类型,如果其值为0,就表示信号量是当前进程的局部信号量,否则信号量就可以在多个进程间共享,value为sem的初始值。
该函数调用成功返回0,失败返回-1。
【sem_destroy函数】:
该函数用于对用完的信号量进行清理,其原型如下:
成功返回0,失败返回-1。
【sem_wait函数】:
该函数用于以原子操作的方式将信号量的值减1。原子操作就是,如果两个线程企图同时给一个信号量加1或减1,它们之间不会互相干扰。其原型如下:
sem指向的对象是sem_init调用初始化的信号量。调用成功返回0,失败返回-1。
sem_trywait()则是sem_wait()的非阻塞版本,当条件不满足时(信号量为0时),该函数直接返回EAGAIN错误而不会阻塞等待。
sem_timedwait()功能与sem_wait()类似,只是在指定的abs_timeout时间内等待,超过时间则直接返回ETIMEDOUT错误。
【sem_post函数】:
该函数用于以原子操作的方式将信号量的值加1,其原型如下:
与sem_wait一样,sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1。
【sem_getvalue函数】:
该函数返回当前信号量的值,通过restrict输出参数返回。如果当前信号量已经上锁(即同步对象不可用),那么返回值为0,或为负数,其绝对值就是等待该信号量解锁的线程数。
【实例1】:
【实例2】:
之所以称为命名信号量,是因为它有一个名字、一个用户ID、一个组ID和权限。这些是提供给不共享内存的那些进程使用命名信号量的接口。命名信号量的名字是一个遵守路径名构造规则的字符串。
【sem_open函数】:
该函数用于创建或打开一个命名信号量,其原型如下:
参数name是一个标识信号量的字符串。参数oflag用来确定是创建信号量还是连接已有的信号量。
oflag的参数可以为0,O_CREAT或O_EXCL:如果为0,表示打开一个已存在的信号量;如果为O_CREAT,表示如果信号量不存在就创建一个信号量,如果存在则打开被返回,此时mode和value都需要指定;如果为O_CREAT|O_EXCL,表示如果信号量存在则返回错误。
mode参数用于创建信号量时指定信号量的权限位,和open函数一样,包括:S_IRUSR、S_IWUSR、S_IRGRP、S_IWGRP、S_IROTH、S_IWOTH。
value表示创建信号量时,信号量的初始值。
【sem_close函数】:
该函数用于关闭命名信号量:
单个程序可以用sem_close函数关闭命名信号量,但是这样做并不能将信号量从系统中删除,因为命名信号量在单个程序执行之外是具有持久性的。当进程调用_exit、exit、exec或从main返回时,进程打开的命名信号量同样会被关闭。
【sem_unlink函数】:
sem_unlink函数用于在所有进程关闭了命名信号量之后,将信号量从系统中删除:
【信号量操作函数】:
与无名信号量一样,操作信号量的函数如下:
命名信号量是随内核持续的。当命名信号量创建后,即使当前没有进程打开某个信号量,它的值依然保持,直到内核重新自举或调用sem_unlink()删除该信号量。
无名信号量的持续性要根据信号量在内存中的位置确定:
很多时候信号量、互斥量和条件变量都可以在某种应用中使用,那这三者的差异有哪些呢?下面列出了这三者之间的差异:
③ linux 信号量是什么怎么用
Linux信号量(semaphore)是一种互斥机制。即对某个互斥资源的访问会收到信号量的保护,在访问之前需要获得信号量。
在操作完共享资源后,需释放信号量,以便另外的进程来获得资源。获得和释放应该成对出现。
获得信号量集,需要注意的是,获得的是一个集合,而不是一个单一的信号量。
#include
#include
#include
1: int semget(key_t key,int nsems,int semflg);
key:系统根据这个值来获取信号量集。
nsems:此信号集包括几个信号量。
semflg:创建此信号量的属性。 (IPC_CREAT | IPC_EXCL | S_IRUSR | S_IWUSR)
成功则返回该信号量集的ID。
注:
既指定IPC_CREAT又指定IPC_EXCL时,如果系统中该信号量集已经存在,则马上返回。
如果需要获得存在的信号量,则将此参数置0.
2: int semctl(int semid,int senum,int cmd....)
semid:信号量ID。
senum:对信号量集中的第几个信号量进行控制。(从0开始)
cmd:需要进行的操作。(SETVAL是其中的一个)。
根据cmd的不同可能存在第四个参数,cmd=SETVAL时,表示同时信号量可以被获得几次,如第四个参数
num=1表示只能被获得一次,既被信号量保护的资源只能同时被一个程序使用。
该系统调用,是在对信号量初始化时用的。
-3: “3”前面加了"-"表示当需要使用互斥资源时应该做这步。
int semop(int semid,struct sembuf *sem,int num_elements);
struct sembuf {
unsigned short sem_num; //该信号量集中的第几个信号量。
int sem_op;//需要获得还是释放信号量
int sem_flg;//相关动作
};
num_elements:需要对该信号量集中的多少个信号量进行处理。
获得信号量时,将sembuf结构提初始化为:
sem_num = 0; //该信号量集中的首个信号量
sem_op = -1; //获得信号量
sem_flag = IPC_NOWAIT; //如果不能获得信号量,马上返回。
semop(semid,_sem,1);
同理释放信号量时,将sem_op设为1.
以上是对信号量的简单处理
④ Linux下信号量的加减操作问题
void down(struct semaphore *sem); //不可中断
int down_interruptible(struct semaphore *sem);//可中断
int down_killable(struct semaphore *sem);//睡眠的进程可以因为受到致命信号而被唤醒,中断获取信号量的操作。
int down_trylock(struct semaphore *sem);//试图获取信号量,若无法获得则直接返回1而不睡眠。返回0则 表示获取到了信号量
int down_timeout(struct semaphore *sem,long jiffies);//表示睡眠时间是有限制的,如果在jiffies指明的时间到期时仍然无法获得信号量,则将返回错误码。