Linux是如何进行任务调度的?

前言

文章会先介绍知名的调度算法,然后再来介绍操作系统中的具体实现与改进。

调度算法

性能指标

算法的优劣往往是相对的,而且在不同的指标下,也可能优劣反转。在进程调度中,有一些不同的指标是有意义的。下面介绍几种指标

  • 周转时间:任务完成时间减去任务到达系统的时间

    T(周转时间) = T(完成时间) - T(到达时间)

  • 响应时间:任务到达系统到首次运行的时间

    T(响应时间) = T(首次运行) - T(到达时间)

先到先服务

First In First Out——FIFO,也可以称为First Come First Served(FCFS)

先到先服务,它很简单,而且易于实现。在生活中,这种方式似乎随处可见,比如银行。客户需要在挂号机上取个号,便可以在休息区等待某个窗口的呼叫了。

来看一个简单的例子,3个任务A,B和C在大致相同的时间到达系统(系统时间为0),为了符合FIFO的特点,假设A比B早一点点,B比C早一点点,每个工作运行10ms。

在这里插入图片描述

那完成3个任务的平均周转时间为:(10 + 20 + 30)/ 3 = 20ms

再来看一个例子,再次假设3个任务A,B和C,但这次A运行100ms。

在这里插入图片描述

平均周转时间:(100 + 110 + 120) / 3 = 110ms

如果换个顺序,假设B,C稍微先到一点点,A稍微晚一点

在这里插入图片描述

平均周转时间:(10 + 20 + 120)/ 3 = 50ms

到这里,FIFO的问题就很明显了,假设3个任务同时到达系统的情况下,任务的执行时间相同,那么执行的顺序不会改变平均周转时间。但如果任务的执行时间不同,那么执行顺序的不同可能会导致平均周转时间有较大差异。

这个问题通常被称为护航效应,一些耗时较少的潜在资源消费者被排在重量级的资源消费者之后

那么,针对这个问题,可以采用SJF算法来解决

最短任务优先

Shortest Job First——SJF。在FIFO中,产生护航效应的原因是耗时较少的任务在耗时久的任务后到来,需要等待耗时久的任务被执行完才能被调度。既然这样,CPU在执行任务时,优先运行最短的任务,然后是次短的任务。

补充:抢占式调度程序

在过去的批处理计算中,开发了一些非抢占式(non-preemptive)调度程序,这样的系统会将每项工作做完,再考虑是否运行新的工作。几乎所有现代化的调度程序都是抢占式的。临时停止一个运行进程,并恢复(或启动)另一个进程也被叫做上下文切换

按照SJF算法,如果A,B和C同时到达,那么先执行B和C,再执行A

SJF是一种非抢占式的调度程序。

其实,这种算法也有点问题。A,B和C同时到达时会先执行耗时短的任务,但不同时到达时,看下图

在这里插入图片描述

护航问题还是存在。

最短完成时间优先

Shortest Time-to-Completion First——STCF,或抢占式最短完成时间优先(Preemptive Shortest Job First-PSJF)。

这个调度算法是向SJF中添加抢占。每当新任务进入系统时,它就会确定剩余任务和新任务中,谁的剩余时间最少,然后调度该工作。

这里依然沿用A,B,C的例子

在这里插入图片描述

平均周转时间:(120 + 10 + 20)/ 3 = 50ms

如果任务不是同时到达的,那么这种算法的平均周转时间是最少的。

如果任务是同时到达的,那么SJF算法的周转时间是最少的

上面几种算法都是以减少周转时间为基点进行考量的。如果以响应时间作为基点,来计算下他们的响应时间

依旧是A,B,C的例子,假设它们在大致相同的时间到达系统,并且每个任务预计需要10ms完成

在这里插入图片描述

平均响应时间:(0 + 10 + 20)/ 3 = 10ms

也许从这看不出问题,但与下面一个算法对比,你会发现这是比较可怕的

时间片轮转

Round-Robin——RR。RR在一个时间片内运行一个工作,时间片用完就切换到运行队列的下一个任务,而不是运行一个任务直到结束。它反复执行,直到所有任务完成。

在这里插入图片描述

平均响应时间 = (0 + 1 + 2)/ 3 = 1ms

在这里插入图片描述

所以,以响应时间作为指标,RR算法是非常优秀的

时间片长度对于RR是至关重要的,越短,RR在响应时间上表现越好。但是时间片太短也是有问题的,这会造成频繁的上下文切换,影响性能。

上下文切换不仅要保存和恢复少量寄存器的操作系统操作。程序运行时,它们在CPU高速缓存、TLB、分支预测器和其他片上硬件建立了大量的状态,切换会导致这些状态被刷新,同时与当前运行的作业相关的状态被引入,这可能导致显著的性能成本。

切换需要依靠硬件的中断,因此时间片必须是中断周期的倍数

最高优先级调度

