Débuter en kernel-programmation avec un driver en mode caractères

L'auteur

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

I-A. Disposer des sources du noyau

Pour écrire du code pour le noyau, il faut disposer des sources de celui-ci car le noyau que utilise votre système Linux n'est rien d'autre qu'un gros fichier exécutable, lancer au boot, situer dans le dossier /boot.

Les sources de votre noyau actuel sont présentes dans le dossier du système :/usr/src/linux-headers-$(uname -r)/ de votre système.

Celui-ci contient les dossiers suivants sous Ubuntu:

/arch : Contient un dossier pour ce qui est spécifique a une architecture.

/block : Driver en mode block.

/crypto : Cryptage.

/Documentation : Documentation (Il faut la générer comme nous allons le voir).

/drivers : Drivers.

/firmware : Firmwares.

/fs : Système de fichier.

/include : Fichiers d'en-tête a inclure pour la programmation noyau.

/init : Initialisation du kernel.

/ipc : IPC (Inter Process Communication).

/Kbuild :

/Kconfig :

/kernel : Noyau du noyau.

/lib : Bibliothèques utilitaires.

/Makefile : Fichier de compilation du noyau avec l'outil make.

/mm : Gestion de la mémoire (Memory Management).

/Module.symvers :

/net : Résaux.

/samples : Examples de tracepoints.

/scripts : Scripts de génération du noyau.

/security : Sécurité.

/sound : Audio.

/tools : Outils d'analyse de performances.

/ubuntu : Ubuntu.

/usr : User software pour l'initialisation du système de fichiers.

/virt : Virtualisation.

Pour générer la documentation sous Ubuntu : Le paquet docbook-utils est requis.

Sous Ubuntu, la génération de la documentation n'est possible qu'avec les sources téléchargées avec la commande :

 
Sélectionnez
1.
$ sudo apt-get source linux-image-$(uname -r)

Ensuite, il faut se placer dans le dossier des sources - il existe 3 commandes chacune pour un format de sortie différents afin de générer la documentation :

 
Sélectionnez
1.
2.
3.
$ sudo make pdfdocs
$ sudo make psdocs
$ sudo make htmldocs

La documentation générer se trouve dans le format choisi dans le dossier : /Documentation/DocBook/

I-B. Spécificités du GNU C 

Le noyau n'est pas écrit en C ordinaire mais en GNU C qui est une version optimisée du langage C.

Si vous maîtrisez le langage C vous n'aurez aucun mal à assimiler les différences.

Il faut savoir qu'il ne vous sera pas possible de disposer des fonctions de la libc que vous connaissez comme dans un programme ordinaire, ni de compiler votre programme ni notre driver comme un programme ordinaire.

Mais le kernel vous met à disposition certaines fonctions analogues à celles de la libc pour certaines et bien d'autres qui sont spécifiques au noyau.

! Avec les changements de versions du noyau, les fonctions et les fichiers d'inclusion que vous utiliserez évoluent, il faut essayer de maintenir la rétrocompatibilité, mais il n'y a pas de garantie.

Notez que pour compiler notre driver, nous utiliseront l'outil make pour effectuer le linkage avec les sources du noyau.

Les spécificités du GNU C sont :

  • Non utilisation des nombres à virgule flottante pour des questions de précisions :
    Car il faut savoir que selon l'architecture ceux-ci sont codés autrement, malgré le fait que le kernel fournit les types de données universelles pour la programmation noyau, mais les floats ne sont pas de la partie.
  • Programmation orientée objet (P.O.O) :
    Les données et les fonctions doivent être strictement séparées. Les structures emploient beaucoup de pointeurs et sont définies comme propres au noyau dans un esprit de P.O.O., dont certaines sont écrites en langage d'assemblage : l'assembleur. Il existe notamment beaucoup de macros.
  • Emploi de goto :
    Même si on ne les retrouve pas souvent dans les programmes C actuels ils sont légions dans l'écriture de driver, car ils permettent entre autre de sauter dans un bout de codes afin de dé-initialiser des données en cas d'erreur.
  • Séparation de l'espace d'adressage :
    La mémoire est scindée en 2 espaces d'adressage distinct concernant les drivers et la programmation noyau :

    • Un espace d'adressage utilisateur (user-space).
    • Un espace d'adressage noyau (kernel-space).


    Nous effectueront les translations d'adresses nécessaires au transfert d'un espace d'adressage à un autre dans le cadre de l'application cliente du driver.
    D'ailleurs, ces deux espaces sont protégés l'un contre l'autre.

  • Petites piles (stack) :
    Le noyau utilise des stack de 2 pages dont la taille dépend de l'architecture.

    • Architecture 64 bits : page de 8 K bytes * 2.
    • Architecture 32 bits : page de 4 K bytes * 2.
  • Importance de la synchronisation des données.
  • Utilisation de fonction inline pour l'optimisation.
  • Utilisation de l'assembleur de façon inline.
  • Importance de la portabilité :
    Pour cela il existe un répertoire spécifique propre à chaque architecture prise en charge par le noyau Linux. Pour les choses variant d'une architecture à une autre, par exemple la définition des appels systèmes.

I-C. Connaissance de l'outil make

L'outil make permet l'automatisation de la compilation d'un programme en permettant entre autre de définir les conditions sous lesquelles une commande sera lancée et de définir des variables. Nous allons nous en servir ici afin d'effectuer le linkage vers les sources du noyau et générer le fichier module de notre driver.

! Il faut faire attention aux tabulations car make ne supporte pas les espaces remplaçant celles-ci.

Le fichier source est nommé driver.c et doit être situé dans un dossier, dont le nom ne porte pas à confusion Linux (évitez les caractères non-ASCII et espaces) et l'outil make. Ce dossier sera nommé driver.

Le fichier suivant doit être nommé Makefile afin que la commande make reconnaisse notre fichier de compilation.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
obj-m += driver.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
            $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean :
            $(MAKE) -C $(KERNELDIR) m=$(PWD) clean

Suite à quoi nous lançons la commande make qui reconnaît le fichier de compilation nommé Makefile:

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
$ cd driver
$ ls
driver.c   Makefile
$ sudo make
make -C /lib/modules/3.11.0-18-generic/build M=/home/edward/Bureau/driver modules
make[1]: entrant dans le répertoire « /usr/src/linux-headers-3.11.0-18-generic »
CC [M]  /home/edward/Bureau/driver/driver.o
Building modules, stage 2.
MODPOST 1 modules
CC      /home/edward/Bureau/driver/driver.mod.o
LD [M]  /home/edward/Bureau/driver/driver.ko
make[1]: quittant le répertoire « /usr/src/linux-headers-3.11.0-18-generic »
$ ls
driver.c   driver.mod.c  driver.o  modules.order
driver.ko  driver.mod.o  Makefile  Module.symvers

II. Écriture et chargement d'un kernel-module

II-A. Introduction a un kernel-module

Dans ce premier bout de code (qui est un module chargeable à chaud) à l'opposé d'insérer le code dans le noyau lors de la compilation.

Nous allons utiliser une unique fonction du kernel qui permet d'écrire dans le fichier de log de celui-ci : /var/log/kern.log dont la fin est affichable avec la commande dmesg.

La fonction printkprintf() (print kernel) qui est analogue à la fonction printf() dont le flux de sortie est le fichier de log et qui a pour prototype :

 
Sélectionnez
1.
int printk(const char *fmt, ...) ;

Dans ce fichier on peut insérer la nature du message dans les logs du noyau, car il faut bien justifier la notification grâce aux constantes suivantes :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
#define KERN_EMERG      "<0>"           /* System unusable      */
#define KERN_ALERT      "<1>"           /* Need intervention    */
#define KERN_CRIT       "<2>"           /* System critic error  */
#define KERN_ERR        "<3>"           /* Sytem error occur    */
#define KERN_WARNING    "<4>"           /* Warning              */
#define KERN_NOTICE     "<5>"           /* Notice               */
#define KERN_INFO       "<6>"           /* Information          */
#define KERN_DEBUG      "<7>"           /* Debugging            */

De la manière suivante :

 
Sélectionnez
1.
printk(KERN_DEBUG "Here is an message in the kernel logs\n") ;

À noter que selon la nature du message et la présence du caractère linefeed ('\n') l'écriture peut être différée.

Il existe aussi une fonction pour allouer dynamiquement de la mémoire kmalloc, à la manière de malloc, défini dans <linux/slab.h> qui a pour prototype :

void *kmalloc(size_t size, gfp_t flags);

Dont le premier argument définis le nombre de bytes à allouer limité a 128 K Bytes, pour de plus grands espaces mémoires il faut se tourner vers la fonction vmalloc() qui alloue de la mémoire de façon virtuel sachant que l'espace d'adressage d'un module est limité à PAGE_SIZE fois 2.

Le deuxième argument induit le comportement de la fonction et dont les principales valeurs définies dans <linux/mm.h>, sont :

GFP_USER  : Alloue de la mémoire pour l'utilisateur.

La fonction peut dormir.

GFP_KERNEL  : Allocation de mémoire kernel normal

en essayant de puiser dans la réserve de mémoire allouer au module.

La fonction peut dormir.

FP_ATOMIC  : Allocation sans dormir.

Pour des fonctions ou cela n'est pas permis

comme dans un interrupt handler.

La fonction retourne un pointeur sur l'espace mémoire alloué ou NULL en cas d'erreur.

L'espace mémoire alloué doit être libéré avec la fonction définis dans <linux/slab.h> :

void kfree(const void *)

II-B. Structure d'un module 

Un module chargeable n'a pas de fonction main() mais un point d'entrée et de sortie correspondants à l'action d'accomplir quand on charge ou décharge le module.

Le point d'entrée d'un module peut se faire en définissant une fonction :

 
Sélectionnez
1.
2.
3.
4.
int init_module(void) 
{
  return 0 ; /* Le code de retour est important */
}

Et un point de sortie :

 
Sélectionnez
1.
2.
3.
4.
void cleanup_module(void) 
{
  return ;
}

Si l'on respecte strictement le nommage du point d'entrée et de sortie la fonction init_module sera appelée automatiquement lors du chargement du module, et la fonction cleanup_module au déchargement du module.

La fonction init_module sert à initialiser les ressources nécessaires au module, donc aussi à faire des tests pour l'initialisation ou l'enregistrement par le noyau d'un driver - par exemple : le sort du module dépend de son code de retour ; si celui-ci est différent de zéro le module ne sera pas chargé et la fonction cleanup_module sert a dé-initialiser les données et donc à nettoyer, d'où son nom.

Finalement, je précise qu'il est nécessaire de définir la licence du module afin que celui-ci soit chargeable par la macro: MODULE_LICENSE() ;

qui accepte pour valeur définis dans <linux/module.h>:

« GPL » [GNU Public License v2 or later]

« GPL v2 » [GNU Public License v2]

« GPL and additional rights » [GNU Public License v2 rights and more]

« Dual BSD/GPL » [GNU Public License v2or BSD license choice]

« Dual MIT/GPL » [GNU Public License v2 or MIT license choice]

« Dual MPL/GPL » [GNU Public License v2 or Mozilla license choice]

« Proprietary » [Non free products]

II-C. Écriture, compilation et insertion dans le noyau:

Voici le code source du fichier module1.c :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
#include <linux/module.h>

MODULE_LICENSE("GPL") ;
MODULE_DESCRIPTION("Module structure demonstration") ;
MODULE_AUTHOR("mrcyberfighter") ;

int init_module(void) 
{
 printk(KERN_DEBUG "Module inserted in kernel throught insmod !!!\n") ;
 return 0 ;
}

void cleanup_module(void) 
{
 printk(KERN_DEBUG "Module remove from kernel throught rmmod  !!!\n") ;
 return ;
}

Nous le compilons avec le fichier de compilation automatisé Makefile suivant :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
obj-m += module1.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
 $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean :
 $(MAKE) -C $(KERNELDIR) m=$(PWD) clean

$ sudo make
make -C /lib/modules/3.11.0-18-generic/build M=/home/edward/Bureau/dossier modules

make[1]: entrant dans le répertoire « /usr/src/linux-headers-3.11.0-18-generic »

CC [M]  /home/edward/Bureau/dossier/module1.o
Building modules, stage 2.
MODPOST 1 modules
CC      /home/edward/Bureau/dossier/module1.mod.o
LD [M]  /home/edward/Bureau/dossier/module1.ko

make[1]: quittant le répertoire « /usr/src/linux-headers-3.11.0-18-generic »
$ ls
Makefile   module1.ko     module1.mod.o  modules.order
module1.c  module1.mod.c  module1.o      Module.symvers

Puis nous procédons au chargement dans le noyau du module :

 
Sélectionnez
1.
$ sudo insmod module1.ko

Vérifions si le module est présent dans le noyau :

 
Sélectionnez
1.
2.
3.
4.
$ lsmod
Module                  Size  Used by
module1                12426  0
...

Regardons les métadonnées du module :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
$ modinfo module1.ko
filename:       /home/edward/Bureau/module/module1.ko
author:         mrcyberfighter
description:    Module structure demonstration
license:        GPL
srcversion:     6941103565A089C4B575D10
depends:
vermagic:       3.11.0-18-generic SMP mod_unload modversions

Puis déchargeons le module :

 
Sélectionnez
1.
$ sudo rmmod module1.ko

Et finalement regardons si le module a écrit dans les logs :

 
Sélectionnez
1.
2.
3.
4.
$ dmesg
...
[ 4264.823025] Module inserted in kernel throught insmod !!!
[ 4587.017096] Module remove from kernel throught rmmod  !!!

III. Écriture d'un driver

III-A. Introduction à un driver

Il existe 2 types de drivers :

  • Les drivers en mode caractères.
  • Les drivers en mode bloc.

Ils sont soit chargeables à chaud dans le noyau grâce à la fonction insmod ou insérer dans le noyau à la compilation.

Un driver contrôle le périphérique. Dans notre cas nous souhaitons contrôler un périphérique virtuel, avec un driver en mode caractères. Il nous faudra donc créer un périphérique virtuel (un fichier spécial en mode caractères) dans le dossier /dev/.

III-B. Fonctions nécessaires 

Pour cela il faut procéder aux étapes suivantes :

  1. Attribuer un numéro majeur et mineur qui identifiera le périphérique que nous souhaitons contrôler :
    Le numéro peut être défini :
    de manière statique en demandant le numéro majeur que nous souhaitons, mais il faut savoir que le numéro majeur est codé sur un byte non-signé dans la plage de 0-255.
    Seules les plages suivantes peuvent être utilisées :

    • 60 - 63.
    • 120 - 127
    • 240 - 255


    Le numéro ne doit pas être attribué déjà, vérifiez en lisant le fichier /proc/devices.
    $ cat /proc/devices
    de manière dynamique en laissant le noyau réserver un numéro majeur garantit valide et non-attribué.

  2. Réserver le numéro de périphérique en mode caractère.
    En réservant au même temps que l'allocation mémoire du périphérique avec la fonction :
    int alloc_chrdev_region(dev_t major,
    unsigned int minor,
    unsigned int minor_numbers,
    const char *name) ;
    qui renvoie 0 en cas de succès.
  3. Allouer de la mémoire pour instancier notre objet driver avec la fonction :
    Avec la fonction : void *cdev_alloc(void) ; qui renvoie un pointeur de type struct cdev sur l'objet périphérique en cas de succès sinon un pointeur NULL.
  4. Lier l'objet périphérique avec une structure de type file_operations qui permet de prendre en charge les différentes actions que peuvent entreprendre le programme client du driver dont nous reparleront.
  5. Lier l'objet driver au numéro référençant le périphérique par la fonction :
    int cdev_add(struct cdev *driver_object, dev_t dev_num, unsigned int major) ;
  6. Créer une classe de notre périphérique avec la macro :
    class_create(owner, name)
  1. Créer celui-ci finalement et l'enregistrer dans le système de fichier /dev/ grâce à la fonction :
    struct device *device_create(struct class *cls,
    struct device *parent,
    dev_t devt,
    void *drvdata,
    const char *fmt,...)
  2. Détruire les ressources créées en cas d'erreur et retourner un code d'erreur grâce à un saut avec un goto lors d'un contrôle d'un code de retour :

    • Par la fonction :


    void unregister_chrdev_region(dev_t dev_num,unsigned int minor_count) ;
    On restitue l'espace alloué pour notre périphérique et la référence au numéro majeur ayant minor_count numéros mineurs.

    • Par la fonction :


    kobject_put(&driver_object->kobj) ;
    On restitue l'espace alloué à l'objet driver en décrémentant à zéro la référence dans le noyau.

    • On peut retourner un code d'erreur -EIO.
  3. Détruire les ressources créées dans la fonction cleanup_module() :
    Grâce aux fonctions suivantes :
    void device_destroy(struct class *cls, dev_t dev_num);
    qui détruit le périphérique virtuel représenté par la classe du périphérqiue cls lier au numéro majeur dev_num.
    void class_destroy(struct class *cls);
    qui détruit la classe du périphérique virtuel.
    void cdev_del(struct cdev *);
    qui libère l'espace alloué à la structure du driver.
    void unregister_chrdev_region(dev_t dev_num, unsigned int minor_count) ;
    qui restitue l'espace alloué pour notre périphérique et la référence au numéro majeur ayant minor_count numéros mineurs.

III-C. Écriture, compilation et insertion dans le noyau:

Assez de théorie - en pratique cela donne le code suivant :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
#include <linux/module.h>  /** MODULE_LICENSE &&
                               MODULE_DESCRIPTION &&
                               MODULE_AUTHOR                 */

#include <linux/cdev.h>    /** cdev_del() &&
                               cdev_add() &&
                               cdev_alloc()                  */

#include <linux/device.h>  /** class_create() &&
                               device_create()  &&
                               device_destroy()              */

#include <linux/fs.h>      /** alloc_chrdev_region() &&
                               unregister_chrdev_region()    */

MODULE_LICENSE("GPL") ;
MODULE_DESCRIPTION("Driver creating as module.") ;
MODULE_AUTHOR("mrcyberfighter") ;

#define DEVICE_NAME "devname"       /** Device name   */

static dev_t dev_num ;              /** Major number  */
static struct cdev *driver_object ; /** Driver object */
static struct class *device_class ; /** Device class  */


static int __init mod_init(void) 
{

 static int minor=0 ;
 static int minor_numbers=1 ;

 printk(KERN_DEBUG "start device creating\n") ;

 if (alloc_chrdev_region(&dev_num,minor,minor_numbers,DEVICE_NAME) < 0)
 {
      /** Allocate space for device and major number dev_num
          with minor number minor and minor_numbers count
          and as device name devname                          */

    printk(KERN_DEBUG "Device number and space alloctating error !!! \n") ;
    return -EIO ;
 }

 driver_object=cdev_alloc() ; /** Driver space allocating and
                              instantiation as struct cdev object. */

 if (driver_object == NULL) 
 {
   goto free_device_number ;
 }

 driver_object->owner=THIS_MODULE ;


 if (cdev_add(driver_object,dev_num,1)) 
 { 
   /** register the driver object to the kernel with the device number  dev_num and counts of minor numbers 1 */ 
   goto free_device ;
 }

 device_class=class_create(THIS_MODULE,DEVICE_NAME) ;
    /** create new device class      */

 device_create(device_class,NULL,dev_num,NULL,"%s",DEVICE_NAME) ;
    /** register device in /dev/  filesystem  */

 if (device_class == NULL) 
 {
    printk(KERN_DEBUG "Device class creating error !!!\n") ;
    goto free_device ;
 }

 printk(KERN_DEBUG "Device successfull created !!!\n") ;
 return 0 ;

    /** error handling goto ; */
 free_device :
   printk(KERN_DEBUG  "Driver registering error or\n"
                         "Device class creation error.\n"
                         "Must free allocated space for device.\n") ;
   kobject_put(&driver_object->kobj) ;

 free_device_number :
   printk(KERN_DEBUG "chrdev registering error !!!\n") ;
   unregister_chrdev_region(dev_num,1) ;
   return -EIO ;
}

static void __exit mod_exit(void) 
{

  device_destroy(device_class,dev_num) ;
  /** destroy device */

  class_destroy(device_class) ;
  /** destroy device class */

  cdev_del(driver_object) ;
  /** free driver_object memory space */

  unregister_chrdev_region(dev_num,1) ;
  /** unregister dev_t dev_num and minors count 1 */

  printk(KERN_DEBUG "Device destruction succesfull !!!\n") ;
  return ;

}

