Alison Chaiken 发布的文章

熟悉 ABI 的概念、ABI 稳定性的重要性以及 Linux 稳定 ABI 中包含的内容。
LCTT 译注:昨天,AlmaLinux 称将 放弃 对 RHEL 的 1:1 兼容性,但将保持对 RHEL 的 ABI 兼容,以便在 RHEL 上运行的软件可以无缝地运行在 AlmaLinux 上。可能有的同学对 ABI 的概念还不是很清楚,因此翻译此文供大家了解。

许多 Linux 爱好者都熟悉 Linus Torvalds 的 著名告诫:“我们不破坏用户空间”,但可能并非每个听到这句话的人都清楚其含义。

这个“第一规则”提醒开发人员关于应用程序的二进制接口(ABI)的稳定性,该接口用于应用程序与内核之间的通信和配置。接下来的内容旨在使读者熟悉 ABI 的概念,阐述为什么 ABI 的稳定性很重要,并讨论 Linux 稳定 ABI 中包含了哪些内容。Linux 的持续增长和演进需要对 ABI 进行变更,其中一些变更引起了争议。

什么是 ABI?

ABI 表示 应用程序二进制接口 Applications Binary Interface 。理解 ABI 概念的一种方式是考虑它与其他概念的区别。对于许多开发人员来说, 应用程序编程接口 Applications Programming Interface (API)更为熟悉。通常,库的头文件和文档被认为是其 API,以及还有像 HTML5 这样的标准文档。调用库或交换字符串格式数据的程序必须遵守 API 中所描述的约定,否则可能得到意外的结果。

ABI 类似于 API,因为它们规定了命令的解释和二进制数据的交换方式。对于 C 程序,ABI 通常包括函数的返回类型和参数列表、结构体的布局,以及枚举类型的含义、顺序和范围。截至 2022 年,Linux 内核仍然几乎完全是 C 程序,因此必须遵守这些规范。

内核系统调用接口” 的描述可以在《Linux 手册第 2 节》中找到,并包括了可从中间件应用程序调用的类似 mountsync 的 C 版本函数。这些函数的二进制布局是 Linux ABI 的第一个重要组成部分。对于问题 “Linux 的稳定 ABI 包括哪些内容?”,许多用户和开发人员的回答是 “sysfs(/sys)和 procfs(/proc)的内容”。而实际上,官方 Linux ABI 文档 确实主要集中在这些 虚拟文件系统 上。

前面着重介绍了 Linux ABI 在程序中的应用方式,但未涵盖同等重要的人为因素。正如下图所示,ABI 的功能需要内核社区、C 编译器(如 GCCclang)、创建用户空间 C 库(通常是 glibc)的开发人员,以及按照 可执行与链接格式(ELF) 布局的二进制应用程序之间的合作努力。

开发社区内的合作

为什么我们关注 ABI?

来自 Torvalds 本人的 Linux ABI 的稳定性保证,使得 Linux 发行版和个人用户能够独立更新内核,而不受操作系统的影响。

如果 Linux 没有稳定的 ABI,那么每次内核需要修补以解决安全问题时,操作系统的大部分甚至全部内容都需要重新安装。显然,二进制接口的稳定性是 Linux 的可用性和广泛采用的重要因素之一。

Terminal output

如上图所示,内核(在 linux-libc-dev 中)和 Glibc(在 libc6-dev 中)都提供了定义文件权限的位掩码。显然,这两个定义集必须一致!apt 软件包管理器会识别软件包提供每个文件。Glibc ABI 的潜在不稳定部分位于 bits/ 目录中。

在大部分情况下,Linux ABI 的稳定性保证运作良好。按照 康韦定律 Conway's Law ,在开发过程中出现的烦人技术问题往往是由于不同软件开发社区之间的误解或分歧所致,而这些社区都为 Linux 做出了贡献。不同社区之间的接口可以通过 Linux 包管理器的元数据轻松地进行想象,如上图所示。

Y2038:一个 ABI 破坏的例子

