Jörg R. Mühlbacher

Betriebssysteme

Grundlagen

Hinweise:

Im vorliegenden Text wird aus Gründen der Lesbarkeit – z.B. anstatt die Benutzerin und der Benutzer oder der Programmierer und die Programmiererin – häufig nur die männliche Form benutzt. Es ist aber ein Anliegen, ausdrücklich zu betonen, dass damit keinerlei geschlechtsspezifische Absicht verbunden ist.

Abbildungen in diesem Buch können von der Originaldarstellung abweichen, da aus Platzgründen häufig Verkleinerungen durchgeführt wurden.

VORWORT

Dieses Buch behandelt den Stoff der Vorlesung „Betriebssysteme“, die für Studierende der Informatik (2. Semester, Bakkalaureat) an der TNF der Johannes Kepler Universität (JKU) abgehalten wird und ebenso von Studierenden der Wirtschaftsinformatik der JKU besucht wird.

Es stehen daher Grundlagen von Betriebssystemen im Vordergrund, im Einzelfall werden Beispiele (meist) anhand der Windows-Familie herangezogen.

Um bei der Beschreibung von Algorithmen von einer konkreten Programmiersprache unabhängig zu bleiben, wurde ein Pseudocode gewählt, der in seiner Syntax allerdings eng an JAVA bzw. C angelehnt ist.

Neben Überarbeitungen insbesondere der Abschnitte über Benutzerschnittstellen, Sicherheit und Zugangskontrollen, wurde das Buch um eine Einführung in Virtualisierung und deren Anwendun-gen erweitert. Im Kapitel über Netzwerkkommunikation mit dem Schwerpunkt IPv4 wurde eine kurze Beschreibung von IPv6 hinzugefügt.

Das Thema Parallelität wird weitgehend unabhängig vom Thema Betriebssysteme dargestellt, sodass der Einfluss der Entwicklung von Betriebssystemen auf Methoden im Softwareengineering sichtbar wird, denn die Prinzipien des Zusammenspiels von Prozessen in einem Betriebssystem gelten allgemein, wenn ein paralleler Zugriff auf gemeinsame Ressourcen notwendig wird.

Das Kapitel über Sicherheit geht über den Vorlesungsstoff hinaus und findet in einer entsprechenden weiterführenden Lehrveranstaltung seinen Platz. Seine Hereinnahme in dieses einführende Buch soll auch unterstreichen, dass Sicherheitsfragen in heutigen Systemen immer wichtiger werden.

Der Text ist so ausgelegt, dass er auch als Basis im Rahmen eines „Blended Learning“-Ansatzes bei E-Learning verwendet werden kann. Daher enthalten einige Kapitel kurze Begriffswiederholungen aus voran gegangenen Abschnitten, um sie bei Bedarf auch unabhängig von anderen durcharbeiten zu können.

Besonders danken möchte ich Herrn Dr. Rudolf Hörmanseder für die intensive Diskussion und für viele Vorschläge. Mit ihm wurde das Kapitel über Netzwerkkommunikation abgestimmt. Dieses Thema wird in seinem zwischenzeitlich im selben Verlag erschienenen Folgebuch über Computernetzwerke ausführlich behandelt. Daher beschränkt sich dieser Abschnitt auf das Wichtigste, um die Netzwerkschichten eines Betriebssystems verstehen zu können.

Herzlicher Dank gebührt Frau Inge Naderer für das Tippen des Manuskriptes, sowie Frau Sabine Link und Herrn Dipl.-Ing. Christian Praher für Korrekturen und Layout in der Endphase vor der Drucklegung.

Linz, im Februar 2011

Jörg R. Mühlbacher

A BETRIEBSSYSTEME ERSTE GRUNDLAGEN

A.1 Was ist ein Betriebssystem?

Wir beginnen mit einer Definition, die in der deutschen Industrienorm als DIN 44300 zu lesen ist. Dort heißt es:

Die Programme eines digitalen Rechensystems, die zusammen mit den Eigenschaften dieser Rechenanlage die Basis der möglichen Betriebsarten bilden und die insbesondere die Abwicklung von Programmen steuern und überwachen.

Diese Definition ist für jemanden, der sich mit Betriebssystemen erstmals auseinandersetzt, wahrscheinlich nicht sehr hilfreich, da der gesondert zu erklärende Begriff „Betriebsart“ verwendet wird.

Eine ähnlich allgemeine Definition findet man auch in der Encyclopedia of Computer Science, nämlich:

„... a set of software extensions of primitive software, culminating in a virtual machine that serves as high-level programming environment and manages the flow of work within a network of computers”

Diese Definition lässt offen, was man unter einer Virtuellen Maschine versteht, bringt anderseits Netzwerke mit herein.

Aus der Sicht der Anwender lässt sich ein Betriebssystem als Programm beschreiben, welches

- die Computer-Hardware verwaltet,

- die Basis für Anwendungsprogramme zur Verfügung stellt und

- als Bindeglied zwischen Benutzer und Rechner agiert.

So gesehen macht das Betriebssystem die Benutzung des Computers für Anwender erst möglich.

Daher ist es alternativ angebracht, eine Zweckdefinition für ein Betriebssystem voranzustellen:

Bei einem Betriebssystem handelt es sich um Software mit folgendem Zweck

- Es hat für die Ausführung von Anwenderprogrammen zu sorgen.

- Es soll ein Computersystem leichter bedienbar machen.

- Es soll die Computerhardware möglichst effizient nutzen.

Abb. A1  Schichtenmodell

Demnach hat das Betriebssystem zwei Schnittstellen:

- Nach unten erfolgt gleichsam als Abstraktion ein „Verbergen“ von Hardware-Details wie z.B. der Typ des verwendeten Prozessors (CPU) und der sonstigen Elektronik (Motherboard), Größe des Hauptspeichers oder die Art und Weise, wie Daten von und zu den peripheren Geräten übertragen und dort gespeichert werden.

- Nach oben sorgt die als API (Application Programming Interface) bezeichnete Schnittstelle dafür, dass Anwendungsprogramme gezielt Dienste des Betriebssystems in Anspruch nehmen können und – solange das API invariant bleibt – von Versionen und Updates des Betriebssystems unberührt bleiben.

Aus dem Zusammenspiel der beiden Schichten kann man schließen, dass ein Betriebssystem die Benutzung der Hardware für die verschiedenen Anwendungsprogramme steuert und koordiniert.

Da Benutzer eines Computersystems aber nicht (immer) direkt mit der Anwendungssoftware (z.B.: Textverarbeitung, Kalkulationsprogramme, E-Mail, …) interagieren, sondern diese Kommunikation wiederum über Hardware-Einheiten (Bildschirm, Maus, Tastatur, ...) und damit über Dienste des Betriebssystems erfolgt, kommt als wesentliche Betriebssystemkomponente noch die Schnittstelle zum Anwender (Benutzerschnittstelle, User Interface) selbst hinzu. Als Beispiele dazu seien das Fenstersystem mit Mausanschluss oder die zeilenorientierte an die Tastatur gebundene Eingabemöglichkeit (s.u.) genannt, welche vom Betriebssystem als Dienst angeboten und von der Anwendungssoftware genutzt werden können. Dies ist aus der vorigen Abbildung nicht unmittelbar ablesbar.

A.2 Zweck und Aufgaben eines Betriebssystems

Wie vorhin erwähnt kann man ein Betriebssystem durch seine ihm zugedachten Aufgaben beschreiben, anstatt zu definieren, was es ist.

Ein an den Benutzern orientiertes Ziel von Betriebssystemen ist die Benutzerfreundlichkeit, also das Arbeiten mit dem Computer leichter und effizienter zu gestalten. Dies zeigt sich am deutlichsten bei Betriebssystemen für kleine Endgeräte (z.B. Handhelds, Mobile Phones) und ist auch Hauptziel der graphischen Oberflächen für die Anwender. Das für (IBM-) PCs früher weit verbreitete Betriebssystem DOS oder das Betriebssystem Unix z.B. begannen mit einer Tastatur und mit einem Drucker (Teletype) bzw. einem nicht-graphikfähigen Bildschirm als einzige Schnittstellen zum Anwender. Mit der Entwicklung von graphikfähiger Hardware zusammen mit der Erfindung und Verbreitung der Maus (Patent 1970, Welterfolg ab 1984 mit Apple Macintosh) wurden Graphische Benutzerschnittstellen (GUI = Graphical User Interface) hinzugefügt und diese machen Betriebssysteme damit für den Anwender leichter bedienbar.

Das Ziel der Optimierung der Nutzung und Auslastung von Betriebsmitteln (Ressourcen) ist ein besonderer Zweck von Betriebssystemen und steht beim technischen Design von Betriebssystemen auf der Prioritätenliste ganz oben.

Ein Betriebssystem der heutigen Generation ist ein großes und komplexes Gebilde, welches sich nur vernünftig entwerfen lässt, wenn man es bzw. die zu erledigenden Aufgaben in kleinere Teile zerlegt. Die meisten modernen Betriebssysteme bestehen zumindest aus den nachfolgenden Komponenten und erfüllen die dort definierten Aufgaben. Wir betrachten sie anhand der aus dem Zweck abgeleiteten Aufgaben.

A.2.1 Prozessmanagement

Ein Programm „macht nichts“, solange nicht seine Instruktionen von einer CPU ausgeführt werden. Einen Prozess kann man sich als „ein Programm in Ausführung“ vorstellen. Ein Programm ist daher eine statische Beschreibung, ein Algorithmus, während der Prozess den dynamischen Ablauf davon repräsentiert.

Eine genauere Definition von Prozess folgt später, wir geben an dieser Stelle vorerst nur unmittelbar nachvollziehbare Beispiele:

- Ein Anwendungsprogramm wie z.B. ein Compiler erzeugt, sobald er gestartet ist, den Prozess zur Übersetzung eines Quellcodes (sein Input) in eine Maschinensprache (sein Output).

- Ein System-Task wie z.B. das Senden einer Seite an den Drucker bewirkt einen Prozess.

Anmerkung: Der Begriff „Task“ ist historisch unscharf und muss abhängig vom jeweiligen Kontext verstanden werden. Einerseits meint man damit eine Aufgabe (z.B. in Form eines Programms) an ein Computersystem, anderseits kann auch ein Prozess damit gemeint sein, der im System abläuft. (vgl.: Multitasking, siehe später).

Ein Prozess benötigt bestimmte Betriebsmittel (Ressourcen), um ablaufen zu können Dazu gehören  CPU, Hauptspeicher, Dateien auf externen Massenspeichern und Ein-/Ausgabe-Geräte, um die dem Prozess zugedachte Aufgabe zu erfüllen. Diese Ressourcen werden dem Prozess statisch vorab beim Start oder dynamisch während der Ausführung zugeteilt.

Ein Betriebssystem steuert nicht nur die aus den Anwenderprogrammen entstehenden Prozesse, sondern auch seine eigenen, die Systemprozesse. Daher lässt sich aus Prozesssicht ein Betriebssystem als Menge von Prozessen verstehen, die teilweise miteinander kommunizieren und dabei Daten und Nachrichten austauschen. Alle diese Prozesse können auch nebeneinander (parallel) ausgeführt werden, indem sie die CPU „gleichzeitig“ benutzen. Dabei muss sich das Betriebssystem um die Reihenfolge und das korrekte Umschalten zwischen den Prozessen kümmern. Aus diesem Grunde werden wir auf Fragen der Parallelität und damit verknüpften Problemstellungen noch ausführlich eingehen.

Im Zusammenhang mit dem Prozess-Management ist das Betriebssystem für folgende Aktivitäten verantwortlich:

- Erzeugen und Beenden von Prozessen (System- und Benutzerprozesse)

- Anhalten und Fortsetzen von Prozessen

- Bereitstellen von Methoden zur Prozesskommunikation und zur Synchronisation

A.2.2 Hauptspeicherverwaltung

Aus abstrakter Sicht ist der Hauptspeicher ein großes lineares Feld (Array) von Wörtern bzw. Bytes. Jedes Wort oder Byte hat seine eigene Adresse. Typischerweise umfasst ein Wort 32 Bits (4 Bytes) bzw. in der aktuellen Rechnergeneration bereits 64 Bits (8 Bytes).

Der Hauptspeicher ist nicht nur der Aufbewahrungsort von Daten, auf die schnell – z.B. von der CPU – zugegriffen werden muss. Er dient auch der Aufnahme der Instruktionen der auszuführenden Programme. Damit ein Programm ausgeführt werden kann, muss es in den Speicher geladen werden, was wiederum mit Hilfe von Komponenten des Betriebssystems (Ladeprogramm, Loader) geleistet wird.

Wurde die letzte Instruktion eines Programms ausgeführt, kann der benutzte Speicherbereich wieder freigegeben werden und ein neues Programm kann dorthin geladen und ausgeführt werden.

Um die Auslastung der CPU zu erhöhen, werden die Instruktionen mehrerer Programme gleichzeitig im Speicher gehalten. Damit ist ein besonderes Speichermanagement seitens des Betriebssystems erforderlich und es sind dafür spezielle Systemprozesse vorgesehen.

Das Betriebssystem ist u.a. für die folgenden Aktivitäten im Zusammenhang mit Speicherverwaltung verantwortlich:

- Aufzeichnung, welche Bereiche des Speichers von welchen Prozessen gerade benutzt werden.

- Reservieren und Freigeben von Speicherplatz entsprechend den Anforderungen der zu startenden oder ablaufenden Prozesse.

- Gewährleisten von Speicherschutz, sodass Prozesse nur in den für sie reservierten Bereichen ablaufen dürfen. Insbesondere gehört dazu der Schutz der dem Betriebssystem vorbehaltenen Speicherbereiche vor unerlaubtem Zugriff durch Anwenderprogramme oder gar durch bösartige Software (Malware).

A.2.3 Dateiverwaltung

In einem Computersystem werden unterschiedliche physikalische Medien verwendet, um Informationen persistent (dauernd, nichtflüchtig, den letzten Zustand erhaltend) abzuspeichern. Diese können Festplatten aber auch optische Medien (CD, DVD) oder Halbleiterspeicher (vgl.: USB-Memory Stick) sein. Jedes dieser Medien organisiert die Daten auf unterschiedliche Art und Weise und hat unterschiedliche Charakteristika wie z.B. Zugriffsgeschwindigkeit, Kapazität, Daten-Transferrate oder Zugriffsart. Als Oberbegriff kann man die Bezeichnungen Sekundärspeicher in Abgrenzung zu Hauptspeicher (main memory) verwenden.

Das Betriebssystem abstrahiert von den physikalischen Unterschieden der Sekundärspeicher und stellt eine einheitliche, logische Sicht in Form von Dateien (Files) auf diesen zur Verfügung. Ebenso wird die Verwaltungsinformation dazu (Verzeichnisse) bereitgestellt. Global dafür zuständig ist das Filesystem eines Betriebssystems.

Eine Datei ist eine Sammlung von zusammengehörigen Informationen, die von seinem Erzeuger definiert wurde. Eine Datei kann ein Programm- oder Daten-File sein. Um die Übersicht zu bewahren werden Dateien meist in Verzeichnissen und Unterverzeichnissen gegliedert.

Im Zusammenhang mit Dateiverwaltung übernimmt das Betriebssystem folgende Aufgaben:

- Erzeugen, Löschen und Verwalten von Dateien.

- Erzeugen, Löschen und Verwalten von Verzeichnissen.

- Bereitstellen von Funktionen zum Verändern von Dateien und Verzeichnissen und damit insgesamt die Organisation der Sekundärspeicher.

A.2.4 Benutzerschnittstelle

Ein Betriebssystem muss für die Anwender eine Schnittstelle zur Verfügung stellen. Im einfachsten Fall besteht diese aus einer Sammlung von speziellen Kommandos, die einzeln einzugeben sind. Man spricht dann von einer befehlszeilenorientierten Schnittstelle, die heute noch einerseits aus Gründen der Rückwärtskompatibilität und anderseits für Spezialisten bei systemnahen Arbeiten bei allen gängigen Betriebssystemen zu finden ist. Wir haben eingangs schon darauf hingewiesen.

Beispiele dafür sind die DOS-Box mit command.com bzw. cmd.exe bei Windows, Bourne Shell oder C-Shell bei UNIX und Kommandozeilen bei Cisco-Routern, usw.

Zusätzlich verfügen moderne Betriebssysteme auch über eine graphische Oberfläche (GUI, Graphical User Interface), die dem Anwender den Gebrauch des Systems wesentlich erleichtert. Es werden dabei intuitive erfassbare und häufig wiederkehrende Bedienungsparadigmen eingeführt und so Interaktionen zwischen Benutzer und Computer von individuellen Lösungen diverser Anwendungsprogramme losgelöst und zum Betriebssystem verschoben. Innerhalb einer Betriebssystemfamilie, und sofern nicht durch Patente verhindert, auch darüber hinaus, bleiben diese dann gleich oder so ähnlich, dass kein Umlernen notwendig wird. Eine Schattenseite davon ist, dass sich dadurch radikal innovative Änderungen nur schwer am Markt umsetzen lassen.

Manche Hersteller (vgl.: Microsoft Windows, Apple Betriebssystem) haben ein von ihnen fix vorgegebenes GUI und dieses gehört dann quasi zum Betriebssystem dazu. Bei anderen Betriebssystemen wieder (vgl.: Linux) kann man zwischen verschiedenen GUIs bei der Installation wählen, sodass man in diesem Fall eher von einer zu einem Betriebssystem gehörigen Systemkomponente sprechen sollte.

Die GUIs stammen vom Xerox System STAR 8010 ab. Die graphischen Oberflächen z.B. für Apple-Macintosh und MS-Windows nahmen ursprünglich von dieser Entwicklung wesentliche Anleihen in der Art der Benutzerführung. In Europa hat N. Wirth mit dem System Lilith wesentliche Beiträge zur Weiterentwicklung von GUIs beigetragen.

Im Rückblick betrachtet wurden die Entwicklungen bei GUIs durch das generelle Ziel gelenkt, die Bedienungsparadigmen unter Einbezug neuer technischer Erfindungen möglichst dem natürlichen Verhalten des Menschen anzupassen: Man sitzt vor dem Bildschirm (Schreibtisch (desk)) und will darauf (desktop) befindliche Objekte bearbeiten. Dazu zeigt man auf ein mit dem zu wählenden Objekt assoziiertes Bild (Icon) auf dem Bildschirm und setzt dann eine bestimmte Handlung zum Auslösen der gewünschten Aktion.

Ein wesentlicher Schritt in diese Richtung war die Erfindung der Maus als Zeigeinstrument. Sie wird D.C. Engelbart und W. English vom Stanford Research Institut (CA, USA) zugeschrieben und wurde 1968 auf einer Konferenz vorgestellt. Man sollte aber hinzufügen, dass bei Telefunken in Deutschland zeitgleich ein solches Zeigeinstrument entwickelt und vertrieben worden ist.

Der Siegeszug der Maus, erprobt am STAR System, begann aber erst mit der Markteinführung des Apple Macintosh ab 1984.

Abb. A2  Bedienungsparadigma: Löschen eines Dokumentes durch Mausbewegung

Speziell für grafisches Arbeiten direkt auf einem Bildschirm oder einem gesonderten Digitalisierungsbrett (Drawing Pad, Pen Tablet) wurde als Alternative zur Maus die Stifttechnologie entwickelt. Mit einem Stift (stylus) in Analogie zu einem Bleistift kann man auf technisch entsprechend ausgestatteten Oberflächen (Bildschirm, Drawing Pad) auf Objekte zeigen und durch Antippen ihres Icons auswählen. Dadurch wird das Schreiben, Zeichnen usw. mittels eines Editors oder Graphikprogramms auf das ausgewählte Objekt ermöglicht.

Auf die unterschiedlichen Techniken (kapazitive Systeme, induktive Systeme) kann hier nicht weiter eingegangen werden.

In vielen Fällen ist die Notwendigkeit, immer einen speziell präparierten Stylus benützen zu müssen, eher hinderlich.

Daher hat man berührungsempfindliche Oberflächen (Touch Screen) entwickelt, sodass man mit dem Finger oder einem nicht kratzenden Stift als Zeigestab auf den Desktop und die darauf liegenden Objekte zeigen kann.

Man kann sich leicht vorstellen, dass die (teilweise) Abkehr von der Maus und die Möglichkeit, direkt mit einem Finger oder einfachen Zeiger der Alltagssituation „Schreibtischoberfläche und Papier darauf liegend“ schon sehr nahe kommt und sich daraus neue Bedienungsmöglichkeiten für das GUI entwickelt haben.

Ein einfaches Beispiel dafür ist das Umblättern von einer Seite auf die nächste in einem E-Book-Lesegerät beim Lesen eines Textes: einfach auf die Seite mit dem Finger tippen und wie gewohnt nach links (bzw. nach rechts zum Zurückblättern) wischen.

Ein Durchbruch dazu ist der Firma Apple mit dem iPad im Jahr 2010 gelungen. Dort gibt es keine Maus mehr, die Bedienung erfolgt ausschließlich mit den Fingern, wobei schon erste Ansätze in Richtung der Bedienung mittels Gesten gesetzt werden: Berührt man mit Daumen und Zeige- oder Mittelfinger ein Objekt auf dem Display und spreizt die beiden Finger anschließend, so wird das gewählte Objekt vergrößert bzw. durch Zusammenziehen der beiden Finger verkleinert („Zwicken und Spreizen“).

Abb. A3  Vergrössern des Textes durch „Spreizen“

Die Entwicklung von GUIs wurde und wird dominiert von den Spielkonsolen mit ihren hoch auflösenden Graphikdisplays und dem harten Wettbewerb in dieser Branche.

Bei neueren Modellen werden neben den bekannten konventionellen Eingabemöglichkeiten zur Steuerung bereits Bewegungen der Konsole im dreidimensionalen Raum registriert und vom dahinter stehenden Betriebssystem  bzw. vom gestarteten Programm verwertet.

Der nächste Schritt, zusätzlich die Augenbewegungen und den Lidschlag der Spieler oder Spielerinnen mitzumessen, zeichnet sich bereits ab. Diese Technik kann insbesondere für die Bedienung von Personalcomputern durch schwerst behinderte Menschen eine wesentliche Hilfe sein. Prototypen dafür sind als spezielle Anfertigungen zur Unterstützung dieser Personen bereits verfügbar.

Benutzerschnittstellen, die auf eine akustische Kommandoeingabe setzen, sind von der Methode her betrachtet, letztlich der Eingabe von Zeichenketten über eine Tastatur äquivalent. Die wahre Intelligenz liegt hier in einer zuverlässigen Spracherkennungs-Software.

A.2.5 Netzwerkanbindung

Software für die Netzwerkanbindung eines Computersystems ist heute bereits mit dem „eigentlichen“ Betriebssystem tief verzahnt und man sollte sie demnach zum Betriebssystem zählen.

Die Kommunikation in und mit einem Netzwerk verwendet so genannte Protokolle. Das sind genormte Regeln und Formate, an die sich die Kommunikationspartner halten müssen, um miteinander Daten austauschen zu können. (Beispiel: IP = Internet Protokoll. Das „Internet“ ist als ein weltweit verfügbarer Zusammenschluss von Computersystemen etwas anderes, verwendet aber das IP). Diesem Thema werden wir uns in einem eigenen Kapitel kurz widmen.

Die Netzwerkanbindung erfolgt über mehrere Schichten und reicht tief hinunter bis zur Hardwareebene, wo über Netzwerkkarten mit zugehöriger spezieller Software, den Netzwerk-Treibern (network driver), die Anbindung an das Netz selbst erfolgt. Da Netzwerkkarten und zugehörige Treiber von verschiedenen Herstellern für verschiedene Betriebssysteme angeboten werden und die Kommunikation zwischen Computersystemen mit unterschiedlichster Hardware und verschiedenen Betriebssystemen erfolgen soll, ist es besonders wichtig, dass solche Details vom Betriebssystem nach oben hin über Protokolle verborgen werden. Es ist nur notwendig, dass sich Kommunikationspartner über das zu verwendende Protokoll vorab verständigen.

A.2.6 Betriebssystemaufbau

Dass durchaus mehrere Benutzer zur selben Zeit diverse Dienste des Betriebssystems in Anspruch nehmen können, wird in der folgenden Abbildung als Beispiel gezeigt. Zu erkennen ist, dass Betriebssysteme zusammen mit den Programmen der einzelnen Anwender – jedenfalls grundsätzlich – selbst in Schichten zu sehen sind.

Abb. A4  Schichtenmodell (mit mehr Details)

Jede Schicht bietet Dienste für die darüber liegende (sofern vorhandene) Schicht und nimmt umgekehrt Dienste der darunter liegenden (sofern vorhandenen) Schicht in Anspruch.

Eine spezielle Schicht ist die Hardware-Abstraktion als Virtualisierungsschicht. Wie schon erwähnt ist ihr Zweck, bestimmte von Herstellern abhängige Merkmale zu verbergen, sodass das Betriebssystem sozusagen von einer gleich bleibenden virtuellen Maschine ausgehen kann. Auf dieser Ebene liegen auch die Treiber für diverse periphere Geräte unterschiedlichster Hersteller. Man erzielt damit eine größere Unabhängigkeit von der Hardware und erleichtert so die Portierbarkeit von Betriebssystemen. Portierbarkeit hat als Ziel, ein und dasselbe Betriebssystem für unterschiedliche Hardware installieren zu können.

In Abb. A4 ist auch der Begriff Betriebssystem-Kern (Kernel) eingeführt. Der Kernel sorgt für zentrale Betriebssystem-Dienste, die immer verfügbar sein müssen. Wir kommen darauf später (vgl.: Microkernel) nochmals zurück.

A.2.7 Betriebsmittel (Ressourcen)

Man kann ein Betriebssystem auch als Sammlung von speziellen Programmen sehen, die zum Management und zur Benutzung der Ressourcen eines Systems dienen. Aus dieser Sicht heraus ist das Betriebssystem verantwortlich für die Zuweisung, Freigabe und laufende Verwaltung von gemeinsam verfügbaren Ressourcen sowie für die Steuerung der Benutzung von Ressourcen.

Der Begriff Betriebsmittel (Ressource, resource) ist im Zusammenhang mit Betriebssystemen sehr weit gefasst. Grundsätzlich kann man zwischen Hardware-Ressourcen und Software-Ressourcen unterscheiden, jedoch lassen sich Betriebsmittel auch nach der Art ihrer Verwendung einteilen:

- Passive Betriebsmittel: sie werden von Prozessen belegt, insbesondere Speicher (Plattenspeicher, Hauptspeicher), aber auch der im Hauptspeicher stehende Programmcode fallen darunter.

Darüber hinaus unterscheiden sich Betriebsmittel auch dadurch,ob sie mehreren Prozessen in irgendeiner Form gleichzeitig, also gemeinsam, zugeteilt werden dürfen, oder ob streng darauf zu achten ist, dass jeweils nur ein einziger Prozess sie belegt und erst nach der Freigabe ein anderer sie belegen darf.

- Im ersteren Fall spricht man von gemeinsam benutzbaren Betriebsmitteln (shareable resources).

- Der zweite Fall betrifft das exklusive Zuteilen eines Betriebsmittels mit der Sicherstellung, dass kein anderer Prozess zwischenzeitlich es belegen kann. Dieses durchaus heikle Thema wird unter Wechselseitiger Ausschluss (mutual exclusion) später behandelt.

Ein Drucker in einem Netzwerk wird im Allgemeinen für mehrere Arbeitsstationen gemeinsam verfügbar sein, er ist also „shared“. Sobald aber ein Druckvorgang gestartet wurde, ist es wohl wünschenswert, dass der Drucker exklusiv vom betreffenden Druckprozess belegt bleibt, bis der Ausdruck abgeschlossen ist.

B HARDWARE MECHANISMEN

B.1 Allgemeines

Im Wesentlichen besteht ein Prozessor bzw. der im Text als Referenzsystem verwendete – triviale – Prozessor aus folgenden drei Funktionsgruppen, die in Abb. B1 schematisch dargestellt sind:

- Leitwerk: Es steuert die Abarbeitung eines Programms, führt also den Prozess aus, der dadurch entsteht.

- Rechenwerk: Dieses ist zuständig für arithmetische und logische Befehle.

- Registersatz: Dieser besteht aus einzelnen, nicht im Hauptspeicher liegenden, speziellen Speicherplätzen, so genannten Registern, und dient zur Aufnahme von Befehlen und ihren Operanden sowie von Zwischenergebnissen.

Abb. B1 Prozessorarchitektur mit Speicher und Ein-/Ausgabe

Besondere Register sind der Akkumulator a, der Programmzähler (Program Counter) pc und das Instruktionsregister ir, wobei letzteres rein prozessorintern und für den Programmierer i.Allg. nicht direkt sichtbar ist.

Im ir steht jene Instruktion, die als nächste zur Ausführung kommen soll, der pc zeigt auf jene Adresse im Hauptspeicher, aus der die auszuführende Operation im so genannten Fetch-Zyklus zu laden ist.

Das Zusammenspiel von ir, pc und Instruktionsausführung wird durch den nach J. v. Neumann benannten Von-Neumann-Zyklus beschrieben, auf den später genauer eingegangen wird.

Als zusätzliche Komponenten für einen einfachsten Mikrocomputer wird noch ein Schreib-/ Lesespeicher (RAM = Random Access Memory) für Daten und ein Festwertspeicher (ROM = Read Only Memory) für den Programmcode hinzugenommen. (Beachte: Random Access beschreibt die logische Eigenschaft, einzelne Speicherplätze direkt unter Angabe ihrer Adresse erreichen zu können. Read Only ist die physikalische Eigenschaft des Mediums, nach dem erstmaligen „Beschreiben“ nur mehr davon lesen zu können, wobei unterstellt ist, dass der ROM Speicher nichtflüchtig ist, sein Inhalt auch nach Abschalten der Versorgungsspannung persistent bleibt. Dass ein ROM-Speicher typischer Weise aber auch Random Access erlaubt, ist damit nicht angesprochen).

Den Hauptspeicher selbst betrachten wir als eindimensionales Feld m[0], m[1], …, m[max], wobei wir für die nachfolgenden Code-Beispiele folgende Vereinbarung treffen: der (flüchtige) RAM-Speicher sei von m[0] bis m[63] und das (nicht flüchtige) ROM sei in m[64] bis m[max] angelegt.

Neben den genannten Registern habe unser Prozessorsystem noch ein aus nur 2 Bits bestehendes Flagregister f mit der Unterteilung in

f.i : Interrupt-Enable-Flag

f.z : Zero-Flag

Auf deren Zweck und Verwendung kommen wir später im Detail zurück.

Ferner verfüge der Prozessor noch über einen im RAM angelegten Stack st, dessen Lage durch den so genannten Stackpointer sp beschrieben wird. Beim Zugriff auf den Stack st wird zur Realisierung von Einfügen (push) und Herausnehmen (pop) der Stackpointer sp verändert.

Da im Beispielsprozessor der Stack von höheren Speicheradressen in Richtung niederer Adresse wächst, wird beim Einfügen der Stackpointer (implizit) dekrementiert und beim Herausnehmen inkrementiert. In der Notation ist dies nicht sichtbar, denn wir schreiben z.B. st := a bzw. a := st für das Einfügen bzw. Herausnehmen des Akkumulatorinhaltes.

Zur Kommunikation mit der Außenwelt (über die in Abb. B1 links aufgeführten Ein-/ Ausgabeschnittstellen) verfüge der Prozessor über mehrere Ports, die mit p[i], i = 0,1,… bezeichnet werden.

Als maschinenahe Notation verwenden wir einen Assembler-Pseudocode und geben dazu ein Beispiel, wobei die erste Instruktion (willkürlich) auf Adresse 100 stehen soll. Darüber hinaus unterstellen wir jedem Speicherplatz die „wundersame“ Eigenschaft, genau einen Befehl unbeschadet dessen wahrer Länge aufnehmen zu können.

Code B1 Erstes Beispiel für Assembler Pseudocode

B.2 Von-Neumann-Zyklus

Nachfolgend wollen wir durch ein dynamisches Modell den Ablauf eines Programms beschreiben. Das Leitwerk bestimmt über den Program-Counter pc, welche Operation als nächste auszuführen ist.

Die folgenden Referenzen verweisen alle auf die Abb. B2, welche den Von-Neumann-Zyklus schematisch darstellt.

Zur Befehlsausführung wird die Speicherstelle, auf die der Program-Counter zeigt, eingelesen; dies erfolgt über ir:=m[pc].

Im Allgemeinen wird ein Programm sequentiell ausgeführt. Damit ist hier gemeint, dass die Befehle in aufeinander folgenden Speicherstellen stehen und der Reihe nach hintereinander ausgeführt werden. Daher wird der Program-Counter um die Länge des eben in das Instruktionsregister geladenen Befehles erhöht, um auf den folgenden Befehl zu zeigen. Dies erfolgt über pc:=pc+Befehlslänge. In der Beispielarchitektur sind – der Einfachheit halber – alle Befehle gleich lang und passen „per definitionem“ in eine Speicherstelle. Hier reicht also pc:=pc+1.

Die sequentielle Reihenfolge ist zwar der Standardfall des Programmablaufes, Befehle können aber davon abweichen. Man vergleiche dazu den Sprungsbefehl pc:=100 im obigen Code B1, mit welchem der Befehlszähler als Teil der Befehlsausführung auf einen neuen Wert gesetzt wird.

Damit sind wir bereits bei der Besprechung der Ausführung des im Instruktionsregister ir vorliegenden Befehles. Operationen können z.B. Daten von Registern in den Speicher kopieren, Register setzen, Werte abfragen, usw. Für Beispiele sei wieder auf den Code B1verwiesen.

Um auf asynchrone Ereignisse zu reagieren, kann dieser Programmzyklus auch unterbrochen werden (Interrupt). Gründe für Interrupts sind beispielsweise, dass neue Daten von der Eingabe zur Bearbeitung anstehen oder dass eine bestimmte Zeitspanne abgelaufen ist. Im Falle einer Unterbrechung wird der „normale“ Programmzyklus verlassen und die Ursache der Unterbrechung behandelt (Interrupt Handling).

Damit ist der Von-Neumann-Zyklus abgeschlossen und die nächste Befehlsausführung kann beginnen. Es war es zweckmäßig, den Program-Counter vorsorglich in Schritt 2 erhöht zu haben, sodass er bereits die Adresse der nächsten Instruktion enthält. Wie oben erwähnt kann aber im Instruktionsregister ein (bedingter) Sprungbefehl stehen, der den Wert des pc dann überschreibt.

Abb. B2 Von-Neumann-Zyklus

Die Schritte , ,  und  erfolgen dabei im Leitwerk und dienen zur Steuerung, Schritt  wird vom Rechenwerk durchgeführt und ist für die Ausführung einer Instruktion zuständig.

B.3 Laden des Betriebssystems, Reset-Vektor

Der Ablauf des Von-Neumann-Zyklus lässt die Frage offen, welche Instruktion als allererste nach dem Rechnerstart geladen und ausgeführt wird. Diese Startadresse beim Neustart bzw. beim Setzen eines Reset-Signals ist prozessorspezifisch manchmal eine fixe Adresse im ROM-Speicher (z.B. pc:=100). Oft wird aber – zur Steigerung der Flexibilität – an einer fixen Stelle im Speicher ein Zeiger auf die erste auszuführende Instruktion abgelegt. Im Beispielprozessor z.B. werde beim Neustart als prozessorinterne Startsequenz pc:=m[255] ausgeführt. Damit können auch Programme einen Rechnerneustart softwaremäßig simulieren, indem sie als Instruktion pc:=m[255] als indirektes „goto“ ausführen.

Typische Aufgaben der Instruktionsfolge direkt nach dem Start des Rechners sind

- das Setzen von Prozessorregistern (z.B. sp),

- die Initialisierung und Basiskonfiguration der Hardware und

- das Setzen von Werten im RAM-Speicher. Ein Beispiel für die Konfiguration ist das Setzen von Interrupt-Vektoren, deren Funktion und Verwendung in Kapitel B.6.4 bzw. ‎B.6.4 besprochen werden.

Bei aktuellen Rechnerarchitekturen beginnt darauf das "schrittweise" Laden und Starten des Betriebssystems. Dieser Vorgang wird als Bootstrap bezeichnet

Dieser Vorgang soll hier nur kurz skizziert werden: In der Firmware (hardwarenahe Programme in Maschinencode, auf ROM vorgegeben) des Computersystems befindet sich ein kleines Programm, der Bootstraploader. Wenn wie vorhin beschrieben die Startadresse beim Neustart des Systems genau auf die erste Instruktion dieses Bootstraploaders zeigt, beginnt genau dieses Programm zu laufen. Es liest von einer ebenso vordefinierten Stelle weg Daten ein, die „in Wahrheit“ ein wieder in Maschinencode vorliegendes aber schon leistungsfähigeres Programm sind. Dieses wird manchmal als Urlader bezeichnet.

Als letzte Aktion des Bootstraploader setzt dieser den pc auf die erste ausführbare Instruktion des eben hereingeholten Urladers, wodurch nun dieser gestartet wird und nach bestimmten Hardwareinitialisierungen auf jene Harddisk zugreift, auf der das zu startenden Betriebssystem gespeichert ist und von dort jenes Ladeprogramm herein liest.

Wieder ist die letzte Aktion das Setzen des pc nunmehr auf die Startadresse des im Hauptspeicher stehenden Betriebssystem-Ladeprogramms, welches nun endlich nach seinem Start das Betriebssystem (bzw. seinen Kern) laden und starten kann.

B.4 Ein-/Ausgabe

Wie bereits in Abb. B1 gezeigt müssen Prozessorsysteme mit der Außenwelt kommunizieren: Sie erhalten z.B. über Eingabeschnittstellen Informationen wie Tastatureingaben, Mausbewegungen, Schalterstellungen, Daten vom Netzwerk usw. Ergebnisse werden über Ausgabeschnittstellen der Umwelt mitgeteilt. Die Ausgabegeräte können die Ansteuerung von einfachen Leuchtdioden (LEDs) oder Relais, Bildschirmausgaben, Schreiben von Daten auf die Harddisk, Senden von Datenpaketen am Netzwerk usw. übernehmen.

Im Beispielprozessor sei die Ein-/Ausgabe wiederum sehr einfach:

- Eingabe: Von Port p[1] können Eingabedaten – beispielsweise eine Tastatureingabe – gelesen werden. Die Verfügbarkeit von Daten auf p[1] wird dabei durch eine Eins im Bit 0 (=least significant bit) von p[0] angezeigt.

- Ausgabe: Daten, welche auf p[2] geschrieben werden, gelangen (z.B. über eine einfache Siebensegmentanzeige) nach außen.

Damit stelle sich beispielsweise folgende einfachste Aufgabe:

Immer wenn Daten auf p[1] verfügbar sind, sollen diese (möglichst schnell) auf p[2] ausgegeben werden. Dies ist eine typische Echo-Funktion. Aus Gründen der Einfachheit wollen wir hier annehmen, dass keine Umsetzung der Daten (z.B. über eine Tabelle) zu erfolgen hat und auch sonst mit den eingelangten Daten keine weiteren Berechnungen auszuführen sind.

Abb. B3 Hardware-Beispiel

B.5 Polling

Nun kann kein Programm voraussehen, wann Daten am Port p[1] verfügbar sind. Was also bleibt der Software anderes übrig, als zu warten, bis ein Wert ausgelesen werden kann. Softwaretechnisch lässt sich dies durch ständiges Abfragen (polling) einfach lösen, wie die folgenden Programmsequenzen von Code B2 in Java und Code B3 auf Assemblerebene zeigen.

Code B2 Polling-Programmsequenz (Java) 100 a:=p[0]

Code B3 Polling-Programmsequenz (Assembler)

So einfach diese Codesequenz ist, so unbefriedigend ist sie aber auch. Der Prozessor wird „mit vollem Einsatz“ nur damit beschäftigt, auf eine Eingabe zu warten (busy waiting). Diese Wartezeit ist unproduktiv.

B.6 Interrupts

B.6.1 Grundlagen von Interrupts

Eine Unterbrechung des eben laufenden Programms (Prozesses) wird durch ein von außen an den Prozessor kommendes Interrupt-Signal ausgelöst. Der Prozessor hält den aktuell laufenden Prozess an und führt einen anderen Prozess – die so genannte Interrupt-Service-Routine (ISR) – aus. Nach Abschluss der Interrupt-Service-Routine wird der unterbrochene Prozess fortgesetzt.

Ein Interrupt-Signal kann also jederzeit und somit asynchron auftreten. Damit besteht keine Notwendigkeit mehr, über Polling / Busy-Waiting auf das Ereignis zu warten: Das externe asynchrone Ereignis löst über das Interrupt-Signal einen Interrupt aus. Die zugehörige Interrupt-Service-Routine (ISR) behandelt das Ereignis, führt also die erforderlichen Aktionen aus. Nach der softwaremäßigen Behandlung des Ereignisses durch die ISR wird der durch den Interrupt temporär angehaltene Prozess weiter ausgeführt.

Abb. B4 zeigt diesen zeitlichen Ablauf eines Interrupts kurz auf.

Abb. B4 Zeitablauf eines Interrupts

In unserer Beispielhardware ist die Umstellung einfach und soll eine Interrupt-Quelle unterstützen, an die eine Tastatur angeschlossen ist. Bisher wurde das Signal, dass eine Taste gedrückt ist, für die Software nur auf p[0].0 (=least significant Bit von Port 0) bereitgestellt. Nun wird dieses Signal nicht mehr (nur) an diesen Port, sondern (auch) an den Interrupt-Eingang des Prozessors geleitet. Abb. B5 zeigt diese Schaltung als Erweiterung von Abb. B3. Die Interrupt-Logik ist damit wie folgt: Das Interrupt-Enable-Flag f.i im Flagregister des Prozessors wird mit dem von der Interrupt-Quelle kommenden Interrupt-Signal mit „AND“ verknüpft, sodass Interrupts von der Tastatur nur dann ausgelöst werden können, wenn f.i auf Eins gesetzt ist. Damit kann die Software durch f.i:=0 den Interrupt sperren und mit der Instruktion f.i:=1 den Interrupt freigeben.

Abb. B5 Prozessor mit Interrupt-Eingang

Reale Prozessoren besitzen mehrere Interrupt-Eingänge. Die Logik für die Behandlung, für das Erlauben/Verbieten von Interrupts, die Prioritätsbehandlung zwischen verschiedenen Interrupts, wenn sie gleichzeitig oder verschachtelt auftreten, läuft im so genannten Interrupt-Controller ab. Dieser kann im Prozessor integriert oder auch als externer Hardwarebaustein realisiert sein. Auf Details dazu wird hier nicht weiter eingegangen.

B.6.2 Von-Neumann-Zyklus im Detail

Bevor die Implementierung von Interrupt-Service-Routinen (ISR) besprochen wird, ist nun der richtige Zeitpunkt, den Von-Neumann-Zyklus etwas genauer anzusehen. Auf das Grundkonzept zum Laden und Ausführen von Instruktionen wurde bereits im Punkt B.2 näher eingegangen. Dabei wurde in Abb. B2 die Interrupt-Phase aber nur kurz gestreift. Die folgenden Ausführungen beziehen sich auf die verfeinerte Abb. B6.

Betrachten wir vorab die allgemeinen Voraussetzungen:

Eine Interrupt-Service-Routine (ISR) wird nur aufgerufen, wenn

- der Interrupt freigegeben ist – also in der Beispielarchitektur das Interrupt-Enable-Flag f.i im Flagregister f gesetzt ist, und

- auch ein Interrupt zur Bearbeitung ansteht, also pending ist.

Zusammengefasst lauten diese beiden Bedingungen, die im Von-Neumann-Zyklus nach Abarbeitung jeder Instruktion geprüft wird somit:

if (f.i==1) & (Interrupt pending) { ... }

Eine entsprechende logische Schaltung befindet sich als Interrupt-Logik im Prozessor (bzw. im Interrupt-Controller). Schematisch wurde diese Schaltung bereits auf Abb. B5 gezeigt.

Da nach Beendigung einer ISR das unterbrochene Programm „genau in dem Zustand, wie es war“ fortgesetzt werden muss, ist der gesamte Programmzustand für die spätere Programmfortsetzung zu sichern. Dieser Programmzustand (program status) besteht bei unserem einfachsten Beispielprozessor neben anderen Daten aus dem (genauer: wird beschrieben durch den Inhalt des)

Program-Counter (pc), denn ohne diesen wäre unbekannt, mit welcher Instruktion das unterbrochene Programm fortgesetzt werden muss.

Auch sind als Teil des Programmzustandes alle Prozessorregister zu sichern, die durch die ISR geändert werden. In der besprochenen Beispielarchitektur sind dies das Flag-Register f und der Akkumulator a. Üblicherweise spricht man vom Sichern oder Retten eines Registers, meint damit aber das Speichern des Inhaltes davon, sodass dieser später wieder restauriert werden kann.

Im Beispiel wird dafür der bereits früher vorgestellte Stack st als Zwischenspeicher eingesetzt. Dabei wird das Flagregister f in der Interrupt-Phase automatisch gesichert. Somit ist auch der Wert des Interrupt-Enable-Flags f.i vor dem Interrupt-Aufruf bekannt. (Dies hat zwar momentan noch wenig Bedeutung, wird aber beimPunkt B.6.4 später noch interessant.)

Die Sicherung des Akkumulators (als wichtiges Beispiel für ein Prozessorregister) wird hingegen der ISR selbst überlassen. Diese führt also als erste Instruktion st:=a aus, um den Akkumulatorinhalt des unterbrochenen Programms zu retten, wie später in Abb. B7 zu sehen ist.

Es sei erinnert, dass Unterbrechungen als asynchrone Signale jeder Zeit auftreten könnten, demnach sogar noch ehe ein Interrupt vollständig behandelt worden ist. Um sicherzustellen, dass nicht in den Instruktionen der ISR dieser Interrupt wieder auftritt, sich also Interrupt-Service-Routinen „endlos“ schachteln, wird in der Interrupt-Phase automatisch das Interrupt-Enable-Flag f.i auf Null gesetzt. Wir lassen hier offen, ob bzw. mit welchen Maßnahmen ein solches ignoriertes Signal später behandelt oder einfach als verloren betrachtet wird.

Offen bleibt noch, wo im Speicher sich die ISR befindet. Oft kann durch die Software ein Zeiger auf die einer Interrupt-Quelle zugehörige Interrupt-Service-Routine gesetzt werden. Der Einfachheit befinde sich dieser Zeiger (pointer) in der Beispielarchitektur auf Adresse 0, also in m[0].

Wir erhalten somit folgenden – in Abb. B6 dargestellten – Ablauf des Von-Neumann-Zyklus:

Abb. B6 Von-Neumann-Zyklus mit detaillierter Interrupt-Behandlung

B.6.3 Aufbau einer Interrupt-Service-Routine (ISR)

Die in Abb. B7 gezeigte ISR beginnt somit mit dem Retten des Akkumulators (st:=a) als Beispiel für das Sichern aller Prozessorregister und führt dann die Aktivität der Serviceroutine aus, welche sich im Beispiel auf den Transfer der Daten von Port 1 auf Port 2 beschränkt (a:=p[1]; p[2]:=a).

Anmerkung: Eine Prüfung, ob auf Port 1 wirklich gültige Daten ausgelesen werden können, wie dies bei der Polling-Lösung der Fall war, erübrigt sich, da der Interrupt nur ausgelöst wird, wenn dies der Fall ist. Man vergleiche dazu die schematische Schaltung in Abb. B5. Ein Interrupt wird durch das AND-Gatter „&“ nur dann ausgelöst, wenn (a) das Interrupt-Enable-Flag auf 1 steht, also Interrupts nicht gesperrt sind und (b) auch Daten von der Tastatur auf Port 1 anliegen.

Vor Verlassen der Serviceroutine wird der Akkumulator wieder restauriert. Die Instruktion  reti = „Return from Interrupt“  muss nun dafür sorgen, dass wieder in das durch den Interrupt unterbrochene Programm zurückgekehrt wird.

Abb. B7 Prozessor- und Speicherstatus kurz vor Verlassen der ISR

Abb. B7 zeigt den System-Gesamtstatus direkt nach Ausführung des Befehls auf Adresse 203: Die ISR ist damit zwar bereits abgearbeitet, die Rückkehr in das unterbrochene Programm aber noch nicht vollzogen. Der Akkumulator a wurde eben restauriert (restored).

Betrachtet man nun den  Inhalt des durch das Register sp adressierten Stacks st, so ist zu erkennen, dass sich direkt vor dem Rücksprung aus der ISR noch der Program-Counter pc auf m[sp+1] und das Flagregister f auf Position m[sp+2] befinden. Die Instruktion Return from Interrupt (reti) muss damit diese Daten noch vom Stack holen. Dadurch wird u.a. auch das Interrupt-Enable-Flag f.i wieder korrekt gesetzt und durch das Restaurieren des Program-Counters pc wird das unterbrochene Programm an der richtigen Stelle (in Abb. B7 auf Adresse 215) fortgesetzt.

B.6.4 Software Interrupts (SWI)

Interrupts sind asynchrone Ereignisse und daher werden ISRs zu zeitlich nicht (genau) abschätzbaren Zeitpunkten aktiv. Möchte man nun eine ISR synchron aktivieren, so gibt es dazu mehrere Möglichkeiten:

- Man könnte z.B. den Interrupt mit f.i:=0 sperren und dann so lange warten, bis ein Interrupt pending ist. Direkt nach dem Befehl f.i:=1 würde der Interrupt aktiv.

- Besser ist, den Interrupt softwaremäßig zu simulieren. Dazu werden die Aktivitäten des Von-Neumann-Zyklus beim Vorliegen einer Interrupt-Anforderung in Software nachgebildet. Man vergleiche Abb. B7.

- Prozessoren unterstützen auch durch eigene Instruktionen das softwaremäßige Auslösen von Interrupts bzw. Aufrufen von Interrupt-Service-Routinen. Die Instruktion „swi“ ruft dann die ISR softwaremäßig (daher auch der Name swi = software interrupt) auf. Da handelsübliche Prozessoren mehrere Interrupts unterstützen, wird oft bei „swi“ als Zusatzinformation die Nummer des Interrupts angegeben. Die Instruktion „swi 0“ aktiviert dann (konsistent zu Abb. B7) die ISR auf Adresse 200 für die Tastatur, „swi 1“ die Service-Routine auf Adresse 180, usw.

Wir werden später bei der Besprechung von Betriebssystemaufrufen wieder auf das Konzept der Software-Interrupts zurückkommen.

B.7 Prozeduren und Prozeduraufrufe

B.7.1 Vergleich Interrupts, Software-Interrupts, Prozeduren

Interrupts, Software-Interrupts und Prozeduraufrufe (procedure calls) haben eine Gemeinsamkeit. Alle drei Konzepte müssen nach Abschluss ihrer Funktion den Programmablauf wieder nach der Aufrufinstruktion fortsetzen. Neben der Art des Aufrufes gibt es Unterschiede beim Zeitverhalten, bei Parametern und im resultierenden Programmstatus:

- Zeitverhalten: Während „echte“ Interrupts asynchron und nicht vorhersehbar auftreten, sind Software-Interrupts und Prozeduren durch den Aufruf per Prozessor-Instruktion(en) deterministisch im Programmablauf eingebettet.

- Parameter: Asynchrone ISRs können keine Parameter verwenden, während bei Prozeduren die Parameterversorgung und damit das Konzept der Parameterübergabe besonders wichtig ist.

- Programmstatus: Asynchrone (also „echte“) ISRs dürfen den Zustand des angehaltenen Programms nicht ändern und sind damit für die Sicherung und das Restaurieren des Status zuständig. Verwendet eine asynchrone ISR z.B. ein Prozessorregister, so muss dieses vorher gerettet und nachher wieder restauriert werden (vgl. Punkt B.6.2). Prozeduren hingegen dürfen bestimmte Register des Prozessors ändern, ohne sich um den früheren Registerwert kümmern zu müssen. (Die Aufrufkonventionen für Prozeduren sind von der Prozessorarchitektur, der verwendeten Programmiersprache und dem eingesetzten Compiler und teilweise von Compiler-Switches abhängig. Welche Register von Prozeduren wie verwendet werden bzw. werden dürfen, lässt sich also nicht generell sagen.)

Die folgende Tabelle B1 zeigt in tabellarischer Form Unterschiede nochmals auf, wobei man sieht, dass Software-Interrupts irgendwie ein „Zwitterdasein“ führen.

Tabelle B1 Vergleich Interrupts, Software-Interrupts, Prozeduren

B.7.2 Prozeduren und ihre Parameterversorgung

Heute kann kein Programm ohne Prozeduraufrufe auskommen, und jeder Prozessor unterstützt dafür notwendige Instruktionen. Da Prozeduraufrufe u.a. bei

- Betriebssystemaufrufen (d.h. Aufruf von Komponenten eines Betriebssystems z.B. durch ein Anwenderprogramm, aber auch durch das Betriebssystem selbst beim Übergang von einer Schicht zu einer anderen) und bei

- RPCs (Remote Procedure Calls, zum Aufruf und Starten von Prozessen über ein Netzwerk hinweg) verwendet werden, wollen wir ihr Basiskonzept im Folgenden kurz besprechen.

B.7.2.1 Prozeduraufrufe generell

Das grundlegende Konzept für Prozeduraufrufe ist kurz skizziert wie folgt: Da eine Prozedur (procedure) bzw. ein Unterprogramm (subroutine) nach Abarbeitung ihrer/seiner Funktion wieder zum Aufrufer zurückkehren muss, ist die Rücksprungsadresse – also die Adresse der dem Prozeduraufruf folgenden Instruktion – beim Aufruf zu sichern. Um Prozeduren auch schachteln und rekursiv aufrufen zu können bietet sich der Stack als ideale Stelle für das Abspeichern der Rücksprungsadressen an.

Wir führen dazu zwei neue Instruktionen für unseren Referenzprozessor ein:

- Die Instruktion Jump-to-Subroutine „js” rettet die Rücksprungsadresse am Stack (schematisch: m[sp]:=pc; sp:=sp-1) und führt dann einen Jump (pc:=xxx) durch.

- Am Ende der aufgerufenen Prozedur führt die Return-Instruktion „ret“ quasi die inverse Funktion aus: Der Program-Counter wird vom Stack geholt (schematisch also pc:=m[sp+1]; sp:=sp+1).

Als Beispiel soll nun eine Prozedur WaitForKey vorgestellt werden, welche einfach „wartet“, (Beschäftigtes Warten; busy waiting) bis eine Taste gedrückt wird und dann erst zum Aufrufer zurückkehrt.

Code B4 Prozedur WaitForKey inkl. Prozeduraufruf

Um Daten einer Prozedur übergeben zu können, werden zwei grundsätzliche Konzepte eingesetzt: (a) Bei Call by Value wird der Wert eines Parameters als Kopie an die Prozedur übergeben, während bei (b) Call by Reference die Adresse, wo der Parameter im Hauptspeicher liegt, der Prozedur mitgeteilt wird. Beide Konzepte werden in den folgenden Punkten jeweils an Hand von Beispielen vorgestellt.

B.7.2.2 Rückgabe eines Funktionswertes

Eine Erweiterung der Prozedur WaitForKey zur Rückgabe des Tastenwertes ist einfach möglich. Wir wollen hier annehmen, dass – wie in vielen gängigen Implementierungen – der Funktionswert im Akkumulator a an den Aufrufer zurückgegeben wird. Die Prozedur public char ReadKey() lautet daher:

Code B5 Prozedur ReadKey inkl. Prozeduraufruf

Anmerkung: Die Prozedur ReadKey wird später im Kontext Betriebssystemaufrufe (Stichwort „API“) wieder als typisches Beispiel auftauchen.

Ähnlich wie die Rückgabe von so genannten Funktionswerten können auch Werte als Eingabeparameter für die Prozedur über Register übergeben werden. Leider ist die Anzahl der Prozessorregister beschränkt, sodass ein allgemeines speicherbasiertes Konzept zur Parameterübergabe erforderlich wird.

B.7.2.3 Parameter über Call by Value

Mit Call by Value ist eine Technik angesprochen, bei der eine Kopie des beim Aufruf mitzugebenden Datums an einer vereinbarten Stelle abgelegt wird. Dafür eignet sich wieder der Stack als Adressierung. Der Aufrufer legt vor dem Prozeduraufruf die Parameter auf den Stack. Die aufgerufene Prozedur greift über den Stack darauf zu. Meist nimmt das aufgerufene Programm beim Rücksprung die Daten direkt vom Stack. Dazu wird die Return-Instruktion um einen Wert erweitert, um welchen (in der Referenzarchitektur) der Stackpointer sp beim Rücksprung erhöht wird.

Als Beispiel werden in der Prozedur Sum0 einfach zwei Zahlen, die im Beispiel auf m[24] und m[25] stehen, zusammengezählt und das Ergebnis als Funktionswert (also im Akkumulator a) zurückgegeben. Der Inhalt von a wird schlussendlich vom Aufrufer auf Adresse m[24] abgelegt, weil er valA:=valA+valB berechnen möchte. (Dabei sei jeder Wert jeweils eine Speicherstelle groß.)

Da an dieser Stelle die Adressierung innerhalb des Stacks besondere „Herausforderungen“ stellt, wollen wir dieses Beispiel nicht nur als Code, sondern auch die Datenwerte und den Stack in Abb. B8 ansehen.

Abb. B8 Prozedur Sum0: „Call by Value“

Gut zu sehen sind die Kopien der Werte valA und valB als Parameter am Stack. Diese beiden Werte wurden vom Aufrufer mit den Instruktionen st:=m[24] und st:=m[25] in den Adressen 160 und 161 auf den Stack gelegt. Das Betreten der Prozedur Sum0 erfolgt durch Ausführung des Befehls js 180 in der Annahme, dass der Code dafür ab Adresse 180 steht.

Die Rücksprungadresse 163 wird dadurch in den Stack gebracht und der Stackpointer sp auf den Wert 55 adjustiert.

Der Zustand unmittelbar vor der Rückkehr aus dem Unterprogramm ist dann wie folgt: Der Prozessor hat eben die Instruktion 181 beendet und die Summe steht bereits im Akkumulator a. Nun ist nur noch der Rücksprung durchzuführen: „ret 2“ holt den geretteten Program-Counter mit Wert 163 vom Stack und löscht dann zusätzlich die beiden Parameter. Letzteres geschieht, weil statt ret die Instruktion ret 2 ist und dadurch der sp um insgesamt 3 heruntergezählt wird. Damit muss das aufrufende Programm den Wert nur mehr auf valA (m[24]) schreiben und die Operation valA:=Sum0(valA, valB) ist abgeschlossen.

Möchte man nun aber die gesamte Operation valA:=valA+valB in das Unterprogramm packen, so stellt sich ein neues Problem. Offensichtlich macht es keinen Sinn für die Prozedur Sum0, die Kopie von valA (also parA) auf dem Stack zu ändern, da diese Änderung durch das darauf folgende ret 2 verworfen wird. Bis jetzt wurden nur die Werte als Kopie – daher die Bezeichnung Call by Value – übergeben.

B.7.2.4 Parameter über Call by Reference

Damit ein Unterprogramm einen Parameter auch sinnvoll ändern kann, muss dem Unterprogramm nicht eine Kopie des Wertes, sondern die Adresse des Parameters selbst übergeben werden. Daher kommt auch die Bezeichnung Call by Reference.

Die Abb. B9 zeigt die Prozedur Sum1(), die zum Paramter1 den Wert des zweiten Parameters hinzuzählt. Mit Sum1(valA, ValB) wird also valA:=valA+valB ausgeführt.

Anstatt also den Wert 4 als Kopie von valA auf den Stack zu legen, wird der Aufrufer nun die Adresse von valA (also 24) dem Unterprogramm bekannt geben. Das Unterprogramm selbst muss von jedem Parameter wissen, ob dieser über Call by Value oder Call by Reference übergeben wird. Nur so kann die Prozedur die Adressierungsart richtig wählen.

In der noch nicht ausgeführten Instruktion 185 wird die Summe 9 auf die Speicherstelle 24 geschrieben und dann die Prozedur mit „ret 2“ verlassen.

Abb. B9  Prozedur Sum1: „Call by Reference“ für den ersten Parameter

B.7.2.5 Reale Implementierungen

Bereits in Punkt B 7.1 wurde darauf hingewiesen, dass reale Compiler bei Prozeduren und deren Aufrufen verschiedene Konventionen beachten und Optimierungen durchführen. Ein Beispiel wäre die Übergabe von Teilen der Parameter über Register, dann erst über den Stack. Auch haben Prozessoren eigene Register, Befehle und Adressierungsarten, um effizient mit Parametern umgehen zu können. Für weitere Details sei hier auf entsprechende Bücher über Compilerbau und Rechnerarchitektur verwiesen.

C BETRIEBSARTEN

An ein Betriebssystem werden vom Anwender bestimmte Aufgaben zur Erledigung übertragen. Unter Betriebsart versteht man die Art und Weise, wie das Betriebssystem solche Aufgaben übernehmen und durchführen kann.

Ein Betriebssystem (bzw. das von ihm verwaltete Computersystem) kann dezidiert für nur einen einzigen Benutzer (vgl.: Workstation, Arbeitsplatzrechner) oder für mehrere Benutzer (vgl.: Terminalserver) konzipiert sein. Im ersteren Fall spricht man von einem Einbenutzerbetrieb (single user mode), im zweiten Fall von einem Mehrbenutzerbetrieb (multi user mode).

Das System kann dafür ausgelegt sein, mehrere als Prozesse ablaufende Aufgaben (Tasks) einfach der Reihe nach oder nach Prioritäten oder sogar parallel bzw. ineinander verzahnt abzuarbeiten.

Auf die Frage, wie „Parallelität“ bei einem System erreicht wird, das nur über eine einzige CPU verfügt, kommen wir später im Detail zurück.

Abhängig von Betriebsarten kann man eine prinzipielle Klassifikation von Betriebssystemen vornehmen, nämlich

- Stapelverarbeitung (Batchprocessing)

- Mehrprogrammbetrieb (Multitasking, auch Multiprogramming genannt)

- Teilnehmerbetrieb (Timesharing)

- Echtzeitverarbeitung (Real-Time)

In der Realität beherrschen moderne Betriebssysteme oft mehrere Betriebsarten gleichzeitig, sind also in diesem Sinne hybrid.

C.1 Stapelverarbeitung (Batchprocessing)

Das Betriebssystem früherer Maschinen war sehr einfach und entsprechend den damals verfügbaren Hauptspeichergrößen auch klein im Umfang. Die Hauptaufgabe war, nach Ablauf eines Benutzerauftrages (Job) automatisch den nächsten Job zu starten. Während ein Job ablief hatte er Kontrolle über die gesamte Maschine. Es war keine Benutzerinteraktion möglich. Sobald der Job fertig abgearbeitet war, gab er die Kontrolle an das Betriebssystem zurück und der nächste Auftrag wurde ausgeführt. Um die Ausführungsgeschwindigkeit zu erhöhen wurden ähnliche Aufträge gestapelt (batched) und sobald der Computer bzw. die CPU wieder frei war, wurde der jeweils nächste Job aus dem Auftragstapel ausgeführt. „Stapel“ kommt bildlich daher, dass die Benutzeraufträge in Form von Lochkarten-Stapeln nacheinander (übereinander) gereiht und diese in einen Lochkartenleser gelegt worden sind.

Dies setzt demnach voraus, dass in einem Stapelverarbeitungssystem jeder Auftrag vorab zusammen mit den benötigten Eingabedaten vollständig definiert sein muss. Die Kommandos an das Betriebssystem wurden in einer eigenen herstellerabhängigen Notation, der Job Control Language (JCL) codiert und bei Lochkarteneingabe in den Stapel eingefügt oder über eine Tastatur dem System übermittelt.

Im Betriebssystem MS–DOS z.B. gibt es dazu eigene Batch-Befehle und Batch-Dateien (.BAT). Dieses Konzept ist auch bei Windows in der so genannten „DOS-Box“ mit den dort definierten Befehlen vorzufinden.

Mit der Einführung von Festplattenspeichern konnte man die Jobstapel (Benutzeraufträge, Jobs) zunächst in einen Puffer dorthin lesen und dann von dort für die Ausführung abholen. Damit besteht die Möglichkeit, die in diesem Zwischenpuffer abgelegten Jobs noch vor dem Beginn ihrer Abarbeitung z.B. nach Prioritäten umzureihen.

Während der Abarbeitung einzelner Aufträge ist die CPU oft ungenützt, weil I/O Geräte wesentlich langsamer arbeiten. Eine Effizienzsteigerung erzielte man dadurch, dass periphere Geräte in bestimmtem Umfang autonom arbeiten konnten bzw. können und währenddessen als eigenständige I/O Prozesse nicht die CPU in Anspruch nehmen müssen. Unter Ausnutzung des Interrupt Konzeptes kann man eine zeitliche Überlappung mit I/O Prozessen erzielen: Letzteres findet man bei so genannten Spooler-Systemen (Simultaneous Peripheral Operation OnLine).

Ein Spooler (!das ist ein Kunstwort!) ist ein Betriebssystemprozess zusammen mit einem Puffer, der die Ein-/Ausgabe für ein Gerät verwaltet und typischerweise mit der CPU über Interrupts kommuniziert. Das Spooling-Konzept wurde erstmals 1961 auf der in Manchester (UK) entwickelten „Atlas-Maschine“ eingesetzt.

Das folgende Beispiel soll das Spooler-Konzept nochmals verdeutlichen: Ein Drucker kann nur einen Druckprozess sinnvoller Weise übernehmen und muss weitere anstehende zurückreihen, also die Aufträge serialisieren: Wenn mehrere Applikationen nahezu gleichzeitig drucken wollen, werden solche Druckaufträge bzw. die zugehörigen Daten in einem speziell dafür eingerichteten Ausgabe-Puffer zwischengespeichert, bis sie an die Reihe kommen. Dabei läuft die Bedienung des Druckprozesses überlappt mit der CPU ab, und zwar durch Unterbrechungssignale (Interrupts) gesteuert: wenn eine I/O Operation erfolgreich abgeschlossen ist, meldet sich der angesprochene Drucker mit einem Unterbrechungssignal zurück und wird über die CPU mit einer neuen Aufgabe (z.B. nächste Zeile drucken) betraut. Bis zum Eintreffen des nächsten I/O – Interrupts kann dann die CPU an dem eigentlichen Programm weiterarbeiten. Spooling ist in diesem Sinne eine einfache Art des Mehrprogrammbetriebes bzw. ein Vorläufer davon.

C.2 Mehrprogrammbetrieb (Multiprogramming, Multitasking)

Beim Multiprogramming (Multitasking, die Bezeichnungen in der Literatur sind dazu nicht einheitlich bzw. beide Begriffe werden synonym verwendet) nützt das Betriebssystem die Zeit aus, die ein in Ausführung befindlicher Prozess mit (unfreiwilligem) Warten zubringt, um zwischenzeitlich einen anderen Prozess auszuführen.

Insbesondere bei verschiedenen I/O-Operationen kann es vorkommen, dass die CPU nicht ausgelastet bleibt, nicht nur deswegen, weil I/O Geräte in ihren Ausführungsgeschwindigkeiten um mehrere 10er Potenzen langsamer als die CPU sind. Man denke nur an das einfache Beispiel, dass ein laufendes Programm auf die Eingabe von Daten über die Tastatur warten muss. Generell kann es sein, dass ein benötigtes Betriebsmittel (noch) nicht vorhanden ist und der anfordernde Prozess bis dahin zu warten hat.

In solchen Fällen lässt sich das System effektiver gestalten, wenn während dieser Wartezeit die CPU zwischenzeitlich einem anderen Prozess zugeteilt wird. Tritt auch dort wieder ein Wartezustand auf, so kann auf den nächsten Prozess weitergeschaltet werden. Die maximale Anzahl derart bedienbarer Prozesse nennt man Multiprogramming-Grad.

In seiner ursprünglichen Form war Multitasking so konzipiert, dass ein Prozess sehr hohe Priorität hat und die CPU zwischenzeitlich einem anderen Prozess nur dann zugeordnet werden kann, wenn sie, aus welchem Grund auch immer, frei wird. Ein solcher Grund, wie schon erwähnt, wäre das Warten im Zusammenhang bei Ein/Ausgabe. Heute sind Multitaskingsysteme und das Hin- und Herschalten der CPU sehr komplex geworden. Wir gehen auf dieses Thema unter dem Fachbegriff CPU-Scheduling in einem eigenen Kapitel nochmals ein

C.3 Teilnehmerbetrieb (Timesharing)

Timesharing-Systeme gehen noch einen Schritt weiter: Jedem anstehenden Prozess wird (der Reihe nach zyklisch) eine gewisse Zeit die CPU zugeteilt. Nach Ablauf dieser Zeit (man spricht in diesem Zusammenhang von Rechenzeitperioden (time slice, time quantum) muss der Prozess die CPU wieder abgeben, und der nächste Prozess kommt zum Zug. Dies wird zyklisch wiederholt.

Die Reihenfolge der Prozesse und die „Länge“ einer Rechenzeitperiode hängen von den verwendeten Scheduling-Verfahren ab. Tritt innerhalb eines Zeitabschnittes ein Wartezustand auf, so kann z.B. sofort zum nächsten Prozess weiter geschaltet werden. Die zugeteilten Perioden sind a priori nicht für alle Prozesse gleich lang, sodass man damit implizit auch Prioritäten (im Sinne einer Bevorzugung) in der Abarbeitung von Prozessen setzen kann.

Timesharing macht erst Sinn, wenn der Hauptspeicher eine bestimmte Minimalgröße hat, sodass mehrere Prozesse darin gleichzeitig resident gehalten werden können. Insofern spielen Schutzmechanismen bei Multiprogramming und Timesharing eine wichtige Rolle, weil verhindert werden muss, dass einzelne Prozesse unbefugt auf Speicherbereiche zugreifen, die für andere Prozesse reserviert sind.

C.4 Echtzeitverarbeitung (Real-Time Processing)

Echtzeitbetriebssysteme (Real-Time Systems) unterscheiden sich wesentlich in ihrer Zielsetzung von den voran erwähnten Systemen: Ein Benutzerauftrag muss in einer definierten Zeit, vor Ablauf einer Deadline erledigt werden bzw. das System muss innerhalb einer bestimmten Zeitspanne reagieren. Eine verspätete Reaktion wird als schwerer Fehler gesehen.

Damit sind Real-Time Systeme in ihrer Architektur von anderen Systemen unterschiedlich aufgebaut: Das wesentliche Merkmal eines Real-Time Operating Systems ist demnach, dass es in zeitlich vorhersehbarer Weise auf das Eintreffen von externen Signalen reagiert.

Man klassifiziert Real-Time Betriebssysteme in Hard Real-Time Systems und Soft Real-Time Systems.

- Hard Real-Time Systeme sind in der Lage, unmittelbar und rechtzeitig bzw. innerhalb von der Anwendung vorgegebenen Schranken auf eintreffende Signale zu reagieren. Ein Beispiel dazu ist die Software für ein Antiblockiersystem (ABS). Ein verspätetes Reagieren auf das Blockieren der Räder eines Fahrzeuges würde das System unbrauchbar machen. Ein weiteres dramatisches Beispiel ist ein Überwachungssystem in einer Intensivstation. Bei einer lebensbedrohlichen Situation muss sofort Alarm ausgelöst werden.

- Bei Soft Real-Time Systemen versucht man, möglichst nahe an die idealen Vorgaben heranzukommen, nimmt aber gelegentliche Ausnahmen davon in Kauf. Moderne Betriebssysteme verfügen mittels Prioritäten bei Prozessen über ein solches Konzept. Einem Viren Scanner sollte es z.B. möglich sein, beim Erkennen eines Virus sofort reagieren zu können, obgleich die Reaktion i.Allg. hier nicht besonders zeitkritisch ist.

Der grundlegende Unterschied zwischen Hard Real-Time und Soft Real-Time Systemen kann so gesehen werden: Ihre „Echtzeitanforderungen“ sind unterschiedlich: Bei Hard Real-Time Systemen ist das Überschreiten einer Zeitschranke (Deadline) ein schwerwiegender Systemfehler, bei Soft Real-Time Systemen ist der Mittelwert der Antwortzeiten ausschlaggebend. Wird ausnahmsweise die Reaktionszeit überschritten, so kann dies – abhängig von der konkreten Anwendung – entweder hingenommen werden oder das Ergebnis wird schlicht und einfach verworfen.

C.5 Gridcomputing

Die bisher besprochenen Betriebsarten konzentrieren sich auf die Unterschiede, wie ein oder mehrere Prozesse über das Betriebssystem gesteuert werden, ohne einen größeren Zusammenhang zu berücksichtigen. Sieht man den Betrieb eines oder mehrerer Systeme jedoch vor einem breiteren Hintergrund, so kommt man auf eine andere Betrachtungsebene.

Vor allem stellt sich die Frage, welche Instanz benötigte Ressourcen letztlich zur Verfügung stellt und wo diese Ressourcen zu finden sind.

Bei vernetzten Computersystemen (siehe auch ‎D.3.3, Verteilte Systeme) können aktive und passive Betriebsmittel als Knoten in einem Computernetzwerk liegen und von dort angefordert werden.

Wir gehen aus vom Begriff Rechnerverbund (Computer Cluster):

Unter einem (Computer-) Cluster versteht man eine Anzahl von Computern, die bzw. deren Ressourcen über ein Computernetzwerk miteinander verbunden sind.

Die Ziele, die mit einem solchen Cluster erreicht werden können, sind vielfältig, die wichtigsten sind:

- Bereitstellen von Betriebsmittel, die lokal nicht verfügbar sind

- Erhöhung der Verfügbarkeit eines Systems für den Fall, dass lokal z.B. Hardwarekomponenten nicht oder temporär nicht verfügbar sind (Failover-Funktion)

- Erhöhung der Computerleistung, indem für spezielle Aufgaben auf einen aktiven Netzknoten (Computer) zugegriffen werden kann, der für die anstehende Aufgabe in seiner Architektur und Betriebsmittelausstattung optimiert ist. Ein Beispiel dazu ist, dass eine CPU-intensive mathematische Berechnung mit Hilfe des Betriebssystems an einen anderen Rechner im Cluster delegiert wird, der für „number crunching“ ausgelegt ist.

- Bessere Lastverteilung (Load Balancing)

- Sind zwei oder mehrere Rechner als Knoten in einem Verbund für mehr oder weniger dieselben Aufgaben konzipiert und mit entsprechender Software ausgestattet, so kann der Fall eintreten, dass während eines Beobachtungsintervalles der eine Rechner überlastet ist, während das andere System unterbeschäftigt bleibt. Hier kann man durch Verschieben der anstehenden Last eine bessere Gesamtperformance erreichen.

Um die genannten Vorteile in einem Cluster ausnutzen zu können, bedarf es einiger Erweiterungen des Betriebssystems durch Einführung einer zusätzlichen Schicht, die Middleware genannt wird (Abb. C1).

Eine spezielle Variante beim Einsatz eines Rechnerverbundes vor allem für rechenintensive Anwendungen ist das sogenannte Grid Computing.

Ein Grid ist ein Netzwerk von zusammengeschalteten einzelnen Rechnern mit dem Ziel, insgesamt einen leistungsfähigen „Supercomputer“ dem Anwender zur Verfügung zu stellen.

Eine Aufteilung einer (rechenintensiven) Aufgabe an durchaus inhomogene Knoten – bei den im Verbund stehenden Rechnern in diesem Netz können im Prinzip unterschiedliche Betriebssysteme aufgesetzt sein – wird insbesondere dann effizient, wenn eine Parallelisierung vorgenommen werden kann. Eine solche Zerlegung ist nicht trivial und von vielen Tücken begleitet: den Themen Parallelität und Synchronisierung ist in diesem Buch eine umfangreiche Darstellung gewidmet (siehe Kapitel G und H).

Hinzuzufügen ist, dass für einen geplanten Einsatz von Software für Gridcomputing bereits in der Entwurfsphase passende Zerlegungen nach Möglichkeit vorzusehen sind.

Diese Bemerkung gilt allgemein für Software-Engineering bei parallelen Systemen.

Besondere Aufgaben des über eine Middleware (siehe Abb. C1) für Gridcomputing erweiterten Betriebssystems sind:

- die Auswahl passender Ressourcen im Grid

- Unterstützung beim späteren Zusammenfügen der im Grid partiell berechneten Ergebnisse

sowie allgemein:

- die Überwachung der Knoten im Grid

- das Bereitstellen von „genormten“ Schnittstellen zwischen den (möglicherweise) inhomogenen Knoten und, je nach Anwendung,

- das Einbinden von standardisierten Programmbibliotheken.

Zur Interpretation der unten stehen Abbildung vergleiche man die Abb. A4 zum Schichtenmodell eines Betriebssystems. Die Middleware ist für alle Betriebssysteme (in ihrer Funktion) gleich und auf die jeweiligen Betriebssysteme der einzelnen Knoten aufgesetzt. Sie ist demnach als spezielle Erweiterung eines Betriebssystems zu verstehen.

Abb. C1  Über Middleware für Gridcomputing erweitertes Betriebssystem

Ein wichtiger Punkt bei Gridcomputing ist auch, dass die Anwender bzw. Betreiber des Grid a priori wissen, welche Ressourcen im Grid bereitstehen und diese entweder gezielt z.B. durch Adressierung (Angabe der Netzwerkpfade) anfordern können oder über die genannte Middleware zugeteilt bekommen.

Allgemein sei festgehalten, dass es bei Gridcomputing darum geht, im Grid verteilte Betriebsmittel gemeinsam nutzen zu können, ohne dass eine zentrale Steuerung über einen besonders ausgezeichneten Knoten gegeben ist.

Stellt man den generellen Zweck eines Betriebssystems (vgl. Abb. A4) in den Mittelpunkt und wendet man den Blick weg von einer zentralisierten Hardware hin in Richtung Rechner-Cluster, so stehen Dienste zur Lösung von Aufgaben im Fokus:

Anwender wollen Dienstleistungen in Anspruch nehmen. Wenn der Preis, die Performanz, Verfügbarkeit, Zuverlässigkeit und Sicherheitsanforderungen für diese Dienste stimmen, so sind technische Details wie der Ort der benötigten Ressourcen, die konkret verwendete Software usw. letztlich von nachrangigem Interesse. Man möchte zum Zeitpunkt einer anstehenden Aufgabe die richtige Hardware und nur diese, die passende Software, und nur diese, usw. anfordern können. Dazu benötigt man einen Dienstanbieter (Service Provider), der diese Ressourcen bereitstellt. Man spricht in diesem Zusammenhang unter Anderem von Software as a Service (SaaS) sowie von Infrastructure as a Service (IaaS) und, wenn es „nur“ um die (temporäre) Bereitstellung einer Plattform geht, von Platform as a Service (PaaS).

Mit diesen Organisationsformen und Dienstangeboten kommt man zum Konzept des Cloud Computing, wo Computerressourcen und damit verbundene Dienste – wie der Name suggeriert – wolkig verteilt sind und Anwender nur das in Anspruch (und bezahlen) müssen, was sie benötigen. Beim Cloud Computing sind die in einem Cluster zusammengehörigen Rechner im allgemeinen Fall über das Internet miteinander verbunden.

Im Vergleich zu Gridcomputing, das seine speziellen Vorzüge in Richtung zusammengeschalteter Hochleistungsrechner ausspielen kann, steht bei Cloud Computing ein Organisationskonzept mit einer zentralen Verwaltungsinstanz über den Serviceprovider im Vordergrund.

D KLASSIFIKATION VON BETRIEBSSYSTEMEN

Eine Unterteilung (Klassifikation) von Betriebssystemen kann man insbesondere nach folgenden Gesichtspunkten vornehmen:

- Betriebsarten

- Strategie der CPU-Steuerung (CPU-Scheduling)

- Technischer Aufbau (Architektur)

- Hauptspeicherverwaltung

- Einbenutzerbetrieb oder Mehrbenutzerbetrieb

- Schnittstelle zum Benutzer

D.1 Betriebsarten

Auf das Thema Betriebsarten sind wir schon eingegangen. Es ist naheliegend, dass die Betriebsarten als Klassifikationsmerkmal herangezogen werden können. Nochmals sei daran erinnert, dass moderne Betriebssysteme mehrere Betriebsarten gleichzeitig betreiben können. Die Betriebsarten wieder hängen eng damit zusammen, wie die technisch-logische Steuerung der CPU vor sich geht. Damit meint man die Prinzipien und Regeln (bzw. Algorithmen), nach denen das Betriebsmittel CPU den abzuarbeitenden Prozessen zugeteilt wird.

Solche Zuteilungsstrategien fasst man unter dem Fachbegriff CPU-Scheduling zusammen und die Methoden des Scheduling sind ein besseres Kriterium bei der Einteilung von Betriebssystemen.

D.2 CPU-Scheduling

Scheduling betrifft ganz allgemein Strategien für die Zuteilung von Betriebsmitteln. Darunter fällt z.B. Disk-Scheduling mit der Aufgabe, das Betriebsmittel „externer Speicher” möglichst effizient jenen Prozessen zuzuteilen, die darauf schreiben oder davon lesen wollen.

Wir konzentrieren uns im Folgenden auf CPU-Scheduling, die Planung, nach welchen Kriterien die CPU den Prozessen zugeteilt wird. Zur Vereinfachung gehen wir, wenn nicht gesondert hingewiesen wird, immer davon aus, dass wir ein System mit nur einer CPU vor uns haben.

Zunächst muss man zwischen einer langfristigen Planung (Long-Term-Scheduling) und einer kurzfristigen Planung (Short-Term-Scheduling) unterscheiden.

Im ersteren Fall geht es um die grundsätzliche Reihung von Aufgaben. In dieser Phase sind sie noch nicht als Prozesse gestartet, sondern sind sozusagen im Abschnitt „Arbeitsvorbereitung“.

So kann man z.B. bei Stapelverarbeitung die Jobs, die in einer Warteschlange hintereinander gereiht sind, umreihen, noch bevor mit ihrer Ausführung durch eine CPU-Zuteilung begonnen worden ist. Man erkennt hier schon ein erstes wichtiges Prinzip bzw. Problem: eine solche Strategie muss fair sein, sodass keine Task Gefahr läuft, in der Warteschlange zu „verhungern” (vgl. später: starvation problem). Das könnte eintreten, wenn ständig andere Aufgaben vorgereiht werden.

Beim Short-Term-Scheduling hat man die Situation, dass Prozesse bereits laufen und überlegt, inwiefern es möglich ist – oder eben nicht – einem laufenden Prozess zu Gunsten eines anderen die CPU derart zu entziehen, dass er trotzdem zu einem späteren Zeitpunkt „an der richtigen Stelle” wieder fortgesetzt werden kann.

Zwischen dem Long-Term-Scheduling und dem Short-Term-Scheduling lässt sich noch eine Zwischenstufe (Mid-Term-Scheduling) einfügen, sodass man zu einem 3-stufigen Konzept kommt, welches als Three-Level-Scheduling bezeichnet wird.

Alle Aufträge (Jobs) werden anfangs in eine dafür vorgesehene Input Queue gestellt. Diese Prozessphase übernimmt nach obiger Sprechweise das Long-Term-Scheduling.

Aus dieser Input Queue werden durch eine als Admission Scheduler bezeichnete Betriebssystem-Komponente nach bestimmten Regeln Jobs in den Hauptspeicher geladen. Demnach erledigt der Admission Scheduler das Mid Term Scheduling. Die Kriterien für die Auswahl sind unterschiedlich: Prioritäten, I/O Intensität, erwartete Laufzeit („Kurzläufer“, „Langläufer“), benötigte Ressourcen wie Speicherplatz usw. Getrachtet wird – global gesehen – nach einem guten Mix, der eine optimale CPU-Auslastung erwarten lässt.

Das Short-Term-Scheduling kümmert sich schlussendlich dann um die Ausführung der im Hauptspeicher vorhandenen Prozesse. Scheduling unterliegt einer Politik (policy), an Hand der die CPU-Zuteilung erfolgt.

Wenn wir von CPU-Scheduling sprechen, meinen wir von nun an Short-Term-Scheduling.

CPU-Scheduling lässt sich grob in 2 Kategorien fassen:

- non preemptive Scheduling

- preemptive Scheduling

„Preempt” heißt „durch Vorkaufsrecht etwas erwerben“, „mit Beschlag belegen“, „verdrängen”. Man meint damit, dass ein Prozess – aus bestimmten durch die Scheduling–Strategie vorgegebenen Gründen – einen anderen Prozess verdrängen kann, also an seiner statt die CPU erhält, ein „Vorkaufsrecht“ auf die CPU hat. Da dieses Verdrängen i.Allg. durch einen speziellen Interrupt erfolgt, spricht man (im Deutschen) häufig auch von unterbrechbarer bzw. nicht unterbrechbarer CPU-Steuerung.

An dieser Stelle können wir auch plausibel erklären, warum das früher genannte Spooling-Konzept nicht unter preemptive Scheduling eingeordnet wurde, obgleich hier auch mit Interrupts der Lauf eines Prozesses unterbrochen wird: Aus der Sicht des laufenden (Haupt/Main)- Prozesses nützt man „nur“ die Zeit, bis sich ein peripheres Gerät (meist: der Drucker oder ein Eingabegerät) wieder für die Übernahme eines neuen Auftrages zurückmeldet.

Tatsächlich ist die Situation jedoch umgekehrt: es läuft ständig ein Ein/Ausgabe (E/A, I/O) Prozess, und nur wenn dieser nichts zu tun hat oder warten muss, wird die CPU dem „eigentlichen“ Hauptprozess zugeteilt. Eine typische Wartesituation ist „print line, carriage return, line feed“, also die Zeit, bis der Schreibkopf des Druckers eine Zeile gedruckt hat und auf den Beginn der nächsten Zeile vorgeschoben worden und für das Drucken einer neuen Zeile wieder bereit ist. Im allgemeinen Fall legt ein laufender Prozess Daten in einen für die E/A vorgesehenen Puffer ab bzw. holt von dort Daten, sobald welche zur Übernahme bereitstehen. Der Spooling-Prozess entnimmt Daten aus diesem Puffer (bzw. stellt sie dort hinein) und betreut Interrupt getrieben das angesprochene E/A Gerät.

Bei non preemptive Scheduling ist ein Prozess, dem die CPU einmal zugeteilt wurde, bis zu seiner Beendigung im Prinzip nicht mehr unterbrechbar (Ausnahme: I/O-Interrupts). Diese Art des Scheduling wird in allen Batch-Systemen verwendet.

Eine Sonderform stellt das so genannte kooperative Multitasking dar. Hier gibt ein Prozess unter bestimmten Voraussetzungen die CPU freiwillig ab. Beispiele dafür sind das alte Windows 3.x (Umschalten zwischen DOS-Box und Windows Programm Pool) oder die frühen Versionen des Apple Macintosh Betriebssystems.

Kooperatives Multiprogramming findet man heute in Betriebssystemen für Handhelds oder Mobile-Phones (Symbian, PalmOS u.a.). Dies gilt jedenfalls im Prinzip, denn bei einem hereinkommenden Anruf muss natürlich in Echtzeit auf diesen reagiert werden.

Preemptive Scheduling ist die Basis aller Multiprogramming- und Timesharing-Betriebssysteme. Dabei kann einem Prozess (nahezu) jederzeit die CPU auf Grund einer höheren Strategie zu Gunsten eines anderen Prozesses entzogen werden. Zuständig dafür ist der Scheduler. Er wählt dann nach einer definierten Strategie einen der wartenden und zur Ausführung bereiten Prozesse aus und sorgt dafür, dass diesem dann die CPU zugeteilt wird.

Wir halten an dieser Stelle fest, dass nahezu alle Arbeitsplatzrechner (Workstations, mit den Betriebssystemen Windows Familie, MacOS X.y, Linux, Unix,....) eine Mischform zwischen Multiprogramming und Timesharing auf der Basis von preemptive Scheduling realisieren.

Folgende Methoden werden aus grundsätzlicher Sicht vorgestellt:

Non preemptive Scheduling:

- First Come First Served (FCFS)

- Shortest Job First (SJF)

- Prioritätssteuerung mit/ohne „Aging“ (altern)

Preemptive Scheduling:

- Prioritätssteuerung

- Zeitscheiben-Verfahren (Round Robin)

- Warteschlangen mit Prioritäten (Multilevel Feedback Queues Scheduling)

D.3 Technischer Aufbau (Architektur)

Wählt man die interne Struktur als Klassifikationsmerkmal von Betriebssystemen aus, kann man wie folgt unterscheiden:

- Monolithische Betriebssysteme

- Geschichtete (Layered) Betriebssysteme

- Client-Server Modelle

D.3.1 Monolithische Betriebssysteme

Diese Art der Implementierung eines Betriebssystems ist historisch gewachsen. Zwar ist eine klare Trennung zwischen Anwendungs- und Systemprogrammen vorhanden, aber die interne Systemarchitektur ist unstrukturiert. Im Extremfall kann man sogar von jeder Systemprozedur aus einen Zugriff auf die Hardware durchführen.

Ein besonderes Problem dieses Ansatzes ist die große Verwundbarkeit des Gesamtsystems bei einer Fehlfunktion einer einzelnen Systemprozedur. Auch werden Fehlerbehebungen (patches) problematisch. Sie können, weil die Struktur undurchschaubar (geworden) sein kann, durchaus unvorhergesehene Nebeneffekte auslösen.

Abb. D1  Monolithisches Betriebssystem

Die nachstehende Abbildung zeigt am Beispiel des PC-Betriebssystems DOS, wie in der Konzeption der Übergang zu Schichten in Betriebssystemen eingeleitet wurde. Eine strenge Einteilung gibt es deswegen noch nicht, weil jederzeit möglich ist, Schichten zu überspringen.

Abb. D2  Schichtenmodell in DOS

D.3.2 Geschichtete Betriebssysteme (Layered Operating Systems)

Diese Architektur ist vom Einfluss der strukturierten Programmierung geprägt. Es erfolgt eine strenge Einteilung in Schichten (layers), wobei jede Schicht nur Zugriff auf die unmittelbar darunter liegende Schicht hat und nur über diese Schnittstelle deren Dienste in Anspruch nehmen kann und darf. Der historische Wegbereiter ist das THE-Betriebssystem (Technische Hogeschool Eindhoven, E. W. Dijkstra (*1930, †2002)).

Das THE-System war ein einfaches Batch-System mit nach heutigen Maßstäben entsprechend bescheidener Hardware (32 K Hauptspeicher, Wortlänge 27 (!!) Bits).

Die Schichten einschließlich der Ebene für Anwender (User) und Bediener (Operator) sind in Abb. D3 dargestellt.

Abb. D3   Schichtenmodell des THE-OS

Das Modell kann aus Architektursicht überzeugen, hat aber in der Realität gewisse Probleme: Einerseits will man z.B. die Gerätetreiber für den Massenspeicher entsprechend der Schichtenphilosophie oberhalb des Speichermanagements legen, anderseits benötigt aber das Auslagern von Speicherseiten (Virtual Memory Management) bereits den Zugriff auf die Harddisk und damit die Treiber.

Diesen Problemen stehen aber wieder entscheidende Vorteile gegenüber:

- Dem Modell entsprechend können neue Schichten einfach hinzugefügt bzw. Schichten aufgeteilt werden ohne das Gesamtsystem massiv verändern zu müssen.

- Da jede Schicht eine klar definierte Schnittstelle zur unmittelbar darunter liegenden Schicht hat, ist es möglich, eine Schicht zu ersetzen, ohne Einfluss auf andere Schichten zu nehmen. Wichtig bei diesem Schritt ist natürlich, dass die Schnittstelle unverändert bleibt.

- Debugging wird ebenfalls vereinfacht. Man beginnt bei der untersten Schicht und arbeitet sich bis zum Fehler nach oben vor.

D.3.3 Client Server Modell

Im Client Server Modell wird ein völlig anderer Ansatz gewählt. Die Aufteilung des Betriebssystems erfolgt nicht a priori in Schichten, sondern in einzelne Prozesse. Auf diese Sichtweise wurde schon im einleitenden Abschnitt über Prozessmanagement hingewiesen.

Jeder Prozess erfüllt für sich ganz bestimmte Aufgaben. Man spricht davon, dass er bestimmte Dienste (Services) anbietet. In diesem Fall wirkt der Prozess als Server, der bestimmten Klienten (Clients) Dienste offeriert. Solche Clients können andere Systemprozesse, aber auch Benutzerprogramme sein. Zu beachten ist, dass die Eigenschaft, Server oder Client zu sein, nicht statisch ist. Es kann durchaus sein, dass ein Prozess als Server für Klienten arbeitet, aber selbst als Client Dienste anderswo anfordert.

Im Client Server Modell fordern Clients einen bestimmten Dienst eines Systemprozesses an, indem sie ihm eine Nachricht schicken. Nun kommt eine zentrale Komponente ins Spiel: der Kernel.

Der Kernel ist das Herzstück jedes Betriebssystems. Er ist gleichsam die zentrale Schaltstelle für jegliche Kommunikation. Eine von einem Client gesendete Nachricht wird nach vorgegebenen

Sicherheitskriterien überprüft (hat er das Recht den Dienst des Servers in Anspruch zu nehmen?) und anschließend an den gewünschten Server weitergeleitet. Der Server führt den Dienst aus und sendet das Resultat an den Kernel zurück. Der Kernel schickt dem Client das Resultat des angeforderten Dienstes bzw. wo die Ergebnisse zu finden sind.

Im Idealfall laufen alle Server (Systemkomponenten) im User-Mode und nur der Kernel im Kernel-Mode (siehe dazu auch D.6.1..).

In einem solchen Fall spricht man von einem Microkernel.

In der Realität laufen aus Performancegründen meist auch andere Prozesse im Kernel-Mode. In Windows sind z.B. die Device Treiber und das GUI auch im Kernel-Mode angesiedelt, die Client Server Architektur wird dort nicht konsequent durchgehalten.

Abb. D4   Architektur Client Server-System

Die Vorteile eines Client Server Systems liegen auf der Hand: Die einzelnen Server sind kleine kompakte Systemprozesse, die in sich funktional abgeschlossen sind. Bei einem Ausfall oder einer Fehlfunktion eines Serverprozesses wird nicht das ganze System in Mitleidenschaft gezogen, sondern nur diese eine Komponente. Einem Neustart des betreffenden Serverprozesses steht auch nichts im Wege.

Generell können bei Bedarf Systemprozesse gestartet oder gestoppt werden und das System ist auf diese Weise dynamisch adaptierbar. Außerdem ist so ein System relativ leicht erweiterbar und jede Komponente kann ersetzt werden.

Ein weiterer interessanter Vorteil ist, dass es keine Rolle spielt, wo die einzelnen Prozesse laufen. Sie können auf unterschiedliche Prozessoren bzw. Computer verteilt werden. Voraussetzung ist, dass die Netzwerkverbindung entsprechend breitbandig ausgelegt sein muss. Damit macht man einen wichtigen Schritt in Richtung verteilter Systeme.

Dies ist in der folgenden Abbildung schematisch dargestellt.

Abb. D5   Client Server-Modell über Netzwerk

Unter einem Verteilten System versteht man allgemein eine Sammlung von unabhängigen Rechnern, die aus der Sicht der Anwender aber wie ein einziges Computersystem arbeiten.

Ein Spezialfall davon ist demnach ein Verteiltes Betriebssystem, bei dem verschiedene seiner Komponenten auf verschiedenen Rechnern liegen und ablaufen, die untereinander durch ein entsprechend leistungsfähiges Netzwerk verbunden sind.

D.4 Speicherverwaltung

Die Verwaltung des Hauptspeichers orientiert sich zunächst an folgenden Fragestellungen und deren Kombination:

Wenn mehrere Prozesse im Hauptspeicher ablaufen, muss man diesen geeignet unterteilen. Die einzelnen Bereiche können zusammenhängend sein (fixe Partitionen) oder können gleichsam wie ein Flickwerk im Speicher verstreut liegen und werden in diesem Fall über ein geeignetes Verzeichnis (Tabelle) „nur“ logisch als zusammenhängend geführt. Ferner kann man vorsehen, dass die einmal eingeführten Grenzen zwischen den Partitionen, also die Größe derselben, durchaus verschiebbar und somit die Aufteilung flexibler werden kann.

Abb. D6   Hauptspeicheraufteilung mit Partitionen fixer Größe

Wenn der Hauptspeicher insgesamt zur Aufnahme eines oder gar mehrerer Prozesse nicht ausreicht, könnte man gleichsam virtuell den Massenspeicher (Harddisk) zu seiner Erweiterung heranziehen. Bei diesem Ansatz gibt es natürlich massive Performance-Probleme. Methoden der so genannten virtuellen Speicherverwaltung haben das Ziel, derartige Nachteile möglichst gering zu halten. Ein besonders wichtiges Verfahren ist das Paging, auf das wir später ausführlich eingehen werden.

Abb. D7   Aufgaben der Speicherverwaltung

D.5 Benutzersicht

Betriebssysteme in der PC-Welt bzw. für leistungsfähige Arbeitsplatzrechner (Workstations) sind so ausgelegt, dass genau ein Benutzer zu einer bestimmten Zeit interaktiv darauf arbeitet. Natürlich können – zu unterschiedlichen Zeiten – auch andere Benutzer (login, logout) das System verwenden. Bei größeren Systemen (Linux, Windows Familie, Macintosh OS X.y, …) ist durchaus auch vorgesehen, dass ein Benutzer während seiner Sitzung (session) mehrere Prozesse zu unterschiedlichen Zwecken gleichzeitig startet. Es handelt sich aber primär immer um Einbenutzersysteme.

Großrechnersysteme als Mehr-Benutzersysteme gehen davon aus, dass sie viele Anwender gleichzeitig bedienen müssen. Dabei bleibt offen, ob diese Anwender mit einem einfachen Terminal an das System gekoppelt sind oder ob sie auf ihrer Workstation einen zusätzlichen Prozess als Terminal-Emulation gestartet haben oder das Terminal dort simuliert wird. Entscheidend ist, dass die Applikation auf dem zentralen System abläuft und das Terminal (mit zugehöriger Peripherie wie z.B. Drucker) nur dem Prozessstart und der E/A von Daten dient.

Ein weiterer Gesichtspunkt ist die Gestaltung der Schnittstelle zum Benutzer. Sie kann befehls¬zeilenorientiert sein oder eine komfortable graphische Oberfläche bieten. Letztere ist bei allen Workstations vorgesehen. Darüber wurde in der Einleitung schon gesprochen.

D.6 Virtuelle Maschinen

D.6.1 Grundprinzip

Zunächst klären wir kurz, was man unter einer virtuellen Maschine versteht und erläutern dann, wie unter Anlehnung an das Schichtenmodell eine spezielle Architektur entsteht.

Aus der Sicht eines Anwenders ist eine virtuelle Maschine ein Computer, der (teilweise) durch ein Programm simuliert wird.

Das Konzept kommt aus dem Bereich Softwareengineering und wir skizzieren es kurz an einem Beispiel:

Code D1   Schnittstellen virtuelle Tastatur

Stellt man einer Java-Programmiererin diese Klasse zur Verfügung, so kann sie die Schnittstelle (hier exemplarisch: die Methoden keyPressed() und readKey()) verwenden. Die angesprochene Tastatur ist virtuell, das entstehende Programm wird im Prinzip unabhängig von der Hardware. Bei Bedarf sind bloß die Methoden an eine geänderte Tastatur anzupassen.

Ähnlich kann man auch bei Betriebssystemen vorgehen. Das Betriebssystem verbirgt die darunter liegende Hardware und bietet den Anwenderprogrammen an seiner Schnittstelle zu diesen eine Sammlung von Prozeduraufrufen. Diese heißt API (Application Programming Interface). So betrachtet ist das Betriebssystem eine Virtuelle Maschine, deren „Befehlsvorrat“ durch sein API definiert ist.

Zur Umsetzung dieses Konzeptes bedient man sich insbesondere folgender Möglichkeiten und Kombinationen davon: Systemaufrufe über Unterprogramme und Systemaufrufe über Software- Interrupts.

Aus Sicherheitsgründen wird bei einem Betriebssystem zwischen zwei Betriebsarten unterschieden: User-Mode und Kernel-Mode (auch: System-Mode). Prozesse, die im Kernel-Mode ablaufen, haben höhere Rechte und Privilegien, die eigentlich nur wenigen Betriebssystemfunktionen vorbehalten sein sollten. (Siehe dazu auch die Ausführungen beim Client Server Modell Punkt D.3.3.)

Daher versucht man, selbst bei Systemaufrufen möglichst große Teile des Codes im User-Mode ablaufen zu lassen und in den Kernel-Mode nur dann umzuschalten, wenn dies unbedingt notwendig ist. Gleichzeitig soll dies dem Programmierer verborgen bleiben.

In der Windows Familie setzt man diese Idee dadurch um, dass zwischen API und den „eigentlichen“ Systemaufrufen unterschieden wird, wobei auf Programmiersprachenebene die involvierten Systemaufrufe nicht offen gelegt werden, das API also eine reine Prozedur- bzw. Klassenbibliothek ist.

Durch das standardisierte API Interface können Modifikationen des Codes von Systemaufrufen – man denke an Security Patches – jederzeit durchgeführt werden, ohne dass die im Anwenderprogramm verwendeten API-Calls betroffen werden, solange die Schnittstelle (Parameter usw.) zwischen beiden Ebenen nicht verändert wird.

Die Abbildung zeigt die Situation, dass auf einem Host-Betriebssystem als Basis nicht nur die Anwendungen App01- App0x laufen, sondern speziell zwei weitere Gast-Betriebssysteme (Betriebssystem 1, Betriebssystem 2) aufgesetzt sind. Auf diesen wieder laufen die Anwendungen App1i i=1, 2,…, y und App2j j=1, 2, …, z.

Abb. D8   Virtualisierung für mehrere Betriebssysteme auf einer Hardware

Der erste wirklich kommerzielle Einsatz des Architekturansatzes auf Basis virtueller Maschinen findet sich bei IBM bei dem System VM/370 Mitte der 1970er Jahre. Damit wurde es möglich, auf der damals neuen 370er Architektur nicht nur das weit verbreitete Betriebssystem OS/360 einzusetzen und damit alle bisherigen Anwender sozusagen bei der Stange zu halten, sondern auch ein Timesharing Konzept einzuführen, das als eigenes System parallel zum OS/360 auf dem VM/370 lief.

Die Idee der virtuellen Maschine finden wir heute z.B. unter Windows, wo in der „DOS-Box“ das Betriebssystem DOS der 16-Bit Generation emuliert wird. Man beachte: bei einer softwaremäßigen Emulation eines Systems wird dieses im Detail funktionsgleich abgebildet. Bei einer Simulation wird nur das nach Außen wirksame oder das für den Betrachter relevante Verhalten nachvollzogen, die inneren Abläufe können aber durchaus völlig anders als beim Original umgesetzt sein. Ein Flugsimulator fliegt nicht wirklich, nur die darin sitzende Person hat diesen Eindruck.

Für das Apple-Betriebssystem OS X.y gibt es eine virtuelle Maschine, die dem Windows XP entspricht.

Ein weit verbreitetes Produkt ist VMware, das z.B. auf Windows oder Linux aufgesetzt werden kann und dann seinerseits verschiedene andere Systeme als Gast-Systeme unterstützt. (Ein besonderes Beispiel dazu ist das Betriebssystem MINIX3 von A. S. Tanenbaum, welches auf allen Rechnern installiert werden kann, auf denen VMware läuft.) Eine effiziente Variante von VMware ist VMware ESX, das eine Bare-Metal (Hypervisor) Architektur verwendet. Bei diesem Produkt setzt die Virtualisierungsschicht nicht wie in der Standardversion als Applikation auf einem bereits vorinstallierten Host-Betriebssystem auf, sondern in Form eines eigenen Kernels direkt auf der Gerätehardware. Durch diese schlanke Architektur und der Möglichkeit des direkten Hardware Zugriffes ist VMware ESX derzeit die bevorzugte Virtualisierungsvariante für den Realbetrieb etwa in Rechenzentren oder Serverfarmen.

Microsoft wieder stellt mit VirtualPC eine virtuelle Maschine zur Verfügung, die einen Standard-PC (mit typischer Hardwareausstattung) emuliert, sodass auf diesem z.B. wieder Windows als Gast-Betriebssystem eingesetzt werden kann. Ebenso wie VMware bietet auch Microsoft ein eigenes Virtualisierungsprodukt für den professionellen Bereich namens Hyper-V.

Ergänzende Details werden in den Abschnitten D.6.2. bis D.6.6. besprochen.

D.6.2 Systemaufrufe (system calls) über Unterprogramme (Prozeduren)

Das API als Sammlung von Prozeduren und damit als Bibliothek von Systemaufrufen zu realisieren, folgt einem konventionellen aber durchaus zweckmäßigen Ansatz. Damit braucht ein Programmierer auch bei systemnahen Aufgaben die gewohnte Entwicklungsumgebung nicht verlassen. In einer objektorientierten Umgebung ist das API als spezielle Klassenbibliothek ausgelegt. Das folgende Beispiel liest mit Hilfe der API-Funktion void GetSystemInfo(LPSYSTEM_INFO lpSys-teminfo) von Windows verschiedene hardwarebezogene Systemdaten aus, darunter die Anzahl der vorhandenen Prozessoren, und weist sie einer Variablen mit vordefinierter Datenstruktur SYSTEM_INFO zu, auf die mit der Adresse &siSysInfo verwiesen wird:

Code D2  Auslesen hardwarebezogener Systemdaten in Windows

D.6.3 Systemaufrufe mittels Software-Interrupts (SWI)

Dieses Konzept entspricht einem indirekten Prozeduraufruf, denn anstatt die benötigte Betriebssystemfunktion so wie oben direkt aufzurufen, wird durch SWI(i) die i-te Position in einer zugehörigen Interruptvektor-Tabelle angesteuert, in der die Startadresse des gewünschten Systemaufrufes steht. Dieser Ansatz wird vor allem bei kleineren Betriebssystemen gewählt.

So wird z.B. im früheren PC-Betriebssystem DOS über SWI(33) (d.h.: SWI(21H)) im Wesentlichen die gesamte E/A erledigt, wobei Parameter in Prozessorregistern abzulegen sind. Das DOS selbst wieder bedient sich des darunter liegenden BIOS ebenfalls über Software-Interrupts. Beispielsweise sorgt SWI(5) für einen Ausdruck des Bildschirminhaltes auf dem angeschlossenen Drucker.

Am Rande sei vermerkt, dass ein Software-Interrupt, der zugleich einen Wechsel vom User-Mode zum Kernel-Mode erzeugt, oft auch als TRAP bezeichnet wird.

D.6.4 Hypervisor

D.6.4.1 Aufgabenstellung und Anwendung

Für Anwenderinnen und Anwender ist es sehr zweckmäßig, wenn auf ihrem Arbeitsplatzrechner bei Bedarf neben dem oder innerhalb des aktuell verwendeten Betriebssystems ein weiteres Betriebssystem gestartet werden kann, mit dem man früher schon gearbeitet hat. Damit wird man unter Umständen von lästigen Problemen einer mangelhaften Rückwärtskompatibilität befreit. Durchaus können auch Lizenzierungsfragen eine gewichtige Rolle spielen.

Eines von mehreren möglichen Beispielen für dieses Szenario ist der XP-Mode unter Windows-7, wenn man einerseits die Vorgabe hat, ein neueres Betriebssystem zu installieren, anderseits, z.B. aus einer Flottenpolitik vorgesehen, bei ein und demselben Hersteller verbleiben will oder muss.

Der Hauptgrund für die steigende Verwendung von Virtualisierungskonzepten liegt aber bei Einsparungsmöglichkeiten bei Servern, insbesondere, wenn diese typischerweise im Normalbetrieb wenig ausgelastet sind.

In diesem Zusammenhang, der häufigsten Realität entsprechend, gehen wir von einem Mehrprozessorsystem aus, das über eine symmetrische Multiprozessorarchitektur (SMP) verfügt. Hier sind alle CPUs im Hostsystem (Host-Hardware) ident und vor allem gleichberechtigt bei der Ressourcenzuteilung, insbesondere bei der Zuteilung von Prozessen.

Bei Servern in einer Firma ist das Ziel folgendes: Anstatt mehrere Server für unterschiedlichste Aufgaben zu installieren und zu betreiben, werden diese Aufgaben an virtuelle Maschinen übertragen und diese parallel auf einem (z.B.) SMP Server ausgeführt. Damit kann man die Gesamtauslastung optimieren und eine oder mehrere CPUs bzw.- CPU-Leistung von momentan nicht besonders beschäftigten nunmehr virtuellen Servern an solche Server zuteilen, die temporär einer besonders hohen Auslastung unterliegen.

Studien haben ergeben, dass damit auch Energiekosten, Wartungskosten und vor allem auch die Kosten für den Betrieb einer räumlichen Infrastruktur eingespart werden. Bei Virtualisierung steht damit insbesondere die Servervirtualisierung durch das hohe Einsparungspotential im Vordergrund.

Das in Abb. D8 gezeigte Schema lässt offen, ob das Basisbetriebssystem (Basis-Hostbetriebssystem auch kurz: Host System) ein beliebiges Betriebssystem sein kann und/oder ob spezielle Anforderungen an dieses gestellt werden müssen. Ebenso erhebt sich die Frage, ob die Gastbetriebssysteme mit jenem des Hostsystems mehr oder weniger „verträglich“ sind (z.B. bei ihren API- Schnittstellen) oder völlig andere Betriebssysteme mit durchaus unterschiedlicher Architektur zum Einsatz kommen.

Ebenso kann die Variante überlegt werden, dass das Host-System und die (darüber?) laufenden Gastbetriebssysteme zwar unterschiedlich (konfiguriert) sind, sich aber einen gemeinsamen Kernel teilen.

Der entscheidende Punkt für alle Lösungsansätze ist, dass Gastbetriebssysteme letztlich auf die nur ein Mal verfügbare gemeinsame Hardware (shared hardware) zugreifen müssen.

Ein besonderes Problem dabei ist folgendes: Ein Gastbetriebssystem führt aus seiner Sicht auch Kernel-Mode Prozesse aus, die privilegierte CPU-Instruktionen verwenden, kann also aus Sicht des Basis-Hostbetriebssystems nicht so ohne weiteres (vollständig) im User-Mode ablaufen.

Somit stellt sich die Frage, wie solche Instruktionen eines Gastbetriebssystems ermöglicht werden können. Dazu ist eine zusätzliche Software-, allenfalls Hardwareunterstützung erforderlich, die solche Ressourcen-Zugriffe verwaltet und auch überwacht. Diese Software wird als Virtual Machine Monitor (VMM) oder Hypervisor bezeichnet.

Der entscheidende Unterschied gängiger Implementierungen eines solchen Hypervisors (oder VMM) besteht darin, auf welcher Ebene/Schicht dieser eingebettet wird:

- oberhalb des Host-Betriebssystems

(Hosted Virtualisation, Hypervsior Typ 2)

oder

- direkt auf der gemeinsamen Hardware

(Bare Metal Virtualisation, Hypervsior Typ 1).

Zugleich muss man sich offen lassen, ob ein Gastbetriebssystem letztlich für dieselbe Hardware (insbesondere: CPU) ausgelegt ist und damit ausschließlich Instruktionen ausführt, die auch auf dem Host-System ausgeführt werden können oder, ob das Gastsystem für einen völlig anderen CPU-Typ und damit unterschiedlichen Instruktionsvorrat ausgelegt ist.

Daher sprechen wir im Folgenden auch oft von einer virtuellen Maschine (VM) anstatt – eingeschränkt – von einem Gastbetriebssystem.

Abb. D9   Hypervisor – Allgemeine Architektur

Vergleicht man die Abb. ‎D 9 mit der früheren Darstellung in Abb. ‎D 8, so kann ein Hypervisor auch als ganz spezielles „Betriebssystem“ verstanden werden, dessen Aufgabe es ist, den parallelen Betrieb von virtuellen Maschinen VMi zu steuern und zu überwachen.

D.6.4.2 Hosted Hypervisor („Typ 2“)

Hier wird der Hypervisor (VMM) direkt auf das Host-Betriebssystem (Basis Betriebssystem) aufgesetzt. Dieser Ansatz ist nahe liegend und konform zu Abb. D8 und ist in Abbildung Abb. D10 dargestellt.

Abb. D10  Hosted Hypervisor („Typ 2“)

Vertreter des Konzeptes „Hosted Hypervisor" sind VMware Server, Microsoft Virtual Server und diverse Workstation-Lösungen wie VMware Workstation, Microsoft Virtual PC. Ebenso ist Oracle Virtual Box dieser Klasse zuzurechnen.

Wir halten fest: Bei der Hosted Hypervisor Methode läuft der Hypervisor auf einem vorinstallierten Basisbetriebssystem und damit sind die VMi auf der dritten Ebene aufgesetzt (Abb. D10).

Zum Zugriff auf die Hardware wird das Basisbetriebssystem verwendet. Damit müssen insbesondere Instruktionen einer VMi im Kernel-Mode vom Hypervisor durch einen Trap (siehe D.6.3.) aufgefangen, emuliert und dann an das darunter liegende Basisbetriebssystem zur Bearbeitung weitergereicht werden. Gleiches gilt für I/O-Anforderungen. Periphere Geräte und Anschlüsse, für die über den Hypervisor keine Emulationssoftware verfügbar gemacht wird, können dann nicht gemeinsam angesprochen werden.

Ein besonderer Nachteil der Hosted Methode ist auch, dass sie für Real Time VMi nicht geeignet ist, denn dem Basis Betriebssystem obliegt letztlich das gesamt CPU-Scheduling und ebenso das Scheduling für den allgemeinen Ressourcenzugriff auf der gemeinsamen Hardware: In der Praxis ist das Host-Betriebssystem nämlich nicht für Real Time ausgelegt.

Ein Vorteil besteht in einer einfachen Installation ohne besondere Modifikation des Gastbetriebssystems bzw. der VMi, sofern  sie vom Hypervisor unterstützt werden.

Außerdem kann innerhalb einer Herstellerfamilie der Hypervisor naturgemäß auf das bekannte Gastbetriebssystem maßgeschneidert und optimiert werden.

D.6.4.3 Bare Metal Hypervisor („Typ 1“)

In diesem Fall sitzt der Hypervisor (Virtual Machine Monitor) direkt auf der Hardware und setzt daher kein vorinstalliertes Betriebssystem voraus (Abb. D11).

Abb. D11  Bare Metal Hypervisor („Typ 1“)

Demnach erinnert diese Situation an die eingangs getroffene Bemerkung, den Hypervisor als spezialisiertes Betriebssystem sehen zu können, dessen primäre Aufgabe darin besteht, virtuelle Maschinen VMi, i = 1,2,…,n, auf einer gemeinsamen Hardware auszuführen.

Vertreter dieser Architektur sind z/VM von IBM, VMware ESX-Server, Xen Hypervisor und Microsoft Hyper-V.

Ein besonderer Vorteil betreffend Effizienz besteht bei der Bare Metal Lösung darin, dass hier bei I/O Anforderungen der Hypervisor ohne den Umweg über ein Basis Betriebssystem kommunizieren kann. Allerdings werden dazu zwei besondere Schnittstellen (sie heißen hier auch: Treiber ) benötigt.

- Zunächst ist eine explizite Schnittstelle nach unten zur gegebenen Hardware erforderlich, da dieser Zugriff nicht mehr über das („fehlende“) Basisbetriebssystem bereitgestellt wird.

- Nach oben hin, zu den virtuellen Maschinen VMi ist analog zu Hosted Version ebenso Emulations-Software für die I/O Anforderungen (I/O Requests) erforderlich. Gleiches gilt für privilegierte Instruktionen, wenn eines der VMi ein Gastbetriebssystem ist, da eine Kernel-Mode Instruktion einer Gast VMi aus der Sicht des Host-Systems für dieses einerseits keine privilegierte Instruktion ist, anderseits aber einer speziellen Prüfung und Behandlung bedarf.

Da ein Bare Metal Hypervisor speziell für gegebene Hardware entwickelt oder angepasst ist, kann er bei neueren Prozessoren von Architekturerweiterungen, z.B.: von Intel-VT („Vanderpool Technology“) oder AMD-V („Pacifica“) unmittelbar und ohne Umwege Gebrauch machen. Dies ist ein attraktiver Vorteil des Verfahrens. Daher wird ihm besondere Zukunftsträchtigkeit zugemessen, da man davon ausgehen kann, dass diese Architekturerweiterungen auf allen gängigen Prozessortypen zu finden sind.

Eine andere Möglichkeit für den I/O-Zugriff aus einer VMi besteht darin, dass die angesprochenen Einheiten (Devices) vom Hypervisor aufgeteilt werden. Der Hypervisor weist ein Device einer einzelnen VM exklusiv zu. Dieser VM wird damit ein „direkter“ Zugriff auf das Betriebsmittel gestattet.

Ein weiterer Vorteil der Bare Metal Version liegt auch darin, dass als VM auch ein Real Time Betriebssystem möglich ist, da der Hypervisor auf das dort notwendige spezielle CPU-Scheduling (z.B.) durch Prioritätensetzung eingehen kann.

Die erforderliche Vielfalt an Treibern bei einer Bare Metal Hypervisor Version ist natürlich ein gewisser Nachteil, da der Hypervisor mit jedem Treiber umgehen können muss. Daher unterstützen solche Systeme nur ein klar umgrenztes (zertifiziertes) Set von Hardware.

D.6.5 Paravirtualisierung

Die vorhin angesprochene Virtualisierungsmethoden fallen unter die Rubrik „vollständige Virtualisierung“ . Es gibt noch eine spezielle Variante davon, die als Paravirtualisierung bezeichnet wird.

Die Idee dazu basiert auf Performance-Überlegungen, denn insbesondere privilegierte Instruktionen einer virtuellen Maschine müssen, wie schon erwähnt, vom Hypervisor (besten Falls) mit Hardwareunterstützung abgefangen, emuliert und das Ergebnis an das Host-Betriebssystem weitergereicht werden.

Um diese Umwege zu vermeiden oder zu reduzieren, kann man, sofern der Code des Betriebssystems oder Code einer VMi offen gelegt ist bzw. weil dieser dem jeweiligen Hersteller bekannt ist, das Gastbetriebssystem (die VMi) entsprechend verändern. Dann sind seitens einer derart modifizierten VMi „nur“ verhältnismäßig einfache Aufrufe von API-Funktionen des Hypervisors erforderlich, was zu einem Performance-Gewinn führt.

Aus der Gliederungssicht für Hypervisor-Varianten trifft man bei Paravirtualisierung typischerweise eine Bare Metal („Typ 1“) Lösung an.

Vertreter dieser Variante sind PowerHypervisor, Xen, Microsoft Hyper-V, VMware ESX-Server.

Der evidente Nachteil der Paravirtualisierung ist, dass die Gast-VMi (bzw. die Gastbetriebssysteme) zu modifizieren sind und damit wegen der notwendigen Kenntnis über den Source Code oder durch die Übernahme von vorgefertigten Varianten Herstellerabhängigkeiten in Kauf genommen werden müssen.

D.6.6 Kernelbasierte Virtuelle Maschinen

Am Anfang des Abschnitts D.6.4 ist in einem Nebensatz die Variante angedacht, dass ein Hostsystem und die darüber laufenden Gastbetriebssysteme sich einen gemeinsamen Kernel teilen können. Die von der Linux- Gemeinde entwickelte „Kernel-Based Virtual Maschine“ (KVM) geht in etwa in diese Richtung. Sie setzt dabei eine CPU-Unterstützung durch Intel-VT oder AMD-V voraus.

Die KVM (entwickelt von der Firma Qumranet) wird in den Kernel des Linux-Betriebssystems integriert und wirkt dann zusammen mit diesem Kernel als Hypervisor.

Insofern ist eine Klassifikation nach dem „Typ 2“ (Hosted Hypervisor) gerechtfertigt, da der Linux- Kernel der Host ist und zentrale Aufgaben wie CPU-Steuerung oder Speicherverwaltung übernimmt. Anderseits liegt die KVM als Erweiterung des Linux-Kernel direkt auf der Hardware, sodass auch eine Klassifikation als „Typ 1“ argumentierbar wäre.

Die KVM stellt  hinsichtlich der Virtualisierung für die Gastsysteme selbst keine virtuelle Hardware zur Verfügung, sondern setzt eine Emulationssoftware namens QEMU voraus. Die Bereitstellung von virtualisierten Geräten wird von QEMU für den jeweiligen Gast übernommen. Schematisch erhält man einen Aufbau, der in Abbildung Abb. D12 gezeigt ist.

Abb. D12   Kernelbasierte Virtuelle Maschine (KVM)

E PROZESSE

E.1 Zustände

In der ersten Einleitung haben wir den Unterschied zwischen Prozess und Programm erläutert und im Zusammenhang mit den Betriebsarten grundsätzliche Methoden der CPU-Zuteilung beim Short-Term-Scheduling genannt. Auf eine erweiterte Beschreibung des Prozessbegriffes gehen wir später im Zusammenhang mit der Parallelität von Prozessen ein. Vorerst begnügen wir uns mit der bislang eingeführten Erklärung, dass ein Prozess durch die Ausführung eines Programms entsteht.

Bei einem sequentiellen Prozess P, der in den Zeittakten t_1, t_2, … abläuft, wird zu jedem ti genau eine Instruktion ausgeführt.

Alle von P zum Zeitpunkt ti belegten Betriebsmittel definieren seinen Zustand (state). Um von einem Zustand sprechen zu können, bedarf es einer bestimmten Abstraktionsebene für den Beobachter insofern, dass die für ihn relevanten Betriebsmittel erklärt sind, andere aber der Abstraktion sozusagen zum Opfer fallen.

Für die Besprechung des Zustandsdiagramms eines Prozesses P und die Aufgabe seines Prozessbeschreibungsblockes (Process Control Block PCB) rechnen wir dem Zustand von P folgendes zu:

- die CPU mit ihren Registerinhalten,

- den zugewiesenen Speicher mit seinem Inhalt,

- Zeiger auf assoziierte Warteschlagen und Files,

- besondere Attribute, die das Betriebssystem dem Prozess P zugedacht hat:

- die Prozessnummer (Process-ID),

- die Priorität von P

- andere Scheduling-Information und auch die Einbettung (Position) von P innerhalb anderer im System ablaufender Prozesse.

Beim Umschalten von einem Prozess auf einen anderen spricht man von einem Kontextswitch. Bei einem Einprozessorsystem bedeutet das Umschalten von einem Prozess P_1 auf einen Prozess P_2 nicht nur (trivialerweise) die Zuteilung der CPU an P_2, sondern konsequenterweise auch, dass dem Prozess P_1 die CPU vorher entzogen wird.

Da man entsprechend den Scheduling-Richtlinien i.Allg. später P_1 dort wieder fortsetzen will, wo die Unterbrechung stattgefunden hat, muss man seinen Zustand an dieser Stelle vorab retten, um diesen dann wieder restaurieren zu können.

Auch ein erzeugter Prozess, der in einem Einprozessorsystem die CPU gerade nicht zugeteilt hat, befindet sich in einem Zustand.

Daher unterscheidet man „im Prinzip“ folgende drei Zustände insbesondere:

- laufend (aktiv, rechnend, running, active):

P hat die CPU und läuft, „rechnet“. Die Bezeichnung „rechnend“ leitet sich aus dem nicht treffsicheren aber verwendeten „Rechner“ (Computer, computare (lat.) = rechnen) ab.

- wartend (blockiert, waiting, blocked):

P wurde die CPU entzogen und muss auf das Eintreten eines Ereignisses warten, welches letztlich der Grund für das Anhalten war.

- bereit (ready):

P wäre bereit, bei einer CPU-Zuteilung sofort weiterlaufen zu können. Alle – bis auf die CPU – benötigten Betriebsmittel sind zugewiesen.

Ein Beispiel für den Zustand wartend ist, dass ein Betriebsmittel noch nicht verfügbar oder bereit ist: Ein Lesen von einer Diskette verlangt, dass selbige schon im angesprochenen Laufwerk eingelegt ist, ein Schreiben auf den Drucker kann nur erfolgen, wenn dieser eingeschaltet ist. Ein etwas diffizileres Beispiel ist das durch ein Interrupt-Signal ausgelöstes Warten auf die Rückmeldung eines vorher angesprochenen peripheren Gerätes (I/O complete).

Eine andere sehr häufig auftretende Situation – I/O complete ist genau genommen ein Spezialfall von dieser – ist folgende: Ein Prozess P_1 muss auf einen anderen Prozess P_2 warten, weil er Ergebnisse (Daten) von P_2 benötigt. Das Anhalten eines Prozesses P_1 wegen bzw. zu Gunsten eines P_2 wird später unter dem Thema Synchronisation nochmals im Detail aufgegriffen werden.

Der Zustand bereit (ready) ist deswegen so interessant, weil neben wartenden (blockierten) Prozessen bei einem Multiprogramming oder Timesharing System meist auch mehrere andere Prozesse im ready Zustand vorhanden sind, die jederzeit durch CPU-Zuteilung weiter getrieben werden könnten. Sie „warten“ nur mehr darauf, dass das global arbeitende Scheduling  eine Entscheidung fällt, welcher Prozess aus dieser Menge auszuwählen ist und die CPU-Zuteilung vornimmt.

Man beachte in diesem Zusammenhang den Unterschied in der Bedeutung des Wortes „warten“! Daher verwendet man zur genaueren Unterscheidung auch die zweckmäßigeren Bezeichnungen „blockiert“ versus „bereit“.

Wir unterscheiden „prinzipiell“ zwischen den Zuständen laufend, bereit und wartend (blockiert). In der Realität werden herstellerabhängig (je nach Betriebssystem und auch CPU-Architektur) durchaus noch feinere Unterscheidungen getroffen.

E.2 Prozessbeschreibungsblock

Um einen Prozesszustand aufzeichnen, retten und wiederherstellen (restaurieren) zu können, bedarf es systemintern einer Darstellung desselben. Dies geschieht durch den Prozessbeschreibungsblock (Process Control Block, PCB, Task Control Block, TCB).

Das nachstehende Bild zeigt einen typischen Aufbau eines PCB:

Abb. E1  Prozessbeschreibungsblock

In einem System laufende, wartende oder bereite Prozesse werden in einer Prozessliste (Process Queue) zusammengefasst, die meist als zirkulare lineare Liste realisiert wird.

Ein anderer häufig gewählter Implementierungsansatz dazu ist, dass drei zirkulare Listen aufgebaut werden, je eine für die entsprechende Klasse (waiting, ready, running). Bei einem Einprozessorsystem ist die Liste der laufenden (running, rechnend) Prozesse naturgemäß trivial.

Später werden wir noch zwischen Prozessen einerseits und aus ihnen abgeleiteten leichtgewichtigen Prozessen (Threads) unterscheiden. Für die grundsätzliche Erörterung von Zuständen brauchen wir aber diesen Unterschied vorerst nicht.

Operationen auf Prozesse sind demnach Operationen auf ihren TCBs oder auf der Liste der TCBs, wie z.B.:

- CreateProcess (Prozess erzeugen)

- TerminateProcess (Prozess beenden)

- Fork (in einen parallelen zweiten Prozess aufspalten)

- SetPriority (Priorität zuweisen)

- SetState (in einen der Zustände ready, running, waiting bringen)

- GetState (Zustandsinformation auslesen)

- …

E.3 Zustandsdiagramm

Eine Zuteilung der CPU, ein Blockieren von Prozessen und die Wiederzuteilung der CPU für die Weiterarbeit bedeuten Zustandsüberführung, einen Übergang von einem Zustand zu einem anderen. Welche Zustandsübergänge in einem Betriebssystem möglich sind, wird durch das Prozesszustandsdiagramm (process state diagram) festgelegt. Dieses ist typisch für alle heutigen Betriebssysteme.

Abb. E2   Prozesszustandsdiagramm

Zu beachten ist, dass zwischen bereit (ready) und rechnend (running, aktiv, active) in beide Richtungen hin- und hergeschaltet werden darf, während ein blockierter Prozess  in dieser Darstellung nie (jedenfalls nach diesem Diagramm) direkt in den Zustand rechnend kommt, sondern bloß in den Zustand bereit übergeführt wird. Die Folge ist, dass ein Prozess nach der Aufhebung seiner Blockierung nicht notwendigerweise sofort weiterläuft, also wieder rechnend wird. Die Entscheidung darüber obliegt dem Scheduling Verfahren nach Inspektion der Ready Queue.

Auch hier gilt wieder, dass obiges Diagramm die Situation „nur“ im Prinzip zeigt, aber unterschiedlichste Verfeinerungen bei Systemen verschiedener Hersteller anzutreffen sind. Das hängt auch damit zusammen, dass ein Prozess dort in ein Bündel von feineren Prozessen (leichtgewichtige Prozesse, Threads) zerlegt wird.

Zum Vergleich sei das Zustandsüberführungsdiagramm (auf Thread Ebene) der Windows-Familie gezeigt (Quelle: Microsoft). Man beachte, dass hier für eine spezielle Situation von waiting direkt nach running zurückgekehrt werden kann, was wir bei unseren Darstellungen grundsätzlich nicht zulassen wollen. Auch ist neben ready noch ein Zustand standby vorgesehen. Ferner wird unterschieden, ob ein Prozess (hier: Thread) im User-Mode oder im privilegierten System- bzw. Kernel-Mode abläuft. Man kann aber sehen, dass das obige prinzipielle Diagramm enthalten ist.

Abb. E3    Prozesszustandsdiagramm für MS-Windows (nach Unterlagen der Firma Microsoft)

Wir halten nochmals fest, dass Short-Term-Scheduling aus der Schlange der „ready Prozesse“ einen Prozess für running auswählt.

Bis jetzt wurde noch nicht näher darauf eingegangen, wie eigentlich einem Prozess die CPU entzogen wird, damit auf einen anderen Prozess umgeschaltet werden kann.

Ein Prozess kann die CPU freiwillig (cooperative) abgeben, indem er eine Betriebssystem-Routine aufruft. Dieser Fall wäre gemäß Zustandsdiagramm ein von ihm selbst initiierter Wechsel von rechnend auf wartend mit der Implikation, dass die aufgerufene Betriebssystem-Routine zum laufenden Prozess wird. Als Beispiel kann ein Prozess angeführt werden, der die Betriebssystem-Routine Read-Key aufruft. Ist noch keine Taste gedrückt, so wird das Betriebssystem den Prozess in die Schlange der wartenden Prozesse einreihen und einen anderen bereiten Prozess weiterlaufen lassen. Erst beim Eintreffen eines Tastendrucks kann der wartende Prozess wieder unter bereit eingereiht werden.

Gibt aber ein Prozess die CPU nicht freiwillig ab, so kann das Betriebssystem als Überwachungsinstanz den Prozess zur Abgabe der CPU zwingen (Preemption). Die Abbildung zeigt dies durch den direkten Wechsel vom Zustand rechnend zum Zustand bereit. So wird z.B. in periodischen Zeitabständen – in Analogie mit einem externen „Wecker“ auch als Timer bezeichnet – durch einen Hardware-Interrupt dem laufenden Prozess die CPU entzogen. Die zugehörige Interrupt-Service-Routine (ISR) ist Teil des Betriebssystems.

Die ISR überprüft vorerst, ob der aktuell laufende Prozess weiterlaufen darf. In diesem Fall kann die ISR – wie in Kapitel „Hardware-Mechanismen“ gezeigt – einfach wieder verlassen werden. Stellt das Betriebssystem in der ISR aber fest, dass nun ein anderer Prozess an die Reihe kommen soll, muss es einen Kontextswitch durchführen: Dazu wird der Zustand des unterbrochenen Prozesses gerettet, um ihn später wieder fortsetzen zu können und der Prozess wird unter bereit eingereiht. Darauf kann das Betriebssystem einen anderen bereiten Prozess durch CPU-Zuteilung weiterlaufen lassen.

Sofern diese Methode (Preemption) vorgesehen ist, kann damit verhindert werden, dass ein rechenintensiver oder irrtümlich nicht terminierender Prozess alle anderen in der Ready Queue verweilenden Prozesse benachteiligt, gleichsam dort verhungern (starvation) lässt.

Könnte ein Benutzerprozess alle Interrupts sperren, so wäre ein Umschalten seitens des Betriebssystems unmöglich und dieser Prozess damit in der Lage, den Rechner quasi zu monopolisieren. Befehle wie das Sperren aller Interrupts (f.i:=0; vgl. ‎B.6.1) sind daher besonders sensibel und dürfen von „normalen“ Programmen nicht ausgeführt werden. Sie sollten zu den privilegierten Befehlen gehören, die nur im Kernel-Mode verfügbar sind.

F CPU-STEUERUNG (CPU-SCHEDULING)

F.1 Einleitung und Ausgangssituation

Scheduling bedeutet allgemein eine zeitliche Anordnung von Aufgaben oder Zuweisungen wie bei einem Fahrplan. Scheduling von Ressourcen befasst sich mit deren Zuteilung an sie anfordernde Prozesse im Zeitablauf.

In diesem Abschnitt beschäftigen wir uns mit CPU-Scheduling, mit Zielen und Methoden, um die wohl wichtigste Ressource eines Computer-Systems den Prozessen zuzuteilen.

Für CPU-Scheduling sind eine oder mehrere Betriebssystem-Komponenten zuständig: der Long-Term-Scheduler (langfristige Steuerung) und der Short-Term-Scheduler (kurzfristige Steuerung).

In beiden Fällen wird eine Warteschlange für die ankommenden Tasks (bzw. anstehenden Prozesse) angelegt und die konkrete Aufgabe besteht insbesondere darin, eine Task (bzw. Prozess, repräsentiert durch seinen PCB) aus der Schlange auszuwählen und weiterzureichen.

Für die Übernahme der Aufgaben (Jobs) von außen ist meist ein Interrupt-getriebener Mechanismus vorgesehen, der auf dem Spooling-Konzept beruht. Bei Arbeitsplatzrechnern erfolgt die Übernahme von Jobs durch direkte Aktionen (Tastatur, Maus) seitens des Benutzers.

Die verwendeten Scheduling Methoden werden mit Hilfe der Warteschlangentheorie modelliert und haben sich aus daraus abgeleiteten Aussagen entwickelt.

F.2 Long-Term-Scheduling

Die Aufgabe des Long-Term-Scheduler ist, die von Außen an das (Betriebs-) System herangetragenen Benutzeraufträge (Tasks) in die dafür vorgesehene(n) Warteschlange(n) zu übernehmen, diese geeignet zu organisieren (z.B. nach FIFO oder nach Prioritäten zu reihen) und dem Short-Term-Scheduler eine oder mehrere Tasks zuzuführen. Dies geschieht durch Einreihen in die dem Short-Term-Scheduler zugeordnete Warteschlange, der Ready Queue. Es ist eine Frage des Gesichtspunktes oder der Implementierung, ob der Long-Term-Scheduler die angekommenen Aufträge in die Ready Queue einreiht oder umgekehrt der Short-Term-Scheduler bei Bedarf diese aus den Warteschlangen des Long-Term-Scheduler auffüllt. Bei Single-User Systemen wie Arbeitsplatzrechner ist es letztlich der Anwender selbst, der die Aufträge erteilt und daher in gewissem Sinne die Funktion des Long-Term-Scheduling übernimmt.

Abb. F1   CPU-Scheduling – Warteschlangen

F.3 Short-Term-Scheduling

Die Aufgabe des Short-Term-Scheduler ist zunächst nicht unähnlich der Arbeitsweise des Long-Term-Scheduling: Befüllen von Warteschlangen und Entnehmen daraus. Es gibt aber folgende wesentliche Unterschiede, die sich aus dem schon besprochenen Prozess-Zustandsdiagramm ableiten, das in Abb. F1 enthalten ist:

- Aus der zugehörigen Ready Queue wird (bei einem Einprozessorsystem) genau ein Prozess herausgenommen und ihm die CPU zugeteilt.

- Es kann aber vorkommen, dass – abhängig von der (Short-Term)-Scheduling Strategie – die CPU-Zuteilung widerrufen wird, der Prozess in die Ready Queue zurückgelegt und daraus ein anderer Prozess ausgewählt wird.

- Zusätzlich gibt es eine weitere Warteschlange, die Waiting Queue mit folgendem Zweck: Es kann sein, dass ein laufender Prozess anhält (blockiert wird), etwa weil er eine bestimmte Ressource benötigt, diese aber momentan nicht verfügbar ist. Bei der Besprechung von Parallelen Prozessen finden wir einen anderen wichtigen Grund als Beispiel: Ein Prozess muss warten, weil er eine Kritische Region (CR) vorläufig nicht betreten darf.

- Fällt schlussendlich die Ursache für dieses Warten (die Blockierung) weg, wird der Prozess (sein PCB) in die Ready Queue umgehängt. Wir nehmen an, dass auch dafür die Kompetenz beim Short-Term-Scheduler liegt, obgleich dies nicht so sein muss und eine gesonderte Komponente des Betriebssystems dafür verantwortlich sein kann.

Der wichtigste Unterschied zwischen Long-Term-Scheduler und Short-Term-Scheduler besteht vor allem darin:

- Der Long-Term-Scheduler organisiert die Warteschlange(n) für die ankommenden Tasks und legt fest, welche davon anschließend in die Ready Queue gestellt werden können.

- Der Short-Term-Scheduler bedient die Ready Queue und entnimmt aus dieser jenen Task (Prozess), welcher in den Zustand rechnend (active, running) zu überführen ist. Unter bestimmten Voraussetzungen kann er einen laufenden Prozess zu Gunsten eines anderen Prozesses in die Ready Queue zurückstellen.

Wir besprechen nachfolgend ausschließlich Methoden der kurzfristigen Steuerung, des Short-Term-Scheduling.

Damit stellen sich für eine Klassifikation von Scheduling-Strategien folgende Fragen:

- Darf einem laufenden Prozess überhaupt die CPU entzogen werden, sofern er sie nicht von sich aus abgibt oder wegen des Bedarfs eines nicht verfügbaren Betriebsmittels ohnedies in die Waiting-Queue wandern muss?

- Wenn ja, nach welchen Regeln hat das zu geschehen und nach welcher Strategie ist anschließend ein Prozess für die CPU-Zuteilung auszuwählen?

F.3.1 Klassifikation von Short-Term-Scheduling

Die beiden gestellten Fragen führen im Short-Term-Scheduling zu folgender Einteilung:

- nicht unterbrechendes Scheduling: nonpreemptive scheduling

- unterbrechendes Scheduling: preemptive scheduling

In der deutschen Übersetzung findet man bisweilen für preemptive das Wort präemptiv. Eigentlich bedeutet „preempt“ verdrängen. Da ein präemptives Scheduling durch Interrupts umgesetzt wird, ist in der Wortwahl die Präferenz für unterbrechend.

Bevor wir einige Methoden exemplarisch besprechen, erläutern wir die Ziele, die beim Entwurf und der Implementierung von solchen Methoden vorgegeben werden.

F.3.2 Ziele von Short-Term-Verfahren

Die verschiedenen Zielvorgaben für Scheduling Algorithmen sollen bestmöglich erfüllt werden. Dabei fällt auf, dass einige Anforderungen einander widersprechen.

Man berücksichtigt folgende Kriterien:

- CPU-Auslastung (CPU utilisation)

- Durchsatz (throughput)

- Verweilzeit (turnaround time)

- Antwortzeit (response time)

- Wartezeit (waiting time)

Maximiert werden sollen: die CPU-Auslastung und der Durchsatz.

Minimiert werden sollen: die Verweilzeit, die Antwortzeit sowie die Wartezeit.

Zusätzlich ist auch Fairness gegenüber den einzelnen Prozessen als Ziel eines jeden Scheduling Algorithmus zu nennen.

Unter Fairness versteht man beim Scheduling allgemein die Zusatzbedingung, dass eine Betriebsmittelanforderung nach endlich langer Zeit gewährt wird.

„Fair“ heißt in diesem Zusammenhang aber nicht, dass alle Anforderungen gleich behandelt werden!

Unfair wäre bei der CPU-Zuteilung eine Strategie, in der möglicherweise ein in einer Warteschlange weilender Prozess nie die CPU erhält und dort verhungert. Das Fairness-Prinzip verlangt nur, dass nach endlicher Verweilzeit in einer Warteschlange – ohne Angabe der Dauer – der Prozess die CPU zur Weiterarbeit zugeteilt bekommen muss.

Umgekehrt wird von einem Prozess verlangt, dass er nach endlicher Zeit ein belegtes Betriebsmittel wieder frei gibt.

F.3.2.1 CPU-Auslastung

Es liegt nahe, dass die CPU-Auslastung maximiert werden soll. Dies wäre leicht zu erreichen, indem rechenintensive Prozesse, auch als „Langläufer bezeichnet“, generell bevorzugt würden.

Damit würde man sich aber eine Tendenz zur Unfairness einhandeln, weil implizit Kurzläufer benachteiligt würden und ein Verhungern (Starvation) für sie drohte.

F.3.2.2 Durchsatz

Der Durchsatz gibt die Anzahl der fertig gerechneten Benutzeraufträge in einem (nicht näher definierten) Beobachtungszeitraum an. Dabei spielt natürlich der durchschnittliche Rechenbedarf („Kurzläufer“, „Langläufer“) und damit der so genannte Jobmix der anstehenden Prozesse eine große Rolle.

Das Kriterium „Durchsatz“ ist für von Dienstleistern betriebene Großrechner und bei allen Servern (Fileserver, Mailserver,…) ein wichtiger Gesichtspunkt.

F.3.2.3 Verweilzeit

Die Verweilzeit beschreibt die Zeitspanne von der Übergabe eines Jobs an das System bis zu dessen Fertigstellung. Dabei spielt auch die Leistungsfähigkeit von Ausgabegeräten eine wesentliche Rolle, da die dafür benötigte Zeit ebenfalls enthalten ist.

Abb. F2   Definition der Verweilzeit

F.3.2.4 Antwortzeit

Die Antwortzeit beschreibt die Zeitspanne vom Ende einer Benutzereingabe an einen Prozess bis zum Beginn der nachfolgenden Reaktion des Systems. Kurze Antwortzeiten sind bei Timesharingsystemen mit angeschlossenen Terminals erforderlich.

Daher ist bei interaktiven Systemen die Antwortzeit ein besseres Kriterium als die Verweilzeit.

Bei Real-Time-Systemen ist die Antwortzeit ein besonderes Kriterium.

F.3.2.5 Wartezeit

Die Wartezeit beschreibt die Summe aller Zeitspannen, die ein Prozess in der Ready Queue (!) und damit ohne CPU-Zuteilung verbringt, also die Gesamtzeit, in der er sich im Zustand ready befindet und auf eine Zuteilung der CPU warten muss. Angesprochen ist das durch den Scheduler erzwungene Warten in der Ready Queue und dieses muss minimiert werden.

Man beachte den Unterschied zwischen der Wartezeit nach dieser Definition und der „Wartezeit“, die ein Prozess im Zustand „waiting“ verbringt. Diese beiden Zeiten haben also nichts gemeinsam.

Abb. F3   Definition der Wartezeit (aus Sicht des CPU-Schedulings)

F.3.3 Nonpreemptive Algorithmen

Bei dieser Klasse von Scheduling Algorithmen bleibt die CPU einem Prozess zugeteilt, bis dieser sie „freiwillig“ zurückgibt (z.B. wegen einer I/O-Anforderung). Daher wird diese Form auch als run to completion bezeichnet. Die wichtigsten Vertreter sind:

- First Come First Served (FCFS),

- Shortest Job First (SJF),

- Priority-Scheduling sowie

- Priority-Scheduling mit Aging.

F.3.3.1 First Come First Served (FCFS)

Ein Prozess, der die CPU anfordert, reiht sich an letzter Stelle in die Warteschlange ein. Mit anderen Worten in Anlehnung an ein Sprichwort: „Wer zuerst kommt, mahlt zuerst“.

Diese Methode ist nicht besonders effizient, da keine Eigenschaften der Benutzeraufträge (wie z.B.: erwartete Laufzeit, benötigte Betriebsmittel oder Priorität) berücksichtigt werden. Insbesondere die Eigenschaft, dass lange laufende Prozesse die CPU zu Lasten anderer wartender aber zur Ausführung bereiter Prozesse blockieren können, wirkt sich negativ auf das Verhalten bei globaler Sicht aus. Die Abbildung zeigt, dass mit P0 begonnen, mit P1 fortgefahren wird usw., entsprechend der Reihenfolge beim Eintreffen der Jobs.

Abb. F4   Beispiel für First Come First Served (FCFS)

F.3.3.2 Shortest Job First (SJF)

Bei dieser Methode wird mit jedem Prozess die (geschätzte) Dauer der Rechenzeit assoziiert. Ist die CPU verfügbar, so wird sie dem Prozess mit der kürzesten Rechenzeit zugeordnet. Die Abbildung zeigt, dass mit P0 begonnen wird und weil die Strategie nonpreemptiv ist, dieser Prozess auch fertig gerechnet wird. Dann aber wird aus den zwischenzeitlich angekommenen Prozessen P2 und P3 letzterer vorgereiht, P1 kommt erst zum Schluss an die Reihe.

Abb. F5   Beispiel für Shortest Job First (SJF)

Shortest Job First ist hinsichtlich der Gesamtwartezeit einer anstehenden Menge von Jobs optimal.

Man überlege sich das Argument anstatt eines formalen Beweises: Wenn in der Reihenfolge ein lange dauernder Auftrag mit einem kurzen vertauscht und zwar diesem nachgereiht wird, dann wird die Wartezeit des kurzen Auftrages um die Länge des langen verkürzt. Der lange Job hingegen muss nur um die Rechenzeit des vorgereihten kurzen Jobs länger warten. Damit wird die gesamte Wartezeit aller Jobs verkleinert.

Die tatsächliche Dauer der Rechenzeit ist allerdings nicht bekannt und kann daher nur geschätzt werden. Bei derartigen Schätzungen kann es durchaus zu absichtlichen Fehleinschätzungen seitens der Benutzer kommen, um eine höhere Priorität durch Vorreihung zu erhalten. Eine solche Vorgehensweise ist natürlich vom System-Management zu unterbinden. Nicht immer ist es aber sinnvoll, Prozesse bei Zeitüberschreitung abzubrechen, oder „nur“ die Gebühren für CPU-Zeitüberschreitun¬gen zu erhöhen.

In größeren IT-Servicezentren werden bestimmte Jobs periodisch ausgeführt, sodass ihre Laufzeiten mitprotokolliert werden können. In diesem Fall ist das Management nicht mehr nur auf die Benutzerangaben angewiesen, sondern kann eine Schätzung der zu erwartenden Prozessorzeit mittels in der Vergangenheit gemessener Werte Tiund einem vom Benutzer vorgegebenen Startwert S_1 machen:

S_1 = geschätzter oder bekannt gegebener Anfangswert

Sn = berechneter Schätzwert für den n-ten Ablauf des Jobs, n = 2,3,…

Tn = gemessene CPU-Zeit bei der n-ten Job Ausführung

a = Gewichtungsfaktor, Systemparameter mit 0 < a < 1 und daher auch 0 < (1-a) < 1

Dann setzt man rekursiv an

Sn+1 = a Tn + (1-a) Sn

Die Wirkung dieser Formel erkennt man, wenn man sie für einige Schritte entwickelt. Man beachte auch, dass zur Berechnung von Sn+1 jeweils nur die zuletzt gemessene Größe Tn und der zuletzt berechnete Schätzwert Sn benötigt werden. Wählt man a=1/2, so ist die Formel besonders einfach und effizient zu implementieren, da die Division durch 2 mit einem shift right Befehl erledigt werden kann.

S_2 = a T_1 + (1-a) S_1

S_3 = a T_2 + (1-a) S_2 = a T_2 + (1-a) (a T_1 + (1-a) S_1) = a T_2 + (1-a) a T_1 + (1-a)2 S_1

……………

Sn+1 = a Tn + (1-a) a Tn-1 + …+ (1-a)i a Tn-i ...+ (1-a)n-1 a T_1+ (1-a)nS_1

Am stärksten geht für Sn+1 die zuletzt gemessene Zeit Tn ein, gewichtet mit a. Je weiter Ti zurückliegt, desto weniger trägt Ti wegen (1-a) < 1 und a < 1 und daher (1-a)i a << 1 zur Schätzung von Sn+1 bei.

F.3.3.3 Priority Scheduling (HPF)

Bei dieser Methode wird jedem Prozess eine Priorität zugeordnet. Der Prozess mit der jeweils höchsten Priorität erhält die CPU zugeteilt. Daher bezeichnet man diese Strategie auch als Highest Priority First (HPF).

Bei Prozessen mit gleicher Priorität wird wieder gemäß FCFS entschieden.

Die Vergabe der Prioritäten wird von Kenngrößen der Ressourcen-Nutzung wie Zeitlimits, Speicherbedarf oder Anzahl der offenen Dateien bestimmt. Oft sind aber auch Faktoren, die von außerhalb kommen (wie z.B. Fristen, Deadlines), ausschlaggebend.

Shortest Job First ist als Spezialfall von Priority Scheduling anzusehen. Die Priorität wird dabei in Abhängigkeit der geschätzten Rechenzeit vergeben.

Abb. F6   Priority Scheduling (Highest Priority First = HPF) im Vergleich zu Shortest Job First (SJF)

F.3.3.4 Priority Scheduling mit Aging

Ein Problem bei Priority Scheduling liegt darin, dass manche Prozesse möglicherweise ewig (oder ungebührlich lange) auf die CPU warten, weil laufend Prozesse mit höherer Priorität ankommen und vorgereiht werden. Diese Situation wird als Starvation Problem oder Indefinite Blocking Problem bezeichnet.

Der Fachbegriff Starvation wird der deutschen Variante „Verhungern“ üblicherweise vorgezogen.

Priority Scheduling mit Aging ist nicht nur eine Sonderform von Priority Scheduling, sondern Priority Scheduling wird wegen des Starvation Problems auch nur zusammen mit Aging (Altern) eingesetzt. Dabei werden die Prioritäten entsprechend der bisherigen Wartezeit eines Prozesses angepasst, sodass ewiges Warten vermieden wird: die Priorität hängt nicht nur von der Eingangspriorität ab, sondern auch davon, wie lange der Prozess schon auf CPU-Zuteilung gewartet hat. Das Betriebssystem prüft periodisch, welche Prozesse warten, und erhöht deren Priorität.

Mit anderen Worten: Prioritäten werden von einem Anfangswert ausgehend mit zunehmender Wartezeit erhöht.

Abb. F7   Beispiel für Starvation bei Shortest Job First

Mit Pij ist der Prozess i mit der Laufzeit j bezeichnet. Unter der Annahme, dass ständig Jobs mit Laufzeit < 12 hereinkommen, wird P1_12 verhungern, wenn nicht mit Aging dagegen gesteuert wird.

F.3.4 Preemptive Algorithmen

Bei unterbrechender Ablaufsteuerung können laufende Prozesse vom Scheduler in den Status ready versetzt werden. Dieser Ansatz ist sehr effektiv und wird deshalb bei modernen Betriebssystemen verwendet.

Wir besprechen folgende Verfahren:

- Preemptive Priority Scheduling,

mit Spezialfall Preemptive Shortest Job First

- Round Robin und

- Multilevel Feedback Queue Scheduling.

F.3.4.1 Preemptive Priority Scheduling

Die preemptiven Varianten von Priority Scheduling und Preemptive Shortest Job First funktionieren dem Prinzip nach wie ihre nicht-unterbrechenden Versionen. Steht ein Prozess mit höherer Priorität als der gerade laufende an, so wird dem laufenden Prozess die CPU entzogen.

In der präemptiven SJF Version entsteht eine Priorität dadurch, dass man den geschätzten Wert der verbleibenden CPU-Zeit bis zur Fertigstellung als Kennzahl nimmt. Die Methode heißt dann auch preemptive Shortest Remaining Time (SRT). So gesehen ist SRT ein spezielles Verfahren für dynamische Prioritäten. Die Schätzung kann wie bei nonpreemptiv SJF aus Messungen des CPU-Verbrauches von früher protokollierten Prozessabläufen stammen.

Eine andere Überlegung verwendet das beobachtete Prozessverhalten als Schätzgröße für den CPU-Bedarf: Prozesse wechseln bei ihrer Ausführung zwischen heftigen CPU-Inanspruchnahmen (CPU-burst) und I/O-Phasen (z.B.: Datentransfer zum/vom Speicher, Verweilen in der Waiting Queue) ab.

Die Methoden sind nachstehend graphisch gegenübergestellt. P0 mit Priorität 1 wird unterbrochen, sobald der Prozess P1 mit der höheren Priorität 7 ankommt. Auch dieser wird wegen der hohen Priorität von P2 unterbrochen, aber dann fortgesetzt. Zum Schluss wird P0 fertig gerechnet.

Abb. F8  Beispiel für den Vergleich Non-Preemptive versus Preemptive Priority Scheduling

Das Entziehen der CPU wirft einige Probleme auf:

Ein Wechsel des PCB ist erforderlich. Es muss also der Prozesszustand gerettet werden. Darüber hinaus hat ein Prozesswechsel auch Einfluss auf die Speicherzuteilung bzw. Datenstrukturen, die zur Speicherverwaltung dienen. Beispielsweise ist dies in seitenverwalteten Systemen (Hauptspeicherverwaltung mittels Paging) der Fall, wo auch die Seitentabelle-Einträge (page table entries) zu retten und dann neu zu setzen sind.

Es stellt sich die Frage, ob zu beliebigen Zeitpunkten umgeschaltet werden kann bzw. soll.

Häufiges Umschalten zwischen Prozessen (Kontextswitch) verursacht Performanceprobleme, insbesondere bei Seitenverwalteten Systemen (werden später besprochen). Diese Zeitprobleme sind ein Grund – wir gehen an anderer Stelle nochmals darauf ein –, dass man zwischen (schwergewichtigen) Prozessen und leichten Prozessen (Threads) unterscheidet und preemptive Scheduling auf Thread-Ebene und nicht auf Prozessebene erfolgt.

Beim Wechsel zwischen verschiedenen Prozessen ist eine Komponente des Betriebssystems beteiligt, der Dispatcher. Dieser ist für den Kontext-Wechsel, den Wechsel von System-Mode zu User-Mode und für das Starten des jeweiligen Prozesses verantwortlich.

F.3.4.2 Round Robin (RR)

Bei diesem Verfahren wird jedem Prozess die CPU für eine vorgegebene Zeitspanne zugeteilt. Nach dieser Zeitspanne kommt ein anderer Prozess an die Reihe. So wechseln die Prozesse der Reihe nach zyklisch ab. Ist die Einteilung der Zeitspannen klein genug, so entsteht der Eindruck des gleichzeitigen Abarbeitens der Prozesse. Round Robin wurde ursprünglich für Timesharing-Systeme erfunden. Jeder über ein Terminal mit dem Timesharing-System verbundene Benutzer hat subjektiv den Eindruck, für sich alleine das gesamte System zu haben, jedenfalls solange die Anzahl der Mitbenutzer keine Systemüberlastung erzeugt.

Naturgemäß kann Round Robin nur zum Scheduling von Betriebsmitteln eingesetzt werden, bei denen Preemption möglich bzw. sinnvoll ist. (Beispiel: Eine Druckerzuteilung an Prozesse mittels RR ist wohl sinnlos.)

Eine Zeitspanne wird auch Zeitquantum (Zeitscheibe, Time Quantum) genannt. Sie bewegt sich meist in einem Bereich von 10 bis 100 ms. Die Wahl der Länge kann dabei die Effizienz des Algorithmus stark beeinflussen. Ist sie zu kurz, so wird zu viel Zeit für die Prozess-Wechsel (Kontextswitch) verschwendet. Ist sie zu lange, so wird auch die Zeit des Wartens für einen einzelnen Prozess zwischen zwei seiner Zeitscheiben oft zu lange.

Abb. F9   Round-Robin bei Timesharing

Eine Möglichkeit zur Variation besteht darin, die Dauer der Zeitscheiben kleiner zu halten und Prozessen mehr als eine Zeitscheibe zuzuteilen.

Heute setzen alle modernen Betriebssystemen RR ein. Meist wird eine Kombination von Priority Scheduling mit Round Robin verwendet.

Dabei wird Round Robin innerhalb einer Klasse von Prozessen (Threads, siehe später!) gleicher Priorität eingesetzt. Möglich sind auch variabel lange Zeitscheiben.

Je länger die RR-Queue ist, desto weniger oft pro Zeitintervall bekommt ein Prozess die CPU zugewiesen. Daher kann man auch die RR Queue begrenzen: Man denke an das Zusammenspiel von Long-Term und Short-Term-Scheduling. Eine andere Variante ist, zwischen neu ankommenden Prozessen und bereits servisierten zu unterscheiden und in der RR Warteschlange eine (variable) Grenze zu ziehen. Bereits bediente Prozesse werden dann nicht ganz an das Ende sondern nur an diese Grenze zurückgereiht. Damit wird implizit eine Priorität eingeführt, was zu einer weiteren Variante von RR führt. In Anlehnung an SJF (Shortest Job First) kann man bei initialer Schätzung der zu erwartenden Gesamtlaufzeit die Restlaufzeit berechnen und diese als Kriterium zum Einreihen in die RR Queue verwenden.

Ein allgemeiner Ansatz besteht im Anlegen von mehreren Queues, die untereinander nach Prioritäten bedient werden.

F.3.4.3 Mehrere Warteschlangen mit Prioritäten

Dieses Verfahren heißt Multilevel Feedback Queue Scheduling.

Die verwendete Basisstrategie ist präemptiv mit Zeitquanten Ti und prioritätsorientiert betreffend der Warteschlange Qi, in die ein Prozess eingereiht ist: Dazu sind Warteschlangen Q_1, Q_2, …, Qn vorgesehen, wobei Q_1 die höchste und Qn die niederste Priorität haben.

Jede Qi wird nach FCFS abgearbeitet, wobei für die in Qi stehenden Prozesse ein Zeitquantum Ti vorgesehen ist.

Abb. F10   Multilevel Feedback Queue Scheduling

Zu Beginn wird ein Prozess P in Q_1 eingereiht (d.h.: seine Prozessnummer bzw. ein Zeiger auf seinem TCB). Läuft das Quantum T_1 für P ab oder kommt er aus anderen Gründen in einen Wait-Zustand, wird P an das Ende von Q_2 verschoben und der nächste in Q_1 stehende Prozess erhält die CPU. Das allgemeine Prinzip ist: Prozesse werden nach Ablauf ihres Zeitquantums unterbrochen und wandern zur weiteren Behandlung um eine Stufe nach unten.

Prozesse in der untersten Ebene, also auf Qn, verbleiben dort und werden „klassisch“ nach RR mit Zeitquantum Tn bedient.

Damit werden „Kurzläufer“ bevorzugt behandelt, da sie in der Hierarchie der Warteschlangen nicht allzu weit nach unten wandern. Außerdem und insbesondere gelte

T_1 < T_2, … < Tn, z.B. Ti+1= 2 Ti.

Dennoch verbleibt die Gefahr, dass „Langläufer“ verhungern (Starvation Problem), wenn ständig neue Prozesse in Q_1 eingereiht werden. Diesem Problem kann man mit dem Aging Konzept begegnen: nach einer Verweilzeit von t (Systemparameter) wird ein Prozess aus Qi(i ≥ 2) wieder nach oben in Qi-1, verschoben.

In welche Queue Qmin ein bestimmter neuer Prozess kommt und in welche Queue Qmax (mit 1 ≤ min ≤ max ≤ n) er prioritätsmäßig maximal „absinken“ kann, ist in manchen Betriebssystemen von einer Job-Basispriorität abhängig. Damit könnte beispielsweise ein wichtiger Realtime-Prozess mit max = min = 1 immer auf der höchsten Priorität verbleiben.

F.3.4.4 Fair Share Scheduling

Wir haben schon einmal kurz erwähnt, dass ein Prozess in ein Bündel von „leichtgewichtigen“ Prozessen, so genannten Threads, zerfällt und CPU-Scheduling letztlich auf der Ebene dieser Threads durchgeführt wird. Die hier kurz anzusprechende Problemstellung lässt sich aber auf Prozessebene ebenso erklären:

Wenn ein Benutzer A in geschickter Weise eine Aufgabe (Job) so organisiert, dass daraus ein Bündel von Prozessen entsteht, so wird A nach den bislang besprochenen Verfahren gegenüber einem Benutzer B bevorzugt, der nur einen einzigen Prozess gestartet hat. Das ist nicht gerade fair.

Die Philosophie von Fair Share Scheduling ist es, Gruppen von zusammengehörigen Prozessen zu identifizieren und die Zeitquanten nicht einzelnen Prozessen, sondern diesen Prozessgruppen zuzuteilen.

Damit wird eine globale Sicht der CPU-Zuteilung beim Scheduling eingeführt: ein preemptive Scheduling mit Zeitquanten Tibezieht sich nicht auf Prozesse (bzw. Threads), sondern auf Scheduling-Klassen. Prioritätenvergaben erreicht man z.B. dadurch, dass Klassen unterschiedliche Zeitquanten Ti zugeteilt werden.

Diesem Konzept folgt der CFS Scheduler (Completely Fair Scheduler) ab Linux-Kernel 2.6.23.

G PARALLELITÄT

G.1 Prozesse allgemein

Prozesse stehen im Mittelpunkt, wenn Betriebssysteme betrachtet werden. Dabei ist Prozess aber gerade hier ein eher ungenau verwendeter Begriff.

Im ersten Kapitel haben wir einfach davon gesprochen, dass ein Prozess durch die Ausführung eines Programms entsteht. Im Zusammenhang mit Betriebssystemen werden aber alle Betriebsmittel wichtig, die benötigt werden, um einen Prozess tatsächlich ablaufen lassen zu können. Damit wird der gesamte Kontext – die Umgebung eines Prozesses, die zu einem Prozessablauf gehört – von Interesse.

In diesem Sinne wollen wir festhalten:

Ein Prozess wird durch eine Gruppe von Registern bzw. Speicherplätzen definiert, die sich nach gewissen Regeln ändern. Diese Regeln werden üblicherweise durch ein Programm spezifiziert. Das Programm wird durch einen Prozessor ausgeführt.

Typische Komponenten sind demnach: der Code für das Programm, die von ihm benötigten Daten, die CPU mit ihren Registern, der dem Prozess zugeordnete Task Control Block (TCB, PCB), der zugehörige Runtime-Stack beim Ablaufen des Programms.

Der Runtime-Stack eines Prozesses ist ein ihm speziell zugeordneter Arbeitsspeicherbereich, der für das Ablegen von Zwischenergebnissen im Zuge der Prozessausführung dienlich ist.

Im Wesentlichen unterscheiden wir zwischen nichtdeterministischen Prozessen und deterministischen Prozessen.

Dabei ist ein Prozess deterministisch, wenn sein Output durch einen gegebenen Input festgelegt ist. Dies sichert die Reproduzierbarkeit. Daher wird auch von den Methoden zum Datenaustausch zwischen Prozessen Determinismus gefordert. Es liegt nahe, dass nichtdeterministische Prozesse bei Programmausführungen unerwünscht sind.

Auf die spezielle Variante, dass durch überlegtes Zusammenspiel von a priori nicht deterministischen Prozessen ein deterministischer Gesamtprozess entstehen kann, wird hier nicht weiter eingegangen.

Bei einem sequentiellen Prozess wird zu jedem Zeitpunkt genau ein Befehl ausgeführt, seine Befehle werden in einer festen Reihenfolge ausgeführt, einer nach dem anderen. Was man dabei unter „Befehl“ versteht, hängt vom jeweiligen Abstraktionsgrad ab. Daher kann man etwas abstrakter von Ereignissen sprechen und kommt zu folgenden präziseren Definitionen:

- Zwei Ereignisse e1 und e2 eines Prozesses heißen nebenläufig, wenn sie in keiner kausalen Relation zueinander stehen.

Damit können e1 und e2 in irgendeiner Reihenfolge, aber durchaus auch zeitlich parallel stattfinden.

- Ein Prozess P ist sequentiell, wenn er keine nebenläufigen Ereignisse aufweist.

- Zwei Prozesse heißen nebenläufig, wenn sie voneinander vollständig unabhängig ablaufen können, wenn es also egal ist, ob sie hintereinander (in unbekannter Reihenfolge, parallel oder     irgendwie ineinander verzahnt (interleaved) ablaufen.

Nun kann man nebenläufige Prozesse zeitlich koordinieren, sodass ihre Aktivitäten in eine bestimmte Reihenfolge gebracht werden. Diesen Vorgang nennt man Synchronisieren oder Synchronisation von Prozessen.

Sequentialisierung ist nicht immer notwendig. Wenn aber P_1 Daten liefert, die Input von P_2 sind, muss eine Sequentialisierung durch Synchronisation erzwungen werden.

Abb. G1   (partiell) vorgegebene zeitliche Reihenfolge von Operationen

Ein wichtiger Punkt in der Erklärung, was man unter Nebenläufigkeit versteht besteht darin, dass sowohl „echte“ Parallelität von Prozessen darunter fällt als auch eine Verzahnung von Prozessen, wenn z.B. nur ein einziger Prozessor zum Vorantreiben derselbigen vorhanden ist.

Dies nehmen wir als Ausgangspunkt für eine im Betriebssystem-Umfeld zweckmäßige Definition, die unabhängig davon ist, ob wir es mit einem Einprozessorsystem oder einem Mehrprozessorsystem zu tun haben.

- Zwei Prozesse sind parallel, falls der erste Befehl des einen beginnt, bevor der letzte des anderen beendet wurde.

- Zwei Prozesse sind voneinander unabhängig bzw. disjunkt (disjoint), falls jeder von ihnen nur einen eigenen und damit privaten Datenbereich verwendet. Sie heißen interagierend, wenn sie einen gemeinsamen Datenbereich haben.

Die erste Formulierung beschreibt ganz allgemein, was wir in Hinkunft unter Parallelität verstehen wollen. Der zweite Punkt hebt speziell die Situation hervor, wenn sich parallele Prozesse konkurrierend um gemeinsam benötigte Betriebsmittel bewerben. Diese Prozesse werden dann im Englischen auch Concurrent Processes genannt.

Zusammenfassend halten wir fest:

- Intuitiv nimmt man an, dass zwei Prozesse P_1 und P_2 (allgemein: n > 1) zueinander parallel sind, wenn sie nebenläufig sind und damit (teilweise) gleichzeitig ausgeführt werden.

- Wenn aber nur eine CPU vorhanden ist, muss die CPU zum Erreichen einer zeitlichen Überlappung zwischen P_1 und P_2 (in nicht festgelegter Reihenfolge) hin und her geschaltet werden: P1 und P2 sind zeitlich verzahnt.

- Mehrere Prozesse können also parallel ausgeführt werden, und es bleibe offen, ob diese Parallelität durch Verzahnung oder durch die Verfügbarkeit mehrerer Prozessoren verwirklicht wird. Sind mehrere CPUs vorhanden, so könnte man auch von echter Parallelität sprechen. Wir verzichten aber auf diesen weiteren Unterschied in der Formulierung. Dennoch wird der Übergang von Einprozessorsystemen zu Mehrprozessorsystemen einige zusätzliche Probleme aufwerfen.

Abb. G2   Verzahnung bzw. Parallelität bei 1,2 oder 3 Prozessoren (CPUs)

Folgende syntaktische Erweiterung wurde für Programmiersprachen vorgeschlagen, um Parallelität der (in sich jedoch sequentiellen) Anweisungen Pi auszudrücken (E. W. Dijkstra, P. Brinch Hansen):

Code G1  Syntaktische Erweiterung für Programmiersprachen zur Darstellung von Parallelität

P1, …, Pn sind zueinander parallel, die Tücke wird im Detail liegen.

Wichtig ist mit Blick auf die Eigenschaft nebenläufig, dass über den Ablauf der zeitlichen Verzahnung von P1…. Pn in der Notation „cobegin...coend“ keine Aussage getroffen wird.

Abb. G3   Keine Aussagen über die zeitliche Abfolge

Ebenso sind Annahmen über die relative Geschwindigkeit der Prozesse untereinander unzulässig.

Seien A = <a1; a2> und B = <b1; b2> zwei parallele sequentielle Prozesse, dann sind folgende Verzahnungsmuster von A und B möglich:

<a1;a2;b1;b2>   <b1;b2;a1;a2>

<a1;b1;b2;a2>   <b1;a1;a2;b2>

<b1;a1;b2;a2>   <a1;b1;a2;b2>

G.2 Gründe für parallele Prozesse

Im Folgenden werden einige Gründe angeführt, warum Softwaresysteme parallele Prozesse einsetzen.

G.2.1 Information Sharing

Verschiedene Benutzer bzw. Prozesse sind an derselben Information interessiert bzw. machen von ihr gemeinsam Gebrauch. Dies lässt sich über gemeinsamen Speicher (Hauptspeicher, Massenspeicher) oder durch Nachrichtenübermittlung realisieren.

Allgemein wird die Kommunikation zwischen Prozessen zum Austausch von Daten modelliert mit gemeinsamen Variablen und durch Austausch von Signalen, die letztlich wieder Variablen sind.

Eine typische Basisidee ist, dass ein Prozess (Erzeuger, Producer) Daten aufbereitet, ein anderer Prozess (Konsument, Consumer) parallel dazu diese verwendet. Der Datenaustausch erfolgt über einen Puffer und die beiden Prozesse mit durchaus wechselnden Geschwindigkeiten und unbekannter Verzahnung füllen bzw. leeren den Puffer.

Die Verallgemeinerung besteht in e > 1 Erzeugern und c > 1 Konsumenten, die parallel auf einen gemeinsamen Zwischenbestand (Puffer, Lager) zugreifen und sich dabei dort nicht stören sollten, insbesondere, was die Lagerbestandsführung betrifft.

G.2.2 Beschleunigung (Computation Speedup)

Aufgaben werden in Teilaufgaben zerlegt. Diese bearbeitet man dann getrennt parallel und fügt Teilergebnisse später geeignet zusammen. Notwendig dazu sind in diesem Fall mehr als eine CPU, I/O-Kanäle, etc.

Die Abbildung zeigt den arithmetischen Ausdruck (a+b)*(c+d) – (e/f), in welchem die drei Klammerausdrücke unabhängig voneinander berechnet werden können.

Abb. G4   Zeitlicher Ablauf zur Berechnung eines Klammerausdrucks

G.2.3 Modularität

Man denke an Module eines Softwaresystems. Das System (das Betriebssystem) wird in verschiedene unabhängige oder kooperierende parallele Prozesse aufgeteilt. Die Zerlegung in parallele Prozesse ist demnach auch eine Entwurfsentscheidung. Diese betrifft einerseits die Implementierung der Prozesse und anderseits die Synchronisation, das Zusammenspiel dieser Prozesse und den Datenaustausch zwischen ihnen.

G.2.4 Zweckmäßigkeit (Annehmlichkeit, Convenience)

Der einzelne Systemnutzer (User) kann viele Aufgaben haben, die parallel erledigt werden müssen: Editieren, Drucken, Senden bzw. Empfangen von E-Mails, etc.

In einem Textverarbeitungssystem besteht ein Prozess P aus mehreren Teilprozessen, die wir vorerst ohne nähere Angabe von Gründen Threads nennen, wie:

- ein Thread für Texteingabe;

- ein Thread zur Rechtschreibprüfung;

- ein Thread für das Layout;

- ein Thread zum Drucken.

Alle Threads greifen auf dasselbe File zu, das Dokument, das gerade geschrieben wird. Man erkennt an diesem Beispiel auch, dass Synchronisation erforderlich ist: eine Rechtschreibprüfung kann erst vorgenommen werden, sobald Wörter vom für die Texteingabe zuständigen Thread bereitgestellt worden sind.

G.3 Threads und Context Switching

Solange ein Prozess ausgeführt wird, gibt es Ressourcen, die mit ihm verbunden sind. Diese Tatsache haben wir beim eingangs erklärten Prozessbegriff für Betriebssysteme mitberücksichtigt. Bei einem Einprozessorsystem bedeutet das Umschalten von einem Prozess zu einem anderen, dass der gesamte Kontext gerettet und der neue gesetzt werden müssen. Dies ist zeitaufwändig und kann einem Ziel der Beschleunigung erheblich entgegenstehen.

Zum Kontext eines laufenden Prozesses gehören auch z.B. alle Zugriffsrechte und Angaben über den zugewiesenen Speicherbereich usw. Anderseits, wenn wir an das Beispiel Textverarbeitungssystem denken, wechselt dort im Sinne der Parallelität nicht der User und der von ihm gestartete Prozess (Task), sondern es wird nur zwischen den aus seiner Aufgabe entstandenen Teilprozessen hin und her geschaltet. Diese Teilprozesse aber haben zumindest einen Teil des globalen Kontexts gemeinsam. Somit muss beim Umschalten nicht immer der gesamte Kontext gerettet und später restauriert werden. Es genügt, sich auf bestimmte Teile davon zu konzentrieren, was die Sache erheblich einfacher und vor allem schneller machen kann.

Daher führt man in Betriebssystemen so genannte „leichte“ Prozesse ein, für die der Kontextswitch einfacher zu realisieren ist. Man nennt sie Threads.

Ein Prozess besteht demnach aus mindestens einem Thread, i.Allg. aber setzt sich ein Prozess aus einem Bündel von Threads zusammen. Das Thread-Konzept hat unabhängig von dem hier genannten einfachen Beispiel wesentliche Konsequenzen für die Betriebssystemarchitektur. Diese betreffen vor allem die CPU-Scheduling Strategien, das sind die Regeln, nach denen die CPU den Prozessen im Zeitablauf zugewiesen wird.

Threads arbeiten ganz ähnlich wie Prozesse. Sie teilen sich die CPU und nehmen in ihrem Ablauf verschiedene Zustände ein (ready, running, waiting, terminated).

Moderne Betriebssysteme führen CPU-Scheduling auf Thread-Level durch, z.B. Priority Scheduling global zwischen Prozessen und damit auf zu ihnen gehörenden Thread-Bündeln, aber Time Sharing zwischen Peer-Threads untereinander, die aus einem Prozess entstanden sind.

Wir besprechen einige Details in ausgewählten Fallstudien.

Die Ausführungen beim Thema „Paging“ werden uns nochmals bewusst machen, wie komplex und wie häufig das Umschalten zwischen Prozessen sein kann und dass daher ein allgemeiner Kontextswitch zu Performanceproblemen führen würde, hätte man nicht das Konzept der leichtgewichtigen Prozesse (Threads) eingeführt.

Ein wesentlicher Vorteil von Threads aus Sicht des Betriebssystems besteht darin, dass ein Kontextswitch zwischen Threads aus dem gleichen Prozess wesentlich effizienter ist als ein Kontext¬switch zwischen verschiedenen Prozessen, genauer zwischen Threads verschiedener Prozesse. Beim Umschalten von Threads innerhalb eines Prozesses erspart sich das Betriebssystem das Sichern großer Teile des Process Control Blocks (PCB). Ein Thread darf ohnehin alle Ressourcen „seines“ Prozesses verwenden. Seine Beschreibung im Betriebssystem reduziert sich damit auf wenige Werte wie Registerinhalte, Runtime-Stack und Scheduling Informationen wie etwa die Thread-Priorität.

G.3.1 Threads im Single User Multitasking Betriebssystem SYMBIAN

Beim Symbian OS (mit EKA2 Kernel: EPOC Kernel Architecture 2) ist ein Prozess, ausgehend von einem zugehörigen Binary Image (ausführbares File), eine Sammlung von ein oder mehreren Threads, die sich einen ganz bestimmten Adressraum aufteilen.

Sofern hardwaremäßig keine weitere Unterstützung in der Speicherverwaltung geboten wird, können Threads nicht auf den Speicher zugreifen, der den Threads eines anderen Prozesses zugeteilt worden ist. Wohl aber können sie – zum selben Prozess gehörig – untereinander von ihrem Speicher lesen und darauf schreiben.

Für die Liste der Threads im Zustand ready gibt es, Prioritäten entsprechend, bis zu 64 Ready Queues.

Ein Maskenregister zeigt mit einem gesetzten i-ten Bit an, ob in der i-ten Ready Queue tatsächlich auch ein Thread vorhanden ist. Threads werden aus der Queue mit höchster Priorität gewählt. Befinden sich mehrere Threads in der i-ten Queue, so wird dort nach Round Robin vorgegangen.

Neben diesen Threads gibt es eine besondere Klasse, die als Active Objects bezeichnet werden. Diese unterliegen einem eigenen Scheduler, dem Active Scheduler, der nicht präemptiv ist und über eine Schleife (event loop) alle eingehängten Active Objects abhängig von Ereignissen (events) steuert. Damit ist zwischen Active Objects im Gegensatz zu den anderen Threads nur cooperative Multithreading möglich, denn sie laufen unter dem Active Scheduler als ein einziger Thread ab.

G.3.2 Threads bei der Java Virtual Machine

Zunächst ist die Situation nicht unähnlich dem Symbian OS. Allerdings gibt es nur 10 Prioritätsebenen. Sind mehrere Threads derselben Prioritätsklasse vorhanden, so wird wieder Round Robin ins Spiel gebracht.

Die Situation ist aber deswegen anders, weil die Java Maschine eine virtuelle Maschine (Java VM) ist und daher auf einem anderen Host-Betriebssystem aufgesetzt wird. Dann hängt es von der – nicht genormten – Implementierung ab, ob die Threads als so genannte Green Threads auf der Java VM selbst modelliert werden oder ob sie auf die Threads des Hostbetriebssystem (Abbildung auf Native Threads) aufgesetzt werden.

Der Vorteil der ersten Methode ist, dass eine bessere Unabhängigkeit vom darunter liegenden System gegeben ist. Ihr Nachteil ist, dass ein eigener Scheduler zu implementieren ist. Dieser muss dann aber letztlich dem Scheduler des Host Systems erst recht wieder „gehorchen“.

Das folgende Programm erzeugt zwei Threads. Der Main Thread macht dabei nichts anderes als den anderen Thread (Thread1) zu starten. Main1.java enthält die Methode main().

Der zweite Thread erweitert die Klasse Thread. Er wird durch t1=new Thread1 erzeugt und mit t1.start() gestartet. Dann wird einfach Text ausgegeben.

Code G2  Threads in der Java VM – Text Ausgabe eines Threads

Wir modifizieren unser Programm geringfügig dahingehend, sodass sowohl main() als auch der gestartete Thread zu Testzwecken Bildschirmausgaben durchführen.

Code G3   Threads in der Java VM – Text Ausgabe beider Threads

Falls dieses Beispiel auf einer Plattform mit Preemptive Scheduling läuft, so sieht die Ausgabe in etwa wie folgt aus, die Länge der einzelnen Phasen hängt von der Größe des Zeitquantums (Time-Slice) ab.

Abb. G5   Threads in der Java VM – Konsolen Ausgabe von Code G3 bei preemptiven Scheduling

G.3.3 Thread Prioritäten in Windows

Man unterscheidet zwischen Real-Time Klassen und den dynamischen Klassen.

Abb. G6   Thread-Prioritäten in Windows

Grundsätzlich sieht Windows (von Anfang an, seit Windows NT) vor, dass Scheduling auf Thread-Ebene stattfindet, wobei Prioritätsklassen vorgesehen sind. Innerhalb einer Prioritätsklasse gilt Round Robin.

Mit den Prioritätsklassen Real-Time erreicht man eine quasi Real-Time-Möglichkeit (Soft Real-Time).

Threads der Klasse „variable Priorität“ können durch einen Priority Boost (kurzzeitig) höhere Priorität erhalten, typischer Weise wenn ein Thread nach einer I/O Anforderung aus seinem Wartezustand befreit wird. Ebenso erfolgt eine temporäre Prioritätenanhebung, wenn ein Fenster als Vordergrundfenster (Foreground Window) gesetzt wird. Diese Priorität wird dann schrittweise wieder bis zur Ausgangspriorität (base priority) reduziert.

Die tatsächliche Bevorzugung eines Foreground-Thread erfolgt jedoch letztlich über eine größere Zuteilung der Time-Slice (Quantum).

Auch ist es möglich, in Programmen über das API eine Prioritäts-Erhöhung/Erniedrigung innerhalb der Klasse mit variablen Prioritäten zu bewirken. Davor sei aber gewarnt.

Abhängig von der Plattform unterbricht ein Timer über eine Interrupt-Routine alle 10 – 15 msek einen laufenden Thread und führt dabei eine Nutzungsstatistik der CPU für den Thread. Diese Information nutzt man, um CPU dominierende Threads in ihrer Priorität zu verringern, damit andere nicht „verhungern“ (vgl.: starvation problem).

Dabei kann es zu Ungerechtigkeiten kommen, wenn das System einem Thread die gesamte zugedachte Intervallbreite zurechnet, wie es bei Windows NT und XP der Fall ist. Es kann ja sein, dass die tatsächliche CPU-Zuteilung erst gegen Ende der Time-Slice erfolgt, oder auf Grund anderer Unterbrechungen der Thread die CPU nur für einen Teil der Zeitscheibe nutzen kann, die gesamte Zeitscheibe aber dem Thread zugerechnet wird. Beim Nachfolgesystem wurde hier eine Änderung vorgenommen. Heutige CPUs verfügen dafür über ein zusätzliches Register, das Information über die CPU-Zyklen (nicht: Zeit!, hängt von der Taktrate ab) liefert. Beim Pentium Prozessor steht dafür die RDTSC Instruktion (read timestamp counter) zur Verfügung.

Die Abbildung zeigt den Unterschied zwischen beiden Zuteilungsvarianten.

Abb. G7  Genauere CPU-Zuteilung in Windows

Ein völlig anderes Argument für diese „exaktere Zuteilung“ von Prozessor-Zyklen kann auch fiskalisch motiviert werden: je genauer und fairer die Inanspruchnahme der CPU für einen Kundenauftrag nachgewiesen wird, desto weniger strittig ist eine Faktura über die bezogene Leistung.

G.4 Klassische Problemstellungen

G.4.1 Grundgedanke

Wir verstehen ein Betriebssystem als eine Menge von Systemprozessen, die parallel laufen. Diese Prozesse operieren auf gemeinsamen Daten und tauschen untereinander Nachrichten aus. Dabei werden Probleme aufgeworfen, die im Zusammenhang mit parallelen Prozessen von allgemeinem Interesse sind.

Die hier vorgestellten Probleme sind typisch für Prozess-Kommunikation in Betriebssystemen, werden hier aber allgemeiner formuliert, da sie auch generell für den Entwurf und die Implementierung von Software wichtig sind.

G.4.2 Producer-Consumer-Problem

G.4.2.1 Problemstellung

Diese Aufgabe wurde im vorigen Kapitel schon genannt. Ein Erzeuger (Producer) P generiert Daten vom Typ item und legt sie in einen Puffer ab, aus dem sie ein Consumer C genau in der Reihenfolge abholen soll, in der sie in den Puffer gebracht worden sind.

Der Puffer ist typischerweise als Ringstruktur angeordnet, also ein Array item b[BUFFER_SIZE], das zyklisch über einen Index modulo BUFFER_SIZE adressiert wird.

Code G4  Producer-Consumer – Puffer

- Die Variable in zeigt auf die nächste freie Position im Puffer b.

- Die Variable out zeigt auf die erste besetzte Position, falls es eine gibt.

- Bedingung für einen leeren Puffer ist: counter == 0. (== heißt „ist gleich“)

- Bedingung für einen vollen Puffer ist: counter == BUFFER_SIZE.

Erwähnt sei noch: Es gibt auch Lösungen, die keine zusätzliche Variable (counter) benützen, um anzuzeigen, wie viele Objekte im Puffer sind.

Die Variable counter dient als Beispiel für eine shared Variable und soll deren Verwendung zeigen: man beachte, dass sie von den Prozessen P und C sowohl beschrieben (verändert) als auch gelesen werden kann. P und C sollen dabei parallel zueinander laufen und man darf auch nichts über die relative Geschwindigkeit der beiden Prozesse zueinander voraussetzen.

Die Kapazität des Puffers b ist endlich. Demnach darf P nichts in einen vollen Puffer stellen. Umgekehrt kann wohl C nichts aus einem leeren Puffer entnehmen.

Der Puffer glättet Unterschiede in der Geschwindigkeit von P und C, wodurch der Produzent P als auch der Konsument C zeitweise schneller sein können.

Der Consumer verwendet die Variable nextConsumed vom Typ item wie folgt:

Code G5   Producer-Consumer – Consumer holt nächstes Element vom Puffer

Der Producer verwendet in analoger Weise die Variable nextProduced:

Code G6   Producer-Consumer – Produzent generiert nächstes Element und legt es in den Puffer

Für eine feste Wahl von BUFFER_SIZE lässt sich der Puffer dann wie folgt veranschaulichen:

Abb. G8  Producer-Consumer – Puffer

G.4.2.2 Busy Waiting

Wie bereits erwähnt darf P in einen vollen Puffer nicht einfügen, also:

Code G7   Producer-Consumer – Producer busy waiting

Umgekehrt kann C nichts aus einem leeren Puffer entnehmen:

Code G8   Producer-Consumer – Consumer busy waiting

Das Problem dabei ist: P und C verbrauchen CPU-Zeit, während sie warten. Zwei Möglichkeiten dies zu umgehen sind:

- Das Problem wird von der Scheduling Strategie gelöst.

- Ein spezielles Wake-Up Konzept wird verwendet.

Ein gravierendes Problem bleibt jedoch immer noch bestehen, auf das wir nun näher eingehen: Obwohl beide, Produzent und Konsument, korrekt arbeiten, greifen sie parallel zu nicht vorher bekannten Zeiten auf die gemeinsame (shared) Variable counter zu:

Abb. G9   Producer-Consumer – Zugriff auf gemeinen Zähler „counter“

Wir nehmen nun aus didaktischen Gründen an, dass für das Erhöhen bzw. Erniedrigen der Variablen nicht inc(counter) bzw. dec(counter) verwendet wird. Damit umgehen wir das Argument, dass auf heutigen Prozessoren und vorliegenden Sprachimplementierungen ohnedies spezielle nicht unterbrechbare Instruktionen vorliegen und sich das Problem nicht stellt.

Wir unterstellen, der Programmierer habe „klassisch“ wie nachstehend programmiert

- Operation von P:

counter = counter + 1;

- Operation von C:

counter = counter – 1;

Bei der Übersetzung seien Hilfsregister (regP, regC) verwendet worden, wie sie in Abb. G9 und Abb. G10 benannt sind, sodass sich zur Umsetzung der Codefolge genau die in der Abbildung gezeigte Verzahnung ergeben könnte:

Eine typische Folge von Zuweisungen könnte etwa wie folgt aussehen:

- Zu Beginn sei counter=5 gesetzt.

- Dann werde ein Element von P produziert und ein Element von C konsumiert.

- Das erwartete Ergebnis ist dann wieder counter==5.

Wir sehen uns diesen zeitlichen Ablauf anhand der folgenden Abbildung an und stellen fest, dass das Ergebnis falsch ist, wenn die Operationen in der dort angegebenen Reihenfolge verzahnt waren.

Abb. G10   Producer-Consumer – Zugriff auf gemeinsamen Zähler „counter“ führt zu Problemen

G.4.2.3 Race Condition

Eine Situation, bei der parallele Prozesse auf gemeinsame Daten lesend und schreibend zugreifen und das Resultat von der speziellen Reihenfolge bzw. der relativen Geschwindigkeit der Prozesse untereinander abhängt, nennt man Race Condition.

Diese Art von Fehlern ist besonders unangenehm, da sie zeitabhängig sind. Sie neigen dazu, gerade beim Austesten von Software nicht aufzutreten und bleiben nicht selten unentdeckt.

G.4.3 Wechselseitiger Ausschluss und Kritische Regionen

Um die Race Condition in unserem Beispiel zu verhindern (counter bildet einen gemeinsamen Datenbereich) ist folgendes zu beachten:

- Es ist zu garantieren, dass zu jedem Zeitpunkt höchstens ein Prozess die Variable counter manipulieren kann. Dies erfordert eine passende Synchronisation der beteiligten Prozesse.

- Es gibt im vorgestellten Consumer-Producer-Problem einen Codeabschnitt von P und C, in dem beide die gemeinsame Variable möglicherweise verändern, sodass es dort zu einer Race Condition kommen könnte. Einen solchen Abschnitt nennt man Kritische Region (Critical Section, Critical Region, CR).

- Weder P noch C darf gestattet werden, eine solche Kritische Region zu betreten, wenn der jeweils andere gerade sich darin befindet.

Eine andere Sprechweise ist: Für eine Kritische Region muss wechselseitiger Ausschluss (mutual exclusion) gewährleistet werden.

Besondere Aufmerksamkeit werden wir später der Frage widmen müssen, wenn noch keiner der beiden Prozesse die Kritische Region betreten hat, beide aber nun gleichzeitig hinein wollen und sich darüber zu verständigen haben.

Im Beispiel haben wir immer nur von zwei Prozessen gesprochen. Das Problem stellt sich allgemein, wenn bei n ≥ 2 parallelen Prozessen mehr als einer auf gemeinsame (shared) Variablen schreibend zugreift.

Eine andere Aufgabenstellung ist das Reader-Writer-Problem: In einem System bewerben sich r ≥ 1 Leseprozesse (Reader) und w ≥ 1 Ausgabeprozesse (Writer) parallel um einen Filezugriff. Im Gegensatz zum Producer-Consumer-Problem, liest ein Reader nur Daten, ohne das File zu verändern. Dies ist typisch bei Datenbanksystemen, wo mittels Reader Anfragen beantwortet werden und Updates über Writer eingespielt werden. Ferner soll es Prioritäten geben, wobei verschiedene Varianten möglich sind. Eine Version des Reader-Writer-Problems verlangt, dass ein Reader nur dann warten muss, wenn ein Writer bereits ein Zugriffsrecht auf das File zum Schreiben erhalten hat. Damit können Reader parallel zugreifen und müssen nur im Interesse der Datenintegrität auf die Beendigung eines Writer-Prozesses warten.

G.4.3.1 Anforderungen an eine zulässige Lösung

Um zu einem Lösungsansatz für Mutual Exclusion zu kommen, betrachten wir einen Prozess, der zyklisch eine CR betreten will und gehen von folgender schematischer Programmstruktur aus:

Abb. G11   Mutual Exclusion über Kritische Region

Beim Eintritt in die CR muss sich der Prozess um die Erlaubnis dazu bewerben, in der CR selbst wird (definitionsgemäß) der kritische Code unter Mutual Exclusion ausgeführt. Beim Verlassen der CR müssen bestimmte organisatorische Informationen gesetzt werden, damit das Verlassen auch publik wird. Der folgende Rest (in Abb. G11 „weiterer nicht-kritischer Code bzw. Abschnitt“) ist dann unkritisch, bis der Prozess wieder in die CR will.

E. W. Dijkstra erläuterte in seinem klassischen Paper „Cooperating Sequential Processes“, welche typischen Probleme auftreten können, wobei er falsche Lösungsversuche analysierte. Von ihm wurden auch folgende Forderungen für eine korrekte Lösung zusammengefasst:

- Mutual Exclusion:

Höchstens ein Prozess darf sich zu einem Zeitpunkt in der CR befinden.

- Bounded Waiting:

Wenn ein Prozess eine CR betreten möchte, so muss ihm dies innerhalb endlicher Zeit gewährt werden. Diese Bedingung heißt deshalb auch Fairness-Bedingung. Wenn demnach ein Prozess sich für das Betreten der CR angemeldet hat, dann darf den anderen Prozessen nur mehr endlich oft („bounded“) der Zugang gewährt werden. Es sind auch keine Annahmen über die relative Geschwindigkeit der Prozesse oder über die Reihenfolge der Zutritte erlaubt.

- Progress:

Ein Prozess darf nur eine endliche Zeit in einer CR bleiben. Prozesse, die bereits außerhalb laufen (in Abb. G11 „(weiterer) nicht-kritischer Code bzw. Abschnitt“), dürfen nicht in die Entscheidung eingebunden werden, wer als nächster die CR betreten soll.

G.4.3.2 Falsche Lösungsversuche

Die nachstehenden Beispiele sind didaktischer Natur, um das Problem besser verstehen zu können und lassen erkennen, warum ein besonderer Lösungsansatz außerhalb einer Programmiersprache und unter Einbindung von Betriebssystem und auch Rechnerarchitektur zweckmäßig wird.

Für die Diskussion genügt es, von zwei parallelen Prozessen auszugehen. [Dij85, Nachdruck von 1965], [Bri75]

G.4.3.2.1 Verletzung von Mutual Exclusion

Code G9  Verletzung von Mutual Exclusion

Diese Lösung verwendet eine shared Variable free und verstößt bereits hier gegen Mutual Exclusion. Beide Prozesse lesen free = = true und setzen dann im guten Glauben free = false.

G.4.3.2.2 Reihenfolge wird erzwungen

Wir ordnen den Prozessen P und Q eine gemeinsame Variable p_turn zu, aus der jeder Prozess erkennen kann, ob er in die CR hinein darf oder nicht: Ist p_turn = = true, so darf P hinein, andernfalls Q.

Code G10 Mutual Exclusion durch erzwungener Ablaufreihenfolge

Hier haben wir zwar Mutual Exclusion erzielt, es wird aber eine strikte alternierende Reihenfolge erzwungen. Wenn z.B. Q nach einer Weile nicht mehr die CR betritt, kann dann auch P nicht mehr hinein.

Unbeschadet dessen beobachten wir auch, dass ein busy waiting in den while-Schleifen ausgeführt wird. Dieses Beschäftigte Warten vergeudet Prozessorzeit bzw. wäre bei einer no preemption scheduling Strategie ohnedies nicht sinnvoll: In diesem Fall würde ein Prozess dauernd warten und die CPU nie mehr abgeben. Damit wird es wegen „no preemption“ unmöglich, dass zwischenzeitlich ein anderer Prozess den Grund für das Beschäftigte Warten aufhebt.

G.4.3.2.3 Progress Verletzung

Im Folgenden geben wir einen Algorithmus an, der zwar Mutual Exclusion erfüllt, nicht aber die Bedingung Progress. Dieser Lösungsansatz kann zu einem Fehler führen, der als Deadlock bezeichnet wird. Dies ist ein zeitabhängiger Fehler, er kann auftreten, muss es aber nicht. Außerdem erfolgt wiederum ein busy waiting in einer while-Schleife.

Zur Ablaufsteuerung verwenden wir für den Prozess P = P0 und den Prozess Q = P1 eine direkt zugeordnete Variable flag[0] bzw. flag[1].

Code G11   Mutual Exclusion, aber Progress Verletzung

Mutual Exclusion ist erfüllt. P0 setzt flag[0] auf true, um zu zeigen, dass er die Critical Region betreten möchte. Dann wartet P0, bis flag[1] false wird. Dies passiert, sobald P1 die Critical Region verlassen hat, und somit P0 wie gewünscht fortfahren kann.

Man beachte jedoch die in Abb. G12 dargestellte Sequenz:

Abb. G12   Wechselseitiger Ausschluss erreicht, aber Verklemmung möglich

Nun sind P0 und P1 in einer Endlosschleife. Keiner kann das flag[i] des anderen für die Zutrittsberechtigung neu setzen. Damit ist die Bedingung Progress nicht erfüllt.

G.4.4 Hardwarehilfen

Hat man eine Variable, die von mehreren Prozessen gemeinsam gelesen und geschrieben wird, so handelt es sich bereits um eine CR! Ein Gedankengang ist, solche „kurze“ Kritische Regionen mit Hardware-Unterstützung zur Verfügung zu stellen und dann damit lange, also mehrere Statements umfassende CRs zu implementieren.

Die Lösungsansätze von vorhin basieren alle auf busy waiting. Bei kurzen CRs fällt dies definitorisch nicht so ins Gewicht, solange preemptive Scheduling vorausgesetzt werden kann.

Somit verbleibt als Aufgabe, zu verhindern, dass während des Schreibens (Setzen) bzw. Lesen einer solchen shared Variablen – man erinnere sich an die Zerlegung in Einzelschritte bei Registerzuweisungen – sichergestellt wird, dass kein anderer Prozess darauf zugreifen kann. Dabei kommt es ganz wesentlich darauf an, ob man ein Einprozessorsystem oder ein Mehrprozessorsystem vor sich hat.

G.4.4.1 Einprozessorsysteme

Es besteht die Möglichkeit, Interrupts zu verbieten, solange eine shared Variable geändert wird. Dabei werden alle Interrupts gesperrt, danach auf die shared Variable zugegriffen und anschließend die Interrupts wieder freigegeben. Damit kann der laufende Prozess, der gerade auf die shared Variable schreibt oder sie liest bzw. sich in einer CR aufhält, nicht unterbrochen werden. In diesem Sinne sind diese Operationen bzw. die Folge der Operationen in der CR atomar, also unteilbar.

Diese Lösung ist aber bei einem Mehrprozessorsystem nicht ausreichend. Man müsste nicht nur alle Interrupts sperren, sondern auch noch zusätzlich sicherstellen, dass die in den anderen Prozessoren laufenden Prozesse nicht auf die shared Variable zugreifen und alle anderen aktiven Prozesse allenfalls sogar anhalten. Der Vorteil eines Mehrprozessorsystems würde verloren gehen.

G.4.4.2 Mehrprozessorsysteme

Moderne Architekturen stellen einen speziellen TestAndSet Befehl zur Verfügung. Es ist dies ein auf CPU- und Memory-Ebene atomarer Hardware-Befehl. Der Inhalt einer Speicherstelle flag kann innerhalb einer Instruktion überprüft und gesetzt werden, ohne dass währenddessen ein Interrupt passiert und ohne dass ein anderer Prozessor auf diese Speicherstelle zugreifen kann.

In Pseudocode erklärt hat TestAndSet bei Unteilbarkeit folgende Wirkung, wobei unterstellt ist, dass die Parameterübergabe von flag mit Call by Reference und nicht (!) mit Call by Value erfolgt. Dies ist auch in den Codeausschnitten unterstellt, die TestAndSet verwenden.

Code G12   TestAndSet

Eine andere (in C formulierte) Variante, obige TestAndSet Operation zu beschreiben, verdient Beachtung. Hier ist berücksichtigt, dass der Typ boolean durch den Typ int mit 0 für false und 1 für true zu ersetzen ist, int *flag sorgt für Call by Reference.

Code G13   TestAndSet vereinfacht

Falls zwei TestAndSet Befehle von verschiedenen CPUs ausgeführt werden, so geschieht dies in irgendeiner Reihenfolge, über die keine Annahmen getroffen werden darf. Die TestAndSet Instruktion selbst ist aber unteilbar!

Ausgerüstet mit dieser Unterstützung kann man das Mutual Exclusion Problem – jedenfalls für kurze Kritische Regionen – wie folgt lösen.

Code G14   Mutual Exclusion gelöst durch TestAndSet für kurze CRs

Die gewählte metasprachliche Bedeutung der Variablen free in Beispiel Code ‎G 14 impliziert, dass die CR betreten werden darf, wenn sie noch „frei“ ist. Daher wurde free anfangs mit true initialisiert. In der while-Schleife wird solange gewartet, bis der Wert free von true auf false gesetzt werden kann.

G.4.5 TestAndSet als Methode einer Java Klasse

Da Java Multithreading erlaubt, ist es sehr hilfreich, dass Synchronisationsfragen für Prozesse durch spezielle Klassen unterstützt werden.

Eine dieser Klassen ist java.util.concurrent.atomic.AtomicBoolean. Diese Klasse stellt neben dem Konstruktor AtomicBoolean(boolean intialValue) unter anderem eine Methode getAndSet(boolean newValue) zur Verfügung. Diese setzt atomar den gegebenen Parameterwert und retourniert jenen Wert, der vorher gesetzt war.

Mit ihr lässt sich ein wie vorhin besprochenes TestAndSet wie folgt umsetzen:

Code G15  TestAndSet in Java

Man beachte, dass ein Thread, der in der while-Schleife warten muss, aus ihr nur dann befreit wird, wenn ein anderer paralleler Thread mit der Methode free.set(true) sein Verlassen der CR mitgeteilt hat.

G.4.6 Spin Lock

Eine erweiterte Lösung zu oben sind Spin Locks. Sie basieren wieder auf busy waiting, verwenden aber anstatt einer booleschen Variablen flag einen Zähler. Spin Locks sind als spezielle Art von „schwachen“ Semaphoren zu betrachten, welche im Weiteren noch genau besprochen werden.

Ein Spin Lock stellt für eine Variable s folgende Operationen zur Verfügung:

- Warten und atomares Dekrementieren: lock(s).

Der Prozess wartet im Falle s <= 0 mit busy waiting, bis von einem anderen Prozess der Zähler erhöht wurde.

- Atomares Inkrement: unlock(s).

- Atomare Initialisierung mit einem Integer n >=0: init(s,n)

Aus dem folgenden Pseudocode entnimmt man auch, dass eine Spin Lock-Variable s als Zähler gesehen wird.

Ist beispielsweise s mit n = 3 initialisiert, so lässt lock(s) bis zu 3 Prozesse in die zu überwachende Region hinein. Dann ist zwar kein Mutual Exclusion gewährleistet, man hat aber einen Mechanismus zur Verfügung, der die maximale Anzahl der auf die durch s geschützte Ressource parallel zugreifenden Prozesse begrenzt. In welcher Reihenfolge dies erlaubt wird, ist nicht spezifiziert.

Man beachte auch, dass mit einem Spin Lock kein Fair Scheduling verbunden ist und die Gefahr bestehen kann, dass einer der Prozesse mit lock(s) verhungert.

Die Lock-Operation wird hier nur symbolisch (!!) beschrieben. Das Codestück soll andeuten, dass der betreffende laufende Prozess solange in busy waiting bleibt, bis der Zähler von anderen Prozessen auf > 0 gesetzt worden ist. Tatsächlich wäre der Codeabschnitt z.B. mit dem vorhin erläuterten TestAndSet zu implementieren. Es könnten ja beim Prüfen auf s <= 0 zwei Prozesse gleichzeitig feststellen, dass s == 1 ist. Beide würden dann verbotener Weise den geschützten Abschnitt betreten, obwohl jeweils nur einer von ihnen das darf.

Code G16   Spin Lock

Ein mit 1 (entspricht true) initialisierter Spin Lock s kann zur Realisierung einer Critical Region wie folgt verwendet werden:

Code G17   Anwendung Spin Lock

G.4.7 Semaphore

G.4.7.1  Motivation

Wir erweitern das Spin Lock Konzept, indem eine Warteschlange damit assoziiert wird und kommen so zu Semaphoren. Dabei sollen Prozesse in diese Warteschlange Q gestellt werden, sobald sie warten müssen. Ist dies der Fall, wechselt ein Prozess vom Zustand running auf waiting. Beim Herausnehmen aus der Warteschlange Q kommt der Prozess vom Zustand waiting in den Zustand ready bzw. running.

Mit diesem Ansatz erreicht man, dass wegen des wechselseitigen Ausschlusses anzuhaltende Prozesse während des Wartens in der Warteschlange verweilen und so keine Rechenzeit benötigen. Damit muss zur Realisierung einer Semaphore das Betriebssystem mit seiner Scheduling Strategie involviert werden.

Ob nach dem Entnehmen eines Prozesses oder Threads P aus der Warteschlange Q sofort running wird oder bloß   wie es i.Allg. der Fall ist   in den Zustand ready für späteres Scheduling versetzt wird, ist Implementierungssache und im Semaphorkonzept nicht weiter spezifiziert. Wichtig ist auch für den Fall, dass mehrere Prozesse in Q warten, Starvation durch eine faire Entnahmestrategie verhindert wird.

Die Bezeichnung Semaphore kommt von den griechischen Wörtern sema = Zeichen und pherein = tragen.

Semaphoren werden, dem Namen entsprechend, für den Austausch von Signalen zwischen Prozessen verwendet. Von E. W. Dijkstra stammen die Bezeichner p (proberen = versuchen) und v (verhogen = erhöhen) in seiner Abhandlung über parallele Prozesse und dort eingeführte Semaphore [Dij85, Nachdruck von 1965].

G.4.7.2 Beschreibung der Semaphor-Operationen

Eine Semaphore S besteht im Wesentlichen aus einer Integer Variable mit einer Initialisierung init(S,n) und zwei atomaren Operationen:

- wait:

p-Operation

- signal:

v-Operation.

Weiters ist einer Semaphore eine Warteschlange Q von Prozessen zugeordnet.

Die Wirkung der Operationen ist wie folgt:

- init(S,n):

Die Semaphore S wird mit einem Wert n initialisiert. Falls S verwendet wird, um eine Critical Region zu implementieren, nimmt man init(S,1). Ein Spezialfall ist eine binäre Semaphore mutex, deren Initialisierung man mit init(mutex,true) bzw. init(mutex,false) durchführt.

Im Detail geschieht durch diese atomaren Operationen folgendes:

- wait(S):

Der Prozess P überprüft, ob S > 0. Ist dies der Fall, wird unteilbar (atomar) S = S – 1 ausgeführt und P fährt ohne Verzögerung fort. Andernfalls wird P in den Zustand waiting versetzt und in die dazu gehörige Warteschlange Q eingefügt. (Das Einhängen in die Warteschlange Q erfolgt durch Einfügen des TCBs oder durch Einhängen eines Verweises (Pointer) auf den TCB des betreffenden Prozesses.)

- signal(S):

Der Prozess P überprüft mittels P.signal(S), ob gegenwärtig ein oder mehrere Prozesse in Q gereiht sind. In diesem Fall wird ein Prozess Pi aus Q ausgewählt und in den Zustand ready (bzw. running, s.o.) versetzt. Die Auswahl aus Q muss fair sein, damit Starvation verhindert wird. Falls die Schlange Q leer war, wird atomar S = S + 1 ausgeführt, sodass die Wirkung von signal nicht verloren geht.

Abb. G13   Semaphore mit zwei wartenden Prozessen W und X

Angenommen, S sei mit init(S,3) auf einen Anfangswert gesetzt worden. Dann können mit P1.wait(S), P2.wait(S) und P3.wait(S) maximal drei Prozesse die Semaphore ohne Verzögerung passieren. Erst ein Prozess P4 wird nach P4.wait(s) in die Warteschlange gestellt und verbleibt dort ohne CPU-Verbrauch, bis ein Pi ein Pi.signal(s) sendet.

Die erforderliche Unteilbarkeit der Semaphor-Operationen kann wie folgt erreicht werden:

- bei Nonpreemptive Scheduling:

Leicht zu erzielen, da keine Scheduling-Interrupts erlaubt sind, wenn eine Folge von Instruktionen ausgeführt wird.

- bei Preemptive Scheduling mit nur einer CPU:

Hier kann das Interrupt Register maskiert werden, um Interrupts für eine gewisse Zeit zu verhindern.

- im allgemeinen Fall (preemptive Scheduling und Mehrprozessorsystem):

Spin Lock Operationen Lock und Unlock können verwendet werden. Das bedeutet, dass man die Semaphore selbst als kurze Kritische Region (busy waiting!) betrachtet, mit der man lange Kritische Regionen (Prozess wird schlafen gelegt, Zustandswechsel zu waiting) implementieren kann.

G.4.7.3 Beispiel Code

Alle Operationen auf einer Semaphore sind atomar zu realisieren. Wir betrachten jede Semaphor-Operation für sich als kurze Critical Region und können dazu Spin Locks einsetzen.

Im Folgenden verwenden wir die Notation lock und unlock, um zu garantieren, dass die dazwischen liegenden Befehle atomar sind:

Code G18   lock / unlock für atomare Code Abschnitte

Ebenso verwenden wir einen hier nicht näher definierten Datentyp für eine Warteschlange Q (bzw.: S.Q als die der Semaphore zugeordnete Warteschlange) als Liste von Prozessbeschreibungsblöcken PCB vom Typ process. Auf Q seien als Operationen enqueue und dequeue erklärt.

Die gewählte Notation ist an eine prozedurale Schreibweise angelehnt:

Code G19   Hinzufügen/Entfernen eines Prozesses in/von der Warteschlange der Semaphore

Dabei repräsentiere PCB den laufenden Prozess P und dequeue soll im schon früher erklärten Sinn fair einen Prozess entnehmen, trivialer weise durch FIFO.

Ferner unterstellen wir seitens des Betriebssystems die Verfügbarkeit der API-Calls.

Code G20 Prozess Zustandsüberführungsfunktionen block und wakeup

Entsprechend dem Task-Zustandsdiagramm wird durch block(PCB) bzw. wakeup(PCB) ein durch seinen PCB repräsentierter Prozess vom Zustand running in den Zustand waiting bzw. von waiting nach ready gebracht.

Wir erinnern daran, dass der Übergang von waiting nach ready den allgemeinen im System vorgegeben Scheduling Prinzipien folgt, dies aber nicht unbedingt so sein muss. Man könnte den betroffenen Prozess hier sofort in den Zustand running versetzen, würde damit aber einen Eingriff in die globale Scheduling Strategie nur um eines „lokalen Vorteiles“ wegen provozieren.

Eine Semaphore kann damit wie folgt vereinbart werden:

Code G21  Beschreibung Semaphore in Pseudocode

Die Befehle (hier: procedure calls) wait und signal selbst beschreiben wir als an die Programmiersprache C angelehnte Prozeduren vom Typ void, da sie keinen Rückgabewert liefern. Ferner sei wieder ein Call by Reference unterstellt. Wäre semaphore als Klasse in Java definiert, wäre mit semaphore s = new semaphore(); das angelegte s ohnedies ein Zeiger auf das angelegte Klassenobjekt.

Dies ist bei einer Umsetzung des Pseudocodes von Code G21 zu beachten.

Code G22   Operationen signal und wait auf eine Semaphore

Es soll vorab auffallen, dass der Code nicht streng der klassischen Beschreibung von Semaphoren folgt. Man beachte nämlich, dass in dieser Implementierung S.value negativ ist, sobald Prozesse auf ein signal warten.

Es repräsentiert S.value < 0 mit seinem Absolutwert die Anzahl der wartenden Prozesse. Diese Beobachtung kann man nachvollziehen, wenn man etwa init(S,1) annimmt, einen Prozess bei wait vorbeilässt und dann überlegt, dass alle folgenden sich um das Passieren der Semaphore bewerbenden Prozesse schlafen gelegt werden müssen, bis mittels signal ein Prozess aus der Warteschlange geholt wird.

Abb. G14   Semaphore zeigt durch S.value = -3 drei wartende Prozesse

Daher muss bei der vorliegenden Implementierung in jedem Fall bei wait bzw. signal der Zähler herunter (S.value--;) bzw. hinauf (S.value++;) gezählt werden.

In der „klassischen“ Definition zählt ein signal den Zähler nur dann hinauf, wenn kein Prozess aus der Warteschlange zu entnehmen ist.

G.4.7.4 Anwendungen von Semaphoren

G.4.7.4.1 Prozess-Synchronisation

Man betrachte zwei parallele Prozesse P1, P2 und verwende eine Semaphore sync wir folgt:

Abb. G15   Synchronisation über Semaphore mit wait und signal

P2 kann die Anweisungsfolge S2 erst ausführen, wenn P1 signal(sync) ausgeführt hat: P2 muss auf P1 warten, beide Prozesse werden synchronisiert.

Dieses Beispiel, erweitert auf 2 Semaphore, werden wir im Abschnitt über Systemverklemmungen (Deadlocks) als Ausgangspunkt der Diskussion wieder aufgreifen.

G.4.7.4.2 Critical Region (CR)

Eine Critical Region CR kann über eine Semaphore mutex (die Wahl des Namens ist an mutual exclusion angelehnt) realisiert werden, die mit 1 bzw. true zu initialisieren ist. Vor dem Betreten der Critical Region muss ein Prozess ein wait(mutex) ausführen. Unmittelbar nach dem Verlassen der CR ist ein signal(mutex) erforderlich, damit ein anderer Prozess an die Reihe kommen kann. Das Signal signal(mutex) teilt also mit, dass die CR nunmehr wieder frei geworden ist.

Im Pseudocode verwenden wir wieder die Typvereinbarung semaphore.

Code G23   Mutex Semaphore in Pseudocode zur Realisierung einer CR

G.4.7.4.3 Producer-Consumer-Problem mit Semaphoren

Nun sind wir gerüstet, das am Anfang des Kapitels vorgestellte Producer-Consumer-Problem mit allen erforderlichen Nebenbedingen zu lösen:

- Zugriff auf den Puffer: unter mutual exclusion

- Reihenfolge: Erzeugte Daten (items) müssen in der Reihenfolge aus dem Puffer genommen werden, in der sie dort abgelegt worden sind

- Kein busy waiting vor einem leeren Puffer für den Consumer

- Kein busy waiting vor einem vollen Puffer für den Producer

Wir verwenden dazu drei Semaphore:

- mutex:

Sichert mutual exclusion für den Pufferzugriff.

- empty:

Repräsentiert die Anzahl der freien Plätze und wird mit der Pufferkapazität n durch init(emtpy, n) initialisiert. Zählen ist atomar!

- full:

Repräsentiert die Anzahl der belegten Plätze und wird mit init(full,0) initialisiert. Zählen ist atomar!

Der Produzent sieht dann schematisch wie folgt aus.

Code G24  Producer-Consumer: Produzent in Pseudocode realisiert mit Semaphore

Der Konsument ist im Prinzip symmetrisch dazu:

Code G25  Producer-Consumer: Konsument in Pseudocode realisiert mit Semaphore

Durch wait(mutex) (siehe Code G24 und Code G25) ist mutual exclusion beim Zugriff auf den Puffer gesichert.

Der Konsument (siehe Code G25) überprüft mit wait(full), ob ein Element im Puffer zum Entnehmen ist, ansonsten schläft er. Die Operation signal(empty) wird verwendet, um die Anzahl der leeren Positionen im Puffer zu zählen (also ein inc(empty)), sowie um einen schlafenden Produzenten zu wecken, falls dies notwendig ist.

Die Lösung ist symmetrisch. Im umgekehrten Fall erhöht der Produzent (siehe Code G24) via signal(full) die Anzahl der belegten Pufferpositionen, nachdem ein neues Element in den Puffer gestellt wurde (also ein inc(full)). Falls full vorher gleich Null war, wird ein schlafender Konsument geweckt.

G.4.8 Kritische Region in Java

Java stellt das synchronized Statement zur Verfügung, um eine CR zu implementieren. Das synchronized Statement versucht eine exklusive Sperre für ein Objekt zu erhalten, das durch einen Ausdruck spezifiziert wird. Die CR wird nicht ausgeführt, bevor die Sperre nicht im Besitz ist.

Code G26  Critical Region realisiert in Java durch synchronized

G.4.9 Monitore

G.4.9.1 Allgemeines

Monitore stellen ein weiteres Synchronisationskonzept zur Verfügung. Sie sind ähnlich zu abstrakten Datentypen und übertragen das Prinzip einer Datenkapsel auf den Zugriff mittels paralleler Prozesse. Monitore wurden von P. Brinch Hansen und A. Hoare, voneinander unabhängig, eingeführt (1973,1974).

Es gibt eine Reihe von Zugriffsfunktionen, die vom Anwender definiert werden, und eine Menge von Variablen, auf die damit zugegriffen wird. In objektorientierter Programmierung heißen solche Zugriffsfunktionen Methoden, die in der Deklaration einer Klasse definiert und auf Objekte dieser Klasse angewandt werden.

Die Zugriffsfunktionen, bzw. Methoden arbeiten also in gewohnter Weise, jedoch wird Mutual Exclusion dabei beachtet. Auf anstehende, wartende Zugriffe (Methodenaufrufe) wird ein Scheduling ausgeführt. Wir gehen darauf hier nicht weiter ein.

G.4.9.2 Monitore in Java

Das vorhin genannte Monitor-Konzept wurde auf Java übertragen. Wie bei den Kritischen Regionen wird wieder das Schlüsselwort synchronized verwendet. Jede Klassenmethode muss wie aus Code G27 ersichtlich deklariert werden. Dies bewirkt:

- Jedes erzeugte Objekt ist implizit mit einem Monitor verbunden.

- Eine synchronized Methode sperrt das Objekt, bevor sie ausgeführt werden kann. Jeder andere Thread, der versucht ein gesperrtes Objekt anzusprechen, kommt in eine zugehörige Warteschlange.

Code G27  Monitor in Java, Sperre auf Objekte

Das synchronized Schlüsselwort zeigt an, dass die Methode als Ganzes eine Critical Section ist. Die Sperre erfolgt nur beim instanzierten Objekt. (Statt „Instanz“ sollte man besser „Exemplar“ sagen, denn eine Instanz ist im Deutschen etwas Anderes als „instance“ auf Englisch).

Falls static hinzugefügt wird, erfolgt die Sperre bei der Klasse:

Code G28  Monitor in Java, Sperre auf Klasse

H SYSTEMVERKLEMMUNGEN (DEADLOCKS)

H.1 Betriebsmittelverwaltung

Allgemeiner Ausgangspunkt ist die Betriebsmittelverwaltung in einem System mit parallelen Prozessen. Dabei geht es um die Zuteilung von Betriebsmitteln, die von mehreren Prozessen gleichzeitig unter Beachtung von wechselseitigem Ausschluss (mutual exclusion) angefordert werden.

- Bei einer statischen Betriebsmittelzuteilung erhält ein Prozess alle von ihm benötigten Betriebsmittel zu Beginn des Prozessablaufes.

- Bei einer dynamischen Betriebsmittelvergabe lässt man es sozusagen darauf ankommen, welche Betriebsmittel ein Prozess während seiner Ausführung tatsächlich benötigt. Erst bei einer konkreten Anforderung wird geprüft, ob die Zuteilung erfolgen kann bzw. darf.

H.2 Synchronisation von Prozessen

Wenn ein non-shareable (nicht gemeinsam benutzbares) Betriebsmittel B vom Prozess P_1 angefordert wird, aber momentan von einem anderen Prozess P_2 belegt ist, dann ist das Prinzip wechselseitiger Ausschluss (mutual exclusion) zu beachten.

In diesem Fall muss P_1 angehalten werden und darauf warten, bis von P_2 oder einer dazu ausersehenen Instanz ein Signal kommt, mit dem mitgeteilt wird, dass B nunmehr frei ist und dem Prozess P1 zugeteilt werden kann. Damit stellt sich allgemein die Frage, wie ein solches Anhalten realisiert werden kann, wobei in der Lösung ein busy waiting des angehaltenen Prozesses zu vermeiden ist.

Das erste Thema ist demnach die Synchronisation von Prozessen. Eine elegante Lösung dazu besteht in der Verwendung eines Semaphors sync, wie wir sie im Kapitel über Parallelität schon vorgestellt haben.

Abb. H1   Synchronisation über Semaphore mit wait und signal

H.2.1 Petrinetze

In der Theorie der Informatik verwendet man oft Petrinetze, um den Fluss von zueinander parallelen Ereignissen beschreiben zu können. Mit Petrinetzen (Carl Adam Petri, *1926) bringt man Dynamik in spezielle Graphen ein, die man insbesondere zur abstrakten Beschreibung von Prozessen verwendet.

Ein Petrinetz ist ein gerichteter bipartiter Graph, d.h. seine Knoten V zerfallen in zwei disjunkte Mengen S (Stellen oder Plätze) und Transitionen T, also V = S ∪T, und es gibt keine direkten Kanten von Stellen zu Stellen und keine von Transitionen zu Transitionen.

Üblicherweise werden in Zeichnungen von Petrinetzen Stellen als Kreise und Transitionen als Rechtecke dargestellt. Die Stellen sind mit Marken (Tokens) markiert und haben gegebenenfalls eine Kapazität für die jeweilige maximale Anzahl von Marken (Tokenzahl) zugeordnet. Ist jedoch eine solche Kapazität nicht angegeben, so nimmt man implizit eine unbegrenzte Kapazität für die jeweilige Stelle an.

Die Belegung der Stellen heißt Markierung des Petrinetzes und beschreibt damit den Zustand des Netzes.

Einer Kante kann ein Gewicht zugeordnet sein, das die Kosten der Kante repräsentiert. Ist das nicht der Fall, so wird 1 als Gewicht angenommen.

Abb. H2   Beispiele für das Schalten eines Petrinetzes

Die angesprochene Dynamik entsteht nun dadurch, dass Transitionen nach bestimmten Regeln (firing rules) schalten können, indem sie von allen Eingangsstellen entsprechend der Kantengewichte Marken abziehen und auf alle Ausgangsstellen den Kantengewichten eine entsprechende Anzahl von Marken hinzufügen. In den folgenden Überlegungen wird immer 1 als Gewicht angenommen, ebenso wird für die Stellen eine unbegrenzte Kapazität vorausgesetzt.

Bei Petrinetzen wird nicht vereinbart, wann welche Transition schaltet. Aber eine Transition kann nur schalten, wenn auf jeder ihrer Eingangsstellen mindestens eine Marke liegt. In diesem Fall wird von jeder Eingangsstelle genau eine Marke abgezogen und zu jeder Ausgangsstelle genau eine weitere Marke hinzugefügt.

Die vorhin beschriebene einfache Synchronisationsaufgabe könnte man mit einem Petrinetz wie nachstehend gezeigt modellieren. Das Wesentliche am Modell ist, dass es die Situation abstrakt beschreibt, ohne dass auf eine konkrete Implementierung (wie z.B. das Semaphor-Konzept) zurückgegriffen werden muss.

Abb. H3   Beispiel einer Schaltfolge eines Petrinetzes (t1 schaltet vor t2)

H. Deadlocks

H.3.1 Ausgangssituation

Unsere eingangs skizzierte Aufgabe wird um einiges komplexer, wenn – was allerdings die Standardsituation bei Betriebssystemen ist –

- mehrere Prozesse P_1, P_2, …, Pn sich um Betriebsmittel B_1, B_2, …, Bm bewerben, jedoch einige der Bi von einigen Pj bereits belegt sind und

- um die verbleibenden Betriebsmittel ein dynamischer Wettbewerb entsteht, welcher Prozess Pj welches Bi erhält, freigibt usw.

Damit die Erklärung des Problems nicht unnötig kompliziert wird, beschränken wir uns auf folgendes Szenario:

Gegeben seien zwei Prozesse P_1 und P_2 und zwei non-shareable Betriebsmittel u und v.

Die beiden Betriebsmittel u und v werden durch Semaphore repräsentiert, sodass man unteilbar (dem Wesen der Semaphore entsprechend) erkennen bzw. mitteilen kann, ob ein Betriebsmittel belegt/frei ist oder belegt/freigegeben wird. Dazu vereinbaren wir:

- init(u,1); init(v,1):

Beide Betriebsmittel u, v sind anfangs frei.

- wait(u):

Versuche u zu belegen. Falls u gerade belegt ist, muss gewartet werden.

- signal(u):

u wird freigegeben. Falls ein Prozess u haben möchte (wait(u)), so kann er jetzt u belegen.

Um zu beschreiben, welcher Prozess Pi welche Semaphoroperation ausführt, verwenden wir die Schreibweise Pi.wait(u), Pi.signal(u). Ferner sei init(u,a) die Zuweisung mit dem Anfangswert a an die Semaphore u.

Für die beiden Prozesse P_1 und P_2 könnte folgende Anweisungsfolge programmiert worden sein, jeweils mit den Anfangswerten init(u,1) und init(v,1):

P_1: P_1.wait(u); …; P_1.wait(v); …; P_1.signal(v); P_1.signal(u);

und

P_2: P_2.wait(v); …; P_2.wait(u); …; P_2.signal(u); P_2.signal(v);

Denkt man sich die Semaphore u und v als Repräsentanten für zwei (verschiedene) Exemplare von Büchern, so kann man obige Anweisungsfolge als Modellierung des „Buchausleiheproblems“ verstehen:

Studentin P_1 entlehnt das Buch u, und fordert – im Sinne einer dynamischen Betriebsmittelzuteilung – später das Buch v an, ohne Buch u zwischenzeitlich zurückzugeben. Für den Studenten P_2 ist die Situation symmetrisch.

(i) Man beachte ferner, dass für das Besitzen von u bzw. v die mutual exclusion Bedingung gilt: Entweder P_1 oder P_2 hat das Buch u (bzw. v). Mit anderen Worten: Beide Personen lesen nicht gleichzeitig darin.

Zusätzlich zu unserem Beispiel unterstellen wir eine sehr ungeschickte Bibliotheksordnung:

(ii) Die Bibliothek kann nicht, z.B. wegen Überschreitung der Entleihfrist, ein Buch u zurückfordern: no preemption.

(iii) Während Studentin P_1, die u bereits besitzt (belegt hat), auf das andere Buch v wartet, gibt sie u nicht frei, obgleich sie momentan damit gar nichts anfangen kann, weil zum Weiterarbeiten v benötigt wird: hold and wait.

Da nun die beiden „Prozesse“ P_1 und P_2 zueinander parallel sind, ist jede beliebige Verzahnung der vorhin genannten Aktionen denkbar, wie z.B. insbesondere:

P_1.wait(u); P_1.wait(v); P_1.signal(v); P_2.wait(v); P_1.signal(u); P_2.wait(u).

Diese Verzahnung entspricht der Situation, dass P_1 das Buch u ausleiht, dann das Buch v und letzteres aber wieder zurückgibt, sodass es von P_2 entlehnt werden kann. Und ehe P2 auch das zweite Buch u benötigt, hat es P_1 schon retourniert.

Bei parallelen und damit verzahnten Prozessen darf aber grundsätzlich über die Reihenfolge (Verzahnung, zeitlicher Ablauf) nichts vorausgesetzt werden. Daher kann auch folgender Sequenz auftreten:

P_1.wait(u); P_2.wait(v); P_1.wait(v); P_2.wait(u).

Dann geht gar nichts mehr, die Situation ist verklemmt, die Prozesse befinden sich in einem Deadlock.

Wir beobachten in diesem Zusammenhang folgendes im Detail:

(iv)  P_1 wartet auf ein Ergebnis, das von P_2 ausgelöst werden müsste, aber P_2 wartet auf ein Ereignis (Freigabe eines Betriebsmittels), welches wiederum nur von P_1 ausgelöst werden kann: Ein zirkuläres Warten (circular wait) liegt vor.

Diese „Bedingung“ spielt später im allgemeinen Fall noch eine wesentliche Rolle.

Wir definieren wie folgt:

Eine Menge von Prozessen befindet sich in einem Deadlock (Verklemmung), wenn jeder Prozess aus dieser Menge auf ein Ereignis wartet, das nur von einem anderen Prozess aus derselben Menge ausgelöst werden kann.

Die obige Situation bzw. das obige Phänomen wurde 1971 von Coffman systematisch untersucht und kann durch vier Bedingungen beschrieben werden. Dabei ist jede der Bedingungen (i), (ii), (iii) und (iv) notwendig, während das Zusammentreffen aller vier Bedingungen hinreichend für Deadlock ist. [Cof71]

Dabei bedeutet:

(1) notwendig: wenn überhaupt ein Deadlock eintreten könnte (oder eingetreten ist), dann folgt, dass die Bedingungen (i), (ii), (iii) und (iv) zutreffen. Tun sie das nicht, kann gar kein Deadlock vorliegen.

Der Umkehrschluss ist besonders wichtig: wenn man in irgendeiner Form bei der Betriebsmittelvergabe erreichen kann, dass eine der notwendigen Bedingungen unmöglich ist, dann ist Deadlockfreiheit sichergestellt.

(2) hinreichend: wenn die Bedingungen (i) – (iv) zusammen, also gleichzeitig, beobachtet werden können, dann folgt daraus, dass ein Deadlock eingetreten ist: hinreichend ist also die Bedingung ((i) & (ii) & (iii) & (iv)).

Die Bedingungen zusammengefasst lauten:

(i) Wechselseitiger Ausschluss (mutual exclusion)

Jedes von einem der beteiligten Prozesse benötigtes Betriebsmittel ist entweder im Sinne von Mutual Exclusion (also exklusiv) belegt oder es ist frei.

(ii) Halten und Warten (hold and wait)

Wenn ein Prozess bereits Betriebsmittel belegt hat und auf die Zuteilung eines weiteren wartet, gibt er während dieses Wartens die belegten Betriebsmittel nicht frei.

(iii) Kein Betriebsmittelentzug (no preemption)

Ein Betriebsmittelentzug, ähnlich wie bei Preemptive Scheduling, ist nicht erlaubt. Die Prozesse müssen die von ihnen belegten Betriebsmittel freigeben, wenn sie diese nicht mehr benötigen.

(iv) Zirkuläres Warten (circular wait)

Es muss eine geschlossene, zirkulare Kette (Kreis) von zwei oder mehreren Prozessen P0, P_1, P_2, … Pn-1, Pn = P0 geben, wobei jeweils Pn-1 ein Betriebsmittel anfordern möchte, das vom in der Kette nachfolgenden Prozess Pi belegt ist.

H.3.2 Modellierung mittels Petrinetz

Das vorhin beschriebene Buchausleiheproblem kann mit einem Petrinetz wie folgt modelliert werden, wobei die durch Semaphore repräsentierten Bücher u und v anfangs in der Bibliothek liegen, damit verfügbar sind und daher mit 1 initialisiert zu denken sind.

Abb. H4  Buchausleihen als Petrinetz

In der Transition t3 wäre {P_1.signal(u); P_1.signal(v)} für „Studentin P_1 gibt beide Bücher zurück“ zu denken. Soweit kann es aber nicht kommen, wenn – wie hier dargestellt – der Student P2 bereits erfolgreich P2.wait(v) ausgeführt hat, also im Besitze von Buch v ist.

Abb. H5  Buchausleihen als Petrinetz, Deadlock

H.3.3 Bewältigung von Deadlocks

H.3.3.1 Allgemeine Konzepte

In der Informatik werden grundlegende Probleme manchmal mit einem Schuss besonderen Humors dargestellt. Ein Beispiel dieser Art ist das „Problem der essenden Philosophen“, in dem die Deadlockproblematik illustriert wird und dazu auch „falsche Lösungen“ aus didaktischen Gründen angegeben werden.

Kurz skizziert geht es um folgende Situation:

- 5 Philosophen Phi verbringen ihr Leben mit Denken, und essen notwendigerweise dazu bescheiden Reis. Sie sitzen um einen Tisch und auf dem Tisch sind 5 Ess-Stäbchen mit einer Schüssel Reis. Hygiene-Fragen spielen hier keine Rolle, daher stehen auch nur 5 Stäbchen zur Verfügung.

- Wenn Phi isst, dann benötigt er zwei Stäbchen, das eine zu seiner Linken, das andere zu seiner Rechten. Allerdings gilt mutual exclusion.

Abb. H6  Die essenden Philosophen: Ausgangssituation

Die folgende „falsche“ Lösung garantiert zwar mutual exclusion, jedoch kann ein Deadlock eintreten: Nämlich dann, wenn alle Philosophen gleichzeitig essen wollen und damit beginnen, das zu ihrer Linken liegende noch unbelegte Stäbchen aufzunehmen. In diesem Falle werden sie vor voller Schüssel verhungern.

Abb. H7   Die essenden Philosophen: Deadlock-Situation

Das Programm (in Pseudocode) beschreibt die Situation für den i-ten Philosophen (modulo 5), entsprechend ist chopstick[i] das links von ihm liegende Stäbchen.

Code ‎H 1  Essende Philosophen falscher Versuch

Zur Behandlung von Deadlocks gibt es im Prinzip folgende Ansätze:

- Gar nichts tun: Dieser Zugang ist durchaus oft anzutreffen, er macht wohl aber nur Sinn, wenn man aus Erfahrung weiß, dass eine Systemverklemmung aus irgendwelchen Gründen un- wahrscheinlich ist und keine weiteren bösen Folgen hat, außer dass man seinen Rechner wieder neu starten muss. Die Frage ist nur, wie man erkennt, dass ein Deadlock vorliegt und nicht etwa ein „bu- sy waiting“ oder Ähnliches entstanden ist.

- Spezielles Protokoll: Man verwendet ein Protokoll (d.h. Zuteilungsstrategie), welches garantiert, dass ein Deadlock erst gar nicht eintreten kann. Dazu hat man wieder zwei grundsätzlich unter- schiedliche Varianten.

- Vorbeugen (Prevention): Wenn von vornherein sichergestellt ist, dass zumindest eine der notwendigen Bedingungen (Coffman, (i) – (iv)) gar nicht möglich sind, kann kein Deadlock eintreten. Die einfachste Lösung dazu ist wohl eine statische Betriebsmittelvergabe. Ein Prozess wird überhaupt erst gestartet, wenn er vorab alle Betriebsmittel erhält, die er anfordern könnte, unabhängig davon, ob er auch alle wirklich beim Prozessablauf benötigt.

- Vermeiden (Avoidance): Man geht von einer dynamischen Betriebsmittelvergabe zunächst ohne irgendwelche Einschränkungen aus. Bevor man aber eine Betriebsmittelanforderung wirklich umsetzt, prüft man, ob man damit nicht in Gefahr läuft, dass ein Deadlock eintreten könnte. Dazu sei erinnert, dass eine Systemverklemmung ein zeitabhängiger Fehler ist: Sie kann eintreten, muss aber nicht.

- Erkennen und Wiederherstellen (Detection and Recovery): Die Idee ist, dass man zunächst eine Betriebsmittelanforderung grundsätzlich erfüllt, sofern das Betriebsmittel auch verfügbar ist. Man nimmt in Kauf, dass dies in der Fol- ge zu einem Deadlock führen könnte. Daher prüft das (Betriebs-)System periodisch in geeigneten Abständen, ob ein Deadlock eingetreten ist und stellt dann die Situation wieder her, die vorher noch ohne Verklemmung vorhanden war. Man erkennt das besonders gravierende Zusatzproblem: Oft ist das Wiederherstellen (roll back, Rückgängigmachen einer Transaktion) gar nicht möglich.

Sehen wir uns einige der Verfahren etwas im Detail an, wobei wir immer unterstellen, dass Mutual Exclusion einzuhalten ist. Ansonsten stellt sich das Thema gar nicht.

H.3.3.2 Deadlock Vorbeugung

H.3.3.2.1 Hold and Wait sei nicht erlaubt

Das heißt nichts anderes, als dass im angedachten Protokoll diese notwendige Coffman´sche Bedingung nicht hält und somit kein Deadlock eintreten kann.

Die einfachste Methode dazu wurde schon erwähnt: die statische Betriebsmittelvergabe. Hier stellt sich die Frage gar nicht: Gibt man einem Prozess vor Beginn alle die von ihm (möglicherweise) benötigten Betriebsmittel, startet ihn erst dann, wenn diese Zuteilung wirklich möglich ist und gibt der Prozess nach Beendigung alle von ihm belegten Betriebsmittel ordnungsgemäß zurück, so kann kein Deadlock eintreten.

Die Alternative ist, dass ein Prozess während des Wartens auf ein Betriebsmittel die bereits zugeteilten freigibt, wenn sie von einem anderen Prozess angefordert werden sollten. Auch hier trifft Hold and Wait nicht zu.

Dieser Ansatz ist wohl nur für Betriebsmittel möglich, deren Zustand zum Zeitpunkt der freiwilligen Freigabe gerettet werden kann, sodass später eine Wiederherstellung (Restauration, Fortfahren beim früheren Zustand) möglich ist.

H.3.3.2.2 No preemption wird aufgehoben

Die Vorgangsweise ist im Prinzip ähnlich der oben genannten Lösung. Der Unterschied besteht darin, dass einem wartenden Prozess die (anderen) Betriebsmittel gewaltsam vom System entzogen werden.

Auch hier gilt, dass dies nur bei Betriebsmitteln möglich ist, deren Zustand wieder und vor allem auch leicht restaurierbar ist.

H.3.3.2.3 Verhindere zirkuläres Warten

Dieses Verfahren lässt einerseits eine dynamische Betriebsmittelvergabe (in gewissem Ausmaß) zu, anderseits aber gibt es doch markante Beschränkungen:

- Man fasst „gleichartige“ Betriebsmittel in Klassen Ri (i = 1, …, n) zusammen. Zur Vereinfachung der Sprechweise nehmen wir an, dass jede Klasse Ri genau ein Betriebsmittel enthält.

- Es wird eine Ordnungsrelation darauf definiert: R1 < R2 < … < Ri < … < Rn.

- Dann kommt folgendes Protokoll (Strategie) zur Anwendung:

- Benötigt ein Prozess ein Betriebsmittel aus der Klasse Ri, so muss er zugleich alle anderen Betriebsmittel anfordern, die er aus dieser Klasse allenfalls anfordern wird. (Wir haben allerdings angenommen, dass jede Klasse ohnedies nur 1 Betriebsmittel enthält.)

- Hat ein Prozess bereits die Betriebsmittel aus der Klasse Ri erhalten, so darf er in Hinkunft nur mehr Klassen (Betriebsmittel) anfordern, die im Sinne der Relation „<“ größer sind.

Man beachte: Der triviale Fall, dass es überhaupt nur eine einzige Klasse R_1 gibt – also alle Betriebsmittel in eine einzige Klasse gestellt sind –, entspricht demnach der statischen Betriebsmittelvergabe.

Das Protokoll verhindert „Circular Wait“, denn sobald Rn angefordert ist, kann sich nicht mehr der Kreis mit einer Anforderung nach R_1 schließen und damit ist Deadlockfreiheit garantiert: Es ist (der Beweis ist indirekt zu führen) so nicht möglich, dass eine Menge von Prozessen {P_1, P2, …, Pm} existiert, in der Pm auf eine Ressource wartet, die von P_1 belegt ist.

Die Wahl der Ordnungsrelation „<“ sollte möglichst einer natürlichen Folge von Betriebsmittelanforderungen entsprechen: Ein File (Disk) muss zuerst zugeordnet sein, ehe ein Prozess einen Inhalt auf die Ressource Drucker bringen kann.

H.3.3.3 Deadlock Vermeiden

Man bezeichnet einen Zustand als sicher, wenn bei laufenden Prozessen Pi, i=1,…,n, eine Betriebsmittelzuteilung so möglich ist, dass eine Folge von Zuständen (beschrieben durch den Status der Zuteilungen) der Art existiert, dass alle Betriebsmittelanforderungen erfüllt und damit die Prozesse Pi beendet werden können.

Die Idee ist: Man überprüft vor einer neuen Betriebsmittelanforderung, ob diese nicht in einen unsicheren Zustand führen würde. Ist dies der Fall, wird die Betriebsmittelanforderung nicht erfüllt. Ein unsicherer Zustand ist dadurch charakterisiert, dass in der Folge durch weitere Betriebsmittelzuteilungen ein Deadlock entstehen könnte (aber nicht unbedingt eintreffen muss. Deadlocks sind „dynamische“ Fehler, sie können, müssen aber nicht eintreten).

In Abb. H8 ist ein Beispiel gegeben, bei dem deutlich gemacht wird, wie leicht man in einen unsicheren Zustand gelangen kann, obgleich zum Zeitpunkt einer Betriebsmittelanforderung noch „genügend“ Betriebsmittel unbelegt und damit disponibel sind. Im Beispiel laufen 3 Prozesse Pi, i=1,2,3, die im Zuge ihrer Abarbeitung Geräteanforderungen unter mutual exclusion tätigen. Der jeweilige Maximalbedarf jedes Prozesses ist vorab bekannt, wobei insgesamt nur 10 solcher Einheiten verfügbar seien. Die erste Abbildung zeigt, wie ausgehend von einem sicheren Zustand durch Betriebsmittelvergabe ein unsicher Zustand erreicht wird, obgleich noch 2 Geräte verfügbar sind: Es könnten – das muss aber nicht so sein – dann P_1 und P2 jeweils 3 Einheiten anfordern, obgleich im Gerätepool nur mehr 2 vorhanden sind. Dann käme es zu einem Deadlock.

Abb. H8  Sicherer Zustand --> unsicherer Zustand

Wie in Abb. H9 gezeigt, kann das Betriebssystem Anforderungen von Prozess P3 erfüllen, denn der verfügbare Pool reicht aus, um sicherzustellen, dass P3 abgeschlossen werden kann. Mit den damit wiederum verfügbaren 3 Einheiten kann P2 sicher beendet werden. Und mit den damit verfügbaren 7 Geräten lässt sich auch der problemlose Abschluss von P_1 sicherstellen.

Abb. H9  Sicherer Zustand --> sicherer Zustand

Ein Verfahren zur Deadlock-Vermeidung ist von E. Dijkstra unter dem Namen Banker‘s Algorithm beschrieben worden. Es hat allerdings den Nachteil, dass bei m Ressourcen und n Prozessen eine Zeitkomplexität O(m*n2) gegeben ist (bzw. Verbesserungen: O(m*n) und bei jeder Ressourcenzuteilung das Prüfen auf „sicher/unsicher“ durchzuführen ist.

Der Name „Banker´s Algorithm“ leitet sich vom Beispiel ab, das Dijkstra für die Beschreibung seiner Methode gewählt hat. Eine Bank hat n Kunden, denen sie bestimmte Kreditrahmen zugebilligt hat. Die Bank selbst hat in Summe nicht genügend Barreserven für den Fall, dass alle Kunden gleichzeitig ihren Kreditrahmen in Anspruch nehmen wollen. Sie geht von einer dynamischen Ausschöpfung der Beträge aus und nimmt an, dass zwischenzeitlich Kredite getilgt werden.

Die Situation ist demnach analog zum vorigen Beispiel: Anstatt von Geräten werden Kredite vergeben, für jeden Kunden ist anfangs bekannt, wie viel er maximal an Kredit erhalten darf und bei jeder Geldzuteilung an Kunden muss die Bank in einem sicheren Zustand bleiben.

Ob sich das Kreditwesen bei Banken wirklich so abspielt, ist wohl eine andere Frage.

H.3.3.4 Erkennen und Wiederherstellen

Man überprüft periodisch, ob im System ein Deadlock eingetreten ist und führt dann eine Wiederherstellung (recovery) durch.

Damit sind zwei Fragen zu klären:

- wie erkennt man, dass ein Deadlock eingetreten ist?

- wie wird die Wiederherstellung tatsächlich umgesetzt?

H.3.3.4.1 Erkennen

Zum Erkennen bemüht man Algorithmen aus der Graphentheorie. Im System wird ein Ressourcen-Zuordnungsgraph oder (etwas einfacher) ein Warte auf Graph mitgeführt:

Abb. H10  „Warte auf Graph“: Links mit Ressourcen, rechts die vereinfachte Form

In beiden Fällen wird mitgespeichert, welcher Prozess Pi auf welchen Prozess Pj gerade wartet, weil Pj eine Ressource belegt hat, die Pi haben möchte.

Mit Blick auf die Bedingungen von Coffman ((i) – (iii) sind vorausgesetzt) ist ein Deadlock sicher eingetreten, wenn im „Warte auf Graph“ ein Kreis (Zyklus) entdeckt wird entsprechend der Bedingung (iv) circular wait.

Abb. H11  Kreis im „Warte auf Graphen (P_1, P2) und zusätzlich blockierte Prozesse P3 und P4

H.3.3.4.2 Wiederherstellen

Dazu gibt es wieder zwei Vorgangsweisen.

- Entweder man stoppt einen Prozess im entdeckten Kreis und gibt seine Ressourcen frei. Dieser Prozess muss später wieder neu begonnen werden (recovery).

- Oder man entzieht den Prozessen Ressourcen (welche?) solange, bis ebenfalls kein Kreis mehr entdeckt werden kann.

Bei beiden Varianten entstehen nicht leicht in den Griff zu bekommende Probleme:

- Welcher Prozess soll abgebrochen werden? (Kosten, bisherige Laufzeit).

- Welche Ressourcen sollen freigegeben werden (preemption)?

Leider gilt auch, dass das Suchen von Kreisen zeitaufwendig ist: alleine schon die Untersuchung, ob ein gerichteter Graph mit n Knoten kreisfrei (zyklenfrei) ist, erfordert O(n^2) Schritte. Ist er es nicht, müssten noch die Kreise selbst bestimmt werden. So gesehen wäre es besser, bei jeder Resourcenanforderung bereits zu prüfen, ob dadurch ein Kreis entsteht. Dies ist zwar algorithmisch wesentlich einfacher, vergrößert aber den Aufwand bei den Zuteilungen zu Lasten der allgemeinen Systemperformance.

Also stellt sich auch die Frage, in welchen zeitlichen Abständen der Algorithmus „Detection“ aufgerufen werden soll.

I SPEICHERVERWALTUNG

I.1 Allgemeine Problemstellung

Der Hauptspeicher (main memory) gehört zu den wichtigsten Betriebsmitteln bei jedem Computersystem. Daher ist die Verwaltung des Hauptspeichers eine zentrale Aufgabe des Betriebssystems.

Man sagt: „Software verhält sich wie Gas: Gas füllt jeden Raum aus, Software jeden Hauptspeicher“. Damit ist grundsätzlich davon auszugehen, dass der real verfügbare Hauptspeicher eines Computersystems immer knapp ist. Daraus abgeleitet ist eine Aufgabe der Speicherverwaltung vorgegeben: die Verwaltung einer knappen Ressource.

Wie bereits im Kapitel ‎B Hardware Mechanismen gezeigt, betrachtet man den Hauptspeicher oft als ein Array von Wörtern oder Bytes, jedes mit einer eigenen Adresse entsprechend der Position im Array (m[0], … m[1024], m[1025], …).

Der (Maschinen-) Code eines Programms muss vor seiner Ausführung (wenigstens teilweise: siehe Virtueller Speicher) in den Hauptspeicher geladen werden. Bei Multiprogramming kann man davon ausgehen, dass neben dem Betriebssystem bzw. seinem Kernel mehrere Programme gleichzeitig im Hauptspeicher geladen sind. Alle diese Codestücke und Datenbereiche müssen sauber voneinander getrennt werden. Ein unbeabsichtigtes Überschreiben z.B. eines Programmcodes P_1 durch einen laufenden Prozess P2hätte fatale Folgen. Also geht es bei Hauptspeicherverwaltung auch um Schutzmechanismen, um die angedeuteten Konflikte zu vermeiden.

Neben dem Hauptspeicher ist in einem System ein Massenspeicher (Hintergrundspeicher, secondary storage) erforderlich, der für das persistente, also nicht-flüchtige Speichern von Benutzerdaten und auch der Programme dient. Eine spezielle Verwendung von Massenspeichern im Zusammenhang mit Betriebssystemen besprechen wir im Kapitel über Virtuelle Speicher.

Die Verwaltung und die Organisation von Massenspeichern wie Harddisks, Flash-Memories usw. sind verschieden von der Art, wie man Hauptspeicher organisiert und dessen Inhalte anspricht (adressiert). Die Ziele aber sind ähnlich: es geht um ein ökonomisches und effizientes Verwalten von Daten.

I.2 Adressbindung

Ein Programm sei bereits auf einem Massenspeicher als Binary Executable File gespeichert. Das Programm wurde also bereits kompiliert (übersetzt) und liegt in ausführbarem Maschinencode vor. Zur Ausführung (program execution) muss es in den Hauptspeicher gebracht werden, woraus dann ein Prozess entsteht.

Man muss davon ausgehen, dass ein solches Binary Executable nicht unbedingt jedes Mal vor einem Programmstart in denselben Hauptspeicherbereich geladen wird. Es kann sogar sein, dass Prozesse (bzw. deren Code) während ihrer Laufzeit im Hauptspeicher verschoben oder zwischen externem Speicher und Hauptspeicher transferiert (ausgelagert, eingelagert) werden. Die meisten Systeme erlauben heute, dass Prozesse in jedem Bereich des physischen Hauptspeichers ablaufen können, abgesehen von besonders geschützten Bereichen für Systemprozesse.

Daher muss die erste Instruktion eines Prozesses nicht unbedingt auf Adresse 0 liegen, obgleich wir später oft symbolisch annehmen, dass die erste ausführbare Instruktion von der Adresse 0 aus geladen wird.

In diesem Kapitel über die Speicherverwaltung werden wir sehen, dass bei modernem Speichermanagement nicht einmal die Notwendigkeit besteht, den einem Programm zugeordneten Speicherbereich kontinuierlich (d.h.: zusammenhängend) zu halten. Damit ist gemeint, dass der Code eines Programms im Hauptspeicher durchaus in Speicherblöcke unterteilt in verschiedenen nicht unmittelbar aufeinander folgenden Speicherbereichen liegen kann.

Diese Sicht erfordert die Unterscheidung zwischen den symbolischen Adressen des Binary Executable und den realen Adressen, denen sie zugeordnet werden:

Damit wird eine Abbildung von den symbolischen Adressen des Quellprogramms auf die tatsächlichen physikalischen Adressen im Speicher benötigt. Diese Abbildung nennt man Adresszuweisung oder auch Adressbindung (address binding).

Interessant ist die Frage, wann eine solche Zuweisung durch das Betriebssystem – durchaus auch mit Hardwareunterstützung – erfolgen kann. Der Zeitpunkt der Adressbindung stellt ein grundsätzliches Unterscheidungskriterium dar.

Die Zuweisung kann erfolgen zur

- Übersetzungszeit (compile time);

- Ladezeit (load time);

- Laufzeit (execution time, run time).

I.2.1 Absolute Adressen

Eine fixe (absolute) Adresszuweisung findet man heute nur mehr bei kleinen Mikroprozessor-Systemen: Programme für Mikrocontroller (z.B. für eine Ampel- oder Liftsteuerung) werden teilweise mit absoluten Adressen programmiert. Dabei wird a priori festgelegt, wo eine Instruktion gespeichert wird und wo im Hauptspeicher (an welcher Adresse) die Operanden einer Instruktion liegen.

Diese Art der Adresszuweisung ist typisch für das Programmieren im Maschinencode. Die Zuweisung erfolgt also zur „Programmier-Zeit“.

Das folgende Codebeispiel zeigt einen bekannten Programmausschnitt (vgl. Kapitel B), in dem das gesetzte Bit 0 von Port 0 anzeigt, dass Daten auf Port 1 bereitstehen. Das Programm wartet also so lange, bis Daten auf Port 1 zur Verfügung stehen und speichert den Datenwert auf die Hauptspeicherstelle 168 ab.

Code I1   Programmausschnitt – Speicherung von Daten vom Port 1 in Hauptspeicher

Abweichend zur früheren abstrahierten Sicht sind die Befehle nun verschieden lang, und man sieht im – an den Prozessor Intel 8080 angelehnten – Hexcode des Programms die Portnummern 0 und 1 sowie die Sprungadresse 110 und die Hauptspeicheradresse 168, an welcher die Eingabedaten gespeichert werden. (Bemerkung: „Hexcode“ ist die jargonmäßige Bezeichnung für „Maschinencode, byteweise als Zahlen im Hexadezimalsystem angeschrieben“.)

Ein Compiler oder Assembler kann absolute Adressen erzeugen. Dies geschieht normalerweise durch eine Option (Setzen eines logischen Schalters, switch) zur Übersetzungs- bzw. Assemblierungszeit und durch das Festlegen eines Ladepunktes.

Daher kann man Assemblercode oder sogar Hochsprachen zur Erstellung von ausführbaren Programmen mit absoluten Adressen z.B. für Mikrocontrollersysteme verwenden.

In diesem Falle spricht man von Adressbindung zur Übersetzungszeit: Die Anordnung (Layout) des Codes im Speicher muss zur Übersetzungszeit schon bekannt sein, vor allem ist die Startadresse des Programms im Vorhinein fixiert.

Zum Beispiel haben CP/M-Programme (8-bit Betriebssystem, Mitte 1970er Jahre) aber auch (ältere) Programme (mit der Datei-Extension *.com, von CP/M portiert) für das 16-Bit Betriebssystem DOS ihren Ladepunkt immer auf der Adresse 100H (= 256 dezimal), wobei Ausnahmen diese Regel bestätigen.

Man beachte jedoch, dass Übersetzer und Assembler normalerweise Code mit relativen Adressen erzeugen.

I.2.2 Relative Adressen

Relative Adressen beziehen sich auf einen vorgegebenen angenommenen, aber nicht endgültigen Ladepunkt. Die Entfernung zu diesem Ladepunkt heißt Offset.

Durch relative Adressen kann man daher den Zeitpunkt der Adressbindung solange verzögern, bis die absolute Adresse des Ladepunktes (starting point) definitiv bekannt ist. Dann werden die vom Ladepunkt abhängigen Adressen entsprechend angepasst.

Eine solche Situation findet man bei älteren bzw. kleinen Systemen vor, wenn z.B. das Betriebssystem in seinem Umfang variabel und damit der Programmladepunkt nicht fixiert ist.

Zunächst entsteht das Programm mit symbolischen Adressen, z.B. wie in folgender Codesequenz dargestellt:

Code I2   Programm mit symbolischen Adressen

Zum obigen Programm sei bemerkt: das Programm (mit Sprungziel „label“, der Sprungbefehl wird im Beispiel maschinennahe durch Setzen des Programmzählers pc notiert und nicht z.B. mittels „goto label“) und die Daten sind hier mit symbolischen Adressen versehen, die Ein-/Ausgabeports mit den Nummern 0 und 1 bleiben jedoch konstant. Dies hat einen realen Hintergrund: Die Adressen von Ports hängen vom Hardwarelayout ab und bleiben damit fix.

Bei der Assemblierung oder Kompilation werden diese symbolischen Adressen auf fiktive reale Adressen so umgesetzt, dass man von einem definierten Ursprung, typischerweise Adresse 0 ausgeht. Mit „r“ wollen wir symbolisieren, dass z.B. 0000r noch keine absolute Adresse ist.

Code I3   Programm mit fiktiven realen Adressen

I.3 (Programm-)Lader

Um ein Programm in den Hauptspeicher zu laden und dort zur Ausführung zu bringen, bedarf es der Betriebssystemunterstützung mittels eines Ladeprogramms (Loader). Beim Laden zur Programmausführung erfolgt dann die definitive Adressbindung, indem der Loader vom Betriebssystem den wahren Startwert, also die Adresse der ersten ausführbaren Instruktion, mitgeteilt bekommt. Damit kann der Loader diesen Startwert (heißt auch: Basisadresse) zu den jeweiligen fiktiven relativen Adressen hinzuzählen und dann diesen Code ab der fixierten Basisadresse laden und zur Ausführung bringen.

Wird das angegebene Programmbeispiel ab Adresse 100 im Speicher abgelegt, so übernimmt der Loader die Aufgabe, zu jeder relativen Adresse den Offset 100 zu addieren. Wenn die Leserin / der Leser für das angegebene Programmstück selber Loader „spielen“ möchte, so sollte das Ergebnis genau so aussehen wie das Codestück im  Punkt I.2.1 „Absolute Adressen“.

Compiler und Assembler erzeugen meist verschiebbaren Code (relocateable code), und erst zur Ladezeit werden die relativen Adressen vom Loader in absolute Adressen umgewandelt. Daher spricht man hier von dynamischem Laden oder load time binding. Der Programmlader kann als Komponente des Betriebssystems oder als ein dieses unterstützendes Systemprogramm angesehen werden. Um die Aufgabe der Relokierung zu unterstreichen, findet man auch die Bezeichnung Relokationslader (relocation loader), vor allem beim Zusammenspiel mit dem Linker (s.u.).

Ein Programm ist auf der Festplatte als Executable vorhanden (*.exe File, also ausführbar, *.com Files sind historisch obsolet). Ziel ist, das Programm in den Hauptspeicher zu laden und auszuführen.

Aus den vorigen Erläuterungen können wir wie folgt zusammenfassen:

- bei Adresszuweisung zur Übersetzungszeit:

Der Lader kümmert sich nicht um Adressen, er lädt einfach das Programm-Image 1:1 ab einer fixen Speicheradresse (Ladepunkt, Basisadresse).

- bei Adresszuweisung zur Ladezeit:

Der Lader bildet die relativen Adressen auf absolute ab (Adressbindung)

I.3.1 Lader und Adressbindung

Wir betrachten den Weg vom Quellcode bis zur Ausführung nun schematisch. Zunächst treffen wir dabei vereinfachende Annahmen. Etwa gebe es nur ein Modul, keine Systembibliothek, etc. Diese Vereinfachung bedeutet, dass nur ein einziges „Hauptprogramm“ vorliegt und dieses nicht von anderen getrennt übersetzten Modulen bzw. Programmen aus einer Bibliothek Gebrauch macht.

Diese zusätzlichen Optionen sind in der folgenden Abbildung daher hell unterlegt.

Abb. I1   Übersetzen und anschließendes Laden eines Moduls

Der Lader lädt das Programm (üblicherweise von der Festplatte) in den Hauptspeicher und erledigt das Anpassen der Adressen. Im Falle von absoluten Adressen entfällt die zweite Aufgabe.

Dann veranlasst er das Starten des Programms. Dies kann mit weiterem Aufwand begleitet sein:

- der Lader weist die Adresse der ersten Anweisung dem Programmzähler (Instruktionszähler) zu, sodass nach dem Prinzip des Von-Neumann-Zyklus unmittelbar die Programmausführung beginnt,

oder

- der Lader führt vorab noch Initialisierungen und sonstige Verwaltungsaufgaben aus und setzt dann wie vorher fort.

Ein einfaches Beispiel für letzteres ist, dem Betriebssystem bzw. speziell von diesem festgelegten Registern mitzuteilen, von wo bis wohin im Hauptspeicher sich das eben geladene Programm befindet. Diese Information kann das Betriebssystem später für Speicherschutz-Mechanismen ausnützen.

I.4 Linker

Die deutsche Bezeichnung für Linker (Kurzform für: Linkage Editor) ist Binder. Sie hat sich mit Ausnahme des Zeitwortes „binden“ als terminus technicus nicht so durchgesetzt und kann auch zu Missverständnissen führen: Obgleich der Linker ebenfalls Adressbindungen durchführt, meint man mit „binden“ in diesem Zusammenhang primär das Zusammenführen von kompilierten oder assemblierten Modulen in ein Modul. Wir verwenden „Binden“ und „Linken“ synonym.

I.4.1 Aufgabe

Üblicherweise besteht ein Programm aus mehr als einem Objektmodul. Jedes solche *.obj Modul ist das Ergebnis einer getrennten Übersetzung des Quellcodes. Alle diese Objektmodule sind mit relativen Adressen – bezogen auf den Ursprung 0000H – zu denken. Ein Modul davon ist als Hauptmodul ausgezeichnet. Eine typische Situation ist, dass ein Hauptmodul programmiert wird und dieses von zugekauften oder selbst erstellten Programmbibliotheken Gebrauch macht.

Der Linker als spezielle System-Software bzw. Komponente des Betriebssystems kombiniert alle diese Module zu einem einzigen Modul, welches geladen und ausgeführt werden kann.

Die Adressen werden daher relativ zur Startadresse des Hauptmoduls gesetzt. Man stelle sich vor, dass jedes dieser Module eine in sich abgeschlossene Prozedur (Unterprogramm) ist. Dann wird es Prozeduraufrufe zwischen den einzelnen Modulen, also über Modulgrenzen hinweg, geben und es müssen die Adressen (Startadressen der Unterprogramme) relativ zum Ursprung des Hauptmoduls adaptiert werden. Damit ist die Situation auf „nur ein Modul“ zurückgeführt.

Abb. I2    Linken mehrerer Object-Module und anschließendes Laden

Der Output des Linkers ist ein Lademodul. Dieses enthält dann entweder:

- nur mehr absolute Adressen, das Modul kann daher vom Lader einfach 1:1 geladen werden, oder

- alle Adressen sind relativ zum Ladepunkt des Lademoduls. Das Lademodul wird dann von einem Relokationslader (relocation loader) geladen, der die relativen Adressen auf absolute Adressen umsetzt.

I.4.2 Statisches und dynamisches Linken

I.4.2.1 Prinzip

Wir betrachten zwei grundlegende Techniken:

- Statisches Linken (static linking)

Dabei werden die System-Libraries genau wie jedes andere Objektmodul behandelt, also vom Lader zum endgültigen Lademodul hinzugefügt. Dieser Ansatz ist immer noch in Verwendung bei kleinen Systemen, die z.B. ohne Multiprogramming arbeiten, gilt aber bereits als veraltet.

- Dynamisches Linken (dynamic linking)

Dabei wird ein ähnliches Konzept verfolgt wie beim dynamischen Laden, das Linken wird also zeitlich hinausgeschoben. Entweder erfolgt dieses Linken zur Ladezeit oder sonst zur Laufzeit.

I.4.2.2 Nachteil von statischem Linken

Der herausragende Nachteil von statischem Linken ist, dass mehrfach verwendete *.obj-Module auch entsprechend oft im Hauptspeicher stehen.

Abb. I3  Statisches Linken: Module l1 und l2 befinden sich mehrfach im Speicher

Damit wird unnötig Speicher durch redundanten Code belegt. Man denke dabei an (fast alle) Programme, die für I/O Bibliotheken benötigen. In jedem Einzelfall wird bei statischem Linken das entsprechende Library Modul in das erzeugte Lademodul eingebunden.

Ein besonderer zusätzlicher Nachteil ist folgender: wenn durch ein Update ein Modul einer Bibliothek verändert wurde, dann müssen – wenn man davon profitieren möchte – alle (!) Lademodule, in denen dieses Modul eingebunden war, neuerlich einem Link-Vorgang unterzogen werden.

I.4.2.3 Vorteil von dynamischem Linken

Module, die bei Bedarf angefordert werden können, werden in Bibliotheken, den Dynamic Link Libraries (DLL), zusammengefasst. Falls ein DLL-Modul zur Link- bzw. Laufzeit benötigt wird, prüft man, ob es sich nicht schon im Hauptspeicher befindet, denn die DLL könnte von einem anderen Prozess bereits geladen worden sein. Der Linkage Editor bindet nur kleine Tabellen (Verweise) und zugehörigen Code (Stubs) ein, mit denen man die DLL bzw. die benötigte Komponente davon finden kann.

Zum Laden eines Bibliotheks-Moduls wird ein Stub verwendet. Ein Stub ist ein kleines Stück Code, welches dem Verweis auf ein Bibliotheks-Programm (Modul) hinzugefügt wird. Der Stub wird benutzt, um ein referenziertes, speicherresidentes Bibliotheks-Modul zu lokalisieren und einzubinden. Falls es notwendig ist, wird es dann dynamisch nachgeladen.

Diese Methode ist vor allem wichtig für das Betriebssystem und seine eigenen Bibliotheken.

Abb. I4  Dynamisches Linken: d1 und d2 werden parallel für c1 und c2 verwendet

Mehrfach verwendete Bibliotheksmodule stehen bei diesem Konzept nur einmal im Speicher.

Wenn eine von einem Programm geladene DLL auch von einem anderen Programm (Prozess) referiert wird, dann wird ein Referenzzähler für die betreffende DLL erhöht bzw. nach Freigabe der DLL (z.B. zum Prozessende) erniedrigt. Über diesen Referenzzähler kann das Betriebssystem herausfinden, ob eine im Speicher stehende DLL noch benötigt wird oder der ihr zugewiesene Speicherbereich freigegeben werden kann.

Dieser Zähler ist durchaus sehr heikel: wenn in Folge eines Fehlers (Programm- „Absturz“ o.Ä.) der Zähler nicht dekrementiert wird, verbleibt die DLL im Speicher. Bestenfalls werden (bei Paging Systemen; dazu später) ihre zugeordneten Frames ausgelagert.

Ein besonderes Problem bei DLL tritt auf, wenn unterschiedliche Programme verschiedene Versionen einer DLL benötigen. Wenn ein Programm bei seiner Installation z.B. eine neuere DLL vorsieht und die alte Version davon überschreibt, dann kann es zu Inkompatibilitäten bei anderen Programmen kommen, die noch eine ältere DLL-Version benötigen. Dieses (in der Windows Welt) auftretende Problem wird im Jargon als DLL-Hell (Hölle) bezeichnet.

I.5 Logische und physische Adressen

Bei der Besprechung der Funktionsweise eines Laders haben wir diesem auch die gesamte Arbeit der Adressumsetzung, z.B. das Addieren einer Basisadresse aufgehalst. Mit Hardwareunterstützung durch eine Memory Management Unit (MMU) wird die Aufgabe wesentlich vereinfacht und erlaubt auch, konzeptionell zwei verschiedene Sichten auf Adressen einzuführen: logische und physische.

Der Ansatz ist dabei wie folgt:

Bei einer Adressgröße von n Bits stehen prinzipiell 2n Adressen zur Verfügung und sie bilden den logischen Adressraum. Ferner – zur Vereinfachung – nehmen wir an, dass alle Programme nach dem Linken mit der Startadresse 0 versehen sind. Ein Programmcode umfasst damit eine zusammenhängende Folge von logischen Adressen. Diese sind in den Hauptspeicher ab einer bestimmten vom Betriebssystem vorgegebenen tatsächlichen Adresse (dem Ladepunkt) zu laden.

Der Hauptspeicher stellt den realen physischen Adressraum dar. Dieser hängt von der Speichergröße ab (2m mit m ≤ n; sofern keine besonderen Adressumsetzungstechniken verwendet werden).

Wenn nun bei der Programmausführung bei jedem Speicherzugriff mit Hilfe einer MMU der Inhalt eines dort vorgesehenen Relokationsregisters dazuaddiert wird, hat man denselben Effekt, als würde man vorab beim Laden die Adressen adaptieren.

Dies ist ein einfacher Fall von runtime binding: Die Bindung einer logischen Adresse an eine reale Adresse erfolgt zur Laufzeit.

In Abb. I5 wurde ein einfacher Fall mit nur einem Relokationsregister angeführt. Die generelle Aufgabe einer MMU kann wesentlich komplexer sein, wenn z.B. verschiedene Speicherbänke zu adressieren sind oder wenn der Hauptspeicher selbst in Speicherhierarchien mit unterschiedlicher Performanz (vgl.: Cache-Speicher) organisiert ist. Sich darum zu kümmern, sollte ja nicht primäre Aufgabe eines Programms sein.

Abb. I5    Basisfunktion einer Memory-Management Unit (MMU)

Eine MMU (Memory Management Unit) ist dabei oft auf dem CPU-Chip integriert bzw. selbst ein Teil der CPU.

Abb. I6   Beispiel MMU (Basisadressregister)

Dieser Ansatz mit MMU eröffnet eine Reihe von weiteren Möglichkeiten. Zunächst kann man der MMU ein weiteres Register zuordnen, welche das Ende jenes Hauptspeicherbereiches markiert, in den ein Programm geladen wird. Beide Register zusammen wirken wie ein „Zaun“ (daher auch: Fence Register), in dem sich jede Adressierung des Programms bewegen muss: ein Zugriff nach außen kann von der MMU als Fehler erkannt und unterbunden werden.

Mit Blick auf Multiprogramming unterstellen wir, dass gleichzeitig mehrere Programme im Hauptspeicher stehen. Schaltet das Betriebssystem von P_1 auf P2 (Kontextswitch) um, dann muss unter anderem der Inhalt des Relokationsregisters und des Fence Registers durch die für P2 vorgesehenen Werte ersetzt werden.

Und schließlich kommt ein ganz besonderer Vorteil hinzu: Soll aus welchen Gründen auch immer während der Ausführung von Pii im Hauptspeicher von der Startadresse „a“ auf die Startadresse „b“ verschoben werden, so kann dies unter Neusetzen der genannten Register problemlos erfolgen. Bei einem Loadtime-Binding ohne MMU wäre das nicht so einfach möglich.

I.6 Methoden für die Speicherverwaltung

Wir beginnen mit einfachen Ansätzen, wie ein Betriebssystem die Zuteilung von Hauptspeicher an Programme bzw. ablaufende Prozesse bewerkstelligen kann. Teilweise sind diese historisch bedingt, weil früher nicht nur der physische Hauptspeicher sehr klein (Größenordnung: KBytes und nicht GBytes!) war, sondern auch der logische Adressraum z.B. bei Mikroprozessorsystemen mit 16-Bit Wortbreite relativ beschränkt ist. Die hier skizzierten Ansätze sind daher primär als historischer Rückblick zu sehen. Heute trennt man strikt zwischen logischem und physischem Adressraum und verwendet Paging Methoden, wie dies im zugehörigen Abschnitt beschrieben wird.

I.6.1 Kontinuierlicher Speicherbereich

Dabei wird einem Prozess ein durchgehender Speicherbereich zugewiesen (Contiguous Memory Allocation).

Ein einfacher Ansatz ist: Teile den Speicher in zwei Partitionen,

- eine für das Betriebssystem und

- eine für den Benutzerprozess.

Abb. I7  Einfachste Speicheraufteilung: Betriebssystem / Benutzerprozess(e) / frei

Natürlich ist im Allgemeinen mehr als ein Benutzerprozess vorhanden, sodass die beschriebene Partitionierung verallgemeinert werden kann, sofern dies bei gegebener Hauptspeichergröße auch sinnvoll ist. Jedenfalls wird damit der erste Schritt zu Multiprogramming geleistet:

Man teilt den Speicher in n > 2 Partitionen, jeweils mit fester, nicht notwendigerweise gleicher Größe auf: eine für das Betriebssystem und n - 1 für die Benutzerprozesse Pi.

Abb. I8    Speicheraufteilung bei fixen Partitionen

Damit bekommt man allerdings einige Zusatzprobleme: Programme Pi sind verschieden groß und eine Partition könnte zu groß sein. Dann bleibt der übrige Speicher ungenützt. Oder eine Partition ist zu klein angelegt. Dieser Fall ist besonders unangenehm, denn ein Prozess Pi kann nicht ausgeführt werden, obwohl z.B. zwei benachbarte Partitionen zusammen genügend Platz dafür hätten und beide Partitionen noch dazu unbelegt sind.

Das folgende Beispiel, in dessen Ablauf verschiedene Prozesse gestartet und auch beendet werden, zeigt das Problem. Von den verfügbaren 15 MB Speicher belege das Betriebssystem (BS) 3 MB, sodass für die Prozesse noch 12 MB verbleiben, die in zwei Partitionen zu je 4 MB (x4) und 2 Partitionen zu je 2 MB (x2) aufgeteilt sind.

Der Speicherbedarf der Prozesse P1 bis P5 sei wie in der folgenden Tabelle beschrieben und ist zusätzlich tief gestellt zum jeweiligen Prozessnamen notiert.

Tabelle I1   Beispiel für externe Fragmentierung – Speicherbedarf der Prozesse

Abb. I9  Das Problem mit fixen Partitionen

Die obige Abbildung zeigt die Speicherallokation nach dem Start von P13, P22, P32 und später P41. Dieser Prozess, obgleich sein Speicherbedarf gering ist, muss in eine große Partition, da sonst kein Platz mehr verfügbar ist. Darauf terminieren P22 und P32, die Prozesse P13 und P41 laufen noch weiter. Will nun Prozess P53 starten, so sind zwar gesamt 8 MB frei und davon wären sogar 7 MB zusammenhängend, trotzdem findet P53 wegen der fixen Partitionierung keinen Platz.

Daher will man von einer vorab vorgegebenen fixen Partitionierung absehen und sich dynamisch dem aktuellen Speicherbedarf der gerade aktiven Prozesse anpassen. Dazu gibt es verschiedenste Ansätze, von denen wir einige vorstellen, ohne auf ihre algorithmische Umsetzung einzugehen.

I.6.2 Dynamische Speicherverwaltung

Eine Verwaltung der freien Blöcke kann z.B. so ausgelegt werden, dass sie doppelt verkettet werden. Zum einfacheren Verschmelzen zu größeren Blöcken sind freie (f) und belegte (b) Blöcke am Anfang und am Ende markiert. Jeder Block enthält weiters einen Eintrag über seine aktuelle Länge (len).

Abb. I0  Doppelt verkettete Liste zur Verwaltung der freien Blöcke

Ausgehend von dieser Datenstruktur kann man insbesondere folgende Verfahren als Politik für eine Blockauswahl verwenden. In allen Fällen können Reststücke (Fragmente) auftreten und es soll offen bleiben, ob sie abgetrennt und als freie Blöcke mit Verwaltungsdaten versehen in die Liste einzuhängen sind, oder ob man diesen Verschnitt in Kauf nimmt. In periodischen Abständen oder beim Einhängen eines frei gewordenen Blockes kann geprüft werden, ob unmittelbare freie Nachbarn vorhanden sind und diese verschmolzen werden können. Solche Lösungsdetails werden hier nicht weiter besprochen.

- First Fit

Der erste passende Block wird genommen. Man kümmert sich nicht weiter darum, ob dieser Block zu groß ist und es andere besser passende Kandidaten gibt.

- Worst Fit

Der größte Block wird genommen. Die Philosophie zu diesem Vorgehen ist: Das übrig gebliebene Fragment ist hoffentlich so groß, dass es später für eine andere Anforderung verwendet werden kann.

- Buddy Methode

Die vorigen Methoden haben einen besonderen Nachteil: es ist nicht einfach zu erkennen, ob in der Liste zwei kleinere Blöcke benachbart sind und daher im Interesse des globalen Zieles zu einem größeren verschmolzen werden könnten. In der Buddy Methode (buddy = Kamerad, Kumpel) werden Blöcke der Größen 2x (16, 32, 64, 128, ...) verwendet: Dies vereinfacht algorithmisch das Vereinen von zwei Nachbarn. Allerdings ist man in der Wahl der Blockgrößen nicht mehr so frei.

Obige Methoden findet man bei heutigen Systemen weniger, um den Realspeicher zu verwalten, sondern um Teile davon für spezielle Zwecke zu organisieren. Beispielsweise wird einem laufenden Prozess innerhalb des ihm zugeordneten Speicherbereichs ein als Heap bezeichneter Bereich zugeteilt. In diesem werden Speicherblöcke verwaltet, die von dynamisch erzeugten Objekten benötigt und später wieder freigegeben werden. (Siehe dazu auch: Garbage Collection im nächsten Abschnitt.)

I.6.3 Speicherfragmentierung

Beim Laden und Entfernen von Prozessen aus dem Speicher wird der freie Speicher in verschieden große Stücke zerteilt, die dann nicht alle genutzt werden können. Dies nennt man Speicherfragmentierung. Man unterscheidet zwischen

- interner Fragmentierung und

- externer Fragmentierung.

I.6.3.1 Interne Fragmentierung

Ein Prozess P braucht einen Speicherplatz der Größe p und erhält einen Block der Größe b > p. Die Differenz (b – p) geht verloren. Die Fragmentierung ist intern zum zugeteilten Block.

Abb. I11  Interne Fragmentierung

Wenn (b – p) sehr klein ist, dann zahlt sich der Aufwand für die Einbindung in die Liste des freien Speichers nicht aus.

Abb. I12  Speicherlayout bei interner Fragmentierung

Obgleich die Speicherblöcke der Prozesse P1, P2 und P3 dicht aneinander liegen, geht trotzdem Platz verloren, da z.B. das Betriebssystem Speicherblöcke nur in Vielfachen von 4 KB verwalten kann. Damit sind im Schnitt ca. 2 KB je Prozess unbelegt.

I.6.3.2 Externe Fragmentierung

Wir erläutern die externe Fragmentierung an einem Beispiel, in dem im Ablauf der Zeit verschiedene Prozesse gestartet und auch beendet werden.

Das Beispiel ist ähnlich gelagert wie jenes, bei dem die Nachteile einer festen Partitionierung besprochen worden sind. Von den verfügbaren 15 MB Speicher belege das Betriebssystem (BS) 3 MB, sodass für die Prozesse noch 12 MB verbleiben. Der Speicherbedarf der Prozesse P1 bis P5 sei wie in der folgenden Tabelle beschrieben und wird zusätzlich tiefgestellt zum jeweiligen Prozessnamen geschrieben.

Tabelle I2   Beispiel für externe Fragmentierung – Speicherbedarf der Prozesse

Abb. I13 zeigt dann die Speicherallokation nach dem Start von P1_3, P22, P33 und P42. Darauf terminieren P13 und P33, die Prozesse P22 und P42 laufen weiter. Will nun Prozess P54 starten, so sind zwar gesamt 8 MB frei, trotzdem findet P54 keinen Platz.

Abb. I13  Beispiel für externe Fragmentierung - Speicherlayout

Die Fragmentierung liegt zwischen den Blöcken, also extern zu jedem Block. Obgleich in Summe noch genügend Platz für ein Programm Pij im Speicher wäre, kann durch eine externe Fragmentierung das Programm nicht mehr geladen (und damit nicht mehr gestartet) werden („Schweizer Käse“).

Eine Lösung des Problems besteht in einer Verdichtungsstrategie (compacting strategy).

I.6.3.3 Maßnahmen gegen die externe Fragmentierung

Verdichtung

Ziel dieses Ansatzes ist, die Speicherblöcke so anzuordnen, dass freie Blöcke nebeneinander liegen und zu größeren kombiniert werden können. Dies erfordert aber eine Adressbindung zur Laufzeit und damit Hardware-Unterstützung durch eine Memory Management Unit (MMU), wie wir dies vorhin schon besprochen haben.

Zu beachten ist aber auch, dass eine Neuanordnung der Blöcke einen Kopieraufwand impliziert. Somit benötigt man eine Strategie, um den Aufwand dafür zu minimieren.

In der nachstehenden Abbildung sind zwei verschiedene Verdichtungen gezeigt und man erkennt, wie unterschiedlich der Aufwand für das Verschieben sein kann.

Abb. I14  Beispiel für Verdichtung

Garbage Collection

Garbage Collection Algorithmen sammeln generell jene Teile des Speichers, die nicht mehr von einem Programm gebraucht werden und organisieren sie für neuerliche Verwendung.

So gesehen ist Garbage Collection ein Überbegriff für bestimmte Methoden der Speicherverwaltung. Sie findet aber weniger bei der Speicherverwaltung in Betriebssystemen Verwendung, sondern bei der Implementierung von Programmiersprachen und deren Laufzeitsystemen. Wenn die Zuweisung von (dynamischem) Speicher für Variablen, Datenstrukturen und allgemein für Objekte vorgesehen ist und später automatisch wieder freigegeben werden soll, dann ist dafür der Garbage Collector zuständig.

Solche Verfahren sind jedoch relativ zeitaufwendig und können bei Echtzeitsystemen Probleme bereiten, wenn zu einem unpassenden Zeitpunkt der Garbage Collector gestartet wird und währenddessen das System blockiert ist.

I.6.4 Seitenverwaltung

I.6.4.1 Virtueller Adressraum

Wir rekapitulieren die besprochene Unterscheidung zwischen physischem Adressraum und logischem Adressraum. Letzterer wird auch virtueller Adressraum genannt.

Nun kommt eine neue Variante ins Spiel, wie logische Adressen interpretiert werden können.

Zunächst wird der physische Speicher in Blöcke mit einer fixen Größe (2n) unterteilt. Diese Blöcke heißen Seitenrahmen (frame).

Der logische Speicherraum (Virtueller Adressraum), wird nun in Blöcke derselben Größe (u =2n) zerlegt. Man nennt diese Blöcke Seiten (page).

Für die Begriffe Seite und Seitenrahmen werden als Fachbegriffe auch Page und Frame eingedeutscht verwendet. Hinzuzufügen ist noch, dass im Jargon sprachlich oft nicht mehr zwischen Seitenrahmen und Seiten unterschieden wird und man generell von Seiten bzw. Pages spricht

Dass Frames und Pages dieselbe Größe haben ist wichtig. Damit kann man genau eine Seite in einen Frame bringen und eine externe Fragmentierung fällt dadurch weg. Eine interne Fragmentierung ist aber möglich, falls eine Seite nicht komplett mit Code oder Daten ausgefüllt ist.

Abb. I15  Struktur einer logischen Adresse bei Paging

Ein Page Offset (0 ≤ Offset < 2n) beschreibt die Position innerhalb jeder Seite bzw. eines Seitenrahmens in Bezug auf die Basisadresse der Seite.

Jede Seite hat eine Nummer, die Seitennummer. Somit ist die Interpretation einer virtuellen Adresse wie folgt: Sie besteht aus einer Seitennummer und einem Offset. Das Offset bestimmt die Position innerhalb der Seite relativ zum Seitenanfang.

Offen bleibt die Frage nach der Adressbindung. Da bei einer Einbettung einer Page in einen Frame das Offset gleich bleibt, bezieht sich die Adressbindung auf eine Zuordnung von Seiten zu Frames.

I.6.4.2 Zuordnung über Seitentabelle

Die Zuordnung des logischen Adressraums zum physischen und damit die Adressbindung erfolgt durch eine als Tabelle organisierte Datenstruktur, die Seitentabelle (Page Table).

Der logische Adressraum, er wird später mit Blick auf eine wichtige Anwendung der Methode virtueller Adressraum genannt, besteht demnach aus einer kontinuierlichen Folge von Seiten. Durch die Zuordnung von Pages zu Frames über eine Tabelle ist der als Menge von Frames vorhandene reale Adressraum nicht mehr notwendigerweise zusammenhängend. Die Frames können irgendwo im Realspeicher stehen und über die Framenummer, die für die reale Basisadresse des Frames steht, gibt die Page Table Auskunft. Abb. I16 zeigt diesen Zusammenhang.

An dieser Stelle setzen wir immer noch voraus, dass alle von einem Programm benötigten Seiten in den Hauptspeicher passen und sehen die Verwaltung mittels Pages vorerst als effiziente Methode, den Hauptspeicher zu verwalten. (Genau diese Voraussetzung wird im folgenden Kapitel über Virtuellen Speicher mit allen Konsequenzen fallen gelassen.)

Abb. I16  Zuordnung Seiten des logischen Adressraumes zu Rahmen im realen Adressraum

Die Größe u der Seite bzw. des Seitenrahmens wird von der Hardware vorgegeben. Sie ist eine Potenz von 2 und normalerweise 4 KBytes (= 2^12) groß. Dies vereinfacht die Übersetzung der logischen Adresse in die Seitennummer und das verbleibende Offset:

Wenn der logische Adressraum 2m und die Seitengröße 2n ist, dann enthalten die high order m - n Bits die Seitennummer und die low order n Bits das Offset.

Hinter der üblichen Seitengröße von 4 KB steht eine realistische Annahme:

Je kleiner eine Seitengröße u bei mittlerer Programm- (Prozess-) Größe s im Hauptspeicher ist, desto weniger interne Fragmentierung fällt an. Im Mittel wird die letzte einem Prozess zugeordnete Seite nur zur Hälfte belegt sein.

Anderseits werden die Seitentabellen sehr groß, wenn für viele (kleine) Seiten je ein Eintrag der Länge e Bytes erforderlich ist.

Der Gesamtspeicherbedarf G für die Seitentabelle und die interne Fragmentierung ist demnach

da jeder Prozess der Größe s insgesamt s/u Seiten verwendet und damit (s*e)/u Bytes Platz in der Seitentabelle belegt. Die Fragmentierung schlägt durchschnittlich mit einer halben Page (u/2) zu Buche.

Einen minimalen Overhead G und damit ein Optimum erhält man durch die erste Ableitung dieser Funktion und Nullsetzen mit:

und somit

Nimmt man als durchschnittliche Größe eines Lademoduls z.B. s = 2 MB und bei einer 32-Bit Architektur e = 4 so ergibt sich u = 4 KB.

Das Programm bzw. der Programmierer sieht einen kontinuierlichen logischen Adressraum. Die Zuordnung des logischen Adressraums zu den physischen Adressen über die Seitentabelle bleibt den Programmen gegenüber verborgen.

Abb. I17  Von der logischen zur physikalischen Adresse

Wie bereits erwähnt gibt es keine externe Fragmentierung, d.h. jeder freie Seitenrahmen kann einer Seite eines Prozesses zugeordnet werden, wenn er ihn braucht. Durch interne Fragmentierung könnte höchstens der letzte zugeordnete Seitenrahmen nicht vollständig belegt sein.

Da Seiten(-rahmen) die gleiche Größe haben, geht das Aus- und Einlagern vom bzw. in den Hauptspeicher schneller als bei Blöcken variabler Größe. Es gibt jedoch auch Systeme, die mit zwei verschiedenen Seitengrößen arbeiten, um eine noch bessere Anpassung zu erreichen.

I.6.5 Eigener Adressraum für jeden Prozess

Wenn das Betriebssystem Seitenverwaltung benutzt, dann erhält jeder Prozess seinen eigenen Adressraum durch die zugewiesene Tabelle.

Das Betriebssystem führt also für jeden Prozess eine Seitentabelle. Der Wechsel zwischen Programmen bedeutet damit auch einen Wechsel der Seitentabellen. Dieser Wechsel kann entweder durch eine Aktualisierung des Speicherbereichs, der die Seitentabelle enthält, erfolgen oder man hält im Hauptspeicher für jeden der laufenden Prozesse zugleich die zugehörige Page Table ebenfalls resident.

Man beachte, dass jeder Speicherzugriff über die Seitentabelle gehen muss. Daher ist Effizienz wichtig. Zum Zugriff auf eine reale Adresse sind zwei Zugriffe erforderlich: Zunächst auf die Page Table und dann erst kann mit der aus ihr entnommenen Framenummer der Zugriff auf den zugehörigen Frame erfolgen.

Deswegen haben moderne Systeme entsprechende Hardwareunterstützung dafür vorgesehen.

Die Seitentabelle kann durch ein Set von besonderen, zusätzlichen Registern der CPU implementiert werden.

Dieser Ansatz unterliegt jedoch gewissen Einschränkungen: Es sind nur kleine Tabellen (z.B.: 256 Einträge) möglich. Bei einem Prozess-Wechsel (Kontextswitch) müssen alle Registerinhalte für den neuen Prozess ausgetauscht werden.

Dieses Problem beim Kontextswitch lässt sich durch ein zusätzliches Seitentabellen-Basisregister (Page-Table-Base-Register, PTBR) beheben, sodass die Seitentabelle vollständig im Hauptspeicher liegen kann. Damit sind dann große Tabellen möglich.

Das Page-Table-Base-Register verweist auf die Basisadresse der Tabelle für den laufenden Prozess. Bei einem Wechsel zu einem anderen Prozess ist dann nur ein zusätzliches Register betroffen. Allerdings verbessert diese Methode die Zugriffszeit nicht. Es sind nach wie vor zwei Speicherzugriffe notwendig.

Auf komplexere Methoden, dass man die Seitentabelle eines Prozesses selbst wieder mittels Seitentabellen verwalten kann und dadurch bei einer Kaskade solcher Tabellen nicht die gesamte Tabelle im Hauptspeicher halten muss, gehen wir an dieser Stelle noch nicht ein.

I.6.6 Translation Look-Aside Buffer

Eine wesentliche Performanzsteigerung erhält man, wenn die CPU auf einen Translation Look-Aside Buffer (TLB) Zugriff hat. Dieser ist ein spezieller Hardware-Cache, der als ein sehr schneller Assoziativspeicher arbeitet. Jeder Eintrag enthält einen Schlüssel und einen Wert. Alle Schlüssel werden gleichzeitig verglichen, wenn ein Element gesucht wird. Die Suche erfolgt daher sehr schnell. Die Anzahl der Einträge beträgt normalerweise 64 bis zu 1024. Der TLB enthält also nur eine kleine Anzahl von Seitenrahmennummern. Eine Seitennummer wird zunächst dem TLB weitergegeben: Falls er sie findet, wird dies als TLB Treffer (TLB hit) bezeichnet und die Seitenrahmennummer ausgegeben.

Abb. I18  Optimierung über Translation Look-Aside Buffer (TLB)

Andernfalls spricht man von einem TLB Fehler (TLB miss), ein normaler Zugriff auf die Seitentabelle ist dann wie bisher notwendig. Zusätzlich aber erfolgt ein Eintrag im TLB, sodass er beim nächsten Zugriff auf den TLB genützt werden kann. Dies erfordert jedoch im TLB eine Ersetzungsstrategie (TLB Replacement Strategie), weil ja ein anderer Eintrag stattdessen entfernt werden muss.

I.6.7 Seitenverwaltung und Schutzmaßnahmen

Die Seitenverwaltung als Methode zur Organisation des Hauptspeichers bringt auch viele Möglichkeiten für Sicherheitsmaßnahmen, die weit über die Funktionalität hinausgehen, wie sie durch

Fence Register gegeben sind. Insbesondere kann man den Tabelleneinträgen für unterschiedliche Zwecke Schutz-Bits hinzufügen, die für den zugehörigen bestimmten Frame gelten. Einige besonders wichtige Beispiele werden kurz besprochen:

I.6.7.1 Read-Only-Bit

Das Read-Only-Bit wird gesetzt (on), falls eine Seite nur gelesen werden darf. Ansonsten (off) ist das Lesen und das Schreiben (Verändern) auf dem Frame zulässig.

I.6.7.2 Valid-Bit

Abb. I19  Read-Only-Bit und Valid-Bit in den Einträgen der Seitentabelle

Das Valid-Bit zeigt an, ob die angesprochene Seite überhaupt zum logischen Adressraum des Prozesses gehört. Beim Zugriff wird überprüft, ob dieser zulässig ist. Unzulässige Zugriffe können somit abgefangen werden, was durch einen speziell auszulösenden Interrupt erfolgt.

Die Situation kann einfach dadurch entstehen, dass in der Seitentabelle noch Platz für weitere Seiten wäre, der Prozess diesen aber, z.B. weil das Programm sehr klein ist, nicht benötigt. Da in einer Tabelle aber immer irgendein Bitmuster steht, muss markiert werden, ob dieser Inhalt eine gültige Adresse bzw. ungültig ist, denn sonst käme es zum Zugriff auf einen Frame, der dem laufenden Prozess gar nicht zugeordnet ist.

I.6.7.3 Gemeinsam benutzte Seiten (Page Sharing)

Eine Seite (Seitenrahmen) kann zu verschiedenen Prozessen gehören (shared page). Dieses Konzept erlaubt es, Daten und Code zwischen Prozessen auszutauschen bzw. gemeinsam zu verwenden, z.B. bei gemeinsamen Datenpuffern oder DLLs, die von mehreren Prozessen verwendet werden, um das Konzept des Dynamic Linking voll ausschöpfen zu können.

Abb. I20  Shared Pages

Bei gemeinsam verwendeten Programmen bzw. Modulen muss deren Code allerdings ablaufinvariant sein (reentrant code). Das bedeutet zunächst, dass der in den Hauptspeicher geladene Code (z.B. eine DLL) nur gelesen und nicht verändert werden darf, was man ohnedies i.Allg. voraussetzt. Wenn die Ausführung das Ablegen von temporären Daten – man denke an Parameter und Rücksprungadressen bei Prozeduraufrufen – erforderlich macht, muss gesondert für jeden Prozess ein eigener Speicherbereich exklusiv reserviert werden, der dann nicht gemeinsam benutzt werden kann.

Auch gemeinsam benutzte Daten (s.o.: Datenpuffer) sind zwischen mehreren Prozessen durch entsprechende Tabelleneinträge möglich. Ein Problem besteht dabei im wechselseitigen Ausschluss, da ja hier ein read-only nicht gegeben sein muss. Ebenso kann eine gesonderte Synchronisation erforderlich werden, da ein Prozess aus einem Puffer nur etwas lesen sollte, was ein anderer vorher hineingestellt hat.

Dieses Thema wird wegen seiner Bedeutung im Betriebssystembereich gesondert abgehandelt (Synchronisation, Semaphore, mutual exclusion).

Das copy on write genannte Konzept kombiniert shared pages mit dem Read-Only-Bit. Dabei verwenden Prozesse Bereiche im Hauptspeicher so lange gemeinsam, wie nur Leseoperationen auf diese Frames ausgeführt werden. Dieser nur lesende Zugriff ist in der Seitentabelle durch das Read-Only-Bit gekennzeichnet. Will nun ein Prozess das erste Mal auf den Speicherbereich schreiben, so wird diese Sicherheitsverletzung vom Betriebssystem in einem Interrupt behandelt. Das Betriebssystem kopiert die Daten für den Prozess in einen eigenen Frame und ändert die Seitentabelle entsprechend. Auch das gelöschte Read-Only-Bit erlaubt nun den Schreibzugriff. Dann kann der Prozess mit seiner Schreiboperation fortgesetzt werden.

Der Vorteil des Konzeptes ist einleuchtend: Wenn Prozesse die Daten nur lesen, so wird Speicherplatz eingespart, da erst bei Bedarf – daher die Bezeichnung copy on write – eine private Kopie der Page erstellt wird.

J VIRTUELLER SPEICHER

J.1 Zielsetzung

Bevor wir auf einige Details der Verwaltung und Organisation virtueller Speicher eingehen, besprechen wir dieses Konzept auf einer abstrahierten Ebene, in der die grundsätzlichen Ideen dazu skizziert werden.

Eine der wichtigen Vorgaben der Softwareentwicklung ist die Möglichkeit, Programme in einzelne Module zu zerlegen, diese getrennt zu übersetzen und erst zum Ausführungszeitpunkt zusammenzubinden und in den Speicher zu laden. Da nicht immer alle Module gleichzeitig benötigt werden, genügt es, einen Teil der Programme zu laden. Man kann also einfach abwarten, was man an Code und Daten zur Ausführungszeit (Laufzeit) wirklich braucht.

Auch kann es sein, dass häufig benötigte Module, z.B. für I/O, Fensterverwaltung usw. schon von anderen Prozessen geladen worden sind und gemeinsam benutzt (shared) werden können. Dies wurde beim dynamischen Linken bereits erläutert, spart Speicherplatz und erleichtert es, mehrere Programme gleichzeitig im Speicher resident halten zu können: ein hoher Multiprogramming-Grad ist eine gute Voraussetzung für eine hohe CPU-Auslastung.

Weiters muss und soll man davon ausgehen können, dass dem Programmierer / der Programmiererin unbekannt bleibt, wie viel an physischem Speicher auf der Zielmaschine tatsächlich vorhanden ist. Ebenso ist im Allgemeinen a priori nicht festgelegt, in welchen Speicherbereich ein Programm zur Ausführungszeit letztlich wirklich geladen wird, sodass Fragen der Adressbindung eine Rolle spielen. Diese Überlegungen führen zum Ziel, eine Entkoppelung zwischen dem für ein Programm vorgesehenen Adressraum (logischer Adressraum), und jenem Speicherbereich (physischer Speicher), in den das Programm oder Teile davon geladen werden, vorzunehmen. Ein Programm soll als ausführbares Lademodul zunächst in einen Virtuellen Speicher (virtual memory) und erst dann ganz oder teilweise in den realen Hauptspeicher zur Ausführung geladen werden.

Eine Grundphilosophie besteht darin, dem Programmierer die „Illusion“ eines „beliebig“ großen Speichers zu geben und die Abbildung der einzelnen logischen Adressen dieses virtuellen Adressraumes auf den physischen Hauptspeicher einer Betriebssystemkomponente (allenfalls mit Hardwareunterstützung) zu überlassen. Die logischen Adressen eines Programms heißen daher auch virtuelle Adressen, weil sie in einen virtuellen Adressraum abgebildet sind.

Damit ist zwischen virtuellen Adressen im logischen Adressraum und realen Adressen im Hauptspeicher zu unterscheiden. Softwareentwickler brauchen sich somit um die Größe des realen Hauptspeichers – jedenfalls im Prinzip – nicht weiter zu kümmern. Allerdings gibt es (für das Betriebssystem!) bei der notwendig werdenden Adressumsetzung damit verbundene Probleme zu lösen.

Man kann davon ausgehen, dass der logische Adressraum N = {a} = {0, 1, … n – 1 | mit n = 2w, w = Wortbreite} größer ist als der physische Hauptspeicher M = {a´} = {0, 1, … m - 1} also n >>> m. Damit ist klar, dass N in M nicht vollständig Platz hat, eine 1:1 Abbildung ist i.Allg. nicht möglich. Ein w = 64-Bit System hat 16 EByte logischen Adressraum, eine Workstation hat z.Zt. üblicherweise 4 GByte Hauptspeicher. Die Annahme n >>> m ist zwar nicht zwingend, aber im allgemeinen Fall und in der Praxis ist der logische (der virtuelle) Adressraum größer als der physische.

Für die Adressumsetzung  definiert man zu jeder virtuellen Adresse a ∈ N eine Funktion

Während der Ausführung eines Programms wird eine Folge von (virtuellen) Adressen a_1,  a_2,  a_3 … im Sinne des Von-Neumann-Zyklus angesprochen, wobei N auf einem Hintergrundspeicher eingebettet gedacht wird und dynamisch eine Adressbindung über die Funktion f(a) erfolgt.

Sieht man von der Hauptspeicherbelegung nach dem Bootvorgang ab, muss man nach dem Start einiger Prozesse davon ausgehen, dass i.Allg. zu einem Beobachtungszeitpunkt t der Hauptspeicher M voll belegt ist, also jedem a´ ∈ M bereits ein a ∈ N zugeordnet ist.

Daher kann bzw. wird folgendes Problem auftreten: Sobald eine virtuelle Adresse a angesprochen wird, die keiner Hauptspeicheradresse a´ zugeordnet ist, d.h. f(a) = ∅, gibt es einen „Fehler“, einen missing-address-fault.

Das Betriebssystem erzeugt dann zur Fehlerbehandlung einen Interrupt und die zugehörige Interrupt-Service-Routine muss für das Laden des Inhaltes der angesprochenen Adresse in den Hauptspeicher sorgen, bevor fortgesetzt werden kann.

Damit steht das Betriebssystem vor der Aufgabe, zu entscheiden, wohin der Inhalt von a geladen werden soll: Mittels einer Platzierungsstrategie (placement policy) ist f(a) = a´ zu berechnen. Unter den gegebenen Annahmen werden im Hauptspeicher M aber keine Adressen unbelegt sein. Demnach muss eine Adresse b´ in M freigemacht und ihr Inhalt in den virtuellen Speicher nach f-1(b´) für allfällige spätere Verwendung zurückkopiert werden, sofern er verändert worden ist.

Die Auswahl einer „geeigneten“ Adresse  b´ ist einer Ersetzungsstrategie (replacement policy) des Betriebssystems vorbehalten.

Eine weitere wichtige Frage ist, zu welchem Zeitpunkt (Inhalte von) Adressen in den Hauptspeicher geladen werden sollen. Es liegt auf der Hand einfach zu sagen: Jene Adressen  sind betroffen, die man sofort oder die man (mutmaßlich) in naher Zukunft benötigen wird. Da man die Zukunft wohl nicht (genau) kennt, kann man entweder gleichsam auf Verdacht (look ahead-strategy) bestimmte Adressen auswählen oder „es darauf ankommen lassen“.

Letzteres ist der übliche Ansatz, es gibt jedoch auch Mischformen dazu. Man wartet, bis ein missing-address-fault eintritt und reagiert dann über die Ersetzungsstrategie entsprechend. Dieses Konzept wird als Laden bei Bedarf (fetching on demand) bezeichnet.

Dazu kommt nun ein weiteres Problem: Für jede kleine Einheit wie „Inhalt eines Speicherwortes“ muss bei auftretendem missing-address-fault die Speicherhierarchie Hintergrundspeicher (Disk) → Hauptspeicher (RAM) jedes Mal komplett durchlaufen werden.

Die Hardwarearchitektur ist aber so ausgelegt, auch mit Blick auf die Plattenorganisation (Sektoren, Spuren, Zylinder), dass man gleich einen ganzen Block übertragen kann, ohne dass dies länger dauert. Das sollte man ausnützen.

Eine weitere Beobachtung lässt sich bei der Codeinspektion von Programmen machen, vor allem wenn man sich den Von-Neumann-Zyklus (siehe Punkt B.2) vergegenwärtigt:

PC := PC + 1, Instruktionszähler um „eins“ (bzw. Befehlslänge) erhöhen.

Dabei wird eine Folge von Instruktionen (Befehle, Statement) der Reihe nach durchlaufen, ehe z.B. wegen einer Programmverzweigung (if-statement, procedure call) in einen völlig anderen Codeabschnitt verzweigt wird. Aus dieser Beobachtung folgt: Wenn eine Adresse a_1 aus M angesprochen wird, ist es sehr wahrscheinlich, dass beim nächsten Schritt ihr Nachbar a_2 = a_1 + 1 referenziert wird und dieser im selben Block liegt. (Diese Beobachtung wird später nochmals unter dem Thema Lokalitätsprinzip aufgegriffen.)

Eine Methode die Speicherabbildungsfunktion f(t,a) zu realisieren besteht darin, im Hauptspeicher eine Tabelle  zu halten, die bei jedem auftretenden missing-address-fault auf den neuesten Stand gebracht wird. Dabei ist aber zu bedenken, dass diese Tabelle sehr groß wird.

J.2 Paging zur Verwaltung von Virtuellem Speicher

J.2.1 Grundlagen

Nimmt man die vorgebrachten Argumente zusammen, so ergibt sich folgendes Konzept:

- Es ist besser, ganze Blöcke auf einmal vom Virtuellen Speicher in den Hauptspeicher zu transferieren und umgekehrt.

- Unter Einbezug der Hardwarearchitektur ist es effizienter, Blöcke gleicher Länge zu wählen.

- Die Tabelle f(t,a) reduziert sich damit auf ein Verzeichnis, welche Blöcke im Hauptspeicher M geladen sind, und wird entsprechend kleiner.

Aus diesen Gründen wird zur Realisierung des Konzepts Virtueller Speicher die Speicherverwaltungsmethode Paging herangezogen. Dieses Verfahren wurde in Punkt I.6.4 bereits vorgestellt.

Die als Tabelle umgesetzte Funktion f(t,a) entspricht der dort erklärten Seitentabelle (Pagetable), der virtuelle Adressraum wird in Seiten und der reale Adressraum in gleich große Frames unterteilt.

Eine Referenz auf eine virtuelle Adresse a besteht dann aus zwei Schritten:

- der Bestimmung der zugehörigen Seitennummer

- Zugriff relativ dazu über das Offset

Über die Seitentabelle wird, wie schon beschrieben, der zugehörige Frame im Realspeicher bestimmt.

Weil die Speicherverwaltung mittels Paging bei gängigen Systemen (nahezu) dominant ist, werden beide, die Verwaltung eines Virtuellen Speichers und Paging, oft ident gesetzt. Das ist aber nicht korrekt, weil Paging ein grundsätzliches Verfahren ist, um einen Hauptspeicher verwalten zu können. Aber Paging ist sehr gut geeignet, um die benötigte Funktion f(t,a) : N → M realisieren zu können und hat sich daher für die Verwaltung von virtuellen Speichern durchgesetzt.

Auf andere Methoden wie Segmentierung oder Kombination von Segmentierung und Paging soll hier nicht weiter eingegangen werden.

J.2.2 Fallstudie Windows

Die Ausgangssituation ist wie folgt: wenn der logische Adressraum sehr groß ist, bei 32-Bit Architekturen sind es 2^32 Bytes, dann wird die benötigte Seitentabelle ebenfalls groß. Bei einer Seitengröße von 4 KB sind dies 2^32/2^12 = 220 ∼ 1 Million Einträge zu je 4 Bytes, womit die Seitentabelle alleine ~ 4 MB Platz benötigt.

Daher verwendet man das Paging Konzept selbst wieder zur Adressierung der Page Table und nimmt ein mehrstufiges Verfahren, wie dies in Abb. J1 gezeigt ist.

Abb. J1   Zweistufiges Paging (ab Intel 80386; nach Unterlagen der Firma Intel)

Die logische Adresse wird hier in 3 Teile zerlegt:

- 10 Bits Page-Directory-Index

- 10 Bits Page-Table-Index

- 12 Bits Offset, also wie bisher 4 KB Seiten- / Rahmengröße.

Die 10 Bits des Page-Directory-Index verweisen auf eine Position (Index) in einer ersten Tabelle, die Seitenverzeichnis (Page-Directory) heißt. Eine Position darin nennt man Page-Directory-Entry (PDE) und ihr Inhalt enthält die Seitenrahmennummer (Page Frame Number, PFN) einer Page Table, wie wir sie von vorhin kennen.

Die Hauptspeicherposition des Seitenverzeichnisses und damit die Adresse des ersten Eintrages wird vom Betriebssystem dem entsprechenden PCB entnommen, der hier KPROCESS heißt und – wenn die Hardware das vorsieht – redundanterweise auch in einem Register (CR3) gehalten wird. Für jeden Prozess gibt es ein Seitenverzeichnis mit 1024 Einträgen.

Dies ist die erste Stufe. Jeder Eintrag (PDE) zeigt auf eine Seitentabelle. Diese Seitentabellen können aber ausgelagert werden, sind also nicht notwendigerweise im Hauptspeicher resident.

Mit dem Page Table Index erhält man aus dem angesprochenen Eintrag dann die Hauptspeicheradresse des zugehörigen Frames, zu der zum Schluss das Offset (wird auch als Byteindex bezeichnet) zu addieren ist.

Jedem Prozess sind demnach (bis zu) 1024 Seitentabellen zugeordnet und jede Seitentabelle wieder enthält bis zu 1024 Einträge für die Pageframes.

Bei Windows in der 32 Bit Version gibt es allerdings hier eine Trennung: Für Anwenderprozesse sind davon nur maximal 512 Einträge vorgesehen, die restlichen 512 sind für involvierte Systemprozesse vorbehalten.

Aus diesen Nebenbedingungen folgt:

(1) für Anwenderprozesse sind 2 GByte = 2^31 Bytes möglich:

2^9 = 512 Einträge aus dem Seitenverzeichnis (page directory)

2^10 Einträge aus jeder Seitentabelle und

2^12 Einträge in jedem Seitenrahmen

(2) für systemweite Prozesse analog 2 GByte

Windows sieht in seiner 32-Bit Version 2 GByte virtuellen Adressraum für Anwenderprozesse vor, nämlich den Bereich x00000000 bis x7FFFFFFF und reserviert für alle systemnahen Prozesse gemeinsam geschützt den 2 GB großen Bereich von x80000000 bis xFFFFFFFF, wie dies in Abb. J2 schematisch dargestellt ist.

Bei Server-Versionen kann eine andere Aufteilung festgelegt werden.

Abb. J2  Speicheraufteilung in Windows für die 32-Bit Version

J.3 Strategien für Paging Systeme

Wir übertragen nun die in ‎J.1 genannten Aufgaben

- Platzierungsstrategie (placement strategy)

- Ladestrategie (fetch policy)

- Ersetzungsregel (replacement strategy)

auf Paging und besprechen sie näher.

Einem missing-address-fault entspricht ein Seitenfehler (Pagefault), der einen page fault interrupt auslöst und die Behandlung dieses Interrupts erfordert.

J.3.1 Platzierungsstrategie (placement strategy)

Bei einem Seitenfehler werden über die Ersetzungsregel (s.u.) eine oder auch mehrere Seiten zum Austausch vorgeschlagen.

Für die Umsetzung in realen Systeme sei noch bemerkt: Das Betriebssystem trachtet, aus Performancegründen immer einen Pool von verfügbaren Seiten zu haben.

Die Aufgabe der Placement-Strategy ist im Vergleich zu Systemen, die kein Paging verwenden, relativ einfach. Da alle Seiten gleich groß sind und jeder Seitenrahmen genau eine Seite aufnehmen kann, ist jeder von der Replacement-Strategy vorgeschlagenen Rahmen gleich gut für die Aufnahme einer Seite geeignet.

Etwas komplexer kann die Sache werden, wenn von der Replacement-Strategy nicht genau ein Frame, sondern eine Menge von solchen aus einem Pool von freigemachten Frames vorgeschlagen wird oder die Placement-Strategy dabei Speicherhierarchien (Hauptspeicher, Cache-Speicher) zu berücksichtigen hat.

J.3.2 Ladestrategie (fetch policy)

Im Prinzip unterscheidet man zwischen Laden bei Bedarf (fetching on demand) und Laden im Voraus (look ahead fetching, prefetching).

Üblicherweise verwendet man Laden bei Bedarf, lädt dann aber mehrere Seiten herein (s.u.). Das bedeutet, dass man beim Prozessablauf wartet, bis ein Seitenfehler auftritt, und dann wird im Zuge der Behandlung des Page Fault-Interrupts die benötigte Seite aus dem Hintergrundspeicher (Page File) nachgeladen.

Typische Einzelschritte sind demnach:

- warte, bis die benötigte Seite gebraucht wird (demand)

- überprüfe anhand des Valid-Bit, ob die Seite schon geladen ist

- ist dies nicht der Fall, lade (fetch) diese Seite vom Page File in den Hauptspeicher.

Dieses Grundprinzip kann verfeinert werden und damit kommt prefetching mit ins Spiel.

Wenn beispielsweise ein Programm neu geladen wird, so ist die Wahrscheinlichkeit hoch, dass zu Beginn eine Folge von Instruktionen für Initialisierungszwecke durchlaufen wird, die – aus der Sicht des logischen Adressraums – in benachbarten Seiten der Reihe nach enthalten. Daher ist es beim Programmstart zweckmäßig, vorab mehrere Seiten zu laden, um zu verhindern, dass nach Abarbeitung der Instruktionen der ersten Seite gleich wieder ein Seitenfehler auftritt. (Beispielsweise lädt Windows XP zum Zeitpunkt eines Programmstarts zwischen 30 und 145 Seiten herein, wobei der genaue Wert von der Hauptspeichergröße und anderen Parametern abhängt.) Wenn die Prefetch Strategie bei ihrer Auswahl „daneben liegt“ und nicht benötigte Seiten lädt, dann könnte ein Prefetch sogar kontraproduktiv werden.

Wesentlich komplexer wird die Situation während des Ablaufs eines Prozesses. Im Regelfall sind alle Frames des Speichers mit Seiten belegt. Man muss dann bedenken, dass ein Nachladen von mehreren Seiten im Voraus immer

- entweder zu Lasten anderer parallel ablaufender Prozesse geht, also sich global auswirkt

- oder mehrere andere Seiten des laufenden Prozesses ausgelagert werden müssen, was sich auf das Prozessverhalten lokal auswirkt

Damit kommen wir zum Thema Ersetzungsregel.

J.3.3 Ersetzungsregel (replacement strategy)

Wenn der Hauptspeicher bereits „voll“ ist, kann eine Seite nur hereingeholt werden, wenn für sie ein Frame freigemacht wird, oder bereits frei gemacht wurde. Dies bedarf einer Ersetzungsregel (replacement strategy). Wie schon erwähnt, wird bei realen Systemen durchaus vorausschauend ein Pool von freien Seiten durch geeignetes Replacement geschaffen, worauf wir hier im Detail nicht näher eingehen.

Die Algorithmen zur Implementierung einer Ersetzungsregel sind ein zentrales Thema im Zusammenhang mit virtueller Speicherverwaltung. Gerade hier unterscheiden sich verschiedene Betriebssysteme, obgleich der Vorgang dem Anwender/der Anwenderin letztlich verborgen bleibt.

Intuitiv ist klar, dass jeder Prozess danach trachtet, möglichst viele seiner Seiten im Hauptspeicher eingelagert zu haben, damit der Prozessablauf nicht durch Seitenfehlerbehandlungen verzögert wird. Die Ersetzungsregel hat nun die Aufgabe, bei einem Seitenfehler entscheiden zu müssen, welcher Frame für die benötigte Seite freizumachen ist.

Dies kann zu Lasten des Prozesses gehen, bei dem der Seitenfehler aufgetreten ist, oder es wird einem anderen Prozess (welchem? auf welcher Entscheidungsgrundlage?) ein Frame genommen. Bei einer look ahead fetch Politik wären sogar mehrere Frames, wie schon erwähnt, zu besorgen.

Zugleich sei auf ein allgemeines Dilemma verwiesen: Je komplexer und aufwendiger die Replacement-Strategy konzipiert ist, desto umfangreicher ist ihr Code und desto mehr Frames muss sie zu Lasten anderer Anwendungsprozesse oder Systemprozesse während ihrer eigenen Ausführung belegen.

J.3.4 Aufwand bei einem Seitenfehler

Ehe wir auf weitere Details dazu eingehen, betrachten wir – in der einfachen Version: demand paging ohne look ahead – den Aufwand, der vom Betriebssystem zu betreiben ist, bis ein Seitenfehler ordnungsgemäß behandelt, also die benötigte Seite eingelagert ist, sodass der Prozess (der „PagefaultProcess“), der diesen Seitenfehler ausgelöst hat, fortgesetzt werden kann. Das Ziel der Überlegungen ist, klar zu machen, wie aufwendig die Behandlung eines Seitenfehlers tatsächlich ist und wie sehr das System damit belastet wird. Wichtige Schritte, wobei unterstellt ist, dass ein freier Frame bereits verfügbar ist, sind:

Maschineninstruktion im Prozess PagefaultProcess spricht eine Adresse an, die derzeit nicht im Hauptspeicher ist, dann folgt:

- Trap zum Betriebssystem

- Register und Prozess-Status speichern

- Erkennen der Interrupt Quelle

- Seitenfehler analysieren (legal? Wo auf Disk?)

- Festplatten-Leseoperation auslösen

- Während des Wartens andere Prozesse zum Zug kommen lassen (Scheduling)!

- Interrupt von der Festplatte (I/O vollständig)

- Register und Prozess-Status speichern

- Erkennen der Interrupt Quelle

- Seitenverzeichnis(se) korrigieren

- Warten, bis PagefaultProcess die CPU wieder erhält

- Register und Prozess-Status wiederherstellen

- Neustart des unterbrochenen Maschinenbefehls

Der entscheidende Punkt ist der benötigte Zeitaufwand, den wir hier grob abschätzen:

Die effektive (erwartete) Zugriffszeit auf den Hauptspeicher sei mit ezt bezeichnet und wir vernachlässigen, dass wegen des „Umweges über die Seitentabellen“ der Zugriff auf eine Seite sogar länger dauert.

Als Größen gehen in die Überlegung ein:

- ezt: die effektive Zugriffszeit

- mzt: die für einen Speicherzugriff benötigte Zeit

- p: die Wahrscheinlichkeit des Auftretens eines Seitenfehlers

- pft: die Zeit zur Behandlung eines Seitenfehlers.

Damit erhält man

ezt = mzt * (1-p) + pft * p

denn entweder es tritt kein oder ein Seitenfehler auf und beide Ereignisse schließen einander aus.

Bei angenommenen Werten mzt = 10 nsek, pft = 10 msek = 10 * 10^6 nsek und der Vorgabe, die ezt sollte höchstens um 20 % mehr als mzt sein, erhalten wir einen erstaunlich kleinen Wert für p:

12 = 10 * (1-p) + 10 * 10^6 * p und

p ≤  0,0000002 (Größenordnung 10-7).

Man hat demnach möglichst zu vermeiden, dass eine Seite ausgelagert wird, die womöglich in einem der nächsten Instruktionszyklen gleich wieder referenziert wird.

J.3.5 Wichtige Ersetzungsstrategien

Der Ersetzungsregel kommt ganz besondere Bedeutung zu, sie ist das Um und Auf in PagingSystemen und bestimmt wesentlich ihre Effizienz.

Zunächst sollte man glauben, dass das Problem leicht durch eine einfache Vergrößerung des zur Verfügung stehenden Hauptspeichers gelöst werden kann. Die von L.A. Belady beschriebene Anomalie zeigt aber, dass dieser schlichte Ansatz nicht immer funktioniert.

Der Grund ist der, dass eine Vergrößerung des Hauptspeichers dann nichts nutzt, wenn man beim Austausch Seiten wählt, die unmittelbar danach wieder benötigt werden. Gesucht sind daher Strategien, welche – sofern es möglich ist – die in naher Zukunft im Prozessablauf benötigten Seiten möglichst treffsicher prognostizieren können und diese im Hauptspeicher belassen.

Abb. J3  Anomalie nach Belady

Gesucht ist also ein optimales Page-Replacement Verfahren bei gegebener Hauptspeichergröße.

Die konkrete Strategie dazu heißt Optimalitätsprinzip nach L.A. Belady und lautet: Wähle jene Seite, die in Zukunft am längsten nicht mehr referenziert (adressiert) wird.

Diese Strategie hat nur einen Schönheitsfehler: Man kennt die Zukunft nicht, da der Page Reference String, das ist die Folge von Seiten, die beim Ablauf eines Prozesses adressiert wird, von vielerlei Umständen (z.B. von den Eingangsdaten) abhängt.

Die optimale Strategie macht Sinn, wenn man experimentell die Leistung von alternativen Verfahren beurteilen will. Dann kann man einen resultierenden Page Reference String mitprotokollieren und anschließend die Seitenfehlerrate mit jener vergleichen, wäre man nach der optimalen Strategie vorgegangen. Ansonsten muss man sich mit Verfahren begnügen, die dem Optimalitätsprinzip möglichst nahe kommen.

J.3.5.1 LRU

Viele Verfahren gehen im Prinzip vom Konzept LRU (Least Recently Used) aus:

Man nimmt die Vergangenheit als Schätzung für die Zukunft und unterstellt, dass Seiten, die schon lange nicht mehr benötigt worden sind, wohl auch in naher Zukunft nicht benötigt werden: Zum Auslagern wird jener Frame bestimmt, dessen Inhalt in der Vergangenheit schon am längsten nicht mehr angesprochen (referiert) worden ist.

LRU darf nicht mit LFU (Least Frequently Used) verwechselt werden. Bei LFU wählt man jene Seite aus, die in der Vergangenheit am seltensten angesprochen worden ist. Dies ist bei frisch geladen Seiten offensichtlich ein Problem, sodass LFU nur in Kombination mit anderen Kriterien verwendet werden kann.

Abb. J4  Beispiel: Least Recently Used (LRU) mit Größe des Working-Sets = 3

J.3.5.2 NUR (Clock Algorithm)

Der Aufwand, um LRU wirklich umzusetzen, ist beachtlich, da Zeitmarkierungen (Timestamps) bei jedem Seitenzugriff mitgeführt werden müssten. Da ja LRU auch nur eine Approximation der optimalen Strategie ist versucht man, die Grundidee beizubehalten und LRU zu approximieren.

Diese Idee führt zur Strategie NUR (Not Used Recently), auch Clock Algorithm oder Second Chance genannt.

Das Prinzip dazu ist einfach. Man hängt jedem Frame als weiteren Eintrag in der Seitentabelle ein extra Reference Bit (Ref-Bit) an und initialisiert dieses mit 0. Immer wenn man auf den Frame (bzw. die darin enthaltene Seite) zugreift, wird dieses Ref-Bit auf 1 gesetzt. Tritt nun ein Seitenfehler auf, so zeichnen sich jene Seiten, die in letzter Zeit referenziert worden sind, dadurch aus, dass das zugehörige Ref-Bit auf 1 gesetzt war. Man braucht also nur (zyklisch, daher „Clock Algorithm“) die Seitentabelle auf eine solche Seite mit Ref-Bit = 0 zu durchsuchen und hat sein Opfer gefunden. Gleichzeitig aber – daher der Name Second Chance – setzt man das Ref-Bit aller angetroffenen Seiten auf 0 zurück. So müssen sie bis zum nächsten Seitenfehler wieder referenziert werden, um nicht beim nächsten Durchlauf in Gefahr zu geraten, ausgelagert zu werden.

Im Sinne von LRU gibt es hier also keine „am längsten in der Vergangenheit nicht referenzierte Seite“, sondern eine Menge von Seiten, die nicht angesprochen wurden und dies durch ihr Ref-Bit = 0 zu erkennen geben.

Abb. J5  Beispiel: Not Used Recently (NUR) mit Größe des Working-Sets = 3

In der obigen Abbildung ist der erste Pagefault zum Zeitpunkt 4 mit allen Einzelschritten ausgeführt. Dann wird pro Eintrag im Page-Reference-String nur mehr der Speicherinhalt angeführt.

In Anlehnung an das Originalpaper „WSClock – A Simple an Effective Algorithm for Virtual Memory Management“ von R. W. Carr und J. L. Hennessy zeigt der Codeausschnitt Code J1 die grundsätzliche Vorgangsweise [Car81].

Code J1   Prinzip des NUR (Clock) Seitenersetzungs-Algorithmus

Ein für jeden Frame mitgeführtes Dirty-Bit zeigt an, ob die Page modifiziert wurde. Soll eine Page ersetzt werden, so ist ihr Inhalt nur dann auf die Disk auszulagern, wenn das Dirty-Bit gesetzt ist. Andernfalls kann man den betreffenden Rahmen direkt überschreiben, da eine exakte Kopie der Seite ohnedies auf dem Hintergrundspeicher liegt. Dies ist im Code-Beispiel Code J1 bereits berücksichtigt.

Das NUR Verfahren kann damit optimiert werden. Man nimmt z.B. bei der Wahl zwischen zwei Frames, deren Ref-Bit 0 ist, jenes als Opfer zum Austausch, dessen Dirty-Bit 0 ist, weil man sich dann wenigstens das Zurückschreiben erspart.

Unbeschadet aller dieser Ansätze kann der Fall auftreten, dass dennoch der Hauptspeicher zu klein wird, entweder weil er a priori zu klein bemessen wurde, oder weil zu viele Prozesse gestartet worden sind. In einem solchen Fall lassen sich (zu viele) Seitenfehler einfach nicht mehr verhindern.

Aus der Berechnung über den Aufwand zur Reaktion auf einen Seitenfehler haben wir aber erkannt, dass dies für die Performance des Systems fatal ist, da es dann nur mehr mit dem Ein- und Auslagern von Seiten beschäftigt ist, anstatt, wie geplant, die gestarteten Prozesse voranzutreiben. Dieses Phänomen nennt man Seitenflattern (Thrashing).

Die nachstehende Abbildung zeigt das Phänomen abhängig vom Multiprogramming-Grad. Zunächst steigt die CPU-Auslastung, um dann ab einem bestimmten Punkt wegen Seitenflatterns rapide einzubrechen.

Abb. J6   Viele parallele Prozesse, kleine Workingsets, Thrashing

J.4 Lokalität und Arbeitsmenge

Abgesehen von der direkten Verhinderung von Thrashing durch Anhalten sowie Entfernen von Prozessen (d.h. Multiprogramming-Grad absenken) und durch Zuteilung des dadurch frei gewordenen Hauptspeichers an den flatternden Prozess, führt das Thema direkt zum Lokalitätsprinzip. Dieses erklärt, warum für Prozesse jeweils eine gewisse Mindestmenge von Pages im Hauptspeicher resident zu halten ist.

Intuitiv ist das Lokalitätsprinzip leicht erklärbar:

Man denke an eine while Schleife (while(<condition>) {.....}) oder an eine längere lineare Sequenz von Instruktionen. Der zugehörige Code hat sehr oft in einer oder in benachbarten Seiten Platz. Damit ändern sich für einen bestimmten Beobachtungszeitraum die angesprochenen virtuellen Seitennummern nicht, im entsprechenden Abschnitt des Page Reference String stehen immer dieselben Seiten. Damit entsteht also – in einem Zeitintervall – so etwas wie eine von einem Prozess bevorzugte Seitenmenge und diese Menge ändert sich als Menge und auch im Umfang nur sehr langsam.

Ein extremes Beispiel dazu sind die dem Kernel eines Betriebssystems zugeordneten Seiten. Daher verbietet man gleich das Auslagern dieser Kernel-Seiten und hält den Kernel überhaupt resident. Auch hier gibt es allerdings in der Praxis Kompromisse: nicht alle dem Kernel zuzurechnenden Pages sind immer resident bzw. man kann das Betriebssystem diesbezüglich parametrisieren.

Das vorhin beschriebene (von Ausnahmen abgesehen) grundsätzliche Verhalten bezeichnet man als Lokalitätsprinzip von Prozessen und wird im Working Set Model (Peter Denning) beschrieben.

Abb. J7  Beispiele für Working Sets

Das Prinzip ist folgendes: Man betrachtet in einem Zeitfenster Δ die Menge der dort referenzierten Seiten und erhebt sie zur Arbeitsmenge (Working Set) des betreffenden Prozesses. Und genau diese Working Set eines Prozesses soll resident gehalten werden. Das Problem ist nur: Wählt man Δ zu groß, so ist die Working Set zu Lasten anderer Prozesse zu groß, wählt man Δ zu klein, so kommt es zum Seitenflattern (Siehe Abb. J6).

Daher haben einige Betriebssysteme ergänzend zur verwendeten Replacement Strategy noch ein sehr ausgeklügeltes Optimierungs-Instrument (working set tuning), um die Working Sets aller gerade ablaufenden Prozesse so zu bemessen, dass keinem Prozess zuviel an Frames und keinem zu wenig zugeteilt wird.

Beispielsweise verfügt Windows über einen aufwendigen Working Set Manager als eigenen Betriebssystemprozess, der periodisch oder bei zu hoher Seitenfehlerrate (page fault frequency) aktiviert wird. Dabei ist hinzuzufügen, dass Windows eine Kombination von FIFO (first in first out, nachteilig beim Starten eines Prozesses) und NUR Strategie beim Seitenaustausch verfolgt, zuerst nur Seiten vom betroffenen Prozess betrachtet und erst, falls erforderlich, vom Tuning der Working Set global Gebrauch macht.

Dies verlangt aber, dass bei den Zuweisungsalgorithmen bzw. Replacementalgorithmen eine globale Strategie möglich ist: Man kann sich bei der Suche nach austauschbaren Seiten dann durchaus auch „in Nachbars Garten“ umsehen.

Geübte Programmierer/-innen wissen um die Lokalität Bescheid und versuchen, ihre Programme so zu schreiben, dass sie von vornherein dem Lokalitätsprinzip folgen. Ein Entwurf, der zu einer Zerlegung in sich abgeschlossener Module führt, ist dabei ein gutes Konzept.

C-Programmierer schätzen in diesem Zusammenhang die Verwendung von Inline-Code anstatt eines Aufrufes einer (getrennt übersetzten) Prozedur, weil damit die Lokalität erhöht wird.

Ein besonders extremes und in der Literatur viel zitiertes Beispiel findet man bei der Verwaltung von Arrays. Es hängt von der (Implementierung einer) Programmiersprache ab, ob eine 2 dimensionale Matrix zeilenweise oder spaltenweise in den linearen Adressraum abgebildet wird:

In FORTRAN werden Matrizen spaltenweise, in der PASCAL-Familie, in C oder Java aber zeilenweise abgespeichert.

Damit erhält man z.B. für a = new int[4][3] für a[i][j] die Beziehung index = i*jmax + j mit jmax =3 und imax = 4 unter der Annahme, dass a[0][0] den Indexwert 0 hat.

Man stelle sich eine quadratische Matrix vor, die so groß dimensioniert ist, dass eine Zeile (bzw. Spalte) in genau eine Seite (z.B. der Größe 4 KB) passt. Des Beispiels wegen nehmen wir vorerst einen knappen Hauptspeicher an, der es – neben dem Programmcode – gerade noch zulässt, dass genau eine Zeile (oder Spalte) resident gehalten werden kann. Daraus folgt, dass bei jedem Zeilenwechsel (Spaltenwechsel) ein Seitenfehler auftritt.

Somit kommt es auf die Indizes in a[j][i] an, in welcher Reihenfolge (zeilenweise, spaltenweise) sie durchlaufen werden und in welcher Programmiersprache entwickelt wird.

Wir gehen von folgendem Code aus:

Code J2   Zeilenweiser bzw. spaltenweiser Zugriff auf eine quadratische Matrix

Als vereinfachende Voraussetzungen für die folgende Berechnung soll sich beim Betreten des obigen Codefragmentes keine Seite der Matrix a im realen Hauptspeicher befinden. Auch muss die Matrix zum Anschluss vollständig am Hintergrundspeicher ausgelagert sein.

Tabelle J1  Anzahl der notwendigen Lade- und Speicheroperationen

Man erkennt sofort, was hinsichtlich Seitenfehlerrate passiert, würde man die Matrix spaltenweise durchlaufen. In den Zahlen wird LRU als Austauschstrategie angenommen. Man sieht, dass erst dann (!), wenn die gesamte Matrix bzw. die 2048 ihr zugeordneten Frames resident gehalten werden können, das Vertauschen der Indizes keinen „Schaden“ anrichtet. Sind „nur“ 2047 Frames resident, so würde LRU unglücklicherweise auch immer die „falsche“ Seite ersetzen.

In den Überlegungen wird davon ausgegangen, dass die Pages für den Code resident im Hauptspeicher bleiben. Unter dieser Voraussetzung zeigt die Tabelle das Laufzeitverhalten der beiden Versionen sozusagen als Funktion der Seitenfehlerrate. Das häufig eingesetzte Konzept NUR als Approximation von LRU hat damit typischerweise ähnliches Verhalten.

Das nachfolgende Experiment gibt einen ungefähren Einblick, was der Working Set Manager unter Windows beim sogenannten Working Set Trimming bewirkt, wenn ein Prozess exzessiv Page Faults produziert, weil seine Working Set (zunächst) zu klein ist. Die Darstellung in der Abb. J8 ist auch insofern vereinfacht, dass die gesamte Working Set des jeweils überwachten Prozesses betrachtet wird. Tatsächlich unterscheidet Windows bei den zu einer Working Set (WS) gehörigen Seiten zwischen „private“ und „shared“ Pages sowie zwischen verschiedenen Pools, aus denen zur Vergrößerung der WS Seiten angefordert werden können: Standby Page List, Modified Page List, Free Page List, Zero Page List. Ebenso soll auf den Unterschied zwischen Hard- und Soft- Page Fault nicht näher eingegangen werden: ersterer benötigt einen Zugriff auf den Pagefile, während bei letzterem die benötigten Daten noch im Hauptspeicher (typischerweise in der Standby oder Modified Page List) verfügbar sind.

Jeder Prozess beginnt mit einer „leeren“ WS und baut diese über anfangs entstehende Seitenfehler bis zu einer bestimmten Größe auf, wobei zunächst (von der Hauptspeichergröße und anderen Systemparametern abhängig) eine untere und eine obere Schranke festgelegt werden.

Das System versucht im ersten Ansatz für den Trimmprozess die Seitenfehlerrate eines Prozesses dadurch zu reduzieren, indem die WS bis zur oberen Grenze erweitert wird (lokales Trimming).

Reicht dies nicht, weil der Hauptspeicher ausgeschöpft ist bzw. in den genannten Listen keine Pages mehr zur Verfügung stehen, beginnt ein globales Trimmen: einem oder mehreren Prozessen, deren WS oberhalb der unteren Schranke liegt, werden Pages zu Gunsten des „pagefaulting“ Prozesses entzogen (globales Trimming).

Auf weitere Nebenbedingungen, wann und in welchem Umfang ein solcher Trimmprozess gestartet wird, wird hier nicht weiter eingegangen.

Die nachfolgende Abbildung zeigt diesen Vorgang, wobei im Experiment ein Prozess namens „loser“ (weil seine WS reduziert wird) gestartet wird. Der Verlauf der gestrichelten Linie zeigt, dass dieser Prozess zunächst mit einer bestimmten WS-Größe läuft und ihm dann zum Vorteil für den parallel gestarteten Prozess „winner“ (weil er Pages für seine WS dazu gewinnt) aus seiner WS Seiten weggenommen werden: „loser“ ist ein Prozess, der auch mit einer kleineren WS auskommen kann. Der Prozess „winner“ wurde in Anlehnung an das vorige Beispiel für das wiederholte Befüllen einer Matrix so codiert, dass er eine hohe Seitenfehlerrate provoziert.

Für den Prozess „winner“ ist in der Abb. J8 zusätzlich noch die gemessene CPU-Auslastung ein¬gezeichnet: sie ist zunächst wegen der hohen Seitenfehlerrate gering, steigt dann aber extrem an, nachdem durch den Trimmprozess die benötigte WS-Größe erreicht worden ist.

Abb. J8  Working Set (WS) Messung

Hier war der Prozess „winner“ längere Zeit inaktiv, daher wurde seine WS auf einen Minimalwert reduziert / „getrimmt“.

Beginnt der Prozess „winner“ nun weiter zu laufen, so erzeugt er viele Seitenfehler. Dabei bleibt die CPU-Auslastung durch den Prozess gering, da durch das Seitenflattern die CPU keine produktive Arbeit für den Prozess durchführen kann. Als Reaktion auf diese vielen Page Faults weist das Betriebssystem dem Prozess schrittweise eine immer größere WS zu.

Hier wird vom Betriebssystem freier Speicher dem Prozess „winner“ zugewiesen und dann

dafür zusätzlich Speicher anderen Prozessen (im Beispiel „loser“) weggenommen. Die WS von „loser“ wird also getrimmt, da dieser Prozess offensichtlich derzeit auch mit weniger Hauptspeicher das Auslangen finden kann.

Trotz weiter vergrößerter WS bleibt in diesem Bereich die CPU-Auslastung durch den Prozess „winner“ konstant niedrig. Offensichtlich ist seine WS noch zu klein und wenig Lokalität im Prozess gegeben.

Erst wenn die WS von „winner“ genügend groß ist – hier wenn sich alle Daten im Hauptspeicher befinden – steigt die CPU-Auslastung von „winner“ stark an.

K FILES, FILESYSTEME

K.1 Aufgaben und Schichtenmodell

Eine weitere wichtige Aufgabe des Betriebssystems ist im Rahmen der Betriebsmittelverwaltung die Dateiverwaltung. Dazu gibt es ein eigenes Dateisystem (Filesystem) das neben den Daten selbst auch noch Zusatzinformationen darüber verwaltet, die Metadaten. Dabei schafft das Betriebssystem durch Verbergen der physikalischen Eigenschaften des jeweiligen Datenträgers eine einheitliche logische Sicht auf Daten. Die zugehörige logische Speichereinheit heißt Datei bzw. – eingedeutscht – der (das) File.

Ein File ist letztlich eine Folge von Bytes (array of byte). Aus Performanzgründen werden Bytes in Blöcke zusammengefasst und diese allenfalls wieder gruppenweise als Cluster übertragen.

Historisch bedingt sagt man, dass ein File aus einzelnen Datensätzen (records) besteht. Dies ist aber nur sinnvoll, wenn man mit File nur Dateien im engeren Sinne meint, also an ein Datenbanksystem denkt und dort zusammengehörige Datensätze identifiziert. So könnte ein Datensatz in einer Personaldatei alle Einzeldaten über eine Person zusammenfassen und die Menge der einzelnen Datensätze bildet dann den einen File, der diese Personaldatei repräsentiert. Von einem Betriebsystem müssen aber alle Arten von Files verwaltet werden. Da ein binärer File z.B. ein ausführbares Programm sein kann, ist eine generelle Unterteilung in einzelne Datensätze daher nicht sinnvoll.

Dateien sind normalerweise auf nichtflüchtigen Medien wie Magnetplatten, CD-ROMs, DVDs, Disketten oder USB-Sticks abgelegt, da ihr Inhalt persistent bleiben soll, also auch nach dem Abschalten der Spannungsversorgung erhalten bleiben muss.

Eine Ausnahme dazu sind RAM-Disks: Hier wird ein Datenträger im Hauptspeicher simuliert, sodass eine höhere Zugriffsgeschwindigkeit erzielt werden kann, vorausgesetzt, dass im RAM-Speicher auch genügend Platz vorhanden ist. Daher kommen RAM-Disks heute noch für die temporäre Aufnahme des Inhalts von Disketten in Frage. Bei Live-Systemen, ohne Einsatz einer Harddisk, sind RAM-Disks aber nach wie vor wichtig: Da Live-Systeme meist von CD/DVD oder USB-Stick (mit einem vollständigen Betriebssystem darauf) booten und keine Harddisk verwenden (sollen), bietet sich ein Teil des RAMs als simulierte Disk an.

Zusammenfassend halten wir folgende Begriffe fest:

- Persistenz

Daten bleiben nach Beendigung des sie bearbeitenden Prozesses und nach Abschaltung der Versorgungsspannung erhalten

- Datei (File)

Zusammengehörige Folge von Bytes, die persistent zu speichern ist

- Datensatz (Record)

Sammlung von zusammengehörigen Datenelementen

- Dateisystem (File System)

Teil(e) des Betriebssystems, welche Dateien verwalten, wobei die Abspeicherung großer Datenmengen im Vordergrund steht

Das Ziel, die technischen und physikalischen Eigenschaften der Datenträger zu verbergen, führt zu einem Schichtenkonzept.

In der obersten Schicht ist als Schnittstelle zu den Anwenderprozessen die Dateiverwaltung im engeren Sinne angesiedelt, sie läuft weitgehend im User-Mode ab.

In der untersten Schicht findet I/O von und zu den Datenträgern statt, wofür eigene Treiber (device driver) eingebunden sind. Der Datentransfer erfolgt dabei i.Allg. Interrupt-getrieben.

Abb. K1 zeigt ein Schichtenmodell für die Dateiverwaltung bis zum Zugriff auf die Device-Controller.

Abb. K1   Schichtenmodell für die Dateiverwaltung

Eine besondere Aufgabe eines Filesystems ist, seine Daten nicht nur persistent, sondern auch konsistent zu halten. Man muss beispielsweise bedenken, dass die Verwaltung eines Files aus einer Reihe von Einzeloperationen besteht, die teilweise sogar parallel ausgeführt werden. Damit kommen auch klassische mutual exclusion Probleme vor, die, wenn sie nicht beachtet werden, in einer Race Condition enden und damit zu einem inkonsistenten Inhalt führen können. Als einfaches Beispiel denke man an das Löschen eines Files. Dieses umfasst das Entfernen des Eintrages im Fileverzeichnis (directory) und ein Freigeben der vom File auf dem Massenspeicher belegten Bereiche. Diese können, wie wir später sehen werden, durchaus verstreut (fragmentiert) auf dem Massenspeicher liegen. Gleichzeitig kann ein anderer Benutzer zufällig eine neue Datei unter demselben Namen anlegen wollen. Was könnte passieren, wenn dazwischen der Strom ausfällt oder das System aus einem anderen Grund in einen nicht definierten Zustand gerät, der neue File schon im Directory eingetragen ist, die belegten Blöcke des vorigen Files aber noch nicht neu zugeordnet worden sind?

Filesysteme, die solche Situationen nach Möglichkeit beherrschen, nennt man Journaling File Systems. Die Bezeichnung rührt daher, dass über die anfallenden Transaktionen ein eigene Liste (Journal) angelegt wird, in der vermerkt wird, welche Transaktionen erledigt sind und insbesondere welche noch anstehen. Solche Transaktionen sind dabei atomar zu denken.

Bei einem Systemausfall kann an Hand des Journals dann die Folge der Transaktionen wiederholt und somit wieder ein konsistenter Zustand erreicht werden. Ein verbreiteter Vertreter eines Journaling File Systems ist das third extended filesystem (ext3) von Linux. Es gestattet drei Risikostufen: Journal (geringstes Risiko, Daten und Metadaten werden im Journal gespeichert), Ordered und Writeback (höchstes Risiko, nur Metadaten werden in das Journal geschrieben).

K.2 Operationen auf Files

Folgende typische und selbsterklärende Operationen auf Files müssen von einem Filesystem unterstützt werden:

- Einrichten (create file)

- Schreiben (write to file)

- Anhängen (append to last record)

- Lesen (read from file)

- Löschen (delete)

- Satzzeiger positionieren (repositioning within a file)

- Abschneiden (truncate)

Wenn ein File aus aufeinander folgenden Sätzen bzw. Blöcken besteht und es erforderlich ist, Sätze einzeln „anzusprechen“ wird mit ihm ein Satzzeiger mitgeführt, der seinem Namen entsprechend auf den „i-ten“ Satz zeigt. (Zur Verwendung des Begriffes „Satz“ sei auf die Einleitung verwiesen).

Beim Anhängen (append) eines neuen Satzes wird dieser auf die nächste freie Stelle (zugewiesenen Satz) anschließend vorgerückt. Wird die Operation „Satzzeiger positionieren“ vom System unterstützt, kann man demnach direkt den i-ten Satz ansprechen und dort lesen bzw. schreiben: Random Access.

Ist das nicht der Fall – was physikalisch z.B. bei Magnetbändern begründet wäre – müsste man den Zeiger zunächst an den Beginn des Files, also auf den ersten Satz stellen und dann die Folge der Sätze sequentiell durchlaufen, bis der gewünschte i-te Satz erreicht ist.

Abschneiden (truncate) hat gegenüber Löschen (delete) eine besondere Bedeutung. Während beim Löschen der gesamte File entfernt bzw. als gelöscht markiert wird, bleibt der File beim Abschneiden erhalten, es werden nur die Sätze bzw. allokierten Blöcke entfernt. Damit verbleiben wesentliche Metadaten wie Eigentümer (Erzeuger) usw., die oft auch die Zugriffsrechte von Benutzern auf die Datei festlegen. (Über Berechtigungen bei Dateien wird im folgenden Kapitel über Sicherheit noch ausführlich zu sprechen sein.)

K.3 File Typen – Metadaten

Das Dateisystem verwaltet nicht nur die Files und die ihnen zugeordneten Blöcke auf den Datenträgern, sondern auch Zusatzinformationen darüber, so genannte Metadaten. Dazu gehören (auszugsweise):

- Name des Files

- Zeitpunkt der Erzeugung

- Datum des letzten modifizierenden Zugriffs („save“)

- Zugriffsrechte

- Schutzbits wie z.B. „nur lesen“ (read only)

- Größe des Files und Information über den

- Typ des Files.

Ein File kann ein reiner Textfile (vgl.: *.txt, *.doc) sein oder ein Programm, also ein ausführbarer File (vgl.: *.exe). Der Versuch, einen Textfile zur Ausführung zu bringen, ist wohl ebenso unergiebig wie (im Allgemeinen) der Ausdruck der binären Darstellung eines ausführbaren Lademoduls.

Man erwartet sich von modernen Betriebssystemen eine geordnete Behandlung von Filetypen. Dazu gibt es im Prinzip folgende unterschiedliche Ansätze.

K.3.1 Filetyp auf Basis des Namens

Man strukturiert den Filenamen mit <name>.<extension>, z.B. myprogr.exe, und gibt dem Anwender letztlich unverbindliche Empfehlungen für die Benennung:

- *.asm für Files, die Assemblercode enthalten,

- *.txt für reine (unformatierte) Textfiles,

- *.lib für Bibliotheksfiles oder

- *.dll für Dynamic Link Libraries usw.

Das Betriebssystem nimmt dann an, dass der Anwender zweckmäßigerweise diesen Empfehlungen folgt und verweigert den Versuch, z.B. einen Textfile namens inhalt.doc zur Ausführung zu bringen. Dies ist bzw. war der Ansatz bei DOS.

In einer speziellen Tabelle – in Windows ist das die Registry – wird verzeichnet, welche Extensions (Filenamenerweiterungen) mit welchen Programmen assoziiert werden:

- *.doc mit dem Textsystem Word,

- *.exe mit dem Loader des Betriebssystem

- *.pdf mit dem Adobe Reader

Diese Einstellungen kann man bei Bedarf ändern.

Bemerkenswert ist, dass Microsoft in der Oberfläche anbietet, die Dateierweiterungen auszublenden, im Ansatz also die Filenamenerweiterung als zusätzliche Metainformation betrachtet, die „nur mehr aus historischen Gründen“ Teil des Dateinamens ist.

K.3.2 Filetyp auf Basis des erzeugenden Programms

Beim Erzeugen eines Files wird das dazu verwendete Programm (Texteditor, Compiler usw.) als Metadatum z.B. im so genannten Fileheader (Anfangsblock des Files) eingetragen. Beim Aufruf bzw. Aktivieren eines solchen Files wird dann implizit gleich der Erzeuger mitgestartet. An diesem Konzept orientiert sich z.B. die Familie der Apple Betriebssysteme Mac OS. Will man einen Erzeuger durch einen anderen passenden ersetzen, bedarf es einer gesonderten Intervention.

K.4 Verzeichnisse

Um abgespeicherte Dateien über ihren Namen oder zugeordneten Metadaten auffinden zu können, führt das Betriebssystem ein Verzeichnis. Ein Verzeichnis (directory) ist somit eine Datenstruktur, die dem Benutzer eine bestimmte Sicht der bzw. über die abgelegten Files zur Verfügung stellt. Ein solches Verzeichnis gibt allerdings i.Allg. keine Information darüber, wie die Organisation der Files auf den Datenträgern wirklich ist.

Eine einfachste Form eines Verzeichnisses wäre eine Tabelle. Eine solche wird aber schnell unhandlich, wenn sie zu viele Einträge enthält. Aus diesem Grunde und weil hierarchisch geordnete Übersichten oft zweckmäßig sind, verwendet man als Sicht eine Baumstruktur.

Zu diesem Zweck führt man neben den Objekten Files (Programme, Textdokumente usw.) weitere spezielle Objekte ein: Ordner (folder, container). Diese können ihrerseits als Inhalt wieder Objekte, also Files und/oder Ordner haben.

Ein weiteres wichtiges Argument, das gegen einfache Tabellen spricht, sind Namenskollisionen. Denn häufig tritt der Fall ein, dass verschiedene Benutzer denselben Namen für Files unterschiedlichen Inhalts wählen wollen. Man denke z.B. an „Protokoll.doc“.

Bei einer hierarchischen Ordnung mit einer gemeinsamen Wurzel (root) besteht der Name eines Files letztlich aus zwei Komponenten:

- Weg (Pfad) von der Wurzel bis zum entsprechenden Baumknoten

- Name im engeren Sinn

Beide Komponenten zusammen werden als Fully Qualified Name (FQN) bezeichnet.

Abb. K2   Verzeichnisstruktur mit Wurzel (root), Unterverzeichnissen und Dateien

Im Beispiel existieren demnach zwei unterschiedliche Files, C:\Users\Christian\Protokoll.doc und C:\Users\Inge\Protokoll.doc.

Im allgemeinen Fall geht es um die Benennung von Objekten, sodass Namenskonflikte ausgeschlossen werden können. Dies erreicht man, dass man zu einem gewählten Namen auch seine Umgebung betrachtet. Dieser Kontext heißt Namensraum und entspricht in seiner graphischen Darstellung einem Baum mit definierter und vorgegebener Wurzel. Ein (Zwischen-)Knoten entspricht einem  Folder und Blätter verweisen auf die in Ordnern enthaltenen Dateien, wenn man vom Sonderfall absieht, dass ein leerer Folder auch durch ein Blatt des Baumes repräsentiert ist.

Gängige bzw. notwendige Kommandos für Verzeichnisse sind:

- mkdir, md:

Anlegen eines Verzeichnisses als Unterverzeichnis zum momentanen Verzeichnis

- chdir, cd:

Wechsel in ein anderes Verzeichnis

- rm, rmdir, del, rd:

Löschen eines Verzeichnisses und implizit dazu: Löschen der darin enthaltenen Files und Unterverzeichnisse

- ls, dir:

Anzeigen der Einträge im gewählten Verzeichnis

K.5 File-Allokation auf Massenspeichern

K.5.1 Grundlegendes

Wenn Daten vom Hauptspeicher zu einem Massenspeicher oder umgekehrt übertragen werden, kopiert man diese nicht bit- oder byteweise, sondern man spricht einen ganzen Block zusammenhängender Bytes an und überträgt diesen als Ganzes. Dies geschieht aus Performanzgründen, wobei die Blockgröße von der jeweiligen Bauart bzw. den physikalischen Gegebenheit des angesprochenen Massenspeichers abhängt. Üblich ist eine Blockgröße von 512 Bytes bzw. 2048 Bytes auf optischen Medien wie CD-ROM oder DVD.

Daher muss ein solcher Massenspeicher vor seiner ersten Verwendung formatiert werden. Durch den Formatierungsvorgang wird das angesprochene Gerät physikalisch in eine Folge von Blöcken unterteilt und darüber ein Verzeichnis angelegt, sodass diese dann adressierbar werden.

Bei Magnetplatten entspricht dieser Vorgang der Einteilung in Spuren (konzentrische Kreise) und Sektoren. Sie erfolgt auf der Ebene der Controller und wird als „low level formatting“ bezeichnet.

Danach hat eine Anpassung zum verwendeten Filesystem des Betriebssystems zu erfolgen. Dies ist jener Formatierungsvorgang, der dem Anwender (vgl.: Befehl format oder mkfs) zugänglich ist.

Bei Festplatten kann man bei Bedarf dabei noch vorab eine Partitionierung vornehmen.

Ein „altgedientes“ Konzept zur Einteilung in Partitionen ist der Master Boot Record (MBR). Diese logische Unterteilung ermöglicht auch, spezielle Partitionen für die Aufnahme eines Betriebssystems festzulegen und bei Bedarf auch, zwischen verschiedenen zu wählen. Die jeweils im MBR festgelegte aktive Partition ist die, von der das Betriebssystem beim Boot-Vorgang zu laden ist.

In den einzelnen Partitionen gibt es jeweils einen eigenen Partition Boot Record (PBR), der vom MBR geladen und gestartet wird. Der Code im PBR lädt dann seinerseits die ersten Teile des Betriebssystems (sofern in der Partition eines vorhanden ist). Man vergleiche dazu Punkt B.3 „Laden des Betriebssystems, Reset-Vektor“

Abb. K3  Aufteilung einer Harddisk im mehrere Partitionen + Boot von der Harddisk

Betriebssysteme bieten, abhängig von der Natur des Massenspeichers und speziellen Anforderungen seitens der Benutzer, mehrere Filesysteme als Option an. Beim Formatieren kann die Entscheidung getroffen werden, welches dieser Dateisysteme auf einer Partition eingesetzt werden soll.

Gleichzeitig mit dem Formatieren werden die benötigten Verzeichnisse angelegt und abhängig vom gewählten File System Cluster-Größen festgelegt. Unter einem Cluster versteht man eine zusammengehörige Menge von Blöcken, die vom Filesystem als ansprechbare und verwaltbare Einheiten angesehen werden. Typische Werte sind Zweierpotenzen (512, 1024, 2048, 4096, 8192, 16384, …). Die kleinste Sektorgröße bei Harddisks ist üblicherweise 512 Byte, deshalb ist diese Größe auch als kleinster Cluster angegeben.

Die globalen Ziele eines Filesystems sind:

- Zugriffsoptimierung, also Performanz hinsichtlich der Übertragungsrate.

Dabei muss ein Dateisystem so ausgelegt sein, dass Prozesse auf die Dateien und die darunter liegende Hardware parallel zugreifen können

- Minimale Fragmentierung, also Optimierung der Blöcke bzw. Cluster bei der Zuordnung zu Files, sodass der verfügbare Speicherplatz bestens genutzt wird.

- Verwaltung von Verzeichnisstrukturen zum effektiven Anlegen, Löschen und Finden von Dateien.

Die Zugriffsoptimierung hängt nicht nur von der physikalischen Bauweise des Massenspeichers ab, sondern auch vom Verwendungszweck:

- Bei Datenträgern wie CDs oder DVDs wird i.Allg. nur einmal darauf persistent geschrieben. Hier steht das schnelle sequentielle Lesen der Folge einem File zugeordneter Blöcke im Vordergrund. Daher sind die Blöcke von vornherein größer (s.o.).

- Bei einer Magnetplatte hingegen spielt die Bewegung des Lesekopfes beim Zugriff auf Blöcke oder Clusters eine Rolle und bei Neuanlegen oder Erweitern (append) eines Files ist zu überlegen, wo die Blöcke auf der Disk anzulegen sind.

K.5.2 Datenstrukturen

K.5.2.1 Fragmentierung

Bei der Einbettung von Files als Folge von Clustern entstehen im Prinzip gleiche Aufgaben- und Fragestellungen wie wir sie bereits bei der Besprechung der (Haupt-) Speicherverwaltung kennen gelernt haben.

Im Besonderen ist dies die Fragmentierung.

Wird die Blockgröße bzw. die Clustergröße zu groß gewählt, so erzeugen kleine Files, die darin Platz finden, eine interne Fragmentierung, da der letzte Cluster im Mittel nur zur Hälfte belegt ist.

Ist die Clustergröße hingegen zu klein gewählt, muss ein erheblicher Verwaltungsaufwand getrieben werden, denn das Ansprechen eines Files vom Directory aus erfordert die Kenntnis der dem File zugeordneten Clusternummern bzw. die Möglichkeit, diese beim Durchlaufen des Files in der Folge eruieren zu können.

Legt man die (maximale) Größe eines Files beim Anlegen bereits fest und speichert die zugehörigen Cluster hintereinander ab, so hat man gleich zwei Probleme: ein File kann nicht mehr vergrößert werden (vgl. append) oder müsste gelöscht und entsprechend größer an anderer Stelle angelegt werden. Dadurch entsteht externe Fragmentierung und es kann sogar sein, dass ein neuer File auf Grund seiner Größe nicht angelegt werden kann, obgleich in Summe genügend freie Cluster verfügbar wären. In diesem Fall muss dann vorab eine aufwändige Diskdefragmentierung ausgeführt werden.

Die Diskdefragmentierung kann auch bei anderen File-Cluster-Zuordnungen zweckmäßig sein. Sie hat aber dort primär den Zweck, große zusammenhängende Block-/Clusterfolgen zu erzeugen, um hohe Übertragungsraten und damit hohe Performanz zu erzielen.

Aus den oben genannten Gründen sieht man von einer kontinuierlichen festen Speicherzuweisung ab.

K.5.2.2 Files als verkettete Listen

Dieser Ansatz verwendet Zeiger und implementiert einen File als verkettete Liste von Clustern.

Vom zugehörigen Namenseintrag eines Files weist ein Zeiger in Gestalt der Clusternummer auf den ersten Block, von dort auf den nächsten Block usw.

Im letzten Block steht entweder ein spezieller Wert (NIL, EOF) oder ein Zeiger, der zurück in den Directory zeigt, oder der Directory-Eintrag enthält zwei Zeiger, einen für den ersten und einen für den letzten Cluster.

Der besondere Nachteil dieses Ansatzes ist, dass die gesamte Folge von Datenblöcken auch dann durchlaufen werden muss, wenn man bloß das Ende des Files ansprechen möchte.

Im Beispiel von Abb. K4 müssen für den Zugriff auf Cluster c_7 vorab c_1, c_2, c_3, c_4, c_5, c_6 gelesen werden, um die Position von c_7 zu kennen.

Abb. K4   Verkettung in den Daten-Clustern

Eine erweiterte Variante sieht eine doppelt verkettete Liste vor, sodass man sich beim Lesen/Schreiben eines Clusters im File in beide Richtungen bewegen kann.

Ein solch einfaches, auf reinen sequentiellen Zugriff ausgelegtes Verfahren, wird die freien Cluster wahrscheinlich ebenfalls als verkettete Liste organisieren. Unter der Annahme aber, dass vom System auf dem Datenträger ein Verzeichnis über bereits belegte und noch freie Cluster gehalten wird (Block Availability Map, BAM, als Bitvektor), löst man dann das Anlegen und das Schreiben in folgende Schritte auf:

- create:

Erzeuge einen neuen Eintrag im Verzeichnis mit dem gewünschten Filenamen und den vorgesehenen Metadaten. Setze den Cluster-Zeiger auf NIL und die Filegröße auf 0.

- read/write:

Bestimme mit Start im Verzeichnis durch Durchlaufen der Zeigerkette den entsprechenden Cluster und lies von dort in einen vorgesehenen Puffer oder übertrage die Daten von dort. Bei Bedarf kann der Inhalt des Puffers vorab/anschließend mit einem Befehl „flush buffer“ geleert werden.

- write mit append:

Finde z.B. über BAM einen freien Cluster, im Idealfall physisch benachbart, und teile dem System mit (Markierung im BAM), dass er ab sofort belegt ist. Setze die Zeiger im Directory bzw. im letzten Block neu.

K.5.2.3 Clusterzuordnungstabelle (FAT)

Als Variante zu einer über Zeiger verketteten Liste besteht die Möglichkeit, über eine Tabelle (File Allocation Table, FAT) festzulegen, welche Cluster (bzw. Blöcke, hängt von der Granularität der Sicht ab) einem File zugeteilt sind.

Diese FAT Tabelle wird pro Datenträger bzw. logischer Disk (disk volume) gesondert angelegt, wobei auf die FAT Tabelle die Cluster folgen.

Die Analogie zum Prinzip der verketteten Liste besteht darin, dass die in den Clustern enthaltenen Zeigern (= Clusternummern) separat von den Daten in der FAT gespeichert werden. Damit erhält man eine implizite Zuordnung vom FAT Eintrag x auf den x-ten Cluster im Datenbereich. Abb. K5 zeigt dies.

Abb. K5   FAT Dateisystem

Da durch einen Lesezugriff in der FAT Tabelle mehrere/viele FAT Einträge auf einmal gelesen werden können, ist dieses Konzept um vieles effizienter als das in Punkt K.5.2.2 vorgestellte Verfahren. Auch kann das Betriebssystem große Teile der FAT Tabelle im Hauptspeicher halten, während beim vorhin vorgestellten Verfahren diese Daten erst nach dem Lesen der gesamten Disk verfügbar werden.

Die Freispeicherverwaltung von FAT ist einfach: Ein spezieller Wert in einem FAT Eintrag (also ein Wert, der nicht für die Verkettung verwendet wird) zeigt, dass der zugehörige Cluster von keiner Datei belegt ist. Beim Formatieren werden die FAT Einträge auf diesen speziellen Wert gesetzt.

Das FAT System wurde von Microsoft für das Betriebssystem MS-DOS entwickelt und liegt in Versionen FAT-x vor, wobei x die Anzahl der Bits für die Clusteradressen angibt.

- FAT-12 kann 2^12 = 4096 Cluster ansprechen, wobei bei einer Blockgröße von 512 Bytes die maximale Clustergröße 4096 Bytes sein kann, sodass Datenträger mit einer Kapazität bis zu 16 MB verwendet werden können. FAT-12 wird noch für die klassische 3,5“ Diskette verwendet.

- FAT-16 kann 2^16-12 Cluster ansprechen (12 Cluster bleiben speziell reserviert). Bei einer Clustergröße von 1 Block = 512 Bytes und maximal 64 Blöcke = 32 KB können Massenspeicher von 32 MB bis zu maximal 2 GB verwaltet werden.

- FAT-32 verwendet 28 Bits zur Adressierung von Clustern. Es hat allerdings eine Beschränkung auf 4 GB große Dateien, obgleich ein FAT-32-Dateisystem insgesamt bis zu 32 GB groß sein kann.

Die ersten FAT-Implementierungen nutzten für den Namensraum zur Bezeichnung der Dateien das „8.3“ Schema: bis zu 8 Zeichen für den Dateinamen und 3 Zeichen als Erweiterung für den Dateityp (z.B. com, exe, txt, asm, lib).

In einer Weiterentwicklung (VFAT, Virtualized File Allocation Table) kann man lange Namen (bis 255 Zeichen) verwenden, sodass beim Datenaustausch mit anderen Filesystemen (z.B. NTFS) hinsichtlich des Namensraumes Kompatibilität gegeben ist.

K.5.2.4 Indexknoten

Eine weitere Variante mit Zeigern auf einzelne Blöcke ist, vom entsprechenden Directory-Eintrag mit einem Zeiger zunächst auf einen Index als speziellen Block zu verweisen. Von dort führen dann Zeiger zu den einzelnen Blöcken (Clustern) des betreffenden Files. Die Idee ist in Abb. K6 dargestellt:

Abb. K6  Clusterzuordung über Indexblock

Dieser Ansatz hat gleich mehrere Nachteile: Ist ein File klein und besteht er daher nur aus wenigen Blöcken, so bleiben im vorgesehenen Indexblock viele Positionen frei bzw. sind mit NIL belegt. Anderseits aber ist durch die Größe des Indexes die maximale Größe eines Files beschränkt.

K.5.2.5 Data-Runs

Für einen effizienten Dateizugriff versucht das Filesystem einer Datei möglichst viele direkt hintereinander liegende Cluster zuzuordnen. Eine solche Folge wird oft als Data-Run bezeichnet. Damit macht eine Optimierung von Indexblöcken Sinn, bei denen für jeden Run nur der Anfang (Clusternummer) und die Run-Länge (Anzahl der Cluster) eingetragen sind.

Die Abb. K7 zeigt einen File mit drei Runs. Der erste beginnt ab Clusternummer 5 mit der Länge 5 Cluster, der zweite Run besteht nur aus dem Cluster 15, der letzte Run der Länge 2 enthält die Cluster 13 und 14.

Abb. K7  Clusterzuordnung über Data-Runs

Data-Runs werden im NTFS von Windows eingesetzt.

K.5.2.6 Inodes

In UNIX bzw. UNIX-Derivaten bedient man sich einer besonderen Variante von Indexknoten, den Inodes (Indexnode, Informationsnode). Abb. K8 lässt bereits die darunter liegende Baumstruktur erkennen.

Abb. K8  Clusterzuordnung über Inodes (insb. Unix Dateisystem)

Zu jedem File wird ein Inode angelegt. Jeder Directory-Eintrag enthält neben dem Filenamen, u.a. auch einen Zeiger (= Nummer) auf den zugehörigen Inode. In diesem sind n Zeiger zur Clusterzuordnung vorgesehen. Die ersten n – 3 Einträge verweisen direkt auf Datenblöcke, sodass kleine Files direkt ohne zusätzlichen Verwaltungsaufwand angesprochen werden können. Die restlichen 3 Zeiger zeigen auf Blöcke (bzw. Cluster), die ihrerseits ausschließlich Zeiger auf Blöcke als Inhalt haben. Dabei verweist der erste Zeiger der drei auf einen single (indirect) block und dessen Zeiger auf Datenblöcke des Files, der zweite Zeiger auf einen double (indirect) block, dessen Zeiger auf single blocks zeigen und der dritte Zeiger auf einen triple (indirect) block, dessen Zeiger auf double blocks zeigen usw.

Mit diesem Konzept kann man nicht nur kleine Files einfach direkt ansprechen, sondern auch große Files verwalten. Die maximale Dateigröße ist abhängig von der Blockgröße, der Anzahl der Bytes für Zeiger, d.h. pro Clusteradresse, und der Zahl der Zeiger, die nach Abzug der unumgänglichen Verwaltungsinformation in einem indirekten Block untergebracht werden können.

Als kleines Rechenbeispiel verwenden wir n = 13, die Blockgröße (absichtlich relativ klein) mit 1 KByte und 256 Zeiger à 4 Bytes in einem indirekten Block. Dann sind Files bis zu 10 Blöcken, also 10 KByte, direkt ansprechbar, mit single blocks erreicht man bis 256+10 Datenblöcke, über double blocks daher 256^2+ 256+10 Blöcke und somit ~ 64 MBytes und schließlich über triple blocks weitere 256^3 Blöcke, also 16 GBytes. Man überlege selbst die maximale Dateigröße bei einer Blockgröße von 4 KB. (Ergebnis  4*(10+1024^1+1024^2+1024^3) KB.)

In einem Inode ist neben üblichen Metadaten auch ein Referenzzähler für die Zahl der Verweise (Link) aus dem Verzeichnis dorthin gespeichert. Abb. K9 zeigt die Situation, dass zwei Verzeichniseinträge auf einen Inode und damit auf eine physikalische Datei zeigen. Erst wenn der Referenz-Zähler durch Löschen von Einträgen im Verzeichnis auf Null (referencecount == 0) gesetzt wird, kann der File gelöscht werden.

Abb. K9   Zeiger auf Inode und Referenzzähler

Eine typische Implementierung verwendet Bitmaps zur Verwaltung der freien bzw. besetzten Blöcke, eine für Inodes und eine für Cluster. Vorab wird eine lineare Liste für Inodes angelegt, womit die maximale Anzahl möglicher Files im Filesystem fixiert ist. Ist das Bit B[i] für den i-ten Inode gesetzt, gilt der zugehörige File als noch vorhanden. Erst wenn referencecount == 0 wird, kann der File gelöscht und mit B[i]=0 der angesprochene Inode als neuerlich verfügbar markiert werden.

Damit ergibt sich folgende Einbettung in eine (in diesem Beispiel) aktive Partition, wobei ein Superblock für allgemeine Verwaltungsdaten (Metadaten) über das angelegte Filesystem vorgesehen ist.

Abb. K10   Beispiel für die Struktur einer Partition

K.5.2.7 Verwaltung über eine Tabelle

Ein wesentlich anderer Ansatz fußt auf folgender Sicht: Die einzelnen Verzeichniseinträge (directory entries) und die Files werden über ein relationales Datenbanksystem verwaltet.

Beim NTFS (New Technology File System), wie es dominant in Windows-Installationen verwendet wird, besteht diese relationale Datenbank aus einer großen Tabelle, dem Master File Table (MFT). Den Zeilen des MFT entsprechen Files bzw. Directory-Einträge, die ebenfalls als Files gesehen werden. Die Spalten beschreiben die Attribute eines Eintrages. Darunter fällt neben dem Namen und zugehörigen Metadaten auch der Inhalt des Files selbst, sofern dieser in einer Zeile der MFT Platz findet. Darauf gehen wir später noch ein.

Das NTFS ignoriert – wie andere Dateisysteme auch – die Einteilung des zu verwaltenden Massenspeichers z.B. in Sektoren und Spuren auf Disks und verwaltet Cluster, die mit einer logischen Clusternummer (LCN) angesprochen werden. Die Abbildung der LCNs auf physische Adressen wird dem jeweiligen Treiber überlassen.

In dem MFT sind die ersten beiden Zeilen (0 und 1) zur Beschreibung der MFT selbst reserviert, die dritte (d.h. Zeile 2) bis einschließlich Zeile 15 enthalten Metadaten über das NTFS, erst ab Zeile 16 beginnen die Einträge für Dateien und Verzeichnisse, womit auf dieser Ebene folgende MFT-Struktur erklärt ist:

Abb. K11   Struktur des Windows Master File Table (MFT)

Die Attribute eines Files sind neben so genannter Standard Information (Standard Attribute Types) wie z.B. typische MS-DOS-Attribute (read only, hidden, time stamps,…), der File Name ($FILE_NAME), Sicherheitsmerkmale betreffend Zugriffsrechte und schließlich die „eigentlichen“ Daten (Attributname $DATA) selbst, was zu folgendem Bild führt.

Abb. K12   Struktur eines MFT-Records

Jedes Attribut beginnt mit einem eigenen Header und wird logisch als eigenständige Folge von Bytes (stream of bytes) betrachtet, sodass Attribute, und damit insbesondere die Daten selbst, gesondert angesprochen werden können.

Eine Zeile im MFT, als Eintrag MFT-Record bezeichnet, ist mindestens 1 KB und maximal 4 KB „groß“. Damit haben nur kleine Files darin Platz. Für größere Files wird der File in Data-Runs (Pkt.  ‎K.5.2.5) unterteilt, wobei diese von der MFT getrennt wieder auf Basis von Clustern angelegt werden. Die Runs werden über Virtuelle Cluster Nummern (VCN) angesprochen, wobei vom NTFS eine Abbildung dieser VCNs auf die LCNs gemacht wird.

NTFS unterscheidet zwischen Resident Attributes und Non-Resident Attributes. Ein residentes Attribut hat zusammen mit seinem Header vollständig Platz im MFT-Eintrag, wie dies in der folgenden Zeichnung schematisch dargestellt ist.

Abb. K13  NTFS residente und nicht-residente Attribute am Beispiel Dateiinhalt

Nichtresidente Attribute jedoch müssen außerhalb des MFT gespeichert werden. Typischerweise ist dies für die „Daten“ ($DATA) eines Files mit seinen Data-Runs der Fall. Dann enthält der Header des Attributes die Startadressen (als VCN) der Runs und die Anzahl der konsekutiv nachfolgenden Clusters je Data-Run. Dies ist im Prinzip in Abb. K7 und Abb. K13 dargestellt.

Man erkennt, dass die zu $DATA gehörigen Data-Runs unterschiedliche Größe haben können und ein großer File demnach in mehrere kleinere Runs fragmentiert sein kann.

Für den Fall, dass der Platz zur Aufnahme aller Attribute eines Files innerhalb eines MFT-Eintrages nicht ausreicht, wird ein zweiter Eintrag im MFT dazu eröffnet. Dies wird im Attribut-Header geeignet vermerkt bzw. es ist ein eigenes Attribut ($ATTRIBUTE_LIST) als Verzeichnis über den Speicherort der Attribute vorgesehen.

Verzeichnisse (directories) werden im NTFS strukturell anders organisiert. Eingangs wurde erwähnt, dass im NTFS ein Verzeichnis wie Files angesehen wird und daher auch als Eintrag im MFT zu finden ist. Nun ist es so, dass ein solches Verzeichnis aus vielen kleinen Einträgen besteht.

Daher werden beim Zugriff auf ein Verzeichnis besondere Anforderungen gestellt: Einerseits muss ein File über seinen Namen schnell aufgefunden werden bzw. eine schnelle Entscheidung getroffen werden, ob ein File mit einem bestimmten Namen überhaupt auf dem Massenspeicher vorhanden ist.

Anderseits ist oft ein schnelles Auflisten aller vorhandenen Filenamen in sortierter Reihenfolge erforderlich, wobei ein Suchen mit Wildcards (vgl. dir *.exe, dir m?file.doc, …) ebenso zu implementieren ist.

Daher unterscheiden sich die MFT-Einträge für ein Verzeichnis in ihrem strukturellen Aufbau. Als Datenstruktur wird ein B-Baum verwendet, mit Hilfe dessen die erforderlichen Operationen effizient realisiert werden können.

Wir gehen an dieser Stelle auf B-Bäume und ihre Umsetzung im MFT nicht weiter ein.

In Abb. K14 ist daher nur das Prinzip dargestellt, wobei als Bezeichner willkürlich „dir2, file1,

file2, …“ gewählt wurden. Der Zeiger links von „file4“ verweist auf Cluster mit ihren virtuellen Clusternummern VCN, in denen Einträge mit lexikographisch kleinerem Namen abgelegt sind, der Zeiger zwischen den Einträgen „file4“ und „file8“ im Index verweist auf Namen, die alphabetisch zwischen „file4“ und „file8“ liegen. Mit dem Zeiger rechts von „file8“ erreicht man alle Einträge, deren Namen lexikographisch größer als „file8“ sind.

Abb. K14  Baum-Struktur eines Directory Eintrages (Quelle: H.Custer)

Jeder Verzeichniseintrag verweist seinerseits auf die MFT-Zeile, in welcher die Datei bzw. das Verzeichnis steht. Dies ist in der Abb. K14 für „dir“ und „file1“ exemplarisch skizziert.

K.6 Disk Quota

Ein Mehrbenutzer Betriebssystem benötigt einen Mechanismus, um einzelnen Benutzern individuell vereinbarte Plattenkontingente (Disk Quota) zuteilen zu können.

Bei einem Fileserver in einem lokalen Netzwerk erreicht man damit, dass Anwender nicht unüberlegt beliebig viele Dateien dort abspeichern. Auch stehen fiskalische Überlegungen hinter der Forderung nach Disk Quota-Zuteilungen.

Die Buchführung erfolgt über eine Quota Tabelle. Kontingente können für einzelne Benutzer und auch für Benutzergruppen vorgesehen sein. Das Betriebssystem muss daher über den von einem Benutzer in Anspruch genommenen Speicherbereich laufend Buch führen und im Anlassfall mit entsprechenden Warnungen und/oder Verweigerung eines weiteren Zugriffs reagieren. Man unterscheidet zwischen Soft Quota, die eine Warnung provozieren und nur eine temporäre Überschreitung des vereinbarten Kontigents erlauben und Hard Quota, die eine strikte Verweigerung zur Folge haben.

Die beschriebene Klasse von Disk Quota fällt unter Block Quota, weil hier der maximale Speicherplatz beschränkt bleiben soll. Daneben gibt es auch Beschränkungen hinsichtlich der Anzahl von Files bzw. Directory Einträge. In diesem Fall spricht man von File Quota.

In Windows beispielsweise erreicht man den Dialog zu den Quota Settings über die Eigenschaften der anzusprechenden Platte. Dann kann man für verschiedene Benutzer die Quoten festlegen und Ebenen einstellen, bei denen ein Benutzer gewarnt wird. Gleichzeitig kann der Administrator auch in einem Logfile mitprotokollieren, wer wann wegen einer Überschreitung gewarnt worden ist bzw. sein Kontingent überschritten hat.

K.7 Mehrere Filesysteme gleichzeitig

An ein System sind im Normalfall mehre physikalisch unterschiedliche Massenspeicher bzw. Laufwerke angeschlossen: Plattenspeicher, Halbleiterspeicher, CDs, DVDs, vielleicht noch Disketten usw. Für einige ist ein spezielles Format vorgesehen, bei anderen Typen wieder hat man eine Auswahl, zB. FAT-32 oder NTFS je nach Verwendungszweck. Damit muss ein Betriebssystem in der Lage sein, parallel mehrere Filesysteme zu verwalten und vor allem einen Datenaustausch zwischen ihnen zu ermöglichen. Auf Benutzerebene (vgl.: User-Mode) sollten aber die unterschiedlichen Architekturen und Aufzeichnungsformate nicht merkbar sein. Erst auf unterster Ebene, bei den Treibern, wird die Abbildung auf die Struktur des Filesystems des angesprochenen Datenträgers vollzogen.

Die nachstehende Abbildung zeigt eine Situation, wie sie bei der Windows Familie zu finden ist.

Abb. K15  Treiberstruktur in Windows zur Unterstützung mehrerer Dateisysteme

K.8 RAID

K.8.1 Stufen von RAID

Eine spezielle Aufgabe der unteren Schichten eines Dateisystems ist, für Datensicherheit bei Ausfall einer Harddisk zu sorgen, soweit dies möglich ist.

Zur Ausfallsicherheit kann man durch Redundanz von Daten bzw. Hinzufügen von Prüfbits bzw. Prüfbytes beitragen. Ein spezieller Ansatz dazu ist RAID (Redundant Array of Independent Disks (alte Bezeichnung: … Inexpensive Disks)).

RAID dient zur Organisation mehrerer physischer Festplatten zu einem logischen Laufwerk, wobei – je nach Variante – eine höhere Ausfallsicherheit und/oder größerer Datendurchsatz im Vergleich zu einer einzigen Platte angepeilt wird.

Wenn RAID softwaremäßig umgesetzt wird, dann ist es üblicherweise im Treiber (Disk-Driver) eingebettet, ansonsten – als performantere Lösung – über den Disk-Controller verfügbar.

Dem RAID Ansatz begegnet man in verschiedenen Stufen, RAID_0 bis RAID_6, wobei RAID_1, RAID_0 in Kombination mit RAID_1 sowie RAID_5 häufig anzutreffen sind.

K.8.2 RAID_0: Striping

Man geht von einem Zusammenschluss von mindestens zwei Festplatten aus und teilt die Platten in gleich große Streifen (stripes) auf. Eine Datei als Folge von Datenblöcken wird, wie in der Abbildung gezeigt, auf die Streifen ABCDEF…, der Disks aufgeteilt. Ein Block ist dabei eine (feste) Folge von zu übertragenden Bytes, seine Größe wird vom Disk-Controller vorgegeben und ist z.B. ein ganzzahliges Vielfaches von Sektoren einer Disk.

Das nachstehende Bild zeigt RAID_0 mit 4 Platten zusammengefasst.

Abb. K16   RAID_0: Striping

Der Vorteil von RAID_0 ist die Erhöhung der Performanz, weil die gebündelten Platten parallel angesprochen werden können. Der besondere Nachteil aber ist, dass die Ausfallsicherheit geringer wird, wie nachstehende Überlegung für n ≥ 2 Festplatten zeigt.

Angenommen wird, dass die Ausfallwahrscheinlichkeit p einer Disk für alle gleich und unabhängig davon ist, ob andere Platten auch ausfallen oder nicht. Dann ist die Wahrscheinlichkeit q, dass eine Festplatte nicht ausfällt q = (1-p) und wegen der Unabhängigkeit der Ereignisse ist qalle = (1-p)n die Wahrscheinlichkeit, dass alle n Festplatten nicht ausfallen.

Damit gilt für die Wahrscheinlichkeit pmind, dass mindestens eine Platte ausfällt pmind = 1 - (1-p)n

Wegen (1-p) < 1 ist (1-p)n <<< 1 für n >> 2 und die Ausfallswahrscheinlichkeit steigt, je mehr Platten zusammengeschaltet sind. Dies ist auch intuitiv einsichtig. Ein Ausfall einer Disk hat aber fatale Folgen, da alle in den dort liegenden Stripes gespeicherten Daten verloren gehen.

Daher wird RAID_0 alleine eher selten eingesetzt, sondern in Kombination mit anderen Verfahren.

K.8.3 RAID_1: Spiegelung

Spiegeln (mirroring) einer Platte über das Betriebssystem bzw. einen dafür ausgelegten Disk-Controller hat als Ziel, durch die offensichtliche Redundanz die Ausfallsicherheit zu erhöhen.

Abb. K17  RAID_1: Mirroring (Spiegelung) und Kombination von RAID 0 mit RAID 1

Der Nachteil des Verfahrens besteht darin, dass die Datenübertragung (Schreiben) aufwändiger ist, da entsprechende Kopien auf dem(n) Disk-Mirror(s) anzulegen sind. Um die Performance von RAID_0 und die Sicherheit von RAID_1 zu erhalten, kombiniert man - vor allem bei Anwendungen mit Datenbanken – RAID_0 mit RAID_1.

K.8.4 RAID_5: Stripes mit Prüfbits

Die RAID Versionen RAID_2 bis RAID_4 führen Prüfbits ein, um bei einem Ausfall von einer

(oder mehreren) Platten aus den verbliebenen die Daten rekonstruieren zu können. Die Prüfbits können als redundante Information auf einer Platte gesondert abgelegt oder in Kombination mit Striping über die Festplatten verteilt werden. Vom letzteren Ansatz macht RAID_5 Gebrauch.

Den mathematischen Hintergrund über die Bestimmung der Prüfbits und ihre Anzahl liefert die Codierungstheorie als Spezialgebiet der Algebra und der dort behandelten endlichen Körper.

RAID_5 kann bei Ausfall einer Disk mit XOR über die Daten der noch intakten n-1 Platten zusammen mit den Parity Bits den Inhalt rekonstruieren.

Abb. K18   RAID_1: Mirroring (Spiegelung) und Kombination von RAID_0 mit RAID_1

RAID_6 erweitert das RAID_5 durch zusätzliche Redundanz und kann sogar den gleichzeitigen Ausfall von 2 Platten aus den verbliebenen n-2 kompensieren.

L SICHERHEIT IN BETRIEBSSYSTEMEN

L.1 Zum Begriff Sicherheit

Wir beginnen mit einer Diskussion, was unter Sicherheit zu verstehen ist. Im Englischen gibt es zwei Wörter: Safety (vgl.: Sicherheitsgurt, safety belt) und Security (vgl.: sicheres Passwort, secure password). Was man demnach mit Sicherheit meint und anspricht, hängt vom Kontext, also vom behandelten Thema ab.

Es können Security und Safety durchaus miteinander in Beziehung stehen: Einerseits hat der Safety-Check des Piloten vor dem Start nichts mit dem Security-Check der Passagiere vor dem Einstieg zu tun. Anderseits trägt dieser personenbezogene Sicherheits-Check dazu bei, dass wir einen sicheren (safety!) Flug erwarten können, wenn der Safety-Check des Piloten ergeben hat, dass das System „Flugzeug“ im technischen Sinne sicher ist. Letzteres heißt, dass die erwartete Systemfunktion während des Betriebes so ausgeübt wird, dass ein verbleibendes Restrisiko (leider) toleriert werden kann oder muss. Man sagt: „Ein bis auf ein Restrisiko ordnungsgemäß arbeitendes System“.

Im Zusammenhang mit Betriebssystemen gehen wir von der (Wunsch-)Vorstellung aus, dass es sich um ein ordnungsgemäßes System handelt. Damit verlangt man, dass alle vom System gemachten Aktionen

- korrekt entsprechend der Spezifikation des Systems umgesetzt werden,

- zum vorgesehenen Zeitpunkt, also rechtzeitig ausgeführt werden, und

- keine Aktionen ausgeführt werden, die nicht angefordert werden.

Die beiden ersten Bedingungen unterstellen eine fehlerfreie Implementierung aller Komponenten eines Betriebssystems. Inwieweit hier Vorgabe und Realität auseinanderklaffen können, zeigen die periodisch durchzuführenden Updates (security patches) bei den am Markt befindlichen Betriebssystemen.

Die dritte Bedingung zielt darauf ab, dass ein a priori korrekt spezifiziertes und implementiertes System nicht z.B. durch bösartige Software (Malware) kompromittiert worden ist.

Die häufigste Verletzung dieser Forderung entsteht dadurch, dass über Softwareschwachstellen, d.h. Programmierfehler, vom Anwender unbemerkt Malware eingeschleust worden ist.

Dieser Punkt gibt Gelegenheit, einige Begriffe kurz einzuführen:

- Bug: ein Programmierfehler, der nicht nur zu Programmabstürzen führen kann, sondern auch die Sicherheit – der Begriff wird noch zu klären sein – eines Systems gefährden kann.

- Bugfix: bezeichnet die erfolgreiche Behebung eines Bugs.

- Vulnerability: ist die Verletzbarkeit eines Systems, ausgeführt durch einen Spezifikationsfehler oder durch einen Bug bzw. Ausnutzung desselben durch einen Eindringling.

- Exploit: ein Computerprogramm, das eine Schwachstelle eines anderen Programms, im Besonderen eines Betriebssystems, gezielt ausnutzt.

Einleitend wurde festgestellt, dass die Interpretation von Sicherheit im Zusammenhang mit der angesprochenen Systemumgebung zu sehen ist. Dabei kann man vom (technischen) System auf seine Umgebung blicken oder umgekehrt von dieser auf das System, womit wir folgende allgemeine Begriffsbestimmungen erhalten:

- Sicherheit im Sinne von Safety ist der Schutz der Systemumgebung (einschließlich der betroffenen Menschen!) vor einem nicht ordnungsgemäßen Verhalten des Systems. Daher spricht man auch von Funktionssicherheit.

- Sicherheit im Sinne von Security ist der Schutz des Systems vor bedrohendem Verhalten seitens der Systemumgebung und zielt demnach auf alle Maßnahmen ab, einen solchen Schutz zu erreichen.

Bei der Funktionssicherheit verlangt man daher, dass die realisierte Ist-Funktionalität aller seiner Komponenten mit der spezifizierten und erwarteten Sollfunktionalität übereinstimmt. Bei den einzelnen Zustandsübergängen darf nie ein Zustand erreicht werden, der nicht spezifiziert und damit geplant beherrscht werden kann. Das Thema Restrisiko bleibe dabei in dieser Festlegung ausgeklammert.

Der genannte Sicherheitsbegriff im Sinne von Security kann nun speziell für informationsverarbeitende Systeme erklärt werden.

Informationssicherheit liegt vor, wenn ein ordnungsgemäß funktionierendes System nur solche Systemzustände annehmen kann, die zu keiner unautorisierten Informationsveränderung oder Informationsgewinnung führen.

Wir begnügen uns vorerst mit einem intuitiven Verständnis, was mit autorisiert bzw. unautorisiert gemeint sein soll.

Zielt man mit Informationssicherheit speziell auf die Repräsentanten von Information, also auf Daten und die sie darstellenden Datenstrukturen ab, so kommt man speziell zum Begriff Datensicherheit, eine Unterscheidung die im Detail von C. Eckert getroffen wird [Eck06]:

Datensicherheit (Protection) liegt vor, wenn nur autorisierter Zugriff auf Daten bzw. mit ihnen assoziierten Ressourcen möglich ist.

Datensicherheit unterscheidet sich damit wesentlich vom vor allem im juristischen Bereich verwendeten Datenschutz (Privacy). Darunter versteht man das Recht und die Fähigkeit einer Person, die Weitergabe persönlich betreffender Daten selbst bestimmen und damit kontrollieren zu können.

Ehe wir uns jenen Sicherheitsthemen zuwenden, die insbesondere bei Betriebssystemen von zentralem Interesse sind, führen wir die allgemeinen Schutzziele für IT-Sicherheit kurz an:

- Authentizität (authenticity):

Darunter versteht man die Echtheit und Glaubwürdigkeit eines Objektes. Wir gehen im Kapitel „Identifizieren und Authentifizieren“ genauer darauf ein.

- Integrität (integrity):

Diese Eigenschaft erfordert, dass es Subjekten nicht möglich ist, Daten unautorisiert und auch nicht unbemerkt zu manipulieren.

- Informationsvertraulichkeit (confidentiality):

Damit wird verlangt, dass keine unautorisierte Informationsgewinnung möglich ist. Eine wirksame Methode dazu ist das Verschlüsseln von Daten.

- Verfügbarkeit (availability):

Berechtigte, d.h. authentifizierte und autorisierte Subjekte können (i.Allg.) ohne Beeinträchtigung die ihnen zugestandenen Berechtigungen wahrnehmen. Der Zusatz „(i.Allg.)“ ist deshalb angefügt, weil es durchaus auch autorisierte Hemmnisse geben kann, z.B., wenn ein Dokument, das von mehreren Personen gleichzeitig bearbeitet wird, temporär für den Schreibzugriff seitens Dritter gesperrt wird.Verfügbarkeit kann durch Redundanz der benötigten Ressourcen gesteigert werden.

- Verbindlichkeit (auch: Zuordenbarkeit, non repudiation):

Zuordenbarkeit wird dann von einem System geleistet, wenn es einem Subjekt nicht möglich ist, die Durchführung einer gesetzten Aktion im Nachhinein zu bestreiten. Damit bezieht man sich u.a. auf die Rechtsverbindlichkeit von Transaktionen, die von Subjekten durchgeführt worden sind und dann nicht mehr bestritten werden können. Bei Betriebssystemen setzt man dazu u.a. Logfiles ein, die Aktionen mitprotokollieren, bei digitalen Dokumenten verwendet man Signaturen und Zertifikate.

- Anonymität (anonymity):

Das zugehörige Zeitwort anonymisieren beschreibt das Verändern von personenbezogenen Daten derart, dass Einzelangaben über persönliche oder auch sachliche Verhältnisse einer Person dieser praktisch nicht mehr zugeordnet werden können. Eine schwächere Variante ist die Pseudonymität. Hierbei gibt es zwar eine Zuordnungsvorschrift, diese ist aber Dritten nicht allgemein bekannt. Als Beispiel wird angeführt, dass ein Autor unter einem Decknamen (Pseudonym) schreibt, die Zuordnung zum wahren Namen aber nur dem herausgebenden Verlag bekannt ist.

L.2 Identifizieren und Authentifizieren

L.2.1 Authentizität und Authentifizierung

Jemand legt eine Kopie einer Urkunde vor und behauptet, sie sei ihrem Inhalt nach authentisch zum Original. Eine beigezogene Expertin authentifiziert nach genauer Prüfung des vorgelegten Schriftstückes diese, sodass die Authentizität der Urkunde nun unbestritten ist und diese somit z.B. durch einen anerkannten Prüfvermerk authentisiert werden kann. Der Überbringer wird Kraft der vorgelegten Urkunde, die von der Expertin authentifiziert und somit authentisiert worden ist, als Herr PJH identifiziert und wird entsprechend dem Urkundeninhalt autorisiert, bestimmte darin genannte Geschäfte und Rechte autoritativ auszuüben.

Dieses Sprachbabylon rührt daher, dass man im Deutschen verschiedene, aber in ihrer Bedeutung eng verwandte Lehnwörter mit griechisch/lateinischem Ursprung verwenden kann.

Im Englischen ist die Ausdrucksweise einfacher: authentic heißt  „echt, als wahr bekannt“, authenticate bedeutet „die Echtheit, Authentizität beweisen“ und damit auch „rechtsgültig machen“ und die damit erzielte Eigenschaft ist authenticity.

Wir fassen zusammen:

Unter Authentizität einer Information versteht man die sichere Zuordnung zum Sender mit erbrachtem Nachweis, dass diese Information nach ihrem Versand nicht mehr verändert worden ist.

Authentifizierung ist der Vorgang der Überprüfung der behaupteten Identität eines Gegenübers, wie z.B. einer Person oder eines Computersystems.

Authentisierung ist der Vorgang des Nachweises der eigenen Identität.

Damit müsste es streng genommen heißen: „A authentisiert sich gegenüber B und wird von B als A authentifiziert“.

Abweichend von „sich authentisieren“ wird „etwas authentisieren“ oft im Sinne von „glaubwürdig“ und insbesondere „rechtsgültig machen“ verwendet. Da in der Informatik der Großteil an Fachbegriffen dem Englischen entlehnt sind, werden wir zwischen Authentifizierung und Authentisierung nicht weiter unterscheiden und verwenden „sich authentifizieren“, sowie „jemanden authentifizieren“ ebenso wie authentisieren.

Autorisieren beschreibt das Einräumen von Rechten. Mit Identifizieren ist im Beispiel die Feststellung der Persönlichkeit („Identität“) gemeint. In den beiden nachstehenden Abschnitten wird darauf noch ausführlicher eingegangen.

L.2.2 Identifizieren

Ebenso wichtig für die Informatik aber weitaus vielschichtiger sind der Begriff Identität und das Verb identifizieren. Ausgangspunkt ist das Identitätsprinzip, formuliert nach Gottfried Wilhelm Leibniz:

„Ein Gegenstand A ist genau dann mit einem Gegenstand B identisch, wenn sich zwischen A und B kein Unterschied finden lässt.“

Das praktische Problem dabei ist, welche Eigenschaften der Gegenstände insgesamt miteinander verglichen werden, denn die Überprüfung muss ja innerhalb praktikabler Zeit ein Ende finden. Dazu kommt noch das grundsätzliche Problem, dass bei aller sorgfältiger Prüfung der Eigenschaften von A und B einem eine Ungleichheit verborgen bleiben könnte, weil man dafür noch kein Messverfahren hat bzw. von der Existenz einer bestimmten Eigenschaft noch gar nichts weiß.

Wie unterschiedlich „Identität“ und „identifizieren“ vom Sachgebiet abhängig verwendet werden, sollen folgende kurze Sätze illustrieren: Ein Chemiker meint mit „identifizieren“ den Nachweis des Vorhandenseins eines bestimmten Stoffes. Ein Zeuge identifiziert anhand eines Fotos einen Täter. Bei einer Schauspielerin schätzt man ihre Identifikation (auch: Identifizierung) mit einer Rolle. Mitarbeiter identifizieren sich mit den Zielen ihrer Firma.

In der Informatik geht es in diesem Zusammenhang hauptsächlich um Folgendes:

Jemand identifiziert sich (seiner Ansicht nach) gegenüber einem (Computer-)System. Dieses hat nun zu prüfen, ob die behauptete Identität auch tatsächlich vorliegt und verwendet dabei eine Reihe von vorab spezifizierten charakteristischen Eigenschaften. Dabei geht es um die Prüfung der Authentizität eines Subjektes, wobei abhängig vom Anwendungsfall und den notwendigen Sicherheitsvorkehrungen bestimmte Merkmale als charakteristische Eigenschaften in den Prüfungsprozess miteinbezogen werden: Es wird eine Authentifizierung (Authentisierung) vorgenommen.

Dazu gibt es im Prinzip folgende Ansätze:

- etwas haben:

Schlüssel, Mitgliedsausweis, TAN, …

- etwas wissen:

PIN, Passwort, Name der Katze der Mutter, …

- etwas sein:

Gesichtszüge, Fingerabdruck, DNA-Probe, …

- irgendwo sein:

Telefonnummernerkennung und Rückruf, GPS-basierte Authentifizierung, IP-Adresse,

…

allgemein: man verwendet Information über den Aufenthalt einer Person

Jeder der angeführten Ansätze hat seine Schwächen: ein Schlüssel kann gestohlen werden, ein Passwort kann ausgespäht oder gar freiwillig weitergegeben werden. Selbst ein genetischer Fingerabdruck (DNA-Probe) kann bei einem eineiigen Zwilling versagen, wenn nur die in der Forensik üblichen Abschnitte aus der DNA herangezogen werden. Dazu kommt, dass der zutreffende Aufwand in einem entsprechenden Verhältnis zu den Sicherheitsanforderungen stehen muss.

In der Realität nimmt man daher oft in Kauf, dass eine Authentisierung für eine behauptete Identität für mehrere Benutzer zutreffen kann. Der typische Fall dazu ist die (freiwillige oder unfreiwillige) Weitergabe eines Passwortes, womit die Verantwortung an den ursprünglichen Identitätsgeber zurückgespielt wird. Man spricht von einer starken Authentisierung, wenn ein Angreifer abgehorchte Daten nicht wieder verwenden kann. Ein Beispiel dazu sind Einwegpasswörter wie TANs (Transaktionsnummern).

L.2.3 Zugangskontrollen

Zugangskontrollen basieren in der Informatik überwiegend auf dem Prinzip „etwas wissen“ (Passwort), wobei bei erforderlicher Sicherheit noch „etwas sein“ (Fingerabdruck) dazu kommt. Bei der Eröffnung eines Benutzerkontos (Account) am System wird dem betreffenden Benutzer durch den Systemadministrator ein eindeutiger Name in Form einer Zeichenkette, z.B. PJH, zugewiesen. Das vom Administrator diesem Account zugewiesene Passwort muss von PJH entsprechend einer vorgegebenen Sicherheitspolitik (security policy) nach dem erstmaligen Login verändert werden. Damit kann man später davon ausgehen, dass nur PJH dieses Passwort kennt.

Heutige Betriebssysteme bieten die Möglichkeit, die Einhaltung der genannten Sicherheitspolitik zu erzwingen, wobei noch zusätzliche Anforderungen an die Qualität des Passwortes (z.B. mindestens 8 Zeichen) eingefordert werden können und auch sollten.

Auf den Punkt, dass nur PJH und weder das System noch der Administrator das neu gewählte Passwort kennen dürfen und dennoch ein Vergleich durchzuführen ist, kommen wir später zurück.

Bei einem Login mit Eingabe „PJH“ behauptet die das Login durchführende Person, PJH zu sein, was das System in einem nachfolgenden Authentifizierungsprozess zu überprüfen hat. Dazu müssen im System zwei Referenzdateien gespeichert sein: der Login Name und eine Information über das gültige Passwort.

In Schritt (1) wird überprüft, ob für die behauptete Identität PJH, für das Subjekt PJH, ein Benutzerkonto angelegt worden ist.

In Schritt (2) wird von PJH das vereinbarte Wissen, hier das Passwort, abverlangt, damit der Authentifizierungsprozess durch Vergleich mit den verfügbaren Referenzdaten abgeschlossen werden kann. Endet das Verfahren positiv, so wird der Benutzer autorisiert, für die Dauer der Sitzung (bis zum Log off) jene Rechte wahrzunehmen, die seinem Account zugestanden worden sind.

Eine Erweiterung des Ansatzes „etwas wissen“ ist das als Challenge Response bezeichnete Verfahren. Das Prinzip besteht zunächst darin, dass man nicht ein Passwort überträgt, sondern den Nachweis erbringt, dass man es kennt (oder gekannt haben muss).

In der einfachsten Form geschieht dies, indem Fragen zu beantworten sind, die nur die zu authentifizierende Person kennt. Ein typischer Anwendungsfall dafür ist, wenn ein solches Frage und Antwortspiel sich auf Fragen bezieht, die zum Zeitpunkt der Festlegung des Passwortes mit der authentifizierenden Stelle für den Fall vereinbart worden sind, dass man das Passwort vergessen hat. Beantwortet man alle Fragen korrekt, kann ein neues Passwort übersandt werden, das dann wieder vom Benutzer neu zu setzen ist.

Tiefer geht das Prinzip, wenn dem Benutzer eine bestimmte Aufgabe (Challenge) gestellt wird und wir beschreiben – in vereinfachter Version – einen solchen Vorgang:

Die Benutzer A und B haben entweder einen Schlüssel k geheim vereinbart oder verwenden zum Informationsaustausch ein Public-Key Verfahren. Zunächst sendet B eine Zufallszahl n (nonce) und bittet – als Challenge – nun A, dieses n zu verschlüsseln und das Ergebnis m = encrypt(n) zu retournieren, womit B mittels decrypt(m) vergleichen kann, ob decrypt(m) = n ist.

Es ist wichtig, dass bei jedem solchen Authentifizierungsvorgang eine neue Zufallszahl gewählt wird, um Replay-Attacken (Einspielen einer abgehörten früheren Kommunikation) zu verhindern. Auch kann man zusätzlich einen Zeitstempel verschlüsselt mitsenden, sodass der Empfänger inner-halb eines gewissen Zeitintervalls davon ausgehen kann, dass die gesandte nonce genügend „frisch“ ist.

Das Verfahren kann auf wechselseitige Authentifizierung ausgedehnt werden, indem auch der Client eine Challenge Response an den Server sendet, um sicherzustellen, dass die behaupteten Identitäten beider Seiten gegeben sind.

Eine besondere Art einer Zugangskontrolle mittels Challenge Response liegt vor, wenn es darum geht, bei einem über das Web angebotenen Anmelde- oder Registrierungsformular sicherzustellen, dass es von einem Menschen und nicht von einem Automaten (Computerprogramm) ausgefüllt wird.

Die Idee besteht darin, dem Gegenüber, dem Client, vom Server eine Aufgabe zu stellen, die nur von einem Menschen Kraft seiner kognitiven Fähigkeiten erledigt werden kann. Dazu übermittelt der Server auf dem angebotenen Formular ein Captcha genanntes Bild von verzerrten Zeichen, die zwar der Mensch noch als solche identifizieren kann, nicht aber ein Zeichenerkennungsalgorithmus (Pattern Recognition-Verfahren). Ein Captcha bildet demnach die Challenge und das Eintippen der erkannten Zeichenfolge ist die Response.

Abb. L1  Challenge Response Verfahren mittels einem Captcha

Hinzufügen ist, dass ein Wettlauf zwischen ausgefeilten Captcha-Generierungsmethoden auf der einen Seite und Algorithmen zur automatischen Erkennung derselben auf der anderen Seite zu beobachten ist.

Captcha ist ein Akronym für „Completely Automated Public Turing Test to tell Computers and Humans Apart“ und leitet sich daraus ab, dass in Modifikation des von A.Turing vorgeschlagenen Testablaufes nun eine Maschine (Computerprogramm) feststellen soll, ob ihr Gegenüber ein Mensch oder ein Automat ist.

L.2.4 Betriebssysteminterne Identitäten

Wenn ein Benutzer die Zugangskontrolle erfolgreich absolviert hat, erhält er seine Identität. Nach dem hohen Aufwand für die vorangegangene Identifizierung und Zugangskontrolle ist es für das Betriebssystem wichtig, künftig Benutzeridentitäten nie zu verwechseln.

Als erstes wäre es nahe liegend, den vereinbarten Login Namen „PJH“ dafür zu verwenden. Dies wirft jedoch folgende Probleme auf:

Wenn „PJH“ gelöscht und dann ein neuer Benutzer mit dem nun frei gewordenen Bezeichner „PJH“ angelegt wird, wäre es aus Sicht des Systems derselbe Benutzer und er hätte fatalerweise auch Rechte seines Namensvorgängers.

Als Lösung erzeugt man daher für jeden Benutzer Account eine eindeutige noch nicht vergebene interne Repräsentation der Identität.

Im einfachsten Fall würde eine fortlaufende Nummer genügen. Da in Netzwerken bzw. verteilten Systemen Benutzeridentitäten zwischen Rechnern ausgetauscht werden, reicht das nicht. Stattdessen verwendet man eine geeignet strukturierte Zeichenkette, die weltweit Eindeutigkeit praktisch sicherstellt.

In Windows heißt dieser Identitätsbezeichner Security Identifier (SID) und besteht unter Anderem aus rechnerspezifischen, betriebssystemabhängigen Teilen und einer innerhalb des Systems eindeutigen Kennung, wie z.B.

Abb. L2  Windows Security Identifier (SID)

L.3 Speichern von Passwörtern

L.3.1 Passwortsicherheit

Aus den vorigen Überlegungen können wir schließen, dass eine Authentifizierung über ein Passwort allein nur dann für eine Zugangskontrolle genügend Sicherheit bieten kann, wenn für Passwörter geeignete Qualitätskriterien eingehalten werden und wenn für die im System zu speichernde Referenzdatei (Passwortdatei) besondere Vorkehrungen getroffen werden.

Bei zurzeit üblichen Rechnerleistungen gilt ein Passwort als sicher, wenn es aus

- mindestens 8, besser aber 10 Zeichen besteht

- Groß- und Kleinbuchstaben sowie Ziffern enthält

- mindestens ein Sonderzeichen enthält

- als Zeichenkette nicht in einem Wörterbuch oder Sachbuch als Teilstring zu finden ist

Die ersten drei Forderungen zielen darauf ab, den Schlüsselraum möglichst groß zu halten, um dadurch einer Brute-Force Attacke standhalten zu können (Beispiel: Schlüssellänge 6 Zeichen, nur Kleinbuchstaben liefert 26^6 + 26^5 + 26^4 + 26^3 + 26^2+ 26 = 321.272.406 Möglichkeiten, die leicht durchzuprobieren sind (siehe Tabelle).

Ein zufälliges Erraten eines sicheren Passwortes beim Login scheidet praktisch aus, denn der Administrator kann als Sicherheitspolitik vorgeben, dass bei jeder Eingabe eines falschen Passwortes die Zeit bis zum nächstmöglichen Login jedes Mal erhöht wird und nach n erfolglosen Versuchen der betreffende Account überhaupt gesperrt wird. Die Sicherheitspolitik kann auch festlegen, dass von Benutzern in definierten Abständen das Passwort zu ändern ist. Eine Verschärfung dieser Politik ist das Mitführen einer Passworthistorie, z.B. für die letzten n verwendeten Passwörter, sodass ein Benutzer nicht kurz ein neues Passwort definieren und anschließend dieses wieder auf eines der zuletzt verwendeten rücksetzen kann. Diese Politik nennt man Password-Aging.

Gelingt es aber einem Angreifer, sich der Passwortdatei zu bemächtigen bzw. darauf zuzugreifen, kann man alle möglichen Zeichenkombinationen (im Prinzip jedenfalls) erzeugen und durchprobieren, sofern nicht dagegen systemseitig besondere Vorkehrungen getroffen worden sind, was noch zu diskutieren ist. Hinzuzufügen ist, dass ein Systemadministrator immer Zugriff auf das Passwortfile hat und alleine schon deswegen insbesondere das Passwort für den Administrator-Account besonders heikel ist.

Die folgende Tabelle zeigt den Zeitaufwand für eine Brute-Force Attacke. Verwendet wurde eines der vielen im Handel erhältlichen Programme, das unter Windows die benötigten Informationen aus der Registry ausliest, in einen temporären File ablegt und dann eine Attacke gegen ein ausgewähltes Benutzerkonto fährt. Zum Einsatz kam ein Laptop mit Intel Dualprozessor und 2 GHz Taktrate und mit der Berechtigung, ein Login als Administrator machen zu können.

Dasselbe Programm ermöglicht auch einen Abgleich mit einzuspielenden bzw. bereits mitgelieferten Wörterbüchern, womit auch die vierte Forderung für sichere Passwörter nachzuvollziehen ist.

Tabelle L1  Zeitaufwand für Brute-Force Passwort Attacken

L.3.2 Kryptographische Hashfunktionen

Wir überlegen nun systematisch, was seitens eines Betriebssystems zur Passwortsicherung beigetragen werden kann.

Der erste Ansatz wäre, die Passwortdatei mit einem nur dem Administrator bekannten Schlüssel zu verschlüsseln und als zusätzlichen Schutz das Ergebnis in einer Shadow-Datei zu verstecken, auf die nur der Administrator bzw. das System Zugriff haben. Neben dem Problem des Schlüsselgeheimnisses ist der besondere Nachteil, dass der Administrator diese Datei auslesen und damit alle Benutzerkennwörter dechiffrieren kann. Der Administrator muss zwar die Möglichkeit haben, ein (z.B. vergessenes) Benutzerpasswort zurücksetzen zu können, sollte aber als Systemadministrator in einem Netz keine Information über die Klartexte der von den Benutzern verwendeten Passwörter haben. Genau aus diesem Grunde wird beim Anlegen eines Benutzerkontos dieses mit einem Initialpasswort versehen und der Benutzer beim ersten Login aufgefordert, dieses zu verändern. (Ausnahmen bestätigen die Regel: Es kann ein Account für mehrere Benutzer gemeinsam eröffnet werden und dann darf das vereinbarte Kennwort von Einzelnen nicht verändert werden. Letzteres kann beim Anlegen eines Accounts mittels eines Parameters verhindert werden).

Die Lösung zum geschilderten Problem besteht in der Verwendung von kryptographischen Hashfunktionen. Eine kryptographische Hashfunktion h(m) hat folgende Eigenschaften:

- aus einer Zeichenfolge m mit beliebiger Länge |m| wird eine Zeichenfolge a mit fester Länge |a| erzeugt. Diese heißt Message Authentification Code (MAC): h(m)=a.

Typischerweise – in Bit angegeben – ist |a|=128, 160 oder 256.

- Die inverse Funktion h-1(a)=m muss schwierig, d.h. praktisch nicht möglich, zu berechnen sein.

- h(m) soll kollisionsresistent sein, d.h. nach Möglichkeit sollten keine Kollisionen

h(m) = h(m’) mit m ≠ m’ auftreten.

Insbesondere die letztere Eigenschaft ist noch präziser zu beschreiben, da bei |m|>|a| es Kollisionen geben muss, wenn man alle m betrachtet.

Mit „praktisch unmöglich“ meint man, dass außer dem Durchprobieren aller Möglichkeiten (beachte: |m|>>>|a|) und einiger anderer kryptographischer Angriffsmethoden – die hier nicht besprochen werden – es keinen effizienten Algorithmus zur Berechnung von m bei gegebenem MAC a geben darf.

Dabei heißt nicht effizient, dass in praktischen Anwendungsfällen die Laufzeit T(m) (Rechenzeit) eines Verfahrens so hoch ist (Jahrzehnte und länger), dass das Verfahren eben praktisch unbrauchbar ist: T(m) ist nicht polynomialzeitbeschränkt, T(m) wächst exponentiell mit der Länge |m| von m an.

Somit definieren wir:

- Einwegfunktion: h(m) heißt Einwegfunktion, wenn es von trivialen Sonderfällen abgesehen praktisch unmöglich ist, zu einem beliebigen MAC ein Urbild m= h-1(a) zu finden:

- Schwach kollisionsresistent: Die Funktion h heißt schwach kollisionsresistent, wenn es zu einem vorgegebenen m praktisch unmöglich ist, ein m’ mit einer Kollision h(m) = h(m’). zu finden:

- Stark kollisionsresistent: Die Funktion h heißt stark kollisionsresistent, wenn es praktisch nicht möglich ist, irgendeine Kollision zu finden, also zwei m, m’ zu finden, sodass h(m)=h(m’). „Stark kollisionsresistent“ impliziert „schwach kollisionsresistent“.

- Kryptographische Hashfunktion: Die Funktion h(m) ist eine kryptographische Hashfunktion, wenn sie stark kollisionsresistent ist.

Damit das System den Hashwert (MAC) des eingegebenen Passwortes mit dem gespeicherten vergleichen kann, bedarf es eines Übereinkommens über das verwendete Verfahren.

Es gibt dazu einige international spezifizierte Methoden (Auswahl):

- MD5 (Message Digest Algorithm5) mit 128 Bit für den MAC. (1991, Ronald L.Rivest).

Das Verfahren wird eingesetzt zur Überwachung der File-Integrität in Betriebssystemen wie Linux, Unix, Mac OS X. Allerdings wurden im Jahr 2004 Kollisionsangriffe entdeckt, sodass das Verfahren abgelöst werden wird.

- SHA-1 (Secure Hash Algorithm) mit 160 Bit, (1995, NIST (Nat.Inst.of Standards and Technology)). Bemerkenswert ist, dass im August 2006 (Tagung Crypto2006) neue Angriffe vorgestellt wurden, sodass sich auch hier eine Ablöse abzeichnet.

- SHA-2 (Varianten SHA- 256, SHA -384 …) Löst in Zukunft SHA-1 ab, (NIST 2002, 2004 (SHA-254)).

Zu den bekannt gewordenen Attacken bzw. Schwächen ist folgendes zu bemerken: man kann bei den verwendeten Verfahren nicht allgemein beweisen, dass sie die verlangten Eigenschaften haben. Stattdessen versucht weltweit die Gemeinschaft der Kryptologen mit erprobten und systematischen Attacken erfolgreich zu sein. Hält ein Verfahren aber allen diesen Angriffen stand, so wird es als „sicher“ angesehen. Wie sich zeigt, kann sich eine solche Einschätzung aber ändern.

Alle wichtigen Verfahren sind im Prinzip ähnlich. Daher stellen wir hier das einfachere MD5 Verfahren vor und verwenden dazu folgende Schreibweisen

Als Input sei ein Bit-String M der Länge m < 264 angenommen, d.h., wie Zeichen als Bits codiert werden, lassen wir hier weg. Der resultierende MAC hat die Länge 160 Bit.

Folgende Initialkonstanten (in Hex) werden verwendet:

A = 0x67452301

B = 0xEFCDAB89

C = 0x98BADCFE

D = 0x10325476

E = 0xC3D2E1F0

Zunächst wird der Inputstring M (der Länge m) in Blöcke zu je 512 Bit wie folgt unterteilt. Dann wird an M 1 angehängt, gefolgt von 0…0, sodass der letzte Block genau 448 Bits hat. In den dort verbleibenden 64 Bits wird die Länge m des Inputstrings (im big endian Format) eingetragen. Das Ergebnis dieser Aufbereitungsphase sind dann n Blöcke zu je 512 Bits.

Abb. L3   Blockstruktur beim MD5

Der erste der 512 Bit langen Blöcke wird nun aufgeteilt in 16 Wörter mt, t = 0, …, 15 zu je 32 Bit Länge und wird anschließend auf 80 Wörter wt zu je 32 Bit mit wt = mt für t = 0, 1, ..., 15 „aufgeblasen“. wt = (wt-3 ⊕ wt-8 ⊕ wt-14 ⊕ wt-16) <<< 1 für t = 16, 17, …, 79

Es folgen 80 „Runden“, bei denen folgende Funktionen abhängig von t verwendet werden:

Ferner werden in der „for-Schleife“ (s.u.) spezielle Konstanten verwendet:

kt = 0x5A827999         t =  0, …, 19

kt = 0x6ED9EBA1       t = 20, …, 39

kt = 0x8F1BBCDC      t = 40, …, 59

kt = 0xCA62C1D6       t = 60, …, 79

Die in for (t = 0; t < 80; t++) verwendeten Variablen a, b, c, d, e werden mit A, B, …, E initialisiert und bilden vorab einen ersten Hashwert mit 160 Bit Länge:

a = A d.h. a = 0x67452301

...

e = E d.h. e = 0xC3D2E1F0

Dann aber werden in der 80 Runden umfassenden Schleife for (t = 0; t < 80; t++) diese Variablen a, b, c, d, e verändert und liefern einen Hashwert h mit 160 Bits.

Code L1   Ausschnitt MD5 Hasherzeugung

Diese a, b, c, d, e werden dann (anstatt von A, …, E beim ersten Block) für den zweiten Block als Initialwerte verwendet, usw., bis alle Blöcke und damit der Eingabestring abgearbeitet sind.

L.3.3 Salzen

Die Verwendung einer (vereinbarten) kryptographischen Hashfunktion wie MD5 oder SHA-2 bringt den Vorteil, dass die Passwörter sicher im System abgelegt und dennoch mit dem Eingabepasswort verglichen werden können, ohne dass Dritten, auch nicht dem Administrator, der Klartext des Passwortes bekannt wird.

Es verbleiben noch zwei Probleme. Neben schwachen, weil zu kurzen Passwörtern, die durch eine Brute-Force Attacke aufgedeckt werden können, sind noch Wörterbuch-Attacken abzuwehren.

Solche sind im Prinzip Brute-Force Verfahren, aber eingeschränkt auf die im verwendeten Wörterbuch eingetragenen Wörter oder Silben oder Kombinationen davon.

Man muss auch davon ausgehen, dass sehr oft Benutzer als Passwort leicht zu merkende Wörter verwenden und diese bestenfalls – z.B. um einer vom Systemadministrator vorgegebenen Sicherheitspolitik zu genügen – um Ziffern und auf der Tastatur zu findenden Sonderzeichen ergänzen: „Lotte4b*“.

Findet sich „Lotte“ im verwendeten Wörterbuch, so braucht man nur mehr diese Zeichenkette mit allen verbleibenden Zeichenkombinationen zu konkatenieren (aneinanderreihen) und dann auszuprobieren.

Nehmen wir an, dass eine Passwortlänge von 8 Zeichen vermutet wird und mit einer Ergänzung von bis zu drei Groß- und Kleinbuchstaben, Ziffern und Sonderzeichen (rund 20 auf der Tastatur) gerechnet wird: 26 + 26 + 10 + 20 = 82 Zeichen. Demnach sind 1 + 82 + 82^2 + 82^3 = 558.175 Zeichenketten durchzuprobieren. Das sind nicht viele, auch wenn man dies für jeden Eintrag im Wörterbuch zu wiederholen hat. Der Duden beispielsweise hat rund 120.000 Stichwörter, mit einem PC nach dem Stand der Technik kann man aber pro Sekunde bis zu 30 Millionen Vergleiche durchführen, wenn die MACs vorab schon berechnet werden: Eine Beschleunigung einer Wörterbuch-Attacke kann man nämlich erreichen, indem man alle Einträge einschließlich der vorhin geschilderten Zeichenkombinationen vor dem Angriff der Hashfunktion zuführt oder von Dritten eine derart aufbereitete Tabelle erhält.

Dieser Ansatz hat allerdings seine Grenzen für den allgemeinen Fall „sicherer“ und vor allem langer Passwörter, weil die Länge derart aufbereiteter Tabellen exponentiell anwächst.

Es gibt außerdem über Rainbow Tables weiter verbesserte Attacken. Ein Rainbow Table ist eine spezielle Datenstruktur, in der Passwortsequenzen dargestellt werden, sodass der Tabellenumfang drastisch reduziert werden kann.

Um derartige Angriffe abzuwehren oder zumindest zu erschweren, verwendet man „Salz“ und spricht von Salted Hash.

Das Prinzip ist wie folgt: Man ergänzt ein Passwort durch Anhängen einer Zufallszahl (bzw. zufälligen Zeichenkette), die als Salz bezeichnet wird. Erst auf diese (längere) Zeichenkette wird die in Betracht kommende Hashfunktion angewandt. Zusätzlich jedoch muss die Zufallszahl im Klartext gespeichert werden, damit ein Passwortvergleich noch durchgeführt werden kann.

Durch dieses Salz wird ein Passwort nicht nur länger und sicherer. Es ist auch der resultierende Hashwert selbst dann verschieden, wenn zwei Benutzer dasselbe Passwort gewählt haben, da bei entsprechender Qualität des Zufallszahlengenerators unterschiedliches Salz angehängt wird. Dies alleine ist schon ein besonderer Vorteil.

Mit Salz werden Wörterbuchangriffe, auch mit dem Rainbow-Verfahren, deutlich erschwert: Man müsste nämlich jeden Wörterbucheintrag um alle möglichen Salzwerte ergänzen, möchte man im Voraus erstellte Tabellen mit Hashwerten zum Einsatz bringen. Ansonsten muss für jeden Eintrag das Salz ausgelesen werden und erst dann kann der MAC berechnet und mit dem tatsächlichen verglichen werden.

Ergänzend halten wir noch fest, dass einige Betriebssysteme zusätzlich noch den Passwordfile mit einem systeminternen Schlüssel verschlüsseln.

L.4 Schutz von Objekten

L.4.1 Aufgabenstellung

In einem Betriebssystem einschließlich der zugehörigen Filesysteme sind Objekte vor unbefugtem Zugriff zu schützen (Protection, Datensicherheit). Typische Vertreter solcher Objekte o einer Objektmenge O sind Files, die ihrerseits Datenfiles oder Programme sind.

Aber auch viele andere (Hardware-)Betriebsmittel des Systems wie periphere Geräte, Schnittstellen nach außen usw. gehören zur Objektmenge O. Ebenso gehören Verzeichnisse eines Datensystems und softwaremäßig definierte Strukturen wie Semaphore oder Pagetables dazu. Objekte haben einen eindeutigen Namen (Bezeichner) und es ist vorab erklärt, welche Operationen {op} darauf überhaupt ausführbar sind.

Auf diese o ∈ O wollen Subjekte s einer Subjektmenge S zugreifen. Ein s ∈ S kann ein Benutzer bzw. ein von ihm gestarteter Prozess sein. Da ein Subjekt s_1 durchaus auch auf ein anderes Subjekt  s_2 einwirken kann, sind die Mengen S und O nicht notwendigerweise disjunkt. Ein Beispiel dazu ist, dass der Prozess (Subjekt)  s_1 einen anderen Prozess  s_2 anhalten möchte.

Die allgemeine Situation ist demnach, dass Subjekte s ∈ S auf Objekte o ∈ O bestimmte Operationen {opi} ausführen möchten und daher zu prüfen ist, ob dafür entsprechende Rechte r ∈ R vorliegen.

Bei einem begehrten Zugriff werden wir immer annehmen, dass die darin angegebene Operation auf dem Objekt erklärt ist.

Die Rechtemenge R (rights, permissions, beide Wörter sind nicht ganz deckungsgleich) wird hier nicht näher spezifiziert. Beim Zugriff auf Files sind dies typischerweise Rechte für read, write und bei Files, die ausführbare Programme enthalten, zusätzlich das Recht auf die Operation execute. In vereinfachter Sprech- und Schreibweise meint man z.B. mit read das Recht, die Operation read auszuführen.

Besondere Rechte r ∈ R sind jene, die es gestatten, ein neues Objekt o anzulegen (create, wir schreiben auch create(o)), ein bestehendes zu entfernen (delete, destroy), ein neues Subjekt s anzulegen oder wieder zu entfernen.

Auch gilt es, bestehende Rechte eines Subjektes  s_1 auf ein anderes  s_2 ∈ S zu übertragen (confer) oder einem Subjekt  s_3 bestehende Rechte wegzunehmen (revoke). Analoges gilt für Objekte.

Zur Realisierung der Schutzmechanismen dient ein Referenzmonitor.

Abb. L4  Referenzmonitor

Ein Subjekte s ∈ S begehrt (request) eine Operation auf ein Objekt o ∈ O. Der Referenzmonitor hat als Wächter (guard) zu prüfen, ob einem solchen Begehren stattgegeben werden darf, also ob s auf o die Rechte für den Request wirklich hat.

L.4.2 Modelle

Um diese Aufgabenstellung zu beschreiben, bedarf es eines Modells und dazu verwenden wir eine Zugriffskontroll-Matrix M (Access Control Matrix), kurz: Zugriffsmatrix, wie folgt:

Gegeben sind

S: eine Menge von Subjekten (auch: Principals)

O: eine Menge von Objekten

R: eine Menge von (Zugriffs)-Rechten

Dann ist eine Zugriffskontrollmatrix M erklärt als M = {M[s,o]} mit s ∈ S, o ∈O und M[s,o] ∈ R; d.h., M: S ∈ O → 2R (Potenzmenge von R)

Wenn z.B. read ∈ M[s,o], so bedeutet dies, dass das Subjekt s ∈ S das Recht hat, das Objekt o ∈ O zu lesen .

Die nachstehende Abbildung zeigt einen Ausschnitt einer Zugriffskontroll-Matrix M mit den Subjekten A, B, FTP-Server und Objekten File1, File2, Port20, Port21.

Tabelle L2  Beispiel Zugriffskontroll-Matrix

Im Beispiel kann das Subjekt (der Prinzipal) A alles lesen, was B auf File2 schreibt: M[A,File2] = {read}, M[B;File2]={read,write}

Wie erwähnt bedarf es auch Operationen wie create(s), create(o), destroy(o) oder revoke(r,s,o). Diese Operationen verändern die Matrix M, sodass man bei S und O genau genommen von den Mengen der momentan (current) erklärten Subjekte und Objekte sprechen muss.

Man kann auch aus administrativen Gründen Benutzer {si} zu Gruppen {gj} zusammenfassen, sodass dann auch Gruppen als solche zur Subjektmenge S gezählt werden. Gruppen bringen eine Verwaltungsvereinfachung, wenn Benutzer mit gleichen Zugriffsrechten zusammengefasst werden. In diesem Sinne kann man Gruppen dann als „virtuelle“ Subjekte verstehen.

Interessant ist noch der Begriff der Konfiguration eines Schutzsystems:

Das Tripel (S, O, M) heißt Konfiguration eines Protection-Systems, wobei S die Menge der momentanen Subjekte, O die Menge der momentan eingerichteten Objekte und M die korrespondierende Acess-Control-Matrix ist.

Ein allgemeines Protection System hat demnach nicht nur zu prüfen, ob eine von einem Subjekt angeforderte Operation auf ein Objekt o ∈ O zugelassen werden darf. Es muss auch sichergestellt werden, dass bei einer die Menge S (vgl.: create(s)), O oder M verändernden Operation die resultierende Konfiguration (S´,O´,M´) zulässig ist.

Wir erklären hier nicht formal weiter, was man unter einem zulässigen Übergang (S,O,M) → (S´,O´,M´) genau versteht, geben aber dafür ein einfaches Beispiel:

Im Allgemeinen hat das Subjekt s = „administrator“ umfassende Rechte. Demnach kann das Subjekt „administrator“ einem anderen Benutzer (Subjekt „helpadmin“) seine Rechte prinzipiell übertragen (vererben). Gleichzeitig aber soll nach einer vorgegebenen Sicherheitspolitik der diese Rechte erbende „helpadmin“ seinerseits nicht an weitere Subjekte solche Rechte vererben dürfen.

Dieser Aspekt führt zu einem besonderen Sicherheitsproblem, das als Confinement Problem bezeichnet wird. Betrachten wir die Zugriffsmatrix aus Abb. L5. Dort ist M[A,File2] = {} und M[B,File2] ={read, write}. Das Ziel ist, dem Subjekt A das Lesen von Daten nicht zu erlauben, die ein Geheimnisträger B produziert und auf File2 ablegt. Anderseits hat B umfangreiche Privilegien, insbesondere das Recht, von A auf File1 abgelegte Daten nicht nur zu lesen, sondern auch dorthin zuschreiben. Es soll ja B Mittelungen an A machen können.

Auf Grund der vorgenommenen Rechtevergabe kann aber B aus Ungeschicklichkeit oder absichtlich wegen M[B,File2] ={read, write} einen vertraulichen Text in File2 ablegen, diesen kopieren ({read}) und mit dem Recht M[B,File1] = {read, write} in File1 schreiben und so ein Geheimnis verraten.

Abb. L5  Informationsfluss

Im Beispiel hängt eine Zugriffsprüfung für einzelne Benutzer (Subjekte) und einzelne Objekte nur von M ab, also welche Rechte dem Paar (s,o.) zugestanden sind.

In diesem Fall spricht man von einem DAC-System (Discretionary Access Control System):

Diese benutzerbestimmte Zugriffskontrolle bedeutet, dass die Zugriffsrechte pro identifizierten Benutzer festgelegt werden.

Führt man zusätzlich globale Regeln ein, die auch Übergänge (S,O,M) → (S´,O´,M´) betreffen oder zusätzlich prüfen, unter welchen Umständen welche Rechte bei Informationsflüssen in Anspruch genommen werden dürfen, so spricht man von MAC-Systemen (Mandatory Access Control System, zwingend erforderliche Zugriffs- bzw. Zugangskontrolle).

Lässt man auf einem höheren Abstraktionsniveau das Zusammenfassen von Subjekten anhand ihrer in einem System (bzw. allgemein: betrieblichen Organisation, in der das Computersystem ein Werkzeug ist) zu erfüllenden Aufgaben (Rollen) zu, so spricht man von einem RBAC-System (Role Based Access Control System).

Im einfachsten Fall kann RBAC mittels S = {roles} als DAC-System modelliert werden, erlaubt aber im allgemeinen Fall weitergehende Politiken, die über DAC hinausgehen.

L.4.3 Implementierung der Zugriffsmatrix

Die Zugriffsmatrix dient als Modell und wird nicht unmittelbar als Datenstruktur vom Typ Matrix implementiert. Sie ist eine extrem dünn besetzte Matrix mit vielen Spalten (Objekten).Viele Einträge M[s,o] bleiben leer, weil meist ein Subjekt s nur auf relativ wenige Objekte o ∈ 0 irgendein Zugriffsrecht besitzt.

Somit gibt es im Prinzip zwei herausragende Methoden, um {M[s,o]} zu speichern: spaltenweise oder zeilenweise, wobei in beiden Fällen Listen erhalten werden, in der nur die M[s,o] ≠  ∅ abgelegt sind. Für die konkrete Speicherung der Listen sind verschiedene Methoden möglich.

L.4.3.1 Zugriffskontrolllisten (ACL)

Speichert man für {M[s,o]} jede ein Objekt repräsentierende Spalte gesondert ab, so hat man für jedes Objekt übersichtlich zusammengefasst, welche Subjekte welche Rechte darauf haben. Eine solche Liste heißt Zugriffskontrollliste (Access Control List, ACL).

Im eingangs abgebildeten Beispiel erhalten wir:

File1: (A: {read, write}, FTPServer: {read, write})

File2: (A: {read}, B: {read, write})

Port20: (FTPServer: {receive, send})

Port21: (FTPServer: {receive})

Die Aufnahme von Gruppen als ACL Einträge bringt einen Implementierungsvorteil. Man muss dann nicht mehr zu jedem einzelnen Objekt alle Subjekte mit ihren Rechten einzeln abspeichern, auch wenn sie dieselben Rechte darauf haben. Es genügt, nur die Gruppe, der sie angehören, in der ACL einzutragen und die Rechte der Gruppe zu spezifizieren.

Konsequenterweise gibt es z.B. bei Windows neben dem Security Identifier (SID) als Repräsentant der betriebssysteminternen Identität für jeden User auch einen GroupSID für jede eingerichtete Gruppe.

Um diesen Ansatz voll ausnützen zu können, bedarf es seitens der Systemadministration einer Planung vor dem Einrichten von Gruppen, vor allem, wenn Gruppen selbst wieder Gruppen enthalten sollen. Auch kann man Gruppen dazu einsetzen, um betriebliche Organisationsformen bei den Zugangsberechtigungen abzubilden, wenn man unterstellt, dass Mitarbeiter mit den selben Aufgaben auch die selben Rechte am System haben sollten.

Zur Erleichterung sind vorab Standardgruppen mit vordefinierten Rechten vorgesehen, wie z.B. die Gruppen administrators, users, everyone. (Man beachte: In Windows ist der Benutzer Administrator Mitglied der Gruppe Administrators und man kann den Benutzer PJH zur Gruppe Administrators hinzufügen).

Der wesentliche Nachteil bei ACLs besteht im Aufwand, zu einem gegebenen Subjekt s bzw. zu einer Gruppe g herauszufinden, welche Rechte auf welche Objekte zugeteilt worden sind.

L.4.3.2 Fähigkeiten (Capabilities)

Um dem bei ACLs genannten Nachteil zu begegnen, kann man anstatt von Spalten von Zeilen von M ausgehen und speichert für jedes einzelne Subjekt s die zugehörige Zeile M[s,*] als Liste ab. Damit weiß man sofort, welche Rechte {r} laut Zugriffsmatrix für das Subjekt vorgesehen sind.

Man spricht hier von einer Liste von Fähigkeiten, wobei sich als Fachbegriff Capability-List auch im Deutschen durchgesetzt hat.

Der erzielte Vorteil bringt einen Nachteil mit sich, der gewissermaßen reziprok zum ACL-Konzept ist: welche Subjekte si auf ein bestimmte Objekt zugreifen dürfen, kann im Prinzip nur durch Inspektion aller Zeilen von {M[si,o]} eruiert werden.

Eine Capability legt für ihren Besitzer jene Rechte fest, die er auf ein bestimmtes Objekt hat. Demnach führt ein z.B. Prozess eine Liste von Capabilities mit sich und bei einem versuchten Zugriff auf ein Objekt kann durch Inspektion der Liste eruiert werden, ob dieser gestattet werden kann.

Losgelöst von der Struktur einer Zugriffsmatrix ist der Capability-Ansatz an sich ein sehr allgemeines Konzept: ein Prozess „trägt die Information mit sich“, die er für einen erfolgreichen Zugriff auf ein Objekt benötigt.

Insofern kann man einen Frame im Hauptspeicher als ein Objekt ansehen und den zugehörigen Eintrag in der Page Table des Prozesses als Capability: Die Page Table liefert nämlich nicht nur die physikalische Adresse des Frames, sondern enthält auch für jeden Frame Berechtigungen wie read, write, execute.

L.4.4 Vergabe von Berechtigungen in Windows

L.4.4.1 Beschreibung des Szenarios

Zur Vertiefung soll ein „minimales“ Beispiel aus dem Betriebssystem Windows dienen. Zwei Benutzer Joerg und Rudi verwenden gemeinsam ein Dokument Sec1.doc, welches sich im Verzeichnis Skripten befindet. Während Joerg als Autor diese Datei schreiben und lesen darf, hat Rudi als Lektor auf die Datei nur Leseberechtigungen.

Im Sinne der bisherigen Modellierung kann diese Situation wie folgt dargestellt werden:

Subjekte S = {Joerg, Rudi}

Objekte O = {Skripten, Sec1.doc}

Berechtigungen R = {Read, Write}

Tabelle L3  Beispiel: Zugriffskontroll-Matrix M

L.4.4.2 Sicht von Windows

Bei der Abbildung dieser Situation auf Windows sind mehrere Punkte zu beachten:

Jedes Subjekt s ∈ S wird intern durch einen eindeutigen Security Identifier (SID) repräsentiert. Die Zeilen von M werden damit über die vergebenen SIDs adressiert.

Die Zugriffskontroll-Matrix M ist als Access Control Listen (ACL) implementiert. Bei jedem Objekt ist also die entsprechende Spalte der Zugriffskontroll-Matrix M abgelegt. Jeder der ACE (Access Control Entry) genannten Einträge in der ACL gibt an, wer was auf dem Objekt machen darf.

Jeder ACE besteht also aus einem Wertetupel <SID, Berechtigungen>.

Die Berechtigungen in Windows sind leistungsfähig (und komplex). Sie enthalten neben Read und Write zusätzliche Abstufungen für weitere Operationen. Am Beispiel von Dateien in der vereinfachten Sicht ergibt sich bereits ein partiell geordnetes Set von Berechtigungen, welches in Abb. L6 als Hasse-Diagramm dargestellt ist.

Abb. L6  Beispiel: Hasse-Diagramm

Bei Read bzw. Read & Execute ist die Abstufung durch die Namensgebung bereits klar beschrieben.

Modify enthält nicht nur Read, Execute und Write, sondern erlaubt zusätzlich das Löschen des Objektes. (Da Benutzer Joerg seine Dateien bei Bedarf auch wieder löschen können soll, wird daher anstatt Read + Write zukünftig Modify verwendet.

Full Control repräsentiert alle überhaupt möglichen Berechtigungen auf dem Objekt inklusive der Erlaubnis, auch (!) den ACL-Eintrag zu ändern.

Die folgenden Abbildungen zeigen das einfache Szenario als (teilweise gekürzte) Bildschirmausgaben von Windows Vista Business: Abb. L7 zeigt die Liste der Benutzer,

Abb. L7  Beispiel: Liste der Benutzer

Abb. L8 und Abb. L9 zeigen die Access-Control-Liste (ACL) auf der Datei Sec1.doc. In Abb. L8 sind die Informationen für Joerg zu sehen, in Abb. L9 von Rudi.

Abb. L8  Beispiel: ACL der Datei Sec1.doc für Joerg

Abb. L99  Beispiel: ACL der Datei Sec1.doc für Rudi

Auch auf dem Verzeichnis Skripten benötigen die Benutzer Berechtigungen, z.B. um die Dateien im Verzeichnis aufzulisten. Abb. L10 und Abb. L11 zeigen die ACL dazu. Die Berechtigungen dafür sind Read und für die Anzeige des Verzeichnisinhaltes die Berechtigung List folder contents. Diese Kombination wird in der Zugriffskontrollmatrix M im folgenden Text mit Read, List dargestellt.

Abb. L10  Beispiel: ACL des Verzeichnisses Skripten für Joerg

Abb. L11  Beispiel: ACL des Verzeichnisses Skripten für Rudi

L.4.4.3 Erzeugen von neuen Objekten

Nach dieser statischen Sicht auf die Berechtigungen in unserem „Mini-Szenario“ lassen wir die beiden Benutzer einmal arbeiten: Da Joerg weitere neue Unterlagen produziert, legt er bald eine neue Datei Sec2.doc an. Damit erhält formal gesehen die Matrix M eine neue Spalte.

Wir besprechen nun, welche Berechtigungen auf dieser neuen Datei gelten.

Die Wunschberechtigungen im Szenario sind klarerweise, dass – analog zur Datei Sec1.doc – Joerg diese Datei ändern und Rudi diese nur lesen kann.

Diese Berechtigungen sollen ohne spezielle Intervention des Benutzers, welcher die Datei anlegt, wirksam werden. Es ist auch nicht sinnvoll, die Vergabe von Berechtigungen für neue Objekte der Intervention der Administration zu überlassen, denn in echten Umgebungen mit vielen tausenden neuen Objekten/Dateien jeden Tag wäre dies praktisch nicht möglich. „Händische Vergabe von Berechtigungen für neue Objekte nur in Ausnahmefällen durch die Administration“ ist daher die Devise.

Die bei Windows als (ein) Standard vorgesehenen Berechtigungen für ein neues Objekt hängen davon ab, wo in welchem Verzeichnis (oder genereller Container), ein Objekt angelegt wird. In Windows werden die Berechtigungen vom Verzeichnis Skripten automatisch auf die neue Datei Sec2.doc vererbt. Dieses Prinzip der Vererbung von Berechtigungen vom Container auf die darin neu erstellen Objekte ist als Konzept weit verbreitet.

Im Beispiel erhält also Joerg die Modify-Berechtigung auf die Datei Sec2.doc, während Rudi sich wiederum mit Leseberechtigungen begnügen muss. Abb. L12 und Abb. L13 zeigen die von Joerg neu angelegte Datei samt der durch die Vererbung implizit vergebenen Berechtigungen.

Abb. L12  Beispiel: ACL einer im Verzeichnis Skripten neu errichteten Datei für Joerg

Windows zeigt durch die graue (anstatt der schwarzen) Farbe der Häkchen an, dass die Berechtigungen nicht direkt auf dem Objekt vergeben wurden, sondern vom Verzeichnis geerbt werden.

Die „Advanced View“ bietet nicht nur eine feinere Abstufung der Berechtigungen, sondern auch noch eine genauere Konfiguration der Vererbung. Darauf wird hier nicht näher eingegangen.

Abb. L13  Beispiel: ACL einer im Verzeichnis Skripten neu errichteten Datei für Rudi

L.4.4.4 Erzeugen von neuen Subjekten

Während faktisch alle Benutzer und Benutzerinnen (= Subjekte) Objekte erzeugen können, ist das Erzeugen neuer Subjekte hingegen meist der Administration vorbehalten. Mit jedem neuen Subjekt bzw. Benutzer-Account wächst – abstrakt gesehen – die Zugriffskontroll-Matrix M um eine Zeile.

Im Szenario wird Michael als zusätzlicher Lektor aufgenommen.

Die Administratorin / der Administrator legt den Account für Michael an. Zusätzlich sind die Berechtigungen für Michael zu vergeben. Im Beispiel sind das nur das Verzeichnis Skripten und die darin enthaltenen Dateien. In der Realität sind viele Berechtigungen auf viele Objekte in vielen verschiedenen Containern dafür zu setzen.

Die Zugriffskontroll-Matrix M als schematische Repräsentation der Berechtigungen hat damit die in Tabelle L4 gezeigte Form.

Tabelle L4   Beispiel: Zugriffskontroll-Matrix M für weitere Benutzer

L.4.4.5 Gruppe für viele/mehrere Subjekte

Die Zweckmäßigkeit des Gruppenkonzeptes wurde schon erwähnt: Der hohe Aufwand für das Administrationsteam zum Setzen der Berechtigungen für neue Accounts (im Szenario) erfordert eine Erweiterung durch Zusammenfassen in Gruppen.

Nehmen wir in unserem Beispiel an, dass noch zusätzliche Lektoren und Lektorinnen angestellt werden. Damit können die ACLs durch ihre Länge bereits unhandlich und unübersichtlich werden. Abb. L14 zeigt als Beispiel die ACL auf einer der nun vielen Dateien im Verzeichnis Skripten.

Daher wird die Konfiguration wie folgt umgestellt:

- Es erhält nicht mehr jede Lektorin und jeder Lektor quasi ad personam die für die Aufgabe notwendigen Berechtigungen, sondern

- die Berechtigungen werden an eine Gruppe (mit Namen LektorInnen) vergeben.

- Jede Lektorin und jeder Lektor wird Mitglied der Gruppe LektorInnen und erhält durch die Mitgliedschaft auch die Berechtigungen der Gruppe.

Abb. L14  Beispiel: ACL auf Datei sec9999.doc mit weiteren LektorInnen

Abb. L15  Beispiel: ACL auf Verzeichnis Skripten für Autor Joerg und die Gruppe der LektorInnen

Damit spart das Administrationsteam viel Zeit und auch die Fehlermöglichkeiten werden kleiner: Nach dem Anlegen eines Accounts ist nur noch die Gruppenmitgliedschaft einzutragen, was in Abb. L16 zu sehen ist.

Zusätzlich werden die ACLs wieder sehr kurz, wie Abb. L15 auf dem Verzeichnis Skripten im Vergleich mit der vorigen Variante in Abb. L14 zeigt. Diese kurzen ACLs bieten nicht nur besseren Überblick, sondern machen es auch dem Betriebssystem leichter, welches ja diese Listen durchsuchen muss.

Die Vererbung von Berechtigungen wurde bereits in Punkt L.4.4.3 besprochen. Hat das Admi-nistrationsteam die Gruppe LektorInnen bereits vorab in die ACL des Verzeichnisses Skripten eingetragen, so erhalten alle von Joerg später dort angelegten Dateien automatisch die richtigen Berechtigungen.

Abb. L16  Beispiel: Gruppenmitgliedschaft(en) in Windows

L.4.4.6 Erweiterung auf mehrere Gruppen

Bisher wurde nur eine Gruppe in das Szenario aufgenommen. In realen Umgebungen sind Benutzer und Benutzerinnen Mitglied in mehreren Gruppen, da sie oft auch in mehreren Bereichen eingesetzt werden. Und auch das Betriebssystem selbst verwendet mehrere Standard-Gruppen, um Benutzern das Login zu erlauben und andere Standard-Berechtigungen zu vergeben.

Eine Benutzerin Christine in einem erweiterten Szenario wäre dann vielleicht – zusätzlich zur Mitgliedschaft bei den LektorInnen – auch Mitglied in weiteren Gruppen wie beispielsweise „SkriptenDruck“. Abb. L17 zeigt, wie diese Gruppe die Berechtigung erhält, auf einem speziellen Schnelldrucker auszudrucken. Wir sehen also, dass nicht nur auf Verzeichnissen und Dateien Berechtigungen überprüft werden (können).

Abb. L17  Beispiel: Berechtigungen der Gruppe SkriptenDruck auf einem LaserJet 9500

Auf die vom Betriebssystem bereits standardmäßig vergebenen Berechtigungen auf Drucker, die in Abb. L17 ebenfalls zu sehen sind, wird hier im Beispiel nicht näher eingegangen.

Allgemein gesehen erhält ein Subjekt durch die Gruppenmitgliedschaften nicht nur die Berechtigungen der Zeile in M, welche durch den SID des Subjektes adressiert wird, sondern jede einzelne Gruppenmitgliedschaft verhilft dem Subjekt zu einem zusätzlichen SID, welcher eine zusätzliche Zeile in der Matrix M festlegt.

Ein Subjekt hält damit neben dem persönlichen SID auch ein Set von SIDs aus seinen Gruppen-Mitgliedschaften. Diese Gesamtmenge von SIDs wird als Access-Token des Subjekts bezeichnet und in Windows beim Login dem Subjekt auf Basis der Accountdaten zugewiesen. Da das Subjekt diesen Access-Token quasi mit sich führt, um Zugriffe durchführen zu dürfen, kann dieser Access-Token letztlich als Capability gesehen werden.

Abb. L18 zeigt als Beispiel zwei Access-Tokens:

- Benutzer Joerg hat in der durch seinen SID adressierten Zeile der Access-Matrix die Berechtigungen zur Modifikation des Verzeichnisses Skripten und der Dateien darin. Durch die Mitgliedschaft in der Gruppe SkriptenDruck kann Joerg auch Ausdrucke seiner Dateien erstellen.

- Benutzerin Christine ist durch ihre Mitgliedschaft in den Gruppen SkriptenDruck und LektorInnen nicht nur dazu berechtigt, Dokumente Sec*.doc zu lesen, sondern auch, diese am Drucker 9500 auszudrucken. Eine Ausnahme davon bildet die – rein wegen des Beispieles hier eingeführte – Datei div1.doc, auf die Christine durch ihre Gruppenmitgliedschaft(en) Leseberechtigungen und als Person zusätzlich Schreibberechtigungen erhält.

- Auffällig ist auch, dass Lektoren wie Michael oder Rudi in der Zugriffskontroll-Matrix überhaupt keine direkten Berechtigungen besitzen. Die Berechtigung zum Lesen der Dateien erhalten sie ausschließlich über die Mitgliedschaft bei der Gruppe LektorInnen.

Abb. L18  Beispiel: Access-Token von User Joerg und Christine + daraus resultierende Berechtigungen

Dieser einfache Überblick von Abb. L18 zeigt bereits, dass eine gut an die Aufgabenstellungen angepasste Gruppenstruktur mit einer konsistenten Vergabe der Berechtigungen ein wichtiger Bestandteil jeder Benutzerverwaltung ist.

L.4.4.7 Überprüfung der Berechtigungen durch das Betriebsystem

Bei Zugriffen auf ein Objekt hat das Betriebssystem vorab zu prüfen, ob dieser Zugriff auch berechtigt ist. Dazu wird die ACL des Objektes analysiert. Es wird die Vereinigungsmenge jener Berechtigungen gebildet, welche durch die SIDs im Access-Token adressiert werden können. (Man vergleiche dazu die Ergebnisse der Berechtigungen in Abb. L18) Abschließend wird diese Gesamtberechtigung mit dem gewünschten Zugriff verglichen. Reichen die Berechtigungen für den gewünschten Zugriff nicht aus, so wird dieser abgebrochen.

Im Beispiel Abb. L19 versucht User Christine testweise, die Datei div1.doc zu löschen. Christine erhält über jede ihrer beiden Gruppenmitgliedschaften Leseberechtigungen auf die Datei div1.doc. Die zusätzliche Schreibberechtigung kommt direkt über den persönlichen SID. Da aber {Delete} \ {Read,Write} = {Delete} ist (vgl. L.4.4.2.), wird der Zugriff vom Betriebssystem verweigert

Abb. L19  Beispiel ACL: Versuch von User Christine, die Datei div1.doc zu löschen

Auf weitere Details zur Datei div1.doc, insbesondere in welchem Verzeichnis sich diese befindet und welche Berechtigungen auf diesem Verzeichnis vergeben sind, wird hier nicht näher eingegangen.

In den Abbildungen ab Abb. L8 findet man auch eine Spalte „Deny“ für „negative Berechtigungen“. Diese dienen dazu, zugestandene Berechtigungen - quasi als Ausnahme - explizit zu widerrufen.

Beim Windows Dateisystem NTFS gibt es eine implementierungsspezifische Besonderheit: Es wird zwischen vererbten und auf einem Objekt direkt vergebenen Berechtigungen unterschieden. So kann z.B. das von einem Folder auf eine darin liegende Datei vererbte Deny keine auf der Datei direkt vergebenen Berechtigungen widerrufen.

L.4.5 Rollenbasierende Zugriffsrechte

Wie vorhin erwähnt, kann man Subjekte durch ihre Rollen ersetzen, die sie in einer betrieblichen Organisation zu erfüllen haben. Beim ersten Hinsehen könnte man meinen, dass somit Rollen letztlich den Gruppen in einem DAC System entsprechen. Der wesentliche Unterschied besteht aber darin, dass die Rechte nicht mehr den Benutzern, also Subjekten, zugeordnet werden, sondern deren Rechte ausschließlich von den eingenommen Rollen abhängen.

Die Administration hat damit zwei Aufgaben: eine Festlegung von Rollen und deren Rechte entsprechend den betrieblichen Vorgaben und anschließend eine Zuweisung von Benutzern zu einer oder mehreren Rollen, die sie einnehmen dürfen.

Das auf dem Rollen Konzept basierende Zugriffsmodell Role Based Access Control (RBAC), wurde 2004 als ANSI Standard (ANSI INCITS 359-2004) spezifiziert. Dieser Standard untergliedert RBAC in vier Komponenten:

- Basis RBAC (Core RBAC)

- Hierarchisches RBAC (Hierarchical RBAC)

- Statische Trennung von Zuständigkeiten (Static Separation of Duties)

- Dynamische Trennung von Zuständigkeiten (Dynamic Separation of Duties)

L.4.5.1 Basis RBAC (Core RBAC)

Core RBAC beschreibt die wesentlichen Eigenschaften, welche jede Implementierung von RBAC besitzen muss, die dem ANSI Standard genügen will. Es enthält die wichtigsten Komponenten, Mengen und Funktionen, die Role Based Access Control auszeichnen. Wie der Name vermuten lässt, ist das zentrale Konzept bei RBAC die Rolle, die gewissermaßen der Brennpunkt zwischen Benutzern und ihren Rechten ist.

Was eine „Rolle“ aus einer Menge ROLES genau ist, obliegt der Deutung im jeweiligen Anwendungsfeld. Beispiele dafür sind Arzt, Schwester, Patient in einem Krankenhaus, oder Kunde, Kassier, Finanzberater im Bankenbereich. Ebenso sind die anderen Mengen USERS (Anwender, Benutzer), OPS (Operationen) und OBS (Objekte) nicht weiter erklärt und leiten ihre Semantik aus der Namenswahl ab.

Man geht davon aus, dass sich Rollen r ∈ ROLES innerhalb einer Organisation nur selten verändern, während Individuen in ihren Rollen öfters wechseln werden. Ein solcher Wechsel kann sich durch Änderungen im Personalstand ergeben oder es kann das Prinzip „Job-Rotation“ ein grundsätzliches Element in der Aufgabenzuteilung an Mitarbeiter vorgesehen sein. Jedenfalls bleiben ROLES im Vergleich zu USERS relativ stabil, sodass meist nur die Zuweisung von Anwendern u ∈ USERS zu ihren Rollen r ∈ ROLES von Änderungen betroffen sind, was – das ist als Hauptvorteil angegeben – die Verwaltung von Zugriffsmöglichkeiten auf Objekte vereinfacht.

Ferner ist es vorgesehen, dass Anwender mehrere Rollen innehaben können, sodass von einer Relation UA ⊆ USERS x ROLES auszugehen ist.

Die konkrete Zuordnung von Benutzern zu Rollen über diese User Assignment (UA) Relation ist eine Management Entscheidung und wird vom Systemadministrator zur Entwurfszeit der Anwendungen, bzw. wann immer Änderungen notwendig sind, statisch getroffen.

Mit der Funktion assigned_users, ist es dem Administrator möglich, die Zuordnung der Benutzer zu einer Rolle zu überprüfen,

assigned_users: (r:ROLES) → 2USERS

mit assigned_users(r) = {u ∈ USERS | (u,r) ∈ UA}.

(Wir übernehmen aus dem ANSI-Standard hier die Schreibweise 2M für die Potenzmenge einer Menge M).

Was nun die Zugriffsmöglichkeiten auf die Menge der Objekte OBS betrifft, sind folgende Ebenen zu unterscheiden:

Eine Ebene betrifft die im System vorgegebene Menge von Operationen OPS, wobei nicht weiter festgelegt ist, auf welcher Granularitätsebene diese Menge OPS angesiedelt ist. Bei Betriebssystemen sind es typischer Weise {read, write, append, execute, …}.

Nicht jede dieser Operationen op ∈ OPS macht für jedes Objekt ob ∈ OBS Sinn oder ist nicht für jedes Objekt erlaubt: ein read-only File darf nicht beschrieben werden und eine reine Textdatei kann nicht wie ein Programm ausführbar (execute) sein.

Damit sind mögliche Aktivitäten für die einzelnen Objekte zu fixieren, die in RBAC als Permissions PRMS bezeichnet werden:

PRMS = 2(OPS x OBS)

Auf dieser Ebene wird demnach festgelegt, was beim Ausüben einer Rolle auf welche Objekte möglich ist und mit assigned_users(r:ROLES) erhält man jene Benutzer, die eine solche Rolle ausüben.

Die beiden Funktionen Ob und Op geben Auskunft was mit welchem Objekt gemacht werden kann:

Ob (p:PRMS) → {ob ∈ OBS} liefert die Menge der Objekte, auf denen die Permission p möglich ist.

Op (p:PRMS) → {op ∈ OPS} gibt für jede Permission p an, welche Operationen in ihr enthalten sind.

Die zweite Ebene legt fest, welche Permissions den Rollen zugeteilt sind:

PA ⊆ PRMS x ROLES

Analog zur Zuordnung von Benutzern zu Rollen, sieht der ANSI RBAC Standard auch eine Funktion zur Überprüfung der einer Rolle zugeordneten Zugriffsrechte vor:

assigned_permissions (r:ROLES) → 2PRMS mit

assigned_permissions(r) = {p ∈ PRMS | (p,r) ∈ PA}.

Diese Funktion stellt einen weiteren Unterschied zum auf dem DAC-Modell basierten Gruppen-Modell dar, welches es nicht einfach ermöglicht, alle Berechtigungen einer Gruppe abzufragen.

Im Vergleich zu Gruppen in einem DAC-Modell sieht das RBAC-Modell darüber hinaus vor, dass einem Benutzer nicht immer alle in der Funktion User Assignment (UA) zugeordneten Rollen zur Verfügung stehen müssen.

Durch ein im RBAC ANSI Standard verpflichtendes Sitzungs-Konzept ist es einem Anwender möglich, unterschiedliche Rollen Aktivierungen für einzelne Sitzungen zu definieren, womit der effektive Berechtigungsraum des Benutzers von Sitzung zu Sitzung variieren kann.

Das RBAC erlaubt dazu, dass jemand zu Beginn der Arbeit eine sogenannte Sitzung (session, s ∈ SESSIONS) eröffnet, in der er festlegt, welche Rollen in dieser für ihre Dauer angenommen werden, also für ihn sitzungsspezifisch zugeteilt werden. Nach dem ANSI Standard ist es sogar möglich, dass ein Anwender u ∈ USERS gleichzeitig mehrere Sitzungen s ∈ SESSIONS eröffnet, sodass folgende Zuweisungen erfolgen:

session_user (s:SESSIONS) → USERS

Für jede eröffnete Sitzung s ∈ SESSIONS wird dieser genau ein u ∈ USERS zugeordnet.

Mit der Funktion user_sessions (u:USERS) → 2SESSIONS erhält man umgekehrt die Information, welche Sitzungen vom Anwender u eröffnet worden sind.

In den einzelnen Sitzungen werden also Rollen r ∈ ROLES wahrgenommen, wobei nur jene von einem Anwender gewählt werden können, die ihm oder ihr über die Funktion UA zugeteilt worden sind:

session_roles (s:SESSIONS) → 2ROLES

mit den Bedingungen

session_roles (si) ⊆ {r | r ∈ ROLES & (session_user (si), r) ∈ UA}

Eröffnet demnach ein Anwender eine oder mehrere Sitzungen si ∈ SESSIONS und legt dort bzw. damit fest, welche Rollen ausgeübt werden sollen, so sind damit die Rechte und Möglichkeiten (Permissions) als Vereinigungsmenge über die den ausgewählten Rollen zugeordneten Permissions für die Dauer der Sitzung(en) erklärt.

Die Funktion avail_session_perms liefert alle Berechtigungen eines Benutzers innerhalb einer bestimmten Sitzung:

Nachstehende Abbildung zeigt eine konkrete Instanz des Core RBAC Modells, indem ein Benutzer aus der Menge von drei Anwendern (USERS) eine aktive Sitzung hat. Obwohl dem Benutzer drei Rollen über seine User Assignment (UA) Funktion zugewiesen wurden, werden in der aktiven Sitzung nur zwei Rollen davon verwendet, was automatisch den verfügbaren Rechteraum in der Sitzung auf jene Permissions beschränkt, die durch die zwei aktiven Rollen impliziert sind.

Abb. L20   Benutzer mit einer aktiven Sitzung im Basis RBAC Modell (Core RBAC)

In der RBAC ANSI Spezifikation ist auch ein repräsentatives Diagramm über die Zusammenhänge und Interaktionen zwischen den einzelnen eben beschriebenen RBAC Bausteinen vorgesehen. Die nachstehende Abbildung zeigt das Core RBAC Referenzmodell.

Abb. L21   Core RBAC Referenz Modell

Wie die Graphik verdeutlicht, ist das zentrale Konzept von RBAC die Rolle, die ein semantisches Konstrukt zur Formulierung von Richtlinien zur Zugriffsbeschränkung bietet. Die beiden wichtigsten Relationen sind User Assignment (UA) und Permission Assignment (PA), welche die Zuordnung von Benutzern, respektive Berechtigungen zu einer Rolle definieren. Die Pfeile in den Beziehungen in der Abbildung bezeichnen eine M:N Kardinalität, d.h. ein Benutzer kann mehreren Rollen zugeordnet sein und eine Rolle kann von mehreren Anwendern ausgeübt werden. Gleiches gilt sinngemäß für die einer Rolle zugeordneten Berechtigungen.

Die momentan aktiven Sitzungen sind durch die SESSIONS Menge dargestellt.

Die Beziehung user_sessions ist eine 1:N Relation, d.h. ein Benutzer kann gleichzeitig mehrere offene Sitzungen haben, wobei eine Sitzung aber genau einem Benutzer zugeordnet sein muss.

L.4.5.2 Hierarchisches RBAC (Hierarchical RBAC)

Hierarchisches RBAC stellt eine optionale Erweiterung des ANSI RBAC Basis Modells dar, welche Rollen Hierarchien und Rollenvererbung beschreibt. Hierarchien sind ein wichtiges Mittel zur Modellierung von Zugriffskontrollsystemen, um die natürliche betriebliche Organisationsstruktur besser auf ein Computer System abbilden zu können.

In gewissem Sinne entspricht die Rollenvererbung der Vererbung von Klassen bei objektorientierter Programmierung (OOP), allerdings ist die Vererbung der Benutzerrechte gesondert zu definieren:

Die Vererbung einer Rolle in RBAC bedeutet, dass alle Benutzer der „erbenden“, höheren Rolle auch Benutzer der niedrigeren, „geerbten“ Rolle werden. Gleichzeitig stehen alle Berechtigungen der niedrigeren Rolle auch der in der Vererbungshierarchie höheren Rolle zur Verfügung. Die Vererbungsflüsse von Berechtigungen und Benutzern sind also genau entgegengesetzt. Erbt, wie in Abb. L22 dargestellt, beispielsweise eine Rolle R2 die Rolle R1, d.h. die Rolle R2 ist mächtiger als die Rolle R1, „wandern“, alle Berechtigungen von R1 zu R2. Gleichzeitig „wandern“ die Benutzer von R2 hinunter zu R1, d.h. die Benutzer von R2 werden auch Benutzer von R1.

Abb. L22  Hierarchisches RBAC – Benutzer und Rollen Vererbung

Der RBAC ANSI Standard unterscheidet zwischen allgemeiner und beschränkter Rollenvererbung. Allgemeine Rollenvererbung bedeutet, dass die Rollen Hierarchie als beliebige partielle Ordnung, etwa als Verband, definiert werden kann. Somit bietet die allgemeine Vererbung das Konzept der Mehrfachvererbung, d.h. eine Rolle kann gleichzeitig mehrere Rollen erweitern.

Bei der beschränkten Rollenvererbung ist die Hierarchie auf Baumstrukturen (die Wurzel wird unten gezeichnet) beschränkt, d.h. eine Rolle kann immer nur höchstens von einer Rolle erben, wobei es aber möglich ist, dass mehrer Rollen von der gleichen Rolle abgeleitet sind.

L.4.5.3 Beschränktes RBAC (Constrained RBAC) – Statische und Dynamische Trennung von Zuständigkeiten

Nun gibt es ein weiteres Problem, nicht unähnlich dem wechselseitigen Ausschluss (mutual exclusion) beim Zugriff auf Objekte:

Manche Rollen widersprechen sich und sind miteinander nicht verträglich: ein Bankkassier darf nicht gleichzeitig in derselben Sitzung die Rolle Kunde annehmen, obgleich er sicherlich bei der eigenen Bank ein Konto hat. Ein weiteres Beispiel sind Aktionen, die auf Grund ihres hohen Missbrauchspotentials auf zwei oder mehrere Mitarbeiter aufgeteilt werden sollten. So könnte es beispielsweise in einem Bankensystem keinem einzelnen Mitarbeiter erlaubt sein, Überweisungen in gewisser Höhe gleichzeitig zu initiieren und durchzuführen.

Beim RBAC-Ansatz wird dieses Problem als Separation of Duties bezeichnet.

Es gibt dazu zwei Lösungsansätze: die statische und die dynamische Trennung von Rollen.

Bei der Static Separation of Duties (SSD)  wird bereits bei der Festlegung von assigned_users und der Relation UA ⊆ USERS x ROLES geprüft, ob nur verträgliche Rollen einem Anwender zugeteilt sind.

Wie man am Beispiel Bank mit {Kassier, Kunde} oder bei Organisationen mit Job-Rotation erkennen kann, ist dies in der Praxis aber nicht immer möglich.

In der formalen Beschreibung der statischen Trennung von Rollen sieht der Standard eine allgemeinere Formulierung vor, die eine Menge SSD von Rollenmengen rs (z.B.: rsi = {r_1, r_5, r_8},

rsj = {r_2, r_5, r_7}) festlegt, welche nicht gleichzeitig einem Benutzer zugewiesen werden dürfen. Anstatt SSD ⊆ 2ROLES schreibt man SSD ⊆ 2ROLES x N, erweitert also jedes rsi zu einem 2-Tupel durch n, 2 ≤ n∈N, mit folgender Bedeutung: Einem Benutzer können nicht gleichzeitig n oder mehr Rollen aus rsi  zugewiesen werden.

Durch diese Zusatzbedingung wird der ursprünglich scharfe Ausschluss aufgeweicht.

Im obigen Beispiel könnte man mit n = 3 zulassen, dass aus rsi = ({r_1, r_5, r_8},3) dennoch {r_1, r_5} oder {r_1, r_8} oder {r_5, r_8} gleichzeitig zugeteilt werden können.

Diese scheinbare Flexibilität als Aufweichung der strengen Deutung von SSD stößt jedoch nicht auf allgemeine Zustimmung, da ungewollte Konstellationen definiert werden können:

Sei SSD ={ ({rr_1, r_5},2), ({r_1, r_5, r_8},3) }, dann stellt sich die Frage, ob es wirklich gewollt war, dass r_1 und r_5 in einer Sitzung wegen  ({r_1, r_5, r_8},3) noch gemeinsam gewählt werden dürfen, dies aber über ({r_11, r_5},2) sofort wieder revidiert wird.

Bei der Dynamic Separation of Duties (DSD)  hat man bei der Festlegung von

session_roles (s:SESSIONS) → 2ROLES

darauf zu achten, dass innerhalb einer eröffneten Sitzung nur Rollen gewählt werden können, die miteinander verträglich sind. D.h. ein Benutzer oder eine Benutzerin ist zwar grundsätzlich Rollen zugeordnet, die zueinander in einem Interessenskonflikt stehen, er bzw. sie darf diese Rollen aber niemals zur gleichen Zeit in den offenen Sitzungen aktiviert haben.

SSD ist ein stärkeres Konzept zur Verhinderung von rollenbedingten Interessenskonflikten, jedoch nicht so flexibel und dynamisch wie DSD.

Formal gesehen ist DSD analog zu SSD, bezieht sich aber auf Sitzungen.

DSD hilft in seiner intendierten Flexibilität dem Ziel, einem Benutzer jedenfalls zu Beginn einer Sitzung jene Rollen und damit assoziierten Berechtigungen zu geben, die für die Erledigung der anliegenden Aufgabe notwendig sind.

Gleichzeitig aber wird es leichter möglich, das wichtige Prinzip „Nur jene Rechte vergeben, die unbedingt erforderlich sind!“ („principle of least privilege“) zu verfolgen.

Im genannten Beispiel {Kassier, Kunde} kann durchaus ein Benutzer die Rollen {Kassier} und {Kunde} durch UA zugeteilt erhalten, bei der Eröffnung der Sitzung muss er sich aber in Folge der DSD-Festlegung für eine der beiden Rollen entscheiden, was dynamisch vom System überprüft wird.

Graphisch wird im Standard DSD durch die Ergänzung des RBAC Referenzmodells um eine „DSD Komponente“ dargestellt (siehe Abb. L23):

Abb. L23  Core RBAC Referenzmodell mit Dynamic Separation of Duties (DSD)

M NETZWERKKOMMUNIKATION

M.1 Netzwerkkommunikation ist Aufgabe des Betriebssystems

Die Software für Netzwerkkommunikation gehört zu den Komponenten eines Betriebssystems. Dies war aber nicht immer so. Bei Microsoft Windows 3.0 (1990) wurde eine Netzwerkunterstützung noch von Drittherstellern bereitgestellt, z.B. die auf Shareware-Basis verfügbare „Trumpet Winsock“. Erst bei Windows 3.11 (1993) kam eine TCP/IP Protokollunterstützung optional dazu. Heute ist die Unterstützung von TCP/IP fixer Bestandteil jedes Betriebssystems, welches über eine Netz¬werkanbindung verfügt.

Das folgende Kapitel beschränkt sich auf Basisinformation und ist entsprechend komprimiert verfasst, gibt aber trotzdem auch einige Beispiele zur Konfiguration und zum Testen einer Netzwerkan¬bindung.

M.2 TCP/IP ist die dominante Protokoll-Familie

In den obigen Zeilen wurde Netzwerkkommunikation stillschweigend mit TCP/IP gleichgesetzt, was eine starke Einschränkung bzw. Vereinfachung darstellt, aber nicht ganz unbegründet ist:

- TCP/IP nimmt durch seinen hohen Verbreitungsgrad und die starke Durchdringung des Internets in vielen Bereichen der Kommunikation den unumstrittenen ersten Platz bei Netzwerkprotokollen ein.

- Auf Basis der auf der RFC-Editor Webpage (http://www.rfc-editor.org) öffentlich ver¬fügbaren Standards und Empfehlungen in sogenannten RFCs (Request for Comment) wird TCP/IP von allen IT-Herstellern unterstützt, bietet damit einen großen Markt an Hard- und Softwareprodukten und durch diese Konkurrenzsituation auch günstige Preise.

- Durch seine Eignung zum Aufbau von räumlich weit verteilten Netzen – der WANs (Wide Area Networks) – hat TCP/IP in diesem Bereich (fast) eine Art Monopolstellung erreicht.

- Mit dem Einsatz im Internet hat TCP/IP auch seine hohe Skalierbarkeit bewiesen. Dieses Kapitel konzentriert sich auf die heute weit verbreitete Version 4 von TCP/IP. Auf das Nachfolgeprotokoll IP Version 6 (IPv6) mit dem viel größeren Adressraum – der zu kleine Adressraum ist ja heute bei der Version 4 das Problem – wird in einem Überblick ebenfalls eingegangen.

M.3 Begriff: Protokoll

Da der Protokoll-Begriff in der Kommunikation wesentlich ist, soll im Folgenden eine kurze Beschreibung dazu gegeben werden. Ein Protokoll legt u.a. in der zwischenmenschlichen Kommunikation erwartete Regeln der Höflichkeit und ihre Form fest. Protokolle in der Diplomatie vereinbaren, wer was zu wem in welcher Sprache und in welcher Reihenfolge sagen muss oder darf, wer wen in welcher Reihenfolge begrüßt, ob man sich zu verneigen oder die Hand zu geben hat, welche Themen absolut tabu sind, usw. Die Kommunikation ist hier also formalisiert und eingeschränkt, sehr ähnlich wie bei Protokollen im Netzwerkbereich:

- Netzwerkprotokolle legen den Aufbau der Nachrichten fest bzw. die Datenpakete, in die sie eingepackt sind. Das ist sehr ähnlich wie bei einer Postkarte: Hier gibt es eben¬falls Vorschriften, wo und in welcher Form die Zieladresse wie „FIM, Johannes Kepler Universität, A-4040 Linz, Austria“ zu schreiben ist. Auch die Reihenfolge innerhalb der Adresse ist dabei festgelegt: Ganz unten muss das Land stehen, darüber die Postleitzahl und der Ort, … Man überlege selbst, was mit einem Poststück passiert, wenn Ziel- und Absenderadresse vertauscht werden.

- Auch die korrekte Abfolge der Nachrichten zwischen den Kommunikationspartnern ist wichtig und wird vom Protokoll festgelegt. Vergleichen wir das wiederum mit dem Protokoll in der Diplomatie, das Fragen wie „Wer muss wen zuerst grüßen?“ oder „Wer darf wen überhaupt ansprechen?“ regelt. Für ein IT-Kommunikationsprotokoll könnte eine notwendige Reihenfolge beispielsweise lauten: „Erst nachdem ich eine Anfrage gesendet habe, bin ich bereit, eine Antwort anzunehmen“.

- Dass Systeme und damit das Protokoll Reaktionen auf empfangene Nachrichten setzen, ist klar. Daneben gibt es auch Reaktionen auf weitere Ereignisse, die ebenfalls vom Protokoll festgelegt werden. Ein wichtiges Ereignis ist auch, dass eine erwartete Nachricht nicht empfangen wird. In der Welt der Diplomatie könnte dies z.B. bedeuten: „Wenn mich der Verhandlungspartner in den nächsten 10 Sekunden nicht grüßt, dann verlasse ich den Verhandlungssaal“. Bei IT-Systeme wird oft, wenn auf eine Anfrage längere Zeit keine Antwort eintrifft, die Anfrage wiederholt. Nach mehreren erfolglosen Anfragen wird der gewünschte Kommunikationspartner als „nicht-erreichbar“ eingestuft und die Kommunikation mit einer Fehlermeldung abgebrochen.

M.4 Zum Namen TCP/IP

Mit dem Sammelnamen „TCP/IP“ bezeichnet man meist die gesamte Protokoll-Familie des Internet. Innerhalb dieser vielen Protokolle sind IP (Internet Protocol) und TCP (Transmission Control Protocol) besonders wichtig. Der Bezeichner „TCP/IP“ repräsentiert oft mit seinen zwei wichtigen Protokollen „pars pro toto“ die Protokolle im Internet.

M.4.1 Grundfunktionalität von IP

Eine Nachricht wird in einzelne Teile (Pakete) zerlegt und jedes Paket für sich unabhängig an den Empfänger versandt. Ein IP-Paket ist mit einer Postkarte vergleichbar: Diese wird ebenfalls „einfach so“ auf den Weg geschickt in der Hoffnung, dass sie irgendwann einmal beim Ziel ankommt.

Diese Unabhängigkeit jedes IP-Paketes hat einige Konsequenzen, die hier kurz beschrieben werden sollen:

- So wie auf jeder Postkarte die Zieladresse stehen muss und auch die Absenderadresse vermerkt sein soll, enthält jedes IP-Paket eine Destination Address und eine Source Address.

- Obgleich die Post zuverlässig arbeitet, geht mitunter eine Postkarte verloren. Auch bei IP kann es zum Verlust von Paketen kommen. Man sagt daher, dass IP nur Best-Effort-Delivery bietet.

- Wie bei der Post kann bei IP der Fall eintreten, dass Pakete nicht in der Reihenfolge, in der sie aufgegeben wurden, beim Empfänger ankommen. Manchmal überholen später gesendete Pakete ältere Daten und langen damit früher beim Empfänger ein.

- Auch besteht bei IP die Möglichkeit, dass Duplikate eines Paketes empfangen werden, also einmal gesendete Daten eventuell auch mehrfach beim Empfänger eintreffen.

- Sind Pakete für eine Weiterleitung zu groß, so kann IP ein Paket auch in mehrere kleinere Pakete, sogenannte Fragmente, unterteilen und getrennt voneinander weitersenden.

Der Empfänger ist dann dafür zuständig, diese Fragmente wieder korrekt zusammenzusetzen.

Abb. M1  IP Datenübertragung

Obgleich der vollständige Mangel irgendwelcher Garantien für die Zustellung bei IP durchaus nicht vertrauenerweckend scheint, ist IP das Hauptprotokoll im Internet, über welches faktisch alle Daten übertragen werden.

M.4.2 Grundfunktionalität von TCP

Das Transmission Control Protocol (TCP) erweitert IP um wichtige Eigenschaften. Dazu tauschen Sender und Empfänger vor der eigentlichen Datenübertragung Informationen über den geplanten Datentransfer aus.

Daher wird TCP als verbindungsorientiert bezeichnet. Innerhalb dieser Verbindung werden die Daten durchnummeriert. Durch diese Nummern können Duplikate und das Eintreffen von Daten außerhalb der korrekten Reihenfolge vom Empfänger erkannt und korrigiert werden. Auch ein Verlust von Datenpaketen wird durch die fortlaufende Nummerierung entdeckt. Bei TCP wird die Vollständigkeit der Daten gewährleistet, indem der Empfänger jeweils Bestätigungen (Acknowledges, ACKs) an den Sender zurückschickt, welche Daten er bereits empfangen hat. Bleibt die Empfangsbestätigung für Daten aus, so wiederholt der Sender die Übertragung.

Abb. M2  TCP Datenübertragung

M.5 Schichtenmodell

TCP erfindet die Übertragung nicht neu, sondern verwendet – was naheliegend ist – selbst die Funktionalität von IP. Dies ist vergleichbar mit Unterprogrammaufrufen, dem Aufruf von Methoden im OOP oder – abstrakt gesehen – dem Aufruf des APIs einer virtuellen Maschine. Bei Netzwerken spricht man von Schichten und Diensten: Eine Schicht bietet der über ihr liegenden Schicht Dienste an und verwendet selbst die Dienste der darunterliegenden Schicht.

Abb. M3  IP Schichtenmodell und Beispiele für Protokolle

Applikations-Protokolle wie HTTP (Hypertext Transfer Protocol), DNS (Domain Name Services) oder SMTP (Simple Mail Transfer Protocol) bedienen sich der Dienste der Transport-Schicht. Diese ihrerseits verwendet Dienste der Internet-Schicht usw.

M.6 IP Version 4 (IPv4)

Auf der Internet-Schicht ist das Internet Protocol IP, genauer gesagt IP in der Version 4 (IPv4) durch seine hohe Verbreitung dominant. Daher werden einzelne allgemeine Konzepte von TCP/IP vorerst im Kontext von IPv4 erläutert und erst danach wird auf die speziellen Eigenschaften des neueren und derzeit noch wenig verbreiteten IP Version 6 (IPv6) kurz eingegangen.

M.6.1 Adressierung

Um weltweit Post empfangen zu können, benötigt man eine Postadresse. Auch bei IP ist das so. Ohne weltweit gültige IP-Adresse kann ein Host nicht im Internet kommunizieren. Auch Konzepte, die IP-Adressen umsetzen, benötigen schlussendlich zumindest eine weltweit gültige IP-Adresse. (Weitere Informationen dazu findet man unter Bezeichnungen wie Network Address Translation (NAT), Port Address Translation (PAT), oder der Abkürzung NAPT, die für „Network Address Port Translation“ steht.)

M.6.1.1 IP-Adressen

Eine IP-Adresse (genauer eine IPv4-Adresse) ist 32 Bits lang und besteht aus 4 Oktetts/Bytes. In der Standardschreibweise wird jedes Oktett als vorzeichenlose Dezimalzahl im Bereich 0 bis 255 angeschrieben, wobei die 4 Zahlen jeweils durch Punkte getrennt sind. 140.78.3.160 wäre ein Beispiel für eine solche IP-Adresse, die in Abb. M4 zusätzlich als Bitmuster dargestellt ist.

Abb. M4  Beispiel IP-Adresse in Standardschreibweise und als Bitmuster

M.6.1.2 IP-Adressen und Subnetting

M.6.1.2.1 Struktur eines IP-Netzes

Zusätzlich zur Adressinformation benötigt jeder IP-Host grundlegende Informationen über die Netzwerkstruktur. Innerhalb des eigenen lokalen Netzwerkes (LAN) ist direkte Kommunikation möglich, nach außen muss sich der Host der Dienste eines Vermittlers bedienen.

Dies ist ähnlich wie der Versand einer Postkarte, die man in den nächsten Postkasten wirft und dann darauf vertraut, dass die Post die Karte richtig weiterleitet und zustellt. Bei IP wird dieser Zwischenpunkt, welcher die Weiterleitung übernimmt, Default Gateway, Default Router oder Standard-Gateway genannt.

Abb. M5  Direkte Kommunikation innerhalb des eigenen Netzes, sonst Default Gateway

Im Beispiel von Abb. M5 können damit die Systeme R_1, R_2 bis Rn untereinander direkt kommunizieren. Die Kommunikation nach außerhalb läuft über das Default-Gateway GW.

Die Unterscheidung, ob der Kommunikationspartner sich „innerhalb des eigenen lokalen Netzes“ oder „außerhalb des LANs“ befindet, muss der IP-Host selbst treffen. Dabei geht er davon aus, dass die IP-Adressen aus zwei Teilen bestehen:

- Der erste Teil der IP-Adresse gibt das Netzwerk an (Netzwerk-ID).

- Der zweite Teil spezifiziert den Host innerhalb des Netzes (Host-ID).

Abb. M6  Aufteilung einer IP-Adresse in Netzwerk-ID und Host-ID

Aus dieser Sicht ist die Entscheidung einfach: Hat der gewünschte Kommunikationspartner die gleiche Netzwerk-ID wie der Sender, so liegt er im selben Netz. Offen ist aber noch, wo die Grenze zwischen Netzwerk-ID und Host-ID liegt.

M.6.1.2.2 Netzwerk-Klassen

Eine einfache Unterteilung lässt sich erreichen, indem die IP-Adressen in Klassen eingeteilt werden. Am Wert des ersten Oktetts der IP-Adresse, genauer an den „ersten“ most-significant Bits wird festgelegt, wo die Netzwerk-ID endet und die Host-ID beginnt. Abb. M7 und Tabelle M1 zeigen diesen Zusammenhang.

Abb. M7    IP Netzwerkklassen: Überblick

Tabelle M1   IP Netzwerkklassen: Anzahl der verfügbaren Bits, Netzwerk-IDs und Host-IDs

Diese Einteilung bietet wenige große „Klasse A“ Netze, und viele kleine „Klasse C“ Netze. Die Netzwerkklasse B liegt in den Eigenschaften zwischen A und C.

Gerade bei den großen “Klasse A“ Netzen fällt auf, dass die Anzahl der Hosts in einem Netz eher theoretisch zu sehen ist, denn niemand wird mehrere Millionen Hosts in einem einzelnen physikalischen Netzwerk betreiben. Auch ist die Situation aus anderen Gründen (noch) unbefriedigend: Hat ein Provider beispielsweise ein IP-Netz der Klasse B zugewiesen erhalten, so möchte er dieses Netz intern weiter strukturieren und kleine Ausschnitte davon – vielleicht nur mit einem Dutzend IP-Adressen – an einzelne Kunden zuteilen können. Für diese Funktionalität ist eine variable Grenze zwischen Netzwerk-ID und Host-ID erforderlich, welche im Folgenden besprochen wird.

M.6.1.2.3 Netzwerk-Maske

Flexibler als über die fixen Netzwerkklassen kann man dem Computer über die Netzwerk-Maske (Subnetzmaske, Subnet-Mask) die Information geben, welcher Bereich von IP-Adressen sich mit ihm im gleichen physikalischen Netzwerk befindet. Diese Subnetzmaske teilt (binär gesehen) die IP-Adresse wiederum in zwei bzw. drei Teile:

- Die Netzwerk-ID nach der Netzwerkklasse (A-B-C)

- Die Subnetzwerk-ID

- Die Host-ID im letzten Teil der IP-Adresse

Abb. M8   IP-Adresse mit Netzwerk-ID, Subnetzwerk-ID und Host-ID

Abb. M8 oben zeigt diese Verfeinerung. Das Beispiel in Abbildung Abb. M9 geht von der IP-Ad¬resse 10.0.6.20 aus. Die 10 als erstes Oktett legt die Netzwerkklasse A fest. Die eingesetzte Subnetz¬maske gibt mit 255.255.255.0 vorne 24 Einsbits an. Durch diese Subnetzmaske wird die Host-ID auf 8 Bits, also das letzte Oktett, eingeschränkt.

Abb. M9    IP-Adresse und Subnetzmaske

Will ein Host allgemein auf Basis der Netzwerkmaske feststellen, ob er selbst mit einem anderen Host im gleichen Netz liegt, so

- blendet er durch bitweises AND (&) mit der Subnetzmaske bei der eigenen und bei der IP des anderen Hosts die Host-ID aus

- und vergleicht die beiden verbleibenden Netzwerk-IDs auf Gleichheit.

Das folgende Codestück zeigt dies schematisch:

Code M1   Liegen Rechner [eigeneIP, subnetzMaske] und [fremeIP] im gleichen Netz?

Im Beispiel befinden sich also die Rechner mit IP-Adressen 10.0.6.1 bis 10.0.6.254 im selben physikalischen Netz und können somit direkt miteinander kommunizieren.

Die kleinste und größte Host-ID in einem Netzwerk, hier 0 und 255 (also 10.0.6.0 sowie 10.0.6.255), sind für Sonderaufgaben reserviert und dürfen nicht als IP-Adressen auf Rechnern vergeben werden! Bei einer Subnetzmaske von 255.255.255.0 sind also 254 Host-IDs verfügbar.

Die binäre Darstellung – ohne sie kommt man bei IP-Adressen oft nicht aus – zeigt, dass die Maske 255.255.255.0 am Anfang aus 24 Einsbits besteht. Daher wird oft statt der Angabe der 4-teiligen Subnetzmaske die Anzahl der Einsbits in der Maske gezählt. Dieser Wert wird dann durch einen Schrägstrich getrennt hinter die IP-Adresse gestellt. In unserem Beispiel ist damit die gängige Schreibweise für IP-Adresse und Subnetzmaske „10.0.6.20/24“.

Ein weiteres Beispiel sei noch hinzugefügt: 140.78.0.0/16 gibt an, dass die ersten 16 Bits und damit die vorderen zwei Bytes der Subnetzmaske aus Einsen aufgebaut sind. Die Maske ist in diesem Fall also 255.255.0.0. Somit sind hier aus Sicht von IP die Rechner 140.78.0.1 bis 140.78.255.254 in diesem – der Universität Linz zugewiesenen – Netz der Klasse B. Die Host-ID besteht bei 140.78.0.0/16 aus 16 Nullbits und gibt damit nicht einen Host, sondern das Netz als solches an.

Um einfach rechnen zu können, waren die bisherigen Beispiele für die Subnetzmaske immer auf Oktetts ausgerichtet. IP ist hier flexibel, Subnetzmasken lassen sich mit (fast) beliebigen Bitzahlen festlegen.

Das folgende Beispiel zeigt ein kleines Netz mit Basis 10.5.6.80/29. Damit sind die verwendeten Host-IDs auf die letzten 3 Bits beschränkt. Tabelle M2 gibt die Adressaufteilung dazu an.

Tabelle M2   IP-Adressen innerhalb des Netzes 10.5.6.80/29

Theoretisch könnten Subnetzmasken aus beliebigen Bitfolgen aufgebaut werden. Dies war schon immer verpönt und wird schon lange als Konfigurationsfehler angesehen. Die Schreibweise der Subnetzmaske mit Schrägstrich lässt syntaktisch ohnehin nur mehr korrekte Konfigurationen zu: /x legt fest, dass die ersten x Bits der Netzwerkmaske auf Eins und die folgenden 32-x Bits auf Null stehen.

M.6.1.3 Verfeinerung der Adressierung von Host --> Prozess

Eine IP-Adresse als Zieladresse bestimmt, welcher Rechner angesprochen werden soll. Diese Adressierung ist aber noch „zu ungenau“, da auf einem Computer viele Programme ausgeführt werden und bei den ankommenden Daten klar sein muss, welchem Prozess diese Daten zur Bearbeitung zu übergeben sind.

So macht es z. B. einen großen Unterschied, ob eine HTTP-Anfrage wirklich an den Webserver und damit an den HTTP-Server-Prozess geht, oder ob sie irrtümlich einem SMTP-Server übergeben wird, der für die Weiterleitung von Emails zuständig ist.

Jeder Prozess setzt daher zu seiner Identifikation für die Kommunikation eine 16 Bit große Zahl ein, die Port-Nummer:

- Damit Serverdienste wie Namensauflösung (DNS), Webserver (HTTP) oder Mailserver von außen einfach erreicht werden können, verwenden diese Server-Prozesse üblicherweise für sie reservierte Ports. Beispiele dafür sind 53 für DNS, 80 bzw. 8080 für HTTP, 443 für HTTPS oder 25 für das Mailprotokoll SMTP usw.

- Um die Antworten von Servern korrekt zu erhalten, benötigen auch alle Client-Prozesse, wie z. B. Webbrowser, solche Port-Nummern. Clients haben es hier aber einfacher. Sie fragen beim Betriebssystem um freie Port-Nummern an.

Das Betriebssystem weist freie und damit quasi zufällige Ports aus einem Pool (>1024) zu und die Clients verwenden diese Ports.

Abb. M10    Portnummern für Server- und Client-Prozesse

Abb. M10 zeigt ein Beispiel mit zwei Servern und zwei Clients.

Wie üblich werden dabei IP-Adresse und Port nur durch einen Doppelpunkt voneinander getrennt geschrieben. Diese Abbildung zeigt auch unter den Punkten (e)+(f) und (g)+(h), wie zwei verschiedene Browser von einem Client aus parallel auf einen Serverdienst zugreifen können. Bei näherer Betrachtung fällt zusätzlich auf, dass bei manchen Abfragen keine Port-Nummer anzugeben ist. Die Browser verwenden dann den Standard-Port des Protokolls: Für HTTP-Zugriffe wird der bekannte Port 80, für HTTPS Port 443 als Standardwert angenommen.

Mit dem Konzept der hier beschriebenen Ports arbeiten insbesondere zwei Protokolle der TCP/IP-Familie:

- Das Transmission Control Protocol (TCP) bietet die bereits besprochenen Vorteile wie wiederholte Datenübertragung im Fehlerfall, richtige Reihenfolge der Daten usw. Daher setzen z. B. HTTP, HTTPS, SMTP oder FTP (File Transfer Protocol) auf TCP auf, damit sich die Applikation nicht mehr selbst darum kümmern muss.

- Das User Datagram Protocol (UDP) hingegen erweitert IP „nur“ um die Portnummern, ist also mit „allen Problemen von IP“ behaftet. Anderseits bietet UDP Vorteile: Für kurze und einfache Kommunikation können der höhere Verwaltungsaufwand und die zusätzlich übertragenen Daten von TCP eingespart werden. So passt beispielsweise eine typische Anfrage an einen Domain-Name-Server (DNS) wie „Welche IP-Adresse hat www.jku.at?“ in ein einzelnes IP-Paket. Und auch die Antwort „www.jku.at hat IP-Ad¬resse 140.78.3.160“ ist sehr kurz. Hier wäre der Verwaltungs- und Zeitaufwand für die zusätzlichen IP-Pakete in TCP zu hoch und es ist einfacher, bei Ausbleiben einer Antwort die Anfrage nochmals zu senden. Daher werden z.B. DNS-Anfragen üblicherweise über UDP abgewickelt. (Ergänzend sei vermerkt, dass DNS daneben auch TCP und hier wiederum den Port 53 verwendet.)

M.6.2 IP Client-Konfiguration

M.6.2.1 Wesentliche Konfigurationsinformationen

Abb. ‎M 11 zeigt eine typische IP-Basiskonfiguration.

Abb. M11    IP-Einstellungen am Beispiel Windows

M.6.2.1.1 IP-Adresse, Subnetzmaske, Default Gateway

Über die IP-Adresse und die Subnetzmaske wurde bereits gesprochen. Wird festgestellt, dass der Kommunikationspartner außerhalb des eigenen Netzes liegt, wird das Paket an das Default-Gateway geschickt. Dieses übernimmt dann die Weiterleitung z.B. in das Internet. Ein Rechner benötigt daher die IP-Adresse dieses Default-Gateways, um Nachrichten nach außerhalb an das Gateway wei¬terreichen zu können. Es ist klar, dass das Default-Gateway direkt ansprechbar sein muss und daher im eigenen Subnetz zu liegen hat.

M.6.2.1.2 Domain Name System (DNS) Server

Um im Netz mit einprägsameren Namen und nicht nur mit IP-Adressen arbeiten zu können – www.jku.at anstatt 140.78.3.160 – benötigt ein IP-Host die Information, wer für ihn diese Auflösung von symbolischen Namen auf die zugehörigen IP-Adressen übernimmt.

Damit die Namensauflösung nicht nur auf einen einzigen DNS-Server angewiesen ist, kann die Konfiguration auf mehrere DNS-Server verweisen. Um möglichst hohe Geschwindigkeit zu erreichen stellt man „nahe“ DNS-Server ein, beispielsweise den des eigenen Netzes (wenn vorhanden) und dann die DNS-Server des Providers.

Abb. M12    Funktion von DNS und Reverse DNS

Abb. M12 zeigt, dass DNS auch umgekehrt (reverse DNS) dafür eingesetzt werden kann, von der IP-Adresse zu einem symbolischen Namen zu kommen.

M.6.2.2 Typen der Konfiguration eines IP-Hosts

M.6.2.2.1 Manuelle Konfiguration

Die manuelle Konfiguration wurde bereits im Punkt M.6.2.1 am Beispiel Windows besprochen. Will jemand z. B. ein Notebook in ein neues IP-Netzwerk integrieren, so ist eine Reihe von IP-Konfigurationsdaten nach den Vorgaben des jeweiligen Netzes einzustellen. Besonders ist auch darauf zu achten, dass nicht mehrere Geräte dieselbe IP-Adresse verwenden.

M.6.2.2.2 Dynamische Konfiguration auf Basis eines DHCP-Servers

Da die manuelle Konfiguration fehleranfällig und auch organisatorisch schwer zu administrieren ist, bieten viele Netzwerkzuständige einen Server mit dem Dynamic Host Configuration Protocol (DHCP) an.

Von einem solchen DHCP-Server erhalten Rechner ohne weitere IP-Konfiguration die benötigten IP-Informationen. Dabei ist lediglich anzugeben, dass die Einstellungen auf Basis DHCP erfolgen sollen (vgl. Abb. M11). Die Rechner erhalten dann vom DHCP-Server alle wesentlichen IP-Informationen wie IP-Adresse, Subnetzmaske und Default-Gateway. Auch können die Einstellung der DNS-Server (siehe Abb. M11) und weitere IP-spezifische Daten vom DHCP-Server bezogen werden. Damit ist DHCP eine komfortable Sache. Aus Benutzersicht muss man sich um die IP-Basiseinstellungen nicht mehr kümmern.

M.6.2.2.3 Automatische eigenständige Konfiguration

Antwortet nach mehreren Aufforderungen kein DHCP-Server, so hätte der Rechner keine IP-Adresse und könnte damit nicht über IP kommunizieren. Dieses Verhalten wäre nicht sehr komfortabel, denn für das schnelle Verbinden von zwei oder drei Rechnern untereinander möchte man keinen DHCP-Server konfigurieren und in das Netz integrieren.

Hier bietet das Konzept der Automatic private IP Address (APIPA) eine Möglichkeit, trotzdem IP-Adressen zu erhalten. Jeder IP-Knoten verwendet dazu eine zufällige Adresse aus dem dafür reservierten Bereich 169.254.0.0/16. Um Kollisionen mit gleichen Adressen zu vermeiden wird vor der Verwendung überprüft, ob die gewählte Adresse bereits belegt ist und bei Bedarf eben eine andere zufällige Adresse erzeugt. Da der IP-Knoten hier nur eine IP-Adresse und die Subnetzmaske 255.255.0.0 (aber kein Default-Gateway!) erhält, kann die Kommunikation nur innerhalb des eigenen lokalen Netzes stattfinden.

M.6.3 Testen einer IP-Konfiguration

Die folgende Kurzübersicht nennt einige wichtige Befehle zum Test von IP und erklärt ihre Verwendung jeweils an einem einfachen aber typischen Beispiel. Schnelle Informationen zu den Befehlen bietet beispielsweise die Online-Hilfe wie „man ifconfig“ bei Unix / Linux oder „ping -?“.

M.6.3.1 Auslesen der Konfiguration (ifconfig / ipconfig / GUI)

Oft will man die IP-Konfiguration ansehen, z.B. um zu wissen, welche IP-Adresse man über APIPA erhalten oder welche sonstigen IP-Informationen man von einem DHCP-Server bezogen hat. Diese Informationen lassen sich, wie in den Abb. M13 und Abb. M14 gezeigt, unter den Netzwerkeigenschaften auslesen oder auch mit dem Unix/Linux-Befehl „ifconfig“ bzw. unter Windows mit „ipconfig“ feststellen.

Abb. M13  Feststellen der IP-Konfiguration über Kommandozeile

Abb. M14    Feststellen der IP-Konfiguration über das GUI

Im GUI können auch die weiteren Details wie z.B. eingestellte DNS-Server abgefragt werden. Bei der Kommandozeile ist dazu in Windows „ipconfig /all“ erforderlich.

M.6.3.2 Testen der Kommunikation (ping, arp)

Will man testen, ob ein bestimmter IP-Host erreichbar ist, verwendet man den Befehl ping, welcher Testpakete versendet und die Antwort der Gegenstelle abwartet. Die Funktion von „echo request“ und „echo reply“ wird vom Internet Control Message Protocol (ICMP) bereitgestellt. Als Beispiel wird hier versucht, die Erreichbarkeit des Default-Gateway zu testen.

Abb. M15  Ping auf das Default-Gateway

Im obigen Output Abb. M15 läuft alles wie erwartet, das Default-Gateway auf 10.0.6.254 antwortet. Meist ist die Verbindung zum Gateway sehr schnell. Dies zeigt sich auch in der statistischen Auswertung. Die Laufzeiten zeigen jeweils 0 Millisekunden an, da die Auflösung der Uhrzeit hier nur gerundete Messungen zulässt.

Erhält man hingegen beim Ping auf das Default-Gateway keine Antwort, sondern Fehlermeldungen wie „Request timed out“, so sind der Anschluss des Rechners an das Netzwerk und die Rechnerkonfiguration sowie nötigenfalls auch das Gateway zu prüfen, ob dieses überhaupt arbeitet.

Im Beispiel wird davon ausgegangen, dass beim Default-Gateway vorgesehen ist, auf Pings zu antworten. Sollte dies nicht der Fall sein, so kann unmittelbar (!) nach dem fehlgeschlagenen Ping mit dem Kommando arpa für Knoten im lokalen Netz getestet werden, ob diese überhaupt verfügbar sind: Wenn ja, erhält man im Output eine Zeile ähnlich der in Abb. M16 (ARP steht für "Address Resolution Protocol“ ).

Abb. M16  Erreichbarkeitstest eines Rechners im lokalen Netzwerk mit ARP

Hat man, weil alle Versuche fehlschlagen, den Verdacht, dass vielleicht der IP-Stack – das ist die IP-Software am Rechner – nicht (mehr) korrekt installiert ist, dann trägt ein Ping auf sich selbst zur Klärung bei. Bei IP adressiert 127.0.0.1 (genauer jede IP im Netz 127.0.0.0/8) immer den eigenen Rechner, und zwar das Loopback-Interface. Arbeitet auch ein Ping auf 127.0.0.1 nicht korrekt, so ist dies ein Problem im IP-Teil des Betriebssystems.

M.6.3.3 Testen der Namensauflösung (nslookup)

Ohne Namensauflösung (vgl.‎ M.6.2.1.2 über DNS) wäre das Internet praktisch unbrauchbar. Daher ist bei Problemen ein Test der Namensauflösung notwendig. Am einfachsten lässt sich dies durch den Ping-Befehl erreichen, indem man beispielsweise einen Host mit seinem Namen „anpingt".

Abb. M17  Test der DNS Namensauflösung mit Ping

Im Beispiel ‎Abb. M17 arbeitet die Namensauflösung korrekt und auf das Ping werden auch Antworten empfangen. Fehler wie „unknown host“ oder „Ping request could not find host … Please check the name” zeigen hingegen, dass die in der IP-Konfiguration (vgl.‎ M.6.2.1.2) eingestellten DNS-Server nicht erreichbar sind oder der beim Ping angegebene Hostname unbekannt ist.

Ein gutes Werkzeug zum Testen der DNS-Funktionalität ist nslookup, da hier der zu testende DNS-Server wählbar ist und auch mittels vieler Optionen spezifisch befragt werden kann. Die folgenden Zeilen (Abb. M18) zeigen eine typische Abfragesequenz.

Abb. M18  Tool nslookup zum Test der Namensauflösung (Output gekürzt)

M.6.3.4 Feststellen einer Route (traceroute)

In einem IP-Netz werden die Daten meist über mehrere Zwischenpunkte, so genannte Hops, weitergereicht, bevor sie bei der Destination einlangen. Der erste Hop ist das Default-Gateway, welches seinerseits die Daten wiederum an einen anderen Router weiterreicht, usw. Die Abb. M19 zeigt diesen Zusammenhang exemplarisch, wobei die Route (der Weg) von Test-Source zu Test-Destination über 3 Hops führt. (Mit Knoten „im gleichen Netz“ kommuniziert Test-Source natürlich direkt, also ohne „Zwischen-Hop“.)

Abb. M19  IP-Route mit Hops zwischen Source und Destination

Möchte man den Weg feststellen, den Pakete zu einer bestimmten Destination nehmen, so wird man dafür das Programm traceroute (bei Windows: tracert) einsetzen. Im Beispiel-Output ‎Abb. M20 wird die Route zum Webserver der Johannes-Kepler-Universität (JKU) verfolgt.

Abb. M20  Traceroute von einem Laborrechner zu www.jku.at (Output gekürzt!)

Da sich der Rechner, von dem aus dieser Test gestartet wurde, ebenfalls im Netzwerk der JKU befindet, sind die Laufzeiten mit ~ 2 Millisekunden sehr klein und der Weg entsprechend kurz. Er stimmt mit der Netzwerkstruktur der Abb. M21 überein.

Abb. M21   IP-Route vom Laborrechner zu www.jku.at

Die Rechnernamen im Output von Traceroute werden auf Basis der IP-Adresse durch reverse DNS (vgl. Abb. M12 im Punkt M.6.2.1.2) erzeugt.

M.7 IP Version 6 (IPv6)

Der gesamte verfügbare Adressraum von IPv4 mit seinen (nicht einmal ganz) 32 Bits ist mit rund 4,3*10^9 Adressen beschränkt. Bereits relativ bald war klar, dass damit die verfügbaren Adressen im Lichte des rasant wachsenden Bedarfes knapp werden. Daher wurden bereits seit 1991 Vorschläge für ein Nachfolgeprotokoll eingebracht, aus denen schlussendlich IPv6 im Jahr 1995 fixiert wurde.

IPv6 bietet gegenüber IPv4 zusätzliche bzw. verbesserte Funktionalität. Der essentielle Vorteil von IPv6 ist der – mit 128 Bit großen Adressen – riesige Adressraum. Damit lassen sich konzeptionell 2128  ~3,4*10^38 Adressen darstellen und – zur Veranschaulichung – jedem Quadratzentimeter unseres Planeten ~6,6*10^19 Adressen zuweisen. Dies ist allerdings nur ein theoretischer Wert. Wie bei IPv4 erfolgt eine „topologische“ Aufteilung des Adressraumes über mehrere Ebenen, was zu einem entsprechenden Verschnitt führt. Selbst sehr pessimistische Annahmen mit sehr geringer Auslastung der einzelnen Subnetze (vgl. [Hör10]) erreichen aber immer noch ~81 IP-Adressen je Quadratmeter.

Die wesentlichen Weichenstellungen für IPv6 stammen – wie bereits erwähnt – schon aus dem Jahr 1995. Aber erst in letzter Zeit „gewinnt der IPv6-Zug richtig an Fahrt“. Daher soll hier ein kurzer Überblick zu IPv6 gegeben werden.

M.7.1 Adressierung

M.7.1.1 IPv6-Adressen

Die 16 Bytes großen IPv6-Adressen werden als 8 Blöcke á 16 Bits geschrieben, wobei als Trennzeichen jeweils ein Doppelpunkt dient.

Beispiel: 2001:628:2010:100a:0:0:0:abc

Zur weiteren Verkürzung kann innerhalb einer Adresse eine einzelne Folge von Nullen wie z.B. 0:0 ausgelassen werden. Zur Wahrung der Eindeutigkeit des angegebenen Wertes darf die dazu verwendete Zeichenfolge „::“ nur einmal in einer IPv6-Adresse aufscheinen.

Beispiel: 2001:628:2010:100a:0:0:0:abc   ==   2001:628:2010:100a::abc

Da aus Kompatibilitätsgründen auch IPv4-Adressen in den IPv6-Adressraum aufgenommen wurden, gibt es dafür eine entsprechende „kompatible“ dezimale Schreibweise.

Beispiel: ::ffff:140.78.3.160   anstatt von   ::ffff:8c4e:3a0

M.7.1.2 IPv6-Adressen und Subnetting

Die Grundstruktur von IPv6-Netzen und das Subnetting sind wie bei IPv4. Die Netzwerkmaske wird in der bereits bei IPv4 besprochenen „Schrägstrich-Schreibweise“ angegeben. Typische Ethernet-Netze haben eine Netzwerkmaske von /64, wie das folgende Beispiel zeigt:

2001:628:2010:100a: 0:0:0:abc /64

Netzwerk-ID Host-ID Subnetmask

Abb. M22  Zusammenwirken von Netzwerkmaske, Netzwerk-ID und Host-ID

Anstatt der Netzwerkklassen von IPv4 sind verschiedene Adressbereiche für bestimmte Aufgaben reserviert. Die folgende Tabelle gibt dafür einige Beispiele an, ohne diese jedoch alle näher im Text zu erläutern.

Tabelle M3   Beispiele für den Einsatz von IPv6-Adressbereichen

M.7.1.3 Verfeinerung der Adressierung von IPv6-Host --> Prozess

Möchte man eine IPv6-Adresse mit einer Portnummer schreiben, so wird zur Wahrung der Eindeutigkeit die IPv6-Adresse mit eckigen Klammern zusammengefasst.

Als Beispiel soll ein Zugriff auf den Webserver http://ipv6.jku.at mit der ausgeschriebenen IPv6-Adresse 2001:628:2010:6::111 und der expliziten Angabe des Standard-Ports 80 für HTTP dienen. Damit schreibt man im Webbrowser dann http://[2001:628:2010:6::111]:80.

Abb. M23   Webbrowser mit Angabe der IPv6-Adresse und Port-Nummer

M.7.2 IPv6 Client-Konfiguration

M.7.2.1 Wesentliche Konfigurationsinformationen

Abb. M24 zeigt eine typische IP-Basiskonfiguration, wie diese über die Windows-Kommandozeile ausgelesen wird.

Abb. M24   Feststellen der IPv6-Konfiguration über Kommandozeile (Output gekürzt!)

Bei Unix bzw. Linux setzt man dafür Befehle wie „ifconfig“ bzw. „ip“ ein.

M.7.2.2 Typen der Konfiguration eines IPv6-Hosts

Meist werden IPv6-Host nicht manuell konfiguriert, sondern erhalten ihre IPv6-Konfiguration von einem DHCPv6-Server.

IPv6 unterstützt die automatische eigenständige Konfiguration für die Kommunikation innerhalb eines physikalischen (Sub-)Netzes. Dafür verwendet IPv6 den „Link-Lokalen Adressbereich“, wobei die Host-ID meist aus der 6 Bytes großen „Physical Address“ der Ethernet-Netzwerkkarte errechnet wird. Man beachte dazu die Ähnlichkeit der Adresse der Netzwerkkarte 02-11-22-33-0A-11 mit der Link-Lokalen Adresse fe80::11:22ff:fe33:a11 in der vorhergehenden Abbildung.

Des Weiteren können Router – typischerweise das Default Gateway – den IPv6-Hosts im Netz mitteilen, in welchem IPv6-Netzwerk bzw. in welchen IPv6-Netzwerken sie sich befinden. Diese Konfigurationsart wird als Stateless Address Autoconfiguration (SLAAC) bezeichnet.

In Abb. ‎M 24 ist zu sehen, dass der Host die IPv6-Adresse 2001:628:2010:100a:11:22ff:fe33:a11 für die globale Kommunikation einsetzt.

Das angeführte Netzwerk-Präfix 2001:628:2010:100a::/64 hat der Host dabei von seinem Default Gateway erhalten, die Host-ID stammt wiederum von der Hardwareadresse der Netzwerkkarte.

Der IPv6-Host erkennt hier sein Default Gateway automatisch, da dieses sich selbst und das Netzwerk-Präfix durch sogenannte „Router Advertisements“ im Netz bekannt macht. Auch können bei dieser Konfiguration zusätzlich weitere Informationen (wie z.B. die DNS-Server) von einem DHCPv6-Server bezogen werden. Die vorige Abb. M24 zeigt dies.

Eine Anmerkung darf bei dieser oft verwendeten Art der Ableitung der öffentlichen IPv6-Adresse von der Hardware-Adresse der Netzwerkkarte nicht fehlen: Egal unter welchem Netz ein Rechner (z.B. Notebook) betrieben wird, er ist durch die fixe Adresse der Netzwerkkarte und damit durch fixe Teile der IPv6-Adresse „identifizierbar“. So kann beispielsweise ein von einem Notebook von verschiedenen Lokationen angesprochener Webserver damit den Rechner als „roaming“ erkennen und daraus Rückschlüsse ziehen. Es gibt auch andere Möglichkeiten, die Host-ID zu erzeugen.

M.7.3 Testen einer IPv6-Konfiguration

Das Auslesen der aktuellen IPv6-Konfiguration wurde an einem Beispiel bereits gezeigt. Wir testen im Folgenden die Konfiguration und die Erreichbarkeit mit einem Ping und mit Traceroute.

Anzumerken dazu ist, dass IPv6 kein ARP (Address Resolution Protocol) verwendet!

Abb. M25   Ping und Tracert (ping6 und traceroute bei Unix/Linux)

Das obige Beispiel „Ping“ zeigt sehr klar, dass das Arbeiten mit IPv6 ohne DNS-Server auch für Tests bereits sehr mühsam ist. Ab Traceroute wird daher bereits mit DNS-Namensauflösung gearbeitet.

M.7.4 Auswahl zwischen IPv4 und IPv6

In der näheren Zukunft werden viele Systeme im Internet mit IPv4 und IPv6 gleichzeitig arbeiten müssen. Das IP-Schichtenmodell eines Systems mit IPv4- und IPv6-Netzwerkstack kann man damit wie folgt sehen:

Abb. M26   IP Schichtenmodell für ein System mit IPv4- und IPv6-Netzwerkstack

Daher stellt sich für einen Benutzer zukünftig die Frage, über welches der beiden Protokolle (IPv4 oder IPv6) man z.B. auf einen Webserver zugreift oder die DNS-Namensauflösung abläuft. Diese Fragen werden insbesondere dann auftreten, wenn man z.B. über IPv6 auf einen Server zugreifen möchte, dieser aber über IPv6 überhaupt nicht oder nur schlecht/langsam erreichbar ist.

Bei den bisherigen einfachen Beispielen mit direkter Angabe einer IP-Adresse war eindeutig, mit welchem Protokoll jeweils gearbeitet wird. Sobald aber DNS-Namen eingesetzt werden, können die Applikation bzw. das Betriebssystem das Protokoll auswählen. Voraussetzung für die Auswahlmöglichkeit ist, dass zu dem DNS-Namen (zumindest) sowohl eine IPv4- als auch eine IPv6-Adresse verfügbar sind:

Tabelle M4    Protokollauswahl je nach Typ der IP-Adresse (IPv4 oder IPv6)

Manche Applikationen kann man so konfigurieren, dass sie speziell nur mit einem Protokoll arbeiten. Als Beispiel dafür sei wiederum der Test-Befehl „ping“ bei Windows und Unix/Linux angegeben.

Tabelle M5   Auswahl von IPv4 oder IPv6 im Ping-Befehl

Bei einem Webbrowser hingegen gilt typischerweise die Protokoll-Präferenz der Applikation bzw. des Betriebssystems. Während Windows XP und Windows 2003 noch IPv4 als Standardprotokoll einsetzen, hat sich die Reihenfolge bei Windows 2008 und Windows 7 auf „zuerst IPv6 und dann IPv4 probieren“ geändert.

Um den Benutzern eine einfache Möglichkeit zu geben, zwischen IPv4 und IPv6 auswählen zu können, werden für IPv4- und IPv6-Adressen oft verschiedene DNS-Namen vorgegeben. Ein Beispiel dafür ist zurzeit Google. Während www.google.com auf IPv4-Adressen aufgelöst wird, zeigt der Name ipv6.google.com an, dass hier IPv6-Adressen zum Einsatz kommen.

Abb. M27   Tool nslookup zum Test der Namensauflösung (Output gekürzt)

M.7.5 IPv6-Adressvergabe

Während man heute um jeden neuen kleinen IPv4-Adressbereich „ringen“ muss, erhält man – beispielsweise als Kleinunternehmen – ohne weitere Begründung von seinem Internet-Service-Provider (ISP) einen Präfix der Größe /48. Damit hat man als Firma 80 Adressbits für die „private Topology“ zur Verfügung. Mit einer Standard-Subnetzmaske von /64 für die Subnetze bleiben 16 Bits für mehr als 65000 Subnetze. In jedem einzelnen Subnetz lassen sich faktisch unbeschränkt viele (theoretisch maximal 264) IPv6-Adressen adressieren.

Abb. M28   Typische Aufteilung der 128 Bits einer IPv6-Adresse

Diese typische Adressaufteilung soll am Beispiel der Johannes Kepler Universität (JKU) kurz wiederholt werden. Dabei wird der Adressraum hierarchisch weiter unterteilt. So weist die JKU den einzelnen Instituten entsprechende Teilnetze zu, die diese wiederum unterteilen können.

Tabelle M6   IPv6 Adressaufteilung am Beispiel von JKU und FIM

Wie das Beispiel zeigt, kann die JKU mit diesem Schema rund 4000 Institute mit jeweils 16 „riesigen“ Netzen versorgen. Sollte trotzdem noch weiterer Bedarf an IPv6-Adressen bestehen, so erhält die JKU ohne jegliche Probleme weitere /48-Präfixe zugewiesen.

ABBILDUNGSVERZEICHNIS

Abb. A1   Schichtenmodell

Abb. A2  Bedienungsparadigma: Löschen eines Dokumentes durch Mausbewegung

Abb. A3   Vergrössern des Textes durch „Spreizen“

Abb. A5   Schichtenmodell (mit mehr Details)

Abb. B1   Prozessorarchitektur mit Speicher und Ein-/Ausgabe

Abb. B2   Von-Neumann-Zyklus

Abb. B3   Hardware-Beispiel

Abb. B4   Zeitablauf eines Interrupts

Abb. B5   Prozessor mit Interrupt-Eingang

Abb. B6   Von-Neumann-Zyklus mit detaillierter Interrupt-Behandlung

Abb. B7   Prozessor- und Speicherstatus kurz vor Verlassen der ISR

Abb. B8   Prozedur Sum0: „Call by Value“

Abb. B9   Prozedur Sum1: „Call by Reference“ für den ersten Parameter

Abb. C1   Über Middleware für Gridcomputing erweitertes Betriebssystem

Abb. D1   Monolithisches Betriebssystem

Abb. D2   Schichtenmodell in DOS

Abb. D3   Schichtenmodell des THE-OS

Abb. D4   Architektur Client Server-System

Abb. D5  Client Server-Modell über Netzwerk

Abb. D6   Hauptspeicheraufteilung mit Partitionen fixer Größe

Abb. D7   Aufgaben der Speicherverwaltung

Abb. D8   Virtualisierung für mehrere Betriebssysteme auf einer Hardware

Abb. D9  Hypervisor – Allgemeine Architektur

Abb. D10   Hosted Hypervisor („Typ 2“)

Abb. D11   Bare Metal Hypervisor („Typ 1“) 4

Abb. D12   Kernelbasierte Virtuelle Maschine (KVM)

Abb. E1   Prozessbeschreibungsblock

Abb. E2   Prozesszustandsdiagramm

Abb. E3   Prozesszustandsdiagramm für MS-Windows (nach Unterlagen der Firma Microsoft)

Abb. F1   CPU-Scheduling – Warteschlangen

Abb. F2   Definition der Verweilzeit

Abb. F3  Definition der Wartezeit (aus Sicht des CPU-Schedulings)

Abb. F4   Beispiel für First Come First Served (FCFS)

Abb. F5   Beispiel für Shortest Job First (SJF)

Abb. F6  Priority Scheduling (Highest Priority First = HPF) im Vergleich zu Shortest Job First (SJF)

Abb. F7   Beispiel für Starvation bei Shortest Job First

Abb. F8   Beispiel für den Vergleich Non-Preemptive versus Preemptive Priority Scheduling

Abb. F9   Round-Robin bei Timesharing

Abb. F10   Multilevel Feedback Queue Scheduling

Abb. G1   (partiell) vorgegebene zeitliche Reihenfolge von Operationen

Abb. G2   Verzahnung bzw. Parallelität bei 1, 2 oder 3 Prozessoren (CPUs)

Abb. G3   Keine Aussagen über die zeitliche Abfolge

Abb. G4   Zeitlicher Ablauf zur Berechnung eines Klammerausdrucks

Abb. G5   Threads in der Java VM – Konsolen Ausgabe von Code ‎G 3 bei preemptiven Scheduling

Abb. G6   Thread-Prioritäten in Windows

Abb. G7   Genauere CPU-Zuteilung in Windows

Abb. G8   Producer-Consumer – Puffer

Abb. G9   Producer-Consumer – Zugriff auf gemeinen Zähler „counter“

Abb. G10   Producer-Consumer – Zugriff auf gemeinsamen Zähler „counter“ führt zu Problemen

Abb. G11   Mutual Exclusion über Kritische Region

Abb. G12  Wechselseitiger Ausschluss erreicht, aber Verklemmung möglich

Abb. G13   Semaphore mit zwei wartenden Prozessen W und X

Abb. G14   Semaphore zeigt durch S.value = -3 drei wartende Prozesse

Abb. G15   Synchronisation über Semaphore mit wait und signal

Abb. H1   Synchronisation über Semaphore mit wait und signal

Abb. H2   Beispiele für das Schalten eines Petrinetzes

Abb. H3   Beispiel einer Schaltfolge eines Petrinetzes (t1 schaltet vor t2)

Abb. H4   Buchausleihen als Petrinetz

Abb. H5   Buchausleihen als Petrinetz, Deadlock

Abb. H6   Die essenden Philosophen: Ausgangssituation

Abb. H7   Die essenden Philosophen: Deadlock-Situation

Abb. H8   Sicherer Zustand  unsicherer Zustand

Abb. H9   Sicherer Zustand  sicherer Zustand

Abb. H10   „Warte auf Graph“: Links mit Ressourcen, rechts die vereinfachte Form

Abb. H11   Kreis im „Warte auf Graphen (P1, P2) und zusätzlich blockierte Prozesse P3 und P4

Abb. I1   Übersetzen und anschließendes Laden eines Moduls

Abb. I2   Linken mehrerer Object-Module und anschließendes Laden

Abb. I3 Statisches Linken: Module l1 und l2 befinden sich mehrfach im Speicher

Abb. I4   Dynamisches Linken: d1 und d2 werden parallel für c1 und c2 verwendet

Abb. I5   Basisfunktion einer Memory-Management Unit (MMU)

Abb. I6   Beispiel MMU (Basisadressregister)

Abb. I7   Einfachste Speicheraufteilung: Betriebssystem / Benutzerprozess(e) / frei

Abb. I8   Speicheraufteilung bei fixen Partitionen

Abb. I9   Das Problem mit fixen Partitionen

Abb. I10   Doppelt verkettete Liste zur Verwaltung der freien Blöcke

Abb. I11   Interne Fragmentierung

Abb. I12   Speicherlayout bei interner Fragmentierung

Abb. I13    Beispiel für externe Fragmentierung - Speicherlayout

Abb. I14   Beispiel für Verdichtung

Abb. I15   Struktur einer logischen Adresse bei Paging

Abb. I16   Zuordnung Seiten des logischen Adressraumes zu Rahmen im realen Adressraum

Abb. I17   Von der logischen zur physikalischen Adresse

Abb. I18   Optimierung über Translation Look-Aside Buffer (TLB)

Abb. I19   Read-Only-Bit und Valid-Bit in den Einträgen der Seitentabelle

Abb. I20   Shared Pages

Abb. J1   Zweistufiges Paging (ab Intel 80386; nach Unterlagen der Firma Intel)

Abb. J2  Speicheraufteilung in Windows für die 32-Bit Version

Abb. J3   Anomalie nach Belady

Abb. J4   Beispiel: Least Recently Used (LRU) mit Größe des Working-Sets = 3

Abb. J5   Beispiel: Not Used Recently (NUR) mit Größe des Working-Sets = 3

Abb. J6   Viele parallele Prozesse, kleine Workingsets, Thrashing

Abb. J7   Beispiele für Working Sets

Abb. J8   Working Set (WS) Messung

Abb. K1   Schichtenmodell für die Dateiverwaltung

Abb. K2   Verzeichnisstruktur mit Wurzel (root), Unterverzeichnissen und Dateien

Abb. K3   Aufteilung einer Harddisk im mehrere Partitionen + Boot von der Harddisk

Abb. K4   Verkettung in den Daten-Clustern

Abb. K5   FAT Dateisystem

Abb. K6   Clusterzuordung über Indexblock

Abb. K7   Clusterzuordnung über Data-Runs

Abb. K8   Clusterzuordnung über Inodes (insb. Unix Dateisystem)

Abb. K9   Zeiger auf Inode und Referenzzähler

Abb. K10   Beispiel für die Struktur einer Partition

Abb. K11   Struktur des Windows Master File Table (MFT)

Abb. K12   Struktur eines MFT-Records

Abb. K13   NTFS residente und nicht-residente Attribute am Beispiel Dateiinhalt

Abb. K14   Baum-Struktur eines Directory Eintrages (Quelle: H.Custer)

Abb. K15   Treiberstruktur in Windows zur Unterstützung mehrerer Dateisysteme

Abb. K16   RAID_0: Striping

Abb. K17   RAID_1: Mirroring (Spiegelung) und Kombination von RAID 0 mit RAID 1

Abb. K18   RAID_1: Mirroring (Spiegelung) und Kombination von RAID_0 mit RAID_1 1

Abb. L1   Challenge Response Verfahren mittels einem Captcha

Abb. L2   Windows Security Identifier (SID)

Abb. L3   Blockstruktur beim MD5

Abb. L4   Referenzmonitor

Abb. L5   Informationsfluss

Abb. L6   Beispiel: Hasse-Diagramm

Abb. L7   Beispiel: Liste der Benutzer

Abb. L8   Beispiel: ACL der Datei Sec1.doc für Joerg

Abb. L9   Beispiel: ACL der Datei Sec1.doc für Rudi

Abb. L10   Beispiel: ACL des Verzeichnisses Skripten für Joerg

Abb. L11   Beispiel: ACL des Verzeichnisses Skripten für Rudi

Abb. L12   Beispiel: ACL einer im Verzeichnis Skripten neu errichteten Datei für Joerg

Abb. L13   Beispiel: ACL einer im Verzeichnis Skripten neu errichteten Datei für Rudi

Abb. L14   Beispiel: ACL auf Datei sec9999.doc mit weiteren LektorInnen

Abb. L15   Beispiel: ACL auf Verzeichnis Skripten für Autor Joerg und die Gruppe der LektorInnen

Abb. L16   Beispiel: Gruppenmitgliedschaft(en) in Windows

Abb. L17   Beispiel: Berechtigungen der Gruppe SkriptenDruck auf einem LaserJet 9500

Abb. L18   Beispiel: Access-Token von User Joerg und Christine + daraus resultierende Berechtigungen

Abb. L19   Beispiel ACL: Versuch von User Christine, die Datei div1.doc zu löschen

Abb. L20   Benutzer mit einer aktiven Sitzung im Basis RBAC Modell (Core RBAC)

Abb. L21   Core RBAC Referenz Modell

Abb. L22   Hierarchisches RBAC – Benutzer und Rollen Vererbung

Abb. L23   Core RBAC Referenzmodell mit Dynamic Separation of Duties (DSD)

Abb. M1   IP Datenübertragung

Abb. M2   TCP Datenübertragung

Abb. M3   IP Schichtenmodell und Beispiele für Protokolle

Abb. M4   Beispiel IP-Adresse in Standardschreibweise und als Bitmuster

Abb. M5   Direkte Kommunikation innerhalb des eigenen Netzes, sonst Default Gateway

Abb. M6   Aufteilung einer IP-Adresse in Netzwerk-ID und Host-ID

Abb. M7   IP Netzwerkklassen: Überblick

Abb. M8   IP-Adresse mit Netzwerk-ID, Subnetzwerk-ID und Host-ID

Abb. M9   IP-Adresse und Subnetzmaske

Abb. M10   Portnummern für Server- und Client-Prozesse

Abb. M11   IP-Einstellungen am Beispiel Windows

Abb. M12   Funktion von DNS und Reverse DNS

Abb. M13   Feststellen der IP-Konfiguration über Kommandozeile

Abb. M14   Feststellen der IP-Konfiguration über das GUI

Abb. M15   Ping auf das Default-Gateway

Abb. M16   Erreichbarkeitstest eines Rechners im lokalen Netzwerk mit ARP

Abb. M17   Test der DNS Namensauflösung mit Ping

Abb. M18   Tool nslookup zum Test der Namensauflösung (Output gekürzt)

Abb. M19   IP-Route mit Hops zwischen Source und Destination

Abb. M20   Traceroute von einem Laborrechner zu www.jku.at (Output gekürzt!)

Abb. M21   IP-Route vom Laborrechner zu www.jku.at

Abb. M22>   Zusammenwirken von Netzwerkmaske, Netzwerk-ID und Host-ID

Abb. M232   Webbrowser mit Angabe der IPv6-Adresse und Port-Nummer

Abb. M24   Feststellen der IPv6-Konfiguration über Kommandozeile (Output gekürzt!)

Abb. M25   Ping und Tracert (ping6 und traceroute bei Unix/Linux)

Abb. M26   IP Schichtenmodell für ein System mit IPv4- und IPv6-Netzwerkstack

Abb. M27   Tool nslookup zum Test der Namensauflösung (Output gekürzt)

Abb. M28   Typische Aufteilung der 128 Bits einer IPv6-Adresse

VERZEICHNIS DER PSEUDOCODE BEISPIELE

Code B1   Erstes Beispiel für Assembler Pseudocode

Code B2   Polling-Programmsequenz (Java)

Code B3   Polling-Programmsequenz (Assembler)

Code B4   Prozedur WaitForKey inkl. Prozeduraufruf

Code B5   Prozedur ReadKey inkl. Prozeduraufruf

Code D1   Schnittstellen virtuelle Tastatur

Code D2   Auslesen hardwarebezogener Systemdaten in Windows

Code G1   Syntaktische Erweiterung für Programmiersprachen zur Darstellung von Parallelität

Code G2   Threads in der Java VM – Text Ausgabe eines Threads

Code G3   Threads in der Java VM – Text Ausgabe beider Threads

Code G4   Producer-Consumer – Puffer

Code G5   Producer-Consumer – Consumer holt nächstes Element vom Puffer

Code G6   Producer-Consumer – Produzent generiert nächstes Element und legt es in den Puffer

Code G7   Producer-Consumer – Producer busy waiting

Code G8   Producer-Consumer – Consumer busy waiting

Code G9   Verletzung von Mutual Exclusion

Code G10   Mutual Exclusion durch erzwungener Ablaufreihenfolge

Code G11   Mutual Exclusion, aber Progress Verletzung

Code G12   TestAndSet

Code G13   TestAndSet vereinfacht

Code G14   Mutual Exclusion gelöst durch TestAndSet für kurze CRs

Code G15   TestAndSet in Java

Code G16   Spin Lock

Code G17   Anwendung Spin Lock

Code G18   lock / unlock für atomare Code Abschnitte

Code G19   Hinzufügen/Entfernen eines Prozesses in/von der Warteschlange der Semaphore

Code G20   Prozess Zustandsüberführungsfunktionen block und wakeup

Code G21   Beschreibung Semaphore in Pseudocode

Code G22   Operationen signal und wait auf eine Semaphore

Code G23   Mutex Semaphore in Pseudocode zur Realisierung einer CR

Code G24   Producer-Consumer: Produzent in Pseudocode realisiert mit Semaphore

Code G25   Producer-Consumer: Konsument in Pseudocode realisiert mit Semaphore

Code G26   Critical Region realisiert in Java durch synchronized

Code G27   Monitor in Java, Sperre auf Objekte

Code G28   Monitor in Java, Sperre auf Klasse

Code H1   Essende Philosophen falscher Versuch

Code I1   Programmausschnitt – Speicherung von Daten vom Port 1 in Hauptspeicher

Code I2   Programm mit symbolischen Adressen

Code I3   Programm mit fiktiven realen Adressen

Code J1   Prinzip des NUR (Clock) Seitenersetzungs-Algorithmus

Code J2   Zeilenweiser bzw. spaltenweiser Zugriff auf eine quadratische Matrix

Code L1   Ausschnitt MD5 Hasherzeugung

Code M1   Liegen Rechner [eigeneIP, subnetzMaske] und [fremeIP] im gleichen Netz?

TABELLENVERZEICHNIS

Tabelle B1   Vergleich Interrupts, Software-Interrupts, Prozeduren

Tabelle I1   Beispiel für externe Fragmentierung – Speicherbedarf der Prozesse

Tabelle I2   Beispiel für externe Fragmentierung – Speicherbedarf der Prozesse

Tabelle J1   Anzahl der notwendigen Lade- und Speicheroperationen

Tabelle L1   Zeitaufwand für Brute-Force Passwort Attacken

Tabelle L2   Beispiel Zugriffskontroll-Matrix

Tabelle L3   Beispiel: Zugriffskontroll-Matrix M

Tabelle L4   Beispiel: Zugriffskontroll-Matrix M für weitere Benutzer

Tabelle M1   IP Netzwerkklassen: Anzahl der verfügbaren Bits, Netzwerk-IDs und Host-IDs

Tabelle M2   IP-Adressen innerhalb des Netzes 10.5.6.80/29

Tabelle M3   Beispiele für den Einsatz von IPv6-Adressbereichen

Tabelle M4   Protokollauswahl je nach Typ der IP-Adresse (IPv4 oder IPv6)

Tabelle M5  Auswahl von IPv4 oder IPv6 im Ping-Befehl

Tabelle M6   IPv6 Adressaufteilung am Beispiel von JKU und FIM

LITERATUR

[Bel66] Belady, L. A.: “A Study of Replacement Algorithms for a Virtual Storage Computer”. IBM Systems Journal 5, 79-101, 1966.

[Bel69] Belady, L. A., Nelson, R. A. und Shedler, G. S.: “An Anomaly in Space-Time Characteristics of Certain Programs Running in a Paging Machine“. Commun. of the ACM, 12, 349-353, 1969.

[Bel73] Bell, D. und La Padula, L.: “Secure Computer Systems: Mathematical Foundations and Model“. Technical Report MTR 2547 v2, Mitre Corp., 1973.

[Bri72] Brinch Hansen, P.: “Structured Multiprogramming”. Commun. of the ACM 15(7), 574-578, 1972.

[Bri73] Brinch Hansen, P.: “Operating System Principles”. Prentice Hall, Englewood Cliffs. NJ 1973.

[Bri75] Brinch Hansen, P.: “The Programming Language Concurrent Pascal“. IEEE Trans. on Software Engineering, SE-1, 199-207, 1975.

[Bün07] Bünning, U., und Rzepka, D.: Windows Vista, Hanser 2007.

[Car81] Carr, R. W. und Hennessy, L. J.: “WSCLOCK – a simple and effective algorithm for virtual memory management”. ACM SIGOPS Operating Systems Review, Vol. 15, No. 5, 87-95, ACM 1981.

[Car05] Carrier, B.: “File System Forensic Analysis” Addison Wesley (Pearson Edition), 2005.

[Che94] Chen, P. M., Lee, E. K., Gibson, G. A., Katz, R. H. und Patterson, D. A.: “RAID: High Performance Reliable Storage“. Comp. Surv., 26, 145-185, 1994.

[Cof71] Coffman, E. G., Elphick, M. J. und Shoshani, A.: “System Deadlocks“. Computing Surveys, 3, 67-78, 1971.

[Cus93] Custer, H.: “Inside Windows NT”. Microsoft Press, Redmond, Washington 1993.

[Cus94] Custer, H.: “Inside the Windows NT File System” Microsoft Press, Redmond, Washington 1994

[Dal68] Daley, R. C. und Dennis, J. B.: “Virtual Memory, Process, and Sharing in MULTICS“. Commun. of the ACM, 11, 306-312, 1968.

[Den76] Denning, D.: “A Lattice Model of Secure Information Flow“. Commun. of the ACM, 19, 236-243, 1976.

[Den68/1] Denning, P. J.: “The Working Set Model for Program Behavior“. Commun. of the ACM, 11, 323-333, 1968.

[Den68/2] Denning, P. J.: “Thrashing: Its Causes and Prevention“. In: Proc. AFIPS National Computer Conf., 915-922. AFIPS, 1968.

[Den68/3] Denning, P. J.: “Thrashing: Its Causes and Prevention“. In: Proc. AFIPS National Computer Conf., 915-922. AFIPS, 1968.

[Den70] Denning, P. J.: “Virtual Memory“. Computing Surveys, 2, 153-189, 1970.

[Den80] Denning, P. J.: “Working Sets Past and Present“. IEEE Trans. on Software Engineering, SE-6, 64-84, 1980.

[Den00] Denning, D.: “Information Warfare and Security”. Addison Wesley, 2000.

[Di86] Dierstein, R: Basic Function of Secure Systems. In: Proc. Securicom 86, 467 – 476, Paris, 1986.

[Di90] Dierstein, R: The Concept of Secure Information Processing Systems and their Basic Functions. In: Proc. IFIP SEC 90, Elsevier, 1991.

[Dij68] Dijkstra, E. W.: “The Structure of THE Multiprogramming System“. Commun. of the ACM, 11, 341-346, 1968.

[Dij85] Dijkstra. E. W.: “Cooperating Sequential Processes”. Technical Report, 1965. Reprinted in Genuys (ed.): Programming Languages. Academic Press, London 1985.

[Eck06] Eckert, C.: “IT Sicherheit”. Oldenbourg, 4. überarbeitete Auflage, 2006.

[Fer01] Ferraiolo, D. F., Sandhu, R., Gavrila, S., Kuhn, D. R. und Chandramouli, R. “Proposed NIST Standard for Role-Based Access Control“. ACM Transactions on Information and System Security, Vol. 4, No. 3, 224 - 274, 2001.

[Fot61] Fotheringham, J.: “Dynamic Storage Allocation in the Atlas Including an Automatic Use of a Backing Store“. Commun. of the ACM, 4, 435-436, 1961.

[Gla06] Glatz. E.: Betriebssysteme. dpunkt.verlag, 2006

[Gol01] Gollman, D.: “Computer Security”, J. Wiley, 2001.

[Hab69] Habermann, A. N.: “Prevention of System Deadlock”. Commun. of the ACM 12(7), 373-377, 385, 1969.

[Har76] Harrison, M. A., Ruzzo, W. L. und Ullman, J. D.: “Protection in Operating Systems“. Commun. of the ACM, 19, 461-471, 1976.

[Hav68] Havender, J. W.: “Avoiding Deadlock in Multitasking Systems“. IBM Systems Journal, 7, 74-84, 1968.

[Hoa74] Hoare, C. A. R.: “Monitors: An Operating System Structuring Concept”. Commun. of the ACM 17(10), 549-557, 1974.

[Hoa78] Hoare, C. A. R.: “Communicating Sequential Processes”. Commun. of the ACM 21(8), 666-677, 1978.

[Hol72] Holt, R. C.: “Some Deadlock Properties of Computer Systems”. ACM Computing Surveys 4, 179-196, 1972.

[Hör10] Hörmanseder, R.: Computernetzwerke, Grundlagen. Trauner Verlag, 2010. ISBN 978-3-85499-788-7.

[Lam68] Lampson, B. W.: “A Scheduling Philosophy for Multiprogramming Systems“. Commun. of the ACM, 11, 347-360, 1968.

[Lam73] Lampson, B. W.: “A Note on the Confinement Problem“. Commun. of the ACM, 10, 613-615, 1973.

[Li07] Li, N., Byun J.-W. und Bertino E.:. “A Critique of the ANSI Standard on Role-Based Access Control “. IEEE Security & Privacy, 41-49, 2007.

[New79] Newton, G.: “Deadlock Prevention and Resolution: An Annotated Bibliography“. Operating Systems Review, 13, 33-44, 1979.

[Pat88] Patterson D.A., Gibson G., Katz R. H.: “A Case for Redundant Arrays of Inexpensive Disks (RAID)”. Proc. of the ACM SIGMOD, Juni 1988

[Pet81] Peterson, G. L.: “Myths about the Mutual Exclusion Problem”. Information Processing Letters 12, 115-116, 1981.

[Riv92] Rivest, R. L.: “The MD5 Message-Digest Algorithm“. RFC 1320, 1992.

[Sal05] Sales, J.: “Symbian OS Internals”. J. Wiley & Sons, 2005.

[Sal75] Saltzer, J. H. und Schroeder, M. D.: “The Protection of Information in Computer Systems“. In: Proc. IEEE, Band 63, 1278-1308, 1975.

[San96] Sandhu, R. S., Coyne, E. J., Feinstein H. L. und Youman C., E.: “Role-Based Access Control Models“. IEEE Computer, Vol. 29, No. 2, 38-47, 1996.

[Sil05] Silberschatz, A., Galvin, P. B. und Gagne, G.: “Operating System Concepts“. Wiley, John & Sons Incorporated, 2005 (7. Auflage).

[Sol00] Solomon, D. A. und Russinovich, M. E.: “Inside Windows 2000”. Microsoft Press, Redmond, WA, 3. Auflage, 2000.

[Sta01] Stallings, W.: “Operating Systems”. Prentice Hall, Upper Saddle River, NJ, 4. Auflage, 2001.

[Sta03] Stallings, W.: “Betriebssysteme, Prinzipien und Umsetzung”. Pearson Studium, 2003.

[Tan87] Tanenbaum, A. S.: “Operating Systems: Design and Implementation”. Prentice Hall, Upper Saddle River, NJ, 1987.

[Tan03] Tanenbaum, A. S.: “Moderne Betriebssysteme”. 2., überarbeitete Auflage, Pearson Studium, 2003.

[Tan08] Tanenbaum, A. S., van Stehen, M: “Verteilte Systeme”.

Pearson Studium, 2008.

INDEX

A

ablaufinvariant Siehe reentrant

absolute Adressen

Access Control Matrix

Access-Token

ACE (Access Control Entry)

ACL (Access Control List)

Admission Scheduler

Adressbindung (address binding)

zur Übersetzungszeit

Adresse des Ladepunktes Siehe starting point

Aging

Akkumulator

Antwortzeit

API (Application Programming Interface)

APIPA (Automatic Private IP Address)

arp

ARP – Adress Resolution Protocol

atomar

atomare Operationen

Auslastung

Authentifizierung

authentisch

authentisieren

Authentisierung

starke

Authentizität

B

BAM (Block Availability Map)

Banker‘s Algorithm

Basisadresse

Batch-Dateien

Belady-Anomalie

Benutzerauftrag

Benutzerschnittstelle

Beschäftigtes Warten Siehe busy waiting

Beschleunigung

Best Fit

Bestätigungen (Acknowledges, ACKs)

Best-Effort-Delivery

Betriebsart

Betriebsmittel

Aktive

Dynamische

Passive

Statische

Betriebsmittelverwaltung

Betriebssystem-Kern Siehe Kernel

Binder Siehe Linker (Linkage Editor)

block

Block

Bootstrap

bounded waiting

Brute-Force Attacke

Buddy Methode

busy waiting

C

Call by Reference

Call by Value

Capability-List

CFS Scheduler (Completely Fair Scheduler)

Challenge Response

circular wait

Client Server Modell

Clients Siehe Klienten

Clock Algorithm Siehe NUR (Not Used Recently)

Cloud Computing

Cluster

compacting strategy

Concurrent Processes

Confinement Problem

Consumer

Contiguous Memory Allocation

Controller

cooperative

copy on write

CPU-burst

CPU-Scheduling

CR (Critical Region, Critical Section)

D

DAC (Discretionary Access Control)

Data-Runs

Dateisystem Siehe Filesystem

Dateiverwaltung

Datenschutz

Datensicherheit

Deadline

Deadlock

Erkennen

Erkennen und Wiederherstellen (Detection and Recovery)

Vermeiden (Avoidance)

Vorbeugen (Prevention)

Wiederherstellen

Default Gateway (Default Router, Standard-Gateway)

Defragmentierung

Destination Address

device driver

Device-Controller

DHCP (Dynamic Host Configuration Protocol)

Dienste

directory

Disk Quota

Block Quota

File Quota

Hard Quota

Quota Tabelle

Soft Quota

disk volume

Disk-Mirrors

DNS (Domain Name Services)

DNS-Server

Durchsatz

Dynamic Link Libraries (DLL)

Dynamisches Linken (dynamic linking)

E

echo request

Echtzeitbetriebssysteme

Echtzeitverarbeitung

Einbenutzerbetrieb

Einwegfunktion

Emulation

EPOC Kernel

Ereignis

nebenläufig

Ersetzungsregel (replacement strategy)

Ersetzungsstrategie (replacement policy)

Erzeuger Siehe Produzent (Producer)

F

Fähigkeiten Siehe Capability-List

Fair Share Scheduling

Fairness

FAT (File Allocation Table)

FCFS (First Come First Served)

Fence Register

fetching on demand

Fetch-Zyklus

File

Fileheader

Filesystem

Fileverzeichnis Siehe directory

First Fit

fixe Partitionen

Flagregister

folder

Foreground Window

FQN (Fully Qualified Name)

Fragmente

Fragmentierung

externe

interne

frame

Framenummer

Funktionssicherheit

G

Garbage Collection

Garbage Collector

Green Threads

Gridcomputing

GroupSID

Gruppen

GUI (Graphical User Interface)

H

Halten und Warten (hold and wait)

Hard Real-Time Systems

Hashfunktion

Hauptspeicher

Heap

Hop

Host-ID

HPF (Highest Priority First)

HTTP (Hypertext Transfer Protocol)

Hypervisor

Bare Metal Hypervisor („Typ 1“)

Hosted Hypervisor („Typ 2“)

I

ICMP (Internet Control Message Protocol)

Identität

Indefinite Blocking Problem Siehe Starvation Problem

Informationssicherheit

Infrastructure as a Service (IaaS)

init(S,n)

Inodes

double (indirect) block

single (indirect) block

triple (indirect) block

Interrupt

Interrupt Handling

Interrupt-Controller

Interrupt-Service-Routine

Interrupt-Signal

Interruptvektor-Tabelle

IP (Internet Protocol)

IP Client-Konfiguration

IP-Adresse

IPv4 = Internet Protocol Version 4

IPv6 = Internet Protocol Version 6

J

Java VM

JCL (Job Control Language)

Journaling File Systems

K

Kein Betriebsmittelentzug Siehe no preemption

Kernel

Kernelbasierte Virtuelle Maschinen (KVM)

Kernel-Mode

Klienten

kollisionsresistent

Konfiguration eines Schutzsystems

Konsument Siehe Consumer

Kontext

Kontextswitch

kooperatives Multitasking

Kritische Region Siehe CR (Critical Region, Critical Section)

kryptographische Hashfunktion

L

Lademodul

Laden bei Bedarf Siehe fetching on demand

Laden im Voraus Siehe look ahead fetching (prefetching)

Ladeprogramm (Loader)

LAN (Local Area Network)

Layers Siehe Schichten

Leitwerk

LFU (Least Frequently Used)

Linker (Linkage Editor)

load time binding

lock

lock(s)

logischer Adressraum

logischer Speicher

Lokalitätsprinzip

Long-Term-Scheduler

Long-Term-Scheduling

look ahead fetching (prefetching)

look ahead-strategy

Loopback-Interface

LRU (Least Recently Used)

M

MAC (Mandatory Access Control)

MAC (Message Authentification Code)

MBR (Master Boot Record)

MD5 (Message Digest Algorithm5)

Mehrbenutzerbetrieb

Mehrprogrammbetrieb

Metadaten

MFT (Master File Table)

Microkernel

Mid-Term-Scheduling

MINIX3

mirroring

missing-address-fault

MMU (Memory Management Unit)

Modularität

Monitore

multi user mode Siehe Mehrbenutzerbetrieb

Multilevel Feedback Queue Scheduling

Multiprogramming (Multitasking)

Grad

Multithreading

mutual exclusion

N

Nachricht

Namensauflösung

Namensraum

Native Threads

Netzwerk-ID

Netzwerk-Klassen

Netzwerkkommunikation

Netzwerk-Treiber

nicht unterbrechbare CPU-Steuerung (non preemptive scheduling)

no preemption

nonce

nslookup

NTFS (New Technology File System)

NUR (Not Used Recently)

O

Offset

Oktett

Operationen auf Files

optimales Page-Replacement

Optimalitätsprinzip nach L.A. Belady

Ordner Siehe folder

P

page

page fault frequency

Page Lists (standby, modified, freed, zero)

Page Reference String

Page Table

Page-Directory

Page-Directory-Index

Page-Table-Index

Paging

Paket

Passwort

Passworthistorie

patches

PBR (Partition Boot Record)

PCB Process Control Block)

PCB (Process Control Block)

PDE (Page-Directory-Entry)

Permissions

persistent

Petrinetz

PFN (Page Frame Number)

physischer Speicher

ping

Platform as a Service (PaaS)

Platzierungsstrategie (placement policy, placement strategy)

polling

Portierbarkeit

Port-Nummer (Port)

präemptiv

Preemption

Preemptive Priority Scheduling

preemptive scheduling

Preemptive Shortest Job First

preemptive Shortest Remaining Time (SRT)

Principals

Priority Boost

Priority-Scheduling

Priority-Scheduling mit Aging

Privacy Siehe Datenschutz

process state diagram Siehe Prozesszustandsdiagramm

Producer-Consumer-Problem

Produzent (Producer)

Programmzähler (Program Counter)

Programmzustand

progress

Protection Siehe Datensicherheit

Protokoll

Prozess

deterministisch

disjunkt

interagierend

leichtgewichtig Siehe Thread

nebenläufig

Operationen auf Prozesse

CreateProcess

Fork

GetState

SetPriority

SetState

TerminateProcess

parallel

sequentiell

verzahnt (interleaved)

Prozessbeschreibungsblock Siehe PCB (Process Control Block)

Prozessliste

Prozessnummer

Prozesswechsel

Prozesszustände

bereit, ready

laufend, aktiv, rechnend, running

wartend, blockiert, waiting, blocked

Prozesszustandsdiagramm

PTBR (Page-Table-Base-Register)

Q

Quantum

Quota Siehe Disk Quota

R

Race Condition

RAID (Redundant Array of Independent Disks)

RAID_0 – Striping

RAID_1 – Spiegelung

RAID_5 – Stripes mit Prüfbits

Rainbow Table

RAM (Random Access Memory)

Random Access

RBAC (Role Based Access Control)

Constrained RBAC

Core RBAC

Core RBAC Referenzmodell

DSD (Dynamic Separation of Duties)

Hierarchical RBAC

PRMS (Permissions)

Rollenvererbung

Separation of Duties

Sitzung

SSD (Static Separation of Duties)

UA (User Assignment)

Reader-Writer-Problem

Read-Only-Bit

Ready Queue

Real-Time Systems Siehe Echtzeitbetriebssysteme

Rechenwerk

Rechtemenge

reentrant

Reference Bit

Referenzmonitor

Referenzzähler

Registersatz

Registry

relative Adressen

relocatable code

Relokationslader (relocation loader)

Relokationsregister

Ressource (resource) Siehe Betriebsmittel

Ressourcen-Zuordnungsgraph

Rollen

ROM (Read Only Memory)

Round Robin

RPC (Remote Procedure Call)

runtime binding

Runtime-Stack

S

Safety

Salzen (Salted Hash)

Satzzeiger

Scheduler

Scheduling

Schichten

Schichtenmodell

Schutz-Bits

Second Chance Siehe NUR (Not Used Recently)

Security

Seite Siehe page

Seitenfehlerrate Siehe page fault frequency

Seitenflattern Siehe Thrashing

Seitenrahmen Siehe frame

Seitentabelle Siehe Page Table

Seitentabellen-Basisregister Siehe PTBR (Page-Table-Base-Register)

Sektoren

Sekundärspeicher

Semaphore

Services Siehe Dienste

SHA-1 (Secure Hash Algorithm)

SHA-2

Shadow-Datei

shareable resources

shared page

shared Variable

Short-Term-Scheduler

Short-Term-Scheduling

Sicherheitspolitik

SID (Security Identifier)

signal(S)

single user mode Siehe Einbenutzerbetrieb

Sitzung (session)

SJF (Shortest Job First)

SMTP (Simple Mail Transfer Protocol)

Soft Real-Time

Soft Real-Time Systems

Software as a Service (SaaS)

software interrupt

Source Address

Speicherfragmentierung

Speicherverwaltung

Spiegeln Siehe mirroring

Spin Locks

Spooler-Systeme

Spooling

Spuren

Stackpointer

Stapelverarbeitung

starting point

Starvation Problem

state Siehe Zustand

Statisches Linken (static linking)

Stellen

Streifen (stripes)

Stubs

Subnetzmaske (Subnet-Mask)

Subnetzwerk-ID

Superblock

Symbian OS

Synchronisation (Synchronisieren)

synchronized

System-Mode Siehe Kernel-Mode

T

TCP (Transmission Control Protocol)

TCP/IP

Teilnehmerbetrieb

Terminal-Emulation

TestAndSet

Thrashing

Thread

Three-Level-Scheduling

time quantum

Timer

Timesharing

TLB (Translation Look-Aside Buffer)

Fehler (miss)

Replacement Strategie

Treffer (hit)

Token

tracert (traceroute)

Transitionen

TRAP

Treiber Siehe device driver

U

UDP (User Datagram Protocol)

unlock

unlock(s)

unteilbar Siehe atomar

unterbrechbare CPU-Steuerung Siehe preemptive scheduling

User Interface Siehe Benutzerschnittstelle

User-Mode

V

Valid-Bit

VCN (Virtuelle Cluster Nummern)

verbindungsorientiert

Verdichtung

Verdichtungsstrategie Siehe compacting strategy

Vererbung

Verklemmung Siehe Deadlock

verschiebbarer Code Siehe relocatable code

Verteiltes Betriebssystem

Verteiltes System

Verweilzeit

VFAT (Virtualized File Allocation Table)

Virtual Machine Monitor (VMM)

Virtualisierung

Paravirtualisierung

Vollständige Virtualisierung

Virtualisierungsschicht

VirtualPC

Virtuelle Maschine

virtueller Adressraum

Virtueller Speicher (virtual memory)

VMware

Von-Neumann-Zyklus

W

wait(S)

Waiting Queue

wakeup

WAN (Wide Area Network)

Warte auf Graph

Warteschlange

Wartezeit

Wechselseitiger Ausschluss Siehe mutual exclusion

Working Set Manager

Working Set Model

Worst Fit

Wörterbuch-Attacke

Z

Zeitquantum Siehe time quantum

Zirkuläres Warten Siehe circular wait

Zugriffskontrollliste Siehe ACL (Access Control List)

Zugriffsmatrix Siehe Access Control Matrix

Zustand

sicher

unsicher

Zustandsdiagramm

Zustandsüberführung

Inhaltsverzeichnis

VORWORT

A BETRIEBSSYSTEME ERSTE GRUNDLAGEN

A.1 Was ist ein Betriebssystem?

A.2 Zweck und Aufgaben eines Betriebssystems

A.2.1 Prozessmanagement

A.2.2 Hauptspeicherverwaltung

A.2.3 Dateiverwaltung

A.2.4 Benutzerschnittstelle

A.2.5 Netzwerkanbindung

A.2.6 Betriebssystemaufbau

A.2.7 Betriebsmittel (Ressourcen)

B HARDWARE MECHANISMEN

B.1 Allgemeines

B.2 Von-Neumann-Zyklus

B.3 Laden des Betriebssystems, Reset-Vektor

B.4 Ein-/Ausgabe

B.5 Polling

B.6 Interrupts

B.6.1 Grundlagen von Interrupts

B.6.2 Von-Neumann-Zyklus im Detail

B.6.4 Software Interrupts (SWI)

B.7 Prozeduren und Prozeduraufrufe

B.7.1 Vergleich Interrupts, Software-Interrupts, Prozeduren

B.7.2 Prozeduren und ihre Parameterversorgung

B.7.2.1 Prozeduraufrufe generell

B.7.2.2 Rückgabe eines Funktionswertes

B.7.2.3 Parameter über Call by Value

B.7.2.4 Parameter über Call by Reference

B.7.2.5 Reale Implementierungen

C BETRIEBSARTEN

C.1 Stapelverarbeitung (Batchprocessing)

C.2 Mehrprogrammbetrieb (Multiprogramming, Multitasking)

C.3 Teilnehmerbetrieb (Timesharing)

C.4 Echtzeitverarbeitung (Real-Time Processing)

C.5 Gridcomputing

D KLASSIFIKATION VON BETRIEBSSYSTEMEN

D.1 Betriebsarten

D.2 CPU-Scheduling

D.3 Technischer Aufbau (Architektur)

D.3.1 Monolithische Betriebssysteme

D.3.2 Geschichtete Betriebssysteme (Layered Operating Systems)

D.3.3 Client Server Modell

D.4 Speicherverwaltung

D.5 Benutzersicht

D.6 Virtuelle Maschinen

D.6.1 Grundprinzip

D.6.2 Systemaufrufe (system calls) über Unterprogramme (Prozeduren)

D.6.3 Systemaufrufe mittels Software-Interrupts (SWI)

D.6.4 Hypervisor

D.6.4.1 Aufgabenstellung und Anwendung

D.6.4.2 Hosted Hypervisor („Typ 2“)

D.6.4.3 Bare Metal Hypervisor („Typ 1“)

D.6.5 Paravirtualisierung

D.6.6 Kernelbasierte Virtuelle Maschinen

E PROZESSE

E.1 Zustände

E.2 Prozessbeschreibungsblock

E.3 Zustandsdiagramm

F CPU-STEUERUNG (CPU-SCHEDULING)

F.1 Einleitung und Ausgangssituation

F.2 Long-Term-Scheduling

F.3 Short-Term-Scheduling

F.3.2 Ziele von Short-Term-Verfahren

F.3.2.1 CPU-Auslastung

F.3.2.2 Durchsatz

F.3.2.3 Verweilzeit

F.3.2.4 Antwortzeit

F.3.2.5 Wartezeit

F.3.3 Nonpreemptive Algorithmen

F.3.3.1 First Come First Served (FCFS)

F.3.3.2 Shortest Job First (SJF)

F.3.3.3 Priority Scheduling (HPF)

F.3.3.4 Priority Scheduling mit Aging

F.3.4 Preemptive Algorithmen

F.3.4.1 Preemptive Priority Scheduling

F.3.4.2 Round Robin (RR)

F.3.4.3 Mehrere Warteschlangen mit Prioritäten

F.3.4.4 Fair Share Scheduling

G PARALLELITÄT

G.1 Prozesse allgemein

G.2 Gründe für parallele Prozesse

G.2.1 Information Sharing

G.2.2 Beschleunigung (Computation Speedup)

G.2.3 Modularität

G.2.4 Zweckmäßigkeit (Annehmlichkeit, Convenience)

G.3 Threads und Context Switching

G.3.1 Threads im Single User Multitasking Betriebssystem SYMBIAN

G.3.2 Threads bei der Java Virtual Machine

G.3.3 Thread Prioritäten in Windows

G.4 Klassische Problemstellungen

G.4.1 Grundgedanke

G.4.2 Producer-Consumer-Problem

G.4.2.1 Problemstellung

G.4.2.2 Busy Waiting

G.4.2.3 Race Condition

G.4.3 Wechselseitiger Ausschluss und Kritische Regionen

G.4.3.1 Anforderungen an eine zulässige Lösung

G.4.3.2 Falsche Lösungsversuche

G.4.3.2.1 Verletzung von Mutual Exclusion

G.4.3.2.2 Reihenfolge wird erzwungen

G.4.3.2.3 Progress Verletzung

G.4.4 Hardwarehilfen

G.4.4.1 Einprozessorsysteme

G.4.4.2 Mehrprozessorsysteme

G.4.5 TestAndSet als Methode einer Java Klasse

G.4.6 Spin Lock

G.4.7 Semaphore

G.4.7.1 Motivation

G.4.7.2 Beschreibung der Semaphor-Operationen

G.4.7.3 Beispiel Code

G.4.7.4 Anwendungen von Semaphoren

G.4.7.4.1 Prozess-Synchronisation

G.4.7.4.3 Producer-Consumer-Problem mit Semaphoren

G.4.8 Kritische Region in Java

G.4.9.1 Allgemeines

G.4.9.2 Monitore in Java

H SYSTEMVERKLEMMUNGEN (DEADLOCKS)

H.1 Betriebsmittelverwaltung

H.2 Synchronisation von Prozessen

H.2.1 Petrinetze

H. Deadlocks

H.3.1 Ausgangssituation

H.3.2 Modellierung mittels Petrinetz

H.3.3 Bewältigung von Deadlocks

H.3.3.1 Allgemeine Konzepte

H.3.3.2 Deadlock Vorbeugung

H.3.3.2.1 Hold and Wait sei nicht erlaubt

H.3.3.2.2 No preemption wird aufgehoben

H.3.3.2.3 Verhindere zirkuläres Warten

H.3.3.3 Deadlock Vermeiden

H.3.3.4 Erkennen und Wiederherstellen

H.3.3.4.1 Erkennen

H.3.3.4.2 Wiederherstellen

I SPEICHERVERWALTUNG

I.1 Allgemeine Problemstellung

I.2 Adressbindung

I.2.1 Absolute Adressen

I.3 (Programm-)Lader

I.3.1 Lader und Adressbindung

I.4 Linker

I.4.1 Aufgabe

I.4.2 Statisches und dynamisches Linken

I.4.2.1 Prinzip

I.4.2.2 Nachteil von statischem Linken

I.4.2.3 Vorteil von dynamischem Linken

I.5 Logische und physische Adressen

I.6 Methoden für die Speicherverwaltung

I.6.1 Kontinuierlicher Speicherbereich

I.6.2 Dynamische Speicherverwaltung

I.6.3 Speicherfragmentierung

I.6.3.1 Interne Fragmentierung

I.6.3.2 Externe Fragmentierung

I.6.3.3 Maßnahmen gegen die externe Fragmentierung

I.6.4 Seitenverwaltung

I.6.4.1 Virtueller Adressraum

I.6.4.2 Zuordnung über Seitentabelle

I.6.5 Eigener Adressraum für jeden Prozess

I.6.6 Translation Look-Aside Buffer

I.6.7 Seitenverwaltung und Schutzmaßnahmen

I.6.7.1 Read-Only-Bit

I.6.7.2 Valid-Bit

I.6.7.3 Gemeinsam benutzte Seiten (Page Sharing)

J VIRTUELLER SPEICHER

J.1 Zielsetzung

J.2 Paging zur Verwaltung von Virtuellem Speicher

J.2.1 Grundlagen

J.2.2 Fallstudie Windows

J.3 Strategien für Paging Systeme

J.3.1 Platzierungsstrategie (placement strategy)

J.3.2 Ladestrategie (fetch policy)

J.3.3 Ersetzungsregel (replacement strategy)

J.3.4 Aufwand bei einem Seitenfehler

J.3.5 Wichtige Ersetzungsstrategien

J.3.5.1 LRU

J.3.5.2 NUR (Clock Algorithm)

J.4 Lokalität und Arbeitsmenge

K FILES, FILESYSTEME

K.1 Aufgaben und Schichtenmodell

K.2 Operationen auf Files

K.3 File Typen – Metadaten

K.3.1 Filetyp auf Basis des Namens

K.3.2 Filetyp auf Basis des erzeugenden Programms

K.4 Verzeichnisse

K.5 File-Allokation auf Massenspeichern

K.5.1 Grundlegendes

K.5.2 Datenstrukturen

K.5.2.1 Fragmentierung

K.5.2.2 Files als verkettete Listen

K.5.2.3 Clusterzuordnungstabelle (FAT)

K.5.2.4 Indexknoten

K.5.2.5 Data-Runs

K.5.2.6 Inodes

K.5.2.7 Verwaltung über eine Tabelle

K.6 Disk Quota

K.7 Mehrere Filesysteme gleichzeitig

K.8 RAID

K.8.1 Stufen von RAID

K.8.2 RAID_0: Striping

K.8.3 RAID_1: Spiegelung

K.8.4 RAID_5: Stripes mit Prüfbits

L SICHERHEIT IN BETRIEBSSYSTEMEN

L.1 Zum Begriff Sicherheit

L.2 Identifizieren und Authentifizieren

L.2.1 Authentizität und Authentifizierung

L.2.2 Identifizieren

L.2.3 Zugangskontrollen

L.2.4 Betriebssysteminterne Identitäten

L.3 Speichern von Passwörtern

L.3.1 Passwortsicherheit

L.3.2 Kryptographische Hashfunktionen

L.3.3 Salzen

L.4 Schutz von Objekten

L.4.1 Aufgabenstellung

L.4.2 Modelle

L.4.3 Implementierung der Zugriffsmatrix

L.4.3.1 Zugriffskontrolllisten (ACL)

L.4.3.2 Fähigkeiten (Capabilities)

L.4.4 Vergabe von Berechtigungen in Windows

L.4.4.1 Beschreibung des Szenarios

L.4.4.2 Sicht von Windows

L.4.4.3 Erzeugen von neuen Objekten

L.4.4.4 Erzeugen von neuen Subjekten

L.4.4.5 Gruppe für viele/mehrere Subjekte

L.4.4.6 Erweiterung auf mehrere Gruppen

L.4.4.7 Überprüfung der Berechtigungen durch das Betriebsystem

L.4.5 Rollenbasierende Zugriffsrechte

L.4.5.1 Basis RBAC (Core RBAC)

L.4.5.2 Hierarchisches RBAC (Hierarchical RBAC)

L.4.5.3 Beschränktes RBAC (Constrained RBAC) – Statische und Dynamische Trennung von Zuständigkeiten

M NETZWERKKOMMUNIKATION

M.1 Netzwerkkommunikation ist Aufgabe des Betriebssystems

M.2 TCP/IP ist die dominante Protokoll-Familie

M.3 Begriff: Protokoll

M.4 Zum Namen TCP/IP

M.4.1 Grundfunktionalität von IP

M.4.2 Grundfunktionalität von TCP

M.5 Schichtenmodell

M.6 IP Version 4 (IPv4)

M.6.1 Adressierung

M.6.1.1 IP-Adressen

M.6.1.2 IP-Adressen und Subnetting

M.6.1.2.1 Struktur eines IP-Netzes

M.6.1.2.2 Netzwerk-Klassen

M.6.1.2.3 Netzwerk-Maske

M.6.1.3 Verfeinerung der Adressierung von Host --> Prozess

M.6.2 IP Client-Konfiguration

M.6.2.1 Wesentliche Konfigurationsinformationen

M.6.2.1.1 IP-Adresse, Subnetzmaske, Default Gateway

M.6.2.1.2 Domain Name System (DNS) Server

M.6.2.2 Typen der Konfiguration eines IP-Hosts

M.6.2.2.1 Manuelle Konfiguration

M.6.2.2.2 Dynamische Konfiguration auf Basis eines DHCP-Servers

M.6.2.2.3 Automatische eigenständige Konfiguration

M.6.3 Testen einer IP-Konfiguration

M.6.3.1 Auslesen der Konfiguration (ifconfig / ipconfig / GUI)

M.6.3.2 Testen der Kommunikation (ping, arp)

M.6.3.3 Testen der Namensauflösung (nslookup)

M.6.3.4 Feststellen einer Route (traceroute)

M.7 IP Version 6 (IPv6)

M.7.1 Adressierung

M.7.1.1 IPv6-Adressen

M.7.1.2 IPv6-Adressen und Subnetting

M.7.1.3 Verfeinerung der Adressierung von IPv6-Host --> Prozess

M.7.2 IPv6 Client-Konfiguration

M.7.2.1 Wesentliche Konfigurationsinformationen

M.7.2.2 Typen der Konfiguration eines IPv6-Hosts

M.7.3 Testen einer IPv6-Konfiguration

M.7.4 Auswahl zwischen IPv4 und IPv6

M.7.5 IPv6-Adressvergabe

ABBILDUNGSVERZEICHNIS

TABELLENVERZEICHNIS

LITERATUR

INDEX