进程0是一个特殊的进程,它是所有其它进程的祖先进程,所有其它的进程都是fork通过系统调用,复制进程0或者其后代进程产生的。但是进程0却不是通过fork调用产生的。进程0的代码就是内核system模块的代码,所以可以认为系统一启动进程0就开始运行。但是此时并不是真正的进程0,应为此时gdt中还没有设置tss和ldt描述符,直到sched_init()中才设置了tss和ldt并且把tss加载到tr寄存器,所以在此时应该算是进入真正的进程0,之前可以认为是进程0的初始化设置,可以说进程0是手动设置的。
step1、手工设置进程控制块、页目录和页表
进程控制块是直接手工设置好的,在sched.c第115行INIT_TASK
#define INIT_TASK \
/* state etc */ { 0,15,15, \
/* signals */ 0,{{},},0, \
/* ec,brk... */ 0,0,0,0,0,0, \
/* pid etc.. */ 0,-1,0,0,0, \
/* uid etc */ 0,0,0,0,0,0, \
/* alarm */ 0,0,0,0,0,0, \
/* math */ 0, \
/* fs info */ -1,0022,NULL,NULL,NULL,0, \
/* filp */ {NULL,}, \
{ \
{0,0}, \ // ldt第0项是空
/* ldt */ {0x9f,0xc0fa00}, \ //代码段长640K,基地0,G=1,D=1,DPL=3,P=1,TYPE=0x0a
{0x9f,0xc0f200}, \ //数据段长640K,基地0,G=1, D=1, DPL=3,P=1, TYPE=0x02
}, \
/*tss*/ {0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir,\
// esp0 = PAGE_SIZE+(long)&init_task 内核态堆栈指针初始化为页面最后
// ss0 = 0x10 内核态堆栈的段选择符,指向系统数据段描述符,进程0的进程控制
// 块和内核态堆栈都在system模块中
// cr3 = (long)&pg_dir 页目录表,其实linux0.11所有进程共享一个页目录表
0,0,0,0,0,0,0,0, \
0,0,0x17,0x17,0x17,0x17,0x17,0x17, \
_LDT(0),0x80000000, \ // ldt表选择符指向gdt中的LDT0处
{} \
}, \
}
task数组的初始化在sched.c第65行,这里task数组的的一项直接指向了进程0的进程控制块。
即struct task_struct * task[NR_TASKS] = {&(init_task.task), };
从手工设置的进程控制块,我们可以看到:
1、进程0的代码基地址为0段长为640K,数据段基地址也为0段长也为640K。也就是说进程0的代码段、进程0的数据段、系统代码段和系统数据段4者重合。也可以认为系统的代码就是进程0的代码。
2、进程0的进程控制块和内核态堆栈都位于系统模块内。
3、页目录、页表的设置在head.s中完成,进程0的页目录、页表就是系统的页目录、页表。进程0的页表也位于内核模块中,就是内核模块中的4个页表,它们完全映射了16M物理内存。
step2、设置tss段描述符、ldt段描述符和加载TR寄存器
main函数中调用了sched_init()函数,在该函数中设置了进程0的tss段描述符,ldt段描述符,并且加载TR寄存器,使它指向进程0的tss段。这时进程0所有的结构才齐全,才可以参与进程调度。我们可以认为当sched_init()函数结束后进程0正式产生。
step3、切换到用户态运行
接下来需要把进程0从内核态转移到用户态。在main.c的137行有一句move_to_user_mode()把进程0从内核态移到用户态运行。
move_to_user_mode()是一个宏定义,在system.h的第1行。它利用模拟中断返回的方法把进程0从内核态切换到用户态。
进程0数据段的基地址还是0,所以(原SS : 原ESP)指向的还是当前堆栈(user_stack)的栈顶。进程0的代码段基地址仍然是0,所以iret返回的(CS : EIP)指向的就是iret下一条指令。这样执行iret指令后PC指向iret下一条指令,堆栈还是user_stack没变换,改变的仅仅是CPL,CPL从0变成了3,进程0完成了从内核态到用户态的切换。
用fork创建进程
除了进程0,其它所有的进程都是fork产生的。子进程是通过复制父进程的数据和代码产生的。创建结束后,子进程和父进程的代码段、数据段共享。但是子进程有自己的进程控制块、内核堆栈和页表。
我们知道一个进程需要有如下3个结构
1. task[]数组中的一项,即进程控制块(task_struct)
2. GDT中的两项,即TSS段和LDT段描述符
3. 页目录和页表
所以fork()的任务就是为一个新进程构造这3个结构。
sys_fork() 系统调用的实现在2个文件中。fork.c中的全部和system_call.s中208-291行。sys_fork()系统调用分成2步完成,第一步调用函数find_empty_process(),在task[]数组中找一项空闲项,第二步调用copy_process() 函数,复制进程。
在main.c中fork()通过_syscall0实现的,展开该宏,可知实际上是在system_call.s文件中,通过int 0x80中断,调用_sys_fork。
_sys_fork:
call _find_empty_process
testl %eax,%eax
js 1f
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call _copy_process
addl $20,%esp
1: ret
从以上代码可知,此时调用了函数find_empty_process()和copy_process(),以上两个函数在fork.c文件中实现。在copy_process中调用了copy_mem()函数,设置新进程的LDT项(数据段描述符和代码段描述符)中的基地址部分并且复制父进程(也就是当前进程)的页目录和页表,实现父子进程数据代码共享。
以上说明的是fork创建子进程的过程,之后execve加载可执行文件,执行新的进程。关于进程的其它部分将在下篇博客中说明。
此博客参考了赵炯博士的《Linux内核完全剖析——基于0.12版内核》以及袁镱对进程管理分析的文章,在此特别表示感谢。