新起点
Fork (系统调用)
2020-06-11 08:14:32

在计算机领域中,尤其是Unix及类Unix系统操作系统中,fork(进程复制)是一种创建自身行程副本的操作。它通常是内核实现的一种系统调用。Fork是类Unix操作系统上创建进程的一种主要方法,甚至历史上是唯一方法。

在多任务操作系统中,行程(运行的程序)需要一种方法来创建新进程,例如运行其他程序。Fork及其变种在类Unix系统中通常是这样做的唯一方式。如果进程需要启动另一个程序的可执行文件,它需要先Fork来创建一个自身的副本。然后由该副本即“子进程”调用exec(英语:Exec (computing))系统调用,用其他程序覆盖自身:停止执行自己之前的程序并执行其他程序。

Fork操作会为子进程创建一个单独的地址空间。子进程拥有父进程所有内存段的精确副本。在现代的UNIX变种中,这遵循出自SunOS-4.0的虚拟内存模型,根据写入时复制语义,物理内存不需要被实际复制。取而代之的是,两个进程的虚拟内存页面(英语:Virtual memory pages)可能指向物理内存中的同一个页,直到它们写入该页时,写入才会发生。在用fork配合exec来执行新程序的情况下,此优化很重要。通常来说,子进程在停止程序运行前会执行一小组有利于其他程序的操作,它可能用到少量的其父进程的数据结构。

当一个进程调用fork时,它被认为是父进程,新创建的进程是它的孩子(子进程)。在fork之后,两个进程还运行着相同的程序,都像是调用了该系统调用一般恢复执行。然后它们可以检查调用的返回值(英语:Return value)确定其状态:是父进程还是子进程,以及据此行事。

fork系统调用在第一个版本的Unix就已存在,它借用于更早的GENIE(英语:Project Genie) 分时系统。Fork是标准化的POSIX的一部分。

子进程从父进程的文件描述符副本开始。对于进程间通信,父进程通常会创建一个或多个管道,在fork进程之后,进程关闭它们不需要的管道端。

Vfork是与fork具有相同调用约定和很多相同语义的一个变种,但只能在有限的情况下使用它。它起源于Unix的3BSD版本,这是首个支持虚拟内存的Unix版本。它已按POSIX标准化,这使得vfork能具有与fork完全相同的行为。但这已在2004年的版本中被标为过时,并在后续版本中被posix_spawn()取代(其通常通过vfork实现)。

在发出一个vfork系统调用时,父进程被暂停,直至子进程完成执行或被新的可执行映像取代(通过系统调用之“exec(英语:Exec (computing))”家族中的一项)。子进程借用父进程的MMU设置和内存页面,在父进程与子进程之间共享,不进行复制,尤其是没有写入时复制语义;因此,如果子进程在任何共享页面中进行修改,不会创建新的页面,并且修改的页面对父进程同样可见。因为没有页面复制(消耗额外的内存),此技术在纯复制环境中使用exec时较普通fork更优化。在POSIX中,除非是将立即调用exec家族(及其他几个操作)的函数,其他任何目的会导致未定义行为。使用vfork时,子进程借用而非复制数据结构,所以vfork仍比使用写时复制语义的fork更快。

System V在System VR4被引入前不支持此系统函数,因为它的内存共享容易出错:

does not copy page tables so it is faster than the System V implementation. But the child process executes in the same physical address space as the parent process (until an or ) and can thus overwrite the parent's data and stack. A dangerous situation could arise if a programmer uses incorrectly, so the onus for calling lies with the programmer. The difference between the System V approach and the BSD approach is philosophical: Should the kernel hide idiosyncrasies of its implementation from users, or should it allow sophisticated users the opportunity to take advantage of the implementation to do a logical function more efficiently?

同样,Linux对vfork的手册页面强烈不鼓励它的使用:

It is rather unfortunate that Linux revived this specter from the past. The BSD man page states: "This system call will be eliminated when proper system sharing mechanisms are implemented. Users should not depend on the memory sharing semantics of vfork() as it will, in that case, be made synonymous to fork(2)."

使用vfork的其他问题包括死锁 ,它可能发生在多线程程序中,由于与动态链接交互。 作为vfork接口的替代品,POSIX引入了posix_spawn函数家族,它结合了fork和exec的动作。这些函数可以实现为fork的程序库例程,就像Linux那样,或者为了更好的性能实现为vfork ,就像Solaris那样。不过,POSIX规范中注明它是“为内核操作设计”,尤其是用于运行在受限硬件和实时系统上的操作系统。

虽然4.4BSD的实现中摆脱了vfork的实现,使vfork做到与fork相同的行为,它在NetBSD操作系统中因性能原因而恢复。

一些嵌入式操作系统(例如uClinux)省略fork并只实现vfork,因为它们需要在由于缺乏内存管理单元(MMU)而不可能实现写时复制的设备上操作。

Plan 9操作系统由Unix的设计者创造,包括fork,但也有一个名为“rfork”的变种,它允许父进程与子进程之间资源的细粒度共享,包括地址空间(除了调用栈段,那是每个进程独有的)、环境变量和文件系统名字空间;这使它成为了创建进程和其中的线程的一个统一接口。 在FreeBSD和IRIX中采用了来自Plan 9的rfork,后者将其更名为“sproc”。

“clone”(克隆)是Linux内核中的一个系统调用,它创建一个可以与其父共享“执行上下文”的子进程。类似FreeBSD的rfork和IRIX的sproc,Linux的clone受到了Plan 9的rfork启发,并可用于实现线程(尽管应用程序的程序员通常使用更高级的接口,例如pthreads,实现在clone的顶层)。出自Plan 9和IRIX的“separate stacks”(单独堆栈)特性已被省略,因为其导致了太多开销(据Linus Torvalds)。

在VMS操作系统(1977年)的原始设计中,新进程根据当前一些特定地址进行复制来创建被认为是有风险的。当前进程中的错误状态可能被复制给子进程。因此在这里使用了进程“产卵”(spawning)之隐喻:新进程的每个组件的内存布局都是重新创建的。spawn(英语:Spawn (computing))后来被微软的操作系统采用(1993年)。

VM/CMS(OpenExtensions)的POSIX兼容组件提供了一个非常有限的fork实现,其中的父进程在子进程执行时被暂停,并且子与父共享同一地址空间。这本质上是一个名为fork的vfork。(注意,这只适用于CMS客户机操作系统,其他VM客户机操作系统如Linux提供标准的fork功能。)

下列Hello World程序的变种以C语言展示了fork系统调用的机理。该程序fork为两个进程,每个都基于fork系统调用的返回值决定它们执行什么功能。样板代码(英语:Boilerplate code)中的头文件等已被省略。

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的虚拟内存模型,根据写入时复制语义,物理内存不需要被实际复制。取而代之的是,两个进程的虚拟内存页面(英语:virtual memory pages)可能指向物理内存中的同一个页,直至它们写入该页时,写入才会发生。在用fork配合exec来执行新程序的情况下,此优化很重要。通常,子进程在停止程序运行前会执行一小组有利于其他程序的操作,它可能用到少量的其父进程的数据结构。

   else {      int status;      (void)waitpid(pid, &status, 0);   }

其他进程——即父进程,会收到fork传来的子进程的进程标识符,其始终为一个正数。父进程将此标识符传递给 waitpid 系统调用来暂停执行,直至子进程退出。当此情况发生后,父进程继续执行并按return语句的含义退出。

相关:

  • 联合全面行动计划谈判进程
  • 网站公告: