Optymalizacja czasu uruchamiania

Na tej stronie znajdziesz wskazówki dotyczące skracania czasu uruchamiania.

Usuwanie symboli debugowania z modułów

Podobnie jak symbole debugowania są usuwane z jądra na urządzeniu produkcyjnym, musisz też usunąć symbole debugowania z modułów. Usuwanie symboli debugowania z modułów skraca czas uruchamiania, ponieważ zmniejsza:

  • Czas potrzebny na odczytanie binarnych danych z pamięci flash.
  • Czas potrzebny na rozpakowanie pliku ramdisk.
  • Czas potrzebny na załadowanie modułów.

Usunięcie symboli debugowania z modułów może oszczędzić kilka sekund podczas uruchamiania.

Usuwanie symboli jest domyślnie włączone w kompilacji platformy Android, ale aby włączyć je wyraźnie, ustaw parametr BOARD_DO_NOT_STRIP_VENDOR_RAMDISK_MODULES w konfiguracji dotyczącej konkretnego urządzenia w folderze device/vendor/device.

Używanie kompresji LZ4 dla jądra i pliku ramdisk

Gzip generuje mniejszy wynik kompresji niż LZ4, ale LZ4 dekompresuje się szybciej niż Gzip. W przypadku jądra i modułów absolutne zmniejszenie rozmiaru pliku dzięki użyciu Gzip nie jest aż tak znaczące w porównaniu z korzyścią wynikającą z krótszego czasu dekompresji w przypadku LZ4.

Dodano obsługę kompresji LZ4 na dysku RAM w ramach kompilacji na platformę Android za pomocą BOARD_RAMDISK_USE_LZ4. Możesz ustawić tę opcję w konfiguracji konkretnego urządzenia. Kompresję jądra można ustawić za pomocą defconfig jądra.

Przejście na LZ4 powinno skrócić czas uruchamiania o 500–1000 ms.

Unikaj nadmiernego rejestrowania w sterownikach

W ARM64 i ARM32 wywołania funkcji, które znajdują się w większej odległości od miejsca wywołania, wymagają tabeli skoków (tzw. tabeli łączenia procedur, PLT), aby można było zakodować pełny adres skoku. Ponieważ moduły są ładowane dynamicznie, te tabele przekierowań muszą być naprawiane podczas wczytywania modułu. Wywołania, które wymagają przeniesienia, to wpisy przeniesienia z wyraźnymi dodatkami (lub w skrócie RELA) w formacie ELF.

Podczas przydzielania PLT jądro Linuxa wykonuje optymalizację rozmiaru pamięci (np. optymalizację trafień do pamięci podręcznej). W ramach tego commita upstream schemat optymalizacji ma złożoność O(N^2), gdzie N to liczba elementów RELA typu R_AARCH64_JUMP26 lub R_AARCH64_CALL26. Dlatego zmniejszenie liczby takich RELA pomoże skrócić czas wczytywania modułu.

Jednym z częstych wzorców kodowania, który zwiększa liczbę R_AARCH64_CALL26 lub R_AARCH64_JUMP26 RELA, jest nadmierne rejestrowanie w sterowniku. Każde wywołanie funkcji printk() lub dowolnego innego schematu rejestrowania zwykle powoduje dodanie wpisu CALL26/JUMP26 RELA. W tekście zatwierdzania w poprzednim commit-cie widać, że nawet po optymalizacji wczytanie tych 6 modułów zajmuje około 250 ms. Dzieje się tak, ponieważ były to 6 modułów z największą ilością logów.

Zmniejszenie ilości logowania może pozwolić zaoszczędzić 100–300 ms czasu uruchamiania w zależności od tego, jak nadmierne jest obecne logowanie.

Włączanie selektywnego sondowania asynchronicznego

Gdy wczytany moduł obsługuje urządzenie, które zostało już wypełnione z poziomu DT (drzewo urządzenia) i dodane do rdzenia sterownika, wówczas badanie urządzenia jest wykonywane w kontekście wywołania module_init(). Gdy w kontekście module_init() przeprowadzana jest analiza urządzenia, moduł nie może zakończyć wczytywania, dopóki nie zostanie ukończona analiza. Ponieważ wczytywanie modułów jest w większości serializowane, urządzenie, które potrzebuje stosunkowo dużo czasu na przeprowadzenie sondowania, wydłuża czas uruchamiania.

