信号量
1. 问题
程序中,有时存在一种特殊代码,最多只允许一个进程执行该部分代码。这部分区域,称为“临界区”。然而在多进程并发执行时,当一个进程进入临界区,因某种原因被挂起时,其他进程就有可能也进入该区域。就会引起进程之间访问临界资源时互相干扰。
解决办法:使用信号量
2. 什么是信号量
信号量,是一种特殊的变量。
只能对信号量执行P操作和V操作
P操作, 如果信号量的值 > 0, 则把该信号量减1
如果信号量的值 ==0, 则挂起该进程。
V操作: 如果有进程因该信号量而被挂起,则恢复该进程运行
如果没有进程因该信号量而挂起,则把该信号量加1。
注意:P操作、V操作都是原子操作,即其在执行时,不会被中断。
注意:此指的“信号量”是指System V IPC的信号量,与线程所使用的信号量不同。该信号量,用于进程间通信。
3. 信号量的使用
1) 信号量的获取
semget函数
原型:int semget(key_t key, int nsems, int semflg);
功能:获取一个已存在的、或创建一个新的信号量量,返回该信号量的标识符。
参数:key, 键值,该键值对应一个唯一的信号量。类似于共享内存的键值。
不同的进程可通过该键值和semget获取唯一的信号量。特殊键值:IPC_PRIVAT:该信号量只允许创建者本身, 可用于父子进程间通信。
nsems, 需要的信号量数目,一般取1。
sem_flags, 与共享内存的sem_flags类似。IPC_CREAT, 如果该信号量未存在,则创建该信号量。
返回值: 成功,则返回一个正数
失败, 返回返回-1
2) 信号量的操作
semop函数
原型:int semop(int semid, struct sembuf *sops, unsigned nsops);
功能:改变信号量的值,即对信号量执行P操作、或V操作。
参数:semid, 信号量标识符, 即semget的返回值
sops, 是一个数组,元素类型为struct sembuf,下面为该结构体的介绍
struct sembuf {
short sem_num; //信号量组中的编号(即指定对哪个信号量操作)
//semget实际是获取一组信号量
//如果只获取了一个信号量,则该成员取0
short sem_op; // -1, 表示P操作
// 1, 表示V操作
short sem_flg; // SEM_UNDO : 如果进程在终止时,没有释放信号量
// 如果不设置指定标志,应该设置为0。则,自动释放该信号量
}
nsops, 表示第二个参数sops所表示的数组的大小,即表示有几个struct sembuf
返回值: 失败, 返回-1
成功, 返回 0
3) 信号量的控制
semctl函数
原型:int semctl(int semid, int sem_num, int cmd, ...);
功能:对信号量进行控制
参数:semid, 信号量标识符
sem_num, 信号量组中的编号,如果只有一个信号量,则取0
cmd,SETVAL:把信号量初始化为指定的值,具体的值由第4个参数确定
注意:只能对信号量初始化一次,如果在各进程中,分别对该信号量进行初始化,则可能导致错误。IPC_RMID 删除信号量
参数4:
类型为:union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
}
注意:union semun类型要求自己定义。有些Linux发行版在sys/sem.h中定义,有些发行版则没有定义。
可自定义如下:
#if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED)
#else
union semun {
int val;
struct semid_ds *buf;
unsigned short int *array;
struct seminfo *__buf;
};
#endif
4. 实例
实例1:不使用信号量,并发执行多个进程,观察对临界区的访问。
main1.c
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
int i;
pid_t pd = fork();
for (i=0; i<5; i++) {
/* 模拟临界区----begin */
printf("Process(%d) In\n", getpid());
sleep(1);
printf("Process(%d) Out\n", getpid());
/* 模拟临界区----end */
sleep(1);
}
return 0;
}
main1.c 创建的子进程与父进程会共同访问临界资源,使得程序的输出为同时为In或者同时为Out。也就是说没有实现互斥的访问临界资源
实例2:使用信号量,并发执行多个进程,观察对临界区的访问。
main2.c (对main1.c改进)
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
#include <stdio.h>
//对union semun类型的定义
#if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED)
#else
union semun {
int val;
struct semid_ds *buf;
unsigned short int *array;
struct seminfo *__buf;
};
#endif
//对信号量进行初始化
static sem_initial(int semid)
{
int ret;
union semun semun;
semun.val = 1;
ret = semctl(semid, 0, SETVAL, semun);
if (ret == -1) {
fprintf(stderr, "semctl failed!\n");
}
return ret;
}
// P操作
static int sem_p(int semid)
{
int ret;
struct sembuf sembuf;
sembuf.sem_op = -1;
sembuf.sem_num = 0;
sembuf.sem_flg = SEM_UNDO;
ret = semop(semid, &sembuf, 1);
if (ret == -1) {
fprintf(stderr, "sem_p failed!\n");
}
return ret;
}
//V操作
static int sem_v(int semid)
{
int ret;
struct sembuf sembuf;
sembuf.sem_op = 1;
sembuf.sem_num = 0;
sembuf.sem_flg = SEM_UNDO;
ret = semop(semid, &sembuf, 1);
if (ret == -1) {
fprintf(stderr, "sem_v failed!\n");
}
return ret;
}
int main(int argc, char* argv[])
{
int i;
int ret;
int semid;
/* 获取信号量 */
semid = semget((key_t)1234, 1, 0666 | IPC_CREAT);
if (semid == -1) {
printf("semget failed!\n");
exit(1);
}
/* 初始化信号量 */
if (argc > 1) {
ret = sem_initial(semid);
if (ret == -1) {
exit(1);
}
}
for (i=0; i<5; i++) {
if (sem_p(semid) == -1) {
exit(1);
}
/* 模拟临界区----begin */
printf("Process(%d) In\n", getpid());
sleep(1);
printf("Process(%d) Out\n", getpid());
/* 模拟临界区----end */
if (sem_v(semid) == -1) {
exit(1);
}
sleep(1);
}
/* 删除信号量 */
return 0;
}