转自:http://www.bccn.net/Article/kfyy/vc/jszl/200708/5852.html
一、信号灯概述
信号灯与其他进程间通信方式不大相同,它主要提供对进程间共享资源访问控制机制。相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志。
除了用于访问控制外,还可用于进程同步。
信号灯有以下两种类型:
1> 二值信号灯:最简单的信号灯形式,信号灯的值只能取0或1,类似于互斥锁。
注:二值信号灯能够实现互斥锁的功能,但两者的关注内容不同。信号灯强调共享资源,只要共享资源可用,其他进程同样可以修改信号灯的值;互斥锁更强调进程,占用资源的进程使用完资源后,必须由进程本身来解锁。
2> 计算信号灯:信号灯的值可以取任意非负值(当然受内核本身的约束)。
二、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确定了该信号灯在信号灯集中的顺序,这样内核就能够记录每个信号灯的信息了。
四、操作信号灯
对消息队列的操作无非有下面三种类型:
1、 打开或创建信号灯
与消息队列的创建及打开基本相同,不再详述。
2、 信号灯值操作
linux可以增加或减小信号灯的值,相应于对共享资源的释放和占有。具体参见后面的semop()系统调用。
3、 获得或设置信号灯属性:
系统中的每一个信号灯集都对应一个struct sem_array结构,该结构记录了信号灯集的各种信息,存在于系统空间。
为了设置、获得该信号灯集的各种信息及属性,在用户空间有一个重要的联合结构与之对应,即union semun。联合semun数据结构的定义如下:
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;
};
其中,seminfo结构体的定义如下:
struct sem_array定义如下:struct seminfo { int semmap; /* Number of entries in semaphore map; unused within kernel */ int semmni; /* Maximum number of semaphore sets */ int semmns; /* Maximum number of semaphores in all semaphore sets */ int semmnu; /* System-wide maximum number of undo structures; unused within kernel */ int semmsl; /* Maximum number of semaphores in a set */ int semopm; /* Maximum number of operations for semop(2) */ int semume; /* Maximum number of undo entries per process; unused within kernel */ int semusz; /* Size of struct sem_undo */ int semvmx; /* Maximum semaphore value */ int semaem; /* Max. value that can be recorded for semaphore adjustment (SEM_UNDO) */ };
/*系统中的每个信号灯集对应一个sem_array 结构 */
其中,sem_queue结构如下: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结构*/
五、信号灯APIstruct 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 */ };
1、文件名到键值
包含头文件:
#include <sys/types.h>
#include <sys/ipc.h>
函数原型:
key_t ftok (char*pathname, char proj);
它返回与路径pathname相对应的一个键值
返回值:
如果成功,则生成一个键值;
如果失败,则返回-1,并设置相应的errno
2、 linux特有的ipc()调用:
int ipc(unsigned int call, int first, int second, int third, void *ptr, long fifth);
参数:
call取不同值时,对应信号灯的三个系统调用:
1> call为SEMOP时,对应int semop(int semid, struct sembuf *sops, unsigned nsops)调用;
2> call为SEMGET时,对应int semget(key_t key, int nsems, int semflg)调用;
3> call为SEMCTL时,对应int semctl(int semid,int semnum,int cmd,union semun arg)调用;
这些调用将在后面阐述。
注:本人不主张采用系统调用ipc(),而更倾向于采用系统V或者POSIX进程间通信API。(ipc()是Linux系统提供的,可移植性差;)
“ipc() is Linux-specific, and should not be used in programs intended to be portable.”
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参数是一些标志位;可以为以下:IPC_CREAT、IPC_EXCL、IPC_NOWAIT或三者的或结果:
1> IPC_CREAT:创建消息队列;
2> IPC_EXCL:确保创建消息队列成功;
3> IPC_NOWAIT:立刻返回,不阻塞;返回值:
成功则返回与健值key相对应的信号灯集描述字;
失败则返回-1,并设置相应的errno;
注意:
如果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结构体的指针(可以传递数个sembuf结构体),其中每一个sembuf结构都刻画一个在特定信号灯上的操作;
nsops为sops指明sops中锁含有sembuf的个数。
返回值:
成功则返回0;
失败则返回-1,并设置相应的errno;
其中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_op的值大于0,等于0以及小于0确定了对sem_num指定的信号灯进行的三种操作:
1> sem_op > 0的正整数:该操作使得信号集的值(semval)增加sem_op;
2> sem_op==0:表名这是一个等待,直至0的操作。如果semval==0,则操作可以立即执行;否则,如果sem_flag设置为IPC_NOWAIT,则semop()失败,并设置errno==EAGAIN(sops中的操作都不会被执行);否则阻塞等待,并且semzcnt(即等待在该信号上,直至semval=0的进程数)增1,直到有如下其中之一的事情发生:
(1)信号值(semval)变成0,同时,阻塞在该信号上的进程数(semzcnt)减少;
(2)信号集被移除,此时semop()失败,并设置errno=EIDRM;
(3)The calling process catches a signal: the value of semzcnt is decremented and semop() fails, with errno set to EINTR.
(4)semtimedop()调用设置了时间,超时,从而导致调用失败,并设置errno=EAGAIN;
这里需要强调的是semop()同时操作多个信号灯,在实际应用中,对应多种资源的申请或释放。semop保证操作的原子性,这一点尤为重要。尤其对于多种资源的申请来说,要么一次性获得所有资源,要么放弃申请,要么在不占有任何资源情况下继续等待,这样,一方面避免了资源的浪费;另一方面,避免了进程之间由于申请共享资源造成死锁。也许从实际含义上更好理解这些操作:信号灯的当前值记录相应资源目前可用数目;sem_op>0对应相应进程要释放sem_op数目的共享资源;sem_op=0可以用于对共享资源是否已用完的测试;sem_op<0相当于进程要申请-sem_op个共享资源。再联想操作的原子性,更不难理解该系统调用何时正常返回,何时睡眠等待。
3> sem_op < 0:假如信号值(semval)>= |sem_op|,则操作立刻执行:simval-|sem_op|;若该操作为SEM_UNDO,则系统更新该信号对应的UNDO进程数(semadj);
假如信号值(semval)< |sem_op|,若sem_flag设置为IPC_NOWAIT,则调用失败,并设置errno=EAGAIN;否则semncnt(the counter of processes waiting for this semaphore's value to increase)自加1,调用进程挂起;
sem_flg可取IPC_NOWAIT以及SEM_UNDO两个标志。如果设置了SEM_UNDO标志,那么在进程结束时,相应的操作将被取消,这是比较重要的一个标志位。
如果设置了该标志位,那么在进程没有释放共享资源就退出时,内核将代为释放;
如果为一个信号灯设置了该标志,内核都要分配一个sem_undo结构来记录它,为的是确保以后资源能够安全释放。事实上,如果进程退出了,那么它所占用就释放了,但信号灯值却没有改变,此时,信号灯值反映的已经不是资源占有的实际情况,在这种情况下,问题的解决就靠内核来完成。这有点像僵尸进程,进程虽然退出了,资源也都释放了,但内核进程表中仍然有它的记录,此时就需要父进程调用waitpid来解决问题了。
3) 控制信号灯
函数原型:
int semctl(int semid,int semnum,int cmd,union semun arg)
该系统调用实现对信号灯的各种控制操作(对信号灯集或者信号灯集中第semnum个信号灯的操作)
参数:
semid指定信号灯集;
semnum指定对哪个信号灯操作,只对几个特殊的cmd操作有意义;
cmd指定具体的操作类型;
arg用于设置或返回信号灯信息。
该系统调用详细信息请参见其手册页,这里只给出参数cmd所能指定的操作。
1> IPC_STAT:获取信号灯集的信息。
从semid关联的内核数据结构复制到arg.buf指向的semid_ds结构体中(此时,参数semnum被忽略);
2> IPC_SET:设置信号灯集信息 (此时,参数semnum被忽略)
从arg.buf所指向的semid_ds结构体写入到信号灯集相关联的内核中(在manpage中给出了可以设置哪些信息);
3> IPC_RMID:立刻删除信号集,并唤醒所有阻塞在该信号集上的进程;(此时,参数semnum被忽略)
4> IPC_INFO:(Linux专有的)返回关于信号灯的限制和参数信息,结果保存在arg._buf所指向的seminfo结构体中;
5> SEM_INFO:(Linux专有的)返回一个seminfo结构体,与IPC_INFO类似;
6> SEM_STAT:(Linux专有的)返回一个semid_ds结构体,与IPC_STAT类似;
7> GETALL:返回所有信号灯的值,结果保存在arg.array中,参数sennum被忽略;
8> GETNCNT:返回由于信号灯集中的第semnum个信号灯而阻塞(挂起)的进程数,相当于目前有多少进程在等待semnum代表的信号灯所代表的共享资源;
9> GETPID:返回最近对第semnum个信号灯执行semop()操作的进程ID;
10> GETVAL:返回第semnum个信号灯的值;
11> GETZCNT:返回等待第semnum个信号灯的值变成0的进程数;
12> SETALL:通过arg.array设置信号灯集中所有信号灯的值;同时,更新与本信号集相关的semid_ds结构的sem_ctime成员;
13> SETVAL:设置第semnum个信号灯的值为arg.val;
返回值:
调用失败返回-1,并设置相应的errno;
成功时,返回值(非负)与cmd相关:
除下面的命令外,返回0表示调用成功;
GETNCNT the value of semncnt
GETPID the value of sempid
GETVAL the value of semval
GETZCNT the value of semzcnt.
IPC_INFO the index of the highest used entry in the kernel's internal array recording information about all semaphore sets.
EM_INFO As for IPC_INFO.
SEM_STAT the identifier of the semaphore set whose index was given in semid.
五、信号灯的限制
1、 系统调用semop()一次可同时操作的信号灯数目SEMOPM,semop中的参数nsops如果超过了这个数目,将返回E2BIG错误。
SEMOPM的大小与系统相关,例如redhat 8.0中为32;
2、 信号集取值的上限(semval):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(),并且发现信号灯已经存在,此时,第二个进程必须具有判断是否有进程正在对信号灯进行初始化的能力。
有一种绕过这种竞争状态的方法:
当semget()创建一个新的信号灯时,信号灯结构semid_ds的sem_otime成员初始化后的值为0。因此,第二个进程在成功调用semget()后,可再次以IPC_STAT命令调用semctl,等待sem_otime变为非0值,此时可判断该信号灯已经初始化完毕。
实际上,这种解决方法也是基于这样一个假定:第一个创建信号灯的进程必须调用semop,这样sem_otime才能变为非零值。另外,因为第一个进程可能不调用semop,或者semop操作需要很长时间,第二个进程可能无限期等待下去,或者等待很长时间。
七、信号灯应用实例
本实例有两个目的:1、获取各种信号灯信息;2、利用信号灯实现共享资源的申请和释放。并在程序中给出了详细注释。
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <stdio.h> #include <errno.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> #define SEM_PATH "stuy1001593" #define max_tryCount 3 //信号灯集的标识(文件描述符) int semid; //存储信号灯集的信息 union semun semState; void printSemState(); void printftokErrorInfo(int err); void printSemCtrlErrorInfo(int err); void printSemOperErrorInfo(int err); void printSemGetErrorInfo(int err); int main() { int nRet=-1; //struct semid_ds sem_info; //struct seminfo sem_info2; //struct sembuf askfor_res, free_res; //获取对应的键值 key_t key=ftok(SEM_PATH, 25); if(key==-1) { printf("映射键值失败\n"); int tempErrno=errno; printftokErrorInfo(errno); return -1; } //创建并确保创建信号灯集成功 int get_flag=IPC_CREAT|IPC_EXCL; int nSems=1; //标识是否已经创建成功 bool bInitDone=false; //创建一个信号灯集,且其中只有一个信号灯 semid=semget(key, nSems, get_flag); if(semid<0) { //信号灯集已经存在 int tempErrno=errno; if(tempErrno==EEXIST) { get_flag=IPC_CREAT; //获取已有的信号灯集 semid=semget(key, nSems, get_flag); //最多尝试max_tryCount次 for(int nIter=0; nIter<max_tryCount; nIter++) { nRet=semctl(semid, 0, IPC_STAT, semState); //获取信号灯集的状态失败 if(nRet==-1) { int tempErrno=errno; printSemCtrlErrorInfo(tempErrno); break; } //获取信号灯集状态成功 else { //如果sem_otime!=0,则说明初始化信号集成功(为了解决竞争关系) if(semState.buf->sem_otime!=0) { bInitDone=true; break; } }//end of else }//end of for }//end of if(errno==EEXIST) printSemGetErrorInfo(tempErrno); }//end of if(semid<0) else { bInitDone=true; } //创建过程(包括初始化)结束 if(bInitDone==false) { printf("创建或打开信号灯集失败\n"); exit(-1); } //设置信号集的值 semState.val=1; nRet=semctl(semid, 0, SETVAL, semState); if(nRet==-1) { int tempErrno=errno; printSemCtrlErrorInfo(tempErrno); printf("设置信号集的值失败\n"); exit(-1); } //输出信号集的状态信息 printSemState(); //申请资源(使信号值减少) printf("申请资源\n"); //now ask for available resource: sembuf askfor_res; askfor_res.sem_num=0; askfor_res.sem_op=-1; askfor_res.sem_flg=SEM_UNDO; nRet=semop(semid, &askfor_res, 1); if(nRet==-1) { int tempErrno=errno; printSemOperErrorInfo(tempErrno); exit(-1); } //do some handling on the sharing resource here, just sleep on it 3 seconds sleep(3); //释放资源(使信号值增加) printf("释放资源\n"); //对信号集上的一个操作 sembuf semOper; semOper.sem_num=0; semOper.sem_op=1; semOper.sem_flg=SEM_UNDO; //执行具体的操作 nRet=semop(semid, &semOper, 1); if(nRet==-1) { int tempErrno=errno; printSemOperErrorInfo(tempErrno); exit(-1); } //删除信号集 nRet=semctl(semid, 0, IPC_RMID); if(nRet==-1) { int tempErrno=errno; printSemCtrlErrorInfo(tempErrno); exit(-1); } } //输出信号集的状态信息 void printSemState() { if(semid==-1) { printf("信号灯集为空\n"); return; } //semid_ds sem_info; //semState.buf=&sem_info; int nRet=semctl(semid, 0, IPC_STAT, semState); if(nRet==-1) { int tempErrno=errno; printSemCtrlErrorInfo(tempErrno); return; } printf("信号集所属进程的UID为%d\n", semState.buf->sem_perm.uid); printf("创建信号集的进程UID为%d\n", semState.buf->sem_perm.cuid); //seminfo sem_info2; //semState.__buf=&sem_info2; nRet=semctl(semid, 0, IPC_INFO, semState); if(nRet==-1) { int tempErrno=errno; printSemCtrlErrorInfo(tempErrno); return; } printf("Number of entries in semaphore mapis %d\n", semState.__buf->semmap); printf("Maximum number of semaphore sets is %d\n", semState.__buf->semmni); printf("Maximum number of semaphores in all semaphore sets is %d\n", semState.__buf->semmns); printf("System-wide maximum number of undo structures is %d\n", semState.__buf->semmnu); printf("Maximum number of semaphores in a set is %d\n", semState.__buf->semmsl); printf("Maximum number of operations for semop() is %d\n", semState.__buf->semopm); printf("Maximum number of undo entries per process is %d\n", semState.__buf->semume); printf("Size of struct sem_undo is %d\n", semState.__buf->semusz); printf("Maximum semaphore valu is %d\n", semState.__buf->semvmx); printf("Max. value that can be recorded for semaphore adjustment is %d\n", semState.__buf->semaem); }
执行结果如下:
(注意:必须有ROOT权限才可以执行,否则会出现访问权限错误)
输出信号集状态
信号集所属进程的UID为0(0表示根进程)
创建信号集的进程UID为0
Number of entries in semaphore mapis 32000
Maximum number of semaphore sets is 128
Maximum number of semaphores in all semaphore sets is 32000
System-wide maximum number of undo structures is 32000
Maximum number of semaphores in a set is 250
Maximum number of operations for semop() is 32
Maximum number of undo entries per process is 32
Size of struct sem_undo is 20
Maximum semaphore valu is 32767
Max. value that can be recorded for semaphore adjustment is 32767
申请资源
释放资源小结:
信号灯与其它进程间通信方式有所不同,它主要用于进程间同步。
通常所说的系统V信号灯实际上是一个信号灯的集合,可用于多种共享资源的进程间同步。每个信号灯都有一个值,可以用来表示当前该信号灯代表的共享资源可用(available)数量;
如果一个进程要申请共享资源,那么就从信号灯值中减去要申请的数目,如果当前没有足够的可用资源,进程可以睡眠等待,也可以立即返回;
当进程要申请多种共享资源时,linux可以保证操作的原子性,即要么申请到所有的共享资源,要么放弃所有资源,这样能够保证多个进程不会造成互锁;
Linux对信号灯有各种各样的限制,程序中给出了输出结果。