Linux 核心(二)

Linux 核心(二)
 
 www.ibmtc.pku.edu.cn/crs/kernel/kernel.htm
 
 (此文作者允许此文用于学术目的.)
 
 第十一章
 
 进程及进程间通讯机制
 
 程序是保存在磁盘上的文件,其中包含了计算机的执行指令和数据,而进程则可以看成是运行中的程序。程序是静态的,而进程是动态的。和进程联系在一起的不仅有进程的指令和数据,而且还有当前的指令指针、所有的 CPU 寄存器以及用来保存临时数据的堆栈等,所有这些都随着程序指令的执行在变化。
 
 进程在运行过程中,要使用许多计算机资源,例如 CPU、内存、文件等。Linux 是一个多任务操作系统,同时可能会有多个进程使用同一个资源,因此操作系统要跟踪所有的进程及其所使用的系统资源,以便能够管理进程和资源。
 
 Linux 是一个多任务操作系统,它要保证 CPU 时刻保持在使用状态,如果某个正在运行的进程等待外部设备完成工作(例如等待打印机完成打印任务),这时,操作系统就可以选择其他进程运行,从而保持 CPU 的最大利用率。这就是多任务的基本思想,进程之间的切换由调度程序完成。
 
 Linux 中的每个进程有自己的虚拟地址空间,操作系统的一个最重要的基本管理目的,就是避免进程之间的互相影响。但有时用户也希望能够利用两个或多个进程的功能完成同一任务,为此,Linux 提供许多机制,利用这些机制,进程之间可以进行通讯并共同完成某项任务,这种机制称为“进程间通讯(IPC)”。信号和管道是常见的两种 IPC 机制,但 Linux 也提供其他 IPC 机制。
 
 本章主要描述 Linux 进程的管理、调度以及 Linux 系统支持的进程间通讯机制。
 
 11.1 Linux 进程及线程
 
 Linux 内核利用一个数据结构(task_struct)代表一个进程,代表进程的数据结构指针形成了一个 task 数组(Linux 中,任务和进程是两个相同的术语),这种指针数组有时也成为指针向量。这个数组的大小默认为 512,表明在 Linux 系统中能够同时运行的进程最多可有 512。当建立新进程的时候,Linux 为新的进程分配一个 task_struct 结构,然后将指针保存在 task 数组中。task_struct 结构中包含了许多字段,按照字段功能,可分成如下几类:
 
 标识号。系统通过进程标识号唯一识别一个进程,但进程标识号并不是进程对应的 task_struct 结构指针在 task 数组中的索引号。另外,一个进程还有自己的用户和组标识号,系统通过这两个标识号判断进程对文件或设备的访问权。 
 
 状态信息。一个 Linux 进程可有如下几种状态:运行、等待、停止和僵死。 
 
 调度信息。调度程序利用该信息完成进程之间的切换。 
 
 有关进程间通讯的信息。系统利用这一信息实现进程间的通讯。 
 
 进程链信息。在 Linux 系统中,除初始化进程之外,任何一个进程都具有父进程。每个进程都是从父进程中“克隆”出来的。进程链则包含进程的父进程指针、和该进程具有相同父进程的兄弟进程指针以及进程的子进程指针。另外,Linux 利用一个双向链表记录系统中所有的进程,这个双向链表的根就是 init 进程。利用这个链表中的信息,内核可以很容易地找到某个进程。 
 
 时间和定时器。系统在这些字段中保存进程的建立时间,以及在其生命周期中所花费的 CPU 时间,这两个时间均以 jiffies 为单位。这一时间由两部分组成,一是进程在用户模式下花费的时间,二是进程在系统模式下花的时间。Linux 也支持和进程相关的定时器,应用程序可通过系统调用建立定时器,当定时器到期时,操作系统会向该进程发送 SIGALRM 信号。 
 
 文件系统信息。进程可以打开文件系统中的文件,系统需要对这些文件进行跟踪。系统使用这类字段记录进程所打开的文件描述符信息。另外,还包含指向两个 VFS 索引节点的指针,这两个索引节点分别是进程的主目录以及进程的当前目录。索引节点中有一个引用计数器,当有新的进程指向某个索引节点时,该索引节点的引用计数器会增加计数。未被引用的索引节点的引用计数为 0,因此,当包含在某个目录中的文件正在运行时,就无法删除这一目录,因为这一目录的引用计数大于0。 
 
 和进程相关的上下文信息。如前所述,进程可被看成是系统状态的集合,随着进程的运行,这一集合发生变化。进程上下文就是用来保存系统状态的 task_struct 字段。当调度程序将某个进程从运行状态切换到暂停状态时,会在上下文中保存当前的进程运行环境,包括 CPU 寄存器的值以及堆栈信息;当调度程序再次选择该进程运行时,则会从进程上下文信息中恢复进程的运行环境。 
 
 11.1.1 标识符信息
 
 和所有的 UNIX 系统一样,Linux 使用用户标识符和组标识符判断用户对文件和目录的访问许可。Linux 系统中的所有文件或目录均具有所有者和许可属性,Linux 据此判断某个用户对文件的访问权限。对一个进程而言,系统在 task_struct 结构中记录如表 11-1 所示的四对标识符。
 
 表 11-1 进程的标识符信息
 
 uid 和 gid
 
 运行进程所代表的用户之用户标识号和组标识号,通常就是执行该进程的用户。
 
  
 
 有效uid 和 gid
 
 某些程序可以将 uid 和 gid 改变为自己私有的 uid 和 gid。系统在运行这样的程序时,会根据修改后的 uid 及 gid 判断程序的特权,例如,是否能够直接进行 I/O 输出等。通过 setuid 系统调用,可将程序的有效 uid 和 gid 设置为其他用户。在该程序映象文件的 VFS 索引节点中,有效 uid 和 gid 由索引节点的属性描述。
 
  
 
 文件系统uid 和 gid
 
 这两个标识符和上述标识符类似,但用于检查对文件系统的访问许可时。处于用户模式的 NFS 服务器作为特殊进程访问文件时使用这两个标识符。
 
  
 
 保存uid 和 gid
 
 如果进程通过系统调用修改了进程的 uid 和 gid,这两个标识符则保存实际的 uid 和 gid。
 
  
 
 11.1.2 进程状态信息
 
 如前所述,Linux 中的进程有四中状态,如表 11-2 所示。
 
 表 11-2 进程的状态信息
 
 运行状态
 
 该进程是当前正在运行的进程;或者,该进程是可以运行的进程,即正在等待调度程序将 CPU 分配给它。
 
  
 
 等待状态
 
 进程正在等待某个事件或某个资源。这种进程又分为可中断的进程和不可中断的进程两种。可中断的等待进程可被信号中断,而不可中断的等待进程是正在直接等待硬件状态条件的进程,在任何情况下都不能被中断。
 
  
 
 停止状态
 
 进程处于停止状态,通常由于接收到信号而停止,例如,进程在接收到调试信号时处于停止状态。
 
  
 
 僵死状态
 
 进程已终止,但在 task 数组中仍占据着一个 task_struct 结构。顾名思义,处于这种状态的进程实际是死进程。
 
  
 
 11.1.3 文件信息
 
 如图 11-1 所示,系统中的每个进程有两个数据结构用于描述进程与文件相关的信息。其中,fs_struct 描述了上面提到的两个 VFS 索引节点的指针,即 root 和 pwd。另外,这个结构还包含一个 umask 字段,它是进程创建文件时使用的默认模式,可通过系统调用修改这一默认模式。另一个结构为files_struct,它描述了当前进程所使用的所有文件信息。从图中可以看出,每个进程能够同时拥有 256 个打开的文件,fs[0] 到 fs[255] 就是指向这些 file 结构的指针。文件的描述符实际就是 fs 指针数组的索引号。
 
 在 file 结构中,f_mode 是文件的打开模式,只读、只写或读写;f_pos 是文件的当前位置;f_inode 指向 VFS 中该文件的索引节点;f_op 包含了对该文件的操作例程集。利用 f_op,可以针对不同的文件定义不同的操作函数,例如一个用来向文件中写数据的函数。Linux 利用这一抽象机制,实现了管道这一进程间通讯机制(将在后面详细描述)。这种抽象方法在 Linux 内核中非常常见,通过这种方法,可使特定的内核对象具有类似 C++ 对象的多态性。
 
 Linux 进程启动时,有三个文件描述符被打开,它们是标准输入、标准输出和错误输出,分别对应 fs 数组的三个索引,即 0、1和2。如果启动时进行输入输出重定向,则这些文件描述符指向指定的文件而不是标准的终端输入/输出。每当进程打开一个文件时,就会利用files_struct 的一个空闲 file 指针指向打开的文件描述结构 file。对文件的访问通过 file 结构中定义的文件操作例程和 VFS 索引节点信息来完成。
 
 11.1.4 虚拟内存
 
 在前一章中看到,进程的虚拟内存包含了进程所有的可执行代码和数据。运行某个程序时,系统要根据可执行映象中的信息,为进程代码和数据分配虚拟内存;进程在运行过程中,可能会通过系统调用动态申请虚拟内存或释放已分配的内存,新分配的虚拟内存必须和进程已有的虚拟地址链接起来才能使用;Linux 进程可以使用共享的程序库代码或数据,这样,共享库的代码和数据也需要链接到进程已有的虚拟地址中。在前一章中还看到,系统利用了需求分页机制来避免对物理内存的过分使用。因为进程可能会访问当前不在物理内存中的虚拟内存,这时,操作系统通过对处理器的页故障处理装入内存页。为此,系统需要修改进程的页表,以便标志虚拟页是否在物理内存中,同时,Linux 还需要知道进程地址空间中任何一个虚拟地址区域的来源和当前所在位置,以便能够装入物理内存。
 
  
 
 图 11-1 进程的文件信息
 
 由于上面这些原因,Linux 采用了比较复杂的数据结构跟踪进程的虚拟地址。在进程的 task_struct 结构中包含一个指向 mm_struct 结构的指针。进程的 mm_struct 则包含装入的可执行映象信息以及进程的页表指针。该结构还包含有指向 vm_area_struct 结构的几个指针,每个 vm_area_struct 代表进程的一个虚拟地址区域。
 
 图 11-2 是某个进程的虚拟内存简化布局以及相应的进程数据结构。从图中可以看出,系统以虚拟内存地址的降序排列 vm_area_struct。每个虚拟内存区域可能来源不同,有的可能来自映象,有的可能来自共享库,而有的则可能是动态分配的内存区。因此,Linux 利用了虚拟内存处理例程(vm_ops)来抽象对不同来源虚拟内存的处理方法。
 
  
 
 图 11-2 进程的虚拟内存示意
 
 在进程的运行过程中,Linux 要经常为进程分配虚拟地址区域,或者因为从交换文件中装入内存而修改虚拟地址信息,因此,vm_area_struct 结构的访问时间就成了性能的关键因素。为此,除链表结构外,Linux 还利用 AVL(Adelson-Velskii and Landis)树组织 vm_area_struct。通过这种树结构,Linux 可以快速定位某个虚拟内存地址,但在该树中插入或删除节点需要花费较多的时间。
 
 当进程利用系统调用动态分配内存时,Linux 首先分配一个 vm_area_struct 结构,并链接到进程的虚拟内存链表中,当后续的指令访问这一内存区域时,因为 Linux 尚未分配相应的物理内存,因此处理器在进行虚拟地址到物理地址的映射时会产生页故障(详细内容可参阅第十章),当 Linux 处理这一页故障时,就可以为新的虚拟内存区分配实际的物理内存。
 
 11.1.5 时间和定时器
 
 Linux 保存一个指向当前正在运行的进程之 task_struct 结构的指针,即 current。每当产生一次实时时钟中断(又称时钟滴答),Linux 就会更新 current 所指向的进程的时间信息,如果内核当前代表该进程执行任务(例如进程调用系统调用时),那么系统就将时间记录为进程在系统模式下花费的时间,否则记录为进程在用户模式下花费的时间。
 
 除了为进程记录其消耗的 CPU 时间外,Linux 还支持和进程相关的间隔定时器。当定时器到期时,会向定时器的所属进程发送信号。进程可使用三种不同类型的定时器来给自己发送相应的信号,如表 11-3 所示。
 
 表 11-3 三种不同的进程定时器
 
 Real
 
 该定时器实时更新,到期时发送 SIGALRM 信号。
 
  
 
 Virtual
 
 该定时器只在进程运行时更新,到期时发送 SIGVTALRM 信号。
 
  
 
 Profile
 
 该定时器在进程运行时,以及内核代表进程运行时更新,到期时发送 SIGPROF 信号。
 
  
 
 Linux 对 Virtual 和 Profile 定时器的处理是相同的,在每个时钟中断,定时器的计数值减 1,直到计数值为 0 时发送信号。
 
 对 Real 定时器的处理比较特殊,将在第十五章中讲述。
 
 11.1.6 关于线程
 
 和进程概念紧密相关的概念是线程。线程可看成是进程中指令的不同执行路线。例如,常见的字处理程序中,主线程处理用户输入,而其他并行运行的线程在必要时可在后台保存用户的文档。与进程相关的基本要素有:代码、数据、堆栈、文件 I/O和虚拟内存信息等,因此,系统对进程的处理要花费更多的开支,尤其在进行进程调度时。利用线程则可以通过共享这些基本要素而减轻系统开支,因此,线程也被称为“轻量级进程”。许多流行的多任务操作系统均支持线程。
 
 线程有“用户线程”和“内核线程”之分。用户线程指不需要内核支持而在用户程序中实现的线程,这种线程甚至在象 DOS 这样的操作系统中也可实现,但线程的调度需要用户程序完成,这有些类似 Windows 3.x 的协作式多任务。另外一种则需要内核的参与,由内核完成线程的调度。这两种模型各有其好处和缺点。用户线程不需要额外的内核开支,但是当一个线程因 I/O 而处于等待状态时,整个进程就会被调度程序切换为等待状态,其他线程得不到运行的机会;而内核线程则没有各个限制,但却占用了更多的系统开支。
 
 Linux 支持内核空间的多线程,读者也可以从 Internet 上下载一些用户级的线程库。Linux 的内核线程和其他操作系统的内核实现不同,前者更好一些。大多数操作系统单独定义线程,从而增加了内核和调度程序的复杂性;而 Linux 则将线程定义为“执行上下文”,它实际只是进程的另外一个执行上下文而已。这样,Linux 内核只需区分进程,只需要一个进程/线程数组,而调度程序仍然是进程的调度程序。Linux 的 clone 系统调用可用来建立新的线程。
 
 11.1.7 会话和进程组
 
 由于 Linux 是一个多用户系统,同一时刻,系统中运行有属于不同用户的多个进程。那么,当处于某个终端上的用户按下了 Ctrl+C 键时(产生 SIGINT 信号),系统如何知道将该信号发送到哪个进程,从而不影响由其他终端上的用户运行的进程呢?
 
 Linux 内核通过维护会话和进程组而管理多用户进程。如图 11-3 所示,每个进程是一个进程组的成员,而每个进程组又是某个会话的成员。一般而言,当用户在某个终端上登录时,一个新的会话就开始了。进程组由组中的领头进程标识,领头进程的进程标识符就是进程组的组标识符。类似地,每个会话也对应有一个领头进程。
 
 同一会话中的进程通过该会话的领头进程和一个终端相连,该终端作为这个会话的控制终端。一个会话只能有一个控制终端,而一个控制终端只能控制一个会话。用户通过控制终端,可以向该控制终端所控制的会话中的进程发送键盘信号。
 
 同一会话中只能有一个前台进程组,属于前台进程组的进程可从控制终端获得输入,而其他进程均是后台进程,可能分属于不同的后台进程组。
 
  
 
 图 11-3 会话和进程、进程组
 
 11.2 进程调度
 
 由于 Linux 是一个单块式的操作系统,所以Linux 中的进程有一些部分运行在用户模式,而另一些部分运行在内核模式,或称系统模式。运行模式的变化是通过系统调用完成的。系统模式具有更加高级的 CPU 特权级,例如可以直接读取或写入任意的 I/O 端口,设置 CPU 关键寄存器等。Linux 中的进程无法停止当前正在运行的进程,它只能被动地等待调度程序选择它为运行进程,进程的切换操作需要高特权级的 CPU 指令,因此,只能在系统模式中进行,这样,当进行系统调用时,调度程序就有了机会进行进程切换。例如,当某个进程因为系统调用而不得不处于暂停状态时(例如等待用户键入字符),调度程序就可以选择其他的进程运行。进程在运行过程中经常要调用系统调用,因此,调度程序选择其他进程运行的机会还是较多的。但是,进程有时也可能在用户模式下花费大量的 CPU 时间却不调用系统调用,在这种情况下,如果调度程序只能等待该进程在系统调用时才进行进程切换的话,CPU 的分配就显得有些不太公平,更为极端的是,当某个进程进入死循环时,系统就无法响应用户了。为此,Linux 采用抢先式的调度方法,每个进程每次最多只能运行给定的时间段,在 Linux 中为 200ms。当一个进程运行超过 200ms 时,系统选择其他的进程运行,而原有进程则等待下次运行机会。这一时间在抢先式调度中称为“时间片”。
 
 当需要选择下一个运行进程时,由调度程序选择最值得运行的进程。可运行进程实际是仅等待 CPU 资源的进程,如果某个进程在等待其他资源,则该进程是不可运行进程。Linux 使用了比较简单的基于优先级的进程调度算法选择新的进程。当调度程序选择了新的进程之后,它必须在当前进程的 task_struct 结构中保存和该进程相关的 CPU 寄存器和其他有关指令执行的上下文信息,然后从选定进程的 task_struct 结构中恢复 CPU 寄存器以及上下文信息,新的进程就可以继续在 CPU 中执行了。
 
 对于新建的进程,其 task_struct 结构被置为初始的执行上下文,当调度进程选择这一新建进程时,首先从 task_struct 结构中恢复 CPU 寄存器,CPU 的指令计数寄存器(PC)恰好是该进程的初始执行指令地址,这样,新建的进程就可以从头开始运行了。
 
 为了能够为所有的进程平等分配 CPU 资源,内核在 task_struct 结构中记录如表 11-4 所示的信息。
 
 表 11-4 和进程调度相关的 task_struct 信息
 
 字段
 
 描述
 
  
 
 policy(策略)
 
 这是系统对该进程实施的调度策略。Linux 进程有两种类型的进程:一般进程和实时进程。实时进程比所有一般进程的优先级高,只有一个实时进程可以运行,调度程序就会选择该进程运行。对实时进程而言,有两种调度策略,一种称为“循环赛 (round robin)”,另一种称为“先进先出 (first in first out)”。
 
  
 
 priority(优先级)
 
 这是系统为进程给定的优先级,可通过系统调用或 renice 命令修改该进程的优先级。优先级实际是从进程开始运行算起的、允许进程运行的时间值(以 jiffies 为单位)。
 
  
 
 rt_priority(实时优先级)
 
 这是系统为实时进程给定的相对优先级。
 
  
 
 counter(计数器)
 
 这是进程运行的时间值(以 jiffies 为单位)。开始运行时设置为 priority,每次时钟中断该值减 1。
 
  
 
 调度程序在如下几种情况下运行:当前进程处于等待状态而放入等待队列时;某个系统调用要返回到用户模式之前,这是因为系统调用结束时,当前进程的 counter 值可能刚好为 0。下面是调度程序每次运行时要完成的任务:
 
 1. 调度程序运行底半处理程序(bottom half handler)处理调度程序的任务队列。这些处理程序实际是一些内核线程,将在第十五章中讲述。
 
 2. 在选择其他进程之前,必须处理当前进程。如果当前进程的调度策略为循环赛,则将当前进程放到运行队列的尾部;如果该进程是可中断的,并且自上次调度以来接收到信号,则任务状态设置为运行;如果当前进程的 counter 值为 0,则任务状态也变为运行;如果当前进程状态为运行,则继续保持此状态;如果进程既不处于运行状态,也不是可中断的,则从运行队列中移去该进程,这表明调度程序在选择最值得运行的进程时,该进程不被考虑。
 
 3. 调度程序在运行队列中搜索最值得运行的程序。调度程序通过比较权重来选择进程。对实时进程而言,它的权重为 counter 加 1000;对一般进程而言,权重为 counter。因此,实时进程总是会被认为是最值得运行的进程。如果当前进程的优先级和其他可运行进程一致,则因为当前进程至少已花费了一个时间片,因此,总处于劣势。如果许多进程的优先级一样,则调度程序选择运行队列中最靠前的进程,这实际就是“循环赛”调度。
 
 4. 如果最值得运行的进程不是当前进程,就需要交换进程(或切换进程)。进程交换的作用是保存当前进程的运行上下文,同时恢复新进程的运行上下文。交换的具体细节和CPU 类型有关,但需要注意的是,交换进程时调度程序运行在当前进程的上下文中,另外,调度程序还需要设置某些关键的 CPU 寄存器并刷新硬件高速缓存。
 
 Linux 内核已具备在对称多处理系统(SMP)上运行的能力。在多处理器系统中,每个处理器都在忙碌地运行着进程。当运行在某个处理器上的进程耗尽其时间片,或者该进程处于等待状态时,该处理器将单独运行调度程序来选择新的进程。需要注意的是,每个处理器有一个自己的空闲进程,而每个处理器也有自己的当前进程。为了跟踪每个处理器的空闲进程和当前进程,进程的 task_struct 中包含了正在运行该进程的处理器编号(processor 字段),以及上次运行该进程的处理器编号(last_processor 字段)。显然,当一个进程再次运行时,可由不同的处理器运行,但在不同处理器上的进程交换所需开支稍微大一些,为此,每个进程有一个 processor_musk 字段,如果该字段的第 N 位为 1,则该进程可以运行在第 N 个进程上,利用这一字段,就可以将某个进程限制在单个处理器上运行。
 
 11.3 进程的创建
 
 系统启动时,启动程序运行在内核模式,这时,只有一个进程在系统中运行,即初始进程。系统初始化结束时,初始进程启动一个内核线程(即 init),而自己则处于空循环状态。当系统中没有可运行的进程时,调度程序将运行这一空闲进程。空闲进程的 task_struct 是唯一一个非动态分配的任务结构,该结构在内核编译时分配,称为 init_task。
 
 init 内核线程/进程的标识号为 1,它是系统的第一个真正进程。它负责初始的系统设置工作,例如打开控制台,挂装文件系统等。然后,init 进程执行系统的初始化程序,这一程序可能是 /etc/init、/bin/init 或 /sbin/init。init 程序将 /etc/inittab 当作脚本文件建立系统中新的进程,这些新的进程又可以建立新进程。例如,getty 进程可建立 login 进程来接受用户的登录请求。有关系统启动的详细内容可参见第十六章。
 
 新的进程通过克隆旧的程序(当前程序)而建立。fork 和 clone 系统调用可用来建立新的进程。这两个系统调用结束时,内核在系统的物理内存中为新的进程分配新的 task_struct 结构,同时为新进程要使用的堆栈分配物理页。Linux 还会为新的进程分配新的进程标识符。然后,新 task_struct 结构的地址保存在 task 数组中,而旧进程的 task_struct 结构内容被复制到新进程的 task_struct 结构中。
 
 在克隆进程时,Linux 允许两个进程共享相同的资源。可共享的资源包括文件、信号处理程序和虚拟内存等。当某个资源被共享时,该资源的引用计数值会增加 1,从而只有两个进程均终止时,内核才会释放这些资源。图 11-4 说明了父进程和子进程共享打开的文件。
 
  
 
 图 11-4 父进程和子进程共享打开的文件
 
 系统对进程虚拟内存的克隆过程则更加巧妙些。新的 vm_area_struct 结构、新进程自己的 mm_struct 结构以及新进程的页表必须在一开始就准备好,但这时并不复制任何虚拟内存。如果旧进程的某些虚拟内存在物理内存中,而有些在交换文件中,那么虚拟内存的复制将会非常困难和费时。实际上,Linux 采用了称为“写时复制”的技术,也就是说,只有当两个进程中的任意一个向虚拟内存中写入数据时才复制相应的虚拟内存;而没有写入的任何内存页均可以在两个进程之间共享。代码页实际总是可以共享的。
 
 为实现“写时复制”技术,Linux 将可写虚拟内存页的页表项标志为只读。当进程要向这种内存页写入数据时,处理器会发现内存访问控制上的问题(向只读页中写入),从而导致页故障。于是,操作系统可捕获这一被处理器认为是“非法的”写操作而完成内存页的复制。最后,Linux 还要修改两个进程的页表以及虚拟内存数据结构。
 
 11.4 执行程序
 
 和 UNIX 类似,Linux 中的程序和命令通常由命令解释器执行,这一命令解释器称为 shell。用户输入命令之后,shell 会在搜索路径(shell 变量PATH中包含搜索路径)指定的目录中搜索和输入命令匹配的映象名称。如果发现匹配的映象,shell 负责装载并执行该命令。shell 首先利用 fork 系统调用建立子进程,然后用找到的可执行映象文件覆盖子进程正在执行的 shell 二进制映象。
 
 可执行文件可以是具有不同格式的二进制文件,也可以是一个文本的脚本文件。可执行映象文件中包含了可执行代码及数据,同时也包含操作系统用来将映象正确装入内存并执行的信息。Linux 使用的最常见的可执行文件格式是 ELF 和 a.out,但理论上讲,Linux 有足够的灵活性可以装入任何格式的可执行文件。
 
 11.4.1 ELF
 
 ELF 是“可执行可连接格式”的英文缩写,该格式由 UNIX 系统实验室制定。它是 Linux 中最经常使用的格式,和其他格式(例如 a.out 或 ECOFF 格式)比较起来,ELF 在装入内存时多一些系统开支,但是更为灵活。ELF 可执行文件包含了可执行代码和数据,通常也称为正文和数据。这种文件中包含一些表,根据这些表中的信息,内核可组织进程的虚拟内存。另外,文件中还包含有对内存布局的定义以及起始执行的指令位置。
 
 我们分析如下简单程序在利用编译器编译并连接之后的 ELF 文件格式:
 
 #include <stdio.h>
 
 main ()
 
 {
 
 printf(“Hello world!/n”);
 
 }
 
 如图 11-5 所示,是上述源代码在编译连接后的 ELF 可执行文件的格式。从图 11-5 可以看出,ELF 可执行映象文件的开头是三个字符 ‘E’、‘L’ 和 ‘F’,作为这类文件的标识符。e_entry 定义了程序装入之后起始执行指令的虚拟地址。这个简单的 ELF 映象利用两个“物理头”结构分别定义代码和数据,e_phnum 是该文件中所包含的物理头信息个数,本例为 2。e_phyoff 是第一个物理头结构在文件中的偏移量,而e_phentsize 则是物理头结构的大小,这两个偏移量均从文件头开始算起。根据上述两个信息,内核可正确读取两个物理头结构中的信息。
 
 物理头结构的 p_flags 字段定义了对应代码或数据的访问属性。图中第一个 p_flags 字段的值为 FP_X 和 FP_R,表明该结构定义的是程序的代码;类似地,第二个物理头定义程序数据,并且是可读可写的。p_offset 定义对应的代码或数据在物理头之后的偏移量。p_vaddr 定义代码或数据的起始虚拟地址。p_filesz 和 p_memsz 分别定义代码或数据在文件中的大小以及在内存中的大小。对我们的简单例子,程序代码开始于两个物理头之后,而程序数据则开始于物理头之后的第 0x68533 字节处,显然,程序数据紧跟在程序代码之后。程序的代码大小为 0x68532,显得比较大,这是因为连接程序将 C 函数 printf 的代码连接到了 ELF 文件的原因。程序代码的文件大小和内存大小是一样的,而程序数据的文件大小和内存大小不一样,这是因为内存数据中,起始的 2200 字节是预先初始化的数据,初始化值来自 ELF 映象,而其后的 2048 字节则由执行代码初始化。
 
  
 
 图 11-5 一个简单的 ELF 可执行文件的布局
 
 如上一章中所描述的,Linux 利用需求分页技术装入程序映象。当 shell 进程利用 fork 系统调用建立了子进程之后,子进程会调用 exec 系统调用(实际有多种 exec 调用),exec 系统调用将利用 ELF 二进制格式装载器装载 ELF 映象,当装载器检验映象是有效的 ELF 文件之后,就会将当前进程(实际就是父进程或旧进程)的可执行映象从虚拟内存中清除,同时清除任何信号处理程序并关闭所有打开的文件(把相应 file 结构中的 f_count 引用计数减 1,如果这一计数为 0,内核负责释放这一文件对象),然后重置进程页表。完成上述过程之后,只需根据 ELF 文件中的信息将映象代码和数据的起始和终止地址分配并设置相应的虚拟地址区域,修改进程页表。这时,当前进程就可以开始执行对应的 ELF 映象中的指令了。
 
 和静态连接库不同,动态连接库可在运行时连接到进程虚拟地址中。对于使用同一动态连接库的多个进程,只需在内存中保留一份共享库信息即可,这样就节省了内存空间。当共享库需要在运行时连接到进程虚拟地址时,Linux 的动态连接器利用 ELF 共享库中的符号表完成连接工作,符号表中定义了 ELF 映象引用的全部动态库例程。Linux 的动态连接器一般包含在 /lib 目录中,通常为 ld.so.1、llibc.so.1 和ld-linux.so.1。
 
 11.4.2 脚本文件
 
 脚本文件实际是一些可执行的命令,这些命令一般由指定的解释器解释并执行。Linux 中常见的解释器有 wish、perl 以及命令 shell,如 bash 等。
 
 一般来说,脚本文件的第一行用来指定脚本的解释程序,例如:
 
 #!/usr/bin/wish
 
 这行内容指定由 wish 作为该脚本的命令解释器。脚本的二进制装载器利用这一信息搜索解释器,如果能够找到指定的解释器,该装载器就和上述执行 ELF 程序的装载过程一样装载并执行解释器。脚本文件名成为传递给解释器的第一个命令参数,而最初的第一个参数则成为现在的第二个参数,依此类推。为解释器传递了正确的命令参数后,就可由脚本解释器执行脚本。
 
 11.5 信号
 
 信号是 UNIX 系统中最古老的进程间通讯机制之一,它主要用来向进程发送异步的事件信号。键盘中断可能产生信号,而浮点运算溢出或者内存访问错误等也可产生信号。shell 通常利用信号向子进程发送作业控制命令。
 
 在 Linux 中,信号种类的数目和具体的平台有关,因为内核用一个字代表所有的信号,因此字的位数就是信号种类的最多数目。对 32 位的 i386 平台而言,一个字为 32 位,因此信号有 32 种,而对 64 位的 Alpha AXP 平台而言,每个字为 64 位,因此信号最多可有 64 种。Linux 内核定义的最常见的信号、C 语言宏名及其用途如表 11-5 所示:
 
 表 11-5 常见信号及其用途
 
 值
 
 C 语言宏名
 
 用途
 
  
 
 1
 
 SIGHUP
 
 从终端上发出的结束信号
 
  
 
 2
 
 SIGINT
 
 来自键盘的中断信号(Ctrl-C)
 
  
 
 3
 
 SIGQUIT
 
 来自键盘的退出信号(Ctrl-/)
 
  
 
 8
 
 SIGFPE
 
 浮点异常信号(例如浮点运算溢出)
 
  
 
 9
 
 SIGKILL
 
 该信号结束接收信号的进程
 
  
 
 14
 
 SIGALRM
 
 进程的定时器到期时,发送该信号
 
  
 
 15
 
 SIGTERM
 
 kill 命令发出的信号
 
  
 
 17
 
 SIGCHLD
 
 标识子进程停止或结束的信号
 
  
 
 19
 
 SIGSTOP
 
 来自键盘(Ctrl-Z)或调试程序的停止执行信号
 
  
 
 进程可以选择对某种信号所采取的特定操作,这些操作包括:
 
 忽略信号。进程可忽略产生的信号,但 SIGKILL 和 SIGSTOP 信号不能被忽略。 
 
 阻塞信号。进程可选择阻塞某些信号。 
 
 由进程处理该信号。进程本身可在系统中注册处理信号的处理程序地址,当发出该信号时,由注册的处理程序处理信号。 
 
 由内核进行默认处理。信号由内核的默认处理程序处理。大多数情况下,信号由内核处理。 
 
 需要注意的是,Linux 内核中不存在任何机制用来区分不同信号的优先级。也就是说,当同时有多个信号发出时,进程可能会以任意顺序接收到信号并进行处理。另外,如果进程在处理某个信号之前,又有相同的信号发出,则进程只能接收到一个信号。产生上述现象的原因与内核对信号的实现有关,将在下面解释。
 
 系统在 task_struct 结构中利用两个字分别记录当前挂起的信号(signal)以及当前阻塞的信号(blocked)。挂起的信号指尚未进行处理的信号。阻塞的信号指进程当前不处理的信号,如果产生了某个当前被阻塞的信号,则该信号会一直保持挂起,直到该信号不再被阻塞为止。除了 SIGKILL 和 SIGSTOP 信号外,所有的信号均可以被阻塞,信号的阻塞可通过系统调用实现。每个进程的 task_struct 结构中还包含了一个指向 sigaction 结构数组的指针,该结构数组中的信息实际指定了进程处理所有信号的方式。如果某个 sigaction 结构中包含有处理信号的例程地址,则由该处理例程处理该信号;反之,则根据结构中的一个标志或者由内核进行默认处理,或者只是忽略该信号。通过系统调用,进程可以修改 sigaction 结构数组的信息,从而指定进程处理信号的方式。
 
 进程不能向系统中所有的进程发送信号,一般而言,除系统和超级用户外,普通进程只能向具有相同 uid 和 gid 的进程,或者处于同一进程组的进程发送信号。产生信号时,内核将进程 task_struct 的 signal 字中的相应位设置为 1,从而表明产生了该信号。系统不对置位之前该位已经为 1的情况进行处理,因而进程无法接收到前一次信号。如果进程当前没有阻塞该信号,并且进程正处于可中断的等待状态,则内核将该进程的状态改变为运行,并放置在运行队列中。这样,调度程序在进行调度时,就有可能选择该进程运行,从而可以让进程处理该信号。
 
 发送给某个进程的信号并不会立即得到处理,相反,只有该进程再次运行时,才有机会处理该信号。每次进程从系统调用中退出时,内核会检查它的 signal 和 block 字段,如果有任何一个未被阻塞的信号发出,内核就根据 sigaction 结构数组中的信息进行处理。处理过程如下:
 
 1. 检查对应的 sigaction 结构,如果该信号不是 SIGKILL 或 SIGSTOP 信号,且被忽略,则不处理该信号。
 
 2. 如果该信号利用默认的处理程序处理,则由内核处理该信号,否则转向第 3 步。
 
 3. 该信号由进程自己的处理程序处理,内核将修改当前进程的调用堆栈帧,并将进程的程序计数寄存器修改为信号处理程序的入口地址。此后,指令将跳转到信号处理程序,当从信号处理程序中返回时,实际就返回了进程的用户模式部分。
 
 Linux 是 POSIX 兼容的,因此,进程在处理某个信号时,还可以修改进程的 blocked 掩码。但是,当信号处理程序返回时,blocked 值必须恢复为原有的掩码值,这一任务由内核完成。Linux 在进程的调用堆栈帧中添加了对清理程序的调用,该清理程序可以恢复原有的 blocked 掩码值。当内核在处理信号时,可能同时有多个信号需要由用户处理程序处理,这时,Linux 内核可以将所有的信号处理程序地址推入堆栈帧,而当所有的信号处理完毕后,调用清理程序恢复原先的 blocked 值。
 
 11.6 管道
 
 管道是 Linux 中最常用的 IPC 机制。利用管道时,一个进程的输出可成为另外一个进程的输入。当输入输出的数据量特别大时,这种 IPC 机制非常有用。可以想象,如果没有管道机制,而必须利用文件传递大量数据时,会造成许多空间和时间上的浪费。
 
 在 Linux 中,通过将两个 file 结构指向同一个临时的 VFS 索引节点,而两个 VFS 索引节点又指向同一个物理页而实现管道。如图 11-6 所示。
 
  
 
 图 11-6 管道示意图
 
 图 11-6 中,每个 file 数据结构定义不同的文件操作例程地址,其中一个用来向管道中写入数据,而另外一个用来从管道中读出数据。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。管道写函数通过将字节复制到 VFS 索引节点指向的物理内存而写入数据,而管道读函数则通过复制物理内存中的字节而读出数据。当然,内核必须利用一定的机制同步对管道的访问,为此,内核使用了锁、等待队列和信号。
 
 当写进程向管道中写入时,它利用标准的库函数,系统根据库函数传递的文件描述符,可找到该文件的 file 结构。file 结构中指定了用来进行写操作的函数(即写入函数)地址,于是,内核调用该函数完成写操作。写入函数在向内存中写入数据之前,必须首先检查 VFS 索引节点中的信息,同时满足如下条件时,才能进行实际的内存复制工作:
 
 内存中有足够的空间可容纳所有要写入的数据; 
 
 内存没有被读程序锁定。 
 
 如果同时满足上述条件,写入函数首先锁定内存,然后从写进程的地址空间中复制数据到内存。否则,写入进程就休眠在 VFS 索引节点的等待队列中,接下来,内核将调用调度程序,而调度程序会选择其他进程运行。写入进程实际处于可中断的等待状态,当内存中有足够的空间可以容纳写入数据,或内存被解锁时,读取进程会唤醒写入进程,这时,写入进程将接收到信号。当数据写入内存之后,内存被解锁,而所有休眠在索引节点的读取进程会被唤醒。
 
 管道的读取过程和写入过程类似。但是,进程可以在没有数据或内存被锁定时立即返回错误信息,而不是阻塞该进程,这依赖于文件或管道的打开模式。反之,进程可以休眠在索引节点的等待队列中等待写入进程写入数据。当所有的进程完成了管道操作之后,管道的索引节点被丢弃,而共享数据页也被释放。
 
 Linux 还支持另外一种管道形式,称为命名管道,或 FIFO,这是因为这种管道的操作方式基于“先进先出”原理。上面讲述的管道类型也被称为“匿名管道”。命名管道中,首先写入管道的数据是首先被读出的数据。匿名管道是临时对象,而 FIFO 则是文件系统的真正实体,用 mkfifo 命令可建立管道。如果进程有足够的权限就可以使用 FIFO。FIFO 和匿名管道的数据结构以及操作极其类似,二者的主要区别在于,FIFO 在使用之前就已经存在,用户可打开或关闭 FIFO;而匿名管道在只在操作时存在,因而是临时对象。
 
 11.7 System V 的 IPC 机制
 
 为了和其他系统保持兼容,Linux 也提供三种首先出现在 UNIX System V 中的 IPC 机制。这三种机制分别是:消息队列、信号量以及共享内存。System V IPC 机制主要有如下特点:
 
 如果进程要访问 System V IPC 对象,则需要在系统调用中传递唯一的引用标识符。 
 
 对 System V IPC 对象的访问,必须经过类似文件访问的许可检验。对这些对象访问权限的设置由对象的创建者利用系统调用设置。 
 
 对象的引用标识符由 IPC 机制作为访问对象表的索引,但需要一些操作来生成索引。 
 
 在 Linux 中,所有表示System V IPC 对象的数据结构中都包含一个 ipc_perm 结构,该结构中包含了作为对象所有者和创建者的进程之用户标识符和组标识符,以及对象的访问模式和对象的访问键。访问键用来定位System V IPC 对象的引用标识符。系统支持两种访问键:公有和私有。如果键是公有的,则系统中所有的进程通过权限检查后,均可以找到System V IPC 对象的引用标识符。但是,只能通过引用标识符引用 System V IPC 对象。
 
 Linux 对这些 IPC 机制的实施大同小异,我们在这里只主要介绍其中两种:消息队列和信号量。
 
 11.7.1 消息队列
 
 一个或多个进程可向消息队列写入消息,而一个或多个进程可从消息队列中读取消息,这种进程间通讯机制通常使用在客户/服务器模型中,客户向服务器发送请求消息,服务器读取消息并执行相应请求。在许多微内核结构的操作系统中,内核和各组件之间的基本通讯方式就是消息队列。例如,在 MINIX 操作系统中,内核、I/O 任务、服务器进程和用户进程之间就是通过消息队列实现通讯的。
 
 Linux 为系统中所有的消息队列维护一个 msgque 链表,该链表中的每个指针指向一个 msgid_ds 结构,该结构完整描述一个消息队列。当建立一个消息队列时,系统从内存中分配一个 msgid_ds 结构并将指针添加到 msgque 链表。
 
 图 11-7 是 msgid_ds 结构的示意图。从图中可以看出,每个 msgid_ds 结构都包含一个 ipc_perm 结构以及指向该队列所包含的消息指针,显然,队列中的消息构成了一个链表。另外,Linux 还在 msgid_ds 结构中包含一些有关修改时间之类的信息,同时包含两个等待队列,分别用于队列的写入进程和队列的读取进程。
 
  
 
 图 11-7 System V IPC 机制——消息队列
 
 消息队列的写入操作和读取操作是类似的,以消息的写入为例,步骤如下:
 
 1. 当某个进程要写入消息时,该进程的有效 uid 和 gid 首先要和 ipc_perm 中的访问模式进行比较。如果进程不能写入,系统调用返回错误,写操作结束。
 
 2. 如果该进程可以向消息队列写入,则消息可以复制到消息队列的末尾。在进行复制之前,必须判断消息队列当前是否已满。消息的具体内容和应用程序有关,由参与通讯的进程约定。
 
 3. 如果消息队列中当前没有空间容纳消息,则写入进程被添加到该消息队列的写等待队列,否则,内核分配一个 msg 结构,将消息从进程的地址空间中复制到 msg 结构,然后将 msg 添加到队列末尾,这时,系统调用成功返回,写操作结束。
 
 4. 调用调度程序,调度程序选择其他进程运行,写操作结束。
 
 如果有某个进程从消息队列中读取了消息,则系统会唤醒写等待队列中的进程。
 
 读取操作和写入操作类似,但进程在没有消息或没有指定类型的消息时进入等待状态。
 
 11.7.2 信号量
 
 信号量的概念由 E. W. Dijkstra 于 1965 年首次提出。信号量实际是一个整数,进程在信号量上的操作分两种,一种称为 DOWN,而另外一种称为 UP。DOWN 操作的结果是让信号量的值减 1,UP 操作的结果是让信号量的值加 1。在进行实际的操作之前,进程首先检查信号量的当前值,如果当前值大于 0,则可以执行 DOWN 操作,否则进程休眠,等待其他进程在该信号量上的 UP 操作,因为其他进程的 UP 操作将让信号量的值增加,从而它的 DOWN 操作可以成功完成。某信号灯在经过某个进程的成功操作之后,其他休眠在该信号量上的进程就有可能成功完成自己的操作,这时,系统负责检查休眠进程是否可以完成自己的操作。
 
 为了理解信号量,我们想象某机票定购系统。最初旅客在定票时,一般有足够的票数可以满足定票量。当剩余的机票数为 1,而某个旅客现在需要定两张票时,就无法满足该顾客的需求,这时售票小姐让这个旅客留下他的电话号码,如果其他人退票,就可以优先让这个旅客定票。如果最终有人退票,则售票小姐打电话通知上述要定两张票的旅客,这时,该旅客就能够定到自己的票。
 
 我们可以将旅客看成是进程,而定票可看成是信号量上的 DOWN 操作,退票可看成是信号量上的 UP 操作,而信号量的初始值为机票总数,售票小姐则相当于操作系统的信号量管理器,由她(操作系统)决定旅客(进程)能不能完成操作,并且在新的条件成熟时,负责通知(唤醒)登记的(休眠的)旅客(进程)。
 
 在操作系统中,信号量的最简单形式是一个整数,多个进程可检查并设置信号量的值。这种检查并设置操作是不可被中断的,也称为“原子”操作。检查并设置操作的结果是信号量的当前值和设置值相加的结果,该设置值可以是正值,也可以是负值。根据检查和设置操作的结果,进行操作的进程可能会进入休眠状态,而当其他进程完成自己的检查并设置操作后,由系统检查前一个休眠进程是否可以在新信号量值的条件下完成相应的检查和设置操作。这样,通过信号量,就可以协调多个进程的操作。
 
 信号量可用来实现所谓的“关键段”。关键段指同一时刻只能有一个进程执行其中代码的代码段。也可用信号量解决经典的“生产者——消费者”问题,“生产者——消费者”问题和上述的定票问题类似。这一问题可以描述如下:
 
 两个进程共享一个公共的、固定大小的缓冲区。其中的一个进程,即生产者,向缓冲区放入信息,另外一个进程,即消费者,从缓冲区中取走信息(该问题也可以一般化为 m 个生产者和 n 个消费者)。当生产者向缓冲区放入信息时,如果缓冲区是满的,则生产者进入休眠,而当消费者从缓冲区中拿走信息后,可唤醒生产者;当消费者从缓冲区中取信息时,如果缓冲区为空,则消费者进入休眠,而当生产者向缓冲区写入信息后,可唤醒消费者。
 
 Linux 利用 semid_ds 结构来表示 System V IPC 信号量,见图 11-8。和消息队列类似,系统中所有的信号量组成了一个 semary 链表,该链表的每个节点指向一个 semid_ds 结构。从图 11-8 可以看出,semid_ds 结构的 sem_base 指向一个信号量数组,允许操作这些信号量数组的进程可以利用系统调用执行操作。系统调用可指定多个操作,每个操作由三个参数指定:信号量索引、操作值和操作标志。信号量索引用来定位信号量数组中的信号量;操作值是要和信号量的当前值相加的数值。首先,Linux 按如下的规则判断是否所有的操作都可以成功:操作值和信号量的当前值相加大于 0,或操作值和当前值均为 0,则操作成功。如果系统调用中指定的所有操作中有一个操作不能成功时,则 Linux 会挂起这一进程。但是,如果操作标志指定这种情况下不能挂起进程的话,系统调用返回并指明信号量上的操作没有成功,而进程可以继续执行。如果进程被挂起,Linux 必须保存信号量的操作状态并将当前进程放入等待队列。为此,Linux 在堆栈中建立一个 sem_queue 结构并填充该结构。新的 sem_queue 结构添加到信号量对象的等待队列中(利用 sem_pending 和 sem_pending_last 指针)。当前进程放入 sem_queue 结构的等待队列中(sleeper)后调用调度程序选择其他的进程运行。
 
  
 
 图 11-8 System V IPC 机制——信号量
 
 如果所有的信号量操作都成功了,当前进程可继续运行。在此之前,Linux 负责将操作实际应用于信号量队列的相应元素。这时,Linux 检查任何等待的或挂起的进程,看它们的信号量操作是否可以成功。如果这些进程的信号量操作可以成功,Linux 就会将它们从挂起队列中移去,并将它们的操作实际应用于信号量队列。同时,Linux 会唤醒休眠进程,以便可在下次调度程序运行时可以运行这些进程。当新的信号量操作应用于信号量队列之后,Linux 会接着检查挂起队列,直到没有操作可成功,或没有挂起进程为止。
 
 和信号量操作相关的概念还有“死锁”。当某个进程修改了信号量而进入关键段之后,却因为崩溃而没有退出关键段,这时,其他被挂起在信号量上的进程永远得不到运行机会,这就是所谓的死锁。Linux 通过维护一个信号量数组的调整链表来避免这一问题。
 
 11.7.3 共享内存
 
 在第十章中看到,进程的虚拟地址可以映射到任意一处物理地址,这样,如果两个进程的虚拟地址映射到同一物理地址,这两个进程就可以利用这一虚拟地址进行通讯。但是,一旦内存被共享之后,对共享内存的访问同步需要由其他 IPC 机制,例如信号量来实现。Linux 中的共享内存通过访问键来访问,并进行访问权限的检查。共享内存对象的创建者负责控制访问权限以及访问键的公有或私有特性。如果具有足够的权限,也可以将共享内存锁定到物理内存中。
 
 图 11-9 是 Linux 中共享内存对象的结构。和消息队列及信号量类似,Linux 中也有一个链表维护着所有的共享内存对象。
 
  
 
 图 11-9 System V IPC 机制——共享内存
 
 参照图 11-9, 共享内存对象的结构元素说明如下:
 
 shm_segsz:共享内存的大小; 
 
 times:使用共享内存的进程数目; 
 
 attaches:描述被共享的物理内存映射到各进程的虚拟内存区域。 
 
 shm_npages:共享虚拟内存页的数目; 
 
 shm_pages:指向共享虚拟内存页的页表项表。 
 
 在利用共享内存时,参与通讯的进程通过系统调用将自己要共享的虚拟地址区域附加到 attaches 指向的链表中。
 
 某个进程第一次访问共享虚拟内存时将产生页故障。这时,Linux 找出描述该内存的 vm_area_struct 结构,该结构中包含用来处理这种共享虚拟内存的处理函数地址。共享内存页故障处理代码在 shmid_ds 的页表项链表中查找,以便查看是否存在该共享虚拟内存的页表项。如果没有,系统将分配一个物理页并建立页表项。该页表项加入 shmid_ds 结构的同时也添加到进程的页表中。此后,当另一个进程访问该共享内存时,共享内存页故障处理代码将使用同一物理页,而只是将页表项添加到这一进程的页表中。这样,前后两个进程就可以通过同一物理页进行通讯。
 
 当某个进程不再共享其虚拟内存时,利用系统调用将自己的虚拟地址区域从该链表中移去,并更新进程页表。当最后一个进程释放了自己的虚拟地址空间之后,系统释放所分配的物理页。
 
 当共享的虚拟内存没有被锁定到物理内存时,共享内存也可能会被交换到交换空间中。
 
 11.8 套接字
 
 套接字和上述的 IPC 机制有所不同,它能够实现不同计算机之间的进程间通讯,关于套接字的讨论在第十四章中进行。
 
 11.9 相关系统工具及系统调用
 
 11.9.1 系统工具
 
 Linux 中主要有三个命令可以查看当前系统中运行的进程。ps 命令可报告进程状态;pstree 可打印进程之间的父子关系;top 则可用来监视系统中 CPU 利用率最高的进程,也可以交互式地操作进程。
 
 kill 命令则用来向指定进程发送信号,如果没有指定要发送的信号,则发送 SIGTERM 信号,该信号的默认处理是终止进程的运行。如果查看系统所支持的所有信号编号,可用 kill –l 命令获取信号清单。
 
 mkfifo 可用来建立命名管道(FIFO)。
 
 11.9.2 系统调用
 
 表 11-7 简要列出了和进程及进程间通讯相关的系统调用。标志列中各字母的意义可参见 表 10-1 的说明。
 
 表 11-7 相关系统调用
 
 系统调用
 
 说明
 
 标志
 
  
 
 alarm
 
 在指定时间之后发送 SIGALRM 信号
 
 m+c
 
  
 
 clone
 
 创建子进程
 
 m-
 
  
 
 execl, execlp, execle, ...
 
 执行映象
 
 m+!c
 
  
 
 execve
 
 执行映象
 
 m+c
 
  
 
 exit
 
 终止进程
 
 m+c
 
  
 
 fork
 
 创建子进程
 
 m+c
 
  
 
 fsync
 
 将文件高速缓存写入磁盘
 
 mc
 
  
 
 ftime
 
 获取自1970.1.1以来的时区+秒数
 
 m!c
 
  
 
 getegid
 
 获取有效组标识符
 
 m+c
 
  
 
 geteuid
 
 获取有效用户标识符
 
 m+c
 
  
 
 getgid
 
 获取实际组标识符
 
 m+c
 
  
 
 getitimter
 
 获取间隔定时器的值
 
 mc
 
  
 
 getpgid
 
 获取某进程之父进程的组标识符
 
 +c
 
  
 
 getpgrp
 
 获取当前进程之父进程的组标识符
 
 m+c
 
  
 
 getpid
 
 获取当前进程的进程标识符
 
 m+c
 
  
 
 getppid
 
 获取父进程的进程标识符
 
 m+c
 
  
 
 getpriority
 
 获取进程/组/用户的优先级
 
 mc
 
  
 
 gettimeofday
 
 获取自1970.1.1以来的时区+秒数
 
 mc
 
  
 
 getuid
 
 获取实际用户标识符
 
 m+c
 
  
 
 ipc
 
 进程间通讯
 
 -c
 
  
 
 kill
 
 向进程发送信号
 
 m+c
 
  
 
 killpg
 
 向进程组发送信号
 
 m!c
 
  
 
 modify_ldt
 
 读取或写入局部描述符表
 
 -
 
  
 
 msgctl
 
 消息队列控制
 
 m!c
 
  
 
 msgget
 
 获取消息队列标识符
 
 m!c
 
  
 
 msgrcv
 
 接收消息
 
 m!c
 
  
 
 msgsnd
 
 发送消息
 
 m!c
 
  
 
 nice
 
 修改进程优先级
 
 mc
 
  
 
 pause
 
 进程进入休眠,等待信号
 
 m+c
 
  
 
 pipe
 
 创建管道
 
 m+c
 
  
 
 semctl
 
 信号量控制
 
 m!c
 
  
 
 semget
 
 获取某信号量数组的标识符
 
 m!c
 
  
 
 semop
 
 在信号量数组成员上的操作
 
 m!c
 
  
 
 setgid
 
 设置实际组标识符
 
 m+c
 
  
 
 setitimer
 
 设置间隔定时器
 
 mc
 
  
 
 setpgid
 
 设置进程组标识符
 
 m+c
 
  
 
 setpgrp
 
 以调用进程作为领头进程创建新的进程组
 
 m+c
 
  
 
 setpriority
 
 设置进程/组/用户优先级
 
 mc
 
  
 
 setsid
 
 建立一个新会话
 
 m+c
 
  
 
 setregid
 
 设置实际和有效组标识符
 
 mc
 
  
 
 setreuid
 
 设置实际和有效用户标识符
 
 mc
 
  
 
 settimeofday
 
 设置自1970.1.1以来的时区+秒数
 
 mc
 
  
 
 setuid
 
 设置实际用户标识符
 
 m+c
 
  
 
 shmat
 
 附加共享内存
 
 m!c
 
  
 
 shmctl
 
 共享内存控制
 
 m!c
 
  
 
 shmdt
 
 移去共享内存
 
 m!c
 
  
 
 shmget
 
 获取/建立共享内存
 
 m!c
 
  
 
 sigaction
 
 设置/获取信号处理器
 
 m+c
 
  
 
 sigblock
 
 阻塞信号
 
 m!c
 
  
 
 siggetmask
 
 获取当前进程的信号阻塞掩码
 
 !c
 
  
 
 signal
 
 设置信号处理器
 
 mc
 
  
 
 sigpause
 
 在处理下次信号之前,使用新的信号阻塞掩码
 
 mc
 
  
 
 sigpending
 
 获取挂起且阻塞的信号
 
 m+c
 
  
 
 sigprocmask
 
 设置/获取当前进程的信号阻塞掩码
 
 +c
 
  
 
 sigsetmask
 
 设置当前进程的信号阻塞掩码
 
 c!
 
  
 
 sigsuspend
 
 替换 sigpause
 
 m+c
 
  
 
 sigvec
 
 见 sigaction
 
 m
 
  
 
 ssetmask
 
 见 sigsetmask
 
 m
 
  
 
 system
 
 执行 shell 命令
 
 m!c
 
  
 
 time
 
 获取自 1970.1.1 以来的秒数
 
 m+c
 
  
 
 times
 
 获取进程的 CPU 时间
 
 m+c
 
  
 
 vfork
 
 见 fork
 
 m!c
 
  
 
 wait
 
 等待进程终止
 
 m+c
 
  
 
 wait3, wait4
 
 等待指定进程终止 (BSD)
 
 mc
 
  
 
 waitpid
 
 等待指定进程终止
 
 m+c
 
  
 
 vm86
 
 进入虚拟 8086 模式
 
 m-c
 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值