探秘Linux进程调度器:操作系统的核心“指挥官”

系统 Linux
Linux 进程调度器作为操作系统的核心组件,历经多年的发展与演进,已经成为一个功能强大、高度优化的资源管理系统。

在操作系统的复杂世界里,进程就如同一个个忙碌的 “小工人”,它们都渴望获得 CPU 的 “青睐”,以执行自己的任务。而在这背后,有一位默默掌控全局的 “指挥官”,它就是 Linux 进程调度器。想象一下,当你在电脑上同时打开多个应用程序,一边浏览网页,一边播放音乐,还在运行着办公软件,这些进程看似和谐共处,实则在激烈竞争着有限的 CPU 资源。

那么,究竟是谁在有条不紊地安排它们的执行顺序,让整个系统能够高效稳定地运行呢?没错,就是 Linux 进程调度器。今天,就让我们一同走进这个神秘的领域,揭开 Linux 进程调度器的神秘面纱,看看它是如何在幕后指挥着这场进程 “交响乐” 的 。

一、进程调度简介

进程调度是操作系统最重要的内容之一,也是学习操作系统的重点和难点。关于进程调度,我们首先就会问出一些问题,什么是进程调度,为什么要进程调度,如何进行调度。下面我们用一幅图把这些问题关联起来:

图片图片

这张图把进程调度的所有问题和知识点都关联了起来,本文后面所有的内容都是对这张图的解释和扩展延伸,下面让我们来一一讲解。

⑴什么是调度

什么是调度?调度是CPU资源管理器。操作系统的作用之一就是系统资源管理器。CPU是计算机系统中最重要的资源,当然也要管理。所有进程的运行都需要CPU,对CPU该如何管理呢?对于直接共享型的事物,我们有两种管理方法:一种是时间分割管理,另一种是空间分割管理。由于CPU自身的特性,没有空间分割相似性,只有时间分割相似性,所以我们只能对CPU进行时间分割管理。对CPU进行时间分割管理的具体做法就叫做进程调度。

那么调度的是什么呢?进程调度,调度的当然是进程啦,也对也不对。我们知道进程是资源分配的单位,线程是执行的单位。早期的时候没有多线程,进程就是线程,线程就是进程,所以此时进程调度调度的是进程。但是当有了多线程之后,线程变成了执行的单位,进程不再是执行的单位,进程调度调度的就是线程了。不过由于历史原因,大家都习惯叫进程调度,所以现在这个领域的名称还是叫进程调度。后文中说到调度进程的地方都是调度的线程,由于习惯问题,我们还说调度进程不说调度线程,请大家要注意。

对线程的调度可以有两种方式:一种是直接调度线程,不考虑它们所属的进程,这种方式叫做直接调度或者一级调度;另一种是先调度进程,再在进程内部调度线程,这种方式叫做间接调度或者二级调度。POSIX规定,操作系统可以选择这两种方式中的任何一种都行。Linux选择的是一级调度,为什么会这么选择呢?主要是为了提高进程的并发性,充分利用多CPU多核的优势。如果使用二级调度的话,看似每个进程之间都公平了,但是有些进程的计算量比较大,就无法通过多开线程提高自己的性能,这样对系统整体的性能是有害的,也不利用发挥计算机多CPU的优势。一级调度看似对有些进程不公平,但是计算量小的进程少开线程,计算量大的进程多开线程,相对还是很公平的。

就像国家希望每个企业都做大做强,但是同时也会反垄断一样。Linux也推出了cgroup组调度机制,来限制某个或者某一类进程对CPU资源的过度占用。本文中不讲cgroup组调度机制,后文的讲解都是假设没有cgroup组调度。

⑵为什么要调度

我们知道了什么是调度,那么为什么要调度呢,没有调度会怎么样呢?最早的计算机是没有调度的,程序只能一个一个地运行,一个进程死亡之后才能去运行下一个进程。这里面首先存在的问题就是我们没法同时运行多个进程。其次就算我们不需要同时运行多个进程,程序在运行的过程中如果要等IO,CPU就只能空转,这也十分浪费CPU资源。

于是最早的多任务——协作式多任务诞生了,当程序由于要等IO而阻塞时就会去调度执行其它的进程。但是协作式多任务存在着很大的问题,就是每个进程运行的时间片长短是不确定的,而且是很偶然很随机的。如果一个进程它一直在做运算就是不进行IO操作,那么它就会一直霸占CPU。

针对这个问题,当时想出的方法是道德解决方案。内核向进程提供系统调用sched_yield,它会使进程主动放弃CPU让其它进程来执行。然后要求所有的程序员在程序中合适的地方尽量多地加入sched_yield调用。这个方法在当时是管用的,因为当时计算机的使用者(同时也是程序员)仅限于少数科研机构和政府机关的部分人员,一台电脑的共同使用者都认识,面子上还得过得去。

后来随着计算机的普及,以及计算机的使用者和程序员这两个角色的分离,主要靠道德约束的协作式多任务已经行不通了,我们需要强制性多任务,也就是抢占式多任务。抢占式多任务使得每个进程都可以相对公平地平分CPU时间,如果一个进程运行了过长的时间就会被强制性地调度出去,不管这个进程是否愿意。

