開機時間最佳化

本頁面提供改善啟動時間的提示。

從模組中移除偵錯符號

與在實際裝置上從核心中移除偵錯符號的方式類似,請務必從模組中移除偵錯符號。從模組中移除偵錯符號可縮短啟動時間,因為這樣可減少下列項目:

  • 從 Flash 讀取二進位檔所需的時間。
  • 解壓縮 RAMDISK 所需的時間。
  • 載入模組所需的時間。

從模組中移除偵錯符號,可能會在啟動期間節省幾秒鐘的時間

根據預設,Android 平台版本會啟用符號去除功能,但如要明確啟用此功能,請在 device/vendor/device 底下,在裝置專屬設定中設定 BOARD_DO_NOT_STRIP_VENDOR_RAMDISK_MODULES

為核心和 RAM 磁碟使用 LZ4 壓縮

與 LZ4 相比,Gzip 產生的壓縮輸出內容較小,但 LZ4 的解壓縮速度比 Gzip 快。就核心和模組而言,與 LZ4 的解壓縮時間相比,使用 Gzip 縮減的絕對儲存空間大小並不是那麼重要。

Android 平台版本已透過 BOARD_RAMDISK_USE_LZ4 新增 LZ4 ramdisk 壓縮功能。您可以在裝置專屬的設定中設置這個選項。您可以透過核心定義設定進行核心壓縮。

切換至 LZ4 後,開機時間應可加快 500 毫秒至 1000 毫秒

避免在驅動程式中記錄過多資訊

在 ARM64 和 ARM32 中,如果函式呼叫與呼叫位置的距離超過特定距離,就需要跳躍表 (稱為程序連結表或 PLT) 才能編碼完整的跳躍位址。由於模組是以動態方式載入,因此這些跳轉表格必須在模組載入期間進行修正。需要重新安置的呼叫稱為重新安置項目,其中包含 ELF 格式的明確附加項 (或簡稱 RELA) 項目。

Linux kernel 會在分配 PLT 時執行某些記憶體大小最佳化作業 (例如快取命中最佳化)。在這個上游提交中,最佳化配置方案的複雜度為 O(N^2),其中 NR_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_dlkm 分區

由於第一階段的初始化程序是序列化的,因此沒有太多機會可以並行執行啟動程序。如果模組不需要在第一個階段 init 完成,請將模組置於第二階段,方法是將其放入廠商或 vendor_dlkm 分區。

第一階段初始化不需要探測多個裝置,才能進入第二階段初始化。一般啟動流程只需要主控台和快閃儲存空間功能。

載入下列必要驅動程式:

  • watchdog
  • reset
  • cpufreq

針對復原和使用者空間 fastbootd 模式,第一階段 init 需要更多裝置才能探測 (例如 USB) 和螢幕。請在第一階段的 RAM 磁碟區和供應商或 vendor_dlkm 分區中保留這些模組的副本。這樣一來,系統就能在第一階段初始化作業中載入這些檔案,以便進行復原或 fastbootd 開機流程。不過,請勿在一般啟動流程期間,於第一階段 init 載入復原模式模組。復原模式模組可延後至第二階段初始化,以縮短啟動時間。第一個階段 init 不需要的所有其他模組都應移至供應商或 vendor_dlkm 分區。

在提供葉子裝置清單 (例如 UFS 或序列) 的情況下,dev needs.sh 指令碼會找出依附元件或供應商 (例如時鐘、調節器或 gpio) 所需的所有驅動程式、裝置和模組,以便進行探測。

將模組移至第二階段初始化,可透過以下方式縮短啟動時間:

  • 縮減 Ramdisk 大小。
    • 這樣一來,當 Bootloader 載入 RAM 磁碟時 (序列化啟動步驟),就能加快 Flash 讀取速度。
    • 這樣可在核心解壓縮 ramdisk (序列化開機步驟) 時產生更快速的解壓縮速度。
  • 第二階段的初始化作業會以平行方式運作,因此可隱藏模組的載入時間,並在第二階段初始化作業中完成。

將模組移至第二階段可節省 500 到 1000 毫秒的啟動時間,具體取決於您能將多少模組移至第二階段初始化。

模組載入流程

