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 là). 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 |
---|---|---|
IRQ0 | 0x08 | Horloge (PIT) |
IRQ1 | 0x09 | Clavier |
IRQ2 | 0x0A | Cascade PIC esclave |
IRQ3 | 0x0B | Port série 2 (COM2) |
IRQ4 | 0x0C | Port série 1 (COM1) |
IRQ5 | 0x0D | Port parallèle 2 (LPT2) |
IRQ6 | 0x0E | Lecteur de disquettes |
IRQ7 | 0x0F | Port parallèle 1 (LPT1) (+ 'spurious irqs') |
IRQ8 / 0 | 0x70 | Horloge (CMOS RTC) |
IRQ9 / 1 | 0x71 | Libre |
IRQ10 / 2 | 0x72 | Libre |
IRQ 11 / 3 | 0x73 | Libre |
IRQ 12 / 4 | 0x74 | Souris (PS/2) |
IRQ 13 / 5 | 0x75 | FPU |
IRQ 14 / 6 | 0x76 | Primary ATA |
IRQ 15 / 7 | 0x77 | Secondary 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 0xFEE00000 | Description |
---|---|
0x20 | ID register |
0x30 | Version register |
0x80 | Task Priority Register |
0x90 | Arbitration Priority Register |
0xA0 | Processor Priority Register |
0xB0 | EOI (End Of Interrupt) register |
0xF0 | Spurious interrupt vector register |
0x100 - 0x170 | In-Service Registers (8 registres de 32 bits, 256 bits total) |
0x180 - 0x1F0 | Trigger Mode Registers (8 registres de 32 bits, 256 bits total) |
0x200 - 0x270 | Interrupt Request Registers (8 registres de 32 bits, 256 bits total) |
0x280 | Error Status register |
0x300 - 0x310 | Interrupt Command Register (2 registres de 32 bits, 64 bits total) |
0x320 | LVT Timer Register |
0x330 | LVT Thermal Sensor Register |
0x340 | LVT Performance Monitoring ... Register |
0x350 | LVT LINT0 Register |
0x360 | LVT LINT1 Register |
0x370 | LVT Error Register |
0x380 | Initial Count Register (Timer) |
0x390 | Current Count Register (Timer) |
0x3E0 | Divide 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 :
Type | Nom | Description |
---|---|---|
struct SYSTEM_DESCRIPTION_TABLE_HEADER | header | Header ACPI, de signature 'APIC' |
uint32_t | local_apic_address | Adresse physique du local APIC (devrait être 0xFEE00000) |
uint32_t | flags | Flags |
? | 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' :
Type | Nom | Description |
---|---|---|
uint8_t | type | Type de l'entrée, ici Local APIC = 0 |
uint8_t | length | Longueur de l'entrée, ici = 8 |
uint8_t | acpi_processor_id | ID du processeur associé au local APIC |
uint8_t | apic_id | ID du local APIC |
uint32_t | flags | Flags ; 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' :
Type | Nom | Description |
---|---|---|
uint8_t | type | Type de l'entrée, ici IO APIC = 1 |
uint8_t | length | Longueur de l'entrée, ici = 12 |
uint8_t | io_apic_id | ID de l'IO APIC |
uint8_t | reserved | Padding, inutile |
uint32_t | io_apic_address | Adresse physique de l'IO APIC |
uint32_t | gsi_base | IRQ 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 :
Bits | Description |
---|---|
0 - 7 | Vecteur d'interruption |
12 | 'Delivery status' : 0 = idle,1 = 'sent pending' (interruption en attente de traitement) |
16 | Masque : 0 = interruption non masquée, 1 = interruption masquée (n'aura pas lieu) |
17-18 | Mode (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) | Nom | Description |
---|---|---|
0x0 | IOREGSEL | Registre de sélection |
0x10 | IOREGWIN | Registre 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 :
ID | Nom | Description |
---|---|---|
0 | IOAPICID | Contient l'ID de l'IO APIC (bits 24-27) |
1 | IOAPICVER | Version (bits 0-8) et 'nombre de l'entrée IRQ maximale' (bits 16-23) |
2 | IOAPICARB | IO APIC 'arbitration id' |
0x10-0x11 | IOREDTBL #0 | Entrée pour l'IRQ 0 (2 registres de 32-bits, donc 64-bits) |
0x10 + n*2 - 0x10 + n*2 + 1 | IOREDTBL #n | Entrée pour l'IRQ n (64-bits) |
Les entrées IRQ sont de la forme suivante :
Bits | Nom | Description |
---|---|---|
0 - 7 | Vector | Vecteur d'interruption associé à l'entrée |
8 - 10 | Delivery Mode | Manière dont l'interruption sera envoyée au CPU |
11 | Destination Mode | 0 = physical, 1 = logical |
12 | Delivery Status | 1 : IRQ en cours de transfert vers le local APIC (0 sinon) |
13 | Pin Polarity | 0 = active high, 1 = active low |
15 | Trigger Mode | 0 = edge, 1 = level |
16 | Mask | 1 = interruption masquée (i.e. désactivée) (0 sinon) |
56 - 63 | Destination | Si 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 :
Type | Nom | Description |
---|---|---|
uint8_t | type | Type de l'entrée, ici = 2 |
uint8_t | length | Longueur de l'entrée, ici = |
uint8_t | bus | = 0 pour IRQ ISA |
uint8_t | irq | Numéro de l'IRQ ISA |
uint32_t | gsi | Numéro de l'IRQ de l'IOAPIC |
uint16_t | flags | Flags ('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