本期文章我们来深入学习 Linux 进程调度系统。 进程调度是内核按照既定规则,从就绪队列里挑选进程、分配 CPU 使用权,并完成进程切换的整套机制。
进程调度的目标:
公平性:让每个进程都能获得合理的 CPU 时间,避免某个进程独占资源。
效率:最大化 CPU 利用率,减少空闲等待。
响应性:让交互式程序(如浏览器、终端)能快速响应用户操作。
实时性:保证关键任务(如工业控制、音视频处理)在严格时限内完成。
2.进程调度系统
如图1所示,进程调度系统核心概念包括:调度策略、调度类进程、优先级、调度实体、CPU运行队列、调度时机。
图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 定义如下:
structsched_class {void (*enqueue_task) (......); /* 将进程加入CPU运行队列 */bool (*dequeue_task) (......); /* 将进程移出CPU运行队列 */void (*yield_task) (......); /* 主动让出CPU:sched_yield() 时调用 */structtask_struct *(*pick_task)(......); /* 从本调度类中选出一个最应该运行的进程 */structtask_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 结构定义了四个优先级字段:
structtask_struct {int prio; /* 动态优先级 */int static_prio; /* 静态优先级 */int normal_prio; /* 理论标准优先级 */unsignedint 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所示。
图2 进程优先级
我们把进程优先级总结为一张表(见表1),方便查看:
表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,并设置 errnointgetpriority(int which, int who);// 成功:返回 0;失败:返回 -1,并设置 errno。 intsetpriority(int which, int who, int prio);
参数说明:
getpriority 和 setpriority 函数的底层实现原理如图3所示。

图3 设置普通进程优先级
用户程序修改nice值时,内核会通过 NICE_TO_PRIO(120 + nice)将 nice 值转换为静态优先级存储在 task_struct 结构 static_prio 字段。用户程序获取 nice 值时,会通过 PRIO_TO_NICE (static_prio - 120) 将 static_prio 静态优先级转换成 nice 值并返回给用户程序。
5.调度实体
调度实体是可以被调度器调度的最小单位,调度对象(用户进程、用户线程、内核线程、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 运行队列三大子队列:
structtask_struct {structsched_entityse;/* CFS 调度实体 */structsched_rt_entityrt;/* RT 实时调度实体 */structsched_dl_entitydl;/* DL 截止期限调度实体 */ ......};
6.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 排序。
7.调度时机
调度时机是内核决定是否进行进程/线程上下文切换的特定触发点或检查时刻。调度时机分为两大类:
主动调度和被动调度对比见表2。
7.1 sleep 主动调度
用户程序调用 sleep 系列函数(sleep、usleep、nanosleep等函数)后,进程会发生一次主动调用,如图4所示。
用户程序调用sleep函数后,内核依次会调用:clock_nanosleep()->hrtimer_nanosleep()->do_nanosleep(),do_nanosleep 函数会将进程状态设置为 TASK_INTERRUPTIBLE(睡眠态),同时会启动一个高精度定时器由于超时后唤醒进程,最后会调用:schedule()->__schedule_loop()->__schedule()完成进程调度。__schedule函数会将当前进程从运行队列出队,然后从运行队列中挑选下一个最合适的进程运行,最后执行 CPU 上下文切换完成进程调度。 用户程序调用 sched_yield 函数后,进程同样会发生一次主动调度,和sleep 主动调度不同的是,sched_yield 主动调度并不会让进程休眠,具体情况如图5所示。 用户程序调用 sched_yield 函数,内核会调用:do_sched_yield()->yield_task(),yield_task 函数内部会调用调度类成员函数sched_class->yield_task(),如完全公平调度的 yield_task_fair 函数,该函数会更新任务虚拟运行时间和虚拟截止时间。do_sched_yield 函数内部同时也会调用schedule 函数,schedule函数的实现逻辑前面已经介绍过,只不过该场景__schedule 函数不会将当前进程出队,而是将当前进程从运行队列对头移动至队尾。 前面我们介绍了两种进程主动调度的场景,本节我们来介绍一种常见的进程被动调度的场景:时间片耗完。如图6所示。图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 函数完成进程调度。
最后:
我的图书《图解Linux网络编程》发布了,我对Linux网络编程的应用开发技术以及内核源码进行了深入的研究,并以图解方式创作了《图解Linux网络编程》这本书,如果你想系统性地学习Linux网络编程,从底层原理到上层应用彻底通关Linux网络编程,欢迎入手我的这本书。
另外,本期文章内容来自于我的【硬核Linux】视频专栏课件,感兴趣的小伙伴请B站搜用户:物联网心球,找到投稿->充电专属。