自定义嵌入式 Linux 发行版
廉价物联网板的普及意味着现在是时候不仅要控制应用程序,还要控制整个软件平台了。那么,如何构建一个针对特定用途的、带有交叉编译应用程序的自定义发行版呢?正如 Michael J. Hammel 在这里解释的那样,这并不像您想象的那么难。
为什么要自定义?过去,许多嵌入式项目出于多种原因,使用了现成的发行版并将其精简到基本要素。首先,删除未使用的软件包减少了存储需求。嵌入式系统在启动时通常缺乏大量存储空间,而可用的非易失性存储器可能需要将大量操作系统复制到内存中才能运行。其次,删除未使用的软件包减少了可能的攻击途径。如果您不需要潜在的易受攻击的软件包,那么保留它们是没有意义的。最后,删除未使用的软件包减少了发行版管理开销。软件包之间存在依赖关系意味着如果任何一个软件包需要来自上游发行版的更新,则需要保持它们同步。这可能是一场验证噩梦。
然而,从现有的发行版开始并删除软件包并不像听起来那么容易。删除一个软件包可能会破坏各种其他软件包所持有的依赖关系,并且依赖关系可能会在上游发行版管理中发生变化。此外,由于某些软件包在启动或运行时进程中的集成性质,它们根本无法在不造成巨大痛苦的情况下被删除。所有这些都将平台控制权置于项目之外,并可能导致开发中出现意外的延迟。
一种流行的替代方案是使用来自上游发行版提供商的构建工具来构建自定义发行版。Gentoo 和 Debian 都为此类自下而上的构建提供了选项。其中最受欢迎的可能是 Debian debootstrap 实用程序。它检索预构建的核心组件,并允许用户挑选他们感兴趣的软件包来构建他们的平台。但是,debootstrap 最初仅适用于 x86 平台。尽管现在有 ARM(以及可能的其他)选项,但 debootstrap 和 Gentoo 的 catalyst 仍然将依赖关系管理从本地项目中移除。
有些人会争辩说,让其他人管理平台软件(如 Android)比自己做要容易得多。但是,这些发行版是通用型的,当您坐在轻量级、资源受限的物联网设备上时,您可能会三思而后行,是否要放弃任何优势。
系统启动入门自定义 Linux 发行版需要多个软件组件。第一个是工具链。工具链是用于编译软件的工具集合,包括(但不限于)编译器、链接器、二进制操作工具和标准 C 库。工具链是专门为目标硬件设备构建的。在 x86 系统上构建的、旨在用于 Raspberry Pi 的工具链称为交叉工具链。当使用内存和存储空间有限的小型嵌入式设备时,最好始终使用交叉工具链。请注意,即使是用脚本语言(如 JavaScript)为特定用途编写的应用程序也需要在软件平台上运行,而该软件平台需要使用交叉工具链进行编译。

图 1. 编译依赖关系和启动顺序
交叉工具链用于构建目标硬件的软件组件。需要的第一个组件是引导加载程序。当电源施加到电路板时,处理器(取决于设计)会尝试跳转到特定的内存位置以开始运行软件。该内存位置是引导加载程序的存储位置。硬件可以具有内置引导加载程序,可以直接从其存储位置运行,也可以在运行之前先复制到内存中。也可以有多个引导加载程序。例如,第一阶段引导加载程序将驻留在硬件上的 NAND 或 NOR 闪存中。它的唯一目的是设置硬件,以便可以加载和运行第二阶段引导加载程序,例如存储在 SD 卡上的引导加载程序。
引导加载程序具有足够的知识来使硬件达到可以加载 Linux 到内存并跳转到它的程度,从而有效地将控制权交给 Linux。Linux 是一个操作系统。这意味着,按照设计,它实际上除了监视硬件并为更高层软件(又名应用程序)提供服务之外,什么也不做。《Linux 内核》通常附带各种固件 blob。这些是预编译的软件对象,通常包含用于硬件平台的设备的专有 IP(知识产权)。在构建自定义发行版时,可能需要在开始编译内核之前获取 Linux 内核源代码树未提供的任何固件 blob。
应用程序存储在根文件系统中。根文件系统是通过编译和收集各种软件库、工具、脚本和配置文件来构建的。总而言之,这些都提供了项目将运行的应用程序所需的服务,例如网络配置和 USB 设备挂载。
总而言之,完整的系统构建需要以下组件
交叉工具链。
一个或多个引导加载程序。
Linux 内核和相关的固件 blob。
填充了库、工具和实用程序的根文件系统。
自定义应用程序。
交叉工具链的组件可以手动构建,但这是一个复杂的过程。幸运的是,存在使此过程更容易的工具。其中最好的可能是 Crosstool-NG。该项目利用 Linux 内核使用的相同 kconfig 菜单系统来配置工具链的各个部分。使用此工具的关键是为目标平台找到正确的配置项。这通常包括以下项目
目标架构,例如 ARM 或 x86。
字节序:小端(通常为 Intel)或大端(通常为 ARM 或其他)。
编译器已知的 CPU 类型,例如 GCC 使用
-mcpu
或--with-cpu
。CPU 支持的浮点类型(如果有),例如 GCC 使用
-mfpu
或--with-fpu
。binutils 软件包、C 库和 C 编译器的特定版本信息。

