本文是上一博文 Linux 内核学习笔记:进程 1 创建及执行(第 1 部分)的续篇。
任务调度
名词辩解
为了准确,在这里我们将我们常说的进程调度改称为任务调度
,原因主要是:
- 在 Linux 0.11 (其他 Linux 版本也一样,可通过 ulimit -a 查看)最多支持“同时存在” 64 个进程,因为系统资源是有限的(跟 GDT 容量、内存有限等有关)
- 进程的进程号可能是远远大于操作系统支持“同时存在”的进程数的,因为进程可创建也可销毁,只要“同时存在”的进程数是小于等于系统支持的进程数就行了;而且进程号是一直在累加的,即在不考虑特殊情况(溢出,如 long pid,pid 是存在溢出的风险的)的情况下,
存在或已销毁的进程的进程号是不会一样的
- 这些“同时存在”的进程由 task[64] 数组来统一管理。task[64] 中每一项指向这些进程中的某一个进程(具体指向的是该进程的进程描述符 task_struct )或为 NULL。另外,
task[64] 中的每一项可以重复使用
,如 task[64] 中某项指向的进程已被销毁,那这一项就可以给其他新建的进程用 - task[64] 每一项指向的内容都称为一个任务。
我们平常所说的“进程调度”实际是对 task[64] 进行遍历,即对 task[64] 中每一项指向的进程进行遍历,然后切换到符合要求的进程。而这些进程都可称为任务。
- 综上,用“任务调度”这个说法来代替“进程调度”的说法更为具体和准确。
注:
- 上边提到的“同时存在”是指“同时存在而还未被系统销毁”的进程。
task[64]
定义如下:1
2// include/linux/sched.h -----------------------
extern struct task_struct *task[NR_TASKS]; // NR_TASKS = 64
任务调度
这是内核第一次做任务调度。现在执行的是进程 0 的代码。从这里开始,进程 0 准备切换到进程 1 去执行。
《Linux 内核设计的艺术》指出在 Linux 0.11,在以下两种情况下会发生任务切换:
允许进程运行的时间结束
进程在创建时,都被赋予了有限的时间片,以保证所有进程每次都只执行有限的时间。一旦进程的时间片被削减为 0,就说明这个进程此次执行的时间用完了,立即切换到其他进程去执行,实现多进程轮流执行。进程被挂起
当一个进程需要等待外设提供的数据,或等待其他程序的运行结果……或进程已经执行完毕时,在这些情况下,虽然还有剩余的时间片,但是进程已经不具备进一步执行的“逻辑条件”了。如果还等着时钟中断产生后再切换到别的进程去执行,就是在浪费时间,应立即切换到其他进程去执行。
在上一博文 Linux 内核学习笔记:进程 1 创建及执行(第 1 部分)最后我们说到,进程 0 开始执行如下代码:
1 | // init/main.c --------------------------------- |
调用 sys_pause
pause 函数的执行流程跟 fork 函数的类似,都是通过 int 0x80 跳转到 system_call,再根据输入参数调用具体的服务程序。pause 函数具体的服务程序如下:
1 | // kernel/sched.c ------------------------------ |
调用 schedule
调用的 schedule() 函数定义如下:
1 | // kernel/sched.c ------------------------------ |
切换到进程 1
调用 switch_to
1 | // include/linux/sched.h ----------------------- |
上述代码比较难以理解的是 switch_to 这个函数中的一个重要的点——任务门描述符
:
ljmp 通过通过 CPU 的任务门机制,将 CPU 各个寄存器值保存到进程 0 的 TSS 中,将进程 1 的 TSS 数据恢复给 CPU 的各个寄存器,实现从 0 特权级的内核代码切换到 3 特权级的进程 1 代码执行。
在这里,进程 1 的 TSS 选择子为 _TSS(1)=0b110 000 -> 0 特权级,在 GDT,TSS 在 GDT 下标为 6。设置任务门的目的是为了在多进程环境下进行进程切换,通过它切换到另一个进程。而这一切换是通过进程的 TSS 实现的,因为进程的入口点包括段基地址和偏移量都存放在 TSS 中。在这里,任务门描述符内的偏移量没有意义。
任务门描述符格式
如下(也可参照之前博文 Linux 内核学习笔记:预备知识之“存储器管理基础”“门描述符”部分):
图片来源:《Linux 内核完全注释》TSS 格式
(注意不是 TSS 描述符)如下:
图片来源:《Linux 内核完全注释》
注:这里还是有问题的,就是 __tmp 如何跟任务门描述符格式一一对应起来,特别是“段选择符(子)”。
返回 fork
在上一博文 Linux 内核学习笔记:进程 1 创建及执行(第 1 部分)我们提到,进程 1 并不会完全复制进程 0 的所有属性,而是会进行微调:
1 | // kenel/fork.c -------------------------------- |
这些代码说明,当内核调度进程 1 开始运行时,将会跟其父进程(进程 0)一样从 int 0x80 的下一行 if (__res >= 0) 开始执行,并且置返回值 __res 为 0 (p->tss.eax = 0),也即 fork 函数返回 0:
1 | // include/unistd.h ---------------------------- // unistd -> unix standard |
上述代码可用图表示如下:
图片来源:《Linux 内核设计的艺术》
注:从这里我们才真正知道,通过 fork 函数派生的子进程会跟其父进程在派生后从同一位置开始执行,而且在父、子进程中 fork 函数返回的值不一样。
调用 init
init 函数定义如下:
1 | // init/main.c --------------------------------- |
调用 setup
setup 函数的调用跟 fork、pause 函数的调用有点类似;略有区别的是 setup 函数不是通过 _syscall0 而是通过 _syscall1 实现的;具体的实现过程基本类似,也是通过 int 0x80、_system_call、call _system_call_table (,%eax,4)、sys_setup:
1 | // include/unistd.h ---------------------------- |
注:之前在进程 0 调用的 pause 函数的 int 0x80 中断还没返回,现在 setup 函数又产生了一个中断。
调用 sys_setup
以防博文过长,接下来内容请见续篇。