进程之间以及进程与内核之间通过通信来协调它们的活动。Linux 支持多种进程间通信 (IPC) 机制。信号和管道是其中两种,但 Linux 也支持 System V IPC 机制,这些机制以它们首次出现的 Unix TM 版本命名。
内核可以生成一组定义的信号,系统中的其他进程也可以生成这些信号,前提是它们具有正确的权限。您可以使用以下命令列出系统的信号集kill命令 (kill -l),在我的 Intel Linux 机器上,这会给出
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(浮点异常)信号时,默认操作是核心转储然后退出。信号没有固有的相对优先级。如果同时为进程生成两个信号,则它们可能会以任何顺序呈现给进程或处理。也没有处理同一类型多个信号的机制。进程无法区分它接收到 1 个还是 42 个SIGCONT信号。
Linux 使用存储在task_struct中的信息来实现信号。支持的信号数量限制为处理器的字长。字长为 32 位的处理器可以有 32 个信号,而像 Alpha AXP 这样的 64 位处理器最多可以有 64 个信号。当前待处理的信号保存在signal字段中,阻塞信号的掩码保存在blocked中。除了SIGSTOPSIGSTOPSIGKILL和SIGKILL之外,所有信号都可以被阻塞。如果生成了阻塞信号,它将保持待处理状态,直到被解除阻塞。Linux 还保存有关每个进程如何处理每个可能信号的信息,这些信息保存在task_structsigactionSIGKILL数据结构数组中,该数组由每个进程的blockedtask_struct
指向。 除此之外,它还包含将处理信号的例程的地址,或者一个标志,该标志告诉 Linux 进程希望忽略此信号还是让内核为其处理信号。进程通过发出系统调用来修改默认信号处理,这些调用会更改task_structsigactionsignal以及blocked掩码。
并非系统中的每个进程都可以向每个其他进程发送信号,内核和超级用户可以。普通进程只能向具有相同 uid 和 gid 的进程或同一进程组中的进程发送信号1。通过设置signalSIGSTOPblockedtask_structSIGKILL的
signalSIGSTOP字段中的相应位来生成信号。如果进程没有阻塞信号并且正在等待但可中断(处于可中断状态),则通过将其状态更改为运行状态并确保它在运行队列中来唤醒它。这样,当系统下次调度时,调度程序会将它视为运行的候选者。如果需要默认处理,则 Linux 可以优化信号的处理。例如,如果信号SIGFPESIGWINCHSIGKILL(X 窗口更改了焦点)并且正在使用默认处理程序,则无需执行任何操作。
信号不会在生成后立即呈现给进程,它们必须等到进程再次运行时才能呈现。每次进程从系统调用退出时,都会检查其blockedtask_structblocked字段,如果存在任何未阻塞的信号,则现在可以传递它们。 这似乎是一种非常不可靠的方法,但系统中的每个进程都在不断地进行系统调用,例如向终端写入字符。进程可以选择等待信号,如果它们希望这样做,它们会以可中断状态挂起,直到信号呈现。Linux 信号处理代码查看每个当前未阻塞信号的blockedsigaction
$ ls | pr | lpr
SIGSTOP信号的默认处理程序会将当前进程的状态更改为停止状态,然后运行调度程序以选择新的进程来运行。SIGKILL信号的默认操作将核心转储进程,然后导致其退出。 或者,进程可能已指定了自己的信号处理程序。这是一个例程,每当生成信号时都会调用该例程,并且sigaction信号的默认操作将核心转储进程,然后导致其退出。 或者,进程可能已指定了自己的信号处理程序。这是一个例程,每当生成信号时都会调用该例程,并且结构保存着此例程的地址。内核必须调用进程的信号处理例程,以及这种情况如何发生是处理器特定的,但所有 CPU 都必须处理当前进程在内核模式下运行并且即将返回到在用户模式下调用内核或系统例程的进程这一事实。该问题通过操作进程的堆栈和寄存器来解决。进程的程序计数器设置为其信号处理例程的地址,并且例程的参数被添加到调用帧或在寄存器中传递。当进程恢复操作时,看起来好像信号处理例程被正常调用一样。Linux 与 POSIX 兼容,因此进程可以指定在调用特定信号处理例程时阻塞哪些信号。这意味着在调用进程的信号处理程序期间更改blocked
blocked掩码必须恢复为其原始值。因此,Linux 添加了对清理例程的调用,该例程会将原始blocked掩码必须恢复为其原始值。因此,Linux 添加了对清理例程的调用,该例程会将原始掩码恢复到已发出信号的进程的调用堆栈上。Linux 还优化了需要调用多个信号处理例程的情况,方法是将它们堆叠起来,以便每次一个处理例程退出时,都会调用下一个例程,直到调用清理例程。
5.2 管道
常见的 Linux Shell 都允许重定向。例如掩码必须恢复为其原始值。因此,Linux 添加了对清理例程的调用,该例程会将原始管道将掩码必须恢复为其原始值。因此,Linux 添加了对清理例程的调用,该例程会将原始ls
命令的输出(列出目录的文件)输入到
pr
命令的标准输入中,该命令对它们进行分页。最后,
pr命令的标准输出通过管道输入到lpr掩码必须恢复为其原始值。因此,Linux 添加了对清理例程的调用,该例程会将原始命令的标准输入中,该命令将结果打印在默认打印机上。因此,管道是将一个进程的标准输出连接到另一个进程的标准输入的单向字节流。两个进程都没有意识到这种重定向,并且行为方式与正常情况相同。正是 Shell 设置了进程之间的这些临时管道。
数据结构都包含指向不同文件操作例程向量的指针;一个用于写入管道,另一个用于从管道读取。这隐藏了从通用系统调用到读取和写入普通文件的底层差异。当写入进程写入管道时,字节被复制到共享数据页面中,并且当读取进程从管道读取时,字节从共享数据页面中复制出来。Linux 必须同步对管道的访问。它必须确保管道的读取器和写入器同步,为此,它使用锁、等待队列和信号。
当写入器想要写入管道时,它使用标准的写入库函数。这些函数都传递文件描述符,这些文件描述符是进程的
mkfifo如果有足够的空间将所有字节写入管道,并且只要管道未被其读取器锁定,Linux 就会为写入器锁定它,并将要写入的字节从进程的地址空间复制到共享数据页面。如果管道被读取器锁定,或者如果没有足够的空间容纳数据,则当前进程将被置于管道索引节点的等待队列中休眠,并且调用调度程序以便另一个进程可以运行。它是可中断的,因此它可以接收信号,并且当有足够的空间容纳写入数据或管道解锁时,它将被读取器唤醒。写入数据后,管道的 VFS 索引节点将被解锁,并且任何在索引节点的等待队列上休眠的等待读取器本身都将被唤醒。
命令创建。只要进程具有适当的访问权限,就可以自由使用 FIFO。FIFO 的打开方式与管道略有不同。管道(它的两个这隐藏了从通用系统调用到读取和写入普通文件的底层差异。当写入进程写入管道时,字节被复制到共享数据页面中,并且当读取进程从管道读取时,字节从共享数据页面中复制出来。Linux 必须同步对管道的访问。它必须确保管道的读取器和写入器同步,为此,它使用锁、等待队列和信号。file如果有足够的空间将所有字节写入管道,并且只要管道未被其读取器锁定,Linux 就会为写入器锁定它,并将要写入的字节从进程的地址空间复制到共享数据页面。如果管道被读取器锁定,或者如果没有足够的空间容纳数据,则当前进程将被置于管道索引节点的等待队列中休眠,并且调用调度程序以便另一个进程可以运行。它是可中断的,因此它可以接收信号,并且当有足够的空间容纳写入数据或管道解锁时,它将被读取器唤醒。写入数据后,管道的 VFS 索引节点将被解锁,并且任何在索引节点的等待队列上休眠的等待读取器本身都将被唤醒。数据结构、它的 VFS 索引节点和共享数据页面)是一次性创建的,而 FIFO 已经存在,并由其用户打开和关闭。Linux 必须处理读取器在写入器打开 FIFO 之前打开 FIFO 的情况,以及读取器在任何写入器写入 FIFO 之前读取 FIFO 的情况。除此之外,FIFO 的处理方式几乎与管道完全相同,它们使用相同的数据结构和操作。
5.3 套接字这隐藏了从通用系统调用到读取和写入普通文件的底层差异。当写入进程写入管道时,字节被复制到共享数据页面中,并且当读取进程从管道读取时,字节从共享数据页面中复制出来。Linux 必须同步对管道的访问。它必须确保管道的读取器和写入器同步,为此,它使用锁、等待队列和信号。审阅注释在编写网络章节时添加。
5.3.1 System V IPC 机制
Linux 支持三种最早出现在 Unix TM System V (1983) 中的进程间通信机制。这些机制是消息队列、信号量和共享内存。这些 System V IPC 机制都共享通用的身份验证方法。进程只能通过系统调用将唯一的引用标识符传递给内核来访问这些资源。对这些 System V IPC 对象的访问使用访问权限进行检查,就像对文件的访问进行检查一样。System V IPC 对象的访问权限由对象的创建者通过系统调用设置。对象引用标识符由每个机制用作资源表中的索引。它不是一个直接的索引,而是需要一些操作才能生成索引。
ipc_perm
结构,该结构包含所有者和创建者进程的用户和组标识符。此对象的访问模式(所有者、组和其他)和 IPC 对象的键。键用作查找 System V IPC 对象引用标识符的一种方法。支持两组键:公共键和私有键。如果键是公共键,则系统中的任何进程(受权限检查的约束)都可以找到 System V IPC 对象的引用标识符。System V IPC 对象永远不能使用键引用,只能使用其引用标识符引用。
消息队列允许一个或多个进程写入消息,这些消息将由一个或多个读取进程读取。Linux 维护消息队列列表,即msgque
向量;每个元素都指向一个msgquemsqid_ds数据结构,该结构完整地描述了消息队列。创建消息队列时,会从系统内存中分配一个新的msqid_ds数据结构,并将其插入到向量中。图 5.2:System V IPC 消息队列每个msqid_ds数据结构都包含一个ipc_perm数据结构以及指向输入到此队列的消息的指针。此外,Linux 还保留队列修改时间,例如上次写入此队列的时间等等。msqid_ds数据结构以及指向输入到此队列的消息的指针。此外,Linux 还保留队列修改时间,例如上次写入此队列的时间等等。还包含两个等待队列;一个用于队列的写入器,一个用于消息队列的读取器。每次进程尝试向写入队列写入消息时,都会将其有效用户和组标识符与此队列的SIGSTOPmsqid_ds数据结构中的模式进行比较。如果进程可以写入队列,则可以将消息从进程的地址空间复制到数据结构以及指向输入到此队列的消息的指针。此外,Linux 还保留队列修改时间,例如上次写入此队列的时间等等。msg数据结构中,并将其放在此消息队列的末尾。每条消息都标记有应用程序特定的类型,该类型在协作进程之间约定。但是,可能没有足够的空间容纳消息,因为 Linux 限制了可以写入的消息数量和长度。在这种情况下,进程将被添加到此消息队列的写入等待队列中,并且将调用调度程序以选择新的进程来运行。当从该消息队列中读取一条或多条消息时,它将被唤醒。从队列中读取消息的过程类似。同样,检查进程对写入队列的访问权限。读取进程可以选择获取队列中的第一条消息(无论其类型如何),或者选择具有特定类型的消息。如果没有任何消息符合此条件,则读取进程将被添加到消息队列的读取等待队列中,并运行调度程序。当新消息写入队列时,此进程将被唤醒并再次运行。
5.3.3 信号量每次进程尝试向写入队列写入消息时,都会将其有效用户和组标识符与此队列的最简单的形式中,信号量是内存中的一个位置,其值可以由多个进程测试和设置。就每个进程而言,测试和设置操作是不可中断的或原子的;一旦开始,任何东西都无法阻止它。测试和设置操作的结果是信号量的当前值与设置值的相加,设置值可以是正数或负数。根据测试和设置操作的结果,一个进程可能必须休眠,直到信号量的值被另一个进程更改。信号量可以用于实现临界区,即一次只能由一个进程执行的临界代码区域。数据结构以及指向输入到此队列的消息的指针。此外,Linux 还保留队列修改时间,例如上次写入此队列的时间等等。假设您有许多协作进程从单个数据文件中读取记录和向单个数据文件中写入记录。您希望严格协调对该文件的访问。您可以使用初始值为 1 的信号量,并在文件操作代码周围放置两个信号量操作,第一个操作用于测试和递减信号量的值,第二个操作用于测试和递增信号量的值。第一个访问文件的进程将尝试递减信号量的值,并且它将成功,信号量的值现在为 0。此进程现在可以继续使用数据文件,但是如果另一个希望使用它的进程现在尝试递减信号量的值,它将失败,因为结果将为 -1。该进程将被挂起,直到第一个进程完成数据文件的使用。当第一个进程完成数据文件的使用后,它将递增信号量的值,使其再次变为 1。现在,等待的进程可以被唤醒,并且这次它尝试递增信号量将成功。
图 5.3:System V IPC 信号量System V IPC 信号量对象各自描述一个信号量数组,Linux 使用semid_dsmsgque数据结构来表示这一点。系统中的所有task_structsemid_ds
数据结构都由System V IPC 信号量对象各自描述一个信号量数组,Linux 使用semarySystem V IPC 信号量对象各自描述一个信号量数组,Linux 使用(指针向量)指向。每个信号量数组中有task_structsem_nsemsmsgque个信号量,每个信号量由System V IPC 信号量对象各自描述一个信号量数组,Linux 使用sem
数据结构描述,该数据结构由System V IPC 信号量对象各自描述一个信号量数组,Linux 使用sem_baseSystem V IPC 信号量对象各自描述一个信号量数组,Linux 使用指向。所有被允许操作 System V IPC 信号量对象的信号量数组的进程都可以进行系统调用,以对它们执行操作。系统调用可以指定多个操作,每个操作由三个输入描述;信号量索引、操作值和一组标志。信号量索引是指向信号量数组的索引,操作值是将添加到信号量当前值的数值。首先,Linux 测试所有操作是否都会成功。如果操作值加上信号量的当前值将大于零,或者如果操作值和信号量的当前值都为零,则操作将成功。如果任何信号量操作将失败,则 Linux 可能会挂起进程,但前提是操作标志未请求系统调用是非阻塞的。如果要挂起进程,则 Linux 必须保存要执行的信号量操作的状态,并将当前进程放入等待队列中。它通过在堆栈上构建一个task_structsem_queueSystem V IPC 信号量对象各自描述一个信号量数组,Linux 使用数据结构并填充它来实现这一点。新的
数据结构被放在此信号量对象的等待队列的末尾(使用
和sem_pending_last指针)。当前进程被放入sem_queue数据结构(
sleepersem_pending_last)中的等待队列中,并调用调度程序以选择另一个进程来运行。
如果所有信号量操作都将成功,并且当前进程不需要挂起,则 Linux 将继续并将操作应用于信号量数组的相应成员。现在,Linux 必须检查任何等待的、挂起的进程现在是否可以应用其信号量操作。它依次查看操作挂起队列(sem_pending)的每个成员,以查看这次信号量操作是否会成功。如果它们会成功,则它会从操作挂起列表中删除sem_pendingsem_queuesem_pending数据结构,并将信号量操作应用于信号量数组。它唤醒休眠进程,使其在下次调度程序运行时可以重新启动。Linux 不断从头开始查看挂起列表,直到没有信号量操作可以应用,因此没有更多进程可以唤醒。sem_pending_last信号量存在一个问题,即死锁。当一个进程在进入临界区时更改了信号量的值,但随后由于崩溃或被杀死而未能离开临界区时,就会发生这种情况。Linux 通过维护对信号量数组的调整列表来防止这种情况。其思想是,当应用这些调整时,信号量将恢复到进程的一组信号量操作应用之前的状态。这些调整保存在sem_undoSIGSTOP数据结构中,这些数据结构在semid_ds
数据结构和sem_pendingtask_structsem_pending_last数据结构上排队,用于使用这些信号量数组的进程。sem_pending_last每个单独的信号量操作都可以请求维护调整。对于每个信号量数组,Linux 最多将为每个进程维护一个
sem_undosem_pending数据结构。如果请求进程没有一个,则在需要时创建。新的sem_pending_lastsem_undosem_pending_last数据结构被排队到此进程的
task_struct
1 在 Linux 中,管道是使用两个 semid_ds