进程间通信---POSIX信号量实现机制
1. 什么是POSIX
首先,我们需要弄清楚的第一个问题是,什么是POSIX?
POSIX是可移植操作系统接口(Portable Operating System Interface)的缩写,是IEEE为了在各种UNIX操作系统(POSIX最后一个字母来自UNIX的X)上运行软件而定义的一系列API标准总称,正式称呼为IEEE 1003,目前,此协议已经被大多数操作系统所支持。
现在,我们弄明白了什么是POSIX,所谓的POSIX就是一套API的标准,各个操作系统厂商如果支持POSIX协议,那么此协议下的API都要按照协议的要求来实现,以方便软件的系统移植工作。
那么,什么是POSIX信号量?
顾名思义,POSIX信号量,就是在POSIX标准下实现的信号量,至于什么是信号量,请查看《进程间通信----信号量》一节。
在软件的开发过程中,如果你需要使用某个POSIX标准定义的接口,那么首先你需要先确认目标工作机器的操作系统是否支持POSIX标准(目前大多数的操作系统都支持)。
2. POSIX信号量的分类
POSIX信号量分别为有名信号量和无名信号量两类:
2.1 有名信号量
有名信号量通过一个名字来作为标识,名字的格式为"/somename",这个名字的长度为MAX_NAME - 4(NAME_MAX是一个宏定义,大小为,定义在头文件 中,各个系统都会实现这个宏的定义,抱歉,本人未查询到NAME_MAX的相关定义),信号量名字以'/'为开始,以'\0'字符为结尾,并且中间字符串中不能再有'/'。
两个进程之间可以通过sem_open函数来操作同一个有名信号量,本小结中出现的函数会在第4章节中进行详细的介绍。
用户可以通过sem_open函数接口创建一个新的有名信号量或者打开一个已有的有名信号量。有名信号量被打开之后,进程或线程(有名信号量一般应用在进程之间的同步控制,下小节介绍的无名信号量一般应用在线程之间的同步控制)可以通过sem_post或者sem_wait接口进行信号量操作。当一个进程不在使用有名信号量后,可以通过sem_close接口来关闭它,当系统中所有的进程都不在使用某个有名信号量后,可以通过sem_unlink接口将它从系统中移除。
2.2 无名信号量
与有名信号量不同的,无名信号量并没有名字,那么如何去标识一个无名信号量呢,POSIX标准只是规定,无名信号量必须被放置在一段可以被多线程或者多进程所共享的内存区域中。
无名信号量有一个共享属性,分为线程共享属性和进程共享属性两类。
一个线程共享属性(无名信号量的共享属性在后续章节中会介绍,它分为线程共享属性和进程共享属性两类)的无名信号量必须被放置在一个可以被进程中所有线程所共享的内存区域中,比如全局变量。
一个进程共享属性的无名信号量必须被放置在共享内存区域,系统V(System V,后续文章会对System V标准进行介绍)通过shmget(他是一个System V标准定义的一个接口)接口实现共享内存,POSIX通过shm_open接口来实现共享内存区域。
无名信号量在使用之前必须通过sem_init接口进行初始化,之后可以通过sem_wait和sem_post接口进行操作,当不再使用无名信号量,或者放置无名信号的区域被释放之前,用户都应该通过sem_destroy接口来释放无名信号量。
3. POSIX信号量实现了哪些接口函数
本章节主要针对第二章节中涉及到的POSIX标准下的信号量接口进行详细的描述,依据《Linux用户手册release4.04》。
3.1 有名信号量接口说明
3.1.1 sem_open
我们在使用一个有名信号量的时候,第一步就是要创建或者打开一个已有的有名信号量,我们需要的函数就是sem_open,函数原型如下:
(1)sem_t *sem_open(const char *name, int oflag);
(2)sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
函数功能:创建并初始化或者打开一个已有的有名信号量。
返回值:成功的话,函数返回有名信号量的地址,这个返回值会在其它一些相关的函数中作为操作对象的索引而被使用,比如sem_post、sem_wait等函数。失败的话返回SEM_FAILED。我们在使用此函数的时候,都应该去检查返回值,以确保我们的调用是正确的。
参数:
name:有名信号量的名字,在2.1章节进行过详细的描述,这里不再重复。
oflag:此参数控制函数内部的具体操作行为,如果oflag参数里有O_CREAT位,那么如果此信号量不存在则创建此信号量并初始化它,此时信号量的user ID被设置为调用进程的有效user ID,group ID被设置成调用进程的group ID,如果oflag参数里既包含O_CREAT位也包含O_EXCL位,那么如果指定名字的信号量已存在那么就会返回错误。
mode:如果oflag参数里面包含O_CREAT位,那么我们就应用调用第二个同名函数(2),并将操作权限设置到mode参数里面,只要用户想使用此信号量,那么读写权限都应该设置到mode参数里(这里的读写权限,跟我们常用的打开文件函数open一样,比如:O_RDONLY O_WRONLY O_RDWR等)。
value:想要设置的有名信号量的初始值,无符号整形值,即不小于0的整形值。
如果O_CREAT被设置,并且信号量已经存在,那么系统会自动忽略mode跟value参数。
我们在调用sem_open函数的时候,一般有这么几种使用方法:
(1)新建一个有名信号量 sem_open("/name", O_CREAT, O_RDWR, 1);
成功:若果有名信号量不存在,则创建一个名字为”/name"可读可写初始value为1的有名信号量。如果信号量存在,则返回已有有名信号量的地址,并且忽略mode跟value参数的值。
失败:返回SEM_FAILED。
(2)必须新建一个信号量 sem_open("/name", O_CREAT|O_EXCL, O_RW, 1);
若信号量不存在,则创建一个名字为”/name"可读可写初始value为1的有名信号量,如果信号量存在,则失败返回SEM_FAILED。
3.2 无名信号量接口说明
3.2.1 sem_init
函数原型:
int sem_init(sem_t *sem, int pshared, unsigned int value)
函数sem_init的作用是在参数sem指向的地址上初始化一个无名信号量,其value值由参数value指定。
参数pshared指定此无名信号量实在进程之间被共享使用,还是在线程之间被共享使用。
如果参数pshared等0,那么此信号量只能在一个进程内的线程之间共享使用,所以,它应该被实现在可以被所有线程都能访问到的地址上,如全局变量或者动态分配在堆上。
如果参数pshared是一个非0值,那么此信号量被多个进程共享使用,它应该被实现在共享内存里,或者父子进程之间使用。
注意:如果使用此函数去初始化一个已经被初始化的信号量,此函数的行为是未被定义的。
返回值:成功返回0,失败返回-1.
3.2.2 sem_destroy
函数原型:
int sem_destroy(sem_t *sem)
描述:函数sem_destroy的作用是销毁由参数sem指向的无名信号量。只有用sem_init初始化的无名信号量才可以用此函数来销毁,并且程序开发人员也应该使用此函数去销毁这个无名信号量。
当销毁一个无名信号量时,其它应为调用sem_wait函数而堵塞在此信号量上的进程或线程的行为是未定义的。
当使用一个已经被销毁的信号量时,后果也是未定义的。
返回值:成功返回0,失败返回-1
注意:当销毁一段内存时,如果此内存上有已经初始化的无名信号量,那么在销毁内存之前,应该先调用sem_destroy函数来销毁信号量。
3.3 信号量通用接口说明
3.3.1 sem_wait
函数原型:
(1)int sem_wait(sem_t *sem);
(2)int sem_trywait(sem_t *sem);
(3)int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
sem_wait系列操作,即我们所说的PV操作中的P操作,目的是锁住一个信号量。
(1) sem_wait函数将由参数sem指定的信号量减一,即上锁操作。如果信号量的值大于0,那么执行减一操作,并且函数立即返回。如果信号量当前的值为0,那么调用进程会一直阻塞直到信号量变成大于0(由其它进程执行了sem_post操作,sem_post函数会在3.1.3章节进行箱明细描述,它执行信号量加一操作)或者被信号中断此调用。
(2)sem_trywait函数同sem_wait函数的作用一样,不同是如果不能立即执行加一操作,则调用进程不会堵塞而是返回一个错误,errno会被设置成EAGAIN。
(3)sem_timedwait函数同sem_wait函数的作用一样,不同是如果不能立即执行加一操作,则调用进程会堵塞一定的时间段,这个时间段由函数参数abs_timeout指定。如果在指定的时间内信号量仍不能被锁住,则函数返回超时错误,errno会被设置成ETIMEDOUT。如果信号量的减一操作可以被立即执行,则此函数永远都不会返回超时错误,并且参数abs_timeout的有效性也不会被检查。
返回值:成功返回0,失败:信号量的值不变,返回-1。
3.3.2 sem_post
函数原型:
#include
int sem_post(sem_t *sem)
Link with -pthread
与sem_wait相对应的函数就是sem_post,即我们PV操作里面的V操作。
此函数将sem指向的信号量解锁(加一操作),加一操作后如果信号量的值变成大于0,那么另外一个因为调用sem_wait函数而被堵塞的进程或者线程将会被唤醒并且去执行对信号量的加锁操作。
返回值:成功返回0;失败返回-1,并且信号量值不变。
注意:sem_post函数是异步信号安全的,它可以在一个信号处理函数中被调用。
3.3.3 sem_getvalue
函数原型:
int sem_getvalue(sem_t *sem, int *sval)
sem_getvalue函数将参数sem指向的信号量的当前值存储到参数sval指向的整形变量中。
如果,当前有一个或多个进程或线程正在因为调用sem_wait函数而堵塞中,那么POSIX标准规定可以返回两种数据到sval中,一种是0,另外一种是绝对值等于等待进程或线程的数量的和的负数。
返回值:成功返回0,失败返回-1。
注意:在实际的应用开发中很少使用此函数,作为程序开发人员也应该尽量避免使用此函数,因为当获取到value值后,信号量的值可能已经改变了。