一、信号量的概念
信号量(也叫信号灯)是一种用于提供不同进程间或一个给定进程的不同线程间同步手段的原语。信号量是进程/线程同步的一种方式,有时候我们需要保护一段代码,使它每次只能被一个执行进程/线程运行,这种工作就需要一个二进制开关;有时候需要限制一段代码可以被多少个进程/线程执行,这就需要用到关于计数信号量。信号量开关是二进制信号量的一种逻辑扩展,两者实际调用的函数都是一样的。
信号量分为以下三种。
1、System V信号量,在内核中维护,可用于进程或线程间的同步,常用于进程的同步。
2、Posix有名信号量,一种来源于POSIX技术规范的实时扩展方案(POSIX Realtime Extension),可用于进程或线程间的同步,但常用于线程。
3、Posix基于内存的信号量,存放在共享内存区中,可用于进程或线程间的同步。
这里只介绍Sysem V信号量。
为了获得共享资源进程需要执行下列操作:
(1) 测试控制该资源的信号量。
(2) 若此信号量的值为正,则进程可以使用该资源。进程信号量值减1,表示它使用了一个资源单位。此进程使用完共享资源后对应的信号量会加1。以便其他进程使用。
(3) 若此信号量的值为0,则进程进入休息状态,直至信号量值大于0。进程被唤醒后,它返回至第(1)步。
为了正确地实现信号量,信号量值的测试值的测试及减1操作应当是原子操作。为此信号量通常是在内核中实现的。
System V信号量
System V每一个信号量函数都能对成组的通用信号量进行操作,这在一个进程需要锁定多个资源的时候是很容易办到的。
与消息队列相似,信号量也有一个结构semid_ds用于设置信号量的信息。
struct semid_ds
{
struct ipc_perm sem_perm; /*设置权限和所有者*/
struct sem *sem_base;/*描述一个信号量集中的每个信号量的结构*/
unsigned short sem_nsems; /*信号量集中信号量的数量*/
time_t sem_otime; /*最近semop时间*/
time_t sem_ctime; /*最近修改的时间*/
…….
};
sem_perm结构的uid和cuid成员被置为调用进程的有效用户ID,gid和cgid成员被置为调用进程的有效组ID。
sem_otime被置为0,sem_ctime则被置为当前时间。
sem_nsems被置为nsems参数的值。
sem结构是内核用于维护某个给定信号量的一组值的内部数据结构。一个信号量集的每个成员由下面的结构描述:
struct sem
{
unsigned short_t semval; /*信号量的值*/
short sempid; /*对信号量最后调用semop函数的进程ID*/
unsigned short_t semncnt; /*等待其值增长的进程数*/
unsigned short_t semzcnt; /*等待其值变为0的进程数*/
};
内核除维护一个信号量集中每个信号量的实际值之外,内核还给该集合中每个信号量维护另外三个信息:对其值执行最后一次操作的进程的进程ID、等待其值增长的进程数计数以及等待其值变为0的进程数计数。
二、信号量函数
1.
名称:: |
semget |
功能: |
获得一个信号量id |
头文件: |
#include <sys/types.h> #include <sys/ipc.h> #inlcude <sys/sem.h> |
函数原形: |
int semget(key_t key,int nsems,int flag); |
参数: |
key 键值 nsems 信号量数 flag 选项 |
返回值: |
若成功信号量id,若出错返回-1。 |
key是一个建我们可以用ftok函数来获得。key是一个整数值,不相关的进程将通过这个值去访问同一信号量。程序对任何信号量的访问都必须间接地进行,先由程序提供一个键字,再由系统生成一个相应的信号量标识码。
semget用于获得一个信号量ID,同msg相似此函数也需要提供一个唯一的外部键,作为信号量的外部标识。
nsems参数是需要使用的信号量个数。如果是创建新集合,则必须制定nsems。如果引用一个现存的集合,则将nsems指定为0。一旦创建完毕一个信号量集,我们就不能改变其中的信号量数。
flag参数是一组标志,其作用与open函数的各种标志很相似。它低端的九个位是该信号量的权限,其作用相当于文件的访问权限。但它们可以与键值IPC_CREAT做按位的或操作以创建一个新的信号量。即使在设置了IPC_CREAT标志后给出的是一个现有的信号量的键字,也并不是一个错误。我们也可以通过IPC_CREAT和IPC_EXCL标志的联合使用确保自己将创建出一个新的独一无二的信号量来,如果该信号量已经存在,就会返回一个错误。
还有IPC_NOWAIT:如果需要等待,直接返回错误。
当实际操作为创建一个信号量时,相应的semid_ds结构的以下成员将被初始化:
与该集合中每个信号量关联的各个sem结构并不初始化。这些结构是在以SET_VAL或SETALL命令调用semctl时初始化的。
2.
名称:: |
semop |
功能: |
改变信号量的状态 |
头文件: |
#include <sys/types.h> #include <sys/ipc.h> #inlcude <sys/sem.h> |
函数原形: |
int semop(int semid,struct sembuf *sops,size_t nops); int semtimedop (int semid, struct sembuf *sops, unsigned nsops, struct timespec *timeout); |
参数: |
semid:信号集的识别码,可通过semget获取。 |
返回值: |
若成功则为0,若出错则为-1 |
semop函数用来改变信号量对象中各个信号量的状态。
semid是信号量的标识码,也就是semget函数的返回值。
nsops为sops指向数组的大小。
sops是个指向一个结构数组的指针,结构数组中的元素至少应该包含以下几个成员:
sembuf结构如下:
struct sembuf
{
unsigned short sem_num; /*信号量下标*/
short sem_op; /*信号量操作*/
short sem_flg; /*操作标示*/
};
这三个字段的意义分别为:
sem_num:操作信号在信号集中的编号,第一个信号的编号是0。
sem_op:对其何中每个成员的操作有相应的sem_op值规定,此值可以为正负零。
(1),如果其值为正数,这对应于进程释放占用的资源数,sem_op值会加到现有的信号内含值中。
(2),如果sem_op的值为负数,则表示要获取由该信号量控制的资源。
如若该信号量得值大于或等于sem_op的绝对值,则从信号量中减去sem_op的绝对值。这保证信号量的结果值大于或等于0。
如果信号量的值小于sem_op的绝对值,则:
(a),如第三个参数sem_flg指定了IPC_NOWAIT,则semop出错,并返回EAGAIN;
(b),若参数sem_flg未指定了IPC_NOWAIT,则该信号量的semncnt的值加1(因为调用进程将进入休眠状态),然后调用进程被挂起(阻塞)直至下列事件之一发生:
(i),此信号量变成大于或等于sem_op的绝对值时(即某个进程以释放了该信号对应的资源),此信号的semncnt值减1(因为已等待结束),并且从信号量中减去sem_op的绝对值;
(ii),从系统中删除此信号量,在此情况下,函数出错返回EIDRM;
(iii),进程捕捉到一个信号,并从信号处理函数返回。在此情况下,此信号量的semncnt值减1(因为条用进程不再等待该信号量),并且函数出错返回EIDRM;
(3),如果sem_op的值为0,这表示调用进程等待该信号量得值变为0。
如果当前信号量的值为0,则此函数立即返回。
如果当前信号量非0,则:
(a),如第三个参数sem_flg指定了IPC_NOWAIT,则semop出错,并返回EAGAIN;
(b),若参数sem_flg未指定了IPC_NOWAIT,则该信号量的semzcnt的值加1(因为调用进程将进入休眠状态),然后调用进程被挂起(阻塞)直至下列事件之一发生:
(i),此信号量变成0时(即某个进程以释放了该信号对应的资源),此信号的semzcnt值减1(因为已等待结束);
(ii),从系统中删除此信号量,在此情况下,函数出错返回EIDRM;
(iii),进程捕捉到一个信号,并从信号处理函数返回。在此情况下,此信号量的semzcnt值减1(因为条用进程不再等待该信号量),并且函数出错返回EIDRM;
nsops:信号操作结构的数量,恒大于或等于1,该次操作对应了多少个信号。
timeout:当semtimedop()调用致使进程进入睡眠时,睡眠时间不能超过本参数指定的值。如果睡眠超时,semtimedop()将失败返回,并设定错误值为EAGAIN。如果本参数的值为NULL,semtimedop()将永远睡眠等待。
sem_flg:信号操作标志,可能的选择有两种
IPC_NOWAIT :对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息。
IPC_UNDO :程序结束时(不论正常或不正常),保证信号值会被重设为semop()调用前的值。这样做的目的在于避免程序在异常情况下结束时未将锁定的资源解锁,造成该资源永远锁定。
sem_flg可取IPC_NOWAIT以及SEM_UNDO两个标志。如果设置了SEM_UNDO标志,那么在进程结束时,相应的操作将被取消,这是比较重要的一个标志位。如果设置了该标志位,那么在进程没有释放共享资源就退出时,内核将代为释放。如果为一个信号量设置了该标志,内核都要分配一个sem_undo结构来记录它,为的是确保以后资源能够安全释放。事实上,如果进程退出了,那么它所占用就释放了,但信号量值却没有改变,此时,信号量值反映的已经不是资源占有的实际情况,在这种情况下,问题的解决就靠内核来完成。这有点像僵尸进程,进程虽然退出了,资源也都释放了,但内核进程表中仍然有它的记录,此时就需要父进程调用waitpid来解决问题了。
这里需要强调的是semop同时操作多个信号量,在实际应用中,对应多种资源的申请或释放。semop保证操作的原子性,这一点尤为重要。尤其对于多种资源的申请来说,要么一次性获得所有资源,要么放弃申请,要么在不占有任何资源情况下继续等待,这样,一方面避免了资源的浪费;另一方面,避免了进程之间由于申请共享资源造成死锁。
也许从实际含义上更好理解这些操作:信号量的当前值记录相应资源目前可用数目;sem_op>0对应相应进程要释放sem_op数目的共享资源;sem_op=0可以用于对共享资源是否已用完的测试;sem_op<0相当于进程要申请-sem_op个共享资源。再联想操作的原子性,更不难理解该系统调用何时正常返回,何时睡眠等待。
3.
名称:: |
semtl |
功能: |
在指定的信号集或信号集内的某个信号上执行控制操作。 |
头文件: |
#include <sys/types.h> #include <sys/ipc.h> #inlcude <sys/sem.h> |
函数原形: |
int semctl(int semid, int semnum, int cmd, union semun arg); |
参数: |
semid:信号集的识别码,可通过semget获取。 |
返回值: |
若成功则为0,若出错则为-1 |
第二个参数semnum为集合中信号量的编号。如果要标示每个信号量,此值为该信号量的的下标(从0到n-1),如果表示整个信号量集合,则设置为0。
第三个参数cmd为要执行的操作:
对整个信号量操作(第二个参数为0,第四个参数无效):
IPC_STAT (2):设置参数,将与semid关联的内核数据结构拷贝到由arg.buf指针指向的内存区。
IPC_SET (1):获取参数,将由arg.buf指针指向的semid_ds的一些成员写入相关联的内核数据结构,同时更新它的sem_ctime成员。
IPC_RMID (0):立即删除信号集,唤醒所有被阻塞的进程。
IPC_INFO (3):Linux特有命令,返回系统范围内关于信号集的制约和其它参数,并存放在arg.__buf指向的内存区。其结构形态如下:
struct seminfo {
int semmap; /* # of entries in semaphore map; unused */
int semmni; /* Max. # of semaphore sets */
int semmns; /* Max. # of semaphores in all semaphore sets */
int semmnu; /* System-wide max. # of undo structures; unused */
int semmsl; /* Max. # of semaphores in a set */
int semopm; /* Max. # of operations for semop() */
int semume; /* Max. # of undo entries per process; unused */
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_INFO :返回和IPC_INFO相同的信息,不同点有:semusz字段包含有当前系统存在的信号集总量。semaem字段包含有系统内所有信号集的信号总量。
SEM_STAT :返回和IPC_STAT相同的信息。不过参数semid不是一个信号集标识,而是内核内部维持所有信号集信息的数组索引。
对信号量集合中的某个或某些信号量操作,要用到第四个参数:
对于不同的命令,可能需要用到也可能不需要,是一个联合体,原型如下
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 (Linux specific)*/
};
GETPID (11):获取信号量拥有者进程的PID,前一个对此信号进行操作的进程的识别码。第二个参数为0,第四个参数无效,返回进程PID。
GETVAL (12):根据semnun返回信号的值。
GETALL (13):获取所有信号量的值,第二个参数为0,将所有信号的值存入semun.array中。
GETNCNT (14):等待信号值增加的进程的数,第二个参数为0,返回该进程PID。
GETZCNT (15):等待信号值递减的进程的数。第二个参数为0,返回该进程PID。
SETALL (17):将所有semun.array的值设定到信号集中,第二个参数为0。
SETVAL (16:):根据semun设定信号的值。
返回说明: 以上情况下,如果执行成功,有明确的返回值,则返回该值,没的返回0;否则失败返回-1, 即:
成功执行时,根据不同的命令返回不同的非负值
GETNCNT //返回semncnt的值
GETPID //返回sempid的值
GETVAL //返回semval的值
GETZCNT //返回semzcnt的值
IPC_INFO //返回内核内部关于信号集信息的最大可用入口索引
SEM_INFO //如同IPC_INFO.
SEM_STAT //返回信号集标识
剩下的命令返回0。
否则返回-1
semid_ds结构体定义在<sys/sem.h>,原型如下
struct semid_ds
{
struct ipc_perm sem_perm; /* Ownership and permissions
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Last change time */
unsigned short sem_nsems; /* No. of semaphores in set */
};
ipc_perm结构体定义在<sys/ipc.h>,原型如下
struct ipc_perm
{
key_t key; /* Key supplied to semget() */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions */
unsigned short seq; /* Sequence number */
};
三个函数返回说明:
成功执行时,三个系统调用都返回0。失败返回-1,errno被设为以下的某个值
E2BIG:一次对信号的操作数超出系统的限制
EACCES:调用进程没有权能执行请求的操作,并且不具有CAP_IPC_OWNER权能
EAGAIN:信号操作暂时不能满足,需要重试
EFAULT:sops或timeout指针指向的空间不可访问
EFBIG:sem_num指定的值无效
EIDRM:信号集已被移除
EINTR:系统调用阻塞时,被信号中断
EINVAL:参数无效
ENOMEM:内存不足
ERANGE:信号所允许的值越界