嵌入式 Linux 启动时间优化

2015年11月6日星期五

Fast Boot有些应用对系统启动时间有着特殊的要求。在很多场合下,这些系统并不需要针对所有任务立即就位,但是针对某些关键任务(例如接收以太网命令或者显示用户界面)则必须能够应对。该博文将提供一些方法和简单的步骤,在 Toradex 系统模块上优化启动时间。


提示: 文中涉及到的部分方法需要重新编译 U-boot、内核以及文件系统。请参考我们开发者中心网站上的相关文章。

在我们开始动手优化之前, 我们需要一个合适的方法来测量启动时间。如果想要十分精准地测量启动时间,这甚至需要牵涉到硬件(例如 GPIO 和示波器)。在绝大多数场合下,通过监控系统串口控制台输出已经是相当准确了。Tim Bird 的 grabserial 是一个广泛使用的工具,可以用于产看串口控制台输出的时间信息。这个工具能够为收到的每一行信息添加上时间戳,如下面所示:

$ ./grabserial -d /dev/ttyUSB1 -t
[0.000002 0.000002]
[0.000171 0.000169]
[0.000216 0.000045] U-Boot 2015.04-00006-g6762920 (Oct 12 2015 - 15:35:50)
[0.005177 0.004961]
[0.005227 0.000050] CPU: Freescale Vybrid VF610 at 500 MHz
[0.008938 0.003711] Reset cause: POWER ON RESET
[0.011153 0.002215] DRAM:  256 MiB
[0.063692 0.052539] NAND:  512 MiB
[0.065568 0.001876] MMC:   FSL_SDHC: 0

第一列数字代表时间戳(从收到第一个字符算起),第二行代表的是收到当前一行和上一行信息之间的时间间隔。

该文章基本上适用于所有我们的模块。然而,这其中我所使用的方法和改进之处中确实有一些是针对基于 NXP®/Freescale Vybrid 的 Colibri VF61 模块。

Linux 系统的启动,主要可以分为以下 3 个阶段,文章将逐一讨论。

  • Boot loader
  • Linux kernel
  • User space (init system)
嵌入式 linux启动时间优化_第1张图片
视频:Colibri VF61 Linux 2 秒快速启动

Boot loader
实际上在 boot loader 启动之前,还有两个步骤:硬件初始化和 boot ROM。硬件初始化需要满足电源上电顺序以及总线和处理器芯片复位时序要求。这个阶段的耗时一般是固定的,在 10 ~ 200 ms。Arm 处理器从位于内部的 ROM 上启动固件。该固件从启动介质上加载 boot loader。该阶段的时间一般很短,取决于 boot loader 的大小。除了减小 boot loader 体积,很难做其他的优化。实际上能够做的优化和调整还是在 boot loader (U-Boot)。

目前发布的 V2.5 Beta 1 版本,从第一个字符输出到内核启动的时间约为 1.85 秒。主要涉及以下过程:

  • U-Boot 初始化(约 110 ms,从接收到的第一个字符算起)
  • Autoboot delay(1s)
  • UBI 初始化和 UBIFS 挂载(约 300 ms,得益于 Fastmap 功能,不使用该功能将耗时 1.6s)
  • 内核加载(375 ms)
  • 加载和应用 device tree(约 35 ms)
  • 最后跳转至内核起始地址
Boot time to Kernel start: ~1850ms

最显著的优化就是降低 Autoboot delay 。这个值可以使用下面的命令设置为 0:

setenv bootdelay 0
saveenv

这个也可以使用 CONFIG_BOOTDELAY 将其配置为默认值。在目前的发布版本中,如果将 bootdelay 设置为 0,那么将会没有办法直接进入 boot loader 的命令行模式。U-Boot 提供一个选项 CONFIG_ZERO_BOOTDELAY_CHECK,在 bootdelay 为 0 的情况下,用于检测一个字符。我们已经将其添加到下一个发布版本的默认配置中。

Boot time to Kernel start with this improvement: ~860ms

串口输出是同步发送的。这意味着 CPU 将等待,直到字符通过在串口线上发送完毕。因此,每一个输出的字符都将减慢 U-Boot 的启动。特别是 UBI 将会输出大量的信息,这是一个可以优化的地方。有一个配置符号 CONFIG_UBI_SILENCE_MSG 可以实现这个目的。

Boot time to Kernel start with this improvement: ~800ms

为了确保尽可能高效地使用硬件,需要深入了解硬件的功能以及目前实现的方法。目前还没有被使用的功能是 Level 2 Cache(仅 Colibri VF61)。在开启 Level 2 cache 后,启动时间可以提高 40 ms。

Boot time to Kernel start with this improvement: ~760ms

