要理解信号量,先要理解以下几个概念:
1、 临界资源:同一时刻,只能被一个进程访问的资源。2、临界区:访问临界资源的代码区域。
3、原子操作:任何情况下都不能被打断的操作。
4、内核对象:用于对进程间通讯时,多进程能同时访问同一资源的记录。
做个比方,有一个停车位,同一时刻只能停放一辆车,那么该停车位就相当于一个临界资源,车从停放到开走所使用临界资源的过程就是我们所说的临界区,而且在停放的全过程中别的车不能停放就相当于原子操作。可是,当有第二辆车来的时候,并不知道停车位已经被占了,此时,就产生了竞争行为,显然,很不好,因此为了和平的使用停车位,我们有一个停车场看门人,他会时实的记录停车位的使用情况,占用一个车位就减1,腾出一个就加1。类似的信号量就如我们所说的停车场看门人。
假如进程A先持有信号量M,然后进程B试图获得A正在使用的M(假设此时M资源为1),信号量会把B放入等待队列,只有当A释放掉信号量M时,B进程才会获得,从而执行自己的代码。
信号量的这种特性,我们得到信号量的作用:控制进程同步。
信号量的函数操作:
以下函数所需头文件都有
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
(1)、创建或者获取:
函数 | int semget((key_t) key,int nsems, int semflg) |
key的含义 | 信号量的键值,不同信号量使用同一个信号量键值来获得同一个信号量。 |
nsems的含义 | 需要创建信号量的数目,通常取值为1。 |
semflg的含义 | 与open函数权限位一样。使用IPC_CREAT标志创建信号量,如果同一键值的信号量存在,返回原有信号量的ID;不存在,创建一个新的信号量。同时使用IPC_EXCL | IPC_CREAT标志,则创建一个新的唯一的信号量,如果存在,则出错。 |
返回值 | 成功返回一个非负数的信号量标示符;失败返回-1. |
(2)、初始化和删除
创建的信号量不需要初始换;获取的信号量一定要初始话化。
函数 | int semctl(int semid,int semnum,int cmd,union semun arg) |
semid | 信号量标示符。 |
semnum的含义 | 信号量编号,可以理解为数组的下标,通常取值为0,表示要操作第一个信号量。 |
cmd的含义 | 采取的操作:IPC_SETVAL表示将信号量设为arg中val的值;IPC_RMID表示从系统中删除。 |
arg的含义 | 定义信号量时是一个联合体,结构为: union semun{ int val; struct semid_ds *buf; unsigned short *array; } |
返回值 | 成功返回0;失败返回-1。 |
(3)、PV操作
P操作:减1操作。
V操作:加1操作。
函数 | int semop(int semid,struct sembuf *sops,size_t num_sem_ops) |
semid | 信号量标识符 |
sops的含义 | 指向一个结构体的指针; struct sembuf{ short sem_num;//信号量编号 short sem_op;//为-1时是p操作;为1时是v操作; short sem_flg;/*通常设为SEM_UNDO,表示在进程中如果没有释放而退出时,系统自动释放该进程中未释放的信号量;这个设置应该作为一个良好的习惯。*/ } |
num_sem_ops的含义 | 操作sops中的元素数目。 |
返回值 | 成功返回0;失败返回-1。 |
这样冗杂的操作,我们利用封装函数,来提供给用户一个简单的接口更好一点。
封装函数:
在mysem.h中对每个函数声明。在mysem.c中实现对每个函数的封装,如下:
来写两个例子对信号量加以运用:
例一:A进程接受用户输入,当用户输入“ok”时,B进程输出100以内的素数。此时可以理解为A和B进程之间是服务关系,我们把v.val设为0,既停车位刚开始为0,等到输入ok时,产生停车位。
进程A:(sema.c)
进程B:(semb.c)
显示结果:
例二:我们将封装函数中v.val改为1;利用父子进程来实现两进程之间的竞争关系,如下:
当不使用信号量时(mysem_text1.c)与使用信号量时(mysem_text2.c),如下:
对比
运行: