单核再忙,同一时刻也只有一个任务在跑.所谓"多任务",是调度器在毫秒尺度上不停换人:保存旧任务的现场,恢复新任务的现场,放它上CPU.这篇把这个"换人"动作拆开看:一次切换到底贵多少(实测),切换点在哪发生(两种,计数器分开记),以及最底层的 cpu_switch_to 那二十条指令在换什么.
这篇回答几件事:
- schedule() 到新任务跑起来,中间发生什么
- 一次上下文切换的实测成本,进程和线程差多少
- 自愿切换和非自愿切换的区别,怎么观察
- 1.1 篇那个 sp_el0 存 current 的技巧,在切换时怎么兑现
先测成本,管道 ping-pong 法:两个执行流隔着一对管道互相发一个字节,A 写 B 读 B 写 A 读,一轮往返等于两次切换,跑两万轮取平均,计时用 1.1 篇认过的 cntvct.钉在 cpu4(A72)上,线程对和进程对各测一遍:
线程对: 3501 ns/切换 (同进程两线程, 不换 mm)
进程对: 4047 ns/切换 (两进程, 换 mm, 贵约 15%)
先把丑话说在前面:这个数是"两次系统调用加一次切换"的打包价,管道的 read/write 本身就要进出内核各一回,纯切换的裸价要更小.但有意义的部分是相对差:进程对每次多换一套地址空间,activate_mm 换 TTBR0,顺带把旧进程的 ASID 相关 TLB 作废,这 15% 就是地址空间隔离的过路费.这也是各类高性能服务(worker 池,数据库后台线程)偏爱线程的量化注脚 — 隔离该花的花,能省的省.
切换发生的位置有两种,内核分开记账./proc/PID/status 里两行,实测两个极端场景.一个 CPU 密集任务和别的任务抢一个核,两秒钟:
voluntary_ctxt_switches: 1 / nonvoluntary: 245
它从来不想睡,每次下CPU都是被抢占的 — 非自愿.反过来一个反复 sleep 的循环:
voluntary_ctxt_switches: 281 / nonvoluntary: 4
几乎全是主动睡 — 自愿.这对计数器是免费的性能体检表:延迟敏感的任务非自愿数高,说明它在被频繁抢占,可以考虑绑核或提优先级;自愿数高而吞吐低,说明时间都花在等待上,该查它等的资源.系统级汇总在 /proc/stat 的 ctxt 字段,那两个数的总和再乘个放大系数就是系统总切换量.
内核侧流程,schedule() 一路向下:先关抢占拿住 rq 锁,挑下一个任务(pick_next_task,按上一节的调度类顺序问),然后 context_switch.它干三件事:如果新旧任务地址空间不同,切换页表基址;调用 switch_to 宏,传 prev 和 next;还有一个形似玄学的第三参数 — switch_to(prev, next, prev) 最后那个 prev 是输出,因为切换回来时栈上世界已变,调用者需要内核帮忙把"上一个任务"的指针递回来,这是 C 语言的函数调用约定和任务切换语义打架的地方,内核用这个土办法和解.
最底层在 arch/arm64 的 cpu_switch_to,这段汇编值得盯着看:把 x19 到 x28,fp,sp 存进旧任务的 task_struct 里预留的 cpu_context 结构 — 注意只存这十几个,因为 x0-x18 属于调用者保存寄存器,切换发生前它们早被 C 代码的红酒杯规则(scheduler 调用约定)处理完了;然后 sp 指向新任务的 cpu_context,同样一组寄存器弹出来,ret 走人.妙处在最后一步顺手把 sp_el0 换成新任务的 task_struct 指针 — 1.1 篇说过内核拿 SP_EL0 白嫖存 current,兑换的时刻就在这:current 不是个变量,是每次切换都要搬一次的行李.整段代码不到三十条指令,换的是 CPU 的全部"人格":寄存器,栈,地址空间,和身份.
最后留几个容易忽略的细节:
- 切换成本大头不是那三十条汇编,是缓存:新任务的栈和数据都不在 cache 里,冷启动的 memory stall 比切换本身贵好几倍;优化切换多的系统,常赢在数据布局而不是内核路径.
- PREEMPT_VOLUNTARY 的板上内核,用户态抢占点在系统调用返回和中断返回,内核态只有显式让位点;PREEMPT_FULL 内核任意可抢占,延迟更平滑,吞吐略让位 — 发行版桌面内核基本都选了后者.
- voluntary/nonvoluntary 的语义边界:等锁睡着算自愿(might_sleep 的让位),哪怕你并不情愿 — "自愿"说的是代码路径,不是心情.
- 线程切换省 mm 但省不了调度实体那一套账(vruntime,lag 的更新),EEVDF 的簿记每次照做,所以线程切换不是免费的,只是免了最贵的一项.
- cpu_context 挑 x19-x28 保存是 AAPCS 的 callee-saved 集合,跨到其它架构这个名单会变,但"只存必须存的"哲学一致.
- perf sched 和 tracepoint sched_switch 是切换行为的时间线视图,2.3 篇抓过的那几行就是;配合本篇的成本数,可以估出"系统每秒花在换人上的钱".
- pidstat -wt 一秒一报每任务切换数,把体检表做成了连续监测,排查"谁在抖"先开它.