优化启动时间

使用集合让一切井井有条 根据您的偏好保存内容并对其进行分类。

本文档提供合作伙伴指导,以改善特定 Android 设备的启动时间。启动时间是系统性能的重要组成部分,因为用户必须等待启动完成才能使用设备。对于汽车等冷启动频率较高的设备,快速启动时间至关重要(没有人喜欢为了输入导航目的地而等待几十秒)。

Android 8.0 通过支持一系列组件的多项改进来缩短启动时间。下表总结了这些性能改进(在 Google Pixel 和 Pixel XL 设备上测量)。

零件改进
引导加载程序
  • 通过删除 UART 日志节省了 1.6 秒
  • 从 GZIP 更改为 LZ4 节省了 0.4 秒
设备内核
  • 通过删除未使用的内核配置和减小驱动程序大小节省了 0.3 秒
  • 使用 dm-verity 预取优化节省了 0.3 秒
  • 节省 0.15 秒以消除驱动程序中不必要的等待/测试
  • 节省了 0.12 秒来删除 CONFIG_CC_OPTIMIZE_FOR_SIZE
I/O 调整
  • 正常开机节省2s
  • 首次启动节省 25 秒
初始化.*.rc
  • 通过并行初始化命令节省了 1.5 秒
  • 提前启动 zygote 节省了 0.25 秒
  • 通过 cpuset tune 节省了 0.22 秒
开机动画
  • 在没有触发 fsck 的情况下在启动时提前 2 秒启动,在 fsck 触发的启动时启动更大
  • 在 Pixel XL 上节省 5 秒,并立即关闭启动动画
SELinux 政策genfscon 节省了 0.2 秒

优化引导加载程序

要优化引导加载程序以缩短引导时间:

  • 对于日志记录:
    • 禁用对 UART 的日志写入,因为大量日志记录可能需要很长时间。 (在 Google Pixel 设备上,我们发现它会使引导加载程序减慢 1.5 秒)。
    • 仅记录错误情况并考虑使用单独的检索机制将其他信息存储到内存中。
  • 对于内核解压缩,考虑将 LZ4 用于现代硬件而不是 GZIP(示例补丁)。请记住,不同的内核压缩选项可能具有不同的加载和解压缩时间,并且对于您的特定硬件,某些选项可能比其他选项效果更好。
  • 检查去抖动/特殊模式进入的不必要等待时间并将它们最小化。
  • 将引导加载程序中花费的引导时间作为 cmdline 传递给内核。
  • 检查 CPU 时钟并考虑并行化(需要多核支持)以进行内核加载和初始化 I/O。

优化内核

使用以下技巧来优化内核以缩短启动时间。

最小化设备 defconfig

