6张图搞懂 Linux 进程调度系统

6张图搞懂 Linux 进程调度系统

目录

大家好,这里是物联网心球。本期文章我们来深入学习 Linux 进程调度系统。

1.进程调度是什么?

进程调度是内核按照既定规则,从就绪队列里挑选进程、分配 CPU 使用权,并完成进程切换的整套机制。

进程调度的目标:

  • 公平性:让每个进程都能获得合理的 CPU 时间,避免某个进程独占资源。

  • 效率:最大化 CPU 利用率,减少空闲等待。

  • 响应性:让交互式程序(如浏览器、终端)能快速响应用户操作。

  • 实时性:保证关键任务(如工业控制、音视频处理)在严格时限内完成。

2.进程调度系统

如图1所示,进程调度系统核心概念包括:调度策略、调度类进程、优先级、调度实体、CPU运行队列、调度时机。

6张图搞懂 Linux 进程调度系统 图1

图1 进程调度系统

3.调度策略

调度策略是操作系统为进程分配 CPU 资源、决定执行顺序与切换规则所采用的算法和规则。

调度策略分类:

  • SCHED_NORMAL(0):完全公平调度,Linux 默认调度策略,调度器始终选择 vruntime 最小的进程运行。

  • SCHED_FIFO(1):实时先进先出,无时间片,一旦获得 CPU 一直运行,除非被更高优先级实时任务抢占。

  • SCHED_RR(2):实时时间片轮询,与 SCHED_FIFO 唯一的区别是同优先级进程之间增加了时间片轮转。

  • SCHED_BATCH(3):批量处理调度,与SCHED_NORMAL区别是,进程唤醒后不会主动抢占当前正在运行的前台进程。

  • SCHED_IDLE(5):空闲调度,优先级最低,仅系统无其他任务时才运行。

  • SCHED_DEADLINE(6):截止期限调度,优先级最高,基于 EDF(最早截止时间优先) 算法,为任务设置运行周期、运行时长、截止时间,内核保证任务在截止时间前完成。

  • SCHED_EXT(7):可扩展调度器,通过 BPF 程序自定义调度逻辑(Linux 6.12+)。

4.调度类

调度类(struct sched_class) 是 Linux 内核实现模块化调度的核心结构体,它将调度算法、选进程、入队、出队、抢占、切换等一系列调度行为封装成统一接口。

不同类型的进程归属不同调度类,每个调度类对应一套独立调度逻辑,高优先级调度类的进程会无条件抢占低优先级调度类进程。struct sched_class 定义如下:

struct sched_class {
    void (*enqueue_task) (......); /* 将进程加入CPU运行队列 */
    bool (*dequeue_task) (......); /* 将进程移出CPU运行队列 */
    void (*yield_task) (......); /* 主动让出CPU:sched_yield() 时调用 */
    struct task_struct *(*pick_task)(......); /* 从本调度类中选出一个最应该运行的进程 */
    struct task_struct *(*pick_next_task)(......); /* 选择下一个运行进程 */
    void (*put_prev_task)(......); /* 把上一个进程放回CPU运行队列 */
    void (*set_next_task)(......); /* 设置下一个进程为即将运行进程 */
    int  (*select_task_rq)(......); /* 为进程选择目标CPU */
    void (*set_cpus_allowed)(......); /* 设置CPU亲和性 */
    void (*update_curr)(......); /* 更新当前进程运行时间 */
};

Linux 常见调度类(按优先级从高到低)如下:

  • dl_sched_class(DEADLINE 硬实时调度类):优先级最高,用于工业控制、自动驾驶、低延时音视频,按截止时间调度。

  • rt_sched_class(实时调度类):用于实时任务,支持 SCHED_FIFO、SCHED_RR,高优先级可抢占所有普通进程。

  • fair_sched_class(完全公平调度类):Linux 默认调度类,管理普通进程(SCHED_NORMAL、SCHED_BATCH),保证 CPU 公平分配。

  • idle_sched_class(空闲调度类):优先级最低,只有 CPU 完全空闲时才运行,用于系统 idle 任务。

  • stop_sched_class(停止调度类):内核内部最高优先级,用于 CPU 热插拔、内核暂停,用户无法使用。

5.进程优先级

进程优先级是内核决定哪个进程先使用 CPU的权重值,内核永远优先选择优先级更高的进程运行,进程 task_struct 结构定义了四个优先级字段:

