当前位置:Linux教程 - Linux - Interprocess Communication Mechanisms(进程间通讯机制)

Interprocess Communication Mechanisms(进程间通讯机制)



        

    进程之间互相通讯并和核心通讯,协调它们的行为。Linux支持一些进程间通讯(IPC)的机制。信号和管道是其中的两种,Linux还支持系统V IPC(用首次出现的Unix的版本命名)的机制。

     

    5.1 Signals(信号)

     

    信号是Unix系统中使用的最古老的进程间通讯的方法之一。用于向一个或多个进程发送异步事件的信号。信号可以用键盘终端产生,或者通过一个错误条件产生,比如进程试图访问它的虚拟内存中不存在的位置。Shell也使用信号向它的子进程发送作业控制信号。

     

    有一些信号有核心产生,另一些可以由系统中其他有权限的进程产生。你可以使用kill命令(kill –l)列出你的系统的信号集,在我的Linux Intel系统输出:

     

    1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL

    5) SIGTRAP 6) SIGIOT 7) SIGBUS 8) SIGFPE

    9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2

    13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD

    18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN

    22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ

    26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO

    30) SIGPWR

     

    在Alpha AXP Linux系统上编号不同。进程可以选择忽略产生的大多数信号,有两个例外:SIGSTOP(让进程停止执行)和SIGKILL(让进程退出)不可以忽略,虽然进程可以选择它如何处理信号。进程可以阻塞信号,如果它不阻塞信号,它可以选择自己处理或者让核心处理。如果核心处理,将会执行该信号的缺省行为。例如,进程接收到SIGFPE(浮点意外)的缺省动作是产生core并退出。信号没有固有的优先级,如果一个进程同时产生了两个信号,它们会以任意顺序出现在进程中并按任意顺序处理。另外,也没有机制可以处理统一种类的多个信号。进程无法知道它接收了1还是42个SIGCONT信号。

     

    Linux用进程的task_struct中存放的信息来实现信号机制。支持的信号受限于处理器的字长。32位字长的处理器可以有32中信号,而64位的处理器,比如Alpha AXP可以有多达64种信号。当前待处理的信号放在signal域,blocked域放着要阻塞的信号掩码。除了SIGSTOP和SIGKILL,所有的信号都可以被阻塞。如果产生了一个被阻塞的信号,它一直保留待处理,直到被解除阻塞。Linux也保存每一个进程如何处理每一种可能的信号的信息,这些信息放在一个sigaction的数据结构数组中,每一个进程的task_struct都有指针指向对应的数组。这个数组中包括处理这个信号的例程的地址,或者包括一个标志,告诉Linux该进程是希望忽略这个信号还是让核心处理。进程通过执行系统调用改变缺省的信号处理,这些调用改变适当的信号的sigaction和阻塞的掩码。

     

    并非系统中所有的进程都可以向其他每一个进程发送信号,只有核心和超级用户可以。普通进程只可以向拥有相同uid和gid或者在相同进程组的进程发送信号。通过设置task——struct的signal中适当的位产生信号。如果进程不阻塞信号,而且正在等待但是可以中断(状态是Interruptible),那么它的状态被改为Running并确认它在运行队列,通过这种方式把它唤醒。这样调度程序在系统下次调度的时候会把它当作一个运行的候选。如果需要缺省的处理,Linux可以优化信号的处理。例如如果信号SIGWINCH(X window改变焦点)发生而使用缺省的处理程序,则不需要做什么事情。

     

    信号产生的时候不会立刻出现在进程中,它们必须等到进程下次运行。每一次进程从系统调用中退出的时候都要检查它的signal和blocked域,如果有任何没有阻塞的信号,就可以发送。这看起来好像非常不可靠,但是系统中的每一个进程都在调用系统调用,比如向终端写一个字符的过程中。如果愿意,进程可以选择等待信号,它们挂起在Interruptible状态,直到有了一个信号。Linux信号处理代码检查sigaction结构中每一个当前未阻塞的信号。

     

    如果信号处理程序设置为缺省动作,则核心会处理它。SIGSTOP信号的缺省处理是把当前进程的状态改为Stopped,然后运行调度程序,选择一个新的进程来运行。SIGFPE信号的缺省动作是让当前进程产生core(core dump),让它退出。变通地,进程可以指定自己的信号处理程序。这是一个例程,当信号产生的时候调用而且sigaction结构包括这个例程的地址。Linux必须调用进程的信号处理例程,至于具体如何发生是和处理器相关。但是,所有的CPU必须处理的是当前进程正运行在核心态,并正准备返回到调用核心或系统例程的用户态的进程。解决这个问题的方法是处理该进程的堆栈和寄存器。进程程序计数器设为它的信号处理程序的地址,例程的参数加到调用结构或者通过寄存器传递。当进程恢复运行的时候显得信号处理程序是正常的调用。

     

    Linux是POSIX兼容的,所以进程可以指定调用特定的信号处理程序的时候要阻塞的信号。这意味着在调用进程的信号处理程序的时候改变blocked掩码。信号处理程序结束的时候,blocked掩码必须恢复到它的初始值。因此,Linux在收到信号的进程的堆栈中增加了对于一个整理例程的调用,把blocked掩码恢复到初始值。Linux也优化了这种情况:如果同时几个信号处理例程需要调用的时候,就在它们堆积在一起,每次退出一个处理例程的时候就调用下一个,直到最后才调用整理例程。

     

    5.2 Pipes(管道)

     

    普通的Linux shell都允许重定向。例如:

     

    $ ls | pr | lpr

     

    把列出目录文件的命令ls的输出通过管道接到pr命令的标准输入上进行分页。最后,pr命令的标准输出通过管道连接到lpr命令的标准输入上,在缺省打印机上打印出结果。管道是单向的字节流,把一个进程的标准输出和另一个进程的标准输入连接在一起。没有一个进程意识到这种重定向,和它平常一样工作。是shell建立了进程之间的临时管道。在Linux中,使用指向同一个临时VFS I节点(本身指向内存中的一个物理页)的两个file数据结构来实现管道。图5.1显示了每一个file数据结构包含了不同的文件操作例程的向量表的指针:一个用于写,另一个从管道中读。这掩盖了和通用的读写普通文件的系统调用的不同。当写进程向管道中写的时候,字节拷贝到了共享的数据页,当从管道中读的时候,字节从共享页中拷贝出来。Linux必须同步对于管道的访问。必须保证管道的写和读步调一致,它使用锁、等待队列和信号(locks,wait queues and signals)。

    参见include/linux/inode_fs.h

     



    当写进程向管道写的时候,它使用标准的write库函数。这些库函数传递的文件描述符是进程的file数据结构组中的索引,每一个都表示一个打开的文件,在这种情况下,是打开的管道。Linux系统调用使用描述这个管道的file数据结构指向的write例程。这个write例程使用表示管道的VFS I 节点存放的信息,来管理写的请求。如果有足够的空间把所有的字节都写导管到中,只要管道没有被读进程锁定,Linux为写进程上锁,并把字节从进程的地址空间拷贝到共享的数据页。如果管道被读进程锁定或者空间不够,当前进程睡眠,并放在管道I节点的等待队列中,并调用调度程序,运行另外一个进程。它是可以中断的,所以它可以接收信号。当管道中有了足够的空间写数据或者锁定解除,写进程就会被读进程唤醒。当数据写完之后,管道的VFS I 节点锁定解除,管道I节点的等待队列中的所有读进程都会被唤醒。

    参见fs/pipe.c pipe_write()

     

    从管道中读取数据和写数据非常相似。进程允许进行非阻塞的读(依赖于它们打开文件或者管道的模式),这时,如果没有数据可读或者管道被锁定,会返回一个错误。这意味着进程会继续运行。另一种方式是在管道的I节点的等待队列中等待,直到写进程完成。如果管道的进程都完成了操作,管道的I节点和相应的共享数据页被废弃。

    参见fs/pipe.c pipe_read()

     

    Linux也可以支持命名管道,也叫FIFO,因为管道工作在先入先出的原则下。首先写入管道的数据是首先被读出的数据。不想管道,FIFO不是临时的对象,它们是文件系统中的实体,可以用mkfifo命令创建。只要有合适的访问权限,进程就可以使用FIFO。FIFO的大开方式和管道稍微不同。一个管道(它的两个file数据结构,VFS I节点和共享的数据页)是一次性创建的,而FIFO是已经存在,可以由它的用户打开和关闭的。Linux必须处理在写进程打开FIFO之前打开FIFO读的进程,以及在写进程写数据之前读的进程。除了这些,FIFO几乎和管道的处理完全一样,而且它们使用一样的数据结构和操作。

     


    Sockets

     

    注意:写网络篇的时候加上去

     


    System V IPC mechanisms (系统V IPC机制)

     

    Linux支持三种首次出现在Unix 系统V(1983)的进程间通讯的机制:消息队列、信号灯和共享内存(message queues, semaphores and shared memory)。系统V IPC机制共享通用的认证方式。进程只能通过系统调用,传递一个唯一的引用标识符到核心来访问这些资源。对于系统V IPC对象的访问的检查使用访问许可权,很象对于文件访问的检查。对于系统V IPC对象的访问权限由对象的创建者通过系统调用创建。每一种机制都使用对象的引用标识符作为资源表的索引。这不是直接的索引,需要一些操作来产生索引。

     

    系统中表达系统V IPC对象的所有Linux数据结构都包括一个ipc_perm的数据结构,包括了创建进程的用户和组标识符,对于这个对象的访问模式(属主、组和其他)和IPC对象的key。Key 用作定位系统V IPC对象的引用标识符的方法。支持两种key:公开和四有的。如果key是公开的,那么系统中的任何进程,只要通过了权限检查,就可以找到对应的系统V IPC对象的引用标识符。系统V IPC对象不能使用key引用,必须使用它们的引用标识符。

    参见include/linux/ipc.h

     


    Message Queues(消息队列)

     

    消息队列允许一个或多个进程写消息,一个或多个进程读取消息。Linux维护了一系列消息队列的msgque 向量表。其中的每一个单元都指向一个msqid_ds的数据结构,完整描述这个消息队列。当创建消息队列的时候,从系统内存中分配一个新的msqid_ds的数据结构并插入到向量表中

     

    每一个msqid_ds数据结构都包括一个ipc_perm的数据结构和进入这个队列的消息的指针。另外,Linux保留队列的改动时间,例如上次队列写的时间等。Msqid_ds队列也包括两个等待队列:一个用于向消息队列写,另一个用于读。

    参见include/linux/msg.h



    每一次一个进程试图向写队列写消息,它的有效用户和组的标识符就要和队列的ipc_perm数据结构的模式比较。如果进程可以想这个队列写,则消息会从进程的地址空间写到msg数据结构,放到消息队列的最后。每一个消息都带有进程间约定的,应用程序指定类型的标记。但是,因为Linux限制了可以写的消息的数量和长度,可能会没有空间容纳消息。这时,进程会被放到消息队列的写等待队列,然后调用调度程序选择一个新的进程运行。当一个或多个消息从这个消息队列中读出去的时候会被唤醒。

     

    从队列中读是一个相似的过程。进程的访问权限一样被检查。一个读进程可以选择是不管消息的类型从队列中读取第一条消息还是选择特殊类型的消息。如果没有符合条件的消息,读进程会被加到消息队列的读等待进程,然后运行调度程序。当一个新的消息写到队列的时候,这个进程会被唤醒,继续运行。

     


    Semaphores(信号灯)

     

    信号灯最简单的形式就是内存中一个位置,它的取值可以由多个进程检验和设置。检验和设置的操作,至少对于关联的每一个进程来讲,是不可中断或者说有原子性:只要启动就不能中止。检验和设置操作的结果是信号灯当前值和设置值的和,可以是正或者负。根据测试和设置操作的结果,一个进程可能必须睡眠直到信号灯的值被另一个进程改变。信号灯可以用于实现重要区域(critical regions),就是重要的代码区,同一时刻只能有一个进程运行。

     

    比如你有许多协作的进程从一个单一的数据文件读写记录。你可能希望对文件的访问必须严格地协调。你可以使用一个信号灯,初始值1,在文件操作的代码中,加入两个信号灯操作,第一个检查并把信号灯的值减小,第二个检查并增加它。访问文件的第一个进程试图减小信号灯的数值,如果成功,信号灯的取值成为0。这个进程现在可以继续运行并使用数据文件。但是,如果另一个进程需要使用这个文件,现在它试图减少信号灯的数值,它会失败因为结果会是-1。这个进程会被挂起直到第一个进程处理完数据文件。当第一个进程处理完数据文件,它会增加信号灯的数值成为1。现在等待进程会被唤醒,这次它减小信号灯的尝试会成功。

     



     

    每一个系统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),调用调度程序,运行另外一个进程。

    参见include/linux/sem.h

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

     

    在信号灯操作中有一个问题:死锁(deadlock)。这发生在一个进程改变了信号灯的值进入一个重要区域(critical region)但是因为崩溃或者被kill而没有离开这个重要区域的情况下。Linux通过维护信号灯数组的调整表来避免这种情况。就是如果实施这些调整,信号灯就会返回一个进程的信号灯操作前的状态。这些调整放在sem_undo数据结构中,排在sem_ds数据结构的队列中,同时排在使用这些信号灯的进程的task_struct数据结构的队列中。

     

    每一个独立的信号灯操作可能都需要维护一个调整动作。Linux至少为每一个进程的每一个信号灯数组都维护一个sem_undo的数据结构。如果请求的进程没有,就在需要的时候为它创建一个。这个新的sem_undo数据结构同时在进程的task_struct数据结构和信号灯队列的semid_ds数据结构的队列中排队。对信号灯队列中的信号灯执行操作的时候,和这个操作值相抵消的值加到这个进程的sem_undo数据结构的调整队列这个信号灯的条目上。所以,如果操作值为2,那么这个就在这个信号灯的调整条目上增加-2。

     

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

     


    Shared Memory(共享内存)

     

    共享内存允许一个或多个进程通过同时出现在它们的虚拟地址空间的内存通讯。这块虚拟内存的页面在每一个共享进程的页表中都有页表条目引用。但是不需要在所有进程的虚拟内存都有相同的地址。象所有的系统V IPC对象一样,对于共享内存区域的访问通过key控制,并进行访问权限检查。内存共享之后,就不再检查进程如何使用这块内存。它们必须依赖于其他机制,比如系统V的信号灯来同步对于内存的访问。

     

    每一个新创建的内存区域都用一个shmid_ds数据结构来表达。这些数据结构保存在shm_segs向量表中。Shmid_ds数据结构描述了这个共享内存取有多大、多少个进程在使用它以及共享内存如何映射到它们的地址空间。由共享内存的创建者来控制对于这块内存的访问权限和它的key是公开或私有。如果有足够的权限它也可以把共享内存锁定在物理内存中。

    参见include/linux/sem.h

     

    每一个希望共享这块内存的进程必须通过系统调用粘附(attach)到虚拟内存。这为该进程创建了一个新的描述这块共享内存的vm_area_struct数据结构。进程可以选择共享内存在它的虚拟地址空间的位置或者由Linux选择一块足够的的空闲区域。



    这个新的vm_area_struct结构放在由shmid_ds指向的vm_area_struct列表中。通过vm_next_shared和vm_prev_shared把它们连在一起。虚拟内存在粘附的时候其实并没有创建,而发生在第一个进程试图访问它的时候。

     

    在一个进程第一次访问共享虚拟内存的其中一页的时候,发生一个page fault。当Linux处理这个page fault的时候,它找到描述它的vm_area_struct数据结构。这里包含了这类共享虚拟内存的处理例程的指针。共享内存的page fault处理代码在这个shmid_ds的页表条目列表中查找,看是否存在这个共享虚拟内存页的条目。如果不存在,它就分配一个物理页,并为它创建一个页表条目。这个条目不但进入当前进程的页表,也存到这个shmid_ds。这意味着当下一个进程试图访问这块内存并得到一个page fault的时候,共享内存错误处理代码也会让这个进程使用这个新创建的物理页。所以,是第一个访问共享内存页的进程使得这一页被创建,而随后访问的其他进程使得此页被加到它们的虚拟地址空间。

     

    当进程不再需要共享虚拟内存的时候,它们从中分离(detach)出来。只要仍旧有其他进程在使用这块内存,这种分离只是影响当前的进程。它的vm_area_struct从shmid_ds数据结构中删除,并释放。当前进程的页表也进行更新,使它共享过的虚拟内存区域无效。当共享这块内存的最后一个进程从中分离出的时候,共享内存当前在物理内存中的页被释放,这块共享内存的shmid_ds数据结构也被释放。

     

    如果共享的虚拟内存没有被锁定在物理内存中的话会更加复杂。在这种情况下,共享内存的页可能在系统大量使用内存的时候交换到了系统的交换磁盘。共享内存如何交换初和交换入物理内存在第3章中有描述。


    发布人:netbull 来自:非常Linux