旧世界与新世界(底层细节)
这些资料主要面向那些参与 LoongArch 内核研发、发行版集成工作等底层工作的开发者们, 介绍新旧世界问题的技术细节与已知的兼容方案等。
已知的兼容方案有:
导言
虽然新旧世界的程序几乎无法兼容使用,但是这并不意味着新旧世界存在着特别本质的区别。 恰恰相反,二者之间存在着很多共同的特点,使得新旧世界的兼容性变得可能。 事实上,不在新旧世界的差异点内的特性就是他们的共同点。 为了更好地理解新旧世界的差异与共同点,本文仍然会列举一些二者之间较为重要的共同点。
本文将从内核和用户态两个方面来讨论新旧世界的差异与共同点, 主要涵盖的是其对外呈现的特性的异同,而较少涉及其内部实现的差异。 本文不涉及到新旧世界在编译系统方面的异同,因为一般而言,开发者一定会使用一整套同一世界的工具链。 本文也不会涉及到新旧世界的非技术层面的差异,如商业策略、社区合作等。
考虑到目前龙芯生态都在使用 Linux 内核与 glibc C 运行时,且所有「旧世界」系统 都采用此搭配, 简明起见,当我们提到「内核」或「用户态」、「libc」,都默认为指代 Linux 或 glibc。
内核
内核上,对外暴露的接口可以被分为两方面,一个是引导协议,可以被视为上游接口; 另一个是系统调用,可以被视为下游接口。
引导协议
从表面上看,旧世界发行版提供的内核映像文件是 ELF 格式的,需要搭配旧世界移植的 GRUB2 引导器才能启动。 新世界提供的内核映像文件则是 PE 格式的 EFI 应用——EFI Stub,可以直接由 UEFI 固件启动,也可以由新世界移植的 GRUB2 引导器启动。
从固件向内核传参的具体细节上看,两个世界的 Linux 内核期待从上一级引导程序接受的数据结构也有不同。 见以下示意图。
- 方形的节点表示 EFI 应用程序,是 PE 格式的映像文件。
- 圆角的节点表示 ELF 格式的映像文件。
- 有向的边表示控制权的交接顺序,边上的文字代表被交接的数据结构。
首先,新旧世界的 UEFI 固件,如 EDK2,虽然大部分行为都相同,但就传参而言 ,存在以下两点明显区别。
- 各类 UEFI 表格中的指针。
-
旧世界:都是形如
0x9xxx_xxxx_xxxx_xxxx
的虚拟地址。这「碰巧」是龙芯 Linux 在 MIPS 时代,由于架构规定而被迫使用的固定 1:1 映射窗口之一。 快进到 LoongArch 时代,这「碰巧」仍然是旧世界内核,与未全面切换至 TLB 的新世界内核,所共同约定使用的「一致可缓存」直接映射窗口(DMW)。
「一致可缓存」是在《龙芯架构参考手册》卷一 2.1.7 节「存储访问类型」中定义的术语,又叫 Coherent Cached 或 CC。
讽刺的是:虽然在内核拿到控制权之前,固件已经做了相同的 DMW 配置,但在旧世界内核入口点,仍然重复配了一遍…… 这显然意味着其中有一次是多余的!
-
新世界:都是物理地址。
固件不应该,也确实没有替操作系统操心,甚至在某种意义上半强迫操作系统一定要采用某种特定的 DMW 配置。
-
- ACPI 数据结构。
-
旧世界:个别表格,包括但不限于 MADT,遵循的是 ACPI 6.5 定稿前的早期龙芯标准。
这些数据结构与 ACPI 6.5 不兼容:例如,新世界内核见到旧世界 MADT,便会认为此系统有 0 个 CPU, 而最终启动失败。
-
新世界:遵循 ACPI 6.5 或更新的版本。
-
其次,新世界 Linux 期待直接解析 UEFI 系统表(system table)。
旧世界 Linux 则与前代(MIPS)龙芯内核一样,
期待接受龙芯自行定义的「BPI」结构体:struct bootparamsinterface
(在 Loongnix 的 Linux 源码中,叫作 struct boot_params
,本质相同):
在旧世界,将 UEFI system table 相关信息转换为 BPI 结构,是引导器的职责。
再具体一些,控制权交接至 Linux 之后,早期引导流程的差异见以下示意 图。
实线的边表示过程调用。有文字注解的虚线边表示数据流动,无文字注解的虚线边则表示有所简化的过程调用。
图中未包含新世界 FDT 流程的详细介绍,因为它的真实逻辑比较复杂,您可以先自行查阅代码。
但简单来说:新世界传递 FDT 根指针的方式是在 UEFI 系统表中包含一条类型为 DEVICE_TREE_GUID
即 b1b621d5-f19c-41a5-830b-d9152c69aae0
,值为此指针的物理地址的记录。
因此,新世界 Linux 无论是以 ACPI 还是 FDT 方式启动,其传参方式都遵循 UEFI 约定:奇妙的统一!
同时也可发现旧世界不具备这种统一性质了。
新旧世界 Linux 对三个固件参数的解释也有如下不同:
固件参数 | 旧世界解释 | 新世界解释 |
---|---|---|
fw_arg0 | int argc 内核命令行的参数个数 | int efi_boot EFI 引导标志,0 代表 EFI 运行时服务不可用,反之可用 |
fw_arg1 | const char *argv[] 内核命令行参数列表的虚拟地址,如同用户态的 C main 函数一般使用 | const char *argv 内核命令行的物理地址,单个字符串 |
fw_arg2 | struct boot_params *efi_bp BPI 表的虚拟地址 | u64 efi_system_table UEFI 系统表的物理地 址 |
最后,在 2023 年,龙芯为其旧世界 Linux fork 添加了新世界引导协议的兼容支持,
并已经将此支持全面推送至下游商业发行版如 UOS、麒麟等。
这种内核的 EFI stub 形态就支持了新世界方式的引导。
如图所示,此时在内核中走到的代码路径都与新世界本质相同了:基于对 fw_arg0
参数的灵活解释——假定所有「正常的」内核引导,其内核命令行参数个数都超过 1——而得以在实践中无歧义地区分两种引导协议。
此外,2023 年龙芯也为新世界 Linux 制作了旧世界引导协议的兼容支持,并向一些有商业背景的、 需要支持无新世界固件机型的社区发行版,如 openAnolis 等推送了;例如 openAnolis 的相关提交。 (请注意:此功能暂未由第三方社区成员验证。) 这种排列组合暂未在上文的描述中体现。
系统调用
系统调用的 ABI 框架上,新旧世界的内核是一致的,这包括了调用系统调用的方式(通过 syscall 0
指令)、
系统调用号、参数、返回值的寄存器分配;大多数系统调用的编号是相同的;多数系统调用所接受的结构体的定义也是相同的。
本节介绍那些导致新世界不兼容旧世界系统调用的不同点。
新世界废弃的系统调用
LoongArch 是 Linux 内核最新引入的架构,因此决定不再提供一些较老的系统调用; 而这些系统调用在旧世界是存在的。这样的系统调用有:
系统调用名称 | 编号 |
---|---|
newfstatat | 79 |
fstat | 80 |
getrlimit | 163 |
setrlimit | 164 |
可以通过直接补充上述系统调用来实现这一部分的兼容。
信号数量相关
新世界的 NSIG
宏(即最多允许的信号数量)定义为 64,而旧世界的 NSIG
宏定义为 128,
这直接导致了 sigset_t
结构体的大小不同,进而导致 sigaction
结构体的大小不同。
与此同时,一些与 sigset_t
相关的系统调用要求传入 sigsetsize
参数
(即 sigset_t
结构体的长度),并要求传入的值与内核中定义的相同,
因此下列新世界中的系统调用无法处理旧世界用户态的调用:
系统调用名称 | 编号 |
---|---|
rt_sigsuspend | 133 |
rt_sigaction | 134 |
rt_sigprocmask | 135 |
rt_sigpending | 136 |
rt_sigtimedwait | 137 |
pselect6 | 72 |
ppoll | 73 |
signalfd4 | 74 |
epoll_pwait | 22 |
epoll_pwait2 | 441 |
其中,rt_sigprocmask