Unix下信号灯和共享内存的使用方法 1互斥的概念 在Unix中经常遇到多个进程需要使用同一资源的情况。共享资源对每个进程而言就是一份完全属于自己的拷贝。但是由于资源是共享的,就会产生问题:在任意两次操作之间,如果其它进程对该资源做了更改,本进程是无法知道其状态的,因此也不可能进行有效的控制。如果对资源的两次操作是有因果关系的,则更不允许其它进程在操作期间改变资源状态。 因此必须提供互斥机制以使一次只能由一个进程使用资源。常用的方法有文件封锁、信号灯等。文件封锁和信号灯都能够用于Unix文件系统读写的互斥,但信号灯主要用于实现不同进程对共享内存的存取。信号灯作为进程间通信(IPC)的一种方式,不象管道、消息队列那样用于传递大量数据,而是提供了一种机制用来协调不同进程间的同步。 本文阐述了Unix System V下信号灯的使用方法,以及如何通过信号灯实现对共享内存的读写操作。 2信号灯的使用 2.1信号灯的概念 信号灯是一个整数变量,用来代表资源计数器,记录了某个时刻可以获得的资源的数目。最简单的情况是一个二值信号灯,它只有两个值:0和1,分别表示一个资源的“可用”和“不可用”状态。 System V下的信号灯是一个复杂的数据结构,它代表一个非负整数的集合,该集合元素的个数以及每个元素的取值范围都只受系统资源的限制。内核为系统的每个信号灯都维持如下数据结构(其定义包含在头文件sys/sem.h中): struct semid_ds{ struct ipc_perm sem_perm; /* operation permission */ struct sem *sem_base; /* ptr to first semaphore in set */ unsigned short sem_nsems; /* number of semaphores in set */ time_t sem_otime; /* last semop time */ time_t sem_ctime; /* last change time */ }; ipc_perm结构记录了关于信号灯的所有者、同组者和其他用户的PID号,信号灯的读写权限,操作顺序以及信号灯的键值(KEY)等信息。该数据结构适用于所有使用全局唯一标识号KEY的进程通信。其定义参见sys/ipc.h。 sem结构指明了每一个信号灯的值和使用状态,其定义如下(包含在头文件sys/sem.h中): struct sem{ unsigned short semval; /* semaphore value. */ pid_t sempid; /* pid of last operation. */ unsigned short semncnt; /* number of semaphores */ /* awaiting semval>cval. */ unsigned short semzcnt; /* number of semaphores */ /* awaiting semval=0. */ }; 图1是一个信号灯集合的示意图:
2.2关于信号灯操作的系统调用说明 系统为信号灯操作提供了3个调用:semget, semop, semctl,这些调用都使用了以下头文件: #include #include #include 下面是系统调用的详细说明。 2.2.1系统调用semget 作用:创建一个信号灯集合或者访问一个已经存在的信号灯集合。 形式:int semget(key_t key, int nsems, int semflag); 返回值:信号灯集合的标识semid;如果错误返回-1。 描述: key为信号灯集合的标识号,它是全系统范围内唯一的键值;nsems表示创建的信号灯的个数;semflag指明该信号灯集合的打开方式,它是由一些常数组合(按位或)而成的,见下表: 表2-1 信号灯调用semget中sem_flg参数选择项 数值(八进制) 符号 描述 0400 0200 0040 0020 0004 00020100002000 SEM_RSEM_ASEM_R>>3SEM_A>>3SEM_R>>6SEM_A>>6IPC_CREATIPC_EXCL 拥有者可读拥有者可写同组者可读同组者可写其他人可读其他人可写创建或获取一个信号灯集合创建一个新的信号灯集合 说明: 1. 设置semflag的IPC_CREAT位,则创建一个信号灯集合,如果该信号灯集合已经存在,则返回其标识符(semid); 2. 设置semflag的IPC_CREAT|IPC_EXCL位,则创建一个新的信号灯集合,如果已经存在则返回错误信息; 3. 只设置IPC_EXCL位而不设置IPC_CREAT位没有任何意义。 2.2.2系统调用semop 作用:对信号灯集合中一个或多个信号灯进行操作 形式:int semop(int semid, struct sembuf *sops, unsigned int nops); 返回值:操作成功返回0,失败返回-1。 描述: 数组sops的每个元素对应一次操作。nops指明执行几次操作。 sembuf是具有如下形式的一个数据结构的数组: struct sembuf{ unsigned short sem_num; /* semaphore number. */ short semop; /* semaphore operation. */ short sem_flg; /* operation flags. */ } 该结构对应了信号灯的某种操作。 信号灯的sem_num值标明它是信号灯集合的第几个元素,第一个信号灯为0,第二个为1,依次类推。semop确定了对信号灯采取什么样的操作,它可以为负数,正数和零。sem_flg指明操作的执行模式,它有两个标志位: IPC_NOWAIT(04000):指明以非阻塞方式操作信号灯。 SEM_UNDO(010000): 指明内核为信号灯操作保留恢复值。 System V将信号灯的值semval初始化为0。信号灯值为0时可以使用,为非0时不可使用。对信号灯的操作可以用以下原语来描述: 1.If sem_op>0 Then /*sem_op>0对应对信号灯的抢占。*/ semval=semval+sem_op; /*抢占成功,信号灯当前值加上sem_op*/ 2.If sem_op==0 Then /*sem_op==0对应对信号灯使用权的请求。*/ { If semval==0 Then /*semval==0表明信号灯可以使用,*/ 立即返回; /*请求成功. */ Else /*semval!=0表明信号灯不可使用*/ { If(sem_flg&IPC_NOWAIT) /*IPC_NOWAIT指明如果信号灯不可使用,*/ 立即返回; /*则不等待,立即返回。*/ Else /*如果信号灯不可使用,则一直等待。*/ 进程挂起直到:semval==0; } } 3.If sem_op<0 Then /*sem_op<0对应对信号灯的释放。*/ { If(semval+sem_op)>=0 Then /*semval+sem_op>=0表明需要释放信号灯。*/ semval=semval+sem_op; /*信号灯当前值减去|sem_op|,释放信号灯。*/ Else { If(sem_flg&IPC_NOWAIT) /*如果无需释放信号灯, */ 立即返回; /*且IPC_NOWAIT置位,则立即返回。*/ Else /*挂起等待释放信号灯的条件满足。 */ 进程挂起直到:(semval+sem_op)>=0; } } 以上原语描述的情况比较复杂,在一般应用中实际只使用二值信号灯,即semval只能为1或0,其操作流程就很容易理解了。举个实际的例子来说明对信号灯的操作。我们创建一个二值信号灯,给信号灯加锁前先要等待信号灯的值变为0,获取信号灯后将其值加1。给信号灯解锁的是直接给信号灯减1。 加锁的sembuf及其操作如下: static struct sembuf sem_lock[2]= { 0,0,0, /* sem_lock[0] */ 0,1,SEM_UNDO /* sem_lock[1] */ }; semop(semid, &sem_lock[0], 2); 说明: 该调用执行了两次操作:先等待信号灯的值变为0并获取信号灯操作权,然后将信号灯当前值semval加1,表明对信号灯占用成功。其它进程要使用该信号灯必须先等待semval变为0。 注意内核一次将sembuf中多个操作连续执行完,在这个期间,其它进程无法对该信号灯进行操作。比较以下两个操作的组合与以上操作的区别: semop(semid, &sem_lock[0], 1); semop(semid, &sem_lock[1], 1); 虽然这两个操作的组合完成了上面的功能,但对内核而言不一定是连续进行的,在两次操作期间会有其它进程使用该信号灯。因此这两个操作是无效的。 解锁的sembuf如下: static struct sembuf sem_unlock[1]= { 0, -1, (IPC_NOWAIT|SEM_UNDO) } 说明: 如果当前信号灯的值semval=1,则当前值减1为0,表明信号灯再次可用。因为在我们的程序里是先加锁,后解锁,所以semval一定为1。但是为了避免错误使用时导致程序挂起,一般我们将sem_flg的IPC_NOWAIT位置上,当释放失败时立即返回错误信息。 在上面的例子里面我们使用了sem_flg的SEM_UNDO位。系统为每个操作信号灯的进程维持了一个操作调整值(semaphore adjustment value),SEM_UNDO标志指示系统在进程退出时恢复该进程使用信号灯之前的信号灯值。SEM_UNDO标志位的作用可以描述如下: 1. 信号灯初始化时调整值一概为0;在信号灯删除时与其相关的调整值一起被删除。 2. 对每个进程,信号灯值增加,调整值等量减小;信号灯值减小,调整值等量增加。 3. 当进程调用exit退出时,内核自动为该进程恢复其操作以前的信号灯值。 引进SEM_UNDO标志的作用是为了防止当进程异常退出时没有释放信号灯,而其它进程只能永远处于等待状态。 2.2.3 系统调用semctl 作用:提供对信号灯集合中某个信号灯的多种控制功能。 形式:int semctl(int semid, int semnum, int cmd, union semun arg); 返回值:操作成功时根据cmd的不同返回需要的值或0,失败返回-1。 描述: semid是信号灯集合的标识号,semnum是信号灯号。cmd标识了操作类别。arg用于进行某些操作时存放数据。 semun数据结构如下: union semnum{ int semval; /*当cmd为SETVAL时有效。 */ struct semid_ds *buf; /*当cmd为IPC_STAT或IPC_SET时有效。 */ ushort *array; /*当cmd为IPC_GETALL或IPC_SETALL时有效。*/ } cmd操作包括: GETVAL:返回信号灯值。 SETVAL:置信号灯值为arg.val。信号灯调整值同时清零。 GETPID:返回sempid。 GETNCNT:返回semncnt。 GETZCNT:返回semzcnt。 GETALL:获取信号灯集合的每个信号灯的值,存入arg.array。 SETALL:置信号灯集合中每个信号灯的值为arg.array的值,对应的调整值同时清零。 IPC_STAT:获取信号灯集合的semid_ds,存入arg.buf。 IPC_SET:将arg.buf数据结构的如下成员赋给信号灯的semid_ds结构: sem_perm.uid; sem_perm.gid sem_perm.mode; /* 只有低9位有效。 */ 能够进行此项操作的进程限于超级用户、sem_perm.cuid或sem_perm.uid。 IPC_RMID:删除指定信号灯集合。能够进行此项操作的进程限于超级用户、 sem_perm.cuid或sem_perm.uid。
阅读(1708) | 评论(0) | 转发(0) |