本文是前 2 篇博文 Linux 内核学习笔记:初始化程序(第 1 部分)和 Linux 内核学习笔记:初始化程序(第 2 部分)的续篇。
初始化进程 0
进程 0 是 Linux 操作系统中运行的第一个进程,也是 Linux 操作系统父子进程创建机制的第一个父进程。
对于进程 0 能够正常运行,《Linux 内核设计的艺术》有一个简明的阐述:
- 系统先初始化进程 0。
进程 0 管理结构 task_struct 的母本(init_task={INIT_TAST,})已经在代码设计阶段事先设计好了,但这并不代表进程 0 已经可用了,还要将进程 0 的 task_struct 中的 LDT、TSS 与 GDT 相挂接,并对 GDT、task[64] 以及与进程调度相关的寄存器进行初始化设置。
- Linux 0.11 作为一个现代操作系统,其最重要的标志就是能够支持多进程轮流执行,这要求进程具备参与多进程轮询的能力。系统这里对
时钟中断
进行设置,以便在进程 0 运行后,为进程 0 以及后续由它直接、间接创建出来的进程能够参与轮转奠定基础。- 进程 0 要具备处理
系统调用
的能力。每个进程在运算时都可能需要与内核进行交互,而交互的端口就是系统调用程序。系统通过函数 set_system_gate 将 system_call 与 IDT 相挂接,这样进程 0 就具备了处理系统调用的能力了。这个 system_call 就是系统调用的总入口。
这 3 点的执行代码如下(比较长):
1 | // init/main.c --------------------------------- |
上述代码中,
TSS 格式
(注意不是 TSS 描述符)如下:
图片来源:《Linux 内核完全注释》task_struct 格式
如下:
图片来源:《Linux 内核完全注释》
初始化进程 0
TSS 及 LDT 描述符拼接
TSS 及 LDT 描述符格式如下图所示:
图片来源:《Linux 内核设计的艺术》
(也可参考自之前博文 Linux 内核学习笔记:预备知识之“存储器管理基础”“系统段描述符”部分)
执行代码如下:
1 | // include/asm/system.h ------------------------ |
以进程 0 的 TSS 描述符拼接为例,代码执行过程可用图表示如下:
图片来源:《Linux 内核设计的艺术》
注:详细代码解释请参考《Linux 内核设计的艺术》。
最终,进程 0 拼接而成的 TSS、LDT 描述符在 GDT 中的位置如下图所示:
图片来源:《Linux 内核设计的艺术》
task_struct 初始化
进程 0 的 task_struct 初始化代码如下:
1 | // kernel/sched.c ------------------------------ |
这段代码执行结果如下所示:
图片来源:《Linux 内核设计的艺术》
sched_init() 函数接下来用 for 循环将 task[64] 除进程 0 占用的 0 项外的其余 63 项清空,同时将 GDT 的 TSS1、LDT1 往上的所有表项清零:
1 | // kernel/sched.c ------------------------------ |
接下来,初始化进程 0 相关的管理结构的最后一步是将 TR 寄存器和 LDTR 寄存器分别指向 GDT 中的 TSS0 和 LDT0:
1 | // kenel/sched.c ------------------------------- |
综上,进程 0 初始化结束后结果如下:
图片来源:《Linux 内核设计的艺术》
设置时钟中断
这部分代码如下:
1 | // kenel/sched.c ------------------------------- |
时钟中断是进程 0 及其他由他创建的进程轮询的基础。对时钟中断进行设置的过程分为 3 个步骤(总结自《Linux 内核设计的艺术》):
- 对支持轮询的 8253 定时器进行设置。LATCH 定义在 kenel/sched.c 中,值为 1193180/HZ,即系统每 10 毫秒发生一次时钟中断。
- 设置时钟中断。将时钟中断服务程序 timer_interrupt() 函数挂接到 IDT。
- 开启时钟中断。从现在开始,时钟中断每 10 毫秒就产生一次。不过由于此时系统仍处于“关中断”状态,CPU 并不响应,但进程 0 已经具备参与轮转的潜能。
上述过程可用图表示如下:
图片来源:《Linux 内核设计的艺术》
设置系统调用总入口
系统调用是对硬件中断处理过程的模仿,只不过系统调用的“中断”由程序发起,而系统调用则模仿了中断处理的过程。需要注意的是,系统调用也需对 ss esp eflags cs eip 按序压栈,最后通过 iret 返回时再将这些压栈的数据还原给对应的寄存器。
这部分代码如下:
1 | // kenel/sched.c ------------------------------- |
这段代码是将系统调用处理函数 system_call 与 int 0x80 中断描述符挂接。system_call 是整个操作系统中系统调用软中断的总入口。所有用户程序使用系统调用,产生 int 0x80 软中断后,操作系统通过这个总入口找到具体的系统调用函数。
代码执行过程如下图所示:
图片来源:《Linux 内核设计的艺术》
系统调用是操作系统对用户程序的基本支持。用户进程只要想和内核打交道,就调用这套接口程序,之后,就会引发 int 0x80 软中断。后面的事情就不用用户程序管了,而是通过另一条执行路线——由 CPU 对这个中断信号响应,翻转特权级(从用户的 3 特权级翻转到内核的 0 特权级),通过 IDT 找到系统调用端口,调用具体的系统调用函数来处理事务。之后,再 iret 翻转回到进程的 3 特权级,进程继续执行原来的代码。