前边有一篇文章中我有提及到过关于操作系统内部的上下文切换部分,以及一些进程和线程的设计模型。今天这篇文章将会从CPU如何分配资源给到不同的进程使用的角度出发。
为什么会有CPU调度
当我们的进程或者线程发生切换的时候需要将当前的上下文信息保存到pcb或者tcb中,同时读取下一个进程/线程的上下文。
而此时cpu需要从就绪队列中挑选一个合适的进程/线程作为cpu将要运行的下一个单位。那么如何合理地选择下一个执行目标则成为了一个衡量cpu调度是否高效的关键。
我们清楚一个进程的状态是存在开启,就绪,执行中,等待,销毁5种类型的,那么当一个新的进程要发生切换交替的时候,原先的老线程如果处于等待或者销毁状态,那么此时就可以执行下一个进程的任务。
所谓的调度原则我的理解是:操作系统按照一定的依据点去选择该执行哪些进程。
关于调度的抢占
非抢占式调度策略:任何一个进程在运行过程中都不允许终止。
抢占式调度策略:os内核决定适当中断哪些进程。
正在执行的进程被os内核中断的时候,用户态通常是无感知的。另外有些场景下可能是内核态中的多个进程互相调用,此时发生的中断和用户态毫无关联。
用户态和内核态的抢占设计
早期的windows操作系统不支持用户态抢占策略,后期随着算法设计的改善逐渐提供了内核态中做抢占的机制。
如何看待调度算法的好坏
评价指标
- CPU利用率:CPU处于繁忙状态的时间占用百分比
- 吞吐量:进程的效率较高,调度算法对于吞吐量方面设计有好处
- 周转时间:从一个进程初始化到结束,包括等待时间所花费的时间
- 等待时间:进程在就绪队列中的总时间。等待时间当然是期待越短越好,这样可以证明调度算法的效率较高。(从就绪态到运行态时间越短越好,这点和从阻塞态到运行态到时间是两码事)
- 响应时间:从某个请求提交到实际响应的花费时间。
通常我们都是希望操作系统能够提供更佳的性能,对于更佳的理解需要结合实际的应用场景来说明,例如说:
吞吐量大:例如一些文件的拷贝,数据中心的服务器等
响应速度快:桌面应用程序,注重用户的交互感受。
但是在实际的算法设计中,吞吐量和响应时间两者之间一般都是互斥的,只能说在设计算法的时候做折中,每一款调度算法都需要平衡好二者之间的关系。
经典的调度算法设计
先来先服务算法
从名字上就很容易理解这一款调度算法的设计理念,这里我直接通过一张图来进行解释:
假设某一时刻有三个进程依次请求cpu分配时间片,它们请求的顺序为P1,P2,P3。对应消耗的时间片大小为12,4,4。如果采用先来先服务的方式分配资源,整体的分配结果如下图所示:
从图中可以看出,采用FIFO的这种设计思路,对于一些低耗时的任务可能需要较久的等待,整体资源分配的平均响应时长变大。
Average response time = (12 + 16 + 20) / 3 = 16 复制代码
优点:
- 简单好实现
缺点:
- 平均等待时间波动较大
- 耗费时间少的任务可能需要等待较长时间段
短作业优先算法
这类算法从名字上也很容易听懂和理解,OS内部会有一个模块提前对请求的任务做一定的耗时预测,然后根据预测的时间排序,将最短耗时的任务优先执行。(关于如何预测后边会介绍)
整体的流程画图如下所示:
关于耗时部分可以发现,采用了短作业优先算法之后,整体的任务平均耗时要比之前的FIFO设计思路低了些许。
Average response time = (4 + 8 + 20) / 3 = 10.7 复制代码
短作业优先算法是否也存在抢占的设计
假设目前正在执行的任务是P2(4个时间片),当P2执行了1个时间片单位之后,此时来了一个新的任务P4(2个时间片),P4所消耗的时间片要比P2消耗的时间片更短,那么P4是否需要继续执行呢?
此时进程可以选择直接抢占原先running状态的进程继续执行,也可以选择留在就绪队列的头部等待P2完全执行完毕再去运行。
ps:通常我们呢称呼新的更短的进程直接抢断策略为srt调度策略,而新的更短的进程不打断当前进程,转而呆在就绪队列首部的这种方式为spn调度策略。
最优平均等待时间算法
这种调度算法主要是从任务的发起方去思考,考虑发起方到实际任务执行结束之后所消耗的时长。在设计上需要提前根据预测的耗时时间去计算按照不同任务的排列组合,怎样的排序对于最终的执行时间能达到最优效果。
如何预估某个进程的每次发起任务的执行时间
os底层会有一个统计的任务,专门统计在某个时间段内(例如最近1分钟/10分钟/20分钟)某个进程的实际执行耗时,从而对下一次执行耗时做预判计算。
ps:这一点思想让我想起了以前做过的一项数据预测需求,其中用到了移动平均算法,专门用于预测一些用户对某项产品的兴趣。
但是这类算法也存在一定的弊端:
多个连续的短任务可能会将cpu的时间片耗尽,从而导致长任务处于一种饥饿堵塞的状态。
最高响应比算法
在介绍这类算法之前,我们需要先大概知道下什么是响应比:
R = (w+s)/s 复制代码
上边这条公式中,w是指等待时间,s是指执行时间。
这类算法的设计理念是关注于进程的等待时间来进行排序,同样也是需要预测进程的下一个任务所执行的时长。该算法能够较好地避免出现因为有连续的短任务出现从而导致长任务一直延后的这种情况发生。
轮训调度算法
轮训调度算法是一种将时间片存放在一个队列当中,接着各个进程轮流去使用时间片执行任务。如下图所示:
这样设计的好处在于公平性较佳,能够保证每个进程都会有机会去执行。但是也意味着进程的上下文切换频率会变高,从而影响性能问题。
另外在某些极限情况下这类算法对于时间片大小的把控可能会对实际结果有影响。
时间片过大
导致某些任务的等待时间过长,容易整体退化成为先来先服务模式。
时间片过小
需要做大量的进程上下文切换
多级反馈队列算法
这类算法的设计思路则综合了上边的多种算法设计优缺点,它将进程任务切分成了多个队列,不同的队列采用不同的调度算法。
例如一些优先级别较高的进程可以使用短作业优先算法,低级别一些的进程则使用先来先服务的策略算法。
有的任务随着执行时间越久,其任务的等级会上升(例如io密集型)或者级别下降,例如一些cpu计算型任务,计算多的进程对于时间片消耗较高,一般会被放在低级别的队列中。
上边所介绍的几类算法主要都是简单的设计思路,实际到今天,cpu内部的调度算法已经变得丰富多彩,但是文中提到的这几项调度算法依旧是其核心设计的主流思想。