什么是信号量
信号量与其他IPC对象不同,它是一个计数器,用于多个进程对共享数据对象的访问,它的本质是一种数据操作锁,它不像消息队列和管道那样具有数据交换的功能,而是通过控制其他的通信资源(文件,外部设备)来实现进程间通信。
如何通过信号量来控制进程间通信
为了获得共享资源,进程需要执行下列操作:
(1)、测试控制该资源的信号量;
(2)、若此信号量为正,则进程可以使用该资源,在这种情况下进程会将信号量值减一,表示它使用了一个资源单位;
(3)、若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0,进程才被唤醒,此时又返回到步骤(1)。
注:信号量的测试和减一操作应当是原子操作,为此信号量通常是在内核中实现的。
为什么要使用信号量
使用信号量是为了防止因多个程序同时访问一个共享资源而引发的一系列问题,而信号量就是这样的一种方法,它可以保证任一时刻只能有一个执行线程访问代码的临界区域(临界区域是执行数据更新的代码需要独占式的执行),也就是说信号量是用来协调进程对共享资源的访问的。
操作系统是如何对信号量进行管理的
首先,内核为每个信号量集合维护着一个semid_ds结构
这个结构中包含了每个IPC对象都会包含的成员那就是ipc_perm(这个结构主要规定了IPC对象的权限和所有者),另外还含有信号量集合中元素数量sem_nsems成员,以及信号量处理的相关时间。
每个信号量其实是由一个无名结构体所表示的,它包含下列成员
struct {
unsigned short semval;
pid_t sempid;
unsigned short semncnt;
unsigned short semzcnt;
}
另外,操作系统还提供了相关的信号量系统调用接口,来使用户对信号量进行管理。
信号量接口函数
- 信号量的创建与获得
int semset(key_t key,int nsems,int flag);
//若成功,返回信号量ID,若失败,返回-1
当创建一个新的信号量集合时,要对semid_ds结构下列成员赋初值
sem_otime设置为0; //sem_otime是最后一次semop()的时间 sem_ctime设置为当前时间; //是最后一次调用semctl()的时间
sem_nsems设置为nsems; //信号量集合中的信号量数,如果是创建则必须要指定nsems,如果引用现有的那么将nsems设置为0即可。
- semctl包含了多种信号量操作
int semctl(int semid ,int semnum, int cmd,.../*union semun arg*/ );
该函数包含了对信号量的多种操作其中第四个参数是可选的,是否使用取决于cmd
如果使用了第四个参数,那么它的类型是 union semun;
cmd:cmd参数通常指定下列十种参数的一种
IPC_STAT:对此集合取semid_ds结构,并存储在arg.buf中;
IPC_SET:按arg.buf指向的结构中的值,设置此集合相关的结构中的sem_perm.uid,sem_perm.gid和sem_perm.mode字段。
IPC_RMID:从系统中删除该信号量的集合,删除立即生效。
GETVAL:返回成员semnum的semval值,由arg.va指定(这条在对信号量的初始化中用到);
SETVAL:设置成员semnum的setval值;
GETPID:返回成员semnum的sempid值;
GETALL:取信号量集合中所有的信号量值,这些值存储在arg.array中;
SETALL:将该集合中的所有信号量值设置成指向arg.array指向的数组中的值;
- semop函数
int semop(int semid, struct sembuf semoparry[], size_t nops);
//若成功,返回0,若出错,返回-1
nops:nops,规定了该数组(semoparry[])中操作信号量的数量;
* semoparry*:是一个类型为struct sembuf的数组,sembuf的成员如下
sembuf是一个表示信号量操作的数组;
关于对信号量的操作主要与sembuf中的sem_op成员有关:
(1):若sem_op为正值,此时对应于进程释放的或占用的资源数(所以信号量的V操作的sem_op一般为正值,),sem_op值会加到信号量的值上;如果指定了undo标志(对应于sem_flg成员的SEM_UNDO位),则也从该进程的此信号量调整值减去sem_op.
(2):若sem_op为负值,则表示调用进程要获取由该信号量控制的资源(所以我们的P操作的sem_op一般为负值,并且这个值的绝对值要小于等于semval).
(3):若sem_op为0:表示调用进程希望等待到该信号量的值变为0:
SEM_UNDO
SEM_UNDO是semun中的sem_flg成员的标志位,设定该标志位主要是用来异常退出进程时进行调整,操作系统所设置的调整如下:
由于信号量的生命周期是“随系统”,即如果我们创建了信号量集合,但是没有对其进行删除,那么信号量便会一直存在在操作系统中,直到操作系统关闭或者用户使用命令删除为止,而如果我们为信号量设定了SEM_UNDO标志,如果sem_op的值小于0(即此时调用进程占有临界区域),当该进程终止时,内核都会检验进程是否还有尚未处理的信号量调整值,如果有则进行相应的调整。
当操作信号量(semop)时,sem_flg可以设置SEM_UNDO标识;SEM_UNDO用于将修改的信号量值在进程正常退出(调用exit退出或main执行完)或异常退出(如段异常、除0异常、收到KILL信号等)时归还给信号量。
测试用例
我们在父进程中用fork创建子进程,然后父进程向屏幕上打印AA,子进程向屏幕上打印BB,观察在设置信号量和没有设置信号量时所出现的不同的情况,案例源代码如下:
信号量头文件
/*************************************************************************
> File Name: MySem.h
> Author: LZH
> Mail: [email protected]
> Created Time: 2017年02月16日 星期四 02时47分13秒
************************************************************************/
#ifndef __SEM_H__
#define __SEM_H__
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define PATHNAME "."
#define PROJID 0x6666
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(Linux-specific) */
};
int CreatSem(int nsems);
int InitSem(int semid);
int GetSemID();
int P(int semid,int which);
int V(int semid,int which);
int DestorySem(int semid);
#endif //__SEM_H__
信号量源文件
/*************************************************************************
> File Name: MySem.c
> Author: LZH
> Mail: [email protected]
> Created Time: 2017年02月16日 星期四 02时49分08秒
************************************************************************/
#include "MySem.h"
int InitSem(int semid)
{
union semun un;
un.val=1;
int ret = semctl(semid,0,SETVAL,un);
if(ret < 0){
perror("semctl ...\n");
return -1;
}
return 0;
}
static int CommSem(int nsems,int flags)
{
key_t _k=ftok(PATHNAME,PROJID);
if(_k < 0){
perror("ftok error..\n");
return -1;
}
int semid=semget(_k,nsems, flags);
if(semid < 0){
perror("semget error..\n");
return -2;
}
return semid;
}
int CreatSem(int nsems)
{
return CommSem(nsems,IPC_CREAT | IPC_EXCL |0666);
}
int GetSemID()
{
return CommSem(0,0);
}
int SemOp(int semid,int op,int num)
{
struct sembuf buf;
buf.sem_op=op;
buf.sem_num=num;
buf.sem_flg=0;
int ret = semop(semid,&buf,1);
if(ret < 0){
perror("Semop..\n");
return -1;
}
return 0;
}
int P(int semid,int which)
{
return SemOp(semid,-1,which);
}
int V(int semid,int which)
{
return SemOp(semid,1,which);
}
int DestorySem(int semid)
{
int ret = semctl(semid,0,IPC_RMID);
if(ret < 0){
perror("semctl..\n");
}
return 0;
}
* 案例测试文件*
#include "MySem.h"
int main()
{
int semid=CreatSem(10);
printf("Semid:%d\n",semid);
InitSem(semid);
pid_t id = fork();
if(id==0){
while(1){
P(semid,0);
usleep(5300);
printf("A ");
fflush(stdout);
usleep(5000);
printf("A ");
usleep(10000);
fflush(stdout);
V(semid,0);
}
}
else{
while(1){
P(semid,0);
usleep(10300);
printf("B ");
fflush(stdout);
usleep(10000);
printf("B ");
usleep(10000);
fflush(stdout);
V(semid,0);
}
}
DestorySem(semid);
return 0;
}
如果我们没有对父子进程的临界区域(打印字符的代码)使用信号量的相关操作,测试结果如下:
我们会发现不像我们想象的那样出现成对的AA和BB,这时因为父子进程对输出屏幕这个共享竞争的结果,如果我们使用了信号量,所得到的测试结果如下:
这时输出屏幕上出现了成对的AA和BB,这样就体现了信号量原子性操作的价值,原子性操作即要么不做,要做就一次做完。
总结
本文我们主要提到了IPC对象之一——信号量的实现机制和相关操作,我们要能明白是信号量不是进程之间传递消息的共享资源,而是管理进程之间交换数据的资源的计数器,此外内核还提供了相关的接口函数来使用户能够用信号量管理临界区域,避免出现多个程序同时访问一个共享资源所引发的一系列问题,在这些接口函数中,我们特别要注意semop函数,它保证了信号量进行的是原子操作。