给任务设置一个优先级,优先调度优先级高的任务。在生活中这种方法很常见,比如银行中,客户存的钱越多,自然级别越高,那么在银行办理业务时,自然需要被优先处理。在计算机中,需要实时交互的程序,就应该赋予较高的优先级。如果优先级低了,导致任务不能够被及时地处理,产生较高的延迟,这是人们所不能接收的。

到这里,已经介绍了好几种调度算法。基于不同的指标,算法表现出不同的优劣性。想要更快地完成所有任

务,只能牺牲响应时间。想要更快地响应,只能牺牲周转时间。这种权衡在系统中很常见。不仅如此,讨论

这么多的调度算法也是有意义的,它们为一个优秀的,全面的算法贡献了自己的一部分。

多级反馈队列

Multi-level Feedback Queue,MLFQ。

1962年,Corbato首次提出多级反馈队列,应用于兼容时分共享系统(CTSS)。Corbato因在CTSS中的贡献和后来在Multics中的贡献,获得了ACM颁发的图灵奖。该调度程序经过多年的一系列优化,出现在许多现代操作系统中。

多级反馈队列比较好的权衡了周转时间和响应时间。

MLFQ中有许多独立的队列,每个队列有不同的优先级

任何时刻,一个任务只能存在于一个队列中。

队列的时间配额随着优先级的降低而增大。

依下是MLFQ的执行规则

  1. 如果A的优先级 > B的优先级,运行A

  2. 如果A的优先级 = B的优先级,轮转运行A和B

  3. 任务进入系统时,放在最高优先级(最上层队列)

  4. 一旦任务用完了其在某一层的时间配额(无论中间主动放弃了多少次CPU),就降低优先级(移入低一级队列)

  5. 经过一段时间S,就将系统中的所有工作重新加入最高优先级队列。

在这里插入图片描述

规则1和规则3很好理解,所有任务最初都处在最高优先级队列,CPU总是优先处理高优先级队列里的任务。所以需要高响应的任务(一般是短任务),它能被迅速执行。同时这也兼顾到了长任务,因为长任务也能被执行,不像SJF或STCF会导致长任务一直不被执行。

规则4是必要的。想象一下,如果没有降级操作,那么就没有了队列分级的必要了。相当于只在一个队列里不断循环判断任务的优先级,而且必须遍历一遍所有任务,才能确定哪个任务优先级高,这显然性能低下且实现复杂。

规则5,对于长任务来说,在最高优先级队列的时间片用完,它会降级进入下一队列。如果系统中不断地有新任务到来,那么它们都会进入最高优先级队列,这会导致队列优先级越低,它里面的任务越难以被执行。为了避免长任务被饿死,就需要定期将最低优先级队列里的任务放入最高优先级队列,以便长任务能够被再次执行。

LInux进程调度

讲了这么多调度算法,来看看Linux操作系统是如何实现任务的调度的

在Linux内核中,任务用一个结构体task_struct来描述。(描述其实就是在内核中用一个task_struct来记录线程的各种信息以方便调度)

Linux中,进程和线程作为任务对象都用task_struct描述。一个进程内如果没有显示的启动其他线程,那么进程只有一个线程,也被称为主线程,这个主线程用一个tack_struct来描述。如果一个进程内显示的启动了线程,那么启动的线程和主线程分别用task_struct来描述。

在这里插入图片描述

调度器

先来看task_struct中和调度相关的结构:

struct task_struct {
 ......
 const struct sched_class *sched_class;
 struct sched_entity  se;
 struct sched_rt_entity  rt;
 ......
 struct sched_dl_entity  dl;
 ......
 unsigned int   policy;
 ......
}

se,rt,dl从字段名可以看出,它们是调度类调度的实体

  • struct sched_entity se:采用CFS算法调度的普通非实时进程的调度实体。
  • struct sched_rt_entity rt:采用Roound-Robin或者FIFO算法调度的实时调度实体。
  • struct sched_dl_entity dl:采用EDF算法调度的实时调度实体。

调度类是一种抽象的概念,用于表示特定的调度策略和属性。而调度器是根据调度类的定义,实际执行任务调度的部分。

不同的调度类对应不同的调度器实现,调度器实现了具体的任务调度算法和机制。

下面来看Linux默认使用的调度器

CFS调度器

Completely Fair Scheduler,CFS。完全公平调度器。

CFS调度器主要用来调度普通任务。普通任务也会有不同的优先级,高优先级的任务权重应当高,因此优先被执行

CFS调度器用nice值来表示优先级,取值范围是 -20 ~ 19,nice值和权重是一一对应的关系,nice值和权重的转换关系:

const int sched_prio_to_weight[40] = {
 /* -20 */     88761,     71755,     56483,     46273,     36291,
 /* -15 */     29154,     23254,     18705,     14949,     11916,
 /* -10 */      9548,      7620,      6100,      4904,      3906,
 /*  -5 */      3121,      2501,      1991,      1586,      1277,
 /*   0 */      1024,       820,       655,       526,       423,
 /*   5 */       335,       272,       215,       172,       137,
 /*  10 */       110,        87,        70,        56,        45,
 /*  15 */        36,        29,        23,        18,        15,
};

