本文档为合作伙伴提供了关于如何缩短特定 Android 设备启动时间的指南。启动时间是系统性能的重要组成部分,因为用户必须等待启动完成才能使用设备。对于汽车等冷启动更频繁的设备,快速启动时间至关重要(没人喜欢仅仅为了输入导航目的地而等待数十秒)。
Android 8.0 通过支持一系列组件的改进来缩短启动时间。下表总结了这些性能改进(根据 Google Pixel 和 Pixel XL 设备上的测量结果)。
组件 | 改进 |
---|---|
启动加载程序 |
|
设备内核 |
|
I/O 调优 |
|
init.*.rc |
|
启动动画 |
|
SELinux 政策 | 通过 genfscon 节省了 0.2 秒 |
优化启动加载程序
优化启动加载程序以缩短启动时间
- 用于日志记录
- 禁用向 UART 写入日志,因为写入大量日志可能需要很长时间。(在 Google Pixel 设备上,我们发现它使启动加载程序速度减慢了 1.5 秒)。
- 仅记录错误情况,并考虑使用单独的机制将其他信息存储到内存中以进行检索。
- 对于内核解压缩,考虑对现代硬件使用 LZ4 而不是 GZIP(示例 补丁)。请记住,不同的内核压缩选项可能具有不同的加载和解压缩时间,并且某些选项可能比其他选项更适合您的特定硬件。
- 检查不必要的等待时间以进行防抖/特殊模式进入,并尽量缩短这些时间。
- 将启动加载程序中花费的启动时间作为 cmdline 传递给内核。
- 检查 CPU 时钟,并考虑并行化(需要多核支持)以进行内核加载和初始化 I/O。
优化 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 打开 dm-verity 哈希预取大小(默认大小为 128)。
- 为了获得更好的文件系统稳定性和每次启动时发生的强制检查的减少,请通过在 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 中的慢速操作。系统会记录 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 返回“wait for service”时,查看系统日志 - 这通常表明必须首先启动依赖服务。
- 删除 init.*.rc 中任何未使用的服务和命令。早期 init 阶段未使用的任何内容都应推迟到启动完成时。
注意: Property 服务是 init 进程的一部分,因此在启动期间调用 setproperty
如果 init 正忙于内置命令,可能会导致长时间延迟。
使用调度器调优
对早期启动使用调度器调优。来自 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
具有基于文件的加密的设备可以在 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 允许在挂载 userdata 分区之前提前启动启动动画。但是,即使在使用 Android 8.0 中的新 ext4 工具链时,出于安全原因,fsck 仍会定期触发,从而导致启动动画服务启动延迟。
为了使启动动画提前启动,请将 fstab 挂载拆分为两个阶段
- 在早期阶段,仅挂载不需要运行检查的分区(例如
system/
和vendor/
),然后启动启动动画服务及其依赖项(例如 servicemanager 和 surfaceflinger)。 - 在第二阶段,挂载需要运行检查的分区(例如
data/
)。
无论 fsck 如何,启动动画都将更快(并且以恒定时间)启动。
完成清理
收到退出信号后,启动动画会播放最后一部分,其长度可能会减慢启动时间。快速启动的系统不需要冗长的动画,这可能会有效地隐藏所做的任何改进。我们建议使重复循环和结尾都短。
优化 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
Bootchart 提供整个系统中所有进程的 CPU 和 I/O 负载分解。它不需要重建系统镜像,可以在深入研究 systrace 之前用作快速健全性检查。
启用 bootchart
adb shell 'touch /data/bootchart/enabled'
adb reboot
启动后,获取启动图表
$ANDROID_BUILD_TOP/system/core/init/grab-bootchart.sh
完成后,删除 /data/bootchart/enabled
以防止每次都收集数据。
bootchart.png
不存在,请执行以下操作- 运行以下命令
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
- 更新
$ANDROID_BUILD_TOP/system/core/init/grab-bootchart.sh
以指向pybootchartgui
的本地副本(位于~/Documents/bootchart/pybootchartgui.py
)
Systrace
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
- 在设备特定的
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
这会启用跟踪(默认情况下禁用)。
对于详细的 I/O 分析,还要添加 block 和 ext4 以及 f2fs。