有了抢占式多任务,我们在宏观上不仅可以同时运行多个进程,而且它们会一起齐头并进地往前运行,不会出现某个进程被饿死的情况,这样我们使用电脑的体验就非常完美了。抢占式多任务和协作式多任务不是对立的,它们是相互独立的,可以同时存在于系统中。

抢占又分为用户抢占和内核抢占。由于抢占对进程来说是异步的,进程被抢占时不一定运行在什么地方,有可能运行在用户空间,也有可能运行在内核空间(进程通过系统调用进入内核空间)。如果抢占点是在用户空间,那么抢占就是安全的,如果在内核空间就不一定安全,这是为什么呢?因为对于用户空间来说,如果抢占会导致线程同步问题,那么用户空间有责任使用线程同步机制来保护临界区,只要用户空间做好同步就不会出问题。

如果内核也做好了同步措施,内核抢占也不会出问题,但是内核最初的设计就没有考虑内核抢占问题,所以刚开始的时候内核是不能抢占的。后来内核开发者对内核进行了完善,把内核所有的临界区都加上了同步措施,然后内核就是可抢占的了。内核能抢占了不代表内核一定会抢占,内核会不会抢占由config选项控制,可以开启也可以关闭,因为内核抢占还会影响系统的响应性和性能。

开启内核抢占会提高系统的响应性但是会降低一点性能,关闭内核抢占会降低系统的响应性但是会提高一点性能。因此把内核抢占做成配置项,可以让大家灵活配置。服务器系统一般不需要与用户交互,所以会关闭内核抢占来提高性能,桌面系统会开启内核抢占来提高系统的响应性,来增加用户体验。

现在我们再来看一下为什么要调度。因为如果没有调度的话,就不能实现多任务,一次就只能运行一个程序,我们使用电脑的体验就会大大降低。有了调度就有了多任务,我们就能同时在电脑上做很多事情,使用体验就会非常好。

⑶为什么能调度

我们再来看看为什么能调度呢。我们把协作式多任务叫做主动调度,抢占式多任务叫做被动调度。为什么能调度分为两部分:为什么能触发调度和为什么能执行调度。对于主动调度,调度是进程主动触发的,这个是肯定能的。对于被动调度,在图灵机模型中是做不到的,因为图灵机是一条线性一直往前走的,进程在执行时,进程要是不主动,是不可能跳到其它进程来执行的。

被动调度能做到的原因关键就在于中断机制,因为中断是强行在正常的执行流中插入了一段代码,它能改变后续代码的走向。有了中断机制,我们就可以创建一个定时器中断,以固定的时间间隔比如每10ms来触发中断,检测进程是否运行时间过长,如果过长就触发调度。这样任何进程都不可能霸占CPU,所以进程都能公平地共享CPU时间。

图片图片

可以看到在纯图灵机模型中,进程如果不主动进行调度,是没有外力强迫进程进行调度的,进程就能一直霸占CPU。有了中断机制之后,在中断的处理中可以触发调度,在中断返回的点可以执行调度,这样就可以避免进程霸占CPU了。

前面说的是为何能触发进程调度,主动调度是进程自己触发的,被动调度是在中断中触发的。现在来看看为何能执行调度,执行调度包括两部分:选择进程和切换进程。选择进程是纯软件的,肯定能实现。切换进程是怎么切换呢?一个进程执行得好好的,怎么就切换了呢,需不需要硬件的支持呢?进程切换主要是切换执行栈和用户空间,这两个都需要用到CPU特定的指令。

⑷何时调度

我们前面已经讲了主动调度(协作式多任务)和被动调度(抢占式多任务)。

对于主动调度,触发调度和执行调度是同步的、一体的,触发即执行。主动调度发生的时机有IO等待、加锁失败等各种阻塞操作以及用户空间主动调用sched_yield。

对于被动调度,触发调度和执行调度是异步的、分离的,触发调度并不会立马执行调度,而是做个需要调度的标记,然后在之后的某个合适的地方会检测这个标记,如果被设置就进行调度。触发调度的点有:在定时器中断中发现当前进程超时了,在唤醒进程时发现新进程需要抢占当前进程,在迁移进程时发现新进程需要抢占当前进程,在改变进程优先级时发现新进程需要抢占当前进程。

其中第一个触发点是当前进程需要被抢占,它是用来保证公平调度,防止进程霸占CPU的,后三个触发点是新进程需要抢占当前进程,它是用来提高系统响应性的。执行调度的点有:系统调用完成之后即将返回用户空间,中断完成之后即将返回用户空间,如果开启了内核抢占的话则还有,中断完成之后即将返回内核,如果中断发生在禁止抢占临界区中,那么中断完成之后返回内核是不会执行调度的,而是会在临界区结束的时候执行调度。

图片图片

系统调用完成之后即将返回用户空间和中断完成之后即将返回用户空间,是非常好的执行进行调度的点,也就是此图中的三个箭头的地方。CPU异常在意义上不是系统调用,但是在形式上和逻辑上相当于是系统调用。

图片图片

图片图片

