print · rss · source

< Gérer les interruptions - la théorie | TutoOS | Gérer les interruptions du clavier >


Gérer les interruptions - la mise en oeuvre

Sources

Le package contenant les sources est téléchargeable ici : kernel_ManageINT.tgz
Pour naviguer dans l'arborescence : ManageINT
Notez que vous pouvez utiliser la commande diff pour visualiser les parties modifiées ou ajoutées par rapport aux sources du chapitre précédent :

$ diff -u -r -N ReloadGDT/ ManageINT/

Préalable : étendre le code C du noyau à l'aide de directives en assembleur

Un certain nombre d'instructions existent en assembleur, et sont nécessaires à l'écriture de notre noyau, mais n'ont pas d'équivalent en C. C'est notamment le cas des instructions cli, sti, in et out. Les macros définies dans le fichier io.h permettent de contourner cette difficulté en étendant le jeu d'instructions.

/* desactive les interruptions */
#define cli asm("cli"::)

/* reactive les interruptions */
#define sti asm("sti"::)

/* ecrit un octet sur un port */
#define outb(port,value) \
        asm volatile ("outb %%al, %%dx" :: "d" (port), "a" (value));

/* ecrit un octet sur un port et marque une temporisation  */
#define outbp(port,value) \
        asm volatile ("outb %%al, %%dx; jmp 1f; 1:" :: "d" (port), "a" (value));

/* lit un octet sur un port */
#define inb(port) ({    \
        unsigned char _v;       \
        asm volatile ("inb %%dx, %%al" : "=a" (_v) : "d" (port)); \
        _v;     \
})

Nous avons déja vu dans les chapitres précédent comment inclure de l'assembleur dans le code C de gcc avec la directive asm(). Il faut noter la particularité suivante : gcc se base sur gas qui utilise la syntaxe AT&T.

Initialiser la table IDT

Créer les descripteurs système de type Interrupt Gate

Les descripteurs système de l'IDT gérant les interruptions, dont le fonctionnement est expliqué au chapitre précédent, ont ce modèle :

La structure ci-dessous, définie dans le fichier idt.h, sert à créer les descripteurs d'interruption. Notez là encore la présence de la directive __attribute__ ((packed)) pour empêcher gcc d'insérer des octets de padding au sein de la structure :

/* descripteur de segment */
struct idtdesc {
    u16 offset0_15;    
    u16 select;
    u16 type;
    u16 offset16_31;    
} __attribute__ ((packed));

La fonction init_idt_desc() sert à initialiser les descripteur système.

void init_idt_desc(u16 select, u32 offset, u16 type, struct idtdesc* desc) {
    desc->offset0_15 = (offset & 0xffff);
    desc->select = select;
    desc->type = type;
    desc->offset16_31 = (offset & 0xffff0000) >> 16;
    return;
}

Par exemple, pour initialiser le descripteur associé à l'IRQ 1 (interruptions clavier), on utilise le code suivant :

init_idt_desc(0x08, (u32) _asm_irq_1, INTGATE, &kidt[33]); /* clavier */
  • le premier argument pointe sur le descripteur de segment de code qui contient la routine de gestion de l'interruption (dans notre cas le descripteur est à l'offset 0x08 dans la GDT).
  • le deuxième argument est l'offset par rapport au début du segment de code pour trouver le début de la routine. Cet offset correspond au nom de la fonction à exécuter.
  • le troisième argument comprend le type de descripteur dans l'IDT (ici interrupt gate) et le niveau de privilège du segment.
  • le dernier argument correspond au descripteur à initialiser.

Créer une routine d'interruption (ISR)

Lors d'une interruption, un vecteur est transmis par le PIC au processeur pour exécuter la bonne ISR. Une ISR obéit à deux contraintes particulières :

  • Une fois l'interruption traitée, il faut avertir le contrôleur de la fin du traitement de l'interruption en lui envoyant un message End of Interrupt (EOI).
  • Une routine d'interruption doit retourner en utilisant la directive assembleur iret (au lieu de ret, utilisé habituellement lors de l'appel à une fonction).

Pour envoyer au contrôleur un message EOI, indiquant que l'interruption en cours a été traitée, on utilise le code assembleur suivant :

; envoyer EOI au PIC
    mov al, 0x20
    out 0x20, al

En revanche, pour retourner de la routine d'interruption, rien en C ne nous permet d'utiliser iret. La routine d'interruption appelée doit donc être écrite en assembleur. Par exemple, pour gérer l'interruption de l'IRQ 1 :

