Directives du module fournisseur

Utilisez les directives suivantes pour augmenter la robustesse et la fiabilité de vos modules fournisseur. De nombreuses directives, lorsqu'elles sont suivies, peuvent aider à déterminer plus facilement l'ordre correct de chargement des modules et l'ordre dans lequel les pilotes doivent rechercher les périphériques.

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

  • Les modules de bibliothèque sont des bibliothèques qui fournissent des API que d'autres modules peuvent utiliser. Ces modules ne sont généralement pas spécifiques au matériel. Des exemples de modules de bibliothèque incluent un module de chiffrement AES, le framework remoteproc compilé en tant que module et un module logbuffer. 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 à moins d'être déclenché par un module externe.

  • Les modules de pilotes sont des pilotes qui recherchent ou se lient à un type spécifique de périphérique. Ces modules sont spécifiques au matériel. Des exemples de modules de pilotes incluent le matériel UART, PCIe et l'encodeur vidéo. Les modules pilotes s'activent uniquement lorsque leur périphérique associé est présent sur le système.

    • Si le périphérique 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 principal du pilote.

    • Si le périphérique est présent et que le pilote réussit à rechercher ou à se lier à ce périphérique, un autre code de module peut s'exécuter.

Utiliser correctement l'initialisation/la sortie du module

Les modules pilotes doivent enregistrer un pilote dans module_init() et désenregistrer un pilote dans module_exit() . Un moyen simple d'appliquer ces restrictions consiste à utiliser des macros wrapper, 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 pilotes utilisent module_init() et module_exit() car ils enregistrent plusieurs pilotes. Pour un module pilote qui utilise module_init() et module_exit() pour enregistrer plusieurs pilotes, essayez de combiner les pilotes en un seul pilote. Par exemple, vous pouvez différencier en utilisant la chaîne compatible ou les données auxiliaires du périphérique au lieu d'enregistrer des pilotes distincts. Vous pouvez également diviser le module pilote en deux modules.

Exceptions des fonctions d'initialisation et de sortie

Les modules de bibliothèque n'enregistrent pas de 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 travail ou des threads du noyau.

Utilisez la macro MODULE_DEVICE_TABLE

Les modules pilotes doivent inclure la macro MODULE_DEVICE_TABLE , qui permet à l'espace utilisateur de déterminer les périphériques pris en charge par un module 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, reportez-vous au code en amont.

Évitez les incompatibilités CRC dues aux types de données déclarés en avant

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

