Cœurs de processeur ou qu'est-ce que SMP et que mange-t-il

Présentation


Bonjour, aujourd'hui, je voudrais aborder un sujet assez simple qui est presque inconnu des programmeurs ordinaires, mais chacun de vous l'a probablement utilisé.
Il s'agira du multitraitement symétrique (populairement - SMP) - l'architecture que l'on retrouve dans tous les systèmes d'exploitation multitâche, et bien sûr, en fait partie intégrante. Tout le monde sait que plus un processeur a de cœurs, plus le processeur sera puissant, oui, mais comment un système d'exploitation peut-il utiliser plusieurs cœurs en même temps? Certains programmeurs ne descendent pas à ce niveau d'abstraction - ils n'en ont tout simplement pas besoin, mais je pense que tout le monde sera intéressé par le fonctionnement de SMP.

Le multitâche et sa mise en œuvre


Ceux qui ont déjà étudié l'architecture informatique savent que le processeur lui-même n'est pas en mesure d'effectuer plusieurs tâches à la fois, le multitâche ne nous donne que le système d'exploitation, qui commute ces tâches. Il existe plusieurs types de multitâche, mais le plus adéquat, pratique et largement utilisé est l'éviction du multitâche (vous pouvez lire ses principaux aspects sur Wikipédia). Il est basé sur le fait que chaque processus (tâche) a sa propre priorité, ce qui affecte le temps processeur qui lui sera alloué. Chaque tâche reçoit une tranche de temps pendant laquelle le processus fait quelque chose; après l'expiration de la tranche de temps, le système d'exploitation transfère le contrôle à une autre tâche. La question se pose - comment répartir les ressources informatiques, telles que la mémoire, les périphériques, etc. entre les processus? Tout est très simple: Windows le fait lui-même, Linux utilise un système de sémaphore. Mais un noyau n'est pas sérieux, nous allons de l'avant.

Interruptions et PIC


