概念
信号量(semaphore) 与已经介绍过的 PC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
特点
1.信号量用于进程间同步,若要在进程间传递数据需要结合共享内存
2.信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作
PV操作 | 一种实现进程互斥与同步的有效方法。PV操作与信号量的处理相关,P表示通过的意思,V表示释放的意思 |
原子操作 | 指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch(切换到另一个线程) |
3.每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数
4.支持信号量组
使用原理
最简单的信号量是只能取 0和 1的变量,这也是信号量最常见的一种形式,叫做二值信号量 Binary Semaphore) 。而可以取多个正整数的信号量被称为通用信号量。
Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。
semget函数
创建或获取一个信号量组:若成功返回信号量标识符ID,失败返回-1。
函数原型
int semget(key_t key, int nsems, int semflg);
参数解读
key | 所创建或打开信号量集的键值 |
nsems | 信号量的个数,通常为1;如果是引用一个现有的集合,则将nsems指定为 0,该参数只在创建信号量集时有效 |
semflg | 调用函数的操作类型,也可用于设置信号量集的访问权限,两者通过 | 表示 |
semctl函数
控制信号量的相关信息。
函数原型
int semctl(int semid, int semnum, int cmd, ...);
参数解读
semid | semget函数返回的信号量标识符ID |
semnum | 操作信号在信号集中的编号,这里是以数组为单位的,所以第一个单位是数组中的第一个,即为0 |
cmd | 对信号量进行相关操作 |
在semctl函数中的cmd有多种,这里就说两个常用的:
SETVAL | 用于初始化信号量为一个已知的值。所需要的值作为联合体semun的val成员来传递,在信号量第一次使用之前需要设置信号量 |
IPC_RMID | 删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源 |
如果有第四个参数,它通常是联合体union 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) */
};
semop函数
对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1。
函数原型
int semop(int semid, struct sembuf *sops, unsigned nsops);
参数解读
semid | semget函数返回的信号量标识符ID |
*sops | 通常是一个结构体数组,但信号量理论上一次只能执行一个,所以在定义该结构体时,不需要以数组格式定义 |
nsops | 信号操作结构的数量,恒大于或等于1 |
*sops结构体定义格式如下:
struct sembuf
{
short sem_num; // 信号量的编号,默认为0
short sem_op; // 信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,
// 一个是+1,即V(发送信号)操作。
short sem_flg; // 通常为SEM_UNDO,使操作系统跟踪信号,当进程结束之前,取消对信号量的任何操作
};
代码示例
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <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*/
};
void pGetKey(int semid)
{
struct sembuf set;//定义一个结构体
set.sem_num = 0;//第一个信号量
set.sem_op = -1;//拿走钥匙 数量-1
set.sem_flg = SEM_UNDO;//拿不到钥匙就等待
semop(semid,&set,1);//第二个参数为指针,这里需要以地址形式写入
printf("get the key\n");
}
void vPutBackKey(int semid)
{
struct sembuf put;
put.sem_num = 0;
put.sem_op = +1;//放回钥匙 数量+1
put.sem_flg = SEM_UNDO;
semop(semid,&put,1);
printf("put back the key\n");
}
int main()
{
int semid;
int pid;
key_t key;
key = ftok(".",1);
semid = semget(key,1,IPC_CREAT|0666);//以可读可写权限创建信号量
union semun initsem;//调用联合体:初识化钥匙数量
initsem.val = 0;//一开始钥匙数量为0
semctl(semid,0,SETVAL,initsem);
pid = fork();
if(pid > 0)
{
pGetKey(semid);//拿钥匙 但钥匙数量为0 需等待钥匙放回 即等待子进程放回钥匙
printf("this is father\n");
vPutBackKey(semid);//拿到钥匙后使用完毕放回钥匙
}
else if(pid == 0)
{
printf("this is child\n");
vPutBackKey(semid);//将钥匙放回,钥匙数量+1,父进程检测到有钥匙就拿钥匙,即先进入子进程再进入父进程
}
else
printf("error!\n");
return 0;
}
可见,一开始没有钥匙,父进程中没有任何操作,当子进程中将钥匙放回时,父进程检测到后拿到钥匙使用完毕后并放回。实现信号量的检测。