Bootzeit optimieren

Auf dieser Seite finden Sie Tipps zur Verbesserung der Bootzeit.

Symbole zum Debuggen aus Modulen entfernen

Ähnlich wie bei Produktionsgeräten, bei denen Debugging-Symbole aus dem Kernel entfernt werden, sollten Sie auch Debugging-Symbole aus Modulen entfernen. Durch das Entfernen von Debug-Symbolen aus Modulen wird die Bootzeit verkürzt, da Folgendes reduziert wird:

  • Die Zeit, die zum Lesen der Binärdateien aus dem Flash-Speicher benötigt wird.
  • Die Zeit, die zum Dekomprimieren der Ramdisk benötigt wird.
  • Die Zeit, die zum Laden der Module benötigt wird.

Durch das Entfernen von Debug-Symbolen aus Modulen können beim Booten mehrere Sekunden eingespart werden.

Das Entfernen von Symbolen ist standardmäßig im Android-Plattform-Build aktiviert. Wenn Sie es explizit aktivieren möchten, legen Sie BOARD_DO_NOT_STRIP_VENDOR_RAMDISK_MODULES in Ihrer gerätespezifischen Konfiguration unter device/vendor/device fest.

LZ4-Komprimierung für Kernel und Ramdisk verwenden

Gzip erzeugt eine kleinere komprimierte Ausgabe als LZ4, aber LZ4 wird schneller dekomprimiert als Gzip. Bei Kernel und Modulen ist die absolute Speicherplatzreduzierung durch die Verwendung von Gzip im Vergleich zum Vorteil der Dekomprimierungszeit von LZ4 nicht so signifikant.

Die Android-Plattform unterstützt jetzt die LZ4-Ramdisk-Komprimierung über BOARD_RAMDISK_USE_LZ4. Sie können diese Option in der gerätespezifischen Konfiguration festlegen. Die Kernel-Komprimierung kann über die Kernel-Defconfig festgelegt werden.

Durch die Umstellung auf LZ4 sollte die Startzeit um 500 bis 1.000 ms verkürzt werden.

Übermäßige Protokollierung in Treibern vermeiden

In ARM64 und ARM32 benötigen Funktionsaufrufe, die weiter als eine bestimmte Entfernung vom Aufrufpunkt entfernt sind, eine Sprungtabelle (auch als „procedure linking table“ oder PLT bezeichnet), um die vollständige Sprungadresse codieren zu können. Da Module dynamisch geladen werden, müssen diese Sprungtabellen beim Laden des Moduls korrigiert werden. Die Aufrufe, die eine Verschiebung erfordern, werden als Verschiebungseinträge mit expliziten Addenden (oder kurz RELA) im ELF-Format bezeichnet.

Der Linux-Kernel führt bei der Zuweisung der PLT einige Optimierungen der Speichergröße durch, z. B. die Optimierung von Cache-Treffern. Mit diesem Upstream-Commit hat das Optimierungsschema eine Komplexität von O(N^2), wobei N die Anzahl der RELAs vom Typ R_AARCH64_JUMP26 oder R_AARCH64_CALL26 ist. Weniger RELAs dieser Art können also dazu beitragen, die Ladezeit des Moduls zu verkürzen.

Ein häufiges Codierungsmuster, das die Anzahl der R_AARCH64_CALL26- oder R_AARCH64_JUMP26-RELAs erhöht, ist die übermäßige Protokollierung in einem Treiber. Bei jedem Aufruf von printk() oder einem anderen Logging-Schema wird in der Regel ein CALL26/JUMP26-RELA-Eintrag hinzugefügt. Im Commit-Text im Upstream-Commit sehen Sie, dass das Laden der sechs Module trotz der Optimierung etwa 250 ms dauert. Das liegt daran, dass diese sechs Module die sechs Module mit der größten Menge an Logging waren.

Durch weniger Logging können Sie die Bootzeiten um etwa 100 bis 300 ms verkürzen, je nachdem, wie umfangreich das vorhandene Logging ist.

Asynchrones Probing selektiv aktivieren

Wenn ein Modul geladen wird und das unterstützte Gerät bereits aus dem Gerätebaum (Device Tree, DT) geladen und dem Treiberkern hinzugefügt wurde, erfolgt der Geräte-Probe im Kontext des module_init()-Aufrufs. Wenn eine Geräteuntersuchung im Kontext von module_init() durchgeführt wird, kann das Modul erst geladen werden, wenn die Untersuchung abgeschlossen ist. Da das Laden von Modulen größtenteils seriell erfolgt, verlangsamt ein Gerät, das relativ lange zum Testen benötigt, die Startzeit.