移除一些功能有助于减少分配时间和初始化这些功能的时间。例如可以移除显示支持 (DCU),EXT3 和 EXT4 支持以及 USB 外设驱动如 DFU 和 存储设备。这可以将 U-Boot 尺寸减小到 366 KB,同时节省 10 ms 时间。

Boot time to Kernel start with this improvement: ~750ms

根据显示的时间戳,绝大多数的时间被用于加载 UBI 和挂载 UBIFS 以及内核的加载(约 380 ms)。显然内核大小和加载时间具有线性关系,因此,优化内核尺寸将可以进一步提高启动时间。

Kernel
为了只测量内核启动的时间,可以使用 grabserial 的匹配功能重置 boot loader 输出信息中的时间。

./grabserial -d /dev/ttyUSB1 -t -m "^Starting kernel.*"

启动结束的时间多少有点难以确定,因为内核将会继续初始化硬件,即使文件系统已经挂载和第一个用户空间进程(init)开始运行(延时初始化)。“Freeing unused kernel memory” 是 init 进程启动前发出的最后消息,因此将其标记为内核线性任务的结束(请查看 kernel_init in init/main.c)。我将会使用这个信息的时间戳信息来比较启动时间。我们模块上默认内核的压缩尺寸为 4316 KB,启动时间为 2.56 秒。

Kernel boot time to Init start: 2.56s

同 U-Boot 一样,Linux 内核也是同步地将信息发送到串口。具体的方法取决于所使用的串口,LPUART( Vybrid 的 console 驱动)会同步等待直到字符在串口上发送完毕。这个的优点在于,当内核崩溃的时候,那个时候的所有信息都是可见的。假如信息是异步输出,最后输出的信息将不会指示内核所崩溃的地方。

内核中有一个参数,可以最大限度减少输出的信息:“Quiet”。然而,这也将屏蔽我们测试启动时间的字符信息(“Freeing unused kernel memory”)。最简单的方法输出这些信息是利用日志级别输出特定的信息。在 'mm/page_alloc.c' 中搜索 “Freeing %s memory”。我将使用 ‘pr_alert’ 输出信息。这个方法起高了 1.55 秒,减少了超过一般的时间。

Kernel boot time to Init start with this improvement: ~1.01s

进一步提高启动时间的另一个简单的方法是移除功能。Yocto 项目提供了一个方便的工具 ksize.py,这个需要在内核编译目录中运行。这个工具能够显示内核各个部分的大小。第一个表格显示了大致的概况(为了获得准确的概况, 编译之前使用 make clean)。

Linux Kernel              total |       text       data        bss
-------------------------------------------------------------------
vmlinux                 8305381 |    7882273     247732     175376
drivers/built-in.o      2010229 |    1881545     109796      18888
fs/built-in.o           1944926 |    1911100      19422      14404
net/built-in.o          1477404 |    1398316      44832      34256
kernel/built-in.o        628094 |     514935      17099      96060
sound/built-in.o         326322 |     316298       8248       1776
mm/built-in.o            288456 |     276492       8000       3964
lib/built-in.o           160209 |     157659        217       2333
block/built-in.o         137262 |     133614       2420       1228
crypto/built-in.o        104157 |     100063       4082         12
security/built-in.o       37391 |      36303        788        300
init/built-in.o           31064 |      16208      14772         84
ipc/built-in.o            29366 |      28640        722          4
usr/built-in.o              138 |        138          0          0
-------------------------------------------------------------------
sum                     7175018 |    6771311     230398     173309
delta                   1130363 |    1110962      17334       2067

可以被安全移除的一般是应用相关的功能。浏览各个第一层目录,有助于快速确定最可能移除的对象。为了文中的演示,我移除了部分文件系统(cifs, nfs, ext4, ntfs)、音频子系统、多媒体支持、USB 以及无线网络适配器支持。内核最后缩减到 3356 KB,比原来将近小了 1MB。这也减少了约 85 ms 的内核加载时间。

Kernel boot time to Init start with this improvement: ~0.90s

另外一个提高启动时间的方法可以是使用不同的压缩算法,即使是目前我们内核配置中默认算法 LZO,这也已经是被充分得利用。

User Space
在 Linux 用户空间,初始化工作由 init 系统完成。 Toradex BSP 镜像使用 Ångströ标准启动 init 系统,其称为 Systemd。Systemd 目前已成成为桌面 Linux的标准 init 系统,具有丰富的功能,特别是为动态系统所设计。Systemd 同样会影响启动时间。多个守护进程可以用同时启动(利用现在的多核系统); 支持在稍后的一个时间延时加载服务时激活 socket 以及支持按需启动的设备激活。并且,集成的日志守护进程 journald 由于使用二进制日志文件和完善的日志文件管理可以节省空间。

根据实际应用,一个嵌入式系统可能是相当静态的。因此,并不需要 Systemd 的动态功能。不幸的是,Systemd 并不是一个很模块化的系统,各个模块之间由相互依赖关系。这使得精简 Systemd 变得困难。这一节将分文两部分,第一部分使用 Systemd 启动优化技术,第二部分会使用 System V 和其他技术。

在这两部分中,我们使用 “Freeing unused kernel memory” 作为测量基准时间。

./grabserial -d /dev/ttyUSB1 -t -m "^\[ *[]0-9.]* Freeing unused kernel memory.*"

systemd
在这个博文中,我们定义串口输出上的 login shell 作为关键任务。login shell 定义为 “Type=Idle”,根据定义它将在所有服务启动后在运行。

为了启动一个没有界面或者基于 framebuffer 的应用,一般需要创建一个新的服务。Systemd 允许定义服务运行之前所需的条件(例如 Network 中 “Wants=network-online.target”)以及自动确保当所需的条件满足之后服务能够启动。然而,因为这些服务是同时启动的,所以 CPU 资源需要在它们之间共享。但是应用仍有可能在串口控制台可用之前就开始启动和运行。因此,下面的数字有可能更高。

User space boot time to Login without improvements: ~8.6s

内核参数中的 quiet,同样也适用于 Systemd。这个有助于 Systemd 的启动时间,减少约 1.6 秒。

User space boot time to Login with this improvement: ~6.5s

Systemd 提供了 systemd-analyze 工具,当使用 “blame” 时,能够打印出各个服务以及其启动的时间。这个可以发现最消耗启动时间的服务。但是,其中的值可能具有迷惑性,因为测量的时间是实际流逝的时间。服务有可能处于睡眠状态,这时的 CPU 其实在处理其他任务。所以在列表顶部的服务不一定是最耗时的,特别是在单核系统上。

服务可以使用 disable 命令来关闭。有些服务(特别是 Systemd 自身提供的)可能需要掩码才能关闭它们。另外有一些可能是系统运行所需的。因此,在关闭服务时需要特别小心,而且一次只能处理一个。在本博文中,下面的服务已经被关闭:

systemctl disable usbg
systemctl disable connman.service # replaced with networkd
systemctl mask alsa-restore.service
User space boot time to Login with this improvement: ~6.1s

Systemd 自带的系统日志守护进程为 journald。这是一个不应该完全禁用的组件之一。在启动的时候,日志守护进程需要管理和删除在磁盘上的旧文件,以及写入新文件。通过禁止将日志写入磁盘,可以提供启动时间,代价是日志文件将不会被保存。配置 /etc/systemd/journald.conf 中的 Storage=none,禁用日志保存。

User space boot time to Login with this improvement: ~5.6s

System V init 和其他方法
在很长一段时间内,Linux 也使用 SysV 作为标准的 init 系统。由于基于脚本的系统,这是模块化的,并且可以相对容易地精简系统。特别是对相对静态的系统,并不需要 Systemd 的设备激活和 socket 激活。此时,SysV 可以是很好的选择。

我上一篇博文[韬睿硬件的 Yocto 项目参考构建]中提到的 Yocto 项目参考构建 “poky” 就默认使用了 SysV。通过使用 ‘minimal-console-image’ 和静态 IP 配置,Colibri VF61 上用户空间启动时间约为 2.3 s。

User space boot time to Shell with System V: ~2.3s

meta-yocto 层同样提供 ‘poky-tiny’,其使用 shell 脚本作为 init 系统。只要用 “poky-tiny” 替换发布版本,编译一般的 Yocot 镜像,例如 ‘console-image-minimal’。该发布板用作 initramfs。然而,通过移除 conf/distro/poky-tiny.conf 文件中的 MACHINE_ESSENTIAL_EXTRA_RDEPENDS、IMAGE_FSTYPES 和 PREFERRED_PROVIDER_virtual/kernel,我能够编译可用的 UBIFS 镜像。为了可烧写的 root file 系统所需正确的重新配置,你需要创建新的发布版层并且复制配置文件。这样,启动到 shell 的时间是相当快的(220 ms),到可以执行简单命令的总体启动时间小于 2 秒。当然,这仅仅只是提供了挂载 root file 系统、一些基本的虚拟文件支持和 shell 。同样地,根据项目所需功能的数量,可以将这作为一个好的起点。

User space boot time to Shell with a Shell script only: ~0.2s

更多的资源:
http://free-electrons.com/doc/training/boot-time/boot-time-slides.pdf