_asm_irq_1:
    call isr_kbd_int
    mov al,0x20  ; EOI
    out 0x20,al
    iret

Ce code appelle la fonction isr_kbd_int() qui effectue réellement la gestion de l'interruption du clavier, puis elle envoie un EOI au contrôleur avant de retourner. La partie de code en assembleur ne fait donc qu'encapsuler le code écrit en C dans le fichier interrupt.c.

#include "types.h"
#include "screen.h"

void isr_default_int(void)
{
        print("interrupt\n");
}

void isr_clock_int(void)
{
        static int tic = 0;
        static int sec = 0;
        tic++;
        if (tic % 100 == 0) {
                sec++;
                tic = 0;
                print("clock\n");
        }
}

void isr_kbd_int(void)
{
        print("keyboard\n");
}

Les routines en assembleur sont dans le fichier int.asm.

extern isr_default_int, isr_clock_int, isr_kbd_int
global _asm_default_int, _asm_irq_0, _asm_irq_1

_asm_default_int:
        call isr_default_int
        mov al,0x20
        out 0x20,al
        iret

_asm_irq_0:
        call isr_clock_int
        mov al,0x20
        out 0x20,al
        iret

_asm_irq_1:
        call isr_kbd_int
        mov al,0x20
        out 0x20,al
        iret

Est-on vraiment obligés d'utiliser pour chaque routine d'interruption ces enveloppes en assembleur et n'est-il pas plus rapide d'inclure dans le code en C une directive du type asm("iret") ? Le problème est qu'en sortant d'une fonction de cette façon, on oublie de restaurer convenablement les registres, dont le pointeur de pile esp.

Initialiser l'IDT avec la fonction init_idt()

La fonction init_idt() effectue l'initialisation des descripteurs systèmes et le chargement de l'IDT. Étant donné leur similarité, le code nécessaire pour initialiser et charger l'IDT ressemble beaucoup à celui utilisé pour l'initialisation de la GDT. À l'exception des descripteurs associés aux IRQ 0 et 1 (respectivement l'horloge et le clavier), l'ensemble des descripteurs pointent vers un handler d'interruption par défaut :

for (i = 0; i < IDTSIZE; i++)                                   /* defaut */
        init_idt_desc(0x08, (u32) _asm_default_int, INTGATE, &kidt[i]);

init_idt_desc(0x08, (u32) _asm_irq_0, INTGATE, &kidt[32]);      /* horloge */
init_idt_desc(0x08, (u32) _asm_irq_1, INTGATE, &kidt[33]);      /* clavier */

Le programme principal du noyau

#include "types.h"
#include "gdt.h"
#include "screen.h"
#include "io.h"
#include "idt.h"

void init_pic(void);


int main(void);

void _start(void)
{
        kY = 16;
        kattr = 0x0E;

        init_idt();
        print("kernel : idt loaded\n");

        init_pic();
        print("kernel : pic configured\n");

        /* initialisation de la GDT et des segments */
        init_gdt();

        /* Initialisation du pointeur de pile %esp */
        asm("   movw $0x18, %ax \n \
                movw %ax, %ss \n \
                movl $0x20000, %esp"
);

        main();
}

int main(void)
{
        print("kernel : gdt loaded\n");

        sti;

        kattr = 0x47;
        print("kernel : allowing interrupt\n");
        kattr = 0x07;

        while (1);
}
  • La fonction init_gdt() réinitialise la GDT puis la fonction init_idt() configure et charge la table IDT des descripteurs d'interruptions.
  • Les chipsets 8259A (maître et esclave) sont reprogrammés par la fonction init_pic(). Cette fonction, définie dans le fichier pic.c, initialise les contrôleurs d'interruption maître et esclave de la façon décrite au chapitre précédent.
  • Une fois les PIC configurés et la table IDT en mémoire, on réactive les interruptions avec la macro sti.

Le schéma ci-dessous résume l'organisation des données en mémoire physique après les initialisations :

Compiler et exécuter le noyau

$ tar xfz kernel_ManageINT.tgz
$ cd ManageINT
$ make

A l'exécution du noyau, un tic est affiché toutes les 100 interruptions de l'horloge. En revanche, pour le moment, l'appui d'une touche du clavier ne fonctionne qu'une seule fois. Nous verrons plus tard comment gérer correctement le clavier :


< Gérer les interruptions - la théorie | TutoOS | Gérer les interruptions du clavier >

print · rss · source
Page last modified on October 06, 2010, at 09:58 AM