1. 程式人生 > >Android O 安卓啟動時間優化分析

Android O 安卓啟動時間優化分析

文件轉載自AOSP官網

本文件提供了有關改進特定 Android 裝置的啟動時間的合作伙伴指南。啟動時間是系統性能的重要組成部分,因為使用者必須等待啟動完成後才能使用裝置。對於較常進行冷啟動的汽車等裝置而言,較短的啟動時間至關重要(沒有人喜歡在等待幾十秒後才能輸入導航目的地)。

Android 8.0 支援一系列元件的多項改進,因而可以縮短啟動時間。下表對這些效能改進(在 Google Pixel 和 Pixel XL 裝置上測得)進行了總結。

元件 改進
引導載入程式 通過移除 UART 日誌節省了 1.6 秒 通過從 GZIP 更改為 LZ4 節省了 0.4 秒
裝置核心 通過移除不使用的核心配置和減少驅動程式大小節省了 0.3 秒 通過 dm-verity 預提取優化節省了 0.3 秒 通過移除驅動程式中不必要的等待/測試,節省了 0.15 秒 通過移除 CONFIG_CC_OPTIMIZE_FOR_SIZE,節省了 0.12 秒
I/O 調整 正常啟動時間節省了 2 秒 首次啟動時間節省了 25 秒
init.*.rc 通過並行執行 init 命令節省了 1.5 秒 通過及早啟動 zygote 節省了 0.25 秒 通過 cpuset 調整節省了 0.22 秒
啟動動畫 在未觸發 fsck 的情況下,啟動動畫的開始時間提前了 2 秒,而觸發 fsck 時啟動動畫則大得多 通過立即關閉啟動動畫在 Pixel XL 上節省了 5 秒
SELinux 政策 通過 genfscon 節省了 0.2 秒

優化引導載入程式

要優化引導載入程式以縮短啟動時間,請遵循以下做法:

  • 對於日誌記錄:
    • 停止向 UART 寫入日誌,因為如果日誌記錄很多,則可能需要很長時間來處理。(在 Google Pixel 裝置上,我們發現這會使引導載入程式的速度減慢 1.5 秒)。
    • 僅記錄錯誤情況,並考慮將其他資訊儲存到具有單獨檢索機制的記憶體中。
  • 對於核心解壓縮,請考慮為當代硬體使用 LZ4 而非 GZIP(例如補丁程式)。請注意,不同的核心壓縮選項具有不同的載入和解壓縮時間,對於特定硬體,某些選項可能比其他選項更適合。
  • 檢查進入去抖動/特殊模式過程中是否有不必要的等待時間,並最大限度地減少此類時間。
  • 將在引導載入程式中花費的啟動時間以命令列的形式傳遞到核心。 檢查 CPU 時鐘並考慮核心載入和初始化 I/O 並行進行(需要多核支援)。

優化核心

請按照以下提示優化核心以縮短啟動時間。

最大限度地減少裝置 defconfig

最大限度地減少核心配置可以減小核心大小,從而更快速地進行載入、解壓縮、初始化並縮小受攻擊面。要優化裝置 defconfig,請執行以下操作:

  • 識別未使用的驅動程式。檢視 /dev 和 /sys 目錄,並查詢帶有常規 SELinux 標籤的節點(這種標籤表示相應節點未配置為可由使用者空間訪問)。如果找到此類節點,請將其移除。
  • 取消設定未使用的配置。檢視由核心版本生成的 .config 檔案,以明確取消設定所有已預設啟用但並未使用的配置。例如,我們從 Google Pixel 中移除了以下未使用的配置:
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_IOMMU_IO_PGTABLE_FAST_SELFTEST)在開發過程中很有用,但應從正式版核心中移除。

最大限度地減小驅動程式大小

如果未使用相應功能,則可以移除裝置核心中的某些驅動程式,以便進一步減小核心大小。例如,如果 WLAN 通過 PCIe 連線,則不會用到 SDIO 支援,因此應在編譯時將其移除。有關詳情,請參閱 Google Pixel 核心:網路:無線:CNSS:新增選項以停用 SDIO 支援。

移除針對大小的編譯器優化