通过考虑当前正在进行的、缓慢发生 的 “Y2038” ABI 破坏的例子,可以更好地理解 Linux ABI。在 2038 年 1 月,32 位时间计数器将回滚到全零,就像较旧车辆的里程表一样。2038 年 1 月听起来还很遥远,但可以肯定的是,如今销售的许多物联网设备仍将处于运行状态。像今年安装的 智能电表智能停车系统 这样的普通产品可能采用的是 32 位处理器架构,而且也可能不支持软件更新。

Linux 内核已经在内部转向使用 64 位的 time_t 不透明数据类型来表示更晚的时间点。这意味着像 time() 这样的系统调用在 64 位系统上已经变更了它们的函数签名。这些努力的艰难程度可以在内核头文件中(例如 time\_types.h)清楚地看到,在那里放着新的和 _old 版本的数据结构。

里程表翻转

Glibc 项目也 支持 64 位时间,那么就大功告成了,对吗?不幸的是,根据 Debian 邮件列表中的讨论 来看,情况并非如此。发行版面临难以选择的问题,要么为 32 位系统提供所有二进制软件包的两个版本,要么为安装介质提供两个版本。在后一种情况下,32 位时间的用户将不得不重新编译其应用程序并重新安装。正如往常一样,专有应用程序才是一个真正的头疼问题。

Linux 稳定 ABI 里到底包括什么内容?

理解稳定 ABI 有些微妙。需要考虑的是,尽管大部分 sysfs 是稳定 ABI,但调试接口肯定是不稳定的,因为它们将内核内部暴露给用户空间。Linus Torvalds 曾表示,“不要破坏用户空间”,通常情况下,他是指保护那些 “只想它能工作” 的普通用户,而不是系统程序员和内核工程师,后者应该能够阅读内核文档和源代码,以了解不同版本之间发生了什么变化。下图展示了这个区别。

稳定性保证

普通用户不太可能与 Linux ABI 的不稳定部分进行交互,但系统程序员可能无意中这样做。除了 /sys/kernel/debug 以外,sysfs(/sys)和 procfs(/proc)的所有部分都是稳定的。

那么其他对用户空间可见的二进制接口如何呢,包括 /dev 中的设备文件、内核日志文件(可通过 dmesg 命令读取)、文件系统元数据或在内核的 “命令行” 中提供的 “引导参数”(在引导加载程序如 GRUB 或 u-boot 中可见)呢?当然,“这要视情况而定”。

挂载旧文件系统

除了 Linux 系统在引导过程中出现挂起之外,文件系统无法挂载是最令人失望的事情。如果文件系统位于付费客户的固态硬盘上,那么问题确实十分严重。当内核升级时,一个能够在旧内核版本下挂载的 Linux 文件系统应该仍然能够挂载,对吗?实际上,“这要视情况而定”。

在 2020 年,一位受到伤害的 Linux 开发人员在内核的邮件列表上 抱怨道

内核已经接受这个作为一个有效的可挂载文件系统格式,没有任何错误或任何类型的警告,而且已经这样稳定地工作了多年……我一直普遍地以为,挂载现有的根文件系统属于内核<->用户空间或内核<->现有系统边界的范围,由内核接受并被现有用户空间成功使用的内容所定义,升级内核应该与现有用户空间和系统兼容。

但是有一个问题:这些无法挂载的文件系统是使用一种依赖于内核定义,但并未被内核使用的标志的专有工具创建的。该标志未出现在 Linux 的 API 头文件或 procfs/sysfs 中,而是一种 实现细节)。因此,在用户空间代码中解释该标志意味着依赖于“未定义行为”,这是个几乎会让每个软件开发人员都感到战栗的短语。当内核社区改进其内部测试并开始进行新的一致性检查时,“man 2 mount” 系统调用突然开始拒绝具有专有格式的文件系统。由于该格式的创建者明确是一位软件开发人员,因此他未能得到内核文件系统维护者的同情。

施工标志上写着工作人员在树上进行工作

线程化内核的 dmesg 日志

