Consignes concernant les modules de fournisseurs

Suivez les consignes ci-dessous pour améliorer la robustesse et la fiabilité de vos modules fournisseurs. De nombreuses consignes, lorsqu'elles sont suivies, peuvent vous aider à déterminer plus facilement l'ordre de chargement des modules et l'ordre dans lequel les pilotes doivent rechercher les appareils.

Un module peut être une bibliothèque ou un pilote.

  • Les modules de bibliothèque sont des bibliothèques qui fournissent des API à d'autres modules. Ces modules ne sont généralement pas spécifiques au matériel. Parmi les exemples de modules de bibliothèque, on peut citer un module de chiffrement AES, le framework remoteproc compilé en tant que module et un module de tampon de journalisation. Le code du module dans module_init() s'exécute pour configurer les structures de données, mais aucun autre code ne s'exécute, sauf s'il est déclenché par un module externe.

  • Les modules de pilote sont des pilotes qui recherchent ou se lient à un type d'appareil spécifique. Ces modules sont spécifiques au matériel. Les modules de pilote incluent, par exemple, le matériel d'encodeur UART, PCIe et vidéo. Les modules de pilote ne s'activent que lorsque l'appareil associé est présent sur le système.

    • Si l'appareil n'est pas présent, le seul code de module qui s'exécute est le code module_init() qui enregistre le pilote auprès du framework Driver Core.

    • Si l'appareil est présent et que le pilote recherche ou se lie correctement à cet appareil, d'autres codes de module peuvent s'exécuter.

Utiliser correctement l'initialisation et la sortie du module

Les modules de pilote doivent enregistrer un pilote dans module_init() et annuler l'enregistrement d'un pilote dans module_exit(). Pour appliquer ces restrictions, vous pouvez utiliser des macros d'encapsulation, ce qui évite l'utilisation directe des macros module_init(), *_initcall() ou module_exit().

  • Pour les modules qui peuvent être déchargés, utilisez module_subsystem_driver(). Exemples : module_platform_driver(), module_i2c_driver() et module_pci_driver().

  • Pour les modules qui ne peuvent pas être déchargés, utilisez builtin_subsystem_driver(). Exemples : builtin_platform_driver(), builtin_i2c_driver() et builtin_pci_driver().

Certains modules de pilote utilisent module_init() et module_exit(), car ils enregistrent plusieurs pilotes. Pour un module de pilote qui utilise module_init() et module_exit() pour enregistrer plusieurs pilotes, essayez de combiner les pilotes en un seul. Par exemple, vous pouvez faire la différence en utilisant la chaîne compatible ou les données auxiliaires de l'appareil au lieu d'enregistrer des pilotes distincts. Vous pouvez également diviser le module de pilote en deux modules.

Exceptions des fonctions d'initialisation et de sortie

Les modules de bibliothèque n'enregistrent pas les pilotes et sont exemptés des restrictions sur module_init() et module_exit(), car ils peuvent avoir besoin de ces fonctions pour configurer des structures de données, des files d'attente de tâches ou des threads de noyau.

Utiliser la macro MODULE_DEVICE_TABLE

Les modules de pilote doivent inclure la macro MODULE_DEVICE_TABLE, qui permet à l'espace utilisateur de déterminer les appareils compatibles avec un module de pilote avant de charger le module. Android peut utiliser ces données pour optimiser le chargement des modules, par exemple pour éviter de charger des modules pour des appareils qui ne sont pas présents dans le système. Pour obtenir des exemples d'utilisation de la macro, consultez le code en amont.

Éviter les incohérences de CRC dues aux types de données déclarés en amont

N'incluez pas de fichiers d'en-tête pour obtenir de la visibilité sur les types de données déclarés à l'avance. Certaines structures, unions et autres types de données définis dans un fichier d'en-tête (header-A.h) peuvent être déclarés de manière anticipée dans un autre fichier d'en-tête (header-B.h) qui utilise généralement des pointeurs vers ces types de données. Ce modèle de code signifie que le noyau tente intentionnellement de garder la structure de données privée pour les utilisateurs de header-B.h.

