linux 中 fork 原理

二、Linux 中 fork 的原理

Linux 在创建进程时,使用 fork 通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于 PID(每个进程唯一)、PPID(父进程的进程号)和某些资源和统计量(例如,挂起的信号,他没有必要继承)。然后使用 exec 函数读取可执行文件并将其载入地址空间开始运行。

Linux 的 fork 使用“写时拷贝”(copy on write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的计数。内核在 fork 的时候并不复制整个进程地址空间,而是让父进程和子进程共享一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。那么,这样的话,fork 的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化可以避免拷贝大量根本就不会被使用的数据(地址空间常常包含数十M的数据)。

1. fork 的底层实现

以下实现来自于 Linux 2.6 版本。

Linux 使用 clone() 系统调用,通过一系列的参数标志来指明父、子进程需要共享的资源。然后由 clone() 函数去调用 do_fork()。其中 do_fork() 完成了创建进程的大部分工作。其内部会调用 copy_process() 函数,然后让进程开始运行。copy_process() 函数会做如下事情:

  • 调用 dup_task_struct() 为新进程创建一个内核栈、thread_info 结构和 task_struct 结构。字段的填充的值和当前进程的值相同。此时,子进程和父进程的描述符是完全相同的。
  • 检查并确保新创建这个子进程后,当前用户所拥有的进程数目没有超过给他分配的资源的限制
  • 子进程着手使自己与父进程区分开来。进程描述符中内的许多成员都要被清 0 或设置为初始值。包括哪些不是继承而来的进程描述符成员,主要是统计信息。task_struct 中的大多数数据都依然未被修改。
  • 子进程的状态被设置为 task_uninterruptible(不可中断),以保证他不会投入运行
  • copy_process() 调用 copy_flags() 以更新 task_struct 的 flags 成员。将标志 PF_SUPERPRIV 标志被清 0,这是表明进程是否拥有超级用户权限。设置 PF_FORKNOEXEC,表明进程还没有调用 exec() 函数。
  • 调用 alloc_pid() 为新进程分配一个有效的 PID
  • 根据传递给 clone() 的参数标志,copy_process() 拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。在一般情况下,这些资源会被给定进程的所有线程共享。
  • 最后,copy_process() 做扫尾工作并返回一个指向子进程的指针。

再回到 do_fork() 函数,如果 copy_process() 函数成功返回,新创建的子进程被唤醒并让其投入运行。内核会有意选择子进程首先运行,因为一般子进程都会马上调用 exec() 函数,这样可以避免写时拷贝的额外开销。如果父进程首先执行的话,有可能会开始向地址空间写入。虽然想让子进程先运行,但是并非总能如此。