进程一多,CPU 时间怎么分就成了核心问题.Linux 调度器这些年从 O(n) 到 O(1) 到 CFS 再到 EEVDF,名字换了几茬,骨架倒一直稳:调度类分层加每 CPU 运行队列.这篇把骨架看清楚 — 类怎么排,策略怎么落到类,优先级到底有几个体系 — 下一篇讲公平类内部的算法.
这篇回答几件事:
- 调度类怎么组织,选下一个任务的顺序是什么
- SCHED_OTHER,FIFO,RR,BATCH,IDL,EADLINE 这些策略分别进哪个类
- nice 到底是什么,和实时优先级什么关系
- 每个 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 大跳变问题就出在这条线上,新内核有保护,但读账本时知道时间的来源,异常数据才有解释方向.
最后留几个容易忽略的细节:
- nice 的名字来自"友好度":对别人友好就是对自己让利,nice +19 几乎让出一切,但不是 0 — CFS/EEVDF 里没有绝对饿死,权重最低也有份.
- rt_prio 和 nice 数值区间不重叠,task_struct 里一个 prio 字段装下两套体系:100 以下是 rt,100-139 是 fair(nice+120),内核内部看的从来是这一个数.
- BATCH 策略不会让任务变慢,它改变的是唤醒抢占的判定 — 少打扰别人也少被别人打扰,吞吐型负载(编译)值得试.
- idle 类的任务不是优先级低,是"电费优先级":CPU 有真活干它永远不跑,适合跑爬虫压测这种"跑不跑无所谓"的活.
- rq 里还有个 curr 指针永远指着当下任务,调度器自己挑人时先看它,算 preemption 用的 — 读调度代码先找到 curr,不迷路.
- /proc/sys/kernel/sched_* 是另一层旋钮(sched_rt_runtime_us 管实时带宽,下一节实测它),和 debugfs 的 features 是两家人,别混着改.
- sched_clock 在 ARM64 上通常落在 arch timer,24MHz 级,板上 1.1 篇读的 cntvct 就是它的底层.