系统最小化
“你能把这个做得多小?” 是嵌入式工程师在项目开始时经常听到的问题。大多数时候,提出这个问题的人关心的是减少 RAM 和闪存资源,目的是降低设备的单位成本或能源需求。
由于 Linux 及其周边环境最初是为桌面或服务器系统设计的,因此其默认配置并未针对尺寸进行优化。然而,随着 Linux 越来越多地应用于嵌入式设备,使 Linux “小型化” 并不像以前那样令人望而生畏。有几种不同的方法可以减少系统的内存占用。
许多工程师从减小内核大小开始;然而,手边有更容易实现的目标。本文详细介绍了如何减小内核的大小,主要是通过删除典型嵌入式系统中甚至不会使用的代码。
根文件系统 (RFS) 可能是系统中内存资源的最大消耗者。根文件系统包含应用程序使用的基础设施代码以及 C 库。选择用于 RFS 本身的文件系统会对最终大小产生很大影响。标准的 ext3 从嵌入式工程师的角度来看,在几个方面都非常低效,但那是另一篇文章的主题。
即使是最小的 Linux 发行版也至少包含两个部分:内核和根文件系统。有时,这些组件位于同一个文件中,但它们仍然是独立的组件。通过从内核中删除几乎所有功能(网络、错误日志记录和对大多数设备的支持),并使根文件系统仅为应用程序,系统的大小很容易小于 1MB。然而,许多用户选择 Linux 是为了网络和设备支持,因此这不是一个现实的场景。
Linux 内核很有趣,因为它在编译时依赖于 GCC,但在运行时没有任何依赖项。不熟悉 Linux 的工程师会将初始 RAM 磁盘(所谓的 initrd)与内核运行时依赖项混淆。initrd 首先由内核挂载,并运行一个程序来询问系统,以确定需要加载哪些模块才能支持设备,以便可以挂载“真正的”根文件系统。事实上,两步挂载,即 initrd 之后是真正的根文件系统,很少在嵌入式系统中出现,因为在不经常更改的系统中,灵活性的提高不值得额外的空间或时间。但是,这个主题属于根文件系统的范畴,将在本文后面讨论。
减少内核大小的大部分工作在于删除不需要的内容。由于内核是为桌面和服务器系统配置的,因此它启用了许多嵌入式系统中不会使用的功能。
内核可加载模块是可重定位的代码,内核在运行时将其链接到自身。可加载模块的典型用例是允许从用户空间(通常在某些探测过程之后)将驱动程序加载到内核中,并允许在不关闭系统的情况下升级设备驱动程序。对于大多数嵌入式系统,一旦它们出厂,更改根文件系统要么不切实际,要么不可能,因此系统的设计者将模块直接链接到内核中,从而消除了对可加载模块的需求。然而,这方面的空间节省不仅限于内核,因为管理可加载模块的程序(例如 insmod、rmmod 和 lsmod)和加载它们的 shell 脚本也不是必需的。
Linux-tiny 补丁集是一个断断续续的项目,最初由 Matt Mackall 牵头。消费电子 Linux 论坛 (CELF) 投入精力重振该项目,CELF 开发者 Wiki 提供了针对 2.6.22.5 内核的补丁(在撰写本文时)。与此同时,Linux-tiny 项目中的许多更改已包含在主线内核中。即使许多最初的 Linux-tiny 补丁已进入内核,但一些重要的空间节省补丁尚未进入,例如
细粒度 printk 支持:用户可以控制哪些文件可以使用 printk。这使工程师可以获得排除内核中 printk 的大小优势,同时仍然可以在最需要的地方访问他们最喜欢的调试器。
将 CRC 从计算更改为使用表查找:以太网数据包需要 CRC 来验证数据包的完整性。CRC 算法的这种实现使用表查找而不是计算,节省了大约 2K。
网络调整:几个补丁减少了支持的网络协议、缓冲区大小和打开的套接字。许多嵌入式设备仅支持少数协议,不需要服务数千个连接。
无 panic 报告:如果设备有三个状态指示灯和一个串行连接,用户将无法看到 panic 信息(更不用说对其采取行动了),这些信息会出现在(不存在的控制台上)。如果设备发生内核 panic 故障,用户只需重启设备即可。
减少内联:内联是指编译器不生成对函数的调用,而是将其视为宏,将代码的副本放在每个调用它的位置。尽管内联指令在技术上是一种提示,但 GCC 默认会内联任何函数。通过抑制内联函数,代码运行速度会稍慢,因为编译器需要为调用和返回生成代码;然而,作为交换,目标文件会更小。
Linux-tiny 补丁以 tar 存档形式分发,可以使用 quilt 实用程序应用或单独应用。
尽管 Linux-tiny 项目涵盖了很多方面,但几个额外的配置更改将导致显着的占用空间减少
删除 ext2/3 支持并使用不同的文件系统:ext2/3 文件系统很大,略多于 32K。大多数工程师启用闪存文件系统,但不禁用 ext2/3,从而浪费了内存。
删除对 sysctl 的支持:sysctl 允许用户在运行时调整内核参数。在大多数嵌入式设备中,内核配置是已知的并且不会更改,这使得此功能浪费了 1K。
减少 IPC 选项:大多数系统可以不需要 SysV IPC 功能(在代码中 grep 搜索 msgget、msgct、msgsnd 和 msgrcv)和 POSIX 消息队列(grep 搜索 mq_*[a-z]),删除它们可以再节省 18K。
size 命令报告目标文件中的代码和数据量。这与 ls 命令的输出不同,后者报告文件系统中的字节数。
例如,使用 armv5l 交叉编译器编译的内核报告以下内容
# armv5l-linux-size vmlinx text data bss dec hex filename 2080300 99904 99312 2279516 22c85c vmlinux
text 段是编译器发出的代码(发现代码位于 text 段的历史原因留给读者练习)。data 段包含全局变量的值和用于初始化静态符号的其他值。bss 段包含在初始化过程中清零的静态数据。
尽管这些数据具有启发性,但它并没有显示系统的哪些部分正在消耗内存。没有办法查询 vmlinux 以获取该信息,但查看链接在一起以创建 vmlinux 的文件是次好的选择。要获取此信息,请使用 find 查找内核项目中的 built-in.o 文件,并将这些结果传递给 size
# find . -name "built-in.o" | xargs armv5l-linux-size ↪--totals | sort -n -k4
此命令的输出类似于以下内容
text data bss dec hex filename 189680 16224 33944 239848 3a8e8 ./kernel/built-in.o 257872 10056 5636 273564 42c9c ./net/ipv4/built-in.o 369396 9184 34824 413404 64edc ./fs/built-in.o 452116 15820 11632 479568 75150 ./net/built-in.o 484276 36744 14216 535236 82ac4 ./drivers/built-in.o 3110478 180000 159241 3449719 34a377 (TOTALS)
这种技术使发现占用大量空间的代码变得显而易见,因此项目工程师可以首先删除这些功能。采用这种方法时,用户不应忘记在构建之间进行 clean make,因为从内核中删除功能并不意味着将删除先前构建的目标文件。
对于那些刚接触 Linux 内核的人来说,一个常见的问题是如何将某些 built-in.o 文件与内核配置程序中的选项关联起来。这可以通过查看目录中的 Makefile 和 Kconfig 文件来完成。Makefile 将包含如下行
obj-$(CONFIG_ATALK) += p8022.o psnap.o
当用户设置配置变量 CONFIG_ATALK 时,这将导致构建右侧的文件。然而,内核配置工具通常不会轻易公开底层配置变量名称。要查找变量名称和可见内容之间的链接,请在用于驱动内核配置编辑器的文件 (Kconfig) 中查找变量名称(不带 CONFIG_)
find . -name Kconfig -exec fgrep -H -C3 "config ATALK" {} \;
这将产生以下输出
./drivers/net/appletalk/Kconfig-# ./drivers/net/appletalk/Kconfig-# Appletalk driver configuration ./drivers/net/appletalk/Kconfig-# ./drivers/net/appletalk/Kconfig:config ATALK ./drivers/net/appletalk/Kconfig- tristate "Appletalk protocol support" ./drivers/net/appletalk/Kconfig- select LLC ./drivers/net/appletalk/Kconfig- ---help---
仍然有一些搜索工作要做,因为用户需要找到“Appletalk 协议支持”在配置层次结构中出现的位置,但至少对正在寻找的内容有一个清晰的概念。
对于许多刚接触 Linux 的嵌入式工程师来说,嵌入式设备上的根文件系统的概念是一个陌生的概念。Linux 之前的嵌入式解决方案通过将应用程序代码直接链接到内核中来工作。由于 Linux 在内核和根文件系统之间有明确的分离,因此系统最小化的工作并没有随着内核的小型化而结束。在优化之前,根文件系统的大小使内核相形见绌;然而,按照 Linux 的传统,系统的这一部分也有许多旋钮可以转动以减小此组件的大小。
首先要回答的问题是“我根本需要根文件系统吗?” 简而言之,是的。在内核启动过程结束时,它会查找根文件系统,挂载它并运行第一个进程(通常是 init;执行ps aux | head -2将告诉您它在您的系统上是什么)。在缺少根文件系统或初始程序的情况下,内核会 panic 并停止运行。
最小的根文件系统可以是一个文件:设备的应用程序。在这种情况下,init 内核参数指向一个文件,而该文件是用户空间中的第一个(也是唯一的)进程。只要该进程正在运行,系统就可以正常工作。但是,如果程序因任何原因退出,内核将 panic,停止运行,并且设备将需要重新启动。仅凭这一点,即使是空间最受限的系统也会选择 init 程序。对于非常小的开销,init 包括重新生成死亡进程的代码,从而防止在应用程序崩溃时发生内核 panic。
大多数 Linux 系统更复杂,包括多个可执行文件和经常包含设备上运行的应用程序共享代码的共享库。对于这些文件系统,有几种选项可以大大减小 RFS 的大小。
与 GCC 结合使用,大多数用户不会将 C 库视为单独的实体。C 语言仅包含 32 个关键字(或多或少几个),因此 C 程序中的大多数字节都来自标准库。规范的 C 库 glibc 的设计目的是为了兼容性、国际化和平台支持,而不是尺寸。然而,存在一些从一开始就设计为小型的替代方案
uClibc:该项目最初是作为没有内存管理单元 (MMU-less) 的处理器的 C 库实现而启动的。uClibc 从一开始就被创建为小型,同时通过删除国际化、宽字符支持和二进制兼容性等功能来提供与 glibc 相同的功能。此外,uClibc 的配置实用程序为用户提供了很大的自由度来选择哪些代码进入库,从而允许用户进一步减小尺寸。
uClibc++:对于那些使用 C++ 的人来说,这个库是根据相同的设计原则实现的。凭借对大多数 C++ 标准库的支持,工程师可以轻松地在板载部署基于 C++ 的应用程序,只需几兆字节。
Newlib:Newlib 源于 Red Hat 进军嵌入式市场。Newlib 具有非常完整的数学库实现,因此受到进行控制或测量应用程序的用户的青睐。
dietlibc:仍然是其中最小的,dietlibc 是 glibc 替代品中最不为人知的秘密。dietlibc 非常小,实际上只有 70K 小,它通过删除动态链接库等功能来保持小巧。它对 ARM 和 MIPS 具有出色的支持。
Newlib 和 dietlibc 都通过提供一个包装脚本来工作,该脚本使用正确的参数调用编译器,以忽略编译器附带的常规 C 库,而是使用指定的 C 库。uClibc 有点不同,因为它要求工具链从源代码构建,在 buildroot 项目中提供工具来完成这项工作。
一旦您知道如何调用 GCC 以使其使用正确的编译器,下一步是更新项目的 makefile 或构建脚本。在大多数情况下,项目的构建位于 makefile 中,其中包含如下行
CC=CROSS_COMPILE-gcc
在这种情况下,用户需要做的就是运行make并从命令行覆盖 CC 变量
make CC=dietc
这导致 makefile 为 C 编译器调用 diet。虽然很诱人,但不要将参数添加到此宏中;而是使用 CFLAGS 变量。例如
make CC="gcc -Os"
选择 C 库后,根文件系统中的所有代码都需要使用新的编译器编译,以便代码可以利用更新、更小的 C 库。此时,值得评估静态库与共享库是否是目标的正确选择。如果设备将运行任意代码,并且该代码在部署时未知,则共享库效果最佳;例如,设备可能会公开 API 并允许最终用户或现场工程师编写模块。在这种情况下,设备上拥有库将为那些实现新功能的人提供最大的灵活性。
如果系统包含许多单独的程序而不是一两个程序,那么共享库也是一个不错的选择。在这种情况下,拥有共享代码的一个副本将比在多个文件中复制相同的代码要小。
只有少量程序的系统值得仔细考虑。当只使用少量程序时,最好的方法是分别创建一个使用共享库和不使用共享库的系统,并比较结果大小。在大多数情况下,较小的系统是不使用共享库的系统。作为额外的优势,没有共享库的系统加载和启动程序的速度更快(因为没有链接步骤),因此用户也从效率方面受益。
虽然没有神奇的工具可以使系统更小,但有很多工具可以帮助使系统尽可能小。此外,使 Linux “小型化” 不仅仅是减小内核的大小;还需要严格审查和精简根文件系统,因为此组件通常比内核占用更多空间。本文重点介绍了可执行镜像大小;一旦程序运行,减少程序的内存需求构成一个单独的项目。
资源
Linux-tiny 补丁:www.selenic.com/linux-tiny。一系列用于减小镜像大小和运行时资源的内核小型补丁。其中许多补丁已经进入内核。
GNU C 库:www.gnu.org/software/libc。GNU C 标准库是 C 库的规范实现。在几乎每个具有向后兼容性的平台上运行的需求导致 Lib C 比大多数库都大。
uClibc:www.uclibc.org。Lib C 的一个受良好支持的更小实现。
Newlib:sourceware.org/newlib。Red Hat 的小型 C 库。
dietlibc:www.fefe.de/dietlibc。其中最小的 C 库。它可以与现有的交叉编译器很好地配合使用,因为安装会为 GCC 创建一个“包装”程序,使用正确的参数调用它,使使用 dietlibc 构建非常容易。
Gene Sally 在过去七年中一直从事嵌入式 Linux 的各个方面的工作,并且是 LinuxLink Radio 的联合主持人,LinuxLink Radio 是最受欢迎的嵌入式 Linux 播客。可以通过 gene.sally@timesys.com 联系 Gene。