信号量的本质是一种数据结构操作锁,它本身不具有数据交换的功能,而是通过控制其他的通信资源(文件,外部设备)来实现进程间通信,它本身只是一种外部资源的标识,信号量在此过程中负责数据结构的互斥,同步等功能。
当请求一个使用信号量来表示的资源时,进程需要先读取信号量的值来判断资源是否可用。大于0,资源可以请求,等于0,无资源可用,进程会进入睡眠状态直至资源可用。当进程不再使用一个信号量控制共享资源时,信号量的值+1,对信号量的值进行的增减均为原子操作,这是由于信号量主要的作用是维护资源的互斥或多进程的同步访问。而在信号量的创建及初始化上,不能保证操作均为原子性。
信号量本质上是一个计数器,信号量是描述临界资源当中临界树木的计数器。
二元信号量则就是非0即1的信号量,通常实现互斥功能。
信号量的工作原理:由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行。
V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1。
也就相当于,当两个进程共享信号量w,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号,这是第二个进程可以恢复。
下面是信号量的一些操作接口:
int semget(key_t key,int nsems,int semflg); 创建一个信号量,返回值成功返回一个信号量集,第二个参数代表信号量集的个数。
int semctl(int semid,int semnum,int cmd,...); semctl()在semid标识的信号量集上,或者该集合的第semnum个信号量上执行cmd指定的控制命令。(信号量集合索引起始于0)根据cmd不同,这个函数有三个或四个参数。当有四个参数,第四个参数类型是union联合体。
union semun{
int val; //使用的值
struct semid_ds *buf; //IPC_STAT, IPC_SET使用缓存区
unsigned short *array //GETALL, SETALL使用的数组
struct seminfo* _buf //IPC_INFO使用缓冲区
}
该联合体没有定义在任何系统头文件中,因此得用户自己声明。
int semop(int semid,struct sembuf *sops,unsigned nops); P,V操作调用接口,其中第一个参数是信号量集标号,第二个参数定义一个struct sembuf*类型的操作数组,第三个参数是数组的个数。semop允许调用一次对多个信号量进行操作。
查看信号量集命令:ipcs -s
删除信号量集命令:ipcrm -s semid
我们来看下面的程序:
#include"comm.h"
int main()
{
// int semid=createSemSet(1);
// initSemSet(semid,0,1);
pid_t id=fork();
if(id==0){
// int _semid=getSemSet(1);
while(1){
// P(_semid,0);
printf("A");
fflush(stdout);
usleep(123456);
printf("A ");
fflush(stdout);
usleep(123456);
// V(_semid,0);
}
}else{
//father
while(1){
// P(semid,0);
printf("B");
fflush(stdout);
usleep(23456);
printf("B ");
fflush(stdout);
usleep(23456);
// V(semid,0);
}
}
// destroySemSet(semid);
return 0;
}
当没有信号量一些操作函数时,程序结果如下:AB乱序打印
下面程序实现了信号量的操作:
// comm.h
#ifndef _COMM_H_
#define _COMM_H_
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#define PATHNAME "."
#define PROJ_ID 0x0666
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *_buf;
};
int createSemSet(int nums);
int getSemSet(int nums);
int initSemSet(int semid,int which,int val);
int P(int semid,int num);
int V(int semid,int num);
int destroySemSet(int semid);
#endif
// comm.c
#include"comm.h"
static int commSemSet(int nums,int flags)
{
key_t _k=ftok(PATHNAME,PROJ_ID);
if(_k<0){
perror("ftok");
return -1;
}
int semid=semget(_k,nums,flags);
if(semid<0){
perror("semget");
return -2;
}
return semid;
}
int createSemSet(int nums)
{
return commSemSet(nums,IPC_CREAT|IPC_EXCL|0666);
}
int getSemSet(int nums)
{
return commSemSet(nums,IPC_CREAT);
}
int initSemSet(int semid,int which,int val)
{
union semun _un;
_un.val=val;
int ret=semctl(semid,which,SETVAL,_un);
if(ret!=0){
perror("semctl");
return -3;
}
return 0;
}
static int commPV(int semid,int num,int op)
{
struct sembuf _sf;
_sf.sem_num=num;
_sf.sem_op=op;
_sf.sem_flg=0;
if(semop(semid,&_sf,1)!=0){
perror("semop");
return -1;
}
return 0;
}
int P(int semid,int num)
{
return commPV(semid,num,-1);
}
int V(int semid,int num)
{
return commPV(semid,num,1);
}
int destroySemSet(int semid)
{
if(semctl(semid,0,IPC_RMID)<0){
perror("semctl");
return -6;
}
return 0;
}
// test测试用例
#include"comm.h"
int main()
{
int semid=createSemSet(1);
initSemSet(semid,0,1);
pid_t id=fork();
if(id==0){
//child
int _semid=getSemSet(1);
while(1){
P(_semid,0);
printf("A");
fflush(stdout);
usleep(123456);
printf("A ");
fflush(stdout);
usleep(123456);
V(_semid,0);
}
}else{
//father
while(1){
P(semid,0);
printf("B");
fflush(stdout);
usleep(23456);
printf("B ");
fflush(stdout);
usleep(23456);
V(semid,0);
}
}
destroySemSet(semid);
return 0;
}
我们来看实现信号量操作的程序运行结果:AB有序打印
当我们再次运行程序时,则发生创建失败,查看信号量,则发现系统已存在了刚刚创建的信号量。如下图:
从而说明,当信号量被创建时,进程运行结束时,信号量不会随进程消失,所以信号量的生命周期是随内核。
那么这里有一个问题:当进程异常退出时,没有进行V操作,该如何解决?
这时,我们可以将信号量semflg标志位设置成SEM_UNDO,当操作信号量时,将semflg设置成SEM_UNDO标识,SEM_UNDO将用于修改的信号量值在进程正常退出时(调用exit退出或main执行完)或异常退出时(如段异常,除0异常,收到KILL信号等)时归还给信号量。
如信号量初始值是20,进程以SEM_UNDO方式操作信号量减2,减5,加1;在进程未退出时,信号量变成20-2-5+1=14;在进程退出时,将修改的值归还给信号量,信号量变成14+2+5-1=20。