1. 信号量
1.1. 信号量
信号量(semaphore)是一个计数器,用户多进程对共享数据对象的访问,也就是用户管理对资源的访问。
为了获得共享资源,进程需要执行下列操作。
1). 测试控制该资源的信号量;
2). 若此信号量的值为正,则进程可以使用该资源。进程将信号量减一,表示它使用了一个资源单位。
3). 若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0。进程被唤醒后,它返回至第一步。
当进程不再使用由一个信号量控制的共享资源时,该信号量值增一。如果有进程正在休眠等待此信号量,则唤醒它们。
所以,信号量值的测试及减一操作应当是原子操作。为此,信号量在内核中实现。
XSI信号集相对复杂:
1). 信号量集并非是单个非负值,而必须将其定义为含有一个或多个信号量(值)的集合。当创建一个信号量集时,要指定该集合中的信号量(值)的数量;
2). 创建信号量集(semget)与对其赋初值(semctl)分开。这是一个致命的弱点,因为不能原子操作地创建一个信号量集,并且对该集合中的各个信号量(值)赋初值。
3). 即使没有进程正在使用各种形式的XSI IPC,它们仍然是存在的。有些程序在终止时没有释放已经分配给它的信号量集,这令人担心。
内核为每个信号量集设置一个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
.
.
.
};
每个信号量由一个无名结构表示,至少有下列成员:
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);
第一个参数key是整数值,不相关的进程可以通过它访问同一个信号量。有个特殊的信号量键值IPC_PRIVATE(通常是0),作用是创建一个只有创建进程才可以访问的信号量。
参数num_sems指定需要的信号量数目,通常是1。
参数flag是一组标识,它的低端九个比特是该信号量的权限,其作用类似于文件的访问权限。它们可以与值IPC_CREAT做按位或操作以创建一个新信号量。即使设置了IPC_CREAT标志,给出的键是一个已经有信号量的键也不会产生错误。只有联合使用IPC_CREAT和IPC_EXCL来确保创建出的是一个新的唯一的信号量,如果该信号量存在,就返回一个错误(errno设置为EEXSIT)。
函数semget在成功时返回一个正数(非0)值,它就是其它信号量函数将用到的信号量标识符。如果失败,返回-1。
函数semctl包含了多种信号量操作。
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd,
... /* union semun arg * /);
第四个参数是可选的,如果使用该参数,其类型是semun,它是多个特定命令参数的联合(union):
union senun{
int val; //for setval
struct semid_ds *buf; //for IPC_STAT and IPC_SET
unsigned short *array; //for GETALL and SETALL
};
注意:这是一个联合,而非指向联合的指针。
参数semnum在0到nsems-1之间(包括0和nsems-1)。
参数cmd是以下10中命令的一种,在semid指定的信号集合上执行此命令。其中五种命令是针对一个特定的信号量的,它们用来semnum指定该信号集合中的一个信号量成员。
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指定。(常用)
GETTPID 返回成员semnum的sempid值。
GETNCNT 返回成员semnum的semncnt值。
GETZCNT 返回成员semnum的semzcnt值。
GETALL 取该集合中所有信号量的值,并将它们存放在由arg.array指向的数组中。
SETVAL 按arg.array指向的数组中的值,设置该集合中的所有信号量的值。
对于除了GRTALL以外的所有get命令,semctl函数都返回相应的值。其它命令的返回值为0。
函数semop自动执行信号量集合上的操作数据,这是个原子操作。
#include <sys/sem.h>
int semop(int semid, struct sembuf semoparray[], size_t nops);
参数semoparray是一个指针,它指向一个信号量数组,信号量操作由sembuf结构表示:
struct sembuf{
unsigned shor sem_num; //member # in set (0,1,2,...,nsem-1)
short sem_op; //operation (negative,0, or positive)
shor sem_flg; //IPC_NOWAIT,SEM_UNDO
};
参数nops规定该数组中操作的数量(元数量)。
以下是对信号量的undo标志,则也对应于sem_flg成员的SEM_UNDO位。
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). 此信号量变成大于或等于semp_op的绝对值(即某个进程已经释放了某些资源)。此信号量的semncnt值减一(因为已 经结束等待),并且从信号量值中减去sem_op的绝对值。如果指定了undo标志,则sem_op的绝对值也加到该进程的此信号量调整值上。
ii). 从系统中删除了此信号量。此时,函数出错则返回EIDRM。
iii). 进程捕捉到一个信号,并从信号处理程序返回。此情况下,此信号量的semncnt值减一(因为调用进程不再等待),并且函数出错返回EINTR。
3). 如果sem_op为0,则表示调用进程希望等待到该信号量的值变成0。
如果信号量值当前是0,则此函数立即返回。
如果信号量值非0,则:
a). 若指定了IPC_NOWAIT,则semop出错返回EAGAIN;
b). 若没有指定了IPC_NOWAIT,则该信号量的semncnt值加一(因为调用进程将进入休眠状态),然后调用进程被挂起,直到下列事件之一发生:
i). 此信号量变成0。此信号量的semncnt值减一(因为调用进程已经结束等待)。
ii). 从系统中删除了此信号量。此时,函数出错则返回EIDRM。
iii). 进程捕捉到一个信号,并从信号处理程序返回。此情况下,此信号量的semncnt值减一(因为调用进程不再等待),并且函数出错返回EINTR。
注意:设置为undo标志。如果这个进程在没有释放该信号量的情况下终止,操作系统会自动释放该进程持有的持有的信号量。
1.2. 信号量与记录锁
如果多个进程共享一个资源,则可使用信号量或记录锁。
若使用信号量,则先创建一个包含一个成员的信号量集合,然后对该信号量值赋初值。为了分配资源,以sem_op为-1调用semop;为了释放资源,则以sem_op为+1调用semop。对每个操作都指定SEM_UNDO标志,以处理在未释放资源的情况下终止进程的操作。
若使用记录锁,则先创建一个空文件,并且用该文件的第一个字节(无需存在)作为锁字节。为了分配资源,先对该字节获得一个写锁;释放资源时,则对该字节解锁。记录锁的性质确保了当一个锁的属主进程终止时,内核会自动释放该锁。
记录锁与信号量相比,在时间上耗时较多,但是简单易用。
1.3. 实例
本程序semaphore.c用来试验信号量,该程序可以被多次调用。通过一个可选的参数来指定程序是负责创建信号量还是负责删除信号量。
用两个不同的字符的输出来表示进入和离开临界区域。如果程序启动时带有一个参数,它将在进入和退出临界区域打印字符X;而程序的其它运行实例将在进入和退出临界区域打印O。字符X和O应该是成对出现的。
在程序开始,调用ftok和semget函数获得信号量标识符。IPC_CREAT的作用是,如果信号量不存在就创建它。函数sleep作用是,让这个程序实例在执行多次循环之前调用其它程序实例。删除信号量之前,带有参数的启动程序会进入等待状态,以允许其他实例都执行完毕。
执行结果如下:
#./semaphore 1 &
[1] 2072
#./ semaphore
OOXXOOXXOOXXOOXXOOXXOOOOXXOOXXOOXXOOXXXX
2073 - finished
2072 - finished
#
源程序如下: