启动时间优化

本页提供了缩短启动时间的建议。

从模块中剥离调试符号

请确保从模块中剥离调试符号,方法与在正式版设备上从内核中剥离调试符号类似。从模块中剥离调试符号有助于减少以下过程耗用的时间,从而缩短启动时间:

  • 从闪存中读取二进制文件。
  • 解压缩 ramdisk。
  • 加载模块。

从模块中剥离调试符号可在启动过程中节省几秒钟时间

在 Android 平台 build 中,符号剥离默认处于启用状态,但若要明确启用此功能,则需要在 device/vendor/device 下的设备专用 config 中设置 BOARD_DO_NOT_STRIP_VENDOR_RAMDISK_MODULES

对内核和 ramdisk 使用 LZ4 压缩

Gzip 生成的压缩输出比 LZ4 小,但 LZ4 的解压缩速度比 Gzip 快。对于内核和模块而言,使用 Gzip 减少的绝对存储大小相比使用 LZ4 节省的解压缩时间而言,并没有明显的优势。

Android 平台 build 已通过 BOARD_RAMDISK_USE_LZ4 添加了对 LZ4 ramdisk 压缩的支持。您可以在设备专用 config 中设置此选项。内核压缩可通过内核 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 时用不到,请将模块放入 vendor 或 vendor_dlkm 分区,从而将其移至第二阶段 init。

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

加载以下基本驱动程序:

  • watchdog
  • reset
  • cpufreq

对于恢复模式 (Recovery mode) 和用户空间 fastbootd 模式,第一阶段 init 需要探测和显示更多设备(例如 USB)。请在第一阶段 ramdisk 和 vendor 或 vendor_dlkm 分区中保存这些模块的副本。这样一来,便可在第一阶段 init 中加载这些模块来执行恢复或 fastbootd 启动流程。不过,在正常启动流程中,请勿在第一阶段 init 中加载恢复模式 (Recovery mode) 模块。恢复模式 (Recovery mode) 模块可以推迟到第二阶段 init 进行加载,以缩短启动时间。第一阶段 init 中不需要的所有其他模块都应移至 vendor 或 vendor_dlkm 分区。

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

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

  • 缩减 Ramdisk 大小。
    • 这在引导加载程序加载 ramdisk(序列化启动步骤)时可以加快闪存读取速度。
    • 这在内核解压缩 ramdisk(序列化启动步骤)时可以加快解压缩速度。
  • 第二阶段 init 是并行运行的,模块加载与第二阶段 init 中的工作同步进行,因而可以省去单独加载模块的时间。

将模块移至第二阶段可以节省 500 - 1000 毫秒的启动时间,具体取决于可以将多少个模块移至第二阶段 init。

模块加载逻辑

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

  • 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 或 vendor_dlkm 分区中的 /vendor/lib/modules/ 目录下的模块列表。
  • BOARD_VENDOR_KERNEL_MODULES_LOAD:要在第二阶段 init 中加载的模块列表。

ramdisk 中的启动和恢复模块也必须复制到 vendor 或 vendor_dlkm 分区中的 /vendor/lib/modules 下。将这些模块复制到 vendor 分区,可以确保这些模块在第二阶段 init 过程中可见,这对于调试和为 bug 报告收集 modinfo 很有用。

只要尽量缩减启动模块集的大小,就可以最大限度地减少复制内容在 vendor 或 vendor_dlkm 分区中占用的空间。请确保供应商的 modules.list 文件在 /vendor/lib/modules 中提供经过过滤的模块列表,该列表可确保启动时间不会受到二次加载模块(这是一个成本高昂的过程)的影响。

请确保将所有恢复模式 (Recovery mode) 模块作为一个组进行加载。加载恢复模式 (Recovery mode) 模块可以在恢复模式 (Recovery mode) 下完成,也可以在每个启动流程的第二阶段 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 脚本来管理模块加载,以便在必要时可以报告(或忽略)其他逻辑,如错误处理和缓解,或模块加载完成。

可以忽略用户 build 中不存在的调试模块加载失败情况。如需忽略此失败情况,请设置 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 blocklist 功能来拆分第二阶段启动流程,以纳入非必要模块的延迟模块加载。可以将特定 HAL 专用模块的加载推迟到相应 HAL 启动时才进行。

为了让用户明显感觉到启动时间的缩短,可以在模块加载服务中专门选择更适合在启动屏幕之后加载的模块。例如,可以将视频解码器模块或 Wi-Fi 模块明确推迟到 init 启动流程结束(例如,Android 属性信号 sys.boot_complete)之后再加载。请确保延迟加载的模块所对应的 HAL 在内核驱动程序不存在的情况下阻塞足够长的时间。

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

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

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

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

在加载 CPUfreq 驱动程序之前,内核不知道 CPU 频率,因此不会根据当前频率调节 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 中有固件,也会导致不必要的延迟。