最新的 Android 建構功能包含板型設定,可控制要將哪些模組複製到每個階段,以及載入哪些模組。本節將著重於以下子集:

  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES。這份清單會將模組複製到 RAM 磁碟。
  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD。這個清單會在第一階段初始化時載入模組。
  • BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD。在從 RAM 磁碟區選取 recovery 或 fastbootd 時,要載入的模組清單。
  • BOARD_VENDOR_KERNEL_MODULES:這是要複製到供應商或 vendor_dlkm 分區 (位於 /vendor/lib/modules/ 目錄) 中的模組清單。
  • BOARD_VENDOR_KERNEL_MODULES_LOAD。這個模組清單會在第二階段初始化時載入。

您也必須將 RAMDISK 中的啟動和復原模組複製到 /vendor/lib/modules 的供應商或 vendor_dlkm 分割區。將這些模組複製到供應商分割區,可確保模組不會在第二階段初始化期間隱藏,這對於偵錯和收集 modinfo 以製作錯誤報告非常有用。

只要啟動模組集最小化,複製作業應花費最少的廠商或 vendor_dlkm 分區空間。請確認供應商的 modules.list 檔案在 /vendor/lib/modules 中包含篩選過的模組清單。篩選清單可確保啟動時間不會受到模組重新載入 (這項程序耗時且耗用大量資源) 的影響。

請確認復原模式模組是以群組方式載入。載入復原模式模組可在復原模式下,或在每啟動流程的第二個階段初始化時完成。

您可以使用裝置 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,建議您以服務形式執行模組載入作業,以免封鎖啟動流程。使用殼層指令碼來管理模組載入作業,以便視需要回報其他邏輯,例如錯誤處理和緩解,或模組載入完成。

您可以忽略使用者建構中沒有的偵錯模組載入失敗。如要忽略這項失敗,請將 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 啟動時載入模組。

為縮短實際開機時間,您可以在模組載入服務中特別選擇較適合在啟動畫面後載入的模組。舉例來說,您可以在初始啟動流程 (例如 sys.boot_complete Android 屬性信號) 完成後,明確延遲載入影片解碼器或 Wi-Fi 的模組。請確認在沒有核心驅動程式時,延遲載入模組的 HAL 會阻斷足夠長的時間。

或者,您也可以在啟動流程架構指令碼中使用 init 的 wait<file>[<timeout>] 指令,等待特定 sysfs 項目顯示驅動程式模組已完成探測作業。舉例來說,您可以等待顯示器驅動程式在復原或 fastbootd 的背景中完成載入作業,再顯示選單圖形。

在系統啟動載入程式中,將 CPU 頻率初始化為合理的值

由於啟動循環測試期間的熱或電力問題,並非所有 SoC/產品都能以最高頻率啟動 CPU。不過,請務必確保引導程式將所有線上 CPU 的頻率設為 SoC 或產品可安全使用的最高頻率。這點非常重要,因為在使用完全模組化核心時,系統會先執行初始化 RAM 磁碟解壓縮作業,才能載入 CPU 頻率驅動程式。因此,如果啟動載入程式將 CPU 頻率維持在較低的範圍,則在進行 CPU 密集工作 (解壓縮) 時,RAMDISK 解壓縮時間可能會比靜態編譯的核心時間長 (在調整 RAMDISK 大小差異後),因為 CPU 頻率會非常低。記憶體和互連頻率也適用相同的規則。

初始化系統啟動載入程式中大型 CPU 的 CPU 頻率

在載入 CPUfreq 驅動程式之前,核心不會偵測 CPU 頻率,也不會根據目前的頻率調整 CPU 排程容量。如果小 CPU 的負載過高,核心可能會將執行緒遷移至大 CPU。

確保大型 CPU 的效能至少與系統啟動載入程式離開頻率相同。舉例來說,如果大 CPU 在相同頻率下效能是小 CPU 的 2 倍,但啟動載入器將小 CPU 的頻率設為 1.5 GHz,而大 CPU 的頻率設為 300 MHz,則如果核心將執行緒移至大 CPU,啟動效能就會下降。在本例中,如果以 750 MHz 的頻率啟動大型 CPU 是安全的,即使您不打算明確使用該 CPU,也應這麼做。

驅動程式不應在第一階段初始化時載入韌體

在不可避免的情況下,您可能必須在第一階段 init 載入韌體。但一般來說,驅動程式不應在第一階段初始化時載入任何韌體,尤其是在裝置探測情境中。如果第一階段的 RAM 磁碟區中沒有韌體,在第一階段初始化時載入韌體會導致整個啟動程序停滯。即使韌體位於第一階段的 RAM 磁碟區,仍會造成不必要的延遲。