信号量(semaphore)和信号只有一字之差,却是不同的概念,信号量与之前介绍的IPC不同,它是一个计数器,用于实现进程间的互斥于同步
本文参考:
Linux 的信号量_linux 信号量_行孤、的博客-CSDN博客
【Linux】Linux的信号量集_Yngz_Miao的博客-CSDN博客
Linux进程间通信(九)——信号量_linux 信号量_天山老妖的博客-CSDN博客
信号量函数(semget、semop、semctl)及其范例_semget函数_guoping16的博客-CSDN博客
概念
信号量本质上是一个计数器,用于协调多个进程(但不包括父子进程)对共享数据对象的读/写。它不以传输数据为目的,主要是用来保护临界资源(一次仅允许一个进程使用的资源称为临界资源,很多物理设备都属于临界资源,比如打印机,磁带机),保证临界资源在一个时刻只有一个进程独享。
信号量是描述某一种资源是否可用的变量,信号量的值表示当前可用的资源的数量,若信号量的值等于0则意味着目前没有可用的资源。
信号量是一个特殊的变量,只允许进程对它进行等待信号(P)和发送信号(V)操作。
- P操作(拿):等待。如果sv大于0,减小sv。如果sv为0,挂起这个进程的执行
- V操作(放):发送信号。如果有进程被挂起等待sv,使其恢复执行。如果没有进行被挂起等待sv,增加sv
最简单的信号量是只能取0,1的信号量,这也是信号量最常见的一种形式,叫做二值信号量,而可以取多个正整数的信号量称为通用信号量。
信号量集
就是由多个信号量组成的一个数组。作为一个整体信号量集中的所有信号量使用同一个等待队列。Linux的信号量集为进程请求多个资源创造了条件。Linux规定,当进程的一个操作需要多个共享资源时,如果只成功获得了其中的部分资源,那么这个请求即告失败,进程必须立即释放所有已获得资源,以防止形成死锁。
相关API
Linux下的信号量函数都是在通用的信号量数组(信号量集)上进行操作,而不是在一个单一的二值信号上进行操作。
semget函数
获取或创建信号量组
需要添加的库
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型
int semget(key_t key, int nsems, int semflg);
函数参数
- key:IPC键值
- nsems:信号量集中信号量的个数
- semflag:取以下值中的一个,若不取0,还要或上新建信号量集的权限
- 0(取信号量集标识符,若不存在则函数会报错)
- IPC_CREAT:不存在则创建信号量集
- IPC_CREAT|IPC_EXCL:不存在则创建信号量集,若存在会报错
- 返回值:成功返回信号量集ID,失败返回-1
semop函数
对信号量组进行操作,改变信号量的值,也就是对信号量进行P或V操作
需要添加的库
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型
int semop(int semid, struct sembuf *sops, size_t nsops);
函数参数
- semid:信号量集ID,semget的返回值
- sops:一个结构体:
struct sembuf
{
short sem_num; // 信号量集的个数,单个信号量设置为0。
short sem_op; // 信号量在本次操作中需要改变的数据:-1-等待操作;1-发送操作。
short sem_flg; // 把此标志设置为SEM_UNDO,操作系统将跟踪这个信号量。如果设置为IPC_NOWAIT则会直接返回
// 如果当前进程退出时没有释放信号量,操作系统将释放信号量,避免资源被死锁。
};
- nsops:操作信号量的个数,即上一个参数sops结构变量的个数(只有一个就写1)
- 返回值:成功返回0,失败返回-1
使用举例
封装P操作:
void pGet(int semid)
{
struct sembuf sops;
sops.sem_num = 0;
sops.sem_op = -1;
sops.sem_flg = SEM_UNDO;
if(semop(semid,&sops,1) == -1){
printf("pGet error!\n");
}else{
printf("pGet success\n");
}
}
封装V操作:
void vPut(int semid)
{
struct sembuf sops;
sops.sem_num = 0;
sops.sem_op = 1;
sops.sem_flg = SEM_UNDO;
if(semop(semid,&sops,1) == -1){
printf("vPut error!\n");
}else{
printf("vPut success\n");
}
}
semctl函数
控制信号量(常用于设置信号量的初始值和销毁信号量)
需要添加的库
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型
int semctl(int semid, int semnum, int cmd, ...);
函数参数
- semid:信号量集ID,semget的返回值
- semnum:要操作的目标信号量,第一个就是0,第二个就是1....(只有一个就写0)
- cmd:对目标信号量采取的命令,取以下值中的一个:
IPC_STAT
IPC_SET
IPC_RMID:销毁信号量,此时没有第四个参数
IPC_INFO
SEM_INFO
SEM_STAT
GETALL
GETNCNT
GETPID
GETVAL
GETZCNT
SETALL
SETVAL:设置信号量的初始值,此时有第四个参数
- 第四个参数:根据cmd设置的情况决定是否存在这个参数,如果存在,那这个参数的类型就是一个名为semun的联合体:
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) */
};
使用举例
假设我要操作semid是1234的信号量集中的第一个信号量,将这个信号量的值初始化为1:
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()
{
union semun mysemun_1;
mysemun_1.val = 1;
semctl(1234,0,SETVAL,mysemun);
return 0;
}
回顾联合体的知识:和结构体不同,联合体的成员共用存储空间,所以对多个成员的赋值可能会导致覆盖,产生问题,所以此处只需要对val进行赋值,并不用为了防止野指针而对其他成员赋值
实操演示
需求:创建并初始化一个只有一个信号量的信号量集,然后fork一下,之后通过操控信号量让子进程先打印一句话,打印完才允许父进程打印另一句话
sem1.c:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <unistd.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) */
};
void pGet(int semid)
{
struct sembuf sops;
sops.sem_num = 0;
sops.sem_op = -1;
sops.sem_flg = SEM_UNDO;
if(semop(semid,&sops,1) == -1){
printf("pGet error!\n");
}else{
printf("pGet success\n");
}
}
void vPut(int semid)
{
struct sembuf sops;
sops.sem_num = 0;
sops.sem_op = 1;
sops.sem_flg = SEM_UNDO;
if(semop(semid,&sops,1) == -1){
printf("vPut error!\n");
}else{
printf("vPut success\n");
}
}
int main()
{
key_t key;
key = ftok(".",2);
int semid;
pid_t pid;
semid = semget(key, 1, IPC_CREAT|0666);
if(semid == -1){
printf("semget error\n");
return 1;
}else{
printf("get semid = %d\n",semid);
}
union semun mysemun_1;
mysemun_1.val = 0; //初始化为无锁的状态
semctl(semid,0,SETVAL,mysemun_1);
pid = fork();
if(pid>0){
pGet(semid); //尝试获取锁
printf("this is father\n");
semctl(semid,0,IPC_RMID); //销毁信号量
}else if(pid == 0){
printf("this is son\n");
vPut(semid); //放入锁
}else{
printf("fork error\n");
return 1;
}
return 0;
}
实现效果:
由于一开始初始化信号的时候将信号量设置为了0,也就是无锁状态:
而父进程首先是想拿锁,所以必然被阻塞,只有等子进程先打印一句话然后将锁放进去之后,父进程才可以拿到锁并打印自己的那句话。
且此时父进程不需要收集子进程退出状态,因为父子进程都是打了一句话就都返回退出了。