Autor Thema: Ablauf Paging  (Gelesen 3593 mal)

Savatage

  • Beiträge: 28
    • Profil anzeigen
Gespeichert
« am: 24. October 2016, 08:55 »
Wie auch so viele andere im Forum macht mich das Paging ganz schön fertig. Bevor ich nun anfangen kann mein Paging umzusetzen, versuche ich mir eine Art Programmablaufplan zu erstellen, damit ich weiß ob ich alles bedacht habe. Ich würde hier gern darstellen wie ich mir das ganze vorgestellt habe um dann zu erfahren ob das grundsätzlich richtig ist oder ob doch noch irgendwo Verständnisprobleme sitzen.

Allgemein:
- Ausgangssituation: Adressraum ist in 4kB Speicherblöcke als physische Speicher eingeteilt, verwaltet durch eine Bitmap (halt wie im Tutorial)
- Paging soll wie im Tutorial unterteilt sein in PD und PT
- Jeder Prozess mit eignem PD, Kernel wird immer rein gemappt
- Adressraumaufteilung 0-1 GB für den Kernel, 1-4 GB für den Userspace

So, zur Adressraumaufteilung gleich meine erste Frage: Das wird nur realisiert durch entsprechend gesetzte Flags in den PTE's?

Ablauf:
1. Initialisierung des Paging (läuft einmalig bei Kernelstart):
- Speicherkontext anlegen
- Danach werden im Tutorial die ersten 4 MB an dieselbe physische wie virtuelle Adresse gemappen. Das widerspricht der Adressaufteilung von 0-1 Kernel und 1-4 Userspace. Also kommt an diese Stelle das Mapping des Kernels (+ Videospeicher, + Bitmap)
- Speicherkontext aktivieren
- Paging aktivieren

2. Speicherverwaltung (läuft immer wenn ein neuer Task startet):
- Neues PD anlegen und dem Task mitgeben
- Kernel in das PD mappen
- Kontextwechsel beim Start des neuen Task im Interrupthandler

Sozusagen habe ich nur die 2 Funktion + etliche Hilfsfunktionen.

So weit, so kompliziert. Sobald mein Paging aktiviert ist, habe ich nur noch virtuelle Adressen. Um dennoch an die PD ranzukommen habe ich schon mehrfach gelesen dass das PD einen Eintrag auf dich selbst bekommt und ich es sozusagen als zusätzliches PT verwalten kann. Damit komme ich an die phys. Adresse, die ich ja zum aktivieren des PD brauche.

Jetzt noch eine offene Frage:
Wenn mein Task beendet ist wird ja der Speicherkontext gewechselt. Was passiert mit dem alten Kontext? Gilt der als unbenutzt und kann einfach wieder neu belegt werden?

Ich bin unsicher ob noch ein Thread zum Thema Paging wirklich was hilft, aber ich bin doch etwas ratlos und freue mich über alle Kommentare, Anregungen und Einschätzungen

Eure Sava


 


kevin

  • Administrator
  • Beiträge: 2 767
    • Profil anzeigen
Gespeichert
« Antwort #1 am: 24. October 2016, 10:19 »
So, zur Adressraumaufteilung gleich meine erste Frage: Das wird nur realisiert durch entsprechend gesetzte Flags in den PTE's?
Ja. Rein theoretisch brauchst du so eine Aufteilung überhaupt nicht, sondern könntest über das Flag für jede Page individuell dynamisch zur Laufzeit festlegen, ob sie eine Kernel- oder Userspace-Page ist. Nur würde das die Verwaltung so kompliziert machen, dass man lieber in einem bestimmten virtuellen Adressbereich nur Kernelpages und in einem anderen Bereich nur Userpages hat, so dass man einer Adresse direkt ansieht, wohin sie gehört.

Zitat
So weit, so kompliziert. Sobald mein Paging aktiviert ist, habe ich nur noch virtuelle Adressen. Um dennoch an die PD ranzukommen habe ich schon mehrfach gelesen dass das PD einen Eintrag auf dich selbst bekommt und ich es sozusagen als zusätzliches PT verwalten kann. Damit komme ich an die phys. Adresse, die ich ja zum aktivieren des PD brauche.
Genau, so geht es am besten. Wenn du verstanden hast, wie dieser Trick im Detail funktioniert, dann hast du das schwierigste am Paging geschafft.

