啟動時間最佳化

本頁提供縮短啟動時間的訣竅。

從模組中移除偵錯符號

與從正式版裝置上的核心移除偵錯符號類似,請務必也從模組移除偵錯符號。從模組中移除偵錯符號,有助於減少下列項目,進而縮短啟動時間:

  • 從快閃記憶體讀取二進位檔所需的時間。
  • 解壓縮 RAM 磁碟所需的時間。
  • 載入模組所需的時間。

從模組中清除偵錯符號,啟動時可節省幾秒鐘。

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 壓縮支援。您可以在裝置專屬設定中設定這個選項。核心壓縮可透過核心 defconfig 設定。

改用 LZ4 後,開機速度應可提升 500 毫秒至 1000 毫秒。

避免在驅動程式中過度記錄

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

Linux 核心會在分配 PLT 時進行一些記憶體大小最佳化 (例如快取命中最佳化)。有了這個上游提交,最佳化配置的複雜度為 O(N^2),其中 NR_AARCH64_JUMP26R_AARCH64_CALL26 類型的 RLA 數量。因此,減少這類 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 留在哪個頻率,提早探查可節省的電量可能非常少,也可能非常多。

將模組移至第二階段初始化、供應商或供應商_dlkm 分區

由於第一階段的 init 程序是序列化,因此沒有太多機會可平行處理啟動程序。如果完成第一階段初始化不需要某個模組,請將該模組放在供應商或 vendor_dlkm 分區,移至第二階段初始化。

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

載入下列必要驅動程式:

  • watchdog
  • reset
  • cpufreq

如果是復原和使用者空間 fastbootd 模式,第一階段的 init 需要探查更多裝置 (例如 USB) 和螢幕。請在第一階段 ramdisk 和供應商或 vendor_dlkm 分區中保留這些模組的副本。這樣一來,這些驅動程式就能在復原或 fastbootd 開機流程的第一階段初始化時載入。不過,在正常啟動流程中,請勿在第一階段的 init 載入復原模式模組。復原模式模組可以延後到第二階段初始化,以縮短啟動時間。所有第一階段初始化不需要的其他模組,都應移至供應商或 vendor_dlkm 分區。

假設有一份子裝置清單 (例如 UFS 或序列),dev needs.sh 指令碼會找出依附元件或供應商 (例如時鐘、穩壓器或 gpio) 探查所需的所有驅動程式、裝置和模組。

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

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

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

模組載入物流

最新 Android 建構版本提供主機板設定,可控管要將哪些模組複製到各個階段,以及要載入哪些模組。本節著重於下列子集:

  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES。這是要複製到 ramdisk 的模組清單。
  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD。這是要在第一階段初始化中載入的模組清單。
  • BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD。這個模組清單會在從 ramdisk 選取復原或 fastbootd 時載入。
  • BOARD_VENDOR_KERNEL_MODULES。這份模組清單會複製到 /vendor/lib/modules/ 目錄的供應商或 vendor_dlkm 分割區。
  • BOARD_VENDOR_KERNEL_MODULES_LOAD。這個模組清單會在第二階段初始化時載入。

ramdisk 中的開機和復原模組也必須複製到 /vendor/lib/modules 的供應商或 vendor_dlkm分割區。將這些模組複製到供應商分割區,可確保模組在第二階段初始化期間不會隱藏,這有助於偵錯及收集錯誤報告的 modinfo

只要將啟動模組集縮減到最小,複製作業應會佔用供應商或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。

對於第二階段的初始化作業,建議您以服務形式執行模組載入作業,以免阻礙啟動流程。使用殼層指令碼管理模組載入作業,以便在必要時回報 (或忽略) 其他物流作業,例如錯誤處理和緩解,或模組載入完成。

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

如要縮短啟動時間,您可以在模組載入服務中,選擇更適合在啟動畫面後載入的模組。舉例來說,您可以在清除 init 啟動流程後,明確延遲載入視訊解碼器或 Wi-Fi 的模組 (例如 sys.boot_complete Android 屬性信號)。請確認在沒有核心驅動程式時,延遲載入模組的 HAL 會封鎖夠長的時間。

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

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

在開機迴圈測試期間,由於有散熱或電力方面的疑慮,並非所有 SoC/產品都能以最高頻率啟動 CPU。不過,請確保開機載入程式會將所有上線 CPU 的頻率設為 SoC 或產品安全範圍內的最高值。這點非常重要,因為如果核心完全模組化,init ramdisk 解壓縮作業會在 CPUfreq 驅動程式載入前進行。因此,如果開機載入器將 CPU 頻率設為較低的值,在調整 ramdisk 大小差異後,ramdisk 解壓縮時間可能會比靜態編譯核心長,因為 CPU 頻率在執行 CPU 密集型工作 (解壓縮) 時會非常低。記憶體和互連頻率也適用相同原則。

在啟動載入程式中初始化大 CPU 的 CPU 頻率

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

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

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

在某些不可避免的情況下,韌體可能需要在第一階段的 init 中載入。但一般來說,驅動程式不應在第一階段的初始化程序中載入任何韌體,尤其是在裝置探查環境中。如果在第一階段 init 中載入韌體,但第一階段 ramdisk 中沒有韌體,整個啟動程序就會停滯。即使韌體存在於第一階段的 ramdisk 中,仍會造成不必要的延遲。