Programmation modules sous Linux  (partie 2)

Suivant
Pilote PCI
II . Pilote en mode caractère
1 . Ajout d'un pilote au noyau
2 . Table des appels systèmes
3 . Implémentation des appels systèmes
4 . Structure des fichiers
5 . Méthodes open et release
6 . Utilisation de la mémoire
7 . Méthodes read et write
8 . Système de fichiers devfs
9 . Utilisation des ressources d'E/S
10 . Gestion d'interruption
11 . Méthode ioctl
12 . Transfert Noyau/Utilisateur
13 . Gestion de processus

1 . Ajout d'un pilote au noyau

L'ajout d'un nouveau pilote au système a pour conséquence de lui affecter un nombre majeur. Ce nombre majeur identifie donc le pilote dans le noyau. On appelle cette phase : l'enregistrement du pilote dans le noyau.

L'enregistrement doit être exécuté lors de l'initialisation du pilote (au chargement du module, dans init_module()) en appelant la fonction : register_chrdev().

De la même manière, on supprimera le pilote du noyau (au déchargement du module, dans cleanup_module()) en appelant la fonction : unregister_chrdev().

Le prototype de la fonction register_chrdev() défini dans <linux/fs.h> est le suivant :
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);

Cette fonction renvoie 0 ou une valeur positive si l'opération réussie et une valeur négative en cas d'échec.

L'argument major correspond au nombre majeur demandé, qui sera donc l'identifiant du pilote pour le noyau. Une affectation dynamique du nombre majeur est possible si on précise la valeur 0 à major.

L'argument name correspond au nom du périphérique qui apparaîtra dans /proc/devices

L'argument fops est un pointeur vers une structure qui contient des pointeurs de fonction. Ils définissent les points d'entrée du pilote lors d'appels systèmes (open, read, ...).


2 . Table des appels systèmes

Le numéro majeur est le nombre utilisé par le système pour indexer un tableau contenant toutes les tables des points d'entrée de tous les pilotes enregistrés dans le noyau. Le numéro majeur désigne donc un driver unique ce qui permet de l'identifier.

Principe: chaque fois qu'une opération (appel système) sur un fichier de périphérique caractère associé à ce nombre majeur (un des fichiers spéciaux localisés dans /dev), le noyau trouve et appelle la fonction appropriée dans la structure file_operations.

Pour les noyaux 2.4, la déclaration globale de la table est la suivante :

static struct file_operations sample_fops =
{
  owner: THIS_MODULE,
  read: sample_read,
  write: sample_write,
  open: sample_open,
  release: sample_release // correspond à close
};


Les routines standards d’accès au système de fichiers sont ainsi surchargées par les routines du module (notre pilote de périphérique) sample_xxx.
Les points d'entrée possibles sont assez nombreux, mais on utilisera principalement open, read, write et close. Certaines méthodes non implémentées sont remplacées par des méthodes par défaut. Les autres méthodes non implémentées retournent -EINVAL.


3 . Implémentation des appels systèmes

static ssize_t sample_read(struct file *file, char *buf, size_t count, loff_t *ppos)
{
  printk(KERN_DEBUG "read()\n");
  return count;
}

static ssize_t sample_write(struct file *file, const char *buf, size_t count, loff_t *ppos)
{
  printk(KERN_DEBUG "write()\n");
  return count;
}

static int sample_open(struct inode *inode, struct file *file)
{
  printk(KERN_DEBUG "open()\n");
  return 0;
}

static int sample_release(struct inode *inode, struct file *file)
{
  printk(KERN_DEBUG "close()\n");
  return 0;
}

La valeur de renvoie de chaque opération est 0 (ou une valeur positive) en cas de succès ou un code d'erreur négatif pour signaler le type d'erreur.


4 . Structure des fichiers

La structure file, défini dans <linux/fs.h>, représente un fichier ouvert et donc créée par le noyau sur l'appel système open. Elle est transmise à toute fonction qui agit sur le fichier jusqu'au dernier close.

Les champs les plus importants de struct file sont :

mode_t f_mode : indique le mode d'ouverture du fichier (read ou write)
loff_t f_pos : position actuelle de lecture ou d'écriture
unsigned int f_flags : drapeaux de fichiers (notamment utilisé pour les opérations non-bloquantes O_NONBLOCK)
struct file_operations *f_op : opérations associées au fichier
void *private_data : le pilote est libre de créer sa propre utilisation du champ ou de l'ignorer

Un fichier disque sera lui représenté par la structure inode. On utilisera généralement qu'un seul champ de cette structure :

kdev_t i_rdev : le numéro de périphérique actif

Ces macros permettront d'extraire les nombres majeurs et mineurs d'un numéro de périphérique :

int minor = MINOR(inode->i_rdev);
int major = MAJOR(inode->i_rdev);


5 . Méthodes open et release

Dans la plupart des pilotes, open réalise les tâches suivantes :

