进程控制
基本概念
什么是进程控制?
如何实现进程控制?
用“原语”实现。
进程控制相关的原语
- 进程的创建
- 进程的终止
- 进程的阻塞
- 进程的唤醒
- 进程的切换
进程控制的主要功能是对系统中的所有进程实施有效的管理,它具有创建新进程、撤销已有进程、实现进程状态转换等功能。
简单来说,进程控制,就是要实现进程的状态转换。
比如“创建新进程”,不就是让进程由“创建态”转换为“就绪态”吗。“撤销已有进程”,就是让进程由“运行态”转换为“终止态”。
那么,在进程状态转换的过程中,操作系统需要做一些什么事情呢?——这就是这个小节我们要讨论的内容。
进程控制是要用“原语”实现的。
而“原语”的概念,我们在第一章当中提到过。——操作系统的内核中,有一种特殊的程序叫原语,它的执行具有原子性。也就是说,这段程序的运行必须一气呵成,中间不允许被中断。
也就是说,进程控制(实现进程的状态转换)的操作一定是一气呵成的。
思考:为什么进程控制(状态转换)的过程要“一气呵成”?
例如:假设PCB中的变量state表示进程当前所处状态,1表示就绪态,2表示阻塞态……

那么,如果一个进程是处于就绪态的(state=1),这个进程的PCB肯定是要挂在就绪队列里的。而如果state=2的话,这个进程的PCB就一定挂在阻塞队列里。
接下来,考虑这样一种情况:
处于阻塞队列里的进程,它肯定是在等待某一事件的发生。那假设PCB2这个进程,它所等待的事件已经发生了,那么,此时这个进程就要从阻塞态→就绪态了。
所以,操作系统内核程序就需要把这个进程的状态从阻塞态变为就绪态,而进行状态转换的过程,至少需要做两件事情:①把PCB2的state设为1;②把PCB2从阻塞队列放到就绪队列。
接下来,考虑这样一种情况:
假设我把PCB2的state已经设为1了(如上述的步骤①),而此时,(在执行步骤②,也就是将其从阻塞队列放到就绪队列之前)收到了中断信号,系统就会转而去处理中断。就会导致这样一个结果,PCB2的state=1,但是它却处在阻塞队列里。就会导致,该进程的state所表示的状态,和该进程所处的队列不统一。

因此,如果操作系统对进程的状态转换如果不能“一气呵成”。就有可能导致操作系统中的某些关键数据结构信息不统一的情况,这会影响操作系统进行后续的别的一些工作,会让系统出错。
那刚好,“原语”这种程序,它具有一气呵成、不可中断的性质,所以我们可以用“原语”这种特殊的程序来实现“一气呵成”这样的事情,从而来实现进程控制。
原语的执行具有原子性,即执行过程只能一气呵成,期间不允许被中断。
现在会有个疑问,为什么“原语”就“一气呵成、不可中断”了呢?
其实它的原子性,是由两个特权指令:“关中断指令”和“开中断指令”实现的。
我们来看一下关中断、开中断,这两个指令的作用。

假设这是一个正在运行的内核程序,那CPU会依次执行这些指令,而且根据第一章的讲解,我们知道,CPU每执行完一条指令,之后,都会例行检查是否有中断信号需要处理。
如果它在执行完指令2之后,CPU发现有一个中断信号,那么在这种情况下,CPU就会暂停当前执行的程序,转而执行处理中断的程序。等这个中断处理程序完成之后,再回到原来的程序中继续往下执行。

正常情况:CPU每执行完一条指令都会例行检查是否有中断信号需要处理,如果有,则暂停运行当前这段程序,转而执行相应的中断处理程序。
接下来看一下,如果执行到了“关中断指令”,会出现什么情况。
CPU在依次执行这些指令,当它执行了“关中断指令”这条特权指令之后,CPU就不再例行检查中断信号了。
CPU会接着往下执行。那如果在执行指令a后,有一个外部中断信号到来了,它就并不会像之前一样例行检查是否有中断信号,而是会继续往下执行,一直到CPU执行了“开中断指令”之后,它才会恢复以前的那种习惯,即“每执行完一条指令,检查一下此时是否有中断信号”。

