Linux内核学习笔记(5)-- 进程调度概述

  进程调度程序是多任务操作系统的基础,它是确保进程能有效工作的一个内核子系统,负责决定哪个进程投入运行、何时运行以及运行多长时间。只有通过进程调度程序的合理调度,系统资源才能够最大限度地发挥作用,多进程才会有并发执行的效果。在一组处于可运行状态的进程中选择一个来执行,是调度程序所需完成的基本工作。

  在前期的 Linux 版本中,Linux 的调度程序都相当简陋,设计近乎原始,虽然容易理解,但是在可运行进程多或者多处理器的环境下都难以胜任。在 Linux2.5 系列内核中,开始采用一种叫做 O(1) 调度程序的新调度程序,解决了先前版本 Linux 调度程序的许多不足。在 Linux2.6 版本内核开发过程中,又引入了新的进程调度算法,其中最为出名的是 RSDL 算法,该算法引入了公平调度的概念,随后演化为 CFS:完全公平调度算法

  Linux 中,与进程调度相关的信息存放在进程描述符 task_struct 中:

struct task_struct {
        ....
    int prio, static_prio, normal_prio;      // 进程优先级
    unsigned int rt_priority;

    const struct sched_class *sched_class;    // 调度器类
    struct sched_entity se;             // 调度实体
    struct sched_rt_entity rt;                   // 实时进程的调度实体


    unsigned int policy;               // 调度策略
    cpumask_t cpus_allowed;                     // 用于控制进程可以在哪个处理器上运行  
        ....      
}

 

 

一、多任务操作系统

   多任务操作系统就是能够同时并发地交互执行多个进程的操作系统。可以分为两类:非抢占式多任务系统和抢占式多任务系统。

1.1、非抢占式多任务系统

   在非抢占式多任务系统下,除非进程自己主动停止运行,否则它会一直执行下去。进程主动挂起自己的操作称为让步。理想情况下,进程通常会做出让步,以便让每个可运行进程享有足够的处理器时间。这种机制有很多缺点:调度程序无法对每个进程该执行多长时间做出统一规定,所以进程独占的处理器时间可能会很长;更糟的是,如果一个进程一直不作出让步,会使系统崩溃。现在的大多数系统都采用抢占式多任务模式,除了 MAC 0S 9(及其前身)以及 Windows 3.1(及其前身)。

1.2、抢占式多任务系统

   抢占式多任务系统下,由调度程序来决定什么时候停止一个程序的运行,以便其他进程能够得到执行机会,这个强制的挂起动作称为抢占。被抢占的进程仍然是处于 TASK_RUNNING 状态的,只是暂时没有被CPU运行。进程的抢占发生在进程处于用户态执行阶段,在内核态执行时是不能被抢占的。 Linux 是抢占式多任务系统。

 

二、调度策略

  调度策略决定调度程序在何时让什么进程运行,调度策略要根据多方因素拟定,以便让系统资源得到最大限度的利用。

2.1、I/O 消耗型(交互式进程)和处理器消耗型进程(批处理进程)

  进程大致可以被分为 I/O 消耗型和处理器消耗型(还可以既是 I/O 消耗型也是处理器消耗型):

  I/O消耗型进程的大部分时间用来提交 I/O 请求或是等待 I/O 请求。这样的进程经常处于可运行状态,但通常运行时间都很短,因为它在等待更多的 I/O 请求时最后总会阻塞(这里的 I/O 是指任何类型的可阻塞资源,如键盘输入、网络I/O)。

  处理器消耗型进程则是把大部分时间用在执行代码上,除非被抢占,否则它们通常都一直不停地运行,因为它们没有太多的 I/O 需求。对于这类进程,调度策略往往是降低它们的调度频率,而延长其运行时间。一些需要进行大量计算工作的程序(如 MATLAB)就是处理器消耗型进程。

  调度策略通常要在两个矛盾的目标中间寻找平衡:进程响应迅速最大系统利用率。为此,调度程序通常采用一套非常复杂的算法来决定最值得运行的进程投入运行,但它往往并不保证低优先级进程会被公平对待。Linux 为了保证交互式应用和桌面系统的性能,对进程的响应做了优化,更倾向于优先调用 I/O 消耗型。

2.2、进程优先级

  调度算法中最基本的一类就是基于优先级的调度。通常的做法是(Linux并未完全采用),优先级高的进程先运行,低的后运行,相同优先级的进程按轮转方式进行调度。调度程序总是选择时间片未用尽而优先级最高的进程先运行。用户和系统都可以通过设置进程的优先级来影响系统的调度。

  Linux 采用了两种不同的优先级范围:

