一、跟踪分析内核的启动过程实验:
1.启动Menuos:
qemu仿真kernel:
知识补充:
- bzImage 是 vmlinux 经过 gzip 压缩后的文件,是压缩的内核映像,“b”代表的是“big”(bzImage 适用于大内核,zImage 适用于小内核)。vmlinux 是编译出来的最原始的内核ELF文件;
- 根文件系统包括虚拟根文件系统和真实根文件系统。initrd 是“initial ramdisk”的简写,boot loader 将存储介质中的 initrd 文件加载到内存,内核启动时先访问 initrd 文件系统(虚拟的文件系统),然后再切换到真实的文件系统;
2.调试跟踪:
-S 开始处冻结CPU,方便调试
-s 使用tcp端口1234来进行通讯,将进程信息传过去(在后面的调试中会用到)。若不想使用1234端口,可以使用-gdb tcp:xxxx来取代-s选项
启动gdb,把内核加载进来,建立连接:
file linux-3.18.6/vmlinux
target remote:1234
实践过程中出现以下错误:
连接超时应该是没连接上冻结的系统,检查后发现进入gdb调试前讲QEMU窗口关闭了,打开后得以解决:
在 start_kernel 处设置断点,继续执行,停在断点处:
在 rest_init 处设置断点,继续执行,停在断点处:
3.内核启动分析(自己的大致理解):
(1)start_kernel()
main.c 中没有 main 函数,start_kernel() 相当于是C中的main函数。start_kernel是一切的起点,在此函数被调用之前内核代码是用汇编语言写的,完成系统的初始化工作,为c代码的运行设置环境。由调试可得 start_kernel 在500行:
(2)init_task()
start_kernel() 函数几乎涉及到了内核的所有模块,如:trap_init()(中断向量的初始化)、mm_init()(内存管理的初始化)sched_init()(调度模块的初始化)等,首先是510行的init_task():
struct task_struct init_task = INIT_TASK(init_task);
可以看出 init_task(0号进程)是 task_struct 类型,是进程描述符,使用宏INIT_TASK对其进行初始化。接下来就是对各种模块的初始化:
(3)rest_init()
通过rest_init()新建kernel_init、kthreadd内核线程:
403行代码 kernel_thread(kernel_init, NULL, CLONE_FS);
,由注释得调用 kernel_thread()创建1号内核线程(在 kernel_init 函数正式启动):
注:对比 init_task 和 kernel_thread()
kernel_thread()是 fork 出了一个新进程来执行kernel_init 函数,而 init_task 是使用宏进行初始化的。也就是说0进程不是系统通过 kernel_thread 的方式(也就是 fork)创建的(init_task 是唯一一个没有通过 fork()产生的进程)。
405行代码 pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
调用 kernel_thread()执行 kthreadd函数,创建 PID=2的内核线程:
此函数的任务是管理和调度其他内核线程 kernel_thread。for 循环中运行 kthread_create_list 全局链表中维护的 kthread, 在create_kthread()函数中,会调用 kernel_thread 来生成一个新的进程并被加入到此链表中,因此所有的内核线程都是直接或者间接的以 kthreadd 为父进程。
4.总结:
- init_task()(PID=0)在创建了init进程后,调用 cpu_idle() 演变成了idle进程,执行一次调度后,init进程运行;
- 1号内核线程负责执行内核的部分初始化工作及进行系统配置,最后调用do_execve执行 init 函数,演变成 init 进程(用户态1号进程),init 进程是内核启动的第一个用户级进程;
- kthreadd(PID=2)进程由0号进程创建,始终运行在内核空间, 负责所有内核线程的调度和管理。