- Es wird die Erfahrung geteilt, auf der RISC-V-Architektur einen Prototyp-Kernel für ein Time-Sharing-Betriebssystem implementiert zu haben
- Das Konzept und die Funktionsweise eines Time-Sharing-Kernels werden praxisorientiert erläutert; statt in C wurde die Implementierung in Zig vorgenommen, um die Reproduzierbarkeit zu erhöhen
- Es wird ein Unikernel-Ansatz verfolgt, bei dem Kernel und User-Code in einer einzigen Binärdatei gebündelt werden, und eine Schichtenstruktur genutzt, die für Konsolenausgabe und Timer-Steuerung auf OpenSBI setzt
- Threads laufen im User-Modus (U-mode), während der Kernel im Supervisor-Modus (S-mode) per Timer-Interrupt den Kontextwechsel durchführt und per Systemaufruf die Grenze überquert
- Der Kernpunkt ist eine Technik, bei der durch den Austausch des vom Interrupt-Prolog/-Epilog aufgebauten Stack-Frames die Registersätze und CSR eines anderen Threads wiederhergestellt und so der Kontrollfluss umgeschaltet werden
- Auf Basis einer QEMU-VM und aktuellem OpenSBI wird eine für alle reproduzierbare Lernumgebung bereitgestellt; zudem wird das Virtualisierungsspektrum von Threads, Prozessen und Containern konzeptionell verbunden, was das Material als Grundlage für Lehre und Praxisübungen wertvoll macht
Überblick
- Vorgestellt wird der Prozess, einen Time-Sharing-Betriebssystem-Kernel auf der RISC-V-Architektur direkt zu implementieren
- Die Hauptzielgruppe sind Einsteiger in Systemsoftware und Rechnerarchitektur, Studierende sowie Engineers mit Interesse am Verständnis von Low-Level-Abläufen
- Dieses Experiment verwendet statt der Sprache C die Sprache Zig, was die Reproduzierbarkeit der Übungen erhöht und die Installation vereinfacht
- Der finale Code ist im Repository popovicu/zig-time-sharing-kernel veröffentlicht; es kann leichte Abweichungen zum Text geben
- Es wird empfohlen, die Repository-Version statt der Codeauszüge im Artikel als Single Source of Truth zu betrachten
- Für die Übungen erleichtert es die Einrichtung, sich bei Linker-Skript und Build-Optionen an dem Repository zu orientieren
Empfohlene Lektüre
- Der Artikel setzt Grundlagen der Rechnerarchitektur wie Register, Speicheradressierung und Interrupts voraus
- Als vorbereitende Materialien werden Bare metal on RISC-V, SBI-Boot-Prozess und Beispiele für Timer-Interrupts empfohlen
- Der Artikel zur Mikro-Linux-Distribution ist optional hilfreich, um die Philosophie der Trennung von Kernel- und User-Space zu verstehen
Unikernel
- Es wird eine Unikernel-Konfiguration verwendet, bei der Anwendung und OS-Kernel in eine einzelne ausführbare Datei gelinkt werden
- So wird die Komplexität von Loader und Linker zur Laufzeit vermieden und eine Vereinfachung erreicht, bei der User-Code gemeinsam mit dem Kernel in den Speicher geladen wird
- Für Lehr- und Reproduktionszwecke bietet dies Vorteile bei einfacher Verteilung und konsistenter Umgebung
SBI-Schicht
- RISC-V verwendet ein Rechtemodell mit M/S/U-Modus; in diesem Experiment läuft OpenSBI im M-Modus und der Kernel im S-Modus
- Konsolenausgabe und die Steuerung des Timer-Geräts werden an SBI delegiert, um Portabilität zu gewährleisten
- Falls SBI nicht verfügbar ist, wird UART MMIO als Fallback genutzt; für die Übungen wird jedoch aktuelles OpenSBI empfohlen
Ziel des Kernels
- Zur Vereinfachung werden nur statische Threads unterstützt, und Threads bestehen aus Funktionen, die nicht terminieren
- Threads laufen im U-Modus und senden Systemaufrufe an den Kernel im S-Modus
- Es wird Time-Sharing-Scheduling für einen Single-Core implementiert, sodass bei jedem Timer-Tick auf einen anderen Thread umgeschaltet werden kann
Virtualisierung und was genau ein Thread ist
- Time-Sharing-Threading ist eine Form der Virtualisierung, die auf einem einzelnen Kern mehrere Aufgaben parallelisiert, ohne das Programmiermodell zu verändern
- Anders als beim kooperativen Scheduling erfolgt der Wechsel ohne explizites yield, sondern per Timer-Interrupt
- Threads besitzen jeweils einen eigenen, unantastbaren Registersatz und Stack, während der übrige Speicher geteilt werden kann
Stack und Speichervirtualisierung
- Threads müssen einen eigenen Stack besitzen; nach Aufrufkonvention ist er essenziell für lokale Variablen, die Sicherung von
ra und den Erhalt des Ausführungskontexts
- Das Virtualisierungsspektrum reicht von Thread < Prozess < Container < VM, wobei sich Isolationsgrad und Sicht (view) unterscheiden
- Unter Linux werden Container durch die Kombination von Kernel-Mechanismen wie chroot und cgroups umgesetzt
Einen Thread virtualisieren
- Das minimale Virtualisierungsziel dieses Experiments ist die Unveränderlichkeit des Programmiermodells, der Schutz von Registern und einigen CSR sowie die Zuweisung individueller Stacks
- Es wird betont, warum sinnvolle Berechnungen unmöglich werden, wenn die Registersicht nicht geschützt ist
- Durch das Seeden anfänglicher Werte wie a0 auf dem Stack lässt sich die Argumentübergabe beim Thread-Start kompakt handhaben
Interrupt-Kontext
- Interrupts lassen sich als funktionsaufrufähnliches Modell verstehen, bei dem Register durch Prolog/Epilog auf dem Stack gesichert und wiederhergestellt werden
- Damit asynchrone Timer-Interrupts die Register nicht beschädigen, ist die Einhaltung der Sicherungs-Konvention zwingend
- Das Beispiel-Assembly sichert und restauriert zusätzlich zur Erhaltung von x0–x31 auch CSR wie sstatus, sepc, scause, stval
Implementierung (High-Level)
Nutzung der Interrupt-Stack-Konvention
- Der Hauptteil der Interrupt-Routine befindet sich zwischen Prolog und Epilog; wird sp auf einen anderen Speicherbereich umgeschaltet, wird dadurch der Registersatz eines anderen Kontexts wiederhergestellt
- Das entspricht einem Kontextwechsel und ist die Kernidee der Time-Sharing-Implementierung in diesem Experiment
- Periodische Timer-Interrupts greifen regelmäßig ein und führen Hauptfluss und Interrupt-Fluss im Wechsel aus
Trennung von Kernel- und User-Space
- Die Grenze S-Modus-Kernel / U-Modus-User bleibt erhalten; Interrupts und Systemaufrufe werden im S-Modus-Trap-Handler verarbeitet
- Der Boot-Ablauf verläuft in der Reihenfolge OpenSBI im M-Modus → Initialisierung des Kernels im S-Modus → Start der Threads im U-Modus
- Periodische Timer-Interrupts ermöglichen Scheduling und Kontextwechsel
Implementierung (Code)
Assembly-Start
- In
startup.S wird eine minimale Sequenz aufgebaut, die nach BSS-Initialisierung und dem Setzen des initialen Stack-Pointers zu Zigs main springt
- Der Kernel-Einstiegspunkt verwendet zur Anbindung an die C-ABI die Konvention
export
Haupt-Kernel-Datei und I/O-Treiber
kernel.zig prüft in main zunächst die OpenSBI-Konsolenfunktion und fällt bei Misserfolg auf UART MMIO zurück
sbi.debug_print setzt die Register a0/a1/a6/a7 gemäß dem ECALL-Protokoll und ruft darüber auf
- Nach dem Setzen des Timers wird der S-Modus-Interrupt-Handler registriert und Ticks werden aktiviert
S-Modus-Handler und der Kontextwechsel
- Der Handler wird mit Zigs Konvention
naked geschrieben, sodass ein vollständiger Prolog/Epilog einschließlich CSR-Sicherung manuell aufgebaut wird
- Im Hauptteil wird
handle_kernel(sp) aufgerufen; durch Austausch gegen den zurückgegebenen sp wird entschieden, ob ein Wechsel stattfindet
- Über
scause wird zwischen ECALL aus dem U-Modus und Timer-Interrupt unterschieden und entsprechend verzweigt
Die Threads im User-Space
- Der User-Code ist zusammen mit dem Kernel in einer einzigen Binärdatei enthalten; die Beispiel-Threads wiederholen String-Ausgabe → Delay-Loop
syscall.debug_print legt die Systemaufrufnummer 64 in a7 sowie Puffer/Länge in a0/a1 und führt dann ECALL aus
- Bei der Thread-Initialisierung werden Rücksprungadresse und initiale Registerwerte auf dem Stack vorbereitet, sodass bei der ersten Rückkehr Argumente sofort nutzbar sind
Den Kernel ausführen
- Der Build erfolgt mit
zig build; ausgeführt wird in QEMU mit virt-Maschine + nographic + OpenSBI fw_dynamic
- Beim Booten erscheinen nach dem OpenSBI-Banner periodische, nach Thread-ID getrennte Ausgaben im Wechsel
- Wird mit
-Ddebug-logs=true gebaut, werden Interrupt-Quelle, aktueller Stack sowie Queueing-/Dequeueing-Logs detailliert angezeigt
Fazit
- Dieses Experiment modernisiert einen Lehr-Kernel mit der Kombination RISC-V + OpenSBI + Zig und erhöht so Reproduzierbarkeit und Lesbarkeit
- Zwar gibt es Vereinfachungen wie minimale Fehlerbehandlung und überdimensionierte Stacks, der Fokus liegt jedoch auf dem Erlernen des Wesens des Kontextwechsels und der Privilegientrennung
- Eine Portierung auf reale Maschinen ist möglich, sofern Linker-/Treiberkonstanten angepasst werden und SBI-Verfügbarkeit sichergestellt ist
Zusätzliche Notiz: Ordnung des Virtualisierungsspektrums
- Threads: vor allem Virtualisierung von Registern und Stack, hohe Wahrscheinlichkeit geteilter Speicherbereiche
- Prozess: Adressraumvirtualisierung für Speicherisolierung, mit der Möglichkeit mehrerer Threads im Inneren
- Container: eine Isolationseinheit, die durch die Kombination von Dateisystem- und Netzwerk-Namespaces und ähnlichen Mechanismen entsteht
- VM: zielt auf vollständige Virtualisierung der Hardware insgesamt
Zusammenfassung der wichtigsten Implementierungspunkte
- Kontextwechsel durch Austausch des Interrupt-Stacks
- Bewahren/Wiederherstellen des vollständigen Zustands einschließlich CSR im S-Modus-Trap-Handler
- Doppelte Ausgabepfade mit SBI zuerst, UART MMIO als Fallback
- Einfaches Scheduling rund um statische Threads, Single-Core und Time-Slices
- Klare U/S-Grenze durch ECALL-basierte Systemaufrufe
Noch keine Kommentare.