burningcodes  原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

1.进程管理中的关键数据结构

在进程管理中用到了几个关键的数据结构,这里先给出关键结构间的大致关系:

ss

最主要的是三个数据结构:

thread_union是一个联合体,它由thread_info和进程内核栈共同占有,thread_info结构大小固定并且起始地址和thread_union重合,内核栈stack由于是往低地址增长,所以起始地址是thread_union的尾部。在创建进程时,内核会为每个进程创建一个thread_union的结构体(大小为8K)。由于thread_union一般情况下是放在连续的8k的页面中,所以通过esp可以很方便的找到这个thread_unoion的起始地址,只需要屏蔽esp的低13位即可找到起始地址(esp总是在这个8k的范围内移动)。

thread_info中保存了进程的一些关键信息,其中最主要的是指向该进程PCB(task_struct)的指针。

task_struct即是进程PCB,它保存了进程的基本信息,比如进程状态,pid,打开的文件等。

2.通过fork理解Linux中的进程创建

执行fork时会创建一个与父进程基本相同的子进程,并且这两个进程的返回点都是fork的下一条指令。为什么fork会创建一个子进程?为什么父子进程的返回点都一样?内核是如何创建一个新进程的?

要想回答上面的问题就得从源码中找答案,在跟踪具体的代码前,先看一张fork调用的基本执行流程:

fork

由于代码跳转的地方比较多,在看具体的代码可以通过这张图对整理流程有个大致的印象。

在用fork,clone,vfork创建新进程的时候最终会调用do_fork来完成具体的fork操作(/linux-3.18.6/kernel/fork.c#1623):

do_fork函数通过调用copy_process为新进程所必要的一些结构,最主要的是用dup_task_struct拷贝了源进程的task_struct结构体(linux-3.18.6/kernel/fork.c#copy_process):

接下来具体看看dup_task_struct函数(linux-3.18.6/kernel/fork.c#dup_task_struct):

这个函数首先用alloc_task_struct_node创建了新进程的task_struct,然后通过alloc_thread_info_node为新进程创建thread_info, 然后再将thread_info中的内核栈起始地址赋值给task_struct中的stack变量。

在调用完dup_task_struct之后调用了copy_thread(linux-3.18.6/arch/x86/kernel/process_32.c#132):

从copy_thread中可以看出,子进程只复制了父进程内核栈中的部分内容,因为要保证返回时堆栈内容和调用时相同,拷贝的内容为pt_regs结构体具体见(linux-3.18.6/arch/x86/include/asm/ptrace.h#11) 。

接下来就要看看子进进程返回到哪了,子进程的ip被设置成了ret_from_frok(linux-3.18.6/arch/x86/kernel/entry_32.S#290):

ret_from_fork的最后返回到了syscall_exit,而syscall_exit所在的上下文即是ENTRY(system_call),子进程在返回时的内核栈要与之前的相同,这也是为什么新进程只拷贝了父进程部分内核栈的原因(/linux-3.18.6/arch/x86/kernel/entry_32.S#490):

3.gdb跟踪fork的执行流程

从git上更新下menu目录后MenuOS即会多出fork命令。调试前要先为gdb添加调试信息:

具体的调试过程见:从源码中跟踪Linux Kernel的启动过程。下好断点如下:

gdb0

断点执行到copy_thread:

genz

具体的跟踪过程和上面对源代码的分析基本一致,这也验证了上文对fork执行的理解。

4.总结

通过对fork函数的分析,加深了对Linux内核进程创建的理解。在创建进程时首先得为新进程创建一些结构体(task_struct等)用于保存新进程的基本信息 ,然后为新进程指定执行入口点(eip),在初始化好之后由调度器来调度各个进程,新进程从创建时指定的入口开始执行,而老进程从上次切换前保存的地址开始执行。

 

观看更多有关 的文章?

*

+
跳转到评论