中断发生在内核空间的场景,如果开启了内核抢占,如果被抢占的内核代码不是在禁用抢占临界区,中断返回时是执行调度的点。如果被抢占的内核代码在禁用抢占临界区中,在执行调度的点被推迟到了临界区的出口处。

⑸如何调度

现在到了执行调度的时刻了。执行调度分为两步:一是选择下一个要执行的进程,二是切换进程。选择下一个要执行的进程,这就是调度算法了。首先调度算法只能从Runnable的进程中进行选择,不能选择Blocked进程,因为选择了也没有意义。其次算法还要区分进程类型,比如普通进程与实时进程,肯定要优先选择实时进程,在同一类型的进程中还要有具体的算法来决定到底选择哪个进程。在Linux中一共把进程分为了5类,每一类都有一个具体的算法。类之间的关系是优先选择高类的进程,只有当高类没有Runnable进程时才会去选择低类进程。

进程选择好了之后就要切换进程了。切换进程分两步:第一步是切换用户空间,第二步是切换执行栈(线程栈)。如果要切换的两个线程属于同一个进程就不需要切换用户空间了。切换用户空间是一个CPU架构相关的事情,在x86 CPU上是给CR3寄存器赋值新进程的页表树的根指针。

此时切换的执行栈是线程的内核栈,执行栈代表的是当前线程的执行情况,切换执行栈就是切换线程。线程的用户栈信息都在内核栈里保存着。切换完内核栈之后,线程继续执行就会返回用户空间,由于此时用户空间已经切换完成,内核栈上也保存着用户栈的信息,所以线程能返回到正确的用户空间线程上去。下面我们画个图来看一下:

图片图片

对于一个CPU来说,永远只有一个当前进程在运行,当执行进程调度时,需要从其它进程中选择一个进程,把它旋转到最下方作为当前进程,它就开始运行了。

⑹调度均衡

前面所说的都是针对一个CPU的情况,对于多个CPU来说,每个CPU也是这样的逻辑。但是有一点不同的是,如果一个系统上的多个CPU忙的忙死闲的闲死,显然不太好,因此多个CPU之间会进行调度均衡。调度均衡可以分为个体均衡和总体均衡。个体均衡是从进程的角度出发选择到一个相对清闲的CPU上去运行。总体均衡是从CPU的角度出发如何从别的CPU上拉取一些进程到自己这来执行,使得所有CPU的工作量尽量平均。

个体均衡的触发点有三个:一是新进程刚创建时,二是进程要执行新程序时,三是进程被唤醒时,在这三个点进程都可以选择去哪个CPU的运行队列上去等待执行。在个体均衡下,每个进程都尽量选择相对清闲的CPU,所以所有CPU的负载应该还是会比较均衡的。但是时间长了可能还是会出现负载不均衡的情况,此时就要进行总体均衡了。总体均衡的触发点有三个:一是CPU即将idle前会去找到最忙的CPU然后拉取一些任务过来;二是定时器中断的周期性检测,会检查是否所有的CPU都一样忙,如果忙闲差别太大就会进行进程迁移,使得所有CPU忙闲程度接近;三是在idle进程中如果CPU发现自己太忙而有的CPU在idle就会唤醒那个CPU进行负载均衡。

二、基础概念解析

2.1进程的定义与分类

在计算机系统中,进程堪称最基础且重要的概念之一。从本质上讲,进程是程序在计算机中的一次动态执行过程。打个比方,当你在 Linux 系统中启动一个应用程序,如文本编辑器,系统便会为该程序创建一个进程,这个进程涵盖了程序运行所需的各种资源,包括代码、数据、打开的文件以及分配的内存空间等。

进程可以依据不同的标准进行分类。常见的分类方式有:按照对资源的需求特性,可分为 I/O 受限型进程和 CPU 受限型进程 。I/O 受限型进程,正如其名,这类进程的执行过程中,大部分时间都在等待 I/O 操作的完成,例如读取文件、网络请求等,像浏览器在加载网页时,就需要频繁地进行网络 I/O 操作,此时它就是一个典型的 I/O 受限型进程。而 CPU 受限型进程则主要将时间花费在 CPU 的计算上,像一些科学计算程序、加密算法程序等,它们需要大量的 CPU 计算资源来完成复杂的运算任务。

依据进程的实时性要求,进程又可分为实时进程和普通进程 。实时进程对响应时间有着极高的要求,必须在规定的时间内完成任务,否则可能会导致严重的后果。以工业控制系统中的实时监控进程为例,它需要实时采集传感器的数据,并及时做出响应,以确保生产过程的安全和稳定。如果这个进程不能在规定时间内完成数据采集和处理,就可能引发生产事故。相比之下,普通进程对响应时间的要求相对宽松,它们可以在系统资源允许的情况下,逐步完成任务,如我们日常使用的文本编辑、文件下载等操作对应的进程。

2.2进程状态详解

