1、引言
在我的前一篇文章中详细介绍了进程间通讯的其中两种方式——管道和消息队列。而这篇文章主要来讲述一下信号量。确切的说,前两种方式主要是为了方便进程间通信,而信号量的作用是对共享资源的互斥访问
进程关系
1、进程同步:进程之间相互合作,协同工作的关系称为进程的同步。简单来说就是多个相关进程在执行次序上的协调,谁先执行谁后执行都有顺序关系。是一种直接制约关系。
2、进程互斥:多个进程因为争夺临界资源而互斥执行称为进程的互斥。是一种间接制约关系
。
如多个人打篮球,篮球是临界资源,形成互斥,对他们形成了一个间接制约关系。工厂上流水线工作,一道工序接着下一道,有明确的次序,形成同步,上一道的工序对下一道工序有直接影响,是一种直接制约关系。
2、概念
为了讲清楚相关概念,我们还是引进一个特殊的场景来加以说明。就比如说打印机,我们有两个进程会同时循环的访问打印机,如下图所示,那么整个过程应该是怎样的呢?
2.1临界资源
简单来说就是同一时刻只允许一个进程访问的资源。就相当于上面案例中的打印机,AB两个进程只能看到这一份公共资源,一次只能被一个进程使用
2.2临界区
临界区就是访问临界资源的代码段。就如上面实例中的两个printf函数
2.3原子操作
在我之前的一篇博文中有详细的介绍,请参见文件操作系统调用。其实简单来说就是不可中断的一个或者一系列操作,即一件事要么做要么不做。就比如说打印机实例,进程只能选择打印或者不打印。
2.4PV操作
信号量只能进行两种操作——等待和发送信号。即P和V。他们的行为如下:
- P(sv):如果sv的值大于0,就给他减一;如果它的值为0就挂起该进程的执行
- V(sv):如果其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给他加一。
比如说上述的这个例子,我们程序的输出结果不能是ABAB混合的方式。
因为一开始打印机是可以访问的所以把信号量置为1,首先A进程P操作访问临界资源,信号量-1.,此时B进程就阻塞住的,不能对临界资源的访问。使用完后执行V操作,信号量又恢复为1;此时B进程就又可以进行访问。
3、信号量的原理
3.1 信号量定义
我们在之前就说了信号量是进程间通讯的一种方式,确切的说信号量的作用是为了处理多个程序同时访问一个共享资源而引发的一系列问题。
信号量的访问机制就是在任一时刻只能有一个执行线程访问代码的临界区域。也就是说信号量是用来协调进程对共享资源的访问的
3.2 原理
- 若信号量的值为正,则进程可以使用该资源,进程的信号量值减1,表示一个资源被使用;
- 若此信号量为0,则进程进入休眠,直到该信号量值大于0;
- 当进程不再使用一个由一个信号控制的共享资源时,该信号量加1,如果有进程正在休眠等待该信号量,则该进程会被唤醒。
上述操作均为原子操作,所以信号量通常在内核中实现。
4、信号量的使用
4.1一些数据结构的定义
内核为每个信号量集合维护了一个semid_ds结构体,具体成员如下:
strcut semid_ds{
struct ipc_perm sem_perm; //权限结构体
time_t sem_otime; //最后一次操作时间
time_t sem_ctime; //最后一次改变时间
unsigned long sem_nsems; //在集合中信号量
};
我们要操作的信号量包含在这个信号量集合中,信号量的操作大多数也是通过这个信号量集合来操作的。
4.2 semget函数
#include <sys/sem.h>
int semget(key_t key, int num_sems, int sem_flags);
该函数主要是创建一个新信号量集或者取得一个已有的信号量集。
key:
用户标识,多个不相关进程可以通过它访问一个信号量,它代表着可能要使用的某个资源。num_sems:
指定需要的信号量数目,它的值几乎总是1sem_flag:
和消息队列的设置一样,标识函数的行为信号量集合的权限,取值如下:
取值 | 含义 |
---|---|
IPC_CREATE | 创建信号量集合 |
IPC_EXCL | 检测信号量集合是否存在 |
位或权限位 | 可以设置信号量集合的访问权限,和其他两个参数可以或表示,取值和open函数的open_t一样,一般为0664 |
如果semget函数用来创建一个新集合,那么内核会自动把新信号量集合的strut semid_ds结构体做以下初始化:
-
初始化ipc_perm结构体,该结构体中的mode成员按semget函数的flag参数中的相应权限位设置。
-
sem_otime设置为0。
-
sem_ctime设置为当前时间。
-
sem_qnsems设置为semget函数的nsems参数的值。
-
返回值:
成功返回一个相应信号标识符(非零),失败返回-1。
4.3 semop函数
#include <sys/sem.h>
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
该函数具有原子性,要么执行所有的操作,要么一个也不执行,作用是改变信号量的值。
sem_id:
由semget返回的信号量标识符;sem_opa:
表示一个由sembuf结构表示的信号量操作数组,数组中的每个sembuf结构体对应一个信号量ID,以及对该信号量ID进行操作的标志:
struct sembuf{
unsigned short sem_num;//信号量ID
short sem_op; //信号量操作
short sem_flg;//操作标志
};
其中结构体中成员的取值有不同的使用:
(1)sem_op:
1.如果sem_op为正值,如1,表示释放sem_num对应的信号量的资源(信号量的值增加),如同V操作。
2.如果sem_op为负值,如-1,表示获取sem_num对应的信号量资源(信号量的值减少),如同P操作。若此时已经没有资源了进行-1操作:
-
如果指定了IPC_NOWIT,会出错返回。
-
未指定,进程会被挂起,直到有资源或捕捉到信号结束挂起。
(2)sem_flag:
- 默认填0。
- SEM_UNDO:进程退出后,该进程对sem进行的操作都被撤销,例如对信号量值进行加1或减1操作,则进程退出后这些操作都被撤销。
3.如果sem_op为0,表示调用进程希望该信号量值变为0。如果当前信号量是0,则此函数立即返回。
um_sem_ops:
规定该数组中操作的数量。- 成功返回0,失败返回-1。
4.4 semctl函数
# include<sys/sem.h>
int semctl(int semid,int semnum,int cmd,……/*union semun arg*/);
cmd参数为SETAVL,IPC_RMID参数,函数返回值成功为0,失败返回-1
直接控制信号量的信息,可以对信号量集合/信号量进行不同的操作,如初始化、删除等。
参数:
-
sem_id:
信号量标识符; -
sem_num:
用来指定该信号量集合中的某一特定信号量成员,也就是信号量对应的ID。 -
command:
通常是下面两个值中的其中一个:
3.1 SETVAL:用来把信号量初始化为一个已知的值
3.2 IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。