启动时间优化

本页面提供有关如何缩短启动时间的提示。

从模块中剥离调试符号

与生产设备上从内核中剥离调试符号的方式类似,请确保您也从模块中剥离调试符号。从模块中剥离调试符号可通过减少以下方面来缩短启动时间

  • 从闪存读取二进制文件所需的时间。
  • 解压缩 ramdisk 所需的时间。
  • 加载模块所需的时间。

从模块中剥离调试符号可以节省启动期间的几秒钟。

符号剥离在 Android 平台构建中默认启用,但要显式启用它们,请在 device/vendor/device 下的设备特定配置中设置 `BOARD_DO_NOT_STRIP_VENDOR_RAMDISK_MODULES`。

对内核和 ramdisk 使用 LZ4 压缩

与 LZ4 相比,Gzip 生成的压缩输出更小,但 LZ4 的解压缩速度比 Gzip 快。对于内核和模块,与 LZ4 的解压缩时间优势相比,使用 Gzip 带来的绝对存储空间减少并不那么显着。

通过 `BOARD_RAMDISK_USE_LZ4`,已将对 LZ4 ramdisk 压缩的支持添加到 Android 平台构建中。您可以在设备特定的配置中设置此选项。内核压缩可以通过内核 defconfig 进行设置。

切换到 LZ4 应该可以使启动时间加快 500 毫秒到 1000 毫秒。

避免在驱动程序中进行过多的日志记录

在 ARM64 和 ARM32 中,距离调用站点超过特定距离的函数调用需要一个跳转表(称为过程链接表或 PLT)才能编码完整的跳转地址。由于模块是动态加载的,因此需要在模块加载期间修复这些跳转表。需要重定位的调用称为 ELF 格式中带有显式加数(或简称 RELA)条目的重定位条目。

Linux 内核在分配 PLT 时会进行一些内存大小优化(例如缓存命中优化)。通过此上游提交,优化方案具有 `O(N^2)` 复杂度,其中 `N` 是 `R_AARCH64_JUMP26` 或 `R_AARCH64_CALL26` 类型的 RELA 数量。因此,减少这些类型的 RELA 有助于缩短模块加载时间。

一种常见的编码模式会增加 R_AARCH64_CALL26R_AARCH64_JUMP26 RELA 的数量,那就是驱动程序中过多的日志记录。每次调用 printk() 或任何其他日志记录方案通常都会添加一个 CALL26/JUMP26 RELA 条目。在上游提交中的提交文本中,请注意,即使进行了优化,加载这六个模块也需要大约 250 毫秒——这是因为这六个模块是日志记录量最多的前六个模块。

减少日志记录可以节省大约 100 - 300 毫秒的启动时间,具体取决于现有日志记录的过度程度。

有选择地启用异步探测

加载模块后,如果其支持的设备已从 DT (设备树) 填充并添加到驱动程序核心,则设备探测将在 module_init() 调用的上下文中完成。当设备探测在 module_init() 的上下文中完成时,模块在探测完成之前无法完成加载。由于模块加载在很大程度上是串行化的,因此探测时间相对较长的设备会减慢启动时间。

为了避免启动时间变慢,请为探测设备需要一段时间的模块启用异步探测。为所有模块启用异步探测可能没有好处,因为 fork 一个线程并启动探测所花费的时间可能与探测设备所花费的时间一样长。

通过慢速总线(如 I2C)连接的设备、在其探测函数中执行固件加载的设备以及执行大量硬件初始化的设备可能会导致时序问题。确定这种情况何时发生的最佳方法是收集每个驱动程序的探测时间并对其进行排序。

要为模块启用异步探测,仅在驱动程序代码中设置 PROBE_PREFER_ASYNCHRONOUS 标志是不够的。对于模块,您还需要在内核命令行中添加 module_name.async_probe=1,或者在使用 modprobeinsmod 加载模块时传递 async_probe=1 作为模块参数。

启用异步探测可以节省大约 100 - 500 毫秒的启动时间,具体取决于您的硬件/驱动程序。

