4 Punkte von GN⁺ 2023-08-31 | 1 Kommentare | Auf WhatsApp teilen
  • Fomos ist ein experimentelles OS, das in Rust gebaut wurde, und ein Projekt, um Ideen für Non-Unix-OS sowie die Herausforderungen von Exo-Kernel-Mustern zu verstehen
  • Es bietet grafische Ausgabe, dynamische Allokation, paralleles Laden und Ausführen von Apps, Virtio-Maus- und -Tastaturunterstützung sowie kooperatives Scheduling
  • Apps werden als einzelne Funktion der Form pub extern "C" fn _start(ctx: &mut Context) -> i32 behandelt; ohne Standardbibliothek erhalten sie OS-Funktionen über Context
  • Context ist eine Struktur, die Funktionen und Zustände wie Logs, PID, Framebuffer, calloc, cdalloc, store und Eingaben enthält; neue Funktionen werden hinten angefügt, um die Kompatibilität mit älteren Apps zu erhalten
  • Es gibt keine System Calls; Apps geben die Kontrolle per return an das OS zurück, danach wird die Funktion start erneut aufgerufen — ein kooperatives Ausführungsmodell
  • App-Zustand kann in Context.store gespeichert werden, und die Kernel-Loop ist vereinfacht so aufgebaut, dass sie die App-Liste durchläuft und für jede App _start(Context::new(...)) aufruft
  • Da alle Funktionen und Nebeneffekte über Context weitergegeben werden, ist die Grundannahme, dass sich Sandboxing, Instrumentierung und Debugging durch Ersetzen oder Wrappen von Funktionen in Context umsetzen lassen
  • Sicherheit ist derzeit nicht implementiert; Apps können den RAM anderer Apps einsehen. Geplant ist, Datensicherheit ohne Context Switches und ohne app-spezifische virtuelle Speicher-Stacks zu implementieren
  • Fehlende Punkte sind persistenter Speicher, GPU-Unterstützung, Networking sowie Abstraktionen zum Teilen von Daten und Funktionen zwischen Apps; an Virgl wird gearbeitet
  • Der Build wird mit ./build.sh ausgeführt; erforderlich sein können rust nightly, gcc sowie qemu mit Virgl- und SDL-Flags

