Die Entwicklung des x86-Kontextwechsels unter Linux



Als ich am vergangenen Wochenende interessante Fakten über den 80386-Hardware-Kontextwechsel studierte, fiel mir plötzlich ein, dass sich die ersten Versionen des Linux-Kernels darauf stützten. Und ich tauchte in Code ein, den ich seit vielen Jahren nicht mehr gesehen hatte. Jetzt habe ich beschlossen, diese wunderbare Reise durch die Geschichte von Linux zu beschreiben. Ich werde alle Nuggets und lustigen Artefakte zeigen, die ich unterwegs gefunden habe.

Ziel: Verfolgen, wie sich die Kontextumschaltung im Linux-Kernel von der ersten (0,01) auf die neueste Version von LTS (4.14.67) geändert hat, wobei der Schwerpunkt auf der ersten und der neuesten Version liegt.


Tatsächlich geht es in der Geschichte nicht darum, den Kontext zu wechseln, sondern um die Entwicklung von Linux von einem kleinen Projekt zu einem modernen Betriebssystem. Der Kontextwechsel spiegelt einfach diese Geschichte wider.

Über welchen Kontextwechsel sprechen wir?


Obwohl es viele Dinge gibt, die als Kontextumschaltung betrachtet werden können (z. B. Umschalten in den Kernelmodus, Umschalten auf einen Interrupt-Handler), meine ich die allgemein akzeptierte Bedeutung: Umschalten zwischen Prozessen . Unter Linux ist dies das Makro switch_to() und alles darin.

Dieses Makro ist eine einfache mechanische Aktion zwischen zwei viel interessanteren Systemen: einem Taskplaner und einer CPU. Betriebssystementwickler können Aufgabenplanungsstrategien mischen und koordinieren. CPU-Architekturen sind ebenfalls weit offen: Linux unterstützt Dutzende von Typen. Aber der Kontextwechsel ist das Zahnrad zwischen ihnen. Sein „Design“ hängt von seinen Nachbarn ab, daher behauptet der Kontextwechsel, der am wenigsten interessante Teil des Betriebssystems zu sein. Ich wiederhole: Er tut nur das, was getan werden muss.

Eine kurze Liste von Kontextwechselaufgaben:

  1. Überschreiben des Arbeitsbereichs: Stapelwiederherstellung (SS: SP).
  2. Suchen Sie nach der folgenden Anweisung: IP-Wiederherstellung (CS: IP).
  3. Wiederherstellung des Taskstatus: Wiederherstellung von Allzweckregistern.
  4. Austausch von Speicheradressräumen: Aktualisierung des Seitenverzeichnisses (CR3)
  5. ... und vieles mehr: FPUs, Betriebssystemdatenstrukturen, Debug-Register, Hardware-Problemumgehungen usw.

Es ist nicht immer klar, wann und wo diese Aufgaben ausgeführt werden, wenn ein anderer Prozess die CPU übernimmt. Beispielsweise verbirgt das Umschalten des Hardwarekontexts vor Linux 2.2 die Aufgaben 2, 3 und 4. Aufgabe 3 ist begrenzt, da zwischen den Kernelmodi umgeschaltet wird. Das Wiederherstellen eines Benutzer-Threads ist eine iret Aufgabe, nachdem der Scheduler zurückgekehrt ist. Viele dieser Aufgaben in verschiedenen Kernelversionen schweben zwischen switch_to() und dem Scheduler. Wir können nur garantieren, dass in jeder Version immer ein Stack-Swap und ein FPU-Switching stattfinden.

Für wen ist es?


Nicht für irgendjemanden speziell. Zum Verständnis müssen Sie nur den x86-Assembler kennen und wahrscheinlich nur über eine minimale Ausbildung in Bezug auf das Betriebssystemdesign verfügen.

Ich muss sofort sagen , dass ich kein Betreuer oder Mitwirkender am Linux-Kernel bin. Alle Informationen von diesen Kameraden oder von der Mailingliste der Kernelentwickler , die meinen Informationen widersprechen, sollten ernst genommen werden. Ich habe ein zufälliges persönliches Projekt, keinen wissenschaftlichen Artikel in einem von Experten begutachteten Journal.

Frühes Linux vor 1.0: Alte Geschichte (1991)


Der frühe Linux-Kernel ist einfach und funktional und enthält eine kleine Liste der wichtigsten Funktionen:

  • Einzelarchitektur (80386 / i386): Nur eine Art von Kontextwechsel. Viele 80386-Funktionen sind im gesamten Kern fest codiert. Als Referenz zu diesen Teilen habe ich das Intel 80386 Programmer's Guide (1986) genommen.
  • Hardware-Kontextwechsel: Um Aufgaben zu ändern, verwendet der Kernel integrierte Mechanismen 80386.
  • Ein Prozess mit präemptivem Multitasking: Es ist jeweils nur eine CPU mit einem Prozess aktiv. Ein anderer Prozess kann jedoch jederzeit beginnen. Daher werden die üblichen Synchronisationsregeln angewendet: Blockieren gemeinsam genutzter Ressourcen (ohne Spin-Locks). Im Extremfall ist es möglich, Interrupts zu deaktivieren, aber zuerst den Mutex zu sperren.



Schauen Sie sich ohne weiteres die beiden frühen Kontextwechsel an. Der Code ist zur besseren Lesbarkeit formatiert: ein Element pro Zeile ohne Fortsetzungszeichen (\).

Linux 0.01
 /** include/linux/sched.h */ #define switch_to(n) { struct {long a,b;} __tmp; __asm__("cmpl %%ecx,_current\n\t" "je 1f\n\t" "xchgl %%ecx,_current\n\t" "movw %%dx,%1\n\t" "ljmp %0\n\t" "cmpl %%ecx,%2\n\t" "jne 1f\n\t" "clts\n" "1:" ::"m" (*&__tmp.a), "m" (*&__tmp.b), "m" (last_task_used_math), "d" _TSS(n), "c" ((long) task[n])); } 

Linux 0.11
 /** include/linux/sched.h */ #define switch_to(n) { struct {long a,b;} __tmp; __asm__("cmpl %%ecx,_current\n\t" "je 1f\n\t" "movw %%dx,%1\n\t" "xchgl %%ecx,_current\n\t" "ljmp %0\n\t" "cmpl %%ecx,_last_task_used_math\n\t" "jne 1f\n\t" "clts\n" "1:" ::"m" (*&__tmp.a), "m" (*&__tmp.b), "d" (_TSS(n)), "c" ((long) task[n])); } 

Sofort auffällig, wie klein er ist! Klein genug, um jede Zeile einzeln zu analysieren:

 #define switch_to(n) { 

switch_to() ist also ein Makro. Es erscheint an genau einer Stelle: in der allerletzten Zeile des schedule() . Daher teilt das Makro nach der Vorverarbeitung den Scheduler-Bereich. Im globalen Bereich werden unbekannte Links überprüft, z. B. current und last_task_used_math . Das Eingabeargument n ist die Sequenznummer der nächsten Aufgabe (von 0 bis 63).

 struct {long a,b;} __tmp; 