Incrémentation du compteur d'utilisation
Contrôle d'erreurs au niveau matériel (genre « device not ready »)
Initialisation du périphérique (cas de la première ouverture)
Identification du nombre mineur
Allocation et remplissage de la strucure privée qui sera placée dans file->private_data

Le rôle de la méthode release est l'inverse de celui d'open et réalisera généralement les tâches suivantes :

Libérer tout ce que open a alloué dans file->private_data
« éteindre » le périphérique lors de la dernière fermeture
Décrémenter le compteur d'utilisation

On peut maintenant écrire son premier pilote :

#define __KERNEL__
#define MODULE

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>

static int major = 254;

MODULE_DESCRIPTION("sample module char");
MODULE_AUTHOR("tv 2002");
MODULE_SUPPORTED_DEVICE("nothing");
MODULE_LICENSE("GPL");
MODULE_PARM_DESC(major, "major number");
MODULE_PARM(major, "i");

static ssize_t sample_read(struct file *file, char *buf, size_t count, loff_t *ppos)
{
  printk(KERN_DEBUG "read()\n");
  return count;
}

static ssize_t sample_write(struct file *file, char *buf, size_t count, loff_t *ppos)
{
  printk(KERN_DEBUG "write()\n");
  return count;
}

static int sample_open(struct inode *inode, struct file *file)
{
  printk(KERN_DEBUG "open()\n");
  return 0;
}


static int sample_release(struct inode *inode, struct file *file)
{
  printk(KERN_DEBUG "close()\n");
  return 0;
}

static struct file_operations sample_fops =
{
  owner: THIS_MODULE,
  read: sample_read,
  write: sample_write,
  open: sample_open,
  release:sample_release
};

static int __init sample_init(void)
{
  int ret;

  ret = register_chrdev(major, "sample", &sample_fops);
  if(ret <0)
  {
    printk(KERN_WARNING "probleme major sur sample\n");
    return ret;
  }

  printk(KERN_INFO "sample sucess load !\n");
  return 0;
}

static void __exit sample_exit(void)
{
  int ret;

  ret = unregister_chrdev(major, "sample");
  if(ret < 0)
    printk(KERN_WARNING "probleme unregister sur sample\n");

  printk(KERN_INFO "sample success unload !\n");
}

module_init(sample_init);
module_exit(sample_exit);

Après compilation et chargement du pilote, il faut tester le lien entre le pilote en mode noyau et une application en mode utilisateur qui utilise les appels systèmes.

Tout d'abord, il faut avoir créer son fichier spécial : mknod /dev/sample c 254 0

Le premier test peut se réalise simplement par la commande : $ cat sample.c > /dev/sample

En contrôlant avec la commande dmesg, on voit les appels open(), write() et close() réalisés par la commande cat.

Un deuxième test plus complet est d'écrire soi-même l'application côté utilisateur :

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/io.h>

main()
{
  int sampleid;
  int valeur;
  int valeurlue;

  /*ouverture du port*/
  sampleid = open("/dev/sample", O_RDWR);
  printf("id du device = %d\n", sampleid);

  printf("TEST DU DRIVER sample\n\n");

  valeur = 0x81;
  write(sampleid, &valeur, 1);
  sleep(1);
  read(sampleid, &valeurlue, 1);
  printf("valeur lue : %x\n\n", (unsigned char)valeurlue);

  close(sampleid);
}


6 . Utilisation de la mémoire

Tous les programmes sont habitués à gérer l'allocation mémoire. Son utilisation en mode noyau est très peu différente :
On obtient une zone mémoire à l'aide de kmalloc. Et on la libère grâce à kfree

Ces fonctions se comportent comme malloc et free, à l'exeption d'un argument supplémentaire, la priorité. On utilisera généralement une priorité de type :

GFP_KERNEL : allocation normale de la mémoire du noyau
GFP_USER : alloue de la mémoire pour le compte de l'utilisateur (requête de faible priorité)
GFP_ATOMIC : alloue de la mémoire à partir d'un gestionnaire d'interruption

Exemple :

#include <linux/slab.h>

buffer = (char *)kmalloc(buf_size, GFP_KERNEL); // allocation
if(buffer != NULL) printk(KERN_DEBUG "allocation de %d octets\n", buf_size);
else
{
  printk(KERN_WARNING "allocation impossible de %d octets\n", buf_size);
   return -ENOMEM;
}
kfree(buffer); // libération


7 . Méthodes read et write

Les méthodes renverront le nombre de caractères lus pour read et le nombre de caractère écrits pour write.

Exemple simple :

// global
static int buf_size = 64;
static char *buffer;

static ssize_t sample_read(struct file *file, char *buf, size_t count, loff_t *ppos)
{
  int lus = 0;
  printk(KERN_DEBUG "read: demande lecture de %d octets\n", count);
  /* Check for overflow */
  if (count <= buf_size - (int)*ppos)
    lus = count;
  else  lus = buf_size - (int)*ppos;
  if(lus)
    copy_to_user(buf, (char*)buffer + (int)*ppos, lus);
  *ppos += lus;
  printk(KERN_DEBUG "read: %d octets reellement lus\n", lus);
  printk(KERN_DEBUG "read: position=%d\n", (int)*ppos);
  return lus;
}

