原文:eLinux.org
翻译:@lzufalcon
校订:@ibrother
本章包含的话题有启动时间的测量、分析、人因工程(human factors)、初始化技术和优化技巧等。
产品花在启动方面的时间直接影响终端用户对该产品的第一印象。
一个消费电子设备不管如何引人注目或者设计得怎么好,设备从关机状态到可交互的使用状态所需的时间对于获得正面的用户体验尤为关键。案例 #1 就是在关机状态从头启动一个设备的例子。
启动一个设备涉及到许多步骤和一系列的事件。为了使用前后一致的术语,消费电子 Linux 论坛(CE Linux Forum)的启动时间优化工作组起草了一个术语词汇表,该表包括了相关术语在该领域内通用的定义。该词汇表如下:
下面主要介绍与减少 Linux 启动时间有关的各种技术。
有一部分描述了 eLinux.org 上可以下载的本地补丁,而其余部分则介绍了在其他地方维护的项目或者补丁。
cat /proc/uptime
(译注:统计系统已经运行的时间)
关闭控制台 - 避免启动过程中的控制台输出开销(译注:尤其是串口控制台,会严重拖慢系统启动,甚至带来各种实时问题)
关闭调试接口 和 printk
- 避免调试接口和printk
带来的开销,缺点是将丢失大量(对于调试可能有用)的信息
不同步 RTC - 避免在启动时延迟用 RTC 时钟边沿同步系统时间(可能带来时钟漂移)
更短的 IDE 延迟时间 - 减少 IDE 启动延迟的持续时间(有效但是可能危险)
硬编码内核模块信息 - 通过硬编码用于加载重定位信息的内容来减少加载模块的开销
不侦测 IDE - 强制内核使用 ide<x>=noprobe
命令行选项,从而绕过 IDE 侦测
预设 LPJ - 允许使用一个预设的 loops_per_jiffy
(同样可以通过内核命令行选项设置)
异步函数调用 - 允许侦测(Probe)函数或者其他函数并行处理,从而让耗时的启动活动并行起来
kernel/async.c
)延迟 Initcalls - 延迟不重要的模块初始化函数到主要启动过程之后
NAND ECC(Error Correcting Code)技术改进 - 2.6.28 之前的 nand_ecc.c
有改进的空间,可以从 mtd git 仓库 找到这样一个改善的版本,相应文档在这里。这个当且仅当系统使用软 ECC 校验时才有效
检查内核正在使用哪个内存分配器,Slob 或者 Slub 可能比 Slab 更好,早期内核默认使用 Slab,可根据需要切换为 Slob 或者 Slub
如果系统不需要,可以从内核中去掉 SYSFS 甚至 PROCFS 支持。一项测试表明删掉 SYSFS 可以节省 20 ms
仔细调研所有的内核配置选项,看看它们是否有用。即使选上的选项最终没有用到,它也可能增加内核尺寸因而会增加内核加载时间(假设没有使用就地执行 XIP 功能)。通常,这个需要一些试验和测试!例如,选上 CONFIG_CC_OPTIMIZE_FOR_SIZE
(在内核的 General Setup
配置菜单下面)在一个案例中能够产生 20ms 的启动优化。虽然不是很显著,但是对于启动时间优化来说,积少成多就能看到效果(Not dramatic, but when reducing boot time every penny counts!)。
迁移到一个不同的编译器版本可能会产生更短和更快的代码。通常,新的编译器能够产生更优的代码。你也可以玩转一下各种编译器选项看看哪些表现最好。
如果在内核中用上 initramfs 和压缩了的内核,那么最好是不要再压缩 initramfs。这个主要是为了避免重复两次解压数据。一个针对该问题的补丁被提交到了 LKML(译注:Linux 内核邮件列表):http://lkml.org/lkml/2008/11/22/112
对于同样的数据集,不同的文件系统拥有不同的初始化(即挂载,mounting)时间,这取决于元数据(meta-data)是否必须从存储器读到内存并且在挂载过程中使用哪种算法。
优化 RC 脚本 - 减少执行 RC 脚本的开销
并行执行 RC 脚本 - 以并行而不是串行方式执行 RC 脚本
就地执行应用程序 - 允许程序和库能够在 ROM 和 FLASH 中就地执行
预链接 - 避免在首次加载程序时进行运行时链接
静态链接应用程序。这样可以避免运行时链接。如果应用程序少的话会有用,这样也可以减少镜像文件的大小,因为不再需要动态库。
GNU_HASH
: 在动态链接时有大约 50% 左右的速度改善
应用程序初始化优化 - 通过如下方法优化程序加载和初始化时间:
加速模块加载 - 使用 Alessio Igor Bogani 的内核补丁来改善模块加载时间:加速符号定位进程。
避免使用 udev
,因为它花费相当一部分时间来构建 /dev
目录。在嵌入式系统中,通常会事先知道需要哪些设备,在哪些情况下用哪些驱动,所以我们知道在 /dev
下,哪些设备入口需要创建,这些应该静态而不是动态创建。所以,在这里,mknod
是友,udev
是敌。
如果你还是喜欢 udev
而且也喜欢快速启动,也可以尝试这种方法:在系统启动时开启 udev
并备份它创建的设备节点。接着,修改系统的初始化脚本成这样:不再运行 udev
,而是把刚备份的设备节点拷贝到 /dev
目录中,之后再像往常一样安装热插拔守护进程(即 udev
)。这个戏法避免了启动时的设备节点创建但是还是可以让系统在之后(按需动态)创建设备节点。
如果设备有连接网络,最好使用静态 IP 地址。通过 DHCP 获取地址会增加时间并产生相应的额外开销。
迁移到不同的编译器版本可以产生更短和更快的代码。通常新编译器产生更优的代码。你也可以玩转各个选项看看哪个最佳。
如果可能,从 glibc 转到 uClibc。这个会产生更小的可执行文件因此会有更快的加载时间。
库优化工具:http://libraryopt.sourceforge.net/
该工具允许创建优化的库。不需要的函数会被移除因此而获得更好性能。正常情况下,有些库所占用的内存页面包含不会用到的代码(临近用到的代码),经过优化后,这种情况不会再发生,所以可以花更少的内存页面因此会有更少的内存加载,结果一些时间就会省掉。
这是一种技术,用于重排可执行文件中的函数,确保它们根据需要依次出现。它可以改善应用程序加载的时间,因为所有的初始化代码被打包成一组页面,而不是离散成多个不同的页面。
另外一个改善启动时间的途径是利用休眠相关的机制。已知的两种方案是:
使用标准的休眠/唤醒方案。这个已经由来自三星的 Chan Ju, Park 演示过。看表格 23 以及之后的内容:PPT 以及这篇论文 的第 2.7 节。
该方法的一个问题是闪存写比闪存读慢很多,所以实际上创建一个休眠镜像可能消耗相当长的一段时间。
实现快照启动。这个由 Sony 的 Hiroki Kaminaga 完成并且描述在 ARM 快照启动 和 Snapshot-boot-final.pdf
这个类似上述休眠/唤醒方案。但是休眠文件被保留并且在每次启动时使用。缺点是在制作快照时不可以挂载可写分区,否则当分区被修改,而休眠文件中的应用在映像中有未修改分区有关的信息时,就会产生不一致性。
关于压缩 一文讨论了压缩技术在启动时间优化方面的效果。这个能够影响内核与用户空间两者的启动时间。
该节只是对没来得及实现但可能利于启动优化的一些想法做一个记录。
预填充的缓冲区高速缓存 - 因为 initramfs 执行了额外的数据拷贝,所以想法是可以有一个预填充的缓冲区高速缓存。一种简单的场景是当启动完成并且应用初始化完了以后,就允许把缓冲区高速缓存转存出来。然后这个数据可以在后续的启动中用于初始化缓冲区高速缓存(当然不需要拷贝了)。一种可能的方法是把这些数据直接打包到内核镜像中并直接使用,当然也可以选择分开加载。不幸地是,我现在具有的这块知识不足以做一个简单的实现。注意事项:
专用文件系统 - 当前的文件系统中有大量的抽象,这些抽象方便了新文件系统的添加并且为所有文件系统创建了统一视图。无疑这个设计很漂亮整洁,但是这些抽象层也引入了一些开销。一种解决方案或许是创建一个专有的文件系统,只支持 1 到 2 种文件系统,从而可以消除抽象层带来的开销。这或许会有用,不过合并到主线的几率为 0 。
"启动时间优化" - (幻灯
"减少启动时间的正确途径" - (幻灯
"1 秒钟的 Linux 启动演示(新版)" (Youtube 视频(来自MontaVista))
"用于减少启动时间的工具和技巧" - (幻灯 | ODP | PDF
Christopher Hallinan 于 2008 年在 MontaVista 视讯会议上做了一个关于如何减少启动时间的报告,幻灯在这里
PRINTK_TIMES
特性来测量 x86 系统启动时间,包括 BIOS、引导程序、内核以及第一个用户程序。Tim Bird 做的关于启动时间优化技术的调查:
在消费电子设备上并行化 Linux 启动 - (PDF
TI 嵌入式处理器的维基也提供了一些信息,描述了 Linux/Android 启动时间的优化过程
实现 Android 的检查点技术(Checkpointing)
启动 Linux 系统的传统方法是用 /sbin/init
,它是一个初始化程序,负责解析 /etc/inittab
,并能够针对不同的运行级别和系统事件(各类按键组合和电源事件)产生一系列动作。
可查看 init(8) 手册页 和 inittab(5) 首页页 获取更多信息。
BusyBox 通常包含一个 init
小程序。
截止 2000 年,Busybox init 和全功能的 init 之间,inittab
支持的特性稍微有些差异。但是不知道到 2010 以后(译注:现在都 2015 年了,看来这个文章真地有点老了,后面有时间很多材料得好好整理下),情况是否依旧。可以从这里看到一些细节。
Denys Vlasenko, Busybox 的维护人员之一,曾经建议用名叫 runsv
的工具替换掉传统的 init
,可以看下这里。
upstart
是一个新的 Linux 桌面工具,它提供了 /sbin/init
,但是实现了不同的操作语义:
Android init
是专门为启动 Andriod 系统而定制的程序,看这里。
systemd 是一个新的项目 (就当时 2010 年 5 月而言),用于在 Linux 桌面系统中启动守护进程和服务,可以看看这里。
可以从 2009 年 8 月的一个关于 ARM 设备上启动时间的讨论中找到一些事关启动时间优化的提示和建议。
虽然可能会重复本文中已经介绍的内容,但是咱们还是从上述讨论中提取了一个检查清单(译注:可以方便启动优化时做检查):
CPU 时钟频率切到最大了吗?如果内核、引导程序或者硬件负责设置 CPU 功率和速度,那么我们得确保各 CPU 用最大速度而不是最低速度启动。
SOC 上的内存接口硬件(寄存器)的定时配置(例如 RAM 和 NOR/NAND 定时)优化过吗?许多供应商卖硬件时采用保守的设置:“好吧,工作了,以后再优化”。我们想要的配置是:“尽可能地快,但是还得稳定且可靠”。这部分可能需要一些硬件知识并且必须为不同内存设备做不同配置(译注:不同内存设备特性不一样,比如说工作频率会有差异)。
我们的引导程序用了指令和数据缓存吗?例如 U-Boot 在 ARM 设备上默认没有使能数据缓存,因为要使能数据缓存,需要额外定制 MMU 表。
内核从存储设备(例如 NOR 或者 NAND)拷贝到内存中使用了优化过的方法吗?比如 DMA 或者是 ARM 上至少得用上 load/store multiple
命令(ldm/stm)?
如果用 U-boot 的 uImage,在启动参数中设置 verify=no
可以避免校验码验证。
优化内核的尺寸
拷贝了几次内核镜像数据?第一次由引导程序从存储设备拷到内存,然后内核的解压程序负责解压到最终的位置?如果使用压缩了的内核和 NOR 闪存,可以考虑在 NOR 闪存中就地执行(XIP)解压器。
如果使用了压缩内核,检查下压缩算法。zlib 在解压方面较慢,而 lzo 则更快。所以如果采用 lzo 压缩,我们也可以加速一些东西(可以从 LKML 查看这些)。如果没有任何压缩也可能是一件好事(看下一个话题)。
检查是否使用了未压缩的内核(根据系统配置)。在闪存系统上使用未压缩的内核可以优化启动时间。原因是当存储设备的速率低于解压速率时,压缩过的内核才有优势(译注:拷贝时间 v.s. 解压时间)。在支持 DMA 的典型嵌入式系统上,其读取到内存的速率胜过基于 CPU 的解压速率。当然,这取决于很多方面,比如闪存控制器的性能、内核存储文件系统性能、DMA 控制器性能、缓存架构等。所以,每个系统会有个体差异。举例来说:使用未解压的内核(约 2.8MB)解压(在 NOR 闪存中就地执行解压程序)比从闪存拷贝 2.8MB 内核到内存中多花费了约 0.5 秒。
使用提前计算好的 loops-per-jiffy
,(译注:然后通过内核命令行参数传给内核,可避免执行重新计算的代码)
使能内核 quiet
选项(译注:可关闭控制台输出)
如果使用 UBI 文件系统:UBI 用到 MTD 设备上相当缓慢。细节在 MTD 的 UBI 伸缩性 一文中有解释。如果不用 UBI2,我们基本啥也优化不了,UBIFS 将没啥用(UBIFS would stay intact),有一些关于这个的讨论,并且貌似要做 UBI2 相当困难:(一些想法)。
"非常有趣的是,内核 NAND 驱动比 U-boot 中的慢很多,看完发现结果是内核驱动用了中断来判断控制器是否准备好,换成轮询以后 NAND 的性能提升了一倍。UBI 挂载更快并且这个为启动过程又省掉了几秒钟 :-) “
启动时使用静态设备节点,之后再设置 Busybox mdev 来做动态热插拔
如果使能了网络,那么在网络代码路径中可能会有很长的超时设定,具体取决于是否指定了静态地址。可以看下 net/ipv4/ipconfig.c
文件中的定义:CONF_PRE_OPEN
和 CON_POST_OPEN
以及IP 延迟配置补丁.
并行化启动过程
关闭该选项:"Set system time from RTC on startup and resume",可避免减慢内核启动。(要实现 RTC 同步),我们可以在 init 末尾使用 hwclock -s
命令。
分类: