是时候淘汰对操作系统的 fork() 调用了

概述

一般观点认为针对线程创建Unix的fork()与exec()的组合堪称绝配,但微软研究院与波士顿大学联合发表的一篇论文则提出了相反的观点。他们认为fork在当下早已过时,对操作系统和应用程序的设计弊大于利,并给出了一些替代fork的方案和未来的发展路线建议。

1 引言

当初人们在开发Unix的时候需要一种创建线程的机制,于是他们发明了一个特殊的系统调用:fork()。Fork会创建一个与其父进程(fork的调用者)相同的新进程,但系统调用的返回值除外。现在的开发者都习惯了Unix中用fork()加上exec()执行子进程中不同程序的用法,但相比非Unix系的操作系统来说,这种用法还是比较特立独行的[例如,1,30,33,54]。

50年过去了,fork仍然是POSIX上默认的线程创建API:Atlidakis等[8]发现有1304个Ubuntu包(占总数的7.2%)会调用fork,相比之下更现代化的posix_spawn()只有41个包在用。几乎所有的Unix内核系统、主流Web和数据库服务器(例如Apache、PostgreSQL和Oracle)、Google Chrome、Redis键值存储甚至Node.js都在使用fork。大家似乎认为fork是很好的设计。我们审查的几本操作系统教科书[4,7,9,35,75,78]都对其持中立态度,甚至赞誉有加,而且常常会强调它相比其它方法的“简单性”优势。今天的专业课会教学生“fork系统调用是Unix的伟大思想之一”[46],并且“设计创建线程的API有很多条路可选,而fork()和exec()的组合是其中既简单又强大的一条路……Unix的设计者选对了路”[7]。

如今我们就要纠正这种错误。Fork是一种机制:它是上个时代遗留的产物,在现代操作系统中已经过时,甚至有很多害处。我们开发社区对fork很熟悉,但也会因此无视它的问题(§4,第4部分,下同)。公认fork存在的问题包括它没有线程安全、低效且不可扩展,且带来了安全问题。此外,fork已经不再像当年一样简洁了;如今它会影响自己曾经正交过的其它所有操作系统抽象。此外,fork面临的一项根本挑战在于,由于它将进程与其运行的地址空间混为一谈,因此fork会阻碍操作系统功能的用户模式实现,搞乱从缓冲IO到内核旁路网络的所有内容。也许最大的问题在于fork不支持compose——但系统的每一层,从内核到最小的用户模式库都必须支持它。

我们使用在先前研究系统中获得的经验来说明fork对操作系统实现带来的坏处(§5)。Fork限制了操作系统研究者和开发者的创新能力,因为新的抽象都必须专门定做。有效支持fork和exec的系统被迫懒惰地复制每个进程状态。这还促进了状态的中心化,这是不用单内核构建的系统面临的主要问题。另一方面,不支持fork的创新系统原型也无法运行大量需要fork支持的软件。

我们最后讨论了备选方案(§6)并发出了号召(§7):fork应移除出我们系统的第一类原语,并用良好的模拟方法替换,为旧式应用程序提供兼容性。仅向操作系统添加新原语是不够的,fork必须从内核中删掉。

2 历史起源:fork最初是一种取巧

一般认为,最早实现fork操作的项目是Project Genie分时系统[61]。Ritchie和Thompson [70]声称Unix fork“基本上和我们在Genie中实现的是一样的”。但是,Genie监视器的fork调用比Unix更灵活:它允许父进程为新的子进程指定地址空间和机器上下文[49,71]。默认情况下,子进程共享其父进程的地址空间(有点像现代线程);根据需要也可以给子进程一个完全不同的内存块的地址空间供用户访问;后者可能用来运行不同的程序。最重要的是,这里没有工具来复制地址空间,而是由Unix无条件完成的。

Ritchie [69]后来指出“Unix引入fork的主要原因可能是它比较容易实现,不用改变太多东西。”他接着讲到了当年的PDP-7计算机如何用27行代码第一次实现了fork,包括将当前进程复制到虚拟内存,并将子进程保留在内存中。Ritchie还指出,Unix的fork-exec组合“当其中的exec并不存在时,这个组合就会变得非常复杂;它的功能已经由shell使用显式IO执行了。“