CPU执行了关中断指令之后,就不再例行检查中断信号,直到执行开中断指令之后才会恢复检查。
开中断指令执行之后,它这时才会检查是否有中断信号,于是便发现:来了一个外部中断信号我还没有处理。然后转而执行这个中断处理程序。

从这个例子我们就可以看到,在关中断和开中断这两个指令之间的这一系列的指令序列,它们的执行肯定是不可被中断的,这样就实现了所谓的“原子性”。
这样,开中断、关中断 之间的这些指令序列就是不可被中断的,这就实现了“原子性”。
这两个指令是特权指令。
但是,我们思考一下:如果这两个特权指令允许普通的用户程序使用的话,会发生什么情况?
那这是不是就意味着:我可以直接在我的程序最开头,植入一个“关中断指令”,一直到我的程序末尾,再执行“开中断指令”,这样的话,只要我的程序上CPU运行了,那我的程序就会一直霸占着CPU,而不会被中断。那显然,这种情形是不允许发生的。
所以,开中断、关中断指令是特权指令,只能让内核使用,而不能让普通用户使用。
至此,再次总结一下:
1、进程控制(进程的状态转换)这个事情,必须一气呵成。而想要做到一气呵成,我们可以用“原语”这种特殊的程序来实现。
2、而“原语”的实现,需要由开中断指令和关中断指令来配合着完成。
接下来需要探讨的是,实现进程控制的这些“原语”,到底做了哪些事情?
这些原语,每个原语所做的事情具体有哪些,的确很多,但不需要死记硬背,理解其前因后果即可。以理解为主,而不是想着把每一个步骤刻意的默写到位。
首先来看用于实现“进程的创建”的原语。
如果操作系统要创建一个进程,那么它就必须使用创建原语。
而创建原语,会干这样几件事情:
首先,申请一个空白的PCB。因为PCB是进程存在的唯一标志,所以,要想创建一个进程肯定要创建一个和它相对应的PCB。
另外,还会给这个进程分配它所需要的资源。比如内存空间,等等。
然后还会把这个PCB的内容进行一番初始化。比如分配PID、设置UID,等等。
最后,它还会把这个PCB插入到就绪队列。
所以,创建原语让一个进程从创建态→就绪态。
那有一些典型的事件,会引起操作系统使用创建原语创建一个进程。
比如,当一个用户登录的时候。操作系统会给这个用户建立一个与之对应的用户管理进程,或者用户通信进程,等等。
或者,发生作业调度的时候,也会创建一个进程。
作业,就是此时还放在外存里的、还没有投入运行的程序。所谓的作业调度就是指,从外存当中挑选一个程序,把它放入内存,让它开始运行。
那我们知道,当一个程序要运行的时候,肯定是要创建一个进程的。所以,当发生作业调度的时候,就需要使用到创建原语了。
有时,一个进程可能向操作系统提出某些请求,操作系统会专门建立一个进程来处理这些请求。
还有的时候,一个进程可以主动请求创建一个子进程。
总之,发生这些事件的时候,都会引起系统创建一个新的进程,也就是说,它会使用到创建原语。