Aby uniknąć wydłużonego czasu uruchamiania, włącz asynchroniczne sprawdzanie w przypadku modułów, które potrzebują więcej czasu na sprawdzenie urządzeń. Włączenie asynchronicznego sondowania dla wszystkich modułów może nie być korzystne, ponieważ czas potrzebny na rozdzielenie wątku i uruchomienie sondowania może być taki sam jak czas potrzebny na sondowanie urządzenia.

Urządzenia połączone przez wolny magistrali, np. I2C, urządzenia, które ładują oprogramowanie sprzętowe w ramach funkcji sondowania, oraz urządzenia, które wykonują wiele operacji inicjalizacji sprzętowej, mogą powodować problemy z synchronizacją. Najlepszym sposobem na określenie, kiedy to nastąpi, jest zebranie czasu próbkowania dla każdego sterownika i posortowanie go.

Aby włączyć asynchroniczne sprawdzanie modułu, nie wystarczy ustawić tylko flagę PROBE_PREFER_ASYNCHRONOUS w kodzie sterownika. W przypadku modułów musisz też dodać module_name.async_probe=1 do wiersza poleceń jądra lub przekazać async_probe=1 jako parametr modułu podczas wczytywania modułu za pomocą modprobe lub insmod.

Włączenie asynchronicznego sondowania może pozwolić zaoszczędzić 100–500 ms czasu uruchamiania w zależności od sprzętu i sterowników.

Jak najszybciej sprawdź sterownik CPUfreq.

Im wcześniej sterownik CPUfreq przeprowadzi próby, tym szybciej będzie można zwiększyć częstotliwość procesora do maksymalnej (lub do maksymalnej z uwagi na ograniczenia termiczne) podczas uruchamiania. Im szybszy procesor, tym szybsze uruchamianie. Te wytyczne dotyczą również devfreqsterowników, które kontrolują DRAM, pamięć i częstotliwość połączeń.

W przypadku modułów kolejność ładowania może zależeć od poziomu initcall oraz kolejności kompilacji lub łączenia sterowników. Użyj aliasu MODULE_SOFTDEP(), aby mieć pewność, że sterownik cpufreq będzie jednym z pierwszych modułów wczytywanych.

Oprócz wczesnego wczytania modułu musisz też sprawdzić, czy wszystkie zależności potrzebne do sprawdzenia sterownika CPUfreq zostały również sprawdzone. Jeśli na przykład do sterowania częstotliwością procesora potrzebujesz zegara lub uchwytu regulatora, sprawdź, czy są one najpierw sprawdzane. Może też być konieczne załadowanie sterowników termicznych przed sterownikiem CPUfreq, jeśli istnieje możliwość, że procesory staną się zbyt gorące podczas uruchamiania. Zrób wszystko, co możliwe, aby zapewnić jak najwcześniejsze przeprowadzenie przez sterowniki CPUfreq i devfreq testów.

Oszczędności wynikające z wczesnego sprawdzania sterownika CPUfreq mogą być bardzo małe lub bardzo duże w zależności od tego, jak wcześnie można je sprawdzić i z jaką częstotliwością bootloader pozostawia procesory.

Przenoszenie modułów do drugiej fazy inicjowania, partycji dostawcy lub partycji vendor_dlkm

Ponieważ proces inicjalizacji pierwszego etapu jest serializowany, nie ma wielu możliwości równoległego uruchamiania procesu uruchamiania. Jeśli moduł nie jest potrzebny do zakończenia pierwszego etapu inicjalizacji, przenieś go do etapu inicjalizacji 2, umieszczając go w particji dostawcy lub vendor_dlkm.

Inicjowanie pierwszego etapu nie wymaga sprawdzania kilku urządzeń, aby przejść do drugiego etapu. Do normalnego procesu uruchamiania potrzebne są tylko możliwości konsoli i pamięci flash.

Załaduj te niezbędne sterowniki:

  • watchdog
  • reset
  • cpufreq

W przypadku trybu odzyskiwania i przestrzeni użytkownika fastbootd pierwsza faza inicjalizacji wymaga więcej urządzeń do sondowania (np. USB) i wyświetlacza. Zachowaj kopię tych modułów na dysku RAM pierwszego etapu oraz na partycji dostawcy lub partycji vendor_dlkm. Dzięki temu można je załadować na pierwszym etapie inicjalizacji w ramach procesu odzyskiwania lub fastbootd uruchamiania. Nie wczytuj jednak modułów trybu odzyskiwania w ramach pierwszego etapu inicjalizacji podczas normalnego procesu uruchamiania. Aby skrócić czas uruchamiania, moduły trybu odzyskiwania można odroczyć do drugiego etapu inicjalizacji. Wszystkie inne moduły, które nie są potrzebne na pierwszym etapie inicjalizacji, należy przenieść do partycji dostawcy lub vendor_dlkm.

