上接进程间通信(一)
三、信号量
信号量与前面的管道和消息队列不同,它相当一个计数器,用于多进程之间对共享数据对象的访问。
使用信号量对共享资源进行控制之后,进程想要获取共享资源。进程需要执行下列操作:
(1)检测控制该资源的信号量
(2)若此信号量的值为正,则进程可以使用该资源。进程将信号量值减1,表示他使用了一个资源单位。
(3)若此信号的值为0,则进程进入休眠状态,直至信号量值大于0.进程被唤醒后,它返回至第(1)步。
当进程不再使用由一个信号量控制的共享资源时,该信号量值增1.如果进程正在休眠等待此信号量,则唤醒他们。
为了正确的地实现信号量,信号量值的测试及减1操作应当是原子操作。为此,信号量通常在内核中实现。
常用的信号量形式被称为二元信号量或双态信号量,它控制单个资源,初始值为1.但一般而言,信号量的是·初始值可以是任一正值,该值说明有多少个共享资源单位可供共享使用。
XSI的信号量与此相比要复杂得多。三种特性造成了这种非必要的复杂性。
(1)信号量并非是个非负值,而必须将信号量定义为含有一个或多个信号量值的集合。当创建一个信号量时,要指定该集合中信号量值的数量。
(2)创建信号量(semget)与对其赋初值(semctl)分开。这是一个致命的弱点,因为不能原子地创建一个信号量集合,并且对该集合中的各个信号量值赋初值。
(3)即使没有进程正在使用各种形式的XSI IPC,他们任然是存在的。有些程序在终止时并没有释放已经分配给他们的信号量,所以我们不得不为这种程序担心。undo功能就是假定要处理这种情况的。
内核为每个信号量集合设置了一个semid_ds结构:
struct semid_ds {
struct ipc_perm sem_perm; //权限信息
struct sem *sem_base; //指向信号数组的指针
unsigned short sem_nsema; //这个集合中的信号个数
time_t sem_otime; //最后一次semop的时间
time_t sem_ctime; //最后一次改变的时间
...
};
每一个信号量由一个无名结构表示,它至少包含下列成员:
struct {
unsigned short semval; //信号量的值
pid_t sempid; //最后一次操作信号量的进程ID
unsigned short semncnt; //等待该信号量的进程数(即P操作等待)
unsigned short semzcnt; //等待该信号量值变为0的进程数
...
};
要获得一个信号量ID,就得调用semgget()函数。
#include<sys/sem.h>
int semget(key_t key,int nsems,int flag); //成功返回信号量ID,出错返回-1
nsems是该集合中的信号量数。如果是创建新集合(一般在服务器进程中),则必须指定nsems。如果引用一个现有集合(一个客户进程),则将nsems指定为0。
semctl()函数包含了多种信号量操作;
#include<sys/sem.h>
int semctl(int semid,int semnum,int cmd,.../* union semun arg */);
依赖于请求的命令,第四个参数时可选的,如果用该参数,则其类型是semun,它是多个特定命令参数的联合(union);
arg是一个联合体,而非一个指针
union semun {
int val; //SETVAL命令的参数
struct semid_ds *buf; //IPC_STAT和IPC_SET命令的参数
unsigned short *array; //GETALL 和 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()自动执信号量集合上的操作数组,这是个原子操作。(这也就是常说的PV操作)
#include<sys/sem.h>
int semop(int semid,struct sembuf semoparray[],size_t nops);//成功返回0,出错返回-1
参数semoparray是一个指针,它指向一个信号量操作数组,信号量操作有sembuf结构表示;
struct sembuf {
unsigned short sem_num; //要操作信号量集合中信号量的下标(0到nsems-1)
short sem_op; //需要对信号量进行的操作(正数,负数或者0)
short sem_flg; //IPC_NOWAIT和SEM_UNDO标记
};
参数nops规定该数组中操作的数量(元素数)。
对集合中每个成员的操作应由相应的sem_op值规定。此值可以是正值可以是负值也可以是0。(下面的讨论将提到信号量的undo标志。此标志对应于相应sem_flg成员的SEM_UNDO位。sem_flg为0一般表示正常操作)
(1)最易于处理的情况是sem_op为正。这样对应于进程释放占用的资源数。sem_op值加到信号量的值上。如果指定了undo标志。则也从该进程的此信号量调整值中减去sem_op。(即会将进程对信号的调整值sem_op存储在semadj中)这也是常说的V操作。
(2)若sem_op为负,则表示要获取该信号量控制的资源。即要进行P操作
如若该信号的值大于或等于sem_op的绝对值(具有所需的资源),则从信号量中减去sem_op的绝对值。这保证信号量的结果值大于或等于0。如果指定了undo标志,则sem_op的绝对值也加到该进程的此信号量调整值上。
如果信号量值小于sem_op的绝对值(资源不能满足),则:
(a)若指定了IPC_NOWAIT标志,则semop()出错返回EAGAIN。
(b)若未指定IPC_NOWAIT标志,则该信号量的semncnt值加1(因为调用进程进入休眠状态),然后调用进程挂起直至下列事件之一发生为止:(1)此信号量变成大于或等于sem_op的绝对值(即某个进程已经释放了某些资源),此信号量的semncnt值减1(因为调用进程已经结束等待),并且从信号量值中减去sem_op的绝对值。如果指定了undo标志,则sem_op的绝对值也加到该进程的此信号量调整值上。(2)从系统中删除了此信号量。在此情况下,函数出错返回EIDRM。(3)进程捕捉到一个信号,并从信号处理机制返回。在此情况下,此信号量的semncnt值减1(因为调用进程不在等待),并且函数出错返回EINTR。
(3)若sem_op为0,这表示调用进程希望等待到该信号量值变为0。
如果信号量值当前为0,此函数立即返回。
如果信号量值非0,则
(a)若指定了IPC_NOWAIT,则出错返回EAGAIN。
(b)若未指定IPC_NOWAIT,则该信号量的semzcnt值加1(因为调用进程进入休眠状态),然后调用进程被挂起,直至下列事件之一发生为止:(1)此信号量值变成0,此信号量的semzcnt值减1(因为调用进程已经结束等待)。(2)从系统中删除了此信号量。在此情况下,函数出错返回EIDRM。(3)进程捕捉到一个信号,并从信号处理机制返回。在此情况下,此信号量的semzcnt值减1(因为调用进程不在等待),并且函数出错返回EINTR。
semop()函数具有原子性,它或者执行数组中的所有操作,或者什么也不做。
semadj:等待指定信号量针对某个特定进程的调整值。只有sembuf结构的sem_flg指定为SEM_UNDO,semadj才会随着sem_op而更新。(即某个进程,在指定了SEM_UNDO后,对信号量semval值的修改都会反应到semadj上,当该进程终止时,内核会根据semadj的值,重新恢复信号量之前的值)。
若果在进程终止时,它占用了经由信号量分配的资源,那么就会成为一个问题。无论何时,只要对信号量操作指定了SEM_UNDO标志,然后分配资源(sem_op值小于0),那么内核就会记住对于该特定信号量,分配给调用进程多少资源(sem_op的绝对值)。当该进程终止时,不论自愿或不自愿,内核对会检测该进程是否还有尚未处理的信号量调整值,如果有,则按调整值对应量值进行处理。
如果用带SETVAL或SETALL命令semctl设置一信号量的值,则在所有进程中,对于该信号量的调整值都设置为0。
四、共享内存
下接进程间通信(三)