Linux系统提供了 fork 函数用来创建子进程。fork 函数和普通的函数相比,其特殊的地方在于 fork 函数被调用一次,但是会返回两次。一次返回时在父进程中,另一次返回值是在子进程中。
====================================================
函数原型:
返回值:
- 调用失败时返回 -1 给父进程,而且子进程也不会被创建。
- 调用成功时,在父进程中返回 子进程的进程 ID,在子进程中返回 0
====================================================
使用 fork 函数创建出的子进程会复制父进程的地址空间,包括 代码段、数据段、堆和栈,而且子进程也会复制父进程的 PCB ,但是进程 ID 肯定是会不一样的。由此可见,父进程和子进程是相互独立的,各自有各自的地址空间(虽然虚拟地址都是相同的,但是映射的物理地址肯定是不相同的)。如果子进程修改了一个变量值,那么并不会影响父进程中的变量值。
考虑到如果每次创建子进程时都要复制父进程的地址空间,而且通常在fork之后,就会利用exec函数去执行不同的程序,所以又要去清理子进程的代码段和数据段。这样一来,降低了效率。所以如今的 fork 都会采用一种“写时复制(Copy On Write)”的技术来改进这个缺点。这种技术的特点是“读时共享,写时复制”,当子进程想要读数据的时候,和父进程共享地址空间;当子进程想要写数据的时候,才去对父进程进行复制,而且只复制父进程中被修改的那一“页”。这样就减少了复制父进程地址空间的时间,提高了效率。
=======================================================
fork 函数被调用时,在内核中的调用流程?
当我们在应用程序中调用 fork 函数的时候,首先会通过中断指令进入到内核态中(x86架构下使用int指令,arm架构下使用swi指令),然后通过系统调用号来找到 sys_fork 函数,最终 sys_fork 函数会调用 do_fork 函数。
fork 函数在创建子进程的时候,子进程和父进程复制页表项,且内核将页表项设置为只读权限,当父子进程尝试去修改页的内容的时候,再去为子进程修改新的页表项,页的内容是从父进程复制而来。 vfork 函数会复制父进程的页表项,但是除非调用 exec 族函数,否则子进程不会创建新的页表项。