WEBVTT

00:00.000 --> 00:05.880
Die Rechen-Einheit und die Speicher

00:05.880 --> 00:09.640
Unsere Hardware hat in O-Stubs eine Rechen-Einheit und Speicher.

00:09.640 --> 00:14.680
In Mp-Stubs haben wir zusätzlich maximal sieben weitere Rechen-Einheiten.

00:14.680 --> 00:20.840
Das wollen wir multiplexen, damit wir deutlich mehr Anwendungen als Kerne gleichzeitig laufen lassen können.

00:20.840 --> 00:28.000
Jede Anwendung soll also eine eigene illusionierte Hardware mit virtueller CP und Speicher für sich haben.

00:28.000 --> 00:30.640
Dazu müssen wir beides virtualisieren.

00:30.640 --> 00:36.680
Beim Speicher ist das einfach. Wir teilen ihn in Bereiche auf. Jede Anwendung bekommt ein Stück davon.

00:36.680 --> 00:42.960
Allerdings funktioniert das bei der CPU so nicht. Wir können nicht einfach irgendwie die Instruktionen aufspalten.

00:42.960 --> 00:48.200
Allerdings können wir die Ausführungszeit auf der CPU zwischen den Anwendungen aufteilen.

00:48.200 --> 00:52.680
Aber wie kann man so eine Aufteilung umsetzen?

00:52.680 --> 00:57.200
Wir schauen uns das mal anhand zweier Funktionen an, die sich abwechseln sollen.

00:57.200 --> 01:05.640
Um es einfach zu halten, geben Foo und Ba jeweils in einer Schleife nur ihre Namen sowie eine herunterzählende Zahl aus.

01:05.640 --> 01:12.320
Erster Versuch. Wir hängen einen Aufruf vom Ba ans Ende von Foo. Ein einseitiger Aufruf.

01:12.320 --> 01:20.520
Wir sehen, dass zuerst alle Ausgaben von Foo getätigt werden, danach die Ausgaben vom Ba, welches am Ende aufgerufen wird.

01:20.520 --> 01:27.200
Wir haben also einen sequenziellen Stabilbetrieb, nicht die gewünschte gleichzeitige Ausführung.

01:27.200 --> 01:36.640
Nächster Versuch. Die Zellervariablen bekommen einen festen Speicher zugewiesen und wir setzen den Aufruf der jeweils anderen Funktion in die Schleifen,

01:36.640 --> 01:41.440
und zwar bei beiden Funktionen. Ein gegenseitiger Aufruf also.

01:41.440 --> 01:47.000
Und wenn wir das laufen lassen, dann sieht die Ausgabe auch auf einen ersten Blick gut aus.

01:47.000 --> 01:55.160
Aber wenn wir die Aufruf Hierarchie betrachten, sehen wir, dass wir jedes Mal einen neuen Funktionsaufruf tätigen und entsprechend den Stapel füllen,

01:55.160 --> 02:01.080
was in der Regel einen hohen Speicherverbrauch bedeutet und wir Gefahr laufen einen Stapelüberlauf zu provozieren,

02:01.080 --> 02:04.800
insbesondere wenn wir eine Endlosrekursion haben.

02:04.800 --> 02:13.320
Was wir statt dem Aufruf wollen, ist ein Umschalten. Der Kontext von dem alten Faden muss gesichert und erneut geladen werden.

02:13.320 --> 02:18.480
Dabei bilden uns Stapelspeicherinhalt sowie die Register den Kontext.

02:18.480 --> 02:23.240
Außerdem brauchen wir eine Umschaltfunktion. Bei uns heißt diese Kontextswitch.

02:23.240 --> 02:31.240
Sie bekommt als ersten Parameter den Stackzeiger des aktuellen Fadens und wechselt dann zum neuen Faden, den zweiten Parameter.

02:31.240 --> 02:36.760
Betrachten wir die Funktion erst einmal als Blackbox, ob sie denn unseren Anforderungen genügen würde.

02:36.760 --> 02:44.640
Wir erweitern unsere Anwendung um den Aufruf mit statischen Variablen StackBar und StackFu für die Parameter.

02:44.640 --> 02:48.120
Die Ausgabe entspricht wieder dem gewünschten Ergebnis.

02:48.120 --> 02:53.920
Und auch unter der Haube sieht es gut aus. Wir wechseln zwischen den Anwendungen Fu und Bar hin und her,

