Linux进程调度如何决定下一个运行的进程:task_struct与sched_entity内部机制
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
在终端里敲下一条sleep 300,按回车,这个进程就不再出现在top的活跃列表里,但杀进程时它依然"活着"。它没有消失,只是切换了状态,而内核每隔一个时间片都要重新回答同一个问题:下一个 CPU 周期给谁?这个问题的答案就存在两个结构体里:记录进程全部信息的task_struct,以及调度器真正拿来比较的sched_entity(调度实体,可理解为 task_struct 里那张专门给调度器看的"评分卡")。下面把这张评分卡的字段、排序规则和唤醒到重新上 CPU 的完整路径讲清楚。
机制在代码中的位置:三个文件看完全貌
调度机制横跨三处代码:状态与结构体定义在 include/linux/sched.h,CFS 公平调度器的算法主体在 kernel/sched/fair.c,用户可见的统计输出在 kernel/sched/debug.c。
| 字段 / 常量 | 作用 | 源码位置 |
|---|---|---|
task_struct.__state | 32 位位图,标记进程当前处于运行还是哪一类睡眠 | include/linux/sched.h#L843 |
TASK_RUNNING等状态位 | 各状态对应的位掩码(TASK_RUNNING为0x0,"可运行"即无标志位) | include/linux/sched.h#L106-L127 |
task_struct.prio/static_prio/normal_prio | 静态优先级、nice 换算的优先级、含动态调整后的最终优先级 | include/linux/sched.h#L884-L886 |
task_struct.se | 普通进程(fair 类)的调度实体,内嵌在 task_struct 中 | include/linux/sched.h#L889 |
task_struct.rt/dl | 实时类(SCHED_FIFO/RR)与截止时间类(SCHED_DEADLINE)各自的调度实体 | include/linux/sched.h#L890-L891 |
sched_entity.load | 由 nice 值换算出的进程权重,权重越大分到的 CPU 时间越多 | include/linux/sched.h#L574 |
sched_entity.run_node | 红黑树节点,该进程在队列中的挂靠点 | include/linux/sched.h#L576 |
sched_entity.vruntime | 虚拟运行时间,排序与比较的核心 | include/linux/sched.h#L592 |
sched_entity.sum_exec_runtime | 该进程累计的真实 CPU 时间(纳秒),不受 nice 影响 | include/linux/sched.h#L590 |
sched_entity.avg | 负载平均值(PELT,指数加权滑动平均),供负载均衡与 util clamp 使用 | include/linux/sched.h#L618 |
sched_entity.slice/min_vruntime | 当前时间片长度;队列中虚拟时间的最小值缓存 | include/linux/sched.h#L597, L578 |
task_struct 中三种调度类实体并列存放,每个进程同时带着这三张"卡",但同一时刻只有一张卡对当前调度类生效:
struct task_struct { /* ... */ struct sched_entity se; /* fair 类进程使用的调度实体 */ struct sched_rt_entity rt; /* SCHED_FIFO / SCHED_RR 实时进程 */ struct sched_dl_entity dl; /* SCHED_DEADLINE 截止时间进程 */ };se内部与调度决策直接相关的部分(完整定义约 48 行,此处只摘核心):
struct sched_entity { struct load_weight load; /* nice 值换算出的权重 */ struct rb_node run_node; /* 红黑树节点 */ u64 vruntime; /* 虚拟运行时间,排序依据 */ u64 sum_exec_runtime; /* 真实累计执行时间 */ u64 vlag; /* 虚拟滞后的近似值,EEVDF 使用 */ u64 slice; /* 当前时间片 */ struct sched_avg avg; /* 负载平均值 */ };推演:一个进程从睡眠到再次上 CPU 的完整路径
以最典型的事件流走一遍:sleep 300被定时器唤醒之后,CFS 如何把它重新放上 CPU。
- 定时器到期,调度器把
task_struct.__state从TASK_INTERRUPTIBLE改回TASK_RUNNING,进程重新"可运行"。 - 进程经由
enqueue_task_fair()挂回该 CPU 的 CFS 运行队列,实体插入红黑树,位置由vruntime与队列最小值min_vruntime的差决定。 - 当前进程每运行一个时间片,
update_curr()把真实经过的时间除以权重load后累加进vruntime——权重小的进程虚拟时间涨得快,下次轮到它就更晚。 - 需要换人时,
pick_next_entity()取出虚拟期限(deadline,由 vruntime 加时间片推算)最早的进程。 - 调度器执行上下文切换:保存旧进程的寄存器与
pcpu上下文,恢复新进程;旧进程若被抢占,重新排队,回到第 2 步循环。
这里值得单独说明vruntime的"虚拟"二字:它不是墙上时钟时间,而是真实执行时间经过权重缩放后的量。nice 值越高的进程权重越小、vruntime涨得越快,于是它连续获得 CPU 的机会越少——公平性正是靠这一把"变速的尺子"实现的,而不是靠给不同进程分配不同长度的真实时间片。
验证:一条命令读出评分卡
不需要编译内核,/proc/<pid>/sched就是kernel/sched/debug.c直接导出的实体快照:
pid=$(pidof sleep) grep -E "se\.(vruntime|sum_exec_runtime|slice)|nr_involuntary|prio" /proc/$pid/sched典型输出及字段含义:
| 输出字段 | 对应实体字段 | 含义 |
|---|---|---|
se.vruntime | se.vruntime | 虚拟运行时间(秒.纳秒拆分显示),队列排序依据 |
se.sum_exec_runtime | se.sum_exec_runtime | 真实累计 CPU 时间,与 nice 无关 |
se.slice | se.slice | 内核当前分配给该进程的时间片长度 |
nr_involuntary_switches | p->nivcsw | 被内核强制切走的次数(非主动让出) |
prio | p->normal_prio | 最终生效的优先级,含动态调整 |
对比同一进程的两个数就能看懂权重换算:sum_exec_runtime是它实际消耗了多少 CPU,vruntime是被"除以权重"之后的账本——对高 nice 进程,vruntime会明显大于sum_exec_runtime。
当前内核的调度参数与旧资料里的说法不同,速查如下:
| 参数 | 查看方式 | 说明 |
|---|---|---|
base_slice_ns | cat /sys/kernel/debug/sched/base_slice_ns(需挂载 debugfs) | 时间片基准,由 CPU 频率推导而非固定 4 ms |
| 各类调度特性开关 | sysctl -a \| grep kernel.sched_ | 如sched_autogroup_enabled等,EEVDF 时代sched_latency_ns、sched_min_granularity_ns等旧参数已移除 |
| 每 CPU 队列统计 | cat /proc/sched_debug | 各 CPU 上min_vruntime、nr_running等队列状态 |
| 全局调度事件计数 | cat /proc/schedstat | 按 CPU 汇总的切换次数与运行时间 |
误区与边界
vruntime不是真实时间,两者不能相减比较。它是权重缩放后的虚拟量;想统计一个进程真正消耗了多少 CPU,看sum_exec_runtime或用pidstat -t -p,拿vruntime当秒数用会得到错误结论。- 红黑树排序的键已经不是
vruntime本身。自 EEVDF 取代 CFS 以来(设计见 Documentation/scheduler/sched-eevdf.rst),选择依据是deadline(vruntime 加上时间片推算出的虚拟截止时间),min_vruntime也改为缓存最小 deadline 的近似值。旧文档里"挑 vruntime 最小的进程"这一句在当下内核只算近似正确。 sched_entity只对 fair 类进程有效。用SCHED_FIFO提权的实时线程走rt实体、纯轮转链表,dl类进程走截止时间队列;它们运行时会直接抢占 CFS 队列,/proc/<pid>/sched里那几个se.*字段对实时进程基本无意义。判断方法:ps -o pid,cls,ni -p <pid>,看调度类是否为FIFO/RR/DL。
延伸
task_struct是进程的全部档案,sched_entity只是其中交给调度器的那张评分卡;看懂权重如何换算vruntime、deadline如何参与选择,就能解释绝大多数"我的进程为什么拿不到 CPU"的现象。继续深入可先读官方调度器文档,再对照 kernel/sched/fair.c 中enqueue_task_fair、update_curr、pick_next_entity三个函数的实现。
- Documentation/scheduler/sched-design-CFS.rst:CFS 原始设计文档
- Documentation/scheduler/sched-eevdf.rst:EEVDF 调度算法设计
- kernel/sched/debug.c:
/proc/<pid>/sched等接口的实现
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考