{{Navigation |Zurück = [[Teil 8 - Ein erstes Programm]] }} ==Ziel== Ein einzelnes Programm funktioniert nach dem letzten Teil, aber zwei Programme können sich ins Gehege kommen, weil sie denselben Speicher benutzen - wir brauchen noch irgendeinen Mechanismus, um sie voneinander zu trennen: Virtuellen Speicher, den wir über [[Paging]] umsetzen werden. ==Tasks im Konflikt== Die Voraussetzungen, um mehrere Programme starten zu können, sind mittlerweile vorhanden: Anstatt nur das erste Multibootmodul zu starten, könnte ''init_multitasking'' eine Schleife enthalten, die einfach alle Multiboot-Module nacheinander startet. Wenn wir das mit diesem Testprogramm machen, scheint zunächst auch alles zu funktionieren und wir bekommen mit ''-initrd test.bin,test.bin'' die folgende Ausgabe:
0123401234Es gibt da nur ein kleines Problem: Alle diese Programme erwarten ihren Code und ihre Daten an einer bestimmten Stelle im Speicher. Und das zweite Programm möchte dabei natürlich denselben Speicher verwenden wie das erste. Dass es trotzdem scheinbar funktioniert, haben wir zwei Tatsachen zu verdanken: * Es ist zweimal dasselbe Programm. Dass beim Laden das erste Programm direkt wieder vom zweiten überschrieben wird, ist egal, weil es sowieso dasselbe ist. * Die Laufvariable ''i'' im Testprogramm liegt auf dem Stack, von dem jedes Programm seinen eigenen hat. Durch eine einfache Abwandlung des Testprogramms wird der Fehler aber deutlich:
0123456789==Virtueller Speicher== Um den Konflikt zu beseitigen, gibt es zwei Möglichkeiten: Entweder das Programm wird so verändert, dass es damit zurechtkommt, an anderen Adressen zu laufen, oder wir kriegen es im OS irgendwie geregelt, die Programme zu trennen, aber trotzdem an ihrer Adresse laufen zu lassen. Für die erste Variante gibt es zum Beispiel folgende Möglichkeiten ein, das umsetzen: * Die Programme jeweils an eine andere Adresse linken. Offensichtlich, aber auch offensichtlich unflexibel: Jedes Programm braucht seine eindeutige Adresse, die kein anderes Programm belegt. Es kann insbesondere auch nur einmal laufen. * Erst beim Programmstart dynamisch linken und dabei jeweils eine freie Adresse benutzen * Das Programm wird als ''Position Independent Code (PIC)'' kompiliert, so dass es an jeder Adresse funktioniert Die letzten beiden Punkte können beispielsweise sinnvolle Optionen sein, wenn man für Hardware ohne MMU programmiert. Auf x86 haben wir allerdings glücklicherweise Hardwareunterstützung und können die zweite Variante wählen: Das Programm sieht zwar eine bestimmte Adresse, aber diese Adresse muss nicht zwingend etwas mit der Adresse im physischen RAM zu tun haben, sondern kann ganz woanders hinzeigen. Die beiden Optionen, um dies umzusetzen, sind: * Segmentierung: In der [[GDT]] und/oder [[LDT]] werden zusätzliche Segmente angelegt - eins für jedes Programm. Während wir bisher Segmente mit Basis 0 und Limit 4 GB benutzen, würde man dann Segmente mit anderer Basis und anderem Limit benutzen. Das erste Programm könnte z.B. Basis 0x10000000 und das zweite Basis 0x20000000 bekommen. Wenn Programm eins jetzt auf seine ''virtuelle Adresse'' 0x200000 zugreift, ist das in Wirklichkeit die ''physische Adresse'' 0x12000000. Das Limit wird entsprechend gesetzt, damit ein Programm keinen Zugriff auf den Speicher anderer Prozesse (oder des Kernels) hat. * [[Paging]]: Der komplette Speicher wird in ''Seiten'' (z.B. von 4 Kilobytes Größe) eingeteilt. Jeder Seite im ''virtuellen Speicher'' wird individuell eine entsprechende Seite im ''physischen Speicher'' zugeordnet. Vorteil gegenüber Segmentierung ist, dass ein einzelnes Segment immer noch am Stück im physischen Speicher liegen muss. Beim Paging kann ein virtuell zusammenhängender Bereich dagegen physisch beliebig verteilt sein. Dadurch wird beispielsweise Speicherfragmentierung vermieden und es ist leichter, die zugeteilte Speichergröße nachträglich zu erhöhen. Virtueller Speicher bringt ganz nebenbei auch noch ein paar andere Vorteile mit sich: Erst dadurch werden Dinge wie Swapping oder Copy on Write möglich. Wegen der erwähnten Flexibilität habe ich (wie die meisten anderen Betriebssysteme) Paging ausgewählt, um in unserem Kernel virtuellen Speicher umzusetzen. Wenn man besonders abenteuerlustig ist, kann man übrigens Paging und Segmentierung auch gleichzeitig benutzen (tatsächlich gibt es in kleinem Umfang keinen Weg darum herum, wenn man Paging benutzt: Ein [[TSS]] braucht jeder, und das ist ein Segment mit echter Basis und Limit). Für diesen Fall sollte man sich die Reihenfolge merken, in der eine vom Code benutzte virtuelle Adresse umgesetzt wird: * Zuerst übersetzt die Segmentierung die ''virtuelle'' in eine ''lineare Adresse'' * Dann übersetzt Paging die ''lineare Adresse'' in eine ''physische Adresse'' == Paging aktivieren == Nachdem wir uns mit den theoretischen Grundlagen ein bisschen befasst haben, kommen wir zur zentralen Frage: Wie schaltet man Paging ein? Die einfach Antwort lautet: Das Paging-Bit (Bit 31) im Steuerregister ''cr0'' muss gesetzt werden (direkt nach dem Initialisieren der physischen Speicherverwaltung ist vermutlich ein guter Platz, um ''vmm_init'' aufzurufen):
0: v=0e e=0000 i=0 cpl=0 IP=0008:0000000000100385 pc=0000000000100385 SP=0010:0000000000105fd4 CR2=0000000000100385
EAX=80000011 EBX=00009500 ECX=0000000b EDX=00000002
ESI=00000000 EDI=0012c000 EBP=00105fe4 ESP=00105fd4
EIP=00100385 EFL=00000002 [-------] CPL=0 II=0 A20=1 SMM=0 HLT=0
CR0=80000011 CR2=00100385 CR3=00000000 CR4=00000000
check_exception old: 0xe new 0xe
1: v=08 e=0000 i=0 cpl=0 IP=0008:0000000000100385 pc=0000000000100385 SP=0010:0000000000105fd4 EAX=0000000080000011
EAX=80000011 EBX=00009500 ECX=0000000b EDX=00000002
ESI=00000000 EDI=0012c000 EBP=00105fe4 ESP=00105fd4
EIP=00100385 EFL=00000002 [-------] CPL=0 II=0 A20=1 SMM=0 HLT=0
CR0=80000011 CR2=00000070 CR3=00000000 CR4=00000000
check_exception old: 0x8 new 0xe
Triple fault
CPU Reset (CPU 0)
EAX=80000011 EBX=00009500 ECX=0000000b EDX=00000002
ESI=00000000 EDI=0012c000 EBP=00105fe4 ESP=00105fd4
EIP=00100385 EFL=00000086 [--S--P-] CPL=0 II=0 A20=1 SMM=0 HLT=0
CR0=80000011 CR2=00000040 CR3=00000000 CR4=00000000
''v=0e'' verrät den Interrupt, mit dem alles angefangen hat. Interrupt 0x0e = 14 ist eine Exception, und zwar genauer gesagt ein ''Page Fault'' (#PF). Der Page Fault sagt uns, dass Paging angeschaltet ist und beim Zugriff auf eine (virtuelle) Adresse ein Problem gemacht hat. Das Steuerregister ''cr2'' verrät uns, welche Adresse das war, und der Fehlercode welcher Art der Fehler war:
{| {{prettytable}}
! Bit || Wert || Beschreibung
|-
| 0 || 0x1 ||
0: Der virtuellen Adresse ist keine physische Adresse zugeordnet (sie ist nicht ''gemappt'')100382: 0f 22 c0 mov %eax,%cr0 100385: c9 leave 100386: c3 retEr wollte also einfach die nächste Instruktion ausführen (deswegen sind eip und cr2 auch gleich). Beim Zugriff auf den Speicher (die Instruktion will ja erst einmal eingelesen werden) hat es dann geknallt, weil wir nicht gesagt haben, welche virtuelle Adresse an welcher physischen Adresse liegt. Der Rest ist einfach: Beim Versuch, einen Page Fault auszulösen, ist der Zugriff auf die IDT schiefgegangen. Das führt zu einem Double Fault, dessen Handler ebenfalls nicht ausgeführt werden kann, weil die IDT immer noch nicht gelesen werden kann. Und damit sind wir beim Triple Fault, der quasi als letzten Ausweg einen CPU-Reset zur Folge hat. == Page Directory und Page Tables == Bevor wir Paging anschalten, müssen wir dem Prozessor also erst einmal mitteilen wie unser virtueller Adressraum aussehen soll. Das Prinzip von Paging ist einfach: Wie oben erwähnt wird der gesamte Speicher in Pages von 4k Größe eingeteilt. Wir müssen im Prinzip nur noch in eine große Tabelle eintragen, welche physische Adresse gemeint ist, wenn eine bestimmte virtuelle Adresse angesprochen wird (siehe [[Paging#Funktionsweise]]). Weil eine einzige große Tabelle aber in der Regel zu viel Speicher verschwenden würde, wird auf i386 aber ein zweistufiges System eingesetzt: Blöcke von jeweils 1024 Pages (also 4 MB) werden in einer ''Page Table'' beschrieben. Das ''Page Directory'' enthält dann Verweise auf die einzelnen Page Tables. Wenn eine Page Table nicht benötigt wird, kann sie im Page Directory einfach weggelassen werden (z.B. 0 als Eintrag) und wir haben 4k Speicher gespart. Aber fangen wir zunächst einfach einmal damit an, unseren virtuellen Adressraum (auch ''Speicherkontext'' genannt) in Code zu gießen. Der gesamte Adressraum wird durch das Page Directory beschrieben. Wir haben also folgendes (später kommt hier noch mehr dazu, deswegen gleich ein struct):