LES DRIVERS SOUS LINUX SUR INTEL  

les drivers sous Linux sur intel 


 

 

Avertissement :



Sommaire :
   
 
 


Drivers caractères

 
 

     

    Un driver est lié à la table des appels système par une table d'opérations : une structure contenant des pointeurs vers les points d'entrée du driver. Les points d'entrée possibles sont assez nombreux, mais les principaux utilisés sont : open, close, ioctl, read, write

     

    Pour que Linux ait accès à cette table et donc au driver, il convient de l'enregistrer dans le système. D'une manière générale, tout ce qui concerne l'initialisation du driver est réalisé par une fonction d?initialisation exécutée lors du démarrage du système. L'appel à cette fonction doit être incorporé par le programmateur du driver dans le fichier /usr/src/linux/drivers/char/mem.c.

     

    L'enregistrement du driver dans le système est réalisé par la fonction :

    int register_chrdev(unsigned int major, const char *name, struct file_operation *entry) ;

     

    Le paramètre entry étant la table des points d'entrée du driver, name le nom du driver et major, le major number du driver. Le major number est le nombre utilisé par le système pour indexer un tableau contenant toutes les tables de points d'entrée de tous les drivers enregistrés dans le système. Un major désigne donc un driver unique, c'est ce qui permet à Linux de l'identifier. Lors de l'enregistrement du driver, le major passe en argument est généralement 0, ce qui laisse au système le soin de fournir un major disponible. Le major est un nombre codé sur 8 bits, il ne peut donc pas dépasser 255, i.e. on ne peut pas enregistrer simultanément plus de 256 drivers dans Linux.

     

     2 Devices, Major, Minor.

     

    Pour avoir accès à un driver enregistré dans le système, il faut tout d'abord l'ouvrir, c'est ce que fait la fonction open. Cette fonction prend en paramètre un pointeur vers une chaîne de caractères représentant un device, de la forme /dev/my_driver.

     

    Pour créer le device, on utilise la fonction :

    int mknod(const char *name, mode_t mode, dev_t dev) ;

     

    Cette fonction crée un device appelé name, avec pour device number dev. Le device number est codé sur 2 octets. L'octet de poids fort est le major number, l'octet de poids faible est le minor number.

     

    Une fois le device créé, le major number contenu dans le device number permet au système de retrouver la table des points d'entrée du driver. Le minor number est utilisé pour faire passer des informations à la fonction d?ouverture, il permet de mettre en oeuvre le mécanisme de clonage décrit plus loin.

     

    La fonction d'ouverture renvoie un descripteur de fichier contenant le device number permettant d'appeler les autres points d'entrée du driver. Par la suite, les appels aux fonctions du driver utilisent ce descripteur de fichier.

     

     

     

     3 Le clonage des drivers.

 

 

Quand plusieurs process désirent avoir accès au même device, ils ne peuvent pas tous ouvrir un driver identique cela poserait le problème du partage, donc d'un possible écrasement ou d'un mauvais aiguillage, des données internes au driver.

 

Pour remédier à ce problème, on utilise le mécanisme suivant, dit mécanisme de clonage :

 

Le major est celui d'un driver clone du driver ouvert(i.e. un major number et un nom différent, mais une même table de points d'entrée).

Le minor est le numéro du clone.

On attribue ainsi au clone un device number unique qui le différencie des autres clones et permet d'atteindre dans un tableau une structure décrivant l'état interne du driver pour ce clone. On peut donc ouvrir plusieurs fois le même driver sans avoir à en enregistrer plusieurs, et surtout accéder à des données concernant la carte depuis plusieurs drivers identiques, donc ayant accès aux mêmes ressources, en évitant des problèmes de partage des données puisque les données sensibles sont spécifiques à chaque clone du driver.

 

 



 

