文章目录
- 信号量介绍
- 信号量相关函数
- 案例一:有亲缘关系的进程使用信号量通信
- 案例二:无亲缘关系的进程使用信号量通信
使用环境:Ubuntu18.04
使用工具:VMWare workstations ,xshell
作者在学习Linux的过程中对常用的命令进行记录,通过思维导图的方式梳理知识点,并且通过xshell连接vmware中ubuntu虚拟机进行操作,并将练习的截图注解,每句话对应相应的命令,读者可以无障碍跟练。
第八次练习的重点在于Linux的中如何使用信号量进行进程通信。
信号量介绍
- 信号量(信号灯)是一种提供不同进程之间或者给定进程之间的不同线程间同步手段的原语。信号量是进程/线程同步的一种方式,有时候我们需要保护一段代码,使它每次只能被一个执行进程/线程运行,这种工作就需要一个二进制开关;有时候需要限制一段代码可以被多少个进程/线程执行,这就需要用到关于计数信号量,即考研操作系统中的semphore。信号量开关是二进制信号量的一种逻辑扩展,两者实际调用的函数都是一样。
- 信号量分为三种:
- System V信号量,在内核中维护,可用于进程或线程间的同步
- Posix 有名信号量,一种来源于POSIX技术诡诞的实时扩展方案,可用于进程或线程间的同步,常用于线程。
- Posix基于内存的信号量,存放在共享内存区中,可用于进程或线程间的同步。
- 为了获取共享资源进程需要执行如下操作:
- 测试控制该资源的信号量
- 若信号量值为正数,则进程可以使用此资源。进程信号量值-1,表示它使用了一个资源单位。此进程使用完共享资源后对应的信号量会+1。
- 若信号量值为0,则进程是阻塞状态,等待其他进程释放资源,即信号量大于0。
- 为了实现信号量,信号量的增减都是原子操作,即不可中断的操作,原子操作的原理是使用关中断开中断的操作实现,有兴趣的读者可以去看操作系统,计算机四大件之一。
信号量相关函数
//头文件
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/types.h>
- 函数
int semget(key_t key,int nsems,int flag);
创建一个信号量集或访问一个已存在的信号量集。返回值:成功时,返回一个称为信号量标识符的整数,semop和semctl会使用它。出错返回-1.
- 参数key是唯一表示一个信号量的关键字,如果key为IPC_PRIVATE则创建一个只有创建者进程才可以访问的信号量,通常用于父子进程之间。如果key为IPC_CREAT则创建一个可以被多个进程共享的信号量。
- 参数nsems指定需要使用的信号量数目。如果是创建新集合,则必须指定nsems,如果是引用一个存在的集合,则将nsems指定为0。
- 参数flag是一组标志,作用和open函数的标志相似。低端九个位是该信号量的权限,即文件的访问权限。此外还可以与IPC_CREAT按位或操作,已创建一个新的信号量。或者通过IPC_CREAT和IPC_EXCL联合使用,创建出一个新的信号量,如果该信号量已经存咋就会返回一个错误。
- 函数
int semop(int semid,struct sembuf *sops,size_t num_sops);
用于改变信号量对象中各个信号量的状态。成功返回0,失败返回-1.
- 参数semid是由semget返回的信号量标识符
- 参数sops是指向一个结构体数组的指针。每个数组元素至少包含一下几个成员:
struct sembuf{
short sem_num; //操作信号量在信号量集合中的编号,第一个信号量编号为0
short sem_op; //sem_op为-1,就是p操作,即wait操作。为+1就是v操作,即signal操作。
short sem_flg; //通常设为SEM_UNDO,程序结束,信号量为semop调用前的值。
};
- 参数nops为sops指向的sembuf结构体数组的大小
- 函数
**int semctl(int semid, int semnum, int cmd, …);**
用来直接控制信号量的信息。成功返回0,失败返回-1。
- 参数semid是有semget返回的信号量标识符
- 参数semnum为集合中信号量的编号,当要用到成组的信号量时,从0开始。一般取值为0,表示这是第一个也是唯一的信号量。
- 参数cmd为执行的操作。IPC_RMID(立即删除信号集,唤醒所有被阻塞的进程)、GETVAL(根据 semun 返回信号量的值,从 0 开始,第一个信号量编号为 0)、SETVAL(根据 semun 设定信号的值,从 0 开始,第一个信号量编号为 0)、GETALL(获取所有信号量的值,第二个参数为 0,将所有信号的值存入 semun.array中)、SETALL(将所有 semun.array 的值设定到信号集中,第二个参数为 0)等。
- 参数…是一个union semun(需要程序员自己定义),它至少包含一下几个成员变量,通常只用到val:
union semun{
int val; /* SETVAL的值 */
struct semid_ds *buf; /* IPC_STAT, IPC_SET的缓冲区 */
unsigned short *array; /* GETALL, SETALL的集合 */
};
相关结构体:
The semid_ds data structure is defined in <sys/sem.h> as follows:
struct semid_ds {
struct ipc_perm sem_perm; /* Ownership and permissions */
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Last change time */
unsigned long sem_nsems; /* No. of semaphores in set */
};
The ipc_perm structure is defined as follows (the highlighted fields aresettable using IPC_SET):
struct ipc_perm {
key_t __key; /* Key supplied to semget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions */
unsigned short __seq; /* Sequence number */
};
案例一:有亲缘关系的进程使用信号量通信
创建子进程生产V操作,放入信号集合中,在创建父进程消费P操作,放入信号集合中。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <string.h>
#include <sys/wait.h>
union semun //必须重写共用体
{
int val; //信号量的值
struct semid_ds* buf; //IPC_STAT, IPC_SET的缓冲区
unsigned short *array; //GETALL,SETALL的数组
};
int main()
{
int semid = semget(IPC_PRIVATE,1,0666|IPC_CREAT); //创建信号量集
if(semid == -1){ //创建失败的处理
perror("semget error");
exit(-1);
}
if(fork() == 0){ //子进程
struct sembuf sem; //定义信号量结构体
memset(&sem,0,sizeof(struct sembuf)); //初始化结构体
sem.sem_num = 0; //信号量编号,初始为0
sem.sem_op = 1; //+1 表示执行V操作,即signal生产产品
sem.sem_flg = 0; //SEM_UNDO,设置semop调用前的值
union semun arg; //
arg.val = 0; //初始化信号量的值
semctl(semid,0,SETALL,arg); //将信号量的值全部设置到信号量集中,相当于公共信号量
while(1){
semop(semid,&sem,1); //执行指定的V操作,表示生产者生产产品
printf("生产者总数:%d\n",semctl(semid,0,GETVAL)); //打印生产的公共信号量
sleep(1); //休息一秒
if(semctl(semid,0,GETVAL) == 5){
break;
}
}
}
else{ //父进程
sleep(2); //休眠,让子进程先生产点东西
struct sembuf sem; //信号量结构体
memset(&sem,0,sizeof(struct sembuf)); //初始化结构体
sem.sem_num = 0; //信号量编号,初始为0
sem.sem_op = -1; //-1 表示执行P操作,即wait 消费产品
sem.sem_flg = 0; //SEM_UNDO,设置semop调用前的值
while(1){
semop(semid,&sem,1); //执行指定的P操作,表示消费者消费产品
printf("消费者总数:%d\n",semctl(semid,0,GETVAL)); //打印获取的公共信号量
sleep(2); //休息两秒
if(semctl(semid,0,GETVAL) == 0){
break;
}
wait(NULL);
}
}
return 0;
}
运行结果实例:两个if判断就是,子进程生产5个退出,父进程消费所有的退出。
案例二:无亲缘关系的进程使用信号量通信
- 在本案例中,首先使用函数semctl初始化信号量集合sem_id,它包含两个信号,分别是生产的数量和空位,那么在消费者的进程中使用同样的key就可以获得该信号量集合,从而实现两个进程之间的通信。
- 在主函数中设定两个信号量的PC操作,然后在各自的进程汇总对两个信号进行操作。
- 如果只运行生产者进程,则生产10个以后,该进程会因为无法获取空仓库的资源而阻塞,这个时候运行消费者进程,阻塞就会解除。
- 如果先运行生产者,生产几个产品后,关闭该进程,则运行消费者,当消费者将所有产品取走后,该进程会因为得不到产品资源而阻塞,这个时候运行生产者进程,阻塞就会接触。
- 如果同时运行两个进程,由于消费比生产快,所有消费者每次都要等待生产者。
- 本案例中,需要先运行生产者,初始化信号量。
生产者源码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <string.h>
#include <sys/wait.h>
//定义全局变量信号集id
int sem_id;
//初始化信号集
void init();
//删除信号集
void del();
int main()
{
struct sembuf sops[2]; //定义两个信号量
sops[0].sem_num = 0; //设置生产者编号0
sops[0].sem_op = 1; //就是V操作,生产产品
sops[0].sem_flg = 0; //也可以写成SEM_UNDO
sops[1].sem_num = 1; //设置仓库容量编号0
sops[1].sem_op = -1; //就是P操作,仓库容量-1
sops[1].sem_flg = 0; //也可以写成SEM_UNDO
init();//初始化信号量集合,就是生产产品量和仓库空位
printf("生产者开始生产\n");
printf("生产的数量%d\n",semctl(sem_id,0,GETVAL));//使用semctl获取集合中第一个结构体中的val
printf("空闲空间为%d\n",semctl(sem_id,1,GETVAL));//使用semctl获取集合中第二个结构体中的val
while(1){
semop(sem_id,&sops[1],1); //改变第二个数组的中状态,即空闲仓库容量先-1,才能生产产品+1
//先对仓库容量-1的原因是,如果有多个生产者同时生产,就需要用仓库容量来限制
printf("已经申请了仓库,可以开始生产了\n");
semop(sem_id,&sops[0],1); //改变第一个数组的中状态,即产品数量+1
printf("空闲空间为%d\n",semctl(sem_id,1,GETVAL));//使用semctl获取集合中第二个结构体中的val
printf("生产的数量%d\n",semctl(sem_id,0,GETVAL));//使用semctl获取集合中第一个结构体中的val
sleep(2);
}
del();//删除信号集合
return 0;
}
//初始化信号集
void init()
{
int ret;
unsigned short sem_array[2];
union semum
{
int val;
struct semid_ds* buf;
unsigned short* array;
}arg;
sem_id = semget((key_t)1234,2,IPC_CREAT|0644); //使用key创建一个还有两个信号的信号集
if(sem_id == -1){
perror("semget");
exit(-1);
}
sem_array[0] = 0; //产品数量
sem_array[1] = 10; //仓库空位
arg.array = sem_array;
ret = semctl(sem_id,0,SETALL,arg); //将所有semun.array的值设置到集合中,第二个参数代表0号
if(ret == -1){
printf("信号量放入集合失败");
}
printf("生产者%d初始化\n",semctl(sem_id,0,GETVAL));
printf("仓库%d初始化\n",semctl(sem_id,1,GETVAL));
}
//删除信号集
void del()
{
semctl(sem_id,IPC_RMID,0);
}
消费者源码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <string.h>
#include <sys/wait.h>
int sem_id;
//获取生产者创建的信号集标识符
void init();
int main()
{
struct sembuf sops[2];
sops[0].sem_num = 0; //设置消费者编号0
sops[0].sem_op = -1; //就是P操作,消费产品
sops[0].sem_flg = 0; //也可以写成SEM_UNDO
sops[1].sem_num = 0; //设置仓库容量编号0
sops[1].sem_op = 1; //就是V操作,仓库容量+1
sops[1].sem_flg = 0; //也可以写成SEM_UNDO
init();
printf("这是消费者\n");
printf("消费的数量%d\n",semctl(sem_id,0,GETVAL));//使用semctl获取集合中第一个结构体中的val
printf("空闲空间为%d\n",semctl(sem_id,1,GETVAL));//使用semctl获取集合中第二个结构体中的val
while(1)
{
semop(sem_id,&sops[0],1);//先消费,在归还仓库空间
//原因是如果多个消费者同步消费,先归还仓库空间会导致,生产者快速填满,目的是为了生产者和消费者的同步
printf("现在开始消费\n");
semop(sem_id,&sops[1],1);//归还仓库空间
printf("消费的数量%d\n",semctl(sem_id,0,GETVAL));//使用semctl获取集合中第一个结构体中的val
printf("空闲空间为%d\n",semctl(sem_id,1,GETVAL));//使用semctl获取集合中第二个结构体中的val
sleep(1);
}
return 0;
}
//获取生产者创建的信号集标识符
void init()
{
sem_id = semget((key_t)1234,2,IPC_CREAT|0644); //获取信号集ID
}