static ssize_t sample_write(struct file *file, char *buf, size_t count, loff_t *ppos)
{
  int ecrits = 0;
  int i = 0;
  printk(KERN_DEBUG "write: demande ecriture de %d octets\n", count);
  /* Check for overflow */
  if (count <= buf_size - (int)*ppos)
    ecrits = count;
  else  ecrits = buf_size - (int)*ppos;
  if(ecrits)
    copy_from_user((char*)buffer + (int)*ppos, buf, ecrits);
  *ppos += ecrits;
  printk(KERN_DEBUG "write: %d octets reellement ecrits\n", ecrits);
  printk(KERN_DEBUG "write: position=%d\n", (int)*ppos);
  printk(KERN_DEBUG "write: contenu du buffer\n");
  for(i=0;i<buf_size;i++)
    printk(KERN_DEBUG " %d", *(buffer+i));
  printk(KERN_DEBUG "\n");
  return ecrits;
}

L'allocation du buffer sera réalisé au chargement du module de la manière suivante : buffer = (char *)kmalloc(buf_size, GFP_KERNEL);

Pour bien comprendre l'utilisation du buffer et de son curseur ppos, il faudra faire plusieurs tests dans l'application côté utilisateur et notamment en utilisant l'appel système lseek() qui permet de positionner le curseur utilisé dans les apples read et write. On peut remarquer que notre pilote n'a pas implémenté de fonction pour l'appel lseek(), cela veut dire qu'on utilise la fonction par défaut du noyau.

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/io.h>
#include <unistd.h>

#define SIZE 64

main()
{
  int sampleid;
  char datas[SIZE];
  char dataslues[SIZE];
  int i, choix, position;
  int ecrits, lus;

  /*ouverture du port*/
  sampleid = open("/dev/sample", O_RDWR);
  printf("id du device = %d\n", sampleid);

  printf("TEST DU DRIVER sample\n\n");

  for(i=0;i<SIZE;i++)
    datas[i] = i;

  ecrits = write(sampleid, datas, SIZE);
  printf("Donnees ecrites : %d octets\n\n", ecrits);
  sleep(1);

  printf("Donner la position dans le fichier [0-64]: ");
  scanf("%d", &position);
  if(position > SIZE) position = SIZE;
  // DESCRIPTION de lseek :
  // La fonction lseek place la tête de lecture/écriture à la position offset
  // dans le fichier associé au descripteur fildes en suivant la directive
  // whence ainsi :
  // * SEEK_SET: La tête est placée à offset octets depuis le début du fichier.
  // * SEEK_CUR: La tête de lecture/écriture est avancée de offset octets.
  // * SEEK_END: La tête est placée à la fin du fichier plus offset octets.
  lseek(sampleid, position, SEEK_SET);

  printf("Donner le nombre d'octets à lire [0-64]: ");
  scanf("%d", &choix);
  if(choix > SIZE) choix = SIZE;
  lus = read(sampleid, dataslues, choix);
  printf("\n\nDonnees lues : %d octets\n\n", lus);
  for(i=0;i<lus;i++)
    printf("%d ", dataslues[i]);
  printf("\n\nFin du test.\n");

  close(sampleid);
}


8 . Systèmes de fichiers devfs

Depuis la version 2.4, le noyau Linux offre un système de fichier spécial pour les points d'entrée des périphériques : devfs.

Les principaux avantages de devfs sont :

Les points d'entrée dans /dev sont créés à l'initalisation du périphérique et sont supprimés lors de sont retrait ;
Le pilote peut spécifier des noms de périphérique, leur propriétaire et des bits de permissions ;
Il n'est pas nécessaire d'allouer un nombre majeur au pilote de périphérique ni de se préoccuper des nombres mineurs

On utilisera les fonctions suivantes définies dans <linux/devfs_fs_kernel.h> :

devfs_handle_t devfs_mk_dir(devfs_handle_t dir, const char *name, void *info);
devfs_handle_t devfs_register(devfs_handle_t dir, const char *name, unsigned int flags, unsigned int major, unsigned int minor, umode_t mode, void *ops, void *info);
devfs_handle_t devfs_unregister(devfs_handle_t de);

L'inconvénient principal de devfs est que certains systèmes Linux ne l'emploient pas. On pourra alors intégrer dans le source une compilation conditionnelle basée sur CONFIG_DEV_FS.

Exemple :

