为什么将创建进程的 API 设计为 fork 与 exec 分离
在 Unix 下,为什么会将创建进程的 API 设计为需要 fork 与 exec 两个函数来处理?为什么不将两个函数合并为一个函数,比如 createProcess 这样的函数?
针对此问题,在业界存在一些争论,有些人表示这样的设计是 Unix 哲学,即一个函数只专注一个基本功能,而另一些人则对这样的设计表示怀疑,认为这是早期历史原因造成的,在现代操作系统中这样的设计是落后的。
在《操作系统导论》一书中,作者表示操作系统将创建进程的 API 设计为 fork 与 exec 分离,可以实现一些有趣的功能。比如,在 fork 之后,exec 之前,操作系统可以改变子进程的运行环境,从而实现一些特殊的功能。典型的例子,就是 shell 中的重定向功能,若需要将程序的输出从 stdout 重定向到某个文件,shell 在 fork 之后,首先关闭 stdout,然后打开需要保存输出内容的文件,在 open 系统调用执行时,会选择从 0 开始的最小的文件描述符来使用,此时文件描述符为 0 的 stdout 已经被关闭,则文件描述符 0 将被赋予保存子进程输出内容的文件。shell 随后再调用 exec 执行子进程,这样子进程的输出内容将被重定向到某个文件中,而不是 stdout。
在 The Evolution of the Unix Time-sharing System 中有一段说明:
A good example is the separation of the fork and exec functions. The most common model for the creation of new processes involves specifying a program for the process to execute; in Unix, a forked process continues to run the same program as its parent until it performs an explicit exec. The separation of the functions is certainly not unique to Unix, and in fact it was present in the Berkeley time-sharing system [2], which was well-known to Thompson. Still, it seems reasonable to suppose that it exists in Unix mainly because of the ease with which fork could be implemented without changing much else. The system already handled multiple (i.e. two) processes; there was a process table, and the processes were swapped between main memory and the disk.
当在编写 Unix 时,Thompson 对 Berkeley time-sharing system 中的 fork 很熟悉,因此自然地将 fork 的实现移植到 Unix 中,而不是采用将 fork+exec 合并实现 API 的方式。当然这是历史,但是这个 fork 接口是常用接口,为了考虑兼容性,这么多年下来,这个 API 得以一直保留至今,所以,fork+exec 这样的设计,也存在一些历史的原因。
还有一种解释,在 Unix 的早期,是没有进程、线程的概念,也没有多任务,在实现 shell 程序时,shell 需要执行其他程序,exec 则直接将 shell 从内存中杀死,执行新的程序,当执行完成返回后,再重新加载 shell,拖慢性能,因此 fork 出现了,在 exec 之前,先复制一份 shell,然后 exec 新的程序,执行完成后,复制的 shell 可以继续执行。因此,fork+exec 是在当年没有多任务概念的情况下,为了解决多任务的需求而产生的。
2019 年有一篇论文,也探讨了这个主题:a fork in the road