进程间通信之POSIX信号量
POSIX信号量接口,意在解决XSI信号量接口的几个不足之处:
- POSIX信号量接口相比于XSI信号量接口,允许更高性能的实现。
- POSIX信号量接口简单易用:没有信号量集,其中一些接口模仿了我们熟悉的文件系统操作。
- POSIX信号量删除时的处理更加合理。XSI信号量被删除后,使用该信号量标识符的操作将会出错返回,并将errno设置为EIDRM。而对于POSIX信号量,操作可以继续正常执行,直到对该信号量的最后一个引用被释放。
POSIX信号量有两种形式可供选用:有名和无名。它们的区别在于,如何被创建和销毁,其他方面则完全相同。无名信号量只存在于内存中,并且规定能够访问该内存的进程才能够使用该内存中的信号量。这就意味着,无名信号量只能被这样两种线程使用:(1)来自同一进程的各个线程(2)来自不同进程的各个线程,但是这些进程映射了相同的内存范围到自己的地址空间。相反,有名信号量则是通过名字访问,因此,来自于任何进程的线程,只要知道该有名信号量的名字都可以访问。
调用sem_open函数可以创建一个新的有名信号量,或打开一个现存的有名信号量。
#include <semaphore.h> sem_t *sem_open(const char *name, int oflag, ... /* mode_t mode, unsigned int value */ ); 返回值:若成功则返回指向信号量的指针,若出错则返回SEM_FAILED
如果使用一个现存的有名信号量,我们只需指定两个参数:信号量名和oflag(oflag取0)。把oflag设置为O_CREAT标志时,如果指定的信号量不存在则新建一个有名信号量;如果指定的信号量已经存在,那么打开使用,无其他额外操作发生。
如果我们指定O_CREAT标志,那么就需要提供另外两个参数:mode和value。mode用来指定谁可以访问该信号量。它可以取打开文件时所用的权限位的取值(参考http://www.cnblogs.com/nufangrensheng/p/3502097.html中表4-5)。最终赋予信号量的访问权限,是被调用者文件创建屏蔽字所修改过的(http://www.cnblogs.com/nufangrensheng/p/3502328.html)。然而,注意通常只有读写权限对我们有用,但是接口不允许在我们打开一个现存的信号量时指定打开模式(mode)。实现通常以读写打开信号量。
value参数用来指定信号量的初始值。它可取值为:0-SEM_VALUE_MAX。
如果我们想要确保我们在创建一个新的信号量,可以把oflag参数设置为:O_CREAT|O_EXCL。如果信号量已经存在的话,这会导致sem_open调用失败。
为了提高移植性,我们在选择信号量名字的时候,必须遵循一定的约定:
- 名字的首字符必须是斜杠(/)。
- 除首字符外,名字中不能再包含其他斜杠(/)。
- 名字的最长长度由实现定义,不应超过_POSIX_NAME_MAX个字符。
sem_open函数返回一个信号量指针,该指针可供其他对该信号量进行操作的函数使用。使用完成后,调用sem_close函数释放与信号量相关的资源。
#include <semaphore.h> int sem_close(sem_t *sem); 返回值:若成功则返回0,出错返回-1
如果进程还没有调用sem_close就已经退出,那么内核会自动关闭该进程打开的所有信号量。注意,这并不会影响信号量值的状态——例如,如果我们增加了信号量的值,我们退出后这个值不会改变。同样,如果我们调用了sem_close,信号量值也不会受到影响。POSIX信号量机制中并没有如同XSI信号量中的SEM_UNDO标志。
调用sem_unlink函数来销毁一个有名信号量。
#include <semaphore.h> int sem_unlink(const char *name); 返回值:若成功则返回0,出错则返回-1
sem_unlink函数移除信号量的名字。如果当前没有打开的对该信号量的引用,那么就销毁它。否则,销毁被推迟到最后一个打开的引用被关闭。
与XSI信号量不同,我们只能对POSIX信号量的值进行加1或减1。对信号量值减1,就类似于对一个二值信号量加锁或请求一个与计数信号量相关的资源。
注意,POSIX信号量并没有区分信号量类型。使用二值信号量还是计数信号量,取决于我们如果对信号进行初始化和使用。如果信号量值只能取0和1,那么它就是一个二值信号量。当一个二值信号量值为1,我们则说它未加锁;若它的值为0,则说它已加锁。
调用sem_wait或sem_trywait函数,请求一个信号量(对信号量值执行减1操作)。
#include <semaphore.h> int sem_trywait(sem_t *sem); int sem_wait(sem_t *sem); 两个函数返回值:若成功则返回0,出错则返回-1
如果信号量计数为0,这时如果调用sem_wait函数,将会阻塞。直到成功对信号量计数减1或被一个信号中断,sem_wait函数才会返回。我们可以使用sem_trywait函数以避免阻塞。当我们调用sem_trywait函数时,如果信号量计数为0,sem_trywait会返回-1,并将errno设置为EAGAIN。
第三种方法是可以阻塞一段有限的时间,这时我们使用sem_timedwait函数。
#include <semaphore.h> #include <time.h> int sem_timedwait(sem_t *restrict sem, const struct timespec *restrict tsptr); 返回值:若成功则返回0,出错则返回-1
tsptr参数指定了希望等待的绝对时间。如果信号量可以被立即减1,那么超时也无所谓,即使你指定了一个已经过去的时间,试图对信号量减1的操作也会成功。如果直到超时,还不能对信号量计数减1,那么sem_timedwait函数将会返回-1,并将errno设置为ETIMEDOUT。
调用sem_post函数对信号量值加1。这类似于对一个二值信号量解锁或释放一个与计数信号量有关的资源。
#include <semaphore.h> int sem_post(sem_t *sem); 返回值:若成功则返回0,出错则返回-1
当我们调用sem_post的时,如果此时有因为调用sem_wait或sem_timedwait而阻塞的进程,那么该进程将被唤醒,并且刚刚被sem_post加1的信号量计数紧接着又被sem_wait或sem_timedwait减1。
如果我们想要在一个单一进程内使用POSIX信号量,那么使用无名信号量会更加简单。无名信号量只是创建和销毁有所改变,其他完全和有名信号量一样。我们调用sem_init函数创建一个无名信号量。
#include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value); 返回值:若成功则返回0,出错返回-1
pshared参数指示我们是否要在多进程之间使用该无名信号量。如果要在多个进程之间使用,则将pshared设置为非0值。value参数指定信号量的初始值。
另外,我们需要声明一个sem_t类型的变量,并把它的地址传给sem_init,以便对该变量进行初始化。如果我们要在两个进程之间使用该无名信号量,我们需要确保sem参数指向这两个进程共享的内存范围内。
我们可以调用sem_destroy函数来销毁用完的无名信号量。
#include <semaphore.h> int sem_destroy(sem_t *sem); 返回值:若成功则返回0,出错则返回-1
调用sem_destroy后我们将不能再以sem为参数调用任何信号量函数,除非我们再次使用sem_init对sem进行初始化。
我们可以调用sem_getvalue函数来获取信号量值。
#include <semaphore.h> int sem_getvalue(sem_t *sem, int *restrict valp); 返回值:若成功则返回0,出错则返回-1
如果sem_getvalue执行成功,信号量的值将存入valp指向的整型变量中。但是,需要小心,我们刚读出来的信号量值可能会改变(因为我们随时可能会使用该信号量值)。如果不采取额外的同步机制的话,sem_getvalue函数仅仅用来调试。
实例
回忆http://www.cnblogs.com/nufangrensheng/p/3523623.html的表12-4,Single UNIX Specification并没有定义当一个线程锁定了一个normal类型的mutex,而另外一个线程试图对此mutex进行解锁会发生什么(对应于表12-4中“不占用时解锁”栏)。但是对于error-checking类型和recursive类型的mutex,在这种情况下则会出错。因为二值信号量可以像互斥量(mutex)一样使用,我们可以使用信号量创建我们自己的锁原语(primitive)来提供互斥。
假定我们要创建自己的锁:它可以被一个线程加锁,而被另外一个线程解锁。我们的锁结构可以如下:
struct slock { sem_t *semp; char name[_POSIX_NAME_MAX]; };
程序清单15-35显示了一种基于信号量的互斥原语的实现。
程序清单15-35 使用POSIX信号量的互斥
#include "slock.h" #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <error.h> struct slock * s_alloc() { struct slock *sp; static int cnt; if((sp = malloc(sizeof(struct slock))) == NULL) return(NULL); do { snprintf(sp->name, sizeof(sp->name), "/%ld.%d", (long)getpid(), cnt++); sp->semp =sem_open(sp->name, O_CREAT|O_EXCL, S_IRWXU, 1); } while((sp->semp == SEM_FAILED) && (errno == EEXIST)); if(sp->semp == SEM_FAILED) { free(sp); return(NULL); } sem_unlink(sp->name); return(sp); } void s_free(struct slock *sp) { sem_close(sp->semp); free(sp); } int s_lock(struct slock *sp) { return(sem_wait(sp->semp)); } int s_trylock(struct slock *sp) { return(sem_trywait(sp->semp)); } int s_unlock(struct slock *sp) { return(sem_post(sp->semp)); }
我们基于进程ID和计数counter创建一个名字。我们无需使用互斥量对counter加以保护,因为如果两个相互竞争的线程同时调用s_alloc并且分配了相同的名字,但是使用O_EXCL标志调用sem_open时只会有一个调用成功,而另一个以EEXIST出错返回,这时我们只需重新调用一次就行了。注意,我们在打开信号量后随即销毁它。这样其他进程就不能再访问它,而且简化了进程结束时的清理工作。
本篇博文内容摘自《UNIX环境高级编程》(第3版),仅作个人学习记录所用。关于本书可参考:http://www.apuebook.com/。