20169212《Linux内核原理与分析》第五周作业

时间:2023-03-09 13:40:38
20169212《Linux内核原理与分析》第五周作业

关于linux内核源码

两个很关键的目录,一个是arch(architecture),支持不同cpu体系架构的源代码,其中最重要的就是x86(一般把x86留下,其他的目录删掉),另一个是init(其中的main.c是整个linux内核启动的起点,不过这里面不是main()函数,而是一个叫start_kernel的函数),linux内核的核心代码在kernel目录中。上一节中的命令make allnoconfig就是关闭所有的可选项,目的是可以让之后的编译更快的完成。

实验分析

由于这一周自己的电脑坏了,暂时借了一个电脑,所以不能用自己动手去构建一个简单的linux系统,就直接用实验楼的吧。使用实验楼的虚拟机打开shell,输入命令

cd LinuxKernel/
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img

就启动了自己构建(实验楼里已经构建好了)的简单linux的内核的启动,如图

20169212《Linux内核原理与分析》第五周作业

输入help命令,发现这个系统支持三个命令:help、version、quit

20169212《Linux内核原理与分析》第五周作业

接下来使用gdb跟踪调试内核

qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S

其中关于 -s -S # 关于-s和-S选项的说明:

-S freeze CPU at startup (use ’c’ to start execution)意思就是开始的时候先将CPU冻结起来,目的是阻止cpu执行接下来的指令

-s shorthand for -gdb tcp::1234 这个开了个端口号是为了待会儿用gdb调试使用,若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项

bzImage:压缩的内核映像

initrd:是在系统引导过程中挂载的一个临时根文件系统

使用这个命令之后,发现系统开始启动,但是什么操作都没进行,qemu显示stopped,即表明cpu被成功冻结。如图所示

20169212《Linux内核原理与分析》第五周作业

然后再另外打开一个shell窗口(可以在原shell窗口上右键->水平分割),然后输入gdb命令,启动gdb来跟踪调试。进入gdb之后,我们还需要输入几条命令:

file linux-3.18.6/vmlinux

这条命令的意思是加载符号表,不过具体这个vmlinux是什么意思,我又去查了下,它是是未压缩的内核,即编译出来的最原始的文件,用于kernel调试,产生system.map符号表,不能用于直接加载。附带一张linux内核映像生成过程图解:

20169212《Linux内核原理与分析》第五周作业

接下来是命令

target remote:1234

通过刚刚设置的端口号建立gdb和gdbserver之间的连接,在这个过程我们可以通过按c,让qemu上的linux继续运行

接下来就是通过设置断点,来详细跟踪linux内核的启动过程了。比如我们现在在内核启动的起点start_kernel函数处设置一个断点,那么命令就是

break start_kernel

20169212《Linux内核原理与分析》第五周作业

我们在start_kernel函数处设置一个断点之后按c,来让其运行,可以发现qemu虚拟机中的系统执行了一些命令之后便又停住了,感觉像是内核要准备开始启动了,说明我们在start_kernel函数处设置的那个断点是有效的。如图

20169212《Linux内核原理与分析》第五周作业

s进入函数,n单步跟踪:

20169212《Linux内核原理与分析》第五周作业

在跟踪过程中如果对函数有兴趣,可以s进入函数,然后继续跟踪。

我们也可以使用list命令,可以看到start_kernel这句代码上下的代码(显示前后十行,也可以使用list 5,10来显示第5行到第10行的代码),如图

20169212《Linux内核原理与分析》第五周作业

掌握了方法我们还可以设置更多的断点来跟踪内核的启动过程。

认识start_kernel()

接下来,我们要认识下start_kernel这个最关键的函数,但是整个内核中,这个函数颇为复杂,所以我也尽力去学习,简单分析下自己的理解。观察刚刚的实验,我们发现start_kernel这个函数的开始在init/main.c这个文件中的第501行,于是我们用gedit打开init/main.c,找到对应的代码,果然是从501行开始的,如图

20169212《Linux内核原理与分析》第五周作业

继续往下翻可以看到,在start_kernel还进行了一大堆的初始化操作。

