Optymalizacja czasu uruchamiania

Ta strona zawiera wskazówki pozwalające skrócić czas uruchamiania.

Usuń symbole debugowania z modułów

Podobnie jak w przypadku usuwania symboli debugowania 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 pliku 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 wymiany

Gzip generuje mniejszy wynik kompresji niż LZ4, ale LZ4 dekompresuje się szybciej niż Gzip. W przypadku jądra i modułów bezwzględne zmniejszenie rozmiaru pamięci masowej związane z użyciem Gzip nie jest tak istotne w porównaniu z korzyścią dotyczącą czasu dekompresji w 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 urządzenia. Kompresję jądra można ustawić za pomocą defconfig jądra.

Przejście na LZ4 powinno przyspieszyć rozruch od 500 do 1000 ms.

Unikaj nadmiernego zapisywania sterowników

W architekturze ARM64 i ARM32 wywołania funkcji, które są dłuższe niż konkretna odległość od miejsca wywołania, wymagają tabeli przechodzenia (nazywanej tabelą łączenia procedur lub PLT), aby móc zakodować pełny adres zwijania. 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. Mniej 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 commitowaniu 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 module_init() przeprowadza skanowanie urządzenia, moduł nie może zakończyć wczytywania, dopóki skanowanie nie zostanie ukończone. 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ąć wolniejszego rozruchu, włącz sondowanie asynchroniczne w przypadku modułów, których sondowanie urządzeń zajmuje dużo czasu. 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 układu w ramach funkcji sondowania, oraz urządzenia, które wykonują wiele operacji inicjalizacji sprzętu, 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ć sondowanie asynchroniczne dla modułu, nie wystarczy samo ustawienie flagi PROBE_PREFER_ASYNCHRONOUS w kodzie sterownika. W przypadku modułów musisz też dodać module_name.async_probe=1 w wierszu poleceń jądra lub przekazać async_probe=1 jako parametr modułu podczas wczytywania modułu za pomocą modprobe lub insmod.

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

Jak najszybciej spróbuj sondować 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 uwzględnieniem ograniczeń termicznych) podczas uruchamiania. Im szybszy procesor, tym szybsze uruchamianie. Ta wskazówka dotyczy również sterowników devfreq, które kontrolują częstotliwość DRAM, pamięci i częstotliwości połączeń międzysieciowych.

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. Dlatego zrób wszystko, co możliwe, aby zapewnić jak najwcześniejsze przeprowadzenie przez sterowniki CPUfreq i devfreq testów diagnostycznych.

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, do partycji dostawcy lub partycji vendor_dlkm

Proces inicjalizacji pierwszego etapu jest serializowany, więc nie ma wielu możliwości równoległego uruchamiania procesu uruchamiania. Jeśli moduł nie jest potrzebny do ukończenia pierwszego etapu, przenieś go do inicjowania drugiego etapu, umieszczając go w systemie dostawcy lub na partycji 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 vendor_dlkm. Dzięki temu można je załadować na pierwszym etapie inicjalizacji w celu odzyskiwania lub fastbootduruchamiania. Nie wczytuj jednak modułów trybu przywracania w ramach pierwszego etapu inicjalizacji podczas normalnego procesu uruchamiania. Aby skrócić czas uruchamiania, moduły trybu odzyskiwania można odroczyć do drugiej fazy inicjalizacji. Wszystkie inne moduły, które nie są potrzebne na etapie inicjalizacji 1, 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.
    • Umożliwia to szybsze odczyty Flasha, gdy program rozruchowy ładuje dysk ramdisk (etap rozruchu szeregowego).
    • 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 drugiego etapu pozwala zaoszczędzić 500–1000 ms przy uruchamianiu w zależności od tego, ile modułów możesz przejść do inicjowania drugiego etapu.

Logistyka wczytywania modułu