TENEX操作系统[18]为Unix的路子提供了一个值得注意的反例。它也受到了Project Genie的影响,但它的发展和Unix互相独立。它的设计者也为进程创建引入了fork调用,但与Genie更相似的是,TENEX fork要么共享了父进程之间的地址空间,要么创建了具有空地址空间的子进程[19]。它没有Unix风格的地址空间复制,可能是因为它能用到虚拟内存硬件了。

Unix fork不是一种“必然性”[61]的产物。它只是一种权宜之计,照搬PDP-7中的实现而已;结果50年过去了,它却已遍布现代操作系统和应用程序了。

3 FORK API的优点

当Unix为PDP-11计算机(其带有内存转换硬件,允许多个进程保留驻留)重写时,只为了在exec中丢弃一个进程就复制进程的全部内存就已经很没效率了。我们怀疑在Unix的早期发展阶段,fork之所以能幸存下来,主要是因为程序和内存都很小(PDP-11上有只8个8 KiB页面),内存访问速度相对于指令执行速度较快,而且它提供了一个合理的抽象。这里有两点很重要:

Fork很简单。除了易于实现之外,fork还简化了Unix API。最明显的是fork不需要参数,因为它为新进程的所有状态简单地提供了一个默认值:从父进程继承过来。与之形成鲜明对比的是,Windows CreateProcess()API采用显式参数来指定子项内核状态的所有细节——包括10个参数和许多可选flag。

更重要的是,使用fork创建进程和启动一个新程序是正交的,且fork和exec之间的空间有自己的用途。由于fork复制了父进程,因此允许进程修改其内核状态的系统在发起调用后,可以在exec之前在子进程中复用:shell在命令执行之前就可以打开、关闭和重新映射文​​件描述符,并且程序可以减少权限或更改子项的命名空间以在受限上下文中运行它。

Fork简化了并发。在多线程或异步IO流行之前的年代,不用exec的fork提供了有效的并发形式。在共享库流行之前,它带来了一种简单的代码复用形式。程序可以初始化,解析其配置文件,然后fork自身的多个副本,这些副本从相同的二进制文件中运行不同的函数,或处理不同的输入。这种设计延续到了预fork服务器中,我们会在§6中再讲。

4 现代的fork

乍一看,fork现在好像还是很简洁。我们认为这是一个美丽的谎言,而且这种fork效应对现代应用来说弊大于利。

Fork已经不再简洁了。Fork的语义已经影响了所有新的创建进程状态的API设计。POSIX规范列出了25个关于如何将父状态复制到子进程[63]的具体情况:文件锁、定时器、异步IO操作、跟踪等等。此外,许多系统调用flag会控制fork的行为,如内存映射(Linux madvise()flag,MADV_DONTFORK/DOFORK/WIPEONFORK等)、文件描述符(O_CLOEXEC,FD_CLOEXEC)和多线程(pthread_atfork())。所有新式操作系统工具都必须用fork记录其行为,并且必须准备好用户模式库,以便随时fork它们的状态。Fork的简洁性与正交性如今已荡然无存。

Fork不会compose。因为fork复制了整个地址空间,所以它不适合在用户模式下实现的操作系统抽象。缓冲IO就是一个典型的例子:用户必须在fork之前显式刷新IO,以免重复输出[73]。

Fork是非线程安全的。今天的Unix进程支持多线程,但fork创建的子进程只有一个线程(调用线程的副本)。除非父进程对其他线程也逐个fork,否则子地址空间最后可能会与父进程不一致。一个简单但常见的情况是一个线程进行内存分配并持有堆锁,而另一个线程fork。任何在子进程中分配内存的尝试(从而获得相同的锁)都将立即死锁,等待永远不会发生的解锁操作。

编程指南建议不要在多线程进程中使用fork,或者fork之后立即调用exec [64,76,77]。POSIX仅保证在fork和exec之间可以使用一小部分“异步信号安全”函数,特别是排除malloc()以及标准库中其它可能分配内存或获取锁的标准库中的内容。真正的多线程程序如果fork,可能会在实践中出现各种错误并为之困扰[24-26,66]。

很难想象有哪位理智的内核维护者会在内核中加入一个有这么多限制属性的系统调用。

Fork是不安全的。默认情况下,fork出的子进程从其父进程继承所有内容,并且程序员要负责显式删除子进程不需要的状态:他要关闭文件描述符(或将其标记为close-on-exec)、从内存中清除机密 、使用unshare()[52]等隔离命名空间。从安全角度来看,fork的默认继承行为违反了最小特权原则。此外,fork但不执行的程序使地址空间布局随机化无效,因为每个进程都具有相同的内存布局[17]。

