在Linux系统下,fork()的实现是通过调用clone()实现的,这一调用通过不同的参数来指明父子进程之间需要共享的资源。
其中fork、vfork、_clone库函数都是通过系统调用clone(),然后再由clone调用do_fork()实现。
dofork()完成了创建的大部分工作,定义在kernel/fork.c中。
首先定义一个task_struct类型的指针p,每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。他会被装载在RAM中并包含进程信息。他会保存进程的状态信息(即每个进程会将其信息存放在task_struct结构体中)。
通过调用alloc_pidmap()来为子进程分配一个新pid。
检查父进程是否被跟踪,如果为真,就检查debugger程序是否想跟踪子进程,并且子进程不是内核进程(CLONE_ UNTRACED未设置),那么就设置CLONE_PTRACE.CLONE_ UNTRACED防止进程在子进程上强制执行CLONE_PTRACE(继续调试子进程)。
调用copy_process()函数来复制进程描述符。
调用dup_task_struct为子进程创建一个内核栈、thread_info结构和task_struct这些值与当前进程相同。父子进程此时进程描述符完全相同。
alloc_task_struct宏为新进程获取进程描述符。
这里宏替换的是kmem_cache_alloc这个函数,定义在slab.c里,其返回的是__cache_alloc这个函数。
通过alloc_thread_info宏获取一块空闲内存区,存放新进程的thread_info结构和内核栈注意这块内存区字段大小为8KB或4KB。
将父进程的进程描述符复制到tsk所指向的task_struct结构中,并把thread_info置成ti
将父进程的thread_info结构复制到ti中,并将ti_task置成tsk
检查新创建子进程后,用户所拥有的进程数目没有超过可分配数目
然后子进程就要和父进程区分开来,这一阶段进程描述符中的很多成员都将被清零或初始化。注意进程描述符的成员不是继承而来,而是统计信息。进程描述符中大多数都是共享的
调用copy_flags,更新tsk_struct结构中的flags成员,表明进程是否拥有超拥护级权限,并表明进程还没有调用exec函数的PF_FORKNOEXEC标志被设置
为新进程获取有效的pid(内核设计第三版中是以getpid的方式获取有效的pid,这里应该是通过查找pidmap图,来获取有效的pid)
根据传递给clone的参数标志,copy_proess拷贝或共享打开的文件、系统信息、信号处理函数、进程地址空间和命名空间等,一般情况下,这些信息也会被给定的所有线程共享。否则,这些资源对待每个进程都是不同的,所以被拷贝在这里。
copy_files 拷贝文件描述符
copy_mm 拷贝内存描述符,描述进程地址空间的所以信息。
用父进程的现场信息初始化子进程的现场信息,并将子进程的寄存器的值强制改为0,这就是子进程fork返回为0的原因。
调用sched_fork将状态设置为就绪后让父子进程平分剩余的时间片。
最后copy_process返回一个子进程的指针。
回到dofork后,如果copy_process返回成功,那么将新进程唤醒并投入运行。
注意内核有意让子进程优先运行(但事实上并非总是如此),一般子进程都会优先调用exec函数,避免写实拷贝的额外开销。如果父进程优先进行,就会造成地址空间的写入。