移除 CONFIG_CC_OPTIMIZE_FOR_SIZE 的核心配置。此標記是在最初假設較小的程式碼大小會產生熱快取命中(因此速度更快)時引入的。然而,隨著現代移動 SoC 變得更加強大,這一假設不再成立。

此外,移除此標記可以使編譯器針對未初始化的變數發出警告,當存在 CONFIG_CC_OPTIMIZE_FOR_SIZE 標記時,這一功能在 Linux 核心中是停用的(僅這一項更改就已幫助我們在某些 Android 裝置驅動程式中發現了很多有意義的錯誤)。

延遲初始化

很多程序都在裝置啟動期間啟動,但只有關鍵路徑 (bootloader > kernel > init > file system mount > zygote > system server) 中的元件才會直接影響啟動時間。在核心啟動期間執行 initcall 來識別啟動速度緩慢且對啟動 init 程序不重要的外設/元件,然後通過將這些外設/元件移入可載入的核心模組將其延遲到啟動過程的後期來啟動。移入非同步裝置/驅動程式探測還有助於並行啟動核心 > 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 中的緩慢操作。系統會記錄 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 中沒有使用的服務和命令,都應推遲到啟動完成後再使用。 注意:“屬性”服務是 init 程序的一部分,因此,在啟動期間呼叫 setproperty 可能會導致較長時間的延遲(如果 init 忙於執行內建命令)。

注意:“屬性”服務是 init 程序的一部分,因此,在啟動期間呼叫 setproperty 可能會導致較長時間的延遲(如果 init 忙於執行內建命令)。

使用排程程式調整

使用排程程式調整,以便及早啟動裝置。以下是取自 Google Pixel 的示例:

on init
    # update cpusets now that processors are up
    write /dev/cpuset/top-app/cpus 0-3
    write /dev/cpuset/foreground/cpus 0-3
    write /dev/cpuset/foreground/boost/cpus 0-3
    write /dev/cpuset/background/cpus 0-3
    write /dev/cpuset/system-background/cpus 0-3
    # set default schedTune value for foreground/top-app (only affects EAS)
    write /dev/stune/foreground/schedtune.prefer_idle 1
    write /dev/stune/top-app/schedtune.boost 10
    write /dev/stune/top-app/schedtune.prefer_idle 1

部分服務在啟動過程中可能需要進行優先順序提升。例如:

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 支援在裝載使用者資料分割槽之前,及早啟動動畫。然而,即使 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

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 以防止每次都收集日期資料。

systrace

systrace 允許在啟動期間收集核心和 Android 跟蹤記錄。 systrace 的視覺化可以幫助分析啟動過程中的具體問題。(不過,要檢視整個啟動過程中的平均數量或累計數量,直接檢視核心跟蹤記錄更為方便)。

要在啟動過程中啟用 systrace,請執行以下操作:

  • frameworks/native/atrace/atrace.rc 中,將 write /sys/kernel/debug/tracing/tracing_on 0

更改為: #write /sys/kernel/debug/tracing/tracing_on 0

這將啟用跟蹤功能(預設處於停用狀態)。

  • 在device.mk檔案中,新增下面這行內容: PRODUCT_PROPERTY_OVERRIDES += debug.atrace.tags.enableflags=802922

  • 在裝置 BoardConfig.mk檔案中,新增以下內容: BOARD_KERNEL_CMDLINE := ... trace_buf_size=64M trace_event=sched_wakeup,sched_switch,sched_blocked_reason,sched_cpu_hotplug

要獲得詳細的 I/O 分析,還需要新增塊和 ext4。

  • 在裝置專用的 init.rc 檔案中,進行以下更改:
  • on property:sys.boot_completed=1(這會在啟動完成後停止跟蹤)
  • write /d/tracing/tracing_on 0
  • write /d/tracing/events/ext4/enable 0
  • write /d/tracing/events/block/enable 0 在裝置啟動後,獲取跟蹤記錄:
adb root && adb shell "cat /d/tracing/trace" < boot_trace
./external/chromium-trace/catapult/tracing/bin/trace2html boot_trace --output boot_trace.html

注意:Chrome 無法處理過大的檔案。請考慮使用 tail、head 或 grep 分割 boot_trace 檔案,以獲得必要的部分。由於事件過多,I/O 分析通常需要直接分析獲取的 boot_trace。