Fork很慢。自Thompson首次应用fork以来的几十年中,内存大小和相对访问成本不断增长。即使在1979年(当时第三个BSD Unix引入了vfork()[15]),fork已经有了性能问题,多亏了写入时复制技术[3,72]才让它的性能表现可以被接受。今天,就连建立写时复制映射的时间也成了一个问题:Chrome在fork [28]中的延迟长达100毫秒,并且在exec之前fork时,Node.js应用会被阻塞几秒钟[56]之久。

image

Fork现在太拖累性能了,所以C语言库特意避免在posix_spawn()[34,38]中使用它,而Solaris将spawn用作了原生系统调用[32]。但是,只要应用程序还是直接调用fork,它们就会付出高昂的代价。图1对比了在3.6 GHz的Intel i7-6850K CPU上,Ubuntu 16.04.3下不同大小的进程fork和exec的时间。脏线显示使用脏页fork进程的开销,必须将其降级为只读来做写入时复制映射。在碎片化的情况下,父对象只会污染它的堆栈,但会通过交替分配只读和读写页面来模拟复杂应用的内存布局,后者的复杂性体现在共享库、随机化地址空间和实时编译等。相比之下,无论父进程的大小或内存布局如何,posix_spawn()需要相同的时间都一样(大约0.5 ms)。

Fork无法扩展。在Linux中,设置fork的写时复制映射所需的内存管理操作会损害可扩展性[22,82],但真正的问题在更深的层次:正如Clements等人[29]所观察到的,fork API的规范本身就引入了一个瓶颈,因为(与spawn不同)它无法与进程上的其他操作通信。其他因素进一步阻碍了fork的可扩展实现。直观地说,扩展系统规模就要避免不必要的共享。Fork进程启动时就与其父进程共享所有内容。由于fork复制了进程操作系统状态的所有方面,因此它鼓励将该状态集中在单体内核中,这样复制和/或引用计数开销较少。这样就难以实现诸如用于安全性或可靠性的内核划分了。

Fork鼓励内存过度使用。在考虑写时复制页面映射所使用的内存时,fork的实现者面临着一个艰难的选择。这样的页面都代表了一个潜在的分配——如果页面的任何副本被修改,将需要一个新的物理内存页面来解决页面错误。因此,保守的实现会让fork调用失败,除非有足够的后备存储来应对所有潜在的写时复制错误[55]。但是,当一个大进程执行fork和exec时,会创建许多写时复制页面映射但从不去修改,尤其是exec过的子进程很小时更是如此;并且因为最坏的分配情况(进程的虚拟空间加倍)无法实现就导致fork失败是不可理喻的。

另一种方法,也就是Linux上的默认方法是过度使用虚拟内存:建立虚拟地址映射的操作(包括fork的地址空间的写时复制克隆)无论是否存在足够的后备存储,都会立即成功。后续页面错误(例如,对分fork页面的写入)可能无法分配所需的内存,而调用基于启发式的“内存外杀手”来终止进程并释放内存。

需要明确的是,Unix并不需要过度使用,但我们认为写入时复制fork(而不是类似于spawn的工具)的广泛应用让这种现象泛滥了。现实应用程序并没有准备好处理fork [27,37,57]中明显虚假的内存不足错误。Redis使用fork进行持久化,明确建议不要禁用内存过量提交[67];否则,Redis必须限制在总虚拟内存的一半用量,以避免在内存不足的情况下被杀死的风险。

总结。今天的Fork是适合单线程进程的API,具有较小的内存占用和简单的内存布局,需要对其子进程的执行环境进行细粒度控制,但不需要与它们完全隔离。换句话说,它是一个shell。毫不奇怪,Unix shell是第一个fork [69]的程序,fork的支持者也会拿shell举例作为其优雅的证明[4,7]。但是,大多数现代程序都不是shell。为了方便shell而去优化操作系统 API现在还是个好主意吗?

5 实现fork

虽然很难量化在现有系统上实现fork的成本,但有明显证据表明支持fork限制了操作系统体系结构的变化,并限制了操作系统适应硬件演进的能力。