Linux 进程有多种状态,常见的状态包括:

  • R(可执行状态):进程处于 ready 状态,即随时可以执行。在系统中,处于这个状态的进程意味着它已经具备了运行的条件,只等待被分配 CPU 资源。
  • S(可中断睡眠状态):可以中断的睡眠状态。当进程接受消息队列、执行 sleep 等操作时,进程处于阻塞状态(S)。这种状态下的进程可以被某些信号中断,从而转换状态。例如,当接收到特定的信号时,进程可以从睡眠状态被唤醒,进入可执行状态。
  • D(不可中断睡眠状态):不可中断的睡眠状态,比较少见。处于这种状态的进程通常在等待某些关键资源,并且不能被一般的信号中断。比如在等待磁盘 I/O 完成的过程中,进程可能处于不可中断睡眠状态。
  • T(暂停或者跟踪状态):暂停状态是指进程收到 sigstopt 信号变为暂停状态,跟踪状态则是在被调试或跟踪时的状态。例如,在使用调试工具时,进程可以被设置为跟踪状态,以便开发者观察其执行过程。
  • Z(僵尸状态):退出状态,进程称为僵尸进程(子进程退出)。当子进程完成任务后,它会进入僵尸状态,等待父进程读取其退出状态信息。如果父进程没有及时处理,僵尸进程会一直占用系统资源。
  • X(退出状态,进程即将被销毁):退出状态,进程即将被销毁。当进程完成所有任务并释放了所有资源后,会进入这个状态,等待系统最终清理。

进程状态之间可以相互转换。一般来说,进程可能从可执行状态(R)进入睡眠状态(S 或 D),当等待的事件发生时,又从睡眠状态转换回可执行状态。如果进程收到暂停信号,会从可执行状态转换为暂停状态(T),收到继续执行的信号后再转换回可执行状态。当进程完成任务后,会从可执行状态转换为退出状态(Z 或 X)。

三、常见调度策略

3.1实时调度策略

实时调度策略对于那些对时间极为敏感的任务而言,无疑是至关重要的保障。在 Linux 系统中,实时调度策略主要包含 DEADLINE、SCHED_FIFO 以及 SCHED_RR 这几种类型 。

DEADLINE 调度策略,犹如一位精准的时间管家,它主要依据任务的截止时间来进行调度决策。每个任务在创建之初,都会被分配运行时(Runtime)、周期(Period)以及最后期限(Deadline)这三个关键参数。调度器会如同严格的监工,时刻紧盯任务的最后期限,优先调度那些截止时间最早的任务。举例来说,在工业自动化生产线上,传感器数据的实时采集与处理任务就对时间有着严苛的要求,DEADLINE 调度策略能够确保这些任务在规定的时间内完成,从而保障生产线的稳定、高效运行。

Linux 实时进程采用 SCHED_FIFO 和 SCHED_RR 调度算法:

①SCHED_FIFO

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

实时优先级的静态特性影响:实时优先级只有静态优先级,不会调整优先级,默认优先级为 0 - 99(MAX_RT_PRIO = 100)。这意味着一旦确定了实时进程的优先级,在整个运行过程中它不会因为其他因素而改变,保证了高优先级的实时进程能够在需要时尽快得到执行。

②SCHED_RR

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

实时优先级的静态特性影响:同样,由于实时优先级是静态的,SCHED_RR 进程在确定了优先级后,其执行顺序和时间片的分配都是基于这个固定的优先级。这使得系统在处理实时任务时能够有明确的调度规则,确保关键任务能够在规定的时间内得到执行。

3.2完全公平调度策略(CFS)

完全公平调度策略(CFS)是 Linux 进程调度器的核心组成部分,其设计理念精妙绝伦,旨在为系统中的每个进程都提供公平的 CPU 时间分配。

CFS 的核心思想建立在虚拟运行时间(vruntime)这一创新概念之上。简单来讲,虚拟运行时间是一个相对值,它用于衡量每个进程在 CPU 上的执行时间份额。每个进程都拥有一个属于自己的 vruntime,其数值会随着进程的运行而不断增加。不过,这里存在一个关键的区别,那就是不同优先级的进程,其 vruntime 的增加速度并不相同。具体而言,高优先级的进程,其 vruntime 的增长速度相对较慢;而低优先级的进程,vruntime 的增长速度则会更快。这就好比在一场比赛中,高优先级的选手被赋予了较慢的前进速度,而低优先级的选手则以较快的速度前进,这样一来,最终的结果是所有选手在虚拟的时间赛道上能够相对公平地竞争。

为了更直观地理解,我们不妨假设系统中有两个进程 A 和 B,进程 A 的优先级较高,进程 B 的优先级较低。在开始时,它们的 vruntime 都为 0。随着时间的推移,进程 A 和 B 都开始运行。由于进程 A 的优先级高,它的 vruntime 增长速度较慢,假设在一段时间内,进程 A 的 vruntime 只增加了 1;而进程 B 由于优先级低,其 vruntime 增长速度较快,在相同的时间内,进程 B 的 vruntime 增加了 3。此时,调度器在进行调度决策时,会优先选择 vruntime 值最小的进程,也就是进程 A。这样,高优先级的进程 A 就能够得到更多的 CPU 运行时间,同时,低优先级的进程 B 也不会被完全忽视,依然有机会在合适的时候运行。

在 CFS 的实现过程中,使用了红黑树这种高效的数据结构来管理所有进程。红黑树以 vruntime 作为键值,将所有可运行的进程按照 vruntime 的大小进行排序。这样,在每次进行调度时,调度器只需要从红黑树的最左端选取 vruntime 值最小的进程,即可实现高效、公平的调度。这种方式不仅保证了进程间的公平性,还能够有效减少调度的开销,提高系统的整体性能。