Najnowsza wersja Androida zawiera konfiguracje płyty, 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 do skopiowania do katalogu dostawcy lub partycji vendor_dlkm w katalogu /vendor/lib/modules/.
  • BOARD_VENDOR_KERNEL_MODULES_LOAD. Lista modułów do załadowania w drugim etapie inicjalizacji.

Moduł uruchamiania i moduł odzyskiwania na dysku RAM muszą też zostać skopiowane na partycję dostawcy lub vendor_dlkm w miejscu /vendor/lib/modules. Skopiowanie tych modułów na partycję dostawcy daje pewność, że nie będą one niewidoczne podczas drugiego etapu inicjowania, co przydaje się przy debugowaniu i zbieraniu danych 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 dokumencie /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 wstępnej inicjalizacji drugiego etapu.

W przypadku drugiej fazy inicjowania zalecamy uruchamianie ładowania modułu jako usługi, aby nie blokować procesu uruchamiania. Używaj skryptu powłoki do zarządzania wczytywaniem modułu, aby w razie potrzeby zgłaszać z powrotem (lub ignorować) inne funkcje logistyczne, np. obsługę błędów i łagodzenie skutków wczytywania modułu.

Możesz zignorować błąd wczytywania modułu debugowania, który nie występuje w kompilacjach użytkownika. Aby zignorować ten błąd, ustaw właściwość vendor.device.modules.ready tak, aby uruchamiała późniejsze etapy procesu rozruchu skryptów init rc w celu przejścia do ekranu uruchamiania. Skorzystaj z poniższego przykładowego skryptu, jeśli masz ten kod w /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

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

Dodatkowe optymalizacje można wprowadzić po przeniesieniu modułów z pierwszego etapu na drugi. Za pomocą funkcji modprobe blocklist możesz podzielić drugi etap procesu uruchamiania, aby uwzględnić opóźnione wczytywanie nieistotnych modułów. Wczytywanie modułów używanych wyłącznie przez konkretny interfejs HAL można opóźnić, aby wczytać je dopiero po uruchomieniu interfejsu.

Aby poprawić widoczne czasy rozruchu, możesz w usłudze wczytywania modułu w szczególności wybrać moduły, które będą bardziej sprzyjać załadowaniu po wyświetleniu ekranu uruchamiania. Możesz na przykład wczytywać wyraźnie opóźnione moduły dekodera wideo lub Wi-Fi po wyczyszczeniu procesu inicjowania (sys.boot_completena przykład sygnał usługi Androida). Upewnij się, że kody HAL modułów opóźnionego ładowania blokują się na tyle długo, gdy nie ma sterowników jądra.

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

Zainicjuj w programie rozruchowym częstotliwość procesora na rozsądną wartość

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 onlineowych procesorów tak wysoko, jak to jest bezpieczne dla SoC lub produktu. Jest to bardzo ważne, ponieważ w przypadku jądra w pełni modułowym 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 intensywnie obciążających procesor (dekompresji). To samo dotyczy pamięci i częstotliwości połączeń.

Zainicjuj częstotliwość procesora dla 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ą. Jeśli na przykład duży procesor jest 2 razy wydajniejszy niż mały procesor o tej samej częstotliwości, ale program rozruchowy ustawia częstotliwość małego procesora na 1,5 GHz, a częstotliwość dużego procesora na 300 MHz, wydajność rozruchu spadnie, gdy jądro przeniesie wątek do dużego procesora. 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 etapie inicjalizacji. Ogólnie jednak sterowniki nie powinny wczytywać żadnego oprogramowania na pierwszym etapie inicjalizacji, zwłaszcza w kontekście sondowania urządzenia. Wczytywanie oprogramowania układowego na pierwszym etapie powoduje, że cały proces uruchamiania się zawiesza, jeśli nie jest dostępne na dysku RAM pierwszego etapu. Nawet jeśli oprogramowanie układowe znajduje się na dysku RAM pierwszego etapu, i tak powoduje niepotrzebne opóźnienie.