一、跟踪分析内核的启动过程实验 :
1.启动Menuos:
qemu仿真kernel:
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
知识补充:
(1)bzImage 是 vmlinux 经过 gzip 压缩后的文件,是压缩的内核映像,“b”代表的是“big”(bzImage 适用于大内核,zImage 适用于小内核)。vmlinux 是编译出来的最原始的内核ELF文件;
(2)根文件系统包括虚拟根文件系统和真实根文件系统。initrd 是“initial ramdisk”的简写,boot loader 将存储介质中的 initrd 文件加载到内存,内核启动时先访问 initrd 文件系统(虚拟的文件系统),然后再切换到真实的文件系统。
2.调试跟踪:
-S 开始处冻结CPU,方便调试
-s 使用tcp端口1234来进行通讯,将进程信息传过去(在后面的调试中会用到)。若不想使用1234端口,可以使用-gdb tcp:xxxx来取代-s选项
启动gdb,把内核加载进来,建立连接:
file linux-3.18.6/vmlinux
target remote:1234
实践过程中出现以下错误:
连接超时应该是没连接上冻结的系统,检查后发现进入gdb调试前讲QEMU窗口关闭了,打开后得以解决:
在 start_kernel 处设置断点,继续执行,停在断点处:
在 rest_init 处设置断点,继续执行,停在断点处:
3.内核启动分析(自己的大致理解):
(1)start_kernel()
main.c 中没有 main 函数,start_kernel() 相当于是C中的main函数。start_kernel是一切的起点,在此函数被调用之前内核代码是用汇编语言写的,完成系统的初始化工作,为c代码的运行设置环境。由调试可得 start_kernel 在500行:
(2)init_task()
start_kernel() 函数几乎涉及到了内核的所有模块,如:trap_init()(中断向量的初始化)、mm_init()(内存管理的初始化)sched_init()(调度模块的初始化)等,首先是510行的init_task():
struct task_struct init_task = INIT_TASK(init_task);
可以看出 init_task(0号进程)是 task_struct 类型,是进程描述符,使用宏INIT_TASK对其进行初始化。接下来就是对各种模块的初始化:
图片来源于分析Linux内核的启动过程
(3)rest_init()
通过rest_init()新建kernel_init、kthreadd内核线程:
403行代码 kernel_thread(kernel_init, NULL, CLONE_FS);
,由注释得调用 kernel_thread()创建1号内核线程(在 kernel_init 函数正式启动):
注:对比 init_task 和 kernel_thread()
kernel_thread()是 fork 出了一个新进程来执行kernel_init 函数,而 init_task 是使用宏进行初始化的。也就是说0进程不是系统通过 kernel_thread 的方式(也就是 fork)创建的(init_task 是唯一一个没有通过 fork()产生的进程)。
405行代码 pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);1
调用 kernel_thread()执行 kthreadd函数,创建 PID=2的内核线程:
此函数的任务是管理和调度其他内核线程 kernel_thread。for 循环中运行 kthread_create_list 全局链表中维护的 kthread, 在create_kthread()函数中,会调用 kernel_thread 来生成一个新的进程并被加入到此链表中,因此所有的内核线程都是直接或者间接的以 kthreadd 为父进程。
4.总结:
(1)init_task()(PID=0)在创建了init进程后,调用 cpu_idle() 演变成了idle进程,执行一次调度后,init进程运行;
(2)1号内核线程负责执行内核的部分初始化工作及进行系统配置,最后调用do_execve执行 init 函数,演变成 init 进程(用户态1号进程),init 进程是内核启动的第一个用户级进程;
(3)kthreadd(PID=2)进程由0号进程创建,始终运行在内核空间, 负责所有内核线程的调度和管理 。
注:Linux下的进程类别(内核线程、轻量级进程和用户进程)以及其创建方式
参考:
Linux下0号进程的前世(init_task进程)今生(idle进程)
Linux下1号进程的前世(kernel_init)今生(init进程)
二、课本笔记:
1.进程调度:
进程调度程序是在可运行态进程之间分配有限的处理器时间资源的内核子系统。Linux提供了抢占式的多任务模式,在此模式下,由调度程序决定一个进程的运行,以便其他进程能得到执行机会(抢占:抢占的挂起动作)。进程在被抢占前能运行的时间叫做进程的时间片,即分配给每个可运行进程的处理器时间段。
Linux内核将进程分成普通进程和实时进程。普通进程用nice值表示时间片的比例(CFS调度器将处理器的时间比划分给了进程,越大的nice值将被赋予低权重,丧失一小部分的处理器时间使用比);实时进程采用实时优先级,数值越高优先级越高(两种实时调度策略为SCHED_FIFO和SCHED_RR,SCHED_RR是带有时间片的SCHED_FIFO)。任何实时进程的优先级都高于普通进程。
CFS虽没有时间片的概念,但必须维护每个进程运行的时间记账,它会挑一个具有最小vruntime(存放进程的虚拟运行时间)的进程(利用红黑树迅速找到最小vruntime值的进程)进行调度。休眠(被阻塞)进程把自己标记成休眠状态,从可执行红黑树中移出,放入等待队列,然后调用schedule()选择和执行其他进程,唤醒时被置成可执行状态,然后再从等待队列中移到可执行红黑树中。
当内核即将返回用户空间时,内核会检查need_resched是否设置,如果设置,则调用schedule(),发生用户抢占。 内核抢占是指在内核态运行的进程在执行期间可能被另一个进程取代(没有读懂课本对内核抢占的解释,参照Linux用户抢占和内核抢占详解(概念, 实现和触发时机),如果内核处于相对耗时的操作中, 比如文件系统或者内存管理相关的任务, 其他进程无法执行, 无法调度, 这就造成了系统的延迟增加, 用户体验到”缓慢”的响应。比如如果多媒体应用长时间无法得到CPU, 则可能发生视频和音频漏失现象.启用内核抢占可解决此问题)。
2.内核数据结构:
(1)Linux内核链表不同于传统链表,它不是将数据结构塞入链表,而是将链表结点塞入数据结构:
(2)Linux内核通用队列实现成为kfifo,提供enqueue和dequeue,kfifo对象维护入口偏移和出口偏移两个偏移量;
(3)Linux映射一个唯一的标识数(UID)到一个指针(有点像字典类型,每个唯一的id对应一个自定义的数据结构);
(4)红黑树和平衡二叉树的区别在于它使用颜色来标识结点的高度,它所追求的是局部平衡而不是平衡二叉树中的非常严格的平衡。(课本上对红黑树的描述我也没有看明白,查阅资料数据结构之红黑树,了解了红黑树的性质(根是黑色;所有叶子都是黑色(叶子是NIL节点);如果一个节点是红的,则它的两个儿子都是黑的;从任一节点到其叶子的所有简单路径都包含相同数目的黑色节点)及红黑树的插入删除。虽然红黑树追求的是局部平衡,但我感觉和平衡二叉树的插入删除还是难了点,也有可能是刚接触的原因QAQ)。
三、小结:
本周学习中遇到了很多不明白的指令和术语,一边实践一边查阅资料,在阅读资料中又会由此引出新的问题(如在搜索init进程中看到先是内核线程又是用户进程,然后如果不理解二者区别,就没法弄明白1号进程的前世今生,只能继续搜索内核线程和用户进程的区别)过程辛苦,但好在问题大部分得以解决,还有小部分需要再强化理解。