《Linux内核分析》(三)——跟踪分析Linux内核的启动过程

时间:2022-09-30 04:46:15

作者:Sandy 原创作品转载请注明出处
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
实验环境:c+Linux64位 (32位系统可能结果会不同)
依照学术诚信条款,我保证此回答为本人原创,所有回答中引用的外部材料已经做了出处标记。


实验环境:ubuntu14.04操作系统,x86体系结构
实验要求:使用gdb跟踪调试内核从start_kernel到init进程启动


第一步,Linux内核代码结构

本课程提供了一个Linux的内核源码,其结构如下图:

《Linux内核分析》(三)——跟踪分析Linux内核的启动过程

内核地址地址:http://codelab.shiyanlou.com/xref/linux-3.18.6/

不同的文件夹代表了内核的不同模块,其含义是:

  • arch/ 是体系结构相关的代码,其中的/x86 文件夹下的内容是x86体系结构相关代码,是内核分析的重要分析目标。
  • init/ 是内核启动相关的代码,是本文的重点分析对象。/init/main.c 文件是内核启动的起点,是分析内核启动流程的首要分析对象。
  • fs/ 文件系统(file system?)
  • kernel/ 内核相关的代码,一些内核中使用到的结构体、函数等重要对象的定义都在这里面。
  • mm/ 内存管理的相关代码(memory managment?)

第二步,Linux内核启动过程分析

/init/main.c 文件中的start_kernel()函数是一切的起点,在这个函数被调用之前都是系统的初始化工作(汇编语言),所以对内核的启动分析一般都从这个函数开始;main.c中没有main函数,start_kernel()这个函数就相当于是c程序中的main函数。下面从这个函数开始对内核的启动流程进行分析。

由于写博文时没有随时保存的习惯,再加上手贱的原因,所以这是重新写的博文,因为时间的原因实验截图就不放上了,直接写一点自己的理会吧

start_kernel()函数的原型是:
《Linux内核分析》(三)——跟踪分析Linux内核的启动过程

这个函数在执行的过程中初始化、定义了内核中一些十分重要的内容,其执行过程几乎涉及到了内核的所有模块;首先,是init_task

《Linux内核分析》(三)——跟踪分析Linux内核的启动过程

init_task的定义在/linux-3.18.6/init/init_task.c

struct task_struct init_task = INIT_TASK(init_task);

它其实就是一个task_struct,与用户进程的task_struct一样, task_struct中保存了一个进程的所有基本信息,如进程状态,栈起始地址,进程号pid等;init_task的特殊之处在于它的pid=0,也就是通常所说的0号进程(当然最终会进化成为idle进程,在下面会分析);关于0号进程的重要意义在下面会继续分析,在这里只要记得它是被start_kernel()函数创建的就可以啦。

在创建了0号进程之后,start_kernel()函数继续调用各个系统模块进行各种初始化之类的工作,比如:

《Linux内核分析》(三)——跟踪分析Linux内核的启动过程

  • trap_init()是中断向量的相关设置
  • mm_init()是内存管理的设置
  • sched_init()是调度模块的初始化

至于调用的多少模块以及其相关的功能在这里就不仔细写了(其实因为弄丢了原来的博客来不及重写啊),因为我看到了同样学习了这门课的卢晅同学的博客对这个问题分析的很详细,所以就转载一下吧:

《Linux内核分析》(三)——跟踪分析Linux内核的启动过程

上面这张图片转载自卢晅同学的博客:http://blog.csdn.net/myfather103/article/details/44337461

在执行了上面的各项工作之后,是start_kernel()函数的最后一行代码:
《Linux内核分析》(三)——跟踪分析Linux内核的启动过程
这样子就调用了另一个非常重要的函数rest_init(),它的位置在/linux-3.18.6/init/main.c,其代码如下:

393static noinline void __init_refok rest_init(void)
394{
395 int pid;
396
397 rcu_scheduler_starting();
398 /*
399  * We need to spawn init first so that it obtains pid 1, however
400  * the init task will end up wanting to create kthreads, which, if
401  * we schedule it before we create kthreadd, will OOPS.
402  */
403 kernel_thread(kernel_init, NULL, CLONE_FS);
404 numa_default_policy();
405 pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
406 rcu_read_lock();
407 kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
408 rcu_read_unlock();
409 complete(&kthreadd_done);
410
411 /*
412  * The boot idle thread must execute schedule()
413  * at least once to get things moving:
414  */
415 init_idle_bootup_task(current);
416 schedule_preempt_disabled();
417 /* Call into cpu_idle with preempt disabled */
418 cpu_startup_entry(CPUHP_ONLINE);
419}

rest_init()函数中这行代码:

kernel_thread(kernel_init, NULL, CLONE_FS);

kernel_thread这个函数(/linux-3.18.6/kernel/fork.c)的源码是:
《Linux内核分析》(三)——跟踪分析Linux内核的启动过程
从注释部分就可以看出来这个函数的功能是创建一个内核线程(应该是进程吧喂!!不然哪里会有pid啊喂!!)
kernel_thread函数中第一个参数是一个函数指针,也就是说内核此时fork出了一个新进程来执行kernel_init函数(低版本内核中这个函数名为init,为了区分init进程所以将其改为了kernel_init);在kernel_init函数(/linux-3.18.6/init/main.c)正式启动了init进程:
《Linux内核分析》(三)——跟踪分析Linux内核的启动过程