Fork与单个地址空间不兼容。许多现代上下文将执行限制在单个地址空间,包括picoprocess [42]、unikernels [53]和en- claves [14]。尽管有数量庞大操作系统研究者在使用并改进Unix系统,但如果研究者使用的是不基于fork的系统,那么就更容易适应这些环境。

例如,Drawbridge libOS[65]在隔离的用户模式地址空间内实现二进制兼容的Windows运行时环境,称为picoprocess。Drawbridge支持同一共享地址空间内的多个“虚拟进程”; CreateProcess()是通过在地址空间的不同部分加载新的二进制文件和库,然后创建一个单独的线程来开始执行子进程,同时确保跨进程系统调用是按预期运行实现的。不用说,这些进程之间没有安全隔离——主picoprocess负责提供安全边界。然而,该模型已被用于在SGX Enclave内支持完整的多进程Windows环境[14],使包含多进程和程序的复杂应用程序能够部署在enclave中。

相比之下,fork在单地址空间[23]中需要复杂的编译器和链接调整[81]才能实现。因此,从Unix系统派生的Unikernels不支持内部多进程环境[44,45],并且在enclave中运行多进程Linux应用程序要复杂得多。SCONE和SGX-LKL仅支持单进程应用程序[6,50]。Graphene-SGX [79]通过在新的主机进程中创建一个新的接口来实现fork,然后通过加密的RPC流复制父进程的内存;这套操作可能要花几秒钟的时间。

Fork与异构硬件不兼容。Fork将进程的抽象与包含它的硬件地址空间混合在一起。实际上,fork将进程的定义限制为单个地址空间,并且(如前所述)是在某个核心上运行的单个线程。

现代硬件和在其上运行的程序并不是这样的机制。硬件越来越异构化,并且使用诸如带内核旁路NIC[12]的DPDK,或使用GPU的OpenCL的进程无法安全地fork,因为操作系统无法复制NIC/GPU上的进程状态。这种困境似乎已经困扰了GPU程序员至少十年[58-60,74]。随着未来的片上系统包含越来越多的有状态加速器,这种情况只会变得更糟。

Fork会感染整个系统。仅支持fork对系统的设计和运行时环境造成了很大的限制。任何层的高效fork都需要在其下的所有层上都有基于fork的实现。例如,Cygwin是Windows的一个POSIX兼容环境;它实现了fork以运行Linux应用程序。由于Win32 API缺少fork,Cygwin在CreateProcess()[31,47]之上模拟它:它创建一个新的进程,在恢复子进程之前运行与父进程相同的程序并复制所有可写页面(数据部分、堆、堆栈等)。这既不快也不可靠,并且可能由于多种原因而失败,最常见的失败是当父和子进程中的存储器地址因地址空间布局随机化而不同时出现的。

讽刺的是,NT内核本身支持fork;只有Cygwin所依赖的Win32 API才没有(用户模式库和系统服务不支持fork,因此fork的Win32进程会崩溃)。作为一个抽象,fork无法compose:除非每个层都支持fork,否则无法使用它。

在研究用操作系统中fork:K42的经验

许多研究用操作系统都面临着是否(以及如何)实现fork的困境,本文作者就亲身经历了6个这种案例[13,36,41,48,51,80]。这种选择至关重要。实现fork打开了支持大量Unix派生应用程序的大门,其中最先用到的是shell和构建工具,它们可以简化整个系统的创建过程。然而fork也让研究者束手束脚:但凡一个系统实现了fork,尤其是想要高效实现fork或者在开发初期就引入fork的系统都会无可救药地变成类Unix的设计。

K42 [48]是基于我们在Tornado [36]的经验上开发的系统,展示了对多处理器友好的面向对象方法、基于各个应用程序的可定制对象和微内核架构[5]的好处,以实现普遍的局部性和并发优化。我们的目标是构建一个成熟的通用操作系统,在(可能)非常大规模的多处理器平台上支持使用多操作系统特性的大量应用程序。最后,K42与POSIX兼容并且兼容Linux ABI,但是为Linux特性执行fork操作的设计导致fork语义颠覆了整个操作系统设计,对其它特性都带来了负面影响。

我们开始以为我们能够像Cygwin一样实现fork:作为用户级库函数,通过适当地构造必要的新对象实例来创建现有进程的子副本。这本身并不是个问题。相比之下,为了允许任何进程在任何时候都可fork,并且在追求高性能表现的同时有效地做到这一点的努力最终失败了——随之而来的复杂性让我们放弃了几乎所有特性,只剩下对Unix的支持和对我们原生特性的支持了。

