TaterLi 个人博客

[内核学习]实时调度

上一节说调度类绝对隔离:rt 有人,fair 靠边.这话对,但如果只有前半句,一个写坏的实时任务就能把整台机器锁死.这节在单核上把 FIFO 和 RR 的行为全部实测,看隔离的威力,再看内核给这威力套上的那道缰绳 — RT 带宽节流.还有我自己实验里翻的一场好戏,顺便看清 rt 类内部的抢占规则.

这篇回答几件事:

实验设置:全钉在 cpu1,一个普通任务和几个实时任务轮番上场,CPU 时间从 /proc/PID/stat 的 utime 差值读出来,毫秒级精度.第一幕,普通任务独占:

A) normal alone:              2010 ms / 2s

满血.第二幕,一个 SCHED_FIFO 优先级 50 的死循环挤进来:

B) normal + FIFO50:           normal=100 ms   fifo=1910 ms / 2s

普通任务从 2010 掉到 100 毫秒.100 恰好是 2 秒的 5% — 这个数字不是巧合,是内核的刻意设计,后面讲.第三幕,再来一个同优先级的 FIFO:

C) 第二个同优先级 FIFO:        老 FIFO 仍 1900 ms / 2s

新的几乎没得吃.FIFO 的语义到这里见底:先上场的同优先级任务不让位,它不睡不退,后来的就永远排队.rt 类内部的抢占规则是"只认更高的实时优先级",等优先级之间没有绅士风度 — 这和 fair 类"人人有份"的世界观完全相反.

第四幕本来想演 RR 轮转,先翻了个车:场景是"FIFO50 还在场,再加两个 RR50":

D) FIFO50 + 两个 RR50:        rr1=90 ms  rr2=90 ms / 2s

两个 RR 各只捡到 90 毫秒!清场重测:

D2) 干净环境两个 RR50:         rr1=1300 ms  rr2=730 ms / 2s

翻车本身就是答案:FIFO50 和 RR50 的"50"是同一个实时优先级,rt 类里等优先级不抢占,所以在跑的 FIFO 压着两个 RR,它们只能去分节流漏下来的残渣.清了 FIFO 之后,RR 的本性露出来 — 两个任务轮流坐庄,份额大致对半(1300/730 的不对称来自起跑时序和 shell 开销).RR 和 FIFO 的唯一区别就在这:同优先级之间,FIFO 是独木桥,RR 是轮盘,内核给 RR 每人一个 timeslice,到期换人,时间片默认 100 毫秒量级.fifo 和 rr 之外还有个 SCHED_DEADLINE,按"每个周期跑多久,最晚什么时候必须跑完"三元组承诺,EDF 最早截止期优先,活在 dl 类,音频工业控制那类硬需求才用,普通发行版内核默认放开但用的人少,这里点到为止.

第五幕,FIFO 30 对 FIFO 60:

E) FIFO30 vs FIFO60:          fifo30=290 ms / 2s

低优先级只吃到一点残渣.这里有个细节:FIFO30 理论上应该颗粒无收,那 290 毫秒哪来的?答案和 B 幕里普通任务的 100 毫秒同源 — RT 带宽节流,这篇最值得讲的设计.

问题先摆清楚:调度类绝对隔离意味着 rt 任务可以让 fair 任务颗粒无收.一个忘了 yield 的 FIFO 死循环,就能让 shell 冻住,watchdog 咬人,SSH 都进不去 — Linux 作为通用系统不能这么玩.解法是 rt 运行时配额,两个 sysctl:

/proc/sys/kernel/sched_rt_runtime_us = 950000
/proc/sys/kernel/sched_rt_period_us  = 1000000

每个 CPU,每一秒周期,所有 rt 任务加起来最多跑 0.95 秒;超了,这个 CPU 上的 rt 全体被节流挂起,直到下个周期.B 幕的 5%,E 幕的残渣,全是这条规则的余波.它保证了任何 rt 灾难下 fair 类至少有 5% 的窗口 — shell 能敲,能 kill,能自救.要跑到真正的 100%(工控场合),把 runtime 设成 -1 放开,同时你也就交出了那把安全锁.PREEMPT_RT 补丁集则更进一步,把内核里几乎所有自旋锁换成可睡眠锁,让实时任务连内核态的不可抢占区间都大幅缩短,那是另一个故事,主线内核近几年已逐步合入其基础部分.

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

退出移动版