/dev 目录中的文件格式是否保证稳定或不稳定?dmesg 命令 会从文件 /dev/kmsg 中读取内容。2018 年,一位开发人员 为 dmesg 输出实现了线程化,使内核能够“在打印一系列 printk() 消息到控制台时,不会被中断和/或被其他线程的并发 printk() 干扰”。听起来很棒!通过在 /dev/kmsg 输出的每一行添加线程 ID,实现了线程化。密切关注的读者将意识到这个改动改变了 /dev/kmsg 的 ABI,这意味着解析该文件的应用程序也需要进行相应的修改。由于许多发行版没有编译启用新功能的内核,大多数使用 /bin/dmesg 的用户可能没有注意到这件事,但这个改动破坏了 GDB 调试器 读取内核日志的能力。

确实,敏锐的读者会认为 GDB 的用户运气不佳,因为调试器是开发人员工具。实际上并非如此,因为需要更新以支持新的 /dev/kmsg 格式的代码位于内核自己的 Git 源代码库的 “树内” 部分。对于一个正常的项目来说,单个代码库内的程序无法协同工作就是一个明显的错误,因此已经合并了一份 使 GDB 能够与线程化的 /dev/kmsg 一起工作的补丁

那么 BPF 程序呢?

BPF 是一种强大的工具,可以在运行的内核中监控甚至实时进行配置。BPF 最初的目的是通过允许系统管理员即时从命令行修改数据包过滤器,从而支持实时网络配置。Alexei Starovoitov 和其他人极大地扩展了 BPF,使其能够跟踪任意内核函数。跟踪明显是开发人员的领域,而不是普通用户,因此它显然不受任何 ABI 保证的约束(尽管 bpf() 系统调用 具有与其他系统调用相同的稳定性承诺)。另一方面,创建新功能的 BPF 程序为“取代内核模块成为扩展内核的事实标准手段”提供了可能性。内核模块使设备、文件系统、加密、网络等工作正常,因此明显是“只希望它工作”的普通用户所依赖的设施。问题是,与大多数开源内核模块不同,BPF 程序传统上不在内核源代码中。

2022 年春季,一个提案 成为了焦点,该提案提议使用微型 BPF 程序而不是设备驱动程序补丁,对广泛的人机接口设备(如鼠标和键盘)提供支持。

随后进行了一场激烈的讨论,但这个问题显然在 Torvalds 在开源峰会上的评论 中得到解决:

他指出,如果你破坏了“普通(非内核开发人员)用户使用的真实用户空间工具”,那么你需要修复它,无论是否使用了 eBPF。

一致意见似乎正在形成,即希望其 BPF 程序在内核更新后仍能正常工作的开发人员 将需要将其提交到内核源代码库中一个尚未指定的位置。敬请关注后继发展,以了解内核社区对于 BPF 和 ABI 稳定性将采取什么样的政策。

结论

内核的 ABI 稳定性保证适用于 procfs、sysfs 和系统调用接口,但也存在重要的例外情况。当内核变更破坏了“树内”代码或用户空间应用程序时,通常会迅速回滚有问题的补丁。对于依赖内核实现细节的专有代码,尽管这些细节可以从用户空间访问,但它并没有受到保护,并且在出现问题时得到的同情有限。当像 Y2038 这样的问题无法避免 ABI 破坏时,会以尽可能慎重和系统化的方式进行过渡。而像 BPF 程序这样的新功能提出了关于 ABI 稳定性边界的尚未解答的问题。

致谢

感谢 Akkana PeckSarah R. NewmanLuke S. Crawford 对早期版本材料的有益评论。

(题图:MJ/da788385-ca24-4be5-bc27-ad7e7ef75973)


via: https://opensource.com/article/22/12/linux-abi

作者:Alison Chaiken 选题:lkxed 译者:ChatGPT 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

理解运转良好的系统对于处理不可避免的故障是最好的准备。

关于开源软件最古老的笑话是:“代码是 自具文档化的 self-documenting ”。经验表明,阅读源代码就像听天气预报一样:明智的人依然出门会看看室外的天气。本文讲述了如何运用调试工具来观察和分析 Linux 系统的启动。分析一个功能正常的系统启动过程,有助于用户和开发人员应对不可避免的故障。