02:53.920 --> 03:00.600
und zwar ohne dass unser Speicherverbrauch stetig steigt. Also genau das, was wir erreichen wollen.

03:00.600 --> 03:06.440
Aber was muss diese mystische Umschaltfunktion Kontextswitch dafür nun tun?

03:06.440 --> 03:10.560
Zuerst müssen die Register des aktuellen Kontexts gesichert werden.

03:10.560 --> 03:18.360
Wir können nun dafür einen statischen Speicher nehmen, aber noch einfacher ist, wir pushen diese einfach der Reihe nach auf den Stack.

03:18.360 --> 03:22.760
Dann müssen wir uns lediglich den aktuellen Stapelzeiger merken.

03:22.760 --> 03:30.600
Vom neuen Kontext laden wir nun den Stapelzeiger und stellen die dort irgendwann zuvor auf diesen Stapelgesicherten Register wieder her.

03:30.600 --> 03:36.880
Der Instruktionszeiger ist das Register rep. Also wenn wir diesen dann wie die anderen Register wieder herstellen,

03:36.880 --> 03:40.960
können wir gleich wieder an der entsprechenden Stelle fortsetzen, oder?

03:40.960 --> 03:46.480
Nun leider ist es nicht ganz so leicht, denn wir können nicht einfach eine Adresse in das Register rep. schreiben.

03:46.480 --> 03:49.560
Das erlaubt uns die x86 Architektur nicht.

03:49.560 --> 03:58.040
Stattdessen müssen wir einen kleinen Umweg nehmen, indem wir die gewünschte Adresse auf den Stack pushen und dann die Instruktion red ausführen.

03:58.040 --> 04:03.240
Diese poppt vom Stack den aktuellen Eintrag und setzt das als Instruktionszeiger.

04:03.240 --> 04:06.040
Konkret der Kontextwechsel am Beispiel.

04:06.040 --> 04:12.920
Wir haben unsere Struktur Stackpointe, welche derzeit nur einen einzigen Zeiger, den Kernel Stapelzeiger beinhaltet.

04:12.920 --> 04:20.600
In BST werden wir auch noch den Benutzer Stapelzeiger speichern müssen, deshalb packen wir das schon einmal vorsorglich in eine Struktur.

04:20.600 --> 04:25.160
Wir haben für jede Anwendung foo und bar eine solche Struktur instanziert.

04:25.160 --> 04:31.560
Und wir gehen auch davon aus, dass diese bereits sinnvoll initialisiert wurden und wir also mitten im Betrieb drin sind,

04:31.560 --> 04:38.200
gerade in der Abarbeitung von foo, kurz vor dem Aufruf vom Kontext Switch, mit welchem wir zu bar wechseln wollen.

04:38.200 --> 04:44.920
Wie wir von der Aufrufkonvention bereits wissen, müssen vor einem Funktionsaufruf zuerst die flüchtigen Register gesichert werden.

04:44.920 --> 04:53.240
Das generiert uns der Copiler bereits hin, hier entscheidet er beispielsweise, dass er Acht gesichert werden muss, indem es auf den Stack gepusht wird.

04:53.240 --> 04:57.720
Beim Funktionsaufruf wird durch das Call noch die Rücksprungadresse auf den Stack gepusht,

04:57.720 --> 05:05.880
für uns hier das symbolische Label elfoo und dann der Instruktionszeiger auf den Beginn der Funktion Kontext Switch gesetzt.

05:05.880 --> 05:13.480
Da die relevanten flüchtigen Register ja bereits vor dem Aufruf gesichert wurden, müssen wir uns nun nur noch um alle nicht flüchtigen Register kümmern.

05:13.480 --> 05:17.320
Und diese können nun ebenfalls auf den Stack gepusht werden.

05:17.320 --> 05:21.880
Den Stackpointer selbst auf den Stack zu legen, ist natürlich nicht wirklich sinnvoll.

05:21.880 --> 05:25.640
Stattdessen schreiben wir dessen aktuelle Adresse in Stackfoo.

05:25.640 --> 05:28.520
Damit haben wir den Kontext von Foo gesichert.

05:28.520 --> 05:32.120
Nun beginnt das umgekehrte Spiel mit dem Kontext von bar.

05:32.120 --> 05:38.040
Wir setzen den Stackpointer rsp entsprechend der in Stackbar gespeicherten Adresse.

