深夜十一点,城市急诊室灯火通明,可值班医生只有一位。120 不断送来病人:有人心脏骤停,有人骨折流血,有人只是轻微擦伤。分诊护士给每个人挂上不同颜色的牌子——红牌危重,黄牌较重,绿牌轻症。医生每次只能救一个,先救谁?救到什么程度算“告一段落”?这背后的一套秩序,恰好就是操作系统的日常:CPU 是那位唯一的医生,进程和线程是候诊的病人,而内核里的调度策略就是急诊室的接诊规则。Linux 提供两套实时接诊规则:SCHED_FIFO 像“上了手术台就必须把手术做完”,SCHED_RR 像“门诊每人限时三分钟,到点换下一位”。再加上一条铁律——只要推进来一位牌子更红的病人,医生必须立刻放下手里的活冲过去抢救,这就是“优先级抢占”。这套秩序决定了手术机器人会不会卡顿、自动驾驶能不能在 100 毫秒内刹停、股票交易能不能抢在对手前面成交。下面,我们就用急诊室的故事,把这三件事从头讲透。
实时调度是音视频、自动驾驶、工业控制等高确定性系统的地基。理解 SCHED_FIFO/SCHED_RR 与抢占机制,是内核工程师和高性能应用开发者的看家本领。
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
实时调度概述
急诊室的病人并不都是“能等”的。心脏骤停的患者,晚一分钟处理就可能丧命;而一个脚踝扭伤的年轻人,在候诊室多坐半小时也没关系。操作系统里也有这样两类任务:一类是普通进程,晚几毫秒执行只是体验变差;另一类是实时任务,必须在规定时间内拿到结果,否则就是事故。判断一个任务是否“等不得”,看它有没有截止期限(deadline)就一目了然。
有截止期限不代表一定强求。现实中,“手术必须成功”是硬实时,错过期限等于失败;“外卖 30 分钟内送到”是软实时,超时几分钟还能接受,只是体验打折。Linux 的实时调度器并不区分这两者,它只负责一件事:让所有挂了红牌的任务,尽可能在期限内完成。
但医生只有一个,病人却很多。能不能安排得过来,有一个经典的利用率判据:
所以调度器的一切设计,都是在“医生不够用”这个前提下展开的。剩下的问题只有一个:怎样保证最等不及的病人先被照顾到?这就引出了急诊室的第一个工具——优先级牌子。
- C_i(治疗时长)第 i 个任务每次运行需要占用的 CPU 时间。比如心电监测程序每轮要算 10ms,记作 C_i。
- T_i(复发周期)第 i 个任务的释放周期,也就是病人每隔多久来一次。比如每 100ms 必须跑一轮,记作 T_i。
- C_i / T_i(占用率)单个任务占掉医生工作时间的比例,是一个介于 0 和 1 之间的小数。
- 求和 Σ把所有实时任务的占用率加起来,得到系统总负荷。
优先级机制
急诊室不会让所有病人排成一条长队。分诊护士会先给每个人挂上牌子:红牌是危重,立刻抢救;黄牌是较重,尽快处理;绿牌是轻症,排队等着。操作系统也一样,每个进程和线程都有一块“紧急牌”,名字叫优先级(priority)。调度器每次选人,先看牌子再看顺序。
在 Linux 中,实时调度的优先级是 1 到 99 之间的整数,数值越大,紧急程度越高,99 就是最高级别的红牌。这与普通进程的 nice 值正好相反——nice 的取值范围是 -20 到 19,数值越小优先级越高。两套规则方向不同,初学者最容易在这里栽跟头。
给病人挂红牌,在代码里是通过系统调用来完成的:
牌子挂对了,医生才能在关键时刻认出谁最该救。可是“先救谁”只是第一步,另一个问题是:救到什么程度才换人?这就轮到 SCHED_FIFO 出场了。
- pid(病人编号)指定要给哪位病人挂牌,也就是哪个进程或线程。
- SCHED_FIFO(接诊规则)选择用哪套调度策略,本文主角之一,后面会详细展开。
- sched_priority=80(红牌号码)在 1 到 99 之间选一个数,越大越紧急。
- 权限(挂牌资格)调用需要 root 或 CAP_SYS_NICE,否则会被内核拒绝。
FIFO运行策略
想象一位主刀医生:只要上了手术台,就必须把整台手术做完才下台。中途既不会因为“到点了”就换人,也不会因为来了个同级别的病人就停手。这种“一鼓作气”的风格,就是 SCHED_FIFO(First In First Out,先来先服务)的写照。
严格来说,FIFO 根本没有“时间片”这个概念。一个 FIFO 任务只要还能运行,就会一直霸占 CPU,直到发生以下三种情况之一:任务自己调用 sched_yield() 主动让出;任务阻塞,比如等待键盘输入或磁盘数据;来了一个更高优先级的任务,把它强行抢走。除此之外,没有任何机制会打断它。
用公式描述它“一口气能跑多久”:
FIFO 最适合那些“要么一次做完、要么出错”的紧要活儿。但凡事有两面:几个同样紧急的病人同时在场时,医生总抓着一个人不放,其他人会不会有意见?于是就有了第二套规则——SCHED_RR。
- T_run(实际连续运行时间)任务真正获得 CPU 的时间有多长。
- T_need(所需总时间)任务把活儿干完总共需要的执行时间。
- sched_yield(主动让出)任务自己举手说“医生,我暂时不看了”,把 CPU 让给下一位。
- 抢占(被强行打断)更高优先级任务就绪,医生的注意力被强制转移。
RR轮转策略
SCHED_RR(Round Robin,时间片轮转)像门诊叫号:每位病人限时 3 分钟,不管看完没看完,到点必须起身换下一位。所有拿着同色牌子的病人在一个圈里轮流坐庄,谁也别想独占医生。
RR 与 FIFO 的唯一区别,就是给同优先级任务加了一个时间片。Linux 中 SCHED_RR 的默认时间片是 100ms,代码里可以用 sched_rr_get_interval() 查到实际值。时间片用完而任务还没结束,它就被挪到同优先级队列的末尾,等下一圈。注意一个关键点:低优先级任务不会因此受益——RR 只在同一个优先级组内轮转,红牌病人的圈子里不会出现绿牌病人。
时间片模型用公式表达就是:
RR 的价值是“雨露均沾”:几个同等紧急的任务,谁也别想独占医生。但急诊室还有最后一条铁律没讲——如果抢救进行到一半,突然推进来一个牌子更红的病人呢?
- t_slice(时间片)每次轮到任务时,它最多能使用 CPU 的时长,Linux 默认 100ms。
- n(同组人数)同一优先级下处于就绪状态的任务数量。
- 轮转周期(一圈时长)n 个任务各拿一次时间片的总耗时。
- 查询接口用
sched_rr_get_interval()查看当前系统的实际时间片。
优先级抢占
急诊室的墙上贴着一条铁律:无论医生正在给谁看,只要推进来一位牌子更红的病人,医生必须立刻放下手里的一切冲过去。不许说“等我做完这台手术”,也不许说“等下一个整点”。这条铁律,在操作系统里就是“抢占”(preemption)。
抢占的条件只有一条:新就绪任务的优先级 > 当前运行任务的优先级。反过来也成立一条推论:相同优先级的任务之间互不抢占。FIFO 任务之间靠“自觉”——前面的做完或让出,后面的才能上;同组 RR 任务之间靠时间片来换手。谁也不会去抢平级同事的活儿。
抢占这个动作可以用逻辑式表示:
有了抢占铁律,“红牌优先”才真正落地。但这里还有一个隐藏问题:医生换人很快,可医生是怎么在几十个候诊病人里“一眼锁定”最红牌子的呢?答案就藏在急诊室后台的分诊台里。
- P_new(新病人牌号)新到达任务的优先级,相当于120刚送来的危重病人的红牌号。
- P_cur(当前病人牌号)正在运行任务的优先级。
- 保存现场(记录手术进度)把当前任务的寄存器、栈指针等状态存档,相当于记录“刀切到第几层”。
- 上下文切换(换人动作)从存档恢复新任务的现场继续运行,这是抢占的主要开销。
运行队列切换
急诊室后台有一张巨大的分诊台,上面按红、黄、绿分区陈列着所有等待的病人。操作系统里这张台子叫运行队列(runqueue),每个 CPU 一张,专门维护“就绪但还没轮上”的实时任务。台子的设计直接决定了叫号的速度。
在 Linux 中,实时运行队列由“优先级位图 + 链表数组”构成:每个优先级对应一条队列,位图的每一位标记该队列是否为空。要找到下一个该谁上场,只需从最高位往下扫,找到第一个“非空”的位。这个操作只需一条 CPU 指令,复杂度是 O(1),与任务总数无关——无论候诊室挤了 10 个人还是 1000 个人,找最紧急病人的时间都一样。
查找动作的公式化表示:
分诊台的效率保证了“叫号”几乎不浪费时间。但理论归理论,真实系统里怎么给进程挂红牌、怎么配置调度策略?下一个环节,我们进入实操。
- rt_rq(分诊台抽屉)实时运行队列数组,按下标 0~99 对应各个优先级。
- prio(抽屉编号)把优先级当作数组下标,同牌号的病人排进同一个抽屉。
- find_first_set_bit(从高位找起)从最高优先级位往下找第一个为 1 的位,就是最紧急的非空队列。
- O(1)(恒定时间)查找耗时与队列长度无关,始终是常数时间。
实践配置工具
挂红牌不是喊口号,得有趁手的工具。Linux 命令行里最常用的是 chrt 命令:chrt -f -p 80 1234 表示把 PID 为 1234 的进程设为 SCHED_FIFO、优先级 80;chrt -r -p 50 5678 则表示设为 SCHED_RR、优先级 50。一条命令,立竿见影。
在程序代码里设置实时属性,通常通过 pthread 线程属性完成。创建线程之前,先设置一个“挂号单模板”:模板上写好接诊规则和优先级,再把这套模板交给 pthread_create。设置动作发生在创建之前,创建出来的线程才是实时线程,这一点非常容易踩坑。
线程属性的设置过程:
工具和 API 是把调度理论搬进生产系统的“挂号窗口”。但窗口排对了,急诊室依然可能翻车——下一节我们看看最著名的几起“医疗事故”。
- attr(挂号单模板)线程属性对象,保存策略与优先级等配置。
- SCHED_RR(勾选规则)第一句调用指定的调度策略。
- param(优先级结构体)包含
sched_priority字段,填 1~99。 - 调用顺序(先挂号后就诊)必须在 pthread_create 之前设置,否则新线程不继承实时属性。
风险与优化
急诊室也会翻车。最经典的翻车场面叫“优先级反转”:低优先级病人 A 手里紧紧攥着一把手术钳(锁),高优先级病人 B 正等着这把钳子做手术,中优先级病人 C 却不停来抢医生。结果 B 这个最该先救的,反而被 C 拖得最惨——这就是反转。
Linux 的经典解法是“优先级继承”:当 B 在等待 A 的锁时,A 临时升级到 B 的红牌号,先把手术做完交还钳子再恢复原级别。C 看到 A 的牌子变红,就不敢再来抢了。除了反转,还要警惕“饥饿”:一个 99 级的任务永远就绪,其他任务就永远饿着。这两种风险都是实时系统的“慢性病”。
反转场景可以浓缩成一行公式:
急诊室的智慧不是“见人就救”,而是有秩序地救。理解了 FIFO、RR 和抢占这三套规则的边界,你就能设计出真正可靠的实时系统。
- P_H / P_M / P_L(三个牌号)高、中、低三个优先级,满足 P_H > P_M > P_L。
- 反转(顺序倒挂)H 等锁期间 M 抢跑,导致 H 反而最后完成。
- 继承箭头(临时升级)L 临时继承 H 的优先级,压住 M。
- 设计建议(防患未然)实时任务保持短小,避免持锁阻塞,不在实时线程里做动态内存分配和打印。
AI 应用场景
音视频推流中的线程调度
阶段:系统部署与性能调优
解决的问题:直播推流要求采集线程每 33ms 拿到一帧画面,否则就会卡顿掉帧。传统 CFS 调度器按权重分配 CPU,后台的转码、日志任务可能随时抢走 CPU,造成采集延迟。把关键线程设为 SCHED_FIFO 后,采集任务一旦就绪就立即抢占,帧率稳定性大幅提升。
具体案例:某直播平台将摄像头采集线程设为 SCHED_FIFO 优先级 80,将编码线程设为 SCHED_RR 优先级 60。在线人数峰值时,丢帧率从 2.1% 降到 0.3%,弹幕区“画面卡成 PPT”的投诉基本消失。
自动驾驶控制闭环
阶段:实时推理链路优化
解决的问题:自动驾驶的感知→规划→控制链路必须在 100ms 内闭环,传感器融合线程稍有抖动,控制指令就会迟到,影响行车安全。把控制线程设为最高优先级 SCHED_FIFO,感知融合线程设为 SCHED_RR,就能保证关键路径的确定性。
具体案例:某自动驾驶团队在 ROS2 节点中,将控制节点绑定到独立 CPU 核并设为 SCHED_FIFO 优先级 90,传感器融合节点设为 SCHED_RR。实车测试中,控制指令的最大延迟从 38ms 降到 12ms,且抖动明显收敛。
高频量化交易低延迟引擎
阶段:延迟关键型系统优化
解决的问题:量化交易的订单撮合与风控检查,拼的是微秒级延迟。普通调度下,系统中断和其他进程会随机打断交易线程,产生“调度抖动”。将交易核心线程设为 SCHED_FIFO 并固定在专用 CPU 上,能消除大部分抖动。
具体案例:某量化私募把行情解析线程设为 SCHED_FIFO 优先级 99,订单执行线程设为 SCHED_RR 优先级 90,同时把这两个线程绑核。优化后盘中延迟的 P99 从 180μs 降到 95μs,抢单成功率明显提升。
Python 演示
#!/usr/bin/env python3# -*- coding: utf-8 -*-"""模拟 Linux 实时调度 SCHED_FIFO 与 SCHED_RR,并演示高优先级抢占。""" class Task: def __init__(self, name, priority, need, arrive=0): self.name = name self.priority = priority # Linux 实时优先级 1~99,越大越紧急 self.need = need # 还需的 CPU 时间(以时间片为单位) self.arrive = arrive # 到达(变为就绪)的时刻 def simulate(tasks, policy, quantum=3): """逐步模拟调度,返回 [(时刻, 任务名), ...] 的切换序列。""" state = {t.name: [t.need, t.priority, t.arrive] for t in tasks} total = sum(t.need for t in tasks) clock, served, cur, used = 0, 0, None, 0 timeline = [] def ready(): return sorted( (n for n, d in state.items() if d[0] > 0 and d[2] <= clock), key=lambda n: (-state[n][1], n)) while served < total: pool = ready() if not pool: clock += 1 continue nxt = pool[0] if cur is None or nxt != cur: # 切换(可能是高优先级抢占) timeline.append((clock, nxt)) cur, used = nxt, 0 state[cur][0] -= 1 served += 1 clock += 1 used += 1 if policy == "RR" and state[cur][0] > 0 and used >= quantum: same = [n for n in pool if state[n][1] == state[cur][1]] if len(same) > 1: # 同优先级内轮转到下一位 cur = same[(same.index(cur) + 1) % len(same)] timeline.append((clock, cur)) used = 0 elif policy == "FIFO" and state[cur][0] == 0: cur = None # 当前任务完成,重新挑选 return timeline def show(title, timeline): seg, start, buf = None, 0, [] for t, name in timeline: if name != seg: if seg is not None: buf.append("%s@%d-%d" % (seg, start, t - 1)) start, seg = t, name buf.append("%s@%d-%d" % (seg, start, timeline[-1][0])) print(title + ": " + " -> ".join(buf)) def main(): tasks = [ Task("P1A", 95, 6), Task("P1B", 95, 6), # 与 P1A 同优先级,观察 RR 轮转 Task("P2", 50, 8), Task("Alarm", 99, 3, 4), # 第 4 时刻到达的危重任务,演示抢占 ] show("SCHED_FIFO", simulate(tasks, "FIFO")) show("SCHED_RR ", simulate(tasks, "RR", quantum=3)) if __name__ == "__main__": main()
key=lambda n: (-state[n][1], n)把就绪任务按实时优先级从高到低排序,同优先级按名字取稳定顺序,模拟分诊台“先挑最红牌子”。if cur is None or nxt != cur:发现候选人不是当前任务,就是一次调度切换;可能是正常换班,也可能是高优先级任务到达触发抢占。timeline.append((clock, nxt))记录每次切换的时刻与任务名,最后用 show() 输出分段运行序列。if policy == "RR" and state[cur][0] > 0 and used >= quantum:RR 的关键:时间片耗尽且任务没做完,就要在同优先级组内换人。same[(same.index(cur) + 1) % len(same)]在同优先级组里取“下一位”,构成轮转环。elif policy == "FIFO" and state[cur][0] == 0:FIFO 的关键:任务没做完就一直占用 CPU,只有做完了才重新挑选。
常见误区
误区“SCHED_FIFO 就是时间片很长的轮转吧?反正每个任务最后都能跑到,只是 FIFO 等待更久而已。”
正解FIFO 根本没有时间片。任务一旦获得 CPU,只有三种情况会离开:主动调用 sched_yield()、阻塞等待 I/O、或者被更高优先级任务抢占。把 FIFO 当成“长时间片 RR”,会低估它霸占 CPU 的风险——一个写坏的 FIFO 任务可以让整个系统“冻住”其他实时任务。
为何常见:“先来先服务”这个名字实在太像排队机制,很容易让人联想到时间片轮转,忽略了它“运行到完成”的本质。
误区“Linux 里优先级都是数值越小越高吧?nice 值 -20 最优先,那实时优先级应该也是 1 最紧急。”
正解实时优先级恰好反过来:1~99,数值越大越紧急,99 是最高。数值越小越优先的是普通进程的 nice 值(-20~19),它属于完全公平调度器 CFS 的规则。把 nice 的规则套到实时调度上,会写出“高优先级却是小数字”的错误配置,甚至可能让关键任务饿死。
为何常见:nice 值是大多数教程最早讲的优先级概念,“越小越优先”先入为主,很多人没有意识到实时优先级是另一套体系。
误区“用了 SCHED_RR,所有实时任务都会轮流获得 CPU,低优先级的也能定期跑一跑。”
正解RR 只在同一个优先级组内轮转。只要存在一个更高优先级的任务持续就绪,低优先级任务就永远不会轮到 CPU,这叫“饥饿”。RR 面向的是“多个同等紧急任务”的场景,比如三个同优先级的传感器线程按时间片轮流采集,而不是全员大锅饭。
为何常见:“轮转”(Round Robin)这个词太有迷惑性,让人误以为是一个包含所有任务的循环队列。
误区“把普通进程的 nice 调到 -20,优先级拉满,不就能获得实时效果了吗?”
正解nice 属于 CFS 调度类,只影响 CPU 分配权重,它既没有时间片保障,也没有抢占保证,更不存在截止期限的概念。实时必须显式使用 chrt 或 sched_setscheduler 指定 SCHED_FIFO/SCHED_RR 和 1~99 的实时优先级,并具备 root 或 CAP_SYS_NICE 权限。把 nice 当成万能旋钮,在实时场景下是无效的。
为何常见:nice 是接触 Linux 时最早学会的调优工具,“调到最小就最优先”的直觉容易被错误泛化到实时调度领域。
面试题
Q1. SCHED_FIFO 的同优先级任务之间如何调度?
- A.所有任务轮流各跑一回合
- B.先运行的任务运行完才换人← 正确答案
- C.由 nice 值大小决定先后顺序
- D.按随机顺序挑选下一个任务
答案:B。FIFO 的语义就是先来先做,同优先级之间没有时间片,前一个不主动让出或阻塞,后一个就一直等,就像急诊室同级别病人按挂号顺序一单一单地看。A 是 RR 的行为,C 是 CFS 的数值规则,D 毫无确定性可言。
Q2. Linux 实时优先级的取值范围和大小关系是?
- A.0 到 139 数值越大越紧急
- B.1 到 99 数值越大越紧急← 正确答案
- C.-20 到 19 数值越小越优先
- D.100 到 139 数值越小越优先
答案:B。实时任务用 1~99,数值越大优先级越高,99 是红牌中的红牌。选项 C 是 nice 值的规则,属于 CFS 范畴,方向恰好相反。A 和 D 的数字范围都是编造的,记住“RT 就是 1 到 99 越大越急”即可。
Q3. SCHED_RR 与 SCHED_FIFO 最核心的区别是什么?
- A.RR 的实时优先级取值范围更大
- B.RR 增加时间片,耗尽后同组换人← 正确答案
- C.FIFO 不支持高优先级任务抢占
- D.RR 只能用于普通进程的调度
答案:B。RR 可以理解为“FIFO + 时间片”:同优先级任务之间,时间片一到就排到队尾等下一轮。两者都支持高优先级抢占,优先级范围也完全相同,都是 1~99。所以 B 是对的,C 和 D 都是对 FIFO/RR 的错误描述。
Q4. 当更高优先级的实时任务变为就绪时,调度器会怎么做?
- A.等当前任务主动让出 CPU 再上
- B.立即抢占当前正在运行的进程← 正确答案
- C.等当前时间片耗尽后再切换
- D.把新任务放到队尾等待下一轮
答案:B。抢占是实时调度最核心的铁律:新任务优先级大于当前任务时,立即保存现场并切换,正如急诊室来了更红的牌子医生立刻放下手里的活。等让出、等时间片都是非实时的妥协行为,实时系统等不起。
Q5. 优先级反转的经典场景是哪个?
- A.低优先级持锁中优先级抢跑拖住高优先级← 正确答案
- B.高优先级任务一直运行饿死低优先级任务
- C.两个任务互相等待对方释放自己所需锁
- D.时间片设置过小导致频繁切换开销过大
答案:A。反转的核心是“顺序倒挂”:高优先级 H 等低优先级 L 的锁,中优先级 M 趁机抢跑,H 反而最后完成。B 是饥饿,C 是死锁,D 是时间片抖动,都不是反转。
Q6. 将进程设置为实时调度,需要满足哪些条件?
- A.拥有 CAP_SYS_NICE 权限← 正确答案
- B.指定合法的实时优先级 1 到 99← 正确答案
- C.进程隶属于 root 用户组来运行
- D.内核编译时必须关闭抢占功能
答案:AB。设置实时调度需要双重条件:一是权限(root 或 CAP_SYS_NICE),二是合法优先级(1~99)。拥有 CAP_SYS_NICE 的非 root 用户也可以设置,所以 C 说“必须属于 root 用户组”是错误的;抢占是内核的既有能力,D 与设置条件无关。
Q7. 关于“优先级继承”的正确描述是?
- A.高优先级任务自动降低自己的优先级别
- B.持锁任务临时继承等待者的优先级← 正确答案
- C.所有实时任务的优先级最终都相同
- D.内核关闭所有中断来保护临界资源锁
答案:B。优先级继承让低优先级持锁者临时升到等待者的高优先级,相当于给 L 发一张临时红牌,挡住 M 插队,用完锁再还原。A 和 C 方向完全反了,D 是关闭中断保护临界区的另一种做法但并非优先级继承。
Q8. 一个 SCHED_FIFO 任务离开 CPU 的合法途径有哪些?
- A.主动调用 sched_yield 让出处理器← 正确答案
- B.阻塞等待某个 I/O 操作完成返回← 正确答案
- C.同优先级的任务被调度器选中运行
- D.更高优先级的任务就绪立刻抢占← 正确答案
答案:ABD。FIFO 没有时间片,同优先级任务永远不会主动抢走 CPU,所以 C 不会发生。合法的离开途径只有三条:自己让出(sched_yield)、阻塞(等 I/O)、被更高优先级抢占,也就是 A、B、D。
Q9. Linux 实时运行队列查找下一个任务通常用什么技巧?
- A.遍历链表逐个比较找出最大优先级
- B.优先级位图配合 O(1) 位操作查找← 正确答案
- C.红黑树按优先级排序后进行查找
- D.哈希表将任务编号映射到优先级
答案:B。Linux 的实时运行队列把优先级映射到位图,用 find_first_set_bit 从最高位查找非空队列,一次指令完成,耗时恒定 O(1)。遍历链表是 O(n),红黑树是 O(log n),都不符合“确定性优先”的设计目标。
Q10. 编写实时线程时,哪些是推荐的防御性做法?
- A.避免在实时线程中动态申请内存← 正确答案
- B.避免长时间持有锁阻塞实时任务← 正确答案
- C.不在实时线程中打印日志到控制台← 正确答案
- D.把全部业务逻辑都放进实时线程
答案:ABC。动态内存分配可能触发页错误和锁竞争,printf 会导致阻塞,长时间持锁会引发优先级反转,都会破坏实时线程的确定性。实时线程应该只保留最关键路径,D 正好相反——把全部逻辑塞进实时线程会让风险失控。