最小化内核配置可以减小内核大小,从而更快地加载解压、初始化和更小的攻击面。优化设备 defconfig:

  • 识别未使用的驱动程序。查看/dev/sys目录并查找具有一般 SELinux 标签的节点(这表明这些节点未配置为可由用户空间访问)。如果找到,请删除任何此类节点。
  • 取消设置未使用的 CONFIGs 。查看内核构建生成的 .config 文件以明确取消设置默认打开的任何未使用的 CONFIG。例如,我们从 Google Pixel 中删除了以下未使用的 CONFIG:
    CONFIG_ANDROID_LOGGER=y
    CONFIG_IMX134=y
    CONFIG_IMX132=y
    CONFIG_OV9724=y
    CONFIG_OV5648=y
    CONFIG_GC0339=y
    CONFIG_OV8825=y
    CONFIG_OV8865=y
    CONFIG_s5k4e1=y
    CONFIG_OV12830=y
    CONFIG_USB_EHCI_HCD=y
    CONFIG_IOMMU_IO_PGTABLE_FAST_SELFTEST=y
    CONFIG_IKCONFIG=y
    CONFIG_RD_BZIP2=y
    CONFIG_RD_LZMA=y
    CONFIG_TI_DRV2667=y
    CONFIG_CHR_DEV_SCH=y
    CONFIG_MMC=y
    CONFIG_MMC_PERF_PROFILING=y
    CONFIG_MMC_CLKGATE=y
    CONFIG_MMC_PARANOID_SD_INIT=y
    CONFIG_MMC_BLOCK_MINORS=32
    CONFIG_MMC_TEST=y
    CONFIG_MMC_SDHCI=y
    CONFIG_MMC_SDHCI_PLTFM=y
    CONFIG_MMC_SDHCI_MSM=y
    CONFIG_MMC_SDHCI_MSM_ICE=y
    CONFIG_MMC_CQ_HCI=y
    CONFIG_MSDOS_FS=y
    # CONFIG_SYSFS_SYSCALL is not set
    CONFIG_EEPROM_AT24=y
    # CONFIG_INPUT_MOUSEDEV_PSAUX is not set
    CONFIG_INPUT_HBTP_INPUT=y
    # CONFIG_VGA_ARB is not set
    CONFIG_USB_MON=y
    CONFIG_USB_STORAGE_DATAFAB=y
    CONFIG_USB_STORAGE_FREECOM=y
    CONFIG_USB_STORAGE_ISD200=y
    CONFIG_USB_STORAGE_USBAT=y
    CONFIG_USB_STORAGE_SDDR09=y
    CONFIG_USB_STORAGE_SDDR55=y
    CONFIG_USB_STORAGE_JUMPSHOT=y
    CONFIG_USB_STORAGE_ALAUDA=y
    CONFIG_USB_STORAGE_KARMA=y
    CONFIG_USB_STORAGE_CYPRESS_ATACB=y
    CONFIG_SW_SYNC_USER=y
    CONFIG_SEEMP_CORE=y
    CONFIG_MSM_SMEM_LOGGING=y
    CONFIG_IOMMU_DEBUG=y
    CONFIG_IOMMU_DEBUG_TRACKING=y
    CONFIG_IOMMU_TESTS=y
    CONFIG_MOBICORE_DRIVER=y
    # CONFIG_DEBUG_PREEMPT is not set
    
  • 删除在每次启动时导致不必要的测试运行的 CONFIG 。虽然在开发中很有用,但此类配置(即 CONFIG_IOMMU_IO_PGTABLE_FAST_SELFTEST)应该在生产内核中删除。

最小化驱动程序大小

如果不使用该功能进一步减小内核大小,可以删除设备内核中的一些驱动程序。例如,如果 WLAN 通过 PCIe 连接,则不使用 SDIO 支持,应在编译时删除。有关详细信息,请参阅 Google Pixel kernel:net:wireless:cnss:add option to disable SDIO support。

删除大小的编译器优化

删除 CONFIG_CC_OPTIMIZE_FOR_SIZE 的内核配置。这个标志最初是在假设较小的代码大小会产生热缓存命中(因此更快)时引入的。然而,随着现代移动 SoC 变得更加强大,这种假设不再有效。

此外,删除该标志可以启用未初始化变量的编译器警告,当存在 CONFIG_CC_OPTIMIZE_FOR_SIZE 标志时,在 Linux 内核中会抑制该警告(仅进行此更改就帮助我们发现了一些 Android 设备驱动程序中的许多有意义的错误)。

推迟初始化

许多进程在引导期间启动,但只有关键路径中的组件(引导加载程序 > 内核 > 初始化 > 文件系统挂载 > zygote > 系统服务器)直接影响引导时间。在内核启动期间配置initcall以识别启动 init 进程的速度较慢且不重要的外围设备/组件,然后通过移动到可加载的内核模块将这些外围设备/组件延迟到启动过程的后期。移至异步设备/驱动程序探测还有助于并行内核 > 初始化关键路径中的慢速组件。

BoardConfig-common.mk:
    BOARD_KERNEL_CMDLINE += initcall_debug ignore_loglevel

driver:
    .probe_type = PROBE_PREFER_ASYNCHRONOUS,

注意:必须通过添加EPROBEDEFER支持来仔细解决驱动程序依赖关系。

优化 I/O 效率

提高 I/O 效率对于缩短启动时间至关重要,并且读取任何不需要的内容应推迟到启动后(在 Google Pixel 上,启动时读取大约 1.2GB 的数据)。

