Durant le dernier article, un module kernel simple a été écrit et compilé. Cette fois, nous allons voir comment ce module peut interagir avec l’espace utilisateur via un nœud dans /dev. Ce chapitre suit la structure du stream d’Imil en deux parties : une première qui explique comment on peut utiliser des pointeurs sur fonction dans une structure et une seconde qui explique comment mettra tout cela en musique dans notre module kernel.
Note : J’ai pris la liberté de changer un petit peu les exemples par rapport au stream d’Imil.
Introduction
Sous Unix, de façon générale, on discute avec le matériel au travers d’un device, c’est à dire un pseudo fichier localisé dans /dev. Les outils pour le faire sont ceux que nous connaissons depuis longtemps pour interagir avec des fichiers : cat, echo, etc …
Mais si on y regarde d’un peu plus près, tout cela repose toujours sur des syscalls :
Ce qui peut surprendre ici, c’est que les deux commandes utilisent le même syscall (read) alors qu’il s’agit de deux objets très différents
Dans le premier cas, /etc/passwd est un fichier.
Dans le second cas, /dev/input/mouse0 est un pseudo fichier qui permet de discuter avec le driver de la souris.
C’est de nature tout à fait différente … Et pourtant nous y accédons toujours via le syscall read. Magique non ?
En fait, il faut se rappeler que sous Unix, comme le dit le célèbre adage, tout est fichier. Cela veut dire que nos différents drivers ont chacun leur implémentation de la fonction read(), et que le syscall déclenche la bonne fonction lorsque l’on accède au nœud dans /dev.
Grâce à cette petite astuce, pas besoin de multiplier les syscalls. On utilise l’API qui permet d’accéder à un fichier et puis c’est tout !
Des pointeurs de fonction et des structures
Pour comprendre comme le kernel fait, on peut commencer par le programme suivant :
La structure dummy contient deux pointeurs sur fonction, nommés respectivement “open” et “close”. Lors de l’initialisation de la variable d, ces deux pointeurs sont initialisés de façon à pointer respectivement sur dummy_open et dummy_close.
Désormais, lorsque l’on appelle d.open(), c’est en fait dummy_open() qui est appelé. De la même façon, d.close() exécute en fait dummy_close.
Pour bien illustrer la chose, faisons la même chose avec un tableau de deux structures dummy, qui pointent toutes les deux vers des fonctions différentes. Et appelons la fonction open() :
Compilons et testons notre programme :
Comme on peut le voir, c’est la fonction dummy_open() qui est appelée pour d[0], et la fonction yeah_open() pour d[1]. Ça marche !!
Nous allons voir maintenant comment cela est utilisé dans le kernel.
Revenons à notre module !
Il y a, dans le kernel, une structure dont le fonctionnement est très similaire. Elle permet de masquer le fonctionnement d’un device et de n’afficher coté utilisateur que la fonction read via un syscall.
Cette structure se trouve dans les sources du kernel, dans le fichier /usr/src/linux/include/linux/fs.h, et se nomme file_operations. En voici la déclaration :
Comme on peut le voir, cette structure ne contient que des pointeurs sur fonctions ( on parle de “place-holders” ). Elle fonctionne de la même manière que notre structure dummy.
Pour fabriquer notre driver, on crée le fichier brrr.c :
La structure du programme est globalement la même que le module de l’article précédent. Cette fois, cependant, le module comporte trois fonctions :
brrr_init() : qui est l’initialisation du module
brrr_exit() : qui est la fonction appelée lorsque le module est déchargé
brrr_read() : qui implémente la fonction read().
Ce module correspond à un char device. Il s’enregistre auprès du kernel en tant que “brrr” et doit donner son char major. Comme nous n’avons pas de char major attribué, nous laissons le kernel choisir en donnant un 0 à la fonction register_chrdev. Le major qui nous est affecté est renvoyé par la fonction, et stocké dans la variable globale major.
Pour indiquer laquelle de nos fonctions est appelée lors d’un syscall READ, nous lui transmettons également l’adresse de la variable fops, de type file_operations. Cette variable globale est initialisée en haut du code.
C’est finalement la fonction brrr_read qui fait le boulot, grâce à un appel à copy_to_user qui va permettre d’écrire des données en espace utilisateur. Pourquoi ne pas écrire directement sur le pointeur buf ? Et bien tout simplement parce que nous sommes en kernel space, et qu’il faut donc tenir compte de la MMU et de la gestion de mémoire des process !
Pour compiler tout ça, rien de spécial ! On utilise la même méthode que précédemment !
Après avoir vérifié que le module se charge bien en mémoire avec