Einführung
Guten Tag, heute möchte ich ein ziemlich einfaches Thema ansprechen, das gewöhnlichen Programmierern fast unbekannt ist, aber jeder von Ihnen hat es höchstwahrscheinlich verwendet.
Es geht um symmetrisches Multiprocessing (im Volksmund - SMP) - die Architektur, die in allen Multitasking-Betriebssystemen zu finden ist und natürlich ein wesentlicher Bestandteil davon ist. Jeder weiß, dass der Prozessor umso leistungsfähiger ist, je mehr Kerne ein Prozessor hat. Ja, aber wie kann ein Betriebssystem mehrere Kerne gleichzeitig verwenden? Einige Programmierer gehen nicht auf diese Abstraktionsebene - sie brauchen sie einfach nicht, aber ich denke, jeder wird daran interessiert sein, wie SMP funktioniert.
Multitasking und seine Implementierung
Diejenigen, die jemals Computerarchitektur studiert haben, wissen, dass der Prozessor selbst nicht mehrere Aufgaben gleichzeitig ausführen kann. Multitasking gibt uns nur das Betriebssystem, das diese Aufgaben umschaltet. Es gibt verschiedene Arten von Multitasking, aber das am besten geeignete, bequemste und am weitesten verbreitete ist das Verdrängen von Multitasking (Sie können die Hauptaspekte auf Wikipedia lesen). Es basiert auf der Tatsache, dass jeder Prozess (Aufgabe) seine eigene Priorität hat, was sich darauf auswirkt, wie viel Prozessorzeit ihm zugewiesen wird. Jede Aufgabe erhält eine Zeitscheibe, in der der Prozess etwas unternimmt. Nach Ablauf der Zeitscheibe überträgt das Betriebssystem die Kontrolle auf eine andere Aufgabe. Es stellt sich die Frage, wie Computerressourcen wie Speicher, Geräte usw. verteilt werden sollen. zwischen Prozessen? Alles ist sehr einfach: Windows macht es selbst, Linux verwendet ein Semaphorsystem. Aber ein Kern ist nicht ernst, wir gehen weiter.
Interrupts und PIC
Vielleicht wird sich dies für einige als Neuigkeit herausstellen, für andere nicht, aber für die i386-Architektur (ich werde über die x86-Architektur sprechen, ARM zählt nicht, weil ich diese Architektur nicht studiert habe und nie darauf gestoßen bin (selbst auf der Ebene des Schreibens eines Dienstes oder eines residenten Programms)) verwendet Interrupts (wir werden nur über Hardware-Unterbrechungen, IRQ sprechen), um das Betriebssystem oder Programm über ein Ereignis zu benachrichtigen. Zum Beispiel gibt es einen Interrupt 0x8 (für geschützte und lange Modi, zum Beispiel 0x20, abhängig davon, wie der PIC konfiguriert wird, dazu später mehr), der von PIT aufgerufen wird und beispielsweise Interrupts mit jeder erforderlichen Frequenz erzeugen kann. Dann wird die Arbeit des Betriebssystems für die Verteilung von Zeitquanten auf 0 reduziert, wenn ein Interrupt aufgerufen wird, das Programm stoppt und die Steuerung beispielsweise dem Kernel gegeben wird, der wiederum die aktuellen Programmdaten (Register, Flags usw.) speichert und die Steuerung für den nächsten Prozess gibt .
Wie Sie wahrscheinlich verstanden haben, sind Interrupts Funktionen (oder Prozeduren), die zu einem bestimmten Zeitpunkt vom Gerät oder vom Programm selbst aufgerufen werden. Insgesamt unterstützt der Prozessor 16 Interrupts auf zwei PICs. Der Prozessor verfügt über Flags, und eines davon ist das I-Flag - Interrupt Control. Wenn Sie dieses Flag auf 0 setzen, verursacht der Prozessor keine Hardwareunterbrechungen. Ich möchte aber auch darauf hinweisen, dass es sogenannte NMIs - Non-Maskable Interrupts - gibt. Interrupt-Daten werden weiterhin aufgerufen, auch wenn Bit I auf 0 gesetzt ist. Mit der PIC-Programmierung können Sie Interrupt-Daten deaktivieren, jedoch nach Rückkehr von einem Interrupt mit IRET - sie werden wieder nicht verboten. Ich stelle fest, dass Sie unter einem regulären Programm den Interrupt-Aufruf nicht verfolgen können - Ihr Programm stoppt und wird erst nach einer Weile fortgesetzt, Ihr Programm bemerkt es nicht einmal (ja, Sie können überprüfen, ob der Interrupt aufgerufen wurde - aber warum?
PIC - Programmable Interrupt Controller
Aus dem Wiki:
In der Regel handelt es sich um ein elektronisches Gerät, das manchmal als Teil des Prozessors selbst oder als komplexe Chips seines Rahmens hergestellt wird und dessen Eingänge elektrisch mit den entsprechenden Ausgängen verschiedener Geräte verbunden sind. Die Eingangsnummer des Interrupt-Controllers wird durch „IRQ“ angezeigt. Diese Nummer sollte sowohl von der Interrupt-Priorität als auch von der Eintragsnummer in der Interrupt-Vektortabelle (INT) unterschieden werden. So werden beispielsweise in einem IBM PC im Real-Modus (MS-DOS wird in diesem Modus ausgeführt) des Prozessors für die Unterbrechung der Standardtastatur IRQ 1 und INT 9 verwendet.
Die ursprüngliche IBM PC-Plattform verwendet ein sehr einfaches Interrupt-Schema. Der Interrupt-Controller ist ein einfacher Zähler, der entweder nacheinander die Signale verschiedener Geräte durchläuft oder auf den Anfang zurückgesetzt wird, wenn ein neuer Interrupt gefunden wird. Im ersten Fall haben Geräte die gleiche Priorität, im zweiten Fall haben Geräte mit einer niedrigeren (oder höheren in der Zählung) Seriennummer eine höhere Priorität.
Wie Sie verstehen, handelt es sich hierbei um eine elektronische Schaltung, mit der Geräte Interrupt-Anforderungen senden können. In der Regel gibt es genau zwei davon.
Kommen wir nun zum Thema des Artikels.
SMP
Um diesen Standard zu implementieren, wurden neue Schemata auf Motherboards installiert: APIC und ACPI. Sprechen wir über den ersten.
APIC - Advanced Programmable Interrupt Controller, eine verbesserte Version von PIC. Es wird in Multiprozessorsystemen verwendet und ist ein wesentlicher Bestandteil aller neuesten Intel-Prozessoren (und kompatibel). APIC wird für die komplexe Interrupt-Weiterleitung und zum Senden von Interrupts zwischen Prozessoren verwendet. Diese Dinge waren mit der älteren PIC-Spezifikation nicht möglich.
Lokaler APIC und IO APIC
In einem APIC-basierten System besteht jeder Prozessor aus einem "Kern" und einem "lokalen APIC". Der lokale APIC ist für die Verarbeitung der prozessorspezifischen Interrupt-Konfiguration verantwortlich. Unter anderem enthält es eine lokale Vektortabelle (LVT), die Ereignisse wie die "interne Uhr" und andere "lokale" Interruptquellen in einen Interruptvektor übersetzt (z. B. kann der Kontakt LocalINT1 eine NMI-Ausnahme auslösen, während " 2 ”zum entsprechenden LVT-Eingang).
Weitere Informationen zum lokalen APIC finden Sie im "System Programming Guide" moderner Intel-Prozessoren.
Darüber hinaus gibt es ein APIC IO (z. B. Intel 82093AA), das Teil des Chipsatzes ist und eine Interrupt-Steuerung für mehrere Prozessoren bietet, einschließlich statischer und dynamischer symmetrischer Verteilung von Interrupts für alle Prozessoren. Auf Systemen mit mehreren E / A-Subsystemen kann jedes Subsystem seinen eigenen Satz von Interrupts haben.
Jeder Interrupt-Pin wird einzeln als flanken- oder pegelgetriggert programmiert. Der Interrupt-Vektor und die Interrupt-Steuerinformationen können für jeden Interrupt angegeben werden. Das indirekte Registerzugriffsschema optimiert den Speicherplatz, der für den Zugriff auf die internen APIC-E / A-Register benötigt wird. Um die Systemflexibilität bei der Zuweisung von Speicherplatz zu erhöhen, können die beiden APIC-E / A-Register verschoben werden. Der Standardwert ist jedoch 0xFEC00000.
Initialisierung eines "lokalen" APIC
Der lokale APIC wird beim Booten aktiviert und kann durch Zurücksetzen von Bit 11 IA32_APIC_BASE (MSR) deaktiviert werden (dies funktioniert nur bei Prozessoren mit einer Familie> 5, da Pentium keinen solchen MSR hat). Dann empfängt der Prozessor seine Interrupts direkt vom kompatiblen 8259 PIC . Im Intel-Handbuch zur Softwareentwicklung heißt es jedoch, dass Sie den lokalen APIC nach dem Deaktivieren über IA32_APIC_BASE erst einschalten können, wenn er vollständig zurückgesetzt wurde. Das APO IO kann auch so konfiguriert werden, dass es im Legacy-Modus arbeitet, sodass es ein 8259-Gerät emuliert.
Lokale APICs werden der physischen Seite FEE00xxx zugeordnet (siehe Tabelle 8-1 Intel P4 SPG). Diese Adresse ist für jeden lokalen APIC, der in der Konfiguration vorhanden ist, gleich. Dies bedeutet, dass Sie direkt auf die Register des lokalen APIC-Kernels zugreifen können, in dem Ihr Code gerade ausgeführt wird. Beachten Sie, dass es einen MSR gibt, der die tatsächliche APIC-Basis definiert (nur für Prozessoren mit einer Familie> 5 verfügbar). MADT enthält eine lokale APIC-Basis, und auf 64-Bit-Systemen kann es auch ein Feld enthalten, das eine 64-Bit-Neudefinition der Basisadresse angibt, die Sie stattdessen verwenden sollten. Sie können die lokale APIC-Basis nur dort lassen, wo Sie sie finden, oder sie verschieben, wo immer Sie möchten. Hinweis: Ich glaube nicht, dass Sie es weiter als bis zu 4 GB RAM verschieben können.
Damit der lokale APIC Interrupts empfangen kann, müssen Sie das Spurious Interrupt Vector Register konfigurieren. Der richtige Wert für dieses Feld ist die IRQ-Nummer, die Sie falschen Interrupts mit den unteren 8 Bits zuordnen möchten, und das 8. Bit, das auf 1 gesetzt ist, um APIC tatsächlich zu aktivieren (Einzelheiten finden Sie in der Spezifikation). Sie müssen eine Interrupt-Nummer auswählen, für die die unteren 4 Bits gesetzt sind. Am einfachsten ist es, 0xFF zu verwenden. Dies ist für einige ältere Prozessoren wichtig, da für diese Werte die unteren 4 Bits auf 1 gesetzt werden müssen.
Deaktivieren Sie den 8259 PIC korrekt. Dies ist fast so wichtig wie die Konfiguration von APIC. Sie tun dies in zwei Schritten: Maskieren aller Interrupts und Neuzuweisen des IRQ. Durch das Verschleiern aller Interrupts werden diese im PIC deaktiviert. Das erneute Zuordnen von Interrupts haben Sie wahrscheinlich bereits bei der Verwendung von PIC durchgeführt: Sie möchten, dass Interrupt-Anforderungen bei 32 statt bei 0 beginnen, um Konflikte mit Ausnahmen zu vermeiden (im geschützten und langen (langen) Prozessormodus, weil Die ersten 32 Interrupts sind Ausnahmen. Dann sollten Sie vermeiden, diese Interruptvektoren für andere Zwecke zu verwenden. Dies ist erforderlich, da trotz der Tatsache, dass Sie alle PIC-Interrupts maskiert haben, immer noch falsche Interrupts ausgelöst werden können, die dann fälschlicherweise als Ausnahmen in Ihrem Kernel verarbeitet werden.
Fahren wir mit SMP fort.
Symmetrisches Multitasking: Initialisierung
Die Startreihenfolge ist für verschiedene CPUs unterschiedlich. Das Intel Programmer's Guide (Abschnitt 7.5.4) enthält ein Initialisierungsprotokoll für Intel Xeon-Prozessoren und gilt nicht für ältere Prozessoren. Einen allgemeinen Algorithmus für alle Prozessortypen finden Sie unter Intel Multiprocessor Specification.
Für 80486 (mit externem APIC 8249DX) müssen Sie IPIT INIT verwenden, gefolgt von der IPI-Deaktivierung der INIT-Ebene ohne SIPI. Dies bedeutet, dass Sie ihnen nicht sagen können, wo sie mit der Ausführung Ihres Codes (dem Vektorteil von SIPI) beginnen sollen, und sie beginnen immer mit der Ausführung von BIOS-Code. In diesem Fall setzen Sie den CMOS-BIOS-Rücksetzwert auf „Warmstart mit Weitsprung“ (dh setzen Sie die CMOS-Position 0x0F auf 10), damit das BIOS jmp far ~ [0: 0x0469] ausführt, und setzen dann das Segment und den Offset AP-Einstiegspunkte bei 0x0469.
Das IPI "INIT Level De-Assert" wird auf neuen Prozessoren (Pentium 4 und Intel Xeon) nicht unterstützt, und AFAIK wird auf diesen Prozessoren vollständig ignoriert.
Für neuere Prozessoren (P6, Pentium 4) reicht ein SIPI aus, aber ich bin mir nicht sicher, ob ältere Intel-Prozessoren (Pentium) oder Prozessoren anderer Hersteller einen zweiten SIPI benötigen. Es ist auch möglich, dass ein zweiter SIPI im Falle eines Zustellungsfehlers für den ersten SIPI (Busrauschen usw.) vorhanden ist.
Normalerweise sende ich das erste SIPI und warte dann ab, ob der AP die Anzahl der laufenden Prozessoren erhöht. Wenn dieser Zähler nicht innerhalb weniger Millisekunden erhöht wird, sende ich einen zweiten SIPI. Dies unterscheidet sich vom allgemeinen Intel-Algorithmus (der eine Verzögerung von 200 Mikrosekunden zwischen SIPI aufweist), aber es ist nicht so einfach, eine Zeitquelle zu finden, die die Verzögerung von 200 Mikrosekunden während eines frühen Startvorgangs genau messen kann. Ich habe auch festgestellt, dass auf realer Hardware, wenn die Verzögerung zwischen SIPI zu lang ist (und Sie meine Methode nicht verwenden), der Haupt-AP den frühen AP-Startcode für das Betriebssystem zweimal ausführen kann (was in meinem Fall dazu führt, dass das Betriebssystem dies denkt Wir haben doppelt so viele Prozessoren wie wir tatsächlich sind.
Sie können diese Signale auf dem Bus senden, um jedes vorhandene Gerät zu starten. Sie können jedoch auch Prozessoren aktivieren, die speziell deaktiviert wurden (weil sie "defekt" waren).
Suchen Sie nach Informationen mithilfe der MT-Tabelle
Einige Informationen (die auf neueren Computern möglicherweise nicht verfügbar sind) sind für die Mehrfachverarbeitung vorgesehen. Zuerst müssen Sie die MP-Floating-Pointer-Struktur finden. Es ist an einer 16-Byte-Grenze ausgerichtet und enthält eine Signatur am Anfang von "_MP_" oder 0x5F504D5F. Das Betriebssystem sollte in EBDA, im BIOS-ROM-Bereich und im letzten Kilobyte „Basisspeicher“ angezeigt werden. Die Größe des Basisspeichers wird in einem 2-Byte-Wert von 0x413 in Kilobyte minus 1 KB angegeben. So sieht die Struktur aus:
struct mp_floating_pointer_structure { char signature[4]; uint32_t configuration_table; uint8_t length;
So sieht die Konfigurationstabelle aus, auf die die schwebende Struktur des Zeigers zeigt:
struct mp_configuration_table { char signature[4];
Nach der Konfigurationstabelle befinden sich die Einträge entry_count, die weitere Informationen zum System enthalten, gefolgt von einer erweiterten Tabelle. Einträge sind entweder 20 Bytes, um den Prozessor darzustellen, oder 8 Bytes für etwas anderes. So sehen APIC-Prozessor- und E / A-Datensätze aus.
struct entry_processor { uint8_t type;
Hier ist der IO APIC-Eintrag.
struct entry_io_apic { uint8_t type;
Suchen Sie nach Informationen mit APIC
Sie finden die MADT-Tabelle (APIC) in ACPI. In der Tabelle sind die lokalen APICs aufgeführt, deren Anzahl der Anzahl der Kerne auf Ihrem Prozessor entsprechen sollte. Die Details dieser Tabelle sind nicht hier, aber Sie können sie im Internet finden.
Starten Sie AP
Nachdem Sie die Informationen gesammelt haben, müssen Sie den PIC deaktivieren und sich auf den APIC I / O vorbereiten. Sie müssen auch den BSP des lokalen APIC konfigurieren. Starten Sie dann den AP mit SIPI.
Code zum Starten von Kerneln:Ich stelle fest, dass der Vektor, den Sie beim Start angeben, die Startadresse angibt: Vektor 0x8 - Adresse 0x8000, Vektor 0x9 - Adresse 0x9000 usw.
[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' ; ,
Wie Sie verstehen, müssen Sie den Stapel für jeden Kern, jeden Kern, seine Interrupts usw. konfigurieren, damit das Betriebssystem viele Kerne verwenden kann. Das Wichtigste ist jedoch, dass bei Verwendung der symmetrischen Mehrfachverarbeitung alle Ressourcen der Kerne gleich sind: ein Speicher, Eine PCI usw. und das Betriebssystem können nur Aufgaben zwischen den Kernen parallelisieren.
Ich hoffe, dass der Artikel nicht langweilig genug und recht informativ ist. Ich denke, wir können das nächste Mal darüber sprechen, wie sie früher auf dem Bildschirm gezeichnet haben (und jetzt zeichnen sie), ohne Shader und coole Grafikkarten zu verwenden.
Viel Glück