从某些方面看,启动过程非常简单。内核在单核上以单线程和同步状态启动,似乎可以理解。但内核本身是如何启动的呢?initrd(initial ramdisk) 引导程序 bootloader 具有哪些功能?还有,为什么以太网端口上的 LED 灯是常亮的呢?

请继续阅读寻找答案。在 GitHub 上也提供了 介绍演示和练习的代码

启动的开始:OFF 状态

局域网唤醒 Wake-on-LAN

OFF 状态表示系统没有上电,没错吧?表面简单,其实不然。例如,如果系统启用了局域网唤醒机制(WOL),以太网指示灯将亮起。通过以下命令来检查是否是这种情况:

# sudo ethtool <interface name>

其中 <interface name> 是网络接口的名字,比如 eth0。(ethtool 可以在同名的 Linux 软件包中找到。)如果输出中的 Wake-on 显示 g,则远程主机可以通过发送 魔法数据包 MagicPacket 来启动系统。如果您无意远程唤醒系统,也不希望其他人这样做,请在系统 BIOS 菜单中将 WOL 关闭,或者用以下方式:

# sudo ethtool -s <interface name> wol d

响应魔法数据包的处理器可能是网络接口的一部分,也可能是 底板管理控制器 Baseboard Management Controller (BMC)。

英特尔管理引擎、平台控制器单元和 Minix

BMC 不是唯一的在系统关闭时仍在监听的微控制器(MCU)。x86\_64 系统还包含了用于远程管理系统的英特尔管理引擎(IME)软件套件。从服务器到笔记本电脑,各种各样的设备都包含了这项技术,它开启了如 KVM 远程控制和英特尔功能许可服务等 功能。根据 Intel 自己的检测工具IME 存在尚未修补的漏洞。坏消息是,要禁用 IME 很难。Trammell Hudson 发起了一个 me\_cleaner 项目,它可以清除一些相对恶劣的 IME 组件,比如嵌入式 Web 服务器,但也可能会影响运行它的系统。

IME 固件和 系统管理模式 System Management Mode (SMM)软件是 基于 Minix 操作系统 的,并运行在单独的 平台控制器单元 Platform Controller Hub 上(LCTT 译注:即南桥芯片),而不是主 CPU 上。然后,SMM 启动位于主处理器上的 通用可扩展固件接口 Universal Extensible Firmware Interface (UEFI)软件,相关内容 已被提及多次。Google 的 Coreboot 小组已经启动了一个雄心勃勃的 非扩展性缩减版固件 Non-Extensible Reduced Firmware (NERF)项目,其目的不仅是要取代 UEFI,还要取代早期的 Linux 用户空间组件,如 systemd。在我们等待这些新成果的同时,Linux 用户现在就可以从 Purism、System76 或 Dell 等处购买 禁用了 IME 的笔记本电脑,另外 带有 ARM 64 位处理器笔记本电脑 还是值得期待的。

引导程序

除了启动那些问题不断的间谍软件外,早期引导固件还有什么功能呢?引导程序的作用是为新上电的处理器提供通用操作系统(如 Linux)所需的资源。在开机时,不但没有虚拟内存,在控制器启动之前连 DRAM 也没有。然后,引导程序打开电源,并扫描总线和接口,以定位内核镜像和根文件系统的位置。U-Boot 和 GRUB 等常见的引导程序支持 USB、PCI 和 NFS 等接口,以及更多的嵌入式专用设备,如 NOR 闪存和 NAND 闪存。引导程序还与 可信平台模块 Trusted Platform Module (TPM)等硬件安全设备进行交互,在启动最开始建立信任链。

 title=

在构建主机上的沙盒中运行 U-boot 引导程序。

包括树莓派、任天堂设备、汽车主板和 Chromebook 在内的系统都支持广泛使用的开源引导程序 U-Boot。它没有系统日志,当发生问题时,甚至没有任何控制台输出。为了便于调试,U-Boot 团队提供了一个沙盒,可以在构建主机甚至是夜间的持续集成(CI)系统上测试补丁程序。如果系统上安装了 Git 和 GNU Compiler Collection(GCC)等通用的开发工具,使用 U-Boot 沙盒会相对简单:

# git clone git://git.denx.de/u-boot; cd u-boot
# make ARCH=sandbox defconfig
# make; ./u-boot
=> printenv
=> help