完全公平调度算法依赖于虚拟时钟,用以度量等待进程在完全公平系统中所能得到的CPU时间。但数据结构中并没有虚拟时钟的表示,这是因为虚拟时钟可以通过实际时钟,以及与每个进程相关的负荷权重计算出来;所有和虚拟时钟相关的计算都在update_curr中进行的,该函数在系统中各个不同的地方调用。

336 static void update_curr(struct cfs_rq *cfs_rq)
 337 {
 338     struct sched_entity *curr = cfs_rq->curr;
 339     u64 now = rq_of(cfs_rq)->clock;
 340     unsigned long delta_exec;
 341 
 342     if (unlikely(!curr))
 343         return;
 344 
 345     /*
 346      * Get the amount of time the current task was running
 347      * since the last time we changed load (this cannot
 348      * overflow on 32 bits):
 349      */
 350     delta_exec = (unsigned long)(now - curr->exec_start);
 351 
 352     __update_curr(cfs_rq, curr, delta_exec);
 353     curr->exec_start = now;
 354 
 355     if (entity_is_task(curr)) {
 356         struct task_struct *curtask = task_of(curr);
 357 
 358         cpuacct_charge(curtask, delta_exec);
 359     }
 360 }

339 rq_of(cfs_rq)->clock 用于实现就绪队列自身的时钟,每次调用周期性调度器时,都会更新clock的值。

350 curr->exec_start保存了上次更改load时的时间,注意并不是进程的上一次运行时间。当前进程在一次运行过程中,可能会发生多次update_curr。

__update_curr

304 static inline void
 305 __update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,
 306           unsigned long delta_exec)
 307 {
 308     unsigned long delta_exec_weighted;
 309     u64 vruntime;
 310 
 311     schedstat_set(curr->exec_max, max((u64)delta_exec, curr->exec_max));
 312 
 313     curr->sum_exec_runtime += delta_exec;
 314     schedstat_add(cfs_rq, exec_clock, delta_exec);
 315     delta_exec_weighted = delta_exec;
 316     if (unlikely(curr->load.weight != NICE_0_LOAD)) {
 317         delta_exec_weighted = calc_delta_fair(delta_exec_weighted,
 318                             &curr->load);
 319     }
 320     curr->vruntime += delta_exec_weighted;
 321 
 322     /*
 323      * maintain cfs_rq->min_vruntime to be a monotonic increasing
 324      * value tracking the leftmost vruntime in the tree.
 325      */
 326     if (first_fair(cfs_rq)) {
 327         vruntime = min_vruntime(curr->vruntime,
 328                 __pick_next_entity(cfs_rq)->vruntime);
 329     } else
 330         vruntime = curr->vruntime;
 331 
 332     cfs_rq->min_vruntime =
 333         max_vruntime(cfs_rq->min_vruntime, vruntime);
 334 }
  • 313 sum_exec_runtime 该进程消耗的CPU时间累积值,@delta_exec是上一次更新负荷统计量时两次的差值,二者都是真实时间。
  • 316 如果进程的优先级为120(nice = 0),那么虚拟时间和物理时间相同,否则通过calc_delta_mine计算虚拟执行时间。
  • 326 first_fait检测树上是否有最左边的节点,即是否有进程在树上等待调度。
  • 332 cfs_rq->min_vruntime是单调增加的。

在运行过程中,进程调度实体的vruntime是单调增加的,优先级越高的进程,增加的速度越慢,因此他们向右移动的速度也就越慢。这样被调度的机会就越大。

延迟跟踪:内核有一个固有的概念,称之为调度延迟,保证每个进程至少被运行一次的时间间隔。

sysctl_sched_latency:参数用来控制这个行为,缺省定义为20ms,可以通过/proc/sys/kernel/sched_latency_ns来控制。

sched_nr_latency:控制在一个延迟周期内处理的最大活动数目。如果活动进程的数据超过了该上限,则延迟周期也成比例的线性扩展。

__sched_period:延迟周期的长度,通常就是sysctl_sched_latency,但是如果有更多的进程正在运行,那么其值要通过如下公式计算:

__sched_period = sysctl_sched_latency * nr_running / sched_nr_latency

在一个延迟周期内,通过考虑各个进程的权重,将延迟周期的时间在活动进程之间进行分配。对于有某个调度实体表示的给定进程,分配到的时间如下计算:

static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    u64 slice = __sched_period(cfs_rq->nr_running);
 
    slice *= se->load.weight;
    do_div(slice, cfs_rq->load.weight);
 
    return slice;
}

四、调度器的工作机制

4.1进程优先级的确定

在 Linux 进程调度的复杂体系中,进程优先级的确定犹如精密仪器的校准,是保障系统高效、稳定运行的关键环节。这一过程并非简单随意,而是受到诸多因素的综合影响,其中,Nice 值和 Priority 权重值起着核心作用。