尽早探测您的 CPUfreq 驱动程序

您的 CPUfreq 驱动程序探测得越早,您就可以在启动期间越早将 CPU 频率扩展到最大值(或某些热限制的最大值)。CPU 速度越快,启动速度就越快。此指南也适用于控制 DRAM、内存和互连频率的 devfreq 驱动程序。

对于模块,加载顺序可能取决于 initcall 级别以及驱动程序的编译或链接顺序。使用别名 MODULE_SOFTDEP() 以确保 cpufreq 驱动程序是最先加载的几个模块之一。

除了尽早加载模块外,您还需要确保探测 CPUfreq 驱动程序的所有依赖项也已探测。例如,如果您需要时钟或稳压器句柄来控制 CPU 的频率,请确保它们首先被探测。或者,如果您的 CPU 在启动期间可能过热,您可能需要先加载热驱动程序,然后再加载 CPUfreq 驱动程序。因此,请尽您所能确保 CPUfreq 和相关的 devfreq 驱动程序尽早探测。

尽早探测 CPUfreq 驱动程序所节省的时间可能非常小到非常大,具体取决于您可以多早开始探测以及引导加载程序将 CPU 频率保持在什么频率。

将模块移动到第二阶段 init、vendor 或 vendor_dlkm 分区

由于第一阶段 init 进程是串行化的,因此没有太多机会并行化启动过程。如果第一阶段 init 完成不需要某个模块,请通过将其放置在 vendor 或 vendor_dlkm 分区中,将该模块移动到第二阶段 init。

第一阶段 init 不需要探测多个设备才能进入第二阶段 init。正常启动流程只需要控制台和闪存存储功能。

加载以下基本驱动程序

  • watchdog
  • reset
  • cpufreq

对于恢复和用户空间 fastbootd 模式,第一阶段 init 需要探测更多设备(例如 USB)和显示器。在第一阶段 ramdisk 和 vendor 或 vendor_dlkm 分区中保留这些模块的副本。这使它们可以在恢复或 fastbootd 启动流程的第一阶段 init 中加载。但是,在正常启动流程的第一阶段 init 期间,不要加载恢复模式模块。恢复模式模块可以推迟到第二阶段 init 以减少启动时间。所有其他第一阶段 init 中不需要的模块都应移动到 vendor 或 vendor_dlkm 分区。

给定叶设备列表(例如,UFS 或串行),dev needs.sh 脚本查找依赖项或供应商(例如,时钟、稳压器或 gpio)探测所需的所有驱动程序、设备和模块。

将模块移动到第二阶段 init 可以通过以下方式缩短启动时间

  • 减少 Ramdisk 大小。
    • 当引导加载程序加载 ramdisk 时(串行化启动步骤),这会产生更快的闪存读取速度。
    • 当内核解压缩 ramdisk 时(串行化启动步骤),这会产生更快的解压缩速度。
  • 第二阶段 init 并行工作,这会将模块的加载时间与第二阶段 init 中完成的工作隐藏起来。

将模块移动到第二阶段可以节省 500 - 1000 毫秒的启动时间,具体取决于您可以移动到第二阶段 init 的模块数量。

模块加载后勤

最新的 Android 版本具有板级配置,用于控制哪些模块复制到每个阶段以及加载哪些模块。本节重点介绍以下子集

  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES。要复制到 ramdisk 的模块列表。
  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD。要在第一阶段 init 中加载的模块列表。
  • BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD。从 ramdisk 中选择恢复或 fastbootd 时要加载的模块列表。
  • BOARD_VENDOR_KERNEL_MODULES。要复制到 vendor 或 vendor_dlkm 分区 /vendor/lib/modules/ 目录中的模块列表。
  • BOARD_VENDOR_KERNEL_MODULES_LOAD。要在第二阶段 init 中加载的模块列表。

ramdisk 中的启动和恢复模块也必须复制到 vendor 或 vendor_dlkm 分区中的 /vendor/lib/modules。将这些模块复制到 vendor 分区可确保模块在第二阶段 init 期间不会不可见,这对于调试和收集用于错误报告的 modinfo 非常有用。