图 2. Crosstool-NG 配置菜单
前四项通常可以从处理器制造商的文档中获得。对于相对较新的处理器,可能很难找到这些信息,但对于 Raspberry Pi 或 BeagleBoards(及其衍生产品和分支),您可以在 Embedded Linux Wiki 等网站上在线找到这些信息。
binutils、C 库和 C 编译器的版本是将工具链与第三方可能提供的任何其他工具链区分开来的因素。首先,这些东西有多个提供商。Linaro 为较新的处理器类型提供前沿版本,同时努力将支持合并到 GNU C 库等上游项目中。虽然您可以使用各种提供商,但您可能希望坚持使用标准的 GNU 工具链或 Linaro 版本的工具链。
Crosstool-NG 中另一个重要的选择是 Linux 内核的版本。此选择获取用于各种工具链组件的标头,但它不必与您将在目标硬件上启动的 Linux 内核相同。重要的是选择一个不比目标硬件内核更新的内核。如果可能,请选择一个比将在目标硬件上使用的内核更旧的长期支持内核。
对于大多数不熟悉自定义发行版构建的开发人员来说,工具链构建是最复杂的过程。幸运的是,二进制工具链可用于许多目标硬件平台。如果构建自定义工具链变得有问题,请在 Embedded Linux Wiki 等网站上在线搜索预构建工具链的链接。
启动选项工具链之后的下一个关注点是引导加载程序。引导加载程序设置硬件,使其可以被越来越复杂的软件使用。第一阶段引导加载程序通常由目标平台制造商提供,并烧录到硬件上的存储器中,如 EEPROM 或 NOR 闪存。第一阶段引导加载程序将使从 SD 卡等启动成为可能。Raspberry Pi 有这样一个引导加载程序,这使得创建自定义引导加载程序变得不必要。
尽管如此,许多项目都添加了辅助引导加载程序来执行各种任务。其中一项任务可能是提供启动画面动画,而无需使用 Linux 内核或 plymouth 等用户空间工具。更常见的辅助引导加载程序任务是使基于网络的启动或 PCI 连接的磁盘可用。在这些情况下,可能需要三级引导加载程序(例如 GRUB)才能使系统运行。
最重要的是,引导加载程序加载 Linux 内核并启动它运行。如果第一阶段引导加载程序没有提供在启动时传递内核参数的机制,则可能需要第二阶段引导加载程序。
有许多开源引导加载程序可用。《U-Boot 项目》通常用于 ARM 平台,如 Raspberry Pi。CoreBoot 通常用于 x86 平台,如 Chromebook。引导加载程序可能非常特定于目标硬件。引导加载程序的选择将取决于总体项目要求和目标硬件(在线搜索开源引导加载程序的列表)。
现在请出企鹅引导加载程序会将 Linux 内核加载到内存中并启动它运行。Linux 就像一个扩展的引导加载程序:它继续硬件设置并准备加载更高级别的软件。内核的核心将设置和准备内存,以便在应用程序和硬件之间共享,准备任务管理以允许多个应用程序同时运行,初始化引导加载程序未配置或配置不完整的硬件组件,并开始人机交互界面。但是,内核可能未配置为自行执行此操作。它可能包含一个嵌入式轻量级文件系统,称为 initramfs 或 initrd,可以与内核分开创建,以协助硬件设置。
内核处理的另一件事是将二进制 blob(通常称为固件)下载到硬件设备。固件是预编译的对象文件,其格式特定于用于初始化引导加载程序和内核无法访问的位置的特定设备。许多此类固件对象可从 Linux 内核源代码存储库获得,但许多其他固件对象只能从特定的硬件供应商处获得。通常提供自己固件的设备的示例包括数字电视调谐器或 WiFi 网卡。
固件可以从 initramfs 加载,也可以在内核从根文件系统启动 init 进程后加载。但是,创建内核通常将是创建自定义 Linux 发行版时获取固件的过程。
轻量级核心平台Linux 内核做的最后一件事是尝试运行一个特定的程序,称为 init 进程。它可以命名为 init 或 linuxrc,或者程序名称可以由引导加载程序传递给内核。init 进程存储在内核可以访问的文件系统中。在 initramfs 的情况下,文件系统存储在内存中(由内核本身或由引导加载程序放置在那里)。但是,initramfs 通常不够完整,无法运行更复杂的应用程序。因此,需要另一个文件系统,称为根文件系统。

