付何山+原创作品转载请注明出处+《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000;
导读:本文分为三个部分。第一部分将描述实验进程,第二部分将使用gdb跟踪调试内核,借此分析start_kernel函数的执行过程,第三部分将阐述自己对Linux启动过程的理解。
一、课程实验
系统:实验楼虚拟机
实验步骤如下:
1、打开虚拟机终端:
cd LinuxKernel/
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.im
此时即可打开MenuOS,如下图所示。
此时可执行help、version、quit
三条指令。执行quit时即退出系统。
二、GDB调试内核
使用qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
即可打开一个qemu窗口,显示为stopped停止状态。添加的两个参数的意义为:
-S 将CPU冻结在启动程序
-s 等同于-gdp tcp::1234 即gdp调试是通过1234接口来对qemu进行调试的,本指令相当于使用了1234接口,可使用-gdp tcp::XXXX代替,注意最好不要使用1024以下的接口。
打开另一个shell窗口
gdb 进入gdb调试环境
(gdb) file linux-3.18.6/vmlinux 加载符号表
(gdb) target remote:1234 建立连接,此时可用'c'开始执行原来的程序
(gdb) break start_kernel 设置断点,可放置在上一条指令之前
如下图所示:
此时我们已经在start_kernel函数设置了断点,在命令行中输入’c’并回车,可看到程序运行至断点处。此时显示为Booting the kernel,即准备启动内核:
此时gdb调试环境中显示如下,使用’list’指令观察,可看到当前断点附近的数行代码。如下图所示:
下面来仔细观察一下,设置的断点函数中的内容:
由于函数中的内容太多,这里就不贴上完整的代码了。该函数主要是一个调用其他函数进行初始化的函数。直至rest_init()之前都是各种初始化内核的函数,我们设置一个新断点(break rest_init(),或使用简写b)。此时qemu程序的运行如下图:
可以观察到内核被各个被start_kernel调用的函数初始化。而后再执行’c’即可完成rest_init,进入MentOS,进入我们写的命令行程序。
三、Linux启动过程理解及总结
结合老师所给的资料下面对Linux的启动过程进行分析,重点将阐述关于idle进程、1号进程的产生。
我们可以勉强认为内核从start_kernel开始启动,虽然在执行start_kernel之前启动工作一定执行了一些硬件层面的启动或全局变量的定义。但我们的重点不在此处,有兴趣可以通过init.c文件代码自行了解。至于为什么从init.c代码开始运行是受/etc/inittab的控制,具体也可自行了解。
在start_kernel函数中,启动0号进程的是这个函数:
//初始化系统第一个task_struct结构体,手工创建PCB,0号进程即最终的idle进程。
set_task_stack_end_magic(&init_task);
可通过gdb找到该函数定义在kernel/fork.c中,其调用end_of_stack
。其余的函数初始化了内核的各个方面,比如IRQ(中断向量),sched_init(任务调度),page_alloc_init(内存页分配)等(先后顺序不一定如上所示,请自行查阅init.c代码)。
其后于rest_init中:
首先创建使用下面的函数创建1号进程(init task):
//init进程在此时创建好了,但是现在还不能调度它,会阻塞在wait_for_completion处,等待kthreadd_done Signal,以便往后继续执行下去。
kernel_thread(kernel_init, NULL, CLONE_FS);
此后创建内核线程,当内核线程创建成功后,将通过complete变量通知kernel_init线程。随后使用调度机制使1号进程获得处理器,并使当前的0号进程退化为idle进程。如下所示:
//当前0号进程init_task最终会退化成idle进程,所以这里让init_task进程隶属到idle调度类中。即选择idle的调度相关函数
init_idle_bootup_task(current);
//启动一次Linux Kernel Process的排成Context-Switch调度机制, 从而使得kernel_init即1号进程获得处理机
schedule_preempt_disabled();
//调用cpu_idle(),0号线程进入idle函数的循环,在该循环中会周期性地检查。
cpu_startup_entry(CPUHP_ONLINE);
执行完以上步骤后,系统中即存在有idle进程和1号进程。具体流程可再次总结为下(以下引用自网络):
最初执行的进程即是0号进程init_task,它是被静态产生的,内存栈的位置固定,执行一些初始化的工作。一直到start_kernel开始调用执行sched_init(),0号进程被init_idle(current, smp_processor_id())进程初始化成为一个idle task,变成上一次实验中的进程一样的,通过一个while循环不断执行,只要运行栈里没有别的进程它就执行,循环中不断检测运行栈里是否有其他进程并通过schedule函数进行调度。
init进程由idle通过kernel_thread创建,在内核空间完成初始化后, 加载init程序,是系统中所有其它用户进程的祖先进程Linux中的所有进程都是有init进程创建并运行的。首先Linux内核启动,然后在用户空间中启动init进程,再启动其他系统进程。在系统启动完成完成后,init将变为守护进程监视系统其他进程。
kthreadd进程由idle通过kernel_thread创建,并始终运行在内核空间, 负责所有内核线程的调度和管理,它的任务就是管理和调度其他内核线程kernel_thread, 会循环执行一个kthread的函数,该函数的作用就是运行kthread_create_list全局链表中维护的kthread, 当我们调用kernel_thread创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以kthreadd为父进程。
经过本次实验中我初步体会了Linux系统的启动过程,Linux系统远比我想象的要繁杂,仅仅只是做一个理解层面的分析已经十分困难,自己对Linux系统的理解依然还不够深入,需要进一步加强。