system V IPC进程间通信之信号量通信机制
信号量的概念由E. W. Dijkstra于1965年首次提出。信号量实际是一个整数。用来标识系统某种可用资源的个数。通常我们所说的创建一个信号量其实是创建一个信号量集合,在这个集合中可能会有多个信号。Linux利用semid_ds结构(信号量集合数数据结构)来表示System V IPC信号量,见图。
和消息队列类似,系统中所有的信号量组成了一个semary链表,该链表的每个节点指向一个semid_ds结构。从图可以看出,semid_ds结构的sem_base指向一个信号量数组,每个信号量结构用sem结构定义。允许操作这些信号量数组的进程可以利用系统调用执行操作。系统调用可指定多个操作,每个操作由三个参数指定:信号量索引、操作值和操作标志。信号量索引用来定位信号量数组中的信号量;操作值是要和信号量的当前值相加的数值。首先,Linux按如下的规则判断是否所有的操作都可以成功:操作值和信号量的当前值相加大于0,或操作值和当前值均为0,则操作成功。如果系统调用中指定的所有操作中有一个操作不能成功时,则Linux会挂起这一进程。但是,如果操作标志指定这种情况下不能挂起进程的话,系统调用返回并指明信号量上的操作没有成功,而进程可以继续执行。如果进程被挂起,Linux必须保存信号量的操作状态并将当前进程放入等待队列。为此,Linux在堆栈中建立一个sem_queue结构并填充该结构。新的sem_queue结构添加到信号量对象的等待队列中(利用sem_pending和sem_pending_last指针)。当前进程放入sem_queue结构的等待队列中(sleeper)后调用调度程序选择其他的进程运行。如果所有的信号量操作都成功了,当前进程可继续运行。在此之前,Linux负责将操作实际应用于信号量队列的相应元素。这时,Linux检查任何等待的或挂起的进程,看它们的信号量操作是否可以成功。如果这些进程的信号量操作可以成功,Linux就会将它们从挂起队列中移去,并将它们的操作实际应用于信号量队列。同时,Linux会唤醒休眠进程,以便可在下次调度程序运行时可以运行这些进程。当新的信号量操作应用于信号量队列之后,Linux会接着检查挂起队列,直到没有操作可成功,或没有挂起进程为止。
和信号量操作相关的概念还有“死锁”。当某个进程修改了信号量而进入关键段之后,却因为崩溃而没有退出关键段,这时,其他被挂起在信号量上的进程永远得不到运行机会,这就是所谓的死锁。Linux通过维护一个信号量数组的调整链表来避免这一问题。
信号量在创建时需要设置一个初始值,表示同时可以有几个任务可以访问该信号量保护的共享资源,初始值为1就变成互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。
一个任务要想访问共享资源,首先必须得到信号量,获取信号量的操作将把信号量的值减1,若当前信号量的值为负数,表明无法获得信号量,该任务必须挂起在该信号量的等待队列等待该信号量可用;若当前信号量的值为非负数,表示可以获得信号量,因而可以立刻访问被该信号量保护的共享资源。
当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果信号量的值为非正数,表明有任务等待当前信号量,因此它也唤醒所有等待该信号量的任务。
信号量的管理操作
主要有三个系统调用:semget,semctl,semop
1.创建信号量集合
在使用信号量之前,首先需要创建一个信号量集合,该信号量集合中可以包含多个信号量。创建一个信号量集合的函数为 semget,其函数声明如下:
come from /usr/include/sys/sem.h
/* Get semaphore. */
extern int semget (key_t __key, int __nsems, int __semflg) __THROW;
第一个参数:为 key_t 类型的 key 值,一般由 ftok 函数产生。
第二个参数:__nsems 为创建的信号量个数,各信号量以数组的方式存储。这个数组用于初始化数组对象。
第三个参数:__semflg 用来标识信号量集合的权限。如 0770,为文件的访问权限类型。
此外,还可以附加以下参数值。这些值可以与基本权限以或的方式一起使用。
//come from /usr/include/bit/ipc.h
/* resource get request flags */
#define IPC_CREAT 00001000 //如果 key 不存在,创建
#define IPC_EXCL 00002000 //如果 key 存在,返回失败
#define IPC_NOWAIT 00004000 //如果需要等待,直接返回错误
2.控制信号量集合、信号量
在 Linux 操作系统中,使用 semctl 函数对一个信号量集合以及信号量集合中的信号量进行操作。该函数声明如下:
come from /usr/include/sys/sem.h
/* Semaphore control operation. */
extern int semctl (int __semid, int __semnum, int __cmd, ..(union semun arg).) __THROW;
该函数根据cmd最多可有 4 个参数(有可能只有 3 个参数)。
第一个参数:__semid 为要操作的信号量集合标识符,该值一般由 semget函数返回。
第三个参数: cmd参数表示在集合上执行的命令,这些命令及解释如表1所示,这些操作在/usr/include/linux/ipc.h 文件中定义。
表1 cmd命令及解释
命令 | 解 释 |
IPC_STAT | 从信号量集合上检索semid_ds结构,并存到semun联合体参数的成员buf的地址中 |
IPC_SET | 设置一个信号量集合的semid_ds结构中ipc_perm域的值,并从semun的buf中取出值 |
IPC_RMID | 从内核中删除信号量集合 |
GETALL | 从信号量集合中获得所有信号量的值,并把其整数值存到semun联合体成员的一个指针数组arry中 |
GETNCNT | 返回当前等待资源的进程个数 |
GETPID | 返回最后一个执行系统调用semop()进程的PID |
GETVAL | 返回信号量集合内某个信号量的值 |
GETZCNT | 返回当前等待100%资源利用的进程个数 |
SETALL | 与GETALL正好相反,把集合中所有信号量的值,设置为联合体的array成员所包含的对应值 |
SETVAL | 用联合体中val成员的值设置信号量集合中单个信号量的值 |
/* arg for semctl system calls. */
union semun {
int val; //用于SETVAL命令,设置信号量的值
struct semid_ds *buf; //用于IPC_STAT和IPC_SET命令,指向semid_ds结构,
//用于获取或设置信号量控制结构
//cmd为IPC_STAT:获取信号量sem_arry并存入指向的buf
//cmd 为:IPC_SET:将buf指向的结构semid_ds的成员值
//写入相关于该信号集合内核结构*/
unsigned short *array; //用于GETALL和SETALL命令,获取或者设置信号集的值
struct seminfo *__buf; //用于IPC_INFO命令,该命令是linux下特有的,
// 用于返回系统内核定义的信号量集合的定义
void *__pad; //系统内部使用
};
因此,对于 第四个参数:
l 如果操作为 SETVAL,则第四个参数为 val,是相应信号量的值。
l 如果操作为 IPC_STAT & IPC_SET,则第四个参数为 struct semid_ds 结构体变量。
struct semid_ds 结构体定义如下:
/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct semid_ds {
struct ipc_perm sem_perm; /* permissions .. see ipc.h */ //IPC权限
__kernel_time_t sem_otime; /* last semop time */ //最后一次对信号量操作 semop 的时间
__kernel_time_t sem_ctime; /* last change time */ //对此结构最后一次修改的时间
struct sem *sem_base; /* ptr to first semaphore in array *///在信号量数组中指向第一个信号量的指针
struct sem_queue *sem_pending; /* pending operations to be processed */ 待处理的挂起操作
struct sem_queue **sem_pending_last; /* last pending operation */ 最后一个挂起操作
struct sem_undo *undo; /* undo requests on this array */ 在这个数组上的undo请求
unsigned shortsem_nsems; /* no. of semaphores in array */ 在信号数组上的信号量号
};
l 如果操作为 GETALL & SETALL,第四个参数为数组值。
l 如果操作为 IPC_INFO,第四个参数为 struct seminfo 结构体变量。
struct seminfo {
int semmap; //信号量的项目数(定义为mns),内核未使用
int semmni; //系统中允许创建的信号量集的最大数目
int semmns; //系统中允许创建的信号量的最大数目
int semmnu; //系统范围内的undo结构最大个数,内核未使用
int semmsl; //一个信号量集合里信号量个数上限
int semopm; //一次semop可以同时执行的信号量的最大操作个数
int semume; //每个进程内undo结构最大个数,内核未使用
int semusz; //结构sem_undo的尺寸
int semvmx; //信号量值的上限
int semaem; //调整出口最大值(未使用或未实现,无特殊说明)};
除了可以使用 semctl 系统调用访问信号量外,还可以通过 semop 系统调用来操作单个信号量,此函数声明如下:
/* Operate on semaphore. */
extern int semop (int __semid, struct sembuf *__sops, size_t __nsops) __THROW;
第一个参数:为要操作的信号量集合标识符,该值一般由 semget 函数返回。
第二个参数:为 struct sembuf 结构的变量,其定义如下:
//come from /usr/include/linux/sem.h
/* semop system calls takes an array of these. */
struct sembuf {
unsigned short sem_num; /* semaphore index in array */ //信号量下标,从0开始
short sem_op; /* semaphore operation */ //信号量操作
short sem_flg; /* operation flags */ //操作标识:IPC_NOWAIT:非阻塞方式;
SEM_UNDO:内核为信号量操作保留恢复值
};
此结构体有 3 个成员变量。
(1)sem_num为操作的信号量编号。
(2)sem_op 为作用于信号量的操作:该值如果为正整数表示增加信号量的值,如果为负整数表示减小信号量的值,如果为 0 表示对信号量的当前值进行是否为 0 的测试。
(3)sem_flg 为操作标识,可选以下各值:
-
IPC_NOWAIT:在对信号量集合的操作不能执行的情况下,调用立即返回,
对某信号量操作,即使其中一个操作失败,也不会导致修改集中的其他信号量 -
SEM_UNDO:如果未设置 IPC_NOWAIT,则允许在被阻塞的操作失败时撤销操作。
关于 sem_op 的具体情况如下,sem_op 用于指定下列 3 种信号量操作之一:
-
如果 sem_op 是 负整数:
那么就从信号量的值中减去sem_op的绝对值,这意味着进程要获取资源,这些资源是由信号量控制或监控来存取的。如果没有指定IPC_NOWAIT,那么调用进程睡眠到请求的资源数得到满足(其它的进程可能释放一些资源)。
-
如果 sem_op 是 正整数:
把它的值加到信号量,这意味着把资源归还给应用程序的集合
-
如果 sem_op 为 0:
那么调用进程将睡眠到信号量的值也为0,这相当于一个信号量到达了100%的利用
综上所述,Linux按如下的规则判断是否所有的操作都可以成功:操作值和信号量的当前值相加大于 0,或操作值和当前值均为 0,则操作成功。如果系统调用中指定的所有操作中有一个操作不能成功时,则 Linux会挂起这一进程。但是,如果操作标志指定这种情况下不能挂起进程的话,系统调用返回并指明信号量上的操作没有成功,而进程可以继续执行。如果进程被挂起,Linux必须保存信号量的操作状态并将当前进程放入等待队列。为此,Linux内核在堆栈中建立一个 sem_queue结构并填充该结构。新的 sem_queue结构添加到集合的等待队列中(利用 sem_pending和 sem_pending_last指针)。当前进程放入 sem_queue结构的等待队列中(sleeper)后调用调度程序选择其他的进程运行。