从底层定制 Linux—构建您自己的 Linux 基础系统
我们已经见过各种 Linux 发行版,而且还有许多其他发行版不断涌现。有些小到像 DLX 一样,可以放在一张软盘上;有些则大到像 Red Hat 6.2 一样,装在五张 CD 中。随着系统规模的扩大,事情似乎变得越来越复杂,也越来越难以管理。Linux 系统是如何从自由代码片段组装起来的?我们如何为特定目的组装和定制自己的系统?这似乎是一项艰巨的任务。
然而,从基础系统的角度来看,原则上,所有发行版的组装方式都类似。不同之处在于,大型发行版配备了更多的软件包和更多针对更广泛受众的花哨功能,而小型发行版的好东西较少,并且针对相对狭窄和特定的用户群体。
功能齐全的 Linux 发行版对于特殊情况来说通常是不必要地庞大。例如,嵌入式应用程序需要一个精简的基础来适应其特定情况,并且已经有适用于这些目的的小型 Linux 发行版。然而,由于需要考虑的因素太多,没有人可以声称他们的发行版是全面的并且满足所有客户。
通常,应用程序需要一个定制的基础系统才能高效工作。您可以向许多 Linux 服务提供商付费购买解决方案,也可以自己动手。有时,了解如何自己构建基础系统更有益。凭借这样的技能和知识,工程师可以轻松地控制和改进系统的行为,以满足客户的需求。DIY(自己动手)不仅有趣,而且在某些情况下具有战略意义,越来越多的人认识到 Linux 提供的价值。通过这样做,您不仅可以获得定制 Linux 内核的能力,还可以定制系统的所有其他组件,以实现最适合您需求的优化。
本文试图告诉读者,构建您自己的基础 Linux 并非一项艰巨的任务。它讲述了我们的经验,并简要介绍了构建和定制 Linux 系统。这是一个基础系统:小巧、干净且随时可用。我们力求化繁为简,同时不失通用性和有效性。我们展示了如何使构建步骤像 1-2-3 一样简单,以及如何将此系统定制到最小,并容纳在一张软盘中。毕竟,它应该足够好,可以用作运行任何典型应用程序的基础系统的起点。这是可能的,因为我们直接从未经修改的源代码构建系统。这使我们始终可以使用最新的稳定版本,并使所有必需的内核服务可用。
系统简单地定义为固有连接部件的组合。Linux 系统(本文仅考虑软件)是 Linux 内核和其他使内核有用的组件的组合。除内核外的所有软件组件都需要驻留在根文件系统中(可能还有其他一些文件系统,但它们必须挂载在根树下才能可见)。因此,从技术上讲,我们可以简单地将 Linux 系统视为内核和根文件系统的组合。所有 Linux 发行版都是以这种方式安排的。例如,完全安装的 Linux 系统是内核加上一个庞大的根文件系统。Linux 安装盘和救援系统分别用于安装完整系统和修复问题系统。它们的组织方式也相同;也就是说,由内核和初始根文件系统组成,但初始根文件系统很小,只包含执行有限作业所需的一些基本组件(见图 1)。Linux 内核经过编码,可以采取特殊措施来定位根文件系统,可以来自普通文件系统,也可以来自初始根文件系统的压缩映像。

图 1. Linux 系统
给定一个应用程序,我们希望在内核之上使用共享库在盒子上运行它。假设该应用程序不需要 Linux 目前未提供的任何罕见功能。我们想要一个可以运行此应用程序并提供一些基本控制和管理的基础系统。图 2 显示了基础系统的典型案例。请注意,此图并不完整,因为实用程序可能是静态链接的,这不需要共享库,而实用程序可能是 a.out 样式的,这不使用动态加载器。