05:38.040 --> 05:45.320
Der Stackpointer wird nun hoffentlich auf einen Speicher zeigen, auf dem irgendwann vorher der Kontext von bar gesichert wurde,

05:45.320 --> 05:49.880
also konkret auf die zuletzt gesicherten nicht flüchtigen Register von bar.

05:49.880 --> 05:52.120
Diese stellen wir wieder her.

05:52.120 --> 05:59.080
Danach führen wir ein Return aus, welches vom Stack die Rücksprungadresse nimmt und den Instruktionszeiger entsprechend setzt,

05:59.080 --> 06:02.120
was direkt hinter dem Funktionsaufruf sein wird.

06:02.120 --> 06:09.000
Anschließend wird der vom Übersetzer generierte Code zur Wiederherstellung aller vor dem Aufruf gesicherten flüchtigen Register aufgerufen,

06:09.000 --> 06:11.240
hier zum Beispiel R10.

06:11.240 --> 06:18.840
Und die Ausführung von bar wird dort fortgesetzt, wo sie beim Aufruf von ihrem letzten Kontext Switch pausiert wurde.

06:18.840 --> 06:24.280
In dem Beispiel sind wir bis jetzt davon ausgegangen, dass wir bereits mitten im Ablauf stecken.

06:24.280 --> 06:32.760
Wie mache ich das aber ganz zu Beginn, wenn ich bereits beim ersten Aufruf vom Kontext Switch einen validen Zielkontext brauche, in den ich wechseln kann?

06:32.760 --> 06:37.960
Ich muss dafür einen Stack handys vorbereiten, in dem vom Kontext Switch benötigten Format,

06:37.960 --> 06:41.160
welcher dafür sorgt, dass in die Coroutine gesprungen wird.

06:41.160 --> 06:45.800
In unserem Beispiel starte diese mit der parameterlosen Funktion bar.

06:45.800 --> 06:53.960
Somit muss auf dem Stack auch die Startadresse dieser Funktion stehen, in welche dann mittels Red Instruktion gesprungen wird.

06:53.960 --> 07:00.600
Unser Kontext Switch erwartet außerdem vorher auf dem Stack die gesicherten Einträge für den nicht flüchtigen Register.

07:00.600 --> 07:05.480
Und der Stackzeiger muss dabei auf den letzten Registereintrag zeigen.

07:05.480 --> 07:09.240
Aber ich habe eigentlich noch gar keinen Stack für diese Coroutine.

07:09.240 --> 07:16.920
Also nehme ich dafür einfach einen ausreichend großen Speicherbereich und definiere das obere Ende als top of stack.

07:16.920 --> 07:21.880
In den Eintrag an der höchsten Adresse schreibe ich also den Funktionszeiger zu bar.

07:21.880 --> 07:24.920
Für die flüchtigen Register brauche ich nun noch Werte.

07:24.920 --> 07:33.080
Da am Anfang der Funktion keine Annahmen über die Inhalte dieser Register getroffen werden, kann ich irgendwas reinschreiben, zum Beispiel 0.

07:33.080 --> 07:43.960
Die Variable für den Kernstack Zeiger von bar wird dann auf den Eintrag 56 bytes unterhalb von unserem top of stack gesetzt, in dem auch der initiale Wert des letzten Registers steht.

07:43.960 --> 07:46.520
Nun kann Kontext Switch ausgeführt werden.

07:46.520 --> 07:54.840
Es wird, nachdem es zuvor vom foo alle Register gesichert hat, die nicht flüchtigen Register auf 0 setzen und an den Anfang vom bar springen.

07:54.840 --> 07:57.640
Wir haben den ersten Einsprung geschafft.

07:57.640 --> 08:05.160
Allerdings müssen wir aufpassen. Sollte die Funktion bar nun selbst zurückkehren, also ein return ausführen,

08:05.160 --> 08:11.720
nun dann wird der Inhalt, der über unserem top of stack steht, genommen als Adresse interpretiert und angesprungen.

08:11.720 --> 08:15.640
Und dort kann irgendetwas stehen, aber sicherlich nichts vernünftiges.

08:15.640 --> 08:24.200
Ein Invalid Upcode oder General Protection Fault sind wahrscheinlich, aber es kann auch vorkommen, dass es zufällig an irgendeiner anderen Stelle weiterläuft.

08:24.200 --> 08:26.200
Alles nichts was wir wollen.

