Linux 核心--6.进程间通讯机制( 四 )




5.3.3信号灯
信号灯最简单的形式是某个可以被多个进程检验和设置(test&set)的内存单元 。这个检验与设置操作对每个进程而言是不可中断或者说是一个原子性操作;一旦启动谁也终止不了 。检验与设置操作的结果是信号灯当前值加1 ,  这个值可以是正数也可以是负数 。根据这个操作的结果 , 进程可能可以一直睡眠到此信号灯的值被另一个进程更改为止 。信号灯可用来实现临界区(critical region):某一时刻在此区域内的代码只能被一个进程执行 。

如果你有多个协作进程从一个数据文件中读取与写入记录 。有时你可能需要这些文件访问遵循严格的访问次序 。那么可在文件操作代码上使用一个初始值为1的信号灯 , 它带有两个信号灯操作 , 一个检验并对信号灯 值减1 , 而另一个检验并加1 。第一个访问文件的进程将试图将信号灯值减1 , 如果获得成功则信号灯值变成了 0 。此进程于是开始使用这个数据文件 , 但是此时如果另一进程也想将信号灯值减1 , 则信号灯值将为-1 , 这次操作将会失败 。它将挂起执行直到第一个进程完成对此数据文件的使用 。此时这个等待进程将被唤醒 , 这次它对信号灯的操作将成功 。




图5.3 系统V IPC信号灯


每个系统V IPC信号灯对象对应一个信号灯数组 , Linux使用semid_ds结构来表示 。系统中所有semid_ds结构由一组semary指针来指示 。在每个信号灯数组中有一个sem_nsems , 它表示一个由sem_base指向的sem结构 。授权的进程可以使用系统调用来操纵这些包含系统V IPC信号灯对象的信号灯数组 。这个系统调用可以定义许多种操作 , 每个操作用三个输入来描叙:信号灯索引、操作值和一组标志 。信号灯索引是一个信号灯数组的索引 , 而操作值是将被加到信号灯上的数值 。首先Linux将检查是否所有操作已经成功 。如果操作值与信号灯当前数值相加大于0 , 或者操作值与信号灯当前值都是0 , 操作将会成功 。如果所有信号灯操作失败 , Linux仅仅会把那些操作标志没有要求系统调用为非阻塞类型的进程挂起 。进程挂起后 , Linux必须保存信号灯操作的执行状态并将当前进程放入等待队列 。系统还在堆栈上建立sem_queue结构并填充各个域 。这个sem_queue结构将被放到此信号灯对象等待队列的尾部(使用 sem_pending和sem_pending_last指针) 。系统把当前进程置入sem_queue结构中的等待队列(sleeper)中 , 然后启动调度管理器选择其它进程运行 。

如果所有这些信号灯操作都成功则无需挂起当前进程 , Linux将对信号灯数组中的其他成员进行相同操作 , 然后检查那些处于等待或者挂起状态的进程 。首先 , Linux将依次检查挂起队列(sem_pending) 中的每个成员 , 看信号灯操作能否继续 。如果可以则将其sem_queue结构从挂起链表中删除并对信号灯数组发出信号灯操作 。Linux还将唤醒处于睡眠状态的进程并使之成为下一个运行的进程 。如果在对挂起队列的遍历过程中有的信号灯操作不能完成则Linux将一直重复此过程 , 直到所有信号灯操作完成且没有进程需要继续睡眠 。

但是信号灯的使用可能产生一个严重的问题:死锁 。当一个进程进入临界区时它改变了信号灯的值而离开临界区时由于运行失败或者被kill而没有改回信号灯时 , 死锁将会发生 。Linux通过维护一组描叙信号灯数组变化的链表来防止该现象的发生 。它的具体做法是让Linux将把此信号灯设置为进程对其进行操作前的状态 。这些状态值被保存在使用该信号灯数组进程的semid_ds和task_struct结构的sem_undo结构中 。

信号灯操作将迫使系统对它引起的状态变化进行维护 。Linux为每个进程维护至少一个对应于信号灯数组的sem_undo结构 。如果请求进行信号灯操作的进程没有该结构 , 则必要时Linux会为其创建一个 。这个sem_undo 结构将同时放入此进程的task_struct结构和此信号灯数组的semid_ds结构中 。当对信号灯进行操作时 , 信号灯变化值的负数被置入进程的sem_undo结构中该信号的入口中 。所以当操作值为2时 , 则此信号灯的调整入口中将加入一个-2 。

推荐阅读