1. 从一行代码到月球:Apollo-11 制导计算机到底藏着什么
第一次看到“Apollo-11”这个标题,很多人脑子里蹦出来的可能是那段著名的“个人一小步、人类一大步”的录音,或者是那台算力还不如现在一块电子手表的导航计算机。但如果你是个喜欢折腾底层代码、对嵌入式系统和实时控制感兴趣的人,这个标题背后真正值得挖的,是那段被开源出来的制导计算机源码——它用极其有限的资源,完成了一次容错率几乎为零的载人登月任务。
我最初接触这个项目,是因为在做一套工业级的实时控制系统,被任务调度和内存管理折腾得够呛。当时有个前辈丢给我一句话:“你去看看阿波罗制导计算机怎么写的,人家用 72KB 的 ROM 和 4KB 的 RAM 就把人送上月球了。”这句话直接把我拽进了这个项目。后来我发现,这份源码不仅仅是一段历史遗产,它里面关于实时任务调度、优先级抢占、冗余设计、人机交互容错的思路,放到今天的嵌入式开发、机器人控制甚至高可靠性服务端系统里,依然有非常强的参考价值。
这篇文章不打算复述登月历史,也不打算做泛泛的科普。我想从一个实际开发者的视角,把这份源码里最值得借鉴的几个核心机制拆开来讲清楚:它到底是怎么在极端资源约束下做任务调度的?为什么它的中断处理设计得那么“反直觉”?它的内存管理策略对今天的我们有什么启发?以及,如果你想自己跑一遍这套代码,需要做哪些准备、会踩哪些坑。无论你是做嵌入式、做实时系统,还是单纯对“极限工程”感兴趣,这篇内容都能让你拿到一些可以直接迁移到实际项目里的思路。
2. 先搞清楚这套代码的运行环境:不是模拟器,是真实硬件约束
2.1 为什么不能拿现代开发板的思维去读它
很多人第一次打开这份源码,会习惯性地用现代嵌入式开发的思路去理解:找 main 函数、看初始化流程、追中断向量表。但阿波罗制导计算机(AGC)的代码结构和我们熟悉的 C 语言项目完全不同。它是一门叫AGC Assembly的汇编语言,指令字长 15 位,数据字长 15 位,采用一补数表示法。这意味着你不能用补码的直觉去理解它的算术运算,尤其是溢出和进位处理,稍不注意就会看错逻辑。
更关键的是,这台计算机没有我们现代意义上的“操作系统”。它所有的任务调度、中断响应、外设管理,都是通过一套叫Executive的调度程序来完成的,而这个 Executive 本身也是用汇编写的,和用户任务代码混在一起。换句话说,你读的不是一个“应用”,而是一个完整的、自包含的运行时环境。
我建议你在读代码之前,先把这几个硬件参数记在脑子里:
| 硬件项 | 参数 | 对代码的影响 |
|---|---|---|
| 字长 | 15 位 | 所有地址、数据、指令都是 15 位,地址空间只有 32K |
| ROM | 约 72KB(36K 字) | 核心程序全部固化,不可修改 |
| RAM | 约 4KB(2K 字) | 所有变量、堆栈、缓冲区都在这里 |
| 时钟 | 约 2.048 MHz | 指令周期约 1 微秒,时间片以毫秒计 |
| 中断 | 多级向量中断 | 优先级固定,不可动态调整 |
这张表看起来简单,但它解释了很多设计决策。比如为什么它的任务调度器那么“简陋”——因为 RAM 太小,根本放不下复杂的任务控制块;为什么它的中断处理要那么快——因为主频太低,多耽误几个周期就可能错过关键控制窗口。
2.2 源码的获取与本地编译环境搭建
这份源码最早是在 2003 年左右由一位研究员从麻省理工的档案里整理出来的,后来被放到了开源社区。现在你能找到的版本,通常是经过格式整理和注释补充的。如果你想自己编译一遍,需要准备这些东西:
- 汇编器:推荐用
yaYUL,这是专门为 AGC 汇编设计的汇编器,支持源码里的伪指令和宏。 - 模拟器:
yaAGC是最常用的选择,它可以模拟 AGC 的指令执行和中断行为。 - 辅助工具:
yaDSKY用来模拟显示键盘面板,yaTelemetry可以看遥测数据。
在 Linux 环境下,你可以直接从源码仓库拉取这几个工具,编译安装。Windows 用户建议用 WSL,因为这套工具链对文件路径和换行符比较敏感,原生 Windows 环境容易出奇怪的问题。
注意:不要试图用现代汇编器去编译这份代码。AGC 汇编的语法和 x86、ARM 完全不同,伪指令、标号规则、宏展开方式都有自己的特点。用错汇编器,你会在第一步就卡住。
编译成功后,你会得到一个.bin文件,里面是 15 位宽的机器码。把它加载到yaAGC里,再启动yaDSKY,你就能看到一个模拟的显示面板,上面有数字键盘、指示灯和几位数字显示器。这时候你可以输入指令,比如查看当前时间、修改寄存器值,甚至手动触发中断。这个过程本身就能让你对这套系统的实时性有一个直观感受——你按下一个键,到系统响应,中间有肉眼可感知的延迟,但在当年,这已经是极限了。
3. Executive 调度器:没有操作系统的操作系统
3.1 任务调度的核心逻辑:固定优先级 + 时间片轮转
AGC 的 Executive 调度器是整个系统的心脏。它要管理几十个任务,包括制导计算、导航更新、显示刷新、键盘扫描、报警监控等等。在只有 2K 字 RAM 的条件下,它不可能为每个任务维护复杂的上下文。所以它采用了一种非常巧妙的混合策略:固定优先级 + 时间片轮转。
具体来说,每个任务被分配一个固定的优先级,从 0 到 7,数字越小优先级越高。调度器维护一个“任务表”,每个表项只有几个字,记录任务的入口地址、执行周期和状态标志。当调度器运行时,它会从最高优先级开始扫描,找到第一个“就绪”的任务,然后跳过去执行。执行完成后,任务自己调用一个RETURN指令,把控制权交回调度器。
但这里有个问题:如果高优先级任务一直就绪,低优先级任务就永远得不到执行。所以调度器还引入了一个“时间片”机制。每个任务有一个计数器,每执行一次就减一,减到零后强制让出,调度器会把它移到“等待”状态,等下一个周期再唤醒。这个周期通常是几十毫秒,具体取决于任务的紧急程度。
我画了一个简化的调度流程,方便你理解:
- 调度器从优先级 0 开始扫描任务表。
- 找到第一个状态为“就绪”的任务。
- 检查该任务的时间片计数器,如果大于零,跳转执行。
- 任务执行完毕后,计数器减一,状态改为“等待”。
- 调度器继续扫描下一个任务。
- 如果所有任务都不可执行,调度器进入空闲循环,等待中断唤醒。
这个逻辑看起来简单,但实现起来有几个坑。第一个坑是任务表的扫描顺序。因为优先级是固定的,所以任务表必须按优先级排序,否则扫描逻辑就会出错。第二个坑是时间片计数器的重置时机。如果重置太早,任务会占用过多 CPU;如果重置太晚,任务响应会变慢。源码里对这两个点的处理非常精细,值得反复看。
3.2 中断处理:为什么它敢在中断里做那么多事
现代嵌入式开发有个金科玉律:中断处理要尽可能短,复杂逻辑放到主循环里。但 AGC 的代码反其道而行之,它的中断处理程序里做了大量工作,包括读取键盘、更新显示、处理报警、甚至执行部分制导计算。为什么它敢这么做?
原因有两个。第一,它的中断源非常有限,主要是时钟中断和键盘中断,不像现代系统有几十个外设中断。第二,它的中断优先级是硬件固定的,高优先级中断可以打断低优先级中断,但不会出现嵌套失控的情况。更重要的是,它的中断处理程序是可重入的,也就是说,同一个中断在处理过程中如果再次触发,系统会正确保存现场并重新进入,不会丢失事件。
源码里有一个细节让我印象很深:在时钟中断的处理程序里,它先保存累加器和寄存器,然后检查一个“中断嵌套计数器”,如果计数器大于零,说明当前已经在中断里了,它会直接跳到一个“快速返回”路径,只做最紧急的事情,比如更新时钟计数器,然后立刻返回。这个设计保证了即使中断频繁触发,系统也不会因为堆栈溢出而崩溃。
提示:如果你在自己的项目里要做类似的中断嵌套处理,一定要确保堆栈深度足够,并且有明确的嵌套层数限制。AGC 的堆栈只有几十个字,所以它的中断处理程序写得极其精简,每一行代码都要反复推敲。
3.3 任务间的通信:用“标志位”代替消息队列
在资源受限的系统里,任务间通信是个大问题。AGC 没有消息队列,没有信号量,甚至没有我们熟悉的“邮箱”机制。它用的是一种最原始但也最可靠的方式:共享内存 + 标志位。
具体来说,每个任务在 RAM 里有一块固定的数据区,其他任务可以直接读写这块区域。为了防止冲突,源码里定义了一套“标志位”规则:比如一个任务要更新导航数据,它会先检查一个“数据就绪”标志,如果标志为 0,说明数据正在被其他任务使用,它就等待;如果为 1,它就设置标志为 0,更新数据,然后再设回 1。
这套机制听起来很粗糙,但在实际运行中非常有效。因为 AGC 的任务数量有限,而且每个任务的执行时间都很短,所以冲突的概率很低。即使发生冲突,等待的时间也就是几个指令周期,对整体实时性影响不大。
我在自己的项目里试过类似的做法,结论是:在任务数量少于 10 个、每个任务执行时间小于 1 毫秒的场景下,共享内存 + 标志位的方案比消息队列更简单、更可靠。但一旦任务数量增加,或者执行时间变长,这种方案就会导致优先级反转和死锁,这时候就必须上更复杂的同步机制了。
4. 内存管理:4KB RAM 怎么装下整个登月程序
4.1 内存布局:每一块 RAM 都有明确用途
AGC 的 RAM 只有 2K 个字,也就是 4KB 左右。在这么小的空间里,它要存放所有变量、堆栈、缓冲区和临时数据。源码里的内存布局非常讲究,几乎每一块 RAM 都有明确的用途,没有浪费一个字。
大致来说,RAM 被分成这几个区域:
- 核心变量区:存放制导、导航、控制的核心状态变量,比如当前姿态、速度、位置。这些变量在整个任务期间都存在,不能被覆盖。
- 任务工作区:每个任务有自己的临时数据区,任务执行时使用,执行完毕后可以复用。
- 堆栈区:用于中断处理和子程序调用。AGC 的堆栈是向下生长的,从高地址向低地址扩展。
- 显示缓冲区:存放要显示在 DSKY 上的数字和指示灯状态。
- 输入缓冲区:存放键盘输入的指令和数据。
这种布局的好处是确定性。每个变量在编译时就已经确定了地址,运行时不会动态分配内存,也就不会出现内存碎片和分配失败的问题。对于载人航天这种容错率极低的场景,确定性比灵活性重要得多。
4.2 覆盖技术:用时间换空间的经典案例
72KB 的 ROM 听起来不大,但在当年已经算是“海量”了。问题是,制导程序、导航程序、显示程序、报警程序加起来,代码量超过了 ROM 的容量。怎么办?AGC 的工程师用了一个经典的技术:覆盖。
覆盖的思路很简单:把程序分成多个“段”,每个段在 ROM 里占一块固定区域。运行时,根据需要把不同的段加载到同一块 RAM 区域执行。这样,虽然 ROM 里存了多份代码,但 RAM 里只需要一块执行区域。
具体到 AGC,它的覆盖机制是这样的:ROM 被分成多个“存储体”,每个存储体有固定的地址范围。程序执行时,通过一个“存储体切换”指令,把不同的存储体映射到 CPU 的地址空间。这样,CPU 看到的地址空间是连续的,但实际访问的 ROM 区域可以动态变化。
这个技术在现代系统里也有应用,比如一些微控制器的“分页”机制,或者操作系统的“虚拟内存”。但 AGC 的实现更简单、更直接,因为它不需要页表,也不需要地址转换,只需要一个存储体寄存器。
注意:覆盖技术虽然节省空间,但会增加程序复杂度。你在自己的项目里用覆盖时,一定要确保切换逻辑正确,否则会出现“取指错误”或“数据错乱”。AGC 的源码里对存储体切换有严格的检查,每次切换后都会验证当前存储体是否正确。
4.3 堆栈管理:为什么它敢用固定大小的堆栈
现代操作系统通常会给每个线程分配一个较大的堆栈,比如 8MB 或 1MB,以防止递归过深或局部变量过多导致溢出。但 AGC 的堆栈只有几十个字,而且大小固定,不能动态扩展。为什么它敢这么做?
因为它的代码是静态分析过的。在编译时,工程师会计算每个子程序的最大堆栈深度,然后确保总深度不超过堆栈容量。如果某个子程序的堆栈需求太大,就会被打回重写,或者拆分成多个小函数。
这种做法的好处是确定性。运行时不会出现堆栈溢出,因为编译时已经验证过了。坏处是灵活性差,不能写递归函数,也不能在堆栈上分配大数组。
我在做工业控制项目时,也采用了类似的做法:所有函数的堆栈深度都在编译时计算,运行时用静态分析工具验证。虽然麻烦一点,但换来的是极高的可靠性。对于安全关键系统,这种“麻烦”是值得的。
5. 人机交互:DSKY 的设计哲学与容错机制
5.1 为什么用数字键盘而不是全键盘
DSKY 是宇航员和 AGC 交互的唯一界面。它有一个数字键盘、几个功能键、几行数字显示器和一排指示灯。没有全键盘,没有鼠标,没有触摸屏。为什么设计得这么“简陋”?
因为可靠性。在太空环境里,每一个额外的按键、每一根额外的连线,都是潜在的故障点。数字键盘只有 10 个数字键和几个功能键,电路简单,故障率低。而且数字键盘的输入是结构化的,宇航员只能输入数字和预定义的指令码,不能输入任意文本,这大大降低了误操作的概率。
源码里对键盘输入的处理非常严格。每次按键都会触发一个中断,中断处理程序会读取键码,然后检查这个键是否在当前上下文中有效。如果无效,它会忽略这次按键,并可能触发一个报警。这种“严格校验”的思路,在现代的 API 设计和用户输入处理里也很常见。
5.2 显示刷新:如何保证数字不闪烁
DSKY 的显示器是数字式的,由多个七段数码管组成。在低刷新率下,数码管会闪烁,影响阅读。AGC 的解决方案是定时刷新 + 双缓冲。
具体来说,显示缓冲区在 RAM 里有两份:一份是“当前显示”,一份是“待显示”。当程序要更新显示时,它先写入“待显示”缓冲区,然后设置一个标志。时钟中断处理程序会定期检查这个标志,如果标志有效,就把“待显示”的内容复制到“当前显示”,然后刷新硬件。
这个机制保证了显示更新的原子性,不会出现“一半新一半旧”的情况。而且因为刷新是定时进行的,刷新率稳定,不会闪烁。
我在做嵌入式 GUI 时也用过类似的双缓冲机制,效果很好。但要注意,双缓冲会占用双倍的内存,在 RAM 紧张的系统里要谨慎使用。AGC 的显示缓冲区很小,只有几个字,所以双缓冲的开销可以接受。
5.3 报警系统:当程序出错时,它怎么告诉宇航员
AGC 有一套复杂的报警系统。当程序检测到异常时,它会触发一个报警码,显示在 DSKY 上。报警码是一个两位数字,宇航员需要查手册来理解它的含义。比如“1202”报警表示“执行溢出”,“1201”表示“等待任务队列溢出”。
这些报警码的设计非常讲究。首先,它们都是数字,方便在数字显示器上显示。其次,它们的编码有规律,比如“12xx”系列都和 Executive 调度器有关,“21xx”系列和制导有关。这样,宇航员即使记不住具体含义,也能大致判断问题出在哪个子系统。
更重要的是,报警系统是分级的。有些报警只是警告,程序会继续运行;有些报警是严重错误,程序会暂停当前任务,等待宇航员干预。这种分级机制保证了系统在出现小问题时不会过度反应,在出现大问题时能及时止损。
提示:如果你在设计一个高可靠性系统,建议也引入类似的报警分级机制。不要把所有异常都当成致命错误,也不要对所有异常都视而不见。分级处理能让系统在异常情况下保持最大的可用性。
6. 自己动手跑一遍:从编译到模拟的完整踩坑记录
6.1 环境准备中最容易忽略的三个细节
如果你决定自己编译并运行这套代码,有几个细节很容易被忽略,但一旦忽略就会卡住。
第一个是文件编码。AGC 的源码文件里有一些特殊字符,比如用于表示伪指令的符号,这些字符在不同的编码格式下会显示成乱码。建议统一用 UTF-8 编码打开,如果看到乱码,不要强行修改,先检查编码设置。
第二个是换行符。源码文件是在老式系统上创建的,用的是 CRLF 换行。在 Linux 下编译时,如果换行符不对,汇编器可能会报“无法识别的字符”错误。可以用dos2unix工具转换一下。
第三个是路径长度。这套工具链对路径长度比较敏感,如果源码放在很深的目录里,编译时可能会报“文件找不到”。建议把源码放在根目录附近,比如/opt/apollo或~/apollo。
6.2 编译过程中的常见报错与修复
编译时最常见的报错是“未定义的符号”。这通常是因为汇编器找不到某个宏或伪指令的定义。解决方法是检查源码目录里是否有HEADER.agc或类似的包含文件,确保汇编器的搜索路径包含了这些文件。
另一个常见报错是“地址溢出”。AGC 的地址空间只有 32K,如果某个段的代码太长,超过了存储体的容量,就会报这个错。这时候你需要调整存储体分配,把一些代码移到其他存储体里。
还有一个报错是“非法指令”。这通常是因为汇编器版本不对,或者源码里有针对特定硬件版本的指令。建议用官方推荐的yaYUL版本,不要用太新或太旧的版本。
6.3 在模拟器里跑起来之后,怎么验证它真的在工作
编译成功后,你会得到一个二进制文件。把它加载到yaAGC里,然后启动yaDSKY。如果一切正常,你会看到 DSKY 的显示面板亮起来,上面显示“00000”或类似的初始状态。
这时候你可以做几个验证:
- 按数字键,看显示是否更新。
- 输入一个简单指令,比如“R 0000”,看是否能读取内存地址 0000 的值。
- 触发一个时钟中断,看时钟计数器是否增加。
如果这些都能正常工作,说明你的编译和模拟环境已经跑通了。接下来你可以尝试更复杂的操作,比如加载一个制导程序,看它是否能正常执行。
注意:模拟器里的时间和真实时间不完全一致。
yaAGC默认会以尽可能快的速度执行指令,所以你会看到时钟计数器飞快地增加。如果你想模拟真实的时间流逝,需要在模拟器里设置一个“时间缩放”参数。
7. 这套六十年前的代码,对今天的开发者还有什么用
7.1 资源受限下的设计取舍:少即是多
AGC 的代码给我最大的启发是:在资源受限的环境里,简单比灵活更重要。它没有动态内存分配,没有复杂的任务调度算法,没有花哨的通信机制。但正是这种“简陋”,让它的行为完全可预测,不会出现现代系统里常见的“内存泄漏”“死锁”“优先级反转”等问题。
我在做工业控制器时,经常面临一个选择:是用现成的 RTOS,还是自己写一个简单的调度器?每次我都会想起 AGC 的 Executive。如果任务数量少、实时性要求高、可靠性要求极高,自己写一个固定优先级的调度器往往比用 RTOS 更合适。因为 RTOS 的通用性带来了额外的复杂度和不确定性,而这些在安全关键系统里都是风险。
7.2 容错设计的核心思路:假设一切都会出错
AGC 的代码里充满了容错设计。每个输入都经过校验,每个输出都有备份,每个关键操作都有超时检查。这种“假设一切都会出错”的思路,是现代高可靠性系统设计的基石。
比如,它的键盘输入处理程序会检查按键是否在有效范围内,如果不在,就忽略并报警。它的显示刷新程序会检查显示缓冲区是否被意外修改,如果被修改,就重新加载默认值。它的任务调度器会检查每个任务是否在规定时间内完成,如果没有,就强制终止并报警。
这些检查看起来多余,但在实际运行中,它们确实捕获了很多潜在故障。我在自己的项目里也引入了类似的检查,虽然增加了代码量,但系统的稳定性明显提升。
7.3 从汇编到高级语言:哪些思想可以迁移
你不需要用汇编来写现代项目,但 AGC 的一些设计思想可以迁移到高级语言里:
- 固定优先级调度:在 Python 或 Java 里,你可以用线程优先级和定时器来实现类似的效果。
- 共享内存 + 标志位:在 C++ 里,你可以用
volatile变量和原子操作来实现无锁通信。 - 双缓冲显示:在前端开发里,你可以用虚拟 DOM 或离屏 Canvas 来实现类似的效果。
- 分级报警:在服务端系统里,你可以用日志级别和告警策略来实现类似的分级处理。
这些思想的核心是确定性和可预测性。现代系统往往追求灵活性和扩展性,但在某些场景下,确定性和可预测性才是最重要的。AGC 的代码提醒我们,不要为了追求“先进”而牺牲“可靠”。
8. 我在实际项目中踩过的坑与总结的经验
8.1 不要低估静态分析的价值
AGC 的代码之所以能在极端资源下稳定运行,很大程度上归功于编译时的静态分析。工程师会计算每个函数的堆栈深度、每个任务的执行时间、每个变量的生命周期。这些分析在运行时是不做的,因为运行时资源太宝贵了。
我在自己的项目里也尝试过静态分析,但一开始觉得太麻烦,就跳过了。结果上线后出现了堆栈溢出,排查了很久才发现是一个递归函数导致的。后来我引入了静态分析工具,虽然增加了编译时间,但运行时的稳定性大幅提升。
提示:如果你的项目对可靠性要求高,建议在编译阶段就做静态分析。不要等到运行时出问题了再回头补。
8.2 中断处理里的“快慢分离”原则
AGC 的中断处理程序里有一个很聪明的设计:它把中断处理分成“快路径”和“慢路径”。快路径只做最紧急的事情,比如保存现场、更新计数器,然后立刻返回。慢路径做复杂的事情,比如数据处理、状态更新,但慢路径不是在中断里执行的,而是被调度到主循环里执行。
这个“快慢分离”原则在现代系统里也很适用。比如在网络驱动里,中断处理程序只负责接收数据包,然后把数据包放到队列里,由内核线程慢慢处理。这样中断处理时间短,系统响应快。
我在做串口通信时也用了这个原则:中断里只把接收到的字节放到环形缓冲区,主循环里再解析协议。效果很好,即使波特率很高,也不会丢数据。
8.3 文档和注释:为什么 AGC 的代码那么难读
AGC 的源码里注释很少,而且很多注释是缩写或代号,没有上下文根本看不懂。这不是因为工程师懒,而是因为当年的存储介质太贵,注释也占空间。但这也导致了一个问题:后人读代码非常困难,很多逻辑要靠猜。
我在自己的项目里,始终坚持写注释,尤其是那些“为什么这么做”的注释。因为“怎么做”代码本身已经说明了,但“为什么这么做”只有注释能说清楚。AGC 的代码让我更加坚信:注释不是写给机器看的,是写给未来的自己和其他人看的。
8.4 模拟器不是万能的,但它是你最好的朋友
在模拟器里跑通代码,不代表在真实硬件上也能跑通。AGC 的模拟器虽然很精确,但它毕竟不是真实的 AGC。真实硬件有温度漂移、电压波动、电磁干扰,这些在模拟器里都体现不出来。
但模拟器仍然是你最好的朋友。因为它可以让你快速验证逻辑、调试代码、复现问题。我在做嵌入式开发时,总是先在模拟器里跑通,再上真实硬件。这样可以节省大量时间,避免因为硬件问题而误判软件逻辑。
注意:模拟器里的时序和真实硬件可能有差异。如果你的项目对时序敏感,一定要在真实硬件上做最终验证。
9. 如果你想深入,可以从这几个方向继续挖
9.1 阅读源码的推荐路径
AGC 的源码有几千行,全部读完不现实。我建议按这个顺序读:
- Executive 调度器:先看任务表定义和调度循环,理解任务是怎么被选中的。
- 中断处理程序:看时钟中断和键盘中断的处理流程,理解中断嵌套和现场保存。
- 显示刷新程序:看双缓冲的实现,理解显示更新的原子性。
- 制导程序:看核心算法,理解如何在有限资源下做复杂计算。
每读一个模块,都试着画一个流程图,标注关键变量和跳转条件。这样能帮你快速建立整体印象。
9.2 用现代语言重写核心模块
如果你想更深入地理解这套代码,可以尝试用 C 或 Rust 重写 Executive 调度器。不需要重写全部,只要实现任务表、调度循环、时间片计数器和中断处理就行。重写的过程中,你会遇到很多在汇编里不明显的问题,比如任务状态的一致性、中断和主循环的竞争条件、时间片计数器的溢出处理。
我重写过一次,用了大约 500 行 C 代码。虽然功能比原版简化了很多,但核心逻辑是一致的。重写之后,我对调度器的理解深刻了很多,再回头看汇编代码,很多之前看不懂的地方都豁然开朗。
9.3 把 AGC 的设计思想应用到现代项目
最后,不要只把 AGC 当成历史文物。它的设计思想——固定优先级调度、共享内存通信、双缓冲显示、分级报警、静态分析——都可以应用到现代项目里。你可以从一个小模块开始,比如在你的嵌入式项目里引入固定优先级调度,或者在你的服务端项目里引入分级报警。试过之后,你会发现这些“老古董”思想依然很有生命力。
我在最近的一个机器人控制项目里,就借鉴了 AGC 的 Executive 调度器,用固定优先级 + 时间片的方式管理运动控制、传感器读取和通信任务。效果很好,系统响应稳定,没有出现任务饥饿或优先级反转的问题。这让我更加确信:好的设计思想是超越时代的。