现在的位置: 首页 > 综合 > 正文

Linux 进程间通信 – 信号灯(Semaphores)

2019年01月03日 ⁄ 综合 ⁄ 共 5874字 ⁄ 字号 评论关闭

一般意义下,信号灯是一个具有整数值的对象,它支持两种操作P()和V()。P()操作减少信号灯的值,如果新的信号灯的值小于0,则操作阻塞;V()操作增加信号灯的值,如果结果值大于或等于0,则唤醒一个等待的进程。通常用信号灯来做进程的同步和互斥。

最简单形式的信号灯就是内存中一个存储位置,它的取值可以由多个进程检验和设置。至少对于相关的进程来讲,对信号灯的检验和设置操作是不可中断的或者说是原子的:只要启动就不能终止。目前许多处理器提供检验和设置操作指令,如Intel处理器的sete等指令。检验和设置操作的结果是信号灯当前值与设置值的和,可以是正或者负。根据检验和设置操作的结果,一个进程可能必须睡眠直到信号灯的值被另一个进程改变。信号灯可以用于实现临界区(critical regions),就是重要的代码区,同一时刻只能有一个进程运行的代码区域。

比如,有许多协作的进程要从同一个数据文件中读写记录,并且希望对文件的访问必须严格地协调。那么,可以使用一个信号灯,将其初值设为1,用两个信号灯操作(P、V 操作),将进程中对文件操作的代码括起来。第一个信号灯操作检查并把信号灯的值减小,第二个操作检查并增加它。访问文件的第一个进程试图减小信号灯的值,如果它成功(事实上,它肯定成功),信号灯的取值将变为0,这个进程现在可以继续运行并使用数据文件。但是,如果此时另一个进程需要使用这个文件,它也试图减少信号灯的数值,它会失败,因为信号灯的值将要变成-1(但是,信号灯的值仍然保持为0,没有变成-1),这个进程会被挂起直到第一个进程处理完数据文件。当第一个进程处理完数据文件后,它会增加信号灯的值使其重新变为1。现在等待的进程会被唤醒,这次它减小信号灯的尝试会成功。


与消息队列相似,Linux为系统中所有的信号灯维护一个向量表semary ,其定义如下:
struct semid_ds *semary[SEMMNI];

该向量表中的每个元素都是一个指向semid_ds数据结构的指针,而一个semid_ds数据结构则描述了一个信号灯数组。SEMMNI的值是128,它限制了系统中同时存在的信号灯的数组的数量。数据结构semid_ds的定义如下:
struct semid_ds {
struct ipc_perm sem_perm; /* permissions .. see ipc.h */
__kernel_time_t sem_otime; /* last semop time */
__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 */
structsem_undo*undo; /* undo requests on this array */
unsigned short sem_nsems; /* no. of semaphores in array */
};

其中:sem_perm是一个认证;
sem_otime是最后一次在该信号灯上执行操作的时间;
sem_ctime是信号灯最后一次改变的时间;
sem_base是一个信号灯数组,该数组中的每个元素都是一个信号灯;
sem_pending和sem_pending_last描述了一个等待队列;
undo调整(undo)序列;
sem_nsems该信号灯数组中的信号灯数。

由此可见,每一个semid_ds数据结构都表示一个信号灯对象,而System V IPC的每一个信号灯对象都描述了一个信号灯数组。每一个信号灯数组(semid_ds)中都有一个域sem_nsems,表示该信号灯数组中信号灯的数量。信号灯由sem数据结构描述。sem_nsems个sem数据结构组成一个信号灯数组,sem_base域指向该数组。
struct sem {
int semval; /* current value */
int sempid; /* pid of last operation */
};

Linux在信号灯上提供三种操作。
1. 象消息队列一样,进程在使用信号灯以前必须创建信号灯或获得对已存在信号灯的引用标识符,该工作通过系统调用sys_ipc实现,其中的call值为SEMGET。具体实现该操作的函数是sys_semget,其定义如下:
int sys_semget (key_t key, int nsems, int semflg)

