信号灯
信号灯与其他进程间通信方式不大相同,它主要提供对进程间共享资源访问控制机制。相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志。除了用于访问控制外,还可用于进程同步。
一、信号灯概述
信号灯与其他进程间通信方式不大相同,它主要提供对进程间共享资源访问控制机制。相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志。除了用于访问控制外,还可用于进程同步。信号灯有以下两种类型:
- 二值信号灯:最简单的信号灯形式,信号灯的值只能取 0 或 1 ,类似于互斥锁。
注:二值信号灯能够实现互斥锁的功能,但两者的关注内容不同。信号灯强调共享资源,只要共享资源可用,其他进程同样可以修改信号灯的值;互斥锁更强调进程,占用资源的进程使用完资源后,必须由进程本身来解锁。 - 计算信号灯:信号灯的值可以取任意非负值(当然受内核本身的约束)。
二、 Linux 信号灯
linux 对信号灯的支持状况与消息队列一样,在 red had 8.0 发行版本中支持的是系统 V 的信号灯。因此,本文将主要介绍系统 V 信号灯及其相应 API 。在没有声明的情况下,以下讨论中指的都是系统 V 信号灯。
注意,通常所说的系统 V 信号灯指的是计数信号灯集。
1 、系统 V 信号灯是随内核持续的,只有在内核重起或者显示删除一个信号灯集时,该信号灯集才会真正被删除。因此系统中记录信号灯的数据结构( struct ipc_ids sem_ids )位于内核中,系统中的所有信号灯都可以在结构 sem_ids 中找到访问入口。
2 、下图说明了内核与信号灯是怎样建立起联系的:
其中: struct ipc_ids sem_ids 是内核中记录信号灯的全局数据结构;描述一个具体的信号灯及其相关信息。
其中, struct sem 结构如下:
struct sem{ int semval; // current value int sempid // pid of last operation } |
从上图可以看出,全局数据结构 struct ipc_ids sem_ids 可以访问到 struct kern_ipc_perm 的第一个成员: struct kern_ipc_perm ;而每个 struct kern_ipc_perm 能够与具体的信号灯对应起来是因为在该结构中,有一个 key_t 类型成员 key ,而 key 则唯一确定一个信号灯集;同时,结构 struct kern_ipc_perm 的最后一个成员 sem_nsems 确定了该信号灯在信号灯集中的顺序,这样内核就能够记录每个信号灯的信息了。 kern_ipc_perm 结构参见《 Linux 环境进程间通信(三):消息队列》。 struct sem_array 见附录 1 。
四、操作信号灯
对消息队列的操作无非有下面三种类型:
1 、 打开或创建信号灯
与消息队列的创建及打开基本相同,不再详述。
2 、 信号灯值操作
linux 可以增加或减小信号灯的值,相应于对共享资源的释放和占有。具体参见后面的 semop 系统调用。
3 、 获得或设置信号灯属性:
系统中的每一个信号灯集都对应一个 struct sem_array 结构,该结构记录了信号灯集的各种信息,存在于系统空间。为了设置、获得该信号灯集的各种信息及属性,在用户空间有一个重要的联合结构与之对应,即 union semun 。
联合 semun 数据结构各成员意义参见附录 2
信号灯 API
1 、文件名到键值
#include <sys/types.h> #include <sys/ipc.h> key_t ftok (char*pathname, char proj) ; |
它返回与路径 pathname 相对应的一个键值,具体用法请参考《 Linux 环境进程间通信(三):消息队列》。
2 、 linux 特有的 ipc() 调用:
int ipc(unsigned int call, int first, int second, int third, void *ptr, long fifth);
参数 call 取不同值时,对应信号灯的三个系统调用:
当 call 为 SEMOP 时,对应 int semop(int semid, struct sembuf *sops, unsigned nsops) 调用;
当 call 为 SEMGET 时,对应 int semget(key_t key, int nsems, int semflg) 调用;
当 call 为 SEMCTL 时,对应 int semctl(int semid , int semnum , int cmd , union semun arg) 调用;
这些调用将在后面阐述。
注:本人不主张采用系统调用 ipc() ,而更倾向于采用系统 V 或者 POSIX 进程间通信 API 。原因已在 Linux 环境进程间通信(三):消息队列中给出。
3 、系统 V 信号灯 API
系统 V 消息队列 API 只有三个,使用时需要包括几个头文件:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> |
1 ) int semget(key_t key, int nsems, int semflg)
参数 key 是一个键值,由 ftok 获得,唯一标识一个信号灯集,用法与 msgget() 中的 key 相同;参数 nsems 指定打开或者新创建的信号灯集中将包含信号灯的数目; semflg 参数是一些标志位。参数 key 和 semflg 的取值,以及何时打开已有信号灯集或者创建一个新的信号灯集与 msgget() 中的对应部分相同,不再祥述。
该调用返回与健值 key 相对应的信号灯集描述字。
调用返回:成功返回信号灯集描述字,否则返回 -1 。
注:如果 key 所代表的信号灯已经存在,且 semget 指定了 IPC_CREAT|IPC_EXCL 标志,那么即使参数 nsems 与原来信号灯的数目不等,返回的也是 EEXIST 错误;如果 semget 只指定了 IPC_CREAT 标志,那么参数 nsems 必须与原来的值一致,在后面程序实例中还要进一步说明。
2 ) int semop(int semid, struct sembuf *sops, unsigned nsops);
semid 是信号灯集 ID , sops 指向数组的每一个 sembuf 结构都刻画一个在特定信号灯上的操作。 nsops 为 sops 指向数组的大小。
sembuf 结构如下:
struct sembuf { unsigned short sem_num; /* semaphore index in array */ short sem_op; /* semaphore operation */ short sem_flg; /* operation flags */ }; |
sem_num 对应信号集中的信号灯, 0 对应第一个信号灯。 sem_flg 可取 IPC_NOWAIT 以及 SEM_UNDO 两个标志。如果设置了 SEM_UNDO 标志,那么在进程结束时,相应的操作将被取消,这是比较重要的一个标志位。如果设置了该标志位,那么在进程没有释放共享资源就退出时,内核将代为释放。如果为一个信号灯设置了该标志,内核都要分配一个 sem_undo 结 构来记录它,为的是确保以后资源能够安全释放。事实上,如果进程退出了,那么它所占用就释放了,但信号灯值却没有改变,此时,信号灯值反映的已经不是资源 占有的实际情况,在这种情况下,问题的解决就靠内核来完成。这有点像僵尸进程,进程虽然退出了,资源也都释放了,但内核进程表中仍然有它的记录,此时就需 要父进程调用 waitpid 来解决问题了。
sem_op 的值大于 0 ,等于 0 以及小于 0 确定了对 sem_num 指定的信号灯进行的三种操作。具体请参考 linux 相应手册页。
这里需要强调的是 semop 同时操作多个信号灯,在实际应用中,对应多种资源的申请或释放。 semop 保证操作的原子性,这一点尤为重要。尤其对于多种资源的申请来说,要么一次性获得所有资源,要么放弃申请,要么在不占有任何资源情况下继续等待,这样,一方面避免了资源的浪费;另一方面,避免了进程之间由于申请共享资源造成死锁。
也许从实际含义上更好理解这些操作:信号灯的当前值记录相应资源目前可用数目; sem_op>0 对应相应进程要释放 sem_op 数目的共享资源; sem_op=0 可以用于对共享资源是否已用完的测试; sem_op<0 相当于进程要申请 -sem_op 个共享资源。再联想操作的原子性,更不难理解该系统调用何时正常返回,何时睡眠等待。
调用返回:成功返回 0 ,否则返回 -1 。
3) int semctl(int semid , int semnum , int cmd , union semun arg)
该系统调用实现对信号灯的各种控制操作,参数 semid 指定信号灯集,参数 cmd 指定具体的操作类型;参数 semnum 指定对哪个信号灯操作,只对几个特殊的 cmd 操作有意义; arg 用于设置或返回信号灯信息。
该系统调用详细信息请参见其手册页,这里只给出参数 cmd 所能指定的操作。
IPC_STAT | 获取信号灯信息,信息由 arg.buf 返回; |
IPC_SET | 设置信号灯信息,待设置信息保存在 arg.buf 中(在 manpage 中给出了可以设置哪些信息); |
GETALL | 返回所有信号灯的值,结果保存在 arg.array 中,参数 sennum 被忽略; |
GETNCNT | 返回等待 semnum 所代表信号灯的值增加的进程数,相当于目前有多少进程在等待 semnum 代表的信号灯所代表的共享资源; |
GETPID | 返回最后一个对 semnum 所代表信号灯执行 semop 操作的进程 ID ; |
GETVAL | 返回 semnum 所代表信号灯的值; |
GETZCNT | 返回等待 semnum 所代表信号灯的值变成 0 的进程数; |
SETALL | 通过 arg.array 更新所有信号灯的值;同时,更新与本信号集相关的 semid_ds 结构的 sem_ctime 成员; |
SETVAL | 设置 semnum 所代表信号灯的值为 arg.val ; |
调用返回:调用失败返回 -1 ,成功返回与 cmd 相关:
Cmd | return value |
GETNCNT | Semncnt |
GETPID | Sempid |
GETVAL | Semval |
GETZCNT | Semzcnt |
1 、 一次系统调用 semop 可同时操作的信号灯数目 SEMOPM , semop 中的参数 nsops 如果超过了这个数目,将返回 E2BIG 错误。 SEMOPM 的大小特定与系统, redhat 8.0 为 32 。
2 、 信号灯的最大数目: SEMVMX ,当设置信号灯值超过这个限制时,会返回 ERANGE 错误。在 redhat 8.0 中该值为 32767 。
3 、 系统范围内信号灯集的最大数目 SEMMNI 以及系统范围内信号灯的最大数目 SEMMNS 。超过这两个限制将返回 ENOSPC 错误。 redhat 8.0 中该值为 32000 。
4 、 每个信号灯集中的最大信号灯数目 SEMMSL , redhat 8.0 中为 250 。 SEMOPM 以及 SEMVMX 是使用 semop 调用时应该注意的; SEMMNI 以及 SEMMNS 是调用 semget 时应该注意的。 SEMVMX 同时也是 semctl 调用应该注意的。
第一个创建信号灯的进程同时也初始化信号灯,这样,系统调用 semget 包含了两个步骤:创建信号灯;初始化信号灯。由此可能导致一种竞争状态:第一个创建信号灯的进程在初始化信号灯时,第二个进程又调用 semget ,并且发现信号灯已经存在,此时,第二个进程必须具有判断是否有进程正在对信号灯进行初始化的能力。在参考文献 [1] 中,给出了绕过这种竞争状态的方法:当 semget 创建一个新的信号灯时,信号灯结构 semid_ds 的 sem_otime 成员初始化后的值为 0 。因此,第二个进程在成功调用 semget 后,可再次以 IPC_STAT 命令调用 semctl ,等待 sem_otime 变为非 0 值,此时可判断该信号灯已经初始化完毕。下图描述了竞争状态产生及解决方法:
实际上,这种解决方法也是基于这样一个假定:第一个创建信号灯的进程必须调用 semop ,这样 sem_otime 才能变为非零值。另外,因为第一个进程可能不调用 semop ,或者 semop 操作需要很长时间,第二个进程可能无限期等待下去,或者等待很长时间。
本实例有两个目的: 1 、获取各种信号灯信息; 2 、利用信号灯实现共享资源的申请和释放。并在程序中给出了详细注释。
#include <linux/sem.h> #include <stdio.h> #include <errno.h> #define SEM_PATH "/unix/my_sem" #define max_tries 3
int semid; main() { int flag1,flag2,key,i,init_ok,tmperrno; struct semid_ds sem_info; struct seminfo sem_info2; union semun arg; //union semun : 请参考附录 2 struct sembuf askfor_res, free_res; flag1=IPC_CREAT|IPC_EXCL|00666; flag2=IPC_CREAT|00666; key=ftok(SEM_PATH,'a'); //error handling for ftok here; init_ok=0; semid=semget(key,1,flag1);//create a semaphore set that only includes one semphore. if(semid<0) { tmperrno=errno; perror("semget"); if(tmperrno==EEXIST) //errno is undefined after a successful library call( including perror call) so it is saved //in tmperrno. { semid=semget(key,1,flag2); //flag2 只包含了 IPC_CREAT 标志 , 参数 nsems( 这里为 1) 必须与原来的信号灯数目一致 arg.buf=&sem_info; for(i=0; i<max_tries; i++) { if(semctl(semid, 0, IPC_STAT, arg)==-1) { perror("semctl error"); i=max_tries;} else { if(arg.buf->sem_otime!=0){ i=max_tries; init_ok=1;} else sleep(1); } } if(!init_ok) // do some initializing, here we assume that the first process that creates the sem will // finish initialize the sem and run semop in max_tries*1 seconds. else it will not run // semop any more. { arg.val=1; if(semctl(semid,0,SETVAL,arg)==-1) perror("semctl setval error"); } } else {perror("semget error, process exit"); exit(); } } else //semid>=0; do some initializing { arg.val=1; if(semctl(semid,0,SETVAL,arg)==-1) perror("semctl setval error"); } //get some information about the semaphore and the limit of semaphore in redhat8.0 arg.buf=&sem_info; if(semctl(semid, 0, IPC_STAT, arg)==-1) perror("semctl IPC STAT"); printf("owner's uid is %d/n", arg.buf->sem_perm.uid); printf("owner's gid is %d/n", arg.buf->sem_perm.gid); printf("creater's uid is %d/n", arg.buf->sem_perm.cuid); printf("creater's gid is %d/n", arg.buf->sem_perm.cgid);
arg.__buf=&sem_info2; if(semctl(semid,0,IPC_INFO,arg)==-1) perror("semctl IPC_INFO"); printf("the number of entries in semaphore map is %d /n", arg.__buf->semmap); printf("max number of semaphore identifiers is %d /n", arg.__buf->semmni); printf("mas number of semaphores in system is %d /n", arg.__buf->semmns); printf("the number of undo structures system wide is %d /n", arg.__buf->semmnu); printf("max number of semaphores per semid is %d /n", arg.__buf->semmsl); printf("max number of ops per semop call is %d /n", arg.__buf->semopm); printf("max number of undo entries per process is %d /n", arg.__buf->semume); printf("the sizeof of struct sem_undo is %d /n", arg.__buf->semusz); printf("the maximum semaphore value is %d /n", arg.__buf->semvmx);
//now ask for available resource: askfor_res.sem_num=0; askfor_res.sem_op=-1; askfor_res.sem_flg=SEM_UNDO;
if(semop(semid,&askfor_res,1)==-1)//ask for resource perror("semop error");
sleep(3); //do some handling on the sharing resource here, just sleep on it 3 seconds printf("now free the resource/n");
//now free resource free_res.sem_num=0; free_res.sem_op=1; free_res.sem_flg=SEM_UNDO;
if(semop(semid,&free_res,1)==-1)//free the resource. if(errno==EIDRM) printf("the semaphore set was removed/n"); //you can comment out the codes below to compile a different version: if(semctl(semid, 0, IPC_RMID)==-1) perror("semctl IPC_RMID"); else printf("remove sem ok/n"); } |
注:读者可以尝试一下注释掉初始化步骤,进程在运行时会出现何种情况(进程在申请资源时会睡眠),同时可以像程序结尾给出的注释那样,把该程序编译成两个不同版本。下面是本程序的运行结果(操作系统 redhat8.0 ):
owner's uid is 0 owner's gid is 0 creater's uid is 0 creater's gid is 0 the number of entries in semaphore map is 32000 max number of semaphore identifiers is 128 mas number of semaphores in system is 32000 the number of undo structures system wide is 32000 max number of semaphores per semid is 250 max number of ops per semop call is 32 max number of undo entries per process is 32 the sizeof of struct sem_undo is 20 the maximum semaphore value is 32767 now free the resource remove sem ok |
Summary :信号灯与其它进程间通信方式有所不同,它主要用于进程间同步。通常所说的系统 V 信号灯实际上是一个信号灯的集合,可用于多种共享资源的进程间同步。每个信号灯都有一个值,可以用来表示当前该信号灯代表的共享资源可用( available )数量,如果一个进程要申请共享资源,那么就从信号灯值中减去要申请的数目,如果当前没有足够的可用资源,进程可以睡眠等待,也可以立即返回。当进程要申请多种共享资源时, linux 可以保证操作的原子性,即要么申请到所有的共享资源,要么放弃所有资源,这样能够保证多个进程不会造成互锁。 Linux 对信号灯有各种各样的限制,程序中给出了输出结果。另外,如果读者想对信号灯作进一步的理解,建议阅读 sem.h 源代码,该文件不长,但给出了信号灯相关的重要数据结构。
附录 1 : struct sem_array 如下:
/* 系统中的每个信号灯集对应一个 sem_array 结构 */ struct sem_array { struct kern_ipc_perm sem_perm; /* permissions .. see ipc.h */ time_t sem_otime; /* last semop time */ time_t sem_ctime; /* last change time */ struct sem *sem_base; /* ptr to first semaphore in array */ struct sem_queue *sem_pending; /* pending operations to be processed */ struct sem_queue **sem_pending_last; /* last pending operation */ struct sem_undo *undo; /* undo requests on this array */ unsigned long sem_nsems; /* no. of semaphores in array */ }; |
其中, sem_queue 结构如下:
/* 系统中每个因为信号灯而睡眠的进程,都对应一个 sem_queue 结构 */ struct sem_queue { struct sem_queue * next; /* next entry in the queue */ struct sem_queue ** prev; /* previous entry in the queue, *(q->prev) == q */ struct task_struct* sleeper; /* this process */ struct sem_undo * undo; /* undo structure */ int pid; /* process id of requesting process */ int status; /* completion status of operation */ struct sem_array * sma; /* semaphore array for operations */ int id; /* internal sem id */ struct sembuf * sops; /* array of pending operations */ int nsops; /* number of operations */ int alter; /* operation will alter semaphore */ }; |
附录 2 : union semun 是系统调用 semctl 中的重要参数:
union semun { int val; /* value for SETVAL */ struct semid_ds *buf; /* buffer for IPC_STAT & IPC_SET */ unsigned short *array; /* array for GETALL & SETALL */ struct seminfo *__buf; /* buffer for IPC_INFO */ //test!! void *__pad; }; struct seminfo { int semmap; int semmni; int semmns; int semmnu; int semmsl; int semopm; int semume; int semusz; int semvmx; int semaem; }; |