Fork (系统调用)
在计算机领域中,尤其是Unix及类Unix系统操作系统中,fork(进程复制)是一种创建自身行程副本的操作。它通常是内核实现的一种系统调用。Fork是类Unix操作系统上创建进程的一种主要方法,甚至历史上是唯一方法。 概述在多任务操作系统中,行程(运行的程序)需要一种方法来创建新进程,例如运行其他程序。Fork及其变种在类Unix系统中通常是这样做的唯一方式。如果进程需要启动另一个程序的可执行文件,它需要先Fork来创建一个自身的副本。然后由该副本即“子进程”调用exec系统调用,用其他程序覆盖自身:停止执行自己之前的程序并执行其他程序。 Fork操作会为子进程创建一个单独的定址空間。子进程拥有父进程所有内存段的精确副本。在现代的UNIX变种中,这遵循出自SunOS-4.0的虚拟内存模型,根据寫入時複製语义,物理内存不需要被实际复制。取而代之的是,两个进程的虚拟内存页面可能指向物理内存中的同一个页,直到它们写入该页时,写入才会发生。在用fork配合exec来执行新程序的情况下,此优化很重要。通常来说,子进程在停止程序运行前会执行一小组有利于其他程序的操作,它可能用到少量的其父进程的数据结构。 当一个进程调用fork时,它被认为是父进程,新创建的进程是它的孩子(子进程)。在fork之后,两个进程还运行着相同的程序,都像是调用了该系统调用一般恢复执行。然后它们可以检查调用的返回值确定其状态:是父进程还是子进程,以及据此行事。 fork系统调用在第一个版本的Unix就已存在[1],它借用于更早的GENIE 分時系統。[2]Fork是标准化的POSIX的一部分。[3] 通信子进程从父进程的文件描述符副本开始。[3]对于进程间通信,父进程通常会创建一个或多个管道,在fork进程之后,进程关闭它们不需要的管道端。[4] 变种VforkVfork是与fork具有相同调用约定和很多相同语义的一个变种,但只能在有限的情况下使用它。它起源于Unix的3BSD版本[5][6][7],这是首个支持虚拟内存的Unix版本。它已按POSIX标准化,这使得vfork能具有与fork完全相同的行为。但这已在2004年的版本中被标为过时[8],并在后续版本中被posix_spawn()取代(其通常通过vfork实现)。 在发出一个vfork系统调用时,父进程被暂停,直至子进程完成执行或被新的可执行映像取代(通过系统调用之“exec”家族中的一项)。子进程借用父进程的MMU设置和内存页面,在父进程与子进程之间共享,不进行复制,尤其是没有寫入時複製语义;[8]因此,如果子进程在任何共享页面中进行修改,不会创建新的页面,并且修改的页面对父进程同样可见。因为没有页面复制(消耗额外的内存),此技术在纯复制环境中使用exec时较普通fork更优化。在POSIX中,除非是将立即调用exec家族(及其他几个操作)的函数,其他任何目的会导致未定义行为。[8]使用vfork时,子进程借用而非复制数据结构,所以vfork仍比使用写时复制语义的fork更快。 System V在System VR4被引入前不支持此系统函数,因为它的内存共享容易出错:
同样,Linux对vfork的手册页面强烈不鼓励它的使用:[5]
使用vfork的其他问题包括死锁 ,它可能发生在多线程程序中,由于与动态链接交互。[10] 作为vfork接口的替代品,POSIX引入了posix_spawn函数家族,它结合了fork和exec的动作。这些函数可以实现为fork的程序库例程,就像Linux那样[10],或者就像Solaris那样为了更好的性能实现为vfork [10][11]。不过,POSIX规范中注明它是“为内核操作设计”,尤其是用于运行在受限硬件和实时系统上的操作系统。[12] 虽然4.4BSD的实现中摆脱了vfork的实现,使vfork做到与fork相同的行为,但它在NetBSD操作系统中因性能原因而恢复。[6] 一些嵌入式操作系统(例如uClinux)只实现vfork,因为它们需要在由于缺乏内存管理单元(MMU)而不可能实现写时复制的设备上操作。 RforkPlan 9操作系统由Unix的设计者创造,包括fork,但也有一个名为“rfork”的变种,它允许父进程与子进程之间资源的细粒度共享,包括地址空间(除了调用栈段,那是每个进程独有的)、环境变量和文件系统命名空间;[13]这使它成为了创建进程和其中的线程的一个统一接口。[14] 在FreeBSD[15]和IRIX中采用了来自Plan 9的rfork,后者将其更名为“sproc”。[16] Clone“clone”(克隆)是Linux内核中的一个系统调用,它创建一个可以与其父共享“执行上下文”的子进程。类似FreeBSD的rfork和IRIX的sproc,Linux的clone受到了Plan 9的rfork启发,并可用于实现线程(尽管应用程序的程序员通常使用更高级的接口,例如pthreads,实现在clone的顶层)。因为导致太多开销,出自Plan 9和IRIX的“separate stacks”(单独堆栈)特性已被省略(据Linus Torvalds)。[16] 其他操作系统中的Fork在VMS操作系统(1977年)的原始设计中,新进程根据当前一些特定地址进行复制来创建被认为是有风险的。当前进程中的错误状态可能被复制给子进程。因此在这里使用了进程“产卵”(spawning)之隐喻:新进程的每个组件的内存布局都是重新创建的。spawn后来被微软的操作系统采用(1993年)。 VM/CMS(OpenExtensions)的POSIX兼容组件提供了一个非常有限的fork实现,其中的父进程在子进程执行时被暂停,并且子与父共享同一地址空间。[17]这本质上是一个名为fork的vfork。(注意,这只适用于CMS客户机操作系统,其他VM客户机操作系统如Linux提供标准的fork功能。) 应用程序范例下列Hello World程序的变种以C语言展示了fork系统调用的机理。该程序fork为两个进程,每个都基于fork系统调用的返回值决定它们执行什么功能。样板代码中的头文件等已被省略。 int main(void)
{
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
exit(EXIT_FAILURE);
}
else if (pid == 0) {
printf("Hello from the child process!\n");
_exit(EXIT_SUCCESS);
}
else {
int status;
(void)waitpid(pid, &status, 0);
}
return EXIT_SUCCESS;
}
下面是该程序的解析: pid_t pid = fork();
调用中的第一句是调用fork系统调用来分割执行为两个进程。fork的返回值被记录在类型为pid_t的变量中,其中是POSIX类型的进程标识符(PID)。 在计算机领域,尤其是Unix及类Unix系统操作系统中,fork是一种创建自身行程副本的操作。它通常是内核实现的一种系统调用。Fork是在类Unix操作系统上创建进程的一种主要方法,甚至历史上曾是唯一方法。 -1错误表示fork出错:没有新进程被创建。因此要印出一条错误消息。 如果fork成功,那么现在有两个进程。两者都从fork返回时开始执行main函数。为了使进程执行不同的任务,程序必须基于fork的返回值决定其作为子进程或父进程执行某个分支。 else if (pid == 0) {
printf("Hello from the child process!\n");
_exit(EXIT_SUCCESS);
}
Fork操作会为子进程创建一个单独的定址空間。子进程拥有父进程所有内存段的精确副本。在现代的UNIX变种中,这遵循出自SunOS-4.0的虚拟内存模型,根据寫入時複製语义,物理内存不需要被实际复制。取而代之的是,两个进程的虚拟内存页面可能指向物理内存中的同一个页,直至它们写入该页时,写入才会发生。在用fork配合exec来执行新程序的情况下,此优化很重要。通常,子进程在停止程序运行前会执行一小组有利于其他程序的操作,它可能用到少量的其父进程的数据结构。 else {
int status;
(void)waitpid(pid, &status, 0);
}
其他进程——即父进程,会收到fork传来的子进程的进程标识符,其始终为一个正数。父进程将此标识符传递给 waitpid 系统调用来暂停执行,直至子进程退出。当此情况发生后,父进程继续执行并按return语句的含义退出。 参见参考资料
|