linux 信号量是一种IPC(Inter-Process Communication)进程间通信,它是System V semagpore ,是一种计数器;代笔了进程对资源的占用和释放;它的实现机制稍微复杂,主要体现在以下几点:
(1)信号量的数据结构不是单个非负值;而是一个或多个信号量值的集合。也就是说,操作信号量时,你操作的首先是一个集合,集合的实现方式是数组,你要先通过集合才能操作里面的信号量值。集合中信号量值的个数可以在新建时确定。
(2)信号量和创建和赋值是分开的,也就是说不能原子性的创建信号量并对其赋值。信号量的创建函数是semget 函数,而赋值是通过semctl函数实现的。
(3)进程终止时并不会释放分配给它的信号量。
1 内核为每一个信号量值集合建立如下数据结构(包含于文件<sys/sem.h>):
struct semid_ds {
struct ipc_perm sem_perm; /* Ownership and permissions */
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Creation time/time of last
modification via semctl() */
unsigned long sem_nsems; /* No. of semaphores in set */
};
需要注意的是sem_nsems 是代表集合中信号量值的个数,每个信号量值的下标从0开始,到sem_nsems-1结束。ipc_perm数据结构如下:
struct ipc_perm {
key_t __key; /* Key supplied to semget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions */
unsigned short __seq; /* Sequence number */
};
此结构是描述权限和所有者的数据结构。
信号量值的数据结构如下,此结构没有名字
struct {
unsigned short semval; /* semaphore value */
unsigned short semzcnt; /* # waiting for zero */
unsigned short semncnt; /* # waiting for increase */
pid_t sempid; /* PID of process that last
};
需要注意的是semval 才是信号量的值,代表着资源的个数。我们最终需要的还是semval这个参数。
2 主要函数如下:
(1)
key_t ftok(const char *pathname, int proj_id);
此函数返回一个System V IPC的键(key_t,长整型),用于下面几个函数。此函数要求pathname 的文件已存在且可以访问,此函数产生键时使用proj_id的低8位。使用IPC 结构需使用一个键。通常情况下,不同的文件名产生不同的键值。函数执行成功后返回key_t,出错时则返回-1,并赋值errno。
(2)
int semget(key_t key, int nsems, int semflg);
此函数获得信号量值集合的标识符,它既可以用来创建信号量值的集合,也可以获得一个已存在的信号量值的集合。参数key是用第一个函数产生的键,nsems 是代表集合中信号量值的个数;semflg参数代表着信号量值集合产生的权限与方式,可用的几个值有IPC_CREAT,IPC_EXCL,作用类似open 函数的IPC_CREAT,IPC_EXCL。函数执行成功后返回一个非负的描述符,出错时则返回-1,并赋值errno。
首先在工作目录 touch mykey mykey2
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void){
int semid1,semid2;
int nsems=1;
key_t key1,key2;
key1=ftok("mykey",97);
if(key1==-1)
{
perror("ftok");
exit(EXIT_FAILURE);
}
semid1=semget(key1,nsems,0600|IPC_CREAT);
if(semid1==-1)
{
perror("semget");
exit(EXIT_FAILURE);
}
printf("ID1=%d\n",semid1);
key2=ftok("mykey2",65);
if(key2==-1)
{
perror("ftok");
exit(EXIT_FAILURE);
}
nsems++;
semid2=semget(key2,nsems,0600|IPC_CREAT);
if(semid2==-1)
{
perror("semget");
exit(EXIT_FAILURE);
}
printf("ID2=%d\n",semid2);
exit(EXIT_SUCCESS);
}
输出结果如下:
其中semid 由系统决定,从结果中可以看出,建立了2个信号量集合,第一个集合有1个信号量值,第二个集合有2个信号量值。
(3)
int semctl(int semid, int semnum, int cmd, ...);
信号量值操作函数,可以设置信号量的初值,也可以获得现有值。第一个参数代表的是信号量值集合描述符,第二个参数semnum是此描述符中信号量值的下标,具体来说,如果semid有一个信号量值,则semnum只能取0值,代表下标为0,如果semid有2个信号量值,则semnum可取0和1;第三个参数cmd有多种,分别是:IPC_STAT,IPC_SET,IPC_RMID,IPC_INFO(linux-specific),SEM_INFO(linux-specific),SEM_STAT(linux-specific),SEM_STAT_ANY(Linux-specific,since linux 4.17),GET_ALL,GET_NCNT,GET_PID,GET_VAL,GET_ZCNT,SET_ALL,SET_VAL。有第四个可选的参数,union semun arg *(结构体如下所示);需要配合第三个参数使用。此函数执行成功时依据cmd的不同有不同的返回值,出错时返回-1,并赋值errno。
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
已刚才创建的第一个信号值集合为例,
int main(void){
union semun se;
se.val=3;
int semid=1;
int val;
if((val=semctl(semid,0,GETVAL))==-1)
{
perror("semctl");
exit(EXIT_FAILURE);
}
else
printf("the initial val is %d\n",val);
if((val=semctl(semid,0,SETVAL,se))==-1)
{
perror("semctl");
exit(EXIT_FAILURE);
}
if((val=semctl(semid,0,GETVAL))==-1)
{
perror("semctl");
exit(EXIT_FAILURE);
}
else
printf("after set, val is %d\n",val);
exit(EXIT_SUCCESS);
}
结果如下:
从结果来看,未赋值前,初始值为0,赋值后是3。
(4)
int semop(int semid, struct sembuf *sops, size_t nsops);
信号量操作函数,主要增减信号量的值,并由进程作进一步处理,semid是信号量值集合的描述符,sops 是待操作的数据,nsops是指针sops需要操作的数目。sembuf的结构如下:
struct sembuf{
unsigned short sem_num ; /*semaphore number ; 0,1,2...semnum-1 */
short sem_op; /*semaphore operation ;0,negative,positive*/
short sem_flag; /* operation flags ;IPC_NOWAIT,IPC_UNDO*/
};
其中sem_num是信号量集合中的下标,取值从0开始,到semnum-1结束。当sem_num <0或sem_num >=semnum时,会产生File too large 的错误。此函数成功时返回0,失败时返回-1,并赋值errno。以第二个信号量集合为例
int main(void){
union semun se;
se.val=3;
int semid=2;
int val;
struct sembuf sops[2];
sops[0].sem_num=0;
sops[0].sem_op=1;
sops[0].sem_flg=0;
sops[1].sem_num=1;
sops[1].sem_op=-1;
sops[1].sem_flg=0;
for(int i=0;i<2;i++){
if((val=semctl(semid,i,SETVAL,se))==-1)
{
perror("semctl");
exit(EXIT_FAILURE);
}
}
if((val=semop(semid,sops,2))==-1)
{
perror("semop");
exit(EXIT_FAILURE);
}
for(int i=0;i<2;i++){
if((val=semctl(semid,i,GETVAL))==-1)
{
perror("semctl");
exit(EXIT_FAILURE);
}
else
printf("after semop,%dth val is %d\n",i,val);
}
//删除2个信号量值集合
for(int i=2;i!=0;i--){
if((val=semctl(i,i,IPC_RMID))==-1){
{
perror("semctl");
exit(EXIT_FAILURE);
}
}
exit(EXIT_SUCCESS);
}
输出结果如下:
从输出结果来看,0th 的信号量值的semval加了1,1th的信号量值的semval减了1;关于sem_op的取值有三种情况。
(1)sem_op 取值为正,则将当前信号量值的semval 加上sem_op的值,代表着进程释放了sem_op个资源,此操作通常会成功,而且它不需要调用进程等待。如上述例子的0th 的信号量值。
(2)sem_op取值为负,则说明调用进程需要获得|sem_op|(sem_op的绝对值)个资源,如果semval > = | sem_op|,操作会立即执行,执行结束后smeval=semval- |sem_op|;如果semval < |sem_op|,此时若sem_flag 指定了IPC_NOWAIT,则操作出错并立即返回errno,同时sops的数据均不被执行;若sem_flag 未指定IPC_NOWAIT,进程将信号量值数据结构中semncnt加1,代表等待当前semval增加的进程多了一个,然后调用进程进入休眠,直至下列条件发生,进程被唤醒:
(a)semval >= | sem_op|,有进程释放了资源,函数成功执行,同时semncnt 减1。如果sem_flag指定了SEM_UNDO,则调用进程结束时semval 将恢复原来的值(增加之后的值)。
(b)信号量值被删除,函数出错返回,并赋值errno 为EIDRM
(c)调用进程捕捉了一个信号,semncnt减1,函数出错,并赋值errno 为EINTR。
(3)sem_op取值为0,这是一个等待0的操作,如果当前semval为0,函数立即返回,如果信号值不是0,此时若sem_flag指定了IPC_NOWAIT,则函数出错,并赋值errno 为EAGAIN;若sem_flag未指定IPC_NOWAIT,semncnt 加1,则调用进程进入休眠,直至下列条件发生,进程被唤醒:
(a)信号量值变为0,semncnt减1,同时进程唤醒,函数成功返回
(b)信号量值被删除,函数出错返回,并赋值errno 为EIDRM
(c)调用进程捕捉了一个信号,semncnt减1,函数出错,并赋值errno 为EINTR。
说明下,SEM_UNDO 的实现机制是系统为每个进程的每个信号量值维持一个叫做“信号量值调整值”的整形变量semadj,如果是直接操作,如SETVAL,semadj被清除为0,如果指定SEM_UNDO,函数更改数据前,需要调整semadj的值,调整原则是semadj=-sem_op,即semadj 是当前sem_op的负值,调用进程结束时,调整后的semval+semadj为最后的值。
参考资料:
(1)https://man7.org/linux/man-pages/man2/semget.2.html
(2)https://man7.org/linux/man-pages/man2/semop.2.html
(3)https://man7.org/linux/man-pages/man2/semctl.2.html
(4)https://man7.org/linux/man-pages/man3/ftok.3.html