Na podstawie listy urządzeń końcowych (np. UFS lub seryjnych) skrypt dev needs.sh odnajduje wszystkie sterowniki, urządzenia i moduły potrzebne do sprawdzenia zależności lub dostawców (np. zegarów, regulatorów lub gpio).

Przeniesienie modułów do drugiej fazy inicjalizacji skraca czas uruchamiania w następujący sposób:

  • Zmniejszenie rozmiaru dysku RAM.
    • Dzięki temu odczyty z pamięci flash są szybsze, gdy bootloader wczytuje ramdysk (serii zadań podczas uruchamiania).
    • Pozwala to przyspieszyć dekompresję, gdy jądro dekompresuje plik Ramdisk (serializacyjny etap uruchamiania).
  • Drugi etap inicjalizacji działa równolegle, co pozwala ukryć czas wczytywania modułu dzięki pracy wykonywanej na drugim etapie inicjalizacji.

Przeniesienie modułów do drugiej fazy może skrócić czas uruchamiania o 500–1000 ms w zależności od tego, ile modułów możesz przenieść do drugiej fazy inicjalizacji.

Logistyka wczytywania modułu

Najnowsza kompilacja Androida zawiera konfiguracje tablicy, które kontrolują, które moduły są kopiowane na poszczególne etapy i które moduły są wczytywane. W tej sekcji omówiono te podzbiory:

  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES. Lista modułów do skopiowania na dysk RAM.
  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD. Lista modułów wczytywanych na pierwszym etapie inicjalizacji.
  • BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD. Lista modułów, które mają być wczytane, gdy z dysku RAM wybrano opcję odzyskiwania lub fastbootd.
  • BOARD_VENDOR_KERNEL_MODULES. Ta lista modułów ma zostać skopiowana do partycji /vendor/lib/modules/ w katalogu /vendor/lib/modules/ lub vendor.vendor_dlkm
  • BOARD_VENDOR_KERNEL_MODULES_LOAD. Lista modułów do załadowania w drugim etapie inicjalizacji.

Moduł uruchamiania i moduł odzyskiwania na dysku RAM muszą być również skopiowane na partycję dostawcy lub partycję vendor_dlkm na poziomie /vendor/lib/modules. Kopiowanie tych modułów na partycję dostawcy zapewnia, że moduły nie są niewidoczne podczas inicjalizacji drugiego etapu. Jest to przydatne podczas debugowania i gromadzenia modinfo na potrzeby raportów o błędach.

Duplikaty powinny zajmować minimalną ilość miejsca na partycji dostawcy lub vendor_dlkm, o ile zestaw modułów rozruchowych jest zminimalizowany. Upewnij się, że plik modules.list dostawcy zawiera przefiltrowaną listę modułów w pliku /vendor/lib/modules. Filtrowana lista zapewnia, że czas uruchamiania nie jest zależny od ponownego wczytywania modułów (co jest kosztownym procesem).

Upewnij się, że moduły trybu odzyskiwania wczytują się jako grupa. Ładowanie modułów trybu przywracania może odbywać się w trybie przywracania lub na początku drugiego etapu inicjalizacji w ramach każdego procesu uruchamiania.

Aby wykonać te czynności, możesz użyć plików Board.Config.mk na urządzeniu, jak w tym przykładzie:

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

Ten przykład pokazuje łatwiejszy w zarządzaniu podzbiór elementów BOOT_KERNEL_MODULESRECOVERY_KERNEL_MODULES, który można określić lokalnie w plikach konfiguracji tablicy. Powyższy skrypt znajduje i wypełnia wszystkie moduły podzbioru z wybranych dostępnych modułów jądra, pozostawiając pozostałe moduły do zainicjowania drugiego etapu.

W przypadku drugiej fazy inicjowania zalecamy uruchamianie ładowania modułu jako usługi, aby nie blokować procesu uruchamiania. Do zarządzania wczytywaniem modułów użyj skryptu powłoki, aby w razie potrzeby można było zgłaszać (lub ignorować) inne kwestie logistyczne, takie jak obsługa błędów i ich minimalizacja lub zakończenie wczytywania modułu.

Możesz zignorować błąd wczytywania modułu debugowania, który nie występuje w kompilacji użytkownika. Aby zignorować ten błąd, ustaw właściwość vendor.device.modules.ready, aby wywołać późniejsze etapy init rc skryptu uruchamiania, aby przejść do ekranu uruchamiania. Jeśli w pliku /vendor/etc/init.insmod.sh masz ten kod:

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