Zitat
Jetzt noch eine offene Frage:
Wenn mein Task beendet ist wird ja der Speicherkontext gewechselt. Was passiert mit dem alten Kontext? Gilt der als unbenutzt und kann einfach wieder neu belegt werden?
Der Prozessor kennt immer nur ein einziges PD, nämlich das, auf das cr3 gerade zeigt. Für die PTs gilt das ähnlich, der Prozessor schaut nur diejenigen PTs an, auf die ein PTE im aktiven PD zeigt und nur, wenn auf die entsprechenden Adressen zugegriffen wird.

Abgesehen davon, dass du mit dem Prozessor einen zweiten Nutzer der Daten hast, sind es ganz normale Daten im RAM, die du allozierst und freigibst wie alles andere auch. Wenn du weißt, dass der Prozessor nicht mehr auf die Daten zugreift, kannst du sie auch für etwas ganz anderes wiederverwenden.

Zu den meisten Punkten habe ich nichts kommentiert. Wie bei einem guten Unix-Tool heißt das, dass das so passt. ;)
Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end.

Savatage

  • Beiträge: 28
    • Profil anzeigen
Gespeichert
« Antwort #2 am: 03. November 2016, 12:15 »
ich habe jetzt den ersten Abschnitt - die Initialisierung vom Paging, programmiert. (oder besser: es versucht)
Dabei habe ich mich sehr an das tyndur Projekt gehalten. Ich bitte das nicht als abschreiben zu verstehen, sondern als verstehen wollen.

Nach dem ersten Kompilieren habe ich nun etliche Fehler:

1)
#define kernel_page_tables  ((uint32_t*)(kernel_page_directory + (KERNEL_BASE >> PGDIR_SHIFT)))
Diese Zeile gibt mir die Variable kernel_page_tables  an die Hand, die ich dann später den jeweiligen Tasks übergeben kann, bzw. in das Task PD mappen kann. Im Detail kann ich mit der Zeile wenig anfangen, würde sie aber gern verstehen.

2)
Ich habe aus dem Linkerskript Variablen für Anfang und Ende vom Kernel. Ebenso habe ich das jetzt für die Phys. Adresse vom Kernel. Die brauche ich ja um den Kernel in die PT zu mappen. Mit:
extern const void kernel_starthole ich mir die Variablen dann in den Code. Beim Verwenden erhalte ich dann aber die Meldung "incomplete type".  Oder anders gefragt wie kriege ich die Variablen in virtuelle und physische Adressen? Ein
(paddr_t) kernel_phys_startbringt nicht das erhoffte Ergebnis.

Ein paar weitere Fehler habe ich noch, aber an denen versuche ich mich noch ein bisschen selbst.

kevin

  • Administrator
  • Beiträge: 2 767
    • Profil anzeigen
Gespeichert
« Antwort #3 am: 03. November 2016, 12:40 »
#define kernel_page_tables  ((uint32_t*)(kernel_page_directory + (KERNEL_BASE >> PGDIR_SHIFT)))Wenn man es Schritt für Schritt auseinandernimmt, dann ist es gar nicht kompliziert:
  • KERNEL_BASE ist die Startadresse für den "Kernelteil" des Adressraums (also in der Praxis 0 bei tyndur)
  • KERNEL_BASE >> PGDIR_SHIFT ist dann der PD-Index, an dem der Kernelteil beginnt
  • kernel_page_directory ist ein Pointer auf unsigned long
  • Mittels Pointerarithmetik ist der gesame Ausdruck dann ein Pointer auf den ersten PDE, der zum Kernelspace gehört
Wenn man den Kernelspace irgendwo anders als bei 0 anfangen lassen würde, würde man die ganze Berechnung sogar brauchen, aber so ist
kernel_page_tables einfach das gleiche wie kernel_page_directory.

Übrigens, wenn du dich von tyndur inspirieren lassen willst, würde ich dir empfehlen eher kernel2 als kernel anzuschauen. Wir haben mit der Zeit auch das eine oder andere dazugelernt. ;)

