[博客翻译]Raspberry Pi 3快速启动不到2秒


原文地址:https://www.furkantokac.com/rpi3-fast-boot-less-than-2-seconds/


Raspberry Pi 3 快速启动 - 少于2秒

这篇帖子讲述了我为Raspberry Pi 3 (RPI)实现快速启动的旅程。此外,还讨论了一些可以应用于Qt (QML)应用程序的优化方法。最终,我们将从上电到Linux shell的启动时间缩短到1.75秒,从上电到Qt (QML)应用程序的启动时间缩短到2.82秒。

编辑: 有用户请求支持USB和网络功能的演示镜像。我将在空闲时间进行这项工作。你也可以自己尝试。如果遇到问题,请不要犹豫,从下面的支持链接提问。我简要地讨论了这个话题在这里

技术支持: github.com/furkantokac/buildroot/issues
项目文件: github.com/furkantokac/buildroot
演示镜像: github.com/furkantokac/buildroot/releases
你可以在第6部分查看演示镜像的详细信息。


大纲

1. 引言
2. 项目需求
3. Raspberry 启动文件
4. Raspberry 启动优化
  K1 - Raspberry 启动阶段
  K2 - Linux 预启动阶段
  K3 - Linux 启动阶段
  K4 - 初始化系统
  K5 - 应用程序
5. 更多优化!
6. 要点汇总
7. 结果
8. 参考文献

1. 引言

首先,我们需要充分了解目标设备,因为启动优化过程的一些关键阶段是低级别的(硬件依赖的)。我们需要能够回答诸如设备的启动顺序是什么、哪些文件按什么顺序运行以启动设备、哪些文件是绝对必需的等问题。此外,优化应该逐项进行和测试,以便可以看到效果。

