Projet M2 LSE SEE : Communication espace noyau / espace utilisateur Table des matières I.Introduction...1 II.But de l'exercice...2 III.Écriture du module...3 A.Squelette du module...3 B.Gestion de l'entrée dans le /proc...4 1.Structure file_operation et fonctions associées :...5 2.Création et destruction de l'entrée dans le /proc...7 IV.Exemple d'exécution du module réalisé :...7 V.Bibliographie...8 I. Introduction Une fois le «cœur» du traceur implémenté, on souhaite pouvoir, depuis l'espace utilisateur : Récupérer la trace ; Envoyer des commandes au traceur : par exemple le mettre en pause, ou encore remettre le traceur à zéro (reset). Il existe de nombreux moyens de communication entre l espace noyau et l'espace utilisateur : interface procfs et sysfs, signaux, sockets netlink, mapping mémoire, etc. [1]. Pour le projet aucun moyen n'est imposé, le choix est libre tant qu'il s'accompagne d'une justification argumentée. Dans ce document on donne un exemple de communication entre l'espace noyau et l'espace utilisateur en créant une entrée dans le /proc. Cela est réalisé via le développement d'un module pour le noyau Linux. II. But de l'exercice Nous souhaitons mettre en œuvre le système suivant : nous allons écrire un module (du code noyau, donc) qui maintient au niveau du noyau une variable avec une valeur donnée à l'initialisation. On souhaite pouvoir, depuis l'espace utilisateur : Recevoir des information sur ce module : on souhaite connaître la valeur de la variable ; Envoyer des commandes à ce module : on souhaite affecter une nouvelle valeur à cette variable. La communication entre l'espace utilisateur et le module se fera via une entrée dans le /proc : /proc/m2lse. Nous allons y créer un fichier (virtuel). Lorsqu'un programme de l'espace utilisateur tel que cat effectuera une lecture de ce fichier, nous renverrons la valeur de la variable. Lorsqu'un programme écrira dans ce fichier, nous affecterons la valeurs écrite à la Illustration II.1: Schéma du fonctionnement du système que l'on souhaite implémenter dans cet exercice 1/5
variable. Le fonctionnement du système est présenté sur l'illustration II.1 ci-contre. L'entrée dans le /proc sera gérée par le module, qui devra implémenter les fonctions de lecture et d'écriture du fichier /proc/m2lse, en plus de maintenir la variable. III. Écriture du module Pour gérer une entrée dans le /proc depuis du code noyau, il existe diverses méthodes. Ici on présente l'utilisation de la structure de données file_operations [2], qui va nous permettre notamment de définir le comportement de l'entrée dans le /proc lors de la lecture et de l'écriture. Pour gérer l'entrée dans le /proc, il nous faut donc : Créer un objet de type file_operations, et les fonctions associées, qui seront exécutée lors de l'ouverture, la lecture, l'écriture, et la fermeture du fichier dans le /proc ; Déclarer à l'initialisation du module la création de l'entrée dans le /proc en spécifiant son nom, les droits associé, et l'objet file_opération précédemment créé. Pour la création de l'entrée dans le /proc et sa gestion via file_operation, rajoutons les headers suivants dans notre module : #include <linux/proc_fs.h> /* pour l'entrée dans le /proc */ #include <linux/fs.h> /* pour file_operation */ #include <linux/string.h> /* pour manipuler les strings lues et écrites */ #include <asm/uaccess.h> /* pour la copie de données entre l'userspace et le kernel */ 1. Structure file_operation et fonctions associées : On déclare notre objet file_operation de manière suivante, en tant que variable globale à notre module : struct file_operations fops_m2lse =.owner = THIS_MODULE,.read = procfile_m2lse_read,.write = procfile_m2lse_write,.open = procfile_m2lse_open,.release = procfile_m2lse_close, ; Cet objet contient des noms de fonctions qui seront exécutés lors de la lecture, l'écriture, l'ouverture et la fermeture de notre fichier dans le /proc. Il reste à écrire ces fonctions dans notre module. Bien entendu, chaque fonction doit respecter un prototype donné. Les fonctions d'ouverture et de fermeture ne font rien de particulier dans notre cas (il s'agit d'un fichier très simple), on les écrit de la sorte : int procfile_m2lse_open(struct inode *inode, struct file *filp) /*Rien de particulier à faire */ int procfile_m2lse_close(struct inode *inode, struct file *filp) /* Rien de particulier à faire non plus */ 2/5
Pour ce qui est des fonctions de lecture et d'écriture, c est là que les choses importantes se passent. On rappelle que lorsqu'on écrit dans le fichier du /proc, on affecte à la variable la valeur écrite, et lorsqu'on lit ce fichier, on renvoie la valeur de la variable. Fonction de lecture : la fonction de lecture d'un objet file_operation prend en paramètre un certain nombre d'arguments, en particulier un buffer de données utilisateur à remplir avec les données qui sont lues, la taille de ce buffer, et un offset indiquant à quel endroit dans le fichier la lecture commence. L'implémentation de cette fonction est la suivante : ssize_t procfile_m2lse_read(struct file *file, char user *buf, size_t size, loff_t *ppos) /* buf est le buffer utilisateur qu'il faut remplir avec les données lues */ char tmp[32]; int ret, bytes_read = 0; /* Si le buffer utilisateur est trop petit on ne lit rien (pas terrible) */ if(size < 32) /* Si l'offset est différent de 0 la lecture à déja eu lieu : on * renvoie 0 pour terminer l'opération. Si l'offset est à 0 on * effectue la lecture, et on décale l'offset */ if((*ppos) == 0) /* 1. Copie dans un buffer au niveau noyau */ bytes_read = sprintf(tmp, "MaVariable vaut %d\n", mavariable); /* 2. Copie des données noyau vers l'espace utilisateur */ ret = copy_to_user(buf, tmp, bytes_read); /* 3. Décalage de l'offset */ *ppos += bytes_read; /* On renvoie byte_read, le nombre d'octets lus (qui vaut * éventuellement 0 dans le cas ou la lecture à déja été effectuée) */ return bytes_read; Cette fonction contient quelques subtilités. Un programme (cat par exemple) effectuant une lecture de l'entrée dans le /proc va déclencher l'exécution de cette fonction. Le système s'attend à recevoir en valeur de retour de cette fonction le nombre d'octets lus. Tant que ce nombre n'est pas égal à 0, cette fonction sera appelée en boucle : c'est ainsi car les buffers utilisateurs à remplir passés en paramètre ont une taille finie, et il faut parfois remplir plusieurs buffers pour stocker l'intégralité d'un fichier de taille supérieur à la taille du buffer. Dans ce genre de cas, l'offset où lire dans le fichier (paramètre ppos) est incrémenté par la fonction de lecture en fonction du nombre d'octets lus, pour savoir à quel endroit recommencer la lecture lors de l'appel suivant. Dans notre cas, nous souhaitons faire lire à cette fonction une chaîne de caractères très courte, du genre «mavariable vaut 42» : on vérifie en début de fonction que le buffer à au moins une taille égale à 32 octets (suffisant pour écrire ce que l'on veut). Si ce n'est pas le cas, on retourne 0 et rien n'est lu. La commande cat utilise des buffers utilisateurs de 4 Ko, largement suffisants pour notre cas. Si le buffer utilisateur est assez grand, on passe à la phase de lecture. On écrit tout 3/5
d'abord dans un buffer temporaire une chaîne de caractères comportant la valeur de la variable mavariable. On copie alors dans le buffer utilisateur la valeur de cette chaîne de caractères : en effet, les espaces mémoires noyaux et utilisateurs sont très distincts sous Linux [3], et il serait dangereux d'écrire directement dans un buffer utilisateur depuis le noyau. On utilise donc la fonction copy_to_user, dont le prototype est le suivant : int copy_to_user(void *dst, const void *src, unsigned int size); Cette fonction copie size octets de données depuis l'adresse src dans l'espace noyau vers l'adresse dst dans l'espace utilisateur. Une fois la copie réalisée on incrémente l'offset avec le nombre d'octets lus. On renvoie alors le nombre d'octets lus. Comme la valeur renvoyée par la fonction de lecture est différente de 0, cette fonction va directement être ré-appelée. Dans ce deuxième appel l'offset sera alors égal à la valeur définie à l'appel précédent, et donc différent de 0 : on utilise cela comme repère pour savoir que l'on est au niveau du deuxième appel : on renvoie alors 0 pour terminer l'opération de lecture. Fonction d'écriture : Cette dernière est un peu plus simple. Lorsque l'on écrit une valeur dans notre entrée du /proc (par exemple via la fonction echo redirigée dans le fichier), cette fonction est lancée. L'implémentation est la suivante : ssize_t procfile_m2lse_write(struct file *file, const char user *buf, size_t size, loff_t *ppos) int val; /* On récupère la valeur écrite et on la convertit en int */ val = simple_strtoul(buf, NULL, 10); /* On affecte cette valeur à mavariable */ mavariable = val; /* On retourne le nombre d'octets écrits */ return size; La fonction d'écriture possède des arguments semblables à la fonction de lecture. Cette fois, le buffer utilisateur contient les données à écrire. Nous n'allons pas réellement écrire ces données dans le fichier, mais plutôt récupérer la valeur à écrire, la convertir en entier puis affecter à mavariable cette valeur. A la fin de la fonction on renvoie le nombre d'octets que la fonction appelante aurait souhaité écrire (size) pour éviter des erreurs. Attention : penser à placer les prototypes des fonctions auxquelles ont fait référence au niveau de la déclaration de l'objet file_operations avant cette déclaration, sans quoi le compilateur risque de retourner une erreur comme quoi ces fonctions n'existent pas! 2. Création et destruction de l'entrée dans le /proc Il nous reste à créer, au lancement du module, le fichier /proc/m2lse. Pour ce faire on déclare en global dans le module un objet de type struct proc_dir_entry : struct proc_dir_entry *proc_file_flashmon; On utilise alors cet objet pour déclarer, dans la fonction d'initialisation du module, la création de l'entrée dans le /proc. On passe de plus en paramètres le nom du fichier, 4/5
ses droits, ainsi que la structure file_operation qui pointe vers les fonction d'accès définies ci-dessus : proc_file_flashmon = proc_create("m2lse", S_IWUGO S_IRUGO, NULL, &fops_m2lse); Il reste enfin à supprimer ce fichier lorsque le module est déchargé. On place un appel à fonction suivante dans la fonction de sortie du module : remove_proc_entry("m2lse", NULL); IV. Exemple d'exécution du module réalisé : Après compilation et transfert du fichier module_m2lse.ko sur la carte, on peut l'exécuter et vérifier son bon fonctionnement : # ls module_m2lse.ko # insmod module_m2lse.ko # ls -l /proc/m2lse -rw-rw-rw- 1 root root 0 Jul 21 13:14 /proc/m2lse # cat /proc/m2lse MaVariable vaut 42 # echo 33 > /proc/m2lse # cat /proc/m2lse MaVariable vaut 33 # rmmod module_m2lse.ko # ls -l /proc/m2lse ls: /proc/m2lse: No such file or directory V. Bibliographie [1] Une page web détaillant différents moyens de communication entre l'espace noyau et l'espace utilisateur, avec des exemples : http://people.ee.ethz.ch/~arkeller/linux/kernel_user_space_howto.html [2] Des informations sur la structure file_operation : http://thesermon.free.fr/file_operations.html [3] Accès à l'espace mémoire utilisateur depuis le noyau : http://www.ibm.com/developerworks/linux/library/l-kernel-memory-access /index.html 5/5