内核调度 · 何时切换——抢占时机、负载均衡与观测调节
调度不是持续发生的,而是在几个明确的时机点触发"重新挑一个 task"(可能换人,也可能还是原来那个)。本篇讲清切换的四个触发时机、
need_resched标志怎么流转、上下文切换到底贵在哪;再往上到多核层面看负载均衡怎么把任务搬来搬去、绑核为什么能保缓存;最后给一套观测和调节的命令。
相关:"有几种队列"见 scheduling-queues;调度类分层与 CFS 选人见 scheduling-classes;切换的完整实现(switch_mm/switch_to、切线程 vs 切进程)见 context-switch;切换的开销量化见 context-switch-cost;绑核见 thread-affinity。
破题:切换不是"随时可能",而是"几个明确的点"
很多人以为调度器像个裁判一样一直盯着——其实不是。内核只在少数时机才问"要不要换人",平时当前 task 就一路跑下去。这套设计有个著名的名字叫 need_resched(需要重新调度)标志:内核只负责置位这个标志,真正的切换推迟到安全/合适的点才执行。好处很明显——不是每个 tick 都强切,也不是每次中断返回都切,减少无谓的上下文切换开销。
一、四个触发时机
- 主动让出:task 阻塞(等锁/IO/
sleep)、主动sched_yield()、退出——它从运行队列移出,必须选别人。 - 时间片/周期到:调度时钟中断(scheduler tick,默认 100~1000Hz) 定期检查当前 task 是否跑够了它该跑的份额,够了就置"需要重新调度"标志。
- 唤醒抢占:一个 task 被唤醒(如高优先级 rt 任务、或 vruntime 更小的任务变为可运行),若它比当前 task 更该跑,立刻抢占当前 task。
- 返回用户态/中断返回时:内核检查
need_resched标志,若置位就调用schedule()真正切换。
其中"唤醒抢占"最有意思:它其实是在别人的上下文里把 need_resched 置上——比如 CPU A 上有个任务唤醒了 CPU B 上睡着的任务,A 会向 B 发一个 rescheduling IPI,B 收到后在返回路径上发现 need_resched 置位,于是切换。这也是 mpstat 里 RES/s(重新调度中断)的来源。
二、从触发到换人:need_resched 的流转

