信号量及信号量上的操作是E.W.Dijkstra 在1965 年提出的一种解决进程间同步、互斥问题的较通用的方法,并在很多操作系统中得以实现,Linux 改进并实现了这种机制。它的提出是为了防止出现因多个程序同时访问临界资源如显示器、打印机)而引发的一系列问题。
什么是信号量
先举一个例子:
假如有一个停车场,该停车场只有一个车位,一开始该停车场是空的。某个时刻来了一辆车,然后看门人发现停车场空位数量为1,则允许该车进入停车场并将空车位数量减1,过了一会儿又有一辆车过来,但由于此时停车场空车位数量为0,则该车需在外面等待,直到该停车场空车位数为1,它才可以进入。又过了一会儿,停车场中的车离开了,看门人得知后先观察是否有车在外面等待,如果有则让等待的车进入停车场,否则,他将停车场空车位数加1。
在这个停车场系统中,车位是临界资源,每辆车好比一个进程,看门人起的就是信号量的作用。
信号量(semaphore)实际是一个整数,它的值由多个进程进行测试(test)和设置(set)就每个进程所关心的测试和设置操作而言,这两个操作是不可中断的,或称“原子”操作,即一旦开始直到两个操作全部完成。测试和设置操作的结果是:信号量的当前值和设置值相加,其和或者是正或者为负。根据测试和设置操作的结果,一个进程可能必须睡眠,直到有另一个进程改变信号量的值。
信号量的本质是一种数据操作锁,它本身不具有数据交换的功能,而是通过控制其他的通信资源(⽂文件,外部设备)来实现进程间通信,它本身只是一种外部资源的标识。信号量在此过程中负责数据操作的互斥、同步等功能。
信号量结构
Linux 中信号量是通过内核提供的一系列数据结构实现的,这些数据结构存在于内核空间。
下面是定义于linux/include/uapi/linux/sem.h
中的部分信号量相关结构体,还有一些结构被定义于相关的系统调用中:
#include <linux/ipc.h>
/*表示信号量集和的数据结构*、
struct semid_ds {
struct ipc_perm sem_perm; /* IPC权限(每个IPC对象都有一个) .. see ipc.h */
__kernel_time_t sem_otime; /* 最后一次对信号量的操作时间 */
__kernel_time_t sem_ctime; /* 对这个结构的最后一次修改时间 */
struct sem *sem_base; /* 在信号量数组中指向第一个信号量的指针 */
struct sem_queue *sem_pending; /* 待处理的挂起操作 */
struct sem_queue **sem_pending_last; /* 最后一个挂起操作*/
struct sem_undo *undo; /* 在该数组上的undo请求*/
unsigned short sem_nsems; /* 在信号量数组上的信号量的号 */
};
/* Include the definition of semid64_ds */
#include <asm/sembuf.h>
/* 信号量操作时系统调用所需要的结构体 */
struct sembuf {
unsigned short sem_num; /* 信号量在数组中的下标*/
short sem_op; /* 对信号量的操作*/
short sem_flg; /* 操作标记 */
};
/*
下面这个联合在某些Linux版本中没有被定义在任何头文件中,因此需要用户自己声定义明*/
/* arg for semctl system calls. */
union semun {
int val; // 使⽤用的值
struct semid_ds *buf; /*IPC_STAT、IPC_SET 使⽤用缓存区*/
unsigned short *array; /* GETALL,、SETALL 使⽤用的数组*/
struct seminfo *__buf; /* IPC_INFO(Linux特有) 使⽤用缓存区量信息*/
};
struct seminfo {
int semmap;
int semmni;
int semmns;
int semmnu;
int semmsl;
int semopm;
int semume;
int semusz;
int semvmx;
int semaem;
};
#define SEMMNI 32000 /* <= IPCMNI max # of semaphore identifiers */
#define SEMMSL 32000 /* <= INT_MAX max num of semaphores per id */
下面这个最结构在Linux最新的信号量结构中没有出现,也许是我没有找到,
/*每个信号量的数据结构*/
struct sem
{
int semval; /* 信号量的当前值 */
int sempid; /*在信号量上最后一次操作的进程识别号*
};
以及下面这个结构:
/*系统中每一信号量集合的队列结构(sem_queue)*/
struct sem_queue {
struct sem_queue * next; /* 队列中下一个节点 */
struct sem_queue ** prev; /* 队列中前一个节点, *(q->prev) == q */
struct wait_queue * sleeper; /* 正在睡眠的进程 */
struct sem_undo * undo; /* undo 结构*/
int pid; /* 请求进程的进程识别号 */
int status; /* 操作的完成状态 */
struct semid_ds * sma; /*有操作的信号量集合数组 */
struct sembuf * sops; /* 挂起操作的数组 */
int nsops; /* 操作的个数 */
};
几个结构之间的关系:
可以看到,在semid_ds
中定义了个指向sem
信号量的数组,允许操作这些信号量集合的进程可以利用系统调用执行操作。还有指向sem_queue
信号量队列的指针,这些指针则提供对于信号量的挂起操作。下图给出几个结构体之间的关系:
注意,信号量与信号量集合的区别,从上面可以看出,信号量用“sem” 结构描述,而信号量集用“semid_ds”结构描述,实际上,在后面的讨论中,我们以信号量集为讨论的主要对象。
信号量工作原理
由于信号量只能进⾏行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行V(sv):如果有其他进程因等待sv⽽而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.举个例⼦子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻⽌止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。
系统调用segget()
为了创建一个新的信号量集合,或者存取一个已存在的集合,要使用segget()系统调用,其描述如下:
函数原型:int semget ( key_t key, int nsems, int semflg );
返回值: 如果成功,则返回信号量集合的IPC 识别号;如果为-1,则出现错误。
semget()中的第1 个参数是键值,这个键值要与已有的键值进行比较,已有的键值指在内核中已存在的其他信号量集合的键值。对信号量集合的打开或存取操作依赖于semflg 参数的取值,该参数可以取如下值。
• IPC_CREAT :如果内核中没有新创建的信号量集合,则创建它。
• IPC_EXCL :当与IPC_CREAT 一起使用时,如果信号量集合已经存在,则创建失败。
如果IPC_CREAT单独使用,semget()为一个新创建的集合返回标识号,或者返回具有相同键值的已存在集合的标识号。如果IPC_EXCL 与IPC_CREAT 一起使用,要么创建一个新的集合,要么对已存在的集合返回-1。IPC_EXCL 单独是没有用的,当与IPC_CREAT 结合起来使用时,可以保证新创建集合的打开和存取。作为System V IPC 的其他形式,一种可选项是把一个八进制与掩码或,形成信号量集合的存取权限。
第3 个参数nsems 指的是在新创建的集合中信号量的个数。其最大值在“linux/include/uapi/linux/sem.h
中定义。
#define SEMMNI 32000 /* <= IPCMNI max # of semaphore identifiers */
#define SEMMSL 32000 /* <= INT_MAX max num of semaphores per id */
例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int main()
{
key_t _key = ftok(".", 0x6666);//获取一个唯一的key值
int sem_id = semget(_key, 1, IPC_CREAT|IPC_EXCL|0666);创建一个初始个数为1,权限为0666的信号量集
printf("%d\n", sem_id);//打印该信号量id
return 0;
}
注意,这个例子显式地用了0666权限。这个函数要么返回一个集合的标识号,要么返回-1 而出错。键值必须传递给它,信号量的个数也传递给它,这是因为如果创建成功则要分配空间.
运行结果如下:
系统调用semop()
原型:int semop ( int semid, struct sembuf *sops, unsigned nsops);
返回: 如果所有的操作都执行,则成功返回0。如果为-1,则出错。
semop()中的第1 个参数(semid)是集合的识别号(可以由semget()系统调用得到)。第2 个参数(sops)是一个指针,它指向在集合上执行操作的数组。而第3 个参数(nsops)是在那个数组上操作的个数。sops 参数指向类型为sembuf 的一个数组,这个结构在“linux/include/uapi/linux/sem.h
中声明,是内核中的一个数据结构,描述如下:
struct sembuf {
ushort sem_num; /* 在数组中信号量的索引值 */
short sem_op; /* 信号量操作值(正数、负数或0) */
short sem_flg; /* 操作标志,为IPC_NOWAIT 或SEM_UNDO*/
};
如果sem_op 为负数,那么就从信号量的值中减去sem_op 的绝对值,这意味着进程要获取资源,这些资源是由信号量控制或监控来存取的。如果没有指定IPC_NOWAIT,那么调用进程睡眠到请求的资源数得到满足(其他的进程可能释放一些资源)。如果sem_op 是正数,把它的值加到信号量,这意味着把资源归还给应用程序的集合。最后,如果sem_op 为0,那么调用进程将睡眠到信号量的值也为0,这相当于一个信号量到达了100%的利用。
系统调用semop()
原型:int semctl ( int semid, int semnum, int cmd, union semun arg );
返回值: 成功返回正数,出错返回-1。
注意,semctl()是在集合上执行控制操作,比如当你要初始化集合或者获取集合信息时就可以还是用该函数。
semctl()的第1 个参数(semid)是集合的标识号,第2 个参数(semnum)是将要操作的信号量个数,从本质上说,它是集合的一个索引,对于集合上的第一个信号量,则该值为0。
• cmd 参数表示在集合上执行的命令,这些命令及解释如下
IPC_SET设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。
IPC_RMID:将信号量集从内存中删除。
GETALL:用于读取信号量集中的所有信号量的值。
GETNCNT:返回正在等待资源的进程数目。
GETPID:返回最后一个执行semop操作的进程的PID。
GETVAL:返回信号量集中的一个单个的信号量的值。
GETZCNT:返回这在等待完全空闲的资源的进程数目。
SETALL:设置信号量集中的所有的信号量的值。
SETVAL:设置信号量集中的一个单独的信号量的值。
• arg 参数的类型为semun,这个特殊的联合体在include/linux/sem.h
中声明(但在centos
下系统中并没有这个联合体,需要自己定义,反正我用的centos6.5
下并没有),对它的描述如下:
/* arg for semctl system calls. */
union semun {
int val; /* value for SETVAL */
struct semid_ds *buf; /* buffer for IPC_STAT & IPC_SET */
ushort *array; /* array for GETALL & SETALL */
struct seminfo *__buf; /* buffer for IPC_INFO */
void *__pad;
};
下面是演示代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
#include <malloc.h>
//需要自己定义的一个联合
union semun {
int val; // 使⽤用的值
struct semid_ds *buf; // IPC_STAT、IPC_SET 使⽤用缓存区
unsigned short *array; // GETALL,、SETALL 使⽤用的数组
struct seminfo *__buf; // IPC_INFO(Linux特有) 使⽤用缓存区
};
int main()
{
key_t _key = ftok(".", 0x6666);//获取一个唯一key值
int sem_id = semget(_key, 1, IPC_CREAT|IPC_EXCL|0666);//创建一个初始个数为1的信号量集
printf("%d\n", sem_id);
struct sembuf sem; //对信号量操作的结构体
memset(&sem, '\0', sizeof(sem));
sem.sem_num = 0;//要操作的下标
sem.sem_op = 1;//要进行的操作,1表示加1
sem.sem_flg = 0;//操作标志
union semun sem_un;//需要获取信号量信息的一个联合
sem_un.array = (unsigned short*)malloc(sizeof(unsigned short)*1);
semctl(sem_id, 0, GETALL, sem_un);//操作前打印一下
printf("sem[0]is: %d\n", sem_un.array[0]);
semop(sem_id, &sem, 1);//对信号量操(+1)
semctl(sem_id, 0, GETALL, sem_un);//操作完成后打印一下
printf("sem[0]is: %d\n", sem_un.array[0]);
return 0;
}
运行结果如下:
补充:
当操作信号量(semop)时,sem_flg可以设置SEM_UNDO标识;SEM_UNDO用于将修改的信号量值在进程正常退出(调用exit退出或main执行完)或异常退出(如段异常、除0异常、收到KILL信号等)时归还给信号量。
例:我们做如下两步
1. i.创建一个信号量集,该信号量集包含两个信号量,第一个信号量置20,第二个信号量置10。
2. 不设置SEM_UNDO,将第一个信号量减2;进程未退出时,信号量由20变成18;进程正常退出时,保持18不变。设置SEM_UNDO,将第二个信号量减2;进程未退出时,信号量由10变成8;进程正常退出时,将2归还给信号量,信号量重新变回10。
3. 不设置SEM_UNDO,将第一个信号量减2;进程未退出时,信号量由18变成16;进程非正常退出时,保持16不变。设置SEM_UNDO,将第二个信号量减2;进程未退出时,信号量由10变成8;进程非正常退出时,将2归还给信号量,信号量重新变回10。