Optimisation du temps de démarrage

Cette page fournit des conseils pour améliorer le temps de démarrage.

Supprimer les symboles de débogage des modules

Tout comme les symboles de débogage sont supprimés du noyau sur un appareil de production, assurez-vous également de supprimer les symboles de débogage des modules. La suppression des symboles de débogage des modules permet de réduire le temps de démarrage en réduisant les éléments suivants:

  • Temps nécessaire pour lire les binaires à partir de Flash.
  • Temps nécessaire à la décompression du disque RAM.
  • Temps nécessaire au chargement des modules.

Supprimer le symbole de débogage des modules peut économiser plusieurs secondes au démarrage.

Le masquage de symboles est activé par défaut dans le build de la plate-forme Android, mais pour l'activer explicitement, définissez BOARD_DO_NOT_STRIP_VENDOR_RAMDISK_MODULES dans votre configuration spécifique à l'appareil sous device/vendor/device.

Utiliser la compression LZ4 pour le noyau et le ramdisk

Gzip génère une sortie compressée plus petite que LZ4, mais LZ4 se décompresse plus rapidement que Gzip. Pour le noyau et les modules, la réduction absolue de la taille de stockage obtenue avec Gzip n'est pas aussi importante que l'avantage de temps de décompression de LZ4.

La prise en charge de la compression LZ4 du ramdisk a été ajoutée au build de la plate-forme Android via BOARD_RAMDISK_USE_LZ4. Vous pouvez définir cette option dans la configuration spécifique à votre appareil. La compression du noyau peut être définie via le fichier defconfig du noyau.

En passant à LZ4, vous devriez pouvoir démarrer de 500 ms à 1 000 ms plus rapidement.

Éviter l'enregistrement excessif dans vos pilotes

Sous ARM64 et ARM32, les appels de fonction situés à plus d'une distance spécifique du site d'appel nécessitent une table de saut (appelée table de liaison de procédure ou PLT) pour pouvoir encoder l'adresse de saut complète. Étant donné que les modules sont chargés de manière dynamique, ces tables de saut doivent être corrigées lors du chargement du module. Les appels nécessitant une relocalisation sont appelés "entrées de relocalisation" avec des addends explicites (ou RELA, pour "relocation") au format ELF.

