1、信号量的基本概念
通俗的来讲,信号量是用来调度进程间通信顺序的一种方法。信号量本质上是一个数字,用来表征一种资源的数量,当多个进程或者线程争夺这些稀缺资源的时候,信号量用来保证他们合理地、秩序地使用这些资源,而不会陷入逻辑谬误之中。
1.1 信号量的分类
在Unix/Linux系统中常用的信号量有三种:
- IPC信号量组
- POSIX具名信号量
- POSIX匿名信号量
本节课讲解的是第一种IPC信号量组,既然说到它是一个“组”,说明这种机制可以一次性在其内部设置多个信号量,实际上在其内部实现中,IPC信号量组是一个数组,里面包含N个信号量元素,每个元素相当于一个POSIX信号量。
1.2 基本概念
- 临界资源(critical resources)
- 多个进程或线程有可能同时访问的资源(变量、链表、文件等等)
- 临界区(critical zone)
- 访问这些资源的代码称为临界代码,这些代码区域称为临界区
- P操作
- 程序进入临界区之前必须要对资源进行申请,这个动作被称为P操作,这就像你要把车开进停车场之前,先要向保安申请一张停车卡一样,P操作就是申请资源,如果申请成功,资源数将会减少。如果申请失败,要不在门口等,要不走人。
- V操作
- 程序离开临界区之后必须要释放相应的资源,这个动作被称为V操作,这就像你把车开出停车场之后,要将停车卡归还给保安一样,V操作就是释放资源,释放资源就是让资源数增加。
信号量组非常类似于停车场的卡牌,想象一个有N个车位的停车场,每个车位是立体的可升降的,能停n辆车,那么我们可以用一个拥有N个信号量元素,每个信号量元素的初始值等于n的信号量来代表这个停车场的车位资源——某位车主要把他的m辆车开进停车场,如果需要1个车位,那么必须对代表这个车位的信号量元素申请资源,如果n大于等于m,则申请成功,否则不能把车开进去。
2. 函数接口
2.1 创建(或打开)
SEM其他IPC对象类似,首先需要创建(或打开)SEM对象,接口如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
接口说明:
- key:SEM对象键值
- nsems:信号量组内的信号量元素个数
- semflg:创建选项
- IPC_CREAT:如果key对应的信号量不存在,则创建之
- IPC_EXCL:如果该key对应的信号量已存在,则报错
- mode:信号量的访问权限(八进制,如0644)
创建信号量时,还受到以下系统信息的影响:
- SEMMNI:系统中信号量的总数最大值。
- SEMMSL:每个信号量中信号量元素的个数最大值。
- SEMMNS:系统中所有信号量中的信号量元素的总数最大值。
Linux中,以上信息在 /proc/sys/kernel/sem
中可查看,如下
root@ubuntu:/mnt/hgfs/share_file# cat /proc/sys/kernel/sem
32000 1024000000 500 32000
root@ubuntu:/mnt/hgfs/share_file#
下面是一个创建或打开一个有两个元素的信号量组
int main()
{
key_t key = ftok(".", 1); // 创建(若已有则打开)一个包含2个元素的信号量组
int id = semget(key, 2, IPC_CREAT|0666);
}
2.2 PV操作
对于信号量而言,最重要的作用就是用来表征对应资源的数量,所谓的P/V操作就是对资源数量进行 +n/-n 操作,既然只是个加减法,那么为什么不使用普通的整型数据呢?原因是:
- 整型数据的加减操作不具有原子性,即操作可能被中断
- 普通加减法无法提供阻塞特性,而申请资源不可得时应进入阻塞
对于原子性再做个简单的解释,即这种资源数量的加减法不能有中间过程,不管是成功还是失败都必须一次性完成。加减法看似简单,但在硬件层面上并非一个原子性操作,而是包含了多个寄存器操作步骤,因此不能作为P/V操作的手段。
因此在SEM对象中,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:SEM对象ID
- sops:P/V操作结构体sembuf数组
- nsops:P/V操作结构体数组元素个数
- 返回值
- 成功:0
- 失败:-1
具体来看一下sembuf结构体都含有哪些信息:
struct sembuf
{
unsigned short sem_num; /* 信号量元素序号(数组下标) */
short sem_op; /* 操作参数 */
short sem_flg; /* 操作选项 */
};
下面通过两个例程来认识PV操作。
P操作:
P操作的含义类似于申请使用空间,当公共空间有余量空间存在时,P操作才能申请成功,才能使得程序执行下去,否则将阻塞等待。
示例代码如下:
struct sembuf op;
op.sem_num = 0;
op.sem_op = -1;
op.sem_flg = 0;
semop(sem_id, &op, 1); // 应用P或V操作
其中op.sem_num = 0,代表是对第1组信号量做操作,0类似数组下标。 op.sem_op = -1;意味着要申请一个空间。op.sem_flg = 0;这个0是默认值,意味着默认不使用任何特殊选项,你也可以设置为IPC_NOWAIT标志来表示不进行阻塞操作。
那么知道了这个特性后我们便可以封装函数。我们通过函数参数的设定可以更好的理解P操作做了什么事情:
/**
* @description: 对信号量进行P操作
* @param {int} sem_id 信号量对象ID
* @param {int} whitch_group 你要操作的是第几组信号量
* @param {int} require_size 你需要申请多少空间
* @param {int} group_size 信号量组的大小
* @return {int} 成功返回0,失败返回-1
*/
int P(int sem_id,int whitch_group,int require_size,int group_size)
{
struct sembuf op[group_size];
op[whitch_group].sem_num = whitch_group;
op[whitch_group].sem_op = -require_size;
op[whitch_group].sem_flg = 0;
return semop(sem_id, &op[whitch_group], group_size); // 应用P或V操作
}
V操作:
有了申请空间自然就要释放空间,当公共空间有内存被占用时,可以进行V操作进行释放空间,那么要注意的是,释放的空间不会超过初始值,也就是说如果你用semop函数规定了初始空间就是1,即使你进行十次释放操作,空间还是1。
struct sembuf op;
op.sem_num = 0;
op.sem_op = +1;
op.sem_flg = 0;
semop(sem_id, &op, 1); // 应用P或V操作
和P操作同理,+1代表释放资源。
下面通过封装函数来理解
/**
* @description: 对信号量进行P操作
* @param {int} sem_id 信号量对象ID
* @param {int} whitch_group 你要操作的是第几组信号量
* @param {int} free_size 你要释放多少空间
* @param {int} group_size 信号量组的大小
* @return {int} 成功返回0,失败返回-1
*/
int V(int sem_id,int whitch_group,int free_size,int group_size)
{
struct sembuf op[group_size];
op[whitch_group].sem_num = whitch_group;
op[whitch_group].sem_op = free_size;
op[whitch_group].sem_flg = 0;
return semop(sem_id, &op[whitch_group], group_size); // 应用P或V操作
}
案例:
创建两个进程shm1与shm2,要求实现 shm1中对共享内存进行scanf写操作,shm2中对共享内存进行printf读操作。
例程1:使用1个信号量。
shm1.c文件
/*
* @Author: Fu Zhuoyue
* @Date: 2023-07-14 10:44:25
* @LastEditors: Fu Zhuoyue
* @LastEditTime: 2023-07-16 23:18:25
* @Description:
* @FilePath: /share_file/系统编程/5.共享内存/shm1.c
*/
#include "../syshead.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 */
};
/**
* @description: 对信号量进行P操作
* @param {int} sem_id 信号量对象ID
* @param {int} whitch_group 你要操作的是第几组信号量
* @param {int} require_size 你需要申请多少空间
* @param {int} group_size 信号量组的大小
* @return {int} 成功返回0,失败返回-1
*/
int P(int sem_id,int whitch_group,int require_size,int group_size)
{
struct sembuf op[group_size];
op[whitch_group].sem_num = whitch_group;
op[whitch_group].sem_op = -require_size;
op[whitch_group].sem_flg = 0;
return semop(sem_id, &op[whitch_group], group_size); // 应用P或V操作
}
/**
* @description: 对信号量进行P操作
* @param {int} sem_id 信号量对象ID
* @param {int} whitch_group 你要操作的是第几组信号量
* @param {int} free_size 你要释放多少空间
* @param {int} group_size 信号量组的大小
* @return {int} 成功返回0,失败返回-1
*/
int V(int sem_id,int whitch_group,int free_size,int group_size)
{
struct sembuf op[group_size];
op[whitch_group].sem_num = whitch_group;
op[whitch_group].sem_op = free_size;
op[whitch_group].sem_flg = 0;
return semop(sem_id, &op[whitch_group], group_size); // 应用P或V操作
}
int main()
{
key_t key = ftok(".",566);
if(key == - 1) //创建对象键值
{
perror("ftok");
return -1;
}
printf("key = %d\n",key);
int shm_id = shmget(key,1024,IPC_CREAT|0666); //创建共享内存
if( shm_id == -1)
{
perror("shmget");
return -1;
}
int sem_id = semget(key, 1, IPC_CREAT | 0666); //第二个参数是信号量的个数,如果是信号量数组就填数组元素个数
union semun a;
a.val = 1; // 假设将信号量元素初始值定为1
semctl(sem_id, 0, SETVAL, a);
char *p = shmat(shm_id, NULL, 0); //申请获取共享内存地址
if( p == (void *) -1)
{
perror("shmat");
}
while(1)
{
printf("请输入写入共享内存的消息:\n");
scanf("%s",p);
V(sem_id,0,1,1);
}
shmdt(p);
shmctl(shm_id, IPC_RMID, NULL);
semctl(sem_id, 0, IPC_RMID, 0);
}
shm2.c文件
/*
* @Author: Fu Zhuoyue
* @Date: 2023-07-14 10:44:25
* @LastEditors: Fu Zhuoyue
* @LastEditTime: 2023-07-16 23:18:05
* @Description:
* @FilePath: /share_file/系统编程/5.共享内存/shm2.c
*/
#include "../syshead.h"
struct sembuf op;
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 */
};
/**
* @description: 对信号量进行P操作
* @param {int} sem_id 信号量对象ID
* @param {int} whitch_group 你要操作的是第几组信号量
* @param {int} require_size 你需要申请多少空间
* @param {int} group_size 信号量组的大小
* @return {int} 成功返回0,失败返回-1
*/
int P(int sem_id,int whitch_group,int require_size,int group_size)
{
struct sembuf op[group_size];
op[whitch_group].sem_num = whitch_group;
op[whitch_group].sem_op = -require_size;
op[whitch_group].sem_flg = 0;
return semop(sem_id, &op[whitch_group], group_size); // 应用P或V操作
}
/**
* @description: 对信号量进行P操作
* @param {int} sem_id 信号量对象ID
* @param {int} whitch_group 你要操作的是第几组信号量
* @param {int} free_size 你要释放多少空间
* @param {int} group_size 信号量组的大小
* @return {int} 成功返回0,失败返回-1
*/
int V(int sem_id,int whitch_group,int free_size,int group_size)
{
struct sembuf op[group_size];
op[whitch_group].sem_num = whitch_group;
op[whitch_group].sem_op = free_size;
op[whitch_group].sem_flg = 0;
return semop(sem_id, &op[whitch_group], group_size); // 应用P或V操作
}
int main()
{
key_t key = ftok(".",566);
if(key == - 1) //创建对象键值
{
perror("ftok");
return -1;
}
printf("key = %d\n",key);
int shm_id = shmget(key,1024,IPC_CREAT|0666); //创建共享内存
if( shm_id == -1)
{
perror("shmget");
return -1;
}
int sem_id = semget(key, 1, IPC_CREAT | 0666); //第二个参数是信号量的个数,如果是信号量数组就填数组元素个数
union semun a;
a.val = 1; // 假设将信号量元素初始值定为1
semctl(sem_id, 0, SETVAL, a);
char *p = shmat(shm_id, NULL, 0); //申请获取共享内存地址
if( p == (void *) -1)
{
perror("shmat");
}
while(1)
{
P(sem_id,0,1,1);
printf("从共享内存中读出的信息是:\n");
printf("%s\n",p);
}
shmdt(p);
shmctl(shm_id, IPC_RMID, NULL);
semctl(sem_id, 0, IPC_RMID, 0);
}
我们通过semop函数申请了的初始空间为1,在shm1.c文件中可以看到,我们在scanf操作完成后会为信号量资源释放1个空间,紧接着while循环里会再次等待用户输入。而此时在shm2.c的while循环中,先进行P操作即申请了1个空间,使得当前剩余空间为0,在进行下一轮while循环时会由于可用空间不足无法申请空间而产生阻塞,避免了不断printf数据的现象发生,直到在shm1中scanf输入完成后,进行V操作释放空间为止。才会允许shm2中对P操作的阻塞解除,进行printf。