张建帮 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
1. 实验要求
- 使用
gdb
跟踪调试内核从start_kernel
到init
进程启动 - 结合实验截图,分析从
start_kernel
到init
进程启动的过程 - 实验使用的是
linux-3.18.6
的内核源码,点击 这里 进行源码阅读
2. 实验思路与过程
- 使用实验楼的虚拟机打开shell
在命令行中输入下列命令:
cd LinuxKernel/ qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
启动后的
menuOS
如下:可以看到程序中有些小bug:在输入
version
命令前,会自动弹出一些莫名其妙的信息- 使用gdb跟踪调试内核
输入下列命令:
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S # 关于-s和-S选项的说明:
# -S freeze CPU at startup (use ’c’ to start execution)
# -s shorthand for -gdb tcp::1234 若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项
另开一个shell
窗口,输入下面的命令:
gdb
(gdb)file linux-3.18.6/vmlinux # 在gdb界面中targe remote之前加载符号表
(gdb)target remote:1234 # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
(gdb)break start_kernel # 断点的设置可以在target remote之前,也可以在之后
(gdb)c #让内核执行至执行start_kernel之前
(gdb)list #查看当前即将执行的源代码
截图如下:
当电脑上电后时,BIOS的代码开始执行,然后是Linux初始化的代码,这其中大约很长一段时间Linux都没有进程这一概念,但是这不影响CPU执行它的二进制代码,但是由于多任务以及进程调度的需要,内核初始化过程中必须为进程以及进程调度做准备。准备工作在init/main.c
中的 start_kernel
函数中:
/*
*该函数是Linux内核的入口,其前面的代码都是用汇编编写,用以完成一些
*最基本的初始化与环境设置工作,如CPU的基本初始化,为C代码的运行设置环境等
*/
asmlinkage __visible void __init start_kernel(void)
{
..... //完成各种初始化工作,比如内存管理,进程调度,进程通信等
rest_init(); //Linux初始化的尾声,也是我们重点关注的函数
}
从start_kernel
函数的最后一个函数调用rest_init
开始,Linux开始产生进程,在 rest_init
函数中,内核将通过下面的代码产生第一个真正的进程:
static noinline void __init_refok rest_init(void)
{
.....
/*
* We need to spawn init first so that it obtains pid 1, however
* the init task will end up wanting to create kthreads, which, if
* we schedule it before we create kthreadd, will OOPS.
*/
kernel_thread(kernel_init, NULL, CLONE_FS); //创建一个内核线程或者说是内核进程,该进程创建完成后,就开始运行括号中的kernel_init函数
.....
}
kernel_init函数函数最有意思的地方在于它会通过调用kernel_execve来执行根文件系统下的/sbin/init文件(所以此前系统根文件系统必须已经就绪),其代码如下:
930 static int __ref kernel_init(void *unused)
931 {
......
//启动1号进程,init process,它也是所有用户态进程的祖先
965 if (!try_to_run_init_process("/sbin/init") ||
966 !try_to_run_init_process("/etc/init") ||
967 !try_to_run_init_process("/bin/init") ||
968 !try_to_run_init_process("/bin/sh"))
969 return 0;
970
971 panic("No working init found. Try passing init= option to kernel. "
972 "See Linux Documentation/init.txt for guidance.");
973 }
这里的try_to_run_init_process
就是通过execve()
来运行init程序。这里首先运行“/sbin/init”,如果失败再运行“/etc/init”,然后是“/bin/init”,然后是“/bin/sh”(也就是说,init可执行文件可以放在上面代码中寻找的4个目录中都可以)。这里是内核初始化结束并开始用户态初始化的阴阳界。
至于0号进程init_task,它属于一个比较特殊的进程,它的初始化工作在init_task.c
文件中完成,其内核栈通过静态方式分配。在start_kernel
函数中的sched_init()
函数调用中,会通过init_idle(current, smp_processor_id())
函数把init_task
初始化成为一个idle task
,init_idle
函数的第一个参数current就是&init_task
,在init_idle
中将会把init_task
加入到cpu的运行队列中,这样当运行队列中没有别的就绪进程时,init_task
(也就是idle task
)将会被调用,它的核心是一个死循环,在循环中它将会调用schedule函数以便在运行队列中有新进程加入时切换到该新进程上。具体的过程如下所示:
3. 实验总结
这次实验重点在于linux中启动过程的进程产生于分析,正如孟宁老师所说,“道生一(start_kernel….cpu_idle),一生二(kernel_init和kthreadd),二生三(即前面0、1和2三个进程),三生万物(1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先),这些“都符合中国传统文化精神了”,也就不难让人理解了。
参考文章:
http://blog.csdn.net/hardy_2009/article/details/7383815
http://blog.csdn.net/gatieme/article/details/51484562