一、进程管理相关的系统调用
讨论 fork 和 exec 函数族(用来用指定的程序替换当前进程的所有内容)系统调用的实现。通常这些调用不是由应用程序直接发出
的,而是通过一个中间层调用,即负责与内核通信的C标准库。
从用户状态切换到核心态的方法,依不同的体系结构而各有不同。用于在这两种状态之间切换的机制,并解释了用户空间和内核空间之间如何交换参数。就目前而言,将内核视为由C标准库使用的“程序库”即可。
1、进程复制
传统UNIX中用于复制进程的系统调用是fork。但fork并不是Linux为此实现的唯一调用,实际上Linux实现了3个。
(1) fork 是重量级调用,因为fork建立了父进程的一个完整副本,然后作为子进程执行。为减少与该调用相关的工作量,Linux使用写时复制(copy-on-write)技术。
(2) vfork 类似于 fork ,但vfork 并不创建父进程数据的副本。相反,父子进程之间共享数据。这节省了大量CPU时间(如果一个进程操纵共享数据,则另一个会自动注意到)。
vfork 设计用于子进程形成后立即执行 exec 系统调用加载新程序的情形。在子进程退出或开始新程序之前,内核保证父进程处于堵塞状态。由于 fork 使用了写时复制技术, vfork 在速度方面不再有优势,因此应该避免使用 vfork 。
(3) clone 产生线程,可以对父子进程之间的共享、复制进行精确控制。
2、写时复制
内核使用了写时复制(Copy-On-Write,COW)技术,以防止在 fork 执行时将父进程的所有数据复制到子进程。该技术利用了下述事实:进程通常只使用了其内存页的一小部分。在调用 fork 时,内核通常对父进程的每个内存页,都为子进程创建一个相同的副本。这有两种很不好的负面效应。
(1) 使用了大量内存。
(2) 复制操作耗费很长时间。
如果应用程序在进程复制之后使用 exec 立即加载新程序,那么负面效应会更严重。这实际上意味着,此前进行的复制操作是完全多余的,因为进程地址空间会重新初始化,复制的数据不再需要了。
内核可以使用技巧规避该问题。并不复制进程的整个地址空间,而是只复制其页表。建立了虚拟地址空间和物理内存页之间的联系。因此, fork 之后父子进程的地址空间指向同样的物理内存页。
父子进程不能允许修改彼此的页,这也是两个进程的页表对页标记了只读访问的原因,即使在普通环境下允许写入也是如此。
假如两个进程只能读取其内存页,那么二者之间的数据共享就不是问题,因为不会有修改。
只要一个进程试图向复制的内存页写入,处理器会向内核报告访问错误(此类错误被称作缺页异常)。内核然后查看额外的内存管理数据结构,检查该页是否可以用读写模式访问,还是只能以只读模式访问。如果是后者,则必须向进程报告段错误。缺页异常处理程序的实际实现要复杂得多,因为还必须考虑其他方面的问题,例如换出的页。
如果页表项将一页标记为“只读”,但通常情况下该页应该是可写的,内核可根据此条件来判断该页实际上是COW页。因此内核会创建该页专用于当前进程的副本,也可以用于写操作。
写时复制(COW)机制使得内核可以尽可能延迟内存页的复制,更重要的是,在很多情况下不需要复制。这节省了大量时间。
3、执行系统调用
fork 、 vfork 和 clone 系统调用的入口点分别是 sys_fork 、 sys_vfork 和 sys_clone 函数。其定义依赖于具体的体系结构,因为在用户空间和内核空间之间传递参数的方法因体系结构而异。上述函数的任务是从处理器寄存器中提取由用户空间提供的信息,调用体系结构
无关的 do_fork 函数,后者负责进程复制。该函数的原型如下:
kernel/fork.c
long do_fork(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs,