第3章 MenuOS的构造
1 Linux内核源代码简介
计算机的“3大法宝”:存储程序计算机、函数调用堆栈和中断。
操作系统的“两把宝剑”:一把是中断上下文的切换——保存现场和恢复现场;另一把是进程上下文的切换。
Linux内核源码目录如下图所示:
其中可以把内核源代码目录分为系统最核心组件和系统次核心组件。
系统最核心组件包括:
arch目录:该目录是与体系结构相关的子目录列表,里面存放了许多CPU体系结构的相关代码,比如arm、x86、MIPS、PPC等。该目录中的代码在Linux内核代码中占比相当庞大,主要原因是arch目录中的代码可以使Linux内核支持不同的CPU和体系结构。alpha、arm、arm64等不同目录分别支持不同的CPU。
include目录:这个目录包含了Linux源代码目录中绝大部分头文件,每个体系结构都在该目录下对应一个子目录,该子目录中包含了给定体系结构所必需的宏定义和内联函数。
init目录:该目录中存放的是系统核心初始化代码,内核初始化入口函数start_kernel就是在该目录中的文件main.c内实现的。
kernel目录:该目录中存放的是Linux内核的最核心代码,用于实现系统的核心模块,这些模块包括进程管理、进程调度器、中断处理、系统时钟管理和同步机制等。
scripts目录:该目录中不包含任何核心代码,该目录下存放了用来配置内核的脚本和应用程序源码。
lib目录:该目录主要包含两部分内容: gnuzip解压缩算法,用于在系统启动过程中将压缩的内核镜像解压缩;剩余的文件用于实现-一个C库的子集,主要包括字符串和内存操作等相关函数。
mm目录:该目录包含了体系结构无关的内存管理代码,包括通用的分页模型的框架、伙伴算法的实现和对象缓冲器slab的实现代码。
系统次核心组件包括:
Documentation目录:存放了与内核相关的文档。
block目录:用于实现块设备的基本框架和块设备的I/O调度算法。
crypto目录:该目录中存放了相关的加密算法的代码。
driver目录:用于存放各类设备的驱动程序。
net和fs目录:包含linux内核支持的众多网络协议和文件系统。
ipc目录:该目录中的文件用于实现System V的进程间通信模块。
sound目录:存放了声音系统架构,如Open Sound System(OSS)、Advanced LinuxSound Architecture(AL SA)的相关代码和具体声卡的设备驱动程序。
security目录:存放了Security-Enhanced Linux(SELinux)安全框架的实现代码。
usr目录:该目录中的代码为内核尚未完全启动时执行用户空间代码提供了支持。
2 构造一个简单的Linux内核
使用实验楼的虚拟机打开shell,输入以下命令:
1 cd ~/LinuxKernel/
2 qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
上述代码分析:
qemu:启动已经安装在系统中的相当于虚拟机的程序qemu,这个程序为内核的启动提供一个上下文环境。
-kernel 文件名+路径:启动内核,内核经过编译之后形成一个名为init的文件,之前已经将其拷贝到rootfs文件目录下,并通过cpio的方式将rootfs下的文件打包成一个名为roofs.img的镜像文件。
-initrd rootfs.img:指定rootfs为为启动时的硬件驱动。
经过以上代码之后,rootfs.img会找到init这个可执行文件,init又是由MenuOS这个内核源代码编译而来,由此构建的Linux系统MenuOS截图如下所示:
输入help可见其只有三条命令help、version、quit,如下图所示:
3 跟踪调试Linux内核的启动过程
使用gdb跟踪调试内核
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
关于-s和-S选项的说明:
-S:CPU初始化之前冻结起来(可使用‘C’继续执行);
-s:在1234端口上创建了一个gdb-server,可以另外打开一个窗口,用gdb把带有符号表的内核镜像加载进来,然后连接server,设置断点跟踪内核。若不想使用1234端口,可以使用-gdb tcp:xxxx来取代-s选项。
用上面的命令先把内核启动一下,可以看到被冻结起来了,代码没有被运行,如下图所示:
再另外打开一个shell窗口,用Ctrl+Shift+O实现水平分割,启动gdb,把内核加载进来,建立连接。
1 file linux-3.18.6/vmlinux
2 target remote:1234
3 break start_kernel
在此之前内核一直是stop状态,如果按“c”则继续执行,系统开始启动,并启动到start_kernel函数的位置停在断点处,如下图所示:
再设置一个断点rest_init,继续执行,停在断点处,如下图所示:
可以看到rest_init是在start_kernel的尾部进行调用的。
start_kernel()函数的部分代码如下图所示:
1 500 asmlinkage __visible void __init start_kernel(void)
2 501 {
3 502 char *command_line;
4 503 char *after_dashes;
5 504
6 505 /*
7 506 * Need to run as early as possible, to initialize the
8 507 * lockdep hash:
9 508 */
10 509 lockdep_init();
11 510 set_task_stack_end_magic(&init_task);
12 511 smp_setup_processor_id();
13 512 debug_objects_early_init();
14 .............
15 678
16 679 /* Do the rest non-__init'ed, we're now alive */
17 680 rest_init();
18 681 }
C语言代码是从main函数启动的,C程序的阅读也从main函数开始。init目录中的main.c源文件是整个Linux内核启动的起点,但它的起点不是main函数,因为main.c中没有main函数,start_kernel()相当于C语言中的main函数,几乎涉及了内核的所有模块,如:trap_init()(中断向量的初始化)、mm_init()(内存管理模块的初始化)、sched_init()(调度模块的初始化)等。start_kernel是一切的起点,不论分析内核的哪一部分都会涉及到该函数,因为基本上所有模块的初始化都在main.c的start_kernel来调用。set_task_stack_end_magic(&init_task)设置第一个进程(pid=0)。
rest_init函数的部分代码为:
1 393 static noinline void __init_refok rest_init(void)
2 394 {
3 395 int pid;
4 396
5 397 rcu_scheduler_starting();
6 398 /*
7 399 * We need to spawn init first so that it obtains pid 1, however
8 400 * the init task will end up wanting to create kthreads, which, if
9 401 * we schedule it before we create kthreadd, will OOPS.
10 402 */
11 403 kernel_thread(kernel_init, NULL, CLONE_FS);/*创建pid=1的进程*/
12 404 numa_default_policy();
13 405 pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
14 ........
15 410
16 411 /*
17 412 * The boot idle thread must execute schedule()
18 413 * at least once to get things moving:
19 414 */
20 415 init_idle_bootup_task(current);
21 416 schedule_preempt_disabled();
22 417 /* Call into cpu_idle with preempt disabled */
23 418 cpu_startup_entry(CPUHP_ONLINE);/*注意这条语句,pid=0的进程变为了idle进程*/
24 419 }
rest_init这是Linux内核初始化的尾声, kernel_thread(kernel_init, NULL, CLONE_FS)创建一个进程(pid=1)。
cpu_idle_loop函数的部分代码为:
1 189 static void cpu_idle_loop(void)
2 190 {
3 191 while (1) {
4 192
5 ............
6 253 }
7 254 }
8 255
9 256 void cpu_startup_entry(enum cpuhp_state state)
10 257 {
11 258 /*
12 259 * This #ifdef needs to die, but it's too late in the cycle to
13 260 * make this generic (arm and sh have never invoked the canary
14 261 * init for the non boot cpus!). Will be fixed in 3.11
15 262 */
16 ...........
17 273 arch_cpu_idle_prepare();
18 274 cpu_idle_loop();
19 275 }
20 276
cpu_idle_loop是一个无限循环的函数,pid=0的进程(idle进程)根本就不会结束,当没有别的任务,该进程就被调用。
4 总结
Linux内核启动过程为:最初执行的进程即是0号进程init_task,它是在系统初始化阶段由start_kernel()函数从无到有手工创建的一个内核线程,进程0在创建1号内核线程kernel_init后,调用cpu_idle()成为idle进程,而idle进程就是当系统没有进程需要执行的时候来调度用的。1号内核进程负责执行内核的部分初始化工作及进行系统配置,然后使用kernel_thread(kernel_init, NULL, CLONE_FS)函数(也就是fork方式)建立了pid=1的1号进程,也叫init进程(用户态1号进程),成为系统中的其他所有进程的祖先,当调度程序选择到init进程时,init进程继续完成剩下的初始化工作。然后调用kernel_thread执行kthreadd,创建PID为2的内核线程,这一进程始终运行在内核空间,负责所有内核线程的调度和管理。