该函数根据所给的键值(key)创建或获得一个信号灯引用标识符,它所做的工作如下:
1) 如果key == IPC_PRIVATE,则:
* 在数组semary中找一个空位置。
* 申请一块内存,其大小为sizeof (struct semid_ds) + nsems * sizeof (struct sem),即一个semid_ds数据结构的大小加上nsems个信号灯的大小。该块内存区的前部用做semid_ds数据结构,剩余部分用做sem数组。填写semid_ds数据结构数据结构的各个域。将填写好的semid_ds数据结构加入到数组semary的空位置。唤醒在sem_lock上等待的进程。
* 返回该信号灯的引用标识符。

2) 在数组semary上查找键值为key的信号灯对象(semid_ds数据结构)。有下面结果:
A. 没找到。如果在参数semflg中指明要创建新的信号灯,则如1)所述,创建一个新的信号灯。否则,错误返回。
B. 找到。如果在参数semflg中指明要创建新的信号灯,而且该种键值的信号灯不能存在,则错误返回。否则,认证检查,如有错,则错误返回。否则,返回找到的信号灯的引用标识符。

2. 所有允许在一个System V IPC信号灯对象的信号灯数组上操作的进程,都可以通过系统调用sys_ipc对它们操作,其对应的call值为SEMOP,对应的函数为sys_semop。对信号灯的操作,Linux只定义了一个函数,其定义如下:
int sys_semop (int semid, struct sembuf *tsops, unsigned nsops)

其中semid是信号灯引用标识符,tsops是在该信号灯上要求执行的一组操作,nsops是此次要执行的操作的个数。每一个操作都用如下的一个数据结构表示:
struct sembuf {
unsigned short sem_num; /* semaphore index in array */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
};

其中sem_num是信号灯数组的一个索引,指明要做该操作的一个具体的信号灯。
sem_op是一个操作值,是要加到当前信号灯上的数值。
sem_flg是一个标志,表示对该次操作的特别要求。

一个sembuf数据结构表示对一个信号灯的一次操作,sembuf数据结构数组tsops表示对一个信号灯对象上的信号灯数组要做的一组操作。

函数sys_semop就是要在信号灯对象(由semid表示)上完成由tsops指定的一组操作。它所做的工作如下:
1) 检查参数nsops和semid的合法性。
2) 将tsops数组拷贝到内核。
3) 算出引用标识符semid在数组semary中对应的索引:
id = (unsigned int) semid % SEMMNI
4) 检查统计操作数组tsops中指定的各个操作(合法、加、减)。
5) 认证检查。
6) Linux维护对信号灯数组的调整(undo)序列(每个信号灯对象一个)来避免可能出现的死锁现象。基本思路是,如果实施了这些调整,信号灯就会返回到进程对其实施操作以前的状态。这些调整被放在sem_undo数据结构中,排在 semid_ds 数据结构的队列中(由 undo
域指示),同时也排在使用这些信号灯的进程的task_struct数据结构的队列中。

sem_undo数据结构的定义如下:
structsem_undo{
structsem_undo*proc_next; /* next entry on this process */
structsem_undo*id_next; /* next entry on this semaphore set */
int semid; /* semaphore set identifier */
short * semadj; /* array of adjustments, one per semaphore */
};

其中semid是该结构所对应的信号灯对象。semadj是一个数组,信号灯对象的每个信号灯对应该数组的一个元素,其中记录对信号灯的累计操作的结果(在信号灯上增加或减少数值的总和取反)。

如果一个信号灯操作(sembuf数据结构)的标志sem_flg中指明SEM_UNDO标志,则在做该信号灯操作的同时还要维护一个调整动作。Linux至少为每一个进程的每一个信号灯对象都维护一个sem_undo数据结构。如果请求的进程没有该结构,就在需要的时候为它创建一个。这个新的sem_undo数据结构同时在进程的task_struct数据结构和信号灯队列的semid_ds数据结构的相应队列中排队。对信号灯队列中的信号灯执行
undo 操作的时候,和原操作值相反的值(负值)被加到该信号灯上,这个操作值记录在进程的
sem_undo数据结构的调整队列中该信号灯对应的条目上。所以,如果操作值为2,那么这个信号灯的调整条目上记录的是-2。

