Optymalizacja czasu uruchamiania

Na tej stronie znajdziesz wskazówki, które pomogą Ci skrócić czas uruchamiania.

Usuwanie symboli debugowania z modułów

Podobnie jak w przypadku usuwania symboli debugowania z jądra na urządzeniu produkcyjnym, pamiętaj, aby usunąć symbole debugowania z modułów. Usunięcie symboli debugowania z modułów przyspiesza uruchamianie, ponieważ zmniejsza:

  • Czas potrzebny na odczytanie plików binarnych z pamięci flash.
  • Czas potrzebny na rozpakowanie dysku RAM.
  • Czas potrzebny na wczytanie modułów.

Usunięcie symboli debugowania z modułów może skrócić czas rozruchu o kilka sekund.

Usuwanie symboli jest domyślnie włączone w kompilacji platformy Android, ale aby włączyć je jawnie, ustaw wartość BOARD_DO_NOT_STRIP_VENDOR_RAMDISK_MODULES w konfiguracji urządzenia w folderze device/vendor/device.

Używanie kompresji LZ4 w przypadku jądra i dysku RAM

Gzip generuje mniejszy skompresowany plik wyjściowy niż LZ4, ale LZ4 dekompresuje dane szybciej niż Gzip. W przypadku jądra i modułów bezwzględne zmniejszenie rozmiaru pamięci w wyniku użycia Gzip nie jest tak znaczące w porównaniu z korzyścią w postaci krótszego czasu dekompresji LZ4.

Do platformy Android dodano obsługę kompresji LZ4 ramdisk w procesie kompilacji za pomocą BOARD_RAMDISK_USE_LZ4. Możesz ustawić tę opcję w konfiguracji konkretnego urządzenia. Kompresję jądra można ustawić za pomocą pliku defconfig jądra.

Przełączenie na LZ4 powinno zapewnić szybsze uruchamianie o 500–1000 ms.

Unikaj nadmiernego rejestrowania danych w sterownikach

W przypadku architektury ARM64 i ARM32 wywołania funkcji, które znajdują się w odległości większej niż określona od miejsca wywołania, wymagają tabeli skoków (zwanej tabelą łączenia procedur lub PLT), aby można było zakodować pełny adres skoku. Ponieważ moduły są ładowane dynamicznie, te tablice skoków muszą zostać poprawione podczas ładowania modułu. Wywołania, które wymagają przeniesienia, są nazywane w formacie ELF wpisami przeniesienia z wyraźnymi składnikami (w skrócie RELA).

Podczas przydzielania PLT jądro systemu Linux przeprowadza optymalizację rozmiaru pamięci (np. optymalizację trafień w pamięci podręcznej). Dzięki temu zatwierdzeniu w górę złożoność schematu optymalizacji wynosi O(N^2), gdzie N to liczba RELA typu R_AARCH64_JUMP26 lub R_AARCH64_CALL26. Dlatego mniejsza liczba RELA tego typu pomaga skrócić czas ładowania modułu.

Jednym z częstych wzorców kodowania, który zwiększa liczbę błędów R_AARCH64_CALL26 lub R_AARCH64_JUMP26 RELA, jest nadmierne rejestrowanie w sterowniku. Każde wywołanie funkcji printk() lub innego schematu rejestrowania zwykle dodaje wpis RELA CALL26/JUMP26. W tekście zatwierdzenia w zatwierdzeniu upstream zauważ, że nawet po optymalizacji załadowanie 6 modułów zajmuje około 250 ms. Dzieje się tak, ponieważ te 6 modułów było 6 modułami z największą liczbą logów.

Ograniczenie rejestrowania może skrócić czas rozruchu o 100–300 ms w zależności od tego, jak intensywne jest obecne rejestrowanie.

Selektywne włączanie asynchronicznego sondowania

Gdy moduł jest wczytywany, a urządzenie, które obsługuje, zostało już wypełnione z DT (devicetree) i dodane do rdzenia sterownika, testowanie urządzenia odbywa się w kontekście wywołania module_init(). Gdy sprawdzanie urządzenia odbywa się w kontekście module_init(), moduł nie może zakończyć wczytywania, dopóki nie zostanie zakończone sprawdzanie. Ładowanie modułów odbywa się w większości w sposób szeregowy, więc urządzenie, które stosunkowo długo przeprowadza sondowanie, wydłuża czas rozruchu.