Um langsamere Startzeiten zu vermeiden, aktivieren Sie das asynchrone Abfragen für Module, bei denen es eine Weile dauert, bis die Geräte abgefragt werden. Die asynchrone Prüfung für alle Module zu aktivieren, ist möglicherweise nicht sinnvoll, da die Zeit, die zum Forken eines Threads und Starten der Prüfung benötigt wird, genauso lang sein kann wie die Zeit, die zum Prüfen des Geräts benötigt wird.

Geräte, die über einen langsamen Bus wie I2C verbunden sind, Geräte, die Firmware in ihrer Probe-Funktion laden, und Geräte, die eine umfangreiche Hardwareinitialisierung durchführen, können zu dem Timing-Problem führen. Am besten lässt sich das feststellen, indem Sie die Probezeit für jeden Treiber erfassen und sortieren.

Wenn Sie das asynchrone Erfassen für ein Modul aktivieren möchten, reicht es nicht aus, nur das Flag PROBE_PREFER_ASYNCHRONOUS im Treibercode festzulegen. Bei Modulen müssen Sie außerdem module_name.async_probe=1 in der Kernel-Befehlszeile hinzufügen oder async_probe=1 als Modulparameter übergeben, wenn Sie das Modul mit modprobe oder insmod laden.

Durch die Aktivierung des asynchronen Sondierens können Sie je nach Hardware/Treibern etwa 100 bis 500 ms bei der Startzeit einsparen.

CPUfreq-Treiber so früh wie möglich testen

Je früher Ihr CPUfreq-Treiber prüft, desto schneller können Sie die CPU-Frequenz während des Bootvorgangs auf das Maximum (oder ein thermisch bedingtes Maximum) skalieren. Je schneller die CPU, desto schneller der Start. Diese Richtlinie gilt auch für devfreq-Treiber, die die DRAM-, Speicher- und Interconnect-Frequenz steuern.

Bei Modulen kann die Ladeanordnung von der initcall-Ebene und der Kompilierungs- oder Verknüpfungsreihenfolge der Treiber abhängen. Verwenden Sie einen Alias MODULE_SOFTDEP(), um sicherzustellen, dass der cpufreq-Treiber zu den ersten Modulen gehört, die geladen werden.

Neben dem frühen Laden des Moduls müssen Sie auch dafür sorgen, dass alle Abhängigkeiten zum Testen des CPUfreq-Treibers ebenfalls getestet wurden. Wenn Sie beispielsweise eine Takt- oder Reglersteuerung benötigen, um die CPU-Frequenz zu steuern, muss diese zuerst untersucht werden. Möglicherweise müssen thermische Treiber vor dem CPUfreq-Treiber geladen werden, wenn die CPUs während des Bootvorgangs zu heiß werden können. Sorgen Sie also dafür, dass die CPUfreq- und relevanten Devfreq-Treiber so früh wie möglich geprüft werden.

Die Einsparungen durch das frühe Abrufen des CPUfreq-Treibers können sehr gering bis sehr hoch sein, je nachdem, wie früh Sie diese abrufen können und mit welcher Frequenz der Bootloader die CPUs verlässt.

Module in die zweite Phase der Initialisierung, die Anbieter- oder die vendor_dlkm-Partition verschieben

Da der Initialisierungsprozess der ersten Phase serialisiert ist, gibt es nicht viele Möglichkeiten, den Bootvorgang zu parallelisieren. Wenn ein Modul nicht für den Abschluss der Initialisierung der ersten Phase erforderlich ist, verschieben Sie es in die Initialisierung der zweiten Phase, indem Sie es in die Anbieter- oder vendor_dlkm-Partition einfügen.

Für die Initialisierung der ersten Phase ist es nicht erforderlich, mehrere Geräte zu testen, um zur Initialisierung der zweiten Phase zu gelangen. Für einen normalen Bootvorgang sind nur die Konsolen- und Flash-Speicherfunktionen erforderlich.

Laden Sie die folgenden wichtigen Treiber:

  • watchdog
  • reset
  • cpufreq

Für den Wiederherstellungs- und den fastbootd-Modus im Nutzerbereich muss die erste Phase der Initialisierung mehr Geräte untersuchen (z. B. USB) und das Display initialisieren. Bewahren Sie eine Kopie dieser Module in der Ramdisk der ersten Phase und auf der Vendor- oder vendor_dlkm-Partition auf. So können sie in der ersten Phase der Initialisierung für die Wiederherstellung oder den fastbootd-Boot-Vorgang geladen werden. Die Wiederherstellungsmodusmodule dürfen jedoch während des normalen Startvorgangs nicht in der Initialisierung der ersten Phase geladen werden. Module im Wiederherstellungsmodus können auf die zweite Initialisierungsphase verschoben werden, um die Bootzeit zu verkürzen. Alle anderen Module, die für die Initialisierung der ersten Phase nicht benötigt werden, sollten in die Partition „vendor“ oder vendor_dlkm verschoben werden.

