第一次在开源仓库里翻到阿波罗11号那台机载计算机的源码时,我盯着那个只有七段数码管和十几个按键的控制面板看了很久。这台机器要负责把人送到月球轨道、再安全落回月面,可它的内存按今天的标准连一张高分辨率桌面壁纸都放不下。阿波罗11号这个名字,对大多数人来说是历史书上的一个坐标,对做嵌入式、实时系统甚至后端架构的人来说,则是一份被时间封装的极限工程标本:2MHz左右的时钟、4KB左右的内存、大约72KB的只读存储,以及一套靠“织”出来的磁芯存储器承载的飞控软件。这篇文章不是太空史,而是一个工程师对这套系统的拆解笔记,重点回答几个问题:它为什么没有因为资源不够而失败?它的调度和容错在今天看依然成立的地方在哪里?我们这些天天写代码的人,能从半个多世纪前那台“电子表级”计算机里抄到什么作业。
如果你写过嵌入式程序、接触过实时操作系统、维护过必须“躺平也不能挂”的服务,这篇文章应该会对胃口;即便你只是好奇“4KB怎么走下整趟登月”,也可以放心读,涉及原理的地方我会尽量说人话。
1. 先算一笔硬账:2MHz与4KB是怎么把人送出去的
1.1 机载计算机的真实配置,别被电影骗了
| 参数 | 实际数值 | 今天的参照物 |
|---|---|---|
| 主频 | 约2.048MHz | 一块电子表晶振的十倍 |
| 内存(RAM) | 2048字×15位,约4KB | 一段不带图片的普通文章 |
| 只读存储 | 36864字×15位,约72KB(含校验位) | 几首MP3都放不下 |
| 字长 | 15位+1位奇偶校验 | 现代CPU是64位 |
| 逻辑实现 | 约几千个NOR门 | 现在一颗FPGA上亿门起步 |
| 整机重量/功耗 | 约32公斤 / 几十瓦 | 一台小主机的功耗 |
把这张表看懂,你就明白为什么当年那批工程师的所有设计都带着一股“算计”的味道。15位字长意味着什么?无符号整数最多到32767,带符号就只有-16384到16383;要做精度更高的导航计算,只能用两个15位字拼出双精度,再小心处理进位。这不是今天64位CPU上“顺手给个long double”的事,而是连“结算一次位置要分配哪几个寄存器”都要提前规划的技术活。
运算速度同样可怜:一条加法指令耗时大约在十几到二十几微秒,折算一下每秒也就几万次运算。用今天的眼光看,这套系统的算力还不如一块计算器芯片,但它要做的是实时制导:发动机点火、姿态稳定、位置积分,每个环节都踩在硬时间上。落后不可怕,可怕的是在落后的同时还被要求不犯错。阿波罗11号最值钱的地方,恰恰在于把“不够用”变成了“必须想清楚怎么用”。
1.2 磁芯与磁芯绳:断电不丢数据的“老古董”技术
很多人以为当年的存储就像早期电脑一样娇贵,其实恰恰相反。阿波罗11号的RAM用的是磁芯存储器:每一个比特是一个小小的铁氧体磁环,靠电流改变磁化方向来记忆0和1。它断电不丢数据、抗辐射、抗振动,在1960年代是航空航天领域相对可靠的选择。而程序所在的只读部分更特殊,用的是俗称“丝绳存储器”的磁芯绳:一根根导线要么穿过磁环、要么绕开磁环,穿过去代表1、绕开代表0。整份飞行程序就是靠这种方式被“织”进一卷一卷线缆里的。
这也是为什么这套系统几乎没有“后期改代码”的可能性:程序被固化之后,改一个比特等于重穿一根线,代价堪比重新发布一台硬件。后面我会说,这个苛刻条件逼出来的软件开发纪律,才是整个项目最大的遗产。
1.3 面板上的交互:DSKY和它的动词名词体系
机组成员跟这台计算机打交道的窗口叫DSKY,一个看起来活像微波炉操作面板的设备:三个七段数码管区域显示数字,旁边一排告警灯,再加一个数字键盘。它的交互逻辑被简化成了“动词+名词”:动词告诉你“我要做什么”,名词告诉你“对谁做”。比如输入动词06、名词70,就是让机器把某个导航参数显示出来;输入动词01、名词02,则是改变某个目标的数值。
这套体系放在今天看极其反直觉——没有图形界面、没有菜单、没有鼠标点击,但它的价值在于确定性和可学习性:总共就那么几十个动词组,操作员需要记忆的状态量被压到了最低。DSKY上的灯光也不是装饰:PROG灯亮说明有程序在跑,COMP ACTY灯亮说明计算机正忙,GIMBAL LOCK灯亮则代表惯性平台到了危险姿态。信息密度极高的同时,优先级也被设计成明暗两种:重要的灯可以闪,不重要的信息晚半秒刷新完全无所谓。
还有一个容易被忽略的细节:DSKY的显示刷新本身就是计算机的周期性任务。换句话说,界面越花哨,CPU就越忙,导航任务就越容易被挤占。所以极简界面不是审美选择,是资源约束下的最优解。今天很多团队做产品时当然不用受这种罪,但“多余交互会消耗真正的关键资源”这条链路,放在任何系统里都依然成立。
2. 4KB内存里的“操作系统”:AGC的调度与语言
2.1 没有现代OS,但有一套真正的实时调度器
阿波罗11号机载计算机里确实没有我们熟悉的“操作系统”,但已经有一套完整的调度骨架,名叫Executive(执行器)。它的职责很简单:在所有待运行任务里,每时每刻挑出优先级最高的那个去执行。听起来平平无奇,但你要知道它是在什么条件下做的这个决定——整个系统的可运行内存只有4KB,所有任务代码都在只读存储器里,任务队列、任务控制块、各种缓冲区加起来也就那么点开销;为了省内存,他们甚至把任务描述符精简到了极致。系统里还有一套叫Waitlist的机制,用来处理“过一会儿再跑”的任务,基本就是给每项任务挂一个计时器,时间到了再把它唤醒。这套东西放到今天,就是一个微型RTOS的内核雏形,只不过是被焊死在磁芯绳里的。
优先级在这里不是一个抽象概念,而是生存问题。我试着用伪代码还原了一下它的调度思路:
# AGC Executive 的简化伪代码(仅用于理解调度哲学) loop: job = pick_by_priority(ready_jobs) if job is None: job = refresh_dsky() # 平时没事就刷面板 if job.overran_slot(): raise_alarm(1202) # 通知机组:我超载了 drop_low_priority_jobs() # 但绝不丢制导任务 job.step()在这一次飞行里,任务大致分成几档:最关键的是飞行控制循环,姿态误差要尽快折算成发动机摆角和推力指令,这个循环的刷新间隔以毫秒到几十毫秒计;其次是两秒钟左右才需要重算一次的制导方程,它负责不断回答“接下来朝哪飞”;再往下是各种数据记录、面板显示刷新;最不济的才是会合雷达这类辅助设备的处理。这个分级直接决定了后面1202告警发生时系统为什么还能活下来。
2.2 自己造的“方言”:为导航数学设计的解释器
更妙的是,这台15位字长的机器上居然还跑着一个解释器。导航和制导需要大量向量运算,比如叉积、点积、单位化,如果全用汇编一行行写,不是不行,而是太容易出错、太耗时间。于是软件团队干了一件放在今天也不算过时的事:在机器指令之上封装一层虚拟机,设计了一套面向惯性导航的指令集,专门表达向量和矩阵运算。想算两个矢量叉积,一条指令就能解决,内部再去解释执行多步机器码。这等于在“最底层的汇编”和“面向问题的语言”之间插了一层DSL,今天做嵌入式的人看了会觉得眼熟——很多芯片上跑的专用中间层、脚本引擎,干的就是同样的事。
为了确保精度,这类导航运算普遍采用双精度定点数而不是浮点数。这个选择很现实:1960年代的浮点库又慢又贵,定点数占用字长可控,代价是程序员必须在心里死死记住每项数据的量纲和量程。这里没有“溢出之后再说”的余地,数值范围规划出问题,飞到一半可能就崩了。今天我们在各种高级语言里几乎不用操心定点溢出,但“数据范围是否在我的设计假设内”这道审查题,换了载体之后依然存在。
指令舱和登月舱的软件在当年分别有自己代号,指令舱那套叫Colossus,登月舱那套叫Luminary。两个编号相互独立、各有一大堆针对特定飞行阶段的“主程序”(像P63自动制动、P66半自动着陆这些),通过DSKY输入程序号来切换。这不是简单的菜单导航,而是把“当前飞行动作”直接暴露给机组:你选什么程序,计算机就知道该加载哪一套闭环控制逻辑。
2.3 代码怎么被“织”成实物,以及它留下的唯一退路
在第一节我说程序是被织出来的,这可不是修辞。当年负责生产磁芯绳存储器的厂商,接收的“源代码”是一叠厚达几十厘米的程序文档;产线上的工人按0/1图样,拿一根铜线,从上千个磁环中穿过或绕开,穿完一个bit再穿下一个。一根线穿错,整个存储器就报废。这也解释了为什么当时的测试流程严格得像宗教仪式:程序在“织”之前,要在专门的地面仿真环境里反复跑几千个小时,指令舱的、登月舱的、故障注入的,各种异常都要先走一遍。有资料统计,两个航天器上的机载软件加起来大约十余万行——按今天的标准是个小项目,但在当时是手写汇编加解释器代码一点点磨出来的,而且完全不允许带病上线。
好在他们不是完全没有退路。地面控制中心可以遥控读取机载状态;宇航员能通过DSKY手动切换程序、跳过某些自动序列;惯性平台之后还有备用的姿态参考手段。这套“多条腿走路”的备份结构,构成了整个系统最后一道防线,而这条防线在1202告警时被真正用上了一次,下一部分细说。
3. 1202告警:那句“继续飞行”为什么值得写进教科书
3.1 着陆阶段到底发生了什么
阿波罗11号的动力下降段从大约15公里高度开始,发动机持续点火把登月舱从大约每秒1.6公里的轨道速度减到接近零,整个过程约12分钟,完全靠机载制导计算机在线计算、实时控制。问题就出在这12分钟里。在离月面还剩七八公里的时候,指令长报出一句让很多人捏把汗的话:机载计算机给了一串1202“程序告警”。告警灯一亮,按照训练流程,机组必须立刻判断是否终止下降。
地面控制中心没有犹豫太久,几十秒内就给出了“继续飞行”的意见,随后整个下降过程里类似的告警还出现过几次,每一次都重新评估、每一次都判继续。最终登月舱在月面着陆,事后统计剩余的燃料只够再飞大约二十多秒。为什么地面敢做这个判断?因为1202这个告警,他们在地面仿真中已经见过很多次,知道它意味着什么、也知道系统会自己恢复。这不是赌运气,是一次预演过的决策。
3.2 告警的含义:系统在“说话”,而不是在“求救”
要理解这句话,得先分清告警等级。1201和1202都属于“执行器溢出”类告警,通俗讲就是:任务太多、调度窗口不够用了,执行器不得不放弃一部分工作,同时保留最高优先级的核心任务。放在今天互联网的语境里,这相当于一个服务因为负载过高返回了“我现在忙不过来,请稍后再试”的503,而不是“内部已经炸了”的500。前者是可恢复的过载信号,后者才是故障信号,两者的处置流程完全不同。
| 事件 | 表现 | 类比 |
|---|---|---|
| 1202告警 | 调度溢出,低优先级任务被丢弃,核心制导任务保留 | HTTP 503:临时过载,可恢复 |
| 系统崩溃 | 完全没有输出,姿态失控 | HTTP 500:内部故障,必须介入 |
1202发生时,真正被计算机毫不犹豫保住的是导航和制导任务——那是活命的底线。而被丢掉或延后的,恰恰是当时不需要的辅助计算,比如会合雷达数据的处理。事后调查复盘,问题根源正是会合雷达的开关状态不对,雷达一直在往计算机里灌格式不对的数据,把调度挤爆了。计算机在报警的同时,通过内置的恢复逻辑清掉了这堆垃圾负载,重新回到正常的下降制导流程。整个过程发生在几十秒内,机组和地面看到的只是“一个告警码、一个继续的许可、一次系统自我清理”。这不是主角光环,是设计出来的容错行为。
3.3 人机分工的边界:谁负责最后150米
还有一层容易被忽略的细节:计算机虽然厉害,最后贴近月面的那一段,主动权被交还给了人体。当高度降到约150米左右,指令长发现自动着陆目标区布满巨石和环形山,于是把系统切换成半自动模式,由他人工控制下降速度,计算机继续提供姿态、高度和水平速度信息。这里没有“机器全自动”的执念,而是把任务切成两段:机器擅长的地方让机器做,人类眼睛和判断更可靠的地方交给人类。整个系统从设计开始就预留了这个交接点,面板上那个程序号切换操作,就是人机协同的接口。
4. 半个多世纪后,这套系统依然值得抄作业
4.1 当“部署不可逆”被推到极限,验证就成了第一公民
今天的人嘲笑“织ROM”的反潮流,却很少意识到它背后隐藏着一种非常宝贵的管理智慧:当一次发布不可逆且代价极高时,团队就只有一条路可走——把验证前移到开发的最早期,并且让验证覆盖到每一处故障路径。阿波罗11号的软件团队在地面建设了与机载环境等价的仿真系统,尽可能真实地复现软硬件交互,然后一遍遍注入各种异常。那个“继续飞行”的决策,本质上是仿真训练的肌肉记忆。
嵌入式开发者可以从这里面抄到的东西很直接:为你的目标板搭持续集成,为你的告警路径写故障注入测试,把“生产事故预案”变成“演练过几百遍的脚本”,而不是上线那天才翻文档。当年没有这个词,但在本质逻辑上,他们做的事情就是我们现在说的“可靠性工程”。
4.2 优先级不是性能优化,而是生存机制
现代软件里“优先级”这个词通常和“性能”绑在一起,比如线程优先级调高一点、请求队列里插个队。但阿波罗11号用它证明了另一件事:优先级本质上是一份“当资源不够时,先牺牲谁”的预案。任务清单上每一个任务都被明确归类——有些是决定生死的,有些只是锦上添花。当所有东西都在抢时间片时,你才真正需要这份分类,而且必须在事故发生之前就做完。
今天做高并发服务的人同样面对这个问题:流量突增时先丢谁的请求、先保护谁的链路?如果你不在危机到来前想清楚,系统就会随机地“保护”某一段,而随机通常意味着最坏的结果。我见过不少线上事故,根源不是资源不足,而是没人说过资源不足时“谁先死”。
4.3 面向故障设计,而不是面向正常流程设计
整个阿波罗11号的机载软件,从第一天就在回答“如果……怎么办”,而不是“正常情况应该怎么做”。这也解释了为什么1202能被如此轻描淡写地翻篇:故障从未被当作“意外”,而是被当作系统设计必须覆盖的一条路径。告警、恢复、人工接管、中止条件,全部提前定义成了可执行的决策表。
如今的混沌工程、熔断、降级、健康检查,本质上是同一句话:好的系统不是不出错,而是出错时行为可预期。你可以让服务偶尔过载,但绝不能让过载演变成雪崩,更不能让值守的人盯着五个告警码不知道先按哪个按钮。那些看起来平淡的“应急预案”,价值不在文档本身,而在它迫使你把“故障时系统会做什么”提前想清楚。
4.4 三个可以马上带走的实操习惯
- 先写“告警码字典”。做完任何系统,把每一个异常、告警、错误码的理由、含义、处置动作写成一张表,让最没经验的值守员照着表也能做出接近正确的最初判断。当年机组面对1202能在几秒内不慌,靠的就是训练时已经把这张表刻进了脑子。
- 用“仿真环境”养决策。不一定要复刻硬件,但至少要有一条能注入故障的演练路径。把1202这种事故变成“已经见过十次的场景”,遇到时才不会慌。真到了线上,你不会希望自己是第一次看见失败长什么样。
- 明确“人机交接点”。哪怕你做的只是一个消息队列,也要问一问:系统降级到什么程度必须让人介入?介入的入口是不是清晰到新人都能操作?阿波罗11号没有选择全自动,也没有选择全手动,它选择了把交接点设计成面板上的一次程序切换。这种“可交接性”被很多系统忽略了。
最后分享一点个人体会。我自己的习惯是,每做一个实时或高可用项目,先把“优先级分层”和“告警码表”写出来,再去写业务逻辑,这个顺序救过我很多次。每次看到有人把线上告警当噪音、把熔断阈值拍脑袋定,我都会想起那台只有4KB内存的机器:它不是没有被故障骚扰,但它选择在报出1202的同时,把最重要的活儿稳稳保住。这份优雅,比任何高配集群都值得抄。