module_init(mod_init) ; /** Register init_module macro.    */
module_exit(mod_exit) ; /** Register cleanup_module macro. */

Remarquez que nous n'avons pas utilisé la règle de nommage des fonctions d'initialisation (init_module) et de dé-initialisation (cleanup_module ) et l'ajout d'un mot-clef aux fonctions d'initialisation et de dé-initialisation sous forme de : __exit __init qui servent à la spécification de l'allocation mémoire du noyau des fonctions. Notez simplement que __init et __exit sont à placer devant le nom de la fonction - il existe d'autres spécificateurs notamment pour les variables et fonctions d'un driver pour un device hotplugable.

Ici elles n'ont pas de répercussion pour un module chargeable mais sont une bonne habitude à prendre.

Nous nous servons des macros module_init() et module_exit() qui servent à référencer les fonctions d'initialisation et de dé-initialisation afin que l'on puisse les nommer comme nous le souhaitons.

Après exécution du fichier Makefile correspondant au fichier driver.c, on charge le module-driver grâce a la fonction insmod et l'on consulte le fichier /proc/devices qui répertorie les périphériques et est divisé en 2 parties :

  • Les périphériques en mode caractères.
  • Les périphériques en mode bloc.
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
$ sudo make
$ sudo insmod driver.ko
$ cat /proc/devices
Character devices:
1 mem
...
250 devname
...
254 rtc

Block devices:
1 ramdisk
...
254 mdp

on s'aperçoit de la création de notre périphérique « devname » avec comme numéro majeur 250.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
$ sudo rmmod driver.ko
$ cat /proc/devices
Character devices:
1 mem
...
254 rtc

Block devices:
1 ramdisk
...
254 mdp

Après déchargement du module-driver « devname » n'est plus présent comme périphérique.

IV. La structure file_operations

IV-A. Introduction a la structure file_operations

Afin de lier notre driver aux requêtes d'un programme client, il existe une structure définie dans : <linux/fs.h>

Qui a pour prototype :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
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 (*aio_read) (struct kiocb *, const struct iovec *,
  unsigned long, loff_t);
  ssize_t (*aio_write) (struct kiocb *, const struct iovec *,
  unsigned long, loff_t);
  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 (*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 **);
     long (*fallocate)(struct file *file, int mode, loff_t offset,off_t len);
     int (*show_fdinfo)(struct seq_file *m, struct file *f);
};

