早期的 FreeBSD 移植
作者:DOUG RABSON
译者:basebyte
自诞生之初,FreeBSD 便专注于为 i386 架构提供稳固的支持。这种 PC 平台普遍易得且相对廉价,将现有资源集中于此有助于使 FreeBSD 在 i386 上保持稳定和高性能。但是,几年后,我们决定扩大支持范围;第一个目标是 DEC 的 Alpha 平台,因为它是 64 位架构的平台,是一个不错的选择。几年之后,我们又增加了对 IA-64 的支持,当时 Intel 将其定位为 i386 的继任者。
在 1997 年 5 月初,Jordan Hubbard 征求志愿者来将 FreeBSD 移植到 DEC Alpha 平台。Alpha 是一种 64 位的加载/存储体系结构,与 i386 截然不同,它拥有更多的寄存器和 RISC 指令集。当时的硬件使用了我们熟悉的 PCI 和 ISA 总线接口。
我自愿参加了这个项目。DEC 的 Clem Cole 借给了我们一些硬件,这些硬件于 1997 年 7 月运抵(除了 Peter Wemm 的机器,我记得他的机器好像在海关丢失了)。从那时起,我开始逐渐意识到这个项目需要多少工作量,并且需要在许多不同的领域进行改进。
设备驱动程序
在 FreeBSD 中,特别是针对 ISA 的设备驱动程序,需要进行修改以支持新的 Alpha 架构,因为硬件访问方式有很大的不同。为了使这项工作顺利进行,并在 i386 和 Alpha 平台之间尽可能共享代码,我们需要一个抽象层。
我对此有一些想法,这些想法最终演变成了 FreeBSD 的 newbus 框架。我的计划是从顶层系统设备总线开始自动发现设备,对于 i386 和 Alpha 来说,顶层系统设备总线通常是指 PCI 和 ISA。内核将动态构建设备树,然后将这些设备与可用的驱动程序进行匹配。我还希望能够将这个功能与早先开发的内核链接器(KLD)结合起来,以允许系统在初始引导后添加驱动程序。我在 1997 年末为此开发了一个原型,但并没有进展到支持任何"真实"的硬件驱动程序的程度。
文件格式
在 1997 年,FreeBSD 仍在使用 a.out 文件格式,这是一种非常简单的 32 位格式。而当时大多数其他类 Unix 系统已经在使用 ELF 格式,它提供了更多的灵活性,并且已经支持包括 Alpha 在内的 64 位平台。
改用新格式需要修改编译系统,以支持新格式,并支持动态链接,动态链接是通过运行时链接器(RTLD)在用户区实现的。在 1998 年初的几个月里,包括 John Polstra、Jordan Hubbard、Peter Wemm 和我在内的许多人都在为此而努力。
改用新的文件格式涉及对构建系统的修改,以支持新格式和动态链接。动态链接在用户空间中通过运行时链接器(RTLD)来实现。在 1998 年初的几个月里,包括 John Polstra、Jordan Hubbard、Peter Wemm 和我在内的许多人都在为此而努力。
启动
最初这是该项目的一个不确定领域。Alpha 的"预引导"环境非常分散,Digital Unix (DUX)和 VMS 使用 SRM 控制台,NT 使用 AlphaBIOS,Linux 则同时使用这两者以及它们自己的 MILO。
在 Alpha 上,任何操作系统都需要一种称为 PALcode 的东西来处理虚拟页转换、缓存、中断以及用户模式和内核模式之间的转换。DUX、NT 和 VMS 都有各自的 PALcode 变体。尽管 DUX 的 PALcode 可能是我们最好的选择,但我们并不清楚是否可以在没有昂贵的许可证的情况下使用它。后来,我们支持的所有 Alpha 硬件都附带有支持 DUX PALcode 的 SRM 控制台,因此这个问题不再是问题。
i386 引导启动代码的大小被限制在 7.5 KB。这个限制来自于 UFS 文件系统格式,它为引导程序保留了 8KB 的区域,而我们需要其中的 512 字节的扇区来让 PC BIOS 进行引导。这只够将 a.out 格式的内核文件从根文件系统读入内存并启动它。
Alpha 也有类似的限制;SRM 控制台使用磁盘的第一个扇区来标识要加载的一系列连续扇区,这样与 i386 一样,UFS 引导区只剩下 7.5 KB 的空间。在 Alpha 上,由于 RISC 架构的代码密度较低以及 ELF 格式所需的额外复杂性,受限的 7.5KB 空间可能不能够装下完整的引导程序。最终我们重新编写了引导程序,使得这 7.5KB 的引导区能够加载一个更大的引导程序(在现代的 FreeBSD 系统中称为/boot/loader)。这给了我们足够的灵活性来完全支持在 i386 和 Alpha 上启动 ELF 格式的内核,以及其他新特性,如预加载内核模块、网络启动等等。Mike Smith 负责多阶段引导的工作,Peter Wemm 负责内核模块预加载。不知从什么时候开始,Forth 解释器被添加进来了---我想这是 Jordan Hubbard 的功劳。
用户空间
在我们开始这个项目时,FreeBSD 的源代码树并没有为简便的交叉编译而设置。这使得构建用户空间的实用程序具有一些挑战性。John Birrell 致力于在 NetBSD 主机上构建大部分 FreeBSD 源代码树,并成功运行了一个具有相当完整的 FreeBSD 用户空间的 NetBSD 内核系统。
NetBSD 的系统调用接口与 FreeBSD 略有不同,因此 John 早期的用户空间程序开发工作使用了 NetBSD 的 ABI 接口。原生的 FreeBSD 内核需要使用 FreeBSD ABI 的实用程序,但这是一个重要的进展。一旦我们有了一个可用的内核,从混合的 NetBSD/FreeBSD 系统迁移到一个完整的原生 FreeBSD 系统就相对比较简单了。
内核
这可能是整个项目中最大的一部分,它涉及填充内核中的所有与机器相关的部分,为虚拟内存、中断处理、进程上下文切换等提供底层支持。
我通过构建一个 Alpha 交叉编译器,尝试构建内核,看哪些部分无法编译通过,然后填补这些空白,要么使用空的存根,要么从 NetBSD 中导入代码,因为那些代码与 FreeBSD 的代码非常相似。这是一个相当繁琐的过程,花了好几天时间,但最终却得到了一个无法运行的二进制内核文件。
接下来,花费更长时间的移植工作是尝试运行这个内核,看它运行到哪个阶段出现问题,然后解决问题后再进行下一次尝试。为了运行每个测试,我使用了一个名为 SimOS 的工具。它模拟了一台基于 Alpha 的计算机,并配有磁盘和串行端口等模拟硬件。SimOS 支持使用 gdb 对模拟内核进行调试;这非常有帮助,因为我可以逐步执行非常早期的内核初始化过程,比如设置内核虚拟内存等,然后再进入与机器无关的初始化序列。
为了缩短移植过程,我在适当的地方使用了 NetBSD/alpha 的代码。不幸的是,在一些地方我忽略了 NetBSD 的版权信息。我不得不在提交代码后,在公开场合对这些问题进行更正,这让我感到相当尴尬。这也是 FreeBSD 提交历史被修改的极少数时刻之一---我们删除了版权信息不正确的修订。
在虚拟内存支持方面,使用 NetBSD 的代码是行不通的,因为 FreeBSD 在这方面有很大的不同。Alpha 的页表与 i386 类似,都是采用基于树的三层结构(而 i386 当时使用的是两层结构)。我复制了 i386 的代码并进行了修改,以增加了额外的层级。
Alpha 的初步支持于 1998 年 7 月提交,随后几个月里又推出了对 SimOS 仿真器和真正硬件的支持。FreeBSD 3.0 的发布说明中提到了这一点:“对 DEC Alpha 架构的移植已进入“ALPHA”(haha)状态。”
这个项目始于 2000 年,当时 Paul Saab 在 Usenix ATC 年度技术会议上带来了一套 IA-64 的文档,并询问我是否有兴趣将 FreeBSD 移植到这个新平台。当时,雅虎是 i386 上 FreeBSD 的大规模用户,他们的一些工作负载已经达到了 32 位平台的限制。
IA-64 架构在多个方面都非常有趣。其指令编码采用 128 位指令束,每个指令束最多包含三条可同时执行的 41 位指令。这使得它具有大型寄存器集,包括 128 个通用寄存器和 128 个浮点寄存器。
通用寄存器分为两组---32 个“静态”寄存器和 96 个“堆叠”寄存器。处理器会从一个大型寄存器池中分配这些寄存器,并在函数调用和返回时自动保存和恢复。每个寄存器是 64 位,外加一个用于推测执行的 NaT("Not a Thing")位。
条件执行通过 64 个谓词寄存器实现,每个寄存器都是一个比特位,保存比较指令的结果。每条指令都可以根据谓词寄存器的值进行条件执行。
使用 8 个分支寄存器支持间接跳转(例如函数指针),这有助于进行分支预测。
虚拟内存管理由虚拟散列页表(VHPT)控制,该表包含可能的虚拟地址到物理地址映射的子集。软件 TLB 缺失处理程序用于查找 VHPT 中不存在的映射。VHPT 支持两种格式,一种是 "短 "格式,可用于模拟传统的树形页表;另一种是 "长 "格式,即包含冲突链的简单哈希表。
启动
IA-64 硬件使用了 EFI 预启动环境。我在多级引导加载器中添加了对 EFI 的基本支持。在 IA-64 的 EFI 环境中,程序是可重定位的;这需要在 EFI 程序本身中进行处理,而这在调试时可能会很困难。
用户空间
移植 FreeBSD 用户空间工具和实用程序的工作相当简单,因为在这个时候,FreeBSD 构建系统已经支持交叉编译,只需要添加 IA-64 版本的底层库代码,比如字符串比较、内存复制和系统调用等。
内核
我们很幸运能够使用 HP IA-64 仿真器(SKI),Marcel Moolenaar 利用它制作了 FreeBSD 移植程序。它包括一个指令级调试器,对于调试内核早期初始化和陷阱处理非常有帮助。
与 Alpha 相比,内核的移植稍微困难一些。这次,没有其他可供参考的 BSD 移植版本,所以所有的底层支持都是新代码。由于额外的寄存器状态和推测执行以及堆叠寄存器的复杂性,IA-64 架构要求有两个堆栈,一个用于寄存器,另一个用于常规数据,这使得陷阱处理比大多数其他架构要困难得多。
长格式的 VHPT 最终成为 FreeBSD 的虚拟内存系统的一个合理选择。机器无关的虚拟内存系统通过向平台的 pmap 系统发出请求来建立虚拟地址到物理地址的映射。这些映射就是直接添加到 VHPT 中的。
32 位兼容性
在移植过程中,我们使用 Perforce 作为源代码控制工具,当时只有 i386 平台的二进制版本可用。
我希望能在移植过程中在目标平台上使用这个二进制文件,因此我最终实现了 i386 的兼容性,利用了 IA-64 处理器中内置的 i386 支持。这建立在早期 Linux 和 SVR4 仿真工作的基础上,这些工作明确地将系统调用的 ABI 和实现分开。
传统
针对 Alpha 的移植工作,促使了大量必要的支持性开发,这些开发帮助形成了现代的 FreeBSD 内核。从 a.out 到 ELF 格式的过渡是 Alpha 移植的必要步骤,但由于 ELF 迅速成为事实上的标准,使得所有平台上都不再使用 a.out,这避免了我们花费大量精力来支持和扩展过时的格式。事实证明,多阶段引导加载器是一个灵活的平台,它使新的架构移植变得更容易,并支持从现代文件系统(如 OpenZFS)引导。newbus 设备框架促进了驱动程序在不同架构之间的兼容性,并支持动态设备发现,这在设备可随时添加或移除的现代系统中是必需的。
增加对 Alpha 的支持迫使我们解决了内核和用户空间的 64 位兼容性问题。加载/存储架构还暴露了一些其他问题,例如假设对内存进行读-改-写操作以设置标志或递增计数器的操作不会受到硬件中断的影响。为了解决这个问题,我们在内核中添加了一组“原子”操作。John Baldwin 对原子框架进行了扩展,以支持 IA-64 的获取/释放语义,并广泛用于支持多 CPU 平台。
IA-64 移植的灵感来自于摆脱 32 位 i386 平台限制同时保留与传统软件的兼容性的需要。虽然这些目标已经实现,但该平台本身并没有达到更简单的 i386 架构的性价比。IA-64 最终在大规模超级计算中找到了自己的定位,但它并不适合大多数 FreeBSD 工作负载,并被 AMD 对 i386 架构的 x86-64 扩展所取代,后者在现代计算环境中无处不在。
此后,FreeBSD 移除了对这两个平台的支持。Alpha 架构是 Digital 与 Compaq 合并后的牺牲品,尽管它一直作为产品存在,直到 2007 年才停止生产。FreeBSD 于 2006 年移除了对 Alpha 平台的支持。对 IA-64 的支持持续时间稍长一些;Marcel Moolenaar 多年来对支持这个平台的多处理器和 NUMA 变体进行了许多改进。2014 年,FreeBSD 移除了对 IA-64 平台的支持,而该平台也于 2021 年停产。
DOUG RABSON 是一名软件工程师,拥有超过三十年的经验,历经上世纪 80 年代的 8 位文本冒险游戏到本世纪 20 年代的每秒 TB 级分布式日志聚合系统。自 1994 年以来,他一直是 FreeBSD 项目的成员和贡献者,目前正致力于改进 FreeBSD 对现代容器编排系统的支持,如 podman 和 kubernetes 。