第71期 | 最低的活,卡住了最高的活
1997年7月4日,火星探路者着陆成功。几天后,着陆器上的计算机开始反复整机重启。当天剩下的活,全被推迟到第二天。地面团队在实验室不到 18 小时就复现了它¹。凶手不是硬件,也不是算力不够,而是一次排队顺序的错位。
一、火星上的一次堵车
探路者只有一颗 CPU,1553 总线每 0.125 秒一个周期,由两个任务分工:bc_sched 负责排总线事务,是全系统最高优先级(只比 VxWorks 的 tExec 低);bc_dist 负责分发数据,排第三¹。低优先级的,是采集气象数据的 ASI/MET 任务。
出事的那一刻是这样的:ASI/MET 在 select() 里拿互斥锁,走到 semGive() 还没放下锁,就被几个中等优先级任务抢占了。bc_dist 醒来要发新数据,pipeWrite() 撞上同一把锁,堵住了。中等任务继续跑,ASI/MET 始终抢不到 CPU。等到 bc_sched 醒来一看:bc_dist 这周期还没干完——硬截止时间已过,只剩重启¹。
最耐人寻味的是复盘里的两句:地面测试只测了"最好情况",而火星上的数据率比预想的高,科学活动更多,恰恰是"好得出乎意料"的那种情况把毛病逼了出来¹。修复也轻得出人意料:不用改算法,不用换芯片,只把创建信号量时的一个全局标志改掉,打开优先级继承——等锁的高优先级任务,会把自己的优先级临时借给持锁的低优先级任务,让它先跑完、放下锁¹。补丁传上火星,重启停了。
理论其实早就写好了。1990 年,Sha 等人就给出了优先级继承协议与优先级上限协议:上限协议下,高优先级任务最坏只需等低优先级任务的一个临界区²。Linux 后来把这套东西做进了内核:rtmutex 与 PI-futex,用户态 pthread 互斥量设 PTHREAD_PRIO_INHERIT 就能用上;PREEMPT_RT 里自旋锁也换成带继承的 rtmutex,等待者的优先级会临时过户给持锁者³。火星那课的另一面,是发展性研究的一课:原理 1990 年就有了,输在默认配置里没打开——原理到产品之间,差的往往不是一篇论文,而是一个开关。
照见平时看不见的那一层:调度器管的是"谁先跑",锁管的是"谁先放"。公平排队排得再匀,也管不到一把锁前面的等待。优先级反转的可怕不在低优先级慢,而在中等优先级不知情地插队——高优先级等的不是低优先级,而是所有中等优先级加起来的时间。
二、等锁的人,也该有优先级
如果 AI 的操作系统先出现,它不会只给任务排队,还会给等待排队。一个 agent 的世界里,锁到处都是:共享的上下文存储、同一块 KV 缓存、同一个工具调用队列。想象低优先级的后台整理任务正拿着记忆库的锁写一条长记录,中等优先级的批量任务一波波抢占,高优先级的在线问答 agent 在锁外等着,等到超时、重试、再超时——系统不是慢死的,是自己把自己重试死的。
原生调度器该有两本账:一本记运行时间,一本记等待时间。锁被谁拿着、谁在等、等了多久、链条有多长,都该是调度器看得见的状态。设计上三招可选:继承——等锁者的优先级临时借给持锁者;上限——每把锁事先定好天花板,拿锁即升到天花板,堵死的上限就是一个临界区;或者干脆别让低优先级任务长久持锁——临界区切短、快照代替原地改。按时间片切指令流的旧抽象在这里又露一次馅:它能量一段代码跑了多久,却量不出"为别人等了多久"。向量阵列时代也一样:一颗低优先级核上跑着的算子占着显存不放,高优先级的推理一样要在门外等——等的代价,得进调度器的账本。
三、亲手复现一次反转
可以试的一步,是在手边任何一台 Linux 机器上写 30 行 C,把火星那一幕缩小复现:三个线程,高、中、低优先级(SCHED_FIFO 90/50/10),低优先级拿锁后睡 100 毫秒,中优先级空转,高优先级等锁。用默认互斥量跑一轮,再把互斥量属性改成 PTHREAD_PRIO_INHERIT 跑一轮:
gcc -O2 -o pi pi.c -pthread
sudo ./pi
看一个数:高优先级线程从等锁到拿到锁,等了多久。默认配置下,它等的不是 100 毫秒,而是中优先级任务的全部时间;打开继承后,低优先级被临时抬到 90,跑完立刻放锁,高优先级等的就只剩一个临界区。需要 sudo,是因为 SCHED_FIFO 实时优先级要特权;没有特权也别硬来,先把现象记下来。
落到普遍场景里,同一条规矩:一台跑本地模型的机器上,在线服务与批量任务别共用一把长临界区的缓存锁;一台边缘算力盒上关键控制任务的互斥量开继承,别让后台日志任务卡住控制环;视频 AI 平台里解码与推理共享的队列,锁的持有时间按毫秒算;企业私有智能中心里 agent 写共享记忆时,临界区越短,整个系统越不容易在锁前堵死。先把临界区写短,再谈优先级继承——继承治的是反转,治不了把锁当床睡的设计。
觉 然 小 注
优先级排的是谁先跑,锁排的是谁先放。两张表对不上,最高的活,也会被最低的活卡住。
引 用 与 出 处
- [1] Glenn E. Reeves. What Really Happened on Mars? JPL 内部邮件(1997-12-15,火星探路者软件负责人亲述), 1997. https://www.cs.unc.edu/~anderson/teach/comp790/papers/mars_pathfinder_long_version.html
- [2] Lui Sha, Ragunathan Rajkumar, John P. Lehoczky. Priority Inheritance Protocols: An Approach to Real-Time Synchronization. IEEE Transactions on Computers, 1990. https://research.ibm.com/publications/priority-inheritance-protocols-an-approach-to-real-time-synchronization
- [3] Linux 内核文档. Real-time theory(Priority inheritance 章节). kernel.org 在线文档. https://www.kernel.org/doc/html/latest/core-api/real-time/theory.html
· · ·