Les utilisateurs de header-Bh ne doivent pas inclure header-Ah pour accéder directement aux éléments internes de ces structures de données déclarées vers l'avant. Cela entraîne des problèmes de non-concordance CONFIG_MODVERSIONS CRC (qui génèrent des problèmes de conformité ABI) lorsqu'un noyau différent (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 avant comme struct fwnode_handle; dans include/linux/device.h car le noyau essaie de garder les détails de struct fwnode_handle privés des 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 principales du noyau

L'accès direct ou la modification des structures de données principales du noyau peut entraîner des comportements indésirables, notamment des fuites de mémoire, des plantages et une compatibilité rompue avec les futures versions du noyau. Une structure de données est une structure de données centrale du noyau lorsqu'elle remplit l'une des conditions suivantes :

  • La structure des 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 est rendue visible au noyau en étant transmise, indirectement (via un pointeur dans une structure) ou directement, en entrée dans une fonction exportée par le noyau. Par exemple, un module pilote cpufreq initialise la struct cpufreq_driver puis la transmet en entrée à cpufreq_register_driver() . Après ce point, le module pilote cpufreq ne devrait pas modifier directement struct cpufreq_driver car l'appel de cpufreq_register_driver() rend struct cpufreq_driver visible pour le noyau.

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

Accédez aux structures de données principales du noyau uniquement via des fonctions exportées par le noyau ou via des paramètres explicitement transmis en entrée aux hooks du fournisseur. Si vous ne disposez pas d'une API ou d'un hook de fournisseur pour modifier des parties d'une structure de données de base du noyau, c'est probablement intentionnel et vous ne devriez pas modifier la structure de données à partir des modules. Par exemple, ne modifiez aucun champ à l’intérieur struct device ou 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 à l'intérieur struct device.links , utilisez une API de lien de périphérique telle que device_link_add() ou device_link_del() .

N'analysez pas les nœuds Devicetree avec une propriété compatible

Si un nœud d'arborescence de périphériques (DT) a une propriété compatible , un struct device lui est automatiquement alloué 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 (sauf pour certains périphériques 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 appelé of_devlink ) considère les nœuds DT avec la propriété compatible comme des périphériques dotés d'un struct device alloué qui est sondé par un pilote. Si un nœud DT a une propriété compatible mais que le struct device alloué n'est pas sondé, fw_devlink pourrait empêcher ses appareils consommateurs de sonder ou pourrait 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 rechercher directement un nœud DT qui a une propriété compatible , puis analyser ce nœud DT, corrigez le module en écrivant un pilote de périphérique capable de sonder l'appareil ou supprimer la propriété compatible (possible uniquement si elle n'a pas été mise en amont). Pour discuter d'alternatives, contactez l'équipe du noyau Android à l'adresse kernel-team@android.com et soyez prêt à justifier vos cas d'utilisation.

Utilisez les poignées DT pour rechercher des fournisseurs

Référez-vous à un fournisseur utilisant un phandle (une référence/un pointeur vers un nœud DT) dans DT autant que possible. L'utilisation de liaisons et de phhandles DT standard pour faire référence aux fournisseurs permet à fw_devlink (anciennement of_devlink ) de déterminer automatiquement les dépendances inter-appareils en analysant la DT au moment de l'exécution. Le noyau peut alors automatiquement sonder les périphériques dans le bon ordre, éliminant ainsi le besoin d'ordonner la charge des modules ou MODULE_SOFTDEP() .

Scénario hérité (pas de prise en charge DT dans le noyau ARM)

Auparavant, avant que la prise en charge de DT ne soit ajoutée aux noyaux ARM, les consommateurs tels que les appareils tactiles recherchaient des fournisseurs tels que les régulateurs en utilisant des chaînes uniques au monde. Par exemple, le pilote ACME PMIC pourrait enregistrer ou annoncer plusieurs régulateurs (tels que acme-pmic-ldo1 à acme-pmic-ldo10 ) et un pilote tactile pourrait rechercher un régulateur à l'aide regulator_get(dev, "acme-pmic-ldo10") . Cependant, sur une carte différente, le LDO8 peut alimenter le périphérique tactile, créant ainsi un système encombrant dans lequel le même pilote tactile doit déterminer la chaîne de recherche correcte pour le régulateur de chaque carte sur laquelle le périphérique tactile est utilisé.

Scénario actuel (prise en charge de DT dans le noyau ARM)

Une fois la prise en charge de DT ajoutée aux noyaux ARM, les consommateurs peuvent identifier les fournisseurs dans la 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 celui qui la fournit. 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 est similaire à 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 {
        ...
    };
};

Le pire des deux mondes

Certains pilotes portés à partir d'anciens noyaux incluent un comportement hérité dans le DT qui reprend la pire partie du schéma hérité et l'impose au schéma plus récent destiné à faciliter les choses. Dans de tels pilotes, le pilote consommateur lit la chaîne à utiliser pour la recherche à l'aide d'une propriété DT spécifique au périphérique, le fournisseur utilise une autre propriété spécifique au fournisseur pour définir le nom à utiliser pour enregistrer la ressource fournisseur, puis le consommateur et le fournisseur continuent à utiliser le même vieux schéma consistant à utiliser des chaînes pour rechercher le fournisseur. Dans ce pire des deux mondes :

  • Le pilote tactile utilise un code similaire au code suivant :

    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 similaire au suivant :

    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 pas modifier les erreurs de l'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 périphérique tente d'effectuer une sonde mais ne peut pas le faire pour le moment, et que le noyau doit réessayer la sonde. plus tard. Pour vous assurer que la fonction .probe() de votre appareil échoue comme prévu dans de tels cas, ne remplacez ni ne remapper 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 que votre appareil ne soit sondé.

Utiliser les variantes de l'API devm_*()

Lorsque le périphérique acquiert une ressource à l'aide d'une API devm_*() , la ressource est automatiquement libérée par le noyau si le périphérique ne parvient pas à tester, ou si la sonde réussit et est ensuite dissociée. 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 en ce qui concerne la dissociation des pilotes de périphérique et ne laissez pas la dissociation indéfinie, car indéfini n'implique pas non plus. Vous devez soit implémenter entièrement la dissociation du pilote de périphérique , soit désactiver explicitement la dissociation du pilote de périphérique.

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

Lorsque vous choisissez d'implémenter entièrement la dissociation des pilotes de périphérique, dissociez les pilotes de périphérique proprement pour éviter les fuites de mémoire ou de ressources et les problèmes de sécurité. Vous pouvez lier un périphérique à un pilote en appelant la fonction probe() d'un pilote et dissocier un périphérique en appelant la fonction remove() du pilote. Si aucune fonction remove() n'existe, le noyau peut toujours dissocier le périphérique ; le noyau du pilote suppose qu'aucun travail de nettoyage n'est nécessaire de la part du pilote lorsqu'il se dissocie du périphérique. Un pilote qui n'est pas lié à un périphérique n'a pas besoin d'effectuer de travail de nettoyage explicite lorsque les deux conditions suivantes sont vraies :

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

  • Le périphérique matériel n'a pas besoin d'une séquence d'arrêt ou de mise au repos.

Dans cette situation, le noyau 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 le matériel au repos) lorsqu'il se dissocie d'un périphérique. Pour garantir qu'un périphérique peut dissocier proprement un module pilote, 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 périphérique pour acquérir des ressources à l'aide des API devm_*() .

  • Implémentez l'opération du 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 du pilote de périphérique, vous devez interdire la dissociation et interdire le déchargement du module.

  • Pour interdire la dissociation, définissez l'indicateur suppress_bind_attrs sur true dans la struct device_driver ; ce paramètre empêche les fichiers bind et unbind de s'afficher dans le répertoire sysfs du pilote. Le fichier unbind est ce qui permet à l'espace utilisateur de déclencher la dissociation d'un pilote de son périphérique.

  • Pour interdire le déchargement du module, assurez-vous que le module a [permanent] dans lsmod . En n'utilisant pas module_exit() ou module_XXX_driver() , le module est marqué comme [permanent] .

