文章目录:
一、相关基础概念
(一)进程关系
1.进程同步:进程之间相互合作,协同工作的关系称为进程的同步。简单来说就是多个相关进程在执行次序上的协调,谁先执行后执行都有顺序。是一种直接制约关系
。
2.进程互斥:多个进程因为争夺临界资源而互斥执行称为进程的互斥。是一种间接制约关系。
如多个人打篮球,篮球是临界资源,形成互斥,对他们形成了一个间接制约关系。工厂上流水线工作,一道工序接着下一道,有明确的次序,形成同步,上一道的工序对下一道工序有直接影响,是一种直接制约关系。
(二)临界资源&临界区
1.临界资源:在操作系统中,把那些可以被进程共享的资源(文件,打印机等),但是在同一时刻只允许一个进程/线程访问的资源
统称为临界资源或共享变量。
2.临界区:访问临界资源的那段代码。
(三)原子操作
原子操作是指不会被进程/线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何进程切换。故原子操作要不然不做,要不然一直做到结束。
(四)PV操作
这两个字母来源于荷兰语单词:passeren传递,就好像进入临界区;vrijgeven释放,退出临界区。假设信号量为SV,那么P、V操作含义如下:
- P(SV):如果SV的值大于1,进行减一操作,表示获得资源;如果SV的值为0,则挂起进程的执行,即阻塞,因为目前已经没有资源了。
- V(SV):如果当前有挂起的进程在等待资源,那么执行V就会将它唤醒;如果没有,SV加一,即释放资源。
P操作会出现阻塞,V操作永远不会阻塞。
二、信号量概念
(一)定义
信号量和前面介绍的IPC(管道,消息队列)不同,它是一个计数器,用于多线程对共享数据对象的访问。和前面的有本质的不同,前面学的管道,消息队列是为了传送数据,而信号量是以保护共享资源(临界资源)或保证进程同步为目标的,不存储进程间的通信数据。
故信号量机制一般用在进程或线程之间的同步与互斥。
信号量是一个特殊的计数器,一般取正数值。它的值代表允许访问的资源数目。一般有两种常用取值:
- 信号量的值如果只取0,1,将其称为
二元信号量
,控制单个资源,初始值为1。 - 如果信号量的值大于1,则称之为
计数信号量
,说明有多个临界资源单位可共享。
(二)使用
使用信号量获得共享资源,进程需要执行下列操作:
- 测试控制临界资源的信号量。
- 若此信号量的值为正,则进程可以使用该资源,进程P操作将信号量减一,表示它使用了一个资源单位。
- 若此信号量的值为0,则进程进入休眠状态,因为此时没有资源可以获取。如果进程被唤醒后,它返回至第(1)步。
- 当进程不再使用由一个信号量控制的临界资源时,V操作将信号量值加1。如果有进程正在阻塞等待此信号量,则唤醒它。
上述操作均为原子操作,所以信号量通常在内核中实现。
(三)特点
- 本质是一个计数器,内存中有多少个临界资源,信号量的数字就是多少。
- 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作
- 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
- 信号量不能传递复杂消息,只能用来同步。
三、信号量函数
内核为每个信号量集合维护了一个semid_ds结构体,具体成员如下:
strcut semid_ds{
struct ipc_perm sem_perm; //权限结构体
time_t sem_otime; //最后一次操作时间
time_t sem_ctime; //最后一次改变时间
unsigned long sem_nsems; //在集合中信号量
};
我们要操作的信号量就包含在这个信号量集合中,信号量的操作大多数也是通过这个信号量集合来操作的。
(一)创建/获取信号量集合semget函数
对信号量进行操作,我们先创建或获得一个信号量ID,那么调用semget函数,函数原型如下:
#include<sys/sem.h>
int semget(key_t key,int nsems,int flag);
成功返回信号量ID,失败出错返回-1
参数:
- key: 有两种办法可以获得,在消息队列讲解中我们已详细写出两种办法,
两个进程使用相同key值,就可以使用同一个信号量。
- nsems:如果是创建新信号量集合,那么nsems代表新信号量集合中的信号量的数目。如果是获取当前存在的信号量集合,那么此参数为0。
- 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参数的值。
(二)设置信号量集合semctl函数
semctl函数可以对信号量集合/信号量进行不同的操作,如初始化,删除等,函数原型为:
# include<sys/sem.h>
int semctl(int semid,int semnum,int cmd,……/*union semun arg*/);
cmd参数为SETAVL,IPC_RMID参数,函数返回值成功为0,失败返回-1
参数:
-
semid: 信号量集合ID
-
semunm: 用来
指定
该信号量集合中的某一特定信号量成员
,也就是信号量对应的ID。 -
cmd参数: 用来指定对信号量集合的操作,通常用下面两个值:
(1) SETVAL: 在信号量第一次使用之前,把信号量初始一个已知的值,通过自定义union semun中的val成员设置。
(2)IPC_RMID: 删除信号量集合,立即发生。 -
arg参数: 此参数可选的,取决于请求的命令,一般初始化时使用,如果使用该参数,必须要自己定义联合体,表示信号量的相关信息。
union semun{ int val; //信号量的值 struct semid_ds* buf; // ipc_stat,ipc_set的缓冲区 unsigned short* array; //SETALL,GETALL数组 struct seminfo* _buf; //ipc_info缓冲区(Linux专用) };
注意:arg指向的联合体名称,成员均不固定,可以自行选择需要的。一般联合体成员包含信号量值即可。
(三)操作信号量semop函数
利用semop函数可以改变信号量的值,其函数原型为:
# include<sys/sem.h>
int semop(int semid,struct sembuf* sops,unsigned nsops);
成功返回0,出错返回-1
semop函数具有原子性,要不然执行所有操作,要不然一个也不执行。
参数:
-
semid: 信号量集合ID。
-
sops参数: 参数为一个指针,指向一个由struct 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,会出错返回。
- 未指定,进程会被挂起,知道有资源或捕捉到信号结束挂起。
3.如果sem_op为0,
表示调用进程希望该信号量值变为0。如果当前信号量是0,则此函数立即返回。(2)sem_flag:
- 默认填0。
- SEM_UNDO:进程退出后,该进程对sem进行的操作都被撤销,例如对信号量值进行加1或减1操作,则进程退出后这些操作都被撤销。
-
nsops参数:
对应参数sops数据的元素个数。
(四)封装系统调用函数实现一系列功能
在进行进程同步控制时,经常说到P、V操作,那么如何用上述的系统调用封装一系列的函数,让我们使用起来方便。根据需求我们将其分为4类函数:创建/获取信号量;P操作;V操作;删除信号集合。我们将表示信号量信息的union semun联合体(因为业务简单,所以联合体中只包含信号量的值val即可)和函数声明写在自己创建的.h文件中。
# include