调整文件系统

当从头开始读取文件或顺序读取块时,Linux 内核会启动预读,因此需要专门为引导调整 I/O 调度程序参数(与正常应用程序具有不同的工作负载特征)。

支持无缝 (A/B) 更新的设备极大地受益于首次启动时的文件系统调整(例如 Google Pixel 上的 20 秒)。例如,我们为 Google Pixel 调整了以下参数:

on late-fs
  # boot time fs tune
    # boot time fs tune
    write /sys/block/sda/queue/iostats 0
    write /sys/block/sda/queue/scheduler cfq
    write /sys/block/sda/queue/iosched/slice_idle 0
    write /sys/block/sda/queue/read_ahead_kb 2048
    write /sys/block/sda/queue/nr_requests 256
    write /sys/block/dm-0/queue/read_ahead_kb 2048
    write /sys/block/dm-1/queue/read_ahead_kb 2048

on property:sys.boot_completed=1
    # end boot time fs tune
    write /sys/block/sda/queue/read_ahead_kb 512
    ...

各种各样的

  • 使用内核配置 DM_VERITY_HASH_PREFETCH_MIN_SIZE(默认大小为 128)打开 dm-verity 哈希预取大小。
  • 为了获得更好的文件系统稳定性和每次启动时发生的强制检查,请通过在 BoardConfig.mk 中设置 TARGET_USES_MKE2FS 来使用新的 ext4 生成工具。

分析 I/O

要了解引导期间的 I/O 活动,请使用内核 ftrace 数据(也由 systrace 使用):

trace_event=block,ext4 in BOARD_KERNEL_CMDLINE

要分解每个文件的文件访问权限,请对内核进行以下更改(仅限开发内核;不要在生产内核中使用):

diff --git a/fs/open.c b/fs/open.c
index 1651f35..a808093 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -981,6 +981,25 @@
 }
 EXPORT_SYMBOL(file_open_root);
 
