一.进程复制
- fork建立了父进程的唯一完整副本,作为子进程执行。
- vfork 类似于fork 函数,与父进程共享地址空间,并不创建副本
二.写时复制
- 内核采用了写时复制技术,以防止fork执行时将父进程的所有数据复制到子进程
- 在早期的fork函数中,内核对于父进程的每个内存页,都会为子进程创建一个相同的副本。这样会使用了大量内存,复制操作耗费了很长时间。
- 内核通过子进程和父进程共享同一内存块,只有当对于子进程的段进行写入的时候,再为子进程进行分配
原理:
- fork()之后,内核把父进程的所有内存页都设为只读,把子进程的地址空间指向父进程。当父子进程都只读内存,相安无事。当进程写内存,触发缺页中断,内核会把触发异常的页复制一份,父子进程都拥有一份
好处
- 可减少分配和复制大量资源带来的延时。
- 减少不必要的资源分配。fork之后并不是所有的页都需要复制。
缺点
- fork之后,父子进程都还需要继续进行写操作,会产生大量的分页错误
三.fork函数都做了什么?
- fork.vfork 系统调用的入口分别是
sys_fork,sys_vfork
函数 - asmlinkage int sys_fork(struct pt_reges regs)
{
return do_fork(SIGCHLD,regs,esp,®s,0,NULL,NULL);
} - do_fork的流程
do_fork通过调用copy_process开始,后者执行生成新进程的实际工作,并根据指定的标志重用父进程的数据
但在子进程生成之后,内核必须执行下列收尾操作:
-
由于fork要返回新进程的PID,因此必须获得PID。如果设置CLONE_NEWPID标志,需要调用task_pid_nr_ns获取新进程的PID,不然我们只需要调用task_pid_vnr获取局部的PID即可,因为新旧进程都在同一个命名空间中。
-
nr = (clone_flags & CLONE_NEWPID) ? task_pid_nr_ns(p,current->nsproxy->pid_ns) : task_pid_var ( p )
-
wake_up_new_task 将task_struct 添加到调度器队列。
-
如果使用vfork机制,必须启动子进程的完成机制,子进程的task_struct 的vfork_done 用于此. 通过这种办法可以阻塞父进程处于不活动的状态。
我们接下来来看看copy_process
都做了什么?
- copy_process开始会检查一些标志,比如说CLONE_SIGHAND激活信号共享。通常情况下,一个信号无法发送到线程组的各个线程
- 会通过dup_task_struct 来建立父进程的task_struct 的副本,父子进程中的task_struct 实例只有一个成员不同: 新进程分配了一个新的核心态栈,task_struct->stack 。
- 检查并确保创建子进程后 ,进程数目没有超出限制。
- 进程描述符内的许多成员被清0或设为初始值。不是继承而来的进程描述符成员,主要是统计信息。
- 子进程状态被设置为TASK_UNINTERRUPTIBLE。
- 调用copy_flags()更新task_struct的成员flags。PF_SUPERPRIV(是否拥有超级用户权限)被清0,PF_FORKNOEXEC(进程还没有调用exec)被设置。
- 调用alloc_pid()为新进程分配一个有效的PID。
- 根据传递给clone()的参数标志,拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。
- 我们也可以通过调用接口函数sched_fork,让调度器有机会对于新进程进行设置。
- 做扫尾工作并返回一个指向子进程的指针。
总结 : do_fork()函数在copy_process()函数返回后,唤醒新创建的子进程并让其投入运行。