08:26.200 --> 08:33.720
Besser, wir entwickeln eine defensive Lösung, welche uns in so einem Fall eine Fehlermeldung anzeigt und das System anhält.

08:33.720 --> 08:39.720
Zum Beispiel über die Funktion context panic, welche über die Funktion kernel panic eben dies erreicht.

08:39.720 --> 08:45.400
Diese Adresse muss nun am Anfang des stacks stehen, vor der Adresse der Funktion bar.

08:45.400 --> 08:53.160
Dadurch würde nun ein return in bar an den Beginn der Funktion context panic springen und wie gewünscht das System anhalten.

08:53.160 --> 09:01.240
Wir sehen, durch die Funktionsadressen auf dem Stack ist es eigentlich einfach in diese middle thread Instruktion zu springen.

09:01.240 --> 09:04.920
Was aber, wenn wir Funktionsparameter haben?

09:04.920 --> 09:09.560
So soll bar nun mit einem integer Parameter i aufgerufen werden.

09:09.560 --> 09:15.560
Nach der System 5 API wird diese in RDI, einem Flüchtigenregister erwartet.

09:15.560 --> 09:22.200
Dieser kann jedoch einen beliebigen Wert haben, je nachdem was foo vor dem context switch dort reingeschrieben hat.

09:22.200 --> 09:26.600
Wir setzen auf dem Stack nur die Werte für die nicht flüchtigen Register.

09:26.600 --> 09:30.200
Natürlich könnten wir auch die flüchtigen Register in context switch sichern,

09:30.200 --> 09:35.960
aber das wäre ein massiver Overhead bei jedem context wechsel, keine akzeptable Lösung für uns.

09:35.960 --> 09:39.000
Aber brauchen wir auch nicht.

09:39.000 --> 09:44.360
Wir können auch mit dem vorhandenen Ablauf arbeiten, indem wir den Umweg über eine Hilfsfunktion gehen.

09:44.360 --> 09:56.680
Diese soll dabei lediglich aus einem nicht flüchtigen Register, beispielsweise R15, den Wert lesen und nach RDI, dem Register für den ersten Parameter, kopieren und dann die eigentliche Funktion aufrufen.

09:56.680 --> 10:05.640
Und beim Vorbereiten des Stacks geben wir an der Speicherstelle, in der wir den initialen Wert für R15 schreiben, eben den gewünschten Parameterwert an.

10:05.640 --> 10:10.280
Außerdem muss noch die Adresse, durch die der Hilfsfunktion ausgetauscht werden.

10:10.280 --> 10:17.320
Die Hilfsfunktion selbst schreibt man natürlich in der Sampler, damit er nicht der Übersetzer zuvor irgendetwas mit den Registern anstellt.

10:17.320 --> 10:28.360
Außerdem besteht nun noch die Möglichkeit, die Hilfsfunktion so zu erweitern, damit sie nicht nur statisch bar anspringt, sondern generisch ist und auch für FU verwendet werden kann.

10:28.360 --> 10:32.680
Fassen wir die Voraussetzungen für den Start einer Co-Routine zusammen.

10:32.680 --> 10:36.040
Wir müssen einen Speicherbereich als Stack reservieren.

10:36.040 --> 10:38.200
In der Praxis sollten 4K reichen.

10:38.200 --> 10:43.480
Diesen für Context Switch vorbereiten und den Stackzeiger der Co-Routine entsprechend setzen.

10:43.480 --> 10:48.680
Damit sind wir in der Lage mittels Context Switch in eine neue Co-Routine zu wechseln.

10:48.680 --> 11:00.760
Bleibt die Frage, wie komme ich in die allererste Co-Routine direkt nach dem Start des Betriebssystems, nachdem wir die Geräteinitalisierung abgeschlossen haben und den Co-Routinen die Kontrolle übergeben wollen.

11:00.760 --> 11:06.120
Das ist eigentlich sogar ziemlich einfach, ohne dass wir dafür eine neue Funktion brauchen.

11:06.120 --> 11:12.200
Es reicht, wenn man einfach einen temporären Stackpointer anlegt und diese als Current-Parameter übergibt.

11:12.200 --> 11:21.960
Context Switch pusht dann einfach die Register auf den aktuellen Stack, den wir danach eh nicht mehr brauchen und schreibt den Wert des Stackzeigers in die temporäre Variable.

11:21.960 --> 11:26.360
Danach wird der Kontext der ersten Choroutine geladen und diese ausgeführt.

