Utilisation de fichiers de séquence de noyau Linux

Une caractéristique de la programmation moderne est l'utilisation du réseau mondial comme source d'informations de référence, en particulier, une source de modèles pour résoudre des problèmes inconnus ou peu connus pour un programmeur spécifique. Une telle approche fait gagner beaucoup de temps et donne souvent des résultats assez qualitatifs. Cependant, les solutions mises en place dans le réseau, bien que généralement correctes, ne prennent pas toujours en compte toutes les subtilités de la résolution d'un problème, ce qui conduit à l'apparition dans le code source de sections qui fonctionnent généralement correctement, mais dans des circonstances pas tout à fait standard deviennent sources de mauvaises surprises.

Considérez le sujet de l'utilisation des fichiers de séquence dans le noyau Linux, ces fichiers sont considérés comme le mécanisme le plus pratique pour imprimer à partir du mode noyau. Mais en pratique, les utiliser correctement est beaucoup plus difficile que vous ne le pensez.

De nombreux documents sur ce sujet sont disponibles en ligne. Le meilleur est le code source du noyau lui-même qui contient des commentaires assez détaillés. Le problème avec cette source d'information est son volume. Si vous ne savez pas exactement quoi chercher, il vaut mieux ne disposer que d'un temps limité, ne pas essayer du tout. Pour moi, lorsque je me suis intéressé au sujet, Google a fourni plusieurs sources d'informations apparemment excellentes concernant ma recherche: le célèbre livre The Linux Kernel Module Programming Guide et une série d'articles de Rob Day . Ces sources ne sont pas nouvelles, mais très solides.

Voyons d'abord plus en détail quand il est naturel d'utiliser des fichiers de séquence. La situation la plus courante consiste à créer votre propre fichier dans le système de fichiers / proc. En lisant les fichiers de ce système, vous pouvez obtenir une variété d'informations sur l'équipement utilisé, ses pilotes, sa RAM, ses processus, etc.

Il semblerait que l'impression de quoi que ce soit est la tâche la plus simple de la programmation. Mais en fonctionnant en mode noyau, le système d'exploitation impose de nombreuses restrictions qui peuvent sembler complètement inimaginables pour le développeur du logiciel d'application. En mode noyau, la taille du tampon d'impression est limitée par la taille d'une page de mémoire virtuelle. Pour l'architecture x86, elle est de quatre kilo-octets. Par conséquent, un bon programme lors de l'impression de grandes quantités de données doit d'abord atteindre le remplissage maximal du tampon, puis l'imprimer, puis répéter cette itération jusqu'à épuisement complet des données à imprimer. Vous pouvez bien sûr imprimer caractère par caractère, ce qui simplifierait grandement tout, mais nous parlons de bons programmes.

Les sources susmentionnées étaient légèrement pires que prévu. Dans le livre par exemple, certaines informations se sont avérées généralement incorrectes et c'est ce qui m'a poussé à écrire cette note. Il est courant de considérer que les informations fournies sous forme de schéma sont les plus faciles à comprendre et à utiliser. Cependant, dans ce livre, l'image liée au sujet est incorrecte. L'utilisation d'un tel schéma peut conduire à de graves erreurs, bien que l'exemple du livre fonctionne correctement et suive ce schéma même. Cela est dû au fait que dans cet exemple, seuls quelques octets sont imprimés à un moment où / proc / iter est accessible. Si vous l'utilisez comme modèle pour imprimer des textes plus grands qu'une page de mémoire, il y aura des surprises. La série d'articles susmentionnée ne contient pas d'erreurs évidentes, mais ne rend pas compte de certains détails qui sont importants pour comprendre le sujet.

Alors, considérons d'abord le schéma correct de la façon de travailler avec un fichier de séquence.



Pour travailler avec un tel fichier, vous devez créer les fonctions start (), stop (), next () et show (). Les noms de ces fonctions peuvent être quelconques, j'ai choisi les mots les plus courts qui correspondent en sens aux actions des fonctions. Lorsque ces fonctions sont présentes et correctement connectées aux systèmes du noyau, elles commencent à fonctionner automatiquement lors de l'accès au fichier qui leur est associé dans le répertoire / proc. La chose la plus déroutante est l'utilisation de la fonction stop (), qui peut être appelée dans trois contextes différents. L'appeler après start () signifie terminer le travail d'impression. L'appeler après show () signifie que la dernière opération d'impression vers le tampon (généralement la fonction seq_printf est utilisée pour cela) a débordé le tampon de page et cette opération d'impression a été annulée. Son appel après next () est le cas le plus intéressant qui se produit lors de l'impression de certaines données dans le tampon se termine et vous devez soit terminer le travail ou utiliser de nouvelles données. Par exemple, supposons que notre fichier dans le répertoire / proc, lors de son accès, produise d'abord des informations sur les périphériques de bloc, puis sur ceux de caractère. Tout d'abord, la fonction start () initialise l'impression pour les périphériques de bloc, et les fonctions suivantes () et, éventuellement, show () utilisent ces données d'initialisation pour imprimer des informations étape par étape sur les périphériques de bloc. Lorsque tout est prêt, après le dernier appel à next (), l'appel à stop () considéré est effectué, après quoi start () est appelé, qui cette fois devrait déjà lancer une impression supplémentaire pour les périphériques de caractères.