尤其严重的是,以下问题几乎渗透到了系统的每个方面:

反模块化:只要是可能支持正在运行进程的对象实现就需要在进程fork时定义其行为。这让实现专用组件变得非常复杂,这些组件的目的可能仅仅是为长期运行的并行科学计算或服务器引入局部优化而已,根本用不着fork。

内在的懒惰需求:鉴于每个核心的资源,从内存区域和文件到特定特性的抽象,诸如文件描述符和信号处理器都需要fork支持,我们只能实现懒惰写时复制行为来缓解性能压力。这不仅增加了单个对象中的复杂度,还需要对象交互来维护fork创建的层次关系。这与我们限制共享和同步的目标背道而驰,结果损害了局部性。

中心化:操作系统的可扩展性是通过避免中心化的策略和避免确切全局知识的机制来实现的[11]。因此,跨对象实例和服务器的分解状态和功能成为了我们的核心理念。但是,尽管fork是在库代码中协调的,它还是需要与进程可能连接的所有服务器和对象通信。

可扩展性较差:除了违反我们的核心可扩展性原则之外,在NUMA系统中fork必须要么访问父进程位置的存储器,要么将子进程安排在系统的受控部分中;这些都是我们花费大量精力去解决的固有问题。

事后看来,我们犯了一个错误,没有仔细评估fork的实际用例。如果我们将K42的fork局限到单线程进程(例如shell)上,我们就可以避免让它的复杂性影响到核心对象了。

6 取代fork

既然fork有这么多问题,那么该用什么来取代它? 创建新进程会往往会引出混乱的API设计问题,因为任何选项都必须隐式或显式地指定属于新进程的所有操作系统资源的初始状态。Fork的应对很简单:复制一切,结果如我们所见最后成为了fork的软肋。为了取代fork,我们提出了一个上层spawn API和一个底层微内核API的组合,以便在执行之前设置一个新进程。然后我们讨论了无需exec的fork的替代方案。

上层:Spawn。在我们看来,fork和exec的大多数功能最好改由spawn API提供。这种改动所需的重构工作可能会很棘手,尤其是当fork和exec在代码中的位置并不好找的时候;但正如我们在§4中所示,这种方案的性能和可靠性有着显著优势,更不用说可移植性了。值得注意的是,使用fork的主流应用程序(例如,Apache,Chrome,PostgreSQL)的Windows端口并不支持fork,因此fork显然不是必需的。

posix_spawn()API可以简化这种重构。spawn属性不要求在单个调用站点上提供影响新进程的所有参数(如CreateProcess()的情况),而是由可扩展定义的辅助函数设置。例如,fork之前的close()可以用预生成的调用替换,该调用记录了在子进程中发生的“关闭动作”。不幸的是,这意味着API被指定为由fork和exec实现,尽管这实际上没必要[32]。

posix_spawn()的主要缺点在于,它不是fork和exec的完整替代。它尚不支持一些不太常见的操作,例如设置终端属性或切换到隔离的命名空间;它还缺少有效的错误报告机制:在子进程开始执行之前发生的故障(例如无效的文件描述符参数)是异步报告的,并且与子进程的终止无法区分。这些缺点可以而且应该得到纠正。

替代方案:vfork()。这种流行的fork变体由BSD引入作为优化措施[15];它创建了一个直到子进程调用exec前共享父地址空间的新进程,更像是原始的Genie fork [71]。为了让子进程能使用父进程的堆栈,它会阻止父进程执行,直到exec为止。这种编程风格类似于fork,其中新进程在exec之前调整其内核状态。但由于地址空间共享,vfork()很难安全使用[34]。虽然vfork()避免了克隆地址空间的成本,并且在难以重构代码使用spawn的场合可以用来替换fork,但在大多数情况下最好别用它。

