Linux内核完全注释(20周年版·第2版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.4.3 进程初始化

当boot/目录中的引导程序把内核从磁盘上加载到内存中,并让系统进入保护模式下运行后,就开始执行系统初始化程序init/main.c。该程序首先确定如何分配使用系统物理内存,然后调用内核各部分的初始化函数分别对内存管理、中断处理、块设备和字符设备、进程管理以及硬盘和软盘硬件进行初始化处理。在完成了这些操作之后,系统各部分已经处于可运行状态。此后程序把自己“手工”移动到任务0(进程0)中运行,并使用fork()调用首次创建出进程1。在进程1中程序将继续进行应用环境的初始化并执行shell登录程序。而原进程0则会在系统空闲时被调度执行,此时任务0仅执行pause()系统调用,并会再调用调度函数。

“移动到任务0中执行”这个过程由宏move_to_user_mode(include/asm/system.h)完成。它把main.c程序执行流从内核态(特权级0)移动到了用户态(特权级3)的任务0中继续运行。在移动之前,系统在对调度程序的初始化过程(sched_init())中,首先对任务0的运行环境进行了设置。这包括人工预先设置好任务0数据结构各字段的值(include/linux/sched.h)、在全局描述符表中添入任务0的任务状态段(TSS)描述符和局部描述符表(LDT)的段描述符,并把它们分别加载到任务寄存器tr和局部描述符表寄存器ldtr中。

需要强调的是,内核初始化是一个特殊过程,内核初始化代码也即是任务0的代码。从任务0数据结构中设置的初始数据可知,任务0的代码段和数据段基址是0,段限长是640KB。而内核代码段和数据段的基址是0,段限长是16MB,因此任务0的代码段和数据段分别包含在内核代码段和数据段中。内核初始化程序main.c就是任务0中的代码,只是在移动到任务0之前系统正以内核态特权级0运行着main.c程序。宏move_to_user_mode的功能就是把运行特权级从内核态的0级变换到用户态的3级,但是仍然继续执行原来的代码指令流。

在移动到任务0的过程中,宏move_to_user_mode使用了中断返回指令造成特权级改变的方法。该方法的主要思想是在堆栈中构筑中断返回指令需要的内容,把返回地址的段选择符设置成任务0代码段选择符,其特权级为3。此后执行中断返回指令iret时将导致系统CPU从特权级0跳转到外层的特权级3上运行。图2-7是特权级发生变化时中断返回堆栈结构示意图。

图2-7 特权级发生变化时中断返回堆栈结构示意图

宏move_to_user_mode首先往内核堆栈中压入任务0数据段选择符和内核堆栈指针。然后压入标志寄存器内容。最后压入任务0代码段选择符和执行中断返回后需要执行的下一条指令的偏移位置。该偏移位置是iret后的一条指令处。

当执行iret指令时,CPU把返回地址送入CS:EIP中,同时弹出堆栈中标志寄存器内容。由于CPU判断出目的代码段的特权级是3,与当前内核态的0级不同。于是CPU会把堆栈中的堆栈段选择符和堆栈指针弹出到SS:ESP中。由于特权级发生了变化,段寄存器DS、ES、FS和GS的值变得无效,此时CPU会把这些段寄存器清零。因此在执行了iret指令后需要重新加载这些段寄存器。此后,系统就开始以特权级3运行在任务0的代码上。所使用的用户态堆栈还是原来在移动之前使用的堆栈。而其内核态堆栈则被指定为其任务数据结构所在页面的顶端开始(PAGE_SIZE+(long)&init_task)。由于以后在创建新进程时,需要复制任务0的任务数据结构,包括其用户堆栈指针,因此要求任务0的用户态堆栈在创建任务1(进程1)之前保持“干净”状态。