Ecrire un module kernel sous Linux (3)
C From Scratch Episode 25
Qui dit nouveau stream d’Imil, dit aussi nouvelle prise de notes … Nous revoici donc reparti pour un nouvel article sur la création de modules Kernel ! Cette fois-ci, nous allons revenir sur la notion de char device, et approfondir plus particulièrement les notions de Major et Minor, sur lesquelles nous sommes passés un peu rapidement la dernière fois.
Attention: Les notes ci-dessous ne suivent pas toujours le plan suivi par Imil dans son stream, même si -je l’espère- tout sera traité ici aussi !
Un driver ? /dev ? Kezako ?
Un processus n’a pas accès à tout. On l’a largement évoqué sur le stream d’Imil, il ne “voit” qu’une mémoire virtuelle (merci la MMU !) et ne peut ni accéder directement aux disques, ni accéder directement aux fichiers, ni accéder à quoi que ce soit d’autre que la mémoire sans passer par une requête au tout puissant kernel. Et ces requêtes ce sont les syscalls.
Le hardware de notre machine est donc accessible uniquement au kernel et nous autres, pauvres gueux que nous sommes, sommes donc condamnés à faire appel aux services en charge de gérer ce hardware : les drivers. En résumé, notre code émet un syscall, que le kernel traite. En fonction de notre demande, celui-ci déclenche une fonction adaptée dans le driver chargé de gérer ce matériel en particulier. Jusque là tout va bien !
Cependant, il existe une grande variété de périphériques : écrans, webcams, lecteurs d’empreintes, brrr, disques durs, cartes réseau … Quelques exemples et pourtant autant de différences. L’approche suivie par les UNIX a donc été de proposer une interface unifiée pour causer avec tout ce petit monde : on va parler avec eux comme si nous ouvrions des fichiers. L’énorme avantage de cette idée est que les fonctions élémentaires pour manipuler les fichiers sont peu nombreuses, et nous les connaissons bien : open, close, read, write, seek … Cela réduit le nombre de syscalls différents nécessaires !
Les pseudo-fichiers permettant de communiquer avec les drivers sont bien rangés dans /dev.
Prenons un exemple, cela sera sûrement plus clair. Comme vous le savez surement, la commande hexdump permet d’afficher le contenu d’un fichier en hexadecimal :
Bon, ça n’affiche rien … Mais si on joue un peu avec la souris :
Magique non ? En visualisant ce fichier, ce sont des octets en provenance de la souris que nous recevons : nous sommes en fait en train de discuter avec le driver de souris !!!!! Les fichiers dans /dev ne sont pas de vrais fichiers, mais des pseudo-fichiers qui servent à interagir avec les drivers.
D’ailleurs, si on les regarde d’un peu plus près, ils sont bizarres ces pseudo-fichiers :
Ici, point de taille de fichier : elle est remplacée par un couple de nombres : Les chars Major et Minor. De même, le premier caractère du bloc des droits indique c pour les chars devices, et b pour les block devices.
Quelle est cette sorcellerie ?
Des devices, des pilotes et des nœuds
Les services auxquels nous pouvons accéder via un pilote sont le plus souvent des périphériques matériel. On trouve derrière eux un circuit électronique que le pilote gère pour nous. Ce n’est pourtant pas toujours le cas. Les célèbres /dev/zero et /dev/null, par exemple, sont purement logiciels. Cela fait-il une différence ? Dans le fond, pas vraiment. On peut le voir comme du matériel émulé, ou virtuel : cela ne change absolument rien à notre propos. Cela reste une ressource à laquelle nous accédons via le driver. /dev/zero nous fournit des zéros. Que ce soit grâce à une puce ou un programme, peut importe. Le (ou les) device accessible au travers du pilote sont opaques pour nous : ils rendent un service, peut importe comment.
Par contre, la façon dont nous communiquons avec ce device peut être légèrement différente en fonction de la façon dont nous communiquons avec lui. On distingue, de ce fait, deux grandes familles de périphériques :
- Les block devices
- Les char devices
Lorsque l’on discute avec un char device, l’échange de données se fait octet par octet. Nous avons pu le voir avec l’exemple de la souris ci-dessus, la discussion se fait sous forme d’un flux d’octet que nous recevons ou émettons un par un.
Lorsque l’on discute avec un block device, l’échange de données se fait par blocs. L’exemple le plus flagrant, déjà évoqué dans les streams de la série “Linux From Scratch”, en sont les disques durs, dont l’accès se fait 512 Octets à la fois.
On peut visualiser les périphériques char et block dans /sys/dev :
Qui déclare l’existence de tous ces périphériques au kernel ? Et bien ce sont les pilotes. Dans le cas des char devices, ils le font grâce à une fonction telle que register_chrdevice, que nous avons vu la dernière fois.
Et c’est là qu’interviennent les char Major et Minor. Le char major, nous y reviendrons ci-dessous, permet de s’enregistrer auprès du kernel en tant que pilote gérant un device. Mais il est fréquent qu’un pilote doivent gérer plusieurs périphériques. Il va donc également enregistrer autant de minors que de devices qu’il gère.
Houlà … je vous vois palir, nous somme peut-être passé un peu vite là-dessus. Revenons un peu à nos souris de tout à l’heure :
Toutes nos souris présentent le même char major, 13, mais ont chacune un char minor différent (respectivement 32,33,34 et 35) : ce sont des périphériques différents, mais tous sont gérés par le même driver de périphérique.
D’ailleurs, on peut trouver quel est le driver qui a enregistré le char major 10 de façon très simple :
Et voilà !!!
Et le /dev dans tout ça ?
Lorsqu’un pilote déclare un device, le kernel sait qu’il doit désormais router tout syscall associé (read, write, etc.) vers ce driver. Sauf que … la création d’un pseudofichier dans /dev n’est pas du ressort du kernel.
En effet, /dev appartient au userspace : des droits, des propriétaires, des noms de fichier … Tout cela est associé aux pauvres gueux que nous sommes, pas au kernel space !
Nous pouvons créer un noeud dans /dev de façon manuelle à l’aide de la commande suivante :
Grâce à cette commande, nous venons de créer un pseudo-fichier, un “noeud”, qui correspond à un char device (c) dont le char major est 246 et le char minor est 0. Toute tentative de lecture ou d’écriture de ce noeud entrainera des syscalls que le kernel transmettra au driver correspondant.
Pour la petite expérience, on peut vérifier qu’il est tout à fait possible de créer un autre noeud pour le driver de souris :
Notre noeud, /dev/rancune, a les mêmes major et minor que /dev/input/mouse3. L’utiliser ou utiliser /dev/int/mouse3 ne change strictement rien : les syscall seront de toute façon traités par le même driver !
Je sais ce que vous allez me dire : Comment est-ce que /dev est rempli sur mon PC ? Et bien c’est le job de udev, un daemon qui écoute les notifications du kernel ( transmises par une socket spéciale appelée netlink ) et scrute /sys. Lorsque cela est nécessaire, udev crèe un noeud dans /dev en se basant sur un ensemble de rêgles configurables pour gérer les droits, propriétaires, etc. du noeud.
Un petit exemple ?
Bon, comme tout ceci est un peu rude à comprendre, nous allons prendre un petit exemple : le driver mem, dont les sources se trouvent dans les sources du kernel Linux
Mem, c’est un driver qui propose des périphériques très simples :
- /dev/zero, une source infinie de zéros
- /dev/null, un puits sans fond dans lequel on peut écrire
- /dev/random, une source de nombres aléatoires
- etc.
Si on lit ses sources, on y retrouve une structure similaire au driver qui nous a fait vibrer la semaine dernière !
A la ligne 756 de /usr/src/linux/drivers/char/mem.c :
Comme on peut le voir ci-dessus, le driver enregistre tout d’abord le char Major MEM_MAJOR à l’aide de la fonction register_chrdev de la même façon que nous l’avions fait la semaine dernière.
On trouve ensuite une boucle, qui va enregistrer chacun des chars minors dont il a besoin avec la structure fops correspondante. C’est la fonction device_create qui effectue cette tâche.
La définition du tableau contenant chacun des devices à déclarer auprès du kernel se trouve un peu plus haut dans le code, à la ligne 716 :
Le codeur affuté que vous êtes (Ben quoi ? On peut être simple gueux face au kernel et quand même bon en C !) aura noté que chacun des périphériques a sa propre structure file_operations … et donc ses propres fonctions pour réagir aux syscalls read, write, open, etc.
Un petit exemple ? Voici zero_fops, correspondant au périphérique /dev/zero :
Et les fonctions correspondantes sont un peu plus bas dans le code …
Tout pareil que nous je vous dis !
En résumé :
- Un driver, un char major
- A chaque périphérique géré par ce driver :
- un char minor,
- une structure file_operations (fops)
- … et donc un jeu de fonctions pour répondre à chacun des syscalls !
Finalement, c’est pas si compliqué !
Implémentation de notre driver
Pour préparer la prochaine séance, nous avons commencé un nouveau module kernel, le célébre “prout”. Je ne commenterai pas ce code, puisqu’il reprend largement le dernier stream, mais j’invite le lecteur intéressé à regarder le stream précédent et son résumé, tout y est !!!
Fichier prout.c
Fichier Makefile
Fichier Kbuild
A suivre !!!!!!
Rancune.
Note: Comme me l’a fait très justement remarquer Lea, il conviendrait de souligner ici l’usage de min(), utilisée dans notre fonction prout_read. Il s’agit en fait d’une macro déclarée dans kernel.h qui appelle __careful_cmp() et retourne le plus petit de deux éléments. Cette macro permet, notamment, de vérifier qu’il n’y a pas de problème de typage entre les deux éléments que l’on compare.
Bibliographie
- Le Livre de la vie … Et il est gratuit !!!!!
- La video d’Imil