当进程被删除,比如退出的时候,Linux遍历它的sem_undo数据结构数组,并实施对于信号灯数组的调整。如果信号灯被删除,它的sem_undo数据结构仍旧保留在进程的task_struct队列中,但是相应的信号灯数组标识符被标记为无效。在这种情况下,信号灯清除代码只是简单地废弃这个sem_undo数据结构。

函数sys_semop在此处检查该次操作有没有undo操作,如果有undo操作,则看当前进程有没有就该信号灯对象定义sem_undo数据结构,如没有定义,则申请一块内存,为其定义一个sem_undo数据结构,并将其加入到进程和信号灯对象的相应队列中。

7) 在信号灯对象上做tsops中指定的所有操作(增加指定信号灯的数值,对标志sem_flg中指明SEM_UNDO的操作记录其undo数据等)。只有操作数加上信号灯的当前值大于0或者操作值和信号灯的当前值都是0时,操作才算成功。

8) 如果任意一个信号灯操作失败,只要操作标记不要求系统调用无阻塞返回,Linux就会挂起这个进程。如果进程要挂起,Linux必须保存要进行的信号灯操作的状态并把当前进程放到等待队列中。Linux在堆栈中为每一个有进程等待的信号灯建立一个sem_queue数据结构,并填满它来实现上述过程(保存信号灯操作状态、移动进程队列等)。
struct sem_queue {
struct sem_queue * next; /* next entry in the queue */
struct sem_queue ** prev; /* previous entry in the queue, *(q->prev) == q */
struct wait_queue * sleeper; /* sleeping process */
structsem_undo* undo; /* undo structure */
int pid; /* process id of requesting process */
int status; /* completion status of operation */
struct semid_ds *sma; /* semaphore array for operations */
struct sembuf *sops; /* array of pending operations */
int nsops; /* number of operations */
int alter; /* operation will alter semaphore */
};

这个新创建的sem_queue数据结构被放到了该信号灯对象的等待队列的结尾(由semid_ds数据结构中的sem_pending和sem_pending_last指针指出)。当前进程被放到了这个sem_queue数据结构的等待队列中(由它的sleeper指针指出),然后调用调度程序,运行另外一个进程。当该进程被唤醒时,再次执行指定的那组操作,如果此次成功,则将进程从等待队列中摘下,正常返回。

9) 如果所有的信号灯操作都成功,当前的进程就不需要被挂起。Linux继续向前并把这些操作施加到信号灯数组的合适的成员上。现在Linux必须检查任何睡眠或者挂起的进程,它们的操作现在可能可以实施。Linux顺序查找在该信号灯上的操作等待队列(由sem_pending指出)中的每一个成员,检查它的信号灯操作现在是否可以成功。如果可以,它就把这个sem_queue数据结构从操作等待表中删除,并把这种信号灯操作施加到信号灯数组的合适的成员上。它唤醒睡眠的进程,让它在下次调度程序运行的时候可以继续运行。Linux从头到尾检查操作等待队列,直到无法执行信号灯操作,从而无法唤醒更多的进程为止。
参见include/linux/sem.h,linux/ipc/sem.c

3. Linux在信号灯上实现的第三种操作是对信号灯的控制(call值为SEMCTL的sys_ipc调用),它由函数sys_semctl实现。控制操作包括获得信号灯的状态、获得信号灯的值,设置信号灯的值,释放信号灯对象资源等。

信号灯机制为互相操作的进程提供了一种复杂的同步方法。但信号灯也存在一些问题,如死锁(deadlock)。这发生在一个进程改变了信号灯的值,从而进入一个临界区(critical region),但是因为崩溃或者被kill而没有离开这个临界区域的情况下。Linux使用undo操作来避免死锁,如上所述。另一个问题是所有System V IPC机制同时存在的问题,即必须显式地释放IPC资源,如果某进程忘记释放某IPC资源,则会造成内存垃圾。

from : https://hi.baidu.com/lovechengnuo/item/1dfdb39426e0b014934f41e0

抱歉!评论已关闭.