我们知道了一个进程如何采用请求调页,仅调入包括第一条指令的页面,从而能够很 快开始执行。然而,通过系统调用 fork() 的进程创建最初可以通过使用类似于页面共享的技术,绕过请求调页的需要。这种技术提供了快速的进程创建,并最小化必须分配给新创建进程的新页面的数量。
回想一下,系统调用 fork() 创建了父进程的一个复制,以作为子进程。传统上,fork() 为子进程创建一个父进程地址空间的副本,复制属于父进程的页面。然而,考虑到许多子进程在创建之后立即调用系统调用 exec(),父进程地址空间的复制可能没有必要。
因此,可以采用一种称为写时复制的技术,它通过允许父进程和子进程最初共享相同的页面来工作。这些共享页面标记为写时复制,这意味着如果任何一个进程写入共享页面,那么就创建共享页面的副本。
进程1修改页面C前后
图 1 进程 1 修改页面 C 前后
写时复制如图 1 所示,图中分别反映了修改页面 C 的前与后。
例如,假设子进程试图修改包含部分堆栈的页面,并且设置为写时复制。操作系统会创建这个页面的副本,将其映射到子进程的地址空间。然后,子进程会修改复制的页面,而不是属于父进程的页面。显然,当使用写时复制技术时,仅复制任何一进程修改的页面,所有未修改的页面可以由父进程和子进程共享。
还要注意,只有可以修改的页面才需要标记为写时复制。不能修改的页面(包含可执行代码的页面)可以由父进程和子进程共享。写时复制是一种常用技术,为许多操作系统所采用,包括Windows XP、Linux 和 Solaris。
当确定采用写时复制来复制页面时,重要的是注意空闲页面的分配位置。许多操作系统为这类请求提供了一个空闲的页面池。当进程的堆栈或堆要扩展时或有写时复制页面需要管理时,通常分配这些空闲页面。操作系统分配这些页面通常采用称为按需填零的技术。按需填零页面在需要分配之前先填零,因此清除了以前的内容。
UNIX 的多个版本(包括 Solaris 和 Linux)提供了系统调用 fork() 的变种,即 vfork()(虚拟内存fork(virtual memory fork)),vfork() 的操作不同于写时复制的fork()。
采用 vfork(),父进程被挂起,子进程使用父进程的地址空间。因为 vfork() 不采用写时复制,如果子进程修改父地址空间的任何页面,那么这些修改过的页面对于恢复的父进程是可见的。因此,应谨慎使用 vfork(),以确保子进程不会修改父进程的地址空间。当子进程在创建后立即调用 exec() 时,可使用 vfork()。因为没有复制页面,vfork() 是一个非常有效的进程创建方法,有时用于实现 UNIX 命令外壳接口。