学过操作系统的话,对信号量应该熟悉。当多个进程同时访问计算机资源时,我们需要考虑进程同步问题,以确保任一时刻只有一个进程可以拥有独占式的访问。通常程序对共享资源访问的代码只是很短的一段,但就是这很短的一段代码引发了进程同步问题,我们将这段代码称为临界区。进程同步就是确保任一时刻只有一个进程进入临界区。信号量原语是操作系统为进程同步提供的一种手段。
一、什么是信号量
信号量是一种特殊的变量,他只能取自然数值,并且只支持两种操作,即p、v操作。假设我们当前有信号量sv。
p操作:访问信号量sv,若其值大于0,则sv减1,进程进入临界区。若其值为0,则进程挂起。
v操作:进程退出临界区,sv加1,若有进程阻塞在信号量上则将其唤醒。
信号量的取值可以是任何自然数。但最常用的、最简单的信号量是二进制信号量,它的值只能取0和1。这里我们只讨论二进制信号量
我们需要注意的是不能使用一个普通变量来模拟二进制信号量,因为任何高级语言都没有一个原子操作能完成下面两步:判断信号量的值和改变信号量的值。当p、v操作不是原子操作的时候是致命的,当A进程进行P操作时,在判断信号量大于0后还未改变信号量的值时失去CPU,然后B进程占有CPU,它也对同一个信号量进行p操作,满足进入临界区的条件,当它对共享资源访问到一半失去cpu后,A又占有CPU,此时A进程也在自己的临界区内。这样就会出现两个进程同时操作共享资源的情况,信号量并没有保证进程的同步
二、信号量的系统调用
Linux操作系统为我们提供了可以实现原子操作的信号量,即信号量原语。定义在sys/sem.h
头文件中,主要包含三个系统条用:semget、semop、semctl。他们都被设计为操作一组信号量即信号量集,而不是单个的信号量。
1.semget系统调用
其用来创建一个新的信号量集,或者获取一个已经存在的信号量集。其函数原型如下:
int semget(key_t key, int nsems, int semflg);
其中key参数是一个键值,用来标识全局唯一的信号量集,就像文件名全局唯一标识一个文件一样。它可以是一个特殊的健值IPC_PRIVATE(其值为0),这样无论该信号量是否已经存在,semget都将创建一个新的信号量,使用该健值并不是像它名字那样表示私有,其他进程,尤其是子进程,也可以访问这个信号量。
semflg参数用来设置信号量的访问权限,其可与IPC_CREAT和IPC_EXCL进行按位或运算。与IPC_CREAT或操作时,表明当不存在信号量时,则创建信号量,信号量存在时,则返回信号量的标识符(不是健值)。IPC_EXCL与IPC_CREAT配套使用,表示当信号量不存在时,创建信号量。信号量存在时,发生错误,返回-1,errno设置为EEXIST。该参数和open函数的mode参数差不多
nsems参数:指定要创建/获取的信号量集中信号量的数目。若是创建信号量集,则该值必须被指定。若是获取信号量集,则值可以设为0。
semget成功返回一个正整数值,它是信号量集的标识符;失败返回-1,并设置errno。
如果semget用于创建信号量集,则与之关联的内核数据结构体semid_ds,将被创建并初始化。semid_ds结构体定义如下:
#include<sys/sem.h>
/*该结构体用于描述IPC对象(信号量、共享内存、消息队列)的权限*/
struct ipc_perm
{
Key_t Key; /*健值*/
uid_t uid; /*所有者的有效用户ID*/
gid_t gid; /*所有者的有效组ID*/
uid_t cuid; /*创建者的有效用户ID*/
gid_t cgid; /*创建者的有效组ID*/
mode_t mode; /*访问权限*/
/*省略其他填充字段*/
}
struct semid_ds
{
struct ipc_perm sem_perm; /*信号量集的操作权限*/
unsigned long int sem_nsems; /*该信号量集中的信号量数目*/
time_t sem_otime; /*最后一次调用semop的时间*/
time_t sem_ctime; /*最后一次调用semctl的时间*/
/*省略其他填充字段*/
}
semget对semid_ds结构体的初始化包括:
将sem_perm.cuid和sem_perm.uid设置为调用进程的有效用户ID。
将sem_perm.cgid和sem_perm.gid设置为调用进程的有效组ID。
将sem_perm.mode设置为semflg。
将sem_nsems设置为nsems。
将sem_otime设置为0。
将sem_ctime设置为当前系统时间。
2.semop系统调用
semop系统调用改变信号量的值,即进行p、v操作。下面是与信号量相关的一些重要的内核变量,semop对信号量的操作实际就是对这些变量的操作。
unsigned short semval; /*信号量的值*/
unsigned short semzcnt /*等待信号量值为0的进程数*/
unsigned short semncnt /*等到信号量值增加的进程数*/
pid_t sempid; /*最后一次执行semop操作的进程ID*/
semop函数原型如下:
int semop(int semid, struct sembuf *sops, size_t nsops);
semid为semget调用返回的信号量集的标识符。
sops参数指向一个struct sembuf类型的结构体数组,其内容如下:
struct sembuf
{
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
}
sem_num的值为信号量在信号量集中的位置,0表示信号量集中的第一个信号量。
sem_op成员指定操作类型,其值可为正整数、0、负整数。
sem_op大于0时,semop将信号量的值semval增加sem_op(v操作)。该操作需要对信号量集拥有写权限。
sem_op等于0时,表示一个等待"0"操作,该操作要求对信号量集拥有读操作。若此时信号量的值为0,则调用成功返回。若此时信号量不为0,若sem_flg设置为IPC_NOWAIT则失败返回-1,并设置errno为EAGAIN。若sem_flg没设置IPC_NOWAIT,则该信号量的semzcnt加1,进程进入阻塞状态直到下列三种条件之一发生:信号量的值semval变为0,此时系统将该信号量的semzcnt减1;信号量集被移除,此时semop系统调用返回错误,并将errno设为EIDRM;semop系统调用被信号中断,此时semop系统调用返回错误,并将errno设置为EINTR,同时系统将该信号量的semzcnt减1。
sem_op小于0时,表示对信号量的值进行减操作,即期望获得信号量(p操作),该操作要求对信号量拥有写权限,若信号量的值semval大于或等于sem_op的绝对值时,则semop操作成功,调用进程立即获得信号量,并且系统将该信号量的值减去sem_op的绝对值。若semval小于sem_op,则semop系统调用失败返回并将errno设置为EAGAIN或者阻塞进程以等待信号量可用并且semncnt加1(看是否设置IPC_NOWAIT)。若进程进程被阻塞,则只要满足下列三个条件之一便可转换为就绪态:semval大于或等于sem_op的绝对值,系统将semncnt的值减1,semval减去sem_op的绝对值;信号量集被进程移除,此时semop失败返回,并设置errno为EIDRM;若semop调用被信号中断,则semop返回失败并设置errno为EINTR,同时系统将该信号量的semncnt减1。
sem_flg成员的可选值为IPC_NOWAIT和SEM_UNDO。当设置为IPC_NOWAIT时,表示无论semop调用是否成功都立即返回,这类似于非阻塞I/O操作。SEM_UNDO表示,系统跟踪进程对信号量的修改情况(更新进程的semadj变量)和当进程退出时取消正在进行的semop操作。
第三个参数指定sops数组中的元素个数。semop对数组sops中的每个成员按照数组顺序依次执行操作,并且该过程为原子操作,以避免别的进程在同一时间按照不同的顺序对该信号集中的信号量执行semop操作导致竟态条件。
semop成功返回0,失败返回-1并设置errno。失败的时候,sops数组中指定的所有操作都不执行。
3.semctl系统调用
semctl系统调用允许调用者对信号量进行直接控制,其函数原型如下:
int semctl(int semid, int semnum, int cmd, ...);
semid是有semget调用返回的信号量集标识符,semnum参数指定被操作信号量在信号量集中的位置。cmd参数指定要执行的命令。有的命令需要传入第四个参数。第四个参数由用户自己定义,但sys/sem.h文件给出了它的推荐格式,具体如下:
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) */
};
struct seminfo {
int semmap; /* Number of entries in semaphore
map; unused within kernel */
int semmni; /* Maximum number of semaphore sets */
int semmns; /* Maximum number of semaphores in all
semaphore sets */
int semmnu; /* System-wide maximum number of undo
structures; unused within kernel */
int semmsl; /* Maximum number of semaphores in a
set */
int semopm; /* Maximum number of operations for
semop(2) */
int semume; /* Maximum number of undo entries per
process; unused within kernel */
int semusz; /* Size of struct sem_undo */
int semvmx; /* Maximum semaphore value */
int semaem; /* Max. value that can be recorded for
semaphore adjustment (SEM_UNDO) */
};
semctl支持的命令可以查看man手册。
semctl成功的返回值取决于cmd参数,失败返回-1,设置errno。
下面给出一个使用信号量的例子:
#include<sys/sem.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
union semun
{
int semval;
struct semid_ds* buf;
unsigned short int* array;
struct seninfo* _buf;
};
/*op为1时执行V操作,op为-1时执行P操作*/
void pv(int sem_id,int op)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = op;
sem_b.sem_flg = SEM_UNDO;
semop(sem_id,&sem_b,1);
}
int main(void)
{
int sem_id = semget(IPC_PRIVATE,1,0666);
union semun sem_un;
sem_un.semval = 1;
semctl(sem_id,0,SETVAL,sem_un);
pid_t pid = fork();
if(pid < 0)
return 1;
else if(pid == 0)
{
printf("child try to get binary sem\n");
pv(sem_id,-1);
printf("child get the sem and would release it after 5 seconds\n");
sleep(5);
pv(sem_id,1);
exit(0);
}
else{
printf("parent tyr to get binary sem\n");
pv(sem_id,-1);
printf("parent get the sem and would release it after 5 seconds\n");
sleep(5);
pv(sem_id,1);
waitpid(pid,NULL,0);
semctl(sem_id,0,IPC_RMID);//删除信号量集
return 0;
}
}