static int __init sample_init(void)
{
  devfs_dir = devfs_mk_dir(NULL, "sample2", NULL);
  if(!devfs_dir) return -EBUSY; //probleme
  devfs_handle = devfs_register(devfs_dir, "sample2", DEVFS_FL_AUTO_DEVNUM, 0, 0, S_IFCHR | S_IRUGO | S_IWUSR, &sample_fops, NULL);
  buffer = (char *)kmalloc(buf_size, GFP_KERNEL);
  if(buffer != NULL) printk(KERN_DEBUG "allocation de %d octets\n", buf_size);
  else
  {
    printk(KERN_WARNING "allocation impossible de %d octets\n", buf_size);
    devfs_unregister(devfs_handle);
    devfs_unregister(devfs_dir);
    return -ENOMEM;
  }
  printk(KERN_INFO "sample sucess load !\n");
  return 0;
}

static void __exit sample_exit(void)
{
  devfs_unregister(devfs_handle);
  devfs_unregister(devfs_dir);
  kfree(buffer);
  printk(KERN_INFO "sample success unload !\n");
}

module_init(sample_init);
module_exit(sample_exit);


9 . Utilisation des ressources d'E/S

Un pilote ne peut accomplir sa tâche sans employer des ressources systèmes comme la mémoire, les ports d'E/S, la mémoire d'E/S, les lignes d'interruptions, les canaux DMA, etc ...

Un pilote peut simplement accèder à ces ressources sans en informer le système d'exploitation, mais ce n'est pas la méthode conseillée. Par exemple, un pilote doit être en mesure d'allouer les ports exacts dont il a besoin en vérifiant au préalable s'il ne sont pas déjà utilisés.

Les informations concernant les ressources déclarées sont disponibles dans les fichiers /proc/ioports, /proc/iomem, /proc/interrupts, /proc/pci, ...

L'interface de programmation permettant d'accèder aux ports d'E/S comporte trois fonctions définies dans <linux/ioport.h> :

int check_region(unsigned long start, unsigned long len);
struct ressource *request_region(unsigned long start, unsigned long len, char *name);
void release_region(unsigned long start, unsigned long len);

L'appel à check_region() permet de vérifier si une plage de ports est disponible. En cas d'erreur, l'appel renvoie une valeur négative (tel que -EBUSY ou -EINVAL). Il est normalement plus nécessaire de réaliser cet appel depuis les versions 2.4.

Les informations concernant les ports d'E/S déclarées sont disponibles dans le fichier /proc/ioports.

La notion de port d'E/S est spécifique à l'architecture Intel.

On accède à ces ports depuis le noyau grâce aux macros :
in{b,w,l}()/out{b,w,l}() : lit/écrit 1, 2 ou 4 octets consécutifs sur un port d’E/S,
in{b,w,l}_p()/out{b,w,l}_p() : lit/écrit 1, 2 ou 4 octets consécutifs sur un port d’E/S et fait une pause (une instruction),
ins{b,w,l}()/outs{b,w,l}() : lit/écrit des séquences de 1, 2 ou 4 octets consécutifs sur un port d’E/S.

Ces macros impliquent l'utilisation de l'option de compilation -O (ou -O2) de gcc.

Prototypes x86 (<asm/io.h>) :

unsigned char inb(unsigned short port);
void outb(unsigned char byte, unsigned short port);

Pour réserver et libérer l'accès à la mémoire d'E/S, le pilote devra utiliser les fonctions :

int check_mem_region(unsigned long start, unsigned long len);
int request_mem_region(unsigned long start, unsigned long len, char *name);
int release_mem_region(unsigned long start, unsigned long len);

La mémoire d'E/S correspond à des zones mémoires qui résident sur le périphérique.

Les informations concernant la mémoire d'E/S déclarée sont disponibles dans le fichier /proc/iomem.

Cette mémoire est accessible comme de la mémoire centrale (contrairement aux ports d’E/S).
Cependant, le noyau manipule des adresses linéaires virtuelles.

On doit donc dans certains cas (par exemple pour les cartes PCI) mapper les plages d’entrées/sorties dans l’espace linéaire du noyau :

void *ioremap(unsigned long offset, unsigned long size); : mappe une plage d’adresses physiques sur une plage d’adresses linéaires,
void iounmap(void *addr) : libère une plage préalablement mappée .

Prototypes x86 (<asm/io.h>) :

// lecture/écriture :
char readb (void *addr) ; void writeb (char byte, void *addr) ;
void * memcpy_{from,to}io (void *dest, const void *src, size_t count) ;
void * memset_io (void *addr, int pattern, size_t count); // remplit une zone avec une valeur fixe
// traduit les adresses virtuelles et les adresses physiques :
unsigned long virt_to_bus (volatile void *addr) ;
void * phys_to_virt (unsigned long addr) ;

On va maintenant présenter le premier pilote matériel. Pour cela on va utiliser le port parallèle, une interface disponible et simple à mettre en oeuvre.
On branchera une petite maquette composée de Leds sur les sorties (D0 à D7) et des boutons poussoirs sur les 5 entrées disponibles (BUSY, ACK, etc ...).

#define __KERNEL__
#define MODULE

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
#include <linux/ioport.h>
#include <asm/io.h>

