Chapitre 3
-------------------------------
Pilotes en mode caractères
[image] [/image]
Le but de ce chapitre est d'écrire un pilote de périphérique en mode caractère complet.
Nous développons un pilote en mode caractère car cette classe est convenable pour la plupart des
périphériques matériels simples. les pilotes en mode caractères sont aussi plus faciles à comprendre
que les pilotes en mode blocs ou les pilotes réseau (que nous traiterons dans les chapitres à venir).
Au travers de ce chapitre, nous présentons des morceaux de code extraits d'un pilote de périphérique
réel:
scull (Simple Character Utility for Loading Localities).
scull est un pilote caractère qui agit sur une zone mémoire comme s'il s'agissait d'un périphérique.
Dans ce chapitre, du fait de la singularité de
scull, nous utiliserons le mot
périphérique
comme remplacement de "La mémoire utilisée par
scull".
L'avantage de
scull est qu'il n'est pas dépendent du matériel.
scull opère sur la mémoire,
allouée à partir du noyau. N'importe qui peut compiler et lancer
scull, et scull est portable sur les
architectures sur lesquelles Linux s'éxécute. D'un autre point de vue, le périphérique ne fait rien d'autre
"d'utile" que de décrire l'interface entre le noyau et les pilotes en mode caractères ainsi que de permettre
a l'utilisateur de lancer quelques tests.
Le concept de scull
La première étape dans l'écriture d'un pilote est de définir les fonctions (le mécanisme) que le pilote
va offrir aux programmes utilisateurs. Puisque notre pilote fait partie de la mémoire de l'ordinateur, nous sommes libres de
faire ce que nous voulons avec lui. Il peut être un périphérique à accès séquentiel ou aléatoire, un périphérique
ou plusieurs, ainsi de suite.
Afin que
scull soit utile en tant que modèle pour écrire de vrais pilotes pour de vrais périphériques, nous
allons vous montrer comment implémenter divers abstractions de périphériques sur la mémoire de l'ordinateur, chaque
périphérique ayant sa spécialité.
Le source
scull implémente les périphériques suivants. Chaque sorte de périphérique implémenté par le module est
référencé en tant que
type
scull0 à scull3
Quatre périphériques, chacun consistant en un zone mémoire qui est la fois globale et
persistante. Global signifie que si le périphérique est ouvert plusieurs fois, les données
contenues au travers du périphérique sont partagées par tous les descripteurs de fichiers
qui l'ont ouvert. Persistent signifie que si le périphérique est fermé et ré-ouvert, les
données ne sont pas perdues. Ce périphérique est bien pratique, car il peut être accédé et
testé en utilisant des commandes conventionnelles, comme cp, cat, ainsi que les redirections
d'entrées/sorties du shell.
scullpipe0 à scullpipe3
Quatre périphériques FIFO (first-in-first-out), qui fonctionnent comme des pipes. Un processus
lit ce qu'un autre processus écrit. Si plusieurs processus lisent le même périphérique, ils
se disputent les données. la constitution interne de scullpipe va nous montrer comment les
lectures et écritures bloquantes et non bloquantes peuvent être réalisées sans avoir recours
aux interruptions. Bien que les vrais pilotes synchronisent avec leur périphérique en utilisant
des interruptions matérielles, la question des opérations bloquantes et non bloquantes est importante
et est distincte de la question de manipulation des interruptions (traitée au Chapitre 10).
scullsingle
scullpriv
sculluid
scullwuid
Ces périphériques sont semblables à
scull0 mais avec quelques limitations intervenant
lorsqu'une ouverture est permise. Le premier (
scullsingle) n'autorise d'utiliser le pilote
qu'a un seul processus à la fois, alors que (
scullpriv) est privé pour chaque console
virtuelle (ou session X terminal), car les processus sur chaque console/terminal obtiennent des
zones mémoires différentes.
sculluid et
scullwuid peuvent être ouverts plusieurs fois mais seulement par un
utilisateur à la fois; le précédent renvoie une erreur "Device Busy" si un autre utilisateur
vérouille déjà le périphérique, tandis que le suivant implémente une ouverture bloquante.
Ces variantes de
scull pourrait sembler confuses du point de vue de leur règles
de fonctionnement, mais sont une bonne représentation, car quelques périphériques dans la
vie courante on besoin de ce type de modélisation.
Chacun des périphériques
scull démontre différentes propriétés d'un pilote et présente différentes
difficultés. Ce chapitre couvre la constitution interne de
scull0 à
scull3; Les périphériques
plus avancés sont traités au Chapitre 6.
scullpipe est décrit à la section "un exemple d'entree/sortie bloquante",
et les autres sont décrits à "Contrôle d'accès sur un périphérique fichier".
Numéros Majeurs et Mineurs.
Les périphériques caractères sont accessibles au moyen de noms dans le système de fichiers. Ces noms sont appelés
"fichiers spéciaux" ou "fichiers de périphériques" ou simplement des " de l'arbre du système de fichiers";
il sont par convention situés dans le répertoire /dev. Les fichiers spéciaux pour les pilotes caractères sont
identifiés par un 'c' en première colonne de l'affichage de la commande ls -l. les périphériques blocs se situent
aussi dans /dev, mais sont identifiés par un 'b'. Ce chapitre est centré sur les périphériques en mode caractères,
mais une bonne part de l'information suivante s'applique aussi aux périphérique en mode blocs.
Si vous exécutez la commande ls -l , vous verrez deux nombres (séparés par une virgule) dans l'entrée fichier
du périphérique avant la date de la dernière modification, ou apparaît habituellement la taille du fichier. Ces correspondent
aux numéros de périphérique majeur et mineurs pour un périphérique particulier. Le listing suivant montre quelques
périphériques tel qu'ils apparaissent sur un système typique. Leur numéros majeur sont 1,4,7 et 10 alors que leur
numéros mineurs sont 1, 3, 5 64, 67, et 129.
1 2 3 4 5 6 7 8 9
|
crw-rw-rw- 1 root root 1, 3 2006-10-19 11:24 null
crw------- 1 root root 10, 1 2005-02-26 07:39 psaux
crw------- 1 root tty 4, 1 2006-11-06 09:46 tty1
crw-rw---- 1 root dialout 4, 64 2005-02-26 07:39 ttyS0
crw-rw---- 1 root dialout 4, 65 2005-02-26 07:39 ttyS1
crw------- 1 root tty 7, 1 2005-02-26 07:39 vcs1
crw------- 1 root tty 7, 129 2005-02-26 07:39 vcsa1
crw-rw-rw- 1 root root 1, 5 2006-10-19 11:24 zero |
Traditionnellement, le majeur identifie le pilote associé au périphérique. Par exemple
/dev/null et
/dev/zero
sont tous les deux gérés par le pilote 1, alors que les consoles virtuelles et terminaux série sont gérés par le pilote 4;
de la même façon les périphériques vcs1 et vcsa1 sont gérés par le pilote 7. Les noyaux Linux récents le partage des
numéros majeurs par plusieurs pilotes, mais la pluspars des périphériques que vous rencontrerez sont encore organisés
avec le principe un majeur pour un pilote.
Le numéro mineur est utilisé par le noyau pour déterminer de façon précise quel périphérique est concerné. En fonction de la
façon dont votre pilote est écrit (comme nous le verrons plus loin), vous pouvez soit obtenir du noyau un pointeur direct
vers votre périphérique, ou vous pouvez utiliser vous-même le numéro mineur comme index d'un tableau local de périphériques.
Autrement dit, le noyau lui-même ne sait presque rien des numéros mineurs à part le fait qu'il se réfèrent aux périphériques
implémentés par votre pilote.
La représentation interne de numéros de périphérique
Dans le noyau, le type dev_ti (définit dans <linux/types.h>) est utilisé pour manipuler des numéros de périphérique----
à la fois la partie majeure et mineure. Depuis la version 2.6.09 du noyau, dev_t est une valeur codée sur 32 bits avec
12 bits réservés pour la numéro majeur et 20 pour le numéro mineur. Votre code ne devrait jamais faire bien sur de
suppositions sur l'organisation des numéros de périphérique; il devrait; plutôt, utiliser une collection de macros
définies dans <linux/kdev_t.h>. Pour obtenir les parties majeure et mineure d'un dev_t, utilisez ceci:
1 2 3
|
MAJOR(dev_t dev);
MINOR(dev_t dev); |
Si par contre, vous avez les numéros majeur et le mineur et devez les transformer en dev_t,
utiliser ceci:
1 2
|
MKDEV(int major, int minor); |
Notez que le noyau 2.6 peut héberger un grand nombre de périphériques, alors que les versions précédentes de noyaux
étaient limitées à 255 numéros majeurs et 225 mineurs. On estime que la plus grande zone sera suffisante pratiquement
à chaque fois, mais le champs calculé est jonché d'estimations erronées de cette nature. Donc vous devrez vous attendre
à ce que le format de dev_t pourrait changer dans le future; si vous écrivez vos pilotes prudemment,de toutes façons,
ces changements ne seront pas un problème.
Allocation et libération de numéros de périphérique
Une des premières choses que votre pilote aura besoin de faire pour établir un périphérique caractère est d'obtenir un ou
plusieurs numéro de périphérique pour travailler avec. La fonction nécessaire à cette tâche est
register_chrdev_region, qui est déclarée dans <linux/fs.h>.
int register_chrdev_region(dev_t first,unsigned int count, char *name);
Ici, first est le numéro de périphérique de la région que vous aimeriez allouer. La partie mineure de first est souvent 0, mais
il n'y a pas d'obligation à cela.. count est le nombre total de numéros de périphériques contigus dont vous avez besoin.
Notez que, si count est grand, la zone que vous demandez pourrait empiéter sur le numéro de majeur suivant; mais tout marchera
bien tant que le numéro que vous demandez est disponible. Enfin,
nom est le nom du périphérique qui devrait
être associé avec ce numéro; il apparaîtra dans /proc/devices et sysfs.
Comme avec la pluspars des fonctions du noyau, la valeur de retour de
register_chrdev_region sera 0 si l'allocation
s'est déroulée avec succès. En cas d'erreur, un code erreur négatif sera retourné, et vous n'aurez pas accès à la région
demandée.
register_chrdev_region fonctionne bien si vous savez à l'avance exactement quels numéros de périphériques vous voulez.
Souvent, pourtant, vous ne saurez pas quel numéros majeurs votre périphérique va utiliser; il y a un effort constant de la
communauté des développeurs du noyau de passer à l'utilisation des numéros de périphériques alloués dynamiquement. Le noyau
allouera joyeusement un numéro majeur pour vous à la volée, mais vous devez demander cette allocation en utilisant une
autre fonction:
1 2
|
int alloc_chrdev_region(dev_t *dev,unsigned int firstminor,unsigned int count,char *name); |
Avec cette fonction, dev est paramètre de sortie qui va si l'action se déroule avec succès, représenter le premier nombre dans
votre zone allouée. firstminor devrait être le premier numéro mineur à utiliser; qui est généralement 0. Les paramètres count
et name ont la même fonction que ceux donnés dans
request_chrdev_region.
Sans vous soucier de la façon dont vous allouer vos numéros de périphériques, vous devriez les libérer quand ils ne sont plus
utilisés. Les numéros de périphériques sont libérés avec :
1 2
|
void unregister_chrdev_region(dev_t first,unsigned int count); |
L'appel de
unregister_chrdev_region devrait être habituellement placé dans la fonction cleanup de votre module.
Les fonctions précédentes allouent des numéros de périphérique pour l'utilisation de votre pilote, mais elles ne disent
rien au noyau de ce que vous allez faire à présent avec ces numéros. Avant que l'espace-utilisateur d'un programme puissent
accéder à l'un de ces numéros de périphériques votre pilote doit les connecter à ses fonctions internes qui implémentent
les opérations du périphérique. Nous allons décrire rapidement comment cette connexion est accomplie, mais il y a quelques
détours nécessaires à prendre en compte auparavant.
Allocation dynamique de numéros majeurs
Quelques numéros majeurs de périphériques sont affectés de manière statiques aux périphériques les plus communs. Une liste
de ces périphériques peut être consultée dans [i]Documentation/devices.txt contenu dans l'arborescence source du noyau.
Les chances qu'un numéro statique ai été déjà affecté pour l'utilisation de votre nouveau pilote sont minces, pourtant,
et les nouveaux numéros ne sont pas en cours d'+affectation. Donc, en tant que programmeur de pilotes, vous avez un choix:
vous pouvez simplement recueillir un numéro qui semble ne pas être en utilisation, ou vous pouvez allouer des numéros
majeur de manière dynamique. Choisir un nombre peut fonctionner tant que le seul utilisateur de votre pilote est
vous-même; une fois que votre pilote est plus largement déployé, un choix de périphérique majeur fait au hasard vous
conduira à des conflits et des problèmes.
Ainsi, pour les nouveaux pilotes, nous vous suggérons fortement d'utiliser l'allocation dynamique pour obtenir votre numéro
de périphérique majeur, au lieu de choisir au hasard un numéro parmi ceux qui sont libres. En d'autres mots votre pilote
devrait presque toujours plutôt utiliser
alloc_chrdev_region au lieu de
register_chrdev_region.
L'inconvénient du choix dynamique est que vous ne pouvez pas créer les noeuds de périphériques à l'avance, car le numéro
majeur délivré pour votre va varier.+Pour une utilisation normale du pilote, ce n'est guère un problème, car une fois que
la numéro à été choisit, vous pouvez le lire à partir de
/proc/devices..*
Afin de charger un pilote utilisant un numéro majeur dynamique,ainsi , l'invocation de
insmod peut être
remplacée par un simple script qui, après avoir appelé
insmod, lit
/proc/devices afin de créer le(s) fichier(s)
spécial(aux).
Un fichier typique /proc/devices ressemble à ce qui suit:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
Character devices:
1 mem
2 pty
3 ttyp
4 /dev/vc/0
4 tty
5 /dev/tty
5 /dev/console
5 /dev/ptmx
6 lp
7 vcs
10 misc
13 input
29 fb
128 ptm
136 pts
180 usb
:Block devices:
1 ramdisk
2 fd
3 ide0
8 sd
22 ide1
65 sd
66 sd
67 sd
68 sd
69 sd
70 sd
71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd |
*Encore mieux, l'information de périphérique peut être obtenue à partir de sysfs,généralement monté sur /sys sur
les systèmes basés sur le noyau 2.6. Faire en sorte que
scull exporte ses information via sysfs dépasse le cadre
de ce chapitre, de tout façons, nous reviendrons sur ce sujet au Chapitre 14.
Le script qui charge un module dont un numéro dynamique à été assigné peu, ainsi, être écrit en utilisant un utilitaire
comme awk pour récolter l'information à partir de /proc/devices afin de créer les fichiers dans /dev.
Le script suivant scull_load, fait partie de la distribution
scull. L'utilisateur qui possède un pilote
distribué sous la forme d'un module peut invoquer un tel script à partir du fichier système rc.local ou bien l'+appeler
manuellement à chaque fois que le module est nécessaire.
1 2 3
|
script scull_load à insérer... |
Le script peut être mis au point pour un autre pilote en redéfinissant les variables et en ajustant les lignes
mknod . Le script décrit à l'instant crée quatre périphériques car quatre est la valeur par défaut dans
les sources de scull.
Les quelques dernières lignes du script peuvent paraître obscures: pourquoi changer le groupe et le mode d'un périphérique?
La raison est que le script doit être lancé par le super-utilisateur, donc les fichiers spéciaux nouvellement crées
appartiennent à root. Le bit de permission par défaut est tel que seul root a l'accès en écriture, et n'importe qui à le
droit en lecture. Normalement, un noeud de périphérique à besoin de plusieurs règles d'accès, afin que en fonction
de la situation les droits d'accès puissent être modifiés. La règle par défaut dans notre script et de donner l'accès
à un groupe d'utilisateurs, mais vos propres besoins peuvent différer. A la section
"Contrôle d'accès sur un fichier de périphérique" au Chapitre 6, le code de
sculluid démontres comment le pilote
peut imposer ses propres règles d'accès au périphérique.
Un script
scull_unload est aussi disponible pour nettoyer le répertoire /dev et décharger le module.
Comme alternative à l'utilisation d'une paire de scripts pour charger et décharger, vous pourriez écrire un script
d'initialisation afin de le placer dans le répertoire qui' utilise votre distribution pour ces scripts.*
Avec les sources de scull, nous incluons un superbe exemple de script complet et configurable, appelé
scull.init;
il accepte les paramètres conventionnels start, stop, et restart, et opère le rôle de scull_load et de scull_unload.
Si le fait de créer et détruire avec répétition des noeuds dans /dev semble assez fastidieux, il existe une alternative
intéressante. Si vous chargez et déchargez seulement un unique pilote, vous pouvez seulement utiliser
rmmod et
insmod après la première fois que vous créez les fichiers spéciaux avec votre script: les numéro dynamiques ne
sont pas créés aléatoirement, et vous pouvez espérer que le même numéro sera choisit chaque fois tant que vous ne
chargez pas d'autres modules (dynamiques). Il est utile d'éviter d'écrire des scripts trop longs pendant le développement.
Mais cette astuce, clairement, ne s'applique pas à plus d'un pilote à la fois.
La meilleure façon d'affecter les numéros majeurs, d'après notre opinion, est de prendre par défaut l'allocation
dynamique en vous laissant le choix de spécifier le numéro majeur au moment du chargement, ou encore au moment de la
compilation. L'implémentation de
scull fonctionne de cette manière; elle utilise une variable globale, scull_major, pour
manipuler le numéro choisit (il existe aussi un scull_minor pour le numéro mineur). La variable est initialisée à SCULL_MAJOR
définie dans scull.h. la valeur par défaut de scull_major dans le source fournit est 0, qui signifie
"utiliser l' affectation dynamique". L'utilisateur peut accepter la valeur par défaut ou choisir un numéro majeur particulier,
soit en modifiant la macro avant la compilation ou en spécifiant une valeur pour scull_major sur la ligne de commande
insmod. Finalement, en utilisant le script
scull_load, l'utilisateur peut passer des paramètres à insmod sur la
par l'intermédiaire de la ligne de commande de scull_load.
Voici le code que nous utilisons dans le source de
scull afin d'obtenir un numéro majeur:
1 2 3 4 5 6 7 8 9 10 11 12
|
if (scull_major) {
dev = MKDEV(scull_major, scull_minor);
result = register_chrdev_region(dev,scull_nr_devs,"scull");
} else {
result = alloc_chrdev_region(&dev,scull_minor,scull_nr_dev,"scull");
}
if (result < 0) {
printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
return result;
} |
La pluspars des échantillons de pilotes utilisés dans ce livre utilisent du code semblable pour l'affectation de leur
numéro majeur.
Partager