Singularity

From
Jump to navigation Jump to search

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:

  1. Memory Management Unit (MMU)
    Sie überprüft die Zugriffsberechtigung eines Prozesses bei der Übersetzung von virtueller in physische Speicheradresse anhand der Pagetable.
  2. 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:

  1. "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.
  2. "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.
  3. "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.
  4. "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.