图 3. Buildroot 配置菜单
initramfs 文件系统可以使用 Linux 内核本身构建,但更常见的是,它使用名为 BusyBox 的项目创建。BusyBox 将 GNU 实用程序(如 grep 或 awk)的集合组合到一个二进制文件中,以减小文件系统本身的大小。BusyBox 通常用于快速启动根文件系统的创建。
但是,BusyBox 是有目的地轻量级的。它并非旨在提供目标平台所需的所有工具,即使它确实提供的工具也可能功能减少。BusyBox 有一个姊妹项目,称为 Buildroot,可用于获取完整的根文件系统,提供各种库、实用程序和脚本语言。与 Crosstool-NG 和 Linux 内核一样,BusyBox 和 Buildroot 都允许使用 kconfig 菜单系统进行自定义配置。更重要的是,Buildroot 系统自动处理依赖关系,因此选择给定的实用程序将保证它所需的任何软件也将在根文件系统中构建和安装。
Buildroot 可以生成各种格式的根文件系统存档。但是,重要的是要注意文件系统只是存档的。单个实用程序和库既未打包为 Debian 格式,也未打包为 RPM 格式。使用 Buildroot 将生成根文件系统映像,但其内容不是托管软件包。尽管如此,Buildroot 确实为 opkg 和 rpm 软件包管理器都提供了支持。这意味着将在根文件系统上安装的自定义应用程序可以进行软件包管理,即使根文件系统本身不是。
交叉编译和脚本Buildroot 的功能之一是生成暂存树的能力。此目录包含可用于交叉编译其他应用程序的库和实用程序。借助暂存树和交叉工具链,可以在主机系统上而不是在目标平台上在 Buildroot 之外编译其他应用程序。然后,使用 rpm 或 opkg,可以使用软件包管理软件将这些应用程序安装到目标运行时根文件系统。
大多数自定义系统都围绕使用脚本语言构建应用程序的想法而构建。如果目标平台需要脚本,则 Buildroot 提供了多种选择,包括 Python、PHP、Lua 和通过 Node.js 的 JavaScript。还支持需要使用 OpenSSL 加密的应用程序。
下一步是什么Linux 内核和引导加载程序的编译方式与大多数应用程序类似。它们的构建系统旨在构建特定的软件位。Crosstool-NG 和 Buildroot 是元构建。元构建是围绕软件集合的包装器构建系统,每个软件集合都有自己的构建系统。这些的替代方案包括 Yocto 和 OpenEmbedded。Buildroot 的好处是它可以很容易地被更高级别的元构建包装,以自动化自定义 Linux 发行版的构建。这样做打开了将 Buildroot 指向项目特定的缓存存储库的选项。使用缓存存储库可以加速开发并提供快照构建,而无需担心上游存储库的更改。
更高级别构建系统的示例实现是 PiBox。PiBox 是围绕本文讨论的所有工具的元构建。其目的是在所有工具周围添加一个通用的 GNU Make 目标构造,以便生成一个核心平台,可以在该平台上构建和分发其他软件。PiBox 媒体中心和信息亭项目是在核心平台上安装的应用程序层软件的实现,旨在生成一个专用平台。《钢铁侠项目》旨在扩展这些应用程序,用于家庭自动化,并与语音控制和物联网管理集成。
但是,如果没有这些核心软件工具,PiBox 就什么也不是,如果没有对完整的自定义发行版构建过程的深入理解,它就永远无法运行。而且,如果没有这些项目的开发团队的长期奉献,PiBox 就不可能存在,他们使自定义发行版构建成为大众的任务。