分析fork函数对应的内核处理过程sys_clone,理解创建一个新进程如何创建和修改task_struct数据结构
Linux中创建进程一共有三个函数:
fork :创建子进程
vfork:和fork类似
clone:创建线程
进程创建过程:
SYSCALL_DEFINE0(fork)
{
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
}
#endif
SYSCALL_DEFINE0(vfork)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
0, NULL, NULL);
}
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
{
return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}
结论::fork、vfork和clone这三个函数最终都是通过do_fork函数实现的。
分析do_fork的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
|
结论:
do_fork处理了以下内容:
1. 调用copy_process,将当期进程复制一份出来为子进程,并且为子进程设置相应地上下文信息。 2. 初始化vfork的完成处理信息(如果是vfork调用) 3. 调用wake_up_new_task,将子进程放入调度器的队列中,此时的子进程就可以被调度进程选中,得以运行。 4. 如果是vfork调用,需要阻塞父进程,知道子进程执行exec。
进程创建的关键copy_process:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
|
分析copy_process的大体流程:
检查各种标志位(已经省略)
调用dup_task_struct复制一份task_struct结构体,作为子进程的进程描述符。
检查进程的数量限制。
初始化定时器、信号和自旋锁。
初始化与调度有关的数据结构,调用了sched_fork,这里将子进程的state设置为TASK_RUNNING。
复制所有的进程信息,包括fs、信号处理函数、信号、内存空间(包括写时复制)等。
调用copy_thread,这又是关键的一步,这里设置了子进程的堆栈信息。
为子进程分配一个pid
设置子进程与其他进程的关系,以及pid、tgid等。这里主要是对线程做一些区分。
在copy_process中,copy_thread函数为子进程准备了上下文堆栈信息
copy_thread的流程如下:
1. 获取子进程寄存器信息的存放位置
2. 对子进程的thread.sp赋值,将来子进程运行,这就是子进程的esp寄存器的值。
3. 如果是创建内核线程,那么它的运行位置是ret_from_kernel_thread,将这段代码的地址赋给thread.ip,之后准备其他寄存器信息,退出
4. 将父进程的寄存器信息复制给子进程。
5. 将子进程的eax寄存器值设置为0,所以fork调用在子进程中的返回值为0.
6. 子进程从ret_from_fork开始执行,所以它的地址赋给thread.ip,也就是将来的eip寄存器。
创建的新进程是从哪里开始执行的?
——ret_from_fork
*childregs = *current_pt_regs(); //复制内核堆栈
childregs->ax = 0; //为什么子进程的fork返回0,这里就是原因!
p->thread.sp = (unsigned long) childregs; //调度到子进程时的内核栈顶
p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址
ip指向的是ret_from_fork,所以是从这里开始执行的。
复制内核堆栈的时候是复制的pt_regs,即只复制了SAVE_ALL相关的那一部分,即系统调用压栈的那一部分。
pt_regs里面内容有:
Entry(ret_from_fork):
最终会跳转到syscall_exit,这之前的内核堆栈状态和syscall_call的一致,然后返回用户态,变成子进程的用户态。
实验截图
实验总结 :见第六周总结