程峰 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
今天我们来探索一下内核的启动过程,使用的是 linux-3.18.6 的源码,可以在这里在线阅读。
打开内核源码,你会看到很多目录,是不是有点眼花缭乱,这些目录到底都是干什么用的呢?
main.c
、创建早期用户空间的代码以及其他初始化代码。sprintf
和atoi
的系列函数。与 arch/lib 下的代码不同,这里的库代码都是使用C编写的,在内核新的移植版本中可以直接使用。哎呀,好多好乱,我自己也搞晕了...其实这里我们只需要重点关注arch\x86
、init
和kernel
三个目录,其它的可以暂时忽略,免得把自己看晕了。
这里我们使用实验楼上已经搭建好的实验环境。(不知道是不是网络原因,我用的时候实验楼好卡好卡,所以大家也可以参照这张图片在自己电脑上搭建实验环境。)
进入实验目录cd LinuxKernel/
,使用如下命令启动内核:
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
注意后面的-s
和-S
选项:
-s
是-gdb tcp::1234
的缩写,是指在 tcp 的1234端口上创建了一个gdb server。-S
是在内核启动之前将其冻结起来,方便后续调试。然后,打开另一个 shell 窗口,启动 gdb 。
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 之前,也可以在之后
这里的 start_kernel 函数就相当于我们平时写的 C 程序的 main 函数,是内核初始化的入口函数。到这一步,我们便可以进行调试了。(因为实验环境太卡太卡...所以后面就没有贴 gdb 调试的截图了,我们直接在源码上分析,还请大家谅解。)
我们从start_kernel
函数开始分析内核的启动过程。因为start_kernel
内容比较多,我们选择其中的几个重要的初始化部分进行解释。
首先是init_task
,它是一个全局变量,是0号进程的PCB,在此初始化。0号进程是一个特殊的进程,它是内核开发者人为制造出来的,而不是其他进程通过do_fork来完成。
然后是561行的trap_init()
函数,对应源码在/linux-3.18.6/arch/x86/kernel/traps.c
的792行。下面是它的部分代码:
该函数完成对一些中断向量的初始化,其中设置了很多中断向量门,其中比较重要的是839行的set_system_trap_gate
,设置系统陷阱门,即系统调用。
初始化完中断向量后,开始初始化内存模块mm_init()
和进程调度模块sched_init
,在sched_init
中初始化了0号进程,即idle
进程,但是并没有设置idle进程的NEED_RESCHED标志,所以还会继续完成内核初始化剩下的事情。
在完成大部分模块的初始化后,start_kernel
函数的最后还有一个rest_init
初始化函数,这个函数到底初始化了哪些东西呢?我们来看看。
rest_init
函数做了一件非常重要的事,它调用系统函数kernel_init
创建了1号用户态进程init
,它是所有用户态进程的祖先。同时它还创建了一个内核线程kthreadd
用来管理系统的资源。最后调用cpu_startup_entry
函数启动0号进程,当系统没有进程需要执行时就调度到idle进程。
idle
进程从内核启动后就会一直存在,相当于 windows 系统上的 systemidle 进程,只不过在 linux 系统下我们看不到,idle
进程创建了1号进程init
,还创建了一些内核线程。这样,系统就启动起来了。