Nice 值,作为用户可以对进程优先级进行调整的重要手段,其取值范围在 - 20 到 19 之间 。这里需要明确的是,数值越小,代表进程的优先级越高。例如,当一个进程的 Nice 值被设置为 - 10 时,相较于 Nice 值为 5 的进程,它在 CPU 资源竞争中会更具优势,更有可能优先获得 CPU 的执行时间。

通常情况下,普通用户仅能将 Nice 值设置在 0 到 19 的区间内,这意味着普通用户只能降低自己进程的优先级,以避免对系统中其他重要进程造成资源抢占。而超级用户则拥有更大的权限,能够在 - 20 到 19 的全范围内进行调整,从而根据系统的实际需求,灵活地为特定进程赋予更高或更低的优先级。比如,在进行系统维护任务时,超级用户可以将相关维护进程的 Nice 值设置为较低数值,以确保这些任务能够优先且高效地执行。

Priority 权重值,并非孤立存在,它与 Nice 值紧密关联,共同影响着进程的优先级 。系统会依据一系列复杂的算法,结合 Nice 值以及其他诸如进程的资源使用情况、运行历史等因素,来精确计算出每个进程的 Priority 权重值。这个权重值就如同进程在 CPU 资源分配 “赛场” 上的实力评估指标,权重值越高,进程在竞争 CPU 资源时的地位就越有利,能够获得的 CPU 时间份额也就越多。

例如,对于一个长时间运行且对系统性能至关重要的后台服务进程,系统可能会根据其稳定的资源使用情况和重要性,为其分配较高的 Priority 权重值,从而保证它在多进程环境中能够持续、稳定地获得足够的 CPU 资源,以维持服务的高效运行。

Linux 提供两种优先级:普通进程优先级和实时进程优先级。

⑴实时进程优先级:

实时优先级采用两种调度算法:SCHED_FIFO(先入先出调度算法)和 SCHED_RR(时间片轮询调度算法)。

实时优先级只有静态优先级,不会调整优先级,默认优先级为 0 - 99(MAX_RT_PRIO = 100)。nice 值只影响 100 ~ 100 + 40 的进程优先级。

对于 SCHED_FIFO 算法,只有当进程执行完毕才能轮到其他进程执行;对于 SCHED_RR 算法,一旦时间片消耗完,则将该进程放到队列末端,其他进程才能执行。

⑵普通进程优先级

普通优先级采用的调度算法是 SCHED_NORMAL(CFS 调度器实现)。

普通优先级根据动态优先级调度,动态优先级由静态优先级调整而来。静态优先级由内核隐藏,但可以由 nice 值计算得到:static_prio = MAX_RT_PRIO(默认 100)+ nice + 20。nice 取值范围是 -20 ~ 19,所以静态优先级为 100 ~ 139。

进程时间片也是由静态优先级得到。如果静态优先级小于 120,时间片为 (140 - static_prio) * 20;如果静态优先级大于等于 120,时间片为 (140 - static_prio) * 5。

动态优先级主要考虑静态优先级和进程平均运行时间 bouns 值,计算公式为:dynamic_prio = max (100, min (static_prio - bouns + 5, 139))。当 bouns 值大于 5 表示优先级提高,小于 5 时优先级变低。Linux 内核会根据进程的平均运行时间动态改变进程的动态优先级。一般来说,交互式进程的平均运行时间比较长,因此 Linux 内核会奖励从而增加 bouns 的值。

4.2调度的时机与触发

进程调度的时机与触发,就像是一场精心编排的交响乐演出中的节奏转换,在 Linux 系统运行的每一刻都发挥着关键作用。了解这一过程,能够让我们深入洞悉系统是如何在不同场景下,巧妙地进行进程切换,以实现资源的最优配置。

当进程的状态发生变化时,调度器会敏锐地捕捉到这一信号,并适时地介入调度 。比如,当一个进程从运行状态转变为等待状态时,这通常意味着它需要等待某种外部资源的就绪,如等待磁盘 I/O 操作完成、等待网络数据的接收等。以一个正在读取大型文件的进程为例,当它发出读取文件的请求后,由于磁盘读写速度相对较慢,它不得不进入等待状态,此时调度器会感知到这一状态变化,将 CPU 资源从该进程转移到其他处于就绪状态的进程上,以避免 CPU 资源的闲置浪费。同样,当进程从等待状态转变为就绪状态时,例如等待的网络数据已经接收完毕,调度器会将其重新纳入可调度的进程队列中,参与 CPU 资源的竞争。

时间片用完也是引发调度的重要时机 。在 Linux 系统中,每个进程在被调度执行时,都会被分配一个特定的时间片,这就好比运动员在比赛中被分配的一段特定比赛时间。当进程的时间片耗尽时,即使该进程尚未完成当前任务,系统也会强制暂停它的运行,并将 CPU 资源分配给其他就绪进程。这样做的目的在于确保系统中的各个进程都能有机会公平地使用 CPU 资源,避免某个进程长时间独占 CPU,导致其他进程饥饿。例如,在一个多用户的服务器环境中,如果某个进程长时间占用大量 CPU 时间,会使得其他用户的进程响应迟缓,而通过时间片轮转机制,能够保证每个用户的进程都能在合理的时间内得到执行,提高系统的整体公平性和响应性能。