Peut-être que pour certains, ce sera une nouvelle, pour certains non, mais l'architecture i386 (je vais parler de l'architecture x86, ARM ne compte pas, parce que je n'ai pas étudié cette architecture, et je ne l'ai jamais rencontrée) (même au niveau de l'écriture d'un service ou d'un programme résident)) utilise des interruptions (nous ne parlerons que des interruptions matérielles, IRQ) afin de notifier le système d'exploitation ou le programme d'un événement. Par exemple, il y a une interruption 0x8 (pour les modes protégés et longs, par exemple 0x20, selon la façon de configurer le PIC, plus à ce sujet plus tard), qui est appelée par PIT, qui, par exemple, peut générer des interruptions avec n'importe quelle fréquence nécessaire. Ensuite, le travail de l'OS pour la distribution des tranches de temps est réduit à 0, lorsqu'une interruption est appelée, le programme s'arrête et le contrôle est donné, par exemple, au noyau, qui à son tour enregistre les données du programme actuel (registres, drapeaux, etc.) et donne le contrôle au processus suivant .

Comme vous l'avez probablement compris, les interruptions sont des fonctions (ou procédures) qui sont appelées à un moment donné par l'équipement ou par le programme lui-même. Au total, le processeur prend en charge 16 interruptions sur deux PIC. Le processeur a des drapeaux, et l'un d'eux est le drapeau «I» - Interrupt Control. En définissant cet indicateur sur 0, le processeur ne provoquera aucune interruption matérielle. Mais, je tiens également à noter qu'il existe des dénommés NMI - Interruptions non masquables - les données d'interruption seront toujours appelées, même si le bit I est défini sur 0. En utilisant la programmation PIC, vous pouvez désactiver les données d'interruption, mais après être revenu d'une interruption avec IRET - ils ne seront à nouveau pas interdits. Je note que sous un programme régulier, vous ne pouvez pas suivre l'appel d'interruption - votre programme s'arrête et ne reprend qu'après un certain temps, votre programme ne le remarque même pas (oui, vous pouvez vérifier que l'interruption a été appelée - mais pourquoi?

PIC - Contrôleur d'interruption programmable

De Wiki:
En règle générale, il s'agit d'un appareil électronique, parfois fabriqué dans le cadre du processeur lui-même ou de puces complexes de sa trame, dont les entrées sont connectées électriquement aux sorties correspondantes de divers appareils. Le numéro d'entrée du contrôleur d'interruption est indiqué par «IRQ». Ce numéro doit être distingué de la priorité d'interruption, ainsi que du numéro d'entrée dans la table des vecteurs d'interruption (INT). Ainsi, par exemple, dans un PC IBM en mode de fonctionnement réel (MS-DOS fonctionne dans ce mode) du processeur, l'interruption du clavier standard utilise IRQ 1 et INT 9.

La plate-forme IBM PC d'origine utilise un schéma d'interruption très simple. Le contrôleur d'interruption est un simple compteur qui itère séquentiellement sur les signaux de différents appareils ou est réinitialisé au début lorsqu'une nouvelle interruption est trouvée. Dans le premier cas, les appareils ont une priorité égale; dans le second, les appareils avec un numéro de série inférieur (ou supérieur dans le comptage) ont une priorité plus élevée.

Comme vous le comprenez, il s'agit d'un circuit électronique qui permet aux appareils d'envoyer des demandes d'interruption, généralement il y en a exactement 2.

Passons maintenant au sujet de l'article.

SMP


Pour mettre en œuvre cette norme, de nouveaux schémas ont commencé à être mis sur les cartes mères: APIC et ACPI. Parlons du premier.

APIC - Advanced Programmable Interrupt Controller, une version améliorée de PIC. Il est utilisé dans les systèmes multiprocesseurs et fait partie intégrante de tous les derniers processeurs Intel (et compatibles). APIC est utilisé pour le transfert d'interruption complexe et pour l'envoi d'interruptions entre processeurs. Ces choses n'étaient pas possibles en utilisant l'ancienne spécification PIC.

APIC local et IO APIC


Dans un système basé sur APIC, chaque processeur se compose d'un «cœur» et d'un «APIC local». L'APIC local est responsable de la gestion de la configuration d'interruption spécifique au processeur. Entre autres, il contient une table vectorielle locale (LVT), qui traduit les événements, tels que "l'horloge interne" et d'autres sources d'interruption "locales", en un vecteur d'interruption (par exemple, le contact LocalINT1 peut déclencher une exception NMI tout en conservant " 2 ”à l'entrée LVT correspondante).

Pour plus d'informations sur l'APIC local, consultez le "Guide de programmation système" des processeurs Intel modernes.

De plus, il existe un APIC IO (par exemple, Intel 82093AA), qui fait partie du chipset et fournit un contrôle d'interruption multiprocesseur, y compris une distribution symétrique statique et dynamique des interruptions pour tous les processeurs. Sur les systèmes avec plusieurs sous-systèmes d'E / S, chaque sous-système peut avoir son propre ensemble d'interruptions.

Chaque broche d'interruption est programmée individuellement comme déclenchée par front ou par niveau. Le vecteur d'interruption et les informations de commande d'interruption peuvent être spécifiés pour chaque interruption. Le schéma d'accès au registre indirect optimise l'espace mémoire nécessaire pour accéder aux registres d'E / S internes APIC. Pour augmenter la flexibilité du système lors de l'allocation d'espace mémoire, les deux registres d'E / S APIC sont déplaçables, mais la valeur par défaut est 0xFEC00000.

Initialisation d'un APIC «local»


L'APIC local est activé au démarrage et peut être désactivé en réinitialisant le bit 11 IA32_APIC_BASE (MSR) (cela ne fonctionne qu'avec les processeurs avec une famille> 5, car Pentium n'a pas un tel MSR), puis le processeur reçoit ses interruptions directement du PIC 8259 compatible . Cependant, le guide de développement logiciel d'Intel indique qu'après avoir désactivé l'APIC local via IA32_APIC_BASE, vous ne pourrez pas l'activer tant qu'il n'aura pas été complètement réinitialisé. L'APO IO peut également être configuré pour fonctionner en mode hérité afin d'émuler un périphérique 8259.

Les APIC locaux sont mappés à la page physique FEE00xxx (voir Tableau 8-1 Intel P4 SPG). Cette adresse est la même pour chaque APIC local qui existe dans la configuration, ce qui signifie que vous pouvez accéder directement aux registres du noyau APIC local dans lequel votre code s'exécute actuellement. Notez qu'il existe un MSR qui définit la base APIC réelle (disponible uniquement pour les processeurs avec une famille> 5). MADT contient une base APIC locale, et sur les systèmes 64 bits, il peut également contenir un champ spécifiant une redéfinition 64 bits de l'adresse de base, que vous devez utiliser à la place. Vous ne pouvez quitter la base APIC locale que là où vous la trouvez, ou la déplacer où vous le souhaitez. Remarque: je ne pense pas que vous puissiez le déplacer plus loin que le 4ème Go de RAM.

Pour permettre à l'APIC local de recevoir des interruptions, vous devez configurer le registre vectoriel d'interruption parasite. La valeur correcte pour ce champ est le numéro IRQ que vous souhaitez mapper sur les fausses interruptions avec les 8 bits inférieurs et le 8e bit défini sur 1 pour activer réellement APIC (pour plus de détails, voir la spécification). Vous devez sélectionner un numéro d'interruption dont les 4 bits inférieurs sont définis; Le moyen le plus simple consiste à utiliser 0xFF. Ceci est important pour certains processeurs plus anciens, car pour ces valeurs, les 4 bits inférieurs doivent être définis sur 1.

Désactivez correctement le PIC 8259. C'est presque aussi important que de configurer APIC. Vous effectuez cette opération en deux étapes: masquage de toutes les interruptions et réaffectation de l'IRQ. Déguiser toutes les interruptions les désactive dans le PIC. Le remappage des interruptions est ce que vous avez probablement déjà fait lorsque vous avez utilisé PIC: vous voulez que les demandes d'interruption commencent à 32 au lieu de 0 pour éviter les conflits avec les exceptions (en mode processeur protégé et long (Long), car Les 32 premières interruptions sont des exceptions). Vous devez ensuite éviter d'utiliser ces vecteurs d'interruption à d'autres fins. Ceci est nécessaire car, malgré le fait que vous ayez masqué toutes les interruptions PIC, il pourrait toujours générer de fausses interruptions, qui seraient alors incorrectement traitées comme exceptions dans votre noyau.
Passons à SMP.

Multitâche symétrique: initialisation


La séquence de démarrage est différente pour différents CPU. Le Guide du programmeur Intel (Section 7.5.4) contient un protocole d'initialisation pour les processeurs Intel Xeon et ne couvre pas les processeurs plus anciens. Pour un algorithme général «tous les types de processeur», voir Spécifications multiprocesseurs Intel.

Pour 80486 (avec APIC 8249DX externe), vous devez utiliser IPIT INIT suivi de l'IPI «INIT level de-assert» sans SIPI. Cela signifie que vous ne pouvez pas leur dire où commencer à exécuter votre code (la partie vectorielle de SIPI), et ils commencent toujours à exécuter le code BIOS. Dans ce cas, vous définissez la valeur de réinitialisation du BIOS CMOS sur «démarrage à chaud avec saut loin» (c'est-à-dire, définissez la position CMOS 0x0F sur 10) afin que le BIOS exécute jmp loin ~ [0: 0x0469], puis définissez le segment et le décalage Points d'entrée AP à 0x0469.

