前言
在找到了共享内存存在的问题后,进程君父子着手开始解决这些问题。他们发明了一个新的神通——信号量。
信号量
信号量是一个计数器,用于管理对共享资源的访问权限。主要特点包括:
(1)是一个非负整数
(2)提供两种原子操作:P(等待)和V(释放)
(3)可以用于进程同步和互斥
(4)用于显示资源的数量
如何理解这些功能呢,他和普通的变量实现的计数器的区别是什么呢?请看下面的例子:
普通变量实现进程间共享资源计数的问题
假设有一个停车场,内部有若干停车位。当前剩余的停车位数量会显示在大门前,当进入一辆车时,大门打开,确保车辆进入后,大门合上,剩余停车位减1。随着规模的扩建,现在停车场增加了一个大门,共有东西两个大门。假设当前停车场有一个空位,如下图所示:
如图,剩余车位为1,有两辆车要进入我们的停车场。由于停车场的传感器机制要等到汽车确定进入停车场并关闭大门后,才会重新计算剩余停车场数量,对于图中情况,两个车都靠近停车场,将会出现下面的情况:
上图可以看到两辆车几乎同时进入停车场,且剩余车位仍然显示为1,因为大门没有及时合上。等到两辆车都进入停车场后,稍微晚一点进入的车辆发现了现在车场里面已经没有空位了,如下图所示。
这里停车场出现问题的主要原因在哪里呢?就是他加装了一个大门。我们现在来把停车场给抽象出来,如下表:
例子中的元素 | 计算机 |
---|---|
单个大门的停车场 | 普通虚拟内存 |
单个大门 | 单个进程 |
多个大门的停车场 | 共享内存 |
多个大门 | 多个进程 |
剩余车位显示屏 | 普通变量实现的计数器 |
为了防止同时写入造成冲突,我们约束同一时刻只能有一个进程访问共享内存,一个进程进入后,表示共享资源个数的计数器会从1变成0。上节课我们讲过,对于普通变量实现的计数器的增减操作是非原子操作,会被拆分成3个机器指令。当两个进程同时想要访问共享内存时,如果时间刚刚好相差不大,就会引起两个进程同时获得计数器值为1的情况。就类似于上面的例子中两辆车几乎同时进入停车场。
信号量的使用
我们再来看一下信号量的优点:
信号量是一个计数器,用于管理对共享资源的访问权限。主要特点包括:
(1)是一个非负整数
(2)提供两种原子操作:P(等待:值减1)和V(释放:值加1)
(3)可以用于进程同步和互斥
(4)用于显示资源的数量
看到这里大家应该清楚了,信号量就是用来在进程间计数与同步的,那么让我来看看如何使用它们吧:
创建或获取信号量集合:
int semget(key_t key, int nsems, int semflg);
这些参数真的很多都是老朋友了,包括这些函数的命名方法,当然还是要介绍一下:
key
:键值
nsems
:表示在这个集合中创建多少个信号量
semflag
:状态位
ret
:返回值为信号量集合的ID
使用方法如下面代码块所示:
int main() {
int semid = semget((key_t)1, 1, 0666 | IPC_CREAT);
if (semid == -1) {
printf("sem create fail\n");
return 0;
}
printf("the id of sem is %d\n", id);
return 0;
}
编译后运行程序,输出
lol@hyl:~/work/linux_study/sem$ gcc -o p1 proc1.c
lol@hyl:~/work/linux_study/sem$ ./p1
the id of sem is 1
运行ipcs
指令可以看到:
lol@hyl:~/work/linux_study/sem$ ipcs
------ Message Queues --------
key msqid owner perms used-bytes messages
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
------ Semaphore Arrays --------
key semid owner perms nsems
0x00000001 1 hyl 666 1
到这里,我们ipcs
指令下的三大元老:共享内存,消息队列,信号量就全部和大家见面了。同样,这个信号量是不依赖于进程的,属于操作系统的,因此我们发现主函数执行完成后我们的信号量仍然存在。为了防止发生资源泄露,我们需要删除这个信号量,输入:ipcrm -s 1
,这里-s表示信号量的意思。从输出信息中我们可以发现信号量被删除了。
lol@hyl:~/work/linux_study/sem$ ipcrm -s 1
lol@hyl:~/work/linux_study/sem$ ipcs
------ Message Queues --------
key msqid owner perms used-bytes messages
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
------ Semaphore Arrays --------
key semid owner perms nsems
信号量操作:
int semop(int semid, struct sembuf *sops, unsigned int nsops);
semid
:信号量集合的ID
nsops
:需要操作的信号量的个数,当集合内信号量数量大于1有效,一般另其为1
sops
:struct sembuf
的地址,决定了信号量的行为
这里struct sembuf
的定义为:
struct sembuf {
unsigned short sem_num; // 信号量在集合中的索引
short sem_op; // 操作值
short sem_flg; // 操作标志
};
sem_num
:要操作的信号量的索引,当集合内有多个信号量时才有用
sem_op
:
大于0表示释放资源,信号量值增加sem_op
,
小于0表示获取资源,信号量值减去sem_op
,若操作后为值为负则不进行操作并阻塞线程
等于0表示阻塞等待信号量值为0
sem_flg
:
=0
表示阻塞等待,默认操作
=IPC_NOWAIT
时表示不阻塞,直接返回错误,对应上面sem_op
可能阻塞的情况
=SEM_UNDO
这个不做讨论
信号量控制:
int semctl(int semid, int semnum, int cmd, ...);
semid
:信号量集ID
semnum
:信号量在集合中的索引
cmd
:控制命令
...
:c语言中的变参数,当cmd为某些命令时需要追加参数,此时需要定义一个联合体
这个函数的用法使用代码来展示:
union semun {
int val; // SETVAL用的值
struct semid_ds *buf; // IPC_STAT, IPC_SET用的缓冲区
unsigned short *array; // GETALL, SETALL用的数组
}; // 用于追加参数的一个结构体,必须由用户定义
// 初始化信号量集合中的第一个信号量值为1
union semun arg;
arg.val = 1;
if (semctl(semid, 0, SETVAL, arg) == -1) {
perror("semctl SETVAL failed");
}
// 获取信号量值
int val = semctl(semid, 0, GETVAL);
printf("Current semaphore value: %d\n", val);
// 删除信号量集
if (semctl(semid, 0, IPC_RMID) == -1) {
perror("semctl IPC_RMID failed");
}
简单代码案例
这里只是为了让大家理解信号量的用法,下一节还会有更加合适的案例去帮大家了解信号量。我们使用fork()
创建了两个进程,子进程每两秒释放一个信号量,父进程每0.5秒获取一个信号量。信号量被初始化为5。
这里补充一点,对于fork()函数,我们之前讲过它会将当前进程内部的资源克隆一份。(不了解的同学请移步:linux多线(进)程编程——(2)身外化身fork())但是由于信号量是属于操作系统的,因此它不会fork不会克隆信号量,但是它创建出的子进程会继承信号量的操作权限。
完整代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int main() {
int semid = semget((key_t)1, 1, 0666 | IPC_CREAT);
if (semid == -1) {
printf("sem create fail\n");
return 0;
}
struct sembuf buf = {0};
union sempun {
int val; // 用于SETVAL
struct semid_ds *buf; // 用于IPC_STAT/IPC_SET
unsigned short *array; // 用于GETALL/SETALL
} arg; // 这个联合体用于追加参数,由用户自己定义与创建
arg.val = 5;
if(-1 == semctl(semid, 0, SETVAL, arg)) { // 将信号量值初始化为5
printf("sem init fail!\n");
semctl(semid, 0, IPC_RMID);
return 0;
}
printf("sem init val is %d!\n", arg.val);
pid_t id = fork();
if(id < 0) {
printf("fork ret -1, error!\n");
semctl(semid, 0, IPC_RMID);
return 0;
}
if(id == 0) { // 子进程,每隔2秒释放一个信号量
buf.sem_num = 0;
buf.sem_op = 1;
buf.sem_flg = 0;
while(1) {
semop(semid, &buf, 1);
int val = semctl(semid, 0, GETVAL);
printf("son: I release a semaphore, and the val is %d\n", val);
sleep(2); // 阻塞2秒
}
}
else { // 父进程,每隔0.5秒获取一个信号量
buf.sem_num = 0;
buf.sem_op = -1; // 值小于1,获取信号量
buf.sem_flg = 0; // 设置为阻塞模式
while(1) {
semop(semid, &buf, 1);
int val = semctl(semid, 0, GETVAL);
printf("father: I get a semaphore, and the val is %d\n", val);
usleep(500*1000); // 阻塞0.5s
}
}
semctl(semid, 0, IPC_RMID);
return 0;
}
输出结果与分析
上面程序的执行结果如下所示。可见由于信号量初始值为5,父进程在程序开始时快速获取资源。等到信号量资源耗尽(值为0)时,父进程被阻塞,等待子进程释放资源。有意思的是,当子进程释放资源后,显示信号量的值是0而不是1。原因是由于父进程在阻塞等待,因此当子进程释放资源后,还没进入下一条指令,马上会被父进程拿走。最后子进程看到的信号量的值为0。
lol@hyl:~/work/linux_study/sem$ ./p1
sem init val is 5!
father: I get a semaphore, and the val is 4
son: I release a semaphore, and the val is 5
father: I get a semaphore, and the val is 4
father: I get a semaphore, and the val is 3
father: I get a semaphore, and the val is 2
son: I release a semaphore, and the val is 3
father: I get a semaphore, and the val is 2
father: I get a semaphore, and the val is 1
father: I get a semaphore, and the val is 0
son: I release a semaphore, and the val is 0
father: I get a semaphore, and the val is 0
son: I release a semaphore, and the val is 0
father: I get a semaphore, and the val is 0
son: I release a semaphore, and the val is 0
father: I get a semaphore, and the val is 0
son: I release a semaphore, and the val is 0
father: I get a semaphore, and the val is 0
强调一点,这里对信号量的增加删除操作全部都是由操作系统内核与底层硬件支持的原子操作,不会出现普通变量那种被打断的情况,因此信号量是进程间安全的。下面这张图就表示了使用信号量的车库:
小结
这节课我们讲解了用于实现共享内存进程间同步的信号量。
我们要知道为什么不能使用普通变量来实现进程间计数,以及为什么可以使用信号量实现进程间计数。(原子性)
大家还要熟悉一下相关的信号量的函数。
关于信号量的具体使用,为了方便大家理解,我将在下一节将其与共享内存结合,并且提供几个案例,让大家更好的理解信号量的用法。
传送阵:linux多线(进)程编程——(9)信号量(二)
结束语
在信号量的帮助,共享内存又一次成为了修真界人们经常使用的传音术。