Skip to content

Valentin Haudiquet

Utilisation de l'APIC plutôt que du PIC : pourquoi et comment

Un système d'exploitation se doit de gérer les interruptions matérielles. Il est donc nécessaire de programmer un contrôleur d'interruption. Beaucoup de tutoriels et de ressources en ligne proposent de programmer le PIC, un contrôleur simple. Cet article expose les avantages de l'APIC, et montre comment le programmer.

Introduction : késako ?

Un ordinateur, plus précisément un processeur, exécute le code qui lui est fourni en mémoire linéairement, instruction par instruction. Comment est-il alors possible de réagir à des évènements extérieurs (clé usb branchée, paquet provenant du réseau, mise à jour d'une horloge car une certaine unité de temps est passée...) ? Une réponse naïve serait de dire "le processeur vérifie périodiquement tous les contrôleurs/... du système", par exemple toutes les x secondes le processeur demande au contrôleur USB "il-y-a t-il une clé branchée sur ton port ?". Cette réponse n'est évidement pas adaptée, car cela gâcherait un nombre de cycle processeur incroyable ; de plus, il n'y a pas alors de moyen d'interrompre l'exécution du programme en cours pour demander à tous les périphériques si ils ont des nouvelles informations. La solution à ce problème est les interruptions matérielles. Une interruption matérielle est comme un signal qui vient d'un périphérique et interrompt le processeur dans son exécution pour lui dire "j'ai une nouvelle information à te transmettre". Les processeurs modernes comportent donc un contrôleur d'interruptions, qui permet par exemple d'associer une interruption à chaque périphérique, et d'interrompre le processeur.

Une utilisation importante des interruptions matérielle est l'ordonnancement des processus : il est effectué à chaque interruption reçue de l'horloge embarquée dans l'ordinateur.

Une autre forme d'interruption très utile est les exceptions : si le processeur rencontre une instruction de division par 0, il réagit en s'interrompant lui même par une interruption 'division par zéro'. Cela permet au système d'exploitation d'afficher un message à l'utilisateur et de tuer le processus ayant tenté une division par 0.

La description précédente des interruptions est brève et factuellement inexacte car simplifiée, mais l'idée de base des interruptions est là. Plus d'informations sur les interruptions sont disponibles ici. Le reste de l'article introduit la manière dont un système d'exploitation programme le contrôleur d'interruption.

Le PIC (Intel 8259) : Programmable Interrupt Controller

Sur un PC x86, les interruptions ont historiquement été gérées par le PIC : Programmable Interrupt Controller (plus d'informations ici et ). La plupart des 'tutoriels' en ligne proposant de réaliser son propre système d'exploitation/noyau utilisent le PIC (PépinOS, The little book about OS development, Operating System Development Series). Je ne vais pas détailler ici la programmation du PIC, car cela a déjà été fait dans les liens précédents. Rappelons tout de même les éléments essentiels.

Le PIC est programmable à travers des registres mappés sur des ports d'I/O (accessibles par les instructions outx/inx).

Sur un processeur x86, les vecteurs d'interruptions 0 à 31 sont réservées comme des exceptions (division par 0, faute de page, ...). Ces interruptions sont gérées par le processeur lui-même, et ne nécessitent pas de contrôleur externe comme le PIC. Le PIC permet de gérer 8 interruptions 'IRQ' : Interrupt ReQuest. Cette limitation à 8 est dépassée en utilisant la plupart du temps 2 PICS en cascade : un PIC 'maître' ayant sur sa ligne IRQ2 un second PIC 'esclave' ; on arrive donc à une table de 16 IRQ :

IRQ#Vecteur (défaut)Description
IRQ00x08Horloge (PIT)
IRQ10x09Clavier
IRQ20x0ACascade PIC esclave
IRQ30x0BPort série 2 (COM2)
IRQ40x0CPort série 1 (COM1)
IRQ50x0DPort parallèle 2 (LPT2)
IRQ60x0ELecteur de disquettes
IRQ70x0FPort parallèle 1 (LPT1) (+ 'spurious irqs')
IRQ8 / 00x70Horloge (CMOS RTC)
IRQ9 / 10x71Libre
IRQ10 / 20x72Libre
IRQ 11 / 30x73Libre
IRQ 12 / 40x74Souris (PS/2)
IRQ 13 / 50x75FPU
IRQ 14 / 60x76Primary ATA
IRQ 15 / 70x77Secondary ATA

Comme vous l'avez peut-être remarqué, les IRQ 0 à 7 sont par défaut mappées sur des vecteurs d'interruptions réservés par le processeur pour les exceptions (merci IBM). Il faut donc reprogrammer le PIC en lui demandant de déplacer les vecteurs d'interruptions ; en général on déplacera toutes les IRQ, pour les associer aux vecteurs d'interruptions 32 (0x20) à 47 (0x2F). Après avoir remappé le PIC, il est utilisable et les interruptions peuvent êtres activées (sous réserve bien sûr d'avoir défini dans l'IDT (Interrupt Descriptor Table) des routines à exécuter en cas d'interruption).

Le processeur gèrera ensuite les interruptions ; la seule partie qui nous reste à programmer est dans les routines d'interruptions : il faut envoyer le signal 'EOI' End Of Interrupt au PIC, pour lui signifier que nous avons fini de traiter l'interruption. La seule difficulté ici est d'envoyer le signal aux 2 PICS : le maître et l'esclave ; il faut savoir qu'il faut d'abord envoyer EOI à l'esclave et ensuite au maître. Cela peut être fait très facilement en écrivant simplement dans un registre du PIC.

Voilà. Il n'y a rien d'autre à programmer, c'est terminé, nous pouvons traiter les interruptions matérielles (notamment directement le timer PIT ou le clavier...).

Utiliser le PIC est très bien dans un OS 'tutoriel', et pour commencer (c'est un contrôleur facile à utiliser, comme nous venons de voir) ; cependant, le PIC n'est en pratique plus du tout utilisé aujourd'hui par les systèmes d'exploitation modernes, pour plusieurs raison. La raison principale est le fait qu'un seul PIC est présent pour tous les CPUs/cœurs : dans un environnement multicœur/multiprocesseur, il est en pratique inutilisable. En effet, comment savoir à quel processeur sont destinées les données de certains périphériques, comment communiquer entre les processeurs, comment initialiser et ordonnancer les processus entre les processeurs, etc... De plus, le PIC est un vieux contrôleur, et n'est pas aussi performant que le contrôleur suivant (en termes de capacités d'interruptions par seconde par exemple). Il y a d'autres raisons de ne plus utiliser le PIC aujourd'hui, mais celles-là sont les plus importantes.

APIC, Local APIC, IO APIC : Advanced Programmable Interrupt Controller

L'APIC est, comme son nom l'indique, le successeur du PIC sur les PC x86. APIC est un terme générique : en réalité, un PC possède un 'Local APIC' par processeur logique et un ou plusieurs 'IO APIC'.

Un 'Local APIC' gère les interruptions locales au processeur logique en question : horloge (APIC timer), erreur de l'APIC, interruptions venant des capteurs thermiques... Les interruptions provenant des périphériques sont gérées par un 'IO APIC'. Un local APIC peut aussi envoyer une interruption à un autre processeur (IPI, Inter Processor Interrupt) ou recevoir des interruptions d'un autre processeur. Le local APIC reçoit les interruptions depuis l'IO APIC sous forme d'IPI.

Là où les registres du PIC sont mappés sur des ports d'I/O, les registres du local APIC sont mappés en mémoire, plus précisément à l'adresse physique 0xFEE0_0000 (par défaut). La table des registres du local APIC est disponible dans Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3, Section 10.4.1, Table 10-1 (au moment où j'écris cet article). Voilà un récapitulatif des registres intéressants :

Offset depuis 0xFEE00000Description
0x20ID register
0x30Version register
0x80Task Priority Register
0x90Arbitration Priority Register
0xA0Processor Priority Register
0xB0EOI (End Of Interrupt) register
0xF0Spurious interrupt vector register
0x100 - 0x170In-Service Registers (8 registres de 32 bits, 256 bits total)
0x180 - 0x1F0Trigger Mode Registers (8 registres de 32 bits, 256 bits total)
0x200 - 0x270Interrupt Request Registers (8 registres de 32 bits, 256 bits total)
0x280Error Status register
0x300 - 0x310Interrupt Command Register (2 registres de 32 bits, 64 bits total)
0x320LVT Timer Register
0x330LVT Thermal Sensor Register
0x340LVT Performance Monitoring ... Register
0x350LVT LINT0 Register
0x360LVT LINT1 Register
0x370LVT Error Register
0x380Initial Count Register (Timer)
0x390Current Count Register (Timer)
0x3E0Divide Configuration Register (Timer)

Ces informations nous permettrons de programmer le local APIC.

Il y a deux choses qui vont nous intéresser dans la programmation de l'APIC : mettre en place un timer (comme le PIT pour le PIC) avec l'APIC timer (local APIC) et mettre en place l'IO APIC et le local APIC pour recevoir des interruptions des périphériques (par exemple le clavier ou les disques durs).

Détection : CPUID, spécifications MP, table ACPI MADT

Certains ordinateurs ne possèdent pas d'APIC, uniquement un PIC (il faut quand même aller chercher très loin, avant 1995). Il est possible de détecter la présence d'un local APIC en utilisant l'instruction CPUID : le bit 9 du registre EDX est mis à 1 si le processeur contient un APIC, 0 sinon. Cela est suffisant pour détecter un local APIC, et il est d'ores et déjà possible de passer à l'étape suivante : Initialisation.

Cependant, il est aussi possible de détecter le nombre de local APIC dans l'ordinateur, et donc de détecter le nombre de processeurs logiques. Il y a deux méthodes :

  • Utiliser les tables 'MP' (MultiProcessor Spec.) ; cette méthode est ancienne, et ne fonctionnera pas sur des périphériques plus récents
  • Utiliser les tables ACPI (Advanced Configuration and Power Interface), plus précisément la MADT : Multiple APIC Description Table

Un système d'exploitation doit donc d'abord essayer de détecter la présence d'ACPI, et, seulement si les tables sont absentes, utiliser la 'MultiProcessor Spec.'.

Les tables ACPI (ou les spécifications MP le cas échéant) permettent aussi de détecter la présence et le nombre d'IO APIC.

D'abord, essayons donc d'utiliser les tables ACPI. Pour cela, il faut d'abord avoir détecté le RSDP, Root System Description Pointer. Cette détection ne sera pas expliquée plus en détails ici ; voir ACPI, et les spécifications ACPI (ACPI Specifications) pour plus d'informations. En considérant que l'OS à un accès à la table RSDT (Root System Description Table), et arrive à y lire un pointeur vers la MADT (Multiple APIC Description Table), il faut maintenant parser la MADT. Voilà sa structure :

TypeNomDescription
struct SYSTEM_DESCRIPTION_TABLE_HEADERheaderHeader ACPI, de signature 'APIC'
uint32_tlocal_apic_addressAdresse physique du local APIC (devrait être 0xFEE00000)
uint32_tflagsFlags
?entries[]Contenu de la table

Les 'entries' de la MADT peuvent avoir plusieurs forme ; elles commencent toutes par un octet 'identifiant' et un octet 'taille', permettant de parser l'entrée. Voilà la structure des entrées qui nous intéressent :

Entrée 'Local APIC' :

TypeNomDescription
uint8_ttypeType de l'entrée, ici Local APIC = 0
uint8_tlengthLongueur de l'entrée, ici = 8
uint8_tacpi_processor_idID du processeur associé au local APIC
uint8_tapic_idID du local APIC
uint32_tflagsFlags ; le seul flag intéressant est le bit 0, à 1 pour signaler que le local APIC est activé i.e. présent et utilisable par l'OS

Entrée 'IO APIC' :

TypeNomDescription
uint8_ttypeType de l'entrée, ici IO APIC = 1
uint8_tlengthLongueur de l'entrée, ici = 12
uint8_tio_apic_idID de l'IO APIC
uint8_treservedPadding, inutile
uint32_tio_apic_addressAdresse physique de l'IO APIC
uint32_tgsi_baseIRQ de départ de l'IO APIC

Le code ci-dessous permet donc de parser la MADT :

void acpi_madt_parse(struct MADT* madt)
{
  //calculer la taille des entrées de la MADT en fonction de la taille de la table :
  //taille entrées = taille table - taille (header + local_apic_address + flags)
  size_t entries_size = madt->header.length - sizeof(madt->header) - sizeof(uint32_t)*2;
	
  uint8_t* entry = madt->entries;
  //tant que l'on ne dépasse pas entries + entries_size
  while((entry - madt->entries) < entries_size)
  {
    uint8_t entry_type = *entry;
    switch(entry_type)
    {
      case 0:
        //traiter l'entry de type local APIC
        break;
      case 1:
        //traiter l'entry de type IO APIC
        break;
    }

    //aller à l'entrée suivante : entry[1] contient length, la longueur de l'entrée courante
    entry += entry[1];
  }
}

Pour traiter les entrées, on pourra par exemple maintenir des tableaux globaux contenant le nombre, les ID, les adresses, etc... des différents local APIC/IO APIC.

La norme ACPI a été introduite en 1996 ; il faut donc encore une fois chercher des ordinateurs très très vieux pour trouver une incompatibilité. Je ne détaillerai pas ici l'utilisation des spécifications MP pour détecter PIC et APIC ; voir MP_Specification et les spécifications officielles : MultiProcessor Specification. Si vous souhaitez initialiser l'APIC sans ACPI, après avoir parsé les tables MP, assurez vous de prendre en compte les 'model specific registers', et d'envoyer un signal pour certains très vieux PC n'ayant pas par défaut connectés les pins de l'IO APIC ; normalement, si vous vous êtes bien renseignés, le code devrait ressembler à ça :

asm volatile("mov $0x1B, %%ecx; rdmsr; or $0x800, %%eax; wrmsr;":::"eax", "ecx");
outb(0x22, 0x70);
outb(0x23, 0x1);

NOTE : ce code n'est pas nécessaire dans le cas d'un PC supportant l'ACPI, normalement.

L'étape suivante, après avoir détecté local APIC (et éventuellement IO APIC) est l'initialisation du local APIC.

Initialisation

Tout d'abord, il faut désactiver le PIC ; pour cela, on effectue le remapping des IRQ de la manière classique, puis on masque toutes les interruptions :

outb(0x21, 0xFF); //Masquer les interruptions sur le PIC Master
outb(0xA1, 0xFF); //Masquer les interruptions sur le PIC Slave

La raison pour laquelle il faut remapper les IRQ avant est un peu technique : les 'spurious irq' sont des 'fausses' interruptions provoquées pour différentes raisons, par exemple du bruit sur la ligne électrique. Ces interruptions ne pourront pas être masquées, et leur vecteur d'interruption doit donc être correctement configuré pour ne pas que le noyau les confondent avec une exception.

Ensuite, on active l'APIC : le bit 8 du registre 'Spurious interrupt' du LAPIC sert à activer/désactiver logiciellement le local APIC. Par défaut, ce registre devrait contenir 0xFF, et le local APIC devrait donc être désactivé (opération réalisée par le micrologiciel de la carte mère). Il faut alors définir ce registre à 0x1FF pour activer l'APIC (ce registre définit aussi le vecteur d'interruption pour les 'spurious irq' du local APIC, d'où son nom).

Pour être sûr que l'APIC ne va pas bloquer des IRQ de plus faibles priorité durant le traitement d'une IRQ, il faut utiliser le Task Priority Register : ce registre permet de définir quelles interruptions seront considéré 'prioritaires' et donc désactiveront les autres. Nous allons définir toutes les IRQ comme 'non-prioritaires', en mettant ce registre à 0 (normalement, c'est sa valeur par défaut, mais on ne sait jamais).

Le local APIC permet d'envoyer des interruptions à d'autres processeurs, mais nous allons nous contenter de son mode de destination ("Destination Format Register") le plus simple, en définissant ce registre à 0xFFFFFFFF (ce qui devrait être sa valeur par défaut) pour le modèle 'flat model' (il est possible de changer de modèle pour rediriger les interruptions vers des clusters de processeurs...).

Le local APIC génère des interruptions de la part des capteurs thermiques, ou en cas d'erreur, ou encore depuis les vecteurs LINT0/LINT1. On peut choisir d'utiliser ces vecteurs comme on veut, mais ici, on les ignorera, en les masquant.

Ci-dessous le code d'initialisation du local APIC, qui effectue ces étapes dans l'ordre :

void local_apic_init()
{
  //on considère ici avoir identity-mappé l'apic, ou ne pas avoir activé de pagination/mémoire virtuelle
  uint8_t volatile* apic = (uint8_t*) 0xFEE00000;
	
  /* setup des registres de mode : TPR, DFR */
  *((uint32_t volatile*) (apic + 0x80)) = 0; //Task Priority
  *((uint32_t volatile*) (apic + 0xE0)) = 0xFFFFFFFF; //Destination Format
	
  /* setup de LINT0, LINT1 : adaptez ! */
  *((uint32_t volatile*) (apic + 0x350)) = (1 << 17); //bit 17 : interruption masquée
  *((uint32_t volatile*) (apic + 0x360)) = (1 << 17); //bit 17 : interruption masquée
	
  /* masquage des interruptions ERROR, THERMAL, PERFORMANCE */
  *((uint32_t volatile*) (apic + 0x370)) = (1 << 17); //bit 17 : interruption masquée
  *((uint32_t volatile*) (apic + 0x330)) = (1 << 17); //bit 17 : interruption masquée
  *((uint32_t volatile*) (apic + 0x340)) = (1 << 17); //bit 17 : interruption masquée
	
  /* activation de l'APIC */
  *((uint32_t volatile*) (apic + 0xF0)) = 0x1FF;
}

APIC Timer

L'APIC timer va compter un certains nombre de ticks, de manière décroissante : il définit la valeur du registre Current Count Register à la valeur initiale (registre Initial Count Register), puis retire 1 pour chaque 'tick' qui passe, jusqu'à arriver à 0. Il y a plusieurs modes possible :

  • le mode 'one shot', une fois arrivé à 0, une interruption est déclenchée, et le timer s'arrête
  • le mode 'periodic', une fois arrivé à 0, une interruption est déclenchée, et le timer est réinitialisé à la valeur initiale (registre Initial Count Register)

Le mode de l'APIC timer est contrôlé par les bits 17 et 18 du registre 'LVT Timer Register' : le mode 'one shot' correspond à 00b et le mode 'periodic' à 01b.

Le registre 'LVT Timer Register' est organisé de la façon suivante :

BitsDescription
0 - 7Vecteur d'interruption
12'Delivery status' : 0 = idle,1 = 'sent pending' (interruption en attente de traitement)
16Masque : 0 = interruption non masquée, 1 = interruption masquée (n'aura pas lieu)
17-18Mode (00b = one shot, 01b = periodic)

Contrairement au PIT qui oscille à fréquence fixée, l'APIC timer oscille à une fréquence dépendant de la fréquence du processeur et/ou du bus. On va donc initialiser l'APIC timer de la façon suivante : le démarrer en mode 'one shot', attendre un certain temps t avec le PIT, et mesurer le nombre de tick écoulés pour l'APIC timer. Ainsi, pour le faire osciller à la fréquence 1/t (une interruption tous les t), on va définir le compteur de tick initial au nombre de ticks qu'on a mesuré. Une fois cela fait, on peut passer l'APIC timer en mode 'periodic' pour obtenir un timer périodique sur une interruption comme l'était le PIT, nous permettant par exemple d'ordonnancer.

Le code ci-dessous permet d'initialiser l'APIC timer à une fréquence définie :

/* Rappel des registres :
* 0x320 : LVT Timer Register
* 0x380 : Initial Count Register (Timer)
* 0x390 : Current Count Register (Timer)
* 0x3E0 : Divide Configuration Register (Timer)
*/
void apic_timer_init(uint32_t t_us) //t est un temps en microsecondes
{
  /* setup de l'APIC timer */
  //on considère ici avoir identity-mappé l'apic, ou ne pas avoir activé de pagination/mémoire virtuelle
  uint8_t volatile* apic = (uint8_t*) 0xFEE00000; 
  //mappe l'interruption du timer sur le vecteur d'interruption 32, activant le timer en mode 'one shot'
  *((uint32_t volatile*) (apic + 0x320)) = 32 | (0b00 << 17); //00b : mode 'one shot'
  //définit le 'divider' de l'APIC à 16, pour ne pas qu'il soit trop rapide à décompter les ticks
  *((uint32_t volatile*) (apic + 0x3E0)) = 0x3; 
	
  /* préparer le PIT à un 'sleep'(on considère avoir déjà un 'driver' pour le PIT) */
  pit_prepare_sleep(t_us);
	
  /* définir le compteur de tick initial de l'apic timer à (-1), démarrant effectivement celui-ci */
  *((uint32_t volatile*) (apic + 0x380)) = 0xFFFFFFFF;
	
  /* attendre t avec le pit */
  pit_sleep();
	
  /* stopper l'APIC timer */
  //on mets le masque d'interruption à 1, ce qui stoppe l'APIC timer
  *((uint32_t volatile*) (apic + 0x320)) |= 1 << 16;
	
  /* on compte le nombre de ticks passés durant le temps t */
  uint32_t ticks_during_t = 0xFFFFFFFF - *((volatile uint32_t*) (apic + 0x390));
	
  /* on redémarre le timer en mode périodique avec le bon nombre de ticks */
  *((uint32_t volatile*) (apic + 0x320)) = 32 | (0b01 << 17); //01b : mode 'periodic'
  //redéfinir le divider (nécessaire pour certains hardware...)
  *((uint32_t volatile*) (apic + 0x3E0)) = 0x3; 
  //définir le compteur de tick, démarrant effectivement le timer
  *((uint32_t volatile*) (apic + 0x380)) = ticks_during_t;
}

On a maintenant effectivement remplacé le couple PIC/PIT par l'APIC et son timer.

IO APIC

Maintenant, on veut pouvoir obtenir des interruptions de la part des périphériques. Il est tout à fait possible d'utiliser le local APIC avec le PIC pour réaliser cela ; en fait, certains systèmes uniprocesseur pourvus d'un local APIC peuvent être dépourvus d'IO APIC (voir la section précédente 'Détection' pour détecter la présence d'IO APIC). Dans ce cas, la configuration s'arrête là.

Néanmoins, pour être prêt face à des systèmes multiprocesseurs et accéder à des périphériques plus récents, il est nécessaire d'utiliser l'IO APIC. En effet, en plus de permettre la redirection des interruption à chaque processeur, un IO APIC gère 24 IRQ (contrairement au PIC qui en gérait 8), qui sont de deux natures différentes :

  • ISA IRQ (ISA : Industry Standard Architecture) : les IRQ 'legacy' du PIC, même description
  • PCI IRQ : une IRQ liée à un périphérique PCI (plus exactement à un triplet bus/périphérique/fonction)

Pour accéder aux IRQ des périphériques PCI n'ayant pas d'équivalent ISA, il est donc nécessaire d'utiliser un IO APIC.

L'IO APIC possède un grand nombre de registres. Pour cela, il utilise un registre de sélection, dans lequel on écrit un ID, et un registre de donnée, correspondant au données présente dans le registre ayant l'ID présent dans le registre de sélection. Ces registres sont mappés en mémoire, à l'adresse 0xFEC0_0000 par défaut (mais cela peut dépendre du système, préférer utiliser l'adresse trouvée en parsant la table ACPI MADT, cf 'Détection'). Ces deux registres ont les caractéristiques suivantes :

Offset (depuis IO_APIC_BASE)NomDescription
0x0IOREGSELRegistre de sélection
0x10IOREGWINRegistre de données, contient le contenu du registre d'ID la valeur du registre de sélection

Ils permettent d'accéder aux registres suivants :

IDNomDescription
0IOAPICIDContient l'ID de l'IO APIC (bits 24-27)
1IOAPICVERVersion (bits 0-8) et 'nombre de l'entrée IRQ maximale' (bits 16-23)
2IOAPICARBIO APIC 'arbitration id'
0x10-0x11IOREDTBL #0Entrée pour l'IRQ 0 (2 registres de 32-bits, donc 64-bits)
0x10 + n*2 - 0x10 + n*2 + 1IOREDTBL #nEntrée pour l'IRQ n (64-bits)

Les entrées IRQ sont de la forme suivante :

BitsNomDescription
0 - 7VectorVecteur d'interruption associé à l'entrée
8 - 10Delivery ModeManière dont l'interruption sera envoyée au CPU
11Destination Mode0 = physical, 1 = logical
12Delivery Status1 : IRQ en cours de transfert vers le local APIC (0 sinon)
13Pin Polarity0 = active high, 1 = active low
15Trigger Mode0 = edge, 1 = level
16Mask1 = interruption masquée (i.e. désactivée) (0 sinon)
56 - 63DestinationSi mode = physical, local apic id de destination

Avec ces informations, nous allons pouvoir commencer à programmer l'IO APIC.

Nous allons :

  • déterminer quelle IRQ de l'IOAPIC correspond à quelle IRQ ISA
  • définir les vecteurs d'interruptions dans IOREDTBL de manière à avoir une correspondance avec le PIC (mais le code est assez générique pour qu'il soit adaptable, l'important est de garder une variable/structure permettant de faire la correspondance interruption/périphérique ; ici nous utiliserons la correspondance faîte avec les IRQ ISA une fois le PIC remappé)
  • définir la destination de chaque interruption comme l'ID du local APIC du processeur actuel (pour pouvoir recevoir les interruptions)

Pour déterminer la correspondance périphérique - irq IOAPIC, il faut de nouveau regarder la table ACPI MADT (cf. 'Détection'). Par défaut, on considère que les IRQ ISA sont mappées '1 to 1' avec les IRQ de l'IO APIC, sauf si la MADT dit le contraire. Une autre entrée de cette table nous sera utile :

Entrée IOAPIC Interrupt Source Override :

TypeNomDescription
uint8_ttypeType de l'entrée, ici = 2
uint8_tlengthLongueur de l'entrée, ici =
uint8_tbus= 0 pour IRQ ISA
uint8_tirqNuméro de l'IRQ ISA
uint32_tgsiNuméro de l'IRQ de l'IOAPIC
uint16_tflagsFlags ('polarity' et 'trigger mode')

Vous remarquerez que je n'ai pas parlé des IRQ provenant de périphériques PCI ; il est plus difficile d'obtenir les correspondances pour celles-ci et ce n'est pas le but de cet article (je rappelle que nous souhaitons juste adapter un code utilisant le PIC pour le remplacer par l'APIC).

On peut donc par exemple garder un tableau en parsant la MADT, permettant la correspondance IRQ ISA / IRQ IOAPIC, initialisé à l'identité. Les champs 'polarity' et 'trigger mode' devront être répliqués sur l'IO APIC ; leur valeur par défaut pour les IRQ ISA doivent être 'active high' et 'edge'. Nous allons donc également garder un tableau avec leur état.

uint32_t ioapic_irq[16] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
uint8_t polarity_irq[16] = {0}; //0 : active high
uint8_t trigger_mode_irq[16] = {0}; //0 : edge

void acpi_madt_parse(struct MADT* madt)
{
  //calculer la taille des entrées de la MADT en fonction de la taille de la table :
  //taille entrées = taille table - taille (header + local_apic_address + flags)
  size_t entries_size = madt->header.length - sizeof(madt->header) - sizeof(uint32_t)*2;
	
  uint8_t* entry = madt->entries;
  //tant que l'on ne dépasse pas entries + entries_size
  while((entry - madt->entries) < entries_size)
  {
    uint8_t entry_type = *entry;
    switch(entry_type)
    {
      case 0:
        //traiter l'entry de type local APIC
        break;
      case 1:
        //traiter l'entry de type IO APIC
        break;
      case 2:
        //ici, on fait la correspondance ISA-IOAPIC
        struct INTERRUPT_SOURCE_OVERRIDE* ent = (struct INTERRUPT_SOURCE_OVERRIDE*) entry;
				
        //si le bus n'est pas à 0, ce n'est pas une IRQ ISA : on ignore
        if(ent->bus != 0) break;
				
        //on fait la correspondance dans le tableau
        ioapic_irq[ent->irq] = ent->gsi;
				
        //on récupère trigger mode : bits 2-4 de flags
        //01b = edge, 11b = level, 00b = default (default = edge pour ISA)
        if(((ent->flags >> 2) & 0x3) == 0b11)
          trigger_mode_irq[ent->irq] = 1; //1 : level triggered
				
        //on récupère polarity : bits 0-2 de flags
        //01b = active high, 11b = active low, 00b = default
        //default (pour ISA) dépends du trigger mode : active low pour trigger, active high pour edge
        if(((ent->flags & 0x3) == 0b11) || (trigger_mode_irq[ent->irq] && (ent->flags & 0x3) == 0b00))
          polarity_irq[ent->irq] = 1; //1 : active low
				
        break;
    }
		
    //aller à l'entrée suivante : entry[1] contient length, la longueur de l'entrée courante
    entry += entry[1];
  }
}

Maintenant que nous avons cette correspondance, nous pouvons initialiser l'IO APIC, en remplissant les registres :

//tableau de correspondance IRQ ISA - IRQ IOAPIC
extern uint8_t ioapic_irq[];
//tableaux des flags 'polarity' et 'trigger mode'
extern uint8_t polarity_irq[];
extern uint8_t trigger_mode_irq[];
//adresse (physique) du premier IOAPIC trouvé dans la MADT, défaut 0xFEC00000
extern uint32_t io_apic_address;
//adresse (virtuelle) du local APIC
extern uint8_t* apic; //défaut 0xFEE00000, si identity mapping

void io_apic_init()
{
  /* d'abord, il faut récupérer l'ID du local APIC actuel
   * on aurait pu le récupérer dans la MADT, mais sinon il suffit de 
   * lire le registre 'ID' du local APIC (Offset 0x20) 
   * l'id est alors présent dans les bits 24-31 du registre */
  uint8_t local_apic_id = (*((uint32_t volatile*) (apic + 0x20))) >> 24;
 
  //on considère ici avoir identity-mappé l'ioapic, ou ne pas avoir activé de pagination/mémoire virtuelle
  uint8_t volatile* ioapic = (uint8_t*) io_apic_address; 
	 
  /* ici, on considère que l'IO APIC contiendra bien les entrées 
     correspondantes à chaque IRQ. Un code plus précotionneux pourrait
     vérifier le 'nombre de l'entrée IRQ maximale' dans le registre IOAPICVER
     et ne pas continuer au delà... */
  //pour chaque IRQ ISA
  for(size_t i = 0; i < 16; i++)
  {
    //on mappe au vecteur d'interruption gsi + i, pour avoir un comportement
    //semblable à celui du pic remappé du point de vue des vecteurs d'interruption
    //du processeur
    uint32_t irq_entry_low = i | (polarity_irq[i] << 13) | (trigger_mode_irq[i] << 15);
    //apic id : bit 56 du registre 64-bits, i.e. bit 24 du registre 32-bits haut
    uint32_t irq_entry_high = local_apic_id << 24;
		
    /* on sélectionne et écrit dans IOREDTBL #(ioapic_irq[i]) */
    //on écrit l'index du registre IOREDTBL #(ioapic_irq[i]) bas dans le registre
    //de sélection IOREGSEL
    *((uint32_t volatile*) (ioapic + 0x0)) = 0x10 + ioapic_irq[i]*2;
		
    //on écrit le bas de l'entrée IRQ dans IOREDTBL sélectionné, en écrivant
    //dans le registre de données IOREGWIN
    *((uint32_t volatile*) (ioapic + 0x10)) = irq_entry_low;
		
    //on écrit l'index du registre IOREDTBL #(ioapic_irq[i]) haut dans le registre
    //de sélection IOREGSEL
    *((uint32_t volatile*) (ioapic + 0x0)) = 0x10 + ioapic_irq[i]*2 + 1;
		
    //on écrit le haut de l'entrée IRQ dans IOREDTBL sélectionné, en écrivant
    //dans le registre de données IOREGWIN
    *((uint32_t volatile*) (ioapic + 0x10)) = irq_entry_high;
  }
	 
  /* les IRQ sont mappées correctement, on peut maintenant activer les interruptions
   * en utilisant asm("sti"); */
}

EOI : End Of Interrupt

La seule chose qui reste à faire est de remplacer le code envoyant le signal 'EOI' à la fin des routines d'interruptions au PIC par celui envoyant EOI à l'APIC : il suffit d'écrire 0 dans le registre approprié.

*((uint32_t volatile*) (apic + 0xB0)) = 0; 

Et maintenant ?

Après avoir activé l'APIC et l'avoir configuré correctement, il est possible de l'utiliser pour envoyer des interruptions aux autres processeurs, et ainsi les initialiser puis y exécuter l'ordonnanceur, de manière à obtenir un ordonnancement multiprocesseur des processus. Suivre la procédure ci-dessus permet donc de préparer son noyau à une future adaptation pour le multiprocesseur, en plus de profiter des autres avantages de l'APIC.

Dans ce qui est exposé précedemment, beaucoup de choses sont simplifiées : pour apronfondir, je vous conseille de vous renseigner sur :

  • la manière de faire la correspondance entre périphérique PCI - IRQ de l'IOAPIC (il faut interpréter de l'AML dans les tables ACPI...)
  • les 'trigger mode' et 'polarity' : leur signification, ...
  • le destination mode : le mode 'logical', le 'cluster model' ; pour envoyer les interruptions de l'IO APIC vers plusieurs processeurs par exemple

x2APIC

L'APIC tel que nous l'avons programmé précedemment était en mode 'xAPIC' ; Intel a étendu l'APIC avec un nouveau mode, appelé 'x2APIC'. L'x2APIC permet de meilleures performances car il nécessite moins d'instructions à la programmation pour réaliser certaines opérations ; de plus, il permet en théorie d'utiliser beaucoup plus de processeurs. Plus de détails sont disponibles dans Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3, Section 10.12 : Extended XAPIC (x2APIC)

Sources, voir aussi

OSDev, notamment APIC, IOAPIC, APIC timer

De plus ce thread sur l'APIC et ce thread sur l'IO APIC sont très intéressants

Pour les systèmes multiprocesseurs : MADT, Multiprocessing, ACPI

Pour le PIC et les interruptions : PIC, Interrupts, PIT

Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3, Chapitre 10 : APIC (et Chapitre 6 : Interruptions et gestions des exceptions)

ACPI Specifications : les spécifications ACPI