Je donne un exemple légèrement modifié (le contenu du fichier evens.c) de l'article de Rob Day. J'ai dû remplacer l'appel d'une fonction, qui est absente dans les noyaux modernes par son équivalent actuel. Les commentaires sont également légèrement modifiés.

#include <linux/module.h> #include <linux/moduleparam.h> #include <linux/init.h> #include <linux/kernel.h> #include <linux/proc_fs.h> #include <linux/fs.h> #include <linux/seq_file.h> #include <linux/slab.h> static int limit = 10; //default value, it can be changed here or module_param(limit, int, S_IRUGO); //transfered as a module parameter static int* even_ptr; //we will work with dynamic memory /** * start */ static void *ct_seq_start(struct seq_file *s, loff_t *pos) { printk(KERN_INFO "Entering start(), pos = %Ld, seq-file pos = %lu.\n", *pos, s->count); if (*pos >= limit) { // are we done? printk(KERN_INFO "Apparently, we're done.\n"); return NULL; } //Allocate an integer to hold our increasing even value even_ptr = kmalloc(sizeof(int), GFP_KERNEL); if (!even_ptr) // fatal kernel allocation error return NULL; printk(KERN_INFO "In start(), even_ptr = %pX.\n", even_ptr); *even_ptr = (*pos)*2; return even_ptr; } /** * show */ static int ct_seq_show(struct seq_file *s, void *v) { printk(KERN_INFO "In show(), even = %d.\n", *(int*)v); seq_printf(s, "The current value of the even number is %d\n", *(int*)v); return 0; } /** * next */ static void *ct_seq_next(struct seq_file *s, void *v, loff_t *pos) { printk(KERN_INFO "In next(), v = %pX, pos = %Ld, seq-file pos = %lu.\n", v, *pos, s->count); (*pos)++; //increase my position counter if (*pos >= limit) //are we done? return NULL; *(int*)v += 2; //to the next even value return v; } /** * stop */ static void ct_seq_stop(struct seq_file *s, void *v) { printk(KERN_INFO "Entering stop().\n"); if (v) printk(KERN_INFO "v is %pX.\n", v); else printk(KERN_INFO "v is null.\n"); printk(KERN_INFO "In stop(), even_ptr = %pX.\n", even_ptr); if (even_ptr) { printk(KERN_INFO "Freeing and clearing even_ptr.\n"); kfree(even_ptr); even_ptr = NULL; } else printk(KERN_INFO "even_ptr is already null.\n"); } /** * This structure gathers functions which control the sequential reading */ static struct seq_operations ct_seq_ops = { .start = ct_seq_start, .next = ct_seq_next, .stop = ct_seq_stop, .show = ct_seq_show }; /** * This function is called when a file from /proc is opened */ static int ct_open(struct inode *inode, struct file *file) { return seq_open(file, &ct_seq_ops); }; /** * This structure gathers functions for a /proc-file operations */ static struct file_operations ct_file_ops = { .owner = THIS_MODULE, .open = ct_open, .read = seq_read, .llseek = seq_lseek, .release = seq_release }; /** * This function is called when this module is loaded into the kernel */ static int __init ct_init(void) { proc_create("evens", 0, NULL, &ct_file_ops); return 0; } /** * This function is called when this module is removed from the kernel */ static void __exit ct_exit(void) { remove_proc_entry("evens", NULL); } module_init(ct_init); module_exit(ct_exit); MODULE_LICENSE("GPL"); 


Les fonctions permettant de travailler avec un fichier de séquence utilisent deux pointeurs avec des fonctionnalités qui se chevauchent (cela est également quelque peu déroutant). L'un d'eux doit pointer vers l'objet courant à imprimer dans le tampon par show () - il s'agit du pointeur `v 'dans le programme. L'autre pointeur `pos 'est généralement utilisé pour pointer vers le compteur.

Pour ceux qui voudraient pour la première fois exécuter leur programme en mode noyau, je donne un exemple de Makefile pour une construction réussie. Bien sûr, pour une construction réussie, vous devez avoir des en-têtes de source de noyau Linux dans le système.

 obj-m += evens.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean 


La connexion au noyau se fait avec la commande sudo insmod evens.ko , en vérifiant la fonctionnalité du fichier / proc / evens, qui est apparu après cela, avec la commande cat /proc/evens , en lisant le journal des événements expliquant les opérations du système avec la commande sudo cat /var/log/messages .

Pour déborder le tampon de page, définissez le paramètre limit sur une valeur plus élevée, par exemple 200. Cette valeur peut être entrée dans le texte du programme ou utilisée lors du chargement du module avec une commande sudo insmod events.ko limit=200 .

L'analyse du journal peut expliquer les points restants peu clairs. Par exemple, vous pouvez remarquer qu'avant d'appeler stop () après next () ou start (), le système met à zéro la variable `v '. Vous pouvez également remarquer qu'avant d'appeler start () après stop (), le système imprime le contenu du tampon.

Je serais reconnaissant à quelqu'un de signaler toute inexactitude trouvée dans ma note ou tout autre élément qui devrait être mentionné.

Le code source est également disponible ici .

Source: https://habr.com/ru/post/fr444620/


All Articles