跟踪分析Linux内核的启动过程
潘俊洋
原创作品转载请注明出处
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
一.准备
搭建环境
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
cd ~ /Work/ wget https: //www .kernel.org /pub/linux/kernel/v3 .x /linux-3 .18.6. tar .xz xz -d linux-3.18.6. tar .xz tar -xvf linux-3.18.6. tar cd linux-3.18.6 make i386_defconfig make cd ~ /Work/ mkdir rootfs git clone https: //github .com /mengning/menu .git # 话说这里为什么用MenuOS 我个人觉得老师一来是节约编译时间 二来也可以做做广告 cd menu sudo apt-get install libc6:i386 lib32stdc++6 # 这两行安装非常有必要 sudo apt-get install lib32readline-gplv2-dev # 在64bit的Ubuntu环境下不能编译这个MenuOS的roofs 需要这些包来支持 即使用了-m32 gcc -o init linktable.c menu.c test .c -m32 -static -lpthread cd .. /rootfs cp .. /menu/init ./ find . | cpio -o -Hnewc | gzip -9 > .. /rootfs .img cd ~ /Work/ qemu -kernel linux-3.18.6 /arch/x86/boot/bzImage -initrd rootfs.img sudo apt-get install libncurses5-dev # 保证make menuconfig可用 make menuconfig kernel hacking-> copile- time checks and compile options [*] compile the kernel with debug info qemu -kernel linux-3.18.6 /arch/x86/boot/bzImage -initrd rootfs.img -s -S |
然后打开另一个shell,执行下面的命令:
1
2
3
4
|
gdb file linux-3.18.6 /vmlinux # 在gdb界面中targe remote之前加载符号表 target remote:1234 # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行 break start_kernel # 断点的设置可以在target remote之前,也可以在之后 |
设置完断点后,可以使用c让内核继续进行加载,加载到第一个断点start_kernel时
二.分析
在执行start_kernel时,期初会对CPU、内存等各种硬件设备进行初始化,这期间涉及到非常多的不同内核模块的加载。
在start_kernel的最后一项初始化,就是有关内核进程管理的初始化了。一旦这一项初始化完成,内核就加载成功了。
my_start_kernel,插入这个函数之后,我们自己的内核通过PCB的进程管理单元来管理了我们依次创建的四个简单进程,并通过时间片轮转的方式进行了调度。那么在实际的linux内核代码中,rest_init()到底是干什么才使得我们需要在它之前执行my_start_kernel呢?原因就是rest_init实际上是linux内核初始化进程的函数。如果我们在它执行之前自行创建我们自己的进程,并且利用自己的调度算法来调度之后创建的进程,那么rest_init则永远不会被执行,因为在它执行之前,我们自己的进程已经在轮转调度不会结束了。
下面我们就来看看实际linux初始化进程的内核代码rest_init(删掉了不关心的部分):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
void rest_init( void ) { int pid; ……………… kernel_thread(kernel_init, NULL, CLONE_FS); numa_default_policy(); pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); rcu_read_lock(); kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns); rcu_read_unlock(); complete(&kthreadd_done); init_idle_bootup_task(current); schedule_preempt_disabled(); cpu_startup_entry(CPUHP_ONLINE); } |
在rest_init的代码中,kernel_thread,被定义在文件arch/x86/kernel/fork.c中,它的功能是用来fork一个内核线程。
1
2
3
4
|
pid_t kernel_thread( int (*fn)( void *), void *arg, unsigned long flags){ return do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long )fn, (unsigned long )arg, NULL, NULL); } |
上面的代码我们可以看到,kernel_thread实际上就是取fork一个线程。
在执行kernel_thread时,kernel_init作为将要执行的函数指针传入,进程ID会被置为1。所以在这里,kernel_init内核线程被创建,进程号为1。
在完成内核进程的创建后,会创建kthreadd内核线程,作用则是管理和调度其他的内核线程。
kernel_init既然是将要执行,我们就来看看kernel_init又会执行什么:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
static int kernel_init( void *unused) { int ret; kernel_init_freeable(); async_synchronize_full(); free_initmem(); mark_rodata_ro(); system_state = SYSTEM_RUNNING; numa_default_policy(); flush_delayed_fput(); if (ramdisk_execute_command) { ret = run_init_process(ramdisk_execute_command); if (!ret) return 0; pr_err( "Failed to execute %s (error %d)\n" , ramdisk_execute_command, ret); } if (execute_command) { ret = run_init_process(execute_command); if (!ret) return 0; pr_err( "Failed to execute %s (error %d). Attempting defaults...\n" , execute_command, ret); } if (!try_to_run_init_process( "/sbin/init" ) || !try_to_run_init_process( "/etc/init" ) || !try_to_run_init_process( "/bin/init" ) || !try_to_run_init_process( "/bin/sh" )) return 0; panic( "No working init found. Try passing init= option to kernel. " "See Linux Documentation/init.txt for guidance." ); } |
事实上,kernel_init会继续进行内核的最后一些初始化的工作,直到最后一行实际上整个内核的初始化工作就已经正式完成了。
注意,我们创建的进程ID实际上是从1开始的。其中在kernel_init中创建的是1号进程,在刚才的kthreadd中创建的是2号进程。
那么接下来,为了让系统能够运作起来,剩下的这三行代码完成了非常重要的工作,它完成了CPU对任务的调度初始化,让内核真正的开始进入用户主导的阶段:
1
2
3
|
init_idle_bootup_task(current); schedule_preempt_disabled(); cpu_startup_entry(CPUHP_ONLINE); |
首先,init_idle_bootup_tast()会初始化一个idle(闲置)进程,这个进程不做任何其他事情,只负责消耗时间片。
然后通过schedule_preempt_disabled来设置这个进程是不会被调度。因为CPU显然利用率越高越好,不可能让调度程序调度一个只消耗时间片的进程。
最后,cpu_startup_entry 就会使得CPU在idle这样一个循环内进行工作,不断往复,从不返回。
1
2
3
4
5
|
void cpu_startup_entry( enum cpuhp_state state) { arch_cpu_idle_prepare(); cpu_idle_loop(); } |
自此,整个内核的启动过程就全部完成了。
三.实验过程
我们来逐步加载idel进程和1号进程。
通过上面的分析,我们注意到有下面几个比较重要的断点需要我们设置:
start_kernel, page_address_init, trap_init, mm_init, rest_init, kernel_init, kthreadd, init_idle_bootup_task, cpu_startup_entry