8、信号量
信号量和我们前面提到的IPC不太一样,它是一个计数器,用来提供在多个进程之间共享数据的访问。
Single UNIX Specification 有一个对信号量的实时扩展,这里不对它进行讨论。
为了获得一个共享的资源,进程需要做如下的工作:
- 对控制资源的信号量进行测试。
- 如果信号量的值是整数,那么进程可以对共享资源进行访问,这时候将把信号量的值减少1,表示使用了一个单位的资源。
- 如果信号量的值大于0,那么进程睡眠直到信号量大于0,然后进程醒来执行步骤1。
如果一个进程操作完信号量控制的共享数据,那么将会给信号量加1,如果有其他进程由于等待信号量而处于睡眠状态,那么这些进程将会被唤醒。
为了正确地执行信号量, 信号量值的测试和减少操作必须是一个原子操作 。因此,信号量一般在内核中实现。
有一个叫二进制的比较通用的信号量,这个信号量控制一个单一的资源,它的值被初始化为1。一般来说,信号量可以被初始化成为任何正数,数值表示可用的共享资源的单元数目。
XSI的信号量比这复杂多了,有三个特性导致了这样的复杂性:
- 信号量并不简单地是一个单个的非负值。相反,我们将信号量定义成了包含一个或者多个信号量值的 集合 。当创建了一个信号量的时候,我们指定集合中值的数目。
- 信号量的创建(semget)是和它的初始化(semctl)相独立的。这个缺点比较致命,因为我们不能创建一个信号量集合同时初始化集合中的所有值。
- 由于所有形式的XSI IPC即使在没有进程使用它们的时候也会保持存在,所以我们可能会担心一个应用程序没有释放分配给它的信号量就终止了。后面我们讨论的undo特性用来处理这种情况。
内核为每一个信号量集合维持一个semid_ds数据结构,如下:
struct semid_ds {
struct ipc_perm sem_perm; /* see Section 15.6.2 */
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标准定义的一些成员,具体实现还可以定义其他的成员。
每个信号量由至少有以下成员的匿名结构体所表示:
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 */
.
};
对于信号量集合的系统限制这里就不给出了,具体参见参考资料。
semget
首先调用semget获得一个信号量ID。
#include <sys/sem.h>
int semget(key_t key, int nsems, int flag);
返回值:如果成功返回信号量ID,如果错误返回1。
前面我们讨论了将一个关键字转换成标识的方法,以及创建一个新的信号量集合和对已经存在的信号量集合的引用。当创建一个新的信号量集合的时候,semid_ds结构变量的如下成员将会被初始化。
- ipc_perm结构如同前面描述的那样被初始化(即所有的成员都会被初始化)。结构的mode成员将会被设置成相应的flag权限位。
- sem_otime 被设置成0。
- sem_ctime 被设置成当前时间。
- sem_nsems 被设置成nsems。
集合中的信号量的数目用nsems进行表示。如果一个 新的集合被创建(一般这个集合都是被服务进程创建),我们必须指定nsems ;如果我们 只是引用一个已经存在的集合(一般集合被客户进程引用),我们可以指定nsems为0 。
semctl
semctl函数用来进行各种类型的信号量操作。
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */);
返回值:参见后面。
第4个参数是可选的,它取决于请求的命令,如果存在,那么这个参数的类型是semun,这是一个各种命令的参数的联合。
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]。
- IPC_STAT 获取集合相应的semid_ds结构,把它存放在第4个参数arg.buf中。
- IPC_SET 设置和集合相关联的semid_ds结构变量的sem_perm.uid, sem_perm.gid,和sem_perm.mode成员,设置的值来自arg.buf。这个命令的执行进程的有效用户id必续和sem_perm.cuid或者sem_perm.uid相等,或者执行这个命令的进程是具有超级用户权限的。
- IPC_RMID 从系统中删除信号量集合。删除的动作立即生效。任何使用这个信号量的进程再次操作信号量的时候将会得到EIDRM错误。这个命令的执行进程的有效用户id必续和sem_perm.cuid或者sem_perm.uid相等,或者执行这个命令的进程是具有超级用户权限的。
- GETVAL 返回semnum对应的信号量的信号量值。
- SETVAL 设置semnum所对应的信号量的值。这个值通过arg.val来进行指定。
- GETPID 返回semnum对应的信号量的sempid成员。
- GETNCNT 返回semnum对应的信号量的semncnt成员。
- GETZCNT 返回semnum对应的信号量的semzcnt成员。
- GETALL 获取集合中的所有信号量的值。这些值被存放在arg.array所指向的数组中。
- SETALL 设置集合中的所有信号量值,这些值来自arg.array数组。
semop
函数semop会原子性地对一个信号量集合执行数组中指定的一系列操作。
#include <sys/sem.h>
int semop(int semid, struct sembuf semoparray[], size_t nops);
返回值:如果成功返回0,如果错误返回1。
semoparray参数是一个指向信号量操作的数组的指针,其元素的数据结构如下:
struct sembuf {
unsigned short sem_num; /* member # in set (0, 1, ..., nsems-1) */
short sem_op; /* operation (negative, 0, or positive) */
short sem_flg; /* IPC_NOWAIT, SEM_UNDO */
};
nops参数指定操作数组中元素的数目。
对集合中的每个成员的操作通过相应的sem_op值来进行指定。这个值可以是负数,0,或者是正数。(在下面的讨论中,我们会引用到信号量的"undo"标记,这个标记相应于sem_flg成员中的SEM_UNDO比特位)
情况I. 最简单的情况是sem_op是正数。
这个情况相应于进程所返回的资源的数目。sem_op的值会被添加到信号量的值中。如果undo标记被指定了,那么会从这个进程的信号量调整值减去sem_op。
II. 如果sem_op是负数,那么表示我们想要获取信号量控制的资源。
如果信号量的值大于或者等于sem_op的绝对值(资源可用),那么会将信号量的值减去sem_op的绝对值。这保证信号量的结果值大于或者等于0。如果undo标记被指定,那么sem_op的绝对值也会被加到这个进程的信号量调整值中。
如果信号量的值比sem_op的绝对值小(资源不可用),那么会发生如下的情况:
- 如果IPC_NOWAIT被指定了,那么semop会返回一个EAGAIN错误。
- 如果IPC_NOWAIT没有被指定,那么这个信号量的semncnt值会增加(因为调用者将要睡觉),然后调用者会挂起直到如下的情况发生。
- 信号量的值变成了大于或者等于sem_op的绝对值的时候(也就是说其他的进程释放了一些资源)。信号量的semncnt的值会被减少(因为调用进程正在进行等待),同时sem_op的绝对值会被从信号量的值中被减去。如果undo标记被指定,那么sem_op的绝对值也会被加到这个进程的信号量调整值上。
- 信号量被从系统中移除的时候。这个时候,函数返回一个EIDRM错误。
- 进程捕获到了一个信号,并且信号处理函数返回。这个情况,信号量的semncnt的值会被减少(因为调用进程不会再进行等待),并且函数返回一个EINTR错误。
III. 如果sem_op的值是0,这表示调用进程想要等待,一直到信号量的值变成为0。
如果信号量的值当前是0,那么函数会立即返回。
如果信号量的值为非0,那么会根据如下情况进行处理:
- 如果IPC_NOWAIT被指定了,那么返回错误EAGAIN。
- 如果没有指定IPC_NOWAIT,那么信号量的semzcnt值会被增加(因为调用这将要进行睡眠),同时调用进程挂起,直到如下的情况发生。
- 信号量的值变成了0,信号量的semncnt的值会被减少(因为调用进程正在进行等待)。
- 信号量被从系统中移除的时候。这个时候,函数返回一个EIDRM错误。
- 进程捕获到了一个信号,并且信号处理函数返回。这个情况,信号量的semncnt的值会被减少(因为调用进程不会再进行等待),并且函数返回一个EINTR错误。
semop函数的操作是原子性质的,要么数组中的操作全部被做,要么一个也不做。
在退出时候对信号量的调整
我们前面说过,如果一个进程通过信号量分配了资源,那么当进程结束的时候,可能会出现问题。当我们为信号量操作指定SEM_UNDO标记并且我们分配一个资源(sem_op值小于0),内核会记住我们给那个信号量分配了多少资源(sem_op的绝对值)。当进程结束的时候,无论是主动的还是非主动地结束,内核都会检查这个进程是否具有信号量调整值,如果有,将会把对应的信号量调整值叠加到信号量上,实现相应的调整。
如果我们通过SETVAL或者SETALL命令调用semctl设置信号量的值,那么那个信号量在所有进程的的调整值都被设置成0。
信号量和记录锁的时间对比的例子
如果我们在多个进程之间共享一个单个的资源,我们可以使用信号量或者记录锁。
通过信号量技术,我们创建了一个只有一个信号量成员的信号量集合,并且将这个成员信号量的值初始化为1。分配资源的时候,我们调用semop函数,其中的sem_op值为-1;释放资源的时候,我们执行同样的函数但是其中的sem_op的值为+1。我们也可以为每一个操作指定SEM_UNDO,以处理进程结束而没有释放资源的情况。
通过记录锁的技术,我们创建一个空的文件,然后将文件的第一个字节做为锁住的字节(不一定非得存在)。当分配资源的时候,我们获取一个在这个字节上面的写锁;释放资源的时候,我们将这个字节解锁。记录锁的特性可以保证如果一个进程在持有锁的时候结束了,那么这个锁会自动被内核释放。
下面的表格中,给出了Linux上面两个技术的时间对比情况。每种情况下,资源被分配和释放100,000此。通过三个不同的进程同时进行。表中的时间是所有三个进程的总共秒数。
时间对比的表格
+--------------------------------------------------+
| Operation | User | System | Clock |
|--------------------------+------+--------+-------|
| semaphores with undo | 0.38 | 0.48 | 0.86 |
|--------------------------+------+--------+-------|
| advisory record locking | 0.41 | 0.95 | 1.36 |
+--------------------------------------------------+
在Linux上面,使用记录锁的时间会比信号量锁的时间多60%。
尽管记录锁比信号量锁要慢,如果我们只是对一个单个的资源加锁(例如共享内存段)并且不需要XSI信号量提供的高级功能的化,我们还是喜欢使用记录锁。原因就是记录锁非常容易被使用,并且系统会自动处理进程结束的时候的资源释放等问题。
译者注
-
这里的信号量其实是一个信号量集合,集合中每个信号量都有包含信号量的数目以及权限信息。
-
使用semget获取或创建信号量集合的标识
-
使用semctl初始化,semun类型参数用于获取或者设置信号量集合的相应值
-
使用semop对信号量集合中的一个或多个信号量进行申请释放,sembuf类型参数,包含UNDO标记,以便进程异常结束后信号量的清理。
-
每个信号量集合的结构包含信号量数目,每个信号量是匿名结构包含值和等待情况。
-
int semget(key_t key, int nsems, int flag)
中,创建的是一个信号量集,而非单一信号量。权限是集合的权限;集合中每个信号量都有特定的值。 -
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */)
中, semnum表示是集合中的第semnum
个信号量。 -
int semop(int semid, struct sembuf semoparray[], size_t nops)
中,设置SEM_UNDO标记并非改变信号操作,而是额外记录一个调整值以备异常。
记录锁性能不如信号量但是使用简单。信号量每次创建一个信号量集合,而非单个信号量,在某些场景比如:需要的资源不止一种的时候,可能维护起来会相对方便,因为集中定义资源到集合中了,而非单独各自定义。
网友评论