L'IPI «INIT level de-assert» n'est pas pris en charge sur les nouveaux processeurs (Pentium 4 et Intel Xeon), et AFAIK est complètement ignoré sur ces processeurs.

Pour les nouveaux processeurs (P6, Pentium 4), un seul SIPI suffit, mais je ne sais pas si les anciens processeurs Intel (Pentium) ou les processeurs d'autres fabricants ont besoin d'un second SIPI. Il est également possible qu'un deuxième SIPI existe en cas d'échec de livraison du premier SIPI (bruit de bus, etc.).

Habituellement, j'envoie le premier SIPI, puis j'attends de voir si l'AP augmente le nombre de processeurs en cours d'exécution. S'il n'augmente pas ce compteur en quelques millisecondes, j'enverrai un deuxième SIPI. Ceci est différent de l'algorithme général d'Intel (qui a un retard de 200 microsecondes entre SIPI), mais il n'est pas si simple de trouver une source de temps qui puisse mesurer avec précision le retard de 200 microsecondes lors d'un démarrage précoce. J'ai également constaté que sur du matériel réel, si le délai entre SIPI est trop long (et que vous n'utilisez pas ma méthode), l'AP principal peut exécuter deux fois le code de démarrage de l'AP pour l'OS (ce qui, dans mon cas, amènera l'OS à penser que nous avons deux fois plus de processeurs que nous le sommes actuellement).

Vous pouvez diffuser ces signaux sur le bus pour démarrer chaque appareil présent. Cependant, vous pouvez également activer des processeurs spécialement désactivés (car ils étaient "défectueux").

Recherche d'informations à l'aide de la table MT


Certaines informations (qui peuvent ne pas être disponibles sur les machines plus récentes) destinées au multitraitement. Vous devez d'abord trouver la structure du pointeur flottant MP. Il est aligné sur une limite de 16 octets et contient une signature au début de "_MP_" ou 0x5F504D5F. Le système d'exploitation doit regarder dans EBDA, l'espace ROM du BIOS et dans le dernier kilo-octet de «mémoire de base»; la taille de la mémoire de base est spécifiée dans une valeur de 2 octets de 0x413 en kilo-octets, moins 1 Ko. Voici à quoi ressemble la structure:

struct mp_floating_pointer_structure { char signature[4]; uint32_t configuration_table; uint8_t length; // In 16 bytes (eg 1 = 16 bytes, 2 = 32 bytes) uint8_t mp_specification_revision; uint8_t checksum; // This value should make all bytes in the table equal 0 when added together uint8_t default_configuration; // If this is not zero then configuration_table should be // ignored and a default configuration should be loaded instead uint32_t features; // If bit 7 is then the IMCR is present and PIC mode is being used, otherwise // virtual wire mode is; all other bits are reserved } 

Voici à quoi ressemble la table de configuration sur laquelle pointe la structure flottante du pointeur:

 struct mp_configuration_table { char signature[4]; // "PCMP" uint16_t length; uint8_t mp_specification_revision; uint8_t checksum; // Again, the byte should be all bytes in the table add up to 0 char oem_id[8]; char product_id[12]; uint32_t oem_table; uint16_t oem_table_size; uint16_t entry_count; // This value represents how many entries are following this table uint32_t lapic_address; // This is the memory mapped address of the local APICs uint16_t extended_table_length; uint8_t extended_table_checksum; uint8_t reserved; } 

Après la table de configuration se trouvent les entrées entry_count, qui contiennent plus d'informations sur le système, suivies d'une table étendue. Les entrées sont soit 20 octets pour représenter le processeur, soit 8 octets pour autre chose. Voici à quoi ressemblent le processeur APIC et les enregistrements d'E / S.

 struct entry_processor { uint8_t type; // Always 0 uint8_t local_apic_id; uint8_t local_apic_version; uint8_t flags; // If bit 0 is clear then the processor must be ignored // If bit 1 is set then the processor is the bootstrap processor uint32_t signature; uint32_t feature_flags; uint64_t reserved; } 

Voici l'entrée IO APIC.

 struct entry_io_apic { uint8_t type; // Always 2 uint8_t id; uint8_t version; uint8_t flags; // If bit 0 is set then the entry should be ignored uint32_t address; // The memory mapped address of the IO APIC is memory } 

Recherche d'informations avec APIC


Vous pouvez trouver la table MADT (APIC) dans ACPI. Le tableau répertorie les APIC locaux, dont le nombre doit correspondre au nombre de cœurs sur votre processeur. Les détails de ce tableau ne sont pas ici, mais vous pouvez les trouver sur Internet.

Lancer AP


Après avoir collecté les informations, vous devez désactiver le PIC et vous préparer aux E / S APIC. Vous devez également configurer le BSP de l'APIC local. Ensuite, démarrez l'AP en utilisant SIPI.

Code de lancement des noyaux:

Je note que le vecteur que vous spécifiez au démarrage indique l'adresse de départ: vecteur 0x8 - adresse 0x8000, vecteur 0x9 - adresse 0x9000, etc.

 // ------------------------------------------------------------------------------------------------ static u32 LocalApicIn(uint reg) { return MmioRead32(*g_localApicAddr + reg); } // ------------------------------------------------------------------------------------------------ static void LocalApicOut(uint reg, u32 data) { MmioWrite32(*g_localApicAddr + reg, data); } // ------------------------------------------------------------------------------------------------ void LocalApicInit() { // Clear task priority to enable all interrupts LocalApicOut(LAPIC_TPR, 0); // Logical Destination Mode LocalApicOut(LAPIC_DFR, 0xffffffff); // Flat mode LocalApicOut(LAPIC_LDR, 0x01000000); // All cpus use logical id 1 // Configure Spurious Interrupt Vector Register LocalApicOut(LAPIC_SVR, 0x100 | 0xff); } // ------------------------------------------------------------------------------------------------ uint LocalApicGetId() { return LocalApicIn(LAPIC_ID) >> 24; } // ------------------------------------------------------------------------------------------------ void LocalApicSendInit(uint apic_id) { LocalApicOut(LAPIC_ICRHI, apic_id << ICR_DESTINATION_SHIFT); LocalApicOut(LAPIC_ICRLO, ICR_INIT | ICR_PHYSICAL | ICR_ASSERT | ICR_EDGE | ICR_NO_SHORTHAND); while (LocalApicIn(LAPIC_ICRLO) & ICR_SEND_PENDING) ; } // ------------------------------------------------------------------------------------------------ void LocalApicSendStartup(uint apic_id, uint vector) { LocalApicOut(LAPIC_ICRHI, apic_id << ICR_DESTINATION_SHIFT); LocalApicOut(LAPIC_ICRLO, vector | ICR_STARTUP | ICR_PHYSICAL | ICR_ASSERT | ICR_EDGE | ICR_NO_SHORTHAND); while (LocalApicIn(LAPIC_ICRLO) & ICR_SEND_PENDING) ; } void SmpInit() { kprintf("Waking up all CPUs\n"); *g_activeCpuCount = 1; uint localId = LocalApicGetId(); // Send Init to all cpus except self for (uint i = 0; i < g_acpiCpuCount; ++i) { uint apicId = g_acpiCpuIds[i]; if (apicId != localId) { LocalApicSendInit(apicId); } } // wait PitWait(200); // Send Startup to all cpus except self for (uint i = 0; i < g_acpiCpuCount; ++i) { uint apicId = g_acpiCpuIds[i]; if (apicId != localId) LocalApicSendStartup(apicId, 0x8); } // Wait for all cpus to be active PitWait(10); while (*g_activeCpuCount != g_acpiCpuCount) { kprintf("Waiting... %d\n", *g_activeCpuCount); PitWait(10); } kprintf("All CPUs activated\n"); } 

 [org 0x8000] AP: jmp short bsp ;     -   BSP xor ax,ax mov ss,ax mov sp, 0x7c00 xor ax,ax mov ds,ax ; Mark CPU as active lock inc byte [ds:g_activeCpuCount] ;   ,   jmp zop bsp: xor ax,ax mov ds,ax mov dword[ds:g_activeCpuCount],0 mov dword[ds:g_activeCpuCount],0 mov word [ds:0x8000], 0x9090 ;  JMP   2 NOP' ;   ,   

Maintenant, comme vous le comprenez, pour que le système d'exploitation utilise plusieurs cœurs, vous devez configurer la pile pour chaque cœur, chaque cœur, ses interruptions, etc., mais la chose la plus importante est que lors de l'utilisation du multitraitement symétrique, toutes les ressources des cœurs sont les mêmes: une mémoire, un PCI, etc., et le système d'exploitation ne peut que paralléliser les tâches entre les cœurs.

J'espère que l'article n'est pas assez ennuyeux et assez informatif. La prochaine fois, je pense que nous pourrons parler de la façon dont ils dessinaient sur l'écran (et maintenant ils dessinent), sans utiliser de shaders et de cartes vidéo sympas.

Bonne chance

Source: https://habr.com/ru/post/fr426497/


All Articles