Aby uniknąć dłuższego czasu uruchamiania, włącz asynchroniczne sprawdzanie modułów, których sprawdzanie urządzeń trwa dłużej. Włączenie sondowania asynchronicznego dla wszystkich modułów może nie być korzystne, ponieważ czas potrzebny na rozwidlenie wątku i uruchomienie sondowania może być tak samo długi jak czas potrzebny na sondowanie urządzenia.

Urządzenia podłączone przez wolną magistralę, np. I2C, urządzenia, które w funkcji sondy ładują oprogramowanie sprzętowe, oraz urządzenia, które wykonują wiele operacji inicjowania sprzętu, mogą powodować problemy z czasem. Najlepszym sposobem na wykrycie tego zjawiska jest zebranie i posortowanie czasu sondowania każdego kierowcy.

Aby włączyć asynchroniczne sondowanie modułu, nie wystarczy ustawić flagę 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 asynchronicznego sondowania może skrócić czas rozruchu o 100–500 ms w zależności od sprzętu i sterowników.

Jak najszybciej przetestuj sterownik CPUfreq

Im wcześniej sterownik CPUfreq przeprowadzi sondowanie, tym szybciej podczas uruchamiania będzie można zwiększyć częstotliwość procesora do maksymalnej wartości (lub do maksymalnej wartości ograniczonej termicznie). Im szybszy procesor, tym szybsze uruchamianie. Ta wytyczna dotyczy też devfreqsterowników, które kontrolują częstotliwość pamięci DRAM, pamięci i połączeń.

W przypadku modułów kolejność ładowania może zależeć od initcall poziomu oraz kolejności kompilacji lub łączenia sterowników. Użyj aliasu MODULE_SOFTDEP(), aby upewnić się, że sterownik cpufreq jest jednym z pierwszych modułów, które zostaną wczytane.

Oprócz wczesnego wczytania modułu musisz też zadbać o to, aby wszystkie zależności sterownika CPUfreq zostały zbadane. Jeśli na przykład potrzebujesz zegara lub regulatora do kontrolowania częstotliwości procesora, upewnij się, że są one sprawdzane w pierwszej kolejności. Może też być konieczne wczytanie sterowników termicznych przed sterownikiem CPUfreq, jeśli procesory mogą się zbytnio nagrzewać podczas uruchamiania. Zrób wszystko, co w Twojej mocy, aby sterowniki CPUfreq i odpowiednie sterowniki devfreq zostały wykryte jak najwcześniej.

Oszczędności wynikające z wcześniejszego sondowania sterownika CPUfreq mogą być bardzo małe lub bardzo duże w zależności od tego, jak wcześnie można przeprowadzić sondowanie i z jaką częstotliwością program rozruchowy pozostawia procesory.

Przenoszenie modułów do drugiego etapu inicjowania, partycji vendor lub vendor_dlkm

Ponieważ proces inicjowania pierwszego etapu jest szeregowy, nie ma wielu możliwości równoległego uruchamiania systemu. Jeśli moduł nie jest potrzebny do ukończenia inicjowania pierwszego etapu, przenieś go do inicjowania drugiego etapu, umieszczając go w partycji dostawcy lub vendor_dlkm.

Pierwszy etap inicjowania nie wymaga sprawdzania kilku urządzeń, aby przejść do drugiego etapu inicjowania. W przypadku normalnego procesu uruchamiania wymagane są tylko funkcje konsoli i pamięci flash.

Załaduj te niezbędne sterowniki:

  • watchdog
  • reset
  • cpufreq

W przypadku trybu odzyskiwania i przestrzeni użytkownika fastbootd inicjowanie pierwszego etapu wymaga zbadania większej liczby urządzeń (np. USB) i wyświetlania. Zachowaj kopię tych modułów w pierwszym etapie dysku RAM oraz w partycji dostawcy lub vendor_dlkm. Dzięki temu można je wczytać w pierwszej fazie inicjowania na potrzeby odzyskiwania lub fastbootd procesu rozruchu. Nie wczytuj jednak modułów trybu przywracania w pierwszym etapie inicjowania podczas normalnego uruchamiania. Moduły trybu odzyskiwania można odroczyć do drugiego etapu inicjowania, aby skrócić czas rozruchu. Wszystkie inne moduły, które nie są potrzebne w pierwszej fazie inicjowania, powinny zostać przeniesione do partycji dostawcy lub vendor_dlkm.