注意流程里的关键判断:即使 need_resched 置位,pick_next_task() 挑出来的可能还是当前 task(比如它仍是 vruntime 最小者)——那就什么都不用做,继续跑。切不切换,最终取决于"有没有更该跑的人"。
上下文切换的代价
保存/恢复寄存器、切页表(跨进程要换 CR3 → TLB 多半失效,见 tlb)、缓存变冷。切换太频繁本身就是开销——这正是绑核(减少迁移)、减少锁争用的意义(见 thread-affinity)。切换的完整实现(switch_mm/switch_to、切线程 vs 切进程)见 context-switch,量化数字见 context-switch-cost。
抢占内核
现代内核是可抢占的(CONFIG_PREEMPT),即使在内核态执行,高优先级任务也能抢占(有临界区保护除外)。老内核只有用户态可抢占、内核态跑完一段才让出,实时性差很多。可抢占内核意味着"中断/系统调用返回路径"这个切换点更频繁地被使用——高优先级任务不用等内核慢慢退出,快到临界区边界就能被切进来。
三、多核:负载均衡与队列间迁移
每核一个运行队列,自然带来"各核忙闲不均"的问题。调度器周期性做负载均衡(load balancing):把 task 从繁忙核的运行队列迁移到空闲核的运行队列。
- 迁移触发:周期性 tick 里检查、或某核变空闲时主动"拉"任务过来。按调度域(scheduling domain,反映 CPU 拓扑:同 SMT 超线程 < 同 L3 < 同 NUMA 节点 < 跨节点)由近到远地找任务迁移,优先在"便宜"的层级内均衡——跨 NUMA 节点迁移最贵(内存访问延迟暴涨),能不动就不动。
- 迁移的代价:task 被迁到新核,养热的 L1/L2/TLB 全丢在旧核 → 冷缓存 → 尾延迟尖刺。这正是 thread-affinity 讲的:绑核 = 禁止迁移、保住缓存热度,代价是放弃自动均衡。
- 亲和性约束迁移:
task->cpus_ptr(亲和性掩码)限定 task 只能进哪些核的运行队列;isolcpus把某些核排除在负载均衡之外,留给关键线程独占。
观测迁移:
perf stat看migrations、pidstat -w看上下文切换/迁移(见 pidstat)。
三·五、调度延迟(wakeup latency)——"被唤醒"到"真正跑起来"要等多久
上面讲的是"切换什么时候发生",实际排查里更常问的是另一个问题:事件已经来了(比如网络包到达、锁释放),唤醒的线程要多久才能真正执行? 这个时间叫唤醒/调度延迟(wakeup latency),是实时性和低延迟场景的核心指标。
事件到达(包进内存/锁释放)
→ 内核唤醒线程: 置 RUNNABLE、塞回目标核运行队列 (微秒级)
→ 如果目标核正在跑别的任务:
├─ 高优先级 → 发送 RES IPI 触发抢占 (几微秒)
└─ 同优先级 → 只能等目标核自己到抢占点 (几十到几百微秒)
→ 目标核 tick/中断返回时发现 need_resched → 切换 (几微秒)
→ 线程真正开始执行: 冷缓存 → L1/L2/TLB 全要重新热身典型数量级:空闲系统唤醒延迟约 5~30µs;高负载或错误绑核(线程被挤到忙核)可到 数百 µs ~ 毫秒。对高频交易、音视频、工业控制这类场景,这比"切换本身"的几微秒开销更值得优化。
怎么测:
# cyclictest: 定时任务实际唤醒延迟的分布(实时场景黄金工具)
sudo cyclictest -m -n -p 99 -i 1000 # 每 1ms 唤醒一次,统计 min/avg/max
# 用 bpftrace 抓 wakeup 到 schedule 的时间(不需要实时优先级)
sudo bpftrace -e 'kprobe:try_to_wake_up { @s[tid]=nsecs; }
kprobe:finish_task_switch /@s[pid]/ { @wakeup_lat_us=hist((nsecs-@s[pid])/1000); @s[pid]=0; }'
# perf 看上下文切换的成因分布
perf sched record -a sleep 5 && perf sched latency降低调度延迟的手段(详见 real-time-scheduling):实时调度类(SCHED_FIFO,杜绝排队等待)、绑核避免跨核唤醒(sched_setaffinity)、isolcpus/nohz_full 把干扰核隔离开、减少中断和锁争抢。但记住:实时优先级是把双刃剑——一个 SCHED_FIFO 死循环能钉死整个核,测试环境里务必设个 timeout 再上。
四、观测与调节
# 看/改一个进程的调度策略与优先级
chrt -p <pid> # 查看:显示 SCHED_OTHER/FIFO/RR/... 及优先级
chrt -f -p 50 <pid> # 改成 SCHED_FIFO 优先级 50(实时,需权限)
nice -n 10 ./app # 以 nice=10(更谦让)启动
renice -n -5 -p <pid> # 调整已运行进程的 nice(负=更优先,需权限)
# 看调度相关的运行时状态
cat /proc/<pid>/sched # 该任务的 vruntime、被调度次数、等待时间等(CFS 细节)
cat /proc/<pid>/status # State、Cpus_allowed(亲和性掩码)、voluntary/nonvoluntary_ctxt_switches
cat /proc/<pid>/stat # priority、nice、policy、utime/stime、处理器号(第39字段=上次跑在哪个核)
cat /proc/schedstat # 每 CPU 的调度统计
pidstat -w 1 # 每秒的自愿/非自愿上下文切换次数(非自愿高=被频繁抢占)
vmstat 1 # 'cs' 列=全系统每秒上下文切换;'r' 列=运行队列里可运行任务数三个判读要点:
vmstat的r列就是"所有运行队列里可运行 task 总数"——持续 > CPU 核数说明 CPU 过载、任务在排队(见 vmstat)。比如 4 核机器r长期在 8~12,就是 2~3 倍的排队压力。- 非自愿上下文切换(nonvoluntary)高:任务频繁被抢占/时间片耗尽,可能 CPU 争抢激烈。结合
pidstat -w能看出具体是哪些任务在被来回切。 - 实时优先级要慎用:
SCHED_FIFO任务不主动让出会饿死同核的普通任务,甚至挂起系统(有sched_rt_runtime_us限流兜底)。改之前想清楚:这个任务真的需要实时吗?万一它写了个死循环,系统可能连ssh都切不进来。
五、一句话总结
切换只在四个时机发生:主动让出、tick 时间片到、唤醒抢占(跨核时靠 RES IPI 通知)、返回用户态/中断返回——统一通过 need_resched 标志推迟到安全点执行。上下文切换的代价在保存现场 + 换页表致 TLB 失效 + 缓存变冷,所以切换太频繁本身就是开销。多核间调度器周期性做负载均衡,按调度域由近到远迁移任务,迁移丢缓存是尾延迟尖刺的来源,绑核就是禁止迁移保热度。观测用 vmstat 的 r/cs、pidstat -w、chrt、/proc/<pid>/sched,调节用 nice/renice/chrt——实时优先级慎用,会饿死普通任务。