#include "sample_lp.h"

static int major = 254;
static int buf_size = 64;

MODULE_DESCRIPTION("sample module lp");
MODULE_AUTHOR("tv 2002");
MODULE_SUPPORTED_DEVICE("nothing");
MODULE_LICENSE("GPL");
MODULE_PARM_DESC(major, "major number");
MODULE_PARM(major, "i");

static char *buffer_read;
static char *buffer_write;

static ssize_t sample_lp_read(struct file *file, char *buf, size_t count, loff_t *ppos)
{
  unsigned char state_output;
  unsigned char state_input;

  //lecture de l'etat des sorties
  state_output = inb(LPT_BASE);
  //lecture de l'etat des entrees
  lire(&state_input);

  *buffer_read = state_input;
  *(buffer_read+1) = state_output;

  //ecriture vers l'espace utilisateur
  copy_to_user(buf, buffer_read, 2);

  #ifdef SAMPLE_LP_DEBUG
  printk(KERN_DEBUG "sample_lp (read): etat E/S renvoye => E: 0x%x S: 0x%x\n", *buffer_read, *(buffer_read+1));
  #endif

  return 2;
}

static ssize_t sample_lp_write(struct file *file, char *buf, size_t count, loff_t *ppos)
{
  int i;
  unsigned char state;
  unsigned char cmd;

  if(count == 1)
  {
    copy_from_user(buffer_write, buf, count);

    #ifdef SAMPLE_LP_DEBUG
    for(i=0;i<count;i++)
      printk(KERN_DEBUG "sample_lp (write): 0x%x\n", *(buffer_write+i));
    #endif

    #ifdef SAMPLE_LP_DEBUG
    //lecture de l'etat des sorties
    state = inb(LPT_BASE);
    printk(KERN_DEBUG "sample_lp (write): etat des sorties avant = 0x%x\n", state);
    #endif
    cmd = *buffer_write;
    ecrire(cmd, REG_DATA); //fixe les sorties
    count = state;
    #ifdef SAMPLE_LP_DEBUG
    //lecture de l'etat des sorties
    state = inb(LPT_BASE);
    printk(KERN_DEBUG "sample_lp (write): etat des sorties apres = 0x%x\n", state);
    #endif
  }

  printk(KERN_DEBUG "sample_lp (write): ecriture de %d octets\n", count);

  return count;
}

static int sample_lp_open(struct inode *inode, struct file *file)
{
  int ret = 0;

printk(KERN_DEBUG "sample_lp: open()\n");
  return ret;
}


static int sample_lp_release(struct inode *inode, struct file *file)
{
  printk(KERN_DEBUG "sample_lp: close()\n");
  return 0;
}

static struct file_operations sample_lp_fops =
{
  owner: THIS_MODULE,
  read: sample_lp_read,
  write: sample_lp_write,
  open: sample_lp_open,
  release:sample_lp_release
};

static int __init sample_lp_init(void)
{
  int ret;

  ret = register_chrdev(major, "sample_lp", &sample_lp_fops);
  if(ret <0)
  {
    printk(KERN_WARNING "sample_lp: probleme numero major\n");
    return ret;
  }

  buffer_read = (char *)kmalloc(buf_size, GFP_KERNEL);
  if(buffer_read != NULL)
    printk(KERN_DEBUG "sample_lp: allocation de %d octets pour read\n", buf_size);
  else
  {
    printk(KERN_WARNING "sample_lp: allocation impossible de %d octets pour read\n", buf_size);
    unregister_chrdev(major, "sample_lp");
    return -ENOMEM;
  }

  buffer_write = (char *)kmalloc(buf_size, GFP_KERNEL);
  if(buffer_write != NULL)
    printk(KERN_DEBUG "sample_lp: allocation de %d octets pour write\n", buf_size);
  else
  {
    printk(KERN_WARNING "sample_lp: allocation impossible de %d octets pour write\n", buf_size);
    kfree(buffer_read);
    unregister_chrdev(major, "sample_lp");
    return -ENOMEM;
  }

  if(check_region(LPT_BASE, SIZE) < 0)
  {
    printk("sample_lp: I/O port conflict sur LPT !!!\n");
    return -EIO;
  }

  request_region(LPT_BASE, SIZE, "sample_lp");

printk(KERN_INFO "sample_lp: success load !\n");
  return 0;
}

static void __exit sample_lp_exit(void)
{
  int ret;

  ret = unregister_chrdev(major, "sample_lp");
  if(ret < 0)
    printk(KERN_WARNING "sample_lp: probleme unregister\n");

  release_region(LPT_BASE, SIZE);

  kfree(buffer_read);
  kfree(buffer_write);

  printk(KERN_INFO "sample_lp: success unload !\n");
}

