欢迎光临散文网 会员登陆 & 注册

万字解析如何调试内核启动流程!

2023-02-23 15:16 作者:补给站Linux内核  | 我要投稿

内核生命周期

uboot 打印完 Starting kernel . . .,就完成了自己的使命,控制权便交给了 kernel 的第一条指令,也就是下面这个函数
init/main.c

start_kernel 相当于内核的 main 函数,内核的生命周期就是从执行这个函数的第一条语句开始的,直到最后一个函数 reset_init(),内核将不再从这个函数中返回,而是陷入这个函数里面的一个 while(1) 死循环,这个死循环被作为 idle 进程,也就是 0 号进程。

所以,内核的生命周期,就是一个完整的 start_kernel 函数。始于 start_kernel 函数的第一条语句,停留在最后的死循环。

init 进程

kernel 会创建众多内核线程,来持续致力于内存、磁盘、CPU 的管理,其中有两个内核线程比较重要,需要我们重点讲解,那就是 1 号内核线程 kernel_init 和 2 号内核线程 kthreadd。1 号内核线程最终会被用户的第一个进程 init 代替,也就成了 1 号进程。如下:

COMMAND 这一列,带中括号的是内核线程,不带中括号的是用户进程。从 PID 统一编址就可以看出,它俩地位是一样的。
下面我们深入分析一下从 start_kernel 到最终运行 init 进程,kernel 都经历了什么


【文章福利】小编推荐自己的Linux内核技术交流群:【749907784】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!(含视频教程、电子书、实战项目及代码)     


打印

添加打印,是分析流程的好方法。

一开始尝试在函数刚开始就添加 printk 打印,结果发现添加完 printk 后内核起不来,最后保守起见,在 131 行 console_init(); 后才开始添加打印。

可是,发现在 console_init() 之前就有不少打印了,这个地方还是有些不解。猜测要么是在 console_init() 之前就已经可以打印了;要么是在 console_init() 之前先将打印缓存着,等初始化之后再打印。这点以后再研究吧。先插个眼 。

接着看打印

从 133 行到 206 行,代码不少,打印却只有寥寥 5 行。少就说明不重要,那就不分析了,哈哈 。
看下面的代码,乖乖,就剩一句了:rest_init();,并且第 210 行的 printk 等到系统完全起来都没有打印,说明 rest_init() 就没返回。看来是个扛把子。

对应打印如下:

打印了 kernel_init(),是因为调用了 schedule_preempt_disabled(),其内部调用了 schedule() 执行进程切换,进入睡眠。当然,在当前进程被唤醒时,程序也是从这个断点开始运行。

执行进程切换后,kernel_init() 才有机会运行

对应打印如下:

可以看到从第 5 行到第 12 行区区几个函数,竟然打印了那么多,而且还看到了众多驱动相关的打印,如 usbConsoleKeyiopinctrlSerialCANRTCinputmmcmdio_bus,说明我们整天开发的驱动,是在这个阶段初始化的呀。这里我直接给出调用关系,可以看到,驱动相关的打印是在 do_initcalls() 中产生的

do_initcalls() 函数循环将编译进内核的内核模块的初始化函数依次调用一遍。这些初始化函数也是有等级的,总共有 7 个级别 1-7,数字越大,级别越低,被调用的就越晚。其中我们最长用的 module_init() 为第 6 级,等级算是比较低的了,调用也会较晚,想想也是,好多外部模块都是使用的 module_init(),它们可都是在内核完全启动后,再手动安装的呀,更晚。
这个阶段打印了很多 log,说明内核中大部分的代码都是驱动,内核中写了大量的内核模块初始化的函数。
驱动初始化完,内核也就接近尾声了

  • 挂载根文件系统

  • 内核收尾工作

  • kernel_init 内核线程调用 do_execve 函数,去执行 init 程序,产生 init 进程(1 号进程)。该内核线程终结。

rest_init() 函数的最后一句 cpu_startup_entry(CPUHP_ONLINE); 最终变成了 idle 进程(0 号进程)。

而在 kernel_init 同期稍晚创建的 kthreadd 内核线程,则一直存在,成为 2 号进程


原文作者:Li-Yongjun


万字解析如何调试内核启动流程!的评论 (共 条)

分享到微博请遵守国家法律