现在,计算机中已经有了一个名副其实的、3特权级的进程——进程0。下面我们要详细讲解进程0做的第一项工作——创建进程1。
进程0现在处在3特权级状态,即进程状态。正式开始运行要做的第一件事就是作为父进程调用fork函数创建第一个子进程——进程1,这是父子进程创建机制的第一次实际运用。以后,所有进程都是基于父子进程创建机制由父进程创建出来的。3.1.1 进程0创建进程1在Linux操作系统中创建新进程的时候,都是由父进程调用fork函数来实现的。该过程如图3-1所示。执行代码如下:
//代码路径:init/main.c: … static inline _syscall0(int,fork) // 对应fork()函数 static inline _syscall0(int,pause) static inline _syscall1(int,setup,void *,BIOS) … void main(void) { sti(); move_to_user_mode(); if (!fork()) { /* we count on this going ok */ init(); } /* * NOTE!! For any other task 'pause()' would mean we have to get a * signal to awaken, but task0 is the sole exception (see 'schedule()') * as task 0 gets activated at every idle moment (when no other tasks * can run). For task0 'pause()' just means we go check if some other * task can run, and if not we return here. */ for(;;) pause(); }从上面main.c的代码中对fork()的声明,可知调用fork函数;实际上是执行到unistd.h中的宏函数syscall0中去,对应代码如下:
//代码路径:include/unistd.h: … #define __NR_setup 0 /* used only by init, to get system going */ #define __NR_exit 1 #define __NR_fork 2 #define __NR_read 3 #define __NR_write 4 #define __NR_open 5 #define __NR_close 6 … #define _syscall0(type,name) \ type name(void) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name)); \ if (__res >= 0) \ return (type) __res; \ errno= -__res; \ return -1; \ } … volatile void _exit(int status); int fcntl(int fildes, int cmd, ...); int fork(void); int getpid(void); int getuid(void); int geteuid(void); … //代码路径:include/linux/sys.h: extern int sys_setup(); extern int sys_exit(); extern int sys_fork(); //对应system_call.s中的_sys_fork,汇编中对应C语言的函 //数名在前面多加一个下划线"_" ,如C语言的sys_fork对应汇编的 //就是_sys_fork extern int sys_read(); extern int sys_write(); extern int sys_open(); … fn_ptr sys_call_table[]={sys_setup, sys_exit, sys_fork, sys_read,//sys_fork对应_sys_call_table的第三项 sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link, sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod, … syscall0展开后,看上去像下面的样子: int fork(void) //参看2.5节、2.9节、2.14节有关嵌入汇编的代码注释 { long __res; __asm__ volatile ("int $0x80" // int 0x80是所有系统调用函数的总入口,fork()是其中// 之一,参看2.9节的讲解及代码注释 : "=a" (__res) //第一个冒号后是输出部分,将_res赋给eax : "0" (__NR_ fork)); //第二个冒号后是输入部分,"0":同上寄存器,即eax, // __NR_ fork就是2,将2给eax if (__res >= 0) // int 0x80中断返回后,将执行这一句 return (int) __res; errno= -__res; return -1; } //重要:别忘了int 0x80导致CPU硬件自动将ss、esp、eflags、cs、eip的值压栈!参看2.14节的讲//解及代码解释int 0x80的执行路线很长,为了清楚起见,将大致过程图示如下(见图3-2)。
详细的执行步骤如下:先执行: "0" (__NR_ fork)这一行,意思是将fork 在sys_call_table[]中对应的函数编号__NR_ fork(也就是2)赋值给eax。这个编号即sys_fork()函数在sys_call_table中的偏移值。紧接着就执行"int $0x80" ,产生一个软中断,CUP从3特权级的进程0代码跳到0特权级内核代码中执行。中断使CPU硬件自动将SS、ESP、EFLAGS、CS、EIP这5个寄存器的数值按照这个顺序压入图3-1所示的init_task中的进程0内核栈。注意其中init_task结构后面的红条,表示了刚刚压入内核栈的寄存器数值。前面刚刚提到的move_to_user_mode这个函数中做的压栈动作就是模仿中断的硬件压栈,这些压栈的数据将在后续的copy_process()函数中用来初始化进程1的TSS。值得注意,压栈的EIP指向当前指令"int $0x80"的下一行,即if (__res >= 0) 这一行。这一行就是进程0从 fork函数系统调用中断返回后第一条指令的位置。在后续的3.3节将看到,这一行也将是进程1开始执行的第一条指令位置。请记住这一点! 根据2.9节讲解的sched_init函数中set_system_gate(0x80,&system_call)的设置,CPU自动压栈完成后,跳转到system_call.s中的_system_call处执行,继续将DS、ES、FS、EDX、ECX、EBX 压栈(以上一系列的压栈操作都是为了后面调用copy_process函数中初始化进程1中的TSS做准备)。最终,内核通过刚刚设置的eax的偏移值“2”查询 sys_call_table[],得知本次系统调用对应的函数是sys_fork()。因为汇编中对应C语言的函数名在前面多加一个下划线“_”(如C语言的sys_fork()对应汇编的就是_sys_fork),所以跳转到 _sys_fork处执行。点评一个函数的参数不是由函数定义的,而是由函数定义以外的程序通过压栈的方式“做”出来的,是操作系统底层代码与应用程序代码写作手法的差异之一;需要对C语言的编译、运行时结构非常清晰,才能彻底理解。运行时,C语言的参数存在于栈中。模仿这个原理,操作系统的设计者可以将前面程序所压栈的值,按序“强行”认定为函数的参数;当call这个函数时,这些值就可以当做参数使用。上述过程的执行代码如下:
//代码路径:kernel/system_call.s: … _system_call: # int 0x80——系统调用的总入口 cmpl $nr_system_calls-1,