本文是为了弄清一下几个问题:
1)什么是进程?
2)进程由那些部分组成?
3)如何创建一个进程?
4)如何确保进程间互不干扰?
5)进程切换做了哪些工作?
6)进程调度是怎么实现的?
7)什么是系统调用?
1)什么是进程?
下面来先来回答第一个问题:什么是进程?百度百科上给出了定义,狭义上讲进程就是一段程序的执行过程,从广义上讲进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。举个例子来说,你准备好菜谱,和烧菜的材料,然后把材料按照菜谱烧菜一道菜的过程就是一个进程。而那菜谱和材料就好比是程序里的代码和数据。程序是静态的,而进程是程序执行的动态过程。
2)进程由那些部分组成?
那么一个进程由那些部分组成呢?这就是上面提出的第二个问题。一般来讲,进程由4个部分组成:进程控制块、代码、数据、堆栈。其中进程控制块又称进程表,它的作用是保存进程的状态,它也是进程的唯一标志。那进程表里是什么内容呢,来看下进程表结构的定义:
typedef struct s_stackframe { u32 gs; /* \ */ u32 fs; /* | */ u32 es; /* | */ u32 ds; /* | */ u32 edi; /* | */ u32 esi; /* | pushed by save() */ u32 ebp; /* | */ u32 kernel_esp; /* <- 'popad' will ignore it */ u32 ebx; /* | */ u32 edx; /* | */ u32 ecx; /* | */ u32 eax; /* / */ u32 retaddr; /* return addr for kernel.asm::save() */ u32 eip; /* \ */ u32 cs; /* | */ u32 eflags; /* | pushed by CPU during interrupt */ u32 esp; /* | */ u32 ss; /* / */ }STACK_FRAME; typedef struct s_proc { STACK_FRAME regs; /* process registers saved in stack frame */ u16 ldt_sel; /* gdt selector giving ldt base and limit */ DESCRIPTOR ldts[LDT_SIZE]; /* local descriptors for code and data */ u32 pid; /* process id passed in from MM */ char p_name[16]; /* name of the process */ }PROCESS;
可以看到PROCESS结构体中,包含了5个成员,他们分别是保存进程相关寄存器的栈帧、局部描述符表选择子、局部描述符表数组、pid、进程名。其中最重要的是保存寄存器的栈帧和局部描述符表,栈帧结构里保存中进程被中断时各个寄存器中的状态,而局部描述符表选择子对应GDT中的一个描述符,根据它来找到局部描述符表数组中对应的LDT(该内容在之前文章中介绍过)。
3)如何创建一个进程?
接下来看下第三个问题:如何创建一个进程?首先看下面这张图,它展示了我们第一个进程的启动过程:
当我们再添加进程是,需要考虑的要素无外乎4点:进程体、进程表、GDT、TSS。
4)如何确保进程间互不干扰?
这个问题可以从两个方面来讲,一个是内存管理,另一个是进程表切换。先将内存管理,这点之前文章已经分析过了。cpu在执行代码的时候,全局或局部描述符表,分别寻找内核态代码和用户态代码。而对于进程来讲,他们拥有各自的局部描述符表,也就是他们都共享一份内核态代码但是用户态代码都是独立的。并且这些代码经过分页机制之后被拷贝到内存,这个内存地址由分页的映射关系决定,而这个映射关系是每个进程都不一样的。相当与内存管理中对进程进行了两层隔离。
另一个方面就是进程表切换,在多进程环境中,为什么进程切换但是有不会相互干扰呢。这是因为在进程调度来临,也就是时钟中断来临时,把当前cpu各个寄存器的值都保存到了进程表中,当要切换回该进程时,再把进程表中的值赋值给cpu各个寄存器。所有进程都这样操作,就确保了进程切换但不会互相干扰。
5)进程切换做了哪些工作?
为了实现进程切换,我们首先要考虑的一个问题是:如何保存非活动进程的状态。
进程要占用内存空间,里面存放的是代码和数据。内存空间不用去管它,放在那里就好。只要系统不将其标记为可用内存就行。
那剩下的问题就是堆栈和寄存器。寄存器的值用一个“pushad”就可以全部压入栈中保存,可是问题就来了:ESP寄存器是栈指针,它的值该如何保存?
Intel CPU规定,在使用pushad时,ESP的值为执行指令时ESP寄存器的值。因此,使用pushad保存的是原来指针的位置。
由于进程切换是运行在内核级的操作,所以它的特权级为0。因此这里又涉及到了一次特权级转换。假设进程运行在特权级3,切换进程时,TSS会自动将SS:ESP切换到0特权级使用的栈,因此我们要从3特权级使用的栈中取出SS:ESP的值,连同其它寄存器储存在某一个地方,这个地方就叫进程表。虽然进程表是一个结构体,而不是栈,但是为了将寄存器压入进程表,我们还需要将ESP指向进程表。
寄存器的值保存完毕后,我们再让ESP指向内核使用的0特权级栈,并开始进程调度。
下一个问题:我们应当在什么时候决定将CPU的控制权交给另一个进程?
一般来说,多个进程共享一个CPU的话,每个进程执行的时间应该是一样的(后面再考虑进程优先级的问题)。在相同的时间内反复发生的事件,也许你已经想到了,就是CPU的时钟中断。在中断的时候,我们将寄存器和堆栈保存到进程表。这个工作叫做保护现场。而调度完毕,准备执行下一个进程前,要恢复各个寄存器的值,这个过程叫做恢复现场。为了准备下一次进程切换,TSS中SS0(内核特权级的栈指针)必须指向下一个进程的进程表。这个工作要在从中断返回前一刻执行。返回中断的时候,iretd实际上是从栈中读取返回时的CS、EIP、SS、ESP的,由于进程表特殊的结构,几个寄存器的值正好会被设定成下一个进程应有的值,从而实现将CPU的控制权交给下一个进程的操作。
因此,进程切换的步骤如下:
发生中断
将ESP保存并指向进程表
将保存的ESP及其它寄存器的值保存到进程表
将ESP指向内核栈
进程调度,选择下一个准备执行的进程
将ESP指向下一个进程的进程表
中断返回
需要注意的问题
如果在中断发生过程中又发生中断会怎么样?这种情况叫做中断重入。经过书中的试验可以得知,出现这样的情况,进程调度模块就会进入死循环,无法回到进程,而是不断递归调用。随着时间的推移,堆栈会溢出,于是意想不到的事情就会发生。
解决办法是:设一个全局变量,如果中断处理程序执行时检测到该变量已置位,则跳过处理的步骤;否则将其置位。在处理结束后,再将其复位即可。
6)进程调度如何实现?
进程调度涉及的是如何在不同优先级的进程之间分配CPU时间的问题。如果所有进程都是同一个优先级,则不需要调度,中断的时候直接跳到下一个进程即可。正因为有了优先级的存在,才有了进程调度模块。为了实现优先级,我们给每一个进程加一个变量ticks。当时钟中断发生时,就将ticks减一。当ticks小于0时,将控制权交给下一个进程,该进程不再有执行的机会,直到所有进程的ticks都为0为止。然后将ticks恢复为各自优先级的数值,并开始下一轮循环。原先的进程调度可以视为每个进程的优先级都为1。由于优先级的数值可以很大,比如10、15,所以有必要将时钟中断的频率调高一些(方法参见书中原文)。这样一来,就打破了进程执行时间的对称性,优先级得以体现。