RPI的启动过程与其他传统设备不同。RPI的启动过程基于GPU而非CPU。建议你在互联网上深入研究这个话题。(参见19

RPI使用Broadcom的闭源芯片作为片上系统(SoC)。因此,与SoC相关的软件以二进制形式提供给我们。(参见2)这意味着没有逆向工程就无法定制它们。这就是为什么RPI启动优化过程中最困难的部分是与SoC相关的内容。

2. 项目需求

  • 使用RPI作为设备。
  • 使用Buildroot进行Linux的定制。
  • RPI的GPIO和UART应当可用。
  • GPIO和UART在Qt中应当可用。
  • Qt (QML) 应用程序应自动启动。

3. Raspberry 启动文件

与RPI启动过程相关的文件及其用途简要如下:

  • bootcode.bin:这是第二阶段引导加载程序,由制造商嵌入RPI中的第一阶段引导加载程序运行。运行在GPU上。激活RAM。其目的是运行start.elf(第三阶段引导加载程序)。
  • config.txt:包含GPU设置。由start.elf使用。
  • cmdline.txt:包含启动内核时传递给内核的参数。由start.elf使用。
  • .dtb:编译后的设备树文件。它包含设备的硬件描述,如GPIO引脚、显示端口等。由start.elf和kernel.img使用。
  • start.elf:这是由bootcode.bin运行的第三阶段引导加载程序。它包含GPU驱动程序。其目的是在GPU和CPU之间分配RAM,将config.txt文件中的设置应用到GPU,通过读取相应的.dtb文件进行必要的调整,并使用cmdline.txt文件中的参数运行kernel.img。执行这些操作后,它将在设备上作为GPU驱动程序运行,直到设备关闭。
  • kernel.img:这是Linux内核,由start.elf运行。内核运行后,我们对一切有了完全的控制。
  • 基本逻辑:RPI上电 -> RPI内部嵌入的软件运行 -> bootcode.bin运行 -> start.elf运行 -> 读取config.txt -> 读取.dtb -> 读取cmdline.txt -> kernel.img运行

4. Raspberry 启动优化

RPI从上电到Qt应用程序启动的过程如下:
K1 - Raspberry 启动阶段(第一和第二阶段引导加载程序)(bootcode.bin)
K2 - Linux 预启动阶段(第三阶段引导加载程序)(start.elf, bcm2710-rpi-3-b.dtb)
K3 - Linux 启动阶段(kernel.img)
K4 - 初始化系统(BusyBox)
K5 - 应用程序(Qt QML)

K1 - Raspberry 启动阶段

在这个部分,制造商嵌入设备中的软件运行bootcode.bin。由于bootcode.bin是闭源的,我们不能直接配置它,所以我们可以做两件事。要么尝试不同的bootcode.bin版本,要么尝试更改由bootcode.bin运行的文件。(我们忽略逆向工程)

我们去看RPI的Git页面(参见12),看到没有不同版本的bootcode.bin可用。我们查看bootcode.bin的旧提交并尝试旧版本,发现速度没有变化。我们可以转到另一个选项。

让我们检查start.elf。在RPI的Git页面上,我们看到有不同版本的start.elf文件:start_cd.elf、start_db.elf、start_x.elf。我们查看这些文件的差异,发现start_cd.elf是start.elf的简化版本。在start_cd.elf中,GPU功能被裁剪,这可能会引起问题,但让我们试一试。当我们用start_cd.elf替换我们的start.elf时,启动过程比以前快了0.5秒。然而,当我们运行Qt应用程序时,它失败了。为什么会失败,我们能修复吗?我们的GUI应用程序运行在OpenGL ES上,而start_cd.elf没有为GPU分配足够的内存。尽管我们尝试克服这一困难,但未成功,但我相信如果投入更多时间,可以解决这个问题。

K2 - Linux 预启动阶段

这部分由start.elf处理。由于start.elf是闭源的,我们不能直接对其进行操作,但有一些与start.elf相关的开源文件:bcm2710-rpi-3-b.dtb、kernel.img

我们可以做的第一件事是检查这些文件是否减慢了start.elf的速度。当我们将Device Tree移除时,内核无法启动。这里有两种可能性;问题是出在start.elf还是内核。我们需要找到一种方法来测试它。一个无需Device Tree即可运行的应用程序可以解决问题,这是一个基础的RPI应用程序。如果我们编写一个小应用程序,并让start.elf运行这个应用程序而不是内核,就可以看出移除Device Tree是否会带来任何速度变化。第二个选项是编译U-Boot并运行U-Boot而不是内核,但第一个选项更好。我们编写了一个基础的LED闪烁应用程序(参见13)。当我们运行它时,我们发现移除Device Tree使启动过程快了1.0秒。我们还尝试更改Device Tree的默认名称(bcm2710-rpi-3-b.dtb),仍然可以正常工作。所以结论是:即使我们不启动内核,Device Tree也会由start.elf处理,并且start.elf特别搜索名为“bcm2710-rpi-3-b.dtb”的Device Tree。总结一下,我们要么去掉Device Tree,要么通过更改其名称来使用它。

设备树选项的重命名可以按以下方式进行;我们可以编写一个由 start.elf 运行的基础软件,并通过使用重命名的设备树来处理内核启动过程。由于我们需要在这里运行额外的代码,因此会有时间损失。因此,让我们检查另一个选项,即取消设备树。

我们在测试中看到,设备树对于启动内核是绝对必要的,但不是 start.elf 所必需的。如果设备树与内核相关,我们可以通过某种方式尝试将设备树配置硬编码到内核中。当我们搜索关于设备树的信息时,发现内核已经存在类似的选项。(参见 3 第11页)当我们进行了必要的设置(K3 包含了此设置的信息)后,我们发现内核可以成功启动。让我们测试一切是否正常工作。

测试后,我们观察到:

  • Qt 应用程序工作正常。
  • UART 停止工作。
  • 我们发现内核的启动时间慢了0.7秒。

让我们检查一下 UART 的问题。我们将 UART 出现问题的内核启动日志和 UART 正常运行的内核启动日志保存下来。(这里所说的启动日志是指“dmesg”命令的输出)。当我们比较日志时,发现从“内核命令行:”开始的行有所不同。在 UART 正常运行的系统中,“8250.nr_uarts = 1” 参数传递给了内核。我们将这个参数添加到问题内核的 cmdline.txt 文件中,UART 就完美地工作了。让我们继续解决其他问题。

我们应该检查是什么导致了大约1.0秒的启动延迟。我们将再次使用相同的日志。当我们比较有问题系统的日志和没有问题系统的日志时,发现有问题的系统中多了一条包含“random”的日志,延迟就在这里。当我们逐个关闭内核中的“random”设置时,找到了问题所在。(K3 包含了此设置的信息)关闭该设置后,我们看到一切恢复了正常。任务完成。

结果,启动过程加快了约2.0秒。K2 的总耗时为0.25秒。我们可以在这里继续优化,比如优化设备树,但我认为通过进入下一步会更高效地利用时间,所以让我们继续前进。

K3 - Linux 启动阶段

我们在 K2 部分解释了一些内核优化。本部分列出了我们已修改的内核特性。要了解特定特性如何影响内核启动过程,请访问项目的 Git 页面(参见 5),并根据需要在线进行详细的设置研究。

启用的特性

ARM_APPENDED_DTB    : 嵌入设备树以加快启动速度。
ARM_ATAG_DTB_COMPAT : 必须将某些参数传递给内核。

禁用的特性

NET
SOUND
HW_RANDOM           # 0.7秒
ALLOW_DEV_COREDUMP  # 0.2秒(Core Release: 2.80a)
STRICT_KERNEL_RWX   #===\ 0.1秒
STRICT_MODULE_RWX   #===/
NAMESPACES          # 0.1秒
FTRACE              # 0.5秒

# 禁用 USB 支持
USB_SUPPORT

# 禁用调试
BLK_DEBUG_FS
DEBUG_BUGVERBOSE
DEBUG_FS
DEBUG_MEMORY_INIT
SLUB_DEBUG
PRINTK
BUG
DM_DELAY
ELF_CORE
KGDB
PRINT_QUOTA_WARNING
AUTOFS4_FS

# 下面这些主要影响大小
MEDIA_DIGITAL_TV_SUPPORT
MEDIA_ANALOG_TV_SUPPORT
MEDIA_CAMERA_SUPPORT
MEDIA_RADIO_SUPPORT
INPUT_MOUSEDEV
INPUT_JOYDEV
INPUT_JOYSTICK
INPUT_TABLET
INPUT_TOUCHSCREEN
IIO
RC_CORE
HID_LOGITECH
HID_MAGICMOUSE
HID_A4TECH
HID_ACRUX
HID_APPLE
HID_ASUS

K4 - 初始化系统

初始化系统并不会花费太多时间,但最快的代码是不运行的代码 :) 这就是为什么我们移除了 BusyBox。我们从 BusyBox 中唯一需要的进程是文件系统挂载。我们可以简单地将这个进程嵌入到应用程序中。

