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 限制了可以写的消息的数量和长度,可能会没有空间容纳消息。这时,进程会被放到消息队列的写等待队列,然后调用调度程序选择一个新的进程运行。当一个或多个消息从这个消息队列中读出去的时候会被唤醒。
每一个系统 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 数据结构的队列中。
共享内存允许一个或多个进程通过同时出现在它们的虚拟地址空间的内存通讯。这块虚拟内存的页面在每一个共享进程的页表中都有页表条目引用。但是不需要在所有进程的虚拟内存都有相同的地址。象所有的系统 V IPC 对象一样,对于共享内存区域的访问通过 key 控制,并进行访问权限检查。内存共享之后,就不再检查进程如何使用这块内存。它们必须依赖于其他机制,比如系统 V 的信号灯来同步对于内存的访问。