1)nice值

  nice值是所有 UNIX 系统中的标准化概念,但是不同的 UNIX 系统由于调度算法不同,nice 值的运用方式也有所差异。 nice 值的范围是从 -20 到 +19 ,默认值为 0;越大的 nice 值意味着更低的优先级,相比高 nice 值(低优先级)的进程,低 nice 值(高优先级)的进程可以获得更多的处理器时间。

  在 Linux 系统中,nice 值代表时间片的比例。使用以下指令可以查看进程的 nice 值:

ps -eo uid,pid,nice

  其中,nice 选项就是查看进程的 nice 值,在输出结果中,NI 那一列即为进程的 nice 值。

2)实时优先级

  实时优先级的值是可以配置的,默认情况下它的变化范围是从 0 到 99(包括0 和 99)。与 nice 值相反,实时优先级值越高意味着进程优先级越高,任何实时进程的优先级都高于普通的进程(即实时优先级和 nice 优先级处于两个互不相交的范畴)。可以使用如下指令查看进程的实时优先级:

ps -eo uid,pid,rtprio

  其中,rtprio 选项就是进程的实时优先级,在输出结果中, RTPRIO那一列就是。如果有进程对应列显示为“-”,则说明该进程不是实时进程。

 

2.3、时间片 与 处理器使用比

  在 UNIX 系统中,进程在被抢占之前能够运行的时间是预先设置好的,被称为时间片。时间片是一个数值,它表明进程在被抢占前所能持续运行的时间。调度策略必须规定一个默认的时间片,既不能太长,也不能太短:时间片太长,则系统交互性较差;时间片太短,则会增大进程切换带来的处理器耗时。一般而言,系统的默认时间片很短,比如10ms。

  但是,Linux 的 CFS 调度器并没有直接分配时间片到进程,Linux 使用的是处理器使用比的方式,将处理器使用比划分给进程,这样一来,进程所获得的处理器时间其实是和系统负载密切相关的。

  处理器使用比受 nice 值影响,具有较高 nice 值的进程将获得较低的处理器使用比;具有较低 nice 值的进程将获得较高的处理器使用比。而 CFS 调度器,则会根据进程的处理器使用比来决定抢占的时机:如果消耗的使用比比当前进程小,则新进程立刻投入运行,抢占当前进程;否则,将推迟其运行。通过摈弃时间片而是分配给进程一个处理器使用比的方式,CFS 确保了进程调度中能有恒定的公平性,而将切换频率置于不断的变动中。

 

三、Linux 调度算法

3.1、调度器类

  Linux 以模块的方式提供调度器,这样做的目的是允许不同类型的进程可以有针对性地选择调度算法。这种模块化结构被称为调度器类,它允许多种不同的可动态添加的调度算法并存,调度属于自己范畴的进程。每个调度器都有一个优先级,基础的调度器代码定义在 kernel/sched.c 文件中,它会按照优先级顺序遍历调度类,拥有一个可执行进程的最高优先级的调度器类胜出,去选择下面要执行的那一个程序。

  在 linux 2.6.34 版本内核里,有三种调度器类:idle_sched_class、rt_sched_class 和 fail_sched_class,在最新的 linux 4.6版本里,已经增加到了五种,另外两种是 stop_sched_class 和 dl_sched_class:

调度器类 描述   对应调度策略
stop_sched_class                      优先级最高的线程,会中断所有其他线程,且不会被其他任务打断 无,不需要调度普通进程          
dl_sched_class 采用 EDF 最早截止时间优先算法调度实时进程 SCHED_DEADLINE     
rt_sched_class 采用提供 Roound_Robin 算法或者 FIFO 算法调度实时进程,具体调度策略由进程的 task_struct -> policy 指定

SCHED_FIFO,

SCHED_RR

fair_sched_class 采用 CFS 算法调度普通的非实时进程

SCHED_NORMAL,

SCHED_BATCH

idle_sched_class 每个 CPU 的第一个pid=0线程:swapper,是一个静态线程;一般运行在开机过程和 CPU 异常的时候做 dump SCHED_IDLE

  可以看到,五种调度器类共有六种调度策略(即调度算法),用于对不同类型的进程进行调度,或者支持某些特殊的功能。每个调度器都有一个优先级,这五个调度器的优先级顺序为:

            stop_sched_class > dl_sched_class > rt_sched_class > fair_sched_class > idle_sched_class 

  另外,关于所对应的调度策略,在内核版本 2.6.34 中有如下定义:

/*
 * Scheduling policies
 */