Les utilisateurs de header-B.h ne doivent pas inclure header-A.h pour accéder directement aux éléments internes de ces structures de données déclarées à l'avance. Cela entraîne des problèmes d'incompatibilité de CRC CONFIG_MODVERSIONS (qui génèrent des problèmes de conformité ABI) lorsqu'un autre noyau (tel que le noyau GKI) tente de charger le module.

Par exemple, struct fwnode_handle est défini dans include/linux/fwnode.h, mais est déclaré en tant que struct fwnode_handle; dans include/linux/device.h, car le noyau tente de préserver la confidentialité des détails de struct fwnode_handle pour les utilisateurs de include/linux/device.h. Dans ce scénario, n'ajoutez pas #include <linux/fwnode.h> dans un module pour accéder aux membres de struct fwnode_handle. Toute conception dans laquelle vous devez inclure de tels fichiers d'en-tête indique un mauvais modèle de conception.

N'accédez pas directement aux structures de noyau principales

L'accès direct aux structures de données du noyau ou leur modification peuvent entraîner un comportement indésirable, y compris des fuites de mémoire, des plantages et une incompatibilité avec les futures versions du noyau. Une structure de données est une structure de données de noyau principal lorsqu'elle remplit l'une des conditions suivantes :

  • La structure de données est définie sous KERNEL-DIR/include/. Par exemple, struct device et struct dev_links_info. Les structures de données définies dans include/linux/soc sont exemptées.

  • La structure de données est allouée ou initialisée par le module, mais elle est rendue visible par le noyau en étant transmise, indirectement (par le biais d'un pointeur dans une structure) ou directement, en tant qu'entrée dans une fonction exportée par le noyau. Par exemple, un module de pilote cpufreq initialise le struct cpufreq_driver, puis le transmet en entrée à cpufreq_register_driver(). Après ce point, le module de pilote cpufreq ne doit pas modifier struct cpufreq_driver directement, car l'appel de cpufreq_register_driver() rend struct cpufreq_driver visible par le noyau.

  • La structure de données n'est pas initialisée par votre module. Par exemple, struct regulator_dev renvoyé par regulator_register().

N'accédez aux structures de données du noyau principal que par le biais de fonctions exportées par le noyau ou de paramètres explicitement transmis en entrée aux hooks du fournisseur. Si vous ne disposez pas d'un hook d'API ou de fournisseur pour modifier des parties d'une structure de données du noyau, c'est probablement intentionnel et vous ne devez pas modifier la structure de données à partir des modules. Par exemple, ne modifiez aucun champ dans struct device ni struct device.links.

  • Pour modifier device.devres_head, utilisez une fonction devm_*() telle que devm_clk_get(), devm_regulator_get() ou devm_kzalloc().

  • Pour modifier les champs dans struct device.links, utilisez une API de liaison d'appareils telle que device_link_add() ou device_link_del().

Ne pas analyser les nœuds devicetree avec la propriété compatible

Si un nœud d'arborescence de périphériques (DT) possède une propriété compatible, un struct device lui est attribué automatiquement ou lorsque of_platform_populate() est appelé sur le nœud DT parent (généralement par le pilote de périphérique du périphérique parent). L'attente par défaut (à l'exception de certains appareils initialisés tôt pour le planificateur) est qu'un nœud DT avec une propriété compatible possède un struct device et un pilote de périphérique correspondant. Toutes les autres exceptions sont déjà gérées par le code en amont.

De plus, fw_devlink (anciennement of_devlink) considère que les nœuds DT avec la propriété compatible sont des appareils avec un struct device alloué qui est sondé par un pilote. Si un nœud DT possède une propriété compatible, mais que le struct device alloué n'est pas sondé, fw_devlink peut empêcher ses appareils consommateurs de sonder ou peut empêcher les appels sync_state() d'être appelés pour ses appareils fournisseurs.

Si votre pilote utilise une fonction of_find_*() (telle que of_find_node_by_name() ou of_find_compatible_node()) pour trouver directement un nœud DT qui possède une propriété compatible, puis analyser ce nœud DT, corrigez le module en écrivant un pilote de périphérique qui peut sonder l'appareil ou supprimer la propriété compatible (possible uniquement si elle n'a pas été transmise en amont). Pour discuter d'alternatives, contactez l'équipe du noyau Android à l'adresse kernel-team@android.com et préparez-vous à justifier vos cas d'utilisation.

Utiliser des handles DT pour rechercher des fournisseurs

Dans la mesure du possible, faites référence à un fournisseur à l'aide d'un phandle (référence ou pointeur vers un nœud DT) dans DT. L'utilisation de liaisons DT et de phandles standards pour faire référence aux fournisseurs permet à fw_devlink (anciennement of_devlink) de déterminer automatiquement les dépendances entre les appareils en analysant le DT au moment de l'exécution. Le noyau peut alors sonder automatiquement les appareils dans le bon ordre, ce qui élimine le besoin d'ordonnancement du chargement des modules ou de MODULE_SOFTDEP().

Ancien scénario (pas de prise en charge DT dans le noyau ARM)

Auparavant, avant l'ajout de la prise en charge de DT aux noyaux ARM, les consommateurs tels que les appareils tactiles recherchaient les fournisseurs tels que les régulateurs à l'aide de chaînes uniques au niveau mondial. Par exemple, le pilote ACME PMIC peut enregistrer ou annoncer plusieurs régulateurs (tels que acme-pmic-ldo1 à acme-pmic-ldo10), et un pilote tactile peut rechercher un régulateur à l'aide de regulator_get(dev, "acme-pmic-ldo10"). Toutefois, sur une autre carte, le LDO8 peut alimenter l'appareil tactile, ce qui crée un système complexe où le même pilote tactile doit déterminer la chaîne de recherche correcte pour le régulateur pour chaque carte sur laquelle l'appareil tactile est utilisé.

Scénario actuel (compatibilité DT dans le noyau ARM)

Une fois la prise en charge du DT ajoutée aux noyaux ARM, les consommateurs peuvent identifier les fournisseurs dans le DT en se référant au nœud de l'arborescence des périphériques du fournisseur à l'aide d'un phandle. Les consommateurs peuvent également nommer la ressource en fonction de son utilisation plutôt que de son fournisseur. Par exemple, le pilote tactile de l'exemple précédent pourrait utiliser regulator_get(dev, "core") et regulator_get(dev, "sensor") pour obtenir les fournisseurs qui alimentent le cœur et le capteur de l'appareil tactile. Le DT associé à un tel appareil ressemble à l'exemple de code suivant :

touch-device {
    compatible = "fizz,touch";
    ...
    core-supply = <&acme_pmic_ldo4>;
    sensor-supply = <&acme_pmic_ldo10>;
};

acme-pmic {
    compatible = "acme,super-pmic";
    ...
    acme_pmic_ldo4: ldo4 {
        ...
    };
    ...
    acme_pmic_ldo10: ldo10 {
        ...
    };
};

Scénario du pire

Certains pilotes portés depuis d'anciens noyaux incluent un comportement hérité dans le DT qui prend la pire partie de l'ancien schéma et la force sur le nouveau schéma qui est censé faciliter les choses. Dans ces pilotes, le pilote consommateur lit la chaîne à utiliser pour la recherche à l'aide d'une propriété DT spécifique à l'appareil, le fournisseur utilise une autre propriété spécifique au fournisseur pour définir le nom à utiliser pour l'enregistrement de la ressource du fournisseur, puis le consommateur et le fournisseur continuent d'utiliser l'ancien schéma d'utilisation de chaînes pour rechercher le fournisseur. Dans ce scénario, qui combine le pire des deux mondes :

  • Le pilote tactile utilise un code semblable à celui-ci :

    str = of_property_read(np, "fizz,core-regulator");
    core_reg = regulator_get(dev, str);
    str = of_property_read(np, "fizz,sensor-regulator");
    sensor_reg = regulator_get(dev, str);
    
  • Le DT utilise un code semblable à ce qui suit :

    touch-device {
      compatible = "fizz,touch";
      ...
      fizz,core-regulator = "acme-pmic-ldo4";
      fizz,sensor-regulator = "acme-pmic-ldo4";
    };
    acme-pmic {
      compatible = "acme,super-pmic";
      ...
      ldo4 {
        regulator-name = "acme-pmic-ldo4"
        ...
      };
      ...
      acme_pmic_ldo10: ldo10 {
        ...
        regulator-name = "acme-pmic-ldo10"
      };
    };
    

Ne modifiez pas les erreurs d'API du framework

Les API du framework, telles que regulator, clocks, irq, gpio, phys et extcon, renvoient -EPROBE_DEFER comme valeur de retour d'erreur pour indiquer qu'un appareil tente de sonder, mais qu'il ne peut pas le faire pour le moment, et que le noyau doit réessayer plus tard. Pour vous assurer que la fonction .probe() de votre appareil échoue comme prévu dans de tels cas, ne remplacez ni ne remappez la valeur d'erreur. Le remplacement ou le remappage de la valeur d'erreur peut entraîner la suppression de -EPROBE_DEFER et empêcher la détection de votre appareil.

Utiliser les variantes de l'API devm_*()

Lorsque l'appareil acquiert une ressource à l'aide d'une API devm_*(), la ressource est automatiquement libérée par le noyau si l'appareil ne parvient pas à sonder, ou s'il sonde avec succès et est ensuite dissocié. Cette fonctionnalité rend le code de gestion des erreurs dans la fonction probe() plus propre, car elle ne nécessite pas de sauts goto pour libérer les ressources acquises par devm_*() et simplifie les opérations de dissociation du pilote.

Gérer la dissociation du pilote de périphérique

Soyez intentionnel lorsque vous dissociez des pilotes d'appareil et ne laissez pas la dissociation indéfinie, car cela n'implique pas que la dissociation est interdite. Vous devez soit implémenter complètement la dissociation du pilote de périphérique, soit désactiver explicitement la dissociation du pilote de périphérique.

Implémenter la dissociation du pilote de périphérique

Lorsque vous choisissez d'implémenter entièrement l'annulation de la liaison du pilote de périphérique, annulez la liaison des pilotes de périphérique de manière propre pour éviter les fuites de mémoire ou de ressources, ainsi que les problèmes de sécurité. Vous pouvez associer un appareil à un pilote en appelant la fonction probe() d'un pilote et dissocier un appareil en appelant la fonction remove() du pilote. Si aucune fonction remove() n'existe, le noyau peut toujours dissocier l'appareil. Le pilote principal suppose qu'aucun nettoyage n'est nécessaire de la part du pilote lorsqu'il se dissocie de l'appareil. Un pilote non lié à un appareil n'a pas besoin d'effectuer de nettoyage explicite lorsque les deux conditions suivantes sont remplies :

  • Toutes les ressources acquises par la fonction probe() d'un pilote le sont via les API devm_*().

  • L'appareil n'a pas besoin d'une séquence d'arrêt ou de mise au repos.

Dans ce cas, le cœur du pilote gère la libération de toutes les ressources acquises via les API devm_*(). Si l'une des affirmations précédentes est fausse, le pilote doit effectuer un nettoyage (libérer les ressources et arrêter ou mettre en veille le matériel) lorsqu'il se dissocie d'un appareil. Pour vous assurer qu'un appareil peut dissocier un module de pilote de manière propre, utilisez l'une des options suivantes :

  • Si le matériel n'a pas besoin d'une séquence d'arrêt ou de mise au repos, modifiez le module de l'appareil pour acquérir des ressources à l'aide des API devm_*().

  • Implémentez l'opération de pilote remove() dans la même structure que la fonction probe(), puis effectuez les étapes de nettoyage à l'aide de la fonction remove().

Désactiver explicitement la dissociation du pilote de périphérique (non recommandé)

Lorsque vous choisissez de désactiver explicitement la dissociation des pilotes de périphérique, vous devez interdire la dissociation et interdire le déchargement du module.

  • Pour interdire le détachement, définissez l'indicateur suppress_bind_attrs sur true dans le fichier struct device_driver du pilote. Ce paramètre empêche l'affichage des fichiers bind et unbind dans le répertoire sysfs du pilote. Le fichier unbind permet à l'espace utilisateur de déclencher la dissociation d'un pilote de son appareil.

  • Pour interdire le déchargement du module, assurez-vous que le module comporte [permanent] dans lsmod. Si vous n'utilisez pas module_exit() ni module_XXX_driver(), le module est marqué comme [permanent].

Ne chargez pas le micrologiciel depuis la fonction de sonde

Le pilote ne doit pas charger le micrologiciel à partir de la fonction .probe(), car il est possible qu'il n'y ait pas accès si le pilote sonde avant le montage du système de fichiers basé sur la mémoire flash ou le stockage permanent. Dans ce cas, l'API request_firmware*() peut se bloquer pendant une longue période, puis échouer, ce qui peut ralentir inutilement le processus de démarrage. Reportez plutôt le chargement du micrologiciel au moment où un client commence à utiliser l'appareil. Par exemple, un pilote d'affichage peut charger le micrologiciel lorsque l'appareil d'affichage est ouvert.

L'utilisation de .probe() pour charger le micrologiciel peut être acceptable dans certains cas, par exemple dans un pilote d'horloge qui a besoin d'un micrologiciel pour fonctionner, mais l'appareil n'est pas exposé à l'espace utilisateur. D'autres cas d'utilisation appropriés sont possibles.

Implémenter le probing asynchrone

Prend en charge et utilise le probing asynchrone pour profiter des futures améliorations, telles que le chargement de modules en parallèle ou le probing d'appareils pour accélérer le temps de démarrage, qui pourraient être ajoutées à Android dans les prochaines versions. Les modules de pilote qui n'utilisent pas le sondage asynchrone peuvent réduire l'efficacité de ces optimisations.

Pour indiquer qu'un pilote prend en charge et préfère le sondage asynchrone, définissez le champ probe_type dans le membre struct device_driver du pilote. L'exemple suivant montre comment activer cette prise en charge pour un pilote de plate-forme :

static struct platform_driver acme_driver = {
        .probe          = acme_probe,
        ...
        .driver         = {
                .name   = "acme",
                ...
                .probe_type = PROBE_PREFER_ASYNCHRONOUS,
        },
};

Pour qu'un pilote fonctionne avec l'analyse asynchrone, aucun code spécial n'est requis. Toutefois, gardez à l'esprit les points suivants lorsque vous ajoutez la prise en charge des tests asynchrones.

  • Ne faites pas d'hypothèses sur les dépendances précédemment analysées. Vérifiez directement ou indirectement (la plupart des appels de framework) et renvoyez -EPROBE_DEFER si un ou plusieurs fournisseurs ne sont pas encore prêts.

  • Si vous ajoutez des appareils enfants dans la fonction de recherche d'un appareil parent, ne supposez pas que les appareils enfants sont recherchés immédiatement.

  • Si une vérification échoue, gérez correctement les erreurs et effectuez un nettoyage (voir Utiliser les variantes de l'API devm_*()).

N'utilisez pas MODULE_SOFTDEP pour ordonner les sondes de l'appareil

La fonction MODULE_SOFTDEP() n'est pas une solution fiable pour garantir l'ordre des sondes d'appareil et ne doit pas être utilisée pour les raisons suivantes.

  • Sonde différée. Lorsqu'un module se charge, la sonde de l'appareil peut être différée, car l'un de ses fournisseurs n'est pas prêt. Cela peut entraîner une incohérence entre l'ordre de chargement des modules et l'ordre de sondage des appareils.

  • Un seul pilote, plusieurs appareils. Un module de pilote peut gérer un type d'appareil spécifique. Si le système inclut plusieurs instances d'un type d'appareil et que ces appareils ont chacun une exigence d'ordre de sonde différente, vous ne pouvez pas respecter ces exigences à l'aide de l'ordre de chargement des modules.

  • Sondage asynchrone : Les modules de pilote qui effectuent un sondage asynchrone ne sondent pas immédiatement un appareil lorsque le module est chargé. Au lieu de cela, un thread parallèle gère le sondage des appareils, ce qui peut entraîner une incohérence entre l'ordre de chargement des modules et l'ordre de sondage des appareils. Par exemple, lorsqu'un module de pilote principal I2C effectue un sondage asynchrone et qu'un module de pilote tactile dépend du PMIC qui se trouve sur le bus I2C, même si le pilote tactile et le pilote PMIC se chargent dans le bon ordre, le sondage du pilote tactile peut être tenté avant le sondage du pilote PMIC.

Si vous avez des modules de pilote qui utilisent la fonction MODULE_SOFTDEP(), corrigez-les pour qu'ils n'utilisent pas cette fonction. Pour vous aider, l'équipe Android a transféré les modifications qui permettent au noyau de gérer les problèmes d'ordre sans utiliser MODULE_SOFTDEP(). Plus précisément, vous pouvez utiliser fw_devlink pour assurer l'ordre de sondage et (après que tous les consommateurs d'un appareil ont sondé) utiliser le rappel sync_state() pour effectuer les tâches nécessaires.

Utiliser #if IS_ENABLED() au lieu de #ifdef pour les configurations

Utilisez #if IS_ENABLED(CONFIG_XXX) au lieu de #ifdef CONFIG_XXX pour vous assurer que le code à l'intérieur du bloc #if continue de se compiler si la configuration passe à une configuration à trois états à l'avenir. Les différences sont les suivantes :

  • #if IS_ENABLED(CONFIG_XXX) renvoie true lorsque CONFIG_XXX est défini sur "module" (=m) ou "intégré" (=y).

  • #ifdef CONFIG_XXX est évalué sur true lorsque CONFIG_XXX est défini sur "intégré" (=y) , mais pas lorsque CONFIG_XXX est défini sur "module" (=m). N'utilisez cette option que si vous êtes certain de vouloir faire la même chose lorsque la configuration est définie sur "module" ou est désactivée.

Utiliser la macro appropriée pour les compilations conditionnelles

Si un CONFIG_XXX est défini sur module (=m), le système de compilation définit automatiquement CONFIG_XXX_MODULE. Si votre pilote est contrôlé par CONFIG_XXX et que vous souhaitez vérifier s'il est compilé en tant que module, suivez les consignes ci-dessous :

  • Dans le fichier C (ou tout fichier source qui n'est pas un fichier d'en-tête) de votre pilote, n'utilisez pas #ifdef CONFIG_XXX_MODULE, car il est inutilement restrictif et ne fonctionne pas si la configuration est renommée CONFIG_XYZ. Pour tout fichier source autre qu'un fichier d'en-tête compilé dans un module, le système de compilation définit automatiquement MODULE pour la portée de ce fichier. Par conséquent, pour vérifier si un fichier C (ou tout fichier source autre qu'un fichier d'en-tête) est compilé dans un module, utilisez #ifdef MODULE (sans le préfixe CONFIG_).

  • Dans les fichiers d'en-tête, la même vérification est plus délicate, car les fichiers d'en-tête ne sont pas compilés directement dans un fichier binaire, mais plutôt dans le cadre d'un fichier C (ou d'autres fichiers sources). Utilisez les règles suivantes pour les fichiers d'en-tête :

    • Pour un fichier d'en-tête qui utilise #ifdef MODULE, le résultat change en fonction du fichier source qui l'utilise. Cela signifie que le même fichier d'en-tête dans la même compilation peut avoir différentes parties de son code compilées pour différents fichiers sources (module par rapport à intégré ou désactivé). Cela peut être utile lorsque vous souhaitez définir une macro qui doit se développer d'une manière pour le code intégré et d'une autre pour un module.

    • Pour qu'un fichier d'en-tête soit compilé dans un élément de code lorsqu'un CONFIG_XXX spécifique est défini sur "module" (que le fichier source l'incluant soit un module ou non), le fichier d'en-tête doit utiliser #ifdef CONFIG_XXX_MODULE.