Na podstawie listy urządzeń końcowych (np. UFS lub serial)dev needs.shskrypt znajduje wszystkie sterowniki, urządzenia i moduły potrzebne do sondowania zależności lub dostawców (np. zegary, regulatory lub gpio).

Przeniesienie modułów do drugiego etapu inicjowania skraca czas rozruchu w następujący sposób:

  • Zmniejszenie rozmiaru dysku RAM.
    • Umożliwia to szybsze odczytywanie pamięci flash, gdy program rozruchowy wczytuje dysk RAM (zserializowany krok rozruchu).
    • Zapewnia to większą szybkość dekompresji, gdy jądro dekompresuje dysk RAM (zserializowany krok rozruchu).
  • Inicjowanie drugiego etapu działa równolegle, co ukrywa czas wczytywania modułu dzięki pracy wykonywanej podczas inicjowania drugiego etapu.

Przeniesienie modułów do drugiego etapu może skrócić czas rozruchu o 500–1000 ms w zależności od tego, ile modułów można przenieść do drugiego etapu inicjowania.

Logistyka wczytywania modułów

Najnowsza kompilacja Androida zawiera konfiguracje płyty, które określają, które moduły są kopiowane na poszczególne etapy i które moduły są wczytywane. Ta sekcja dotyczy następującego podzbioru:

  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES. Lista modułów do skopiowania do ramdysku.
  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD. Lista modułów do załadowania w pierwszej fazie inicjowania.
  • BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD. Lista modułów do załadowania, gdy z dysku RAM zostanie wybrana opcja odzyskiwania lub fastbootd.
  • BOARD_VENDOR_KERNEL_MODULES. Lista modułów do skopiowania do partycji dostawcy lub vendor_dlkm w katalogu /vendor/lib/modules/.
  • BOARD_VENDOR_KERNEL_MODULES_LOAD. Ta lista modułów ma zostać wczytana w drugim etapie inicjowania.

Moduły rozruchowy i przywracania w ramdysku muszą być też skopiowane do partycji dostawcy lubvendor_dlkm w lokalizacji /vendor/lib/modules. Skopiowanie tych modułów do partycji dostawcy sprawia, że nie są one niewidoczne podczas inicjowania w drugim etapie, co jest przydatne do debugowania i zbierania modinfo na potrzeby raportów o błędach.

Duplikacja powinna zajmować minimalną ilość miejsca u dostawcy lub na vendor_dlkm partycji pod warunkiem, że zestaw modułów rozruchowych jest zminimalizowany. Upewnij się, że plik dostawcy modules.list zawiera przefiltrowaną listę modułów w /vendor/lib/modules. Filtrowana lista zapewnia, że czas uruchamiania nie jest wydłużany przez ponowne wczytywanie modułów (co jest kosztownym procesem).

Sprawdź, czy moduły trybu odzyskiwania wczytują się jako grupa. Wczytywanie modułów trybu przywracania można przeprowadzić w trybie przywracania lub na początku drugiego etapu inicjowania w każdym procesie uruchamiania.

Za pomocą plików na urządzeniu Board.Config.mk możesz wykonywać te czynności, 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 BOOT_KERNEL_MODULESRECOVERY_KERNEL_MODULES, który można określić lokalnie w plikach konfiguracyjnych płyty. Powyższy skrypt znajduje i wypełnia każdy z modułów podzbioru wybranymi dostępnymi modułami jądra, pozostawiając pozostałe moduły do inicjowania w drugim etapie.

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

Możesz zignorować błąd wczytywania modułu debugowania, który nie występuje w kompilacjach użytkowników. Aby zignorować ten błąd, ustaw właściwość vendor.device.modules.ready, aby wywołać późniejsze etapy procesu uruchamiania skryptów init rc i przejść do ekranu uruchamiania. Jeśli masz poniższy kod w /vendor/etc/init.insmod.sh, skorzystaj z tego przykładowego skryptu:

