第五章 Linux进程之间的通信

 

进程之间、进程与核心之间互相通信,以协调它们的活动。Linux支持一系列进程间通信机制,信号和管道是其中的两种,此外还有SVR的进程间通信机制。
 
5.1 信号
信号是unix系统最早使用的进程间通信方法之一。它们用来对一个或多个进程发送异步事件。信号可以由键盘中断产生,也可以由进程试图读取虚拟存储器中不存在的位置而引
发。另外,信号也可以用于外壳程序向它们的子进程发送作业控制命令。有一组预先定义的信号,核心可以产生,具有相应优先权的进程也可以产生。使用kill -l命令可以列出系统的信号集合。
5.2 管道
普通的Linux外壳都允许重定向。例如$ ls | pr | lpr
ls命令的输出文件名通过管道作为pr命令的标准输入,后者对之进行分页($$paginate?$$),最后pr的标准输出又通过管道送入lpr的标准输入,lpr把结果打在
缺省打印机上。所以,管道就是连接一个进程的标准输出到另外一个进程的标准输入的单
向字节流。进程无法知道这个重定向,仍然正常工作。负责在进程之间建立临时管道的是
外壳。
Linux中,管道是通过两个指向同一个虚拟文件系统i节点的file结构来实现的,i节点本身则指向内存中的一个物理页。每个file数据结构含有指向不同的文件操作例程向量的指针,一个用于写管道,另一个用于读管道。这就隐藏了与读写普通文件的一般的系统调用之间的区别。当写进程在写管道时,字节被拷到共享数据页上,而当读进程在当读管道时,字节被从共享数据页上拷出来。Linux 必须对共享数据页的存取进行同步,它使用锁、等待队列和信号来保证读写进程之间的轮流。当写进程想写管道时,它使用标准写库函数。这些库函数都传递文件描述符,而文件描述符是进程的file数据结构集合的索引,每一个代表一个打开的文件或者一个打开的管道。Linux系统调用使用描述这个管道的file数据结构指向的例程。那个写例程使用表示管道的i节点中保存的信息来管理写要求。如果有足够的空间供所有的字节写入管道,只要管道没有为读进程锁住,Linux将会为写进程锁住管道,并且把所有的待写字节从进程地址空间拷到共享数据页中。如果管道为读进程锁住,或者没有足够的数据空间,那么将使当前进程睡眠在管道i节点的等待队列,调用调度程序运行另外一个进程。进程的状态是可中断的,所以它能收到信号,能在写数据空间变得足够或是管道被解锁之后被写进程唤醒。写完数据之后,管道的i节点被解锁,睡眠在i节点等待队列的读进程将被唤醒。
从管道读数据与写数据非常类似。
允许进程做非阻塞读(依赖于打开文件或管道的模式),在此情况下,如果没有数据可读或者管道被锁住,将返回一个错误。这意味着进程可以继续运行。另一种方法是等在管道i节点的等待队列里直到写进程完成工作。当两个进程都完成了管道上的工作,管道i节点将被与共享数据页一起丢弃。
Linux也支持“有名”管道,也被称为FIFO,因为管道的工作方式是先入先出的。最早写入这种管道的数据也最早被读出。与一般管道不同的是,FIFO不是临时对象,而是文件系统中的实体,可以用mkfifo命令创建出来。只要进程有足够的存取权限,就能自由地使用FIFO。打开FIFO的方式和打开管道的方式也稍有不同。一个管道(包括它的两个file数据结构,它的虚拟文件系统i节点和共享数据页)是一次性产生的,而FIFO是已经存在的,由用户负责它的打开和关闭。如果在写进程打开FIFO之前,读进程先打开了它,或者读进程去读一个没有被写入数据的管道,Linux必须加以处理。除此之外,FIFO与管道完全相同,因为它们采用的数据结构和操作是一致的。
 
5.3  SVR的进程间通信机制
Linux支持三类SVR首创的进程间通信机制:消息队列、信号灯和共享内存。这些SVR进程间通信机制都共用相同的认证方法。进程只能通过系统调用向核心传送一个唯一的引用标识,才能存取这些资源。这些SVR进程间通信对象的存取通过存取权限来控制,与文件存
取的权限控制非常类似。由对象的创造者通过系统调用来设置对象的存取权限。在每一种机制之中,对象的引用标识被用作资源表的索引。当然,索引本身并不简单,还需要一些操作来产生。
所有表示SVR进程间通信对象的Linux数据结构都包含一个ipc_perm结构,该结构包含了所有者和创造者进程的用户和组标识,对象的存取模式(所有者、组和其它)以及对象的key。这个key只是用于定位对象的引用标识的一种方法。支持两类key:公开key和秘密key。如果key是公开的,那么系统中的任何进程,只要有足够的存取权限,都能找到对象的引用标识。SVR进程间通信对象绝对不能用key来引用,而只能用引用标识来引用。
 
5.4 信号灯
最简单类型的信号灯是内存中一个可以被一个或者多个进程测试并设置的位置。就进程而言,测试并设置操作是不可中断的,或者说是原子的。测试并设置操作的结果是信号灯当前值加上了所设置的数值,这个数值可以随便是正的或负的。根据测试并设置操作的结果,进程可能会被迫睡眠,直到另一个进程改变信号灯的值为止。信号灯可以用于实现关键区--一次只能有一个进程进入运行的关键代码区域。
每一个SVR信号灯对象描述一个信号灯序列,Linux使用semid_ds数据结构来代表之。系统中所有的semid_ds数据结构都被semary向量中的一组指针所指引。每一个信号灯序列中含有sem_nsems信号灯,每一个信号灯用sem_base指向的一个sem 数据结构描述。所有有权操作信号灯序列的进程可以通过系统调用来对它们进行操作。系统调用可以指定很多操作,每个操作用三个输入来描述:信号灯索引,操作值和一组标志。信号灯索引是信号灯序列中的索引,操作值是将被加到信号灯当前值上的数值。首先Linux测试是否所有的操作都能成功。操作能够成功当且仅当操作值加到当前值上之后结果大于0,或者操作值与当前值都是0。如果其中的任何信号灯操作失败,Linux将挂起进程,除非操作标志要求系统调用是非阻塞的。如果需要挂起进程,Linux将保存信号灯操作的状态,并把当前进程送入等待队列。实现的方法是创立并填写一个sem_queue数据结构,放在信号灯对象的等待队列之中(使用sem_pendingsem_pending_last指针),并调用调度程序运行另外一个进程。
 
 
5.5 共享内存
共享内存允许一个或者多个进程通过同时出现在它们的虚地址空间内的内存进行通信。虚存的页面由各个进程的页表的入口指引,并不需要共享内存在每一个进程的虚拟内存的地址都相同。与所有的SVR进程间通信对象一样,共享内存的存取由key和存取权限检查来控制。一旦内存被共享,无法对进程如何使用它进行检查。必须依赖其它机制,如信号灯,来对内存的存取进行同步。
每一个新创建的共享内存区域由一个shmid_ds数据结构代表。这些数据接被保存shm_segs向量之中。shmid_ds数据结构描述了共享内存区域的大小,使用共享内存区域的进程的数量,和关于共享内存如何映射到进程的地址空间的信息。正是共享内存的创建者控制着其存取权限和其key是否公开。如果创建者具有足够的存取权限,它可以把共享内存锁定在物理内存之中。