我们需要做的就是在 Qt 程序的任何地方添加以下代码:
QProcess::execute("/bin/mount -a");

当然,这段代码应该放在合适的位置,因为这个过程可能需要时间,我们不希望我们的应用程序被这个过程阻塞。之后,我们将应用程序放在“/sbin/”文件夹中,命名为“init”,从而完成整个过程。内核加载用户空间后会自动运行“/sbin/init”,因此加载用户空间后的第一个运行的程序将是我们的应用程序。

K5 - 应用程序

Qt Creator 提供了详细的调试工具。使用这些工具,我们可以确定什么因素减慢了 Qt 应用程序的启动过程。

静态编译
我们在 K5 中做的最大改进之一是静态编译。静态编译意味着应用程序所需的所有库都存储在其二进制文件中。(参见 67)当我们使用默认设置编译 Qt 应用程序时,默认情况下会进行动态编译。在动态编译中,应用程序从文件系统中依次调用所需的库,这浪费了时间。在我们的情况下,静态编译没有任何缺点,因此使用它是安全的。这使我们获得了大约0.33秒的提升。

剥离
剥离通过删除二进制文件中不必要的部分来减少文件大小。特别是在静态编译后,这是一个非常有用且必要的步骤。可以通过以下命令执行剥离:strip --strip-all QtApp 在此过程后,应用程序的大小从21MB减少到15MB。

QML 懒加载
在我们的情况下,此功能并没有产生很大的影响,因为我们的应用程序GUI并不复杂,但在较大的 QML 文件中,我们可以通过显示一些图形内容(如动画)来隐藏耗时的进程。

将资源文件嵌入应用程序
通过 .qrc 文件添加到项目的任何资源都将嵌入到编译后的程序中。此功能在 Qt 9.0 之后应该是默认的。只需尝试使用此功能将所有内容(如字体、图像等)保持在二进制文件中。