至此rest_init()函数启动了另一个大名鼎鼎的init进程,也就是1号进程,关于它的重要性以及功能同样放在下面分析;接下来是rest_init()函数的这一行代码:

pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);

由上面的分析可以知道这个行代码folk了一个新的进程来执行函数kthreadd,这个函数的源码在这里/linux-3.18.6/kernel/kthread.c,上面这一行代码实现的功能是创建一个pid=2的内核进程,来管理内核的一些资源。

rest_init()在创建了1号、2号进程之后,我们忽略其余的部分(其实是因为看不懂吧喂!!)直接来分析其最后一行代码:

cpu_startup_entry(CPUHP_ONLINE);

调用了cpu_startup_entry( /linux-3.18.6/kernel/sched/idle.c )函数,其源码:


256void cpu_startup_entry(enum cpuhp_state state)
257{
258 /*
259  * This #ifdef needs to die, but it's too late in the cycle to
260  * make this generic (arm and sh have never invoked the canary
261  * init for the non boot cpus!). Will be fixed in 3.11
262  */
263#ifdef CONFIG_X86
264 /*
265  * If we're the non-boot CPU, nothing set the stack canary up
266  * for us. The boot CPU already has it initialized but no harm
267  * in doing it again. This is a good place for updating it, as
268  * we wont ever return from this function (so the invalid
269  * canaries already on the stack wont ever trigger).
270  */
271 boot_init_stack_canary();
272#endif
273 arch_cpu_idle_prepare();
274 cpu_idle_loop();
275}
276

而cpu_idle_loop中其实就是进入了一个无限循环:


189static void cpu_idle_loop(void)
190{
191 while (1) {
192     /*
193      * If the arch has a polling bit, we maintain an invariant:
194      *
195      * Our polling bit is clear if we're not scheduled (i.e. if
196      * rq->curr != rq->idle).  This means that, if rq->idle has
197      * the polling bit set, then setting need_resched is
198      * guaranteed to cause the cpu to reschedule.
199      */
200
201     __current_set_polling();
202     tick_nohz_idle_enter();
203
204     while (!need_resched()) {
205         check_pgt_cache();
206         rmb();
207
208         if (cpu_is_offline(smp_processor_id()))
209             arch_cpu_idle_dead();
210
211         local_irq_disable();
212         arch_cpu_idle_enter();
213
214         /*
215          * In poll mode we reenable interrupts and spin.
216          *
217          * Also if we detected in the wakeup from idle
218          * path that the tick broadcast device expired
219          * for us, we don't want to go deep idle as we
220          * know that the IPI is going to arrive right
221          * away
222          */
223         if (cpu_idle_force_poll || tick_check_broadcast_expired())
224             cpu_idle_poll();
225         else
226             cpuidle_idle_call();
227
228         arch_cpu_idle_exit();
229     }
230
231     /*
232      * Since we fell out of the loop above, we know
233      * TIF_NEED_RESCHED must be set, propagate it into
234      * PREEMPT_NEED_RESCHED.
235      *
236      * This is required because for polling idle loops we will
237      * not have had an IPI to fold the state for us.
238      */
239     preempt_set_need_resched();
240     tick_nohz_idle_exit();
241     __current_clr_polling();
242
243     /*
244      * We promise to call sched_ttwu_pending and reschedule
245      * if need_resched is set while polling is set.  That
246      * means that clearing polling needs to be visible
247      * before doing these things.
248      */
249     smp_mb__after_atomic();
250
251     sched_ttwu_pending();
252     schedule_preempt_disabled();
253 }
254}
255

也就是说,0号进程在fold了1号进程并且做了其余的启动工作之后,最后”进化“成为了idle进程。

至此,由start_kernel()函数所开始的内核启动告一段落,系统此时已经可以”正常“的接受任务进行工作了。
上述启动流程的图示:

《Linux内核分析》(三)——跟踪分析Linux内核的启动过程

由于只是一次简单的实验,对于复杂的内核启动流程自然不可能仅仅用上面一张图片总结,本文的参考文献中提供了大量的内核启动相关流程,有兴趣的话请参阅

第三,0号进程与1号进程

0号进程:所有进程的”祖先“,由start_kernel()函数在内核启动过程中”手动“创建的一个内核进程,始终处于内核态。在内核启动的之后0号进程完全“进化”成idle进程,系统开始无限循环
1号进程:

《Linux内核分析》(三)——跟踪分析Linux内核的启动过程

《Linux内核分析》(三)——跟踪分析Linux内核的启动过程

补充:init可执行文件是可以在/sbin/init,/etc/init,/bin/init,/bin/sh之中的

上面两张图片截图自课程提供的文档:
http://blog.csdn.net/hardy_2009/article/details/7383815
http://teamtrac.ustcsz.edu.cn/raw-attachment/wiki/Linux2012/Linux-init-process-analyse.pdf



参考文献:

对于复杂的内核启动流程自然不能用一片文章就解释清楚,虽然在实验中做了一些跟踪分析,不过为了完成此文还是参考了不少优秀文献,许多文章中对内核的启动分析都比本文要详细透彻的多,在此对作者表示感谢!

http://blog.csdn.net/titer1/article/details/44423031
http://www.linuxidc.com/Linux/2014-10/108033p5.htm
http://itdreamerchen.com/从源码中跟踪linux-kernel的启动过程/
http://blog.csdn.net/hlchou/article/details/6663994
http://blog.csdn.net/myfather103/article/details/44337461