Retrouvez cet article dans : Linux Magazine 89
- 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.
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- 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éerelease(); - 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.
- 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.
- le bus PCI ;
- le bus USB ;
- les pilotes vidéo (V4L et V4L2).

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
glibcne sont pas disponibles dans l’espace noyau, mais certaines sont implémentées dans le répertoirelibdes 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/CodingStyledes 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/ttyS0Le 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).
$ cat /proc/devices Character devices: 1 mem 4 /dev/vc/0 4 tty 4 ttyS 5 /dev/tty 5 /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 /dev/ttyS0 c 4 64Pour supprimer un fichier spécial, on utilise la commande classique
# 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 danshotplug: 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 à/procdécrivant les périphériques connectés au système.
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 fichierstatic 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 structureDé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 L’allocation/libération du majeur ou du mineur
Chronologiquement, la fonctionstatic struct file_operations mydriver1_fops = {
.owner = THIS_MODULE,
.read = mydriver1_read,
.write = mydriver1_write,
.open = mydriver1_open,
.release = mydriver1_release,
};
Dans le premier cas, le majeur par défaut vaut zéro – ce qui correspond à un majeur dynamique – mais il est possible de passer une valeur en paramètre du module, soit :
static int major = 0; /* Major number */ module_param(major, int, 0644); MODULE_PARM_DESC(major, "Static major number (none = dynamic)");L’enregistrement du pilote s’effectue grâce à la fonction
static int __init mydriver1_init(void)
{
int ret;
ret = register_chrdev(major, “mydriver1”, &mydriver1_fops);
if (ret < 0) {
printk(KERN_WARNING "mydriver1: unable to get a major\n");
return ret;
}
if (major == 0)
major = ret; /* dynamic value */
printk(KERN_INFO "mydriver1: successfully loaded with major %d\n", major);
return 0;
}
La libération du majeur alloué s’effectue dans la fonction de déchargement du module dont le code source est le suivant. Pour cela, on utilise la fonction static void __exit mydriver1_exit(void)
{
if (unregister_chrdev(major, “mydriver1”) < 0) {
printk(KERN_WARNING "mydriver1: error while unregistering\n");
return;
}
printk(KERN_INFO "mydriver1: successfully unloaded\n");
}
La fin du programme correspond simplement à l’API des modules Linux.
module_init(mydriver1_init); module_exit(mydriver1_exit);Dans le cas de l’utilisation du pilote
#include <linux/miscdevice.h> /* misc driver interface */ static struct miscdevice mymisc; /* Misc device handler */Dans la fonction d’initialisation du module, on alloue dynamiquement le mineur au lieu du majeur en utilisant la fonction
static int __init mydriver2_init(void)
{
int ret;
mymisc.minor = MISC_DYNAMIC_MINOR;
mymisc.name = "mydriver2";
mymisc.fops = &mydriver2_fops;
ret = misc_register(&mymisc);
if (ret < 0) {
printk(KERN_WARNING "mydriver2: unable to get a dynamic minor\n");
return ret;
}
return 0;
}
Pour libérer le mineur alloué, on utilise la fonctionstatic void __exit mydriver2_exit(void)
{
misc_deregister(&mymisc);
printk(KERN_INFO „mydriver2: successfully unloaded\n“);
}
Les méthodes open() et release()
Ces deux méthodes sont les points d’entrée principaux d’un pilote UNIX, même si, sous Linux, certaines tâches d’initialisation peuvent être reportées dans les fonctionsstatic int mydriver1_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "mydriver1: open()\n");
return 0;
}
static int mydriver1_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "mydriver1: release()\n");
return 0;
}
Dans d’autres pilotes plus complexes, on peut entre autres tester la validité du mineur. Voici l’exemple du pilote du port parallèle.
static int lp_open(struct inode * inode, struct file * file)
{
unsigned int minor = iminor(inode);
if (minor >= LP_NO)
return -ENXIO;
...
}
Les méthodes read() et write()
Ces méthodes sont, en général, les plus utilisées, car elles permettent d’échanger des données avec le périphérique grâce à un tampon static ssize_t mydriver1_read(struct file *file, char *buf, size_t count, loff_t *ppos)
{
printk(KERN_INFO "mydriver1: read()\n");
return count;
}
static ssize_t mydriver1_write(struct file *file, const char *buf, size_t count, loff_t *ppos)
{
printk(KERN_INFO "mydriver1: write()\n");
return count;
}
Compilation et test du pilote
Le fichierKDIR= /lib/modules/$(shell uname -r)/build obj-m := mydriver1.o all: $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules install: $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules_install depmod -a clean: rm -f *~ $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) cleanOn teste le premier module en effectuant la compilation, puis l’insertion du module grâce à
$ make # insmod mydriver1.ko # dmesg mydriver1: successfully loaded with major 253Le système a affecté dynamiquement le majeur 253 au nouveau pilote, cependant le fichier spécial dans
# mknod /dev/mydriver1 c 253 0En pratique, on peut utiliser un script simple permettant de charger le module et de créer le fichier spécial en lisant
#!/bin/sh
if [ $# -eq 0 ]; then
echo "Usage: $0 module_name"
exit 1
fi
insmod ${1}.ko
X=`grep $1 /proc/devices`
if [ "$X" != "" ]; then
set $X
rm -f /dev/$2
mknod /dev/$2 c $1 0
else
echo "Module $1 not loaded !"
exit 1
fi
A partir de là, le pilote est accessible depuis l’espace utilisateur. Pour tester le pilote, il n’est pas nécessaire d’écrire un programme, puis le langage shell fournit les moyens grâce aux opérateurs de redirection. On peut tester l’ouverture, puis la fermeture par :
# < /dev/mydriver1Ce qui provoque l’affichage suivant dans les traces du noyau :
mydriver1: open() mydriver1: release()On teste l’écriture par :
# echo x > /dev/mydriver1Ce qui provoque l’affichage suivant dans les traces du noyau :
mydriver1: open() mydriver1: write() mydriver1: release()Pour tester la lecture, on peut utiliser la commande
dd bs=1 count=1 < /dev/mydriver1Ce qui provoque l’affichage suivant dans les traces du noyau :
mydriver1: open() mydriver1: read() mydriver1: release()Dans le cas du pilote utilisant l’allocation dynamique du mineur, la procédure est la même. On insère le module.
# insmod mydriver2.koLe point d’entrée correspondant apparaît automatiquement dans
# cat /proc/misc 62 mydriver2 63 device-mapper 175 agpgart 144 nvram 228 hpet 135 rtc 231 snapshot # ls -l /dev/mydriver2 crw------- 1 root root 10, 62 nov 5 13:59 /dev/mydriver2Cette apparition " magique " dans
# ls -l /sys/class/misc total 0 drwxr-xr-x 2 root root 0 nov 5 12:32 agpgart drwxr-xr-x 2 root root 0 nov 5 12:32 device-mapper drwxr-xr-x 2 root root 0 nov 5 12:32 hpetOn peut augmenter le niveau de trace du démon udevd en modifiant le fichierdrwxr-xr-x 2 root root 0 nov 5 14:02 mydriver2drwxr-xr-x 2 root root 0 nov 5 12:32 nvram drwxr-xr-x 2 root root 0 nov 5 12:32 rtc drwxr-xr-x 2 root root 0 nov 5 12:32 snapshot
# udev.conf # The initial syslog(3) priority: "err", "info", "debug" or its # numerical equivalent. For runtime debugging, the daemons internal # state can be changed with: "udevcontrol log_priority=<value>". #udev_log=”err” udev_log=“debug“Lors de l’insertion du module
Nov 5 13:59:48 dhcp-588-2 udevd[438]: udev_event_run: seq 792 forked, pid [5983], ‘add’ ‘module’, 0 seconds old Nov 5 13:59:48 dhcp-588-2 udevd[438]: udev_event_run: seq 793 forked, pid [5984], ‘add’ ‘misc’, 0 seconds old Nov 5 13:59:48 dhcp-588-2 udevd-event[5983]: udev_rules_get_run: rule applied, ‘mydriver2’ is ignored Nov 5 13:59:48 dhcp-588-2 udevd-event[5983]: udev_device_event: device event will be ignored Nov 5 13:59:48 dhcp-588-2 udevd-event[5983]: udev_event_run: seq 792 finished Nov 5 13:59:48 dhcp-588-2 udevd[438]: udev_done: seq 792, pid [5983] exit with 0, 0 seconds old Nov 5 13:59:48 dhcp-588-2 udevd-event[5984]: udev_rules_get_name: no node name set, will use kernel name ‘mydriver2’L’utilisation du piloteNov 5 13:59:48 dhcp-588-2 udevd-event[5984]: create_node: creating device node ‘/dev/mydriver2’, major = ‘10’, minor = ‘62’, mode = ‘0600’, uid = ‘0’, gid = ‘0’
# rmmod mydriver2 # ls -l /dev/mydriver2 ls: /dev/mydriver2: Aucun fichier ou répertoire de ce type # cat /proc/misc 63 device-mapper 175 agpgart 144 nvram 228 hpet 135 rtc 231 snapshot
Echange de données avec le pilote
Les programmes utilisateurs et les pilotes ne fonctionnant pas dans le même espace de mémoire, il est nécessaire d’utiliser des fonctions dédiées pour écrire ou lire des données vers ou en provenance du pilote. Les fonctions à utiliser sont déclarées dansL’exemple que nous proposons utilise un tampon statique – un tableau de 64 caractères – permettant de recevoir des données par une écriture depuis l’espace utilisateur, puis de renvoyer ces mêmes données lors d’une lecture depuis cet espace utilisateur. Pour cela, nous devons déjà inclure le fichier- copy_from_user(void *to, void *from, unsigned long size) - copy_to_user(void *to, void *from, unsigned long size)
#include <asm/uaccess.h> /* copy_{from,to}_user() */
Il faut ensuite déclarer le tampon, ainsi que le nombre d’éléments disponibles :
#define BUF_SIZE 64 static char buffer[BUF_SIZE]; static size_t num = 0; /* Number of available bytes in the buffer */La méthode
static ssize_t mydriver3_write(struct file *file, const char *buf, size_t count, loff_t *ppos)
{
size_t real;
real = min((size_t)BUF_SIZE, count);
if (real)
if (copy_from_user(buffer, buf, real))
return -EFAULT;
num = real; /* Destructive write (overwrite previous data if any) */
printk(KERN_DEBUG "mydriver3: wrote %d/%d chars %s\n", real, count, buffer);
return real;
}
La méthode static ssize_t mydriver3_read(struct file *file, char *buf, size_t count, loff_t *ppos)
{
size_t real;
real = min(num, count);
if (real)
if (copy_to_user(buf, buffer, real))
return -EFAULT;
num = 0; /* Destructive read (no more data after a read) */
printk(KERN_DEBUG "mydriver3: read %d/%d chars %s\n", real, count, buffer);
return real;
}
Pour tester le pilote, nous utilisons la même méthode que précédemment. Tout d’abord, nous chargeons le module, puis nous écrivons une chaîne de caractères sur le fichier spécial associé.
# insmod mydrive3.ko # echo -n salut > /dev/mydriver3Ce qui provoque l’affichage suivant dans les traces du noyau :
mydriver3: open() mydriver3: wrote 5/5 chars salut mydriver3: release()Nous pouvons ensuite lire le contenu du tampon stocké par le pilote.
# dd bs=5 count=1 < /dev/mydriver3 2> /dev/null salut #Ce programme en C permet d’effectuer le même test :
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
main (int ac, char **av)
{
char buf[64];
int n, fd;
fd = open (av[1], O_RDWR);
if (fd < 0) {
perror ("open");
exit (1);
}
n = write (fd, av[2], strlen(av[2]));
read (fd, buf, n);
printf ("buf= %s\n");
close (fd);
}
On utilise le programme comme suit :
# ./mydriver3_user /dev/mydriver3 salut buf= salut
Allocation dynamique de mémoire
Dans l’espace utilisateur, l’allocation et la libération dynamique de mémoire s’effectuent grâce aux fonctions#include <linux/slab.h> /* kmalloc()/kfree() */La taille du tampon est par défaut de 64 octets, mais peut être modifiée en passant le paramètre au chargement du module.
static size_t buf_size = 64; /* Buffer size */ module_param(buf_size, int, 0644); MODULE_PARM_DESC(buf_size, "Buffer size");Le tampon est désormais un pointeur.
static char *buffer; /* copy_from/to_user buffer */L’allocation du tampon s’effectue dans la fonction m
La libération de mémoire s’effectue dans la fonctionbuffer = (char *)kmalloc(buf_size, GFP_KERNEL);if (buffer != NULL) { printk(KERN_DEBUG "mydriver4: allocated a %d bytes buffer\n", buf_size); } else { printk(KERN_WARNING "mydriver4: unable to allocate a %d bytes buffer\n", buf_size); misc_deregister(&mymisc); return -ENOMEM; }
static void __exit mydriver4_exit(void)
{
kfree (buffer);
misc_deregister(&mymisc);
printk(KERN_INFO "mydriver4: successfully unloaded\n");
}
La méthode ioctl()
Cette méthode est utilisée pour effectuer les opérations inaccessibles aux méthodes/* ioctl cmds */ #define SET_MODE 0 #define GET_MODE 1 /* ioctl valid args */ #define MODE1 1 #define MODE2 2 static int my_mode = MODE1;Le code de la méthode
static int mydriver5_ioctl(struct inode *inode, struct file *file,
unsigned int cmd, unsigned long arg)
{
printk(KERN_DEBUG "mydriver5: ioctl()\n");
switch (cmd) {
case SET_MODE :
switch (arg) {
case MODE1 :
my_mode = MODE1;
break;
case MODE2 :
my_mode = MODE2;
break;
default :
printk(KERN_WARNING "mydriver5: arg %x unsupported in the SET_MODE ioctl command\n", (int)arg);
return -EINVAL;
}
break;
case GET_MODE: /* Send my_mode value to user space */
if (copy_to_user((void*)arg, &my_mode, sizeof(int))) {
printk(KERN_WARNING "mydriver5: copy_to_user error\n");
return -EFAULT;
}
break;
default :
printk(KERN_WARNING "mydriver5: 0x%x unsupported ioctl command\n", cmd);
return -EINVAL;
}
return 0;
}
Il est bien entendu nécessaire d’ajouter la ligne suivante à la définition de la structure .ioctl = mydriver5_ioctl,Coté utilisateur, le code qui suit permet de tester la méthode :
for (i = 1 ; i < 4 ; i++) {
printf ("setting mode= %d\n", i);
if ((n = ioctl (fd, SET_MODE, i)) < 0)
perror ("ioctl");
if ((n = ioctl (fd, GET_MODE, &arg)) < 0)
perror ("ioctl");
else
printf ("arg= %d\n", arg);
}
Ce qui donne à l’exécution :
# ./ioctl_test /dev/mydriver5 setting mode= 1 arg= 1 setting mode= 2 arg= 2 setting mode= 3 ioctl: Invalid argument arg= 2
Les files d’attente
Une file d’attente permet à un processus de libérer le processeur lorsqu’il est en attente de données. Le processus pourra être réveillé soit par un signal, soit par l’arrivée des données. L’exemple présenté est dérivé de celui du paragraphe concernant l’allocation dynamique de mémoire. Nous déclarons tout d’abord une file d’attente.static DECLARE_WAIT_QUEUE_HEAD(read_wait_queue); /* Read wait queue */Dans la méthode
/* Sleep if no data available. */Dans la méthoderet = wait_event_interruptible(read_wait_queue, num != 0);if (ret < 0) { printk(KERN_DEBUG "mydriver6: woke up by signal\n"); return -ERESTARTSYS; }
num = real; /* Destructive write (overwrite previous data if any) */ /* Wake up one blocked processes in read_wait_queue */Nous déroulons la séquence de test. L’écriture puis la lecture des données s’effectuent comme dans l’exemple précédent.wake_up_interruptible(&read_wait_queue);printk(KERN_DEBUG "mydriver6: wrote %d/%d chars %s\n", real, count, buffer);
# echo -n salut > /dev/mydriver6 # dd bs=5 count=1 < /dev/mydriver6 2> /dev/null salut #Par contre, une deuxième lecture des données bloque le
# echo -n 12345 > /dev/mydriver6 2> /dev/null 12345 #
Conclusion
Nous avons abordé ici quelques éléments permettant de comprendre la structure des pilotes en mode caractère. Certains points importants, comme la gestion des interruptions, la méthode mmap() ou la création de threads en espace noyau, n’ont pas été traités. Ils seront abordés lors d’un prochain épisode !Retrouvez cet article dans : Linux Magazine 89





Donnez votre avis
Vous devez avoir ouvert une session pour écrire un commentaire.