Blog
Adressraumübergreifende Kopien
2022-06-29
Für fork()
sind Seitenkopien über unterschiedliche virtuelle Adressräume notwendig.
Nachfolgend wird dazu das Beispiel aus der Tafelübung noch einmal im Detail mit Pseudocode erklärt.
Seite aus einem anderen Adressraum kopieren
Wir wollen eine Userspaceseite von einem anderen Prozess (1) in unseren Prozess (2) kopieren. Das soll mittels der Funktion copyPageFromOther
geschehen (es ist gerade Prozess 2 aktiv):
copyPageFromOther(toVirtAddr, fromVirtAddr, fromMapping):
// Physikalische Adresse der Quellseite herausfinden
fromPhysAddr = fromMapping.resolve(fromVirtAddr)
// Finde freien Seitenrahmen (im Kernelspace)
tmpFromPageFrame = KernelPageFrameAlloc.alloc()
// Blende Quellseite temporär im Seitenrahmen ein
currentMapping.map(tmpFromPageFrame, fromPhysAddr)
// Optional: Falls toVirtAddr nicht existiert...
if (currentMapping.resolve(toVirtAddr) == NULL)
// ... blende eine neue Seite an dieser Adresse ein
newUserPageFrame = UserPageFrameAlloc.alloc()
currentMapping.map(toVirtAddr, newUserPageFrame)
// Kopiere in Zielseite
memcpy(toVirtAddr, tmpFromPageFrame, 4096)
// Quellseite wieder ausblenden (optional)
currentMapping.unmap(tmpFromPageFrame)
// Identitätsabbildung wiederherstellen
currentMapping.map(tmpFromPageFrame, tmpFromPageFrame)
// der freie Seitenrahmen wird nicht mehr benötigt
KernelPageFrameAlloc.free(tmpFromPageFrame)
Bitte beachten: Wir gehen davon aus, dass map
automatisch den TLB entsprechend invalidiert.
Was tut diese Funktion?
Es muss davon ausgegangen werden, dass die Quellseite (noch) nicht vom aktuell aktiven Mapping (von Prozess 2) aus erreichbar ist. Ich muss also zuerst die virtuelle Adresse der Quellseite in die physikalische Adresse auflösen, welche ich dann wiederum mittels map
in dem virtuellen Adressraum des laufenden Prozesses einblenden kann.
Zum Einblenden der Quellseite (aus dem anderen Mapping) brauche ich noch einen Bereich im aktuellen virtuellen Adressraum, welcher derzeit noch nicht verwendet wird.
Die Wahl der Adresse ist alles andere als trivial.
Zuerst die Frage: Userspace oder Kernelspace? Da ich aus dem Userspace von Prozess 1 in den Userspace von Prozess 2 kopiere, wirkt es vll. intuitiver, die Quellseite auch in den Userspace einzublenden. Das Problem: Ich weiss nicht welche virtuelle Adresse vom Prozess 2 benutzt wird – das Programm kann beliebig groß sein & es kann mit dem map
Systemaufruf auch beliebige Adressen selbst einblenden. Ich müsste also zum Beispiel bei der ersten Adresse im Userspace beginnen und alle Adressen (seitenweise) testen, ob diese in Benutzung sind (z.B. via currentMapping.resolve()
). Und zwar jedes Mal, wenn ich copyPageFromOther
aufrufe!
Beim Kernelspace ist das schon mal nicht der Fall, da darf ein (User-)Prozess nicht einfach wild was einblenden.
Ich könnte mir da nun eine fixe Adresse ausdenken, z.B. tmpPageFrame = 0x2000000
. Nun müsste ich aber sicherstellen, dass der Bereich von 0x2000000
bis 0x2000fff
(eine Seite) nicht von etwas anderem verwendet wird: Er darf kein Kernelcode an dieser Stelle liegen (kann man über Linkerdatei noch regeln), kein Gerät dort Speicher eingeblendet haben und der Bootloader auch nicht irgendwie weitere Informationen (Bootmodule) da hin legen. Schlussendlich muss der Page Frame Allokator diesen Bereich auch als benutzt markieren. Ziemlich viel Aufwand…
Deshalb wird im obigen Beispiel nicht vorher ein fixer Bereich bestimmt, sondern dynamisch vom Page Frame Allokator: Dieser gibt mir die physikalische Adresse einer freien Seite im Kernel zurück – und da der Kernel identitätsabgebildet ist, gilt die Adresse auch im virtuellen Adressraum.
Allerdings schreibe ich keine Daten in diese (phys.) Seite selbst, sondern nutze diesen Bereich nur als Platzhalter (als Seitenrahmen), und blende da kurzzeitig eine andere Seite rein – für die Dauer des Kopiervorgangs wird dadurch die Identitätsabbildung verletzt (das ist jedoch nicht weiter schlimm, da dies nur ganz kurz temporär passiert, und der Ausgangszustand wiederhergestellt wird).
Seite in einen anderen Adressraum kopieren
Wenn wir eine Userspaceseite von Prozess 1 nach Prozess 2 kopieren wollen, nun aber der Prozess mit der Quellseite sind (also 1), dann müssen wir die Funktion etwas abwandeln (hier gehen wir davon aus, dass die Zielseite im Prozess 2 auch bereits existiert):
copyPageToOther(toVirtAddr, toMapping, fromVirtAddr):
// Physikalische Adresse der Zielseite herausfinden
toPhysAddr = toMapping.resolve(toVirtAddr)
// Optional: Falls toVirtAddr nicht existiert...
if (toPhysAddr == NULL)
// ... blende eine neue Seite an dieser Adresse ein
toPhysAddr = UserPageFrameAlloc.alloc()
toMapping.map(toVirtAddr, toPhysAddr)
// Finde freien Seitenrahmen (im Kernelspace)
tmpToPageFrame = PageFrameAlloc.alloc()
// Blende Zielseite temporär im Seitenrahmen ein
currentMapping.map(tmpToPageFrame, toPhysAddr)
// Kopiere in Zielseite
memcpy(tmpToPageFrame, fromVirtAddr, 4096)
// Zielseite wieder ausblenden (optional)
currentMapping.unmap(tmpToPageFrame)
// Identitätsabbildung wiederherstellen
currentMapping.map(tmpToPageFrame, tmpToPageFrame)
// der freie Seitenrahmen wird nicht mehr benötigt
PageFrameAlloc.free(tmpToPageFrame)
In dieser Funktion stecken keine Überraschungen, jedoch ist der Code von copyPageToOther
zum Teil repetitiv mit copyPageFromOther
.
Wenn man nun auch für zukünftige Erweiterungen wie Copy-On-Write (Aufgabe 7) gewappnet sein will, kann es vorteilhaft sein, wenn man möglichst wenig Codeduplikation hat…
Generischer Ansatz
Schön wäre hierbei eine einzelne Implementierung, welche generisch funktioniert, unabhängig ob gerade Prozess 1 oder 2 (oder gar ein ganz anderer Prozess) aktiv ist. Es müsste entsprechend jeweils die Adresse als auch das Mapping des virtuellen Adressraums als Parameter bekommen, von sowohl Quelle als auch Ziel:
copyPage(toVirtAddr, toMapping, fromVirtAddr, fromMapping)
Eine Implementierung dieser Funktion müsste zwei freie Seitenrahmen – eine für Quelle (tmpFromPageFrame
) und eine für die Zielseite (tmpToPageFrame
) – verwenden. Und es kann natürlich sein, dass in manchen Fällen eine physikalische Seite gleichzeitig zwei Mal im virtuellen Adressraum eingeblendet ist. Aber das tut nicht weiter weh 🙂
Das Beispiel entspricht somit dem generischen deep copy aus der Tafelübung.
Als Optimierung kann man sich noch überlegen, ob die beiden Seitenrahmen für Quelle und Ziel nicht beim Systemstart einmalig allokiert und immer wieder recycled werden. Während es aus Komplexitätssicht keine Änderung darstellt (sowohl alloc()
als auch free()
sind 𝒪(1) ), kann dies ggf. die Fehlersuche vereinfachen. Da wir eigentlich immer copyPage
auf der Epilogebene ausführen, wird dies auch keine Wettlaufsituationen verursachen.