进程间通信之信号量
信号量(semaphore)与已经介绍过的IPC机构(管道、FIFO以及消息队列)不同。它是一个计数器,用于多进程对共享数据对象的访问。
为了获得共享资源,进程需要执行下列操作:
(1)测试控制该资源的信号量。
(2)若此信号量的值为正,则进程可以使用该资源。进程将信号量值减1,表示它使用了一个资源单位。
(3)若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0.进程被唤醒后,它返回至第(1)步。
当进程不再使用由一个信号量控制的共享资源时,该信号量值增1。如果有进程正在休眠等待此信号量,则唤醒它们。
为了正确地实现信号量,信号量值的测试及减1操作应当是原子操作。为此,信号量通常是在内核中实现的。
常用的信号量形式被称为二元信号量或双态信号量(binary semaphore)。它控制单个资源,初始值为1。但是一般而言,信号量的初值可以是任一正值,该值说明有多少个共享资源单位可供共享应用。
遗憾的是,XSI的信号量与此相比要复杂得多。三种特性造成了这种不必要的复杂性:
(1)信号量并非是单个非负值,而必需将信号量定义为含有一个或多个信号量值的集合。当创建一个信号量时,要指定该集合中信号量值的数量。
(2)创建信号量(semget)与对其赋初值(semctl)分开。这是一个致命的弱点,因为不能原子地创建一个信号量集合,并且对该集合中的各个信号量值赋初值。
(3)即使没有进程正在使用各种形式的XSI IPC,它们仍然是存在的。有些程序在终止时并没有释放已经分配给它的信号量,所以我们不得不为这种程序担心。下面将要说明的undo功能就是假定要处理这种情况的。
内核为每个信号量集合设置了一个semid_ds结构:
struct semid_ds { struct ipc_perm sem_perm; unsigned short sem_nsems; /* # of semaphores in set */ time_t sem_otime; /* last-semop() time */ time_t sem_ctime; /* last-change time */ ... };
Single UNIX Specification定义了上面所示的各字段,但是具体实现可在semid_ds结构中定义添加的成员。
每个信号量由一个无名结构表示,它至少包含下列成员:
struct { unsigned short semval; /* semaphore value, always >= 0 */ pid_t sempid; /* pid for last operation */ unsigned short semncnt; /* # processes awaiting semval>curval */ unsigned short semzcnt; /* # processes awaiting semval==0 */ };
要获得一个信号量ID,要调用的第一个函数是semget。
#include <sys/sem.h> int semget(key_t key, int nsems, int flag); 返回值:若成功则返回信号量ID,若出错则返回-1
http://www.cnblogs.com/nufangrensheng/p/3561681.html中标识符与键部分,说明了将key转换为标识符的规则,讨论了是否创建一个新集合,或是引用一个现存的集合。
nsems是该集合中的信号量数。如果是创建新集合(一般在服务器进程中),则必须指定nsems。如果引用一个现存的集合(一个客户进程),则将nsems指定为0。
创建一个新集合时,对semid_ds结构的下列成员赋初值:
- ipc_perm结构按http://www.cnblogs.com/nufangrensheng/p/3561681.html中权限结构所述进行初始化。该结构中mode成员按flag中的相应权限位设置。这些权限用http://www.cnblogs.com/nufangrensheng/p/3561681.html表15-2中的常量指定。
- sem_otime设置为0.
- sem_ctime设置为当前时间。
- sem_nsems设置为nsems。
semctl函数包含了多种信号量操作。
#include < sys/sem.h> int semctl(int semid, int semnum, int cmd, ... /* union semun arg */); 返回值:见下
注意,依赖于所请求的命令,第四个参数是可选的,如果使用该参数,则其类型是semun,它是多个特定命令参数的联合(union):
union semun { int val; /* for SETVAL */ struct semid_ds *buf; /* for IPC_STAT and IPC_SET */ unsigned short *array; /* for GETALL and SETALL */ };
注意,这是一个联合,而非指向联合的指针。
cmd参数指定下列10种命令中的一种,在semid指定的信号量集合上执行此命令。其中有5条命令(如下绿色背景所示)是针对一个特定的信号量值的,它们用semnum指定该信号量集合中的一个成员。semnum值在0和nsems-1之间(包括0和nsems-1)。
IPC_STAT 对此集合取semid_ds结构,并存放在由arg.buf指向的结构中。
IPC_SET 按由arg.buf指向结构中的值设置与此集合相关结构中的下列三个字段值:sem_perm.uid、sem_perm.gid和sem_perm.mode。此命令只能由下列两种进程执行:一种是其有效用户ID等于sem_perm.cuid或sem_perm.uid的进程;另一种是具有超级用户特权的进程。
IPC_RMID 从系统中删除该信号量集合。这种删除是立即发生的。仍在使用此信号量集合的其他进程在它们下次试图对此信号量集合进行操作时,将出错返回EIDRM。此命令只能由下列两种进程执行:一种是其有效用户ID等于sem_perm.cuid或sem_perm.uid的进程;另一种是具有超级用户特权的进程。
GETVAL 返回成员semnum的semval值。
SETVAL 设置成员semnum的semval值。该值由arg.val指定。
GETPID 返回成员semnum的sempid值。
GETNCNT 返回成员semnum的semncnt值。
GETZCNT 返回成员semnum的semzcnt值。
GETALL 取该集合中所有信号量的值,并将它们存放在由arg.array指向的数组中。
SETALL 按arg.array指向的数组中的值,设置该集合中所有信号量的值。
对于除GETALL以外的所有GET命令,semctl函数都返回相应的值。其他命令的返回值为0.
函数semop自动执行信号量集合上的操作数组,这是个原子操作。
#include <sys/sem.h> int semop(int semid, struct sembuf semoparray[], size_t nops); 返回值:若成功则返回0,若出错则返回-1
参数semoparray是一个指针,它指向一个信号量操作数组,信号量操作由sembuf结构表示:
struct sembuf { unsigned short sem_num; /* member # in set ( 0, 1, ..., nsems-1) */ short sem_op; /* operation (negtive, 0, or positive) */ short sem_flag; /* IPC_NOWAIT, SEM_UNDO */ };
参数nops规定该数组中操作的数量(元素数)。
对集合中每个成员的操作由相应的sem_op值规定。此值可以是负值、0或正值。
(1)最易于处理的情况是sem_op为正。这对应于进程释放占用的资源数。sem_op值加到信号量的值上。如果指定了undo标志(此标志对应于相应sem_flag成员的SEM_UNDO位),则也从该进程的此信号量调整值中减去sem_op。
(2)若sem_op为负,则表示要获取由该信号量控制的资源。
如若该信号量的值大于或等于sem_op的绝对值(具有所需的资源),则从信号量值中减去sem_op的绝对值。这保证信号量的结果值大于或等于0。如果指定了undo标志,则sem_op的绝对值也加到该进程的此信号量调整值上。
如果信号量值小于sem_op的绝对值(资源不能满足要求),则:
(a)若指定了IPC_NOWAIT,则semop出错返回EAGAIN。
(b)若未指定IPC_NOWAIT,则该信号量的semncnt值加1(因为调用进程将进入休眠状态),然后调用进程被挂起直至下列事件之一发生:
(i)此信号量变成大于或等于sem_op的绝对值(即某个进程已释放了某些资源)。此信号量的semncnt减1(因为已经结束等待),并且从信号量值中减去sem_op的绝对值。如果指定了undo标志,则sem_op的绝对值也加到该进程的此信号量调整值上。
(ii)从系统中删除了此信号量。在此情况下,函数出错则返回EIDRM。
(iii)进程捕捉到一个信号,并从信号处理程序返回。在此情况下,此信号量的semncnt值减1(因为调用进程不再等待),并且函数出错返回EINTR。
(3)若sem_op为0,这表示调用进程希望等待到该信号量值变成0。
如果信号量值当前是0,则此函数立即返回。
如果信号量值非0,则:
(a)若指定了IPC_NOWAIT,则出错返回EAGAIN。
(b)若未指定IPC_NOWAIT,则该信号量的semzcnt值加1(因为调用进程将进入休眠状态),然后调用进程被挂起,直至下列事件之一发生为止:
(i)此信号量值变成0。此信号量的semzcnt值减1(因为调用进程已经结束等待)。
(ii)从系统中删除了此信号量。在此情况下,函数出错返回EIDRM。
(iii)进程捕捉到一个信号,并从信号处理程序返回。在此情况下此信号量的semzcnt值减1(因为调用进程不再等待),并且函数出错返回EINTR。
semop函数具有原子性,它或者执行数组中的所有操作,或者什么也不做。
exit时信号量调整
正如前面提到的,如果在进程终止时,它占用了经由信号量分配的资源,那么就会成为一个问题。无论何时,只要为信号量操作指定了SEM_UNDO标志,然后分配资源(sem_op值小于0),那么内核就会记住对于该特定信号量,分配给调用进程多少资源(sem_op的绝对值)。当该进程终止时,不论自愿或者不自愿,内核都将检验该进程是否还有尚未处理的信号量调整值,如果有,则按调整值对相应信号量值进行处理。
如果用带有SETVAL或SETALL命令的semctl设置一信号量的值,则在所有进程中,对于该信号量的调整值都设置为0。
实例:信号量与记录锁的耗时比较
如果多个进程共享一个资源,则可使用信号量或记录锁。
若使用信号量,则先创建一个包含一个成员的信号量集合,然后对该信号量值赋初值1。为了分配资源,以sem_op为-1调用semop;为了释放资源,则以sem_op为+1调用semop。对每个操作都指定SEM_UNDO,以处理在未释放资源条件下进程终止的情况。
若使用记录锁,则先创建一个空文件,并且用该文件的第一个字节(无需存在)作为锁字节。为了分配资源,先对该字节获得一个写锁;释放该资源时,则对该字节解锁。记录锁的性质确保了当一个锁的属主进程终止时,内核会自动释放该锁。
在Linux上,记录锁与信号量锁相比,在时间上要多耗时约60%。
虽然记录锁慢于信号量锁,但如果只需要锁一个资源(例如共享存储段)并且不需要使用XSI信号量的所有花哨的功能,则宁可使用记录锁。理由是使用简易,且进程终止时系统会处理任何遗留下来的锁。
本篇博文内容摘自《UNIX环境高级编程》(第二版),仅作个人学习记录所用。关于本书可参考:http://www.apuebook.com/。