struct task_struct {
    int             prio; /* 动态优先级 */
    int             static_prio; /* 静态优先级 */
    int             normal_prio; /* 理论标准优先级 */
    unsigned int      rt_priority; /* 实时优先级 */
    ......
};
  • prio:动态优先级(真实优先级),调度器通过 prio 选择下一个要运行的进程时,通常等于 normal_prio,但可以临时变化(不是固定死的)。范围:-1~139,数值越小,优先级越高。

  • static_prio:静态优先级,只给 CFS 普通进程使用,由 static_prio = 120 + nice 计算而来,范围:100~139。

  • normal_prio:normal_prio 是动态优先级 prio 的初始值。Linux 有优先级继承、优先级翻转等机制,这些机制会临时修改 prio,让进程优先级临时变高,normal_prio 是优先级临时调整后恢复的标准值。

  • rt_priority:实时优先级,只给实时进程使用(SCHED_FIFO/SCHED_RR),范围:1 ~ 99,数值越大优先级越高。

我们通过一张图来直观了解一下这几个优先级,如图2所示。

6张图搞懂 Linux 进程调度系统 图2

图2 进程优先级

我们把进程优先级总结为一张表(见表1),方便查看:

6张图搞懂 Linux 进程调度系统 图3

表1 进程优先级

在进程优先级中,有一个比较重要的概念nice值,nice 值是 Linux 系统里用于调整普通进程(完全公平调度)优先级的参数,取值范围 -20 ~ 19,默认值为 0。数值越小,进程抢占 CPU 的能力越强,优先级越高;数值越大则越谦让、优先级越低。

它仅作用于普通进程,对实时进程、DEADLINE 调度进程无效,同时普通用户只能调高该值,root 用户可全权修改。

静态优先级 static_prio 由 nice 直接计算:static_prio = 120 + nice。Linux 提供了查看和修改nice值的命令,如下:

# 显示进程的 PID、nice 值和命令
ps -eo pid,ni,cmd

# 查看特定进程
ps -o pid,ni,cmd -p <PID>

# 启动新进程时指定 nice 值
nice -n <nice值> 命令

# 修改正在运行的进程的 nice 值,注意:普通用户只能调高 nice 值
renice <新nice值> -p <PID>

这些命令底层依赖 getpriority 和 setpriority 函数,getpriority 和 setpriority 函数(系统调用)用于获取和设置指定进程、进程组或用户的 nice 值。

#include <sys/resource.h>
// 成功:返回目标进程的 nice 值(注意:nice值可能 -1);返回 -1,并设置 errno
int getpriority(int which, int who);
// 成功:返回 0;失败:返回 -1,并设置 errno。
int setpriority(int which, int who, int prio);

参数说明:

  • which: 用于指定操作的目标类型:

  • PRIO_PROCESS:操作单个进程, who 填写目标进程的 PID。

  • PRIO_PGRP:操作进程组,who 填写目标进程组的 PGID。

  • PRIO_USER:操作指定用户所有进程,who 填写目标用户的 UID。

  • who:配合which使用,当 who 传入 0 时,表示操作当前的进程、进程组或用户。

  • prio:指定新的 nice 值。

getpriority 和 setpriority 函数的底层实现原理如图3所示。

6张图搞懂 Linux 进程调度系统 图4

图3 设置普通进程优先级

用户程序修改nice值时,内核会通过 NICE_TO_PRIO(120 + nice)将 nice 值转换为静态优先级存储在 task_struct 结构 static_prio 字段。用户程序获取 nice 值时,会通过 PRIO_TO_NICE (static_prio - 120) 将 static_prio 静态优先级转换成 nice 值并返回给用户程序。

6.调度实体

调度实体是可以被调度器调度的最小单位,调度对象(用户进程、用户线程、内核线程、cgroup调度组)通过调度实体加入 CPU 运行队列,调度实体中包含了进程调度相关的必要信息。

Linux 按调度类划分三大调度实体:

  • CFS 调度实体(struct sched_entity):归属完全公平调度类,对应 SCHED_NORMAL、SCHED_BATCH 调度策略,用于系统普通进程与后台任务。

  • RT 调度实体( struct sched_rt_entity):归属实时调度类,对应 SCHED_FIFO、SCHED_RR 调度策略,用于低延迟实时任务。

  • DL 调度实体(struct sched_dl_entity):归属截止期限调度类,对应 SCHED_DEADLINE 调度策略,用于时延要求高的硬实时任务。

