分析Linux内核创建一个新进程的过程
李洋 原创作品转载请注明出处
《Linux内核分析》MOOC课程 http://mooc.study.163.com/course/USTC-1000029000
系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。整个linux系统的所有进程也是一个树形结构。树根是系统自动构造的,即在内核态下执行的0号进程,它是所有进程的祖先。由0号进程创建1号进程(内核态),1号负责执行内核的部分初始化工作及进行系统配置,并创建若干个用于高速缓存和虚拟主存管理的内核线程。随后,1号进程调用execve()运行可执行程序init,并演变成用户态1号进程,即init进程。
而fork()允许用户态下创建新的进程, fork 创造的子进程复制了父亲进程的资源,包括内存的内容task_struct内容,新旧进程使用同一代码段,复制数据段和堆栈段,这里的复制采用了注明的copy_on_write技术,即一旦子进程开始运行,则新旧进程的地址空间已经分开,两者运行独立。
在 Linux 内核中,供用户创建进程的系统调用fork()函数的响应函数是 sys_fork()、sys_clone()、sys_vfork()。这三个函数都是通过调用内核函数 do_fork() 来实现的。根据
调用时所使用的 clone_flags 参数不同,do_fork() 函数完成的工作也各异。下面结合实验过程简要分析do_fork()是怎么工作的,首先是代码以及简析如下:
/* 1618 * Ok, this is the main fork-routine. 1619 * 1620 * It copies the process, and if successful kick-starts 1621 * it and waits for it to finish using the VM if required. 1622 */ 1623long do_fork(unsigned long clone_flags, 1624 unsigned long stack_start, 1625 unsigned long stack_size, 1626 int __user *parent_tidptr, 1627 int __user *child_tidptr) 1628{ 1629 struct task_struct *p; 1630 int trace = 0; 1631 long nr; 1632 1633 /* 1634 * Determine whether and which event to report to ptracer. When 1635 * called from kernel_thread or CLONE_UNTRACED is explicitly 1636 * requested, no event is reported; otherwise, report if the event 1637 * for the type of forking is enabled. 1638 */ 1639 if (!(clone_flags & CLONE_UNTRACED)) { 1640 if (clone_flags & CLONE_VFORK) 1641 trace = PTRACE_EVENT_VFORK; 1642 else if ((clone_flags & CSIGNAL) != SIGCHLD) 1643 trace = PTRACE_EVENT_CLONE; 1644 else 1645 trace = PTRACE_EVENT_FORK; 1646 1647 if (likely(!ptrace_event_enabled(current, trace))) 1648 trace = 0; 1649 } 1650 //创建进程描述符以及子进程所需要的其他所有数据结构 1651 p = copy_process(clone_flags, stack_start, stack_size, 1652 child_tidptr, NULL, trace); 1653 /* 1654 * Do this prior waking up the new thread - the thread pointer 1655 * might get invalid after that point, if the thread exits quickly. 1656 */ 1657 if (!IS_ERR(p)) { 1658 struct completion vfork; 1659 struct pid *pid; 1660 1661 trace_sched_process_fork(current, p); 1662 1663 pid = get_task_pid(p, PIDTYPE_PID); 1664 nr = pid_vnr(pid); 1665 1666 if (clone_flags & CLONE_PARENT_SETTID) 1667 put_user(nr, parent_tidptr); 1668 1669 if (clone_flags & CLONE_VFORK) { 1670 p->vfork_done = &vfork; 1671 init_completion(&vfork); 1672 get_task_struct(p); 1673 } 1674 1675 wake_up_new_task(p);//新进程加入运行队列,并启动调度程序重新调度,使得新进程获得运行机会 1676 1677 /* forking complete and child started to run, tell ptracer */ 1678 if (unlikely(trace)) 1679 ptrace_event_pid(trace, pid); 1680 1681 if (clone_flags & CLONE_VFORK) { 1682 if (!wait_for_vfork_done(p, &vfork)) 1683 ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid); 1684 } 1685 1686 put_pid(pid); 1687 } else { 1688 nr = PTR_ERR(p); //出错处理 1689 } 1690 return nr; 1691}
实验中几个关键步骤截图:
首先是运行到copy_process():
代码可以查看这里:http://codelab.shiyanlou.com/xref/linux-3.18.6/kernel/fork.c#copy_process
下面描述其最重要的处理步骤:
1. 检查参数clone_flags所传递标志的一致性。
2. 通过调用security_task_create(clone_flags)函数以及稍后的security_task_alloc(p)函数执行所有附加的安全检查。
3. 调用dup_task_struct(current)函数(也是来自/kernel/Fork.c)为子进程获得进程描述符。
4. 检查存放在p->signal->rlim[RLIMIT_NPROC].rlim_cur变量中的值是否小于或等于用户所拥有的进程数。
6. 查系统中的进程数量(存放在nr_threads变量中)是否超过max_threads变量的值。这个变量的缺省值取决于系统内存容量的大小。总的原则是:所有thread_info描述符和内核栈所占用的空间不能超过物理内存大小的1/8。不过,系统管理员可以通过写/proc/sys/kernel/threads-max文件来改变这个值。
7. 如果实现新进程的执行域和可执行格式的内核函数都包含在内核模块中,则递增它们的使用计数器。
8. 设置与进程状态相关的几个关键字段.
9. 把新进程的PID存入tsk_pid字段。
10. 如果clone_flags参数中的CLONE_PARENT_SETTID标志被设置,就把子进程的PID复制到参数parent_tidptr指向的用户态变量中。
11. 初始化子进程描述符中的list_head数据结构和自旋锁,并为与挂起信号、定时器及时间统计表相关的若干字段赋初值。
12. 调用copy_semundo、copy_files、copy_fs、copy_sighand、copy_signal、copy_mm和copy_namespace来创建新的数据结构,并把父进程的相应数据结构的值复制到新数据结构中,除非clone_flags参数指出它们有不同的值。
13. 调用copy_thread(0, clone_flags, stack_start, stack_size, p, regs),用发出clone()系统调用时CPU寄存器的值来初始化子进程的内核栈。不过,copy_thread把eax寄存器对应的字段(这也是fork和clone系统调用在子进程中的返回值)字段强行置为0。进程描述符的thread.esp字段初始化为子进程内核栈的基地址,汇编语言函数ret_from_fork()的地址存放在thread.eip字段中。如果父进程使用I/O权限位图,则子进程获取该位图的一个拷贝。最后,如果CLONE_SETTLS标志被设置,则子进程获取由clone系统调用的参数tls指向的用户态数据结构所表示的TLS段。
14. 如果clone_flags参数的值被置为CLONE_CHILD_SETTID或CLONE_CHILD_CLEARTID,就把child_tidptr参数的值分别复制到tsk->set_child_tid或tsk->clear_child_tid字段。这些标志说明:必须改变子进程用户态地址空间的child_tidptr所指向的变量的值,不过实际的写操作要稍后再执行。
15. 清除子进程thread_info结构的TIF_SYSCALL_TRACE标志,以使ret_from_fork()函数不会把系统调用结束的消息通知给调试进程。
16. 用clone_flags参数低位的信号数字编码初始化tsk-> exit_signal字段,如果CLONE_THREAD标志被设置,就把tsk-> exit_signal字段初始化为-1。正如我们将在下一章进程终止所看见的,只有当线程组的最后一个成员(通常是现在组的头儿)死亡,才会产生一个信号,以通知领头进程的父进程。
17. 调用sched_fork(p)完成对新进程调度程序数据结构的初始化。该函数将新进程的状态设置为TASK_RUNNING,并把thread_info结构的preempt_count字段设置为1,从而禁止内核抢占。此外,为了保证公平的进程调度,该函数在父子进程之间共享父进程的时间片。
18. 把新进程的thread_info结构的cpu字段设置为由smp_processor_id()所返回的本地CPU号。
19. 初始化表示亲子关系的字段。尤其是,如果CLONE_PARENT或CLONE_THREAD被设置,就用current->real_parent的值初始化tsk->real_parent和tskp->parent,因此,子进程的父进程似乎是当前进程的父进程。否则tsk->real_parent和tskp->parent置为当前进程。
20. 如果不需要跟踪子进程(没有设置CLONE_PTRACE标志),就把tsk->ptrrace字段设置为0。 tsk->ptrrace字段会存放一些标志,而这些标志是在一个进程被另外一个进程跟踪时才会用到的。采用这种方式,即使当前进程被跟踪,子进程也不会被跟踪。
21. 执行SET_LINKS宏,把新进程描述符插入进程链表。
22. 如果子进程必须被跟踪(tsk->ptrrace字段的PT_PTRACED标志被设置),就把current->parent赋给tsk->parent,并将子进程插入调试程序的跟踪链表中。
23. 调用attach_pid把新进程描述符PID插入pidhash[PIDTYPE_PID]散列表。
24. 如果子进程是领头进程(CLONE_THREAD标志被清0),否则,如果子进程属于它的父进程的线程组(CLONE_THREAD标志被设置).
25. 现在,新进程已经被加入进程集合:递增nr_threads变量的值。
26. 递增total_forks变量以记录被创建的进程的数量。
27. 终止并返回子进程描述符指针(p,等价于tsk)。
然后执行wake_up_new_task,把新进程加入运行队列,并启动调度程序重新调度,使新进程获得运行机会:
1.调整父进程和子进程的调度参数
2.如果子进程和父进程运行在同一个CPU上,而且父进程和子进程不能共享同一组页表(CLONE_VM标志被清0),那么,就把子进程插入到父进程的运行队列,插入时让子进程恰好在父进程前面,因此迫使子进程优于父进程先运行。如果子进程刷新其地址空间,并且在创建之后执行新程序,那么这种简单的处理会产生较好的性能。而如果我们让父进程先运行,那么写时复制机制将会执行一些不必要的页面复制。
3.否则,如果子进程与父进程运行在不同CPU上,或者父进程和子进程共享同一组页表(CLONE_VM标志被设置),就把子进程插入父进程所在运行队列的队尾。
最后会执行一些出错处理,返回出错信息。