首页UC › fork的那些事

fork的那些事

历史上的linux 在fork()时完整的复制了父进程的进程空间及相应物理页面,这带来的消耗是可观的,更糟的是,往往程序猿fork()之后紧接着就exec()执行新的程序,刚才复制的东西全白复制了。
为避免做这种无用功,可以使用vfork函数创建,子进程会完全共享父进程的地址空间,包括页表。所以进程写入用户空间的内容同时也写入了子进程的 用户空间,反之亦然。为了避免产生”混乱“,内核将父进程挂起,直到两个进程不再共享它们的用户空间(子进程调用了exec()或exit())。

现在的linux采用了“写时复制”的技术,fork()仅仅复制一些描述进程空间的数据结构 及 父进程页表,从而共享物理页面。当某个进程写某页面时,再单独复制一份该页面给它。所以,现在vfork函数几乎不会被使用。

不论fork()和vfork()最终都会调用do_fork(),只不过传递给它的clone_flags参数不同。fork时,传递的 clone_flags参数为SIGCHLD,vfork时,传递的clone_flags参数为 CLONE_VFORK | CLONE_VM | SIGCHLD。
其中CLONE_VM 标志表示共享父进程的mm_struct,包括页表等。

do_fork()进而调用copy_process()。调用关系如图所示:

20130902200548
do_fork()的代码在:http://lxr.linux.no/#linux-bk+v2.6.11.5/kernel/fork.c#L1124。由于创建进程的实质性工作在copy_process(),我们直接来看copy_process()的代码:

dup_task_struct()给子进程分配内核栈、task_struct结构和thread_info结构。并将当前进程的task_struct、thread_info结构的内容复制到子进程的相应结构。

设置子进程的pid。变量pid是由do_fork()传递给copy_process()的。接下来,逐步改变子进程task_struct 的字段,使子进程与父进程区别开来。

复制所有进程信息。我们挑出其中的 copy_mm()和copy_thread()看看。

copy_mm()
copy_mm()函数的工作是继承父进程的用户空间。这种继承是通过复制mm_struct 实现的。
我们看看 copy_mm()的代码,在http://lxr.linux.no/#linux-bk+v2.6.11.5/kernel/fork.c#L424

其中mm_users我们在《进程的虚拟空间》中讲到过。
对 mm_struct 的复制是在 clone_flags中的 CLONE_VM 标志为 0 时才真正进行,否则,就只是把mm赋值为oldmm,表示共享父进程的用户空间,包括页表。
vfork传递的参数中 CLONE_VM标志为1,所以父、子进程通过指针共享用户空间(指向同一 mm_struct 结构),那也说明父进程写入用户空间的内容同时也写入了子进程的用户空间,反之亦然。为了避免产生”混乱“,内核将父进程挂起,直到两个进程不再共享它们 的用户空间(子进程调用了exec()或exit())。

相反,fork真正复制了mm_struct 。对 mm_struct 的复制不只限于这个数据结构本身,还包括了对更深层次数据结构的复制,其中最主要的是 vm_area_struct 结构和页表的复制。因为父子进程的页表是一样的,所以他们共享物理页面。但这种共享是”只读“共享,当某个进程写某页面时,会产生”页错误“,然后”请页 机制“会单独复制一份该页面给它,并更新它的页表。

copy_thread()
copy_thread()函数主要工作主要是:用父进程的内核栈内容 更新子进程的内核栈。栈中的内容记录了父进程通过系统调用 fork()进入内核空间前夕的用户栈地址、CS、EIP等寄存器内容,子进程将要返回到调用fork()的地方,所以要把它复制给子进程。

copy_thread()代码如下:在http://lxr.linux.no/#linux-bk+v2.6.11.5/arch/i386/kernel/process.c#L382

可见copy_thread()将子进程eax寄存器置为0,所以fork()时子进程返回值是0。

copy_process()以及do_fork()返回后,一个新进程诞生了!新进程产生之后,并不是立即运行,而是等待调度程序调度,有调度程序决定它何时得到运行。

发表评论