前面学习了pipe,fifo,共享内存,信号。
本章将讲述信号量。
一、什么是信号量/信号量集?
1.什么是信号量
信号量是一个计数器。信号量用于实现进程间的同步和互斥。而可以取多个正整数的信号量被称为通用信号量。
对信号量的使用场景的解读
房间:临界资源(操作系统中的多个进程,他们共享各种资源,然而很多资源1次只能供1个进程使用。这种1次仅允许1个进程使用的资源称为临界资源。)
钥匙:信号量
只有拿到钥匙才能进入房间(如果进程A正在访问临界资源,则不允许进程B访问临界资源)。
2.什么是信号量集
linux中的信号量不止一个,有很多个。
二、P操作和V操作
p操作:取钥匙
V操作:放回钥匙
三、相关API
1.semget-----创建信号量
通过semget 来创建信号量或获取一个已有的信号量,
函数原型:
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
参数:
key 信号量的关键字,可以通过ftok() 创建,详细看 进程间通信——消息队列
nsems 信号量的数目(比如1代表信号量集合中有1个信号量)。如果是创建新的信号量,必须要指定nsems。如果是引用现有的信号量,nsems指定为0。
shmflg 有两个选项,IPC_CREAT 表示内核中没有此信号量则创建它。IPC_EXCL 当和IPC_CREAT一起使用时,如果信号量已经存在,则返回错误。
返回值:
成功返回标识符,否则返回-1。通过errno和perror函数可以查看错误信息。
注意:
通过nsems 可以看出,创建一个信号量,里面可能包含多个信号量值。该信号量就相当于一个集合。该值表明有多少个共享资源单位可供共享应用。下面semctl 也是通过nsems 中的某一个进行操作。
如果nsems 设为1,该信号量又被称为二元信号量(binary semaphore)
2.semctl------初始化信号量
这个函数可以有3个参数或者4个参数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
@semid:信号量数组标识
@semnum:要操作的信号量数组的信号量下标(比如:0代表操作第0个信号量)(该下表和数组下标一致,比如有1个信号量,则下表为0,有2个信号量则下表为1)
@cmd:
IPC_STAT 查询此信号量数组的数据存入arg.buf(buf为struct semid_ds结构体指针)
IPC_RMID 删除指定semid的信号量数组
GETVAL 获取信号量的当前值,
SETVAL 设置信号量的值,初始化要用的命令
————————————————
版权声明:本文为CSDN博主「abist」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/abist/article/details/117606809
This function has three or four arguments, depending on cmd. When
there are four, the fourth has the type union semun. The calling pro‐
gram must define this union as follows:
当使用4个参数时,要定义下面的联合体
union semun {
int val; /* 钥匙的数量 */
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) */
};
3.semop---信号量的操作
int semop(int semid,struct sembuf *sops,size_t nsops);
函数的参数
参数semid 为信号量集的标识符;
参数 sops 指向进行操作的结构体数组的首地址;
参数 nsops 指出将要进行操作的信号的个数。
返回值
semop 函数调用成功返回 0,失败返回 -1。
sembuf 结构体对应一个特定信号的操作。
该结构定义在 linux/sem.h,如下所示:
struct sembuf{
unsigned short sem_num; //信号在信号集中的索引,0代表第一个信号,1代表第二个信号
short sem_op; //操作类型
short sem_flg; //操作标志
};
sem_num标识信号量集中的第几个信号量,0表示第1个,1表示第2个,nsems - 1表示最后一个。
sem_op标识对信号量的所进行的操作类型。对信号量的操作有三种类型:
sem_op > 0,对该信号量执行挂出操作,挂出的值由sem_op决定,系统会把sem_op的值加到该信号量的当前值semval(参考文章开头关于每个信号量结构的定义)上。如果sem_flag指定了SEM_UNDO(还原)标志,那么相应信号量的semadj值会减掉sem_op的值。下面会说明semadj的含义。
sem_op < 0,对该信号量执行等待操作,当信号量的当前值semval >= -sem_op时,semval减掉sem_op的绝对值,为该线程分配对应数目的资源。如果指定SEM_UNDO,相应信号量的semadj就加上sem_op的绝对值。 当semval < -sem_op时,相应信号量的semncnt就加1,调用线程被阻塞,直到semval >= -sem_op,当此条件满足时,调用线程被唤醒,执行相应的分配操作,然后semncnt减去1。
sem_op = 0,表示调用者希望semval变为0。如果为0则立即返回,如果不为0,相应信号量的semzcnt加1,调用调用线程被阻塞。
sem_flag:信号量操作的属性标志,如果为0,表示正常操作,如果为IPC_WAIT,使对信号量的操作时非阻塞的。即指定了该标志,调用线程在信号量的值不满足条件的情况下不会被阻塞,而是直接返回-1,并将errno设置为EAGAIN。如果为SEM_UNDO,那么将维护进程对信号量的调整值,以便进程结束时恢复信号量的状态。
下面解释一下与单个信号量相关的几个值:
semval:信号量的当前值,在文章开头信号量的结构中已提到。
semncnt:等待semval变为大于当前值的线程数。在文章开头信号量的结构中已提到。
semzcnt:等待semval变为0的线程数。在文章开头信号量的结构中已提到。
semadj:指定信号量针对某个特定进程的调整值。只有sembuf结构的sem_flag指定为SEM_UNDO后,semadj才会随着sem_op而更新。讲简单一点:对某个进程,在指定SEM_UNDO后,对信号量semval值的修改都会反应到semadj上,当该进程终止的时候,内核会根据semadj的值,重新恢复信号量之前的值。
4.P操作(取钥匙)
自定义
要利用semop()来实现
void PGetKey(int id)//id是semget()的返回值
{
struct sembuf set;//该结构定义在 linux/sem.h
set.sem_num = 0;//这是下标,0代表第1个信号;
set.sem_op = -1;//是指对钥匙数量减1;
set.sem_flag = SEM_UNDO;//配置为SEM_UNDO当进程终止时,自动取消对该钥匙的操作
semop(id,&set,1);
}
5.V操作(放回钥匙)
自定义
要利用semop()来实现
void VputbackKey(int id)//id是semget()的返回值
{
struct sembuf set;//该结构定义在 linux/sem.h
set.sem_num = 0;//这是下标,0代表第1个信号;
set.sem_op = 1;//是指对钥匙数量加1;
set.sem_flag = SEM_UNDO;//配置为SEM_UNDO当进程终止时,自动取消对该钥匙的操作
semop(id,&set,1);
}
四、实验
实验要求:
最初状态:没有钥匙。
创建子进程后,
让父进程取钥匙(由于钥匙的数量为0,所以父进程取钥匙的动作被阻塞),使用结束(访问临界资源)后打印“father process”,放回钥匙。
让子进程放钥匙,然后打印“child process”。
(由于最初父进程会被阻塞,所以程序运行结果是先打印"child process"后打印“father process")
思路分析
(1)生成键值
(2)创建信号量
(3)初始化信号量:定义联合体-->钥匙的个数置为0--->使用semctl初始化信号量
(4)创建子进程
(5)判断父子进程
--->在父进程里:取钥匙,打印“father process”,放回钥匙。
---->在子进程里:放钥匙,打印“child process"
#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
(Linux-specific) */
};
void VputbackKey(int id)//id是semget()的返回值
{
struct sembuf set;//该结构定义在 linux/sem.h
set.sem_num = 0;//这是下标,0代表第1个信号;
set.sem_op = 1;//是指对钥匙数量加1;
set.sem_flg = SEM_UNDO;//配置为SEM_UNDO当进程终止时,自动取消对该钥匙的操作
semop(id,&set,1);
printf("put back key\n");
}
void PGetKey(int id)//id是semget()的返回值
{
struct sembuf set;//该结构定义在 linux/sem.h
set.sem_num = 0;//这是下标,0代表第1个信号;
set.sem_op = -1;//是指对钥匙数量减1;
set.sem_flg = SEM_UNDO;//配置为SEM_UNDO当进程终止时,自动取消对该钥匙的操作
semop(id,&set,1);
printf("get key\n");
}
int main()
{
key_t key;
int semid;
key = ftok(".",2);
semid = semget(key,1,IPC_CREAT|0666);
union semun initsem;
initsem.val = 0;
semctl(semid,0,SETVAL,initsem);
int pid = fork();
if(pid > 0)
{
PGetKey(semid);
printf("father process\n");
VputbackKey(semid);
}
else if(pid == 0)
{
VputbackKey(semid);
printf("child process\n");
}
else
{
printf("fork error\n");
}
return 0;
}
五、如何销毁钥匙
semctl(semid,0,IPCIPC_RMID);//3个参数