TaterLi 个人博客

[内核学习]调度器基本结构

进程一多,CPU 时间怎么分就成了核心问题.Linux 调度器这些年从 O(n) 到 O(1) 到 CFS 再到 EEVDF,名字换了几茬,骨架倒一直稳:调度类分层加每 CPU 运行队列.这篇把骨架看清楚 — 类怎么排,策略怎么落到类,优先级到底有几个体系 — 下一篇讲公平类内部的算法.

这篇回答几件事:

先立骨架.内核把"怎么挑下一个任务"分成五个调度类,按优先顺序串成链,kernel/sched/sched.h 里声明一排:

stop_sched_class  >  dl_sched_class  >  rt_sched_class  >  fair_sched_class  >  idle_sched_class

CPU 要挑下一个任务时,for_class_range 从最高的 stop 类往下挨个问"你有人吗",第一个有人 的类说了算.stop 类是给 CPU 热插拔和虚拟化迁移用的"至高特权",平时永远空着;dl 是 SCHED_DEADLINE,基于 EDF 的硬期限调度;rt 是实时,FIFO 和 RR 两种策略;fair 是绝大多数进程的家,EEVDF 就住在这(板上这版内核没编 sched_ext,那是个可插拔 BPF 调度类的实验设施,编了的话它会插在 fair 和 idle 之间);idle 是队列真空时的兜底,进低功耗等中断.这个结构意味着一个铁律:dl 有任务,rt 全体靠边;rt 有任务,fair 再急也没用 — 调度类的隔离是绝对的,后面实时篇实测这条铁律的后果.

策略到类的映射:SCHED_OTHER(又名 NORMAL)和 SCHED_BATCH 进 fair — BATCH 是"批处理口味"的普通任务,唤醒少容忍延迟高,同一类里换个性子;SCHED_IDLE 比 fair 更没地位,系统闲得发慌才跑;SCHED_FIFO/SCHED_RR 进 rt;SCHED_DEADLINE 进 dl.实测自己这个 shell:

chrt -p $$:  SCHED_OTHER, priority 0

priority 0 是实时优先级位,普通任务这里恒 0 — 引出两套优先级体系的问题.rt 任务用 1-99 的实时优先级,数字大者地位高;fair 任务用 nice,-20 到 19,数字小者地位高 — 方向还相反,新手第一坑.两套体系在 /proc/PID/stat 的第 18 字段汇合,但汇合的方式很迷惑:实测 nice 0 的任务显示 20,renice +5 之后变 25 — 对普通任务这个字段是 nice 加 20;而 rt 任务显示的是实时优先级取负,FIFO 99 的任务显示 -99.一个字段两种含义,ps 输出里再翻译一遍,层层编码,别背口诀,记得"看数字先问是哪套体系".

每 CPU 一个 struct rq,这是调度器的中央账房.rt 类在 rq 里挂优先级位图加队列,fair 类挂红黑树(EEVDF 后是按 deadline 排),每个类一个子队列,嵌入在同一个 rq 里.这个账房对外开几个窗口./proc/PID/sched 是单个任务的账本,SCHED_DEBUG 开着就能读,systemd 的第一行长这样:

systemd (1, #threads: 1)
-------------------------------------------------------------------

下面跟着它的调度实体统计 — vruntime,等待时间,迁移次数,EEVDF 的 lag 和 deadline 全在里面,上一篇讲公平时已经用过它,这里记住位置:这是观察内核怎么"看"一个任务的原始视角./sys/kernel/debug/sched/features 是特性开关面板,板上读出来一排:

PLACE_LAG PLACE_DEADLINE_INITIAL PLACE_REL_DEADLINE RUN_TO_PARITY
PREEMPT_SHORT NO_NEXT_BUDDY PICK_BUDDY CACHE_HOT_BUDDY

全是大写命令风格的位开关,echo 新值进去即时生效 — 这张面板就是 EEVDF 各种行为微调的总闸,实验调参不动代码,先来这儿.想看每个 CPU 的队列级统计,内核有 /proc/schedstat,但板上这版没编 CONFIG_SCHEDSTATS,如实说,读不到就是读不到 — 又是"文档说的和配置编的要对上"的例子.

调度时钟最后交代一句:内核给调度器的时间不是墙上时钟,是 sched_clock 到 clock_task 的单调递增纳秒计数,ps 里的 TIME, /proc/stat 的 utime,全都源于它.虚拟化或省电导致时钟源跳变时,调度器的账可能瞬间膨胀,老内核的 vruntime 大跳变问题就出在这条线上,新内核有保护,但读账本时知道时间的来源,异常数据才有解释方向.

最后留几个容易忽略的细节:

退出移动版