Anhand einer Liste von Leaf-Geräten (z. B. UFS oder seriell) findet das dev needs.sh-Skript alle Treiber, Geräte und Module, die für Abhängigkeiten oder Lieferanten (z. B. Taktgeber, Regler oder gpio) zum Testen erforderlich sind.

Durch das Verschieben von Modulen in die zweite Initialisierungsphase werden die Bootzeiten auf folgende Weise verkürzt:

  • Reduzierung der Ramdisk-Größe.
    • Dadurch werden Flash-Lesevorgänge schneller, wenn der Bootloader die Ramdisk lädt (serialisierter Bootvorgang).
    • Dadurch wird die Dekomprimierung beschleunigt, wenn der Kernel die Ramdisk dekomprimiert (serialisierter Bootvorgang).
  • Die Initialisierung der zweiten Phase erfolgt parallel, wodurch die Ladezeit des Moduls durch die Arbeit in der Initialisierung der zweiten Phase verdeckt wird.

Durch das Verschieben von Modulen in die zweite Phase können 500 bis 1.000 ms bei der Bootzeit eingespart werden, je nachdem, wie viele Module Sie in die zweite Phase verschieben können.

Logistik für das Laden von Modulen

Der aktuelle Android-Build enthält Board-Konfigurationen, mit denen gesteuert wird, welche Module in die einzelnen Phasen kopiert und welche Module geladen werden. In diesem Abschnitt geht es um die folgende Teilmenge:

  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES: Diese Liste enthält die Module, die in die Ramdisk kopiert werden sollen.
  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD: Diese Liste enthält die Module, die in der ersten Phase der Initialisierung geladen werden sollen.
  • BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD: Diese Liste der Module wird geladen, wenn die Wiederherstellung oder fastbootd aus der RAM-Disk ausgewählt wird.
  • BOARD_VENDOR_KERNEL_MODULES. Diese Liste der Module wird in das Verzeichnis /vendor/lib/modules/ auf der Partition „vendor“ oder vendor_dlkm kopiert.
  • BOARD_VENDOR_KERNEL_MODULES_LOAD. Diese Liste enthält die Module, die in der zweiten Phase der Initialisierung geladen werden sollen.

Die Boot- und Recovery-Module in der Ramdisk müssen auch in die Anbieter- oder vendor_dlkm-Partition unter /vendor/lib/modules kopiert werden. Durch das Kopieren dieser Module in die Anbieterpartition wird sichergestellt, dass die Module während der Initialisierung in der zweiten Phase nicht unsichtbar sind. Das ist nützlich für die Fehlerbehebung und zum Erfassen von modinfo für Fehlerberichte.

Die Duplizierung sollte nur wenig Speicherplatz auf der Anbieter- oder vendor_dlkm-Partition belegen, sofern das Bootmodul-Set minimiert ist. Achten Sie darauf, dass die modules.list-Datei des Anbieters eine gefilterte Liste von Modulen in /vendor/lib/modules enthält. Die gefilterte Liste sorgt dafür, dass die Bootzeiten nicht durch das erneute Laden der Module beeinträchtigt werden, was ein aufwendiger Prozess ist.

Achten Sie darauf, dass die Module im Wiederherstellungsmodus als Gruppe geladen werden. Das Laden von Wiederherstellungsmodusmodulen kann entweder im Wiederherstellungsmodus oder zu Beginn der zweiten Phase der Initialisierung in jedem Bootvorgang erfolgen.

Sie können die Geräte-Board.Config.mk-Dateien verwenden, um diese Aktionen auszuführen, wie im folgenden Beispiel gezeigt:

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

Dieses Beispiel zeigt eine einfacher zu verwaltende Teilmenge von BOOT_KERNEL_MODULES und RECOVERY_KERNEL_MODULES, die lokal in den Konfigurationsdateien des Boards angegeben werden. Das vorherige Skript sucht und füllt jedes der Teilmengenmodule aus den ausgewählten verfügbaren Kernelmodulen. Die verbleibenden Module werden für die zweite Phase der Initialisierung verwendet.

Für die Initialisierung in der zweiten Phase empfehlen wir, das Laden des Moduls als Dienst auszuführen, damit der Bootvorgang nicht blockiert wird. Verwenden Sie ein Shell-Script, um das Laden des Moduls zu verwalten, damit andere Logistik, z. B. Fehlerbehandlung und ‑behebung oder das Abschließen des Modulladens, bei Bedarf gemeldet (oder ignoriert) werden kann.

