从前面设备环境的初始化以及进程0的激活,计算机中已经存在一个3特权级的进程,它的第一项工作就是作为父进程调用fork函数创建第一个子进程----进程1。之后的所有进程都是基于父子进程创建机制由父进程创建出来。
三、进程1的创建及执行
1)进程1的创建
在main函数里面的执行代码只有简短的几行,但是看完这章之后,我是不得不说说Linux博大精深。执行代码如下:
void main()
{
...
sti(); // 所有初始化工作都做完了,开启中断
move_to_user_mode(); // 进程0翻转到3特权级
if (!fork())
{
init(); // 子进程1执行部分
}
for(;;) pause(); // 父进程0执行部分
}
在这里,父进程0调用fork函数实质是执行系统调用sys_fork(),代码如下:
int fork(void)
{
volatile long __res;
__asm__ volatile ("int $0x80" // int 0x80是所有系统调用函数总入口
: "=a" (__res) // 输出部分,将_res赋给eax
: "0" (__NR_fork) // 输入部分,"0":同上,寄存器eax
)
if (__res >= 0) // int 0x80中断返回后,执行该语句
return (init) __res;
errno = -__res; // 否则置出错号,并返回-1
return -1;
}
这段代码执行步骤如下:
先执行输入部分 :"0" (__NR_fork),找出系统调用表中sys_fork函数的位置;接着产生int $0x80中断,然后CPU从3特权级的进程0跳入0特权级的内核代码中执行,在这里就是跳转至_sys_fork处,执行完毕后输出__res值到eax寄存器并返回,然后执行if (__res >= 0)语句。中断int 0x80的执行路线,如下图:
如图示,在执行_sys_fork过程中,首先,内核需要调用find_empty_process()函数为进程1分配可用的新进程号,然后开始调用copy_process()函数执行。在之前激活进程0的过程中,内核中含有“进程0的task_struct ”和“进程0的页表项”等专属进程0的管理信息,所以,进程0将在copy_process函数中体现父子进程创建机制的工作,具体包括:
1) 为进程1创建task_struct,将进程0的task_struct的内容复制给进程1;
2) 为进程1的task_struct、TSS做个性化设置;
3) 为进程1创建第一个页表,将进程0的页表项内容赋给这个页表;
4) 进程1共享进程0的文件;
5) 设置进程1的GDT项;
6) 最后将进程1设置为就绪态,使其能够参与进程间的轮转。
进入copy_process()函数(请参考linux0.11源码),首先在主内存申请一空闲页面,用于进程1的task_struct及内核栈;然后,将父进程0的task_struct赋给子进程1,并开始子进程的个性化设置,然后特别需要注意的一点,在这里会将子进程1中TSS的eip赋上fork执行中断时的下一行代码if (__res >= 0)的地址和eax赋初值0,而父进程0的eax将是子进程1的进程号,我们知道,fork调用会返回两个值,父进程返回子进程号,子进程返回0,就是在这体现的!之后,就要开始设置进程1的分页管理了。
重要:每个进程都要加载属于自己的代码、数据,这些代码、数据的寻址都是用逻辑地址,即段+偏移的形式表示的。CPU首先自动将逻辑地址计算为可寻址的线性地址,然后,根据操作系统对页目录表,页表的设置,自动将线性地址转换为分页的物理地址。所以,进程1的的分页管理设置路线也如此,先在进程的线性地址空间设置代码段、数据段,然后设置页表,页目录,这里是通过调用cpy_mem()函数实现。
完成分页设置之后,此时,进程1还没有加载自己的程序代码,它的页表又是从父进程0的页表复制而来,所以,他们的管理完全一致,即暂时和父进程共享一套内存页面管理结构。当子进程1有了自己的程序,再将关系解除,并重新组织自己的内存管理结构。这里,会涉及到写时复制的技术,以后再说咯~
最后,copy_process中会让子进程1会与父进程0文件共享,将进程1的TSS和LDT挂接至GDT中,再设置进程1的状态为就绪态,返回进程号1。至此,进程1的创建工作完成,此时,进程1已经具备父进程0的全部能力,可以在主机中正常运行。
此时fork()函数中的中断返回eax上的值,为进程号1,也即fork函数的返回值,main()函数检测到if(!fork())为假,不执行init(),进程0执行到for循环,调用pause()函数。
2)进程1的调度
进程0此刻执行for循环里的pause()函数,pause函数与fork函数一样,都会执行系统调用,在这里,会调用sys_pause函数,代码如下:
int sys_pause (void)
{
current->state = TASK_INTERRUPTIBLE;
schedule ();
return 0;
}
首先就会对当前进程设置可中断等待状态,然后进行调度函数schedule()。此函数会遍历系统内所有进程,然后根据调度策略找到处在就绪态的相应进程,在这就是切换到进程1执行。
这个时候进程1从哪开始执行?这是个问题。其实,在schedule里面,首先会将之前的进程1的各寄存器值恢复,然后,进程1就从TSS中eip寄存器的值处开始执行,就是前面讲的fork中的if (__res >= 0)处。此处的__res为之前创建进程1时里的个性化设置的0,所以,在main函数里面,fork函数返回值为假,进入init()。。。
之后就是进程1的执行了,它会设置硬盘信息,格式化虚拟盘,加载根文件系统等等工作,后续的事反正我就不是太感兴趣了,笔记略过。。。等这些事做完,开始进程2的创建及执行,为系统进入怠速状态做最后准备。