在Linux系统中,根据任务的优先级以及响应要求,主要分为两种,其中优先级的数值越小,优先级越高:

  • 实时任务,对系统的响应时间要求很高,也就是要尽可能快的执行实时任务,优先级在 0~99 范围内的就算实时任务
  • 普通任务,响应时间没有很高的要求,优先级在 100~139 范围内都是普通任务级别

nice值并不表示优先级,而是用来调整任务的优先级。计算式为:

priority(new) = priority(old) + nice

并且,nice只能作用于普通任务

CFS希望实现公平的调度,所以它引入了虚拟时间的概念,虚拟时间的计算公式为:

虚拟时间 += 实际运行时间 * NICE0_TO_weight / weight

NICEO_T0_weight代表的是nice值等于0对应的权重,即1024,weight是该任务对应的权重。

那么CFS调度器是如何实现公平调度的呢?

首先,Linux中的调度器都有它们对应的队列,每次调度时,从队列中选择任务。CFS对应的队列用struct cfs_rq cfs描述,cfs_rq底层实现是红黑树CFS每次调度时,从cfs_rq中取出虚拟时间最少的任务来执行。

为什么使用红黑树呢?因为CFS每次要从队列中取出虚拟时间最少的任务来执行,线性结构的查找时间是O(n)的,而红黑树的查找时间是O(logn),因此使用红黑树保存任务效率更高。

刚加入红黑树的任务,由前面公式可得其虚拟时间为0,因为其实际运行时间是0。根据CFS的调度策略,那么它就很容易被调度。从这里可以看出,这就有点类似多级反馈队列,新任务被加入最高优先级队列,被调度的概率最高。

从虚拟时间的计算公式可以看出,优先级越高,其虚拟时间随实际运行时间的增长越慢,因此,优先级越高,其能获得更多的CPU时间。

但随着高优先级的任务被执行,它的虚拟时间会逐渐增加,最终它会超过低优先级任务的虚拟时间,那么CPU就可以调度低优先级的任务。这样循环往复,高低优先级的任务都能够被调度,因此,CFS实现了公平调度。

其它调度器

除了CFS调度器,Linux还有下面几种调度器

  • Stop调度器:优先级最高的调度类,可以抢占其他所有进程,不能被其他进程抢占
  • Deadline调度器:使用红黑树,把进程按照绝对截止期限进行排序,选择最小进程进行调度运行
  • RT调度器:为每个优先级维护一个队列,先调度高优先级队列里的任务
  • IDLE-Task调度器:每个CPU都会有一个idle线程,当没有其他进程可以调度时,调度运行idle线程

内核需要知道任务需要被哪个调度器调度,已经被如何调度,所以用policy字段来记录任务的调度策略,下面是任务的调度策略

  • SCHED_DEADLINE:使task选择Deadline调度器来调度运行
  • SCHED_RR:时间片轮转,进程用完时间片后加入优先级对应运行队列的尾部,把CPU让给同优先级的其他进程;
  • SCHED_FIFO:先进先出调度没有时间片,没有更高优先级的情况下,只能等待主动让出CPU;
  • SCHED_NORMAL:使task选择CFS调度器来调度运行;
  • SCHED_BATCH:批量处理,使task选择CFS调度器来调度运行;
  • SCHED_IDLE:使task以最低优先级选择CFS调度器来调度运行;
运行队列

前面说到,CFS使用cfs_rq来保存调度的任务。每个调度器都有它对应的任务队列,这些队列被保存在runqueue运行队列中,每个CPU核心都有一个runqueue运行队列。

每个运行队列中有三个调度队列,task 作为调度实体加入到各自的调度队列中。

struct rq {
 ......
 struct cfs_rq cfs;
 struct rt_rq rt;
 struct dl_rq dl;
 ......
}
  • struct cfs_rq cfs:CFS调度队列,其数据结构是红黑树,使用CFS调度器的对象都有一个虚拟运行时间,使用红黑树通过虚拟时间对这些对象排序。
  • struct rt_rq rt:RT调度队列,保存实时任务
  • struct dl_rq dl:DL调度队列,使用红黑树保存任务的截止期限

在这里插入图片描述

下面附上调度类,调度器和调度策略的总结表格

在这里插入图片描述

这几种调度类是有优先级的,优先级如下:Deadline > Realtime > Fair,这意味着 Linux 选择下一个任务执行的时候,会按照此优先级顺序进行选择,也就是说先从 dl_rq 里选择任务,然后从 rt_rq 里选择任务,最后从 cfs_rq 里选择任务。因此,实时任务总是会比普通任务优先被执行

参考:

  • 26
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值