Semaine Athens Linux Device Drivers


Introduction

Vus depuis l'espace utilisateur, les périphériques se répartissent principalement en trois catégories :

La majorité des périphériques étant vus comme des périphériques caractères, ce sont eux que nous étudierons par la suite.

Périphériques caractères

Numérotation majeure / mineure (major / minor)

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 :

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.

Fichiers périphériques

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 :

File operations

Les 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.

Fonction open

La fonction open a pour prototype :

int foo_open(struct inode *inode, struct file *file);

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 :

Cette 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.

Fonction release

La fonction release a pour prototype :

int foo_release(struct inode *, struct file *);

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.

Fonction read

La fonction read a pour prototype :

ssize_t foo_read(struct file *file, char __user *buf, size_t count, loff_t *f_pos);

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 :

Cette 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.

Fonction write

La fonction write a pour prototype :

ssize_t foo_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos);

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.

Transferts de données depuis/vers l'espace utilisateur

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 :

#define __user __attribute__((noderef, address_space(1)))

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.

Ces fonctions retournent 0 en cas de succès et une valeur différente en cas d'échec.

Fonction ioctl

La fonction unlocked_ioctl a pour prototype1 :

long foo_unlocked_ioctl(struct file *file, unsigned int cmd, unsigned long arg);

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.

Fonction llseek

La fonction llseek a pour prototype :

loff_t (*llseek) (struct file *file, loff_t offset, int whence);

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 :

Fonction poll

La fonction poll a pour prototype :

unsigned int (*poll) (struct file *file, struct poll_table_struct *wait);

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 :

Frameworks

De 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.

Misc framework

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 :

Un périphérique s'enregistre en tant auprès du framework misc grâce à la fonction :

int misc_register(struct miscdevice *misc);

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 :

int misc_deregister(struct miscdevice *misc);

L'appel à cette fonction est traditionnellement effectué dans la fonction remove.

Liens entre les structures

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 :

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).

Schéma des liens entre ces différentes structures

Schéma des liens entre ces différentes structures

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...

Travail à faire

  1. Reprenez le pilote de l'accéléromètre que vous avez commencé à écrire. Enregistrez-le auprès du framework misc
  2. Écrivez la fonction 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)
  3. Écrivez la fonction ioctl de manière à pouvoir sélectionner l'axe dont les données sont lues
  4. Testez en écrivant une petite application (dans le langage que vous voulez tant qu'elle peut être exécutée sur la carte) interagissant avec votre pilote

Il existe bien d'autres frameworks. Par la suite, nous étudierons notamment le framework input.

Implémentation directe d'un périphérique caractère

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);
}

  1. Pour les curieux voir cet article de LWN pour plus d'explications sur la présence du unlocked dans le nom du champ.


Retour au sommaire du cours


© Copyright 2017 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).

Licence
Creative Commons