我们接触linux用户编程时,在做多进程程序处理时绕不过去的就是fork调用,经常被告诉它的特殊性:一次调用两次返回,父进程中返回子进程的pid,子进程返回0,和同事讨论的时候,走查代码的时候竟然没有找到子进程是如何返回的。
父进程返回的路径非常清晰,进入内核中do_fork->copy_process,成功之后将新的进程wakeup放到运行队列上,返回子进程的pid。但是子进程从哪返回的呢?
在整个fork的过程中,do_fork只是检查了一些参数的合法性,具体的工作还是交给了copy_process去完成
为子进程分配和初始化一个新的task_struct结构,基本过程是先分配一个新的task_struct,然后将父进程的所有内容统统复制过去,
其中所有的值信息和父进程中一致,指针指向父进程中的资源;
之后就开始分裂,为进程独立的信息分配对象,拷贝父进程的对象信息,对进程中某些信息进行清零,特别是统计信息,初始化自己的管理结构。
1) 从父进程中复制进程独立的信息
1.1) 进程组和会话信息
1.2) 信号状态(忽略、捕获、阻塞信号的掩码)
1.3) nice调度参数
1.4) 对父进程用户凭据的引用
1.5) 对父进程打开文件的引用(文件句柄表及相关引用数据结构,所以父子进程可以共享打开的文件)
1.6) 对父进程限制(resources limitation)的引用
1.7) 拷贝父进程的mm信息,复制mm_struct,vma_area_struct和页表信息,其中在复制pte时实现cow特性,将父子进程中的pte写权限全部清除
1.8) 构建内核栈信息,因为用户栈信息已经在1.7中复制过了
2) 清零统计信息
2.1) cpu利用率统计时间
2.2) 定时器
2.3) ptrace,trace信息
2.4) 挂起信号的信息
3)初始化子进程管理信息
分配新的pid,初始化task_struct管理链表:
1.挂到全局的进程链表中,
2.和父进程链接形成树状管理,
3.放到hash表中可以通过pid快速查找
其中在1.8中在拷贝内核栈信息的时候通过copy_thread_tls,最重要的就是构建内核栈信息,一个是如何返回到用户空间,因为父进程接下来还有继续修饰子进程,它不能共享这部分过程,所以设置了它的ret_addr为ret_from_fork。内核的实现会反复变化,早期的内核通过直接设置rip寄存器为ret_from_fork。
设置内核堆栈,尤其是返回地址、栈指针寄存器,帧基址寄存器
p->thread.sp0 = (unsigned long)task_stack_page(p) + THREAD_SIZE;
childregs = task_pt_regs(p);
fork_frame = container_of(childregs, struct fork_frame, regs);
frame = &fork_frame->frame;
frame->bp = 0;
frame->ret_addr = (unsigned long) ret_from_fork; //call指令会自动将返回地址入栈,现在栈上存放返回地址的地方设置成它,当ret指令时触发。它清理标志能够被抢占和调度,之后就通过系统调用放回路径回到用户空间
p->thread.sp = (unsigned long) fork_frame; //栈指针指向fork_frame,里面低地址存存放一帧的寄存器,高地址存放用户空间寄存器
//复制父进程的寄存器状态
savesegment(gs, p->thread.gsindex);
p->thread.gsbase = p->thread.gsindex ? 0 : me->thread.gsbase;
savesegment(fs, p->thread.fsindex);
.。。。
frame->bx = 0; //
下面设置用户空间时寄存器为0,也就是fork之后子进程返回0
*childregs = *current_pt_regs(); //复制父进程的用户空间寄存器状态:指令寄存器,栈寄存器rsp,由于父子进程的代码和数据是共享的,所以在返回后将继续执行
childregs->ax = 0; //设置返回值为0,也就是fork在子进程中返回0
总结一下fork的过程:
参考链接:https://www.cnblogs.com/LittleHann/p/3853854.html
- 父子进程从同一个代码位置开始继续执行: 因为它们的"进程上下文"相同
- 父进程调用fork()返回子进程的PID: 父进程是正常调用
- 子进程返回0,因为内核态的EAX被设置为了0
- 父子进程不一定同时开始执行,但会有从内核态返回2次,一次是父进程,一次是子进程