Module
Table of Contents
https://embetronicx.com/linux-device-driver-tutorials/
Je recommande très fortement le Eudyptula Challenge pour pouvoir
s'exercer sur la création de modules pour le noyau Linux. On peut
retrouver sur Github des dépôts avec la liste des tâches, vu que le
site est en hiatus depuis quelques temps maintenant. Vous pouvez
retrouver sur le git de Gnous mes réponses. Vous allez aussi retrouver
des notes similaires entre ici et le dépôt. Ces notes sont plus à
jour que celles du README
. Elles sont amenées à évoluer là où le
dépôt illustre mes compétences à l'instant T.
Je vous conseille aussi de lire la section Développement sur Linux afin de voir les bonnes pratiques de code.
1. Ressources
- La documentation du noyau
- manque parfois d'exemple et peut être compliquée à parcourir.
- Linux Device Drivers, Third Edition
- livre un peu vieux, mais contient toujours des exemples fonctionnels. À recouper avec des documents plus récents parfois.
- The Linux Kernel Module Programming Guide
- livre encore en cours d'écriture sur le développement de modules. Contient de bons exemples, mais encore incomplet.
- Linux Kernel Teaching
- compilation de cours sur le noyau Linux.
2. Commandes de base
Quelques commandes basiques pour interagir avec les modules et le noyau.
dmesg
- affiche le journal du noyau
modinfo
- affiche les informations d'un module
lsmod
- liste tous les modules actifs
insmod
- charge un module
rmmod
- décharge un module, il ne doit pas être utilisé par un autre
-
mknod
- permet de créer un fichier device,
avec un nom, un numéro majeur et mineur et un type. Il faut indiquer
le chemin vers le fichier dans le nom (
/dev/test
par exemple). Il peut être de type bloc ou character.
3. Module basique
Un module basique est composé de 2 fonctions : une d'initialisation et une de sortie. Elles sont chargées via des macros. Si l'on peut maintenant donner le nom que l'on souhaite, on était obligé d'utiliser des fonctions bien précises avant la version 2.4.
Pour pouvoir écrire dans le journal, il est possible d'utiliser la
fonction printk()
en lui indiquant le type d'évènement. Une macro
existe pour ne pas avoir à préciser ce type : pr_info()
; cf. la
documentation noyau pour plus d'informations.
// SPDX-License-Identifier: GPL-2.0 #include <linux/kernel.h> #include <linux/module.h> /* int init_module(void) */ static int __init my_init(void) { pr_info("Coucou le gens !!!!\n"); return 0; } /* void cleanup_module(void) */ static void __exit my_exit(void) { pr_info("Tschuss !!!\n"); } module_init(my_init); module_exit(my_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("rick <rick@gnous.eu>"); MODULE_DESCRIPTION("Module de la premiere tache de l'Eudyptula challenge");
4. Devices
Un device est un fichier qui correspond à un périphérique. C'est ce
qu'on peut retrouver dans le dossier /dev/
. On peut connaître le
type de device grâce à la commande ls -l
(cf. exemple). Il en existe 2 :
- character device : on ne lit qu'un caractère à la fois, commence
par
c
dans la sortiels
. Exemple : le clavier. - block device : on lit des blocs (avec
char *
ouchar []
), commence parb
dans la sortiels
. Exemple : le disque dur.
On peut les lire ou écrire dedans, les modules qui gèrent les devices vont se charger de traiter les informations en conséquence.
4.1. Les numéros majeurs et mineurs
Les devices sont gérés par différents modules. Lorsqu'on tape ls
,
on peut voir des numéros séparés par une virgule. Le premier indique
le numéro majeur, le module qui gère le device. Le second indique
le numéro mineur, un numéro identifiant le device pour le module.
ls -l --time-style=+"" /dev/input/mouse* /dev/sda
crw-rw---- 1 root input 13, 32 /dev/input/mouse0 crw-rw---- 1 root input 13, 33 /dev/input/mouse1 crw-rw---- 1 root input 13, 34 /dev/input/mouse2 brw-rw---- 1 root disk 8, 0 /dev/sda
Un numéro majeur peut être partagé entre plusieurs modules.
Il est possible de lister tous les devices et leur numéro majeur
dans le fichier /proc/devices
.
Pour créer un numéro, il faut utiliser la macro MKDEV()
qui prend un
numéro majeur et mineur. Elle génère un type dev_t
. Il est possible
de récupérer ces numéros avec les macros MAJOR()
et MINOR()
.
void test_mkdev(void) { dev_t dev = MKDEV(245, 0); pr_info("Major: %d\nMinor: %d\n", MAJOR(dev), MINOR(dev)); }
4.1.1. Réservation
Lors d'une réservation, le noyau va enregistrer un nom (que l'on
donne) et l'assigner à un numéro majeur et un mineur. Une fois les
opérations terminées, il faut supprimer l'enregistrement avec
unregister_chrdev_region()
, qui prend en paramètre le numéro majeur
(dev_t
) ainsi que le nombre de devices.
- Statique
La réservation statique permet de choisir son numéro majeur, ainsi que le nombre de numéros mineurs que l'on souhaite avoir. Si trop de numéros sont demandés, cela risque de déborder sur le numéro majeur suivant. La fonction
register_chrdev_region()
prend en paramètre une structuredev_t
indiquant le numéro majeur et le premier numéro mineur, le nombre de devices à allouer et le nom du module qui va l'utiliser. Elle retourne 0 ou un code d'erreur négatif.static void example_stat(void) { dev_t dev = MKDEV(244, 0); if (register_chrdev_region(dev, 1, "example") < 0) pr_error("Erreur initialisation."); unregister_chrdev_region(dev, 1); }
- Dynamique
Le noyau va réserver un numéro majeur disponible pour nous. On peut préciser le premier numéro mineur que l'on souhaite ainsi que le nombre de devices voulus. La fonction
alloc_chrdev_region()
prend en paramètre un pointeur d'une structuredev_t
vide qui sera rempli, le premier numéro mineur, le nombre de devices à allouer et le nom du module qui va l'utiliser. Elle retourne 0 ou un code d'erreur négatif.static void example_dyn(void) { dev_t dev = 0; if (alloc_chrdev_region(&dev, 0, 1, "example") < 0) pr_error("Erreur initialisation."); unregister_chrdev_region(dev, 1); }
4.2. Création d'un device
Il est possible de les créer avec la commande mknod
. Il suffit de
mettre le numéro majeur réservé par le module ainsi qu'un numéro
mineur compris dans la plage réservée. Il faut le supprimer avec la
commande rm
après avoir déchargé le module.
4.3. misc character device
4.4. Écriture et lecture
Pour pouvoir interagir avec un fichier device, il faut mettre en
place une structure file_operations
avec des méthodes de lecture
et d'écriture. Cette structure possède beaucoup de champs, chacun avec
comportement différent si on le met à NULL
. Un exemple ci-dessous
montre juste les champs de lecture et d'écriture mais pas d'ouverture
ou de verouillage.
Pour pouvoir interagir avec le userspace, la fonction put_user
permet d'envoyer un caractère à l'utilisateur et copy_from_user
un
tampon (cf. fichier source indiqué dans le lien). Les fonctions
copy_
retournent le nombre de bytes restant à transférer. Il
s'agit là d'une erreur, 0 indique le succès de l'opération.
static ssize_t my_read(struct file *, char __user *buffer, size_t count, loff_t *offset) { /* lecture du fichier par l'utilisateur */ } static ssize_t my_write(struct file *, char __user *buffer, size_t count, loff_t *offset) { /* écriture dans le fichier */ } static const struct file_operations fops = { .owner = THIS_MODULE, .read = my_read, .write = my_write, }; static struct cdev *my_cdev; static int __init my_init(void) { my_cdev = cdev_alloc(); my_cdev->ops = &fops; my_cdev->owner = THIS_MODULE; }
Ces fonctions peuvent être appelées en boucle ! Selon la valeur de
retour, le programme essayant de lire ou d'écrire peut réessayer
l'opération. Il est possible de jouer avec count
et offset
pour
vérifier si on essaie de lire/écrire plus qu'il ne faut et retourner 0
dans ce cas.
Valeur | État |
---|---|
count |
tous les bytes ont été lus / écris |
entre 0 et count |
seul une partie des bytes a été traitée. Dans la plupart des cas, le programme va réessayer l'opération pour la finir. |
0 | fin du fichier en lecture, rien n'a été écrite en écriture + nouvelle tentative d'appel de write . |
inférieur à 0 | erreur |
Les erreurs les plus communes à retourner sont :
-EINVAL
-EFAULT
- mauvaise adresse, à utiliser quand une fonction
copy_
échoue
Les pages 63 à 69 du Linux Device Drivers (chapitre 3) sont très intéressantes sur le fonctionnement de ces fonctions.
Ressources
- Linux Device Drivers; chap. 3: Char Drivers
- Linux Kernel Labs - Character device drivers
- The Linux Kernel Module Programming Guide; 5.6 et 6
- Linux Device Driver Tutorials (4, 5, 32)
- Misc char devices (Nihaal)
5. jiffies
C'est le compteur du nombre de ticks depuis le boot du système. Il s'incrémente à chaque timer interrupt. Chapitre sur les jiffies.
6. Kobject et sysfs
6.1. kobject
Les kobject
sont des obets noyaux génériques. Ils sont derrière
beaucoup de choses dans le noyau, comme le /sys
ou les devices par
exemple. Ils ont un nom, des attributs et parfois un parent.
Les kset
sont des ensembles de kobject
tout en étant eux-même un
kobject
.
Il est possible de mettre les kobject
dans le dossier /sys
, un
dossier sera alors créé.
Le kobject
enregistre le nombre d'utilisation, un peu comme un
sémaphore. Il faut qu'il ne soit plus du tout utilisé pour pouvoir le
supprimer.
Initialisation
Il faut initaliser la zone mémoire avec des 0 dans un premier
temps. Cela peut se faire via memset
. On peut ensuite créer le
kobject
avec kobject_init()
. Il faut bien penser à libérer la
structure lorsqu'on n'a plus besoin de l'utiliser avec kobject_put
.
Pour renommer le kobject
, il faut utiliser kobject_rename()
et non
pas kobject_set_name()
comme indiqué dans le livre ou les articles
LWM.
static struct kobject foo; static int __init my_init(void) { if (!memset(&foo, 0, sizeof(struct kobject))) return -ENOMEM; kobject_init(&foo, NULL); if (kobject_rename(&foo, "my_foo")) return -EINVAL; return 0; } static void __exit my_exit(void) { kobject_put(foo); }
6.2. sysfs
Conseil : il faut bien lire la documentation et les exemples pour
comprendre comment développer les kobject
. Ce n'est pas écrit
explicitement dans le livre et les articles LWN comment faire.
Un kobject
représente un dossier. Un attribute
représente un
fichier.
Pour pouvoir créer un dossier, il faut en premier créer et
initialisier un kobject
puis l'ajouter dans le système de
fichiers. Il faut ensuite le retirer lorsqu'on souhaite le supprimer
du dossier. Il existe plusieurs façons de faire ça :
- tout faire à la main (
kobject_add()
) - l'initialiser et l'ajouter en même temps (
kobject_init_and_add()
) - utiliser directement une fonction pour tout faire à notre place
(
kobject_create_and_add()
)
Pour créer un fichier, il faut ajouter un attribut. Il est lié à un
kobject
, le dossier où il se trouve. On peut ajouter ensuite le
fichier dans le système de fichiers sys avec sysfs_create_file()
,
elle retourne 0 si l'opération s'est bien
déroulée. sysfs_remove_file()
permet de retirer le fichier.
Pour pouvoir gérer les opérations de lecture et d'écriture, il faut
ajouter des fonctions dans une structure sysfs_ops
:
ssize_t show(struct kobject *kobj, struct attribute *attr, char *buffer)
ssize_t store(struct kobject *kobj, struct attribute *attr, const char *buffer, size_t len)
Si on a plusieurs attribute
, il est possible de vérifier leur nom
avec attr->name
et faire un appel à la bonne fonction.
Il faut utiliser la fonction sysfs_emit()
pour pouvoir copier des
informations dans le tampon de l'espace utilisateur, dans la partie
show()
.
Exemple :
static struct kobject foo; static struct attribute bar = { .name = "bar", .mode = 0666 }; ssize_t show(struct kobject *kobj, struct attribute *attr, char *buffer) { return sysfs_emit(buffer, "foo bar"); } ssize_t store(struct kobject *kobj, struct attribute *attr, const char *buffer, size_t len) { ssize_t ret = 0; if (!strcmp(buffer, "foo")) ret = size; return ret; } static const struct sysfs_ops my_ops = { .show = &show, .store = &store, }; static const struct kobj_type my_type = { .sysfs_ops = &my_ops, }; static int __init my_init(void) { if (!memset(&foo, 0, sizeof(struct kobject))) return -ENOMEM; kobject_init(&foo, NULL); if (kobject_init_and_add(&foo, &my_type, NULL, "foo")) { kobject_put(&foo); return -EINVAL; } if (sysfs_create_file(&foo, &bar) { kobject_put(&foo); return -ENOMEM; } return 0; } static void __exit my_exit(void) { kobject_put(&foo); sysfs_remove_file(&foo, &bar); }
6.3. TODO device driver sysfs
Ressources
Quelques articles de LWN qui sont très intéressants mais pas à jour,
par exemple pour la fonction kobject_set_name
.
- The zen of kobjects (LWN)
- kobjects and sysfs (LWN)
- Everything you never wanted to know about kobjects, ksets, and ktypes (Documentation noyau)
- Exemples pour kobject et kset (noyau)
- Linux Device Drivers; chap. 14: The Linux Device Model, Kobjects, Ksets, and Subsystems et Low-Level Sysfs Operations
7. debugfs
Cette partition est normalement montée par défaut sur le système. Elle
permet aux modules de fournir des informations de débug facilement et
d'être mis en place rapidement. Elle se trouve dans
/sys/kernel/debug/
.
mount | grep debug
debugfs on /sys/kernel/debug type debugfs (rw,nosuid,nodev,noexec,relatime)
La documentation noyau est très bien faite sur la création et la
suppression de ces fichiers. Il est possible de faire des fichiers qui
contiennent des valeurs dynamiques comme des int
avec
debugfs_create_u64
. Article de LWN sur le sujet, qui reprend la
documentation et un peu l'historique / le but de cette partition.
8. Synchronisation (sémaphore et spinlock)
Il existe plusieurs mécanismes pour faire de la synchronisation dans le noyau Linux :
- les sémaphores
- attente passive
- les spinlocks
- attente active, utilise 100% du CPU. Pratique pour verrouiller de petites parties de code
Il existe différentes variantes de ces verrous, permettant de faciliter leur utilisation dans certains cas.
8.1. Sémaphores
8.1.1. Lecture/écriture
Un sémaphore de lecture écriture permet d'avoir un verrou en lecture
exclusive. Une seule personne peut accéder en écriture, autant que
l'on souhaite en écriture, mais pas les deux à la fois. Il est
possible de "descendre" le niveau d'un write
pour passer en read
mais l'inverse n'est pas possible.
Les fonctions trylock
vont essayer de prendre le verrou. Si ce n'est
pas possible, elles retournent 0. Cela permet de ne pas faire
d'attente active ou de faire autre chose si le verrou est déjà pris.
#include <linux/rwsem.h> static struct rw_semaphore sem; static void concurrent_read(void) { if (down_read_trylock(&sem)) { /* ... */ up_read(&sem); } } static void concurrent_write(void) { if (down_write_trylock(&sem)) { /* ... */ up_write(&sem); } } static int __init my_init(void) { init_rwsem(&sem); return 0; }
Ressources
Documentation noyau et Chapitre 5 de Linux Device Drivers.
9. Types du noyau
9.1. Liste chainée
Linked Lists du chapitre 11 de Linux Device Drivers (Data Types in the Kernel) explique très bien comment utiliser les listes chaînées du noyau.
Pour faire une liste chaînée d'une structure, il faut lui ajouter
un attribut de type struct list_head
. On peut le nommer comme l'on
veut. Il faut aussi créer la tête de la liste, en déclarant une
structure list_head
que l'on initialise avec INIT_LIST_HEAD()
et
un pointeur vers la structure.
Cette structure contient un pointeur vers l'élement suivant (next
) et
l'élément précédent (pred
).
Il existe plusieurs fonctions pour rajouter un élément (en tête, à la
fin), en supprimer un, le déplacer (cf. la doc.)… Il faut toujours donner la
structure list_head
se trouvant dans la structure. Il est possible
de récupérer un élément de la liste avec la macro list_entry
. Elle
prend en paramètre la tête, la structure qui la contient et le nom du
champ contenant la list_head
de cette structure :
list_entry(my_head, my_struct, list)
.