Semaine Athens Linux Device Drivers


Introduction

Vous allez maintenant écrire votre premier module.

Réflexion

  1. Quels sont les avantages de l'utilisation d'un module plutôt que du code compilé en dur directement dans l'image du noyau ?

Votre premier module

Code

Voici le code de votre premier module.

/* first.c */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

static int __init first_init(void)
{
  pr_info("Hello world!\n");
  return 0;
}

static void __exit first_exit(void)
{
  pr_info("Bye\n");
}

module_init(first_init);
module_exit(first_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("My first module");
MODULE_AUTHOR("The Doctor");

Ce module ne fait pas grand chose, à l'exception d'afficher un message lors de son chargement et un autre lors de son déchargement.

Makefile

Voici le fichier Makefile nécessaire pour compiler le module :

ifneq ($(KERNELRELEASE),)
# kbuild part of makefile
obj-m  := first.o

else
# normal makefile
KDIR ?= /lib/modules/`uname -r`/build

default:
    $(MAKE) -C $(KDIR) M=$$PWD
endif

Attention ! La ligne $(MAKE) -C... qui indique la commande à exécuter pour construire la cible default doit être indentée à l'aide d'une tabulation et non d'un ou plusieurs espaces.

Ce Makefile est très bien adapté à la compilation d'un module depuis la machine sur lequel vous voulez l'utiliser (compilation native). Nous verrons par la suite comment l'utiliser dans le contexte de la compilation croisée (cross-compilation).

Outils de gestion des modules

Plusieurs outils vous permettent de manipuler les modules :

Travail à faire

  1. Copiez le module ci-dessus ainsi que le Makefile
  2. Compilez votre module (depuis le PC)

    Pour cette étape, vous ne pouvez pas vous contenter de taper make (cela marcherait mais il compilerait pour l'architecture que vous êtes en train d'utiliser, c'est-à-dire x86_64 et non pour l'architecture arm de la carte de TP). Il va falloir fournir trois variables particulières à make pour qu'il sache ce que l'on cherche à faire :

    $ make CROSS_COMPILE=arm-linux-gnueabihf- ARCH=arm KDIR=/home/users/XXX/linux-socfpga/build/

    La variable CROSS_COMPILE indique le préfixe des outils de la chaîne de compilation que nous utilisons (donc le gcc qui sera appelé est arm-linux-gnueabihf-gcc). La variable ARCH indique que nous compilons pour l'architecture arm. Enfin KDIR pointe vers le répertoire dans lequel le noyau a été compilé. Remplacez XXX par le répertoire dans lequel vous avez décompressé le noyau.

  3. Chargez votre module (depuis la carte). Rappel : si vous avez compilé votre module quelque part dans /home/users/XXX sur le PC, il est visible depuis la carte grâce à SSHFS
  4. Pourquoi ne se passe-t-il rien (en apparence) ?
  5. Déchargez votre module

Explications

Nous allons maintenant décortiquer le code source de notre module.

Éléments de base

Le module comprend les éléments suivants :

Les fichiers .h inclus au début du module sont spécifiques au noyau Linux. Comme nous l'avons vu, vous n'avez pas accès aux entêtes standards (stdlib.h, stdio.h...).

__init & co.

La macro __init qui décore la fonction d'initialisation, indique que cette fonction n'est appelée qu'à l'initialisation du module (au démarrage du noyau si le module est compilé en dur, au chargement du module dans le cas contraire). La zone mémoire utilisée par cette fonction peut ainsi être libérée une fois l'initialisation terminée.

Il existe une macro __initdata qui peut décorer des variables qui ne sont utilisée que lors de l'initialisation du module et donc qui peuvent être désallouées à la fin de l'initialisation.

La macro __exit indique que la fonction n'est appelée que lors du déchargement du module et donc peut ne pas être chargée en mémoire si le module est compilé en dur ou si la fonctionnalité de déchargement des module n'est pas activée.

Makefile pour la compilation de modules externes

L'utilisation d'un module externes, c'est-à-dire dont les source sont situées à l'extérieur des sources du noyau, permet un développement souvent plus aisé. Néanmoins, dans ce cas il n'est pas intégré au système de configuration et de compilation du noyau, il ne peut pas être compilé en dur dans le noyau et a besoin d'être compilé séparément.

Le Makefile présenté précédemment permet de compiler un tel module.

Toute la documentation concernant la compilation d'un module externe est disponible dans le fichier Documentation/kbuild/modules.txt.

Travail à faire

  1. Lisez la documentation et expliquez comment fonctionne le Makefile présenté plus haut

Intégration au sein des sources du noyau

Pour inclure votre pilote au sein des sources du noyau (il s'agit d'un exemple si vous aviez à la faire) :

  1. Ajoutez votre fichier source (sauf si votre code source est vraiment très gros, la pratique veut qu'il soit contenu dans un seul fichier .c) à l'endroit le plus approprié dans l'arborescence des sources du noyau (exemple : drivers/usb/misc/usbsevseg.c)
  2. Ajoutez une option de configuration pour votre pilote. Pour cela modifiez le fichier Kconfig contenu dans le répertoire choisi. Exemple :

    config USB_SEVSEG
            tristate "USB 7-Segment LED Display"
            help
              Say Y here if you have a USB 7-Segment Display by Delcom
    
              To compile this driver as a module, choose M here: the
              module will be called usbsevseg.
  3. Ajoutez la ligne suivante dans le fichier Makefile situé dans le répertoire choisi :

    obj-$(CONFIG_USB_SEVSEG) += usbsevseg.o

La syntaxe complète du fichier Kconfig est disponible dans Documentation/kbuild/kconfig-language.txt. Il est par exemple possible de faire dépendre un module d'un autre module ou d'une autre fonctionnalité.

De même, la syntaxe des Makefiles du noyau est décrite dans Documentation/kbuild/makefiles.txt. Il est notamment expliqué comment compiler un module qui dépend de plusieurs fichiers sources ou d'autres fichiers générés automatiquement.

Travail à faire

  1. Prenez le temps de parcourir les deux documents mentionnés précédemment.

Utilisation de paramètres

Passage de paramètres à un module

Il est possible de fournir des paramètres à un module. Cela peut être fait :

La valeur courante des paramètres peut être récupérée dans /sys/module/<nom_module>/parameters/. Ce répertoire contient un fichier par paramètre. Chaque fichier porte le nom du paramètre et son contenu est la valeur courante du paramètre en question.

Récupération des paramètres dans le module

/* first_params.c */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

static char *message = "Hello world!";
module_param(message, charp, S_IRUGO);
MODULE_PARM_DESC(message, "The message to print");

static int __init first_init(void)
{
  pr_info("%s\n", message);
  return 0;
}

static void __exit first_exit(void)
{
  pr_info("Bye\n");
}

module_init(first_init);
module_exit(first_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("My first module with parameters");
MODULE_AUTHOR("The Doctor");

La macro module_param (définie dans include/linux/moduleparam.h permet de déclarer un paramètre et prend trois arguments :

La macro MODULE_PARM_DESC permet d'ajouter un message explicatif pour le paramètre. Il sera visible grâce à la commande modinfo.

Il est possible d'utiliser des tableaux de paramètres à l'aide de la macro module_param_array définie dans include/linux/moduleparam.h (voir sa définition pour plus de détails).

Travail à faire

  1. Testez le module ci-dessus
  2. Modifiez-le pour tester plusieurs paramètres de types différents

Quelques fonctions utiles

Comme nous l'avons vu, vous ne pouvez pas utiliser les fonctions de la bibliothèque standard C (ou de n'importe quelle autre bibliothèque en espace utilisateur) lorsque vous développez du code pour le noyau Linux. Néanmoins, le noyau offre plusieurs fonctions utiles mimant pour la plupart celles de la bibliothèque standard.

Cette section va présenter quelques unes de ces fonctions utiles. Les fonctions liées à l'allocation mémoire et à la synchronisation seront vues ultérieurement.

Préliminaires

Pour qu'une fonction (et plus généralement un symbole) soit visible depuis un module, il faut qu'elle ai été exportée explicitement dans les sources du noyau. Cette exportation se fait à l'aide d'une des deux macros suivantes :

EXPORT_SYMBOL(x);
EXPORT_SYMBOL_GPL(x);

La première indique que le symbole x est exporté et donc visible (et utilisable) depuis les modules chargés dynamiquement. La seconde indique que le symbole est exporté uniquement vers les modules dont la licence est GPL (ou GPL + autre). Ils ne sont alors pas accessibles depuis un module dont la licence n'est pas explicitement GPL (modules propriétaires).

Les symboles non exportés du noyau ne sont pas accessibles par les modules.

Un module peut lui aussi exporter un symbole à l'aide de ces deux macros.

Manipulation de chaînes et de tampons

Dans include/linux/string.h, vous trouverez des fonctions de manipulation de chaînes de caractères (strncpy, strncmp, strnchr, strlen...), des fonctions de manipulation de tampons (memset, memcpy...) et des fonctions d'allocation kstrndup, kmemdup... L'implémentation de base de ces fonctions (elle peut être remplacée par une version optimisée selon l'architecture), ainsi que leur documentation, est disponible dans lib/string.c et dans mm/util.c.

Fonctions diverses

Dans include/linux/kernel.h, vous trouverez d'autres fonctions utiles et notamment snprintf, sscanf, kstrtoul (conversion d'une chaîne vers un unsigned long), kstrtol (conversion d'une chaîne vers un long)...

Listes chaînées

Le noyau fournit une implémentation de listes chaînées dans include/linux/list.h.

Si vous voulez faire une liste chaînée de structures, ajoutez un champ struct list_head à votre structure :

struct my_struct
{
  int a;
  /* ... */

  struct list_head node;
};

Vous pouvez ensuite déclarer et initialiser statiquement la tête de liste à l'aide de la macro LIST_HEAD :

static LIST_HEAD(my_struct_list);

Vous pouvez également l'initialiser dynamiquement (par exemple si la tête de liste est contenue dans une autre structure) :

struct list_head my_struct_list;
INIT_LIST_HEAD(&my_struct_list);

Pour ajouter un élément à la fin de la liste :

struct my_struct *ms;
ms = kzalloc(sizeof(struct my_struct), GFP_KERNEL);
list_add_tail(&ms->node, &my_struct_list);

Vous disposez aussi des fonctions suivantes (liste non exhaustive) : list_add (ajout d'un élément), list_del (suppression d'un élément), list_replace (remplacement d'un élément), list_move (déplacement), list_is_last (dernier élément dans la liste ?), list_empty (liste vide ?)... Consultez include/linux/list.h pour la liste complète et la documentation.

Vous pouvez itérer sur les éléments de d'une liste :

struct my_struct *ms;
list_for_each_entry(ms, &my_struct_list, node) {
  tc->a++;
}

D'autres macros permettant d'itérer sont disponibles. Celles qui contiennent safe dans leur nom autorisent le retrait d'un élément de la liste pendant son parcours (les autres non...).

Conversion d'endianness

Le noyau étant prévu pour fonctionner sur différents types d'architectures avec différentes convention d'endianness, les fonctions suivantes permettent de convertir un entier codé sur 16, 32 ou 64 bits (XX) entre l'endianness par défaut du processeur et un endianness fixe (big = be ou little = le).

Les fonctions suivantes prennent en argument l'entier à convertir :

__uXX cpu_to_[bl]eXX(__uXX x);
__uXX [bl]eXX_to_cpu(__uXX x);

Les fonctions suivantes prennent en argument un pointeur vers l'entier à convertir :

__uXX cpu_to_[bl]eXXp(const __uXX *p);
__uXX [bl]eXX_to_cpup(const __uXX *p);

Les fonctions suivantes prennent en argument un pointeur vers l'entier à convertir et stockent le résultat au même endroit :

void cpu_to_[bl]eXXs(__uXX *p);
void [bl]eXX_to_cpus(__uXX *p);

Retour au sommaire du cours


© Copyright 2017 Guillaume Duc. Le contenu de cette page est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 4.0 International (à l'exception des exemples de code tirés du noyau Linux et qui sont distribués sous leurs licences d'origine).

Licence
Creative Commons