处理器作为计算机系统里的一种资源,也是需要管理,从而调度分配给需要的程序以达到最高效率,所以调度器应运而生。

1. 调度器overview

调度器本身也是一个程序,目的是为了给执行用户的程序提供资源,包含了一个决定一组程序中谁会赢得CPU时钟周期的算法。




Android CPU绑核 调频 安卓cpu调度器哪个好_优先级


在桌面机,嵌入式设备或大型机等不同的环境中,产生了不同的调度器。我们一般针对不同层面,分为下面几种调度器:

  1. 高级调度器(long-term scheduler): 通常用于面向批处理的多程序环境中,通过均衡内存中的任务以欧化系统资源,包括CPU,内存和硬盘存储等。
  2. 中级调度器(medium-term scheduler):跟踪CPU上执行的进程的动态内存用量来决定是否要增加或减少“多通道程度“(内存中竞争CPU的进程个数),以防止”颠簸“。颠簸即当前的进程集合的内存需求超过了系统容量,导致进程在各自的执行中变得缓慢。
  3. 低级调度器(short-term scheduler):负责在当前驻留的内存里的进程中选择一个来运行。这篇内容设计的算法主要关注的都是低级调度器。

2. 调度的步骤

在低级调度器中,一般划分为:非抢占式抢占式。非抢占式的调度器中,一个进程要不然就一直执行到底,要不然就自己主动资源放弃处理器,以处理I/O请求,调度器只是安排先后顺序;而抢占式调度器里,调度器从当前进程手中把处理器抢走,交给另一个进程,有这么一个主动的动作。不管是哪一种,调度器的执行步骤一般如下:

  1. 获得处理器的控制权
  2. 把当前正在运行的进程状态(PCB,process control block)保存下来
  3. 选择一个新的进程来执行
  4. 把新选择出来的进程分发给处理器运行,这一步会把第二步中的状态再加载到CPU寄存器中。

可以看出,在调度器调度过程中,进程状态很重要,它包括了很多信息:程序执行到哪儿了(PC指针)以便回来继续执行,程序在内存中的足迹等等。所有的信息会用一个数据结构,进程控制块PCB描述:


enum state_type {new, ready, running, waiting, halted};

typedef struct control_block_type {
    enum state_type state;          /* 当前状态*/
    address PC;                     /* 进程从哪里继续*/
    int reg_file[NUMBERS];          /* 通用寄存器的内容*/
    struct control_block *next_pcb; /* 链表指针 */
    int priority;                   /* 优先级等外来属性 */
    address address_space;          /* 内存位置 */
    ...;

} control_block;


PCB包含了所有必须的描述进程的相关信息,是操作系统中很重要的数据结构,而PCB的维护一般是由一个队列来完成的,称作就绪队列。就绪队列的数据结构的正确表示关系到调度器的性能。


Android CPU绑核 调频 安卓cpu调度器哪个好_数组_02


3. 评价一个调度器的性能指标

调度器的最终目标是运行用户程序,让处理器被合理的利用。那么,评价一个调度器算法的指标是什么?

一般定量的指标,通常我们第一个想到的就是CPU利用率。CPU利用率在一定程度上可以说明问题,表示CPU的繁忙程度,但不够细致,因为我们不清楚CPU到底在忙什么。

从以系统为中心和以用户为中心,大约有以下几个可以利用的指标:

以系统为中心:

  1. CPU利用率:CPU处理器运行指令的繁忙时间的占比
  2. 吞吐量:表示单位时间内所完成的作业个数
  3. 平均周转时间:测量任务进入和离开系统平均所花的时间(t1+t2+...+tn)/n
  4. 平均等待时间:表示系统任务的平均等待时间(w1+w2_...wn)/n

以用户为中心:

  1. 响应时间:表示特定的任务i的周转时间ti
  2. 响应时间方差:表示给定进程的实际响应时间与其期望值的统计差异

除了上面介绍的定量指标,值得一提的调度器算法的定性指标:

  1. 饥饿:在任何进程作业的组合中,调度策略都应该确保所有的任务一直都有进展,如果由于某种原因,一个进程任务并没有任何进展,我们把这种情况称之为饥饿。这种情况的定量表现是,某个特定任务的响应时间没有上限。
  2. 护送效应(convey effect):在任何进程作业的组合中,调度策略应该预防长时间运行的某个任务完全占据CPU的使用。如果出于某种原因,任务的调度符合固定的规律(类似于军队的护卫),这种情况称之为护送效应。这种现象的定量表现为,任务的响应时间的方差很大。

4. 调度算法

第二部分有提到调度算法中分为非抢占式与抢占式。这里介绍几种典型的非抢占式和抢占式的算法。

4.1 非抢占式的调度算法

1)先到先服务算法(FCFS, First-Come First-Served)

这个算法会用到的属性是进程的到达时间,也就是启动运行一个进程的时间。先启动的进程会优先被调度器选中,如下图所示,P1是第一个到达的,然后再是P2, P3,所以根据先到先服务原则,调度器总是会优先选择P1,然后P2,P3。


Android CPU绑核 调频 安卓cpu调度器哪个好_最高响应比调度算法_03


优点:这个算法有一个很好的性质,就是任何进程都不会饥饿,也就是说算法没有回导致任务进程拒绝服务的内在偏向

