2018-2019-1 20189219《Linux内核原理与分析》第四周作业
环境搭建
本次内核环境搭建过程比较复杂,但是书中都给出了详细步骤,按照步骤一步步来就没问题了。附上正常运行的内核:
生成的目录格式如下:
.
├── arch
├── block
├── COPYING
├── CREDITS
├── crypto
├── Documentation
├── drivers
├── firmware
├── fs
├── include
├── init
├── ipc
├── Kbuild
├── Kconfig
├── kernel
├── lib
├── MAINTAINERS
├── Makefile
├── mm
├── modules.builtin
├── modules.order
├── Module.symvers
├── net
├── README
├── REPORTING-BUGS
├── samples
├── scripts
├── security
├── sound
├── System.map
├── tools
├── usr
├── virt
├── vmlinux
└── vmlinux.o
对于内核分析来说最重要的为arch目录下的x86目录下的源文件、init目录下的main.c文件以及kernel目录下和进程调度相关的代码等。此次我们使用gdb和qemu暂停来实现对Linux内核启动过程的追踪。既然要使用gdb,那么必须要有编译信息的可执行文件,这里我们按照书上的操作对内核进行相关的更改,这样vmlinux这个可执行文件便可以对其进行调试了。
启动过程分析
猜想
实验开始之前,我猜想内核的启动过程应该是逐步执行函数中的每个调用函数,以启动每个模块,直至所有模块都启动,最后内核启动。但是,实际好像完全不是我这么猜测的。那么我们进入实验。
实验开始
进入start_kernel函数之后,使用next
命令执行下一步,发现直接跳转到第510行,set_task_stack_end_magic(&init_task)
,即书中所说的首先使用宏INIT_TASK
对init_task
(0号进程)进行初始化。
接着继续使用next命令逐步分析函数,这时奇妙的事情发生了,在start_kernel
函数中,并不是按照语句的顺序来执行的!
可以看出,在执行了0号进程的初始化之后,函数开始跳着执行,执行顺序是
511 smp_setup_processor_id();//针对SMP处理器,用于获取当前CPU的硬件ID,如果不是多核,函数为空。
519 cgroup_init_early();//在系统启动时初始化cgroups,同时初始化需要early_init的子系统。
521 local_irq_disable();//关闭当前CPU的所有中断响应,操作CPSR寄存器,对应522行。
528 boot_cpu_init();//设置当前引导系统的CPU在物理上存在,在逻辑上可以使用,并且初始化准备好,即激活当前CPU。
522 early_boot_irqs_disabled = true;//系统中断关闭标志,当early_init完毕后,会恢复中断设置标志为false,对应第597行的false。
528 boot_cpu_init();
529 page_address_init();//初始化高端内存的映射表 。
530 pr_notice("%s", linux_banner);//输出各种信息。
531 setup_arch(&command_line);//内核架构相关初始化函数,是非常重要的一个初始化步骤。其中包含了处理器相关参数的初始化、内核启动参数(tagged list)的获取和前期处理、内存子系统的早期初始化(bootmem分配器)。
533 setup_command_line(command_line);//对cmdline进行备份和保存
查阅得知setup_arch函数位于linux-3.18.6/arch/x86/kernel/setup.c中,代码行数为397行,这里给出 [代码链接](http://codelab.shiyanlou.com/xref/linux-3.18.6/arch/x86/kernel/setup.c#857/"setup_arch(char **cmdline_p)"),其中注释段给setup_arch函数的定义为architecture-specific boot-time initializations,即特定于体系结构的启动初始化。
以上是部分函数启动的顺序,可以看出来,x86体系下的linux内核进行启动时先初始化进程,紧接着是对cpu、内存与内核等模块的初始化。值得注意的是,执行上述所有函数操作的时候,在qemu上是没有任何输出显示的,包括刚才530行的pr_notice函数都没有在屏幕上输出,即linux内核要在等某个关键模块完全初始化或启动之后才会在屏幕上现实所有的启动信息。
当我们继续逐步运行,直到
607 console_init();
608 if (panic_later)
屏幕上出现信息了。
查看上下代码
602 /*
603 * HACK ALERT! This is early. We're enabling the console before
604 * we've done PCI setups etc, and console_init() must be aware of
605 * this. But we do want output early, in case something goes wrong.
606 */
607 console_init();
608 if (panic_later)
609 panic("Too many boot %s vars at `%s'", panic_later,
610 panic_param);
注释段解释了为何此时在控制台中输出了信息。
这里并不知晓为何控制台会输出信息,从图中信息猜测是在对CPU校准的时候发生了某些错误从而弹出提示信息,正好符合了刚才启动控制台的原因。同时显示了CPU的主频,我的CPU为E3-1230V3@3.3GHz,吻合。
在此处显示BogoMIPS
值,查阅得知:
Bogomips(读作bogumips)是Linux操作系统中衡量计算机处理器运行速度的一种尺度,而提供这种度量的程序也被称为BogoMips.Bogo是Bogus(伪)的意思;MIPS是每秒百万条指令。Bogomips能测出一秒钟内某程序运行了多少次。其实,BogoMips的过程就是一个简单计数循环,看ls可以循环多少次,然后除以500000就得到了BogoMips的数值。
这里表示的是进程号的上下限,从图中看到最小为301,默认为32768,可以看出32768=2^15,若该内核环境为16位,则整形int占用2字节,则该值表示表示最大进程号。
此处显示的为acpi的版本号为20140926,与bios显示的版本一致,同时显示acpi成功配置。
初始化内核安全管理框架,以便提供访问文件/登录等权限。
信号初始化函数,控制台显示挂载点哈希表写入。
显示初始化cgroup,表示进程控制组正式初始化,主要用来为进程和其子进程提供性能控制,比如限定这组进程的CPU使用率为20%
此处表示激活EFI接口,并释放占用的处理备用寄存器32K。
最后便执行到rest_init
函数,此函数执行之后,所有初始化完成,内核开始工作。
以上是逐步执行start_kernel
函数所得出的结论,大部分都不能理解,很多都是上网查阅,即便如此想对每一步有一个精确的理解还是太过困难。
start_kernel函数细究
下面我们对start_kernel
函数对进程初始化的过程进行解析。
从上面的分析中我们了解到,init_task
即0号进程在被初始化之后,start_kernel
函数需要对其他的各种模块,如CPU、内存、中断模块、安全管理模块、输入输出模块、各种接口模块等进行初始化工作,一切就绪之后,再由rest_init
函数创建第一个用户进程即1号进程。这里我们主要从rest_init函数入手。
- 1.函数代码
393 static 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
函数首先定义了pid进程号变量,随后启动进程调度器。结合注释与函数kernel_thread
可以得知,rest_init
函数先调用了kernel_thread
函数生成了kernel_init
进程,即为1号进程,随后生成kthreadd
进程,即2号进程。
- 2.了解了
rest_init
函数中如何生成1号和2号进程之后我们再分别看看两者的进程函数。- 1.
kernel_init
进程函数
- 1.
930static int __ref kernel_init(void *unused)
931{
932 int ret;
933
934 kernel_init_freeable();
935 /* need to finish all async __init code before freeing the memory */
936 async_synchronize_full();
937 free_initmem();
938 mark_rodata_ro();
939 system_state = SYSTEM_RUNNING;
940 numa_default_policy();
941
942 flush_delayed_fput();
943
944 if (ramdisk_execute_command) {
945 ret = run_init_process(ramdisk_execute_command);
946 if (!ret)
947 return 0;
948 pr_err("Failed to execute %s (error %d)\n",
949 ramdisk_execute_command, ret);
950 }
951
952 /*
953 * We try each of these until one succeeds.
954 *
955 * The Bourne shell can be used instead of init if we are
956 * trying to recover a really broken machine.
957 */
958 if (execute_command) {
959 ret = run_init_process(execute_command);
960 if (!ret)
961 return 0;
962 pr_err("Failed to execute %s (error %d). Attempting defaults...\n",
963 execute_command, ret);
964 }
965 if (!try_to_run_init_process("/sbin/init") ||
966 !try_to_run_init_process("/etc/init") ||
967 !try_to_run_init_process("/bin/init") ||
968 !try_to_run_init_process("/bin/sh"))
969 return 0;
970
971 panic("No working init found. Try passing init= option to kernel. "
972 "See Linux Documentation/init.txt for guidance.");
973}
结合953行的注释和函数名我们了解到,此函数是在0号进程创建了1号进程之后,不断生成新的用户进程函数,这便是书中所说的所以其他用户进程的祖先进程。
- 2.kthreadd
进程函数
483int kthreadd(void *unused)
484{
485 struct task_struct *tsk = current;
486
487 /* Setup a clean context for our children to inherit. */
488 set_task_comm(tsk, "kthreadd");
489 ignore_signals(tsk);
490 set_cpus_allowed_ptr(tsk, cpu_all_mask);
491 set_mems_allowed(node_states[N_MEMORY]);
492
493 current->flags |= PF_NOFREEZE;
494
495 for (;;) {
496 set_current_state(TASK_INTERRUPTIBLE);
497 if (list_empty(&kthread_create_list))
498 schedule();
499 __set_current_state(TASK_RUNNING);
500
501 spin_lock(&kthread_create_lock);
502 while (!list_empty(&kthread_create_list)) {
503 struct kthread_create_info *create;
504
505 create = list_entry(kthread_create_list.next,
506 struct kthread_create_info, list);
507 list_del_init(&create->list);
508 spin_unlock(&kthread_create_lock);
509
510 create_kthread(create);
511
512 spin_lock(&kthread_create_lock);
513 }
514 spin_unlock(&kthread_create_lock);
515 }
516
517 return 0;
518}
可以看到,kthreadd
函数就是书中介绍的内核线程,始终运行在内核空间,在create_kthread
函数中调用kernel_thread
来不断创建新的进程并加入链表。因此所有的内核线程都是直接或者间接的以kthreadd
为父进程的。
至此,所有的过程分析结束。
疑问
说实话,内核的启动过程实在复杂,这么一个精简过的内核系统都有着几千行的代码量,让人望而却步。一下为我最开始的实验过程
进入函数之后发现这里面的大多数函数并不能从名字上看出它们的意义,只能一步一步的试,于是我在第一个函数509
lockdep_init()
函数处设置了断点b 509
,然后c,没有任何反应,在init_task这个重要的进程变量处设置断点b 510
,然后c,没有反应,然后我想当然的在发现menuOS竟然开始跑了。但是结果却不尽如意,因为它最终没有成功的启动系统,而是卡死在这一步,于是ctrl+c
中断操作,得到了如下信息。
default_idle () at arch/x86/kernel/process.c:314
314 trace_cpu_idle_rcuidle(PWR_EVENT_EXIT,,smp_processor_id());
于是我去查看了
arch/x86/kernel/process.c:314
:
310 void default_idle(void)
311 {
312 trace_cpu_idle_rcuidle(1, smp_processor_id());
313 safe_halt();
314 trace_cpu_idle_rcuidle(PWR_EVENT_EXIT,smp_processor_id());
315 }
看完之后一头雾水,本打算放弃,我看到了在
main.c:511
中的smp_setup_processor_id()
查阅得知,此函数是针对SMP处理器,用于获取当前CPU的硬件ID,如果不是多核,函数为空。判断是否定义了CONFIG_SMP,如果定义了,调用read_cpuid_mpidr读取寄存器CPUID_MPIDR的值,即当前正在执行初始化的CPU ID结合刚才qemu中给出的信息unable to init device /dev/mcelog
,猜想应该是start_kernel函数在初始化0号进程的时候无法识别cpu的硬件ID从而报错导致卡死。那如何来验证自己的猜想呢?
- 1.查看
/dev/mcelog
。在自己的系统上查看发现这是个c
属性文件,即字符设备,查阅得知,该设备是用于是 x86 的 Linux 系统上用来 检查硬件错误,特别是内存和CPU错误的工具。- 2.查阅有关
smp_processor_id()
的资料,发现这个函数不同于smp_setup_processor_id()
函数,它不可以直接读取寄存器中的值来获取cpuid,而必须获取内核变量保存的处理器id,因此smp_processor_id()
函数必须使用初始化函数。
这大概便是为何刚才初始化0进程时出错的原因了,但是苦于找不到上述两个函数的源码,找到了也不一定能看懂,只能粗略分析至此。
这里我犯了个问题,就是误以为menuOS卡死,实际上menuOS正常启动同样会显示上述的信息。所以这个思路算是彻底错了。但是,从这我了解到了一个重要的信息:
在初始化完成的末尾阶段,init_task(0号进程)会调用cpu_idle函数演变成idle进程,不再参与后续的进程生成和初始化,这也是为什么Ctrl+c
停止后显示的信息是:
default_idle () at arch/x86/kernel/process.c:314
314 trace_cpu_idle_rcuidle(PWR_EVENT_EXIT,,smp_processor_id());
还有另外一个问题,为什么还没执行到rest_init
函数内核就已经启动了呢?难道仅仅只是巧合么?于是我试了其他的断点,发现只要是没有按照上述顺序来执行函数,都会直接启动内核。思考良久未得其解。猜想可能是不在顺序启动的函数一旦被强制启动的话是一定会把其他内核模块初始化函数启动的,即只有在其他的内核模块函数启动之后才能启动这些函数。在和铸君同学探讨之后,我们猜测内核启动的过程可能是个多线程编译过程,是多个模块并发的进行初始化,所以在gdb
中使用next
命令进行调试的时候并不是完全按照start_kernel
中的函数顺序来进行的。当然,这只是个猜想,还需要多加学习。