static int ecrire(unsigned char valeur, int reg)
{
  int ret;
  /* reg permet de choisir le registre de sortie du port
  * reg = 0 ---> 0x378 (REG_DATA) ou
  * reg = 2 ---> 0x37A (REG_CMDE) */

  #ifdef SAMPLE_LP_DEBUG
  printk(KERN_DEBUG "sample_lp (ecrire): data = 0x%x sur 0x%x\n", valeur, LPT_BASE+reg);
  #endif

  /* verification du registre de sortie
* puis, ecriture de la valeur */
  if(reg == REG_DATA || reg == REG_CMDE)
  {
    outb(valeur, LPT_BASE+reg);
    ret = OK;
  }
  else
  {
    ret = ERREUR;
  }

  return(ret); //OK ou ERREUR
}

static int lire(unsigned char *valeur)
{
  /* lecture du port d'entree */
  *valeur = inb(LPT_BASE+REG_STATE);

  #ifdef SAMPLE_LP_DEBUG
  printk(KERN_DEBUG "sample_lp (lire): 0x%x\n", *valeur);
  #endif

  return(OK); /*toujours OK*/
}

//module
module_init(sample_lp_init);
module_exit(sample_lp_exit);

Le fichier sample_lp.h :

#ifndef SAMPLE_LP_H
#define SAMPLE_LP_H

#define LPT_BASE 0x378
#define SIZE 3

#define REG_DATA 0 /* LPT_BASE + 0 = 0x378 */
#define REG_STATE 1 /* LPT_BASE + 1 = 0x379 */
#define REG_CMDE 2 /* LPT_BASE + 2 = 0x37A */

#define VRAI 1
#define FAUX 0
#define ERREUR -1
#define OK 1

//pour le débogage
#define SAMPLE_LP_DEBUG

//prototypes
static int ecrire(unsigned char valeur, int reg);
static int lire(unsigned char *valeur);
#endif

Remarques : le pilote implémente les deux appels systèmes read et write mais en modifiant leur comportement initial (lecture de n octets sur le prériphérique et écriture de n octets sur le périphérique). Ce n'est pas une bonne chose ! mais ce n'était pas indispensable ici (lecture des états de boutons et éciture sur des Leds). En fait, on voit qu'il manque une fonction qui permettrait de contrôler correctement et spécifiquement le matériel pris en charge par le pilote. C'est la fonction ioctl qui permet de faire ça (voir méthode ioctl). On remarque aussi qu'il est donc possible de modifier le comportement de read et de write pour l'adapter à un matériel particulier, même si cette pratique est déconseillée.

Et maintenant, réalisons notre première application qui exploitera notre pilote :

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/io.h>
#include <signal.h>

int sample_lp;

int termine(int signal)
{
  close(sample_lp);
  exit(0);
}

int main()
{
  char valeur[5];
  unsigned char cmd;

  signal(SIGINT, termine);

  /*ouverture du port*/
  sample_lp = open("/dev/sample_lp", O_RDWR);
  if(sample_lp <= 0 )
  {
    printf("Erreur d'ouverture du device !\n");
    exit(-1);
  }

  printf("TEST DU sample_lp\n\n");

  read(sample_lp, &valeur[0], 2);
  printf("Etat actuel des E/S : E: 0x%x S: 0x%x\n\n", valeur[0], valeur[1]);

  printf("Donner une valeur en hexa pour ecrire sur les sorties : ");
  scanf("%x", &cmd);

  write(sample_lp, &cmd, 1);
  read(sample_lp, &valeur[0], 2);
  printf("\n\nEtat des E/S : E: 0x%x S: 0x%x\n\n", valeur[0], valeur[1]);

  close(sample_lp);

  printf("Fin du test ..\n\n");
  exit(0);
}


10 . Gestion d'interruption

Les événements d’E/S souvent asynchrones et imprédictibles .
Deux méthodes pour superviser les E/S :
   par attente active ou scrutation (polling), ou
   par interruptions.

Dans le mode polling, on relâche le CPU avec la fonction schedule() après chaque test infructueux :

while(1)
{
  if(LireEtat(carte) & ETAT_FIN) break;
  schedule();
}

Une interruption est un signal envoyé par le matériel et capable d'interrompre le processeur.
Le pilote enregistrera un gestionnaire d'interruption (interrupt handler) destiné aux interruptions de son périphérique.

Les propriétés générales d'un gestionnaire d'interruption sont :

   doit être rapide et
   ne doit pas appeler des routines qui peuvent dormir (ex : kmalloc() non atomique).