Sie können einen Fehler beim Laden eines Debug-Moduls ignorieren, der in Nutzer-Builds nicht auftritt. Wenn Sie diesen Fehler ignorieren möchten, legen Sie die vendor.device.modules.ready-Eigenschaft so fest, dass spätere Phasen des init rc-Scripting-Bootflows bis zum Startbildschirm fortgesetzt werden. Wenn Sie den folgenden Code in /vendor/etc/init.insmod.sh haben, können Sie sich das folgende Beispielskript ansehen:

#!/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

In der Hardware-RC-Datei kann der Dienst one shot so angegeben werden:

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

Zusätzliche Optimierungen können vorgenommen werden, nachdem Module von der ersten in die zweite Phase verschoben wurden. Mit der Funktion „modprobe blocklist“ können Sie den Bootvorgang der zweiten Phase aufteilen, um das verzögerte Laden von nicht essenziellen Modulen zu ermöglichen. Das Laden von Modulen, die ausschließlich von einem bestimmten HAL verwendet werden, kann verzögert werden, sodass die Module erst geladen werden, wenn das HAL gestartet wird.

Um die scheinbare Bootzeit zu verkürzen, können Sie im Modul-Ladedienst speziell Module auswählen, die sich besser für das Laden nach dem Startbildschirm eignen. Sie können beispielsweise die Module für Videodecoder oder WLAN explizit erst nach dem Abschluss des Bootvorgangs laden (z. B. über das sys.boot_complete-Android-Property-Signal). Achte darauf, dass die HALs für die spät geladenen Module lange genug blockieren, wenn die Kerneltreiber nicht vorhanden sind.

Alternativ können Sie den Befehl wait<file>[<timeout>] von init im Bootflow-RC-Scripting verwenden, um auf ausgewählte sysfs-Einträge zu warten, die anzeigen, dass die Sondierungsvorgänge der Treibermodule abgeschlossen sind. Ein Beispiel hierfür ist das Warten auf das vollständige Laden des Displaytreibers im Hintergrund der Wiederherstellung oder fastbootd, bevor Menügrafiken angezeigt werden.

CPU-Frequenz im Bootloader auf einen angemessenen Wert initialisieren

Nicht alle SoCs/Produkte können die CPU mit der höchsten Frequenz starten, da bei Bootloop-Tests thermische oder Stromversorgungsprobleme auftreten können. Der Bootloader muss jedoch die Frequenz aller Online-CPUs so hoch wie möglich für ein SoC oder Produkt festlegen. Das ist sehr wichtig, da bei einem vollständig modularen Kernel die Dekomprimierung der Init-Ramdisk erfolgt, bevor der CPUfreq-Treiber geladen werden kann. Wenn die CPU also vom Bootloader am unteren Ende ihrer Frequenz belassen wird, kann die Dekomprimierungszeit der Ramdisk länger dauern als bei einem statisch kompilierten Kernel (nach Anpassung an den Unterschied in der Ramdisk-Größe), da die CPU-Frequenz bei CPU-intensiven Arbeiten (Dekomprimierung) sehr niedrig wäre. Dasselbe gilt für den Arbeitsspeicher und die Interconnect-Frequenz.

CPU-Frequenz von Big-CPUs im Bootloader initialisieren

Bevor der CPUfreq-Treiber geladen wird, kennt der Kernel die CPU-Frequenzen nicht und skaliert die CPU-Planungskapazität nicht für die aktuelle Frequenz. Der Kernel migriert Threads möglicherweise zur großen CPU, wenn die Last auf der kleinen CPU hoch genug ist.

Die großen CPUs müssen bei der Frequenz, mit der der Bootloader sie verlässt, mindestens so leistungsstark sein wie die kleinen CPUs. Wenn die große CPU beispielsweise bei gleicher Frequenz doppelt so leistungsstark ist wie die kleine CPU, der Bootloader die Frequenz der kleinen CPU aber auf 1,5 GHz und die Frequenz der großen CPU auf 300 MHz festlegt, sinkt die Bootleistung, wenn der Kernel einen Thread auf die große CPU verschiebt. Wenn es in diesem Beispiel sicher ist, die große CPU mit 750 MHz zu starten, sollten Sie dies tun, auch wenn Sie nicht planen, sie explizit zu verwenden.

Treiber sollten in der ersten Phase der Initialisierung keine Firmware laden

Es kann unvermeidliche Fälle geben, in denen die Firmware in der ersten Phase der Initialisierung geladen werden muss. Im Allgemeinen sollten Treiber in der ersten Phase der Initialisierung keine Firmware laden, insbesondere nicht im Kontext der Geräteerkennung. Wenn die Firmware in der ersten Phase der Initialisierung geladen wird, wird der gesamte Bootvorgang unterbrochen, wenn die Firmware nicht in der Ramdisk der ersten Phase verfügbar ist. Auch wenn die Firmware in der Ramdisk der ersten Phase vorhanden ist, führt dies zu einer unnötigen Verzögerung.