Ne chargez pas le firmware depuis la fonction de sonde

Le pilote ne doit pas charger le micrologiciel à partir de la fonction .probe() , car il pourrait ne pas avoir accès au micrologiciel si le pilote sonde avant le montage du système de fichiers basé sur le stockage flash ou permanent. Dans de tels 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 le périphérique d'affichage est ouvert.

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

Implémenter un sondage asynchrone

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

Pour marquer un pilote comme prenant en charge et préférant le sondage asynchrone, définissez le champ probe_type dans le membre struct device_driver du pilote. L'exemple suivant montre une telle prise en charge activée pour un pilote de plate-forme :

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

Faire fonctionner un pilote avec une sonde asynchrone ne nécessite pas de code spécial. Cependant, gardez les points suivants à l’esprit lors de l’ajout de la prise en charge du sondage asynchrone.

  • Ne faites pas d'hypothèses sur les dépendances précédemment sondé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 sonde d’un appareil parent, ne présumez pas que les appareils enfants sont immédiatement sondés.

  • Si une sonde échoue, effectuez une gestion des erreurs et un nettoyage appropriés (voir Utiliser les variantes de l'API devm_*() ).

N'utilisez pas MODULE_SOFTDEP pour commander des sondes d'appareil

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

  • Sonde différée. Lors du chargement d'un module, la vérification du périphérique peut être différée car l'un de ses fournisseurs n'est pas prêt. Cela peut entraîner une inadéquation entre l'ordre de chargement du module et l'ordre de sonde du périphérique.

  • Un pilote, plusieurs appareils. Un module pilote peut gérer un type de périphérique spécifique. Si le système comprend plusieurs instances d'un type de périphérique et que ces périphériques 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 pilotes qui effectuent une sonde asynchrone ne sondent pas immédiatement un périphérique lorsque le module est chargé. Au lieu de cela, un thread parallèle gère le sondage du périphérique, ce qui peut entraîner une inadéquation entre l'ordre de chargement du module et l'ordre de sondage du périphérique. Par exemple, lorsqu'un module pilote principal I2C effectue une sonde asynchrone et qu'un module 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, la sonde du pilote tactile peut être tentée avant la sonde du pilote PMIC.

Si vous avez des modules pilotes utilisant la fonction MODULE_SOFTDEP() , corrigez-les afin qu'ils n'utilisent pas cette fonction. Pour vous aider, l'équipe Android a apporté des modifications en amont 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 garantir l'ordre des sondes et (une fois que tous les consommateurs d'un périphérique ont sondé) utiliser le rappel sync_state() pour effectuer toutes les tâches nécessaires.

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

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

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

  • #ifdef CONFIG_XXX est évalué à true lorsque CONFIG_XXX est défini sur intégré ( =y ) , mais ce n'est pas le cas lorsque CONFIG_XXX est défini sur module ( =m ). Utilisez-le uniquement lorsque vous êtes certain de vouloir faire la même chose lorsque la configuration est définie sur module ou est désactivée.

Utilisez la macro correcte pour les compilations conditionnelles

Si un CONFIG_XXX est défini sur module ( =m ), le système de construction définit automatiquement CONFIG_XXX_MODULE . Si votre pilote est contrôlé par CONFIG_XXX et que vous souhaitez vérifier si votre pilote est compilé en tant que module, utilisez les directives suivantes :

  • 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 s'interrompt si la configuration est renommée en CONFIG_XYZ . Pour tout fichier source sans en-tête compilé dans un module, le système de construction 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 sans en-tête) est compilé dans le cadre d'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 binaire mais plutôt dans le cadre d'un fichier C (ou d'autres fichiers source). 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 version peut avoir différentes parties de son code compilées pour différents fichiers source (module versus intégré ou désactivé). Cela peut être utile lorsque vous souhaitez définir une macro qui doit se développer dans un sens pour le code intégré et dans un sens différent pour un module.

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