1 Kommentare

 
GN⁺ 2023-08-31
Hacker-News-Kommentare
  • Was mir nicht gefällt: In einem präemptiven System kann while (true) das System zwar verlangsamen, aber in einem kooperativen System steht die Maschine praktisch still, sobald die Kontrolle nicht zurückgegeben wird.
    Auch aus Sicherheitssicht macht ein solches System Denial-of-Service-Angriffe viel zu einfach, und ein einzelner Bug in irgendeiner App kann sich auf das gesamte System ausweiten.
    Ich bin kein Betriebssystementwickler, also korrigiert mich bitte, falls ich falschliege.

    • Das stimmt, und ich halte die Gegenargumentation eher für Ablenkung vom eigentlichen Punkt.
      Betriebssysteme haben kooperatives Multitasking nicht aufgegeben, weil sie damit alle Probleme „ausufernder Ressourcennutzung“ für immer gelöst hätten, sondern weil einfache Fehler auf App-Ebene wie ein blockierter UI-Thread oder eine versehentliche Endlosschleife den Zustand des gesamten Systems zerstören.
      Bei einem System, das darauf ausgelegt ist, beliebige Programme auszuführen, ist das ziemlich fatal.
    • Man sollte das nicht als Alles-oder-nichts-Frage betrachten.
      Man kann Apps erlauben, kooperativ geplant zu werden, und trotzdem verhindern, dass while(true){} das System endlos festhält.
      Zum Beispiel mit einem Zeitlimit pro App oder, experimenteller, durch Erkennung von Schleifen und großzügige Zeitlimits.
      Für Programmierer ist es bequem, wenn die Isolation zwischen nicht zusammengehörigen Programmen maximal und zwischen zusammengehörigen Programmen minimal ist; für Nutzer ist es bequem, wenn Rechenressourcen stark isoliert sind, Speicher oder Berechtigungen aber weniger stark abgeschottet werden.
      In der Praxis braucht man ein komplexes Scheduling, das eher präemptiv als kooperativ ist, aber doch ein wenig kooperativ.
      Auch unter Linux ist es für ein bösartiges Programm wie eine Fork-Bomb nicht schwer, das System zum Stillstand zu bringen, besonders wenn Swap aktiviert ist; und selbst mit präemptivem Scheduler läuft, wenn ein Programm 99 % der System-Threads belegt, faktisch meistens dieses Programm.
    • Ich bin der Autor.
      Scheduling ist ein Spektrum, und heutige Betriebssysteme sind zwar präemptiv, aber bis zu einem gewissen Grad auch kooperativ.
      Apps können entscheiden, ob sie die Kontrolle abgeben.
      Umgekehrt könnte das Betriebssystem kooperativ bleiben, aber bei Überschreiten von Ressourcenschwellen oder bei Timer-Interrupts selten in einen Fehlermodus mit Kontextwechsel in präemptives Verhalten wechseln, die App beenden und danach wieder in den kooperativen Modus zurückkehren.
      Man könnte das optimistisch kooperativ und pessimistisch präemptiv nennen.
    • Ich bin kein Betriebssystementwickler, aber ich denke, die Anzahl der Kerne macht einen Unterschied.
      Eine while(true)-Schleife legt ein Single-Core-System lahm, aber bei einem Multi-Core-System nicht unbedingt.
      Die Kompromisse könnten heute andere sein als damals, als die Grundstruktur der Betriebssysteme entstand, die wir heute verwenden.
    • Es gibt einen Grund, warum die Welt nach dem Experiment mit kooperativem Multitasking in Windows 3.1 zu präemptivem Multitasking übergegangen ist.
  • Besonders gut fand ich den Teil „In Fomos ist eine App einfach nur eine Funktion“.
    Unix- oder Windows-Executables sind im Vergleich zu eigenständigen Funktionen sehr komplex; es ist kaum vorstellbar, wie elegant ein auf diese Weise geschriebener Kernel sein könnte.
    Ich frage mich, ob Smalltalk/Squeak auch so funktioniert, und hoffe, dass der Autor mit Dateisystem, Task-Manager, sicherem Speicher-Stack und Ressourcenfreigabe weitermacht.
    Natürlich gehört zum Minimalanforderungsprofil eines Proof of Concept auch, DOOM auszuführen.

    • In Smalltalk entspricht das eher Methoden von Klassen als eigenständigen Funktionen, aber insofern stimmt es, als alle Objekte im Image einander Nachrichten senden, also gegenseitig ihre Methoden aufrufen.
      Näher dran sind Lisp-Machine-Betriebssysteme: Anfangs riefen dort eigenständige Funktionen einander ohne Objektsystem auf, später wurden daraus generische Funktionen, die auf Argumentklassen spezialisiert waren.
    • „Eine App ist einfach nur eine Funktion“ klingt nach dem Fluch der Greenfield-Entwicklung.
      Es wirkt, als hätten die Designer noch nicht entdeckt, warum andere Betriebssysteme die Dinge brauchten, die hier derzeit fehlen.
    • Ich frage mich, was in der Praxis der Unterschied zu anderen Betriebssystemen ist, bei denen eine App eine int main() { … }-Funktion ist.
    • Das scheint zur Definition eines Unikernels zu passen.
      Ein anderes Beispiel in Rust ist https://github.com/hermit-os/hermit-rs.
    • Man kann einfach das alte Classic MacOS ausführen.
  • Die Idee ist in Ordnung, aber ständig neue Funktionen an die Context-Struktur anzuhängen, um mit alten Einträgen kompatibel zu bleiben, ist der Weg in die Hölle der Abwärtskompatibilität.
    Man sperrt sich damit selbst ein, weil man alte oder verworfene Einträge nicht mehr aus der Context-Struktur entfernen kann.
    Ein besserer Ansatz scheint zu sein, Semantic Versioning zwischen Betriebssystem und App einzuführen.
    Wenn eine App deklariert, für welche Betriebssystemversion sie gebaut wurde oder von welcher sie abhängt, kann das Betriebssystem die Kompatibilität prüfen und die passende Version der Context-Struktur übergeben.
    Die meisten Abwärtskompatibilitätsprobleme bleiben zwar bestehen, aber man kann die Context-Struktur sauber halten, indem man im Kernel mehrere Strukturen nach Major-/Minor-Versionen vorhält.

    • Ich bin der Autor.
      Gute Idee, aber mir gefällt auch die Einfachheit, nur ein einziges Runtime-Interface zu haben.
      Wenn das Betriebssystem ohnehin alle Versionen behandeln muss, könnten zukünftige Apps Padding verwenden, damit es sich „sauber“ anfühlt.
      struct Context{ padding: [u8;256], // old stuff ctx: ContextV42 }
      Allerdings fühlt sich das, wenn ich es so hinschreibe, auch irgendwie falsch an.
      Dass eine App ihre Version deklariert, wirkt wie ein Problem, das Executable-Formate wie ELF bereits lösen; deshalb probiere ich Alternativen aus.
  • Der Teil „Wie schläft man oder wartet asynchron? Man returnt einfach“ wirkt etwas seltsam.
    Asynchrone Ein-/Ausgabe im Stil von io_uring wäre großartig, aber dieses Modell scheint so etwas auszuschließen, was es erschweren könnte, angemessene Performance zu erreichen.
    Auch keine Unterstützung für Async zu haben, wirkt merkwürdig, weil sich das mit natürlichen Unterbrechungspunkten verbinden ließe.
    Dafür müsste man aber wohl viel von dem Design aufgeben, den Anwendungszustand explizit auf die Festplatte zu speichern und wieder zu laden, und das scheint teuer zu werden.
    Networking dürfte aus ähnlichen Gründen ebenfalls schwierig werden, zumindest wenn es effizient sein soll.

    • Ich vermute, asynchrone Ein-/Ausgabe würde so implementiert, dass man eine I/O-Anfrage im Context aktualisiert und zurückkehrt, woraufhin die Funktion erneut aufgerufen wird, sobald sie bereit ist.
      Diese Funktion wirkt wie das Ende einer Event-Loop, die beliebigen Zustand als Parameter erhält; daher ließe sich das, was eine Event-Loop tut, vermutlich weitgehend verallgemeinern.
      Allerdings gibt man damit Sprachunterstützung für Coroutinen und Async auf.
  • Das angeführte Beispiel ist zu konstruiert
    In einem präemptiven Betriebssystem bleiben Apps normalerweise auf eine Weise hängen, die nicht das gesamte System kooperativ macht, etwa durch Thread-Deadlocks oder Endlosschleifen
    Außerdem kann ein präemptives System eine App beenden, wenn sie zu viele Threads oder Dateien anlegt oder zu viel Speicher verbraucht, lange bevor es faktisch kooperativ würde
    Unser System ist einfach nur nachsichtiger
    Zudem heißt es einerseits: „Sandboxing ist kostenlos, wenn man nur die Prämissen akzeptiert“, andererseits aber: „Jede App kann leicht den RAM anderer Apps einsehen, und das ist ein schwer zu lösendes Problem“ – Sandboxing ist also nicht kostenlos
    Trotzdem ist es eine coole Idee, und ich hoffe, dass der Autor Erfolg damit hat

    • In der Browser-Welt teilen sich alle geöffneten Sites den Heap-Speicher des Browsers, greifen aber nicht ineinander ein
      Eine Lösung für dieses Problem könnte darin bestehen, Funktionen zu verwenden, also Closures zu erzeugen, die Anwendungen kapseln und wie der eigene Context der App wirken
      Ich frage mich, was wäre, wenn eine App andere Apps öffnen könnte – oder wenn eine App für eine andere App zum Betriebssystem werden könnte
    • Dieses Beispiel ist noch konstruierter, weil es annimmt, dass alle Systeme beim Sandboxing so schlecht sind wie Windows und Linux
      Ein System, das korrekt für robustes Sandboxing entworfen ist, setzt Limits für alle Ressourcen und lehnt Anfragen ab, wenn ein Limit erreicht ist
  • Ich frage mich, wie Fomos zwischen Prozessen und ausführbaren Dateien unterscheidet
    Unter Linux ist ein Prozess ein virtueller Adressraum mit argv/envp-Zeigern, Stack, Heap, Signalmaske, File-Handle-Tabelle, Signal-Handlern und ausführbarem Speicher sowie kernelinternen Daten wie uid und gid
    Eine ausführbare Datei ist eine Datei, die genug Bits enthält, damit der Loader diesen Adressraum beim Systemaufruf execve füllen kann
    Auch ohne ausführbare Datei kann man mit clone3 oder fork Prozesse erstellen; der Kernel verwendet ELF, und der Großteil des Userspace nutzt den RTLD-Loader von GLIBC, aber keines von beidem ist zwingend nötig, um Prozesse aus einem bestimmten ausführbaren Dateiformat zu erzeugen
    Statisch gelinkte ausführbare Dateien ohne position-independent code sind aus Sicht eines Assemblers eher „einfach Funktionen“, aber wenn man Laufzeitsymbole ohne ASLR auflöst, wird man anfällig für Buffer-Overflow-Angriffe, sobald die Adressen abhängiger Funktionen bekannt sind
    Ich wünsche mir Alternativen zu den Schwächen von glibc und zum Posix-Prozessmodell, aber ich denke, ein erheblicher Teil der Komplexität von Unix-Executables ist inhärent
    Laufzeit-Symbolauflösung ist schwierig, aber nützlich; beliebige Interpreter zuzulassen ist lästig, aber eine Stärke von Linux gegenüber Windows und MacOS; und eine Kernel-Schnittstelle über stabile Systemaufrufe bereitzustellen, ist eine Stärke von Linux

    • Wenn ich etwas länger darüber nachdenke, scheint einer der großen Fehler die Homogenisierung der ausführbaren Dateiformate zu sein
      Beim Mac ist es Mach-O, bei Windows PE, bei Linux ELF; es gibt aber keinen Grund, warum es kein vielfältiges Ökosystem von Ausführungs- und Link-Formaten geben sollte
      Ein Betriebssystem mit einem sehr einfachen Modell zum Laden von Code ist ein guter Ort für solche Experimente
    • Ich hatte gedacht, die Stärke von Linux seien eher die Treiber als ein stabiles ABI
      Ein stabiles ABI ist weder etwas Einzigartiges an Linux, noch ist der Nutzen dieser Entscheidung besonders zweifelsfrei, aber die Treiberunterstützung ist großartig und schwer zu bestreiten
    • Ich frage mich, ob du dir angesehen hast, wie Zircon (Fuchsia) damit umgeht
      Das ist ziemlich interessant
  • Ich sehe nicht, wie man mit nicht vertrauenswürdigen kooperativen Apps ein gewisses Maß an Security und Safety erreichen kann
    Jede App kann die CPU unbegrenzt festhalten und damit den Kernel und andere Apps anhalten
    Der Grund, warum wir Betriebssysteme mit präemptivem Scheduling verwenden, ist, dass man eine fehlfunktionierende App stoppen kann, ohne den Rest des Systems zu beschädigen

    • Ich erinnere mich, dass Microsoft Research Mitte der 2000er ein ausschließlich in .NET geschriebenes Prototyp-Betriebssystem hatte
      Es verwendete zwar präemptives Multitasking, erzwang aber keinen Speicherschutz; stattdessen gab es den Compiler als Systemdienst, sodass nur ausführbare Dateien laufen konnten, die vom System-Compiler erzeugt und signiert worden waren
      Weil der Compiler den Speicherschutz zur Build-Zeit garantierte, wurden Systemaufrufe und Interprozesskommunikation sehr billig
      Ein etwas ausgefeilterer Compiler könnte auch kooperatives Multitasking auf ähnliche Weise erzwingen, indem er an den nötigen Stellen yield-Aufrufe einfügt
      Das allgemeine Halteproblem lässt sich nicht lösen, aber es gibt trotzdem Klassen von Programmen, bei denen sich durch statische Analyse beweisen lässt, dass sie terminieren oder yielden
      Nur nicht beweisbare Programme müssten gesondert behandelt werden, und mit einem Watchdog-Timer könnte man fehlfunktionierende Programme auch automatisch stoppen
    • In Smalltalk-Systemen wie Squeak oder Pharo unterbricht der Benutzer die Ausführung per Tastenkürzel, wenn ein Thread hängen bleibt
      Nicht vertrauenswürdiger Code läuft nicht im „Haupt“-Image, sondern in einer wegwerfbaren VM
      Auch hier könnte man mit einem Hypervisor dasselbe Modell anwenden, aber niemand nutzt ein Smalltalk-System völlig allein, und man braucht ein gewisses Maß an Infrastruktur
  • Ich frage mich, ob sich in diesem Betriebssystem Sicherheit umsetzen lässt, ohne es komplett neu zu entwerfen – also ohne im Grunde all das noch einmal zu tun, was bestehende Betriebssysteme bereits getan haben.
    Ich kenne zwei Wege, Sicherheit für Anwendungen zu erzwingen, die auf derselben Hardware laufen.
    Der eine besteht darin, Prozesse zur Laufzeit per virtuellem Speicher zu isolieren; der andere darin, dass der Loader beim Laden überprüft, ob der Code beliebige Speicherzugriffe durchführen kann.
    Letzteres wird üblicherweise durch eine virtuelle Maschine erzwungen, die nur Bytecode mit einem eingeschränkten Befehlssatz ohne Pointer-Arithmetik zulässt, wie bei der JVM oder Smalltalk.
    Der Autor von Fomos möchte keine Kontextwechsel und Speicherisolation usw., und der Rust-Compiler erzeugt keinen Bytecode – gibt es da einen anderen Weg?

    • Theseus ist ein Beispiel für den zweiten Ansatz, implementiert in Rust und ohne Bytecode.
      Soweit ich es verstehe, erzwingt ein zertifizierter Compiler Regeln wie das Verbot von unsafe; der Quellcode entspricht hier also praktisch dem Bytecode.
      Auf den ersten Blick wirkt es Midori sehr ähnlich, aber die Details der Implementierung unterscheiden sich ziemlich.
      In Theseus sind Treiber, Anwendungen usw. ELF-Objekte, und alle werden dynamisch zu einer einzigen ausführbaren Datei gelinkt, nämlich dem Kernel; außerdem gibt es interessante Techniken wie Hot Upgrades.
      https://github.com/theseus-os/Theseus
      https://www.theseus-os.com/
    • Nur eine Vermutung, aber alle „Programme“ könnten sich einen einzigen Adressraum teilen, während man per virtuellem Speicher die Sichtbarkeit der zu einem bestimmten Zeitpunkt zugänglichen Pages einschränkt.
      Wenn zur Laufzeit ein Segmentierungsfehler auftritt, würde man etwa prüfen, ob der Aufrufer über ein Sicherheitstoken oder Ähnliches die Berechtigung hat, auf diese Page zuzugreifen und sie aufzurufen.
      Ich weiß nicht, wie praktikabel das tatsächlich wäre.
    • https://en.wikipedia.org/wiki/Capability-based_addressing ?
  • Selbst bei kooperativem Multitasking ist es heute vermutlich nicht wie zu Zeiten von Classic MacOS, weil es sehr viele Kerne gibt.
    Ein oder zwei Prozesse, die nicht freiwillig abgeben, müssen nicht zwangsläufig das ganze System blockieren.
    Wenn eine Funktion fehlerhaft läuft und nicht zurückkehrt, könnte das System sie beenden, sobald alle Kerne ausgelastet sind.
    Kooperatives Multitasking bedeutet nicht zwingend schlechte Performance.
    Timesharing war ursprünglich eine Methode, eine riesige einzelne CPU auf mehrere Nutzer aufzuteilen; da heute Single-User-Multicore-CPUs üblich sind, ist es längst an der Zeit, über andere Arten der Kernnutzung nachzudenken.
    Dass dieses Projekt existiert, finde ich wirklich spannend.

    • Ergänzend: Mit „Kooperatives Multitasking bedeutet nicht zwingend schlechte Performance“ meinte ich schlechte Interaktions-Performance.
      In diesem Modell gibt es keine Kontextwechsel, daher könnte die Performance sogar besser werden.
      Deshalb frage ich mich, was passieren würde, wenn man die Timeslices von Linux auf einen absurden Wert wie 10 Sekunden erhöht.
  • Ich würde gern mehr Details zum Sicherheitsplan hören.
    Insgesamt zeigen solche Experimente meiner Ansicht nach, dass Betriebssysteme durch Greenfield-Design verbessert werden können.
    Es erinnert mich ein wenig an Mirage OS: https://mirage.io/