进程间通信——信号量
1、信号量介绍
多个进程同时访问系统上的同一资源时,就需要考虑同步问题,以确保在任一时刻只有一个进程对资源独占式访问。通常,我们称访问共享资源的代码为临界区,进程同步,也就是确保同一时刻,只有一个进程进入临界区。
信号量是实现进程同步的一种方式,信号量只能取自然数并且只支持两种操作,即等待和信号,在Linux中称为P、V操作。比如有SV信号量,则P、V操作的含义如下:
- P(SV),如果SV的值大于0,就将其减1;如果SV的值等于0,则挂起该进程的执行
- V(SV),如果有其他进程因为等待信号量SV而挂起,则唤醒;如果没有,则将SV加1
信号量可以取任意自然数,但是最常用的信号量为二进制信号量,它只能取0和1,这里只讨论二进制信号量。关于信号量的系统调用主要有3个:semget、semop、semctl。此三个函数都是被设计用来操作信号量集,而不是单个信号量。
2、semget系统调用
#include<sys/sem.h>
int semget(key_t key, int num_sems, int sem_flags)
函数功能为用来创建一个信号量集或获取一个已经存在的信号量集,参数信息如下:
- key参数是一个键值,用来标识一个全局的唯一的信号量集。通过信号量通信的进程使用相同的键值来创建\获取该信号量。当key为特殊键值IPC_PRIVATE(值为0)时,无论该信号量是否已经存在,都将会创建一个新的信号量集。信号量键值一般只有semget函数使用,其他的有关信号量函数,使用semget返回的信号量集标识符来访问该信号量。
- num_sems参数指定要创建/获取的信号量集中的信号量数目。如果是创建信号量,该值必须被设置,如果是获取,则可以置为0。
- sem_flags参数指定一组标志。低9位指示了信号量的权限。它可以和IPC_CREAT标志做按位或操作,用来创建信号量,此时即使信号量已经存在,semget也不会产生错误。还可以联合使用IPC_CREAT和IPC_EXCL标志来确保创建一组新的、唯一的信号量集,这种情况下,如果信号量集已经存在,则semget返回错误并置errno为EEXIST。
- semget成功时返回信号量集标识符(正整数值),失败时返回-1,并设置errno。
3、semop系统调用
#include<sys/sem.h>
int semop(int sem_id, struct sembuf* sem_ops, size_t num_sem_ops)
semop函数用来改变信号量的值,即执行P、V操作,参数信息如下:
- sem_id参数是由semget调用返回的信号量集标识符,指定要改变的信号量集。
- sem_ops参数是一个指向sembuf结构数组的指针。sembuf结构体定义如下:
struct sembuf
{
unsigned short int sem_num;
short int sem_op;
short int sem_flg;
}
sem_num成员指定信号量集中的信号量编号,从0开始。sem_op成员指定操作类型,其值可为正整数、0和负整数,信号量集操作类型较为复杂(可参见APUE),这里就只以二进制信号量为例来说明,sem_op为-1时即为P操作,为1时即为V操作。sem_flg成员可选值为IPC_NOWAIT和SEM_UNDO,IPC_NOWAIT表示系统调用无论是否操作成功,都将立即返回,类似于非阻塞;而SEM_UNDO则表示使内核跟踪信号量,如果进程没有释放信号量而异常退出,则由内核来释放该信号量值。
- num_sem_ops指定sem_ops数组中的元素个数,即要操作的个数。
- semop成功时返回0,失败时返回-1并设置errno。失败时,sem_ops数组中指定的所有操作都不被执行。
4、semctl系统调用
#include<sys/sem.h>
int semctl(int sem_id, int sem_num, int command, ...)
semctl函数用于对信号量进行控制,参数信息如下:
- sem_id参数是由semget调用返回的信号量集标识符。
- sem_num参数指定被操作的信号量的编号,从0开始计数。
- command参数指定要执行的命令,有的命令需要调用者传递第四个参数,第四个参数的类型由用户自定义,推荐格式如下:
union semun
{
int val;
struct semid_ds* buf;
unsigned short* array;
struct seminfo* _buf;
}
command常用的操作有两种,其一,SETVAL,用来把信号量初始化为一个已知值,这个值由union semun中的val成员指定,其作用是在信号量初次使用前进行设置;其二,IPC_RMID,用于删除一个不需要再使用的信号量集,并唤醒所有等待该信号量集的进程。
5、使用IPC_PRIVATE信号量进行进程间通信示例
#include<sys/sem.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#define SEM_FLAGS 0666
union semun
{
int val;
struct semid_ds* buf;
unsigned short int* array;
struct seminfo* _buf;
};
void pv(int sem_id, int option) //pv操作,option为-1时为p操作,option为1时为v操作
{
struct sembuf sem_buf;
sem_buf.sem_num = 0;
sem_buf.sem_op = option;
sem_buf.sem_flg = SEM_UNDO;
semop(sem_id, &sem_buf, 1);
}
int main(int argc, char* argv[])
{
int sem_id = semget(IPC_PRIVATE, 1, SEM_FLAGS); //创建信号量
union semun sem_un;
sem_un.val = 1;
semctl(sem_id, 0, SETVAL, sem_un); //初始化信号量的值为1
pid_t pid = fork();
if(0 == id)
{
pv(sem_id, -1);
printf("child process\n");
sleep(5);
pv(sem_id, 1);
exit(0);
}
else if(id > 0)
{
pv(sem_id, -1);
printf("parent process\n");
sleep(5);
pv(sem_id, 1);
}
else
{
printf("fork error\n");
return 1;
}
wait(NULL);
semctl(sem_id, 0, IPC_RMID, sem_un); //删除信号量
return 0;
}