此外,当系统中出现中断事件时,调度也可能会发生 。中断是指计算机在运行过程中,由于外部设备或内部事件的触发,暂时停止当前正在执行的程序,转而执行相应的中断处理程序的过程。例如,当用户按下键盘上的某个按键时,会产生一个键盘中断信号,通知系统有新的输入事件发生。在处理中断的过程中,系统可能会根据当前的进程状态和资源需求,决定是否进行进程调度。如果在中断处理完成后,发现有更适合运行的进程,调度器会及时进行切换,以确保系统能够快速响应各种外部事件,保持高效的运行状态。

4.3进程调度器特点

Linux 进程调度器对于 CPU 进程调度,采用时间分片的方式,特点如下:

  • 每个进程近似相等的 CPU 使用权:每一个进程有近乎相等的执行时间,对于逻辑 CPU 而言,进程调度使用轮询的方式执行,当轮询完成则回到第一个进程反复。进程数量消耗时间和进程量成正比,虽然这种方式可能导致一些重要任务延迟,但使得系统最为稳定。
  • 进程状态与调度:对于大部分进程来说,不使用时多数处于睡眠状态。除了睡眠状态之外,进程还有运行状态、僵死状态、就绪状态等。当进程处于睡眠状态时,不占用 CPU 时间,只有在某些条件触发时才会获得 CPU 调度分配,如外部存储器访问、用户键入或者鼠标操作触发事件、等待指定时间等。
  • 吞吐量和延迟:吞吐量是处理完成的进程数量与耗费时间的比值。如果进程多过逻辑 CPU 的数量,则再增加进程无法增加吞吐量。延迟是结束处理时间与开始处理时间的差值,多个进程执行会获得近似平均的延迟,进程越多延迟越高。在实际系统中,可能出现空闲进程、进程运行态但未就绪、进程运行态且都就绪等情况,不同情况会对延迟和吞吐量产生不同影响。

五、Linux调度器的演变历程

Linux 调度器的发展历程,犹如一部不断演进的科技史诗,见证了操作系统为满足日益复杂的计算需求而进行的持续创新与优化 。

早期的 Linux 系统采用了相对简单的调度算法,如 O (n) 调度器。这种调度器的工作方式较为直观,它会在一个全局的任务队列中,依次遍历所有进程,从中挑选出优先级最高的进程来执行。不难想象,当系统中的进程数量较少时,这种调度方式能够较为有效地工作。然而,随着计算机技术的飞速发展,系统中同时运行的进程数量大幅增加,O (n) 调度器的弊端便逐渐暴露出来。由于每次调度都需要遍历整个任务队列,其时间复杂度与进程数量成正比,这就导致在高负载情况下,调度的效率急剧下降,系统性能受到严重影响。

为了应对 O (n) 调度器的性能瓶颈,O (1) 调度器应运而生。它的出现,犹如为 Linux 系统注入了一剂强心针,带来了显著的性能提升。O (1) 调度器的核心创新在于,为每个 CPU 都配备了独立的运行队列,并且通过精妙的算法,实现了在常数时间内完成调度决策。这意味着,无论系统中存在多少进程,调度器都能以几乎相同的速度进行调度,极大地提高了调度效率。具体来说,O (1) 调度器将进程按照优先级分为不同的队列,通过维护活动队列和过期队列,以及使用位图等数据结构,快速地找到下一个需要执行的进程。这种设计使得 O (1) 调度器在多处理器系统中表现出色,能够更好地平衡负载,提高系统的整体性能。

然而,技术的发展永无止境,O (1) 调度器在使用过程中也逐渐暴露出一些问题。例如,它在处理 I/O 密集型任务时,识别准确率有待提高;而且,在进行活动队列和过期队列的交换以及重新评估优先级时,会耗费一定的时间,这在一定程度上影响了系统的性能。为了解决这些问题,Completely Fair Scheduler(CFS)调度器横空出世 。CFS 调度器的设计理念独树一帜,它摒弃了传统的固定时间片概念,引入了虚拟运行时间(vruntime)的创新概念。每个进程都拥有自己的 vruntime,这个值会随着进程的运行而不断变化。CFS 调度器通过红黑树数据结构,对所有进程的 vruntime 进行排序,每次调度时,选择 vruntime 最小的进程运行,从而确保了每个进程都能公平地获得 CPU 时间。这种公平性的设计,使得 CFS 调度器在多任务环境下表现卓越,能够为各种类型的进程提供高效、公平的调度服务。

近年来,随着硬件技术的不断进步,如多核处理器、超线程技术的广泛应用,以及用户对系统性能和响应性要求的日益提高,Linux 调度器也在持续进化。一些新的调度器,如 SCHED_DEADLINE、BORE(Burst-Oriented Response Enhancer)等应运而生 。SCHED_DEADLINE 调度器专门针对具有严格时间限制的任务,能够确保这些任务在截止日期前完成,在实时性要求极高的场景,如工业自动化控制、航空航天等领域,发挥着重要作用。BORE 调度器则通过引入 “突发性” 这一概念,对任务的权重和延迟进行动态调整,能够更好地适应不同类型的系统负载,在多任务环境中,为用户提供更流畅、高效的使用体验。

