
大家好这里是物联网心球。本期文章我们来深入学习 Linux 进程调度系统。1.进程调度是什么进程调度是内核按照既定规则从就绪队列里挑选进程、分配 CPU 使用权并完成进程切换的整套机制。进程调度的目标公平性让每个进程都能获得合理的 CPU 时间避免某个进程独占资源。效率最大化 CPU 利用率减少空闲等待。响应性让交互式程序如浏览器、终端能快速响应用户操作。实时性保证关键任务如工业控制、音视频处理在严格时限内完成。2.进程调度系统如图1所示进程调度系统核心概念包括调度策略、调度类进程、优先级、调度实体、CPU运行队列、调度时机。图1 进程调度系统3.调度策略调度策略是操作系统为进程分配 CPU 资源、决定执行顺序与切换规则所采用的算法和规则。调度策略分类SCHED_NORMAL0完全公平调度Linux 默认调度策略调度器始终选择 vruntime 最小的进程运行。SCHED_FIFO1实时先进先出无时间片一旦获得 CPU 一直运行除非被更高优先级实时任务抢占。SCHED_RR2实时时间片轮询与 SCHED_FIFO 唯一的区别是同优先级进程之间增加了时间片轮转。SCHED_BATCH3批量处理调度与SCHED_NORMAL区别是进程唤醒后不会主动抢占当前正在运行的前台进程。SCHED_IDLE5空闲调度优先级最低仅系统无其他任务时才运行。SCHED_DEADLINE6 截止期限调度优先级最高基于 EDF最早截止时间优先 算法为任务设置运行周期、运行时长、截止时间内核保证任务在截止时间前完成。SCHED_EXT7可扩展调度器通过 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) (......); /* 主动让出CPUsched_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_classDEADLINE 硬实时调度类优先级最高用于工业控制、自动驾驶、低延时音视频按截止时间调度。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_prionormal_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并设置 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所示。图3 设置普通进程优先级用户程序修改nice值时内核会通过 NICE_TO_PRIO120 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 运行队列三大子队列struct task_struct { struct sched_entity se; /* CFS 调度实体 */ struct sched_rt_entity rt; /* RT 实时调度实体 */ struct sched_dl_entity dl; /* 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.调度时机调度时机是内核决定是否进行进程/线程上下文切换的特定触发点或检查时刻。调度时机分为两大类主动调度进程主动放弃 CPU直接调用 schedule()无标志位延迟立刻切换。被动调度内核发现有更适合运行的任务先打标记 TIF_NEED_RESCHED不会立刻切换仅走到安全检查点才执行 schedule()。主动调度和被动调度对比见表2。表2 主动调度和被动调度对比7.1 sleep 主动调度用户程序调用 sleep 系列函数sleep、usleep、nanosleep等函数后进程会发生一次主动调用如图4所示。图4 sleep 主动调度用户程序调用sleep函数后内核依次会调用clock_nanosleep()-hrtimer_nanosleep()-do_nanosleep()do_nanosleep 函数会将进程状态设置为 TASK_INTERRUPTIBLE睡眠态同时会启动一个高精度定时器由于超时后唤醒进程最后会调用schedule()-__schedule_loop()-__schedule()完成进程调度。__schedule函数会将当前进程从运行队列出队然后从运行队列中挑选下一个最合适的进程运行最后执行 CPU 上下文切换完成进程调度。7.2 sched_yield 主动调度用户程序调用 sched_yield 函数后进程同样会发生一次主动调度和sleep 主动调度不同的是sched_yield 主动调度并不会让进程休眠具体情况如图5所示。图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 函数不会将当前进程出队而是将当前进程从运行队列对头移动至队尾。7.3 时间片耗完被动调度前面我们介绍了两种进程主动调度的场景本节我们来介绍一种常见的进程被动调度的场景时间片耗完。如图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网络编程”购买。