图 2. 基础系统
如果应用程序是自给自足的,也就是说,在运行时与所需的一切静态链接,则它可以直接在内核之上运行,而无需共享库的支持。在这种情况下,基础系统可能仅意味着 Linux 内核本身。然而,几乎所有系统都需要一个或多个实用程序的支持来管理文件操作和系统监控等事项,使用诸如 mount 和 ps 之类的命令。我们认为基础系统是内核、动态加载器、一组库和一组实用程序的组合。
与许多其他系统一样,我们的目标是展示如何在软盘上创建系统,其中包含内核和初始根文件系统的压缩映像,如图 3 所示。这个压缩的初始根文件系统将被内核解压缩并放入 ramdisk 中,ramdisk 是为保存小型 Linux 文件系统而预留的 RAM 空间。创建这样一个基础系统通常很简单,但对于大多数人来说却相当乏味。我们简化了该过程。我们的想法是设计一个组织良好的 makefile 层次结构,该层次结构将提取源代码、编译并设置初始 ramdisk 的内容,然后准备并打包整个系统。

图 3. 系统组件的排列
上述基础系统的定制取决于将在盒子上运行的应用程序的要求。为内核、动态加载器、应用程序和实用程序所需的一组库以及控制和管理系统所需的一组基本实用程序选择配置。然后,在一致的环境中编译所有这些东西。之后,打包结果并使其可启动。如果缺少某些东西,您可以自由地将其添加到列表中,然后再次制作。
好奇的读者可能会问我们为什么要这样做,以及这样做有什么好处。与许多其他人一样,我们希望在一个盒子上运行一些复杂的软件,这些软件可以概括为在没有硬盘或显示器的典型 PC 类机器上的多任务应用程序。我们需要一个 OS 内核和一些元素作为基础实验平台。该平台应该是健壮的、可维护的和可定制的。为此目的编写一个好的 OS 内核对于许多人来说太可怕了。感谢 Linux 和开源社区,我们现在有了一个绝佳的选择。
基本材料已准备就绪并可免费获得。现在是时候挑选我们需要的部件,组装我们自己的引擎并控制它了。然后,是时候享受了。
在我们开始之前,我们需要知道一些关键问题的答案:如何编译内核?如何编译共享库?如何创建初始根文件系统?如何将内核映像和压缩文件系统放到软盘或 EPROM 上?如何使用共享库运行应用程序?如何调试?诸如此类的问题还有很多。答案已经记录在案,据我们所知,并非在一个地方,而是分散在各种文档中。我们不想为这些问题编写一份全面的文档,而是讲述我们的故事和我们答案的主要部分。
一旦制定了定制计划,就可以将详细步骤付诸行动。以下描述了我们工作中的一般步骤。
1. 设置开发环境 安装完整的 Linux 发行版,例如 Red Hat 6.0 作为开发平台。确保它支持 gcc。为了简化操作,假设我们运行定制系统的目标机器和主机(即开发机器)使用相同类型的 CPU,在我们的例子中是 Intel x86。否则,我们必须准备一个交叉编译器。 2. 定制内核 获取最新的稳定内核源代码(在撰写本文时为 2.2.13 版本)。如何在源文件中详细记录了如何配置和编译内核。我们不想在此重复细节。但是我们需要选择对初始 ramdisk、可加载模块和其他必要选项的支持。如果我们使用串行控制台端口,请选择串行控制台支持。如果我们有一块想要设置的硬件,例如以太网卡,我们可以将它们选择为模块。然后我们可以在我们的基础系统中安装和使用这些模块。 3. 准备标准库 获取最新的稳定标准库 glibc(版本 2.2.1)。这包括几乎所有我们想要的东西:动态加载器、标准 C 库和 math lib 等。虽然我们不需要 glibc 提供的所有东西,但最好将它们全部构建在一起,然后选择我们想要的东西。glibc 源代码树中的文档详细说明了如何编译它。因为我们将其用于我们的目标机器,所以我们必须使用与步骤 2 一致的内核头文件来编译它。这可以使用配置期间的 --with-headers 来指定。此外,我们必须安装所有库头文件,以便目标机器的其他组件的编译可以使用它们。 4. 让编译器知道如何交叉编译 安装内核和 glibc 头文件后,我们需要准备编译器 gcc 以使用它们。Glibc2-HOWTO 更详细地描述了这一点。简而言之,我们需要使用选项 -b 告诉 gcc 在哪里查找规范。在我们的例子中,由于目标机器和主机基本上相同,我们只需要使用主机规范。这可以通过使用命令 gcc -v 找到。例如,在我的机器上,回复是
Reading specs from /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.91.66/specs gcc version egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)通过添加选项 -b 来编译系统的其他组件,如下所示
gcc -b i386-redhat-linux5. 选择和准备实用程序和其他库 为了始终如一地编译实用程序程序(例如 mount)或 glibc 之外的其他库(例如 termcap),我们必须告诉编译器在哪里查找所有头(include)文件和库。首先,我们通过指定 --nostdinc 告诉编译器不要在默认路径中搜索头文件。其次,使用选项 -b $MACHINE(如步骤 2 中所述)告诉编译器我们正在为目标机器编译源代码。第三,通过指定选项 -I 准确说明在哪里找到内核头文件和标准库头文件。第四,通过使用 -L 和 -l 选项告诉加载器要使用哪些库以及在哪里找到它们。 6. 构建应用程序 就像编译实用程序和库一样,我们需要交叉编译我们的应用程序,以便在基础系统上使用它们。从系统角度来看,没有特别需要考虑的事项,除非确保已安装应用程序所依赖的一切。 7. 将东西打包在一起 一旦所有组件都准备就绪,我们需要以这样一种方式安排它们,以便系统可以启动,并且所有东西都可以正确找到。以下部分将更详细地讨论此问题。 8. 继续前进 将基础系统用作您想要做的任何事情的起点。可以根据需要,在基础系统之上添加更多功能。一个单独的部分专门讨论这个问题。
据我们所知,很难找到一份文档详细告诉我们如何将映像、可执行文件、二进制文件和脚本放在一起;换句话说,将东西打包在一起,组装一个系统。不同的系统可能会采用不同的打包方法,尽管组件可以以相同的方式创建。最简单和最流行的方法是在软盘上打包。将可启动系统打包到单张软盘(即启动/根软盘)上的一般步骤可以概括为以下几个步骤
1. 创建系统所需的各个项目,例如内核映像、库、可执行文件、脚本和配置等。2. 为基础系统创建初始根文件系统的目录结构。3. 将东西移动到根文件系统并创建设备节点之类的项目。4. 创建压缩的根文件系统。5. 通过在内核映像中设置一些标志来告诉内核在哪里找到初始根文件系统映像。6. 将内核和压缩的根映像写入软盘并使其可启动。为了显示更多细节,我们编写了一组 makefile。它们实际上是实现上述步骤并从免费提供的软件包创建小型基础系统的说明。我们运行一个名为 netperf 服务器的简单应用程序,该应用程序测试 TCP/IP 堆栈性能,也可以从 Web 上免费获得。我们在“资源”中提供了这些说明。运行它可能并不简单,但好奇的读者可以深入研究代码行,找到如何从头开始构建基础系统。
应用程序可以以不同的方式启动,具体取决于基础系统的配置方式。在大多数情况下,Linux 内核被配置为在内核启动后,在初始根文件系统中运行启动脚本或二进制可执行文件,称为 init 或 linuxrc。这个 init 程序通常会执行诸如重新挂载根文件系统以允许读/写权限、挂载其他文件系统(如 proc)以及初始化系统的其他部分(例如启动 shell 界面或立即运行应用程序)之类的操作。SysVInit 程序在大多数 Linux 发行版中非常流行,用于此目的。
对于我们的基础系统,我们不需要复杂的 init 序列来演示。因此,我们只是编写一个如下所示的 shell 脚本。任何人都可以自由更改并向其中添加更多命令
mount -n -o remount,rw / mount /proc /proc -t proc echo MyCompanyName, Version X.Y. Built Z, August 2000 exec /bin/sh
作为练习,如果您在应用程序中包含上述 echo 行,并在脚本末尾启动应用程序而不是运行标准 shell,可能会看起来更好。C++ 中的一个例子
cout << COMPANY << VERSION_NO << BUILD_NO << __DATE__ << __TIME__;在我们的例子中,系统启动后会提示。
pipe-elinux> MyCompanyName, Version X.Y, Build Z, August 2000 pipe-elinux>
基础系统启动后,您可能会认为它没有任何有趣的应用程序,用处不大。但它是一个基础,您可以从中开始您的大型项目。您可以逐步将内容添加到此基础系统中,使其越来越有吸引力。以下示例可能值得考虑
init 程序:SysVInit 是一个不错的选择,但对于简单的应用程序来说似乎太大了
安全设施:添加登录支持
编辑器:vi 或 emacs
更多网络服务:telnet 或 ftp 守护程序
GUI:X 是一个选择
非易失性存储:闪存和硬盘支持。
添加更多可加载模块
使用 rpm 管理软件包
我们实际上不需要重新编译我们选择的每个软件包,因为我们可以轻松找到已为处理器编译的二进制文件。像我们的系统一样,主机和目标机器是相同类型的,因此,我们可以只使用在主机上找到的大多数二进制文件。例如,对于实用程序 top,我们只需将二进制文件复制到我们的基础系统中;然后它就可以运行了。然而,事情并非总是那么简单。因为大多数时候我们必须理清可执行文件的依赖项,即它需要的共享库和它读取的配置文件,通常我们只有在运行它时才知道。然而,一些工具可以帮助我们解决这个问题。ldd 和 strace 可以告诉我们这些依赖项。例如,有一次我尝试通过简单地将可执行文件 (emacs-nox)、一些共享库和配置文件复制到基础系统,成功地在基础系统上运行 Emacs。这通常在开发过程中对我们有很大帮助,并节省大量时间。
您可能对从软盘启动不满意。相反,您可以实现从 EPROM 或其他设备启动。为此,您必须重新设计您的打包方法,但组件基本保持不变。这里的特殊之处在于内核映像加载器。启动可以像这样实现
从 EPROM 启动
从网络启动
从闪存、硬盘分区、CD-ROM 和 ZIP 磁盘等设备启动;也就是说,任何软盘以外的设备
从其他操作系统启动
如果您喜欢并想花更多时间,您可以使您的系统像该领域中许多其他成功的系统一样花哨。
使用 Linux 的优势之一是有许多文档和工具可以帮助您定制系统和解决问题。代码不是秘密。内部和外部的一切都是开放的。此外,还有许多其他有用的印刷和 Web 资源。在这方面,没有其他系统可以与 Linux 相提并论,甚至 Free BSD 也不行,更不用说任何专有操作系统了。
通常,对于一个问题,我们可能会想出几种不同的解决方案。我们总是想选择最好的一个,当然,但在我们尝试过每一个解决方案之前,很难知道哪个是最好的。为了解决问题,在许多情况下,我们可以通过查阅 Linux HOWTO 和文档,或向我们组织中的 Linux 人员询问来找到答案。作为一种替代方案,您可以在 Linux 新闻组上发布消息,并希望有人在那里给您快速回复。如果您想付费,也有许多与 Linux 相关的公司提供技术服务。(如果问题很棘手,作为最后的稻草,像我有时做的那样,踢您的错误百出的盒子几脚。您必须小心——不要弄坏它,然后重新启动。这应该会起作用;否则,从头开始重复问题解决序列。)
gdb 是对基础系统上的应用程序的出色调试支持。如果我们不想或无法在目标系统(即基础系统)上运行完整的 gdb,我们可以运行小型远程 gdb 设施,作为目标上的 gdb stub 或 gdb 服务器。此外,诸如 syslogd 守护程序之类的东西也可以帮助在目标系统上进行调试。
有很多好的问题解决策略。无论我们使用哪种方法,目标都是找到合适的解决方案。遵循成功的例子通常是安全的。例如,我们通过检查 Red Hat 救援系统内部的东西来学习一些东西。我们可以简单地使用以下几个命令来做到这一点
cat rescue.img | gzip -d > rescue_root.img mkdir rescue_root mount -o loop rescue_root.img rescue_root
这里 rescue.img 是在 Red Hat 发行版的 images 目录中找到的压缩救援软盘映像。然后我们可以通过以下方式检查其内容
ls rescue_root它显示
bin dev etc lib lost+found mnt proc sbin tmp usr您可以在软盘中获得所有详细信息。
本文只是 Linux 基础系统定制的介绍。对于特定情况,它可能相当复杂,尤其是在需要代码级别的修改(例如支持专用硬件)时。但是,我们已经表明,这是一项可管理的任务。我们的目的是使事情变得简单,以鼓励人们接受挑战。通过适度努力创建我们自己的定制基础系统,我们获得了一个强大的引擎,可以推动我们走向光明的未来。
