启动时间优化

此页面提供了一组提示,您可以从中进行选择,以缩短启动时间。

从模块中去除调试符号

与在生产设备上从内核中剥离调试符号的方式类似,请确保您也从模块中剥离调试符号。从模块中去除调试符号有助于减少以下启动时间:

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

从模块中剥离调试符号可能会在引导期间节省几秒钟。

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

对内核和 ramdisk 使用 LZ4 压缩

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

通过BOARD_RAMDISK_USE_LZ4在 Android 平台构建中添加了对 LZ4 ramdisk 压缩的支持。您可以在特定于设备的配置中设置此选项。内核压缩可以通过内核defconfig来设置。

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

避免过度登录您的驱动程序

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

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

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

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

有选择地启用异步探测

加载模块时,如果它支持的设备已经从 DT(设备树)中填充并添加到驱动程序核心,则设备探测在module_init()调用的上下文中完成。在module_init()的上下文中完成设备探测时,模块无法完成加载,直到探测完成。由于模块加载主要是串行化的,因此需要较长时间进行探测的设备会减慢启动时间。

为避免较慢的启动时间,请为需要一段时间来探测其设备的模块启用异步探测。为所有模块启用异步探测可能没有好处,因为分叉线程和启动探测所需的时间可能与探测设备所需的时间一样长。

通过 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 完成不需要模块,则将模块移动到第二阶段 init,方法是将其放置在 vendor 或vendor_dlkm分区中。

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

加载以下基本驱动程序:

  • 看门狗
  • 重启
  • cpufreq

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

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

将模块移动到第二阶段 init 通过以下方式减少启动时间:

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

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

模块装载物流

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

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

ramdisk 中的引导和恢复模块也必须复制到/vendor/lib/modules的 vendor 或vendor_dlkm分区。将这些模块复制到供应商分区可确保这些模块在第二阶段初始化期间不可见,这对于调试和收集modinfo以获取错误报告很有用。

只要最小化引导模块集,复制就应该在 vendor 或vendor_dlkm分区上花费最少的空间。确保供应商的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。

对于第二阶段初始化,我们建议将模块加载作为服务运行,这样它就不会阻塞引导流程。使用 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 独占使用的模块。

为了改善明显的启动时间,您可以在模块加载服务中专门选择更有利于在启动屏幕后加载的模块。例如,您可以在初始化引导流程被清除后显式延迟加载视频解码器或 wifi 的模块(例如sys.boot_complete Android 属性信号)。当内核驱动程序不存在时,确保延迟加载模块的 HAL 阻塞足够长的时间。

或者,您可以在引导流程 rc 脚本中使用 init 的wait<file>[<timeout>]命令等待选择sysfs条目以显示驱动程序模块已完成探测操作。例如,在显示菜单图形之前,等待显示驱动程序在 recovery 或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 是安全的,即使您不打算明确使用它,您也应该这样做。

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

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