所以,整个start_kernel,就完成调用一系列的初始化函数,完成内核本身的设置:设置与体系结构相关的环境、进程调度器初始化、内存初始化等各种初始化。在Start_kernel函数的最后调用了rest_init()函数,在rest_init中建立了init线程,并在最后调用cpu_idle()函数。可以这样理解:start_kernel最后clone出一个新的进程,也就是init进程,然后原来的进程就去执行cpu_idle()函数了,也就变成了idle进程(相当于windows系统中systemidle这个进程),当发生一次进程调度后,init进程被调度运行。

总结

此次学习让我比较系统的了解了linux内核的启动过程,自己一边学习,一边上网查询自己不太理解的知识点和专业的名词术语。但是整个内核启动复杂而又庞大,学习理解起来有困难,需要进一步加强学习。

内核启动过程包括start_kernel之前和之后,之前全部是做初始化的汇编指令,之后开始C代码的操作系统初始化,最后执行第一个用户态进程init。学习了之后感觉整个内核启动真的就应了中国传统文化里的“道生一,一生二,二生三,三生万物”,即道生一(start_kernel产生reset_init这个0号进程),一生二(kernel_init(1号)和kthreadd(2号)),二生三(即前面0、1和2三个进程),三生万物(1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先),从而又增加了自己学习的兴趣。

书上的内容

看书笔记:

  1. 多任务系统划分为两类:非抢占式多任务(主动挂起自己—让步)和抢占式多任务,Unix为抢先式的多任务。
  2. Linux进程调度:静态时间片算法和针对每一处理器的运行队列,它们帮助我们拜托了先前调度程序设计上的限制;进程分为:I/O消耗型(提交请求或等待请求)和处理器消耗型(执行代码);进程优先级:(1) nice值,范围-20到+19,默认为0,越大的nice值意味着越低的优先级;(2)实时优先级,默认范围是从0到99,数值越大优先级越高,而且任何实时进程优先级都高于普通进程。任何进程所获得的处理器时间是由它自己核其他所有可运行nice值的相对差值决定的;时间片:时间片过短过长都会产生一定的影响,I/O消耗型不需要长的时间片,处理器消耗性的进程则是希望越长越好。文字编辑程序属于I/O消耗型,视频编码程序属于处理器消耗型。
  3. Linux调度器是以模块的方式提供的,这样做的目的是允许不同类型的进程可以有针对的选择调度算法。这种模块化结构称为调度类,它允许不同的可动态添加的调度算法并存,调度属于自己范畴的进程。
  4. CFS完全公平调度,是一个针对普通进程的调度类,在Linux中称为SCHED_NORMAL,CFS算法实现定义在文件kernel/sched_fair.c中。出发点基于进程调度的效果应如同系统具备一个理想中的完美多任务处理器。
  5. 更高优先级的进程运行更频繁,而且也会被赋予更多的时间片。
  6. Linux调度实现的四个组成部分:

    时间记账:所有调度器都必须对进程时间做记账

    进程选择:CFS调度算法的核心为选择最小vruntime的任务

    调度器入口:主要入口点为schedule(),定义在kernel/sched.c中

    睡眠和唤醒:休眠的进程处于一个特殊的不可执行状态
  7. 上下文切换,也就是从一个可执行进程切换另一个可执行进程,由定义kernel/sched.c中的context_switch()函数负责处理。
  8. 抢占在以下情况时发生:从系统调返回用户空间时,从中断处理程序返回用户空间时.
  9. Linux提供了两种实时调度策略:SCHED_FIFO和SCHED_RR。
  10. 红黑树是一种自平衡二叉搜索树,具有特殊着色属性,或红或黑,有下面六个属性,维持半平衡结构:

    a.所有节点要么着红色,要么着黑色;

    b.叶子节点都是黑色

    c.叶子节点不包含数据;

    d.所有非叶子节点都有两个子节点;

    e.如果一个节点是红色,则它的子节点都是黑色;

    f.在一个节点到其叶子节点的路径中,如果总是包含同样数目的黑色节点,则该路径相比其他路径是最短的。