#define SCHED_NORMAL       0
#define SCHED_FIFO         1
#define SCHED_RR           2
#define SCHED_BATCH        3
/* SCHED_ISO: reserved but not implemented yet */
#define SCHED_IDLE         5
/* Can be ORed in to make sure the process is reverted back to SCHED_NORMAL on fork */
#define SCHED_RESET_ON_FORK     0x40000000      

 

  SCHED_DEADLINE 在最新版本中有定义,暂不做叙述。上面的五种调度策略中, SCHED_NORMAL 和 SCHED_BATCH 是针对普通进程(非实时进程)的调度策略,都是通过 CFS 调度器来实现的。更进一步划分的话,SCHED_NORMAL 是针对交互式进程的调度策略;SCHED_BATCH 是针对批处理进程的调度策略。SCHED_FIFO 和 SCHED_RR 是针对实时进程的调度策略,稍后会再做细说。 SCHED_IDLE 的优先级最低,在系统空闲时才运行这类进程。

  鉴于笔者所看的内核版本以及能力问题,暂时就先简单讨论一下实时调度器和完全公平调度器这两种。实际上,大部分进程也都是属于实时调度器和完全公平调度器的。

 

3.2、完全公平调度

  完全公平调度(CFS)是一个针对普通进程的调度类,在 Linux 中被称为 SCHED_NORMAL(也被称作 SCHED_OTHER),CFS 算法实现定义在文件 kernel/sched_fair.c 中。

  CFS 的出发点基于这样一个理念:进程调度的效果应该能让系统感觉像是具备一个理想中的完美多任务处理器。在完美多任务处理器下,每个进程将能获得 1/n 的处理器时间(n 指的是可运行进程的数量)。同时,我们可以调度给它们无限小的时间周期,所以,在任何可测量周期内,我们给予 n 个进程中每个进程同样多的运行时间。但是,这种模型是不现实的,首先,我们无法在一个处理器上真的同时运行多个进程,其次,每个进程的运行时间太小的话,将会增大进程切换带来的额外消耗,这是不高效的。

  CFS 的做法是允许每个进程运行一段时间、循环轮转、选择运行最少的进程作为下一个运行进程,而不是采用分配给每个进程时间片的做法;CFS 在所有可运行进程总数基础上计算出一个进程应该运行多久,而不是依靠 nice 值来计算时间片。每个进程都按其权重在全部可运行进程中所占比例的“时间片”来运行,为了计算准确的时间片,CFS 为完美多任务中的无限小调度周期的近似值设立了一个目标,称为“目标延迟”,目标延迟越小,则系统的交互性越好,同时也更接近完美的多任务;与之相对的,是更高的切换代价和更差的系统总吞吐能力。同时,为了避免可运行进程过多而导致每个进程所获得的处理器使用比太小带来的巨额切换消耗,CFS 引入了最小粒度的概念,用来表示每个进程所能获得的时间片的底线,默认情况下,最小粒度为 1ms。

  在 CFS 策略下,任何进程所获得的处理器时间是由它自己和其他所有可运行进程 nice 值的相对差值决定的,nice 值对时间片的作用不再是算数加权,而是几何加权。所对应的也不再是一个绝对的时间片,而是处理器的使用比。CFS 通过保证分配每个进程的处理器使用比的公平性,从而达到公平调度。当然,CFS 也不是完美的公平,而是近乎完美的多任务,但是确实在一定程度上提升了进程调度公平性。

 

3.3、实时调度

  实时调度是针对实时进程的调度类,Linux 提供了两种实时调度策略:SCHED_FIFOSCHED_RR

  SCHED_FIFO 实现了一种简单的、先入先出的算法:它不使用时间片。处于可运行状态的 SCHED_FIFO 级的进程回比任何 SCHED_NORMAL 级的进程都先得到调度。一旦一个 SCHED_FIFO 进程处于可执行状态,它就会一直执行,知道它自己受阻塞或显式的释放处理器为止。它不基于时间片,可以一直执行下去。只有更高优先级的 SCHED_FIFO 或者 SCHED_RR 任务才能抢占 SCHED_FIFO 任务。如果有两个或更多同优先级的 SCHED_FIFO 级进程,它们会轮流执行,但是依然只有在它们愿意让出处理器时才会退出。只要有 SCHED_FIFO 级进程在执行,其他级别较低的进程就只有等待它变为不可运行状态后才有机会执行。

  SCHED_RR 与 SCHED_FIFO 大体相同,只是 SCHED_RR 级的进程在耗尽事先分配给它的时间后就不在继续执行了。也就是说,SCHED_RR 是带有时间片的 SCHED_FIFO —— 这是一种实时轮流调度算法。当 SCHED_RR 任务耗尽它的时间片时,处于同一优先级的其他实时进程被轮流调度。时间片只用来重新调度同一优先级的进程。对于 SCHED_FIFO 进程,高优先级总是立即抢占低优先级,但是低优先级进程决不能抢占 SCHED_RR 任务,即使它的时间片耗尽。

 

 

 

内核版本:2.6.34

参考:https://blog.csdn.net/gatieme/article/details/51702662

参考书籍:Linux内核设计与实现(第3版)

posted @ 2018-09-06 21:26  tongye  阅读(1280)  评论(0编辑  收藏  举报