撤销原语是终止一个进程所使用的。
使用撤销原语之后,就能使进程由某种状态转向终止态,最终这个进程从系统中彻底消失。
撤销原语需要做这样的一些事情:
很多事件会引起一个进程的终止:
Ctrl+Alt+delete打开任务管理器来强行结束某一进程。
有时一个进程要从运行态→阻塞态,这时操作系统就会在背后执行一个阻塞原语来实现。
阻塞一个进程比较简单:
首先,找到要阻塞的进程,它对应的PCB。
之后,需要保护进程运行的现场,系统还需要将PCB的状态信息设置为“阻塞态”,还要让进程下处理机、暂停其运行。
什么叫“保护进程运行现场”,这个一会儿再解释,因为这又是一个比较庞大的话题。
最后,将PCB插入相应事件的等待队列。
经过之前的学习我们知道,一个进程需要被阻塞,那肯定是因为它主动请求要等待某一事件的发生。
而如果这个进程所等待的事件发生了之后,这个进程就会被唤醒,也就是说,操作系统会让这个进程的状态从阻塞态→就绪态。
那么这个时候就要用到唤醒原语。
唤醒原语需要做这样几个事情:
无论是阻塞原语、唤醒原语,它们做的这个流程其实很容易理解。
但是需要注意的是:一个进程因为什么事情被阻塞,就应该被什么事情给唤醒。所以,唤醒原语和阻塞原语,必须是成对使用的。

切换原语,会让此时正处于运行态的进程下处理机,让它回到就绪队列;并且从就绪队列当中,选择一个处于就绪态的进程,让它上处理机运行。
所以,切换原语会让两个进程的状态发生改变:一个是运行态→就绪态;另一个是就绪态→运行态。
切换原语需要做如下一些事情:
首先,要把进程的运行环境信息存到PCB当中
什么叫“进程的运行环境信息”呢?这点涉及到一些硬件的知识,我们一会儿再展开细聊。
另外,它还会把进程的PCB移到相应的队列。
它还会挑选一个进程,让它上处理机运行,并且更新其PCB。
同时,它还会根据这个新进程的PCB,恢复出它所需要的运行环境。
什么叫“保存运行环境”,什么叫“恢复运行环境”,这是比较难理解的地方,接下来我们深入探讨一下这个问题。
再次拓展:程序是如何运行的?

通过之前的学习,我们已经了解到了,一个程序的运行需要经历这样一个流程。
程序运行之前,需要把它的指令放到内存当中,然后CPU从内存中读取这些一条一条的指令并且执行。
但是接下来我们要拓展一个更深层次的细节:
CPU在执行这些指令的过程中,需要进行一系列的运算。那么,CPU当中会设置很多的“寄存器”来存放这些指令在运行过程当中所需要的某些数据。总之,寄存器,就是CPU里面用于存放数据的一些地方。
那CPU里面会有各种各样的寄存器。
比如说,我们之前提到过的PSW,就是程序状态字寄存器,CPU的状态,内核态/用户态,这个状态信息,就是保存在PSW这个寄存器里面的(当然,PSW里面还保存了一些其他的信息,这个我们就不说了,这是计组需要探讨的内容)。
另外,CPU中还会有一个比较关键的寄存器,叫做PC,也就是程序计数器寄存器,这个寄存器里面存放的是接下来需要执行的指令它的地址是多少。
另外,CPU中还会有一个指令寄存器,IR。这个寄存器中存放的是,当前CPU正在执行的那条指令。
此外,CPU还会有其他的一些通用寄存器,用来存放一些别的重要信息。
总之,CPU中有很多寄存器,我们这只列举了操作系统这门课需要了解一下的寄存器。

接下来我们分析一下,对于如下这样一系列指令的执行,在背后发生了什么样的事情?

