一、什么是进程优先级
进程优先级就是,获得某种资源的先后顺序。其本质就是目标资源比较少,比如CPU,设备等。至于如何实现的,我们可以大胆猜测一下是在task_struct中再添加一个属性。用int变量来表示优先级高低,数字越小代表级别越高。
二、如何查看某个进程的优先级
这里我们可以使用命令:
ps -la
其中,PRI和NI项与我们的优先级有关,由他们共同维护,PRI(即priority)优先的意思,但后面这个ni(nice)值又有什么作用呢?实际上,最终优先级=PRI+NI值,nice值是优先级的修正数据。注意:每次修改后PRI会重置为80。后续改变进程的优先级可以通过修改nice即可。具体修改的方法为:输入top指令,然后输入你要修改的进程的PID,然后输入你想改后的NI值,(NI值范围是[-20,19])然后最终的优先级就=80+你输入的nice值。nice之所以设置范围是不想弄太多的级别,方便CPU调度,我们后续再说。
三、进程切换
1. 知识储备
基于我们对时间片和调度轮转的知识,基本了解了进程运行的大致流程,但是一个进程再时间片到了的时候,有时候不一定就跑完了,但我们还想继续运行下去,就需要进程的切换,下面我们讨论一下是如何切换的。
一个进程,如果未运行完但时间片到了,不能直接退出就结束了,我们需要告诉操作系统我们要退出的动作,让其记录我们的退出信息,这样当我们的进程经调度轮转又运行时就可以找到了,而且恢复运行时也要通知操作系统。
2.切换过程理解
我们的进程在运行时,会产生很多临时数据,这些瞬时数据都会在CPU内寄存器中保存。在上面我们说过,CPU中会有很多临时数据,包括代码进行到第几行等,但是,CPU是怎么知道的?
是因为CPU中存在一个寄存器eip,这里保存着当前执行指令的下一条指令的地址。当CPU处理完某一行的指令,就会向eip获取下一条指令。此外,还有一个寄存器ir,保存着当前执行的指令地址。那么代码的执行流程就是eip->ir->CPU,然后eip更新。CPU保存的这些进程的数据我们称上下文数据。所以,进程的切换的关键就是上下文数据的切换。接下来我们说明一下切换的流程。
当一个进程的时间片到了需要切换时,如果我们不对数据加以操作(即保护上一个进程的上下文数据),那么下个进程运行时就会覆盖掉之前eip、ir等保存的上下文数据,当轮转一圈后又重新运行此进程时,CPU就不知道该从哪里运行了,因为他想从ir中获取当前执行的代码发现已经不是上次运行的结束段了。由此看出,要想顺利进行进程切换,上下文数据的保护是非常重要的。因此,正确的操作是,当要下一个进程时,先把前一个进程的上下文数据移动到“某个地方”保存起来,然后eip、ir再记录下一个进程的信息,然后当进程需要重新运行时,直接把保存的数据拿过来再让CPU获取信息运行即可。这个“某个地方”即指内存中的”PCB“。
四、CPU对进程的调度
1.Linux下真实的调度算法
在CPU的运行队列中,存在两个结构体指针active和expired(活跃与过期),还存在两个数组(两个结构体指针分别包含包含一个数组和其他变量),里面的每一个元素都是task_struct*类型,数组中有140个元素。其中前100个都是给实时进程使用,我们不用管,真正需要我们排列的是后40个,这也对应着进程的优先级一共有40个[60,99]。运算规则大致为pri-起始pri(60)+100对应下标。然后相同的数字我们挂在同一个下标形成队列体现优先级,那么这个数组不就是我们数据结构中的哈希桶吗。相同优先级进程链接在一起。active指向第一个数组,expired指向第二个数组,而CPU调度只会在active所在队列进行。但为什么要有两个数组?
想象一下,当我们新建一个进程时,一般来说优先级都是比较高的,所以就会排在队列前,这就导致一个问题,如果我新建太多进程,导致后边优先级低的进程很久才可以被调度就会造成不均衡的问题,因此,我们要保留原来的调度次序,即把新建的进程放在expired的队列中,时间片到了的未执行完毕程序也会放入expired队列,这样就会使active中的进程越来越少,当调度完毕后,只需要swap两个队列的数据既可重新进行调度轮转。因此,每一个队列还有一个额外的变量,用于记录当前队列的进程数,当少于一定数量时就交换。