38 Punkte von GN⁺ 2025-09-16 | Noch keine Kommentare. | Auf WhatsApp teilen
  • 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-ModusInitialisierung des Kernels im S-ModusStart 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.

Noch keine Kommentare.