第一章 环境
Ubuntu 14.10
Linux Kernel 3.18.6
第二章 调试fork
这里当我在用老师的视频上的断点时,我发现卡住了。这是我才发现我从系统一开始便设置了断点,这样更有趣了。
我发现我彻底卡在了内核初试化阶段。经过了30分钟才结束内核的启动。
断点如下
sys_clone
do_fork
dup_task_struct
copy_process
copy_thread
ret_from_fork
alloc_thread_info_node
第三章 分析
第一次是开始进行do_fork,在do_fork里,对一些情况进行判断。如果没有什么危险的情况,则开始进入copy_process。
copy_process函数在进程创建的do_fork函数中调用,主要完成进程数据结构,各种资源的初始化。初始化方式可以重新分配,也可以共享父进程资源,主要根据传入CLONE参数来确定。
参考资料:http://blog.csdn.net/bullbat/article/details/7088484
然而在调试的时候,我发现有两次copy_process,第一次显示copy_process是在breakpoint 4,第二次在breakpoint 3。
dup_task_struct:
调用dup_task_struct()为新进程创建一个内核栈,它的定义在kernel/fork.c文件中。该函数调用copy_process()函数。然后让进程开始运行。从函数的名字dup就可知,此时,子进程和父进程的描述符是完全相同的。
参考资料:http://www.cnblogs.com/hanyan225/archive/2011/07/09/2101962.html
copy_thread:
*childregs = *current_pt_regs();
childregs->ax = 0;
if (sp)
childregs->sp = sp;
p->thread.ip = (unsigned long) ret_from_fork;
task_user_gs(p) = get_user_gs(current_pt_regs());
p->thread.io_bitmap_ptr = NULL;
tsk = current;
这段代码类似于在第二周的迷你内核myKernel中的进程调度方法。那个是复制了三次自身,从而创建了四个进程,然后互相循环运行。只不过,那个mykernel出现的进程都是平等的,没有父子关系。而由fork()产生的则有父子关系。这里是关键代码,指明了父进程是如何启动子进程的。
int copy_thread(unsigned long clone_flags, unsigned long sp, unsigned long arg, struct task_struct *p)
{
struct pt_regs *childregs = task_pt_regs(p);
struct task_struct *tsk;
int err;
p->thread.sp = (unsigned long) childregs;
p->thread.sp0 = (unsigned long) (childregs+1);
memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));
if (unlikely(p->flags & PF_KTHREAD)) {
/* kernel thread */
memset(childregs, 0, sizeof(struct pt_regs));
p->thread.ip = (unsigned long) ret_from_kernel_thread;
task_user_gs(p) = __KERNEL_STACK_CANARY;
childregs->ds = __USER_DS;
childregs->es = __USER_DS;
childregs->fs = __KERNEL_PERCPU;
childregs->bx = sp; /* function */
childregs->bp = arg;
childregs->orig_ax = -1;
childregs->cs = __KERNEL_CS | get_kernel_rpl();
childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_FIXED;
p->thread.io_bitmap_ptr = NULL;
return 0;
}
*childregs = *current_pt_regs();
childregs->ax = 0;
if (sp)
childregs->sp = sp;
p->thread.ip = (unsigned long) ret_from_fork;
task_user_gs(p) = get_user_gs(current_pt_regs());
p->thread.io_bitmap_ptr = NULL;
tsk = current;
err = -ENOMEM;
if (unlikely(test_tsk_thread_flag(tsk, TIF_IO_BITMAP))) {
p->thread.io_bitmap_ptr = kmemdup(tsk->thread.io_bitmap_ptr,
IO_BITMAP_BYTES, GFP_KERNEL);
if (!p->thread.io_bitmap_ptr) {
p->thread.io_bitmap_max = 0;
return -ENOMEM;
}
set_tsk_thread_flag(p, TIF_IO_BITMAP);
}
err = 0;
/*
* Set a new TLS for the child thread?
*/
if (clone_flags & CLONE_SETTLS)
err = do_set_thread_area(p, -1,
(struct user_desc __user *)childregs->si, 0);
if (err && p->thread.io_bitmap_ptr) {
kfree(p->thread.io_bitmap_ptr);
p->thread.io_bitmap_max = 0;
}
return err;
}
这个函数就是复制父进程堆栈的内容到子进程的堆栈中去,并且设置一下子进程的task_struct内容,我们重点的也就是子进程系统堆栈的分析部分。特别是这句代码
((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) - 1;
P是新建的进程的task_struct结构,他是在新分配的二个页面(THREAD_SIZE)的最低下,而pt_regs是系统的保存系统调用时或者中断时寄存器的数据结构,如果P+THREAD_SIZE就代表子进程的堆栈的最顶端也就是ESP,如果转换成pt_regs结构再减去1,这里的计算结果实在是太模糊,,如果按照pt_regs结构减掉1的话就是分配了一个pt_regs,那也就是说《Linux内核情景分析》书中302页的图。这个图的理解是从系统空间堆栈中减掉一个pt_regs结构,实际上变相分配堆栈给pt_regs了。
参考资料:http://blog.chinaunix.net/uid-7960587-id-2035513.html
ret_from_fork:
对于fork来说,父子进程共享同一段代码空间,所以给人的感觉好像是有两次返回,其实对于调用fork的父进程来说,如果fork出来的子进程没有得到 调度,那么父进程从fork系统调用返回,同时分析sys_fork知道,fork返回的是子进程的id。再看fork出来的子进程,由 copy_process函数可以看出,子进程的返回地址为ret_from_fork(和父进程在同一个代码点上返回),返回值直接置为0。所以当子进 程得到调度的时候,也从fork返回,返回值为0。
关键注意两点:1.fork返回后,父进程或子进程的执行位置。(首先会将当前进程eax的值做为返回值)2.两次返回的pid存放的位置。(eax中)
参考资料:http://blog.csdn.net/guichen83/article/details/4160697
然后便是无法追踪的内核部分。具体见第五份作业。
第四章 总结
具体流程如下:
就像myKernel中的进程调度那样,通过复制父进程的相关资源和进程信息来创建子进程。然后pid设置成0来区分父子进程。说来也巧,我的系统调用便是用了fork这个例子。当时我就很疑惑,看上去这个程序运行了两遍,正巧这次的课程解答了我的疑问。最后我想说的是:fork遇上了printf函数后便会有很奇特的现象。而具体内容就看我看到的两道面试题目。
第五章 拓展:两道关于fork的面试题目(推荐第二道)
http://www.oschina.net/question/195301_62902
http://blog.chinaunix.net/uid-26495963-id-3150121.html
附录
卢晅 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000