task_struct 结构体内部,同时包含三类调度实体,之所以同时要包含调度实体,是进程需要通过这三个调度实体选择性加入 CPU 运行队列三大子队列:

struct task_struct {
struct sched_entity   se;
/* CFS 调度实体 */
struct sched_rt_entity rt;
/* RT 实时调度实体 */
struct sched_dl_entity dl;
/* DL 截止期限调度实体 */
    ......
};

7.CPU运行队列

CPU 运行队列,内核用结构体 struct rq 表示,每一个 CPU 核心单独拥有一个独立运行队列(per-CPU 变量),是该 CPU 上所有就绪任务的统一管理容器。只有处于就绪、等待 CPU任务才会存放在对应 CPU 的运行队列中。

CPU 运行队列包含三大子队列,优先级从高到低:

  • DL 截止期限队列(struct dl_rq):截止期限进程专用,红黑树结构,按 deadline 排序。

  • RT 实时队列(struct rt_rq):实时进程专用,优先级数组 + 双向链表结构,按动态优先级排序。

  • CFS 完全公平队列(struct cfs_rq):普通进程专用,红黑树结构,按 vruntime 虚拟运行时间,Linux 6.6+ 改用虚拟截止时间 vdeadline 排序。

8.调度时机

调度时机是内核决定是否进行进程/线程上下文切换的特定触发点或检查时刻。调度时机分为两大类:

  • 主动调度:进程主动放弃 CPU,直接调用 schedule(),无标志位延迟,立刻切换。

  • 被动调度:内核发现有更适合运行的任务,先打标记 TIF_NEED_RESCHED,不会立刻切换,仅走到安全检查点,才执行 schedule()。

主动调度和被动调度对比见表2。

6张图搞懂 Linux 进程调度系统 图5

表2 主动调度和被动调度对比

8.1 sleep 主动调度

用户程序调用 sleep 系列函数(sleep、usleep、nanosleep等函数)后,进程会发生一次主动调用,如图4所示。

6张图搞懂 Linux 进程调度系统 图6

图4 sleep 主动调度

用户程序调用sleep函数后,内核依次会调用:clock_nanosleep()->hrtimer_nanosleep()->do_nanosleep(),do_nanosleep 函数会将进程状态设置为 TASK_INTERRUPTIBLE(睡眠态),同时会启动一个高精度定时器由于超时后唤醒进程,最后会调用:schedule()->__schedule_loop()->__schedule()完成进程调度。__schedule函数会将当前进程从运行队列出队,然后从运行队列中挑选下一个最合适的进程运行,最后执行 CPU 上下文切换完成进程调度。

8.2 sched_yield 主动调度

用户程序调用 sched_yield 函数后,进程同样会发生一次主动调度,和sleep 主动调度不同的是,sched_yield 主动调度并不会让进程休眠,具体情况如图5所示。

6张图搞懂 Linux 进程调度系统 图7

图5 sched_yield 主动调度

用户程序调用 sched_yield 函数,内核会调用:do_sched_yield()->yield_task(),yield_task 函数内部会调用调度类成员函数sched_class->yield_task(),如完全公平调度的 yield_task_fair 函数,该函数会更新任务虚拟运行时间和虚拟截止时间。do_sched_yield 函数内部同时也会调用schedule 函数,schedule函数的实现逻辑前面已经介绍过,只不过该场景__schedule 函数不会将当前进程出队,而是将当前进程从运行队列对头移动至队尾。

8.3 时间片耗完被动调度

前面我们介绍了两种进程主动调度的场景,本节我们来介绍一种常见的进程被动调度的场景:时间片耗完。如图6所示。

6张图搞懂 Linux 进程调度系统 图8

图6 时间片耗完被动调度

每个 CPU 都维护了一个高精度定制器,定时器会周期性的产生硬件时钟中断,中断函数依次会调用:tick_handle_periodic()->tick_periodic()->update_process_times()->sched_tick()->task_tick(),task_tick 函数会调用调度类成员函数 sched_class->task_tick 函数,如完全公平调度 task_tick_fair 函数,该函数会更新任务虚拟运行时间和虚拟截止时间,如果正在运行的进程时间片耗完,该函数会设置该进程的 TIF_NEED_RESCHED 标志。

中断退出时,内核会检测进程的 TIF_NEED_RESCHED 标志,如果TIF_NEED_RESCHED 标志被设置,内核会调用 schedule 函数完成进程调度。

← 返回文章列表