指令1,那么它会把指令1的内容读到IR当中,并且,PC中会存放接下来它应该执行的那条指令,也就是指令2的地址。 指令1时,它发现,它要做的是在内存中的某个地方,写入变量x的值。那么CPU在执行指令1的时候,就会往内存中的某个地方写入变量x的值,为1。
指令1后,CPU就会执行下一条指令,那么通过PC这个寄存器,它就知道下一条要执行的指令,应该是指令2,所以接下来它会取出指令2,把指令2的内容放在IR中。同时,PC的内容也更新为再下一条指令。 指令2的内容是,把变量x的值放到某一个通用寄存器当中。所以,CPU会从内存中取出这个x变量的值,把它放到通用寄存器当中。于是这个通用寄存器的内容就变成了1。
指令3,然后PC、IR的内容同样也会更新。 指令3的内容,它会把通用寄存器中的数据进行+1的操作。所以,通用寄存器中的值就会从1变成2。指令4,是让它把这个通用寄存器当中的内容,把它重新写回到变量x所存放的位置当中。所以执行了指令4之后,就会把内存中x的值,从1改为2。
可以看到,我们的int x=1;和x++;两个操作,其实CPU是在背后通过执行更基本的指令,来完成的。
并且从刚才讲的这些过程当中我们可以发现,这些指令按顺序执行的过程当中,有很多中间结果是放在这些寄存器当中的。比如x++;这个操作,刚开始得出的2这个值并没有赋给x,而是先放在了通用寄存器当中。
更值得注意的是,这些寄存器,并不是当前正在执行的这个进程所独属的。如果其他进程再上CPU运行的话,那么这些寄存器也会被其他进程所使用。
那么,这就会发生一种情况:
还是刚才的例子,假设CPU执行完指令3之后,此时CPU需要转而执行另一个程序的话。

我们知道,另一个进程如果上CPU运行的话,另一个进程也会同样地使用到这些寄存器。所以,另一个进程上CPU运行的时候,有可能把前一个进程在寄存器当中保留的这些中间结果给覆盖掉。

而我们刚才的那个进程不是执行到了指令3吗,而因为刚才执行到指令3时,寄存器当中得到的中间结果,此时都已经被覆盖掉了,所以这个进程就已经没有办法再往下运行了。
所以,为了解决这个问题,可以采取这样的策略:
当一个进程,它要下处理机的时候,可以把它之前运行的这个运行环境的信息,把它保存在自己的PCB当中。当然,这个PCB当中并不需要把所有的寄存器信息都保存下来,只需要保存一些必要的信息就可以了(比如PSW、PC、通用寄存器)。

比如,这个进程在执行指令3后,要下处理机了,那么我们将此时必要的寄存器信息保存在其PCB当中,接下来才去切换成别的进程。
接下来,别的进程在使用CPU的时候,可能会往这些寄存器里面写各种各样的数据。总之,会覆盖之前那个进程的数据。

但是,当之前的那个进程需要重新回到CPU运行的时候,操作系统就可以根据之前保存下来的这些信息,来恢复它的运行环境了。那把它的运行环境恢复之后,CPU就知道,接下来它要执行的是指令4,并且此时通用寄存器当中存放的数据是2。

所以,既然接下来要执行的是指令4。那CPU就会根据PC的指向,把指令4的指令内容取到IR当中,同时再让PC去指向下一条指令。然后执行当前要执行的指令4,看看该指令具体要干什么,它发现,它需要把当前通用寄存器当中的内容,写回到x存放的位置。

所以,接下来,它就会把2这个数据,写回到x的这个位置。

至此,这两个C语言代码就真正完成了。
上述讲了这么多的内容,主要就是为了让大家理解,什么叫“进程的运行环境信息”。其实所谓的“进程运行环境”,或者叫“进程上下文(Context)”,它就是运行过程当中,寄存器中存储的那些中间结果。当一个进程需要下处理机的时候,需要把它的运行环境存储到自己的PCB当中;而当一个进程需要重新回到CPU运行的时候,就可以重新从PCB当中恢复它之前运行的那个环境,让它继续往下执行了。
所以,保存进程的运行环境、恢复进程的运行环境,这是实现进程并发运行的一个很关键的技术。


(这些硬件的知识,在学过计组后会更好理解了)
对于相关原语具体做了哪些步骤,不需要死记硬背,理解即可。其实,无论是哪种原语,它所做的工作,无非就是三类事情:
1、更新PCB当中的一些信息(主要是:①修改进程状态字state;②保存/恢复运行环境)
2、把PCB插入到某合适的队列当中
3、在进程创建、终止的过程中,还应考虑分配/回收资源的问题
下一篇:unit1-问候以及介绍