Ottimizzazione del tempo di avvio

Questa pagina fornisce suggerimenti per migliorare il tempo di avvio.

Rimuovere i simboli di debug dai moduli

Analogamente a come i simboli di debug vengono rimossi dal kernel su un dispositivo di produzione, assicurati di rimuovere anche i simboli di debug dai moduli. L'eliminazione dei simboli di debug dai moduli riduce il tempo di avvio riducendo quanto segue:

  • Il tempo necessario per leggere i file binari da Flash.
  • Il tempo necessario per decomprimere il ramdisk.
  • Il tempo necessario per caricare i moduli.

L'eliminazione dei simboli di debug dai moduli può far risparmiare diversi secondi durante l'avvio.

Lo stripping dei simboli è abilitato per impostazione predefinita nella build della piattaforma Android, ma per attivarlo esplicitamente, imposta BOARD_DO_NOT_STRIP_VENDOR_RAMDISK_MODULES nella configurazione specifica del dispositivo in device/vendor/device.

Utilizza la compressione LZ4 per il kernel e il ramdisk

Gzip genera un output compresso più piccolo rispetto a LZ4, ma LZ4 si decomprime più velocemente di Gzip. Per il kernel e i moduli, la riduzione assoluta delle dimensioni dello spazio di archiviazione derivante dall'utilizzo di Gzip non è significativa rispetto al vantaggio in termini di tempo di decompressione di LZ4.

Il supporto della compressione del ramdisk LZ4 è stato aggiunto alla compilazione della piattaforma Android fino alla versione BOARD_RAMDISK_USE_LZ4. Puoi impostare questa opzione nella configurazione specifica del dispositivo. La compressione del kernel può essere impostata tramite il file defconfig del kernel.

Il passaggio a LZ4 dovrebbe comportare un tempo di avvio più rapido compreso tra 500 e 1000 ms.

Evita di eseguire un logging eccessivo nei driver

In ARM64 e ARM32, le chiamate di funzione che si trovano a una distanza maggiore da un sito di chiamata specifico richiedono una tabella di jump (chiamata tabella di collegamento delle procedure o PLT) per poter codificare l'indirizzo di jump completo. Poiché i moduli vengono caricati in modo dinamico, queste tabelle di salto devono essere corrette durante il caricamento del modulo. Le chiamate che richiedono la ricollocazione sono chiamate voci di ricollocazione con addendi espliciti (o RELA, per breve) nel formato ELF.

Il kernel Linux esegue alcune ottimizzazioni delle dimensioni della memoria (ad esempio l'ottimizzazione dei colpi in cache) durante l'allocazione della PLT. Con questo commit upstream, lo schema di ottimizzazione ha una complessità O(N^2), dove N è il numero di RELA di tipo R_AARCH64_JUMP26 o R_AARCH64_CALL26. Pertanto, avere meno RELA di questo tipo è utile per ridurre il tempo di caricamento del modulo.

Un pattern di codifica comune che aumenta il numero di R_AARCH64_CALL26 o R_AARCH64_JUMP26 RELA è la registrazione eccessiva in un driver. Ogni chiamata a printk() o a qualsiasi altro schema di registrazione in genere aggiunge una voce CALL26/JUMP26 RELA. Nel testo del commit nel commit di upstream, tieni presente che, anche con l'ottimizzazione, i sei moduli richiedono circa 250 ms per caricarsi, poiché si tratta dei sei moduli principali con il maggior numero di log.

La riduzione della registrazione può far risparmiare circa 100-300 ms sui tempi di avvio, a seconda della quantità eccessiva di log esistenti.

Attivare il monitoraggio asincrono in modo selettivo

Quando viene caricato un modulo, se il dispositivo supportato è già stato compilato dal DT (devicetree) e aggiunto al driver core, la ricerca del dispositivo viene eseguita nel contesto della chiamata module_init(). Quando viene eseguita una verifica del dispositivo nel contesto di module_init(), il caricamento del modulo non può essere completato finché la verifica non è terminata. Poiché il caricamento dei moduli è per lo più serializzato, un dispositivo che impiega un tempo relativamente lungo per eseguire la ricerca rallenta il tempo di avvio.

Per evitare tempi di avvio più lenti, attiva il rilevamento asincrono per i moduli che richiedono un po' di tempo per eseguire il rilevamento dei dispositivi. L'attivazione del monitoraggio asincrono per tutti i moduli potrebbe non essere utile perché il tempo necessario per eseguire il fork di un thread e avviare il monitoraggio potrebbe essere uguale al tempo necessario per eseguire il monitoraggio del dispositivo.

I dispositivi connessi tramite un bus lento come I2C, i dispositivi che eseguono il caricamento del firmware nella funzione di prova e i dispositivi che eseguono molte inizializzazioni hardware possono causare problemi di temporizzazione. Il modo migliore per identificare quando accade questo è raccogliere il tempo di ispezione per ogni driver e ordinarlo.

Per attivare il rilevamento asincrono di un modulo, non è sufficiente impostare solo il flag PROBE_PREFER_ASYNCHRONOUS nel codice del driver. Per i moduli, devi anche aggiungere module_name.async_probe=1 nella riga di comando del kernel o passare async_probe=1 come parametro del modulo quando carichi il modulo utilizzando modprobe o insmod.

L'attivazione del rilevamento asincrono può far risparmiare circa 100-500 ms sui tempi di avvio, a seconda dell'hardware/dei driver.

Esegui la sonda del driver CPUfreq il prima possibile

Prima viene eseguito il probe del driver CPUfreq, prima puoi scalare la frequenza della CPU al massimo (o a un valore massimo limitato termicamente) durante l'avvio. Quanto più rapida è la CPU, più veloce sarà l'avvio. Questa linea guida si applica anche ai devfreq driver che controllano la frequenza della DRAM, della memoria e dell'interconnessione.

Con i moduli, l'ordine di caricamento può dipendere dal livello initcall e dall'ordine di compilazione o collegamento dei driver. Utilizza un alias MODULE_SOFTDEP() per assicurarti che il driver cpufreq sia tra i primi moduli caricati.

Oltre a caricare il modulo in anticipo, devi anche assicurarti che siano state testate tutte le dipendenze per eseguire il probe del driver CPUfreq. Ad esempio, se hai bisogno di un handle di clock o regolatore per controllare la frequenza della CPU, assicurati che vengano prima sottoposti a sonda. In alternativa, potresti dover caricare i driver termici prima del driver CPUfreq se è possibile che le CPU si surriscaldino durante l'avvio. Pertanto, fai del tuo meglio per assicurarti che i driver CPUfreq e devfreq pertinenti eseguano la ricerca il prima possibile.

I risparmi derivanti dal test precoce del driver CPUfreq possono essere molto ridotti o molto elevati, a seconda di quanto presto puoi eseguire il test e a quale frequenza il bootloader lascia le CPU.

Sposta i moduli nella partizione di init di secondo livello, vendor o vendor_dlkm

Poiché il processo di inizializzazione della prima fase è serializzato, non ci sono molte opportunità per parallelizzare il processo di avvio. Se un modulo non è necessario per il completamento dell'inizializzazione di primo livello, spostalo nell'inizializzazione di secondo livello inserendolo nella partizione del fornitore o vendor_dlkm.

L'inizializzazione di primo livello non richiede il rilevamento di più dispositivi per passare all'inizializzazione di secondo livello. Per un normale flusso di avvio sono necessarie solo le funzionalità di archiviazione flash e della console.

Carica i seguenti driver essenziali:

  • watchdog
  • reset
  • cpufreq

Per la modalità di recupero e dello spazio utente fastbootd, l'inizializzazione di primo livello richiede più dispositivi da analizzare (ad esempio USB) e da visualizzare. Conserva una copia di questi moduli nel ramdisk di primo livello e nella partizione del fornitore o vendor_dlkm. In questo modo, possono essere caricati nell'inizializzazione della prima fase per il recupero o il flusso di avvio fastbootd. Tuttavia, non caricare i moduli della modalità di ripristino nell'inizializzazione di primo livello durante il normale flusso di avvio. I moduli della modalità di recupero possono essere posticipati all'inizializzazione di secondo livello per ridurre il tempo di avvio. Tutti gli altri moduli non necessari nell'inizializzazione della prima fase devono essere trasferiti alla partizione del fornitore o vendor_dlkm.

Dato un elenco di dispositivi principali (ad esempio UFS o seriale), lo script dev needs.sh trova tutti i driver, i dispositivi e i moduli necessari per le dipendenze o i fornitori (ad esempio orologi, regolatori o gpio) da sottoporre a sondaggi.

Il trasferimento dei moduli all'inizializzazione di secondo livello riduce i tempi di avvio nei seguenti modi:

  • Riduzione delle dimensioni del ramdisk.
    • In questo modo, le letture del flash sono più veloci quando il bootloader carica il ramdisk (passaggio di avvio serializzato).
    • Ciò consente velocità di decompressione più elevate quando il kernel decomprime il ramdisk (passaggio di avvio serializzato).
  • L'inizializzazione di secondo livello funziona in parallelo, il che nasconde il tempo di caricamento del modulo con il lavoro svolto nell'inizializzazione di secondo livello.

Il trasferimento dei moduli al secondo livello può far risparmiare 500-1000 ms sui tempi di avvio, a seconda del numero di moduli che riesci a spostare nell'inizializzazione del secondo livello.

Logistica del caricamento dei moduli

La build Android più recente include configurazioni della scheda che controllano quali moduli vengono copiati in ogni fase e quali vengono caricati. Questa sezione si concentra sul seguente sottoinsieme:

  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES. Questo elenco di moduli da copiare nella ramdisk.
  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD. Questo elenco di moduli da caricare nella prima fase di inizializzazione.
  • BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD. Questo elenco di moduli da caricare quando viene selezionato il recupero o fastbootd dalla ramdisk.
  • BOARD_VENDOR_KERNEL_MODULES. Questo elenco di moduli da copiare nella partizione del fornitore o vendor_dlkm nella directory /vendor/lib/modules/.
  • BOARD_VENDOR_KERNEL_MODULES_LOAD. Questo elenco di moduli da caricare nell'inizializzazione di secondo livello.

Anche i moduli di avvio e recupero in ramdisk devono essere copiati nella partizione del fornitore o vendor_dlkm in /vendor/lib/modules. La copia di questi moduli nella partizione del fornitore garantisce che i moduli non siano invisibili durante l'inizializzazione di secondo livello, il che è utile per il debug e la raccolta di modinfo per i report di bug.

La duplicazione dovrebbe occupare uno spazio minimo sul fornitore o sulla vendor_dlkm partizione se il set di moduli di avvio è ridotto al minimo. Assicurati che il file modules.list del fornitore contenga un elenco filtrato dei moduli in /vendor/lib/modules. L'elenco filtrato garantisce che i tempi di avvio non siano influenzati dal nuovo caricamento dei moduli (un processo costoso).

Assicurati che i moduli della modalità di recupero vengano caricati come gruppo. Il caricamento dei moduli della modalità di recupero può essere eseguito in modalità di recupero o all'inizio dell'inizializzazione della seconda fase in ogni flusso di avvio.

Puoi utilizzare i file Board.Config.mk del dispositivo per eseguire queste azioni, come mostrato nell'esempio seguente:

# 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)))

Questo esempio mostra un sottoinsieme di BOOT_KERNEL_MODULES e RECOVERY_KERNEL_MODULES più facile da gestire da specificare localmente nei file di configurazione della scheda. Lo script precedente trova e compila ciascuno dei moduli del sottoinsieme dai moduli del kernel disponibili selezionati, lasciando i moduli rimanenti per l'inizializzazione di secondo livello.

Per l'inizializzazione di secondo livello, consigliamo di eseguire il caricamento del modulo come servizio in modo che non blocchi il flusso di avvio. Utilizza uno script shell per gestire il caricamento del modulo in modo che altre operazioni, come la gestione e la mitigazione degli errori o il completamento del caricamento del modulo, possano essere segnalate (o ignorate) se necessario.

Puoi ignorare un errore di caricamento del modulo di debug che non è presente nelle build dell'utente. Per ignorare questo errore, imposta la proprietà vendor.device.modules.ready per attivare le fasi successive del flusso di avvio dello script init rc per continuare alla schermata di lancio. Fai riferimento allo script di esempio riportato di seguito se hai il seguente codice in /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

Nel file rc hardware, il servizio one shot può essere specificato con:

service insmod-sh /vendor/etc/init.insmod.sh /vendor/etc/init.insmod.<hw>.cfg
    class main
    user root
    group root system
    Disabled
    oneshot

È possibile apportare ulteriori ottimizzazioni dopo il passaggio dei moduli dalla prima alla seconda fase. Puoi utilizzare la funzionalità della lista bloccata di modprobe per suddividere il flusso di avvio di secondo livello in modo da includere il caricamento differito dei moduli non essenziali. Il caricamento dei moduli utilizzati esclusivamente da un HAL specifico può essere posticipato per caricare i moduli solo all'avvio dell'HAL.

Per migliorare i tempi di avvio apparenti, puoi scegliere nel servizio di caricamento dei moduli i moduli più adatti al caricamento dopo la schermata di lancio. Ad esempio, puoi caricare in modo esplicito i moduli per il decodificatore video o il Wi-Fi dopo aver completato il flusso di avvio iniziale (sys.boot_complete ad esempio l'indicatore della proprietà Android). Assicurati che gli HAL per i moduli di caricamento tardivo si blocchino per un tempo sufficientemente lungo quando i driver del kernel non sono presenti.

In alternativa, puoi utilizzare il comando wait<file>[<timeout>] di init nello scripting rc del flusso di avvio per attendere che le voci sysfs selezionate mostrino che i moduli del driver hanno completato le operazioni di ispezione. Un esempio è l'attesa del caricamento del driver dello schermo in background del recupero o di fastbootd prima di presentare la grafica del menu.

Inizializza la frequenza della CPU su un valore ragionevole nel bootloader

Non tutti i SoC/prodotti potrebbero essere in grado di avviare la CPU alla frequenza più elevata a causa di problemi termici o di alimentazione durante i test di loop di avvio. Tuttavia, assicurati che il bootloader imposti la frequenza di tutte le CPU online al massimo possibile in sicurezza per un SoC o un prodotto. Questo è molto importante perché, con un kernel completamente modulare, la decompressione del ramdisk di init avviene prima che il driver CPUfreq possa essere caricato. Pertanto, se il bootloader lascia la CPU all'estremità inferiore della sua frequenza, il tempo di decompressione del ramdisk può richiedere più tempo di un kernel compilato in modo statico (dopo l'aggiustamento per la differenza di dimensioni del ramdisk) perché la frequenza della CPU sarebbe molto bassa durante l'esecuzione di attività che richiedono un'elevata intensità di CPU (decompressione). Lo stesso vale per la memoria e la frequenza di interconnessione.

Inizializza la frequenza della CPU delle CPU grandi nel bootloader

Prima del caricamento del driver CPUfreq, il kernel non è a conoscenza delle frequenza della CPU e non scala la capacità di pianificazione della CPU in base alla frequenza corrente. Il kernel potrebbe eseguire la migrazione dei thread alla CPU grande se il carico è sufficientemente elevato sulla CPU piccola.

Assicurati che le CPU grandi abbiano prestazioni almeno pari a quelle delle CPU piccole per la frequenza con cui il bootloader le lascia. Ad esempio, se la CPU grande è due volte più performante della CPU piccola alla stessa frequenza, ma il bootloader imposta la frequenza della CPU piccola su 1,5 GHz e la frequenza della CPU grande su 300 MHz, le prestazioni di avvio diminuiranno se il kernel sposta un thread sulla CPU grande. In questo esempio, se è sicuro avviare la CPU grande a 750 MHz, devi farlo anche se non prevedi di utilizzarla esplicitamente.

I driver non devono caricare il firmware nell'inizializzazione della prima fase

Potrebbero verificarsi casi inevitabili in cui il firmware debba essere caricato nell'inizializzazione della prima fase. In generale, però, i driver non devono caricare alcun firmware nella prima fase di inizializzazione, in particolare nel contesto della ricerca del dispositivo. Il caricamento del firmware nell'inizializzazione della prima fase provoca l'interruzione dell'intero processo di avvio se il firmware non è disponibile nel ramdisk della prima fase. Anche se il firmware è presente nella prima fase della RAM disk, causa comunque un ritardo non necessario.