Reserviert 8 Bytes (64 Bit) auf dem Stapel, auf die über zwei 4-Byte-Elemente a und b . Wir werden einige dieser Bytes später für die Weitsprungoperation setzen.

 __asm__("cmpl %%ecx,_current\n\t" 

Der Kontextwechsel ist ein langer Inline-Block im Assembler. Der erste Befehl bestimmt, ob das Ziel bereits aktuell ist. Dies ist ein subtraktiver Vergleich des Werts im ECX-Register mit dem Wert des aktuellen current vom Scheduler. Beide enthalten Zeiger auf task_struct Prozesses. Unten in ECX befindet sich ein Zeiger auf die Zielaufgabe als gegebene Eingabe: "c" ((long) task[n]) . Das Vergleichsergebnis setzt den Wert des Statusregisters EFLAGS: Zum Beispiel ZF = 1, wenn beide Zeiger übereinstimmen (x - x = 0).

 "je 1f\n\t" 

Wenn die nächste Aufgabe die aktuelle ist, müssen Sie den Kontext nicht wechseln, daher sollten Sie diese gesamte Prozedur überspringen (überspringen). Der Befehl je prüft, ob ZF = 1. Wenn ja, wird nach diesem Punkt im Code, der 8 Zeilen voraus ist, zur ersten Bezeichnung '1' verschoben.

 "xchgl %%ecx,_current\n\t" 

Aktualisiert den globalen current , um die neue Aufgabe widerzuspiegeln. Der Zeiger von ECX (Task [n]) wechselt auf aktuell. Flags werden nicht aktualisiert.

 "movw %%dx,%1\n\t" 

Verschiebt den TSS-Index (Target Selector Descriptor Segment Selector) in einen zuvor reservierten Bereich. Technisch gesehen wird dadurch der Wert aus dem DX-Register in __tmp.b , __tmp.b in die Bytes 5 bis 8 unserer reservierten 8-Byte-Struktur. Der DX-Wert ist die angegebene Eingabe: "d" (_TSS(n)) . Das mehrstufige _TSS Makro wird zu einem gültigen TSS-Segment-Selektor erweitert, auf den ich später noch _TSS werde. Unter dem Strich enthalten die beiden High-Bytes __tmp.b jetzt einen Segmentzeiger auf die nächste Aufgabe.

 "ljmp %0\n\t" 

Ruft einen Hardware-Kontextschalter 80386 auf, der zum TSS-Deskriptor wechselt. Dieser einfache Sprung kann verwirrend sein, da es drei verschiedene Ideen gibt: Erstens ist ljmp ein indirekter Weitsprung, der einen 6-Byte-Operanden (48 Bit) benötigt. Zweitens bezieht sich der Operand% 0 auf die nicht initialisierte Variable __tmp. . Schließlich ist der Wechsel zu einem Segmentselektor in GDT in x86 von besonderer Bedeutung. Werfen wir einen Blick auf diese Punkte.

Indirekter Fernübergang


Der wichtige Punkt ist, dass dieser Übergang einen 6-Byte-Operanden hat. Das Programmierhandbuch für 80386 beschreibt den Übergang wie folgt:



Gehe zu __tmp.a


Denken Sie daran, dass die __tmp Struktur zwei 4-Byte-Werte enthielt und die Struktur auf a basiert. Wenn wir dieses Element jedoch als Basisadresse des 6-Byte-Operanden verwenden, erreichen wir zwei Bytes innerhalb der Ganzzahl __tmp.b . Diese beiden Bytes sind Teil des "Segmentselektors" der Fernadresse. Wenn der Prozessor erkennt, dass das Segment im GDT TSS ist, wird ein Teil des Offsets vollständig ignoriert. Die Tatsache, dass __tmp.a nicht initialisiert wird, spielt keine Rolle, da __tmp.b dank der vorherigen movw Anweisung immer noch einen gültigen Wert hat. Fügen Sie die Übergangsadresse zum Diagramm hinzu:



Woher wissen wir, dass sich diese Adresse auf GDT bezieht? Ich werde die Details in anderen Codezeilen offenbaren, aber die kurze Version ist, dass die vier Nullbits im Selektor eine GDT-Suche auslösen. Das Makro _TSS(n) garantiert das Vorhandensein dieser vier Nullen. Die unteren zwei Bits sind die Segmentberechtigungsstufe (00 entspricht Supervisor / Kernel), das nächste Nullbit bedeutet die Verwendung der GDT-Tabelle (die beim Booten in der GDTR gespeichert ist). Die vierte Null ist technisch gesehen Teil des Segmentindex, der alle TSS-Suchen für die geraden Einträge der GDT-Tabelle erzwingt.

Hardware-Kontextschalter


Die Sprungadresse in __tmp definiert das TSS-Handle im GDT. So wird es im Handbuch für 80386 beschrieben:



Der Prozessor erledigt für uns automatisch Folgendes:

  • Überprüft, ob die aktuelle Berechtigungsstufe zulässig ist (wir befinden uns im Kernel-Modus, sodass alles in Ordnung ist).
  • Überprüft, ob das TSS gültig ist (sollte sein).
  • Speichert den gesamten aktuellen Status der Aufgabe im alten TSS, der noch im Aufgabenregister (TR) gespeichert ist, sodass Sie EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI, ES, CS, SS, DS, FS, nicht verwenden müssen. GS und EFLAGS. EIP wird zum nächsten Befehl erhöht und ebenfalls gespeichert.
  • Aktualisiert TR für eine neue Aufgabe.
  • Stellt alle allgemeinen Register, EIP und PDBR (Swap Address Space) wieder her. Der Taskwechsel ist beendet, daher wird das TS-Flag im CR0-Register gesetzt.

Die einzige Anweisung "ljmp %0\n\t" hat also alle Schritte zum Wechseln des Kontexts ausgeführt. Es bleibt nur ein bisschen aufzuräumen.

 "cmpl %%ecx,%2\n\t" 

Wir überprüfen, ob die vorherige Aufgabe den mathematischen Coprozessor wiederhergestellt hat. Das Argument ist der Zeiger last_task_used_math . Mit dem TS-Flag können Sie überprüfen, ob der Coprozessor einen anderen Kontext hat. Hardwarekontextschalter steuern den Coprozessor nicht.

 "jne 1f\n\t" 

Wenn die letzte Aufgabe den Coprozessor nicht wiederhergestellt hat, fahren Sie mit dem Ende des Kontextwechsels fort. Wir möchten das TS-Flag belassen, damit Sie bei der nächsten Verwendung des Coprozessors eine verzögerte Bereinigung durchführen können. "Faul", weil wir die Aufgabe verschieben, bis sie absolut notwendig wird.

 "clts\n" 

Löschen Sie das TS-Flag, wenn der letzte Prozess den Status des Coprozessors wiederhergestellt hat.

 "1:" 

Endmarkierung des Kontextschalters. Alle Sprünge zu dieser Bezeichnung überspringen einige oder alle Prozeduren.

 ::"m" (*&__tmp.a), 

In diesem Assembler-Block gibt es keine Ausgabe, und die erste Eingabe (% 0) ist die Position im Speicher der ersten vier Bytes des Fernzeigers auf den TSS-Deskriptor in der GDT. Es wird nur als Referenz auf die Adresse verwendet, der Wert wird nicht initialisiert.

 "m" (*&__tmp.b), 

Die zweite Eingabe (% 1) ist die Stelle im Speicher der Bytes 5 und 6 des Fernzeigers auf den TSS-Deskriptor. Technisch gesehen belegt dieser Platz vier Bytes im Speicher, aber nur die ersten beiden werden überprüft und verwendet.

 "m" (last_task_used_math), 

Die dritte Eingabe (% 2) ist die Position im Speicher des Zeigers auf den letzten task_struct , der den Status des Coprozessors wiederhergestellt hat.

 "d" (_TSS(n)), 

Die vierte Eingabe (% 3 / %% edx) ist die Adresse des TSS-Deskriptorsegment-Selektors in der GDT. Schauen wir uns das Makro an:

 #define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3)) #define FIRST_TSS_ENTRY 4 

Dies bedeutet, dass der erste TSS-Deskriptor der 4. Datensatz ist (der Index beginnt mit dem 4. Bit des Segmentselektors). Jedes nachfolgende TSS belegt jeden zweiten GDT-Datensatz: 4, 6, 8 usw. Die ersten acht Aufgaben sehen folgendermaßen aus:

Aufgabe #16-Bit-Segmentauswahl
00000000000100 0 00
10000000000110 0 00
20000000001000 0 00
30000000001010 0 00
40000000001100 0 00
50000000001110 0 00
60000000010000 0 00
70000000010010 0 00

Die Adressbits werden durch das Feldformat getrennt, wie es in 80386 sein sollte:



Die vier niedrigstwertigen Bits sind immer Null, was dem Supervisor-Modus, der GDT-Tabelle, entspricht und sogar Einträge des GDT-Index erzwingt.

 "c" ((long) task[n])); 

Der letzte Eintrag (% 4 /% ecx) ist ein Zeiger auf das neue task_struct, zu dem wir wechseln. Beachten Sie, dass sich der %% ecx-Wert unmittelbar vor dem Kontextwechsel zur vorherigen Aufgabe ändert.

Unterschiede zwischen 0,01 und 0,11


Es gibt zwei Unterschiede zwischen Kontextwechseln. Eine davon ist die einfache Code-Bereinigung und die andere ist die teilweise Fehlerkorrektur.

  • _last_task_used_math als Eingabevariable gelöscht, da das Symbol bereits im globalen Bereich verfügbar ist. Die entsprechende Vergleichsoperation wurde in eine direkte Verknüpfung geändert.
  • Der xchgl Befehl wurde mit movw ausgetauscht, um ihn näher an den Hardware-Kontextschalter ( ljmp ) zu bringen. Das Problem ist, dass diese Operationen nicht atomar sind: Es ist unwahrscheinlich, dass ein Interrupt zwischen xchgl und ljmp , der zu einem weiteren Kontextwechsel mit der falschen current Aufgabe und dem nicht gespeicherten Status der realen Aufgabe führt. Das Ersetzen dieser Anweisungen macht diese Situation sehr unwahrscheinlich. In einem lang laufenden System ist dies jedoch "sehr unwahrscheinlich" - ein Synonym für "unvermeidlich".

Linux 1.x: Proof of Concept


Ungefähr 20 Patches wurden in ungefähr einem Jahr zwischen 0,11 und 1,0 veröffentlicht. Der größte Teil der Bemühungen konzentrierte sich auf Treiber, Funktionen für Benutzer und Entwickler. Die maximale Anzahl von Aufgaben wurde auf 128 erhöht, es wurden jedoch nicht viele grundlegende Änderungen am Kontextwechsel vorgenommen.

Linux 1.0


Linux 1.0 läuft immer noch auf derselben CPU mit einem Prozess, wobei die Hardware-Kontextumschaltung verwendet wird.



Linux 1.0
 /** include/linux/sched.h */ #define switch_to(tsk) __asm__("cmpl %%ecx,_current\n\t" "je 1f\n\t" "cli\n\t" "xchgl %%ecx,_current\n\t" "ljmp %0\n\t" "sti\n\t" "cmpl %%ecx,_last_task_used_math\n\t" "jne 1f\n\t" "clts\n" "1:" : /* no output */ :"m" (*(((char *)&tsk->tss.tr)-4)), "c" (tsk) :"cx") 

Die wichtigste Änderung war, dass das Eingabeargument nicht mehr der Tasknummernindex für das Array von task_struct-Strukturen ist. Jetzt zeigt switch_to() auf eine neue Aufgabe. Sie können also die __tmp Struktur entfernen und stattdessen einen direkten Link zu TSS verwenden. Lassen Sie uns jede Zeile analysieren.

 #define switch_to(tsk) 

Die Eingabe ist jetzt ein Zeiger auf task_struct der nächsten Aufgabe.

 "__asm__("cmpl %%ecx,_current\n\t" 

Nicht geändert. Überprüft, ob die Eingabeaufgabe bereits aktuell ist, sodass kein Schalter erforderlich ist.

 "je 1f\n\t" 

Nicht geändert. Überspringen Sie die Kontextumschaltung, wenn keine Umschaltung vorhanden ist.

 "cli\n\t" 

Deaktiviert Interrupts, damit der Timer (oder eine andere Person) nicht zwischen dem Aktualisieren einer globalen Aufgabe und dem Umschalten des Hardwarekontexts abstürzt. Dieser Interrupt-Banhammer löst das Problem früherer Kernel-Versionen, indem die folgenden zwei Anweisungen (Pseudo) atomar gemacht werden.

 "xchgl %%ecx,_current\n\t" "ljmp %0\n\t" 

Keine Änderung: Tauschen Sie den aktuellen Prozess aus, um die neue Aufgabe widerzuspiegeln, und rufen Sie den Hardwarekontextschalter auf.

 "sti\n\t" 

Schaltet Interrupts wieder ein.

 "cmpl %%ecx,_last_task_used_math\n\t" "jne 1f\n\t" "clts\n" "1:" 

Im Vergleich zu Linux 0.11 ist alles unverändert. Verwaltet das TS-Register und überwacht das Löschen des mathematischen Coprozessors von der vorherigen Aufgabe.

 : /* no output */ 

Dieser eingebaute Assembler hat keine Ausgabe - jemand war sichtlich verärgert über das Fehlen von Kommentaren in früheren Versionen des Kernels.

 :"m" (*(((char *)&tsk->tss.tr)-4)), 

Lädt einen Segmentselektor für den TSS-Deskriptor einer neuen Aufgabe, auf den jetzt direkt über den Zeiger task_struct zugegriffen werden kann. Das Element tss.tr enthält _TSS (task_number) als Referenz auf den GDT / TSS-Speicher, der vor 1.0 im Kernel verwendet wurde. Wir fallen immer noch um 4 Bytes zurück und laden einen 6-Byte-Segment-Selektor, um die obersten zwei Bytes zu übernehmen. Viel Spaß!

 "c" (tsk) 

Fast unverändert - jetzt laden wir den Zeiger direkt und suchen nicht nach dem Index.

 :"cx") 

Die Kontextumschaltung blockiert das ECX-Register.

Linux 1.3


Der Kernel unterstützt jetzt mehrere neue Architekturen: Alpha, MIPS und SPARC. Daher gibt es vier verschiedene Versionen von switch_to() , von denen eine beim Kompilieren des Kernels enthalten ist. Der architekturspezifische Code wurde vom Kernel getrennt, daher müssen Sie an anderer Stelle nach der x86-Version suchen.

Linux 1.3
 /** include/asm-i386/system.h */ #define switch_to(tsk) do { __asm__("cli\n\t" "xchgl %%ecx,_current\n\t" "ljmp %0\n\t" "sti\n\t" "cmpl %%ecx,_last_task_used_math\n\t" "jne 1f\n\t" "clts\n" "1:" : /* no output */ :"m" (*(((char *)&tsk->tss.tr)-4)), "c" (tsk) :"cx"); /* Now maybe reload the debug registers */ if(current->debugreg[7]){ loaddebug(0); loaddebug(1); loaddebug(2); loaddebug(3); loaddebug(6); } } while (0) 

