啟動時間優化,啟動時間優化

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

從模組中去除調試符號

與從生產設備上的核心中刪除調試符號的方式類似,請確保也從模組中刪除調試符號。從模組中剝離調試符號可以透過減少以下內容來幫助啟動時間:

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

從模組中剝離調試符號可能會在啟動過程中節省幾秒鐘的時間。

預設情況下,在 Android 平台建置中啟用符號剝離,但要明確啟用它們,請在 device/ vendor / device下的裝置特定配置中設定BOARD_DO_NOT_STRIP_VENDOR_RAMDISK_MODULES

對內核和 ramdisk 使用 LZ4 壓縮

與 LZ4 相比,Gzip 產生的壓縮輸出更小,但 LZ4 解壓縮速度比 Gzip 更快。對於核心和模組,與 LZ4 的解壓縮時間優勢相比,使用 Gzip 帶來的絕對儲存大小減少並不那麼顯著。

對 LZ4 ramdisk 壓縮的支援已透過BOARD_RAMDISK_USE_LZ4添加到 Android 平台建置中。您可以在裝置特定的配置中設定此選項。核心壓縮可以透過kernel 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 完成不需要某個模組,請將其放置在供應商或vendor_dlkm分區中,將其移至第二階段 init。

第一階段初始化不需要偵測多個裝置即可進入第二階段初始化。正常啟動流程僅需要控制台和快閃記憶體儲存功能。

載入以下基本驅動程式:

  • 看門狗
  • 重置
  • CPU頻率

對於復原和使用者空間fastbootd模式,第一階段 init 需要更多裝置來偵測(例如 USB)和顯示。在第一階段 ramdisk 和供應商或vendor_dlkm分區中保留這些模組的副本。這允許它們在第一階段 init 中載入以進行復原或fastbootd引導流程。但是,在正常引導流程期間,請勿在第一階段 init 中載入恢復模式模組。恢復模式模組可以延遲到第二階段初始化以減少啟動時間。第一階段 init 中不需要的所有其他模組應移動到供應商或vendor_dlkm分區。

給定葉子裝置清單(例如 UFS 或串列), dev needs.sh腳本會尋找依賴項或供應商(例如時鐘、調節器或gpio )所需的所有驅動程式、裝置和模組進行偵測。

將模組移至第二階段 init 可透過以下方式減少啟動時間:

  • Ramdisk 大小減少。
    • 當引導程式載入 ramdisk(序列化引導步驟)時,這會產生更快的快閃記憶體讀取速度。
    • 當核心解壓縮 ramdisk(串行化引導步驟)時,這會產生更快的解壓縮速度。
  • 第二階段 init 並行工作,這隱藏了模組的載入時間以及第二階段 init 中正在完成的工作。

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

模組裝載物流

最新的 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_dlkm分區的模組清單。
  • BOARD_VENDOR_KERNEL_MODULES_LOAD 。要在第二階段 init 中載入的模組清單。

ramdisk 中的引導和復原模組也必須複製到位於/vendor/lib/modules供應商或vendor_dlkm分區。將這些模組複製到供應商分區可確保這些模組在第二階段 init 期間不可見,這對於偵錯和收集 bug 報告的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。

對於第二階段初始化,我們建議將模組載入作為服務運行,這樣它就不會阻止啟動流程。使用 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 blocklist 功能來拆分第二階段引導流程,以包含非必要模組的延遲模組載入。特定 HAL 專用的模組的載入可以推遲,以便僅在 HAL 啟動時載入模組。

為了改善明顯的啟動時間,您可以在模組載入服務中專門選擇更有利於啟動畫面後載入的模組。例如,您可以在清除 init 引導流程後明確延遲載入視訊解碼器或 wifi 的模組(例如sys.boot_complete Android 屬性訊號)。確保當核心驅動程式不存在時,後期載入模組的 HAL 阻塞足夠長的時間。

或者,您可以在引導流程 rc 腳本中使用 init 的wait<file>[<timeout>]命令來等待選定的sysfs條目以顯示驅動程式模組已完成探測操作。一個例子是在顯示選單圖形之前等待顯示驅動程式在 recovery 或fastbootd的背景完成載入。

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

由於啟動循環測試期間的熱或功耗問題,並非所有 SoC/產品都能夠以最高頻率啟動 CPU。但是,請確保引導程式將 SoC/產品的所有線上 CPU 的頻率設定為盡可能安全的高值。這非常重要,因為對於完全模組化的內核,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 中,它仍然會導致不必要的延遲。