W pliku rc sprzętu usługa one shot może być określona za pomocą:

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

Po przeniesieniu modułów z pierwszego etapu na drugi można wprowadzić dodatkowe optymalizacje. Za pomocą funkcji modprobe blocklist możesz podzielić drugi etap procesu uruchamiania, aby uwzględnić opóźnione wczytywanie nieistotnych modułów. Ładowanie modułów używanych wyłącznie przez konkretny interfejs HAL może zostać opóźnione, aby wczytać moduły tylko po uruchomieniu interfejsu HAL.

Aby skrócić czas uruchamiania, możesz w usłudze ładowania modułów wybrać konkretne moduły, które są bardziej odpowiednie do ładowania po ekranie wczytywania. Możesz na przykład wczytywać moduły dekodera wideo lub Wi-Fi po wyczyszczeniu procesu inicjalizacji (sys.boot_completena przykład sygnał usługi Androida). Upewnij się, że bloki HAL dla ładowanych późno modułów są wystarczająco długie, gdy nie ma sterowników jądra.

Możesz też użyć w skrypcie uruchamiania poleceń wait<file>[<timeout>] w bootflow rc, aby zaczekać na wybrane wpisy sysfs, które pokazują, że moduły sterownika ukończyły operacje sondowania. Przykładem jest oczekiwanie na zakończenie wczytywania sterownika wyświetlacza w tle podczas odzyskiwania lub fastbootd, zanim zostanie wyświetlona grafika menu.

Inicjowanie częstotliwości procesora do rozsądnej wartości w programie rozruchowym

Nie wszystkie układy SoC/produkty mogą uruchamiać procesor z najwyższą częstotliwością ze względu na kwestie związane z temperaturą lub zasilaniem podczas testów pętli uruchamiania. Upewnij się jednak, że bootloader ustawia częstotliwość wszystkich procesorów online na tak wysoką, jak to tylko możliwe dla SoC lub produktu. Jest to bardzo ważne, ponieważ w przypadku pełnomodułowego jądra dekompresja init ramdisk odbywa się przed załadowaniem sterownika CPUfreq. Jeśli więc bootloader pozostawi procesor przy niższej częstotliwości, czas dekompresji dysku RAM może być dłuższy niż w przypadku skompilowanego statycznie jądra (po uwzględnieniu różnicy w rozmiarze dysku RAM), ponieważ częstotliwość procesora byłaby bardzo niska podczas wykonywania prac wymagających dużej mocy obliczeniowej (dekompresji). To samo dotyczy pamięci i częstotliwości połączeń.

Inicjowanie częstotliwości procesora dużych procesorów w programie rozruchowym

Zanim wczytany zostanie sterownik CPUfreq, jądro nie zna częstotliwości procesora i nie dostosowuje pojemności procesora do bieżącej częstotliwości. Jeśli obciążenie procesora o małej mocy jest wystarczająco duże, jądro może przenieść wątki na procesor o dużej mocy.

Upewnij się, że duże procesory mają co najmniej taką samą wydajność jak małe procesory w przypadku częstotliwości, z jaką działają po uruchomieniu przez bootloader. Jeśli na przykład duży procesor ma 2 razy większą wydajność niż mały procesor przy tej samej częstotliwości, ale bootloader ustawia częstotliwość małego procesora na 1,5 GHz, a dużego na 300 MHz, to wydajność rozruchu spadnie, jeśli jądro przeniesie wątek na duży procesor. W tym przykładzie, jeśli można bezpiecznie uruchomić procesor big. CPU z częstotliwością 750 MHz, należy to zrobić, nawet jeśli nie zamierzasz go używać.

Sterowniki nie powinny wczytywać oprogramowania na pierwszym etapie inicjalizacji.

W niektórych przypadkach konieczne może być załadowanie oprogramowania na pierwszym etapie inicjalizacji. Ogólnie jednak sterowniki nie powinny wczytywać żadnego oprogramowania na pierwszym etapie inicjalizacji, zwłaszcza w kontekście sondowania urządzenia. Ładowanie oprogramowania układowego w ramach pierwszej fazy inicjalizacji powoduje zatrzymanie całego procesu uruchamiania, jeśli oprogramowanie układowe nie jest dostępne na dysku RAM pierwszej fazy. Nawet jeśli oprogramowanie jest obecne w pierwszym etapie pamięci RAM, nadal powoduje niepotrzebne opóźnienie.