信号量及信号量上的操作是E.W.Dijkstra 在1965年提出的一种解决同步、互斥问题的较通用的方法,并在很多操作系统中得以实现, Linux改进并实现了这种机制。
信号量(semaphore )实际是一个整数,它的值由多个进程进行测试(test)和设置(set)。就每个进程所关心的测试和设置操作而言,这两个操作是不可中断的,或称“原子”操作,即一旦开始直到两个操作全部完成。测试和设置操作的结果是:信号量的当前值和设置值相加,其和或者是正或者为负。根据测试和设置操作的结果,一个进程可能必须睡眠,直到有另一个进程改变信号量的值。
信号量可用来实现所谓的“临界区”的互斥使用,临界区指同一时刻只能有一个进程执行其中代码的代码段。为了进一步理解信号量的使用,下面我们举例说明。
假设你有很多相互协作的进程,它们正在读或写一个数据文件中的记录。你可能希望严格协调对这个文件的存取,于是你使用初始值为1的信号量,在这个信号量上实施两个操作,首先测试并且给信号量的值减1,然后测试并给信号量的值加1。当第一个进程存取文件时,它把信号量的值减1,并获得成功,信号量的值现在变为0,这个进程可以继续执行并存取数据文件。但是,如果另外一个进程也希望存取这个文件,那么它也把信号量的值减1,结果是不能存取这个文件,因为信号量的值变为-1。这个进程将被挂起,直到第一个进程完成对数据文件的存取。当第一个进程完成对数据文件的存取,它将增加信号量的值,使它重新变为1,现在,等待的进程被唤醒,它对信号量的减1操作将获得成功。
上述的进程互斥问题,是针对进程之间要共享一个临界资源而言的,信号量的初值为1。实际上,信号量作为资源计数器,它的初值可以是任何正整数,其初值不一定为0或1。另外,如果一个进程要先获得两个或多个的共享资源后才能执行的话,那么,相应地也需要多个信号量,而多个进程要分别获得多个临界资源后方能运行,这就是信号量集合机制,Linux 讨论的就是信号量集合问题。
1. 信号量的数据结构
Linux中信号量是通过内核提供的一系列数据结构实现的,这些数据结构存在于内核空间,对它们的分析是充分理解信号量及利用信号量实现进程间通信的基础,下面先给出信号量的数据结构(存在于include/linux/sem.h中),其它一些数据结构将在相关的系统调用中介绍。
(1)系统中每个信号量的数据结构(sem)
struct sem {
int semval; /* 信号量的当前值 */
int sempid; /*在信号量上最后一次操作的进程识别号 *
};
(2)系统中表示信号量集合(set)的数据结构(semid_ds)
struct semid_ds {
struct ipc_perm sem_perm; /* IPC权限 */
long sem_otime; /* 最后一次对信号量操作(semop)的时间 */
long sem_ctime; /* 对这个结构最后一次修改的时间 */
struct sem *sem_base; /* 在信号量数组中指向第一个信号量的指针 */
struct sem_queue *sem_pending; /* 待处理的挂起操作*/
struct sem_queue **sem_pending_last; /* 最后一个挂起操作 */
struct sem_undo *undo; /* 在这个数组上的undo 请求 */
ushort sem_nsems; /* 在信号量数组上的信号量号 */
};
(3) 系统中每一信号量集合的队列结构(sem_queue)
struct sem_queue {
struct sem_queue * next; /* 队列中下一个节点 */
struct sem_queue ** prev; /* 队列中前一个节点, *(q->prev) == q */
struct wait_queue * sleeper; /* 正在睡眠的进程 */
struct sem_undo * undo; /* undo 结构*/
int pid; /* 请求进程的进程识别号 */
int status; /* 操作的完成状态 */
struct semid_ds * sma; /*有操作的信号量集合数组 */
struct sembuf * sops; /* 挂起操作的数组 */
int nsops; /* 操作的个数 */
};
(4)几个主要数据结构之间的关系
从7.3图可以看出,semid_ds结构的sem_base指向一个信号量数组,允许操作这些信号量集合的进程可以利用系统调用执行操作 。注意,信号量信号量集合的区别,从上面可以看出,信号量用“sem” 结构描述,而信号量集合用“semid_ds"结构描述,实际上,在后面的讨论中,我们以信号量集合为讨论的主要对象。下面我们给出这几个结构之间的关系,如图7.3所示。
图7.3 System V IPC信号量数据结构之间的关系
Linux对信号量的这种实现机制,是为了与消息和共享内存的实现机制保持一致,但信号量是这三者中最难理解的,因此我们将结合系统调用做进一步的介绍,通过对系统调用的深入分析,我们可以较清楚地了解内核对信号量的实现机制。
2. 系统调用:semget()
为了创建一个新的信号量集合,或者存取一个已存在的集合,要使用segget()系统调用,其描述如下:
原型: int semget ( key_t key, int nsems, int semflg );
返回值: 如果成功,则返回信号量集合的IPC识别号
如果为-1,则出现错误:
semget()中的第一个参数是键值, 这个键值要与已有的键值进行比较,已有的键值指在内核中已存在的其它信号量集合的键值。对信号量集合的打开或存取操作依赖于semflg参数的取值:
IPC_CREAT :如果内核中没有新创建的信号量集合,则创建它。
IPC_EXCL :当与IPC_CREAT一起使用时,但信号量集合已经存在,则创建失败。
如果IPC_CREAT单独使用,semget()为一个新创建的集合返回标识号,或者返回具有相同键值的已存在集合的标识号。如果IPC_EXCL与IPC_CREAT一起使用,要么创建一个新的集合,要么对已存在的集合返回-1。IPC_EXCL单独是没有用的,当与IPC_CREAT结合起来使用时,可以保证新创建集合的打开和存取。
作为System V IPC的其它形式,一种可选项是把一个八进制与掩码或,形成信号量集合的存取权限。
第二个参数nsems指的是在新创建的集合中信号量的个数。其最大值在“linux/sem.h”中定义:
#define SEMMSL 250 /* <= 8 000 max num of semaphores per id */
注意:如果你是显式地打开一个现有的集合,则nsems参数可以忽略。
下面举例说明。
int open_semaphore_set( key_t keyval, int numsems )
{
int sid;
if ( ! numsems )
return(-1);
if((sid = semget( keyval, numsems, IPC_CREAT | 0660 )) == -1)
{
return(-1);
}
return(sid);
}
注意,这个例子显式地用了0660权限。这个函数要么返回一个集合的标识号,要么返回-1而出错。键值必须传递给它,信号量的个数也传递给它,这是因为如果创建成功则要分配空间。
3. 系统调用: semop()
原型: int semop ( int semid, struct sembuf *sops, unsigned nsops);
返回: 如果所有的操作都执行,则成功返回0。
如果为-1,则出错。