底层:跨进程操作。虽然大多数启动新程序的实例都喜欢类似于spawn的API,但为了完全通用,它需要一个flag、参数或者辅助函数控制过程状态的所有可能方面。单个操作系统 API无法完全控制新进程的初始状态。在今天的Unix中,高级用例的唯一后备仍然是在fork之后执行的代码,但是整洁状态设计[例如,40,43]已经演示了一种替代模型,其中修改每个进程状态的系统调用不仅限于当前进程,而可以操纵调用者能访问的任何进程。这就带来了fork/exec模型所有的灵活性和正交性,但避开了后者的大多数缺点:一个新的进程从一个空的地址空间开始,一个高级用户可能以零碎的方式操作它,填充它的地址空间和内核执行前的上下文,无需克隆父项,也不需要在子项的上下文中运行代码。ExOS[43]使用这种原语的顶层用户模式实现了fork。将跨进程API纳入Unix看起来很有挑战性,但也会对未来的研究有所帮助。

替代方案:clone()。这个系统调用是Linux上所有进程和线程创建的基础。就像它之前的Plan 9的rfork()一样,它需要单独的flag来控制子进程的内核状态:地址空间、文件描述符表、命名空间等。这避免了fork的一个问题:它的行为对于许多抽象是隐式的或未定义的。但是,对于每个资源都有两个选项:在父项和子项之间共享资源,或者复制它。因此,clone遇到了fork所面临的大多数问题。

只使用fork的用例。一些特殊情况下,fork后面并不会跟着需要复制父进程的exec。

多进程服务器。传统上,构建并发服务器的标准方法是fork进程。然而,推动多进程服务器的动力早已不复存在:操作系统库是线程安全的,并且困扰的多线程或事件驱动服务器的可扩展性瓶颈已经消失[10]。虽然从故障隔离的角度来看进程边界可能还有其价值,但我们认为使用spawn API启动这些进程更有意义。当大多数并发由多线程处理,且现代操作系统会对内存进行重复数据删除的情况下,fork带来的共享初始状态的性能优势就没那么明显了。最后,使用fork时所有进程要共享相同的地址空间布局,并且容易受到盲目ROP攻击[17]。

写时复制内存。fork的现代实现使用写时复制来减少复制很快会被丢弃的内存的开销[72]。由此以来许多应用程序只依赖fork来获得对写时复制内存的访问权限。一种常见模式是从预先初始化的进程中分离,以减少工作进程的启动开销和内存占用,如Linux上[4]的Android Zygote [39,62]和Chrome站点隔离。另一种模式使用fork来捕获正在运行的进程的地址空间的一致快照,允许父进程继续执行;这包括Redis [68]中的持久性支持,以及一些反向调试器[21]。

POSIX将受益于一个API,它可以无需fork新进程就使用写时复制内存功能。Bittau [16]建议使用checkpoint()和resume()调用来获取地址空间的写时复制快照,从而减少安全隔离的开销。最近,Xu等人[82]观察到fork花费的时间是影响fuzzing工具性能的主要因素,并提出了类似的snapshot()API。这些设计尚不足以涵盖上述所有用例,但也许可以作为新的起点。我们注意到,任何新的写时复制内存API都必须解决§4中描述的内存过度使用问题,但是将此问题与fork解耦应该处理起来会简单些。

7 让我的操作系统摆脱fork!

我们已经解释了为什么fork已经是旧时代的老古董了,它会损害应用程序和操作系统的设计。我们必须做三件事来纠正这种情况。

弃用fork。由于Unix的广泛流行,未来的操作系统在很长时期内都需要支持fork;但不管怎样,50年前的一种偏门技巧不应该决定未来操作系统的设计。因此,我们强烈建议不要在新代码中使用fork,并尝试将其从现有应用程序中删除。一旦fork离开了性能关键路径,它就可以从操作系统的核心中删除,并根据需要重新实现。如果未来的系统仅在有限的情况下支持fork,例如单线程进程[2],那么仍然可以在无需非必要的复杂性的情况下运行传统软件。

改进替代方案。很长一段时间里,fork已经成为类Unix系统上的通用进程创建机制,其他抽象层则叠在最顶层。值得庆幸的是这种情况已经开始改变[32,38],但是还有更多工作要做(§6)。

修正我们的课本。显然,学生需要学习fork,但是目前大多数教科书(和教师)都使用fork [7,35,78]做例子来讲解进程创建过程。这不仅会延长fork的生命期,而且是在灌输过时的知识——这种API根本就不直观。正如现代编程课程不会以goto开头一样,我们建议大家教授posix_spawn()或CreateProcess(),然后将fork作为其历史背景的特殊情况讲一讲就够了(§2)。

本文的备注可在原始论文附注中查看。

查看英文原文:https://www.microsoft.com/en-us/research/publication/a-fork-in-the-road

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章