在 x86\_64 上运行 U-Boot,可以测试一些棘手的功能,如 模拟存储设备 的重新分区、基于 TPM 的密钥操作以及 USB 设备热插拔等。U-Boot 沙盒甚至可以在 GDB 调试器下单步执行。使用沙盒进行开发的速度比将引导程序刷新到电路板上的测试快 10 倍,并且可以使用 Ctrl + C 恢复一个“变砖”的沙盒。

启动内核

配置引导内核

引导程序完成任务后将跳转到已加载到主内存中的内核代码,并开始执行,传递用户指定的任何命令行选项。内核是什么样的程序呢?用命令 file /boot/vmlinuz 可以看到它是一个 “bzImage”,意思是一个大的压缩的镜像。Linux 源代码树包含了一个可以解压缩这个文件的工具—— extract-vmlinux

# scripts/extract-vmlinux /boot/vmlinuz-$(uname -r) > vmlinux
# file vmlinux
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically
linked, stripped

内核是一个 可执行与可链接格式 Executable and Linking Format (ELF)的二进制文件,就像 Linux 的用户空间程序一样。这意味着我们可以使用 binutils 包中的命令,如 readelf 来检查它。比较一下输出,例如:

# readelf -S /bin/date
# readelf -S vmlinux

这两个二进制文件中的段内容大致相同。

所以内核必须像其他的 Linux ELF 文件一样启动,但用户空间程序是如何启动的呢?在 main() 函数中?并不确切。

main() 函数运行之前,程序需要一个执行上下文,包括堆栈内存以及 stdiostdoutstderr 的文件描述符。用户空间程序从标准库(多数 Linux 系统在用 “glibc”)中获取这些资源。参照以下输出:

# file /bin/date
/bin/date: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically
linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32,
BuildID[sha1]=14e8563676febeb06d701dbee35d225c5a8e565a,
stripped

ELF 二进制文件有一个解释器,就像 Bash 和 Python 脚本一样,但是解释器不需要像脚本那样用 #! 指定,因为 ELF 是 Linux 的原生格式。ELF 解释器通过调用 _start() 函数来用所需资源 配置一个二进制文件,这个函数可以从 glibc 源代码包中找到,可以 用 GDB 查看。内核显然没有解释器,必须自我配置,这是怎么做到的呢?

用 GDB 检查内核的启动给出了答案。首先安装内核的调试软件包,内核中包含一个 未剥离的 unstripped vmlinux,例如 apt-get install linux-image-amd64-dbg,或者从源代码编译和安装你自己的内核,可以参照 Debian Kernel Handbook 中的指令。gdb vmlinux 后加 info files 可显示 ELF 段 init.text。在 init.text 中用 l *(address) 列出程序执行的开头,其中 addressinit.text 的十六进制开头。用 GDB 可以看到 x86\_64 内核从内核文件 arch/x86/kernel/head\_64.S 开始启动,在这个文件中我们找到了汇编函数 start_cpu0(),以及一段明确的代码显示在调用 x86_64 start_kernel() 函数之前创建了堆栈并解压了 zImage。ARM 32 位内核也有类似的文件 arch/arm/kernel/head.Sstart_kernel() 不针对特定的体系结构,所以这个函数驻留在内核的 init/main.c 中。start_kernel() 可以说是 Linux 真正的 main() 函数。

从 start\_kernel() 到 PID 1

内核的硬件清单:设备树和 ACPI 表

在引导时,内核需要硬件信息,不仅仅是已编译过的处理器类型。代码中的指令通过单独存储的配置数据进行扩充。有两种主要的数据存储方法: 设备树 device-tree 高级配置和电源接口(ACPI)表。内核通过读取这些文件了解每次启动时需要运行的硬件。

