Singularity
Singularity ist die Bezeichnung für den Prototyp eines neuartigen Betriebssystems, dass gegenwärtig bei Microsoft Research entwickelt wird und vor allem für den Einsatz auf x86-Plattformen konzipiert ist.
Die Besonderheit des Systems gegenüber etablierten Betriebssystemen, wie etwa Windows, Linux oder Solaris, besteht darin, dass Singularity den Schutz von Systemressourcen, insbesondere die Isolation einzelner Prozesse, ausschließlich mithilfe von Softwaremechanismen realisiert und sich dabei nicht auf die Unterscheidung verschiedener CPU-Betriebsmodi (x86-Privilege-Rings) stützt.
Begriffsbestimmung
Motivation
Ziel der Entwicklung von Singularity ist es, ein Betriebssystem zu schaffen, dass sich zum einen durch Robustheit, Stabilität, Zuverlässigkeit und Sicherheit auszeichnet und zum anderen ausreichend leistungsfähig ist, um sich auch für den praktischen Einsatz im Alltag zu eignen.
Schutz von Ressourcen und Prozessisolation
Zu den wünschenswerten Eigenschaften eines Betriebssystems gehören unter anderem Robustheit, Sicherheit und Zuverlässigkeit. Um diese Eigenschaften zu gewährleisten, muss das OS Mechanismen bereitstellen, die das System und die zugrundeliegenden Hardwareressourcen vor Fehlern oder Schäden durch unsachgemäßen oder gar böswilligen Zugriff schützen. Eine zentrale Rolle kommt dabei der Prozessisolation zu: Das OS stellt jedem Prozess einen privaten (Speicher-)Adressraum zur Verfügung, den nur er selbst zu modifizieren berechtigt ist. Diese Vorgehensweise schafft eine Barriere, die die Entstehung von Fehlern durch direkte Wechselwirkungen zwischen den einzelnen Prozessen und deren unkontrollierte, möglicherweise mit irreparablen Schäden verbundene Mulitplikation im System verhindert.
Hardwareunterstützung
Handelsübliche Betriebssysteme stützen sich im wesentlichen auf zwei Hardware-Features moderner Prozessoren, um einen willkürlichen Zugriff einzelner Prozesse auf beliebige Speicheradressen zu verhindern:
- Memory Management Unit (MMU)
Sie überprüft die Zugriffsberechtigung eines Prozesses bei der Übersetzung von virtueller in physische Speicheradresse anhand der Pagetable. - CPU-Betriebsmodi ("x86-Privilege-Rings")
Die Verwendung unterschiedlicher CPU-Betriebsmodi beschränkt die Ausführung bestimmter, als privilegiert gekennzeichneter Instruktionen auf den Kernel. Dazu gehören insbesondere diejenigen Instruktionen, die den Betrieb der MMU und die Adressüberprüfung betreffen, aber auch solche die bspw. für einen Zugriff auf die I/O-Controller des Systems benötigt werden. Als Konsequenz dieses Mechanismus ist ein Zugriff auf geschützte Ressourcen aus dem User-Modus nur mit Unterstützung des Kernels über entsprechende Systemrufe möglich.
Der große Nachteil dieser Vorgehensweise besteht in der Kostspieligkeit des Übergangs vom User- (Ring 3) in den Kernel-Modus (Ring 0). Dabei fallen im wesentlichen dieselben Arbeitsschritte an, wie sie auch bei einem Context-Switch durchzuführen sind:
- Wechsel des CPU-Kontextes (Austausch von Instruction-Pointer und CPU-Registern, Fortführung der Abarbeitung auf dem Kernelstack.)
- Wechsel des Speicherkontextes (Laden der Pagetabelle für den Kernel. Flush des TLB auf x86-Architekturen.)
- Kopieren von Argumenten zwischen User- und Kernelspace.
Dieselbe Arbeit ist noch einmal bei der Rückkehr aus dem Kernel zum Userprozess zu leisten.
Ein solch rigider, hardware-gestützter Prozessisolationsmechanismus benachteiligt vor allem diejenigen Prozesse, die häufig I/O-Operationen durchführen oder intensiv mit anderen Prozessen kommunizieren, da hierzu ständig Systemrufe auszuführen sind. Als Konsequenz daraus wird die strikte Isolierung von Prozessen in den heute verbreiteten Betriebssystemen an vielen Stellen zugunsten gesteigerter Performance und Flexibilität aufgeweicht.
Offene Prozessarchitektur
Bekannte Betriebssystemvertreter wie etwa Linux, Windows oder Solaris basieren auf einer offenen Prozessarchitektur. Die einzelnen Adressräume sind zwar prinzipiell durch die obig beschriebenen Hardwaremechanismen voneinander getrennt, allerdings wird dies zwecks Kostenersparnis und Flexibilität nicht an allen Stellen konsequent durchgehalten.
Die Merkmale einer offenen Prozessarchitektur lassen sich wie folgt zusammenfassen:
- Verwendung von Shared Memory
- Unterstützung für dynamisches Generieren und Nachladen von Programmcode
- Durch Reflektionsmechanismen ist es Prozessen möglich, ihren eigenen Programmcode zur Laufzeit zu analysieren und modifizieren (Bsp.: Just-In-Time Compilation von Programmcode durch die Java Virtual Machine). Ebenso lassen sich Programmerweiterungen und Plug-Ins zur Laufzeit direkt in den Adressraum eines Prozesses laden. So ist bspw. eine Vielzahl an Treibern heutzutage von vorne herein als Modul realisiert, das dynamisch in den Kernel eingebunden werden kann.
- Bereitstellung einer universellen API, die die direkte Modifikation der Daten eines Prozesses ermöglicht (Bsp.: Debugging APIs für unprivilegierte Prozesse).
Die Nachteile einer solchen Architektur liegen auf der Hand:
- Shared Memory stellt zwar einen effektiven Kommunikationsmechanismus dar, ist aber aufgrund der erforderlichen Zugriffssynchronisation recht fehleranfällig und geht mit der Kopplung des Fehlerverhaltens aller beteiligten Prozesse einher.
- Dynamisch nachladbare Softwarekomponenten enthalten potentiell fehlerhaften oder gar böswilligen Code. Trotzdem werden sie ohne weitere Sicherheitsvorkehrungen direkt in den privaten Adressraum eines Prozesses, ja häufig sogar direkt in den Kernel eingebunden. So wird bspw. berichtet [47], dass bis zu 85% aller diagnostizierten Windows- Abstürze auf fehlerhafte Treiber zurückzuführen sind.
- Die Implementation solcher Erweiterungen geschieht häufig unter Verwendung privater Details des Hostcodes, d.h.: ohne die Verwendung klar definierter Interfaces. Diese Verquickung von Host und Erweiterung erschwert zum einen die Wartbarkeit und verursacht zum anderen leicht Kompatibilitätsprobleme, falls sich der Hostcode an entscheidenden Stellen ändert.
- Die Möglichkeit Code zur Laufzeit einzubinden, stellt eine erhebliche Einschränkung für die Optimierung und Verifikation der Korrektheit des Programmcodes durch den Compiler oder andere statische Werkzeuge dar. Diese können in der Regel nicht davon ausgehen, dass sich der bestehende Code zwischen zwei Instruktionen zur Laufzeit nicht ändert. So ist es vorab bspw. unmöglich vorherzusagen, ob ein bestimmter Programmteil zur Laufzeit überhaupt Verwendung finden wird oder nicht. Folglich kann ein Compiler diesen auch nicht von vorneherein als toten Code einstufen und von der Übersetzung ausnehmen. Die Optimierung des Programmcodes muss also zu großen Teilen zur Laufzeit erfolgen, was sich negativ auf die Performance auswirken kann.
Das Hauptziel der Entwicklung von Singularity bestand in der Beseitigung der Schwächen einer offenen Prozessarchitektur, die durch die Aufweichung der Prozessisolation entstehen, wobei es Mechanismen zu entwickeln galt, die effektive Systemrufe sowie eine performante Interprozesskommunikation ermöglichen.
Architektur des Singularity-Prototyps
Singularity verfolgt das Konzept einer geschlossenen Prozessarchitektur ("Sealed Process Architecture"). Ein spezielles Prozessmodell, der softwareisolierte Prozess ("Software Isolated Process" oder SIP), kombiniert mit einem eigens entwickelten Kommunikationsmechanismus, dem so gennannten Channel, sorgt dafür, dass das System eine akzeptable Performance erreicht.
Geschlossene Prozessarchitektur
Eine geschlossene Prozessarchitektur zeichnet sich durch die vier folgenden Invarianten aus:
- "Fixed Code Invariant"
Ist ein Prozess einmal gestartet, so kann sein Programmcode nachträglich nicht mehr verändert werden. Dynamisches Generieren oder Nachladen von Code werden durch das System nicht unterstützt. Module sowie Programmerweiterungen werden jeweils als eigenständiger Prozess gestartet, der mit der Host-Applikation auf gesondertem Wege kommuniziert. - "State Isolation Invariant"
Der direkte Zugriff auf die Daten eines Prozesses ist nur ihm selbst und keinem anderen Prozess gestattet. Es ist einem Prozess demnach strikt untersagt, einen gültigen Verweis auf Daten eines anderen Prozesses zu besitzen. - "Explicit Communication Invariant"
Die gesamte Interprozesskommunikation erfolgt über explizite (verbose) Mechanismen. Der Sender einer Nachricht identifiziert sich explizit gegenüber dem Adressaten, sodass dieser die Möglichkeit erhält, den Empfang oder die Weiterleitung von Mitteilungen eines speziellen Senders abzulehnen. Eine Kommunikationsmöglichkeit über Shared Memory besteht nicht. - "Closed API Invariant"
Die System-API darf keinem unprivilegierten Prozess Funktionalitäten bereitstellen, die es ihm erlauben, eine der drei anderen Invarianten zu verletzen.
Beispiel: Export einer universellen Debugging-API, die einen lesenden und schreibenden Zugriff auf Daten eines beliebigen Prozesses ohne dessen Einwilligung ermöglicht.
Allgemein: Direkte Verwendung privilegierter CPU-Assemblerinstruktionen durch unprivilegierte Prozesse.
Obwohl dieses Systemdesign offensichtlich wesentliche Schwächen einer offenen Prozessarchitektur beseitigt, tut sich bei der konkreten Umsetzung doch vor allem eine gewichtige Schwierigkeit auf:
Um die Zustandsisolation (Invariante 2) der einzelnen Prozesse zu gewährleisten, sind die herkömmlichen, hardwaregestützten Mechanismen (CPU-Betriebsmodi und MMU) offensichtlich ungeeignet, da bei ihrer Verwendung vor allem der Übergang vom User- in den Kernelmodus zu kostspielig ausfiele. Im Falle der Nachrichtenübermittlung wäre zusätzlich ein erheblicher Kopieraufwand zu betreiben. Die auszutauschenden Daten müssten jeweils über den Kernel vom Adressraum des Senders vollständig in denjenigen des Empfängers kopiert werden.
Singularity begegenet diesem Problem wie folgt:
Der bekannte Hardwaremechanismus zur Zugriffskontrolle wird durch eine größtenteils statisch durchführbare, softwarebasierte Überprüfung der Zustandsisolation ersetzt, was sich äußerst günstig auf die Systemperformance auswirkt. Das zur Implementation des Systems verwendete C#-Derivat Sing# verfügt über eine erweiterte Syntax, die eine explizite Spezifikation des Kommunikationsverhaltens von Prozessen unterstützt, wie sie die Kommunikationsinvariante der geschlossenen Prozessarchitektur fordert. Hierdurch wird die Einhaltung der Zustandsisolation selbst im kritischen Fall der Interprozesskommunikation, während der zwangsläufig Daten zwischen Prozessen ausgetauscht werden, bereits zur Installationszeit möglich. Aufgrund der Verwendung von Softwaretools zur Sicherung der Isolation werden Prozesse in Singularity auch als softwareisolierte Prozesse ("Software Isolated Processes" oder SIPs) bezeichnet. Der zur Kommunikation verwendete Channel-Mechanismus bietet zudem die Möglichkeit selbst große Datenmengen ohne Kopieraufwand zwischen einzelnen SIPs zu versenden.
Typ- und Speichersicherheit
Typ- und Speichersicherheit sind eng miteinander verwobene Eigenschaften einer Programmiersprache. Sie bilden die Grundlage der Prozessisolation in Singularity.
Typsicherheit garantiert, dass der Inhalt einer variablen eines bestimmten Typs immer korrekt interpretiert und manipuliert wird, d.h. keine Operationen auf einem Datenobjekt ausgeführt werden, die für den speziellen Objekttyp nicht definiert sind und daher zumeist zu nicht vorhersehbaren Fehlern führen. Cast-Operationen zu nicht verwandten Typen sind somit in jedem Fall untersagt.
Speichersicherheit unterbindet den willkürlichen Zugriff auf beliebige Speicheradressen, stellt sicher, dass vorhandene Referenzen innerhalb eines Programms stets auf Speicherbereiche verweisen, die mit einem gültigen Datenobjekt belegt sind und beseitigt somit weitere typische Quellen für Programmfehler. Dazu wird in der Regel eine manuelle Speicherverwaltung mithilfe von Pointern und Pointerarithmetik gänzlich ausgeschlossen. Garbage Collectoren übernehmen die Deallokation von Heapspeicher und verhindern das Auftreten von dangling References.
Speichersicherheit ist ohne Typsicherheit nicht denkbar. Ohne diese wäre es problemlos möglich, bspw. einen Integer zu einem Pointer zu casten und damit - das Vorhandensein eines zusätzlichen Schutzes durch das Betriebssystem ausgeschlossen - Zugriff auf nahezu beliebige Speicheradressen zu erlangen. Ebenso bildet Speichersicherheit eine notwendige Voraussetzung für die Typsicherheit einer Sprache. Ein willkürlicher Speicherzugriff über Pointer, würde das Kopieren eines beliebigen, typfremden Datenbjekts in den Speicherbereich einer jeden Variablen erlauben.
Beispiele für sichere, objektorientierte Sprachen sind bspw. Java, Python oder C#, auf das auch die zur Implementation von Singularity verwendete Sprache Sing# aufbaut.