Le noyau Linux effectue une certaine optimisation de la taille de la mémoire (telle que l'optimisation des taux de réussite de la mise en cache) lors de l'allocation de la PLT. Avec ce commit en amont, le schéma d'optimisation présente une complexité de type O(N^2), où N est le nombre de RELA de type R_AARCH64_JUMP26 ou R_AARCH64_CALL26. Réduire le nombre de RELA de ces types est donc utile pour réduire le temps de chargement des modules.

Un modèle de codage courant qui augmente le nombre de RELA R_AARCH64_CALL26 ou R_AARCH64_JUMP26 est la journalisation excessive dans un pilote. Chaque appel à printk() ou à tout autre schéma de journalisation ajoute généralement une entrée RELA CALL26/JUMP26. Dans le texte du commit dans le commit en amont, notez que même avec l'optimisation, le chargement des six modules prend environ 250 ms, car ces six modules étaient les six modules les plus chargés en journalisation.

Réduire la journalisation peut vous faire économiser environ 100 à 300 ms sur les temps de démarrage, en fonction de l'excès de journalisation existant.

Activer de manière sélective l'analyse asynchrone

Lorsqu'un module est chargé, si l'appareil qu'il prend en charge a déjà été renseigné à partir du DT (devicetree) et ajouté au noyau du pilote, la sonde de l'appareil est effectuée dans le contexte de l'appel module_init(). Lorsqu'une vérification d'appareil est effectuée dans le contexte de module_init(), le chargement du module ne peut pas se terminer tant que la vérification n'est pas terminée. Étant donné que le chargement de module est principalement sérialisé, un appareil qui prend relativement longtemps à interroger ralentit le temps de démarrage.

Pour éviter de ralentir le démarrage, activez la vérification asynchrone pour les modules qui prennent un certain temps à vérifier leurs appareils. L'activation de la vérification asynchrone pour tous les modules n'est peut-être pas utile, car le temps nécessaire pour dupliquer un thread et lancer la vérification peut être aussi long que le temps nécessaire pour vérifier l'appareil.

Les appareils connectés via un bus lent tel qu'I2C, les appareils qui effectuent le chargement du micrologiciel dans leur fonction de sonde et les appareils qui effectuent de nombreuses initialisations matérielles peuvent entraîner un problème de synchronisation. Le meilleur moyen d'identifier quand cela se produit est de collecter le temps d'exploration pour chaque pilote et de le trier.

Pour activer la vérification asynchrone d'un module, il n'est pas suffisant de définir uniquement l'indicateur PROBE_PREFER_ASYNCHRONOUS dans le code du pilote. Pour les modules, vous devez également ajouter module_name.async_probe=1 dans la ligne de commande du kernel ou transmettre async_probe=1 en tant que paramètre de module lors du chargement du module à l'aide de modprobe ou insmod.

L'activation de la vérification asynchrone peut vous faire gagner environ 100 à 500 ms sur les temps de démarrage, en fonction de votre matériel/vos pilotes.

Analysez votre pilote CPUfreq le plus tôt possible

Plus tôt le pilote CPUfreq effectue des sondages, plus tôt vous pouvez augmenter la fréquence du processeur au maximum (ou à une valeur maximale limitée par la température) au démarrage. Plus le processeur est rapide, plus le démarrage est rapide. Cette consigne s'applique également aux pilotes devfreq qui contrôlent la DRAM, la mémoire et la fréquence d'interconnexion.

Avec les modules, l'ordre de chargement peut dépendre du niveau de initcall, et de l'ordre de compilation ou d'association des pilotes. Utilisez un alias MODULE_SOFTDEP() pour vous assurer que le pilote cpufreq fait partie des premiers modules à charger.

En plus de charger le module tôt, vous devez également vous assurer que toutes les dépendances de vérification du pilote CPUfreq ont également été vérifiées. Par exemple, si vous avez besoin d'une poignée d'horloge ou de régulateur pour contrôler la fréquence de votre processeur, assurez-vous qu'elles sont d'abord sondées. Vous devrez peut-être également charger les pilotes thermiques avant le pilote CPUfreq si vos processeurs peuvent devenir trop chauds au démarrage. Faites donc tout votre possible pour vous assurer que les pilotes CPUfreq et devfreq pertinents effectuent une analyse dès que possible.

Les économies réalisées en analysant votre pilote CPUfreq à un stade précoce peuvent être très faibles ou très importantes, selon la rapidité avec laquelle vous pouvez les analyser et la fréquence à laquelle le bootloader laisse les processeurs en place.

Déplacer des modules vers la partition d'initialisation, de fournisseur ou de fournisseur_dlkm de la deuxième étape

Étant donné que le processus d'initialisation de la première étape est sérialisé, il n'y a pas beaucoup d'occasions de paralléliser le processus de démarrage. Si un module n'est pas nécessaire pour que l'initialisation de premier niveau soit terminée, déplacez-le vers l'initialisation de deuxième niveau en le plaçant dans la partition du fournisseur ou vendor_dlkm.

L'initialisation de la première étape ne nécessite pas d'interroger plusieurs appareils pour passer à l'initialisation de la deuxième étape. Seules les fonctionnalités de stockage de la console et du flash sont nécessaires pour un flux de démarrage normal.

Chargez les pilotes essentiels suivants:

  • watchdog
  • reset
  • cpufreq

Pour le mode fastbootd de récupération et d'espace utilisateur, l'initialisation de la première étape nécessite d'autres appareils à sonder (tels que USB) et à afficher. Conservez une copie de ces modules dans la première étape ramdisk et dans la partition du fournisseur ou vendor_dlkm. Ils peuvent ainsi être chargés lors de l'initialisation de la première étape pour la récupération ou le flux de démarrage fastbootd. Toutefois, ne chargez pas les modules du mode de récupération lors de l'initialisation de la première étape lors du flux de démarrage normal. Les modules du mode de récupération peuvent être différés à l'initialisation de deuxième étape pour réduire le temps de démarrage. Tous les autres modules qui ne sont pas nécessaires à la première étape de l'initialisation doivent être déplacés vers le fournisseur ou la partition vendor_dlkm.

À partir d'une liste d'appareils de feuilles (par exemple, le UFS ou la série), le script dev needs.sh recherche tous les pilotes, appareils et modules nécessaires pour que les dépendances ou les fournisseurs (par exemple, les horloges, les régulateurs ou gpio) puissent effectuer des analyses.

Le transfert de modules vers l'initialisation de deuxième étape réduit les temps de démarrage de la manière suivante:

  • Réduction de la taille du ramdisk.
    • Cela permet d'accélérer les lectures flash lorsque le bootloader charge le ramdisk (étape de démarrage sérialisée).
    • Cela permet d'accélérer la décompression lorsque le noyau décompresse le ramdisk (étape de démarrage sérialisé).
  • La deuxième étape init fonctionne en parallèle, ce qui masque le temps de chargement du module et le travail effectué dans la seconde étape init.

Le fait de déplacer des modules vers la deuxième étape permet d'économiser 500 à 1 000 ms sur les temps de démarrage selon le nombre de modules que vous pouvez déplacer vers l'initialisation de deuxième étape.

Logistique de chargement des modules

La dernière version d'Android propose des configurations de carte qui contrôlent les modules copiés à chaque étape et les modules qui se chargent. Cette section porte sur le sous-ensemble suivant:

  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES. Liste des modules à copier dans le ramdisk.
  • BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD. Cette liste de modules à charger lors de l'initialisation de la première étape.
  • BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD : liste des modules à charger lorsque la récupération ou fastbootd est sélectionnée à partir du ramdisk.
  • BOARD_VENDOR_KERNEL_MODULES : liste des modules à copier dans la partition du fournisseur ou vendor_dlkm dans le répertoire /vendor/lib/modules/.
  • BOARD_VENDOR_KERNEL_MODULES_LOAD. Cette liste de modules à charger lors de l'initialisation de la deuxième étape.

Les modules de démarrage et de récupération dans le ramdisk doivent également être copiés dans la partition du fournisseur ou de vendor_dlkm à /vendor/lib/modules. Copier ces modules dans la partition du fournisseur garantit qu'ils ne sont pas invisibles lors de l'initialisation de la deuxième étape, ce qui est utile pour le débogage et la collecte de modinfo pour les rapports de bugs.

La duplication devrait coûter un espace minimal sur le fournisseur ou la partition vendor_dlkm tant que l'ensemble du module de démarrage est réduit. Assurez-vous que le fichier modules.list du fournisseur comporte une liste filtrée de modules dans /vendor/lib/modules. La liste filtrée garantit que les temps de démarrage ne sont pas affectés par le chargement à nouveau des modules (ce qui est un processus coûteux).

Assurez-vous que les modules du mode de récupération se chargent en groupe. Le chargement des modules en mode récupération peut être effectué en mode récupération ou au début de la deuxième étape d'initialisation de chaque flux de démarrage.

Vous pouvez utiliser les fichiers Board.Config.mk de l'appareil pour effectuer ces actions, comme illustré dans l'exemple suivant:

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

Cet exemple présente un sous-ensemble plus facile à gérer de BOOT_KERNEL_MODULES et RECOVERY_KERNEL_MODULES à spécifier localement dans les fichiers de configuration du tableau. Le script précédent recherche et remplit chacun des sous-ensembles de modules à partir des modules de noyau disponibles sélectionnés, laissant les modules restants pour l'initialisation de la deuxième étape.

Pour l'initialisation de deuxième étape, nous vous recommandons d'exécuter le chargement du module en tant que service afin qu'il ne bloque pas le flux de démarrage. Utilisez un script shell pour gérer le chargement du module afin que d'autres aspects logistiques, tels que la gestion et l'atténuation des erreurs, ou l'achèvement du chargement du module, puissent être signalés (ou ignorés) si nécessaire.

Vous pouvez ignorer une erreur de chargement du module de débogage qui n'est pas présente dans les builds utilisateur. Pour ignorer cet échec, définissez la propriété vendor.device.modules.ready pour déclencher les étapes ultérieures du workflow de démarrage de script init rc afin de continuer sur l'écran de lancement. Reportez-vous à l'exemple de script suivant, si vous avez le code suivant dans /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

Dans le fichier de configuration rc du matériel, le service one shot peut être spécifié avec:

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

Des optimisations supplémentaires peuvent être effectuées après le passage des modules de la première à la deuxième étape. Vous pouvez utiliser la fonctionnalité de liste de blocage modprobe pour diviser le flux de démarrage de la deuxième étape afin d'inclure le chargement différé des modules non essentiels. Le chargement des modules utilisés exclusivement par un HAL spécifique peut être différé pour ne les charger que lorsque le HAL est démarré.

Pour améliorer les temps de démarrage apparents, vous pouvez choisir spécifiquement des modules dans le service de chargement de modules qui sont plus propices au chargement après l'écran de démarrage. Par exemple, vous pouvez charger explicitement les modules pour le décodeur vidéo ou le Wi-Fi après le nettoyage du flux de démarrage d'initialisation (signal de propriété Android sys.boot_complete, par exemple). Assurez-vous que les HAL pour les modules de chargement tardif bloquent suffisamment longtemps lorsque les pilotes du noyau ne sont pas présents.

Vous pouvez également utiliser la commande wait<file>[<timeout>] d'init dans le script de démarrage rc pour attendre que certaines entrées sysfs indiquent que les modules de pilote ont terminé les opérations de sonde. Par exemple, vous pouvez attendre que le pilote d'affichage termine son chargement en arrière-plan de la récupération ou de fastbootd avant de présenter les graphiques du menu.

Initialiser la fréquence du processeur sur une valeur raisonnable dans le bootloader

Tous les SoC/produits ne peuvent pas démarrer le processeur à la fréquence la plus élevée en raison de problèmes thermiques ou d'alimentation lors des tests de boucle de démarrage. Toutefois, assurez-vous que le bootloader définit la fréquence de tous les processeurs en ligne aussi élevée que possible pour un SoC ou un produit. Cela est très important, car avec un noyau entièrement modulaire, la décompression du ramdisk d'initialisation a lieu avant que le pilote CPUfreq ne puisse être chargé. Par conséquent, si le processeur est laissé à la limite inférieure de sa fréquence par le bootloader, le temps de décompression du ramdisk peut être plus long qu'un kernel compilé de manière statique (après avoir ajusté la différence de taille du ramdisk) car la fréquence du processeur serait très faible lors d'une tâche intensive (décompression). Il en va de même pour la fréquence de la mémoire et des interconnexions.

Initialiser la fréquence du processeur des grands processeurs dans le bootloader

Avant le chargement du pilote CPUfreq, le noyau n'est pas au courant des fréquences de processeur et n'adapte pas la capacité de planification du processeur à leur fréquence actuelle. Le noyau peut migrer des threads vers le grand processeur si la charge est suffisamment élevée sur le petit processeur.

Assurez-vous que les processeurs principaux sont au moins aussi performants que les processeurs de petite taille pour la fréquence à laquelle le bootloader les laisse. Par exemple, si le grand processeur est deux fois plus performant que le petit processeur pour la même fréquence, mais que le bootloader définit la fréquence du petit processeur sur 1,5 GHz et celle du grand processeur sur 300 MHz, les performances de démarrage vont baisser si le noyau déplace un thread vers le grand processeur. Dans cet exemple, si vous pouvez démarrer le grand processeur à 750 MHz, vous devez le faire, même si vous ne prévoyez pas de l'utiliser explicitement.

Les pilotes ne doivent pas charger le micrologiciel lors de la première initialisation.

Il peut arriver que le micrologiciel doive être chargé lors de la première étape d'initialisation. Toutefois, en général, les pilotes ne doivent pas charger de micrologiciel lors de l'initialisation de la première étape, en particulier dans le contexte de la sonde de l'appareil. Le chargement du micrologiciel lors de l'initialisation de la première étape entraîne l'arrêt de l'ensemble du processus de démarrage si le micrologiciel n'est pas disponible dans le ramdisk de la première étape. Même si le micrologiciel est présent au premier étage de la RAMdisk, cela entraîne toujours un retard inutile.