对于嵌入式设备,设备树是已安装硬件的清单。设备树只是一个与内核源代码同时编译的文件,通常与 vmlinux 一样位于 /boot 目录中。要查看 ARM 设备上的设备树的内容,只需对名称与 /boot/*.dtb 匹配的文件执行 binutils 包中的 strings 命令即可,这里 dtb 是指 设备树二进制文件 device-tree binary 。显然,只需编辑构成它的类 JSON 的文件并重新运行随内核源代码提供的特殊 dtc 编译器即可修改设备树。虽然设备树是一个静态文件,其文件路径通常由命令行引导程序传递给内核,但近年来增加了一个 设备树覆盖 的功能,内核在启动后可以动态加载热插拔的附加设备。

x86 系列和许多企业级的 ARM64 设备使用 ACPI 机制。与设备树不同的是,ACPI 信息存储在内核在启动时通过访问板载 ROM 而创建的 /sys/firmware/acpi/tables 虚拟文件系统中。读取 ACPI 表的简单方法是使用 acpica-tools 包中的 acpidump 命令。例如:

 title=

联想笔记本电脑的 ACPI 表都是为 Windows 2001 设置的。

是的,你的 Linux 系统已经准备好用于 Windows 2001 了,你要考虑安装吗?与设备树不同,ACPI 具有方法和数据,而设备树更多地是一种硬件描述语言。ACPI 方法在启动后仍处于活动状态。例如,运行 acpi_listen 命令(在 apcid 包中),然后打开和关闭笔记本机盖会发现 ACPI 功能一直在运行。暂时地和动态地 覆盖 ACPI 表 是可能的,而永久地改变它需要在引导时与 BIOS 菜单交互或刷新 ROM。如果你遇到那么多麻烦,也许你应该 安装 coreboot,这是开源固件的替代品。

从 start\_kernel() 到用户空间

init/main.c 中的代码竟然是可读的,而且有趣的是,它仍然在使用 1991 - 1992 年的 Linus Torvalds 的原始版权。在一个刚启动的系统上运行 dmesg | head,其输出主要来源于此文件。第一个 CPU 注册到系统中,全局数据结构被初始化,并且调度程序、中断处理程序(IRQ)、定时器和控制台按照严格的顺序逐一启动。在 timekeeping_init() 函数运行之前,所有的时间戳都是零。内核初始化的这部分是同步的,也就是说执行只发生在一个线程中,在最后一个完成并返回之前,没有任何函数会被执行。因此,即使在两个系统之间,dmesg 的输出也是完全可重复的,只要它们具有相同的设备树或 ACPI 表。Linux 的行为就像在 MCU 上运行的 RTOS(实时操作系统)一样,如 QNX 或 VxWorks。这种情况持续存在于函数 rest_init() 中,该函数在终止时由 start_kernel() 调用。

 title=

早期的内核启动流程。

函数 rest_init() 产生了一个新进程以运行 kernel_init(),并调用了 do_initcalls()。用户可以通过将 initcall_debug 附加到内核命令行来监控 initcalls,这样每运行一次 initcall 函数就会产生 一个 dmesg 条目。initcalls 会历经七个连续的级别:early、core、postcore、arch、subsys、fs、device 和 late。initcalls 最为用户可见的部分是所有处理器外围设备的探测和设置:总线、网络、存储和显示器等等,同时加载其内核模块。rest_init() 也会在引导处理器上产生第二个线程,它首先运行 cpu_idle(),然后等待调度器分配工作。

kernel_init() 也可以 设置对称多处理(SMP)结构。在较新的内核中,如果 dmesg 的输出中出现 “Bringing up secondary CPUs...” 等字样,系统便使用了 SMP。SMP 通过“热插拔” CPU 来进行,这意味着它用状态机来管理其生命周期,这种状态机在概念上类似于热插拔的 U 盘一样。内核的电源管理系统经常会使某个 core 离线,然后根据需要将其唤醒,以便在不忙的机器上反复调用同一段的 CPU 热插拔代码。观察电源管理系统调用 CPU 热插拔代码的 BCC 工具 称为 offcputime.py

请注意,init/main.c 中的代码在 smp_init() 运行时几乎已执行完毕:引导处理器已经完成了大部分一次性初始化操作,其它核无需重复。尽管如此,跨 CPU 的线程仍然要在每个核上生成,以管理每个核的中断(IRQ)、工作队列、定时器和电源事件。例如,通过 ps -o psr 命令可以查看服务每个 CPU 上的线程的 softirqs 和 workqueues。

# ps -o pid,psr,comm $(pgrep ksoftirqd)  
 PID PSR COMMAND 
   7   0 ksoftirqd/0 
  16   1 ksoftirqd/1 
  22   2 ksoftirqd/2 
  28   3 ksoftirqd/3 

# ps -o pid,psr,comm $(pgrep kworker)
PID  PSR COMMAND 
   4   0 kworker/0:0H 
  18   1 kworker/1:0H 
  24   2 kworker/2:0H 
  30   3 kworker/3:0H
[ . . . ]

其中,PSR 字段代表“ 处理器 processor ”。每个核还必须拥有自己的定时器和 cpuhp 热插拔处理程序。

那么用户空间是如何启动的呢?在最后,kernel_init() 寻找可以代表它执行 init 进程的 initrd。如果没有找到,内核直接执行 init 本身。那么为什么需要 initrd 呢?

早期的用户空间:谁规定要用 initrd?

除了设备树之外,在启动时可以提供给内核的另一个文件路径是 initrd 的路径。initrd 通常位于 /boot 目录中,与 x86 系统中的 bzImage 文件 vmlinuz 一样,或是与 ARM 系统中的 uImage 和设备树相同。用 initramfs-tools-core 软件包中的 lsinitramfs 工具可以列出 initrd 的内容。发行版的 initrd 方案包含了最小化的 /bin/sbin/etc 目录以及内核模块,还有 /scripts 中的一些文件。所有这些看起来都很熟悉,因为 initrd 大致上是一个简单的最小化 Linux 根文件系统。看似相似,其实不然,因为位于虚拟内存盘中的 /bin/sbin 目录下的所有可执行文件几乎都是指向 BusyBox 二进制文件 的符号链接,由此导致 /bin/sbin 目录比 glibc 的小 10 倍。

如果要做的只是加载一些模块,然后在普通的根文件系统上启动 init,为什么还要创建一个 initrd 呢?想想一个加密的根文件系统,解密可能依赖于加载一个位于根文件系统 /lib/modules 的内核模块,当然还有 initrd 中的。加密模块可能被静态地编译到内核中,而不是从文件加载,但有多种原因不希望这样做。例如,用模块静态编译内核可能会使其太大而不能适应存储空间,或者静态编译可能会违反软件许可条款。不出所料,存储、网络和人类输入设备(HID)驱动程序也可能存在于 initrd 中。initrd 基本上包含了任何挂载根文件系统所必需的非内核代码。initrd 也是用户存放 自定义ACPI 表代码的地方。

 title=initrd."" title=" title="Rescue shell and a custom initrd."">

救援模式的 shell 和自定义的 initrd 还是很有意思的。

initrd 对测试文件系统和数据存储设备也很有用。将这些测试工具存放在 initrd 中,并从内存中运行测试,而不是从被测对象中运行。

最后,当 init 开始运行时,系统就启动啦!由于第二个处理器现在在运行,机器已经成为我们所熟知和喜爱的异步、可抢占、不可预测和高性能的生物。的确,ps -o pid,psr,comm -p 1 很容易显示用户空间的 init 进程已不在引导处理器上运行了。

总结

Linux 引导过程听起来或许令人生畏,即使是简单嵌入式设备上的软件数量也是如此。但换个角度来看,启动过程相当简单,因为启动中没有抢占、RCU 和竞争条件等扑朔迷离的复杂功能。只关注内核和 PID 1 会忽略了引导程序和辅助处理器为运行内核执行的大量准备工作。虽然内核在 Linux 程序中是独一无二的,但通过一些检查 ELF 文件的工具也可以了解其结构。学习一个正常的启动过程,可以帮助运维人员处理启动的故障。

要了解更多信息,请参阅 Alison Chaiken 的演讲——Linux: The first second,已于 1 月 22 日至 26 日在悉尼举行。参见 linux.conf.au

感谢 Akkana Peck 的提议和指正。


via: https://opensource.com/article/18/1/analyzing-linux-boot-process

作者:Alison Chaiken 译者:jessie-pang 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出