extern const void kernel_startDas wichtige hier ist, dass kernel_start nicht eine Variable ist, die die Startadresse enthält, sondern es ist ein Symbol, das am Anfang des Kernels liegt. Das heißt, du willst nicht den (nicht existenten) Inhalt der Variable, sondern die Adresse von diesem Symbol haben: &kernel_start
Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end.

Savatage

  • Beiträge: 28
    • Profil anzeigen
Gespeichert
« Antwort #4 am: 03. November 2016, 13:15 »
Zitat
Übrigens, wenn du dich von tyndur inspirieren lassen willst, würde ich dir empfehlen eher kernel2 als kernel anzuschauen. Wir haben mit der Zeit auch das eine oder andere dazugelernt. ;)
Du bist ja witzig, da ist ja alles anders  :| Da werde ich wohl noch mal von vorn anfangen und mich da durch kämpfen.

Zitat
aber so ist kernel_page_tables einfach das gleiche wie kernel_page_directory.
Sowas wollte ich hören! Das klingt machbar.

Zitat
Das heißt, du willst nicht den (nicht existenten) Inhalt der Variable, sondern die Adresse von diesem Symbol haben: &kernel_start
An welcher Stelle holt sich tyndur die Adresse? Eine Dereferenzierung habe ich wohl irgendwo überlesen.

So, mit den Änderungen sind es schon deutlich weniger Fehler. Wenn ich es jetzt tatsächlich zum kompilieren bringe müsste mein Kernel doch auch eigentlich starten und laufen. Knallen tut es erst wenn ich Tasks laufen lasse. Aber solange er keine Tasks hat dürfte doch alle prima sein oder?
« Letzte Änderung: 03. November 2016, 13:20 von Savatage »

kevin

  • Administrator
  • Beiträge: 2 767
    • Profil anzeigen
Gespeichert
« Antwort #5 am: 03. November 2016, 13:26 »
tyndur benutzt eine andere Deklaration von kernel_start:
extern void kernel_start(void);Der "Vorteil" an einem Funktionspointer ist, dass &fn == fn ist, aber man sollte besser nicht versuchen, die Funktion aufzurufen, weil an der Adresse eben keine Funktion liegt (oder zumindest nicht wirklich definiert ist, welche). Von daher finde ich das in tyndur eher unschön gelöst.
Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end.

Savatage

  • Beiträge: 28
    • Profil anzeigen
Gespeichert
« Antwort #6 am: 04. November 2016, 10:10 »
Das mit der Funktion ist natürlich ein Trick der nicht sofort ersichtlich ist. Aber danke für die Erklärung. Ich kann verstehen warum ihr das umgebaut habt.

Ich habe jetzt tatsächlich alle Fehler raus und mein Kernel kompiliert. Mit Qemu startet der Kernel erst und scheint dann aber etwas seltsames zu machen (siehe Anhang). Nach diesem Bild startet er sich neu... in Endlosschleife.
Ich habe jetzt noch nicht gedebuggt weil ich nicht weiß ob das nicht so seien muss, schließlich lasse ich ja auch Tasks laufen die noch keine virt. Adresse kriegen, sondern nur auf der phys. Adresse arbeiten.
Wenn ich den Teil auskommentiere der die Tasks initialisiert, scheint mein Kernel irgendwo "festzustecken". Wo genau und warum habe ich auch noch nicht durchschaut.

Mein weiteres Vorgehen wäre jetzt die Task's richtig einzubinden und erst wenn das kompiliert weiter nach Fehlern suchen. Macht das Sinn?


kevin

  • Administrator
  • Beiträge: 2 767
    • Profil anzeigen
Gespeichert
« Antwort #7 am: 04. November 2016, 16:04 »
Ich würde an deiner Stelle erstmal versuchen, den Teil ohne Tasks ans Laufen zu bringen. Wenn der jetzt "feststeckt", wird es sicher nicht besser, indem du noch mehr fehlerhaften Code obendrauf packst. Du tust dich leichter, wenn du einen Fehler nach dem anderen angehst.
Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end.

Savatage

  • Beiträge: 28
    • Profil anzeigen
Gespeichert
« Antwort #8 am: 04. November 2016, 19:11 »
ok, hast du eine Idee was die Meldung im Bild bedeuten konnte, bzw. was das Verhalten erklärt?