5. 更多优化!

尽管在这个部分中有无限不同的可能性,但我们讨论的是那些在可接受的时间内影响巨大的优化方法。此外,还有一些建议。

代码 : G1
相关部分 : K1 (start.elf)
预期效果 : 0.5秒
可用工具/方法 : ARM 反汇编器
描述 : 可以使用 start_cd.elf 而不是 start.elf。为此,需要对 start_cd.elf 文件进行逆向工程以识别问题。首先需要理解 start.elf 的结构。然后可以破解 start_cd.elf 来解决问题。

代码 : G2
相关部分 : K5 (Qt)
预期效果 : 0.9秒
可用工具/方法 : 缓存
描述 : 当用户空间应用程序第一次运行时,由于未被缓存,启动速度较慢。当它们被内核缓存后,启动速度就会快得多。(参见 8)我们的 Qt 应用程序也出现了同样的情况。通过运行 Qt 应用程序,然后关闭并重新运行,可以观察到这种差异。如果我们能够某种方式复制缓存并在启动时将其传递给内核,我们的应用程序将更快启动。内核中的源代码“hibernate.c”(参见 10)和“drop_caches.c”(参见 11)可以用于此目的。

Code : G3
Related section : K3 (kernel.img), K5 (Qt)
Estimated effect : 1.0秒
Available Tools / Methods : 挂起
Description : 通过挂起,我们可以在K3和K5阶段获得显著的提升。为了实现挂起,我们需要完全控制这个过程,因为如果实施不当可能会导致系统不稳定。

Code : G4
Related section : K3 (kernel.img), K5 (Qt)
Estimated effect : -
Available Tools / Methods : initramfs, SquashFS
Description : SD卡上有两个分区:启动分区和文件系统分区(rootfs)。启动分区包含Raspberry启动所需的文件。因此,启动分区会自动被Raspberry读取。内核最初在没有文件系统的情况下运行在RAM中。所以如果我们将整个rootfs放入kernel.img中,那么将kernel.img复制到RAM后的所有操作将会更快,因为它会在RAM上进行。这将增加kernel.img的大小,因此应该尽可能减少kernel.img的大小。

Code : G5
Related section : K3 (kernel.img), K5 (Qt)
Estimated effect : -
Available Tools / Methods : Btrfs, f2fs
Description : 这些文件系统的读取速度很高,同时支持读写。对于快速启动来说,读取速度比写入速度更重要。因此,这些文件系统值得一试。

Code : G100
Description : 调试是优化过程的基础部分,需要详细计划。在开始优化过程之前,不要犹豫为调试计划分配时间,收集必要的调试材料,自动化开发/调试过程。

Code : G101
Description : 我们从最低层开始优化,即从Raspberry的自启动到最高层,即Qt应用的优化。这使得调试变得困难。相反,我认为从最高层开始可能会使调试更容易。

6. 简而言之..

如果你想要一个快速启动的RPI镜像而不必深入细节,请按照以下步骤操作;

  1. git clone https://github.com/furkantokac/buildroot
  2. cd buildroot
  3. make ftdev_rpi3_fastboot_defconfig
  4. make
  5. 在此阶段,准备好的RPI镜像将在buildroot/output/images文件夹中可用。你可以将其写入SD设备并直接启动RPI。当系统启动后,终端应直接显示在屏幕上。

你将得到的镜像没有超频,因此你可以通过超频你的RPI来加速启动过程。另外,你可以静态编译你自己的Qt应用程序并替换sbin/init以在启动时运行它。USB驱动程序已移除,因此你无法通过USB键盘或鼠标控制RPI。

7. 结果

“正常”部分列出了默认Buildroot镜像的测量数据。仅启动延迟时间减少到0。

“ftDev”部分列出了优化镜像的测量数据。

K1 K2 K3 K4 K5 Total
Normal 1.25秒 2.12秒 5.12秒 0.12秒 1.56秒 10.17秒
ftDev 1.25秒 0.25秒 0.25秒 0.00秒 1.07秒 2.82秒

注:测量是通过高速摄像机记录启动过程完成的。因此,它们是准确的。