Apollo-11 这个项目代号,放到任何技术社区的标题栏里都会让人心里一紧:登月、汇编、只读内存、没有第二次机会。我最初被它吸引,不是因为在看航天纪录片,而是偶然翻到那份公开的制导计算机源代码——本以为会是天书,读进去才发现,那是最早一批把“系统工程”真正写进代码里的作品。这篇文章不打算讲发射花絮或英雄故事,而是想以同行的身份,把这台计算机拆成四块来聊:硬件底牌、实时调度与过载保护、验证链路、以及我从里面抄走的工程规矩。如果你是做嵌入式、后端、或者任何跟“可靠性”三个字脱不开关系的工程师,这篇应该能给你一些可以直接带走的东西。
1. 先看清 Apollo-11 的“电脑”:2K字RAM、36K字只读存储、1.024MHz主频
很多人想象中的登月舱计算机,是一屋子嗡嗡作响的设备。实际这台制导计算机也就比一个行李箱大一点,塞在航天员座位下方,带着一块键盘和几位数字显示的仪表。它跑的代码总量,放在今天是任何一个小程序都看不上的量级,但它完成的任务,今天也没几个系统敢打包票。
先说结论:这台机器在硬件上没有任何“余量”。没有硬盘,没有虚拟内存,没有图形界面,没有通用操作系统。所有东西都是直接寻址、直接执行。理解它,等于先理解“在什么都没有的前提下,一群工程师怎么把不可能抠出来的资源一点点抠出来”。
1.1 一张参数表看懂制导计算机
我把核心参数整理成了下面这张表,方便你对照着看:
| 参数 | 数值 | 换算成今天的说法 |
|---|---|---|
| 字长 | 15位 + 1位校验 | 常被归类为“16位机” |
| 主频 | 1.024 MHz | 约为普通手机主频的千分之一 |
| 可写内存(RAM) | 2048字(约4KB) | 连一张高清照片都存不下 |
| 只读存储(ROM) | 36864字(约72KB) | 全部程序、常量、导航表格都在这里 |
| 功耗 | 约55W | 比一盏节能灯高一点 |
| 重量 | 约32kg | 相当于两箱A4复印纸 |
这里有个经常被误解的点:那个“约4KB”的RAM,并不是我们熟悉的DRAM,而是磁芯存储。它同样靠电流改写,但断电后数据不会丢,这已经比很多现代终端设备“高级”了。真正限制它的不是技术可行性,而是体积、重量和功耗预算——在飞船上,每克重量都要用在刀口上,所以可写内存只给了2K字,剩下的大头全部做成只读。
1.024MHz的主频意味着什么?一条普通加法要走大约20微秒,乘法可能到几十微秒。今天的CPU一微秒能做几千次运算,而它一个加法的时间,足够现代芯片完成几十万次操作。但这并不影响它完成制导任务,原因在于:它处理的物理过程本身变化不快,真正稀缺的是“确定性”而不是“峰值性能”。
1.2 “织”出来的程序:铁芯绳只读存储的原理与代价
Apollo-11 的程序存储在一种叫“铁芯绳”的只读存储里,这个设计我每次想起来都觉得又原始又精巧。
它的基本单元是一排铁氧体磁芯和一根根导线。导线穿过某个磁芯代表二进制“1”,绕过这个磁芯代表“0”。成千上万根导线交错穿行,被扎成一股股像绳索一样的模块,所以叫 rope memory。读数据的时候,脉冲电流从一端进入,哪根线感应到了磁芯状态就输出对应的位。
这个方案的直接好处非常硬核:没有机械部件、不会掉电丢失、抗辐射能力强,而且密度在当年能做到极致。三个成年人才能搬动的存储模块,能装下整套登月制导程序,在60年代已经算奇迹。
代价也极其明显:程序一旦“织”进铁丝里,就物理上不可修改。想改一个1bit,不是重新烧写,而是要重新编织整个存储模块,甚至重做一整条线束。用个不太严谨但好懂的类比——这就像把整本书刻在石碑上,发现错别字的时候,唯一的选择是把整块碑重新刻一遍。
这个物理约束直接塑造了整个项目的软件管理风格:代码冻结不是制度偏好,而是硬件暴力逼出来的唯一选项。后面我会专门讲这套“冻结文化”,它到今天都值得所有做固件、做交付型软件的人反复体会。
1.3 DSKY:一块数字按钮,不是给人“操作电脑”的
和这台计算机对话的唯一人机接口,叫 DSKY,其实就是一小组数字键盘和显示屏。上面没有图文界面,没有鼠标,只有两位的 Verb、两位的 Noun,以及几行五位数字。
航天员想启动某个程序,先按动词码再按名词码,比如“Verb 37,Noun 01”,机器会回显一串数字。这不是为了炫技,而是在当时的技术约束下能做到的最可靠交互:数字短、编码稳定、航天员戴着手套也能按。同时,地面团队可以通过遥测看到同样一串数字,双方用同一套编码沟通。
这个设计给后来的我最大的启发是:好的系统状态展示,不是面板越花哨越好,而是“任何一位训练过的人,在危急时刻扫一眼就能明白当前在发生什么”。DSKY上每一个数字、每一个报警灯背后都有明确语义,是为“人在回路”而设计的,不是给工程师欣赏用的。
2. 调度和过载:1202 不是死机,是它在故意“丢包袱”
硬件只是骨架。真正让 Apollo-11 从一台“能做算术的机器”变成“能降落月球的系统”的,是一套极简却相当成熟的实时调度机制。它没有我们熟悉的操作系统,没有进程线程,但它的设计思想,放到今天做实时系统、做高并发服务的人身上,依然能对得上号。
这节我想重点聊一个被无数人误会的情节:登月过程中出现的 1201 / 1202 报警。很多人把它理解成“电脑差点死机、航天员力挽狂澜”,这话只对了一半。真相是:那台计算机正在执行一种精心设计的“负载脱落”策略,报警声说明它在保命,而不是在崩溃。
2.1 Executive、Waitlist、Interrupt:一套极简的实时内核
这套软件体系由三块核心构成:中断管理机构、等待表、以及一个叫 Executive 的任务调度器。
外部传感器(惯性测量单元、雷达、键盘、遥测链路)通过硬件中断把事件塞进系统;等待表负责那些周期性发生的事,比如每隔固定时间采样陀螺数据;而 Executive 维护一个按优先级排列的任务队列,总是挑最高优先级的任务先跑。
你可以把它理解成一个急诊科的分诊台:重症患者永远优先,轻症患者在走廊等着,护士台会根据病情严重程度决定谁先进抢救室。AGC 里的任务也分优先级,导航、制导、姿态控制这些“重症”永远排在前,显示刷新、遥测组帧之类的“轻症”让位。
这套内核用掉的内存极小,但逻辑已经相当完备:有优先级、有任务调度、有时间片概念、有中断嵌套处理。放在今天,很多小型 RTOS 的调度核心,本质上还是这个套路。
2.2 解读1201/1202:调度器在说“我忙疯了,但保命任务还活着”
登月动力下降后期,计算机突然响起了报警。历史记录里,1201 和 1202 这两个报警码反复出现了好几回,地面和航天员一度紧张。
这两个码的意思是“Executive Overflow”。展开说就是:某个瞬间,涌入任务队列的请求太多,Executive 连给新任务分配内存块的地方都不够了。按理说,一个调度器碰到这种情况最直接的反映是“先堵住入口,等有空间再接”,但那样会让队列越积越长,新到的数据永远是过期的。
AGC 的设计者没有这么做。他们让调度器在队列塞满时主动“修剪”作业:优先丢那些低优先级的、锦上添花的请求,保住制导、导航、姿态这些致命任务继续运转。也就是说,报警的时刻,系统不是在死亡边缘挣扎,而是在执行预设的求生策略——只保留能保住飞船的任务,其他全扔。
我当时看到这段原理,真的起了一身鸡皮疙瘩。因为它不是“运行出错”,而是“设计好的出错路径”。后来复盘普遍认为,过载的诱因之一是雷达数据链路源源不断灌入额外的计算请求,让本来就已经满负荷的调度器开始疯狂剪枝。下面是这两条报警的常见处置逻辑:
| 报警码 | 含义 | 处置原则 |
|---|---|---|
| 1201 | Executive 溢出,任务队列超出容量 | 若在动力下降阶段,且雷达数据正常,按预案继续 |
| 1202 | Executive 溢出,且可用内存块被耗尽 | 同上,继续监视着陆程序是否在正常运行 |
这里特别值得说的是地面团队的处置。地面上有不少工程师在模拟演练时把报警码表背得滚瓜烂熟,看到1202的第一反应不是“完了”,而是“这是溢出,程序还在保命,可以继续”。他们没有在飞行中翻手册,因为手册早就在脑子里了。
2.3 优雅降级的教材:过载是设计参数,不是边界情况
我们做互联网服务的人,天天把限流、熔断、降级挂在嘴边,但真正在系统过载时能做到“主动丢任务”的,其实不多。最常见的反应是让服务排队排到天荒地老,用户看到无限转圈,线程池堆满,最后整个进程被拖死。
AGC 给我们的启示是:过载不是异常,是系统运行的一个正常相位,必须预先把“丢谁、保谁”的规则写好。它甚至把“丢弃决策”也做成了报警,让人类决策者看到:我丢了锦上添花的东西,致命逻辑还在。
判定标准也很干脆:如果雷达数据是新鲜的,导航计算还在按周期推进,那就可以继续。这就是“在不确定中寻找确定性”的经典操作:不是假装过载不存在,也不是把全部希望寄托在“运气好不会过载”上,而是让系统在过载时有一个明确的、可验证的存活状态。
3. 没有第二次机会:Apollo-11 的地面验证是怎么“过分认真”的
软件工程里有个老生常谈:测试只能证明错误存在,不能证明错误不存在。放到 Apollo-11 身上,这句话不是哲学,而是生死问题。因为程序烧进铁芯绳之后,飞上天就没有任何“打补丁”的可能。
所以整个项目的验证策略,不是“尽量多测”,而是“在地面上把所有该错的、可能错的、不该错的,全部错一遍”。
3.1 全任务仿真:在模拟器里先“登陆”一百遍
当年没有云计算,没有虚拟机,但他们造了大型全任务模拟器:把制导计算机的真实硬件接上,用模拟的传感器数据去驱动它运行完整的飞行程序。模拟器能设置发动机推力偏差、雷达噪声、陀螺漂移,甚至故意注入故障,让飞行小组在模拟环境中“坠毁”好几次。
这种硬件在环 + 系统联调的做法,放到今天的嵌入式开发里依然是最高的验证级别。它的价值在于:你测的不是“某一函数正确”,而是“整套系统在面对异常输入时,全体硬件、软件、操作人员的行为链是否正确”。
我特别想说的一点是,当年他们测试的“用户”不止是软件,还包括操作它的人。模拟器里会突然冒出异常状态,逼操作团队现场判断。这跟我们现在做混沌工程很像:先把系统搞乱,再看团队和系统怎么反应,把反应中的漏洞填掉。
3.2 代码冻结:为什么改一行字要惊动整个项目组
因为程序是织进线里的,所以任何代码变更的本质是硬件变更。织一条新线束需要时间,装进飞船需要时间,重新验证更需要时间。于是项目管理只能走向一个极端:越临近飞行,变更门槛越高。到了后期,一个细微改动不只要过技术评审,还要评估重量、功耗、进度、风险,所有环节都签字才允许动。
这套“代码冻结”文化在今天的敏捷时代看起来很“反文化”,但它背后有非常清醒的账本:变更成本不等于改代码的成本,而是从改代码到它真正可靠运行之间所有验证的总和。你想想,我们现在的生产环境为什么这么怕半夜发版?因为发布动作的验证成本几乎都转移到线上,一但出问题,回到测试环境重新复盘的代价远大于事前评审。
Apollo-11 把这种成本量化得很清楚:不是在口头上喊着“少改点”,而是让每个人都知道“织好一条线要几周,装回飞船又要几周”。当代价被看见,人自然就谨慎了。
3.3 测试的边界:预演过,才知道什么是“敢继续”
前面提到 1202 报警时地面敢说“继续”,这个决定的底气不是来自某个人的天才瞬间,而是来自预演。在模拟训练中,团队反复见过这个报警码,知道它的含义边界,也知道什么情况下可以继续、什么情况下必须中止。
这就引出了测试哲学里至关重要的一条:测试的真正产出,不是一份“全部通过”的报告,而是让决策者在压力下有可执行的判断依据。你可以不知道所有未来情况,但你必须对已知的关键情况都排练过,确保当它发生时,第一反应不是愣住,而是按预案走。
当然,这并不能消除所有风险。事实上,后续飞行中依然出现过别的意外,但他们把每种已知风险都变成了“有名字、有编号、有对策”的条目。工程上的成熟,不是消灭不确定性,而是把不确定性列成清单,编好号,提前想好对策。
4. 从源码和工程实践里,我抄走的六条规矩
聊完原理和验证,我想从一个普通开发者的角度,说说这些年我从 Apollo-11 这个项目里真正“抄走”的东西。它们不是什么高深公式,而是六条听起来朴素、做起来极难的规矩。
4.1 代码是写给人看的,机器只不过是顺便执行
打开那份公开的制导计算机汇编源码,你会发现里面注释非常多,而且口吻一点都不“官方”,更像是一群工程师在给后来的值班者留纸条。它时刻提醒我:代码最宝贵的读者不是编译器,而是半年后接手故障的那个人。
所以我现在的习惯是:注释里少写“在做什么”,多写“为什么这么写”和“什么情况下这里会出事”。当年连数字面板都有人把注释写得那么有用,我们守着IDE和文档工具,更没有理由写出一堆“改了没人懂”的代码。
4.2 把报警码变成肌肉记忆
1202 事件里最让我后背发凉的,不是机器报警,而是报警来临时大家都能立刻反应。地面和航天员早就把关键报警码背熟了,不需要现场查文档。
我现在做系统,也会给自己维护一份“报警手册”,但更重要的是让值班的人真的练过,而不只是文档躺在知识库里。所谓演练,就是把“从看到报警到做出反应”的链路缩短到最短。
4.3 过载降级要提前写进架构
Apollo-11 教会我最关键的一句工程判断:系统过载时,保住什么是最高优先级,必须提前定义好。它把“丢弃低优先级任务”写成了调度器的正常功能,而不是事后补救。
我现在设计中等规模的后端服务,也会明确区分 must-run 和 nice-to-run:用户核心链路是 must-run,日志上报、非核心统计是 nice-to-run。一旦 resource 紧张,nice-to-run 直接让位,而且要让监控系统看得见“它在让位”,而不是整个系统一起死掉。
4.4 一切资源预算都被显式管理
2K 字内存、36K 字存储、1MHz 主频,每一项资源都有人在管预算。谁多占了一个字,谁多耗了几百个周期,都有人知道。这种“资源预算感”在今天的项目里基本消失了——几百MB内存随便花,CPU 核数多到懒得数。
但我要说一个现实:你的系统或许不缺资源,但缺预算感。没人知道核心链路到底能承受多大流量、哪个模块超了多少内存,于是只能靠“扩容”来解决所有事。Apollo-11 的做法值得借鉴的不是“抠”,而是“知道每一份资源被谁用掉了”。
4.5 变更越晚,代价越大
这张表我是这么理解的,它比任何PPT都直白:
| 阶段 | 产物形态 | 变更代价 |
|---|---|---|
| 设计期 | 图纸、文档 | 低,改一行字而已 |
| 编码期 | 纸带、磁带上的汇编源码 | 中,重新汇编后可生成新版本 |
| 织带期 | 铁芯绳只读模块 | 高,改1bit等于重织整捆线 |
| 飞行中 | 已烧录的运行系统 | 无穷大,无法修改 |
它给我的直接教训是:越靠近交付节点,变更的代价不是线性增长,而是指数爆炸。所以我在项目排期时会刻意把“需求冻结日”和“代码冻结日”全都写清楚,让产品同学和开发同学都提前接受一个现实——没有所谓“顺手改一下”。
4.6 把你最重要的代码和方案沉淀下来
这套制导程序几十年前就已经解密并公开,今天任何一个程序员都能在开源仓库里翻到那份汇编源码,逐行去读当年工程师的思路。它的价值早就超出了登月本身,变成了后来者学习实时系统、学习复杂调度、学习“极端约束下如何设计”的最佳教材。
我自己的体会是:你手头最重要的代码和方案,如果只存在某个私有仓库里,十年后很可能就没了;但如果你把它整理成文章、复盘成文档、提炼成可复用的原则,它的生命周期会远比任何一次发布都长。你的项目也许不会登月,但你的记录可能帮下一个项目避开同一个坑。
最后聊几句我个人的体会。做项目这些年,我见过太多系统在流量洪峰里直接崩掉,也见过团队在故障来临那一刻手忙脚乱地翻手册。每次遇到这种时刻,我都会想起 Apollo-11 那个 1202 报警的瞬间:它做到的其实不是“更聪明的代码”,而是提前想清楚了哪些任务必须活、哪些任务可以丢、丢掉之后怎么让决策者看见。这套思路不挑行业、不挑语言,从一台1MHz的计算机到今天的分布式系统,底层逻辑几乎没变。找个周末,把那份公开的制导源码翻出来,跳到着陆程序那段注释读一读,你大概率会和我一样,在屏幕前安静地看很久。