Pour ces raisons, la gestion des interruptions est souvent découpée en deux parties :

   une partie rapide et ininterruptible (c'est le gestionnaire d’interruptions), et
   Une partie lente placée dans une file de tâches (bottom-half).

Remarque : La première peut exister sans la seconde mais pas l’inverse

Un gestionnaire d’interruptions est déclaré grâce à la fonction request_irq().
Il est libéré grâce à la fonction free_irq(). Les prototypes (définis dans <linux/sched.h>) sont les suivants :

int request_irq( unsigned int irq, void (*handler)(int, void *, struct pt_regs *),
                 unsigned long flags /*SA_INTERRUPT ou SA_SHIRQ*/,
                 const char *device, void *dev_id) ;
void free_irq (unsigned int irq, void *dev_id) ;


Quand l’interruption irq survient, la fonction handler() est appelée.

Le champ dev_id sert à identifier les périphériques en cas de partage d’IRQ (SA_SHIRQ) ;
La liste des IRQ déjà déclarées est disponible dans /proc/interrupts.

On peut déclarer un gestionnaire d’interruptions à 2 endroits au choix :

   au chargement du pilote (module_init()) et au déchargement du module (module_exit()), ou
   à la première ouverture du pilote par un programme utilisateur (open()), il faut alors :
          installer le gestionnaire au premier open() (utilisation d’un compteur d’utilisation), et
          désinstaller le gestionnaire au dernier close().

Le pilote peut activer ou désactiver le suivi des interruptions pour sa propre ligne d'IRQ. Pour cela, il dispose de deux fonctions, définies dans <linux/irq.h> :

void disable_irq(int irq);
void enable_irq(int irq);


Il peut s'avérer utile d'activer ou désactiver l'ensemble des interruptions en utilisant les fonctions cli() et sti(). Il est cependant déconseillé de les utiliser dans les versions récentes de Linux. Il est préférable d'utiliser les appels suivants (attention tout de même aux systèmes multi-processeur) :

unsigned long flags;
save_flags(flags);
cli();
/* maintenant, ça s'exécute avec les interruptions désactivées ... */
restore_flags(flags); // les interruptions sont restaurées

Les fonctions BH (Bottom Halves) du noyau sont utilisées pour la gestion de tâches asynchrones.
Elles sont ordonnancées à chaque retour d’appel système, d’exception ou de gestionnaire d’interruption.

Parmi celles-ci, trois sont particulièrement importantes pour les pilotes de périphériques (<linux/interrupt.h>) :

IMMEDIATE_BH : consomme une queue de tâches (tq_immediate) qui est alimentée par les drivers,
TQUEUE_BH : appelée à chaque tick d’horloge si la queue de tâches tq_timer n’est pas vide,
NET_BH : permet de signaler un événement aux couches réseau supérieures.

Les parties lentes des gestionnaires d’interruptions (tâches) sont mises en queue de tq_immediate (ou autre) par le gestionnaire d’interruption :
   déclaration d’une tâche (structure tq_struct),
   mise en queue de la tâche (fonction queue_task()).
   active la bottom-half avec la fonction mark_bh() ;

Exemple :

// global
static struct tq_struct sample_lp_bh_task;

// Dans open() :
/* Initialise la tache */
INIT_TQUEUE(&sample_lp_bh_task, NULL, NULL);

// Dans le gestionnaire d'interruption :
/* Prepare la tache */
PREPARE_TQUEUE(&sample_lp_bh_task, sample_lp_bh_handler, dev_id);
/* Ajoute la tache la queue BH et active son ordonnancement */
queue_task(&sample_lp_bh_task, &tq_immediate);
mark_bh(IMMEDIATE_BH);


11 . Méthode ioctl

Dans le cadre d'un pilote, la méthode ioctl est généralement utilisée pour contrôler le périphérique.

On peut implémenter toutes les fonctionnalités d’un pilote (driver) avec des commandes passées à ioctl().
Cette fonction permet de passer des commandes particulières au périphérique. Les commandes sont codées par un entier et peuvent avoir un argument. Cet argument peut être un entier ou un pointeur sur une structure de données.

Prototype (<linux/fs.h>) : int ioctl (struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg) ;
L'appel de fonction ioctl() de l'espace utilisateur correspond au prototype suivant : int ioctl(int fd, int cmd, char *argp);

La nature réelle d'argp dépend de la commande cmd de contrôle spécifique émise.

Une fonction type ressemble à :

int (*ioctl) (struct inode * inode, struct file * file , unsigned int cmd , unsigned long arg)
{
 int retval;
 swith(cmd)
 {
   case ... : ... break;
   case ... : ... break;
   default : retval = -EINVAL; break;
 }
 return retval;
}

Si la commande n'existe pas pour le pilote, l'appel système retournera le code erreur EINVAL.

Le programmeur doit choisir les numéros qui correspondent aux commandes.
Le simple choix de numéros commençant par 1 et allant croissant fonctionne, mais il est très déconseillé.
Les numéros de commandes doivent être uniques dans l'ensemble du noyau afin d'éviter les erreurs dues à l'envoie d'une commande au « mauvais » périphérique.
Depuis la version 2.4, les numéros utilisent quatre champs de bits : TYPE, NOMBRE, SENS et TAILLE.

Le fichier header <asm/ioctl.h>, inclus dans <linux/ioctl.h>, définit des macros qui facilitent la configuration des numéros de commandes :

_IO(type, nr) où type sera le nombre magique (voir le fichier ioctl-number.txt)
_IOR(type, nr, dataitem) // sens de transfert : Lecture
_IOW(type, nr, dataitem) // sens de transfert : Ecriture
_IOWR(type, nr, dataitem) // sens de transfert : Lecture/Ecriture


Il existe aussi des macros pour décoder les numéros : _IOC_DIR(nr), _IOC_TYPE(nr), _IOC_NR(nr) et _IOC_SIZE(nr).

Une définition des numéros de commandes ressemblera à :

#define SAMPLE_IOC_MAGIC 't'
#define SAMPLE_IOCSCMDE _IOW(SAMPLE_IOC_MAGIC, 1, mode)
#define SAMPLE_IOCGCMDE _IOR(SAMPLE_IOC_MAGIC, 2, mode)


Et l'implémentation de ioctl() dans le pilote peut alors ressembler à :

switch(cmd){
case SAMPLE_IOCSCMDE:
  if(!capable(CAP_SYS_ADMIN)) return -EPERM;
  ret = get_user(mode, (unsigned char *)arg); // ou __get_user()
  // ...
  break;
case SAMPLE_IOCGCMDE:
  // mode = ...;
  ret = put_user(mode, (unsigned char *)arg); // ou __put_user()
  break;
... }


12 . Transfert Noyau/Utilisateur

On a déjà évoqué le besoin de transférer les données de l’espace mémoire noyau vers l’espace mémoire du processus appelant et inversement.

Les principales fonctions, définies dans <asm/uaccess.h>, sont :

{get,put}_user(expression, addr) : transfert d’une variable unique depuis/vers l’espace mémoire utilisateur (utilisation en lvalue ), et
copy_{from,to}_user(unsigned long dest, unsigned long src, unsigned long len) : transfert d’un buffer depuis/vers l’espace mémoire utilisateur.

Ces fonctions font toutes appel à la fonction access_ok() qui vérifie la validité des buffers utilisateurs.
On peut s’affranchir de cet appel en utilisant les mêmes fonctions préfixées par __ (méthode déconseillée).


13 . Gestion de processus

Un pilote de périphérique peut être amené à faire attendre un processus tant qu'une opération n'est pas exécutée.

Exemple de situation simplifiée : gestion du clavier

un module peut enregistrer une fonction de gestion d'interruption qui sera activée par appui d'une touche.
Lorsqu'un processus attend une entrée clavier, il effectue l'appel système read qui appelle la fonction de lecture du module.
Celle-ci vérifie qu'il n'y a pas de caractère en attente, et s'endort en attendant que l'utilisateur tape une touche.
Dès qu'une touche est pressée, la routine d'interruption est activée : elle lit le caractère et réveille le processus.
Le processus termine l'appel système en récupérant son caractère.
Le noyau fournit quelques fonctions pour mettre en sommeil et reveiller un processus :
Endort le processus courant et le place dans une file d'attente : sleep_on()
Réveille un processus de la file d'attente : wake_up()

Les prototypes, définis dans <linux/sched.h>, sont les suivants :

endort le processus courant (état inéligible), de façon interruptible ou pas et
avec ou sans délais d’expiration, et le place dans une file d’attente :

void {interruptible_}sleep_on(wait_queue_head_t *wq);
long {interruptible_}sleep_on_timeout(wait_queue_head_t *wq, long timeout);

réveille les processus d’une file d’attente et les rend éligibles, de façon interruptible ou pas :

void wake_up{_interruptible}(wait_queue_head_t *wq) ;

endort le processus courant, de façon interruptible ou pas, en attente d’un événement :

void wait_event{_interruptible}(wait_queue_head_t *wq, condition) ;

Lors du réveil après une attente interruptible, il faut s’assurer que le processus n’a pas été réveillé par un signal (dans quel cas l’appel système doit retourner -ERESTARTSYS). La fonction signal_pending() permet de s’en assurer :

int signal_pending (struct task_struct *task) ;

Cette gestion des processus implique donc l'utilisation d'une file d'attente de type wait_queue_head_t.

L'initialisation de cette file d’attente peut se faire de 2 façons :

à la déclaration avec DECLARE_WAIT_QUEUE_HEAD(), ou
durant l’exécution (runtime) avec init_waitqueue_head().

Les prototypes, définis dans <linux/wait.h>, sont les suivants :

DECLARE_WAIT_QUEUE_HEAD (name) ;
void init_waitqueue_head (wait_queue_head_t *wq) ;

Exemple :

static DECLARE_WAIT_QUEUE_HEAD(ma_file);

void *lecture_hard(void *adresse)
{
  ...
  interruptible_sleep_on(& ma_file) ;
  if(signal_pending(current)) return -ERESTARTSYS ;
  ...
}

void mon_handler(int irq, void *priv, struct pt_regs *regs)
{
  ...
  wake_up_interruptible(& ma_file) ;
  ...
}


Précédent
Programmation modules
Sommaire
 
Suivant
Pilote PCI
© 2002 for www.tvtsii.net by tv