![]() |
Pilotes de périphériques pour le noyau Linux |
Vus depuis l’espace utilisateur, les périphériques se répartissent principalement en trois catégories :
/dev
(exemple : /dev/ttyUSB0
). Les applications peuvent interagir avec eux via des opérations classiques sur ce fichier spécial : open
, read
, write
, ioctl
…/dev
(exemple : /dev/sda1
).socket
).La majorité des périphériques étant vus comme des périphériques caractères, ce sont eux que nous étudierons par la suite.
En plus du type (caractère ou bloc), chaque périphérique est identifié grâce à deux nombres : major et minor.
Le nombre major identifie en général la famille du périphérique. Exemples :
/dev/tty[S]XX
)/dev/fbXX
)/dev/videoXX
)/dev/ttyUSBXX
)Certains numéros major sont réservés :
Le nombre minor sert à identifier le périphérique au sein de sa famille.
Ces deux nombres sont alloués statiquement et sont identiques pour toutes les plates-formes. La liste est disponible dans Documentationd/admin-guide/devices.txt.
Dès les premiers Unix, la plupart des périphériques sont représentés sous forme de fichiers spéciaux et les applications interagissent avec ces périphériques grâces à des opérations classiques d’ouverture, lecture et écriture sur ces fichiers.
Ces fichiers périphériques spéciaux (device file) sont par convention stockés dans le répertoire /dev
et sont associés à un triplet (type, major, minor).
Exemple :
$ ls -l /dev
brw-rw---- 1 root disk 8, 0 oct. 29 12:51 sda
brw-rw---- 1 root disk 8, 1 oct. 29 12:51 sda1
brw-rw---- 1 root disk 8, 2 oct. 29 12:51 sda2
brw-rw---- 1 root disk 8, 3 oct. 29 12:51 sda3
crw-rw-rw- 1 root dialout 4, 64 oct. 29 12:51 ttyS0
crw-rw-rw- 1 root dialout 4, 65 oct. 29 12:51 ttyS1
crw-rw-rw- 1 root dialout 4, 66 oct. 29 12:51 ttyS2
crw-rw-rw- 1 root dialout 4, 67 oct. 29 12:51 ttyS3
crw-rw-rw- 1 root dialout 188, 0 oct. 29 12:51 ttyUSB0
[...]
Le premier caractère indique le type de fichier (-
indique un fichier normal, b
un fichier représentant un périphérique bloc, c
un fichier représentant un périphérique caractère, l
un lien symbolique, p
un tube nommé (pipe)…).
Pour les fichiers représentant un périphérique, les colonnes 5 et 6 (entre le groupe propriétaire et la date) représentent respectivement le nombre major et le nombre minor du périphérique associé.
Ces fichiers peuvent être crées de plusieurs manières :
mknod NOM TYPE [MAJEUR MINEUR]
udev
: démon utilisé par de nombreuses distributions Linuxdevtmpfs
: système de fichier virtuelLes applications interagissent avec un périphérique caractère via des opérations classiques de lecture et d’écriture sur le fichier spécial représentant le périphérique.
Un pilote de périphérique caractère doit donc implémenter ces opérations afin de pouvoir réagir aux actions émises par les applications.
Au niveau du noyau, ces opérations sont décrites dans la structure struct file_operations
définie dans include/linux/fs.h :
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*mremap)(struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
};
Chaque entrée de cette structure (à l’exception de owner
) est un pointeur de fonction qui pointe vers la fonction au sein du pilote qui sera appelée lorsqu’une application effectuera l’opération correspondante sur le fichier spécial représentant le périphérique géré par le pilote.
Toutes ces fonctions n’ont pas besoin d’être implémentées, le noyau fournissant un comportement par défaut lorsque le champ correspondant de la structure est laissé à la valeur NULL
.
Nous allons maintenant voir les opérations les plus courantes et utiles.
open
La fonction open
a pour prototype :
Elle est appelée lorsqu’une application ouvre le fichier spécial représentant le périphérique géré par le pilote (appel système open
).
Ses arguments sont :
struct inode *inode
, structure qui représente de façon unique un fichier (ici le fichier spécial représentant le périphérique) dans le système.struct file *file
, structure qui représente un fichier ouvert (plusieurs instances de cette structure peuvent exister pour un unique inode si ce dernier est ouvert par plusieurs applications ou par la même application plusieurs fois). Cette structure est passée ultérieurement à chaque fonction (exemple : read
…). Elle contient un certain nombre d’informations importantes :
fmode_t f_mode
: Est-ce que le fichier a été ouvert en lecture (FMODE_READ
) ou en écriture (FMODE_WRITE
)loff_t f_pos
: Position courante dans le fichierunsigned int f_flags
: Divers drapeaux dont le plus important est O_NONBLOCK
qui est positionné quand l’application demande à ce que les opérations de lecture et d’écriture dans le fichier soient non bloquantesconst struct file_operations *f_op
: Pointeur vers la structure contenant les opérations pour ce fichier. Il est possible de changer ces opérations dynamiquement en modifiant ce pointeurvoid *private_data
: Pointeur que votre pilote peut utiliser pour y stocker des informations. Elles seront alors accessibles via ce champ à chaque fois qu’une opération est appelée pour ce fichierCette fonction renvoie 0
en cas de succès, une valeur négative en cas d’erreur (les codes d’erreur les plus fréquents sont définis dans include/uapi/asm-generic/errno-base.h).
Cette fonction peut ne pas être définie (.open = NULL
dans la structure struct file_operations
) auquel cas l’ouverture du fichier réussira toujours.
release
La fonction release
a pour prototype :
Ses arguments sont les mêmes que pour la fonction open
. Elle est appelée quand le descripteur de fichier est fermé par l’application (dans le cas d’un fork
ou d’un dup
, cette fonction n’est appelée que lorsque toutes les copies du descripteur de fichier sont fermées).
Tout comme la fonction open
, si cette fonction n’est pas définie, la fermeture du fichier réussira toujours.
read
La fonction read
a pour prototype :
Elle est appelée lorsque l’application qui a ouvert le fichier effectue une lecture depuis ce fichier (appel système read
).
Si cette fonction n’est pas définie, la lecture se soldera par l’erreur -EINVAL
.
Ses arguments sont :
struct file *file
: La structure représentant le fichierchar __user *buf
: Le tampon, dans l’espace utilisateur, à remplir avec les données en provenance du périphériquesize_t count
: Le nombre maximum d’octets à lireloff_t *f_pos
: Pointeur vers la la position courante dans le fichier, à mettre à jourCette fonction lit au plus count
octets depuis le périphérique et les écrit dans le tampon dans l’espace utilisateur buf
.
Elle renvoie le nombre réel d’octets lus (qui peut être inférieur à count
), 0
pour indiquer la fin du fichier ou un nombre négatif pour indiquer une erreur.
La fonction doit bloquer jusqu’à ce qu’au moins un octet soit disponible, sauf si le drapeau O_NONBLOCK
est positionné dans file->f_flags
. Dans ce dernier cas, si aucune donnée n’est disponible, la fonction read
doit renvoyer 0
tout de suite.
L’utilisation du tampon en espace utilisateur buf
sera expliqué par la suite.
write
La fonction write
a pour prototype :
Cette fonction lit au plus count
octets depuis le tampon en espace utilisateur buf
, les envoie au périphérique et met à jour la position courante dans le fichier f_pos
.
Elle renvoie le nombre réel d’octets écrits (qui peut être inférieur à count
) ou une valeur négative pour indiquer une erreur.
Le tampon dont l’adresse est fournie aux fonctions read
et write
est situé dans l’espace d’adressage de l’application qui s’exécute en espace utilisateur. Le code tournant dans l’espace noyau (ce qui concerne donc le pilote de périphérique) n’a pas le droit d’accéder directement à ce tampon (en déréférençant directement le pointeur ou en utilisant des fonction comme memcpy
). Premièrement ce n’est pas possible sur certaines architectures, et deuxièmement cela représente un problème de sécurité potentiel (la mémoire référencée par le pointeur peut être volontairement ou involontairement invalide, ce qui peut mener à une faute de pagination dans le noyau, ce qui est interdit, ou pire).
La macro __user
décore d’ailleurs le pointeur buf
avec les attributs suivants quand certains contrôles sont activés dans la configuration du noyau :
Cela indique à gcc
d’interdire le déréférencement de ce pointeur.
Le noyau fournit plusieurs fonctions dans <asm/uaccess.h>
(depuis la version 4.12, les fonctions copy_[to|from]_user
sont déclarées dans <linux/uaccess.h>
) pour permette la copie de données entre l’espace utilisateur et l’espace noyau.
get_user(x, p)
: la variable x
dans l’espace noyau reçoit la valeur pointée par le pointeur p
dans l’espace utilisateurput_user(x, p)
: le contenu de la variable x
dans l’espace noyau est copié vers l’adresse pointée par le pointeur p
dans l’espace utilisateurunsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
Ces fonctions retournent 0
en cas de succès et une valeur différente en cas d’échec.
ioctl
La fonction unlocked_ioctl
a pour prototype1 :
La métaphore lecture/écriture est appropriée pour transférer des données entre un périphérique et une application mais elle ne permet pas aisément à une application de modifier ou de récupérer la configuration d’un périphérique (modifier la vitesse d’un port série, etc.).
Cette fonction, appelée par l’appel système ioctl
, permet justement d’échanger des informations qui ne sont pas des données “utiles” entre le pilote et l’application.
L’application fournit à l’appel système ioctl
un entier qui identifie l’opération à effectuer (argument cmd
de la fonction foo_unlocked_ioctl
) ainsi qu’éventuellement un autre argument, entier ou pointeur (argument arg
).
La signification de la commande cmd
et de son éventuel argument arg
est spécifique à chaque pilote.
Exemple d’implémentation de la fonction ioctl
:
long foo_unlocked_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct foo_struct s;
void __user *argp = (void __user *)arg;
switch (cmd) {
FOO_GET_STRUCT:
if (copy_to_user(argp, &s, sizeof(s))
return -EFAULT;
/* ... */
break;
FOO_SET_STRUCT:
if (copy_from_user(&s, argp, sizeof(s))
return -EFAULT;
/* ... */
break;
default:
return -ENOTTY;
}
return 0;
}
Attention : certaines valeurs de l’entier cmd
ont une signification particulière et sont interceptées par le noyau sans que votre fonction foo_unlocked_ioctl
ne soit appelée (voir la fonction do_vfs_ioctl
dans le fichier fs/ioctl.c). C’est notamment le cas de la valeur 2
! Voir le fichier Documentation/ioctl/ioctl-number.txt pour une explication sur comment choisir les valeurs utiles de cmd
dans son pilote.
llseek
La fonction llseek
a pour prototype :
Son objectif est de changer la position courante dans le fichier. whence
peut valoir SEEK_SET
auquel cas offset
est la nouvelle position par rapport au début du fichier, SEEK_CUR
auquel cas offset
indique un décalage par rapport à la position courante ou SEEK_END
auquel cas offset
indique un décalage par rapport à la fin du fichier. Elle retourne la nouvelle position.
Si elle n’est pas déclarée dans la structure (NULL
), son implémentation par défaut fournie par le noyau se contente de modifier le champ f_pos
de la structure struct file
représentant le fichier.
Pour de nombreux périphériques, cette fonction n’a pas de signification. Pour le signaler (cela est fait automatiquement dans le cas d’utilisation de certains frameworks), il faut :
nonseekable_open(struct inode *inode, struct file *filp)
dans la fonction open
pour marquer le fichier comme non seekablellseek
de la structure struct file_operations
à la valeur no_llseek
poll
La fonction poll
a pour prototype :
Elle est appelée pour implémenter les appels systèmes poll
, select
et epoll
. Ces appels systèmes permettent à une application de surveiller plusieurs descripteurs de fichiers en même temps (ils permettent de détecter quand une donnée est disponible en lecture sur l’un d’entre-eux ou de détecter quand une écriture sur l’un d’entre-eux sera possible sans bloquer).
Pour être implémentée correctement, cette fonction doit faire deux choses :
poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
avec pour arguments : filp = file
, p = wait
et wait_address
la liste d’attente. Cela permet au noyau d’être informé de la modification de l’état du périphérique pour pouvoir de nouveau appeler la fonction poll
OU
bit à bit entre les drapeaux suivants :
POLLIN | POLLRDNORM
si des données peuvent actuellement être lues via read
sans bloquerPOLLOUT | POLLWRNORM
si des données peuvent actuellement être écrites via write
sans bloquerPOLLERR | POLLHUP
si une erreur s’est produite et que les prochaines lectures ou écritures renverront une erreurDe plus en plus de périphériques ne sont plus implémentés directement comme des périphériques caractères mais utilisent un framework qui permet de factoriser les parties identiques des pilotes contrôlant le même type de périphériques et qui permet d’offrir une interface unique et cohérente aux applications (mêmes paramètres ioctl
quel que soit le pilote par exemple). Du point de vue des applications, le périphérique reste vu comme un périphérique caractère normal que son pilote utilise un framework ou pas.
Lorsqu’aucun framework ne semble convenir pour un périphérique, il est possible de l’implémenter comme un périphérique caractère directement ou d’utiliser le framework misc qui simplifie cette tâche.
Un périphérique utilisant ce framework est décrit par la structure struct miscdevice
(définie dans include/linux/miscdevice.h) :
struct miscdevice {
int minor;
const char *name;
const struct file_operations *fops;
struct list_head list;
struct device *parent;
struct device *this_device;
const struct attribute_group **groups;
const char *nodename;
umode_t mode;
};
Les principaux champs de cette structures sont :
minor
: le numéro minor désiré pour le périphérique (le numéro major sera automatiquement celui des périphériques misc, c’est-à-dire 130) ou MISC_DYNAMIC_MINOR
pour en obtenir un dynamiquementname
: le nom du périphérique, utilisé pour créer plus ou moins automatiquement le fichier spécial correspondant dans /dev
fops
: pointeur vers la structure struct file_operations
qui décrit les fonctions utilisées pour répondre aux opérations de lecture, écriture, etc.Un périphérique s’enregistre en tant auprès du framework misc grâce à la fonction :
L’appel à cette fonction est traditionnellement effectué dans la fonction probe
du pilote.
Lorsque le périphérique n’est plus présent, il faut penser à appeler la fonction :
L’appel à cette fonction est traditionnellement effectué dans la fonction remove
.
Un pilote, même simple, va avoir besoin de jongler avec de nombreuses structures de données. Nous allons voir dans cette section les liens entre ces structures et comment passer de l’une à l’autre.
Nous allons prendre pour cela un petit exemple de pilote, s’enregistrant auprès du bus platform et utilisant le framework misc pour communiquer avec l’espace utilisateur. Il faut bien garder à l’esprit que plusieurs périphériques physiques peuvent être gérés par un même pilote.
En interne, le pilote fait le choix de représenter toutes les informations utiles pour un périphérique physique grâce à la structure suivante :
struct foo_device {
/* Données propres à un périphérique (exemple) */
int irq_num;
int parameter_x;
/* Le périphérique misc correspondant */
struct miscdevice miscdev;
};
Lorsqu’un périphérique est détecté (ici nous avons pris l’exemple du bus platform donc la détection provient soit de la lecture du Device Tree soit de la déclaration statique des périphériques présent lors du démarrage de la plate-forme), la fonction probe
est appelée :
static int foo_probe(struct platform_device *pdev)
{
struct foo_device *foodev;
int ret;
/* Alloue la mémoire pour une nouvelle structure foo_device */
foodev = devm_kzalloc(&pdev->dev, sizeof(struct foo_device),
GFP_KERNEL);
if (!foodev)
return -ENOMEM;
/* Initialise la structure foo_device, par exemple avec les
informations issues du Device Tree */
foodev->irq_num = ...:
foodev->parameter_x = ...;
/* Initialise la partie miscdevice de foo_device */
foodev->miscdev.minor = MISC_DYNAMIC_MINOR;
foodev->miscdev.name = "fooX";
foodev->miscdev.fops = &foo_fops;
foodev->miscdev.parent = &pdev->dev; /* (1) */
platform_set_drvdata(pdev, foodev); /* (2) */
/* S'enregistre auprès du framework misc */
ret = misc_register(&foodev->miscdev);
/* ... */
}
Deux liens entre les structures sont mis en place ici :
Un lien depuis l’instance de la structure struct platform_device
vers l’instance de la structure foodev
grâce à la fonction platform_set_drvdata
(voir (2)
). La fonction platform_get_drvdata
peut être utilisée ultérieurement pour récupérer l’instance de la structure foodev
. Ce lien sera utilisé par exemple dans la fonction remove
:
Un lien entre l’instance de la structure struct miscdevice
et l’instance struct device
(champ dev
de la structure platform_device
) (voir (1)
).
Le framework misc met en place un troisième lien automatiquement entre l’instance de la structure struct file
passée aux fonctions open
, read
, write
… et l’instance correspondante de la structure struct miscdevice
via le champ private_data
de struct file
. Exemple :
int foo_open(struct inode *inode, struct file *file)
{
struct miscdevice *mdev = file->private_data;
/* ... */
};
Ce pointeur pointe vers le champ miscdev
de l’instance correspondante de la structure struct foo_device
. Or c’est plutôt cette instance qui serait intéressant de récupérer afin d’obtenir les informations du périphériques concerné. Le noyau fournit une macro dédiée pour cette opération : container_of
. Elle permet de récupérer un pointeur vers l’instance d’une structure à partir d’un pointeur vers l’un de ses membres. Exemple :
int foo_open(struct inode *inode, struct file *file)
{
struct miscdevice *mdev = file->private_data;
struct foo_device *foodev;
/* À partir de mdev qui pointe vers le champ miscdev
de la structure struct foo_device, on obtient un pointeur
vers l'instance de la classe foo_device */
foodev = container_of(mdev, struct foo_device, miscdev);
/* ... */
};
Si à partir du membre dev
(de type struct device
) d’une instance de la structure struct platform_device
(on y a accès par exemple depuis foodev->miscdev.parent) on veut obtenir l’instance correspondante de la structure struct platform_device
, on peut faire appel à la macro container_of
directement ou passer par la macro to_platform_device(dev)
.
Ces liens entre les différentes structures sont importants pour pouvoir facilement passer de l’une à l’autre. On a décrit ici le cas d’un périphérique platform qui s’enregistre auprès du framework misc mais ce modèle se généralise à tous les bus et à tous les frameworks avec quelques petites différences.
Par exemple, l’équivalent de la fonction platform_[get|set]_drvdata
s’appelle i2c_[get|set]_clientdata
pour le bus I2C, spi_[get|set]_drvdata
pour le bus SPI…
read
de manière à récupérer les valeurs d’accélération sur un des axes et à les renvoyer à l’application (pour faire simple, les valeurs d’accélération seront envoyées sous forme d’un entier signé sur 8 bits)ioctl
de manière à pouvoir sélectionner l’axe dont les données sont luesIl existe bien d’autres frameworks. Par la suite, nous étudierons notamment le framework input.
Bien que le plus souvent les périphériques s’enregistrent auprès d’un framework, il reste possible de s’enregistrer directement en tant que périphérique caractère.
Voici l’ébauche de code pour le faire (il manque beaucoup de choses) :
#define DEVICE_NAME "foo"
#define CLASS_NAME "foo"
static int majorNumber;
static int currentMinor = 0;
static int nbDev = 0;
static struct class* fooClass;
static struct file_operations fops = { /* ... */ };
/* Fonction appelée au démarrage du module */
static int __init foo_init(void)
{
int res;
/* Alloue un nouveau nombre major pour le pilote */
majorNumber = register_chrdev(0, DEVICE_NAME, &fops);
if (majorNumber < 0) {
pr_err("Foo: failed to register a major number (%d)\n", majorNumber);
return majorNumber;
}
fooClass = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(fooClass)) {
unregister_chrdev(majorNumber, DEVICE_NAME);
pr_err("Foo: failed to register device class (%d)\n", PTR_ERR(fooClass));
return PTR_ERR(fooClass);
}
/* Enregistrement auprès du bus adéquat */
/* ... */
}
/* Fonction appelée lors de la découverte d'un nouveau périphérique
compatible (sa signature peut changer en fonction du bus) */
static int foo_probe(struct device *busDev)
{
struct device *charDev;
charDev = device_create(fooClass, busDev, MKDEV(majorNumber, nbDev),
NULL, "foo%d", nbDev);
if (IS_ERR(charDev)) {
pr_err("Foo: failed to create char device (%d)\n", PTR_ERR(charDev));
return PTR_ERR(charDev);
}
/* Sauvegarde charDev ainsi que le nombre minor quelque part */
/* ... */
}
/* Fonction appelée lors de la suppression du périphérique */
static int foo_remove(struct device *busDev)
{
/* Récupère charDev et le minor number */
device_destroy(fooClass, MKDEV(majorNumber, minorNumber));
/* ... */
}
/* Fonction appelée lors du déchargement du module */
static void __exit foo_exit(void)
{
/* Désenregistrement du bus */
/* ... */
class_unregister(fooClass);
class_destroy(fooClass);
unregister_chrdev(majorNumber, DEVICE_NAME);
}
Pour les curieux voir cet article de LWN pour plus d’explications sur la présence du unlocked
dans le nom du champ.↩︎
© Copyright 2020 Guillaume Duc. Le contenu de cette page est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 4.0 International (à l'exception des exemples de code tirés du noyau Linux et qui sont distribués sous leurs licences d'origine).