Linux进程间通信之信号量
信号量是一种用于提供不同进程间或一个给定进程的不同线程间同步手段的原语。在UNIX下有三种分别如下:
- Posix有名信号量;
- Posix基于内存的信号量;
- System V信号量。
在这里只和大家分享下有关System V信号量。
System V通过定义计数信号量集来对信号量的操作,计数信号量集是一个或多个信号量构成一个集合,其中每个都是计数信号量。对于系统中的每个信号量集,内核维护一个如下的信息结构,它定义在<sys/sem.h>头文件中。
struct semid_ds{
struct ipc_perm sem_perm; /* operation permission struct */
struct sem *sem_base; /* ptr to array of semaphores in set */
uishort sem_nsems; /* #of semaphores in set */
time_t sem_otime; /* time of last semop() */
time_t sem_ctime; /* time of creation or last IPC_SET */
};
成员struct sem结构如下:
struct sem{
ushort_t semval; /* semaphore value , nonnegative */
short sempid; /* PID of last successful semop(), SETVAL, SETALL */
ushort_t semncnt; /* awaiting semval > current value */
ushort_t semzcnt; /* awaiting semval = 0 */
};
注意在struct semid_ds结构中的sem_base含有指向某个sem结构数组的指针:当前信号量集中的每个信号对应其中一个数组元素。我们可以把内核中的某个选定信号量图解成指向一个sem结构数组的一个semid_ds结构。图解如下:
有了以上的理论,那么接下来我们来探讨下如何对这样的信号量进行操作,linux操作系统为我们提供了操作system v信号量的API函数,以下就开始讲解这些API函数。
- semget函数创建一个信号量集或访问一个已存在的信号量集。
#include <sys/sem.h>
int semget(key_t key, int nsems, int oflag);
该函数成功返回时,其返回值是一个称为信号量标识符的整数,semop和semctl使用它。出错则返回-1。
nsems参数指定集合中的信号量数。如果我们创建一个新的信号量集,而只是访问一个已存在的集合,那就可以把该参数指定为0。一旦创建完一个信号量集,我们就不能改变其中的信号量数。
oflag值是一些权限值的组合,如果是创建一个信号量集,那么得在此oflag的基础上或上O_CREAT或者是O_CREAT|O_EXCL。
此函数不可以对创建的信号量集中的信号量进行初始化,对信号量的初始化是通过另外的一个函数semctl进行的。
- 通过semctl函数对信号量集中的信号量进行初始化。
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, …./* union semun arg*/ );
semid标识其操作待控制的信号量集。
semnum标识该信号量集内的某个成员(0, 1等待,直到nsems -1)。semnum值仅仅用于GETVAL、SETVAL、GETNCNT、GETZCNT和GETPID命令。
第四个参数是可选的,取决于第三个参数cmd。union semnu结构如下:
union semnu{
int val; /* used for SETVAL only */
struct semid_ds *buf; /* used for IPC_SET and IPC_STAT */
ushort *array; /* used for GETALL and SETALL */
};
这个联合体并没有出现在任何头文件中,因而必须由应用程序声明。
第三个参数cmd可以取以下值:
GETVAL 把semval的当前值作为函数返回值返回。
SETVAL 把semval值设置为arg.val。如果操作成功,那么相应信号量在所有进程是的信号
量调整值(semadj)将被置为0。
IPC_RMID 把由semid指定的信号量集从系统中删除掉。
IPC_SET 设置所指定信号量集的semid_ds结构中的某些成员。
IPC_SET R 返回所指定信号量集当前的semid_ds结构。
- 使用semget得到一个信号量集,使用semctl设置信号量集中的信号量,那么对信号量集中的信号通过semop来进行操作。
#include <sys/sem.h>
int semop(int semid, struct sembuf *opsptr, size_t nops);
semid标识其操作的信号量集。
其中opsptr指向一个如下结构的数组:
Struct sembuf{
Short sem_num; /* semaphore number: 0, 1, …., nsems-1 */
Short sem_op; /* semaphore operation: <0, 0, >0 */
Short sem_flag; /* semaphore flags : 0, IPC_NOWAITE, SEM_UNDO */
};
nops参数指出由opsptr指向的sembuf结构数组中元素的数目。该数组中的生个元素给目标信号量集内某个特定的信号量指定一个操作。这个特定的信号量由sem)num指定,0代表第一个元素,1代表第二个元素,依次类推,直到nsems-1,其中nsems是目标信号量集内成员信号量的数目。
semop对信号的操作是由sem_op的值确定的,以下是对sem_op取值的分析:
- 、sem_op 为正数时,会把sem_op的值加到操作的信号量的信号值上。如果sem_flg被设置为IPC_UNDO,无论程序正常结束与否,都会把信号值重新设置为调用semop函数前得值。这对应于进程释放占用的资源数。
- 、sem_op为负数时,如果要操作的信号量的值大于或者等于sem_op的绝对值,则从信号量值中加上sen_op的值。如果信号量值小于sem_op的绝对值,则有如下:
- 如果sem_flg的值为IPC_NOWAIT,那么semop出错,返回EAGAIN。
- 如果sem_flg没有设置为IPC_NOWAIT,则该信号量的semncnt的值加1,然后此进程挂起,直到此信号量的值大于sem_op的绝对值,才执行semop操作;或者此信号量从系统中删除,此时semop返回EIDRM;或者该挂起进程捕捉到信号,从信号处理程序返回,此时,semop出错返回EINTR。
- 如果sen_op的值为0,则调用进程希望等到该信号量值变成0。
- 、sem_op为0时,那么调用者希望等待到semval变为0。如果sem_op已经是0,那就立即返回。
以下是利用信号量来进行一个PV操作,实现代码如下:
//初始化信号量
int init_sem(int semid, int semval)
{
SYS_NUM semnu;
semnu.sem_val = semval;
if((semctl(semid, 0, SETVAL, semnu)) < 0)
{ perror("init_sem semctl");
return -1;
}
return 0;
}
//对信号量进行p操作
int sem_p(int semid)
{
SYS_SEM sembu;
sembu.sem_num = 0;
sembu.sem_op = -1;
sembu.sem_flg = SEM_UNDO;
if((semop(semid, &sembu, 1)) < 0)
{
perror("sem_p semop");
return -1;
}
return 0;
}
//对信号量进行v操作
int sem_v(int semid)
{
SYS_SEM sembu;
sembu.sem_num = 0;
sembu.sem_op = 1;
sembu.sem_flg = SEM_UNDO;
if((semop(semid, &sembu, 1)) < 0)
{ perror("sem_v semop");
return -1;
}
return 0;
}
//删除信号量集
int del_sem(int semid)
{ if((semctl(semid, 0, IPC_RMID)) < 0)
{ perror("del_sem semctl");
return -1;
}
return 0;
}
总结:
信号量往往是用来同步的,保护共享内存。