#!/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ługę one shot można określić za pomocą tego kodu:

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 przejściu modułów z pierwszego do drugiego etapu. Możesz użyć funkcji listy blokowanych modprobe, aby podzielić proces uruchamiania drugiego etapu i uwzględnić opóźnione wczytywanie nieistotnych modułów. Wczytywanie modułów używanych wyłącznie przez konkretną warstwę HAL można odłożyć, aby wczytywać je tylko wtedy, gdy warstwa HAL jest uruchamiana.

Aby skrócić czas uruchamiania, możesz wybrać w usłudze ładowania modułów te moduły, które lepiej nadają się do ładowania po wyświetleniu ekranu uruchamiania. Możesz na przykład jawnie opóźnić wczytywanie modułów dekodera wideo lub Wi-Fi po zakończeniu procesu inicjowania (sys.boot_completesygnał właściwości Androida, na przykład). Upewnij się, że interfejsy HAL dla modułów ładowanych później blokują się wystarczająco długo, gdy nie ma sterowników jądra.

Możesz też użyć polecenia wait<file>[<timeout>] w skrypcie rc procesu rozruchu, aby poczekać na wybrane wpisy sysfs, które wskazują, że moduły sterowników zakończyły operacje sondowania. Przykładem tego jest oczekiwanie na zakończenie wczytywania sterownika wyświetlacza w tle procesu odzyskiwania lub fastbootd przed wyświetleniem grafiki menu.

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

Nie wszystkie układy SoC i produkty mogą uruchamiać procesor z najwyższą częstotliwością z powodu problemów z temperaturą lub zasilaniem podczas testów pętli rozruchowej. Upewnij się jednak, że program rozruchowy ustawia częstotliwość wszystkich procesorów online na najwyższą możliwą wartość, która jest bezpieczna dla układu SoC lub produktu. Jest to bardzo ważne, ponieważ w przypadku w pełni modułowego jądra dekompresja dysku RAM odbywa się przed załadowaniem sterownika CPUfreq. Jeśli program rozruchowy pozostawi procesor na dolnym końcu zakresu częstotliwości, czas dekompresji dysku RAM może być dłuższy niż w przypadku statycznie skompilowanego jądra (po uwzględnieniu różnicy w rozmiarze dysku RAM), ponieważ częstotliwość procesora będzie bardzo niska podczas wykonywania zadań 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 zostanie załadowany sterownik CPUfreq, jądro nie zna częstotliwości procesora i nie skaluje pojemności harmonogramu procesora do bieżącej częstotliwości. Jeśli obciążenie małego procesora jest wystarczająco duże, jądro może przenieść wątki do dużego procesora.

Upewnij się, że duże procesory są co najmniej tak samo wydajne jak małe procesory przy częstotliwości, z jaką bootloader je pozostawia. Jeśli na przykład duży procesor jest 2 razy wydajniejszy niż mały przy tej samej częstotliwości, ale program rozruchowy ustawia częstotliwość małego procesora na 1,5 GHz, a dużego na 300 MHz, wydajność rozruchu spadnie, jeśli jądro przeniesie wątek do dużego procesora. W tym przykładzie, jeśli uruchomienie dużego procesora przy częstotliwości 750 MHz jest bezpieczne, należy to zrobić, nawet jeśli nie planujesz go używać.

Sterowniki nie powinny ładować oprogramowania w pierwszej fazie inicjowania

W niektórych przypadkach nie da się uniknąć konieczności wczytania oprogramowania w pierwszej fazie inicjowania. Ogólnie jednak sterowniki nie powinny wczytywać żadnego oprogramowania układowego w pierwszej fazie inicjowania, zwłaszcza w kontekście sondowania urządzenia. Załadowanie oprogramowania układowego w pierwszej fazie inicjowania powoduje zatrzymanie całego procesu rozruchu, jeśli oprogramowanie układowe nie jest dostępne w pierwszej fazie dysku RAM. Nawet jeśli oprogramowanie układowe jest obecne w ramdysku pierwszego etapu, powoduje to niepotrzebne opóźnienie.