Ein paar kleine Änderungen: Der gesamte Kontextschalter ist in eine gefälschte Do-While-Schleife eingeschlossen. Feykov, weil er nie wiederholt. Die Prüfung für den Wechsel zu einer neuen Aufgabe wurde in C von switch_to() zu Sheduler-Code verschoben. Einige Debugging-Aufgaben wurden von C-Code zu switch_to () , wahrscheinlich um eine Trennung zu vermeiden. Schauen wir uns die Änderungen an.

 #define switch_to(tsk) do { 

Jetzt wird switch_to() in eine do-while (0) -Schleife eingeschlossen. Dieses Design verhindert Fehler, wenn das Makro aufgrund der Bedingung (falls vorhanden) auf mehrere Anweisungen erweitert wird. Derzeit ist dies nicht der Fall, aber angesichts der Änderungen im Scheduler vermute ich, dass dies das Ergebnis der Bearbeitung des Codes ist, nur für den Fall. Meine Vermutung:

Echter Planer in 1.3
 ...within schedule()... if (current == next) return; kstat.context_swtch++; switch_to(next); 

Eine mögliche Option, die switch_to () bricht
 ...within schedule()... if (current != next) switch_to(next); /* do-while(0) 'captures' entire * block to ensure proper parse */ 


 __asm__("cli\n\t" "xchgl %%ecx,_current\n\t" "ljmp %0\n\t" "sti\n\t" "cmpl %%ecx,_last_task_used_math\n\t" "jne 1f\n\t" "clts\n" "1:" : /* no output */ :"m" (*(((char *)&tsk->tss.tr)-4)), "c" (tsk) :"cx"); 

Keine Änderung gegenüber Linux 1.0. Interrupts werden jedoch deaktiviert, bevor der * task_struct-Wechsel von current , dann die Hardware-Kontextumschaltung funktioniert und die Verwendung des Coprozessors überprüft wird.

 /* Now maybe reload the debug registers */ if(current->debugreg[7]){ 

Überprüft die Debugging-Steuerung auf einen neuen Prozess für aktives ptrace (eine Adresse ungleich Null bedeutet hier aktives ptrace). Das Debug-Tracking wurde nach switch_to() . Genau die gleiche Sequenz C wird in 1.0 verwendet. Ich nehme an, die Entwickler wollten sicherstellen, dass: 1) das Debuggen so nah wie möglich am Kontextschalter ist 2) switch_to das neueste im schedule() .

 loaddebug(0); loaddebug(1); loaddebug(2); loaddebug(3); 

Stellt Debug-Haltepunktregister aus einem gespeicherten ptrace-Status wieder her.

 loaddebug(6); 

Stellt das Debug-Steuerregister aus dem gespeicherten ptrace-Status wieder her.

 } while (0) 

Schließt den Block switch_to() . Obwohl die Bedingung immer dieselbe ist, stellt dies sicher, dass der Parser die Funktion als Basiseinheit übernimmt, die nicht mit benachbarten Bedingungen in schedule() interagiert. Beachten Sie das Fehlen eines Kommas am Ende - es steht nach dem Makroaufruf: switch_to(next); .

Linux 2.0: Kandidat (1996)


Im Juni 1996 wurde der Kernel auf Version 2.0 aktualisiert und eine 15-jährige Odyssee unter dieser Hauptversion gestartet, die mit einer breiten kommerziellen Unterstützung endete. In 2.x wurden fast alle grundlegenden Systeme im Kernel radikal verändert. Berücksichtigen Sie alle Nebenversionen vor 2.6. Version 2.6 wurde so lange entwickelt, dass es einen separaten Abschnitt verdient.

Linux 2.0


Linux 2.0 begann mit einer grundlegenden Innovation: Multiprocessing ! Zwei oder mehr Prozessoren können gleichzeitig Benutzer- / Kernelcode verarbeiten. Dies erforderte natürlich einige Verfeinerung. Beispielsweise verfügt jeder Prozessor jetzt über einen dedizierten Interrupt-Controller (APIC), sodass Interrupts auf jedem Prozessor separat verwaltet werden müssen. Mechanismen wie Timer-Unterbrechungen müssen überarbeitet werden (das Deaktivieren von Interrupts betrifft nur einen Prozessor). Die Synchronisierung ist schwierig, insbesondere wenn versucht wird, sie auf eine bereits große und nicht verwandte Codebasis anzuwenden. Linux 2.0 legt den Grundstein für eine große Kernel-Sperre (BKL) ... Sie müssen irgendwo anfangen.

Jetzt haben wir zwei Versionen von switch_to() : die Einzelprozessorversion (UP) von Linux 1.x und die neue verbesserte Version für symmetrisches Multiprocessing (SMP). Betrachten Sie zunächst die Änderungen im alten Code, da einige Änderungen von dort auch in der SMP-Version enthalten sind.

Linux 2.0.1: Uniprozessor-Version (UP)




Linux 2.0.1 (UP)
 /** include/asm-i386/system.h */ #else /* Single process only (not SMP) */ #define switch_to(prev,next) do { __asm__("movl %2,"SYMBOL_NAME_STR(current_set)"\n\t" "ljmp %0\n\t" "cmpl %1,"SYMBOL_NAME_STR(last_task_used_math)"\n\t" "jne 1f\n\t" "clts\n" "1:" : /* no outputs */ :"m" (*(((char *)&next->tss.tr)-4)), "r" (prev), "r" (next)); /* Now maybe reload the debug registers */ if(prev->debugreg[7]){ loaddebug(prev,0); loaddebug(prev,1); loaddebug(prev,2); loaddebug(prev,3); loaddebug(prev,6); } } while (0) #endif 

Zwei Änderungen sind sofort ersichtlich:

  • U switch_to()hat ein neues Argument: den Prozess, von dem *task_structwir wechseln.
  • Makro für die korrekte Verarbeitung von Zeichen im eingebauten Assembler.

Gehen wir wie gewohnt in die richtige Richtung und besprechen die Änderungen.

 #define switch_to(prev,next) do { 

Das Argument prevdefiniert die Aufgabe, von der wir wechseln ( *task_struct). Wir wickeln das Makro immer noch in eine do-while (0) -Schleife ein, um einzeilige ifs um das Makro herum zu analysieren.

 __asm__("movl %2,"SYMBOL_NAME_STR(current_set)"\n\t" 

Aktualisiert die aktuell aktive Aufgabe auf die neu ausgewählte. Dies ist funktional äquivalent xchgl %%ecx,_current, außer dass wir jetzt ein Array mit mehreren task_struct und ein macro ( SYMBOL_NAME_STR) zum Verarbeiten eingebetteter Assembly-Zeichen haben. Warum dafür einen Präprozessor verwenden? Tatsache ist, dass einige Assembler (GAS) das Hinzufügen eines Unterstrichs (_) zum Variablennamen C erfordern. Andere Assembler haben diese Anforderung nicht. Um eine Konvention nicht auf einer Festplatte zu speichern, können Sie sie zur Kompilierungszeit entsprechend Ihren Tools konfigurieren.

 "ljmp %0\n\t" "cmpl %1,"SYMBOL_NAME_STR(last_task_used_math)"\n\t" "jne 1f\n\t" "clts\n" "1:" : /* no outputs */ :"m" (*(((char *)&next->tss.tr)-4)), 

Keine Änderungen, über die wir nicht gesprochen haben.

 "r" (prev), "r" (next)); 

Jetzt tragen wir beide Aufgaben als Eingabe in den Inline-Assembler. Eine geringfügige Änderung besteht darin, dass die Verwendung des Registers jetzt zulässig ist. Es nextwurde zuvor in ECX codiert.

 /* Now maybe reload the debug registers */ if(prev->debugreg[7]){ loaddebug(prev,0); loaddebug(prev,1); loaddebug(prev,2); loaddebug(prev,3); loaddebug(prev,6); } } while (0) 

Alles ist genau wie in Kernel 1.3.

Linux 2.0.1: Multiprozessor-Version (SMP)




Linux 2.0.1 (SMP)
 /** include/asm-i386/system.h */ #ifdef __SMP__ /* Multiprocessing enabled */ #define switch_to(prev,next) do { cli(); if(prev->flags&PF_USEDFPU) { __asm__ __volatile__("fnsave %0":"=m" (prev->tss.i387.hard)); __asm__ __volatile__("fwait"); prev->flags&=~PF_USEDFPU; } prev->lock_depth=syscall_count; kernel_counter+=next->lock_depth-prev->lock_depth; syscall_count=next->lock_depth; __asm__("pushl %%edx\n\t" "movl "SYMBOL_NAME_STR(apic_reg)",%%edx\n\t" "movl 0x20(%%edx), %%edx\n\t" "shrl $22,%%edx\n\t" "and $0x3C,%%edx\n\t" "movl %%ecx,"SYMBOL_NAME_STR(current_set)"(,%%edx)\n\t" "popl %%edx\n\t" "ljmp %0\n\t" "sti\n\t" : /* no output */ :"m" (*(((char *)&next->tss.tr)-4)), "c" (next)); /* Now maybe reload the debug registers */ if(prev->debugreg[7]){ loaddebug(prev,0); loaddebug(prev,1); loaddebug(prev,2); loaddebug(prev,3); loaddebug(prev,6); } } while (0) 

Was wird schon unverständlich? Ich möchte sagen, dass es später besser wird, aber dies wird in der Welt von SMP nicht passieren. Aus Platzgründen werde ich keine unveränderten Zeichenfolgen mehr auflisten.

Drei Ergänzungen für den SMP-Kontextwechsel: 1) Ändern der Funktionsweise eines einzelnen Coprozessors mit mehreren Prozessoren; 2) Steuern der Sperrtiefe, da die Kernelsperre rekursiv ist; 3) Verknüpfen Sie mit APIC, um die CPU-ID für das aktuelle * task_struct abzurufen.

 if(prev->flags&PF_USEDFPU) 

Überprüft, ob für die Aufgabe, zu der wir wechseln, ein Coprozessor verwendet wurde. Wenn ja, müssen Sie den Kontext in der FPU erfassen, bevor Sie wechseln.

 __asm__ __volatile__("fnsave %0":"=m" (prev->tss.i387.hard)); 

Speichert den FPU-Status in TSS. Mit FNSAVE wird die Ausnahmebehandlung übersprungen. __volatile__sollte diese Anweisung vor Änderungen durch den Optimierer schützen.

 __asm__ __volatile__("fwait"); 

Warten auf die CPU, während die FPU mit dem vorherigen Speichern beschäftigt ist.

 prev->flags&=~PF_USEDFPU; 

Deaktiviert das Flag für die Verwendung eines Coprozessors für diese Aufgabe. Es gibt immer Null.

 prev->lock_depth=syscall_count; 

Speichert die Anzahl der verschachtelten Verwendungen der Kernel-Sperre für eine alte Aufgabe.

 kernel_counter+=next->lock_depth-prev->lock_depth; 

Aktualisiert den globalen Kernel-Sperrzähler auf die nächste Aufgabe abzüglich der alten Aufgabe. Entfernt effektiv die Sperre von der jetzt inaktiven alten Aufgabe, und die neue Aufgabe kann an der Stelle weiterarbeiten, an der sie gestoppt wurde.

 syscall_count=next->lock_depth; 