只要启动模块集最小化,重复复制应在 vendor 或 vendor_dlkm 分区上花费最少的空间。确保 vendor 的 modules.list 文件在 /vendor/lib/modules 中包含已过滤的模块列表。过滤后的列表可确保启动时间不受模块再次加载的影响(这是一个昂贵的过程)。

确保恢复模式模块作为一组加载。恢复模式模块的加载可以在恢复模式下完成,也可以在每个启动流程的第二阶段 init 的开始时完成。

您可以使用设备 Board.Config.mk 文件执行这些操作,如以下示例所示

# All kernel modules
KERNEL_MODULES := $(wildcard $(KERNEL_MODULE_DIR)/*.ko)
KERNEL_MODULES_LOAD := $(strip $(shell cat $(KERNEL_MODULE_DIR)/modules.load)

# First stage ramdisk modules
BOOT_KERNEL_MODULES_FILTER := $(foreach m,$(BOOT_KERNEL_MODULES),%/$(m))

# Recovery ramdisk modules
RECOVERY_KERNEL_MODULES_FILTER := $(foreach m,$(RECOVERY_KERNEL_MODULES),%/$(m))
BOARD_VENDOR_RAMDISK_KERNEL_MODULES += \
     $(filter $(BOOT_KERNEL_MODULES_FILTER) \
                $(RECOVERY_KERNEL_MODULES_FILTER),$(KERNEL_MODULES))

# ALL modules land in /vendor/lib/modules so they could be rmmod/insmod'd,
# and modules.list actually limits us to the ones we intend to load.
BOARD_VENDOR_KERNEL_MODULES := $(KERNEL_MODULES)
# To limit /vendor/lib/modules to just the ones loaded, use:
# BOARD_VENDOR_KERNEL_MODULES := $(filter-out \
#     $(BOOT_KERNEL_MODULES_FILTER),$(KERNEL_MODULES))

# Group set of /vendor/lib/modules loading order to recovery modules first,
# then remainder, subtracting both recovery and boot modules which are loaded
# already.
BOARD_VENDOR_KERNEL_MODULES_LOAD := \
        $(filter-out $(BOOT_KERNEL_MODULES_FILTER), \
        $(filter $(RECOVERY_KERNEL_MODULES_FILTER),$(KERNEL_MODULES_LOAD)))
BOARD_VENDOR_KERNEL_MODULES_LOAD += \
        $(filter-out $(BOOT_KERNEL_MODULES_FILTER) \
            $(RECOVERY_KERNEL_MODULES_FILTER),$(KERNEL_MODULES_LOAD))

# NB: Load order governed by modules.load and not by $(BOOT_KERNEL_MODULES)
BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD := \
        $(filter $(BOOT_KERNEL_MODULES_FILTER),$(KERNEL_MODULES_LOAD))

# Group set of /vendor/lib/modules loading order to boot modules first,
# then the remainder of recovery modules.
BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD := \
    $(filter $(BOOT_KERNEL_MODULES_FILTER),$(KERNEL_MODULES_LOAD))
BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD += \
    $(filter-out $(BOOT_KERNEL_MODULES_FILTER), \
    $(filter $(RECOVERY_KERNEL_MODULES_FILTER),$(KERNEL_MODULES_LOAD)))

此示例展示了在板级配置文件中本地指定的更易于管理的 BOOT_KERNEL_MODULESRECOVERY_KERNEL_MODULES 子集。前面的脚本从选定的可用内核模块中查找并填充每个子集模块,将剩余模块留给第二阶段 init。

对于第二阶段 init,我们建议将模块加载作为服务运行,使其不会阻塞启动流程。使用 shell 脚本来管理模块加载,以便可以将其他后勤工作(例如,错误处理和缓解或模块加载完成)报告回来(或忽略)。

您可以忽略用户版本中不存在的调试模块加载失败。要忽略此失败,请设置 vendor.device.modules.ready 属性以触发 init rc 脚本启动流程的后续阶段,以继续进入启动画面。如果您在 /vendor/etc/init.insmod.sh 中有以下代码,请参考以下示例脚本

#!/vendor/bin/sh
. . .
if [ $# -eq 1 ]; then
  cfg_file=$1
else
  # Set property even if there is no insmod config
  # to unblock early-boot trigger
  setprop vendor.common.modules.ready
  setprop vendor.device.modules.ready
  exit 1
fi

if [ -f $cfg_file ]; then
  while IFS="|" read -r action arg
  do
    case $action in
      "insmod") insmod $arg ;;
      "setprop") setprop $arg 1 ;;
      "enable") echo 1 > $arg ;;
      "modprobe") modprobe -a -d /vendor/lib/modules $arg ;;
     . . .
    esac
  done < $cfg_file
fi

在硬件 rc 文件中,可以使用以下内容指定 one shot 服务

service insmod-sh /vendor/etc/init.insmod.sh /vendor/etc/init.insmod.<hw>.cfg
    class main
    user root
    group root system
    Disabled
    oneshot

在模块从第一阶段移动到第二阶段后,可以进行额外的优化。您可以使用 modprobe 阻止列表功能来拆分第二阶段启动流程,以包含非必要模块的延迟模块加载。专门由特定 HAL 使用的模块的加载可以延迟到仅在 HAL 启动时才加载模块。

为了提高表面启动时间,您可以专门选择模块加载服务中更利于在启动画面后加载的模块。例如,您可以在 init 启动流程清除后(例如,sys.boot_complete Android 属性信号),显式地延迟加载视频解码器或 Wi-Fi 的模块。确保延迟加载模块的 HAL 在内核驱动程序不存在时阻塞足够长的时间。

或者,您可以使用 init 的 wait<file>[<timeout>] 命令在启动流程 rc 脚本中等待选定的 sysfs 条目显示驱动程序模块已完成探测操作。例如,在恢复或 fastbootd 的后台等待显示驱动程序完成加载,然后再呈现菜单图形。

在引导加载程序中将 CPU 频率初始化为合理的值

并非所有 SoC/产品都可能能够在最高频率下启动 CPU,这可能是由于启动循环测试期间的热量或功率问题。但是,请确保引导加载程序将所有在线 CPU 的频率设置为 SoC 或产品在安全范围内尽可能高的频率。这非常重要,因为对于完全模块化的内核,init ramdisk 解压缩发生在 CPUfreq 驱动程序加载之前。因此,如果引导加载程序将 CPU 频率保持在其频率的下限,则 ramdisk 解压缩时间可能比静态编译的内核(在调整 ramdisk 大小差异后)更长,因为在执行 CPU 密集型工作(解压缩)时,CPU 频率会非常低。这同样适用于内存和互连频率。

在引导加载程序中初始化大 CPU 的 CPU 频率

CPUfreq 驱动程序加载之前,内核不知道 CPU 频率,并且不会根据其当前频率扩展 CPU 调度容量。如果小 CPU 上的负载足够高,内核可能会将线程迁移到大 CPU。

确保对于引导加载程序将其保持在的频率,大 CPU 的性能至少与小 CPU 一样好。例如,如果对于相同的频率,大 CPU 的性能是小 CPU 的 2 倍,但引导加载程序将小 CPU 的频率设置为 1.5 GHz,而将大 CPU 的频率设置为 300 MHz,那么如果内核将线程移动到大 CPU,则启动性能将会下降。在此示例中,如果以 750 MHz 启动大 CPU 是安全的,即使您不打算显式使用它,也应该这样做。

驱动程序不应在第一阶段 init 中加载固件

在某些不可避免的情况下,可能需要在第一阶段 init 中加载固件。但总的来说,驱动程序不应在第一阶段 init 中加载任何固件,尤其是在设备探测上下文中。如果在第一阶段 ramdisk 中固件不可用,则在第一阶段 init 中加载固件会导致整个启动过程停顿。即使第一阶段 ramdisk 中存在固件,它仍然会导致不必要的延迟。