.
现在我们来总结一下fork的整个处理流程。从C语言中的函数开始,它在glibc库中会被转换为int0x80加调用号的形式,触发中断。该中断在系统初始化过程中注册,它的处理函数是system_call,这个函数在system_call.s文件中,在这里面它首先压栈一些参数,然后会根据调用号调用sys_call_table的相应表项,sys_call_table定义在include/linux/sys.h中,它是一个函数指针数组。现在对应的就是sys_fork函数,它仍然是在system_call.s中定义的。我们来看它的处理过程,首先它会调用find_empty_process函数来从task数组中查找一个还没有使用的task,
1. fork
linux系统中提供了三个系统调用可以创建新进程:clone()、fork()、vfork()。实际上,不管是我们比较熟悉的fork()还是剩下的两个在linux中都是通过clone()实现的。clone()是在c语言库中定义的一个封装函数,它负责建立进程堆栈并且调用对程序员隐藏的clone()系统调用。进一步观察发现,linux内核中又是用do_fork()来处理这三个系统调用的。
新的进程通过复制父进程而建立。为了创建新进程,首先在系统的物理内存中为新进程创建一个 task_struct 结构,将旧进程的 task_struct 结构内容复制到其中,再修改部分数据。接着,为新进程分配新的堆栈,分配新的进程标识符 pid。然后,将这个新 task_struct 结构的地址填到 task 数组中,并调整进程链关系,插入运行队列中。于是,这个新进程便可以在下次调度时被选择执行。此时,由于父进程的进程上下文 TSS 结构复制到了子进程的 TSS 结构中,通过改变其中的部分数据,便可以使子进程的执行效果与父进程一致,都是从系统调用中退出,而且子进程将得到与父进程不同的返回值(返回父进程的是子进程的 pid,而返回子进程的是 0)
fork()底层流程图如下:
然后来看看do_fork的具体过程:
p = copy_process(clone_flags, stack_start, regs, stack_size,
child_tidptr, NULL, trace);- wake_up_new_task(p, clone_flags);
第一步是调用copy_process函数来复制一个进程,并对相应的标志位等进行设置,接下来,如果copy_process调用成功的话,那么系统会有意让新开辟的进程运行,这是因为子进程一般都会马上调用exec()函数来执行其他的任务,这样就可以避免写是复制造成的开销,或者从另一个角度说,如果其首先执行父进程,而父进程在执行的过程中,可能会向地址空间中写入数据,那么这个时候,系统就会为子进程拷贝父进程原来的数据,而当子进程调用的时候,其紧接着执行拉exec()操作,那么此时,系统又会为子进程拷贝新的数据,这样的话,相比优先执行子程序,就进行了一次“多余”的拷贝。
从上面的分析中可以看出,do_fork()的实现,主要是靠copy_process()完成的,这就是一环套一环,所以在看内核的时候,会觉得一下子跳到这,一下子又跳到那,一下子就看晕了的一个很大的原因。不过我觉得这也是linux的一大好处,因为其提高了函数的可重用行,比如本文一开始提到的几个函数的实现,归根到底,都是通过do_fork()实现的。
接着再来看看copy_process()的实现:
- p = dup_task_struct(current); 为新进程创建一个内核栈、thread_iofo和task_struct,这里完全copy父进程的内容,所以到目前为止,父进程和子进程是没有任何区别的。
- 检查所有的进程数目是否已经超出了系统规定的最大进程数,如果没有的话,那么就开始设置进程描诉符中的初始值,从这开始,父进程和子进程就开始区别开了。
- 设置子进程的状态为不可被TASK_UNINTERRUPTIBLE,从而保证这个进程现在不能被投入运行,因为还有很多的标志位、数据等没有被设置。
- 复制标志位(falgs成员)以及权限位(PE_SUPERPRIV)和其他的一些标志。
- 调用get_pid()给子进程获取一个有效的并且是唯一的进程标识符PID。
- 根据传入的cloning flags(具体表示上面有)对相应的内容进行copy。比如说打开的文件符号、信号等。
- 父子进程平分父进程剩余的时间片。
- return p;返回一个指向子进程的指针。
至此,do_fork的工作就基本结束了
写时拷贝技术:传统的fork()系统调用直接把所有的资源复制给新创建的进程。这种实现过于简单并且效率低下,因为它拷贝的数据也许并不共享,更糟的情况是,如果新进程打算立即执行一个新的映像,那么所有的拷贝都将前功尽弃。Linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。在页根本不会被写入的情况下—举例来说,fork()后立即调用exec()—它们就无需复制了。fork()的实际开销就是复制父进程的页表以及给子进程创建惟一的进程描述符。在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化可以避免拷贝大量根本就不会被使用的数据(地址空间里常常包含数十兆的数据)。由于Unix强调进程快速执行的能力,所以这个优化是很重要的。