優化啟動時間

本文檔提供合作夥伴指導,以改善特定 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以防止每次都收集數據。

系統跟踪

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