缺点:但由于上面这个性质,响应时间的方差会很大。举个例子,一个长时间任务到达后,后面跟着一个短时间的任务,那么短任务被长作业挡在后面,它的响应时间就会很糟糕,由于护送效应导致低下的CPU利用率。所以这个算法并没有对短任何给予任何优先考虑。

2)最短作业优先(SJF, Shortest Job First)

既然先到先服务对短任务不是很友好,那么这个算法就是为了让短作业获得更好的响应时间。

优点:调度器会优先选择时间较短的任务,让短任务获得更好的响应时间;

缺点:有可能会让一个长时任务饥饿。

解决这个缺点有一个方案,当一个作业的年龄到达一个阈值,调度器忽略SJF, 选择FCFS算法。

3) 优先级算法

出于调度的目的,多数OS会给每个进程赋予一个属性——优先级。比如,在UNIX系统中,每个用户级进程开始时都有一个固定的默认优先级。Ready Queue中包含多个子队列,每个队列都对应着一个优先级,每个子队列内部采用FCFS算法,如下图所示:


Android CPU绑核 调频 安卓cpu调度器哪个好_Android CPU绑核 调频_04


优点:灵活,可以提供差异化服务

缺点:会产生饥饿,可以根据进程的等待时间来提高优先级

4.2 抢占式调度算法

抢占式与非抢占式的区别在于:在一个新进程或刚完成I/O的进程进入到ready queue中时,会重新评估一些属性(比如剩余执行时间),以决定要不要抢占当前正在运行的进程。原则上说,上面讨论到的任何一个非抢占式算法都能改造成抢占式的,比如FCFS算法,每次重新进入就绪队列时,调度器可以决定抢占当前正在执行的进程(如果新任务的到达时间比较早),类似的,SJF和优先级也一样。

下面介绍两种抢占式算法:

1)最短剩余时间优先(SRTF, Shortest Remaining Time First)

调度器会估计每个进程的运行时间,当一个进程回到就绪队列,调度器计算这个任务的剩余处理时间,根据计算结果,放入ready queue中合适的位置。如果该进程的剩余时间比当前的进程要少,那么调度器就会抢占当前运行的任务,让这个新任务先执行。跟FCFS算法相比,最短剩余时间的平均等待时间一般比较低。

2)RR(Round Robin)调度器

分时环境特别适合使用RR调度器,即每个进程都应该得到处理器时间的一部分。因此,非抢占式的调度器就不适合这种环境。假设有n个就绪的进程,调度器把CPU资源分成一个一个时间片,然后分配给各个进程,如下图所示。就绪队列里每个进程都会得到处理器的时间片q。当时间片用完了,当前调度的进程会被放入就绪队列的尾部,形成一个ring。但考虑到在不通进程切换会有开销,所以选择时间片q的适合要考虑上下文切换。


Android CPU绑核 调频 安卓cpu调度器哪个好_优先级_05


5. Linux调度器

上面讨论了一些基础的算法,那么Linux中是怎么调度任务的?

这里我们讨论Linux 2.6.x版本中采用的调度框架。Linux在调度上试图满足不同的环境:桌面计算和服务器。桌面计算要求高实时性以满足交互性需求,如键盘鼠标输入,会有很多上下文切换,所以响应时间很重要。而服务器不一样,服务器承载密集的负载,因此上下文切换越少,能完成的任务越多。因此Linux在调度算法里试图满足一些目标 :

  1. 高效率:调度器本身的开销要小,这是服务器环境的一个重要要求
  2. 支持交互性:支持桌面级环境
  3. 避免饥饿:确保计算的workload负载不会影响桌面交互性workload
  4. 实时调度:确保计算性的workload负载不会因为交互而受到影响

Linux调度器支持3种任务:

  1. 实时先到先服务
  2. 实时RR
  3. 分时

调度器支持140个优先级,0代表最高优先级。其中0-99是给实时任务的,用于处理交互式的workload,100-139是给分时任务,用来处理计算性workload。

调度器的主要数据结构如下图,140个调度优先级都有一个双向链表:


Android CPU绑核 调频 安卓cpu调度器哪个好_响应时间_06


步骤:

  1. 从活动数组中选择最高优先级的第一个来运行
  2. 如果该任务阻塞了,就先放一边,运行下一个
  3. 如果当前调度的任务的时间片用完了,把它放到期满数组中去
  4. 如果一个阻塞的任务回来了,把它放在活动数组中相应优先级的链表中,调整它的剩余时间片时间
  5. 如果活动数组中不再有任务,那么交换活动数组和期满数组的指针,继续运行调度算法。

值得注意的是,1)这个设计中优先数组保证了调度器能在常数时间内做出调度决定,不依赖进程个数,因此也称作O(1)调度器。2)调度器通过实时的先到先服务和实时RR调度来给交互式任务特殊的处理,以满足实时性的需求。

实际上,调度器不知道哪些任务是交互的,它采用一种启发式的方法,根据执行历史来确定任务的性质。调度器会监控每个任务对CPU的使用模式,如果一个任务经常进行阻塞式I/O请求,那么它式交互式(I/O密集),如果一个任务不怎么进行I/O,那么它式一个CPU密集型的workload。调度器会通过动态地提高优先级来对交互式进行奖励,同时通过降低优先级来惩罚CPU密集型的任务。

最后,为了满足关于避免饥饿的目标,调度器有一个饥饿阈值。如果一个任务没有得到CPU使用机会的时间超出了哪个阈值,那么饥饿的任务优先级会被提高