Programmation Noyau Sous Linux . Pilotes En Mode Caractere.pdf

(421 KB) Pobierz
Programmation noyau sous Linux Pilotes en mode 
caractère
Ce nouvel article de la série est consacré à la programmation des pilotes de périphériques en mode 
dit " caractère ". Les connaissances acquises lors du premier article consacré à l’API des modules 
Linux vont nous permettre d’aborder assez simplement la notion de " pilote Linux " qui, finalement, 
est un module dans une version un peu plus ardue.
Nous aborderons les sujets suivants :
l’introduction aux pilotes Linux : les différents types de pilotes ; 
l’interface avec l’espace utilisateur : le répertoire /dev ; 
l’API des pilotes en mode caractère ; 
le transfert de données avec l’espace utilisateur ; 
l’allocation dynamique de mémoire ; 
la méthode ioctl ; 
les files d’attente. 
Les exemples fournis sont inspirés de ceux développés par Stelian Pop pour la formation " noyau 
Linux " d’Open Wide.
Introduction aux pilotes Linux
Le système Linux étant de la famille UNIX, la structure d’un pilote Linux est relativement proche 
de celle d’un pilote générique UNIX. Un pilote est une portion de code exécutée dans l’espace du 
noyau. Il est chargé de faire l’interface entre un programme utilisateur et un composant matériel. Ce 
dernier point n’est pas forcément vrai puisqu’il existe des pilotes de périphériques virtuels tels les 
systèmes de fichier. En toute rigueur, un programme UNIX devrait systématiquement accéder à un 
périphérique au travers d’un pilote. Ce n’est pas toujours le cas, puisque nous avons vu dans 
plusieurs articles précédents qu’il était possible – sous Linux – d’accéder à une ressource matérielle 
grâce à la fonction ioperm(), puis aux fonctions inb() et outb(). Cette méthode est cependant peu 
recommandée et rarement utilisée sauf pour des cas simples ou très particuliers.
L’API d’un pilote Linux est très fortement inspirée de celle des pilotes UNIX des années 1970. Nous 
rappelons qu’elle est basée sur l’utilisation de fonctions ou méthodes permettant d’effectuer des 
actions basiques.
une méthode open() permettant d’ouvrir le périphérique ; 
une méthode close() permettant de fermer (libérer) le périphérique. Sous Linux, cette 
méthode est nommée release() ; 
une méthode read() permettant de lire des données du périphérique ; 
une méthode write() permettant d’écrire des données sur le périphérique ; 
une méthode ioctl() (Input Output ConTroL) permettant de configurer le périphérique. 
Il existe d’autres méthodes comme lseek(), poll() ou mmap(), mais elles sont moins fréquemment 
utilisées dans les pilotes simples.
Les méthodes sont activées depuis un programme de l’espace utilisateur en passant par les fichiers 
spéciaux du répertoire /dev. Ces fichiers sont appelés nœuds (nodes ou devices en anglais). Nous 
décrirons plus précisément ce répertoire plus loin dans le document.
Le pilote est conforme à l’API des modules Linux décrite dans l’article précédent. Du point de vue 
fonctionnel, l’API des modules n’a rien à voir avec les pilotes. Elle permet simplement de charger 
dynamiquement le pilote dans l’espace du noyau en cours d’exécution.
Remarque :
Un pilote externe aux sources du noyau Linux sera toujours développé sous forme de module.
Il existe différents types de pilotes, liés aux types de périphériques qu’ils peuvent contrôler.
Les pilote en mode caractère (char device driver). Ces pilotes sont destinés à manipuler les 
périphériques les plus courants avec lesquels ils échangent des données sous forme de flux 
d’octets de taille variable (minimum 1 octet). De ce fait, la plupart des pilotes de 
périphérique sous Linux seront en mode caractère. L’exemple le plus courant est le pilote de 
l’interface série RS­232. 
Les pilotes en mode bloc (block device driver). Ces pilotes sont destinés à manipuler des 
périphériques de stockage avec lesquels ils échangent des blocs de données (disque dur, 
CDROM, DVD, disque mémoire, etc.). La taille des blocs peut être de 512, 1024, 2048 (ou 
plus) octets suivant le périphérique. 
Les pilotes de périphériques réseau (network device driver). Ils sont destinés à gérer des 
contrôleurs réseau (exemple : carte Ethernet), mais aussi des piles de protocoles. 
Contrairement aux autres pilotes, ils n’utilisent pas d’entrée dans le répertoire /dev. 
A cette liste, nous pouvons également ajouter quelques pilotes spéciaux le plus souvent dédiés à des 
bus ou des API particulières, citons entre autres :
le bus PCI ; 
le bus USB ; 
les pilotes vidéo (V4L et V4L2). 
La structure générale du noyau Linux est présentée dans la figure ci­dessous. On peut y voir le 
positionnement des principaux types de pilotes (schéma par Julien Gaulmin).
Figure 1 : Structure du noyau Linux
Dans le présent article, nous traiterons uniquement le cas des pilotes en mode caractère.
Quelques règles d’usage
La programmation d’un pilote est notoirement plus complexe que celle d’un programme en espace 
utilisateur.
Le noyau est un élément fondamental du système et un pilote mal conçu peut entraîner des 
dysfonctionnements importants, voire un arrêt du système, ce qui n’est pas le cas dans 
l’espace utilisateur. 
La mise au point dans l’espace noyau est complexe (voir l’article " Débogage dans l’espace 
noyau avec KGDB " dans le magazine LM 88). 
Les fonctions de la glibc ne sont pas disponibles dans l’espace noyau, mais certaines sont 
implémentées dans le répertoire lib des sources du noyau. 
Un pilote doit être programmé en C (pas de C++), mais la structure d’un pilote est " orientée 
objet ". 
En toute rigueur, on doit respecter le style de programmation défini dans le fichier 
Documentation/CodingStyle des sources du noyau. 
Un pilote doit prendre en compte les différentes architectures, particulièrement au niveau des 
" big­endians " et " little­endians ". 
Tout cela doit inciter le lecteur à concevoir des pilotes les plus simples possibles et de reporter la 
difficulté sur l’espace utilisateur. De plus, nous rappelons une nouvelle fois qu’un pilote Linux doit 
en théorie être diffusé sous GPL, ce qui peut poser des problèmes par rapport à certains codes 
sensibles.
Le point de vue de l’espace utilisateur
Dans le cas d’un pilote en mode caractère, le périphérique est vu depuis un programme utilisateur 
comme un fichier spécial du répertoire /dev. Le fichier est dit " spécial ", car il n’occupe quasiment 
pas d’espace sur le disque (uniquement le point d’entrée). Le but du fichier spécial est uniquement 
de faire un lien entre l’espace utilisateur et le pilote de périphérique associé.
Principe du majeur/mineur
Si l’on regarde le fichier spécial associé au premier port série, on obtient :
$ ls -l /dev/ttyS0
crw-rw---- 1 root uucp 4, 64 oct 31 10:34 /dev/ttyS0
Le premier caractère identifie le type de périphérique, soit ici la lettre c pour caractère. Les neufs 
caractères suivants correspondent aux droits d’accès habituels sous Linux. Il en est de même pour le 
propriétaire et le groupe.
Par contre, les deux champs qui suivent sont particuliers aux fichiers spéciaux.
La première valeur appelée " majeur " identifie le type de périphérique (ici le port série). 
La seconde valeur appelée " mineur " identifie l’instance du périphérique. Ces mineurs 
permettent de gérer plusieurs périphériques identiques avec le même pilote (donc le même 
majeur). 
La valeur du majeur est unique. Dans le cas de Linux, la liste des majeurs réservés est disponible 
dans la documentation des sources du noyau, soit le fichier Documentation/devices.txt. La liste des 
majeurs actifs à un instant donné est disponible dans le fichier /proc/devices.
$ cat /proc/devices
Character devices:
1
4
4
4
5
5
...
mem
/dev/vc/0
tty
ttyS
/dev/tty
/dev/console
Remarque :
L’utilisation erronée d’un numéro de majeur réservé entraînerait de fortes perturbations dans le 
fonctionnement du système !
Dans le cas de l’ajout d’un nouveau pilote par l’utilisateur, il sera préférable d’utiliser les 
fonctionnalités d’allocation dynamique de majeur ou de mineur plutôt que d’utiliser une valeur fixe. 
Ce point sera abordé dans la description de l’API.
La création d’un nouveau fichier spécial s’effectue grâce à la commande mknod. Si l’on devait 
créer le fichier spécial /dev/ttyS0, on utiliserait la commande :
# mknod /dev/ttyS0 c 4 64
Pour supprimer un fichier spécial, on utilise la commande classique rm.
# rm -f /dev/ttyS0
Limites du système de majeur/mineur
Cette approche est simple, mais elle pose un problème de duplication d’information entre l’espace 
utilisateur et l’espace noyau. En effet, les fichiers spéciaux sont créés dans /dev avec des valeurs 
passées à mknod ou bien à des utilitaires chargés de créer une liste de fichiers spéciaux (exemple : 
MAKEDEV). Pour que le système fonctionne, il faut que ces mêmes valeurs soient utilisées dans le 
code source des pilotes.
L’approche devfs permettait d’améliorer les choses pour le noyau 2.4. Grâce à devfs, les 
périphériques ne sont plus manipulés sous forme de majeur/mineur, mais sous forme de nom. 
L’entrée dans /dev est créée dynamiquement lors de l’insertion du pilote. Cependant le code de 
devfs est situé dans le noyau ce qui pose des problèmes de nommage et de compatibilité avec le 
standard LSB (Linux Standard Base) qui ne traite pas l’espace noyau.
De ce fait, une autre approche appelée udev est désormais utilisée pour le noyau 2.6 (même si la 
compatibilité devfs est conservée). Le principal intérêt de udev est de fonctionner entièrement en 
espace utilisateur. Un démon nommé udevd reçoit des instructions du noyau afin de procéder à la 
création de l’entrée dans /dev grâce à des règles modifiables par l’administrateur. Techniquement 
parlant, udev est basé sur deux composants liés au noyau 2.6.
hotplug : un démon chargé de la gestion de l’insertion/suppression des périphériques ; 
sysfs (monté sur /sys) : un système de fichier virtuel similaire à /proc décrivant les 
périphériques connectés au système. 
Le lien vers un article complet concernant udev est disponible dans la bibliographie.
API de programmation
Nous avons vu, dans l’article précédent, l’API de programmation des modules Linux. Sachant que, 
dans notre cas, un pilote est systématiquement un module, cette API sera utilisée, enrichie de 
plusieurs éléments permettant de définir les méthodes décrites au début de l’article.
Comme nous l’avons fait pour les précédents articles et afin de faciliter la compréhension, nous 
baserons la démonstration sur un exemple simple et évolutif.
La structure file_operations
Cette structure dont le type est décrit dans le fichier <linux/fs.h> permet de définir les méthodes du 
pilote. La tradition veut que l’on préfixe le nom de la structure et des méthodes par le nom du pilote 
(ici mydriver).
static struct file_operations mydriver_fops = {
.owner =
THIS_MODULE,
.read = mydriver_read,
.write =
mydriver_write,
.open = mydriver_open,
.release =
mydriver_release,
};
De part la nécessité de définir les symboles correspondant aux méthodes, on placera la déclaration 
de la structure après la définition des méthodes. De ce fait, l’architecture générale du pilote sera la 
suivante :
1. Déclaration des en­têtes.
2. Déclaration des macro­instructions identifiant le pilote.
3. Déclaration des variables.
4. Définition des méthodes.
5. Définition de la structure file_operations.
6. Définition des points d’entrée module_init() et module_exit() du module.
Déclarations à effectuer
Dans le cas simple qui nous intéresse, les déclarations sont similaires à celles que nous avons 
effectuées pour l’exemple des modules de l’article précédent.
#include <linux/kernel.h>
/* printk() */
#include <linux/module.h>
/* modules */
#include <linux/init.h> /* module_{init,exit}() */
#include <linux/fs.h>
/* file_operations */
MODULE_DESCRIPTION("mydriver1");
MODULE_AUTHOR("Stelian Pop/Pierre Ficheux, Open Wide");
MODULE_LICENSE(“GPL”);
La seule nouveauté est la présence du fichier d’en­tête <linux/fs.h> nécessaire à la définition de la 
structure file_operations.
Zgłoś jeśli naruszono regulamin