+static void _trace_do_sys_open(struct file *filp, int flags, int mode, long fd)
+{
+       char *buf;
+       char *fname;
+
+       buf = kzalloc(PAGE_SIZE, GFP_KERNEL);
+       if (!buf)
+               return;
+       fname = d_path(&filp-<f_path, buf, PAGE_SIZE);
+
+       if (IS_ERR(fname))
+               goto out;
+
+       trace_printk("%s: open(\"%s\", %d, %d) fd = %ld, inode = %ld\n",
+                     current-<comm, fname, flags, mode, fd, filp-<f_inode-<i_ino);
+out:
+       kfree(buf);
+}
+
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
 {
 	struct open_flags op;
@@ -1003,6 +1022,7 @@
 		} else {
 			fsnotify_open(f);
 			fd_install(fd, f);
+			_trace_do_sys_open(f, flags, mode, fd);

使用以下脚本来帮助分析引导性能。

  • system/extras/boottime_tools/bootanalyze/bootanalyze.py通过对启动过程中重要步骤的细分来衡量启动时间。
  • system/extras/boottime_tools/io_analysis/check_file_read.py boot_trace提供每个文件的访问信息。
  • system/extras/boottime_tools/io_analysis/check_io_trace_all.py boot_trace给出系统级故障。

优化 init.*.rc

Init 是从内核到框架建立的桥梁,设备通常在不同的 init 阶段花费几秒钟。

并行运行任务

虽然当前的 Android init 或多或少是一个单线程进程,但您仍然可以并行执行一些任务。

  • 在 shell 脚本服务中执行慢速命令,稍后通过等待特定属性加入。 Android 8.0 通过新的wait_for_property命令支持此用例。
  • 识别 init 中的慢操作。系统会记录初始化命令 exec/wait_for_prop 或任何耗时较长的操作(在 Android 8.0 中,任何命令耗时超过 50 毫秒)。例如:
    init: Command 'wait_for_coldboot_done' action=wait_for_coldboot_done returned 0 took 585.012ms

    查看此日志可能表明有改进的机会。

  • 尽早启动服务并启用关键路径中的外围设备。例如,某些 SOC 需要在启动 SurfaceFlinger 之前启动安全相关服务。当 ServiceManager 返回“等待服务”时查看系统日志——这通常表明必须首先启动依赖服务。
  • 删除 init.*.rc 中所有未使用的服务和命令。早期初始化中未使用的任何内容都应推迟到启动完成。

注意:属性服务是 init 进程的一部分,因此如果 init 忙于内置命令,则在引导期间调用setproperty可能会导致很长的延迟。

使用调度程序调优

使用调度程序调优进行早期启动。来自 Google Pixel 的示例:

on init
    # boottime stune
    write /dev/stune/schedtune.prefer_idle 1
    write /dev/stune/schedtune.boost 100
    on property:sys.boot_completed=1
    # reset stune
    write /dev/stune/schedtune.prefer_idle 0
    write /dev/stune/schedtune.boost 0

    # or just disable EAS during boot
    on init
    write /sys/kernel/debug/sched_features NO_ENERGY_AWARE
    on property:sys.boot_completed=1
    write /sys/kernel/debug/sched_features ENERGY_AWARE

某些服务在启动期间可能需要优先级提升。例子:

init.zygote64.rc:
service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server
    class main
    priority -20
    user root
...

尽早开始合子

具有基于文件加密的设备可以在 zygote-start 触发器处更早地启动 zygote(默认情况下,zygote 在 main 类启动,这比 zygote-start 晚得多)。执行此操作时,请确保允许 zygote 在所有 CPU 中运行(因为错误的 cpuset 设置可能会强制 zygote 在特定 CPU 中运行)。

禁用省电

在设备启动期间,可以禁用 UFS 和/或 CPU 调控器等组件的省电设置。

注意:为提高效率,应在充电器模式下启用省电功能。

on init
    # Disable UFS powersaving
    write /sys/devices/soc/${ro.boot.bootdevice}/clkscale_enable 0
    write /sys/devices/soc/${ro.boot.bootdevice}/clkgate_enable 0
    write /sys/devices/soc/${ro.boot.bootdevice}/hibern8_on_idle_enable 0
    write /sys/module/lpm_levels/parameters/sleep_disabled Y
on property:sys.boot_completed=1
    # Enable UFS powersaving
    write /sys/devices/soc/${ro.boot.bootdevice}/clkscale_enable 1
    write /sys/devices/soc/${ro.boot.bootdevice}/clkgate_enable 1
    write /sys/devices/soc/${ro.boot.bootdevice}/hibern8_on_idle_enable 1
    write /sys/module/lpm_levels/parameters/sleep_disabled N
on charger
    # Enable UFS powersaving
    write /sys/devices/soc/${ro.boot.bootdevice}/clkscale_enable 1
    write /sys/devices/soc/${ro.boot.bootdevice}/clkgate_enable 1
    write /sys/devices/soc/${ro.boot.bootdevice}/hibern8_on_idle_enable 1
    write /sys/class/typec/port0/port_type sink
    write /sys/module/lpm_levels/parameters/sleep_disabled N

推迟非关键初始化

诸如 ZRAM 之类的非关键初始化可以推迟到 boot_complete。

on property:sys.boot_completed=1
   # Enable ZRAM on boot_complete
   swapon_all /vendor/etc/fstab.${ro.hardware}

优化开机动画

使用以下提示来优化启动动画。

配置早期启动

Android 8.0 可以在挂载用户数据分区之前提前启动启动动画。但是,即使在Android 8.0中使用新的ext4工具链时,出于安全考虑,fsck仍然会周期性的触发,导致bootanimation服务启动延迟。

为了让 bootanimation 尽早开始,将 fstab 挂载分为两个阶段:

  • 前期只挂载不需要运行检查的分区(如system/vendor/ ),然后启动启动动画服务及其依赖项(如 servicemanager 和 surfaceflinger)。
  • 在第二阶段,挂载需要运行检查的分区(例如data/ )。

无论 fsck 是什么,启动动画都将启动得更快(并且在恒定时间内)。

整理清洁

收到退出信号后,bootanimation 播放最后一部分,长度可以减慢开机时间。快速启动的系统不需要冗长的动画,它可以有效地隐藏所做的任何改进。我们建议将重复循环和结局都缩短。

优化 SELinux

使用以下技巧来优化 SELinux 以缩短启动时间。

  • 使用干净的正则表达式 (regex) 。在为file_contexts中的sys/devices匹配 SELinux 策略时,格式不正确的正则表达式会导致大量开销。例如,正则表达式/sys/devices/.*abc.*(/.*)?错误地强制扫描所有包含“abc”的/sys/devices子目录,从而使/sys/devices/abc/sys/devices/xyz/abc都匹配。将此正则表达式改进为/sys/devices/[^/]*abc[^/]*(/.*)?将只为/sys/devices/abc启用匹配。
  • 将标签移动到genfscon 。这个现有的 SELinux 功能将文件匹配前缀传递到 SELinux 二进制文件的内核中,内核将它们应用于内核生成的文件系统。这也有助于修复错误标记的内核创建的文件,防止在重新标记发生之前尝试访问这些文件的用户空间进程之间可能发生的竞争条件。

工具和方法

使用以下工具来帮助您收集优化目标的数据。

引导图

Bootchart 提供了整个系统所有进程的 CPU 和 I/O 负载分解。它不需要重建系统映像,并且可以在深入 systrace 之前用作快速的健全性检查。

要启用引导图:

adb shell 'touch /data/bootchart/enabled'
adb reboot

启动后,获取启动图表:

$ANDROID_BUILD_TOP/system/core/init/grab-bootchart.sh

完成后,删除/data/bootchart/enabled以防止每次都收集数据。

如果 bootchart 不起作用并且您收到错误提示bootchart.png不存在,请执行以下操作:
  1. 运行以下命令:
          sudo apt install python-is-python3
          cd ~/Documents
          git clone https://github.com/xrmx/bootchart.git
          cd bootchart/pybootchartgui
          mv main.py.in main.py
        
  2. 更新$ANDROID_BUILD_TOP/system/core/init/grab-bootchart.sh以指向pybootchartgui的本地副本(位于~/Documents/bootchart/pybootchartgui.py

系统跟踪

Systrace 允许在启动期间收集内核和 Android 跟踪。 systrace 的可视化有助于在启动期间分析特定问题。 (但是,要检查整个引导过程中的平均数或累积数,直接查看内核跟踪更容易)。

要在启动期间启用 systrace:

  • frameworks/native/cmds/atrace/atrace.rc中,更改:
      write /sys/kernel/debug/tracing/tracing_on 0
      write /sys/kernel/tracing/tracing_on 0

    至:

      #    write /sys/kernel/debug/tracing/tracing_on 0
      #    write /sys/kernel/tracing/tracing_on 0
  • 这将启用跟踪(默认情况下禁用)。

  • device.mk文件中,添加以下行:
    PRODUCT_PROPERTY_OVERRIDES +=    debug.atrace.tags.enableflags=802922
    PRODUCT_PROPERTY_OVERRIDES +=    persist.traced.enable=0
  • 在设备BoardConfig.mk文件中,添加以下内容:
    BOARD_KERNEL_CMDLINE := ... trace_buf_size=64M trace_event=sched_wakeup,sched_switch,sched_blocked_reason,sched_cpu_hotplug
  • 对于详细的 I/O 分析,还要添加块和 ext4 和 f2fs。

  • 在设备特定的init.rc文件中,添加以下内容:
    on property:sys.boot_completed=1          // This stops tracing on boot complete
    write /d/tracing/tracing_on 0
    write /d/tracing/events/ext4/enable 0
    write /d/tracing/events/f2fs/enable 0
    write /d/tracing/events/block/enable 0
    
  • 启动后,获取跟踪:

    adb root && adb shell atrace --async_stop -z -c -o /data/local/tmp/boot_trace
    adb pull /data/local/tmp/boot_trace
    $ANDROID_BUILD_TOP/external/chromium-trace/systrace.py --from-file=boot_trace