Drivers PCI
 

     

    Lors du démarrage, le BIOS du PC scrute le bus PCI afin de détecter la présence de carte sur le bus. Les informations sont récupérées par le noyau et stockées dans des structures de la forme suivante :

     

    struct pci_dev {

    struct pci_bus *bus; /* bus this device is on */

    struct pci_dev *dev; /* next device on this bus */

    struct pci_dev *next; /* chain of all device */

    .

    .

    unsigned int devfn; /* slot and function */

    unsigned short vendor; /* vendor ID */

    unsigned short device; /* device ID */

    .

    .

    unsigned int irq; /* irq generated by this device */

    unsigned long base_address[6];

    };

     

    La structure comporte d?autres champs, mais de moindre importance.

     

    Au démarrage du noyau Linux, le système attribue à chaque carte du bus PCI, une IRQ. Cette attribution est arbitraire, et on peut l'ignorer, les IRQ attribuées à ce moment la n'étant pas réservées. L'IRQ attribuée est bien évidemment disponible dans le champ irq de la structure pci_dev.

     

    Toutes les cartes PCI du système sont représentées par de telles structures et toutes ces structures sont chaînées au moyen du champ next. On peut donc, grâce à ça, parcourir la liste de toutes les cartes présentes.

     

    Les champs vendor et device permettent d?identifier une carte : chaque carte porte son vendor ID : numéro attribué au fabricant de la carte, ainsi qu'un device ID, numéro attribué par le fabricant pour identifier ce type de carte.

     

    Le champ devfn permet d'identifier le slot correspondant à la carte, ainsi que la fonction attribuée au device, ici, seul le slot nous intêresse. C'est en effet pour nous la seule possibilité de différencier plusieurs cartes identiques : même vendor ID, même device ID. Lors du démarrage, chaque carte est repérée par son slot, ce qui permet aussi de la repérer physiquement.

     

    Le champ base_address est un tableau dont nous utilisons seulement les 3 premières cases : le système y enregistre au démarrage les adresses physiques(adresses valides sur le bus PCI) des 3 espaces mémoires de la carte : espace d'E/S, espace mémoire de la carte et espace de configuration.
     
     

    2 Initialisation des drivers PCI.

     

    Pour commencer, le driver doit récupérer la structure pci_dev décrivant sa carte. Ceci est fait par l'appel a la fonction :

     

    Struct pci_dev *pci_find_device (unsigned int vendor, unsigned int device, struct pic_dev *from) ;

     

    Avec vendor et device permettant d'identifier la carte. Le paramètre from permet d'écrire des drivers multicartes : la fonction pci_find_device parcours la liste chaînée de toutes les structures pci_dev du système jusqu'à trouver une structure dont les champs vendor et device correspondent à ceux qui lui ont été donnés en paramètres. Si le paramètre from est NULL, le point de départ du parcours est le début de la chaîne. Si from n'est pas NULL, alors il doit être un pointeur vers une structure pci_dev du système et son suivant sert de point de départ au parcours.

    Un seul driver peut donc initialiser plusieurs cartes identiques avec un code du type suivant :

    struct pci_dev *dev=NULL ;

    while (1) {

    /* on reprend le parcours de la liste chaînée depuis la structure trouvée lors de la boucle précédente */

    dev = pci_find_device(VENDOR_ID, DEVICE_ID, dev) ;

    if (dev == NULL) /* si la fonction n'a pas trouvé de carte */

    {

    break ;

    }

    else

    {

    /* initialisation de la carte */

    }

    }
     
     
     

    3 Mémoire partagée.
     
     

      3.1 Espace mémoire.

      L'espace d'adressage du processeur a la forme suivante :

       

       

       
      3.2 Mémoire partagée sur la carte : remapping.

       

      Sur le bus PCI, les adresses mémoire sont des adresses réelles ou physiques.

      Dans le noyau, les adresses utilisées sont virtuelles, elles sont interprétées par le système et transcrites en adresses physiques au moment voulu par le gestionnaire de mémoire de Linux.

      Lorsqu'on récupère l'adresse de l'espace mémoire de la carte dans dev->base_address[2] ou l'adresse de l'espace d'Entrée/Sortie de la carte dans dev->base_address[1], ce sont des adresses physiques qui n'ont donc aucune signification pour le noyau.

      Pour écrire ou lire dans un de ces espaces depuis le système, il est nécessaire d'avoir une adresse virtuelle, utilisable par le noyau, correspondant à ces adresses physiques. L'obtention d'une telle adresse est faite par la fonction :

      void *ioremap(unsigned long phys_addr, unsigned long size) ;

       

      Avec long_phys, l'adresse physique du début de la zone à remapper, et size la taille de cette zone. L'adresse obtenue peut être directement utilisée par le noyau, en lecture comme en écriture, c'est le gestionnaire de mémoire qui se chargera de faire la correspondance. Une fois ce remapping effectué, l'écriture et la lecture sur les zones mémoires et E/S de la carte se font de façon absolument transparente, sans différence avec une écriture/lecture en RAM classique.
       

      3.3 Mémoire partagée sur l'hôte : DMA.

  Lorsqu'on veut gagner du temps sur les accès mémoires, que l'espace mémoire de la carte est trop réduit, et qu'on veut gérer depuis la machine hôte la mémoire partagée, il est plus simple de partager une partie de l'espace RAM avec la carte. Le problème est alors l'inverse de celui posé précédemment :

 

On réserve de la mémoire dans le noyau, mais le pointeur retourne est une adresse virtuelle, or la carte n'utilise que des adresses physiques. On a donc recours à la fonction :

 

void *virt_to_bus(unsigned long addr) ;
 

Fonction qui renvoie l'adresse physique correspondant à l'adresse virtuelle addr. Cette conversion permet à la carte d'accéder directement(a travers le contrôleur PCI) à la RAM du PC. Cette opération est possible car le contrôleur PCI a le même espace d?adressage que le CPU.

 

Lors de la réservation de addr, il est important d'avoir un espace physique correspondant qui soit contigu.

En effet, une fois la zone mémoire partagée réservée, on va communiquer à la carte l?adresse du début de la zone. Lors d'une allocation classique, la zone physique allouée n?est pas forcément continue, c'est le gestionnaire de mémoire de Linux qui s'occupe de rendre cela transparent pour l'utilisateur. La carte n'ayant pas accès à ce gestionnaire, il faut avoir une zone mémoire continue.

 

Pour cela, on utilise la fonction kmalloc, équivalent à malloc, mais qui, avec les marqueurs GFP_KERNEL|GFP_ATOMIC, garantit un espace mémoire contigu. La seule limitation de cette fonction est la taille maximale de l'espace réservé : 128Ko.

 

Pour permettre à la carte de prendre le contrôle du bus PCI, opération nécessaire pour aller accéder à la RAM, il faut la configurer pour lui en donner l'autorisation, ceci est fait au moment de l'initialisation par l'appel à la fonction pci_set_master