分类: 系统运维
2012-04-02 17:11:28
一个信号量是一个和我们已经描述过的其它IPC不相似的IPC形式(管道、FIFO和消息队列)。一个信号量是一个计数器,用来提供多个进程对共享的数据对象的访问。
SUS包含了信号量接口的一个替代集,在它的实时扩展的信号量选项里。我们不在本文讨论这些接口。
为了得到一个共享的资源,一个进程需要做如下的事:
1、测试控制这个资源的信号量;
2、如果信号量的值为正,那么进程可以使用这个资源。在这种情况下,进程把信号量值减一,表明它已经用了一个单位的这个资源;
3、否则,如果信号量的值为0,那么进程进入睡眠,直到信号量值比0大。当进程醒来时,它返回到步骤1。
当一个进程完成了一个被一个信号量控制的共享资源的使用时,信号量值被增一。如果任何其它进程在睡眠,正等待这个信号量,那么它们被唤醒。
为了正确实现信号量,信号量值的测试和这个值的减少必须是一个原子操作。由于这个原因,信号量通常在内核里实现。
一个普遍形式的信号量被称为一个二元信号量(binary semaphore)。它控制单个资源,且它的值被初始化为1。尽管如此,通常一个信号量可以被初始化为任何正值,这个值指定多少单位的共享资源可用。
不幸的是,XSI信号量比这个更复杂。三个特性导致了这个不必要的复杂性。
1、一个信号量不是简单的单个非负值。相反,我们必须定义一个信号量为一个或多个信号量值的集合。当我们创建一个信号量时,我们指明集合进而的值的数量。
2、信号量的创建(semget)和它的初始化(semctl)无关。这是一个致命缺陷,因为我们不能自动创建一个新的信号量集并初始化这个集里所有的值。
3、因为所有形式的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 */
...
};
SUS定义了展示的域,但是实现可以在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 */
};
下表列出了影响信号量集的系统限量(15.6.3节)。
描述 | 典型值 | |||
---|---|---|---|---|
FreeBSD | Linux | Mac | Solaris | |
任何信号量的最大值 | 32767 | 32767 | 32767 | 32767 |
任何信号量的adjust-on-exit值的最大值 | 16384 | 32767 | 16384 | 16384 |
系统范围的信号量集的最大数量 | 10 | 128 | 87381 | 10 |
系统范围的信号量的最大数量 | 60 | 32000 | 87381 | 60 |
每个信号量集的信号量的最大数量 | 60 | 250 | 87381 | 25 |
系统范围的unfo结构体的最大数量 | 30 | 32000 | 87381 | 30 |
每个undo结构体的undo项的最大数量 | 10 | 32 | 10 | 10 |
每个semop调用的操作的最大数量 | 100 | 32 | 100 | 10 |
第一个调用的函数是semget,来得到一个信号量ID。
在15.6.1节,我们描述了转换key到一个标识符的规则并讨论了一个新集合被创建还是一个已有集合被引用。当一个新的集合被创建时,semid_ds结构体的以下成员被初始化。
1、ipc_perm结构体被初始化,如15.6.2节描述的。这个结构体的mode成员被设置为对应的flag的权限位。这些权限由15.6.2节的表里的值指定。
2、sem_otime被设为0。
3、sem_ctime被设为当前时间。
4、sem_nsems被设为nsems。
集里的信号量的数量为nsems。如果一个新的集合被创建(典型地在服务器里),那么我们必须指定nsems。如果我们引用一个已有集合(客户),那么我们可以指定nsems为0。
semctl函数作为各种信号量操作的杂烩。
第4个参数是可选的,取决于请求的命令,如果有的话,它的类型是senum,一个各种命令相关参数的联合体:
union semun {
int val; /* for SETVAL */
struct semid_ds *buf; /* for IPC_START and IPC_SET */
unsigned short *array; /* for GETALL and SETALL */
};
注意可选参数是真实的联合体,而不是联合体的指针。
cmd参数指定以下10个命令的某一个,在semid指定的集合上执行。引用一个特定信号量值的五个命令使用semnum来指明这个集合的一个成员。semnum的值在0和nsems-1之间,包括两者。
IPC_STAT:等到这个集合的semid_ds结构体,把它存储到arg.buf所指的结构体里。
IPC_SET: 设置sem_perm.uid、sem_perm.gid、和sem_perm.mode域,从这个集合的semid_ds结构体里的arg.buf指向 的结构体。这个命令只可以被其用户用户ID等于sem_perm.cuid或sem_permuid,或有超级用户权限的进程执行。
IPC_RMID: 从系统删除信号量集。这个删除是立即的。任何其它正在使用这个信号量的进程将在下次在这个信号量上尝试操作时得到一个EIDRM的错误。这个命令只可以被 其用户用户ID等于sem_perm.cuid或sem_permuid,或有超级用户权限的进程执行。
GETVAL:返回成员semnum的semval值。
SETVAL:设置semnum成员的semval值。这个值由arg.val指定。
GETPID:返回成员semnum的sempid值。
GETNCNT:返回成员semnum的semncnt值。
GETZCNT:返回成员semmum的semzcnt值。
GETALL:得到集合里所有的信号量的值。这些值被存储在arg.array所指的数组里。
SETALL:设置集合里所有的信号量的值为arg.array所指的值。
对于除了GETALL之外的所有GET命令,函数返回对应的值。对于其它的命令,返回值是0。
函数semop自动执行在一个信号量集合上的一个操作数组。
semoparray参数是一个指向一个信号量操作数组的指针,由sembuf结构体表示:
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_UNDO位,在对应的sem_flg成员里。)
1、最简单的情况是当sem_op为正的值。这情况对应于进程资源的返回。这个sem_op的值被加入到信号量的值。如果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值被增加(因为调用者即将睡眠),而调用进程被挂起,直到以下条件发生:
i、
信号量的值变得大于或等于sem_op的绝对值(也就是说,一些其它进程已经释放了一些资源)。这个信号量的semncnt值被减少(因为调用进程已经完
成等待),sem_op的绝对值也从信号量的值里被减去。如果undo标志被指定,那么sem_op的绝对值也为这个进程加到信号量的调整值里。
ii、信号量从系统上被删除。在这种情况下,函数返回一个EIDRM错误。
iii、一个信号被进程捕获,而且信号处理机返回。在这种情况下,这个信号量的semncnt的值减少(因为调用进程不再等待),函数返回一个EINTR的错误。
3、如果sem_op为0,那么这表示这个调用进程想等待,直到信号量的值变为0。
如果信号量的值当前为0,那么函数立即返回。
如果信号量的值不是0,那么以下条件应用:
a、如果IPC_NOWAIT被指定,那么返回一个EAGAIN错误。
b、如果IPC_NOWAIT没有被指定,那么这个信号量的semzcnt值被增加(因为调用者即将睡眠),调用进程被挂起,直到以下某个情况发生:
i、信号量的值变为0。这个信号量的semzcnt的值减少(因为调用进程已经完成等待)。
ii、信号量从系统中被删除。这种情况下,函数返回一个EIDRM的错误。
iii、一个信号被进程捕获且信号处理机返回。这种情况下,这个信号量的semzcnt值减少(因为调用进程不再等待),函数返回一个EINTR的错误。
semop函数自动操作,它或者完成数组里的所有操作,或者一个都不完成。
在exit时的信号量调整(Semaphore Adjustment on exit)
正 如我们之前提到的,如果一个进程在终止时没有释放分配的信号量会有问题。每当我们为一个信号量操作指定SEM_UNDO标志并分配资源(一个小于0的 sem_op)时,内核记住了多们从那个特定信号量里分配了多少资源(sem_op的绝对值)。当进程终止时,不管是否自愿,内核检查进个进程是否有任何 突出的信号量调整值,如果有,那么把这个调整值应用到对应的信号量上。
如果我们使用semctl设置一个信号量的值,使用SETVAL或SETALL命令,那么这个信号量在所有进程里的调整值都被设置0。
例子--信号量和记录锁的计时比较
如果我们在多个进程间共享单个资源,那么我们可以使用信号量或者记录锁。比较这两种技术之间的计时区别是有趣的。
使 用一个信号量,我们创建一个由单个成员组成的信号量集,并把信号量的值初始化为1。要分配这个资源,我们用一个-1的sem_op来调用semop,要释 放资源,我们执行一个+1的sem_op。我们也为每个操作指定SEM_UNDO,来处理一个进程终止而不释放资源的情况。
使用记录锁时,我们创建一个空文件并使用文件的第一个字节(它不必存在)作为锁字节。为了分配内存,我们得到这个字节的写锁,为了释放它,我们解锁这个字节。记录锁的特性保证了如果一个进程握住一个锁时终止,那么这个锁会自动被内核释放掉。
下表展示了在Linux上执行这两个锁技术所需的时间。在每种情况下,资源被分配然后被释放100000资。这由三个不同的进程同时完成。下表是三个进程总的秒数。
操作 | 用户 | 系统 | 挂钟 |
---|---|---|---|
带有undo的信号量 | 0.38 | 0.48 | 0.86 |
建议记录锁 | 0.41 | 0.95 | 1.36 |
在Linux上,记录锁相比于信号量锁在挂钟时间上有大约60%的惩罚。
即使记录锁比信号量锁慢,然而如果我们正锁住单个资源(比如一个共享内存段)而不需要XSI信号量的所有昂贵的特性,那么记录锁会更好。原因是它用起来简单得多,而且系统在一个进程终止时会关照任何苟延残喘的锁。