Gibt den Sperrstatus einer neuen Aufgabe zurück. Es sollte dort sein, wo sie in der letzten Zeit aufgehört hat.

 __asm__("pushl %%edx\n\t" 

Wir werden EDX verwenden, damit wir den aktuellen Wert beibehalten.

 "movl "SYMBOL_NAME_STR(apic_reg)",%%edx\n\t" 

Verschiebt die APIC-E / A-Adresse nach EDX. Wir müssen APIC verwenden, um die CPU-ID zu erhalten, da wir nicht wissen, welcher Prozessor funktioniert. apic_regBroadcast während der Betriebssysteminitialisierung.

 "movl 0x20(%%edx), %%edx\n\t" 

Referenziert den Wert des APIC-Identifikatorregisters im EDX. Die tatsächliche ID befindet sich in den Bits 24-27.



 "shrl $22,%%edx\n\t" 

Verschiebt die APIC-ID auf die Bits 2-5.

 "and $0x3C,%%edx\n\t" 

Maskiert nur die APIC-ID in den Bits 2-5 und belässt die CPU-Nummer * 4.

 "movl %%ecx,"SYMBOL_NAME_STR(current_set)"(,%%edx)\n\t" 

Aktualisiert den Aufgabenzeiger der aktuellen CPU auf die nächste Aufgabe. Die UP-Version hat die spezifische Verwendung von ECX zum Speichern der aktuellen Aufgabe bereits entfernt, wird jedoch weiterhin in der SMP-Version verwendet. EDX enthält die CPU-Nummer in den Bits 2-5, multipliziert mit 4, im Maßstab, um die Zeigergröße von _current_set zu versetzen.

 "popl %%edx\n\t" 

Wir sind mit EDX fertig und werden den Wert wiederherstellen, der vor diesem Verfahren war.

Der Rest der Zeilen ist der gleiche.

Linux 2.2 (1999)


Das Warten auf Linux 2.2 hat sich wirklich gelohnt: Hier kam der Software-Kontextwechsel ! Wir verwenden weiterhin das Taskregister (TR), um auf TSS zu verweisen. SMP- und UP-Prozeduren werden mit einer einheitlichen FPU-Statusverarbeitung kombiniert. Die meisten Kontextwechsel werden jetzt in C. Linux 2.2.0- Code



(integrierter Assembler) durchgeführt.
 /** include/asm-i386/system.h */ #define switch_to(prev,next) do { unsigned long eax, edx, ecx; asm volatile("pushl %%ebx\n\t" "pushl %%esi\n\t" "pushl %%edi\n\t" "pushl %%ebp\n\t" "movl %%esp,%0\n\t" /* save ESP */ "movl %5,%%esp\n\t" /* restore ESP */ "movl $1f,%1\n\t" /* save EIP */ "pushl %6\n\t" /* restore EIP */ "jmp __switch_to\n" "1:\t" "popl %%ebp\n\t" "popl %%edi\n\t" "popl %%esi\n\t" "popl %%ebx" :"=m" (prev->tss.esp),"=m" (prev->tss.eip), "=a" (eax), "=d" (edx), "=c" (ecx) :"m" (next->tss.esp),"m" (next->tss.eip), "a" (prev), "d" (next)); } while (0) 

Dieses neue switch_to()unterscheidet sich grundlegend von allen vorherigen Versionen: Es ist einfach! Im eingebauten Assembler tauschen wir die Stapel- und Anweisungszeiger aus (Kontextwechselaufgaben 1 und 2). Alles andere wird erledigt, nachdem Sie zum C ( __switch_to()) - Code gegangen sind .

 asm volatile("pushl %%ebx\n\t" "pushl %%esi\n\t" "pushl %%edi\n\t" "pushl %%ebp\n\t" 

Speichert EBX, ESI, EDI und EBP im Stapel des Prozesses, den wir tauschen werden. (... warum EBX?)

 "movl %%esp,%0\n\t" /* save ESP */ "movl %5,%%esp\n\t" /* restore ESP */ 

Wie Sie den Kommentaren entnehmen können, tauschen wir Stapelzeiger zwischen dem alten und dem neuen Prozess aus. Der alte Prozess hat den Operanden% 0 ( prev->tss.esp), während der neue Prozess % 5 ( next->tss.esp) hat.

 "movl $1f,%1\n\t" /* save EIP */ 

Speichern des Werts des Befehlszeigers für den nächsten Befehl der alten Aufgabe nach dem Zurückschalten des Kontexts. Beachten Sie, dass der Wert der folgenden Anweisung eine Bezeichnung verwendet 1:

 "pushl %6\n\t" /* restore EIP */ 

Bereiten Sie die folgenden Anweisungen für eine neue Aufgabe vor. Da wir gerade zu einem neuen Stapel gewechselt haben, wird diese IP aus dem TSS der neuen Aufgabe entnommen und oben im Stapel platziert. Die Ausführung beginnt mit der folgenden Anweisung nach 'ret' aus dem C-Code, den wir ausführen möchten.

 "jmp __switch_to\n" 

Wir fahren mit unserem neuen und verbesserten Software-Kontextwechsel fort (siehe unten).

 "popl %%ebp\n\t" "popl %%edi\n\t" "popl %%esi\n\t" "popl %%ebx" 

Wir stellen die Register vom Stapel in umgekehrter Reihenfolge wieder her, vermutlich nachdem wir in einem neuen Zeitintervall zur alten Task gewechselt haben.

Linux 2.2.0 (C)
 /** arch/i386/kernel/process.c */ void __switch_to(struct task_struct *prev, struct task_struct *next) { /* Do the FPU save and set TS if it wasn't set before.. */ unlazy_fpu(prev); gdt_table[next->tss.tr >> 3].b &= 0xfffffdff; asm volatile("ltr %0": :"g" (*(unsigned short *)&next->tss.tr)); asm volatile("movl %%fs,%0":"=m" (*(int *)&prev->tss.fs)); asm volatile("movl %%gs,%0":"=m" (*(int *)&prev->tss.gs)); /* Re-load LDT if necessary */ if (next->mm->segments != prev->mm->segments) asm volatile("lldt %0": :"g" (*(unsigned short *)&next->tss.ldt)); /* Re-load page tables */ { unsigned long new_cr3 = next->tss.cr3; if (new_cr3 != prev->tss.cr3) asm volatile("movl %0,%%cr3": :"r" (new_cr3)); } /* Restore %fs and %gs. */ loadsegment(fs,next->tss.fs); loadsegment(gs,next->tss.gs); if (next->tss.debugreg[7]){ loaddebug(next,0); loaddebug(next,1); loaddebug(next,2); loaddebug(next,3); loaddebug(next,6); loaddebug(next,7); } } 

Beim Software-Kontextwechsel wurde der alte Übergang zum TSS-Deskriptor durch den Übergang zur neuen C: -Funktion ersetzt __switch_to(). Diese Funktion ist in C geschrieben und enthält mehrere bekannte Komponenten, z. B. Debug-Register. Wenn Sie zu C gehen, können Sie sie noch näher an den Kontextwechsel verschieben.

 unlazy_fpu(prev); 

Wir überprüfen die Verwendung der FPU und speichern ihren Status, falls verwendet. Dies geschieht nun für jeden Prozess, bei dem die FPU verwendet wurde, sodass die Reinigung nicht mehr verzögert ist. Die Vorgehensweise ist dieselbe wie bei der SMP-Routine ab 2.0.1, außer dass wir jetzt ein sauberes Makro haben, das die manuelle TS-Optimierung enthält.

 gdt_table[next->tss.tr >> 3].b &= 0xfffffdff; 

Löscht das BUSY-Bit für einen zukünftigen Task-Deskriptor. Verwendet die Aufgabennummer, um die GDT zu indizieren. tss.trenthält den Wert des Task-Segment-Selektors, wobei die unteren drei Bits für Berechtigungen verwendet werden. Wir brauchen nur einen Index, also verschieben wir diese Bits. Das zweite TSS-Byte wird geändert, um Bit 10 zu entfernen.



 asm volatile("ltr %0": :"g" (*(unsigned short *)&next->tss.tr)); 

Das Aufgabenregister wird mit einem Zeiger auf den nächsten Aufgabensegment-Selektor geladen.

 asm volatile("movl %%fs,%0":"=m" (*(int *)&prev->tss.fs)); asm volatile("movl %%gs,%0":"=m" (*(int *)&prev->tss.gs)); 

Die FS- und GS-Segmentregister für die vorherige Aufgabe werden in TSS gespeichert. Beim Hardware-Kontextwechsel wurde dieser Schritt automatisch ausgeführt, jetzt müssen wir dies jedoch manuell tun. Aber warum? Wie verwendet Linux FS und GS?

Unter Linux 2.2 (1999) gibt es keine klare Antwort. Es wird nur gesagt, dass sie verwendet werden, daher sollten Sie sie speichern, damit sie zugänglich bleiben. Der Kernel-Modus-Code "leiht" diese Segmente aus, um Kernel-Segmente oder Benutzerdaten anzuzeigen. Sound- und Netzwerktreiber machen dasselbe. In letzter Zeit (ab ~ 2,6) unterstützen FS und GS häufig lokale Stream-Speicher- und Datenbereiche pro Prozessor.

 if (next->mm->segments != prev->mm->segments) asm volatile("lldt %0": :"g" (*(unsigned short *)&next->tss.ldt)); 

Stellt Segmente der lokalen Deskriptortabelle wieder her, wenn sie nicht bereits dem alten Prozess entsprechen. Dies erfolgt durch Laden des LDT-Registers.

 if (new_cr3 != prev->tss.cr3) asm volatile("movl %0,%%cr3": :"r" (new_cr3)); 

Aktualisiert den Status des virtuellen Speichers für eine neue Aufgabe. Insbesondere wird das CR3-Register festgelegt, das ein Seitenverzeichnis für den Zugriff auf den Speicher in einem neuen Kontext enthält.

 loadsegment(fs,next->tss.fs); loadsegment(gs,next->tss.gs); 

FS und GS werden für eine neue Aufgabe wiederhergestellt. Dies stellt die korrekte Ausrichtung sicher und im Falle eines Problems wird das Nullsegment geladen.

 loaddebug(prev,7); 

Schließlich wird das Debug-Steuerregister nun mit TSS gespeichert und umgeschaltet. Bisher wurde dieses Register nur überprüft und nicht zur Speicherung verwendet.

Linux 2.4 (2001)


In Version 2.4 wurden viele neue Funktionen eingeführt, z. B. Kernel-Threads und Task-Warteschlangen. Trotz dieser und einiger Änderungen im Scheduler hat sich der Kontextwechsel im Vergleich zu Version 2.2 nicht wesentlich geändert, obwohl die Aktualisierung von TR zugunsten des Ersetzens aller Registerdaten eingestellt wurde. Ich nenne es inoffiziell den "letzten Legacy-Kernel", da alle zukünftigen Versionen die 64-Bit-x86-Architektur verwenden.



Linux 2.4.0 (eingebauter Assembler)
 /** include/asm-i386/system.h */ #define switch_to(prev,next,last) do { asm volatile("pushl %%esi\n\t" "pushl %%edi\n\t" "pushl %%ebp\n\t" "movl %%esp,%0\n\t" /* save ESP */ "movl %3,%%esp\n\t" /* restore ESP */ "movl $1f,%1\n\t" /* save EIP */ "pushl %4\n\t" /* restore EIP */ "jmp __switch_to\n" "1:\t" "popl %%ebp\n\t" "popl %%edi\n\t" "popl %%esi\n\t" :"=m" (prev->thread.esp),"=m" (prev->thread.eip), "=b" (last) :"m" (next->thread.esp),"m" (next->thread.eip), "a" (prev), "d" (next), "b" (prev)); } while (0) 

Der Kontextwechsel im 2.4-Kernel nimmt nur wenige geringfügige Änderungen vor: EBX wird nicht mehr gepusht, sondern ist in der Ausgabe des integrierten Assemblers enthalten. Es wurde ein neues Eingabeargument angezeigt last, das denselben Wert wie enthält prev. Es wird über EBX übertragen, aber nicht verwendet.

 :"=m" (prev->thread.esp),"=m" (prev->thread.eip), :"m" (next->thread.esp),"m" (next->thread.eip), 

E / A-Operanden verweisen jetzt auf Stapel- / Befehlszeiger für Kernel-Threads. Kontextschalter, die verwendet werden, um auf Stapelzeiger von TSS zu verweisen.

Linux 2.4.0 (C)
 /** arch/i386/kernel/process.c */ void __switch_to(struct task_struct *prev_p, struct task_struct *next_p) { struct thread_struct *prev = &prev_p->thread, *next = &next_p->thread; struct tss_struct *tss = init_tss + smp_processor_id(); unlazy_fpu(prev_p); tss->esp0 = next->esp0; asm volatile("movl %%fs,%0":"=m" (*(int *)&prev->fs)); asm volatile("movl %%gs,%0":"=m" (*(int *)&prev->gs)); /* Restore %fs and %gs. */ loadsegment(fs, next->fs); loadsegment(gs, next->gs); /* Now maybe reload the debug registers */ if (next->debugreg[7]){ loaddebug(next, 0); loaddebug(next, 1); loaddebug(next, 2); loaddebug(next, 3); /* no 4 and 5 */ loaddebug(next, 6); loaddebug(next, 7); } if (prev->ioperm || next->ioperm) { if (next->ioperm) { memcpy(tss->io_bitmap, next->io_bitmap, IO_BITMAP_SIZE*sizeof(unsigned long)); tss->bitmap = IO_BITMAP_OFFSET; } else tss->bitmap = INVALID_IO_BITMAP_OFFSET; } } 

Der C-Code-Teil hat einige Dinge geändert. Jede Erwähnung des TR-Registers ist verschwunden, stattdessen ändern wir direkt das aktive TSS für den aktuellen Prozessor. Wie im Inline-Assembler verweist jede Task auf TSS-Daten in thread_struct innerhalb von task_struct. Jede aktive CPU verwendet dediziertes TSS von GDT und aktualisiert diese Felder direkt.

 void __switch_to(struct task_struct *prev_p, struct task_struct *next_p) 

Den Zeigern der vorherigen und nächsten Aufgaben wurde ein Suffix hinzugefügt _p. Dies ist eine kleine , aber wichtige Nuance da prevund nextzu konvertieren , um Kernel - Threads gehen.

 struct thread_struct *prev = &prev_p->thread, *next = &next_p->thread; 

Zeiger auf TSS-Daten für jede Aufgabe werden bestimmt.

 tss->esp0 = next->esp0; 

Ersetzen des Stapelversatzrings 0 durch den Versatz der neuen Aufgabe. Bis ein erneutes Laden der Seitentabelle erzwungen wird ...

 asm volatile("movl %%fs,%0":"=m" (*(int *)&prev->fs)); asm volatile("movl %%gs,%0":"=m" (*(int *)&prev->gs)); 

Speichern von FS und GS für die alte Aufgabe. Der Zweck dieser Segmente ist immer noch unverständlich, aber sie werden irgendwie verwendet. In Version 2.6 werden sie daher für den lokalen FS: Thread-Speicher und den GS: Pro-Prozessor-Datenbereich verwendet.

 if (prev->ioperm || next->ioperm) { if (next->ioperm) { memcpy(tss->io_bitmap, next->io_bitmap, IO_BITMAP_SIZE*sizeof(unsigned long)); tss->bitmap = IO_BITMAP_OFFSET; 

Legt die Portzuordnungs-E / A-Berechtigungen im aktiven TSS für die bevorstehende Aufgabe fest.

 } else tss->bitmap = INVALID_IO_BITMAP_OFFSET; 

Zeigt die E / A-Berechtigungen für das aktive TSS für eine bekannte ungültige Bitmap (0x8000) an.

Linux 2.6: Popularität (2003)


Als Kernel 2.5 abreiste, erreichte der Linear-Run-Scheduler die Grenze der praktischen Verwendung, und AMD veröffentlichte eine Erweiterung für x86, die die sofortige Aufmerksamkeit der Kernel-Entwickler erforderte: x86-64.

Linux 2.6.0


In Kernel 2.6.0 wurde ein Kernel-Scheduler mit konstanter Laufzeit angezeigt. Dies ist zwar ein Fortschritt gegenüber dem vorherigen linearen Scheduler, aber in 2.6.23 wurde er schließlich durch einen vollständig fairen Scheduler (CFS) ersetzt. Andererseits hat die neue 64-Bit-Architektur die bislang bedeutendsten Änderungen vorgenommen.

Linux 2.6.0: i386 Version


Dies ist das neueste Erscheinungsbild des 32-Bit-Kontextschalters im Artikel.



Linux 2.6.0 (i386 integrierter Assembler)
 /** include/asm-i386/system.h */ #define switch_to(prev,next,last) do { unsigned long esi,edi; asm volatile("pushfl\n\t" "pushl %%ebp\n\t" "movl %%esp,%0\n\t" /* save ESP */ "movl %5,%%esp\n\t" /* restore ESP */ "movl $1f,%1\n\t" /* save EIP */ "pushl %6\n\t" /* restore EIP */ "jmp __switch_to\n" "1:\t" "popl %%ebp\n\t" "popfl" :"=m" (prev->thread.esp),"=m" (prev->thread.eip), "=a" (last),"=S" (esi),"=D" (edi) :"m" (next->thread.esp),"m" (next->thread.eip), "2" (prev), "d" (next)); } while (0) 


Vier Zeilen gelöscht. ESI und EDI wurden zuvor auf den Stapel verschoben, werden jetzt jedoch über E / A-Operanden übertragen.

Linux 2.6.0 (i386 C)
 /** arch/i386/kernel/process.c */ struct task_struct * __switch_to(struct task_struct *prev_p, struct task_struct *next_p) { struct thread_struct *prev = &prev_p->thread, *next = &next_p->thread; int cpu = smp_processor_id(); struct tss_struct *tss = init_tss + cpu; __unlazy_fpu(prev_p); load_esp0(tss, next->esp0); /* Load the per-thread Thread-Local Storage descriptor. */ load_TLS(next, cpu); asm volatile("movl %%fs,%0":"=m" (*(int *)&prev->fs)); asm volatile("movl %%gs,%0":"=m" (*(int *)&prev->gs)); /* Restore %fs and %gs if needed. */ if (unlikely(prev->fs | prev->gs | next->fs | next->gs)) { loadsegment(fs, next->fs); loadsegment(gs, next->gs); } /* Now maybe reload the debug registers */ if (unlikely(next->debugreg[7])) { loaddebug(next, 0); loaddebug(next, 1); loaddebug(next, 2); loaddebug(next, 3); /* no 4 and 5 */ loaddebug(next, 6); loaddebug(next, 7); } if (unlikely(prev->io_bitmap_ptr || next->io_bitmap_ptr)) { if (next->io_bitmap_ptr) { memcpy(tss->io_bitmap, next->io_bitmap_ptr, IO_BITMAP_BYTES); tss->io_bitmap_base = IO_BITMAP_OFFSET; } else tss->io_bitmap_base = INVALID_IO_BITMAP_OFFSET; } return prev_p; } 

Einige interessante Änderungen: der von der Funktion zurückgegebene Wert und das Erscheinungsbild des Makros unlikely(). Ich werde keine Änderungen an vorhandenem Code in [un] berücksichtigen, um ihn nicht noch einmal zu erklären. Das Makro teilt dem Codegenerator einfach mit, welche Basiseinheit zuerst angezeigt werden soll, um das Pipelining zu unterstützen.

 struct task_struct *__switch_to(...) 

__switch_to gibt jetzt einen Zeiger auf eine alte Aufgabe zurück. Dies wird nirgendwo verarbeitet, daher wurde entweder die Änderung gelöscht oder sie wollten der Konvention folgen. Zum Beispiel die klassische Konvention, dass wir, wenn eine Funktion den Status ändert, den vorherigen Status zurückgeben, um ihn später zu speichern und wiederherzustellen. Dies funktioniert zwar nicht.

 load_TLS(next, cpu); 

Aktualisiert die lokalen Thread-Store-Deskriptoren in GDT für eine neue Aufgabe. Linux 2.6 führt drei TLS-Einträge in GDT für bestimmte Stream-Segmente ein. Glibc verwendet also das erste Segment, Wine - das zweite. Jetzt verwenden wir nacheinander das FS-Segmentregister, um nach Bedarf auf den lokalen Thread-Speicher zu verweisen.

 if (unlikely(prev->fs | prev->gs | next->fs | next->gs)) { 

Jetzt stellen wir nur FS und GS wieder her, wenn sie von der vorherigen oder nächsten Aufgabe verwendet wurden.

Linux 2.6.0: x86_64 Version


Der x86_64-Kontextschalter unterscheidet sich erheblich vom 32-Bit-Kontextschalter. Dieses Mal werden wir den Code sorgfältig studieren, da wir uns in Zukunft nur noch auf die 64-Bit-Version konzentrieren werden.



Linux 2.6.0 (eingebauter Assembler x86_64)
 /** include/asm-x86_64/system.h */ #define SAVE_CONTEXT "pushfq ; pushq %%rbp ; movq %%rsi,%%rbp\n\t" #define RESTORE_CONTEXT "movq %%rbp,%%rsi ; popq %%rbp ; popfq\n\t" #define __EXTRA_CLOBBER ,"rcx","rbx","rdx","r8","r9","r10","r11","r12","r13","r14","r15" #define switch_to(prev,next,last) asm volatile(SAVE_CONTEXT "movq %%rsp,%P[threadrsp](%[prev])\n\t" /* save RSP */ "movq %P[threadrsp](%[next]),%%rsp\n\t" /* restore RSP */ "call __switch_to\n\t" ".globl thread_return\n" "thread_return:\n\t" "movq %%gs:%P[pda_pcurrent],%%rsi\n\t" "movq %P[thread_info](%%rsi),%%r8\n\t" "btr %[tif_fork],%P[ti_flags](%%r8)\n\t" "movq %%rax,%%rdi\n\t" "jc ret_from_fork\n\t" RESTORE_CONTEXT : "=a" (last) : [next] "S" (next), [prev] "D" (prev), [threadrsp] "i" (offsetof(struct task_struct, thread.rsp)), [ti_flags] "i" (offsetof(struct thread_info, flags)), [tif_fork] "i" (TIF_FORK), [thread_info] "i" (offsetof(struct task_struct, thread_info)), [pda_pcurrent] "i" (offsetof(struct x8664_pda, pcurrent)) : "memory", "cc" __EXTRA_CLOBBER) 

Das Makro wurde in x86_64 aktualisiert _switch_to(), daher müssen Sie seine Zeilen erneut durchgehen. Viele Änderungen sind einfach Registernamen ( r..stattdessen e..). Es gibt einige andere Helfer, die ich oben angegeben habe.

 asm volatile(SAVE_CONTEXT 

Speichert den Kernel-Kontext auf dem Stapel über das oben gezeigte Hilfsmakro. Sehr ähnlich der 32-Bit-Version, mit Ausnahme der neuen Registernamen. Das Makro wird am Ende des integrierten Assembler-Blocks mit RESTORE_CONTEXT gepaart.

 "movq %%rsp,%P[threadrsp](%[prev])\n\t" /* save RSP */ 

Speichert den aktuellen Stapelzeiger auf das TSS der alten Task. Beachten Sie die neue Notation, die im Abschnitt [threadrsp]für Eingabeoperanden definiert ist: Dies ist der direkte Offset von thread.rsp in task_struct. %PDereferenziert den prev: threadsp-Zeiger, um sicherzustellen, dass der aktualisierte SP ordnungsgemäß gespeichert wird.

 "movq %P[threadrsp](%[next]),%%rsp\n\t" /* restore RSP */ 

Stellt den Stapelzeiger einer neuen Aufgabe wieder her.

 "call __switch_to\n\t" 

Ruft Teil C des Kontextwechsels auf. Es wird im nächsten Abschnitt beschrieben.

 ".globl thread_return\n" 

Definiert eine globale Bezeichnung thread_return.

 "thread_return:\n\t" 

Übergangspunkt für thread_return. Rein mechanisch sollte der Befehlszeiger ihm zum nächsten Befehl folgen. Wird weder im Kernel noch in der Bibliothek verwendet (z. B. glibc). Ich vermute, dass Pthreads es verwenden könnten ... aber es scheint nicht so zu sein.

 "movq %%gs:%P[pda_pcurrent],%%rsi\n\t" 

Legt den Index für die aktuelle Aufgabe fest, indem auf den Pro-Prozess-Datenbereich (PDA) verwiesen wird. Im Kernelmodus sollte GS immer auf Daten für jeden Prozessor zeigen.

 "movq %P[thread_info](%%rsi),%%r8\n\t" 

Verschiebt die Struktur thread_infonach r8. Dies ist neu in Linux 2.6 und im Wesentlichen eine leichte Version task_struct, die leicht auf den Stack passt.

 "btr %[tif_fork],%P[ti_flags](%%r8)\n\t" 

Speichert den Bitwert TIF_FORK in CF thread_info->flagsund setzt das Bit in der Struktur zurück. Nach einigen Zeilen wird dieses Bit nach dem Fork / Cloning gesetzt und zum Ausführen von ret_from_fork verwendet.

 "movq %%rax,%%rdi\n\t" 

Speichert den task_structvorherigen Stream in der RDI. Die letzte Anweisung, die mit EAX funktioniert __switch_to, besteht darin, die C-Funktion aufzurufen , die prevzu EAX zurückkehrt.

 "jc ret_from_fork\n\t" 

Wenn dieser Thread eine neue Gabel / ein neuer Klon ist, gehen Sie zu ret_from_fork, um erneut zu starten.

 : "=a" (last) 

Der vorherige Stream wird an EAX zurückgegeben.

 : [next] "S" (next), [prev] "D" (prev), [threadrsp] "i" (offsetof(struct task_struct, thread.rsp)), [ti_flags] "i" (offsetof(struct thread_info, flags)), [tif_fork] "i" (TIF_FORK), [thread_info] "i" (offsetof(struct task_struct, thread_info)), [pda_pcurrent] "i" (offsetof(struct x8664_pda, pcurrent)) 

Eingabelinks für Inline-Assembler. Die meisten von ihnen sind direkte Operanden mit Offsets. Oben haben wir sie bereits durchlaufen.

 : "memory", "cc" __EXTRA_CLOBBER) 

Linux 2.6.0 (x86_64 C)
 /** arch/x86_64/kernel/process.c */ struct task_struct *__switch_to(struct task_struct *prev_p, struct task_struct *next_p) { struct thread_struct *prev = &prev_p->thread, *next = &next_p->thread; int cpu = smp_processor_id(); struct tss_struct *tss = init_tss + cpu; unlazy_fpu(prev_p); tss->rsp0 = next->rsp0; asm volatile("movl %%es,%0" : "=m" (prev->es)); if (unlikely(next->es | prev->es)) loadsegment(es, next->es); asm volatile ("movl %%ds,%0" : "=m" (prev->ds)); if (unlikely(next->ds | prev->ds)) loadsegment(ds, next->ds); load_TLS(next, cpu); /* Switch FS and GS. */ { unsigned fsindex; asm volatile("movl %%fs,%0" : "=g" (fsindex)); if (unlikely(fsindex | next->fsindex | prev->fs)) { loadsegment(fs, next->fsindex); if (fsindex) prev->fs = 0; } /* when next process has a 64bit base use it */ if (next->fs) wrmsrl(MSR_FS_BASE, next->fs); prev->fsindex = fsindex; } { unsigned gsindex; asm volatile("movl %%gs,%0" : "=g" (gsindex)); if (unlikely(gsindex | next->gsindex | prev->gs)) { load_gs_index(next->gsindex); if (gsindex) prev->gs = 0; } if (next->gs) wrmsrl(MSR_KERNEL_GS_BASE, next->gs); prev->gsindex = gsindex; } /* Switch the PDA context. */ prev->userrsp = read_pda(oldrsp); write_pda(oldrsp, next->userrsp); write_pda(pcurrent, next_p); write_pda(kernelstack, (unsigned long)next_p->thread_info + THREAD_SIZE - PDA_STACKOFFSET); /* Now maybe reload the debug registers */ if (unlikely(next->debugreg7)) { loaddebug(next, 0); loaddebug(next, 1); loaddebug(next, 2); loaddebug(next, 3); /* no 4 and 5 */ loaddebug(next, 6); loaddebug(next, 7); } /* Handle the IO bitmap */ if (unlikely(prev->io_bitmap_ptr || next->io_bitmap_ptr)) { if (next->io_bitmap_ptr) { memcpy(tss->io_bitmap, next->io_bitmap_ptr, IO_BITMAP_BYTES); tss->io_bitmap_base = IO_BITMAP_OFFSET; } else { tss->io_bitmap_base = INVALID_IO_BITMAP_OFFSET; } } return prev_p; } 

In Version x86_64 wurden dem C-Kontextwechselcode mehrere Änderungen hinzugefügt. Ich werde einfache Falländerungen nicht wiederholen (z. B. Umbenennen von EAX in RAX).

 asm volatile("movl %%es,%0" : "=m" (prev->es)); if (unlikely(next->es | prev->es)) loadsegment(es, next->es); 

Speichert das ES-Segment für die alte Aufgabe und lädt dann bei Bedarf das neue.

 asm volatile ("movl %%ds,%0" : "=m" (prev->ds)); if (unlikely(next->ds | prev->ds)) loadsegment(ds, next->ds); 

Speichert ein Datensegment für eine alte Aufgabe und lädt dann bei Bedarf ein neues.

 unsigned fsindex; asm volatile("movl %%fs,%0" : "=g" (fsindex)); if (unlikely(fsindex | next->fsindex | prev->fs)) { loadsegment(fs, next->fsindex); if (fsindex) prev->fs = 0; } 

Verschiebt das FS-Segment nach fsindexund lädt den FS bei Bedarf für eine neue Aufgabe. Wenn eine alte oder neue Aufgabe einen gültigen Wert für FS hat, wird im Prinzip etwas an seiner Stelle geladen (möglicherweise NULL). FS wird normalerweise für die Speicherung lokaler Streams verwendet, es gibt jedoch auch andere Verwendungszwecke, je nachdem, wann eine Kontextumschaltung erfolgt. Für GS wird genau derselbe Code verwendet, sodass keine Wiederholung erforderlich ist. GS ist normalerweise ein Segment für thread_info.

 if (next->fs) wrmsrl(MSR_FS_BASE, next->fs); 

Wenn das FS-Register in der nächsten Aufgabe verwendet wird, müssen Sie überprüfen, ob der gesamte 64-Bit-Wert geschrieben ist. Denken Sie daran, dass Segmentregister ein Artefakt der 16/32-Bit-Ära sind. Eine spezielle Funktion überprüft daher, ob die oberen 32 Bit geschrieben sind.

 prev->fsindex = fsindex; 

Speichern Sie FS für die alte Aufgabe.

 prev->userrsp = read_pda(oldrsp); write_pda(oldrsp, next->userrsp); write_pda(pcurrent, next_p); write_pda(kernelstack, (unsigned long)next_p->thread_info + THREAD_SIZE - PDA_STACKOFFSET); 

Aktualisieren des PDA für eine bevorstehende Aufgabe, einschließlich Speichern des alten RSP (Syscall) der alten Aufgabe. Der PDA wird mit Stream- und Stack-Informationen aktualisiert.

Linux 3.0: modernes Betriebssystem (2011)


Lassen Sie sich von der Zahl nicht täuschen. Tatsächlich wurde Version 3.0 fast 8 Jahre nach 2.6.0 veröffentlicht. Eine Vielzahl von Änderungen verdient ein ganzes Buch, und ich kann Ihnen nicht alles erzählen. Bei der Kontextumschaltung werden i386 und x86_64 in x86 mit separaten Prozessdateien (process_32.c und process_64.s) kombiniert. Dieser Artikel ist bereits zu umfangreich, sodass wir die x86_64-Version erst später analysieren werden . Wir werden nur einige Details skizzieren und uns die neueste LTS genauer ansehen.



Linux 3.0.1 (eingebauter Assembler x86_64)
 /** arch/x86/include/asm/system.h */ #define SAVE_CONTEXT "pushf ; pushq %%rbp ; movq %%rsi,%%rbp\n\t" #define RESTORE_CONTEXT "movq %%rbp,%%rsi ; popq %%rbp ; popf\t" #define __EXTRA_CLOBBER \ ,"rcx","rbx","rdx","r8","r9","r10","r11","r12","r13","r14","r15" #define switch_to(prev, next, last) asm volatile(SAVE_CONTEXT "movq %%rsp,%P[threadrsp](%[prev])\n\t" /* save RSP */ "movq %P[threadrsp](%[next]),%%rsp\n\t" /* restore RSP */ "call __switch_to\n\t" "movq "__percpu_arg([current_task])",%%rsi\n\t" __switch_canary "movq %P[thread_info](%%rsi),%%r8\n\t" "movq %%rax,%%rdi\n\t" "testl %[_tif_fork],%P[ti_flags](%%r8)\n\t" "jnz ret_from_fork\n\t" RESTORE_CONTEXT : "=a" (last) __switch_canary_oparam : [next] "S" (next), [prev] "D" (prev), [threadrsp] "i" (offsetof(struct task_struct, thread.sp)), [ti_flags] "i" (offsetof(struct thread_info, flags)), [_tif_fork] "i" (_TIF_FORK), [thread_info] "i" (offsetof(struct task_struct, stack)), [current_task] "m" (current_task) __switch_canary_iparam : "memory", "cc" __EXTRA_CLOBBER) 

Acht Jahre - und switch_to()nur vier Änderungen im Makro . Zwei von ihnen sind miteinander verbunden und nichts radikal Neues.

 movq "__percpu_arg([current_task])",%%rsi\n\t 

Verschiebt neue Naht task_structzu RSI. Dies ist die „neue“ Möglichkeit, auf Aufgabeninformationen zuzugreifen: Jede CPU verfügt über ein statisches Symbol. Zuvor waren Informationen über GS verfügbar: [pda offset]. Nachfolgende RSI-Vorgänge sind dieselben wie in Version 2.6.

 __switch_canary 

Mit diesem Makro können Sie zusätzlich prüfen, ob das Makro CONFIG_CC_STACKPROTECTOR während der Kernelassemblierung aktiviert ist. Ich werde dieses Thema nicht zu tief vertiefen, außer dass dieser Mechanismus vor der Zerstörung des Stapels durch Hacker schützt . Tatsächlich speichern wir einen zufälligen Wert und überprüfen ihn später. Wenn sich die Bedeutung geändert hat, bedeutet dies Ärger.

 testl %[_tif_fork],%P[ti_flags](%%r8)\n\t jnz ret_from_fork\n\t 

Überprüft, ob eine neue Aufgabe einfach mit Klonen / Fork erstellt wurde, und fährt dann mit fort ret_from_fork(). Früher war dies eine Anweisung btr, aber jetzt verschieben wir das Zurücksetzen des Bits, bis der Aufruf endet. Der Name wurde aufgrund einer Teständerung in JNZ geändert: Wenn das Bit gesetzt ist, ist TEST (AND) positiv.

 __switch_canary_oparam 

Die Ausgabe von Stack Canary für CONFIG_CC_STACKPROTECTOR.

 __switch_canary_iparam 

Input Stack Canary für CONFIG_CC_STACKPROTECTOR

Linux 3.0.1 (x86_64 C)
 /** arch/x86/kernel/process_64.c */ __notrace_funcgraph struct task_struct * __switch_to(struct task_struct *prev_p, struct task_struct *next_p) { struct thread_struct *prev = &prev_p->thread; struct thread_struct *next = &next_p->thread; int cpu = smp_processor_id(); struct tss_struct *tss = &per_cpu(init_tss, cpu); unsigned fsindex, gsindex; bool preload_fpu; preload_fpu = tsk_used_math(next_p) && next_p->fpu_counter > 5; /* we're going to use this soon, after a few expensive things */ if (preload_fpu) prefetch(next->fpu.state); /* Reload esp0, LDT and the page table pointer: */ load_sp0(tss, next); savesegment(es, prev->es); if (unlikely(next->es | prev->es)) loadsegment(es, next->es); savesegment(ds, prev->ds); if (unlikely(next->ds | prev->ds)) loadsegment(ds, next->ds); savesegment(fs, fsindex); savesegment(gs, gsindex); load_TLS(next, cpu); __unlazy_fpu(prev_p); /* Make sure cpu is ready for new context */ if (preload_fpu) clts(); arch_end_context_switch(next_p); /* Switch FS and GS. */ if (unlikely(fsindex | next->fsindex | prev->fs)) { loadsegment(fs, next->fsindex); if (fsindex) prev->fs = 0; } /* when next process has a 64bit base use it */ if (next->fs) wrmsrl(MSR_FS_BASE, next->fs); prev->fsindex = fsindex; if (unlikely(gsindex | next->gsindex | prev->gs)) { load_gs_index(next->gsindex); if (gsindex) prev->gs = 0; } if (next->gs) wrmsrl(MSR_KERNEL_GS_BASE, next->gs); prev->gsindex = gsindex; /* Switch the PDA and FPU contexts. */ prev->usersp = percpu_read(old_rsp); percpu_write(old_rsp, next->usersp); percpu_write(current_task, next_p); percpu_write(kernel_stack, (unsigned long)task_stack_page(next_p) + THREAD_SIZE - KERNEL_STACK_OFFSET); /* Now maybe reload the debug registers and handle I/O bitmaps */ if (unlikely(task_thread_info(next_p)->flags & _TIF_WORK_CTXSW_NEXT || task_thread_info(prev_p)->flags & _TIF_WORK_CTXSW_PREV)) __switch_to_xtra(prev_p, next_p, tss); /* Preload the FPU context - task is likely to be using it. */ if (preload_fpu) __math_state_restore(); return prev_p; } 

Es gibt einige Änderungen im C-Code, aber angesichts der acht Jahre zwischen den Veröffentlichungen sind sie relativ wenige. Einige von ihnen sind beispielsweise kosmetischer Natur und stehen an der Spitze aller Erklärungen. Folgendes hat sich geändert:

 __notrace_funcgraph struct task_struct * __switch_to(...) 

Eine neue Signatur __notrace_funcgraphverhindert, dass aktive ftrace verfolgt werden switch_to.

 preload_fpu = tsk_used_math(next_p) && next_p->fpu_counter > 5; if (preload_fpu) prefetch(next->fpu.state); 

Überprüft, ob die FPU in der letzten Aufgabe für die letzten 5 Zeitscheiben verwendet wurde, und versucht dann, die Daten für die spätere Verwendung zwischenzuspeichern.

 load_sp0(tss, next); 

Lädt einen Kernel-Space-Stack-Zeiger, aktualisiert Seitentabellen und benachrichtigt den Hypervisor (falls zutreffend).

 savesegment(es, prev->es); 

Speichert das ES-Segment. Dies ist keine Innovation, sondern nur ein Ersatz für den Inline-Assembler ab 2.6:asm volatile("movl %%es,%0" : "=m" (prev->es)); .

 if (preload_fpu) clts(); 

Startet die FPU sofort neu, wenn sie wahrscheinlich verwendet wird. Anwendung clts()- die gleiche Idee , dass wir mit der ersten Version des Linux sehen: "cmpl %%ecx,%2\n\t jne 1f\n\t clts\n".

 jne 1f\n\t clts\n" arch_end_context_switch(next_p); 

Nur für die Virtualisierung relevant . Unter normalen Umständen macht eine Funktion nichts . Weitere Informationen finden Sie in der endgültigen Kernelversion.

 if (unlikely(task_thread_info(next_p)->flags & _TIF_WORK_CTXSW_NEXT || task_thread_info(prev_p)->flags & _TIF_WORK_CTXSW_PREV)) __switch_to_xtra(prev_p, next_p, tss); 

Beschäftigt sich mit Verwaltungsarbeiten, die zuvor am Ende beschrieben wurden switch_to, einschließlich Debug-Registern und E / A-Bitmap-Parametern. Wir werden Ihnen mehr darüber in der Codeüberprüfung 4.14.67 erzählen.

 if (preload_fpu) __math_state_restore(); 

Stellt die FPU wieder her, nachdem ihre Verwendung überprüft wurde. Bei einer erfolgreichen Kombination von Umständen sollten sich die Daten dank der zuvor getroffenen Vorauswahl bereits im Cache befinden.

Linux 4.14.67: neueste LTS (2018)


Dies ist unser tiefstes Eintauchen in das Innenleben des Kontextwechsels. Das Verfahren wurde seit der Veröffentlichung von 3.0 erheblich überarbeitet und der Code organisiert. Insgesamt sieht es jetzt sauberer und organisierter aus als je zuvor. Für x86_64:




Linux 4.14.67
 /** arch/x86/include/asm/switch_to.h */ #define switch_to(prev, next, last) do { prepare_switch_to(prev, next); ((last) = __switch_to_asm((prev), (next))); } while (0) 

Es sieht im Vergleich zu alten Kerneln einfach aus. Diese Reorganisation war das Ergebnis einer Lösung des Problems, als Andy Lutomirski praktisch passende Kernel-Stacks einführte .

 prepare_switch_to(prev, next); 

Stellt sicher, dass Kernelstapel verfügbar sind, bevor Sie versuchen, den Kontext zu wechseln. Dies vermeidet einen möglichen Doppelfehler oder eine Kernel-Panik beim Versuch, den Kontext zu wechseln, wenn virtuell zugeordnete Kernel-Stapel verwendet werden.

 ((last) = __switch_to_asm((prev), (next))); 

Startet den eigentlichen Kontextwechsel.

Schauen Sie sich das an prepare_switch_to, das in derselben Quelldatei definiert ist.

Linux 4.14.67
 /** arch/x86/include/asm/switch_to.h */ static inline void prepare_switch_to(struct task_struct *prev, struct task_struct *next) { #ifdef CONFIG_VMAP_STACK READ_ONCE(*(unsigned char *)next->thread.sp); #endif } 


 #ifdef CONFIG_VMAP_STACK 

Bestimmt, wann der Stapel virtuellen Speicher verwendet. Wir müssen uns nur dann auf den Kontextwechsel vorbereiten, wenn wir virtuelle Stapel verwenden. Dies ist ein Konfigurationsparameter während der Kernel-Erstellung. In vielen modernen Distributionen der Standardwert yes.

 READ_ONCE(*(unsigned char *)next->thread.sp); 

Beziehen Sie sich auf den nächsten Stapel, um Seitentabellen (pgd) zu reparieren. Das Hauptproblem besteht darin, dass wir versuchen, auf einen Zeiger zuzugreifen, der sich nicht nur außerhalb der Seiten befindet (ausgelagert), sondern aufgrund des verzögerten Ladens des vmalloc-Bereichs auch nicht im Kontext des Speichers dieser Aufgabe. Das Fehlen und die Unzugänglichkeit eines Zeigers bedeutet eine Kernel-Panik, wenn Sie das Problem nicht im Voraus lösen.

Linux 4.16.67
 /** arch/x86/entry/entry_64.S */ ENTRY(__switch_to_asm) UNWIND_HINT_FUNC /* Save callee-saved registers */ pushq %rbp pushq %rbx pushq %r12 pushq %r13 pushq %r14 pushq %r15 /* switch stack */ movq %rsp, TASK_threadsp(%rdi) movq TASK_threadsp(%rsi), %rsp #ifdef CONFIG_CC_STACKPROTECTOR movq TASK_stack_canary(%rsi), %rbx movq %rbx, PER_CPU_VAR(irq_stack_union)+stack_canary_offset #endif #ifdef CONFIG_RETPOLINE FILL_RETURN_BUFFER %r12, RSB_CLEAR_LOOPS, X86_FEATURE_RSB_CTXSW #endif /* restore callee-saved registers */ popq %r15 popq %r14 popq %r13 popq %r12 popq %rbx popq %rbp jmp __switch_to END(__switch_to_asm) 

In entry_64.S enthält Arbeit , dass die vorherigen 25 Jahren in der Linux - Inline - Assembler ausgeführt.

 UNWIND_HINT_FUNC 

Generiert QuickInfos, die vom gerade beendeten objtool-Stack-Trace-Tool verwendet werden. Dies ist für spezielle Erstellungsverfahren erforderlich, die nicht den üblichen Konventionen für C-Sprachaufrufe entsprechen. Solche Hinweise sind der Grund für die erfolgreiche Implementierung von ORC-Code-Unwinder in Version 4.6.

 pushq %rbp, %rbx, %r12, %r13, %r14, %r15 

Wir speichern die Register auf dem alten Stapel , von dem wir wechseln.

 movq %rsp, TASK_threadsp(%rdi) movq TASK_threadsp(%rsi), %rsp 

Ich tausche Stapelzeiger zwischen der alten und der neuen Aufgabe. Direkt aus der Umgebungs Montage ist nicht klar, aber enthält RDI und RSI Eingabeargumente task_struct *, prev und weiter in Übereinstimmung mit den Konventionen System V ABI. Hier ist eine Teilmenge der Register zusammen mit ihrer Verwendung:



 #ifdef CONFIG_CC_STACKPROTECTOR movq TASK_stack_canary(%rsi), %rbx movq %rbx, PER_CPU_VAR(irq_stack_union)+stack_canary_offset 

Wenn der Stapelschutz aktiviert ist, wird der Kanarienwert dieser Aufgabe an die entsprechende Stelle im Interrupt-Stapel der aktuellen CPU verschoben. Der Stapelschutz ist normalerweise standardmäßig aktiviert, daher geschieht dies normalerweise.

 #ifdef CONFIG_RETPOLINE FILL_RETURN_BUFFER %r12, RSB_CLEAR_LOOPS, X86_FEATURE_RSB_CTXSW 

Dies ist ein Schutz gegen eine mögliche Ausnutzung der Verzweigungsvorhersage (Spectre-Schwachstelle). Reines Voodoo !

 popq %r15, %r14, %r13, %r12, %rbx, %rbp 

Stellt alle Register aus dem neuen Stapel in umgekehrter Reihenfolge wieder her: (r15, r14, r13, r12, rbx, rbp)

Linux 4.16.67 ( Quelle mit Kommentaren)
 /** arch/x86/kernel/process_64.c */ __visible __notrace_funcgraph struct task_struct * __switch_to(struct task_struct *prev_p, struct task_struct *next_p) { struct thread_struct *prev = &prev_p->thread; struct thread_struct *next = &next_p->thread; struct fpu *prev_fpu = &prev->fpu; struct fpu *next_fpu = &next->fpu; int cpu = smp_processor_id(); struct tss_struct *tss = &per_cpu(cpu_tss_rw, cpu); WARN_ON_ONCE(IS_ENABLED(CONFIG_DEBUG_ENTRY) && this_cpu_read(irq_count) != -1); switch_fpu_prepare(prev_fpu, cpu); save_fsgs(prev_p); load_TLS(next, cpu); arch_end_context_switch(next_p); savesegment(es, prev->es); if (unlikely(next->es | prev->es)) loadsegment(es, next->es); savesegment(ds, prev->ds); if (unlikely(next->ds | prev->ds)) loadsegment(ds, next->ds); load_seg_legacy(prev->fsindex, prev->fsbase, next->fsindex, next->fsbase, FS); load_seg_legacy(prev->gsindex, prev->gsbase, next->gsindex, next->gsbase, GS); switch_fpu_finish(next_fpu, cpu); /* Switch the PDA and FPU contexts. */ this_cpu_write(current_task, next_p); this_cpu_write(cpu_current_top_of_stack, task_top_of_stack(next_p)); /* Reload sp0. */ update_sp0(next_p); /* Now maybe reload the debug registers and handle I/O bitmaps */ if (unlikely(task_thread_info(next_p)->flags & _TIF_WORK_CTXSW_NEXT || task_thread_info(prev_p)->flags & _TIF_WORK_CTXSW_PREV)) __switch_to_xtra(prev_p, next_p, tss); #ifdef CONFIG_XEN_PV if (unlikely(static_cpu_has(X86_FEATURE_XENPV) && prev->iopl != next->iopl)) xen_set_iopl_mask(next->iopl); #endif if (static_cpu_has_bug(X86_BUG_SYSRET_SS_ATTRS)) { unsigned short ss_sel; savesegment(ss, ss_sel); if (ss_sel != __KERNEL_DS) loadsegment(ss, __KERNEL_DS); } /* Load the Intel cache allocation PQR MSR. */ intel_rdt_sched_in(); return prev_p; } 

Mit diesem letzten Codeblock können Sie sich mit den neuesten Änderungen im Kontextwechsel vertraut machen! Wenn Sie den Artikel sofort an diesen Ort gescrollt haben, machen Sie sich keine Sorgen - ich werde die meisten Punkte hier (erneut) und detaillierter betrachten. Beachten Sie einige Ausnahmen, die in den Kontextwechsel fallen.

 __visible __notrace_funcgraph struct task_struct * __switch_to(struct task_struct *prev_p, struct task_struct *next_p) 

Die Signatur für den Kontextwechsel zu C besteht aus mehreren Teilen:

  • __visible - Dieses Attribut stellt sicher, dass durch die Optimierung während des Layoutprozesses das Zeichen nicht entfernt wird __switch_to().
  • __notrace_funcgraph - schützt __switch_to()vor dem ftrace-Tracer. Die Funktion wurde ungefähr in Version 2.6.29 hinzugefügt und bald aktiviert.
  • Die Eingabeargumente sind Zeiger auf die alten und neuen Aufgaben, die an RDI und RSI übergeben werden.

 struct thread_struct *prev = &prev_p->thread; struct thread_struct *next = &next_p->thread; struct fpu *prev_fpu = &prev->fpu; struct fpu *next_fpu = &next->fpu; 

Sammelt einige Informationen aus Eingabedaten task_struct *. Thread_struct enthält die TSS-Daten für die Task (Register usw.). Die Struktur fpu>enthält die FPU-Daten, z. B. die zuletzt verwendete CPU, die Initialisierung und die Registerwerte.

 int cpu = smp_processor_id(); 

Gibt die Prozessornummer zurück, die wir benötigen, um die GDT für TSS-Daten, die lokale Thread-Speicherung und den Vergleich des Status von Tankstellen zu steuern.

 struct tss_struct *tss = &per_cpu(cpu_tss_rw, cpu); 

Zeigt die aktuelle TSS-CPU an .

 WARN_ON_ONCE(IS_ENABLED(CONFIG_DEBUG_ENTRY) && this_cpu_read(irq_count) != -1); 

Legt fest, ob der IRQ-Stapel während der Kontextumschaltung verwendet wird, und meldet dies einmal pro Last. Dies wurde zu Beginn der Entwicklung 4.14 hinzugefügt, und tatsächlich wirkt sich dieser Code nicht auf die Kontextumschaltung aus.

 switch_fpu_prepare(prev_fpu, cpu); 

Speichert den aktuellen Status der FPU, während wir uns in der alten Aufgabe befinden.

 save_fsgs(prev_p); 

Speichert FS und GS, bevor wir den lokalen Stream-Speicher ändern.

 load_TLS(next, cpu); 

Lädt GDT für die lokale Thread-Speicherung neuer Aufgaben neu. Kopiert tls_array mechanisch aus einem neuen Stream in die GDT-Datensätze 6, 7 und 8.

 arch_end_context_switch(next_p); 

Diese Funktion wird nur bei Paravirtualisierung definiert. Ändert den Paravirt-Modus und löscht alle verbleibenden Stapelarbeiten. Eingeführt in neueren Versionen 2.6.x. Ich bin nicht zu stark in dieser Funktionalität, deshalb überlasse ich sie den Lesern zur Recherche .

 savesegment(es, prev->es); if (unlikely(next->es | prev->es)) loadsegment(es, next->es); 

Speichert das ES-Segment und lädt bei Bedarf ein neues. Ein ähnlicher DS-Aufruf wird weggelassen. Auch wenn die neue Aufgabe kein DS / ES verwendet, werden alle alten Werte gelöscht.

 load_seg_legacy(prev->fsindex, prev->fsbase, next->fsindex, next->fsbase, FS); 

Lädt neue FS-Segmente (GS weggelassen). Dadurch werden Register für 32-Bit- und 64-Bit-Registertypen erkannt und geladen. Die neue Aufgabe ist jetzt bereit für TLS.

 switch_fpu_finish(next_fpu, cpu); 

Initialisiert den Status der FPU für eine eingehende Aufgabe.

 this_cpu_write(current_task, next_p); 

Aktualisiert die aktuelle CPU ( task_struct *) - Aufgabe . Aktualisiert effektiv den Status der FPU und des PDA (Datenbereich für jeden Prozessor).

 this_cpu_write(cpu_current_top_of_stack, task_top_of_stack(next_p)); 

CPU aktualisiert den Stapelzeiger, der tatsächlich sp1 als überlastet ist erzeugten Code (Eintrag Trampolin) für die Sicherheit.

 update_sp0(next_p); 

Validierung eines neuen Stapels, um ihn zu überprüfen. Es sieht so aus, als ob hier sp0 angegeben werden sollte, nicht sp1? Sollte wahrscheinlich umbenannt werden.

 if (unlikely(task_thread_info(next_p)->flags & _TIF_WORK_CTXSW_NEXT || task_thread_info(prev_p)->flags & _TIF_WORK_CTXSW_PREV)) __switch_to_xtra(prev_p, next_p, tss); 

Aktualisiert Debug-Register und E / A-Bitmaps. Diese beiden Aufgaben wurden zuvor direkt im Kontextwechsel ausgeführt, werden jetzt jedoch in verschoben __switch_to_xtra().

 #ifdef CONFIG_XEN_PV if (unlikely(static_cpu_has(X86_FEATURE_XENPV) && prev->iopl != next->iopl)) xen_set_iopl_mask(next->iopl); 

Tauscht die E / A-Berechtigungsbits manuell gegen Xen-Paravirtualisierung aus. Anscheinend funktioniert der reguläre Flag-Schalter nicht richtig , und deshalb müssen Sie die aktuellen Bits direkt maskieren.

 if (static_cpu_has_bug(X86_BUG_SYSRET_SS_ATTRS)) { unsigned short ss_sel; savesegment(ss, ss_sel); if (ss_sel != __KERNEL_DS) loadsegment(ss, __KERNEL_DS); 

Blendet unerwartetes Verhalten SYSRET in AMD - Prozessoren, die nicht richtig Segmentbeschreibern aktualisieren.

 intel_rdt_sched_in(); 

Einige Intel-Bereinigungsaufgaben. Aktualisiert RMID und CLOSid .

 return prev_p; 

Fertig!

FAQ


Warum haben Sie diese Kernel-Versionen ausgewählt?
Die erste und letzte Version waren offensichtliche Kandidaten. Ursprünglich wollte ich vier weitere Zwischenversionen in Betracht ziehen (2.1, 2.3, 2.5 und 2.6.26), aber die Änderungen reichten nicht aus, um den Artikel zu stark aufzublasen. Sie ist schon zu groß.

Wie lange hat diese Studie gedauert?
Zwei Wochen. Eine Woche für Code-Analyse, Notizen und technische Tutorials. Dann eine Woche, um Notizen neu zu schreiben, Diagramme zu zeichnen und den Artikel zu formatieren.

4.14.67 - nicht die neueste LTS-Version?
Ich begann am 1. September mit dem Studium des Codes und nahm den Quellcode 4.14.67. Die Version 4.14.68 wurde vier Tage später fertiggestellt.

Ich werde weitere Fragen hinzufügen, sobald sie verfügbar sind.

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


All Articles