啟動時間優化

此頁面提供了一組提示,您可以從中進行選擇,以縮短啟動時間。

從模塊中去除調試符號

與在生產設備上從內核中剝離調試符號的方式類似,請確保您也從模塊中剝離調試符號。從模塊中去除調試符號有助於減少以下啟動時間:

  • 從閃存讀取二進製文件所需的時間。
  • 解壓縮 ramdisk 所需的時間。
  • 加載模塊所需的時間。

從模塊中剝離調試符號可能會在引導期間節省幾秒鐘。

在 Android 平台構建中默認啟用符號剝離,但要顯式啟用它們,請在 device/ vendor / device下的設備特定配置中設置BOARD_DO_NOT_STRIP_VENDOR_RAMDISK_MODULES

對內核和 ramdisk 使用 LZ4 壓縮

與 LZ4 相比,Gzip 生成的壓縮輸出較小,但 LZ4 的解壓縮速度比 Gzip 快。對於內核和模塊,與 LZ4 的解壓縮時間優勢相比,使用 Gzip 所減少的絕對存儲大小並不顯著。

通過BOARD_RAMDISK_USE_LZ4在 Android 平台構建中添加了對 LZ4 ramdisk 壓縮的支持。您可以在特定於設備的配置中設置此​​選項。內核壓縮可以通過內核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 完成不需要模塊,則將模塊移動到第二階段 init,方法是將其放置在 vendor 或vendor_dlkm分區中。

第一階段初始化不需要探測多個設備即可進入第二階段初始化。正常啟動流程只需要控制台和閃存功能。

加載以下基本驅動程序:

  • 看門狗
  • 重啟
  • cpufreq

對於恢復和用戶空間fastbootd模式,第一階段 init 需要更多設備來探測(例如 USB)和顯示。在第一階段 ramdisk 和 vendor 或vendor_dlkm分區中保留這些模塊的副本。這允許它們在第一階段 init 中加載以進行恢復或fastbootd引導流程。但是,在正常引導流程期間,不要在第一階段 init 中加載恢復模式模塊。恢復模式模塊可以推遲到第二階段初始化以減少啟動時間。第一階段 init 中不需要的所有其他模塊應移動到 vendor 或vendor_dlkm分區。

給定一個葉子設備列表(例如,UFS 或串行), dev needs.sh腳本查找依賴項或供應商(例如,時鐘、調節器或gpio )需要探測的所有驅動程序、設備和模塊。

將模塊移動到第二階段 init 通過以下方式減少啟動時間:

  • Ramdisk 大小減小。
    • 當引導加載程序加載 ramdisk(序列化引導步驟)時,這會產生更快的閃存讀取。
    • 當內核解壓縮 ramdisk(序列化引導步驟)時,這會產生更快的解壓縮速度。
  • 第二階段 init 並行工作,這隱藏了模塊的加載時間,工作在第二階段 init 中完成。

將模塊移動到第二階段可以節省500 - 1000 毫秒的啟動時間,具體取決於您能夠移動到第二階段初始化的模塊數量。

模塊裝載物流

最新的 Android 版本具有控制哪些模塊複製到每個階段以及加載哪些模塊的板配置。本節重點介紹以下子集:

  • 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/lib/modules/目錄下的 vendor 或vendor_dlkm分區。
  • BOARD_VENDOR_KERNEL_MODULES_LOAD 。要在第二階段 init 中加載的模塊列表。

ramdisk 中的引導和恢復模塊也必須複製到/vendor/lib/modules的 vendor 或vendor_dlkm分區。將這些模塊複製到供應商分區可確保這些模塊在第二階段初始化期間不可見,這對於調試和收集modinfo以獲取錯誤報告很有用。

只要最小化引導模塊集,複製就應該在 vendor 或vendor_dlkm分區上花費最少的空間。確保供應商的modules.list文件在/vendor/lib/modules中有過濾的模塊列表。過濾後的列表確保啟動時間不受模塊再次加載的影響(這是一個昂貴的過程)。

確保恢復模式模塊作為一個組加載。加載恢復模式模塊可以在恢復模式下完成,也可以在每個引導流程的第二階段 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。

對於第二階段初始化,我們建議將模塊加載作為服務運行,這樣它就不會阻塞引導流程。使用 shell 腳本來管理模塊加載,以便在必要時可以報告回(或忽略)其他後勤工作,例如錯誤處理和緩解,或模塊加載完成。

您可以忽略用戶構建中不存在的調試模塊加載失敗。要忽略此故障,請設置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 阻止列表功能拆分第二階段引導流程,以包括非必要模塊的延遲模塊加載。僅在啟動 HAL 時才可以延遲加載由特定 HAL 獨占使用的模塊。

為了改善明顯的啟動時間,您可以在模塊加載服務中專門選擇更有利於在啟動屏幕後加載的模塊。例如,您可以在初始化引導流程被清除後顯式延遲加載視頻解碼器或 wifi 的模塊(例如sys.boot_complete Android 屬性信號)。當內核驅動程序不存在時,確保延遲加載模塊的 HAL 阻塞足夠長的時間。

或者,您可以在引導流程 rc 腳本中使用 init 的wait<file>[<timeout>]命令等待選擇sysfs條目以顯示驅動程序模塊已完成探測操作。例如,在顯示菜單圖形之前,等待顯示驅動程序在 recovery 或fastbootd的後台完成加載。

在引導加載程序中將 CPU 頻率初始化為合理的值

由於啟動循環測試期間的散熱或電源問題,並非所有 SoC/產品都能夠以最高頻率啟動 CPU。但是,請確保引導加載程序將所有在線 CPU 的頻率設置為對 SoC/產品盡可能安全的高頻率。這非常重要,因為對於完全模塊化的內核,init ramdisk 解壓縮發生在 CPUfreq 驅動程序加載之前。因此,如果引導加載程序將 CPU 留在其頻率的低端,則 ramdisk 解壓縮時間可能比靜態編譯的內核(在調整 ramdisk 大小差異後)需要更長的時間,因為在 CPU 密集型時 CPU 頻率會非常低工作(減壓)。這同樣適用於內存/互連頻率。

在引導加載程序中初始化大 CPU 的 CPU 頻率

在加載CPUfreq驅動程序之前,內核不知道 CPU 頻率的大小,也不會根據當前頻率調整 CPU 的調度容量。如果小 CPU 上的負載足夠高,內核可能會將線程遷移到大 CPU。

確保大 CPU 在引導加載程序保留它們的頻率下至少與小 CPU 一樣性能。例如,如果大 CPU 在相同頻率下的性能是小 CPU 的 2 倍,但引導加載程序設置小 CPU 的頻率為 1.5 GHz,而大 CPU 的頻率為 300 MHz,如果內核將線程移動到大 CPU,啟動性能將會下降。在此示例中,如果以 750 MHz 啟動大型 CPU 是安全的,即使您不打算明確使用它,您也應該這樣做。

驅動程序不應在第一階段初始化中加載固件

可能有一些不可避免的情況需要在第一階段初始化中加載固件。但一般來說,驅動程序不應在第一階段初始化中加載任何固件,尤其是在設備探測上下文中。如果固件在第一階段 ramdisk 中不可用,則在第一階段 init 中加載固件會導致整個引導過程停止。即使固件存在於第一階段 ramdisk 中,它仍然會導致不必要的延遲。