“郭孟琦 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ”
我承认这周课上完有一种崩溃的感觉,感觉难度陡然增加。仔细想想发现,其实难度来自于linux内核的结构庞杂。换一种思路,从大框上入手去分析新进程的创建过程效果会更好一些。
其次在调试过程中发现通过gdb确实可以验证程序的执行过程,但是仅凭gdb分析不是非常的清楚,因此我将精力放在了对源码的理解上。
在例子中是采用了fork系统调用来创建一个新进程。
从效果上看既执行了pid=0的部分,又执行了pid>0的部分。
显然fork返回了2次,对于父进程pid>0,子进程pid=0;
fork系统调用的系统函数是sys_clone,但本质上是执行了
do_fork
其中
p = copy_process(clone_flags, stack_start, stack_size,child_tidptr, NULL, trace);
就是将父进程的堆栈信息复制给子进程(也包含了为子进程在内存上申请空间作为子进程的堆栈)。
主要是通过这一句完成的
p = dup_task_struct(current);而在copy_process接下来的部分是将父进程的 task_struct内容复制给子进程,其中包括了文件、内存、信号量、进程状态等信息的描述。(400多行的结构体也是开了眼了)链接:http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h#1235
而我关注的主要部分是
retval = copy_thread(clone_flags, stack_start, stack_size, p);
在copy_thread中
p->thread.sp = (unsigned long) childregs;调度后子进程堆栈的栈顶。
p->thread.ip = (unsigned long) ret_from_fork;决定了子进程中调度后将从ret_from_fork处执行
childregs->ax = 0;决定了子进程返回值(eax)为0;
这基本上就是课程所讲的内容,但是p和childregs的关系是什么?我对此探究了一下。
首先p是copy_thread的一个参数,这个p来自dup_task_struct()的返回值,同过回头对dup_task_struct()的分析发现p就是指向为子进程申请的task_struct。
而childeregs和p是在copy_thread()中通过
struct pt_regs *childregs = task_pt_regs(p);
#define task_pt_regs(t) (&(t)->thread.regs)很显然childregs是p指向的PCB的一部分。
通过
*childregs = *current_pt_regs();就实现了 将父进程堆栈的内容赋给了子进程。此时我认为子进程堆栈内容就是父进程调用SAVE_ALL后的堆栈内容。
那么子进程在调度后开始运行时就在
ret_from_fork;处执行,进入
syscall_exit
就像执行完一次系统调用,在menu中子进程就会进入0那个部分然后屏幕输出。
总结:
Linux系统创建一个新进程的过程就是自我复制的一个过程。在启动内核的时候,start_kernel先产生1号进程(init)和2号进程(kthreadd),在1号进程中又fork出其他用户态进程,而start_kernel最终初始化完内核后变为0号进程(idle)。这三个进程是代码写出来的进程。其他的进程都是通过上面所写的“复制”“修改”“调度”这样产生出来的子进程。
这样创建新进程的方法在创建一个新进程时,父进程使用的内存并不是真正的全部复制给子进程。它们都指向同一处内存空间,但是把内存页面标记为copy-on-write。当任何一个进程试图向这些内存中写入内容时,就会产生一组新的内存页面由这个进程私有。这样,通过这种方法提高了创建新进程的效率,因为内存空间的复制推迟到了发生写操作的时候。
fork是用当前的进程来复制出一个新的进程,新进程与原进程一模一样,执行的代码也完全相同,但新进程有自己的数据空间、环境变量和文件描述符,我们通常根据fork函数的返回值来确定当前的进程是子进程还是父进程,返回一个pid_t的值用于判断,我们还可以继续执行fork后面的代码。