Comme vous voyez, elle contient des pointeurs vers diverses fonctions et c'est en définissant ces fonctions qu'on va lier une action du programme client au driver chargé dans le noyau. Vous avez sûrement remarqué que le prototype ne donne seulement le type des paramètres (c'est spécifique des prototypes des sources du kernel).

Nous allons nous intéresser aux fonctions et à leurs paramètres:

  1. open().
  2. release() Équivalent a close().
  3. read()
  4. write().

Remarquez que la fonction unlock_ioctl correspond à la fonction ioctl .

La fonction compat_ioctl permet la prise en charge sur un système 64 bits la compatibilité d'un processus 32 bits grâce au transtypage du 3ᵉ argument de qui est de type :

unsigned long sur un système 32 bits.

void * sur un système 64 bits.

IV-B. Description de la fonction open()

Déclaration :

 
Sélectionnez
1.
static int *open(struct inode *device_file, struct file * instance) ;

La structure inode représente le périphérique que nous souhaitons contrôler. Elle est définie dans <linux/fs.h>.

On peut s'en servir pour vérifier :

  1. les droits d'accès,
  2. le propriétaire,
  3. le numéro majeur et mineur du périphérique,

et bien d'autres choses (consultez les membres dans les sources). La structure file représente l'instance du driver et est définie dans <linux/fs.h>. On peut s'en servir pour :

  1. vérifier les droits d'accès (utile dans d'autres fonctions ou il n'y a pas de structure inode passer en argument),
  2. vérifier le mode d'accès (O_NONBLOCK),

et bien d'autres choses (consultez les membres dans les sources). Cette fonction doit retourner 0 en cas de succès sinon un code d'erreur. Cette fonction est appelée dans le driver à chaque ouverture par une application du périphérique que nous souhaitons contrôler.

IV-C. Description de la fonction release() 

Déclaration :

 
Sélectionnez
1.
static int *release(struct inode *, struct file *);

Les paramètres sont les mêmes que pour la fonction open() ;

Cette fonction doit retourner 0 en cas de succès sinon un code d'erreur. Cette fonction est appelée à chaque fermeture par une application du périphérique que nous souhaitons contrôler.

IV-D. Description de la fonction read()

Déclaration :

 
Sélectionnez
1.
2.
static size_t *read(struct file *instance, char __user  *userbuffer,
                                        size_t count, loff_t *offset) ;

Cette fonction est appelée dans le driver à chaque opération de lecture fait par l'application qui communique avec le périphérique que nous souhaitons contrôler.

  • La structure file joue le même rôle que dans la fonction open().
  • Le paramètre userbuffer:

    Notez au passage le mot-clé __user devant le paramètre userbuffer signifie que l'adresse pointée se trouve dans l'espace utilisateur (user-space), pointé sur cette adresse. Ceci est l'argument le plus important, car comme énoncé précédemment l'espace mémoire utilisateur (celui de l'application cliente du périphérique que nous souhaitons contrôler) est séparée de l'espace mémoire du noyau (dans lequel s'exécute le driver). Il faut faire une translation d'adresse entre les deux afin de copier - dans l'espace utilisateur - les bytes (que l'application qui communique avec notre périphérique) demandent à lire, et renvoyer le nombre de bytes copiés dans celui-ci.

    Pour cela il existe plusieurs fonctions fournies par les sources du noyau définies dans <asm/uaccess.h> qui possèdent un fichier propre à chaque architecture.

    copy_to_user(void *dst, const void *src, unsigned long count) ;

    copie count bytes depuis *src dans l'espace d'adresse utilisateur *dst et renvoie le nombre de bytes non-copiés.

    ou la macro qui ne supporte que des adresses de type char* ou int* :

    put_user(const void *to_copy, void *dst) ;

    qui renvoie 0 en cas de succès, sinon -EFAULT.

    (Remarquer la conversion en nombre négatif du code d'erreur typique de la programmation driver du noyau).

    Par exemple pour renvoyer un « Hello world » à l'application qui tente de lire notre périphérique :

    copy_to_user(userbuffer, »Hello world »,strlen(Hello world)+1) ;

    ou

    put_user("Hello world",userbuffer) ;

    Ces fonctions font une vérification de la validité des adresses ce que l'on peut empêcher (pour des questions de performance) en employant les fonctions sous-jacentes de ces fonctions qui ne vérifie pas la validité des adresses :

    __copy_to_user() ;

    __put_user() ;

    Finalement on peut vérifier la validité d'une adresse avec la macro :

    access_ok(type, addr, size) ;

    type : Type of d'access: VERIFY_READ ou VERIFY_WRITE.

    addr : adresse à vérifier.

    Size : nombre de bytes à vérifier.

  • L'argument count correspond au nombre de bytes demandés par l'application communiquant avec notre périphérique - à lire depuis le périphérique.

  • L'argument offset est la position dans l'argument userbuffer à partir duquel l'application communiquant avec notre périphérique souhaite lire.

  • La fonction doit renvoyer le nombre de bytes copiés dans l'espace utilisateur de l'application communiquant avec notre périphérique.

IV-E. Description de la fonction write() 

Déclaration :

 
Sélectionnez
1.
2.
ssize_t *write(struct file *instance, const char __user *userbuffer,
                                        size_t count, loff_t *offset) ;

Cette fonction est appelée dans le driver pour une opération d'écriture (faîtes par l'application communiquant avec le périphérique que nous souhaitons contrôler.

  • La structure file joue le même rôle que dans la fonction open().
  • Le pointeur userbuffer pointe sur l'adresse où sont stockées les données que le client désire communiquer au périphérique.

    Il faut faire une translation d'adresse entre les deux, afin de copier depuis l'espace utilisateur les bytes que l'application communiquant avec notre périphérique demande à écrire - et renvoyer le nombre de bytes copiés dans celui-ci.

    Pour cela il existe plusieurs fonctions fournies par les sources du noyau définies dans <asm/uaccess.h> qui possèdent un fichier propre à chaque architecture.

    copy_from_user(void *src, const void __user *dst, unsigned long count);

    copie count bytes depuis *src dans l'espace d'adresse utilisateur *dst et renvoie le nombre de bytes non-copiés.

    ou la macro qui ne supporte que des adresses de type char* ou int* :

    get_user(void *dst,const void *to_copy) ;

    qui renvoie 0 en cas de succès, sinon -EFAULT.

    ou la fonction qui ne supporte que du texte ASCII :

    strncpy_user(char *dst,const char __user *src,long count) ;

    À copier seulement depuis l'espace utilisateur dans l'espace du noyau avec la fonction :

    strnlen_user(const char __user *src,long count) ;

    Renvoie la taille du string pointé par src jusqu'au '0' final. Sinon 0 en cas d'erreur (adresse invalide, pas droit d'accès aux données).

    Il existe des fonctions sous-jacentes qui ne vérifient pas la validité des adresses.

    __copy_from_user() ;

    __get_user() ;

  • L'argument count correspond au nombre de bytes écrit par l'application communiquant avec notre périphérique.

  • L'argument offsetuserbuffer est la position dans l'argument userbuffer à partir duquel l'application communiquant avec notre périphérique désire écrire des données.

  • La fonction doit renvoyer le nombre de bytes copiés dans l'espace du noyau.

IV-F. Un exemple Hello world

Voici un exemple, qui renvoie « Hello World » à chaque tentative de lecture en écrivant ce que l'application communiquant avec notre périphérique lui envoie dans le fichier de log.

Le code créant le périphérique et driver de celui-ci : hello_world_driver.c

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
#include <linux/module.h>  /** MODULE_LICENSE &&
 MODULE_DESCRIPTION &&
 MODULE_AUTHOR               */

#include <linux/cdev.h>    /** cdev_del() &&
                               cdev_add() &&
                               cdev_alloc()                */

#include <linux/device.h>  /** class_create() &&
                               device_create()  &&
                               device_destroy()            */
#include <linux/fs.h>      /** alloc_chrdev_region() &&
                               unregister_chrdev_region()  */

#include <linux/string.h>  /** strlen()                    */
#include <linux/fs.h>      /** struct file_operations      */

#include <asm/uaccess.h>   /** copy_to_user() &&
                               copy_from_user()            */

MODULE_LICENSE("GPL") ;
MODULE_DESCRIPTION("Hello World device driver") ;
MODULE_AUTHOR("None") ;

#define DEVICE_NAME "devname"

static dev_t dev_num ;
static struct cdev *driver_object ;
static struct class *device_class ;

static char hello_msg[]="hello world" ;

static ssize_t
file_ops_read(struct file *instance,char __user *read_buffer,
                                 size_t count,loff_t *offset ) 
{
    ssize_t to_copy, not_copy ;
    to_copy=min(count,strlen(hello_msg)+1) ;
    not_copy=copy_to_user(read_buffer,hello_msg,to_copy) ;
    printk(KERN_DEBUG "%s send to application !!!\n",hello_msg) ;
    if (not_copy) 
    {
        printk(KERN_DEBUG "copy_to_user() error !!!\n") ;
    }
    return to_copy - not_copy ;
}
static ssize_t
file_ops_write(struct file *instance,const char __user *write_buffer,
                                         size_t count,loff_t *offset )
{
    ssize_t to_copy, not_copy ;
    char kernel_buf[128] ;
    memset(kernel_buf,'\0',128) ;
    to_copy=min(count,strlen(write_buffer)+1) ;
    not_copy=copy_from_user(kernel_buf,write_buffer,to_copy) ;
    printk(KERN_ALERT "Application send: %s !!!\n",kernel_buf) ;
    return to_copy-not_copy;
}

static int
file_ops_open(struct inode *device_file,struct file *instance ) 
{
    printk(KERN_DEBUG "device open function triggering !!!\n") ;
    printk(KERN_DEBUG "driver major: %d minor %d !!!\n",
                     imajor(device_file),iminor(device_file)) ;
    if (instance->f_flags & O_RDWR)
    {
        printk(KERN_DEBUG "device open in rw mode !!!\n") ;
    }
    else if (instance->f_flags & O_RDONLY) 
    {
        printk(KERN_DEBUG "error: device open in read-only mode !!!\n") ;
        return -EIO ;
    }
    else if (instance->f_flags & O_WRONLY) 
    {
        printk(KERN_DEBUG "error: device open in read-only mode !!!\n") ;
        return -EIO ;
    }
    return 0 ;
}

static int
file_ops_release(struct inode *device_file,struct file *instance )
{
    printk(KERN_DEBUG "device release function triggering !!!\n") ;
    return 0 ;
}

/** Define file_operations structure
to bind to driver for client actions
    processing.                   */

struct file_operations fops = 
{
    .read         = file_ops_read  ,
    .write        = file_ops_write ,
    .open         = file_ops_open  ,
    .release      = file_ops_release  ,
} ;

static int __init my_device_init(void) 
{
    static int minor=0 ;
    static int minor_numbers=1 ;
    printk(KERN_DEBUG "start device creating\n") ;
    if (alloc_chrdev_region(&dev_num,minor,minor_numbers,DEVICE_NAME) < 0) 
    {
    /** Allocate space for device and major number dev_num
        with minor number minor and minor_numbers count
        and as device name devname                          */

        printk(KERN_DEBUG "Device number and space alloctating error !!! \n") ;
        return -EIO ;
     }
  driver_object=cdev_alloc() ; 
/** Driver space allocating and instantiation as struct cdev object. */

    if (driver_object == NULL) 
    {
        goto free_device_number ;
    }
    driver_object->owner=THIS_MODULE ;
    driver_object->ops=&fops ;

    if (cdev_add(driver_object,dev_num,1)) 
    { 
        /** register the driver object to the kernel  */
        goto free_device ;
    }
    device_class=class_create(THIS_MODULE,DEVICE_NAME) ;
    /** create new device class     */

    device_create(device_class,NULL,dev_num,NULL,"%s",DEVICE_NAME) ;
    /** register device in /dev/  filesystem  */
    
    if (device_class == NULL) 
    {
        printk(KERN_DEBUG "Device class creating error !!!\n") ;
        goto free_device ;
    }
    printk(KERN_DEBUG "Device successfull created !!!\n") ;
    return 0 ;

  /* Errors handlong gotos */
    free_device :
        printk(KERN_DEBUG "Must free allocated space for device.\n") ;
        kobject_put(&driver_object->kobj) ;
    free_device_number :
        printk(KERN_DEBUG "chrdev registering error !!!\n") ;
        unregister_chrdev_region(dev_num,1) ;
        return -EIO ;
}
static void
__exit my_device_exit(void) 
{
    device_destroy(device_class,dev_num) ;
    /** destroy device */

    class_destroy(device_class) ;
    /** destroy device class */

    cdev_del(driver_object) ;
    /** free driver_object memory space */

    unregister_chrdev_region(dev_num,1) ;
    /** unregister dev_t dev_num and minors count 1 */

    printk(KERN_DEBUG "Driver destruction succesfull !!!\n") ;
    return ;
}

module_init(my_device_init) ;
module_exit(my_device_exit) ;

Et voici le code de l'application client de notre driver : hello_world_client.c

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

int main(void) 
{
 int fd ;
 if ((fd=open("/dev/devname",O_RDWR | O_NOCTTY ) ) >= 0) 
  {
   /** Device opening succesfull */
   fprintf(stdout,"open devname succesfull !!!\n") ;
  }
 else 
  {
    fprintf(stderr,"open err) %m\n") ;
    exit(EXIT_FAILURE) ;
  }

  int ctrl=1 ;
  while (ctrl) 
  {
    char *rd_buf=calloc(128,sizeof(char)) ;
    char *wr_buf=calloc(128,sizeof(char)) ;

    if (read(fd,rd_buf,(size_t) 128) > 0) 
    {
      /** Reading from device succcessfull */
      fprintf(stdout,"device devname answer: %s\n",rd_buf) ;
    }
    else 
    {
      fprintf(stderr,"rd err) %m\n") ;
      exit(EXIT_FAILURE) ;
    }

    fprintf(stdout,"Enter message to send to device\n") ;

    fgets(wr_buf,128,stdin) ;

    wr_buf[strlen(wr_buf)-1]='\0' ;
    /** Disable the linefeed character added from fgets() */

    if (strcmp(wr_buf,"stop") == 0)
    {
      /** We stop the commnunication with the device */
      ctrl=0 ;
      break ;
    }

    if (write(fd,wr_buf,strlen(wr_buf)) > 0) 
    {
      /** Message sending to device succcessfull */
      fprintf(stdout,"message send to device:\n%s\n",wr_buf) ;
    }


    else 
    {
      fprintf(stderr,"wr err) %m\n") ;
      exit(EXIT_FAILURE) ;
    }

    free(rd_buf) ;
    free(wr_buf) ;
  }
  close(fd) ;
  exit(EXIT_SUCCESS) ;
}

Après avoir exécuté le fichier Makefile correspondant au fichier hello_world_driver.c et après compilation de l'application client du driver hello_world_client.c . Vous pourrez les faire communiquer ainsi :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
$ sudo make
$ sudo insmod hello_world_driver.ko
$ gcc hello_world_client.c -o device_client
$ sudo device_client # Sinon changer les droits d'accès de /dev/devname
open devname succesfull !!!
device devname answer: hello world
Enter message to send to device
hi you device
message send to device:
hi you device
device devname answer: hello world
Enter message to send to device
stop
$
$ dmesg
[ 6784.816991] start device creating
[ 6784.817151] Device successfull created !!!
[ 6805.066774] device open function triggering !!!
[ 6805.066779] driver major: 250 minor 0 !!!
[ 6805.066781] device open in rw mode !!!
[ 6805.066866] hello world send to application !!!
[ 6815.855711] Application send: hi you device !!!
[ 6815.855743] hello world send to application !!!
[ 6818.785004] device release function triggering !!!

V. Un exemple de driver de périphérique virtuel 

V-A. Introduction à l'exemple avancé 

Nous allons écrire un driver pour un périphérique virtuel avec quelques notions de programmation noyau en plus.

Nous allons nous servir du type de données atomique : atomic_t

  • Pour gérer le nombre d'application se connectant au périphérique. Nous souhaitons qu'une seule application puisse se connecter.
  • Pour le buffer de lecture et d'écriture.

Pour initialiser une variable de type atomic_t statiquement nous allons utiliser la macro :

ATOMIC_INIT() définie dans <asm/atomic.h> qui initialise une variable de type atomic_t avec l'argument passé en paramètre.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
static atomic_t access_counter = ATOMIC_INIT(-1) ;
/** atomic access counter declaration and initialisation. */

static atomic_t bytes_read_available = ATOMIC_INIT(-1) ;
/** atomic bytes to read checker variable declaration and initialisation.  */

static atomic_t bytes_write_available = ATOMIC_INIT(-1) ;
 /** atomic bytes to write checker variable declaration and initialisation. */
Puis nous allons définir deux macros qui vérifient respectivement si les données sont disponibles en lecture et en écriture :
#define RD_OK ( atomic_read(&bytes_read_available) != 0)
 /** Macro to check if data available to be read.  */

#define WR_OK ( atomic_read(&bytes_write_available) != 0)
/** Macro to check if data available to be written.  */

Nous allons aussi nous occuper de l'aspect de la disponibilité de données en faisant dormir le périphérique - en attendant que les données soit effectivement disponibles et en implémentant un type de données prenant la forme d'une queue. De type wait_queue_head_t définis dans <linux/wait.h>.

Une queue est une structure de données fournis par le noyau, fonctionnant selon le principe que l'on peut écrire et lire des données dans/depuis celle-ci sous forme d'accès FIFO (First In First Out).

Mais ce type de données est une wait_queue_head_t signifiant que l'accès à la queue est soumis à une mise en attente souhaitant qu'un événement se produise. Nous allons utiliser la fonction : wait_event_interruptible(wait_queue_head_t queue,condition) ; définie dans <linux/wait.h>.

Tout cela endort notre périphérique - en attendant un signal - quand il arrive, le signal produit la vérification de la condition (celle-ci décide si le périphérique continue à dormir ou pas).

Afin de générer des nombres aléatoires de calculs, nous allons faire une sorte de process list (commande ps) que nous enregistrerons dans une structure - dont les membres nous donnent matière à générer deux nombres aléatoires - nous prendrons qu'un seul élément (l'index nous est fourni par le nombre des changements de contexte du processus actuellement en cours d'exécution, accessible par la macro current) :

  • Le temps imparti au processus pour s'exécuter dans le processeur (celui-ci est calculé par ordonnanceur).
  • Le temps passé dans le processeur depuis son lancement.

V-B. Code d'un driver de périphérique virtuel 

Voici un exemple, demande à faire une addition à la première tentative de lecture puis enregistre votre réponse envoyée par la fonction write de l'application communicante avec notre périphérique et vous dit à la deuxième tentative de lecture si le résultat est exact en alternance.

Le code créant le périphérique et driver de celui-ci: calc_device_driver.c

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.
197.
198.
199.
200.
201.
202.
203.
204.
205.
206.
207.
208.
209.
210.
211.
212.
213.
214.
215.
216.
217.
218.
219.
220.
221.
222.
223.
224.
225.
226.
227.
228.
229.
230.
231.
232.
233.
234.
235.
236.
237.
238.
239.
240.
241.
242.
243.
244.
245.
246.
247.
248.
249.
250.
251.
252.
253.
254.
255.
256.
257.
258.
259.
260.
261.
262.
263.
264.
265.
266.
267.
268.
269.
270.
271.
272.
273.
274.
275.
276.
277.
278.
279.
280.
281.
282.
283.
284.
285.
286.
287.
288.
289.
290.
291.
292.
293.
294.
295.
296.
297.
298.
299.
300.
301.
302.
303.
304.
305.
306.
307.
308.
309.
310.
311.
312.
313.
314.
315.
316.
317.
318.
319.
320.
321.
322.
323.
324.
325.
326.
327.
328.
329.
330.
331.
332.
333.
334.
335.
336.
337.
338.
339.
340.
341.
342.
343.
344.
345.
346.
347.
348.
349.
350.
351.
352.
353.
354.
355.
356.
357.
358.
359.
360.
#include <linux/module.h>  /** MODULE_LICENSE &&
                               MODULE_DESCRIPTION &&
                               MODULE_AUTHOR               */

#include <linux/cdev.h>    /** cdev_del() &&
                               cdev_add() &&
                               cdev_alloc()               */

#include <linux/device.h>  /** class_create() &&
                               device_create()  &&
                               device_destroy()           */

#include <linux/fs.h>      /** alloc_chrdev_region() &&
                               unregister_chrdev_region() */

#include <linux/string.h>  /** strncpy()                  */

#include <linux/fs.h>      /** struct file_operations     */

#include <asm/uaccess.h>   /** copy_to_user() &&
                               copy_from_user             */

#include <linux/sched.h>   /** struct task_struct         */

MODULE_LICENSE("GPL") ;
MODULE_DESCRIPTION("Addition device driver") ;
MODULE_AUTHOR("None") ;

#define DEVICE_NAME "devname"

static dev_t dev_num ;
static struct cdev *device_object ;
static struct class *device_class ;

static wait_queue_head_t wait_read, wait_write ;

static atomic_t access_counter = ATOMIC_INIT(-1) ;
/** atomic access counter. */

static atomic_t bytes_read_available = ATOMIC_INIT(-1) ;
/** atomic bytes to read checker variable initialisation.  */

static atomic_t bytes_write_available = ATOMIC_INIT(-1) ;
/** atomic bytes to write checker variable initialisation. */



#define RD_OK ( atomic_read(&bytes_read_available) != 0)
/** Macro to check if data available to be read.           */

#define WR_OK ( atomic_read(&bytes_write_available) != 0)
/** Macro to check if data available to be written.        */


unsigned long result ; /** Addition result */

char res_str[128] ;
/** char buffer for getting answers from client application */

int status=0 ;
/** Switcher:
*  Sending adddition.
*  Sending result check.
************************/

struct Data_Random 
{
  /** Data structure for process list */
  unsigned long value1 ;
  unsigned long value2 ;
} 
random[256] ;

u16 c ;
unsigned long value1 ; /** Operand 1  */
unsigned long value2 ; /** Operand 2  */

void register_random_data(void) 
{

  struct task_struct *task ; /** Iterator */

  c=0 ;

  for_each_process(task)  
  {
    /** Registering 256 processes in run. */

    random[c].value1=task->se.vruntime ;
    /** The amount of time given for the process to stay in the      processor. */

    random[c].value2=task->se.exec_start ;
    /** The time the process spend in the processor since start.           */
    c++ ;

    if (c == 256) 
    {
      break ;
    }

  }
  return ;
}



void set_random_data(void)
{
  struct task_struct *task ;

  task=current ; /** Get current process */

  value1=random[(task->fpu_counter < c) ? task->fpu_counter : 0 ].value1 % 255 ;
  /** task->fpu_counter The number of consecutive
      context switches that the FPU is used.   */

  value2=random[(task->fpu_counter < c) ? task->fpu_counter : 0 ].value2 % 255 ;
  /** task->fpu_counter The number of consecutive
      context switches that the FPU is used.   */

  return ;

}

static int
file_ops_open(struct inode *device_file,struct file *instance ) 
{

  if (instance->f_flags & O_RDWR) 
  {
    printk(KERN_ALERT "device open in rw mode !!!\n") ;

    if (atomic_inc_and_test(&access_counter) == 1) 
    {
      /** We permit only one client application at the same time
      *  by testing atomic_inc_and_test(&access_counter)
      *  who return 1 only if access_counter == 1 after incrementing
      ***************************************************************/
      return 0 ;
    }
    else 
    {
      return -EBUSY ;
    }
  }
  else 
  {
    printk(KERN_ALERT "device open in wrong mode !!!\n") ;
    return -EIO ;

  }

  printk(KERN_DEBUG "device open function triggering !!!\n") ;
  return 0 ;
}



static int
file_ops_release(struct inode *device_file,struct file *instance )
{
  atomic_dec(&access_counter) ;
  /** Release access control lock */
  
  printk(KERN_DEBUG "device release function triggering !!!\n") ;
  return 0 ;
}


static ssize_t
file_ops_read(struct file *instance,char __user *read_buffer,
                                 size_t count,loff_t *offset ) 
{

  size_t to_copy, not_copy ;
  char buffer_kern_space[128] ; /** kernel data-space buffer */

  if ((instance->f_flags & O_NONBLOCK) && ! RD_OK) 
  {
    /** case O_NONBLOCK modus and data to read not available. */
    printk(KERN_DEBUG "device read non-block and not ready !!!\n") ;
    return -EAGAIN ;
  }

  if (wait_event_interruptible(wait_read,RD_OK)) 
  {
    /** Wait until data available throught RD_OK macro check.  */
    printk(KERN_DEBUG "device read not wake up error !!!\n") ;
    return -ERESTART ;
  }

  to_copy=min((size_t) atomic_read(&bytes_read_available),count) ;
  /** atomic read operation and return gthe number of bytes available. */

  if (status == 0) 
  {
    register_random_data() ;
    set_random_data() ;

    result = value1 + value2 ;

    sprintf(buffer_kern_space,"\nHello compute:\n %lu + %lu",value1,value2) ;
    /** copy readed data in kernel buffer to answers */
    status=1 ;
  }
  else 
  {
    if (result == simple_strtoul(res_str,NULL,0)) 
    {
      sprintf(buffer_kern_space,"RIGHT\n") ;
      /** Wrinting answers in kernel buffer to send to client. */
    }
    else 
    {
      sprintf(buffer_kern_space,"WRONG\n") ;
      /** Wrinting answers in kernel buffer to send to client. */
    }
    status=0 ;
  }

  not_copy=copy_to_user(read_buffer,buffer_kern_space,to_copy) ;
  /** copy readed data in kernel-space buffer to return to client application.
    * and return number of not read data
    **************************************************************************/

  atomic_sub(to_copy-not_copy,&bytes_read_available) ;
  /** atomic data to read computing */

  return to_copy - not_copy ;

}
static ssize_t
file_ops_write(struct file *instance,const char __user *write_buffer,
                                         size_t count,loff_t *offset ) {

  size_t to_copy, not_copy ;

  printk(KERN_DEBUG "device write function triggering !!!\n") ;

  if ((instance->f_flags & O_NONBLOCK) && ! WR_OK) 
  {
    /** case O_NONBLOCK modus and data to write not available. */
    printk(KERN_DEBUG "device write non-block and not ready !!!\n") ;
    return -EAGAIN ;
  }

  if (wait_event_interruptible(wait_write,WR_OK)) 
  {
    /** Wait until data available throught WR_OK macro check.  */
    printk(KERN_DEBUG "device write not wake up error !!!\n") ;
    return -ERESTART ;
  }

  memset(res_str,'\0',128) ;

  to_copy=min((size_t) atomic_read(&bytes_write_available),count) ;
  /** atomic read operation and return adress from end of read data. */

  not_copy=copy_from_user(res_str,write_buffer,to_copy) ;

  atomic_sub(to_copy-not_copy,&bytes_write_available) ;
  /** atomic data to read computing */


  return to_copy - not_copy ;

}



struct file_operations fops = 
{
  .read         = file_ops_read  ,
  .write        = file_ops_write ,
  .open         = file_ops_open  ,
  .release      = file_ops_release  ,

} ;


static int __init my_device_init(void) 
{
  static int minor=0 ;
  static int minor_numbers=1 ;

  printk(KERN_DEBUG "start device creating !!!\n") ;

  if (alloc_chrdev_region(&dev_num,minor,minor_numbers,DEVICE_NAME) < 0) 
  {
    /** allocate space for device dev_t dev_num with
        minor number minor and minor_numbers count
        as device name DEVICE_NAME */

    printk(KERN_DEBUG "chrdev alloc error !!! \n") ;
    return -EIO ;
  }

  device_object=cdev_alloc() ;
  /** device instantiation with space allocation as struct cdev. */

  if (device_object == NULL) 
  {
    goto free_device_number ;
  }

  device_object->ops=&fops ;
  device_object->owner=THIS_MODULE ;

  if (cdev_add(device_object,dev_num,1)) 
  {
    /** register the device object to the kernel with
        the device number dev_num and
        counts of minor numbers 1 */

    goto free_device ;
  }

  device_class=class_create(THIS_MODULE,DEVICE_NAME) ;
  /** create new device class */
  device_create(device_class,NULL,dev_num,NULL,"%s",DEVICE_NAME) ;
  /** register device in /dev/  filesystem */

  if (device_class == NULL) 
  {
    printk(KERN_DEBUG "device class creating error !!!\n") ;
    return -1 ;
  }

  printk(KERN_DEBUG "device successfull created !!!\n") ;
  return 0 ;

  /** error handling goto ; */
  free_device :
    printk(KERN_DEBUG "kobject triggering\n") ;
    kobject_put(&device_object->kobj) ;
  free_device_number :
    printk(KERN_DEBUG "chrdev registering error !!!\n") ;
    unregister_chrdev_region(dev_num,1) ;
    return -EIO ;
}

static void __exit my_device_exit(void) 
{
  device_destroy(device_class,dev_num) ;
  /** destroy device */
  class_destroy(device_class) ;
  /** destroy device class */

  cdev_del(device_object) ;
  /** free device_object memory space */
  unregister_chrdev_region(dev_num,1) ;
  /** unregister dev_t dev_num and minors count 1 */

  printk(KERN_DEBUG "driver destruction succesfull !!!\n") ;
  return ;

}

module_init(my_device_init) ;
module_exit(my_device_exit) ;

Et voici le code de l'application client de notre driver : calc_device_client.c

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

int main(void) 
{
  int fd ;
  if ((fd=open("/dev/devname",O_RDWR| O_NOCTTY ) ) >= 0) 
  {
    fprintf(stdout,"open devname succesfull !!!\n") ;
  }
  else 
  {
    fprintf(stderr,"1) %m\n") ;
    exit(EXIT_FAILURE) ;
  }


  int c=0 ;
  while (c < 5) 
  {
    char *rd_buf=malloc(128) ;
    if (read(fd,rd_buf,(size_t) 128) > 0) 
    {
      fprintf(stdout,"\ndevname send:\n%s\n",rd_buf) ;
    }
    else 
    {
      fprintf(stderr,"2) %m\n") ;
      exit(EXIT_FAILURE) ;
    }
    free(rd_buf) ;

    fprintf(stdout,"\nEnter answer to send to driver\n>>> ") ;
    char *write_buf=malloc(128) ;

    fgets(write_buf,128,stdin) ;
    write_buf[strlen(write_buf)-1]='\0' ;

    if (write(fd,write_buf,strlen(write_buf)) > 0) 
    {
      fprintf(stdout,"\nsend answer: [ \"%s\" ] to devname\n",write_buf) ;
    }
    else 
    {
      fprintf(stderr,"3) %m\n") ;
      exit(EXIT_FAILURE) ;
    }
    free(write_buf) ;
    char *response=malloc(128) ;
    if (read(fd,response,(size_t) 128) > 0) 
    {
      fprintf(stdout,"\ndevname answer:\n%s\n",response) ;
    }
    else 
    {
      fprintf(stderr,"2) %m\n") ;
      exit(EXIT_FAILURE) ;
    }
    free(response) ;
    c++ ;
    sleep(1) ;
  
  }
  close(fd) ;

  exit(EXIT_SUCCESS) ;

}

Voila vous n'avez qu'à exécuter le fichier Makefile, à insérer le module dans le noyau avec la commande insmod et à compiler et lancer le client…

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2016 Bruggemann Eddie. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.