5.3.2 初始化进程0
POSIX标准规定,符合POSIX标准的操作系统采用复制的方式创建进程,但是内核总得想办法创建第一个原始的进程,否则其他进程复制谁呢?因此,内核静态的创建了一个原始进程,因为这个进程是内核的第一个进程,Linux为其分配的进程号为0,所以也被称为进程0。进程0不仅作为一个模板,在没有其他就绪任务时进程0将投入运行,所以其又称为idle进程。下面我们就看看内核是如何为进程0分配任务结构和内核栈这两个关键数据结构的。
1.创建任务结构
进程0的任务结构的定义如下:
其中变量init_task所在的位置(在内核的数据段中)就是进程0的任务结构。
当前进程的任务结构是一个频繁使用的变量,为了方便获取它,内核中专门定义了一个宏current。这个获取方法几经修改,现在的方式是定义了一个变量current_task指向当前进程的任务结构。在内核初始化时,内核将这个变量设置为指向init_task,换句话说,当前进程是进程0,代码如下:
读者不必关心所谓的PER_CPU,这是内核为了优化而定义的。为了在SMP情况下减少锁的使用,内核中为每颗CPU都定义了一个current_task。
宏current就是通过读取current_task来获取当前进程的任务结构,定义如下:
接下来在内核创建第一个真正意义上的进程(进程1)时,内核将从current指向的进程进行复制,而此时这个current恰恰指向进程0的任务结构。
2.进程0的内核栈
进程0不会切换到用户空间,所以无需用户空间的栈,只需为其安排好内核空间的栈即可。进程内核栈的数据结构抽象如下:
这个抽象中的数组stack就是内核栈,对于IA32,宏THREAD_SIZE定义为8KB,可见内核为进程内核栈分配的大小为两个页面。那么为什么进程的内核栈与另外一个结构体thread_info定义在一起呢?我们后面再讨论这个问题,下面先来具体看一下进程0的内核栈:
其中,变量init_thread_union就是进程0的内核栈所在的位置,这个变量也是在内核的数据段中,当然其栈底是在init_thread_union+THREAD_SIZE处了,如图5-19所示。
图 5-19 进程0的内核栈
在内核初始化时,设定了栈指针esp指向init_thread_union+THREAD_SIZE,代码如下:
因为此时尚未开启分页机制,而符号stack_start以及init_thread_union均使用的是加了偏移(0xc0000000)的虚拟地址,所以这里都要去掉这个偏移。而在开启页式映射后,把这个偏移又加了回来,如下面代码中使用黑体标识的部分:
最后,为了在进程切换时可以找到进程0的内核栈,还要将其保存在进程0的任务结构的结构体thread_struct的对象thread中,代码如下:
结构体thread_struct中的sp0就是记录进程内核栈的栈指针。
3.宏current与进程内核栈
这一小节我们来回答为什么进程的内核栈与另外一个结构体thread_info定义在一起的问题。
在2.4版本以前,内核直接将任务结构嵌入在堆栈的最下方。但是鉴于任务结构也占据不小的空间,而且要把任务结构放在栈底,还需要把任务结构复制到栈中。因此,在2.6版本时,内核设计了结构体thread_info,取而代之的是thread_info放在了栈底。通过对寄存器esp进行对齐运算,即可方便地找到当前进程的thread_info:
而thread_info中有一个指针指向进程的任务结构,因此获取当前进程的任务结构的方法如下:
但是,内核开发者还是认为计算thread_info位置时间过长,于是采用了以空间换时间的办法,从2.6.22版本开始,内核在内存中定义了一个变量current_task记录当前进程的任务结构。内核不再通过计算,而是直接通过一条访存指令来设置或者读取当前进程的任务结构。
我们在前面看到,在内核初始化时,current_task指向进程0的任务结构init_task。以后每次切换进程时,调度函数设置current_task指向下一个投入运行的进程的任务结构: