Ottimizzazione del tempo di avvio

In questa pagina sono riportati suggerimenti per migliorare il tempo di avvio.

Rimuovi i simboli di debug dai moduli

Analogamente alla rimozione dei simboli di debug dal kernel su un dispositivo di produzione, assicurati di rimuovere i simboli di debug anche dai moduli. La rimozione dei simboli di debug dai moduli contribuisce a ridurre il tempo di avvio riducendo:

  • Il tempo necessario per leggere i file binari dalla memoria flash.
  • Il tempo necessario per decomprimere il ramdisk.
  • Il tempo necessario per caricare i moduli.

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

La rimozione dei simboli è attivata per impostazione predefinita nella build della piattaforma Android, ma per attivarli in modo esplicito, imposta BOARD_DO_NOT_STRIP_VENDOR_RAMDISK_MODULES nella configurazione specifica del dispositivo in device/vendor/device.

Utilizza la compressione LZ4 per kernel e ramdisk

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

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

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

Evita la registrazione eccessiva nei driver

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

Il kernel Linux esegue alcune ottimizzazioni delle dimensioni della memoria (ad esempio l'ottimizzazione dei successi della cache) quando alloca la 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 questi tipi è utile per ridurre il tempo di caricamento del modulo.

Un pattern di codifica comune che aumenta il numero di RELA R_AARCH64_CALL26 o R_AARCH64_JUMP26 è la registrazione eccessiva in un driver. Ogni chiamata a printk() o a qualsiasi altro schema di registrazione in genere aggiunge una voce RELA CALL26/JUMP26. Nel testo del commit nel commit upstream, nota che anche con l'ottimizzazione, il caricamento dei sei moduli richiede circa 250 ms, perché questi sei moduli erano i sei moduli principali con la quantità maggiore di registrazione.

La riduzione della registrazione può far risparmiare circa 100-300 ms sui tempi di avvio a seconda di quanto sia eccessiva la registrazione esistente.

Attiva la ricerca asincrona in modo selettivo

Quando viene caricato un modulo, se il dispositivo che supporta è già stato inserito da DT (devicetree) e aggiunto al driver core, la ricerca del dispositivo viene eseguita nel contesto della chiamata module_init(). Quando la ricerca di un dispositivo viene eseguita nel contesto di module_init(), il modulo non può completare il caricamento fino al completamento della ricerca. Poiché il caricamento dei moduli è per lo più serializzato, un dispositivo che richiede un tempo di ricerca relativamente lungo rallenta il tempo di avvio.

Per evitare tempi di avvio più lenti, attiva la ricerca asincrona per i moduli che richiedono un po' di tempo per la ricerca dei dispositivi. L'attivazione della ricerca asincrona per tutti i moduli potrebbe non essere vantaggiosa, poiché il tempo necessario per creare un thread e avviare la ricerca potrebbe essere pari al tempo necessario per la ricerca del dispositivo.

I dispositivi collegati tramite un bus lento come I2C, i dispositivi che eseguono il caricamento del firmware nella funzione di ricerca e i dispositivi che eseguono una grande quantità di inizializzazione hardware possono causare il problema di temporizzazione. Il modo migliore per identificare quando si verifica questo problema è raccogliere il tempo di ricerca per ogni driver e ordinarlo.

Per attivare la ricerca asincrona per 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 della ricerca asincrona può far risparmiare circa 100-500 ms sui tempi di avvio a seconda dell'hardware/dei driver.

Esegui la ricerca del driver CPUfreq il prima possibile

Prima esegui la ricerca del driver CPUfreq, prima puoi scalare la frequenza della CPU al massimo (o a un massimo limitato termicamente) durante l'avvio. Più veloce è la CPU, più veloce è l'avvio. Questa linea guida si applica anche ai driver devfreq che controllano la frequenza di DRAM, memoria e 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 da caricare.

Oltre a caricare il modulo in anticipo, devi anche assicurarti che siano state eseguite le ricerche di tutte le dipendenze per la ricerca del driver CPUfreq. Ad esempio, se hai bisogno di un handle di clock o di regolatore per controllare la frequenza della CPU, assicurati che le ricerche vengano eseguite per prime. In alternativa, potresti aver bisogno che i driver termici vengano caricati prima del driver CPUfreq se è possibile che le CPU si surriscaldino troppo durante l'avvio. Quindi, fai il possibile per assicurarti che i driver CPUfreq e devfreq pertinenti vengano cercati il prima possibile.

Il risparmio derivante dalla ricerca anticipata del driver CPUfreq può variare da molto piccolo a molto grande a seconda di quanto presto puoi eseguire la ricerca e della frequenza con cui il bootloader lascia le CPU.

Sposta i moduli nell'inizializzazione di seconda fase, nella partizione vendor o vendor_dlkm

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

L'inizializzazione di prima fase non richiede la ricerca di diversi dispositivi per passare all'inizializzazione di seconda fase. Per un flusso di avvio normale sono necessarie solo le funzionalità di console e di archiviazione flash.

Carica i seguenti driver essenziali:

  • watchdog
  • reset
  • cpufreq

Per la modalità di ripristino e spazio utente fastbootd, l'inizializzazione di prima fase richiede la ricerca di più dispositivi (ad esempio USB) e la visualizzazione. Conserva una copia di questi moduli nel ramdisk di prima fase e nella partizione vendor o vendor_dlkm. In questo modo, possono essere caricati nell'inizializzazione di prima fase per il flusso di avvio di ripristino o fastbootd. Tuttavia, non caricare i moduli della modalità di ripristino nell'inizializzazione di prima fase durante il normale flusso di avvio. I moduli della modalità Recovery [Recovery mode] possono essere rimandati all'inizializzazione di seconda fase per ridurre il tempo di avvio. Tutti gli altri moduli non necessari nell'inizializzazione di prima fase devono essere spostati nella partizione vendor o vendor_dlkm.

Dato un elenco di dispositivi foglia (ad esempio UFS o seriale), dev needs.sh lo script trova tutti i driver, i dispositivi e i moduli necessari per le dipendenze o i fornitori (ad esempio, clock, regolatori o gpio) per la ricerca.

Lo spostamento dei moduli nell'inizializzazione di seconda fase riduce i tempi di avvio nei seguenti modi:

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

Lo spostamento dei moduli nella seconda fase può far risparmiare 500-1000 ms sui tempi di avvio a seconda del numero di moduli che puoi spostare nell'inizializzazione di seconda fase.

Logistica di caricamento dei moduli

L'ultima build di Android include configurazioni della scheda che controllano quali moduli vengono copiati in ogni fase e quali moduli vengono caricati. Questa sezione si concentra sul seguente sottoinsieme:

  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES. Questo elenco di moduli da copiare nel ramdisk.
  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD. Questo elenco di moduli da caricare nell'inizializzazione di prima fase.
  • BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD. Questo elenco di moduli da caricare quando viene selezionato il ripristino o fastbootd dal ramdisk.
  • BOARD_VENDOR_KERNEL_MODULES. Questo elenco di moduli da copiare nella partizione vendor o vendor_dlkm nella directory /vendor/lib/modules/.
  • BOARD_VENDOR_KERNEL_MODULES_LOAD. Questo elenco di moduli da caricare nell'inizializzazione di seconda fase.

I moduli di avvio e ripristino nel ramdisk devono essere copiati anche nella partizione vendor o vendor_dlkm in /vendor/lib/modules. La copia di questi moduli nella partizione vendor garantisce che i moduli non siano invisibili durante l'inizializzazione di seconda fase, il che è utile per il debug e la raccolta di modinfo per i report sui bug.

La duplicazione dovrebbe occupare uno spazio minimo nella partizione vendor o vendor_dlkm, a condizione che il set di moduli di avvio sia ridotto al minimo. Assicurati che il file modules.list del fornitore contenga un elenco filtrato di moduli in /vendor/lib/modules. L'elenco filtrato garantisce che i tempi di avvio non siano influenzati dal ricaricamento dei moduli (un processo costoso).

Assicurati che i moduli della modalità di ripristino vengano caricati come gruppo. Il caricamento dei moduli della modalità di ripristino può essere eseguito in modalità di ripristino o all'inizio dell'inizializzazione di 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 più facile da gestire di BOOT_KERNEL_MODULES e RECOVERY_KERNEL_MODULES da specificare localmente nei file di configurazione della scheda. Lo script precedente trova e riempie ciascuno dei moduli del sottoinsieme dai moduli del kernel disponibili selezionati, lasciando i moduli rimanenti per l'inizializzazione di seconda fase.

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

Puoi ignorare un errore di caricamento del modulo di debug non presente nelle build 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 avvio. Fai riferimento al seguente script di esempio, 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 potrebbe 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 lo spostamento dei moduli dalla prima alla seconda fase. Puoi utilizzare la funzionalità di blocco di modprobe per suddividere il flusso di avvio di seconda fase in modo da includere il caricamento differito dei moduli non essenziali. Il caricamento dei moduli utilizzati esclusivamente da un HAL specifico può essere rimandato al caricamento dei moduli solo all'avvio dell'HAL.

Per migliorare i tempi di avvio apparenti, puoi scegliere in modo specifico i moduli nel servizio di caricamento dei moduli che sono più adatti al caricamento dopo la schermata di avvio. Ad esempio, puoi caricare in modo esplicito i moduli per il decodificatore video o il Wi-Fi dopo che il flusso di avvio init è stato cancellato (ad esempio, il segnale della proprietà Android sys.boot_complete). Assicurati che gli HAL per i moduli di caricamento tardivo si blocchino abbastanza a lungo quando i driver del kernel non sono presenti.

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

Inizializza la frequenza della CPU a un valore ragionevole nel bootloader

Non tutti i SoC/prodotti potrebbero essere in grado di avviare la CPU alla frequenza più alta a causa di problemi termici o di alimentazione durante i test del loop di avvio. Tuttavia, assicurati che il bootloader imposti la frequenza di tutte le CPU online al valore più alto possibile in sicurezza per un SoC o un prodotto. Questo è molto importante perché, con un kernel completamente modulare, la decompressione del ramdisk 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 rispetto a un kernel compilato staticamente (dopo aver regolato la differenza di dimensioni del ramdisk) perché la frequenza della CPU sarebbe molto bassa durante l'esecuzione di attività che richiedono un uso intensivo della CPU (decompressione). Lo stesso vale per la frequenza di memoria e interconnessione.

Inizializza la frequenza della CPU delle CPU di grandi dimensioni nel bootloader

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

Assicurati che le CPU di grandi dimensioni siano almeno altrettanto performanti delle CPU di piccole dimensioni per la frequenza con cui il bootloader le lascia. Ad esempio, se la CPU di grandi dimensioni è due volte più performante della CPU di piccole dimensioni per la stessa frequenza, ma il bootloader imposta la frequenza della CPU di piccole dimensioni su 1,5 GHz e la frequenza della CPU di grandi dimensioni su 300 MHz, le prestazioni di avvio diminuiranno se il kernel sposta un thread sulla CPU di grandi dimensioni. In questo esempio, se è sicuro avviare la CPU di grandi dimensioni a 750 MHz, dovresti farlo anche se non prevedi di utilizzarla in modo esplicito.

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

Potrebbero esserci alcuni casi inevitabili in cui il firmware deve essere caricato nell'inizializzazione di prima fase. Tuttavia, in generale, i driver non devono caricare alcun firmware nell'inizializzazione di prima fase, soprattutto nel contesto della ricerca dei dispositivi. Il caricamento del firmware nell'inizializzazione di prima fase causa il blocco dell'intero processo di avvio se il firmware non è disponibile nel ramdisk di prima fase. Anche se il firmware è presente nel ramdisk di prima fase, causa comunque un ritardo non necessario.