kevin

  • Administrator
  • Beiträge: 2 767
    • Profil anzeigen
Gespeichert
« Antwort #9 am: 04. November 2016, 22:26 »
Das ist doch die ganz normale Meldung, die bei jedem Bootvorgang kommt?

Vielleicht löschst du den Bildschirm normalerweise und siehst sie deswegen nicht, aber jetzt geht schon vorher etwas schief?
Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end.

Savatage

  • Beiträge: 28
    • Profil anzeigen
Gespeichert
« Antwort #10 am: 05. November 2016, 08:27 »
Zitat
Das ist doch die ganz normale Meldung, die bei jedem Bootvorgang kommt?
Achso? Ich sehe das nie. Dann ist also jetzt das Problem dass er immer wieder neu bootet. Das ist schon mal gut zu wissen. Weißt du durch was ein Reboot ausgelöst wird? Bewusst diesen Befehl eingefügt habe ich zumindest nicht.

Spontan hätte ich vermutet dass er nicht in die Endlosschleife am Ende der Initialisierung kommt und deswegen neu startet. Macht Qemu sowas? Das würde aber bedeuten, dass während meiner vmm_init() irgendetwas schief läuft aber nicht mit ner Exception beendet wird. hm.....

kevin

  • Administrator
  • Beiträge: 2 767
    • Profil anzeigen
Gespeichert
« Antwort #11 am: 05. November 2016, 11:03 »
Ein ungeplanter Reboot ist normal ein Tripe Fault. Das heißt, dass irgendein Interrupt (vermutlich ein Page Fault) aufgetreten ist, aber der Prozessor keinen gültigen Interrupthandler gefunden hat, dass er also entweder die IDT oder GDT nicht lesen konnte (nicht oder nicht richtig gemappt?) oder dass die Einträge darin nicht gültig waren. Danach versucht die CPU dann erst noch einen Double Fault zu melden, aber wenn auch dieser Interrupthandler nicht funktioniert, kommt es zum Triple Fault (= CPU-Reset).

Du kannst mal qemu -d int probieren, dann loggt er dir alle Interrupts mit und zu jedem Interrupt auch noch einen CPU-Dump, d.h. mit den Interruptnummern, eip und cr2 müsstest du eigentlich herausbekommen, was da genau passiert.
Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end.

Savatage

  • Beiträge: 28
    • Profil anzeigen
Gespeichert
« Antwort #12 am: 08. November 2016, 19:55 »
Das erste Problem konnte ich lösen: Die Paging-Initialisierung sollte sicher auch erst nach der Initialisierung der gdt stattfinden. Aber das neue Verhalten verstehe ich noch weniger.

Der Reboot verhält sich bei jedem Starten an. Ich habe mir zum Testen einfach mal ein kprintf in die Funktion gepackt, die eine PT mappt. Ergebnis ist, das unterschiedlich oft der Reboot durchgeführt wird und dann werden unterschiedlich viele PT gemappt. Also scheint es schon mal kein grundsätzliches Problem zu geben, da das mappen ja schon mal funktioniert.

Außerdem habe ich noch ein Problem bzgl. der Tasks. Diese laufen bisher nur mit phys. Adressen. Wenn ich also die vmm_init() auskommentiere laufen meine Task wieder. In diesem Zustand funktioniert auch mein Tastaturtreiber, der ebenfalls Bestandteil von meinem Kernel ist. Wenn ich jetzt aber die Initialisierung der Tasks, bzw. die Initialisierung des Multitasking auch auskommentiere funktioniert mein Tastaturtreiber auch nicht mehr. Es werden also keine Interrupts mehr ausgelöst. Ähnliches gilt dann, wenn das mappen der PT funktioniert hat und mein Kernel sich nicht ständig neustartet.

Ich hoffe ich konnte das jetzt verständlich formulieren. Das Paging bringt mich wirklich an den Rand des Wahnsinns. Ich überlege echt ob ich's sein lasse.

kevin

  • Administrator
  • Beiträge: 2 767
    • Profil anzeigen