回顾 Linux 调度器的演变历程,从简单的 O (n) 调度器到高效的 O (1) 调度器,再到公平的 CFS 调度器以及不断涌现的新型调度器,每一次的变革都紧密围绕着硬件的发展和用户的需求展开。这些演进不仅提升了 Linux 系统的性能和稳定性,还为用户带来了更加出色的使用体验,使得 Linux 操作系统在各种领域中都能保持强大的竞争力。

六、案例分析

在实际的应用场景中,Linux 进程调度器的作用得到了淋漓尽致的体现。以服务器端的 Web 应用为例,在高并发的访问情况下,大量的 HTTP 请求如潮水般涌来,每个请求都对应着一个进程或线程。此时,Linux 进程调度器凭借其高效的调度策略,能够迅速且合理地为这些进程分配 CPU 资源,确保每个请求都能得到及时的处理。比如,当一个用户在浏览器中快速点击多个链接,发起一系列的页面请求时,调度器会快速响应,优先处理那些对响应时间要求较高的交互式请求,让用户感受到流畅的浏览体验,避免出现长时间等待或页面卡顿的情况。

在实时控制系统中,Linux 进程调度器同样发挥着关键作用。以自动驾驶汽车的控制系统为例,车辆在行驶过程中,需要实时采集各种传感器的数据,如摄像头图像数据、雷达距离数据等,并对这些数据进行快速处理,以做出准确的驾驶决策,如加速、刹车、转向等。Linux 进程调度器能够确保这些实时任务在严格的时间限制内完成,保证车辆的行驶安全。在工业自动化生产线中,各种设备的控制和数据采集任务也对实时性要求极高,调度器能够根据任务的优先级和时间要求,精确地调度各个进程,确保生产线的高效、稳定运行。

在移动设备领域,Linux 系统也被广泛应用。以智能手机为例,当用户同时运行多个应用程序,如微信、音乐播放器、浏览器等时,Linux 进程调度器会根据各个应用的优先级和资源需求,合理分配 CPU 时间。对于正在前台运行的应用,调度器会给予较高的优先级,确保其能够流畅运行,为用户提供良好的交互体验;而对于后台运行的应用,调度器会适当降低其优先级,在保证系统整体性能的前提下,让它们在空闲时间进行数据更新等操作。这样,用户在使用手机时,就能够感受到多个应用的同时运行,而不会出现明显的卡顿或延迟现象。

七、全文总结

Linux 进程调度器作为操作系统的核心组件,历经多年的发展与演进,已经成为一个功能强大、高度优化的资源管理系统。从早期简单的调度算法到如今复杂而精妙的调度策略,每一次的变革都紧密贴合着硬件技术的发展以及用户不断增长的需求。它不仅确保了系统的高效运行,还为各种应用场景提供了坚实的支持。

实时调度策略为对时间要求极高的任务提供了精准的保障,使得诸如工业自动化、航空航天等关键领域的系统能够稳定、可靠地运行。而完全公平调度策略(CFS)则秉持着公平的理念,为系统中的每个进程都分配了合理的 CPU 时间,有效避免了进程饥饿现象的发生,极大地提升了系统的整体性能和用户体验。

展望未来,随着科技的飞速发展,Linux 进程调度器有望迎来更多令人瞩目的创新与突破。随着人工智能和机器学习技术的蓬勃兴起,将这些先进技术融入到调度器的设计中,实现更加智能化的调度决策,是一个极具潜力的发展方向。通过对系统运行状态的实时监测和深度分析,调度器能够精准预测进程的资源需求,并据此动态调整调度策略,从而进一步提升系统的资源利用率和响应速度。

此外,随着硬件技术的不断革新,如新型处理器架构的不断涌现,Linux 进程调度器也需要持续优化,以充分发挥这些硬件的强大性能。在面对未来复杂多变的应用场景和用户需求时,相信 Linux 进程调度器将凭借其强大的适应性和创新性,不断发展壮大,为 Linux 操作系统的持续领先地位奠定坚实的基础。

责任编辑:武晓燕 来源: 深度Linux
相关推荐

2012-05-09 20:18:52

2020-03-10 19:34:08

CPU虚拟化调度

2019-06-26 09:10:07

操作系统调度算法

2011-07-28 10:32:32

操作系统军方

2009-12-15 18:27:51

Linux操作系统

2013-03-08 09:46:34

Linux操作系统安全性

2023-06-09 08:06:14

操作系统调度器LLM

2010-04-15 10:41:13

2010-04-15 17:21:40

Unix操作系统

2010-04-14 13:59:45

Unix操作系统

2009-12-09 17:25:19

Linux操作系统

2010-04-16 18:19:32

Unix操作系统

2009-12-22 13:44:33

Linux操作系统

2010-04-08 16:05:49

Unix操作系统

2012-05-04 09:49:34

进程

2009-12-10 17:48:35

Linux操作系统

2024-06-27 17:25:03

锐捷云桌面

2011-01-10 16:34:13

linux安装

2011-04-14 15:14:36

嵌入式操作系统嵌入式

2021-08-20 08:33:19

操作系统OS
点赞
收藏

51CTO技术栈公众号