Gespeichert
« Antwort #13 am: 08. November 2016, 21:10 »
Sein lassen bringt nichts, ohne Paging kannst du kein vernünftiges OS basteln. Und aus der Ferne mit ungefähren Andeutungen rauszufinden, was passiert, ist auch nicht einfach. Da musst du dich jetzt einfach durchbeißen und lernen, wie man ein OS debuggt. Bevor du jetzt wild herumbastelst, solltest du erst einmal versuchen, herauszufinden, was genau vor sich geht, und warum das nicht das ist, was du wolltest. Und erst wenn du verstanden hast, warum passiert, was passiert, machst du dich daran, es zu fixen.

Wenn sich etwas jedesmal anders verhält, dann ist das ein Fehler. Also solltes du damit anfangen herauszufinden, wo das unerwartete/undefinierte Verhalten genau losgeht und dann siehst du auch, warum. Mit viel printf und evtl. Endlosschleifen sollte sich das rauskriegen lassen. Für schwere Fälle, in denen du gar nicht mehr weißt, welcher Code eigentlich ausgeführt wird, kannst du noch die Debug-Logs von qemu dazunehmen.
Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end.

Svenska

  • Beiträge: 1 778
    • Profil anzeigen
Gespeichert
« Antwort #14 am: 11. November 2016, 02:14 »
Im Qemu-Monitor waren zumindest für mich immer "info tlb" und "info mem" hilfreich, denn das zeigt dir immerhin an, was Qemu von deinem Code verstanden. Dafür muss Paging aber aktiv sein.

Du kannst einen GDB an deinen Kernel anflanschen, einen Breakpoint auf deinen "setze das PG-Bit"-Befehl setzen und dann genau einen Single-Step machen. In dem Augenblick ist Paging aktiv, aber der Triple-Fault noch nicht geschehen, und dann kannst du dir das Chaos im Monitor anschauen. Eventuell siehst du dann, was du falsch machst.

kevin

  • Administrator
  • Beiträge: 2 767
    • Profil anzeigen
Gespeichert
« Antwort #15 am: 11. November 2016, 09:18 »
Hast du das mit gdb praktisch ausprobiert? gdb an qemu ranhängen kann ab und zu praktisch sein, aber über einen Kontextswitch oder Moduswechsel weg würde ich ihm keinen Meter weit trauen. Solange jemand an grundlegenden Kernelbausteinen arbeitet, ist meiner Meinung nach gdb nicht die Antwort. Das ist dann aber auch eine Größenordnung, die man selbst mit printf, Endlosschleifen, Registerdumps und Speicher anschauen noch hinbekommt.

Also anstatt einen Breakpoint zu setzen, baust du einfach eine Endlosschleife an der Stelle in den Code ein, dann weißt du hundertprozentig, dass er wirklich stoppt, und zwar an der richtigen Stelle, und kannst den Zustand auch in Ruhe anschauen.
Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end.

Svenska

  • Beiträge: 1 778
    • Profil anzeigen
Gespeichert
« Antwort #16 am: 11. November 2016, 23:39 »
Ja, das hatte ich so praktisch ausprobiert. Mir ging es auch nicht darum, irgendwelche Informationen aus gdb rauszukitzeln (kann ich sowieso nicht), sondern nur darum, Qemu zum richtigen Zeitpunkt anzuhalten.

Und das war genau der Zeitpunkt zwischen "schreibe CR0 mit aktivem PG" und "führe die folgende Instruktion aus". Damit hatte ich Qemu dann in einem Zustand, wo "info tlb" funktioniert, aber der Triple-Fault-Reset noch nicht zuschlägt, und da habe ich dann auch den Bug gefunden. Was es genau war, weiß ich nicht mehr, aber im Monitor war offensichtlich, was falsch war.

Endlosschleifen funktionieren nur, wenn sie auch tatsächlich ausgeführt werden können. In meinem Fall ging das nicht und das Qemu-Log war auch nicht hilfreich, weil die Adressen auch alle stimmten.

Savatage

  • Beiträge: 28
    • Profil anzeigen
Gespeichert
« Antwort #17 am: 17. November 2016, 07:49 »
Vielen Dank für eure wertvollen Tipps. Im Moment habe ich noch Sorgen mit der ... sagen wir ... Projektorganisation. Ich hoffe sehr, dass ich bald weiter entwickeln kann.

Ich melde mich baldmöglichst wieder!
Sava

 

Einloggen