I
Systemsoftware - Grundlagen moderner Betriebssysteme
Jürgen Nehmer hat an der TH Karlsruhe Nachrichtentechnik studiert. Nach einer Industrietätigkeit bei Siemens war er als wiss. Mitarbeiter am Kernforschungszentrum Karlsruhe tätig. 1971 hielt er sich ein Jahr als Gastwissenschaftler am T. J. Watson Research Center der IBM in Yorktown Heights, USA auf. Er promovierte 1973 an der Fakultät für Informatik der Universität Karlsruhe. Seit 1979 ist er Professor für Informatik an der Universität Kaiserslautern; seine Interessen liegen auf den Gebieten Betriebssysteme, Verteilte Systeme, Echtzeitsysteme und Software Engineering.
Peter Sturm hat an der Universität Kaiserslautern Informatik mit Nebenfach Mathematik studiert. Anschließend war er dort am Lehrstuhl für Systemsoftware (Prof. Nehmer) zunächst als wiss. Mitarbeiter und nach der Promotion als Hochschulassistent tätig. Seit 1997 ist er Professor für praktische Informatik an der Universität Trier; seine Arbeitsschwerpunkte liegen im Bereich Systemsoftware und Verteilte Systeme.
dpunkt.lehrbuch Bücher und Teachware für die moderne Informatikausbildung Berater für die dpunkt.lehrbücher sind: Prof. Dr. Gerti Kappel, E-Mail:
[email protected] Prof. Dr. Ralf Steinmetz, E-Mail:
[email protected] Prof. Dr. Martina Zitterbart, E-Mail:
[email protected]
Jürgen Nehmer • Peter Sturm
Systemsoftware Grundlagen moderner Betriebssysteme
2., aktualisierte Auflage
dpunkt.verlag
Prof. Dr, Jürgen Nehmer Universität Kaiserslautern Fachbereich Informatik AG Systemsoftware Postfach 3049 67653 Kaiserslautern E-Mail:
[email protected] Prof. Dr. Peter Sturm Universität Trier Fachbereich IV Informatik Systemsoftware und Verteilte Systeme 54286 Trier E-Mail:
[email protected]
Lektorat: Christa Preisendanz Copy-Editing: Ursula Zimpfer, Herrenberg Satz: FrameMaker-Dateien von den Autoren Herstellung: Josef Hegele Umschlaggestaltung: Helmut Kraus, Düsseldorf Druck: Koninklijke Wöhrmann B.V., Zutphen, Niederlande
Die Deutsche Bibliothek- CIP-Einheitsaufnahme Nehmerjürgen: Systemsoftware Grundlagen moderner Betriebssysteme / Jürgen Nehmer; Peter Sturm. 2., aktualisierte Aufl. - Heidelberg : dpunkt-Verl., 2001 (dpunkt-Lehrbuch) ISBN 3-89864-115-5
Copyright © 2001 dpunkt .verlag GmbH Ringstraße 19 b 69115 Heidelberg
Die vorliegende Publikation ist urheberrechtlich geschützt. Alle Rechte vorbehalten. Die Verwendung der Texte und Abbildungen, auch auszugsweise, ist ohne die schriftliche Zustimmung des Verlags urheberrechtswidrig und daher strafbar. Dies gilt insbesondere für die Vervielfältigung, Übersetzung oder die Verwendung in elektronischen Systemen. Alle Informationen in diesem Buch wurden mit größter Sorgfalt kontrolliert. Weder Autoren noch Verlag können jedoch für Schäden haftbar gemacht werden, die in Zusammenhang mit der Verwendung dieses Buches stehen. In diesem Buch werden eingetragene Warenzeichen, Handelsnamen und Gebrauchsnamen verwendet. Auch wenn diese nicht als solche gekennzeichnet sind, gelten die entsprechenden Schutzbestimmungen.
Vorwort zur zweiten Auflage
In den zurückliegenden drei Jahren hat sich im Bereich Systemsoftware viel bewegt. Der Linux-Kern ist seit kurzem in der Version 2.4 verfügbar, und Microsoft bietet seit geraumer Zeit eine auf Windows NT aufbauende Version Windows 2000 an. Die neuen Versionen dieser Betriebssysteme unterstützen eine Vielzahl an neuen Geräten, sind besser an die besonderen Bedürfnisse großer Server-Systeme anpaßbar und lassen sich leichter in ein vorhandenes Rechnernetz integrieren. Obwohl sich in beiden Systemen deshalb viele Dinge geändert haben, blieben die grundlegende Systemarchitektur und die angebotene Programmierschnittstelle davon in wesentlichen Teilen unberührt. In der vorliegenden zweiten Auflage fand die notwendige Aktualisierung dieses Lehrbuchs statt. Natürlich wurden auch alle bis jetzt bekannt gewordenen Fehler korrigiert und der Text an die momentan gültigen Kenngrößen und Preise im Hardwarebereich angepaßt. Allen interessierten und kritischen Lesern, die uns auf Fehler aufmerksam gemacht haben, sei an dieser Stelle herzlich gedankt.
Kaiserslautern und Trier, im Februar 2001 Jürgen Nehmer, Peter Sturm
Vorwort
Die Betriebsorganisation einzelner Rechner sowie vernetzter Rechnersysteme ist eine Kerndisziplin der Informatik mit einer nahezu 50jährigen Entwicklungsgeschichte. Theorie und Praxis der Betriebssysteme werden in zahlreichen Lehrbüchern behandelt. Die QuasiStandards UNIX (inkl. der Derivate) und Microsoft Windows haben sich durchgesetzt. Vor diesem Hintergrund stellt sich die Frage, welche neuen Entwicklungen heutzutage ein weiteres Lehrbuch auf diesem Gebiet rechtfertigen. Eine Antwort auf diese Frage liegt in den ungebremsten Fortschritten der Rechnertechnologie begründet. Als Bindeglied zwischen Hardware und Anwendungssoftware haben Betriebssysteme schon immer eine Mittlerrolle zwischen den Anwendungsanforderungen und den technologiebedingten Einschränkungen der Rechnerhardware übernommen. Abstraktion war das mächtige Konzept, mit dem dieser Brückenschlag gelang. Dahinter verbirgt sich das Konzept, Anwendungen eine Sicht auf die Rechnerhardware zur Verfügung zu stellen, die von unwesentlichen Details und technologiebedingten Beschränkungen befreit ist. Was adäquate Abstraktionen sind, wird jedoch weitgehend durch ein gegebenes Leistungsspektrum der Hardwaretechnologie bestimmt. So macht es beispielsweise wenig Sinn, die Abstraktion nahezu unbegrenzter Adreßräume für Anwendungen einzuführen, ohne eine effektive Hardwareunterstützung in Form des Konzepts der virtuellen Adressierung zur Verfügung zu haben. Aus diesem Grund haben die Abstraktionen der frühen Betriebssystemgenerationen schrittweise Verallgemeinerungen erfahren, von denen heute jede Anwendung profitiert. Das gewachsene Verständnis über einen strukturierten Aufbau von Betriebssystemen und die wechselseitigen Abhängigkeiten der Mechanismen fand seinen Niederschlag in einer modularen Architektur, die zwischen Kernfunktionen und darauf aufbauenden höheren Funktionen unterscheidet, kernbasierte Systemarchitekturen sind der Schlüssel für die Offenheit und Anpaßbarkeit von Betriebssystemen an unterschiedlichste Anwendungsanforderungen. Bei kernbasierten Betriebssystemarchitekturen wird die Grenze zwischen Anwendungen und klassischen Betriebssystemdiensten fließend. Aus diesem Grund
Vorwort
haben wir uns für den Buchtitel Systemsoftware entschieden: Für den Anwender ist die Bereitstellung einer bestimmten Funktionalität entscheidend, unabhängig davon, ob sie im Kern, in einem Laufzeitpaket des Anwendungsadreßraumes oder durch einen unabhängigen Server erbracht wird. Die damit vollzogene Ablösung der früheren monolithischen Betriebssysteme ist wohl die größte Leistung der Betriebssystemforschung der letzten Jahre und wird künftige Betriebssystemgenerationen nachhaltig beeinflussen. Das vorliegende Lehrbuch stellt die kernbasierte Systemarchitektur in den Mittelpunkt der Betrachtungen. Aufbauend auf der Einführung verbreiteter Laufzeitmodelle werden die heute gängigen Betriebssystemmechanismen vorgestellt und ihre Einbettung in die Gesamtarchitektur demonstriert. Jedes Kapitel behandelt ein Thema aus konzeptueller, allgemeingültiger Sicht und schließt mit einer Demonstration anhand des UNIX-Standards POSIX bzw. der Win32Programmierschnittstelle der Microsoft-Betriebssysteme Windows 95, und Windows NT. Echtzeitaspekte werden immer angesprochen, wenn sie für eine Thematik relevant sind. Wir möchten mit diesem Buch Studenten der Fachrichtungen Informatik und Informationstechnik an Universitäten und Fachhochschulen sowie Softwareentwickler in Wirtschaft und Industrie ansprechen, die sich detaillierte Grundkenntnisse über die Funktionsweise und den Aufbau moderner Systemsoftware erwerben wollen. Ohne die Unterstützung durch einen umfangreichen Personenkreis wäre das Buch sicher nicht zustandegekommen. Namentlich erwähnen möchten wir Frau Hofbauer, die wesentliche Teile der Schreibarbeiten erledigt hat, sowie die Gutachter Frau Prof. M. Zitterbart, TU Braunschweig, und Herrn Prof. T. Braun, Universität Bern, die mit ihren konstruktiven Kommentaren wesentlich zur Verbesserung des Textes beigetragen haben. Der dpunkt.verlag und insbesondere Frau C. Preisendanz haben unendliche Geduld mit uns aufgebracht. Ihnen allen gilt unser herzlicher Dank.
Kaiserslautern und Trier, im Mai 1998 Jürgen Nehmer, Peter Sturm
Inhaltsverzeichnis
1
Einleitung
1
2 2.1 2.2 2.3 2.4 2.5
Hardware-Grundlagen Der Prozessor Der Speicher Ein- und Ausgabegeräte Nebenläufigkeit Eine abstrakte Rechnerarchitektur
5 6 10 14 19 22
3 3.1 3.2 3.3 3.4
Laufzeitunterstützung aus Anwendersicht Unverzichtbare Dienste Elementare Laufzeitmodelle Erweiterung der elementaren Laufzeitmodelle Grobarchitektur von Laufzeitsystemen
27 30 34 37 39
4 4.1 4.2 4.3 4.4 4.5 4.6 4.7
Adreßräume Organisation von Adreßräumen aus Anwendungssicht . . . Physischer Adreßraum Segmentbasierter virtueller Adreßraum Seitenbasierter virtueller Adreßraum Dynamische Seitenersetzung Swapping ganzer Adreßräume Implementierungsaspekte
41 43 53 59 65 76 88 88
5 5.1 5.2 5.3 5.4 5.5 5.6 5.7
Threads Anforderungen Zustandsmodelle Monoprozessor-Scheduling Echtzeit-Scheduling Multiprozessor-Scheduling Thread-Unterstützung durch APIs Implementierungsaspekte
97 100 108 112 122 131 136 145
Inhaltsverzeichnis
6 6.1
6.5
Speicherbasierte Prozeßinteraktion Mechanismen auf der Basis atomarer Speicheroperationen Hardwaregestützte Mechanismen Betriebssystemgestützter Mechanismus: Semaphore . . . . 6.3.1 Das Konzept 6.3.2 Beispiele mit Semaphoren 6.3.3 Implementierungsaspekte 6.3.4 Erweiterungen für die Echtzeitverarbeitung Sprachgestützter Mechanismus: Monitore 6.4.1 Das Konzept 6.4.2 Beispiele mit Monitoren 6.4.3 Implementierungsaspekte 6.4.4 Erweiterungen für Echtzeitverarbeitung Realisierungsbeispiele
7 7.1 7.2 7.3 7.4 7.5 7.6 7.7
Nachrichtenbasierte Prozeßinteraktion 199 Elementare Nachrichtenkommunikationsmodelle 201 Erweiterungen elementarer Kommunikationsmodelle . . 213 Remote Procedure Call (RPC) 218 Signale 222 Echtzeitaspekte 224 Implementierungsaspekte 224 Nachrichtenkommunikation im POSIX-Standard 226
8 8.1 8.2 8.3 8.4
Synchronisationsfehler Beispiele zeitabhängiger Fehler Formale Modelle Erkennungs- und Vermeidungsalgorithmen Realisierungsbeispiele
235 236 240 246 251
9 9.1 9.2 9.3 9.4 9.5
Dateisysteme Anforderungen Dateien Verzeichnisse Schichtenmodell Realisierungsaspekte
253 255 259 267 274 278
10 10.1 10.2 10.3
Ein- und Ausgabe Konzepte Einbettung der E/A in das Dateisystem Dedizierte Geräte-APIs
283 284 289 290
6.2 6.3
6.4
153 158 161 163 163 165 170 172 173 174 178 182 184 185
Inhaltsverzeichnis
11 11.1 11.2
Schutz Die Schutzmatrix Schutz in UNIX
293 293 298
12 12.1 12.2 12.3 12.4
Zugang zur Systemsoftware Start neuer Prozesse Prozeßverwaltung Zugang zum Dateisystem Batch- und Skript-Dateien
301 304 309 312 313
13 13.1 13.2 13.3 13.4 13.5
Implementierungsaspekte für Systemsoftware Speichereinbettung der Kerne Serielle versus nebenläufige Kerne Kerne ohne E/A-Unterstützung Nichtblockierende Kerne Minimalkerne
315 320 321 324 325 327
Glossar
329
Abkürzungen
343
Literaturhinweise
345
Index
353
1
Einleitung
Mit dem vorliegenden Lehrbuch wird eine klassische Disziplin der Informatik - die Betriebssysteme - auf eine neuartige Weise vermittelt, die der Entwicklung dieses Gebietes in den zurückliegenden 10 Jahren Rechnung trägt. Wir haben deshalb im Titel bewußt den Begriff »Systemsoftware« gewählt, um eine entscheidende Veränderung in der Sichtweise auf Betriebssysteme zum Ausdruck zu bringen: die Wandlung vom monolithischen System hin zu einem stark gegliederten System, in dem die Grenzen zur Anwendung fließend sind. Genau genommen läßt sich in einem nach modernen Architekturprinzipien strukturierten Softwaresystem gar nicht mehr klar definieren, welche Komponenten zum Betriebssystem oder zur Anwendung gehören. Mittels Systemsoftware wird Anwendungen der komfortable Zugang zu den Hardwareressourcen eines Rechnersystems eröffnet. Sie erfüllt damit eine wichtige Brückenfunktion zwischen Anwendungen und der Rechnerhardware und sorgt bei einem Mehrbenutzer-/Mehrprogrammbetrieb durch entsprechende Koordinierung dafür, daß sich unabhängige Anwendungsprogramme beim Zugriff auf die Ressourcen des Rechnersystems nicht in die Quere kommen. Was im einzelnen unter einem komfortablen Zugang zur Hardware verstanden wird, ist allerdings sehr interpretationsbedürftig. Eine Anwendung, die beispielsweise Daten persistent auf einem externen Medium speichern möchte, kann dies auf unterschiedliche Weise tun:
• • • •
durch direktes Abspeichern von Blöcken auf einer Platte, durch Benutzung eines Dateisystems, das seinerseits eine Platte benutzt, durch Verwendung eines Datenbanksystems, das auf einer Dateisystem-Schnittstelle aufsetzt, durch Verwendung persistenter Objekte in einer objektorientierten Programmierumgebung, die eine Datenbank als Unterstützung voraussetzt.
Bei jeder dieser Möglichkeiten wird letztlich ein Gerät benutzt. Welche Abstraktionsebene als geeignete Basis für Anwendungen anzusehen ist, läßt sich jedoch nur anwendungsabhängig beantworten. In der
Aufgaben der Systemsoftware
1
Laufzeitplattform
Systemsoftware
Einleitung
Zeit der sogenannten monolithischen Betriebssysteme mit ihrem abgeschlossenen Funktionsvorrat gab es eine klare Aufteilung: hier die Anwendungssoftware, dort das Betriebssystem. Diese Situation hat sich drastisch geändert. Anwendungen wird in ihrem Adreßraum ein Laufzeitpaket zur Verfügung gestellt. Die Menge der darin enthaltenen Funktionen wird oft auch als Laufzeitplattform oder »Application Programming Interface« (API) bezeichnet und eröffnet u.a. den Zugang zu klassischen Betriebssystemdiensten. Es ist jedoch nicht sinnvoll, daß ein Laufzeitpaket alle ansprechbaren Funktionen selbst realisiert. Vielmehr wird die Erbringung der hinter einer Laufzeitroutine stehenden Funktionalität oft an einen Systemkern oder spezielle Server delegiert. Im letzteren Fall spielt der Kern lediglich eine Vermittlerrolle: Er überträgt die Aufträge und Ergebnisse zwischen Auftraggebern (Clients) und den Diensteerbringern (Servern) und nimmt damit eine wichtige Infrastrukturleistung wahr. Die Betriebssysteme alter Prägung sind ersetzt worden durch stark gegliederte Systeme, bei denen die Grenzen zur Anwendung verschwimmen. So können bestimmte Laufzeitroutinen oder Server entweder noch sehr beriebssystemnahe Dienste im klassischen Sinne erbringen oder aber bereits sehr stark auf eine bestimmte Anwendungsdomäne zugeschnitten sein. Der Begriff »Betriebssystem« verliert damit allmählich seinen Sinn. Mit dem neutralen Begriff »Systemsoftware« wollen wir zum Ausdruck bringen, daß die Offenheit und Fähigkeit zur anwendungsangepaßten Konfigurierbarkeit hardwarenaher Programme zentrales Anliegen aller Anwendungen ist. Die Softwarearchitektur, die diese Eigenschaften unterstützt, hat deshalb eine größere Bedeutung als der fest umrissene Funktionsvorrat von Betriebssystemen alter Prägung. Es ist das Anliegen des Buches, diese Botschaft zu vermitteln. Die Vorstellung bekannter Betriebssystemmechanismen erfolgt deshalb im Kontext einer offenen Systemarchitektur, die den übergeordneten Rahmen darstellt. Behandelte Systeme Die vorgestellten Konzepte und Techniken werden am Beispiel realer Systemsoftware konkretisiert. Der Zugang zu der Funktionalität wird in diesem Buch primär durch die jeweiligen Programmierschnittstellen erschlossen. Diese Schnittstellen werden am Beispiel der aktuellen Windows-Systeme von Microsoft sowie im Kontext des POSIX-Standards, der Schnittstellendefinitionen für eine Vielzahl an UNIX-basierten Betriebssystemen festlegt, exemplarisch vorgestellt. Dabei ist es kein Anliegen dieses Buches, als Programmierhilfe für die Entwicklung von Anwendungen auf diesen Systemen zu dienen. Aus der Vielfalt an
1
Einleitung
technischer Literatur sei dazu für das Windows-API auf [Richter 1999] und für POSIX- und UNIX-Programmierung auf [Gallmeister 1995] und [Stevens 1992] verwiesen. Da sich moderne Systeme in ihrem Funktionsangebot und in ihrem grundsätzlichen Aufbau zunehmend ähnlicher werden, wurde zur Vermeidung von Wiederholungen auf in sich abgeschlossene Einführungen in die jeweilige Systemsoftware verzichtet. Vielmehr wird das jeweilige Funktionsangebot der verschiedenen Systeme am Ende des entsprechenden Kapitels vorgestellt. Zusatzinformationen Freundlicherweise hat sich der dpunkt-Verlag bereit erklärt, eine WWW-Seite zu diesem Buch aufzubauen. Über die URL http://www.dpunkt.de/syssoft
können Interessenten auf Zusatzinformationen, interessante Links und Korrekturen zu diesem Buch zugreifen. Zu einem späteren Zeitpunkt sollen auch Übungsaufgaben zu den einzelnen Kapiteln das Angebot ergänzen.
2
Hardware-Grundlagen
Eine wesentliche Aufgabe der Systemsoftware besteht in der Bereitstellung höherer Abstraktionen für den bequemen Zugriff von Anwendungen und Anwendern auf die Hardware eines Rechners. Dazu sind Grundkentnisse über den Aufbau und die wesentlichen Eigenschaften heutiger Rechnersysteme unerläßlich. Eine erschöpfende Behandlung dieses Stoffgebiets ist an dieser Stelle nicht möglich. Der interessierte Leser sei zum Beispiel auf [Hennessy und Patterson 1990], [Stallings 1993] oder [Tanenbaum 1990] verwiesen. Abb. 2-1 Von-Neumann-Rechner
Bis auf wenige, im Forschungsbereich angesiedelte Ausnahmen basieren alle heutigen Computersysteme auf einem 1944 von Eckert, Mauchly und von Neumann entwickelten Architekturprinzip. Danach besteht ein Computersystem in erster Näherung aus einem Prozessor, einem Speieber und ein oder mehreren Ein- und Ausgabegeräten (siehe Abbildung 2-1). Die einzelnen Komponenten des Systems sind über insgesamt drei Busse miteinander verknüpft. Der Adreßbus adressiert einzelne Datenzellen eines Speicherbausteins oder eines E/A-Gerätes. Er besitzt eine Breite von 16, 32 oder 64 Bit (= Anzahl Signalleitungen). Die Breite dieses Busses legt die maximale Anzahl an adressierbaren Zellen fest, z.B. können über einen 32 Bit breiten Adreßbus maximal 232 Zellen angesprochen werden (bei einer Zellengröße von einem Byte wären das 4 GByte). Über den Steuerbus werden die einzelnen Lese- und Schreibzyklen zwischen dem Prozessor und den anderen
Adreßbus
Steuerbus
2
Datenbus
Hardware-Grundlagen
Komponenten koordiniert. Er umfaßt Signalleitungen zur Unterscheidung von Lese- und Schreibaufträgen, zur Festlegung der Zyklusart, zur Synchronisation paralleler Abläufe zwischen den einzelnen Komponenten und zur Signalisierung externer Ereignisse an den Prozessor. Der Datenbus dient der eigentlichen Informationsübertragung. Die Breite dieses Busses bestimmt, wie viele Einzelzyklen für die Übermittlung eines Datums bestimmter Länge notwendig sind. Gängige Datenbusbreiten sind 8, 16 und 32 Bit.
2.1
Registerzugriffszeiten liegen bei modernen Computern im Bereich von 1-3 ns
Der Prozessor
Die wesentliche Aufgabe des Prozessors ist die sequentielle Ausführung einer Instruktions- oder Befehlsfolge (Programm). Alle Instruktionen sind zusammen mit den zu verarbeitenden Daten im Speicher des Rechners abgelegt (von-Neumann-Prinzip). Im Zuge jeder einzelnen Befehlsausführung werden Daten im Prozessor verarbeitet oder zwischen Prozessor, Speicher und Gerät bewegt. In der Summe haben alle diese Aktionen ein von außen beobachtbares und im Idealfall erwünschtes Systemverhalten entsprechend dem Einsatzgebiet des Computers zur Folge. Der Prozessor bildet damit das Herz jedes Rechnersystems. Er umfaßt mindestens einen Registersatz, eine arithmetisch-logische Einheit (ALU) und ein Steuerwerk. Der Registersatz besteht aus einer verhältnismäßig kleinen Menge an sehr schnellen prozessorinternen Speicherzellen. Typischerweise sind zwei dieser Register besonders ausgezeichnet: Der Programmzähler (PC = Program Counter) enthält die Adresse der nächsten auszuführenden Instruktion, das Kellerregister (SP = Stack Pointer) wird zur Umsetzung von Unterprogrammaufrufen eingesetzt. Die ALU erlaubt die Manipulation von Daten. Neben den Grundoperationen für ganze und reelle Zahlen stellt sie verschiedene Logik- und Testfunktionen zur Verfügung. Das Steuerwerk koordiniert alle Einzelaktivitäten der Prozessorhardware bei der Ausführung der einzelnen Instruktionen. Instruktionssatz Die Menge aller vom Steuerwerk verstandenen Befehle definiert den Instruktionssatz eines Prozessors. Instruktionen lassen sich drei Hauptgruppen zuordnen:
• • •
Lade- und Speicheroperationen Arithmetik-, Logik- und Schiebeoperationen Operationen zur Beeinflussung der Ausführungsreihenfolge
2.1
Der Prozessor
Lade- und Speicheroperationen dienen dem Austausch von Daten zwischen Prozessor, Speicher und E/A-Geräten. Dabei sind verschiedene Formen der Kommunikation möglich. Diese sogenannten Adressierungsarten sind prozessorabhängig und können bei ihrer Ausführung zum Teil in eine Vielzahl von Einzelschritten zerfallen. Gängige Adressierungsarten sind:
• •
• • •
Adressierungsarten bestimmen, wie Operanden ermittelt werden
Registeradressierung: Ziel oder Quelle eines Kommunikationszyklus ist ein Prozessorregister. Absolute Adressierung: Direkte Angabe einer Adresse; Ziel oder Quelle ist eine Speicher- oder Gerätezelle. Relative Adressierung: Ziel oder Quelle werden relativ zu einer in einem Register gehaltenen Basisadresse durch ein Offset angesprochen; die tatsächliche Adresse ergibt sich aus der Addition des Registerinhalts und des angegebenen Offsets. PC-relative Adressierung und Kelleradressierung: Spezialfälle der relativen Adressierung mit den Registern PC oder SP als Bezugsbasis. Indirekte Adressierung: Ziel- oder Quelladresse ist der Inhalt eines Registers oder einer Speicherzelle. Indizierte Adressierung: Durch ein sogenanntes Indexregister wird ein Datum aus einer Menge gleich großer Elemente adressiert.
Durch die Ausführung von Befehlen aus der Gruppe der Arithmetik-, Logik- und Schiebeoperationen werden Register- und Speicherinhalte manipuliert. Dabei werden bestimmte Eigenschaften des Ergebnisses der zuletzt ausgeführten Operation, z.B. Ergebnis ist 0, durch das Setzen oder Zurücksetzen einzelner Bits (Flags) in einem ausgezeichneten Prozessorzustandsregister (PSW = Processor Status Word) signalisiert. Alle gängigen Prozessoren verfolgen ein streng sequentielles Verarbeitungsmodell. Dabei wird nach Beendigung der aktuellen Operation implizit die entsprechend der Adresse nachfolgende Instruktion geladen und ausgeführt. Diese sequentielle Befehlsverarbeitung kann durch eine Reihe von Sprungbefehlen beeinflußt werden. Man unterscheidet zwischen einem unbedingten Sprung und einem bedingten Sprung. Unbedingte Sprünge heben die sequentielle Verarbeitungsreihenfolge immer auf und führen die Programmausführung ab einer angegebenen Adresse fort. Bedingte Sprünge machen die Verarbeitungsreihenfolge von dem Wert einzelner Flags im Prozessorzustandswort abhängig, z.B. wird nur bei einem negativen Additionsergebnis an eine andere Stelle im Programm gesprungen; ist die Sprungbedingung nicht erfüllt, wird gemäß der sequentiellen Verarbeitungsreihenfolge automatisch der nächste Befehl ausgeführt. Darüber hinaus unterscheidet man zwischen absoluten und relativen Sprüngen. In Anleh-
Sprungbefehle ändern die sequentielle Programmausführung
2
Unterprogramme
Hardware-Grundlagen
nung an die entsprechenden Adressierungsarten wird das Ziel bei einem absoluten Sprung direkt angegeben, während bei relativen Sprüngen ein angegebenes Offset zum aktuellen Wert des Programmzählers addiert wird. Programme, die ausschließlich Relativsprünge verwenden, haben den Vorteil, daß sie an jeder beliebigen Speicheradresse ausgeführt werden können. Eine Variante des Sprungbefehls wird zur Realisierung von Unterprogrammen eingesetzt. Bei der Ausführung eines UnterprogrammSprungs (z.B. J S R = Jump Subroutine) wird zusätzlich zuerst die nachfolgende Programmadresse (Rücksprungadresse) auf dem Keller gerettet. Danach beginnt die Ausführung der angegebenen ersten Instruktion des Unterprogramms. Durch die Sicherung der Rücksprungadressen in einem Keller sind geschachtelte und rekursive Prozeduraufrufe möglich. Dabei wird der Keller meist auch zur Übergabe der Parameter und zur Mehrfachinstanziierung von prozedurlokalen Variablen eingesetzt. Ein Unterprogramm wird mit der Ausführung eines Return-Befehls beendet. Dieser Befehl interpretiert das oberste Kellerelement als die beim Unterprogrammaufruf gespeicherte Rücksprungadresse und verzweigt die Programmkontrolle an diese Stelle. Synchrone und asynchrone Unterbrechungen
Synchrone Unterbrechungen
Die normale Programmausführung eines Prozessors kann auch durch mehrere Arten von Unterbrechungen verändert werden (siehe Abbildung 2-2). Man unterscheidet zwischen synchronen und asynchronen Unterbrechungen. Synchrone Unterbrechungen sind eine unmittelbare Folge der aktuellen Befehlsausführung, d.h., sie werden synchron mit dem aktuellen Befehl ausgelöst. Sie können explizit durch den Aufruf eines entsprechenden Befehls (z.B. eines Trap-Befehls bei 680xO-Prozessoren) oder implizit im Fehlerfall zum Beispiel bei einer Division durch 0 oder einem Zugriff auf eine nicht existente Speicherzelle eintreten. Bei der letztgenannten Gruppe spricht man auch von sogenannten Ausnahmen (Exceptions).
Abb. 2-2 Unterbrechungsformen
Interrupts sind externe und damit asynchrone Unterbrechungen
Asynchrone Unterbrechungen (Interrupts) sind Ereignisse im Computersystem, die über besondere Steuerbusleitungen an den Prozessor weitergegeben werden. Asynchron bedeutet in diesem Kontext, daß
2.1
Der Prozessor
der eintreffende Interrupt in keiner kausalen Beziehung zum aktuell ausgeführten Befehl steht. Typischerweise informieren Ein- und Ausgabegeräte den Prozessor durch eine Unterbrechung über relevante Ereignisse, z.B. Drücken einer Taste, Ankunft eines Nachrichtenpakets etc. Die meisten Prozessoren stellen mehrere getrennte und in ihrer Wichtigkeit gestaffelte Interruptleitungen zur Verfügung. Durch entsprechende Befehle können Interrupts maskiert werden, d.h., eine Reaktion des Prozessors auf einen eingetroffenen Interrupt kann für einen bestimmten Zeitraum unterbunden werden. Abb. 2-3 Reaktion auf eine synchrone oder asynchrone Unterbrechung
Synchrone und asynchrone Unterbrechungen haben hardwaremäßig die Speicherung des aktuellen Prozessorzustandes zur Folge und lösen im Anschluß daran einen indirekten Sprung über eine im Speicher befindliche Sprungtabelle aus (siehe Abbildung 2-3). Dabei ordnet der Prozessor jeder synchronen und asynchronen Unterbrechung einen festen Index in dieser Sprungtabelle zu. An dieser Stelle steht die Anfangsadresse einer Unterbrechungsroutine, die entsprechende Folgemaßnahmen, z.B. das Anstoßen eines weiteren Leseauftrags an die Platte, einleitet. Die unterbrochene Programmausführung kann durch die Wiederherstellung des gespeicherten Prozessorzustands zu einem beliebigen, späteren Zeitpunkt fortgeführt werden.
Unterbrechungsroutinen werden über eine Sprungtabelle ausgewählt
Ausführungsmodi
Alle modernen Prozessoren unterstützen mehrere Modi der Programmausführung mit abgestuften Privilegien. Die häufigste Form ist die Unterscheidung zwischen einem privilegierten Modus und einem Normalmodus. Intel-Prozessoren ab 80386, Pentium-Prozessoren und z.B. AMD-Prozessoren bieten dagegen z.B. 4 abgestufte Schutzringe
Prozessoren unterscheiden einen privilegierten und normalen Ausführungsmodus
2 Hardware-Grundlagen
Unterschiede zwischen den Ausführungsmodi
0 bis 3 an, dabei entspricht Ring 0 dem privilegierten Modus und Ring 3 dem Normalmodus. Mindestens zwei Modi sind für die Umsetzung von Schutzkonzepten unabdingbar. Der Modus der aktuellen Programmausführung hat u. a. Auswirkungen auf die Ausführbarkeit einzelner Instruktionen. So sind spezielle E/A-Befehle (z.B. die In- und Out-Befehle der 80x86-Familie) nur in einem hinreichend privilegierten Modus oder Ring ausführbar; ihre Ausführung im Normalmodus führt zu einer synchronen Unterbrechung »Privilegsverletzung«. Auch das Maskieren von Interrupts ist nur in einem privilegierten Modus erlaubt. Darüber hinaus sind bestimmte Register des Prozessors ausschließlich im privilegierten Modus zugreifbar und/oder veränderbar; andere Register wie zum Beispiel das Kellerregister und das Prozessorstatuswort sind häufig für jeden Modus getrennt vorhanden. Das Auftreten einer synchronen oder asynchronen Unterbrechung ist immer mit der zwangsweisen Umschaltung in einen privilegierten Modus gekoppelt. Ein Wechsel vom privilegierten Modus in den Normalmodus kann durch besondere Befehle oder durch das Setzen bestimmter Flags im Prozessorstatuswort erreicht werden. Der gezielte Wechsel vom Normalmodus in einen privilegierten Modus kann ausschließlich durch die Ausführung eines synchronen Unterbrechungsbefehls (Trap) erzwungen werden.
2.2
4 GByte Speicher bei 32-Bit-Adressen kosteten Anfang 2001 je nach Qualität zwischen 4000 und 10000 DM
Der Speicher
Die von dem Prozessor ausgeführten Programme und die zugehörigen Daten befinden sich im Speicher des Computers. Aus Sicht des Prozessors besteht der Speicher aus einer hardwareabhängigen Anzahl an Speicherzellen, die durch entsprechende Belegungen des Adreßbusses einzeln angesprochen werden können. Der physische Adreßraum eines Computers wird durch die Menge an gültigen, d.h. ansprechbaren Adressen des Speichers definiert. Er ist in seiner maximalen Größe durch die Breite des Adreßbusses begrenzt; der tatsächliche Speicherausbau eines Rechners liegt jedoch meist darunter. Aufgrund von Beschränkungen bei der Konfigurier- und Erweiterbarkeit der Rechnerhardware ist der Adreßbus außerdem nur in Ausnahmefällen zusammenhängend, meist werden Intervalle vorhandenen Speichers von unterschiedlich langen Lücken unterbrochen. Zugriffe auf solche Lücken im physischen Adreßraum werden von der Hardware erkannt und lösen eine synchrone Ausnahmebehandlung aus. Der Speicher besteht zum größten Teil aus Bausteinen, auf die sowohl lesend als auch schreibend zugegriffen werden kann (RAM-Bausteine = Random-Access-Memory). Diese Bausteine verlieren ihren Speicherinhalt beim Ausschalten des Rechners. Ein kleiner Teil des
2.2
Der Speicher
physischen Adreßraums besteht aus Nurlesespeichern (ROM = ReadOnly-Memory), die ihren Speicherinhalt auch bei fehlender Stromversorgung beibehalten. In diesem Speicher befindet sich ein spezielles Lade- oder Boot-Programm (z.B. das sogenannte BIOS eines PC). Durch eine geeignete Plazierung der ROM-Bausteine im physischen Adreßraum des Prozessors wird dieses Programm, dessen Hauptaufgabe das Starten des eigentlichen Systemsoftware ist, automatisch beim Einschalten oder nach einem Reset ausgeführt. Die Geschwindigkeit beim lesenden und schreibenden Zugriff auf den Speicher ist für die Gesamtleistung des Computers von zentraler Bedeutung. Im PC- und Workstation-Bereich sind gegenwärtig Zugriff szeiten von 14-20 ns Stand der Technik. Bei teureren Hochleistungssystemen mit entsprechend leistungsstarken Prozessoren kann diese Zugriffszeit durch sehr aufwendige Techniken wie z.B. durch eine mehrfache Speicherverschränkung weiter reduziert werden. In allen Fällen hinkt der Wert jedoch um ca. eine Größenordnung hinter der potentiellen Zugriffsleistung des Prozessors hinterher, z.B. kann ein mit 1 GHz getakteter Prozessor im optimalen Fall jeden 2. Zyklus auf eine Speicherzelle zugreifen. Ohne zusätzliche Wartezyklen erfordert das einen Speicher mit einer Zugriffszeit von maximal 2 ns. Obwohl synchrone DRAM-Bausteine (SDRAM) und Speicher mit doppelter Datenrate (DDR-RAM) schrittweise bessere Zugriffszeiten erreichen, bleibt die Kluft in der Bandbreite zwischen Prozessor und Speicher mit der schnell ansteigenden Taktfrequenz (z.B. Prozessoren mit 1,5 GHz und mehr) weiter bestehen.
Diskrepanz zwischen Prozessor- und Speichergeschwindigkeit
Abb. 2-4 Funktionsweise eines Cache-Speichers
Caches
Damit die Gesamtleistung des Systems nicht durch die beschränkte Zugriffsgeschwindigkeit zum Hauptspeicher vermindert wird, besitzen praktisch alle modernen Rechner ein oder mehrere Zwischenspeicher (sogenannte Cache-Speicher). Ein Cache kann den Inhalt einzel-
Caches verbessern die Speicherzugriffszeit
2
Cache Hit
Cache Miss
Deferred Write Cache
Write Through Cache
Caches erreichen aufgrund der Referenzlokalität Trefferraten über 90 %
Hardware-Grundlagen
ner Zellen des Hauptspeichers zusammen mit deren Adresse Zwischenspeichern. Bei jedem Zugriff überprüft der Cache halb- oder vollassoziativ, ob die gewünschte Adresse aktuell zwischengespeichert ist. Im Fall eines Treffers (Cache Hit) kann der Inhalt ohne Verzögerung an den Prozessor weitergeleitet werden. So wird zum Beispiel in Abbildung 2-4 beim Anlegen der Adresse 2033 der entsprechende Eintrag 82 im Cache gefunden und zurückgegeben. Wurde die Adresse nicht gefunden (Cache Miss), so muß das Datum vom langsameren Speicher nachgeladen und an den Prozessor übergeben werden. Dabei wird nach einer bestimmten Regel eine Zelle des Caches mit dem gerade geladenen Wert überschrieben, z.B. wird die älteste gespeicherte Assoziation ausgewählt und dabei gelöscht. Verändert der Prozessor den Inhalt einer Speicherzelle, kann zur Leistungssteigerung nur der Cache-Inhalt aktualisiert werden. In diesem Fall wird der Hauptspeicher zu einem späteren Zeitpunkt angeglichen (Deferred Write). Dabei entsteht eine temporäre Inkonsistenz zwischen dem im Cache befindlichen Wert und dem Inhalt der entsprechenden Zelle im Hauptspeicher. Durch das gleichzeitige Zurückschreiben des Werts in den Hauptspeicher (Write Through Cache) kann dieses Risiko vermieden werden. Nachteilig an diesem Verfahren ist, daß schreibende Zugriffe immer die volle Hauptspeicherzugriffszeit benötigen. Der Aufwand, einen zusätzlichen Zwischenspeicher zu integrieren, lohnt sich natürlich nur, wenn ein hinreichend kleiner aber schneller Cache die mittlere Speicherzugriffszeit des Gesamtsystems deutlich verringert. Der entscheidende Faktor dafür ist die Trefferwahrscheinlichkeit des Caches. Für eine gegebene Trefferwahrscheinlichkeit p ergibt sich eine mittlere Zugriffszeit von:
Dabei wird angenommen, daß im Fall eines Cache Miss die Aktualisierung des Zwischenspeichers parallel zur Weiterleitung der Daten an den Prozessor geschieht. In heutigen Computersystemen kann bereits mit verhältnismäßig kleinen Cache-Speichern eine Trefferwahrscheinlichkeit von ca. 90 % erzielt werden. Dieser überraschend hohe Wert resultiert aus einer stark ausgeprägten Referenzlokalität der meisten Programme, u.a. eine Folge des sequentiellen Verarbeitungsmodells. Referenzlokalität bedeutet, daß für eine aktuell zugegriffene Speicherzelle eine hohe Wahrscheinlichkeit existiert, in naher Zukunft erneut referenziert zu werden. Anschaulich ist diese Situation bei Schleifen im Programmverlauf gegeben, die vollständig im Cache zwischengespeichert werden können. In jedem Schleifendurchlauf wird jede Instruktion erneut ausgeführt. Aber auch im Bereich der Daten ist in begrenzten Zeitintervallen eine starke Beschränkung auf wenige, häufig referenzierte Variablen die Regel. Ein weiterer Grund für diese hohe
2.2
Der Speicher
Trefferrate ist, daß die meisten Caches beim Zugriff auf den Hauptspeicher immer mehrere bzgl. der Adressierung aufeinanderfolgende Zellen (Cache Line) in einem sogenannten Burst schnell hintereinander laden. Neben der eigentlich referenzierten Zelle werden dadurch weitere Speicherinhalte vorab geladen (Prefetch), die aufgrund des sequentiellen Verarbeitungsmodells mit hoher Wahrscheinlichkeit in naher Zukunft referenziert werden. Die Trefferwahrscheinlichkeit hängt auch von dem jeweiligen Anpassungsgrad des Caches an die aktuelle Programmausführung ab. Wird zum Beispiel ein neues Programm gestartet, so entsprechen die Cache-Inhalte zu Beginn nicht den Speicherzellen, die durch dieses Programm häufig referenziert werden. Man spricht in diesem Fall von einem kalten Cache, eine geringe Trefferrate ist die Folge. Im Verlauf der weiteren Programmausführung paßt sich der Cache zunehmend an die Referenzlokalität an, der Cache wird warm. Ein optimal eingestellter Cache ermöglicht eine hohe Trefferrate und wird auch als heißer Cache bezeichnet.
Cache Line
Höhe der Trefferrate = Cache-Temperatur
Abb. 2-5 Speicherhierarchie
Speicherhierarchie Häufig werden mehrere Caches kaskadiert, um zwischen Prozessor und Speicher eine maximale Transferleistung zu erreichen. Aus Gründen der Wirtschaftlichkeit wird dabei versucht, die mit der Kaskadierung zunehmend schnelleren und entsprechend teureren Caches bezüglich der Größe und der angestrebten Leistungssteigerung optimal zu dimensionieren. Auf modernen Prozessoren ist daher bereits auf dem Chip meist ein allererster Cache (Level-1-Cache) mit 64 bis 512 KByte Größe integriert, der häufig mit der vollen Prozessortaktgeschwindigkeit angesprochen werden kann. Um das unterschiedliche Lokalitätsverhalten geeignet zu unterstützen, wird dieser Zwischenspeicher häufig in einen Instruktions- und einen Daten-Cache unterteilt. Wegen der kurzen Signalwege im Chip kann die Leistung dieses Caches durch externe Zwischenspeicher nicht erreicht werden. Caches der zweiten
2
Typische Größe eines Level-2-Cache
Hardware-Grundlagen
Stufe (Level-2-Cache) befinden sich zwischen Prozessor und Hauptspeicher. Ein L2-Cache mit 512 KByte für einen Hauptspeicher bis zu einer Größe von 64 MByte ist z.B. im PC-Bereich üblich. Die Schichtung der unterschiedlichen Speichertypen in einem Computer ist in Abbildung 2-5 zusammen mit den typischen Größen und Zugriffszeiten dargestellt. Diese auch als Speieberpyramide bezeichnete Architektur ist in praktisch jedem modernen Computersystem anzutreffen. Auf die Arbeitsweise der L1- und L2-Caches hat die Systemsoftware eines Rechners in der Regel keinen unmittelbaren Einfluß, abgesehen von der Möglichkeit, den jeweiligen Cache vollständig abzuschalten oder ggf. zu löschen. Indirekt haben diese Caches jedoch entscheidenden Einfluß auf das Betriebssystem, das aus Leistungsgesichtspunkten versucht, kalte Caches zu vermeiden.
2.3
Bediengeräte
Externe Speicher
Netzadapter
Ein- und Ausgabegeräte
Ein- und Ausgabegeräte erweitern die Fähigkeiten eines Computers und erlauben die Kommunikation mit seiner Umgebung. Entsprechend den unterschiedlichen Einsatzgebieten heutiger Computer ist auch das Angebot an E/A-Geräten sehr vielfältig. Eine wichtige Gruppe stellen die Bediengeräte dar, die eine Interaktion zwischen Benutzer und Computer ermöglichen. Zu den typischen Eingabegeräten dieser Gruppe zählen Tastatur, Maus, Joystick, Graphiktablett und Mikrophon. Ausgabegeräte sind im wesentlichen Bildschirme, Drukker, Plotter und angeschlossene Lautsprecher. Eine zweite unverzichtbare Gerätegruppe bilden externe Speicher. Bekannte Vertreter sind Diskettenlaufwerke, Platten, CD- bzw. DVDLaufwerke und CD-Recorder sowie unterschiedlichste Bandlaufwerke wie z.B. DAT- und QlC-Streamer. Je nach Einsatzgebiet dienen sie der langfristigen Datenhaltung, der Sicherung und Archivierung oder sie ermöglichen den lesenden Zugriff auf zusätzliche Daten und Programme (CD-ROM). Bei all diesen Geräten werden Daten durch magnetische und/oder optische Verfahren dauerhaft gespeichert. Netzadapter erlauben den Anschluß von Rechnern an ein Kommunikationsnetzwerk. Gängige Adapter sind Ethernetanschlüsse, die eine preisgünstige Vernetzung mit einer Bandbreite von 10 oder 100 MBit/sec ermöglichen, Modems zur Vernetzung von jeweils zwei Rechnern über analoge Telefonverbindungen und zunehmend auch ISDN-Anschlüsse mit Übertragungsraten bis zu 64 KBit/sec. Im Hochgeschwindigkeitsbereich werden kreuzverschaltete Ethernetsysteme (sogenannte Ethernet-Switches), FDDI-Ringe und ATM-Netzwerke über entsprechende Adapter ebenfalls zugänglich. Zu den Netzadaptern zählt man auch Systeme, die eine drahtlose Kommunikation über digitale Funksysteme (z.B. GSM) ermöglichen.
2.3
Ein- und Ausgabegeräte
Diese Aufzählung ist zwangsläufig nicht vollständig, es gibt eine ganze Reihe weiterer Spezialgeräte. So werden z.B. im Bereich der eingebetteten Systeme, die unterschiedlichste technische Prozesse kontrollieren, parallele Ein- und Ausgabekanäle sowie D/A- und A/DWandler zur Ansteuerung von Sensoren und Aktoren eingesetzt. E/A-Controller Geräte werden aufgrund ihrer unterschiedlichen technischen Ansteuerung in der Regel nicht direkt mit dem Prozessorbus des Computers verbunden. Vielmehr übernimmt ein E/A-Controller die Vermittlerrolle zwischen dem Computer und dem Gerät (siehe Abbildung 2-6). So wandelt z.B. die Graphikkarte eines PCs den digital gespeicherten Bildschirminhalt in eine sichtbare Darstellung um. Über die Geräteschnittstelle werden in diesem Fall Signale zur Ansteuerung und Synchronisation der Farbkanonen des Monitors übertragen. Die Geräteschnittstellen sind in den meisten Fällen auf eine bestimmte Geräteklasse zugeschnitten. In manchen Fällen sind sie aber zur Ansteuerung von z.T. sehr unterschiedlichen Geräten geeignet. So können z.B. über serielle RS232-Schnittstellen Tastaturen und Mäuse, aber auch Modems angeschlossen werden. Zur Steuerung komplizierter Geräte verfügen E/A-Controller häufig selbst über einen eigenen Prozessor und eigenen Speicher, d.h., sie bilden in diesem Fall eigenständige kleinere Computersysteme.
E/A-Controller vermitteln zwischen Prozessor und Gerät
Abb. 2-6 Anschluß von Gerät/Netz über E/A-Controller
Die Interaktion zwischen Prozessor und E/A-Controller geschieht über den Prozessorbus (siehe auch Abbildung 2-6). Jeder E/A-Controller stellt einen E/A-Adreßbereich mit einer bestimmten Menge an E/A-Registern zur Verfügung. Diese Register werden unterteilt in:
• •
Kommandoregister zur Übermittlung von Befehlen an den E/A-Controller, z.B. zur Festlegung der Bildwiederholrate bei einer Graphikkarte. Statusregister zur Abfrage des Controller- und Gerätezustands, z.B. ob eine Taste der Tastatur gedrückt wurde.
E/A-Register
2
Hardware-Grundlagen
•
Datenregister (bei einem größeren zusammenhängenden Speicherbereich spricht man auch von einem E/A-Puffer) für den eigentlichen Informationsaustausch, z.B. der Bildschirmspeicher einer Graphikkarte, der jedem Bildpunkt eine bestimmte Menge an Bits zuordnet.
E/A-Architekturvarianten Speicherbasierte E/A
Der E/A-Bereich eines Controllers kann vom Prozessor auf zwei Arten angesprochen werden. Bei der speicherbasierten Ein- und Ausgabe (siehe Abbildung 2-7) sind die Register für den Prozessor nicht von normalen Speicherzellen zu unterscheiden; bis auf eine veränderte Semantik entsprechen die E/A-Controller in ihrer Ansteuerung herkömmlichen Speicherbausteinen. Durch normale Lese- und Schreiboperationen und ohne Einschränkungen bei den verwendeten Adressierungsarten kann in diesem Fall auf die E/A-Register zugegriffen werden.
Abb. 2-7 Speicherbasierte Controller-Ansteuerung
Abb. 2-8 Dedizierter E/A-Bus
E/A-Bus
Die zweite Realisierungsvariante verwendet einen dedizierten E/A-Bus zur Ansteuerung der Controller (siehe Abbildung 2-8). Prozessoren, die diese Variante unterstützen, bieten eigene Lese- und Schreiboperationen für die Interaktion mit den Controllern an. Ein Beispiel dafür sind die In- und Out-Operationen der Intel-Prozessorfamilie 80x86. Für die Controller- und Registerauswahl wird bei diesen Prozessoren ebenfalls der Adreßbus eingesetzt. Zusätzliche Signalleitungen des Steuerbusses unterscheiden dabei zwischen einem Speicher- und einem E/A-Zugriff. Es ist offensichtlich, daß bei E/A-Bus-basierten Prozessoren auch speicherbasierte E/A-Strukturen realisiert werden können.
2.3
Ein- und Ausgabegeräte
Prozessoren, die über diese zusätzlichen Signalleitungen nicht verfügen, können E/A-Geräte nur speicherbasiert ansprechen; ein E/A-Bus kann in diesem Fall nicht realisiert werden. Erwähnenswert ist auch, daß meistens nicht alle vom Prozessor unterstützten Adressierungsarten bei E/A-Operationen eingesetzt werden können. Außerdem können sie in der Regel nur im privilegierten Modus ausgeführt werden, ein Nachteil, der auf bestimmte Betriebssystemarchitekturen gravierende Auswirkungen hat. Der Eintritt relevanter Ereignisse kann dem Prozessor über asynchrone Interrupts mitgeteilt werden. Je nach Wichtigkeit der einzelnen Geräte werden dazu die vorhandenen Interruptleitungen entsprechend ihrer Priorität auf die Controller verteilt. Diese Priorisierung ist besonders zur Unterstützung von zeitkritischen und/oder sehr schnellen Geräten wie z.B. Platten notwendig, da die Ausnahmebehandlung eines Gerätes mit höherer Priorität sowohl die normale Programmausführung als auch die Behandlung eines Gerätes mit niedriger Priorität unterbrechen kann. Bei Eingabegeräten ist die Bedeutung eines ausgelösten Interrupts offensichtlich, aber auch bei Ausgabeanweisungen nutzen Controller Interrupts z.B. zur Signalisierung der Auftragsbeendigung.
Nachteile eines E/A-Busses
Interruptprioritäten erlauben eine adäquate Systemreaktion je nach Gerätetyp
Abb. 2-9 E/A-Bus-Controller
E/A-Bus-Controller Eine besondere Form von Controllern stellen sogenannte E/A-BusController dar, die ausgangsseitig einen standardisierten Bus für den Anschluß der eigentlichen Geräte-Controller zur Verfügung stellen (siehe Abbildung 2-9). Diese Unterteilung in Prozessor- und Gerätebus hat eine Reihe von Vorteilen:
•
Es sind nur wenige Komponenten an den schnellen und in der Ausdehnung stark beschränkten Prozessorbus angeschlossen (keine Beeinträchtigung in der Systemleistung durch langsame E/A-Geräte).
2
Hardware-Grundlagen
•
•
Busmaster-Fähigkeit
Der Bus-Controller kann eine Reihe von Grundfunktionen, z.B. die Bearbeitung von Interrupts und DMA-Aufträgen (siehe unten), für alle angeschlossenen Geräte-Controller zentral übernehmen. Bei entsprechend standardisierten Gerätebussen können einzelne E/A-Controller in unterschiedlichste Computersysteme integriert werden.
Die bekanntesten Gerätebus-Systeme sind ISA, EISA und PCI für den PC-Bereich und SCSI, das ursprünglich aus dem UNIX-Bereich kam, aber zunehmend auch als breitbandige Alternative im PC-Bereich eingesetzt wird. Diese Busse unterscheiden sich hinsichtlich der maximalen Übertragungsrate und der Höchstzahl anschließbarer Controller. Ein weiteres wichtiges Kriterium ist, ob einzelne E/A-Controller am Gerätebus zeitweise die Initiative beim Datentransfer übernehmen können (Busmaster-Fähigkeit); in diesem Fall kann z.B. ein Kopiervorgang von einer Platte zu einem CD-Recorder ohne jede Prozessormitwirkung autonom durchgeführt werden. Zeichen- und blockorientierte Geräte
Zeichenorientierte Geräte
Blockorientierte Geräte
DMA
In Abhängigkeit von der kleinsten Übertragungseinheit zwischen Prozessor und Controller wird zwischen zeichen- und blockorientierter Ein- und Ausgabe unterschieden. Zeichenorientierte Geräte sind z.B. Tastatur, Maus und Drucker. Die Ankunft eines Datums bei einem zeichenorientierten Eingabegerät wird durch das Setzen der zugeordneten Interruptleitung signalisiert. Zur Reduzierung der Interruptrate wird zunehmend auch ein Puffer eingesetzt, der mehrere eintreffende Zeichen Zwischenspeichern kann. Als Reaktion auf den Interrupt kopiert der Prozessor das empfangene Zeichen in den Hauptspeicher oder in ein Register. Umgekehrt wird das Datum bei der Ausgabe zuerst in ein E/A-Datenregister des Controllers kopiert. Anschließend wird durch entsprechende Belegungen der E/A-Kommandoregister die Weiterverarbeitung durch den Controller initiiert. Die Gruppe der blockorientierten Geräte umfaßt Platten, Disketten- und CD-Laufwerke sowie die meisten Netzadapter. Hier wird mit jedem Lese- und Schreibauftrag immer ein ganzer Block bestimmter Länge zwischen Speicher und Gerät übertragen. Bei Geräten dieser Gruppe besteht zusätzlich die Möglichkeit, den Prozessor mit der Hilfe von DMA-Techniken (DMA = Direct Memory Access) zu entlasten. Diese Technik wird entweder vom Controller selbst oder von einem eigenständigen DMA-Controller eingesetzt und beschränkt sich auf die Übertragung von Datenblöcken zwischen Hauptspeicher und E/A-Puffer. Zu Beginn eines DMA-Transfers teilt der Prozessor lediglich Quell- und
2.4
Nebenläufigkeit
Zieladresse sowie Länge des zu übertragenden Blocks mit. Der eigentliche Kopiervorgang wird anschließend ohne weitere Prozessorintervention selbständig abgewickelt. Dabei macht man sich zunutze, daß der Prozessor nicht bei jeder Instruktion einen Speicherzyklus durchführen muß (z.B. reine Registeroperationen) und daß aufgrund der Referenzlokalität adressierte Speicherzellen häufig bereits im Cache gefunden werden. Die resultierende Entlastung des Hauptspeichers reicht häufig aus, den Speichertransfer praktisch ohne Verzögerung des Hauptprozessors durchführen zu können.
2.4
Nebenläufigkeit
Nebenläufigkeit, d.h. die Gleichzeitigkeit mehrerer Aktivitäten, findet bereits in unterschiedlichen Ausprägungen auf der Hardwareebene statt. Der maximale Grad an Nebenläufigkeit wird durch die jeweilige Hardwarekonfiguration festgelegt, er ist ein direktes Maß für die potentielle Leistungsfähigkeit eines Systems. Im wesentlichen wird der Grad an Nebenläufigkeit durch das mehrfache Vorhandensein autonomer Ausführungseinheiten bestimmt: • • • •
Mehrere Mehrere Mehrere Mehrere
Einheiten innerhalb eines Prozessors E/A- und DMA-Controller am Prozessorbus E/A-Controller in autonomen E/A-Subsystemen allgemein einsetzbare Prozessoren
Nebenläufigkeit innerhalb eines Prozessors Alle modernen Prozessoren sind mittlerweile in der Lage, mehrere Befehle gleichzeitig auszuführen. Durch eine sogenannte InstruktionsPipeline befinden sich Instruktionen in unterschiedlichen Ausführungszuständen. Man kann mindestens vier Zustände unterscheiden: Instruktion laden, Instruktion auswerten, Operanden laden sowie ein oder mehrere Stufen der Instruktionsausführung. Darüber hinaus können bei entsprechender Prozessorauslegung mehrere arithmetischlogische Instruktionen gleichzeitig ausgeführt werden, da entweder mehrere Recheneinheiten vorhanden sind oder die Recheneinheit selbst über eine eigene Pipeline verfügt. Prozessoren setzen zusätzlich meist ein Instruktionsfenster fester Größe ein, innerhalb dessen sie Befehle im Hinblick auf eine Durchsatzerhöhung in der Ausführungsreihenfolge umordnen können, solange die Semantik der sequentiellen Befehlsabarbeitung nicht verletzt wird. Nebenläufigkeit innerhalb eines Prozessors wird vollständig durch das Steuerwerk koordiniert und bleibt mit der Ausnahme einer erhöhten Komplexität bei der Fehlerund Interruptbehandlung für die Systemsoftware transparent.
Nebenläufigkeit durch Instruktions-Pipelining
Nebenläufigkeit durch mehrere Funktionseinheiten
2
Hardware-Grundlagen
Nebenläufigkeit im E/A-Bereich
Nebenläufigkeit durch autonome Busse
Eine erste sichtbare Stufe der Nebenläufigkeit ist durch das Vorhandensein mehrerer autonomer E/A-Controller am Prozessorbus oder innerhalb von E/A-Bussystemen (die durch einen Bus-Controller vom Hauptbus entkoppelt sind) gegeben. Dadurch können mehrere Geräte selbständig durch die jeweils zugeordneten Controller bedient werden. Prozessor und Bus-Controller senden dazu lediglich kurze Aufträge an die Controller; der Prozessor- oder E/A-Bus selbst kann anschließend für andere Aufgaben benutzt werden. Zusätzlich können durch den Einsatz von DMA-Techniken mehrere Datentransfers zeitlich verschränkt stattfinden. Beispielsweise kann ein Prozessor eine Platte mit dem Laden eines bestimmten Blocks beauftragen. Je nach Plattengeschwindigkeit verstreichen mehrere Millisekunden bis der Block tatsächlich gelesen, anschließend über DMA an den entsprechenden Platz im Hauptspeicher kopiert und über einen Interrupt die Auftragsbeendigung an den Prozessor signalisiert wurde. Der Prozessor selbst und/oder der Bus-Controller können während dieser Zeit andere anstehende Tätigkeiten durchführen. In Master-fähigen E/ABussen (z.B. SCSI) können sogar E/A-Controller untereinander direkt in Kontakt treten, d.h., ein entsprechend leistungsfähiger Controller übernimmt zeitweilig die Buskontrolle und initiiert unabhängig vom Hauptprozessor ein oder mehrere Unteraufträge an andere Geräte. MuItiprozessorsysteme
Nebenläufigkeit durch mehrere Arbeitsprozessoren
Zunehmend an Bedeutung gewinnt auch Nebenläufigkeit in Form von Multiprozessorsystemen, in denen mehrere Prozessoren gleichzeitg Instruktionen ausführen können (siehe Abbildung 2-10). Mehrprozessorsysteme sind im Workstationbereich bereits üblich und im PC-Bereich stark im Kommen; erste Hauptplatinen für den breiten Markt mit Steckplätzen für bis zu 4 Prozessoren sind ebenfalls bereits erhältlich. Durch den Einsatz mehrerer Prozessoren, die alle über ihren Cache an einen zentralen Hauptspeicher angeschlossen sind, kann tatsächlich eine erhebliche Leistungssteigerung gegenüber einem Monoprozessorsystem erzielt werden. Die Begründung dafür ergibt sich aus der bereits angesprochenen Tatsache, daß ein Prozessor die maximale Übertragungskapazität des Speichers nicht voll ausschöpft: eine große Zahl von Speicherzugriffen wird bereits im Cache aufgefunden, ein voller Speicherzyklus wird damit unnötig. Da jeder Prozessor über eigene Caches an den Hauptspeicher angeschlossen ist, können potentiell mehrere Caches den Inhalt einer bestimmten Speicherzelle Zwischenspeichern. Dadurch kann es zu Inkonsistenzen kommen, wenn ein Prozessor den zwischengespeicherten
2.4
Nebenläufigkeit
Wert verändert. Die Änderung wird zwar unmittelbar (Write Through) oder zeitversetzt (Deferred Write) im Hauptspeicher nachgezogen, ohne zusätzliche Hardwarevorkehrungen werden andere Caches jedoch nicht aktualisiert. Dieses als Cache-Kohärenz bezeichnete Problem, die Sicht auf den Hauptspeicher trotz der zwischengeschalteten Caches konsistent zu halten, wird meist durch sogenannte Snoopy Caches gelöst. Diese Spezialform eines Write Through Caches hört den Speicherbus permanent ab und führt jeden erkannten Schreibzugriff auf einem ebenfalls zwischengespeicherten Datum nach. Mit Hilfe von Snoopy Caches kann starke Kohärenz auf Kosten einer maximalen Parallelität erzielt werden, d.h., alle Prozessoren haben tatsächlich die gleiche Sicht auf den Speicher. Es existieren auch Systeme, die einen höheren Grad an Nebenläufigkeit durch eine Abschwächung dieser strengen Kohärenz erreichen. Die Programmierung dieser recht exotischen Systeme ist jedoch gewöhnungsbedürftig, da sich der Speicher nicht so verhält, wie man das von der sequentiellen Programmierung gewohnt ist.
Problematik kohärenter lokaler Prozessor-Caches
Snoopy Caches und Cache-Kohärenz
Abb. 2-10 Multiprozessorsystem
Man unterscheidet bei Multiprozessorsystemen zwei gängige Architekturvarianten. Asymmetrische Systeme sind aus einfachen Monoprozessorsystemen entstanden. Ausgangspunkt ist ein vollständig ausgerüsteter Rechner, an den zusätzliche Arbeitsprozessoren über einen geringfügig modifizierten Bus an den Speicher angeschlossen werden. Die Asymmetrie ergibt sich aus der resultierenden ungleichen Arbeitsteilung, da praktisch alle E/A-Aufträge vom sogenannten Master-Prozessor durchgeführt werden müssen. Nur dieser Prozessor kann Interrupts empfangen und verarbeiten, im Fall eines dedizierten E/A-Busses ist er sogar für die Abwicklung des gesamten E/A-Datenverkehr zuständig. Die zusätzlichen Prozessoren werden als sogenannte Slaves eingesetzt, die jeden E/A-Auftrag an den Master-Prozessor übergeben müssen. Dem Vorteil der einfachen Realisierbarkeit dieser Architekturvariante stehen zwei gewichtige Nachteile entgegen: Der MasterProzessor bildet den Flaschenhals hinsichtlich der Leistung des Gesamtsystems und ein Ausfall dieses Prozessors hat den Ausfall des Gesamtsystems zur Folge. Im Gegensatz dazu verfügen symmetrische MultiprozessorSysteme über eine stark erweiterte E/A-Struktur, so daß
Asymmetrischer Multiprozessor
Symmetrischer Multiprozessor
2
Obere Schranke für die Prozessoranzahl
Hardware-Grundlagen
jeder Prozessor gleichberechtigt Zugriff auf alle E/A-Controller, E/ABusse und Interrupts hat. Der maximalen Prozessoranzahl in einem Multiprozessorsystem sind harte Grenzen gesetzt. Jeder zusätzliche Prozessor erhöht die Zugriffslast auf den Speicher trotz zwischengeschalteter Caches. Durch notwendige Hardwaremechanismen für das Einhalten bestimmter Kohärenzkriterien wird die maximale Transferleistung des Speichers zusätzlich reduziert. Gängige Systemgrößen liegen im Bereich 8 bis 16 Prozessoren, in Ausnahmefällen können aber auch bis zu 64 Prozessoren verschaltet werden. Aufgrund der ungleichen Lastverteilung ist die obere Schranke bei asymmetrischen Systemen niedriger anzusetzen. Über den Maximalwert an Prozessoren hinaus ist praktisch keine Leistungssteigerung zu erzielen, vielmehr wirkt sich die resultierende Überlast auf dem Bus verzögernd auf die Ausführungsgeschwindigkeit aller Anwendungsprogramme aus. Nebenläufigkeit und Systemsoftware In allen Fällen der Nebenläufigkeit durch autonome E/A-Controller und zusätzliche Arbeitsprozessoren muß es das Ziel jeder Systemsoftware sein, die vorhandenen aktiven Einheiten sinnvoll auszulasten, um den Durchsatz des Gesamtsystems zu erhöhen und damit letztendlich Anwendungsprogramme möglichst schnell auszuführen. Bei einem symmetrischen Multiprozessorsystem gehört dazu auch die nicht leicht zu lösende Frage, an welchen Prozessor eintreffende asynchrone Unterbrechungen weitergeleitet werden sollen. Durch die gleichzeitige Ausführung mehrerer paralleler Handlungsstränge können außerdem Fehler passieren, die zu einem sichtbaren Fehlverhalten einzelner Anwendungsprogramme führen. Der Systemsoftware obliegt dabei die wichtige Aufgabe, die Abwicklung dieser parallelen Aktivitäten zu koordinieren.
2.5
Abstrakte Maschine M0
Eine abstrakte Rechnerarchitektur
Die Ausführungen der vorhergehenden Abschnitte haben gezeigt, daß es in heutigen Rechnern zum Teil erhebliche Architekturunterschiede gibt: Instruktionssatz, Registerstruktur, Caches, E/A-Konzept usw. variieren sehr stark von Rechner zu Rechner. Ein Anliegen dieses Buchs ist es aber, Systemsoftware-Technologie in einer weitgehend abstrakten, von einer konkreten Rechnerarchitektur unabhängigen Form zu vermitteln. Es ist deshalb angebracht, eine geeignete Abstraktion realer Rechnersysteme vorzugeben, die in der Definition eines Funktionssatzes einer abstrakten Maschine MO mündet. Auf diesem Funktions-
2.5
Eine abstrakte Rechnerarchitektur
satz werden alle nachfolgend eingeführten hardwarenahen Betriebssystemmechanismen aufsetzen. Es würde zu weit führen, einen fiktiven Prozessorbefehlssatz für eine abstrakte Maschine MO festzulegen. Statt dessen wird hier und in den nachfolgenden Kapiteln eine C-ähnliche Notation eingesetzt, von der man ausgehen kann, daß sie durch geeignete Compiler jeweils in die gewünschte Maschinensprache übersetzt werden kann. Wichtige Bestandteile einer abstrakten Rechnerarchitektur sind dagegen die folgenden vier Aspekte:
• • • •
Sichern und Restaurieren bestimmter Registerinhalte oder des gesamten Prozessorzustands An- und Abmelden einzelner Unterbrechungsroutinen Interruptmaskierung Ein- und Ausgabefunktionen
Prozessorzustand sichern Durch das Sichern des Prozessorzustands können höhere Betriebssystemschichten den aktuellen Zustand der Programmausführung jederzeit einfrieren und in einem anderen Kontext fortführen. Dies kann aus einer Reihe von Gründen geschehen, u.a. wenn beispielsweise eine längere E/A-Operation angestoßen wurde und der Prozessor zwischenzeitlich einer anderen Aufgabe zugeordnet werden kann. Der Prozessorzustand ist im wesentlichen durch die sichtbaren Register definiert, die durch den Aufruf der Funktion MO.ContextSave(Address)
an die angegebene Adresse im Hauptspeicher kopiert werden. Analog dazu kann ein gesicherter Prozessorzustand mittels MO.ContextRestore(Address)
wieder als aktueller Zustand des realen Prozessors restauriert werden. Da das direkte Überladen bestimmter Register unmittelbare Konsequenzen auf die aktuelle Programmausführung hat (z.B. führt das Überladen des Programmzählers zu einer impliziten Verzweigung an die angegebene Programmadresse), muß in manchen Fällen eine feste Reihenfolge beim Sichern und Wiederherstellen der Registerinhalte eingehalten werden. Dies gilt im wesentlichen für Programmzähler, Kellerregister und Prozessorstatuswort, für die es daher eigene M0Funktionen gibt: MO.RegisterSave(PC|SP|PSW,Address) MO.RegisterRestore(PC|SP|PSW,Address)
Sichern und Wiederherstellen des Prozessorzustands
2 Hardware-Grundlagen
Durch die Kapselung des Registersatzes wird die tatsächliche Prozessorstruktur für höhere Komponenten der Systemsoftware verborgen. Unterbrechungsverwaltung Unterbrechungen
Da die Anzahl an synchronen und asynchronen Unterbrechungen sowie die Position und Struktur der zugehörigen Sprungtabelle ebenfalls sehr stark vom jeweils eingesetzten Prozessor abhängt, ist auch hier eine Definition der relevanten Funktionen als Teil der abstrakten Maschine MO notwendig: Address MO.RegisterlSR(Index,Address) Address MO.UnregisterlSR(Index) MO.DefaultlSR(Address)
Mit der Funktion RegisterISR kann die Adresse einer Unterbrechungsroutine (ISR = Interrupt Service Routine) für eine synchrone oder asynchrone Unterbrechung eingetragen werden. Dabei wird angenommen, daß alle Unterbrechungen entsprechend ihrem Index in der Sprungtabelle durchnumeriert sind. Der Index in der Tabelle kann damit als eindeutiger Bezeichner für eine Unterbrechung eingesetzt werden. Die Funktion liefert die Adresse einer eventuell vorher eingetragenen Unterbrechungsroutine als Ergebnis zurück. Durch den Aufruf der Funktion UnregisterISR wird eine registrierte Unterbrechungsroutine, ausgetragen; im Gegensatz dazu wird beim Eintreffen eines entsprechenden Ereignisses die mit DefaultISR angegebene Standardunterbrechungsroutine aufgerufen. MO.InterruptDisable(Interrupt) MO.InterruptEnable(Interrupt)
Durch die Funktion i n t e r r u p t D i s a b l e kann eine bestimmte Unterbrechung zeitweise maskiert werden. Mit InterruptEnable wird eine Maskierung wieder aufgehoben, d.h., eintreffende Interrupts lösen den Aufruf der Unterbrechungsroutine aus. Diese Funktionen können nur sinnvoll auf asynchrone Unterbrechungen (Interrupts) ausgeführt werden, sie haben bei synchronen Unterbrechungen keine Bedeutung. MO.Raise(Interrupt) Mit der letzten Funktion Raise aus dieser Gruppe kann der Aufruf einer Unterbrechungsroutine erzwungen werden. Diese Funktion dient im wesentlichem dem kontrollierten Wechsel in den SupervisorModus. Sie wird auf die jeweils vom Prozessor angebotenen synchronen Ausnahmebefehle (z.B. Trap) abgebildet.
2.5
Eine abstrakte Rechnerarchitektur
Ein- und Ausgabe Die letzte Funktionsgruppe der MO umfaßt die Ansteuerung von Einund Ausgabegeräten. Wie in Abschnitt 2.3 diskutiert, muß zwischen zeichen- und blockorientierten Geräten unterschieden werden: Char MO.In(EA_Address) MO.Out(EA_Address,Char) Block MO.InBlock(EA_Address) MO.OutBlock(EA_Address,Block)
Die bei diesen Funktionen jeweils einzusetzende E/A-Adresse hängt von der Plazierung des entsprechenden E/A-Controllers im Prozessoroder im dedizierten E/A-Bus ab. In Abhängigkeit von der konkreten E/A-Struktur werden die zeichenweisen Ein- und Ausgabebefehle auf einfache Speichertransferbefehle oder auf spezielle E/A-Instruktionen abgebildet. Die blockorientierten Funktionen werden entweder als Speicherkopierfunktion realisiert oder bei vorhandener DMA-Funktionalität in einen entsprechenden DMA-Auftrag umgewandelt.
E/A-Operationen
3
Laufzeitunterstützung aus Anwendersicht
Der direkte Umgang der Anwendungsprogramme mit der Rechnerhardware, wie sie in Kapitel 2 vorgestellt wurde, ist aus verschiedenen Gründen problematisch:
• •
• •
Die Schnittstellen zu den Geräte-Controllern sind heute zwar weitgehend standardisiert, aber dennoch unhandlich. Die Koordination von Prozessor und Geräten mit den hardwareseitig dafür zur Verfügung stehenden Hilfsmitteln wie Interruptmechanismus oder zyklische Abfrage (Polling) führt auf schwer durchschaubare Softwarestrukturen, die man auf Anwendungsebene besser vermeiden sollte. In einem Mehrbenutzerbetrieb resultieren aus dem direkten Zugriff zur Hardware schwerwiegende Schutzprobleme durch die unvermeidbare Benutzung privilegierter Instruktionen. Die Abwicklung unabhängiger Benutzeraktivitäten in einem Mehrbenutzerbetrieb erfordert einen Koordinationsaufwand, der ohne ein unterstützendes Programm undenkbar ist.
Aus Anwendungssicht ist es deshalb wünschenswert, einen indirekten Zugang zur Rechnerhardware über eine Dienstschicht zu organisieren. Ziel dieser Schicht ist die Realisierung einer virtuellen Maschine, die einen komfortablen und sicheren Umgang mit der realen Hardware eines Rechners ermöglicht und auch die notwendige Koordination zwischen mehreren Benutzern durchführt. Die in Diensten bereitgestellten Abstraktionen stellen einen Rahmen für die Organisation von Anwendungen zur Laufzeit dar, d.h., sie definieren ein Laufzeitmodell. Anschaulich repräsentiert ein Laufzeitmodell eine fest umrissene Funktionsmenge, die Anwendungsprogrammierern und Benutzern zur Verfügung steht. Die folgenden Dienste sind sehr häufig Bestandteil eines Laufzeitmodells: -
Systembedienung Prozeßmanagement Prozeßinteraktion Datenhaltung Gerätemanagement
Dienstschicht realisiert eine virtuelle Maschine
Laufzeitmodell = Abstraktionen der Dienstschicht
3
Systembedienung
Prozeßmanagement
Prozeßinteraktion
Datenhaltung
Gerätemanagement
Laufzeitunterstützung aus Anwendersicht
Der Dienst Systembedienung ist für die interaktive Führung des Benutzers verantwortlich. Über eine Kommandoschnittstelle wird dem Benutzer der Zugang zum System eröffnet. Dazu gehören wenigstens die oben aufgezählten Standarddienste. Moderne Bediensysteme erleichtern über ausgefeilte graphische Techniken wie Fenster, Menüs und Symbole den Umgang mit dem System. Mit dem Prozeßmanagement bezeichnet man den Dienst zur Verwaltung von Rechenaufträgen. Rechenaufträge werden von dem Augenblick ihrer Erzeugung bis zu ihrer Beendigung als sogenannte Prozesse organisiert. Prozesse definieren damit die Arbeitseinheit für einen Prozessor. Leistungsfähige Dienste für das Prozeßmanagement erlauben es einem Anwender, mehrere Prozesse zu einem Zeitpunkt in der Bearbeitung zu haben und damit freie Kapazitäten des physischen Prozessors möglichst gut auszunutzen. Obligatorischer Bestandteil aller Prozeßmanagement-Dienste ist ein Mechanismus zur Ausnahmebehandlung. Er gestattet es, den Kontrollfluß eines Prozesses bei Auftreten eines Hardwarefehlers oder eines arithmetischen Überlaufs instruktionsgenau zu stoppen und auf eine vordefinierte Ausnahmebehandlungsprozedur umzuschalten. Nach erfolgreicher Behandlung der Ausnahmesituation kann üblicherweise der unterbrochene Prozeß fortgesetzt werden. In Echtzeitsystemen sind Zeitdienste ein weiterer wichtiger Bestandteil des Prozeßmanagements. Mit ihrer Hilfe können Prozesse bis zum Verstreichen einer vorgegebenen Relativzeit oder dem Erreichen einer Absolutzeit in einen Ruhezustand versetzt werden. Unter Zuhilfenahme des Dienstes Prozeßinteraktion können gezielte Formen des Informationsaustausches zwischen Prozessen und notwendige Synchronisationen der Prozesse - z.B. beim Zugriff auf gemeinsam genutzte Ressourcen - wirksam unterstützt werden. Der Dienst Datenhaltung sorgt für die langlebige Aufbewahrung von Daten und Programmen. Dateien können erzeugt oder gelöscht, für eine vorgesehene Bearbeitungsform geöffnet bzw. geschlossen und gelesen, erweitert und modifiziert werden. Datenhaltung ist einer der aufwendigsten und wichtigsten Dienste eines Laufzeitmodells und häufig Voraussetzung für darauf aufbauende Dienste. Der komfortable Zugriff auf Geräte, die unter Kontrolle einer Anwendung stehen, wird durch den Dienst Gerätemanagement realisiert. Für Anwender ist eine Sichtweise auf Geräte sehr attraktiv, die sie aussehen läßt wie Dateien. Da Geräte üblicherweise nicht erzeugt und gelöscht werden können (es sei denn, es handelt sich um virtuelle Geräte), reichen häufig die Funktionen OPEN, CLOSE, READ und WRITE aus, um einen hardwareunabhängigen Zugang zur Rechnerperipherie zu eröffnen. Schwieriger gestaltet sich jedoch die Modellierung interaktiver Geräte wie z.B. graphischer Terminals, die - ausgelöst durch
3
Laufzeitunterstützung aus Anwendersicht
Anschlagen der Tastatur oder Mausbewegungen - spontane Reaktionen auf der Seite des weiterverarbeitenden Programms wünschenswert machen. Ein Beispiel für einen höherwertigen Gerätedienst stellen Druckdienste dar, die das Ausdrucken von textuellen und graphischen Dokumenten wählbarer Qualität unterstützen. Gewöhnlich benötigen sie den Systemdienst Datenhaltung, in dem die zu druckenden Dokumente gespeichert sind. Druckdienste sind ein typisches Beispiel für einen Diensttyp, der den Zugriff zu einem Gerät über eine höherwertige Schnittstelle eröffnet. Insbesondere sind Druckdienste darauf präpariert, Dokumente aus beliebigen Formaten in das für den Drucker notwendige Format - z.B. Postscript - zu wandeln. Fragestellungen Die Diskussion der oben aufgezählten Dienste wirft eine Reihe grundsätzlicher Fragen auf:
•
Welche Dienste gehören zu einem Laufzeitmodell?
Mit dieser Frage ist vor allem die Offenheit eines Laufzeitmodells angesprochen, d.h. die Erweiterbarkeit um weitere Dienste. Wünschenswert wäre natürlich unbegrenzte Offenheit, d.h., der Diensteumfang kann von der leeren Dienstmenge bis hin zu einer einsatzabhängigen Dienstmenge beliebig konfiguriert werden. Totale Offenheit in diesem Sinne setzt Ortbogonalität, d.h. freie Kombinierbarkeit aller Dienste, voraus. Obwohl Orthogonalität eine wünschenswerte Systemeigenschaft darstellt, ist sie nicht immer vollständig erreichbar.
•
Gibt es einen inneren Zusammenhang unter den Diensten?
Mit dieser Fragestellung ist die Dienstestrukturierung unter dem Aspekt der Wirtschaftlichkeit angesprochen. Gibt es elementare Dienste, auf die andere Dienste aufsetzen können? Diese Frage kann bei den heute vorliegenden Erfahrungen eindeutig positiv beantwortet werden.
•
Welche Dienstmenge ist unverzichtbar?
Diese Frage beschäftigt noch heute weltweit die Forschung auf dem Betriebssystemgebiet. Die Festlegung einer unverzichtbaren, minimalen Dienstmenge, auf der alle weiteren Dienste aufbauen, maximiert offenbar die Offenheit eines Systems.
• Wo werden Dienste realisiert?
Druckdienst
3
Laufzeitunterstützung aus Anwendersicht
Diese Frage zielt auf die grundlegende Architektur eines Unterstützungssystems für Anwendungen, das aus einer unverzichtbaren und daraus abgeleiteten Dienstmenge besteht. In den nachfolgenden Unterabschnitten wird auf diese wichtigen Fragen näher eingegangen. Die Antworten tragen dazu bei, ein grundlegendes Verständnis über elementare Laufzeitmodelle mit den darin definierten Abstraktionen zu vermitteln sowie die Grobarchitektur von Laufzeitsystemen als Basis der nachfolgenden Kapitel festzulegen. Unter einem Laufzeitsystem wird hier die Menge von SystemsoftwareKomponenten verstanden, die ein spezifisches, auf eine bestimmte Anwendungsdomäne spezialisiertes Laufzeitmodell realisieren.
3.1
Unverzichtbare Dienste
Alle Rechneranwendungen sind letztendlich auf die Zuweisung von Prozessor und Speicher angewiesen. Der Zugriff auf diese Ressourcen bildet deshalb eine unverzichtbare Basis, auf der höherwertige Dienste aufgesetzt werden können. Ein Laufzeitsystem, das diese Dienste bereitstellt, muß jedoch zwei wesentliche Randbedingungen erfüllen:
• Keine Monopolisierung von Ressourcen
Isolation der Anwendungen untereinander
•
Es muß verhindern, daß eine Anwendung die physischen Ressourcen Prozessor und Speicher auf Dauer vollständig für sich in Anspruch nehmen kann. Diese Monopolisierung der Ressourcen würde nämlich zur Folge haben, daß auf einem Rechner gleichzeitig abgewickelte Anwendungen überhaupt keine Chance mehr hätten, jemals in den Genuß der Zuweisung von Prozessor und Speicher zu kommen. Es muß gleichzeitige Anwendungen so gegeneinander abschotten, daß eine unerwünschte wechselseitige Beeinflussung ausgeschlossen wird. Gewollte Interaktionen müssen jedoch in einer kontrollierten Weise möglich sein.
Die Dienste Prozeßmanagement und Prozeßinteraktion aus dem oben angegebenen Dienstekatalog stellen bei richtiger Auslegung genau die Funktionalität für Anwendungen bereit, die für den nichtmonopolisierenden, geschützten Zugriff zu den Ressourcen Prozessor und Speicher benötigt werden. Sie sind deshalb die geeigneten Kandidaten für die Realisierung elementarer Laufzeitmodelle. Prozesse [Dijkstra 1968] sind dynamische Objekte, die sequentielle Aktivitäten in einem System repräsentieren. Jeder Prozeß ist definiert durch einen Adreßraum, eine darin gespeicherte Handlungsvorschrift in Form eines sequentiellen Programms und einen Aktivitätsträger, der mit der Handlungsvorschrift verknüpft ist und sie ausführt. Adreßräume sind Abstraktionen des physischen Speichers
3.1
Unverzichtbare Dienste
und Aktivitätsträger Abstraktionen des physischen Prozessors. Aktivitätsträger werden in der Fachliteratur auch als Threads bezeichnet. Der Begriff deutet an, daß ein Thread für einen sequentiellen Kontrollfluß verantwortlich ist. Ein Prozeß kann damit als eine Art virtueller Rechner aufgefaßt werden, der auf die Ausführung eines ganz bestimmten sequentiellen Programms spezialisiert ist. Aus Anwendungssicht sollten im Idealfall so viele Adreßräume und Threads definiert werden können, wie zur Lösung eines Problems erforderlich sind. Die zeitweise Zuordnung der physischen Ressourcen Prozessor und Speicher zu Adreßräumen und Threads durch geeignete Multiplexmechanismen ist dann Aufgabe des Laufzeitsystems. Der Dienst Prozeßinteraktion ist eine natürliche Ergänzung des Dienstes Prozeßmanagement. Er unterstützt die gezielte wechselseitige Beeinflussungsmöglichkeit zwischen ansonsten unabhängigen Prozessen und kann als eine Art virtueller Ein- und Ausgabemechanismus aufgefaßt werden. Nachfolgend werden die elementaren Abstraktionen Adreßraum, Thread und Prozeßinteraktion etwas genauer beschrieben. Adreßräume Unter einem Adreßraum versteht man einen von der Speichertechnologie und den beschränkten Ausbaumöglichkeiten physischer Speicher unabhängigen virtuellen Speicher [Denning 1970]. Ein einzelner Adreßraum wird durch Speicherzellen (gewöhnlich Bytes) definiert, die von der Adresse 0 an aufwärts durchnumeriert sind. Adreßräume können nach Bedarf erzeugt und gelöscht werden. Sie stellen die Behälter für die Aufnahme abgegrenzter Programme mit zugeordneten Daten dar. Adreßräume sind gegeneinander abgeschottet, d.h., Threads, die in einem Adreßraum A operieren, können in der Regel nicht auf Speicherzellen anderer Adreßräume zugreifen, es sei denn, der gemeinsame Zugriff wird explizit vereinbart. Damit erfüllen Adreßräume eine grundlegende Schutzfunktion. Die räumliche und zeitliche Zuordnung aller Adreßräume eines Systems zu den vorhandenen Speichern (Cache, Arbeitsspeicher, Platten) geschieht durch einen Speicher-Multiplexer.
Adreßraum = Abstraktion des physischen Speichers
Threads Ein Thread stellt die Abstraktion eines physischen Prozessors dar. In dieser Rolle ist er Träger einer sequentiellen Aktivität, die durch die Ausführung eines dem Thread zugeordneten Programms - seiner Handlungsvorschrift - bestimmt ist. Ist A ein Adreßraum, H eine Handlungsvorschrift in Form eines sequentiellen Programms und T
Thread = Abstraktion des physischen Prozessors
3
Laufzeitunterstützung aus Anwendersicht
ein Thread, dann repräsentiert das Tripel ( A , H , T ) einen sequentiellen Prozeß. Da ausschließlich sequentielle Prozesse betrachtet werden, sprechen wir vereinfacht von Prozessen. Ebenso wie Adreßräume können Threads nach Bedarf erzeugt und terminiert werden. Die Selbstterminierung eines Threads erfolgt immer dann, wenn das zugeordnete Programm bis zu Ende abgewickelt, d.h. der Rechenauftrag vollständig ausgeführt wurde. Die zeitweise Zuordnung der physischen Prozessoren eines Rechners zu den Threads ist Aufgabe des Prozessormultiplexers [Nehmer 1975]. Prozeßinteraktion
Konkurrenz
Kooperation
Disjunkte Prozesse, d.h. Prozesse, die völlig isoliert voneinander ablaufen, stellen eher die Ausnahme dar. Sehr häufig kommt es gewollt oder ungewollt zu Wechselwirkungen zwischen Prozessen: Die Prozesse interagieren. Die Unterstützung der Prozeßinteraktion stellt einen unverzichtbaren Dienst dar, ohne den ein koordiniertes Nebeneinander von Prozessen in einem System undenkbar wäre. Betrachtet man diese Prozeßwechselwirkungen genauer, dann lassen sich zwei grundlegende Interaktionsmuster unterscheiden, die mit Konkurrenz und Kooperation bezeichnet werden [Ben-Ari 1990]. Eine Konkurrenzsituation unter Prozessen liegt dann vor, wenn sie sich gleichzeitig um ein nur exklusiv benutzbares Betriebsmittel wie z.B. einen Drucker bewerben. Durch entsprechende Koordinierung der beteiligten Prozesse beim Zugriffsversuch muß sichergestellt werden, daß das Betriebsmittel zu einem Zeitpunkt immer höchstens einem Prozeß zugeordnet wird. Das wird durch zeitliche Verzögerung der beteiligten Prozesse erreicht, durch die insgesamt eine serielle Benutzung des Betriebsmittels erzwungen wird. Die zeitliche Abstimmung unter nebenläufigen Prozessen - z. B. durch Verzögerung einiger Prozesse - wird als Prozeßsyncbronisation bezeichnet. Synchronisationsmechanismen dienen demnach vornehmlich der systematischen Behandlung von Konkurrenzsituationen. Man spricht von Prozeßkooperation, wenn die beteiligten Prozesse gezielt Information untereinander austauschen. Im Gegensatz zu konkurrierenden Prozessen müssen sich kooperierende Prozesse kennen. Eine häufige Form der Kooperationsbeziehung ist eine Auftragsbeziehung, d.h., eine Gruppe von Prozessen - die Clients - erteilt Aufträge an andere Prozesse - die Server. Das typische Muster einer Auftragsabwicklung zwischen einem Client und einem Server ist untenstehend veranschaulicht:
3.1
Unverzichtbare Dienste
Kooperationsbeziehungen zwischen Prozessen werden durch Mechanismen der Prozeßkommunikation unterstützt. Sehr häufig ist einem Prozeß nicht bewußt, daß er in Interaktionsbeziehungen mit anderen Prozessen verstrickt ist. In Abbildung 3-1 ist z.B. ein Prozeß dargestellt, der Funktionen einer tieferliegenden Schicht aufruft, die zur Durchführung ihrer Aufgabe mit anderen Prozessen interagiert. Die Interaktion ist jedoch in der Schicht gekapselt und äußert sich lediglich darin, daß der Prozeß in der Funktion ggf. für eine bestimmte Zeit blockiert wird. Er erhält die Kontrolle erst nach vollständiger Durchführung der Funktion wieder zurück. Man spricht dann auch von blockierenden Aufrufen. Prozeßinteraktionen, die über blockierende Aufrufe zustande kommen, nennen wir implizit und grenzen sie von der expliziten Prozeßinteraktion ab, bei der Prozesse bewußt interagieren.
Explizite und implizite Interaktion
Abb. 3-1 Implizite Prozeßinteraktion durch blockierenden Aufruf
Abbildung 3-2 faßt die Ergebnisse der obigen Diskussion in einem groben Klassifikationsschema für das Gebiet der Prozeßinteraktion zusammen (siehe dazu auch [Andrews und Schneider 1983] und [Herrtwich und Hommel 1994]).
12
Zugang zur Systemsoftware
Betriebssysteme verdeutlicht. Im nachfolgenden Abschnitt 12.1 werden die für den Start neuer Anwendungen notwendigen Shell-Kommandos vorgestellt. Abschnitt 12.2 beschäftigt sich mit den Funktionen zur Verwaltung aller aktuell ausgeführten Prozesse eines Benutzers oder des Gesamtsystems. Ein weiterer wichtiger Bestandteil einer Betriebssystem-Shell ist der Zugang zum Dateisystem. Er ist Gegenstand von Abschnitt 12.3. Abschließend werden in Abschnitt 12.4 Möglichkeiten vorgestellt, komplexe Kommandofolgen in einer textuellen Shell mit der Hilfe von Batch- und Skript-Dateien zu automatisieren.
12.1 Start neuer Prozesse
Dienstprogamme
Kommandozeilenoptionen und Argumente
Eine Hauptaufgabe jeder Systemsoftware-Shell ist es, dem Benutzer die Erzeugung und den Start neuer Anwendungsprozesse zu ermöglichen. Insbesondere wird ein wesentlicher Teil der Funktionalität an der Bedienungsoberfläche selbst von eigenständigen Programmen (Dienstprogramme) erbracht, die von der Shell bei Eingabe der entsprechenden Befehlszeile aufgerufen werden. Bei jedem Start muß vom Benutzer der Name der Datei angegeben werden, die den auszuführenden Programmcode enthält. Je nach Betriebssystem wird dann von der Shell z.B. mit Hilfe der Funktion CreateProcess() im Fall der Microsoft-Betriebssysteme Windows 9x und Windows NT/2000 oder durch den Aufruf der Funktionen fork() und exec() in UNIX-Systemen ein neuer Adreßraum angelegt. Anschließend beginnt der initial erzeugte Thread und damit die vom Benutzer gestartete Anwendung mit der Programmausführung ab einer systemspezifischen Adresse. Der Benutzer kann neben dem obligatorischen Programmnamen auch sogenannte Kommandozeilenoptionen und weitere für die Ausführung notwendige Argumente angeben. Die Shell wandelt alle Optionen und Argumente in ein für das jeweilige Betriebssystem spezifisches Format um und übergibt sie an das neu erzeugte Anwendungsprogramm. Dies soll am Beispiel der UNIX-Funktionen verdeutlicht werden. In diesem Beispiel startet der Benutzer einen C-Compiler, der eine C-Quelldatei c_prog.c übersetzen und binden soll: cc -g -o c_prog c__prog.c
Dabei weist z.B. die Option -g den Compiler an, alle für die Fehlersuche (Debugging) notwendigen Informationen in die fertige Programmdatei zu integrieren. Die zweite Option -o legt den Namen der erzeugten Programmdatei auf das unmittelbar der Option folgende Argument (hier c_prog) fest. Ohne die explizite Angabe des resultierenden Programmnamens würde der Compiler als sogenanntes De-
3.2
Elementare Laufzeitmodelle
Abb. 3-3 Speichergekoppelte Prozesse (Team)
B: Nachrichtengekoppelte Prozesse(Abbildung 3-4) Dieses Laufzeitmodell unterstützt die Strukturierung einer Anwendung in n Prozesse, die jeweils aus einem Adreßraum mit zugeordnetem Thread bestehen. Da die Adreßräume als disjunkt vorausgesetzt werden, kommen als Mittel zur Prozeßinteraktion ausschließlich nachrichtenbasierte Interaktionstechniken in Frage. Prozesse sind die natürlichen Einheiten der Rechnerallokation, d.h., ein oder mehrere Prozesse werden auf einem Rechner plaziert. Für Prozesse, die auf einem Rechner liegen, wurden besonders effiziente Realisierungen für die nachrichtenbasierte Prozeßinteraktion entwickelt. Die Attraktivität dieses Laufzeitmodells liegt in seiner Offenheit: Abgeleitete Dienste lassen sich mühelos in Form eigenständiger Prozesse realisieren; die Abbildung des Modells auf Rechnernetze bietet sich förmlich an.
Abb. 3-4 Nachrichtengekoppelte Prozesse
C: Nachrichtengekoppelte Teams(Abbildung 3-5) Dieses Laufzeitmodell kombiniert die Modelle A und B und damit auch deren Vorteile. Innerhalb eines Teams interagieren Prozesse über den gemeinsamen Speicher (enge Kopplung), während Prozesse aus verschiedenen Teams über Nachrichten interagieren (lose Kopplung).
3
Laufzeitunterstützung aus Anwendersicht
Abb. 3-5 Nachrichtengekoppelte Teams
D: Überlappende Adreßräume(Abbildung 3-6) Wesentliches Charakteristikum dieses Laufzeitmodells ist die Möglichkeit der Vereinbarung gezielter Überlappungen unabhängiger Adreßräume. Diese Fähigkeit eines Laufzeitmodells ist potentiell interessant für Anwendungen, bei denen die gemeinsame Bearbeitung großer Datenmengen zwischen ansonsten weitgehend unabhängigen Teilsystemen gefragt ist. Im Gegensatz zu den Modellen A-C werden allerdings zwei gravierende Probleme aufgeworfen:
•
•
Abb. 3-6 Überlappende Adreßräume
Eine natürliche Strategie der Lokalisierung von Adreßräumen/Threads in einem Rechnernetz existiert nicht mehr. Der Zugriff von Threads aus verschiedenen Adreßräumen auf gemeinsame Segmente macht eine Adreßraum-globale Synchronisation der beteiligten Threads notwendig.
3.3
3.3
Erweiterung der elementaren Laufzeitmodelle
Erweiterung der elementaren Laufzeitmodelle
Die in Abschnitt 3.2 eingeführten elementaren Laufzeitmodelle, die auf den Abstraktionen Adreßraum, Thread und Prozeßinteraktion beruhen, können in vielfältiger Weise ergänzt bzw. erweitert werden. Zusätzliche Leistungen können entweder durch abgeleitete Dienste oder Erweiterung/Ergänzung der vorhandenen Basisabstraktionen eingebracht werden. Dies soll anschließend anhand einiger Beispiele erläutert werden. Gerätemanagement Der kontrollierte Zugriff auf Geräte muß durch jede Laufzeitumgebung unterstützt werden. Bei einer speicherbasierten Ein/AusgabeArchitektur kann dies z.B. durch Einblenden der ein Gerät repräsentierenden Register in den Adreßraum des verantwortlichen Prozesses/Teams geschehen. Der mit einem Gerät assoziierte Dienst wird dann durch den Prozeß bzw. das Team erbracht, in dem die Zugriffsrechte liegen. Die Inanspruchnahme der Dienste für das Gerätemanagement durch andere Prozesse erfolgt mit den Hilfsmitteln der Prozeßinteraktion. Gerätemanagement kann deshalb als ein abgeleiteter Dienst aufgefaßt werden, der die Basisabstraktionen Adreßraum, Threads und Prozeßinteraktion verwendet. In ähnlicher Weise können die Dienste Systembedienung, Druckdienst, Zeitdienst usw. realisiert werden.
Zugriff auf Geräte
Datenhaltung Laufzeitunterstützung für die langfristige Haltung von Daten ist für nahezu alle Anwendungen obligatorisch. Dateisysteme stellen den traditionellen Ansatz für die Bereithaltung dieses Dienstes dar und können z.B. als abgeleiteter Dienst realisiert werden, der spezielle Zugriffsrechte zu einem nichtflüchtigen Speichermedium - in der Regel einer Platte - besitzt. Anwendungen benutzen diesen Dienst über die existierenden Mechanismen der Prozeßinteraktion und stellen damit eine Verbindung zwischen dem flüchtigen Speicher der Adreßräume und dem nichtflüchtigen Speicher eines Dateisystems her. Attraktiv sind jedoch auch Erweiterungen der Basis-Laufzeitmodelle um das Konzept eines persistenten Speichers. Sollen Speicherbereiche eines Adreßraums dessen Lebensdauer überstehen, dann müssen sie traditionell explizit mittels WRITE-Operationen in Dateien abgelegt werden. In neu erzeugten Adreßräumen, in denen auf Dateien zugegriffen werden soll, müssen diese Dateien dagegen mittels READOperationen zunächst in den flüchtigen Speicher geladen werden.
Dateisystem
Persistenter Speicher
3
Single-Level-Store
Laufzeitunterstützung aus Anwendersicht
Diese Trennung zwischen flüchtigem und nichtflüchtigem Speicher ist etwas unnatürlich und kann überwunden werden, wenn man die Abstraktion Adreßraum um Funktionen zur Vereinbarung persistenter Segmente erweitert. Man spricht dann auch vom Konzept des SingleLevel-Store [Chase et al. 1994]. Der explizite Informationsaustausch zwischen flüchtigem und nichtflüchtigem Speicher wird dann überflüssig, da das Laufzeitsystem diese Transporte selbst organisieren kann: Beim Einrichten eines neuen Adreßraumes werden automatisch alle als persistent gekennzeichneten Segmente geladen; spätestens mit dem Löschen eines Adreßraumes werden die persistenten Segmente auf einen nichtflüchtigen Speicher zurückgeschrieben. Die konsequente Fortentwicklung dieser Idee mündet letztlich in persistenten Speichern mit einer transaktionsorientierten Zugriffssemantik, die den Adreßraum auch bei fatalen Fehlern - z.B. dem Ausfall des Rechners - in einem konsistenten Zustand hinterläßt. Echtzeitanwendungen
Einhaltung vorgegebener Zeitschranken
Der indirekte Zugang zur Rechnerhardware über eine Dienstschicht entzieht den Anwendungen notwendigerweise die vollständige Kontrolle über sie. Das ist aus Schutzgesichtspunkten unerläßlich und bildet ferner die Basis für faire Ressourcen-Zuteilungsverfahren. Für harte Echtzeitanwendungen, die auf die Einhaltung vorgegebener Zeitschranken, angewiesen sind, ist dieser Entzug von Kontrolle jedoch nur bis zu einem gewissen Grade tolerierbar. Deshalb müssen durch Erweiterungen der elementaren Laufzeitmodelle Einflußmöglichkeiten auf die Zuteilungsstrategien für Prozessor und Speicher geschaffen werden. Das Thread-Konzept muß zu diesem Zweck um Mechanismen erweitert werden, die die anwendungsspezifische Zuteilung von Prozessoren zu Threads unterstützen. Bei Echtzeitprogrammen kann in der Regel die dynamische Verdrängung von Programmen aus dem Speicher bei Engpaßsituationen nicht toleriert werden, da sie ein indeterministisches Zeitverhalten bewirkt. Hier müssen Einflußmöglichkeiten für Anwendungen geschaffen werden, um Teile eines Adreßraumes im Arbeitsspeicher zu verankern.
3.4
3.4
Grobarchitektur von Laufzeitsystemen
Grobarchitektur von Laufzeitsystemen
Die Bereitstellung der Dienste eines Laufzeitsystems kann grundsätzlich auf drei verschiedene Weisen erfolgen:
• • •
im Adreßraum der Anwendung, in einem abgeschotteten Kern und in Servern, die in separaten Adreßräumen liegen.
Daraus ergibt sich eine Grobarchitektur für Laufzeitsysteme gemäß Abbildung 3-7. Der Zugriff zu einem Dienst beginnt immer durch Aufruf einer Funktion im Adreßraum der Anwendung. Diese Funktion kann den Dienst entweder selbständig erbringen oder die Unterstützung des Kerns bzw. anderer Server in Anspruch nehmen. Obwohl die Diskussion darüber, wo welche Dienste am geeignetsten realisiert werden, noch nicht abgeschlossen ist, lassen sich doch einige generelle Anmerkungen machen.
Abb. 3-7 Grobarchitektur eines Laufzeitsystems bestehend aus Adreßraum-Iokalen Laufzeitroutinen, Servern und einem Kern
Kerne müssen mindestens einen geschützten, nicht monopolisierenden Zugriff zu den Ressourcen Prozessor und Speicher unterstützen. Damit muß der Kern elementare Abstraktionen für Threads, Adreßräume und die Prozeßinteraktion bereitstellen. Um seine Schutzfunktion sicher erfüllen zu können, muß der Kern gegenüber Anwendungen vollständig abgeschottet werden: Der direkte Zugriff auf den Speicher des Kerns ist zu unterbinden, und der Aufruf von Kernfunktionen sollte nur über den Trap-Mechanismus möglich sein, der eine Umschaltung vom Normalmodus, in dem zwangsläufig alle
Elementarabstraktionen
3
Mikrokern
Clien t/Server-Architektur
Laufzeitunterstützung aus Anwendersicht
Prozesse oberhalb des Kerns ablaufen, in den privilegierten Modus bewirkt. Es ist jedoch ratsam, nur ein Minimum an Funktionalität zur Verfügung zu stellen und alle Spezialisierungen auf höhere Ebenen zu verlagern, um die universelle Einsatzmöglichkeit des Kerns für möglichst viele Anwendungsgebiete offenzuhalten. Die Suche nach einem minimalen Kern, der in dem Begriff Mikrokern (micro kernel) zum Ausdruck kommt, beschäftigt noch immer die Forschung auf dem Betriebssystemgebiet [Liedtke 1995]. Die Realisierung von Diensten in separaten Servern ist dann angebracht, wenn sie entweder von sehr allgemeinem Charakter sind (und damit von vielen Anwendungen potentiell genutzt werden) und/oder mit diesen Diensten der geschützte Zugriff auf Geräte verbunden ist, auf die Anwendungen nicht direkt zugreifen dürfen. Ein Beispiel sind Dateidienste. Adreßraum-lokale Realisierung von Diensten ist unter diesen Randbedingungen immer dann vorzuziehen, wenn sie anwendungsbezogene Spezialisierungen eines allgemeineren, durch den Kern oder externe Server bereitgestellten Dienstes darstellen und keine fundamentalen Schutzprinzipien verletzt werden. Der Vorteil Adreßraumlokaler Dienstebereitstellung liegt in der Effizienz begründet: Der Aufruf eines Dienstes erfolgt durch einen einfachen Prozeduraufruf. Die weitaus aufwendigere Umschaltung in den Kern mittels des TrapMechanismus entfällt.
4
Adreßräume
Der Adreßraum ist eine der zentralen Abstraktionen, die von der Systemsoftware eines Rechners zur Verfügung gestellt werden muß. Über einen Adreßraum sind alle für die Ausführung eines Anwendungsprogramms notwendigen Instruktionen und Datenstrukturen zugreifbar. Allgemein wird ein Adreßraum durch eine zusammenhängende Menge von Adressen und deren Inhalte definiert. Die maximale Größe eines Adreßraums kann aus dem Adreßbusaufbau der verwendeten Prozessorarchitektur abgeleitet werden. Sehr weit verbreitet sind gegenwärtig 4 GByte große Adreßräume; in diesem Fall wird jede Adresse durch einen 32-stelligen Binärwert dargestellt (hexadezimal umfaßt ein solcher Adreßraum die Werte von 0x00000000 bis 0xffffffff). Eine Tendenz zu größeren Adreßräumen ist erkennbar, so werden 64-Bit-Adressen bereits heute von Prozessoren wie z.B. SPARC (Sun) oder ALPHA (DEC) unterstützt. Der Inhalt einer vorgegebenen Adresse in einem Adreßraum ist entweder Undefiniert (= Undefinierte Adresse) oder er entspricht einem n-stelligen Binärwert (meistens 1 Byte). Aufgrund unbelegter Adressen entstehen nichtreferenzierbare Lücken im Adreßraum. Der Zugriff auf eine Position innerhalb der Adreßlücke hat einen Laufzeitfehler (Ausnahme) zur Folge, der in der Regel von der Systemsoftware behandelt wird und im Fall eines fehlerhaften oder unzulässigen Zugriffs zum Abbruch des betroffenen Anwendungsprogramms führt. Der konkrete Typ eines Adreßinhalts wird beim Zugriff auf eine definierte Adresse durch den jeweiligen Zugriffskontext festgelegt. So stellen alle in der Instruktionsladephase referenzierten Adreßinhalte binärkodierte Maschinenbefehle dar. Analog dazu werden in der Operandenladephase alle Inhalte als Daten interpretiert, deren Typ sich implizit aus der verwendeten Instruktionsart ergibt. Einer Anwendung wird immer mindestens ein Adreßraum zugeordnet. Grundlage für jede Zuordnung ist der physische Adreßraums eines Rechners. Dieser Adreßraum entsteht durch eine direkte Abbildung der 16, 32 oder 64 Bit breiten Adressen des Prozessors auf die vorhandenen Speicherbausteine und E/A-Controller. Diese meist statische Abbildung wird von Hardwareadreßdekodern durchgeführt
Definition Adreßraum
Undefinierte Adresse
Physischer Adreßraum
4
Einblenden
Adreßräume
(siehe Abbildung 4-1), die in Abhängigkeit von der jeweils anliegenden Adresse über eine einzelne Freigabeleitung (Chip Select) den angesprochenen Baustein freischalten. Man kann in diesem Kontext auch vom Einblenden der RAM- und ROM-Bausteine sowie der E/A-Controller in den physischen Adreßraum des Rechners sprechen. Die Größe eines eingeblendeten Bereichs entspricht der Anzahl an adressierbaren Zellen der jeweiligen Komponente; so kann ein 4 MByte großes RAMModul z.B. den physischen Adreßbereich 0x1a000000 bis 0x1a3fffff belegen.
Abb. 4-1 Abbildung des physischen Adreßraums auf die Hardwarekomponenten
Virtueller oder logischer Adreßraum
MMU
Die unmittelbare Nutzung des physischen Adreßraums bei der Anwendungsentwicklung ist aufgrund der notwendigen detaillierten Kenntnisse über seine Struktur und Zusammensetzung nur in bestimmten Fällen empfehlenswert. Im wesentlichen gilt dies für industrielle Steuerungen überschaubarer Komplexität, bei denen ein minimaler Hard- und Softwareaufwand aus wirtschaftlichen Gründen gefordert wird. Als Sonderfall gelten ältere aber weit verbreitete Betriebssysteme wie z. B. MS-DOS der Firma Microsoft, die für Anwendungen ausschließlich den physischen Adreßraum des Rechners bereitstellen. Allgemein ist es sinnvoller, einer Anwendung einen »bereinigten« Adreßraum zur Verfügung zu stellen, der alle technischen Details verbirgt und vorhandene Beschränkungen aufhebt. Erreicht wird dies durch einen zweiten Abbildungsschritt, in dem ein logischer oder virtueller Adreßraum aufgebaut und auf den physischen Adreßraum des Rechners abgebildet wird. Obwohl dieser zweite Abbildungsschritt zusätzlichen Hardware- und Softwareaufwand kostet, wird der Overhead wegen der resultierenden Vereinfachung bei der Nutzung und Verwaltung von Adreßräumen in den meisten Fällen akzeptiert. Die Programmausführung findet in diesem Fall in einem virtuellen Adreßraum statt; der Prozessor referenziert virtuelle Adressen, die typischerweise durch eine nachgeschaltete sogenannte MMU (= Memory Management Unit) auf der Grundlage einer veränderbaren
^
4.1
Organisation von Adreßräumen aus Anwendungssicht
Abbildungsvorschrift in physische Adressen transformiert werden (siehe Abbildung 4-2). Der Vorteil virtueller Adreßräume gegenüber der direkten Nutzung des physischen Adreßraums liegt primär in dieser programmgesteuerten Veränderbarkeit der Abbildungsvorschrift begründet, die der Systemsoftware eine umfassende Kontrolle und Koordinierung von ein oder mehreren Anwendungsadreßräumen ermöglicht. Abb. 4-2 Abbildung eines virtuellen Adreßraums auf den physischen Adreßraum
Im nachfolgenden Abschnitt 4.1 werden die typischen Eigenschaften von Adreßräumen aus Anwendungssicht diskutiert und Anforderungen an deren Realisierung definiert. Danach werden die Möglichkeiten und Konsequenzen der direkten Nutzung des physischen Adreßraums vorgestellt. In den Abschnitten 4.3 und 4.4 werden anschließend segment- und seitenbasierte virtuelle Adreßräume erläutert. Die Einbeziehung externer Hintergrundspeicher zur Vergrößerung virtueller Adreßräume über die Grenzen des physischen Speichers und die Behandlung von Speicherengpässen durch die Auslagerung ganzer virtueller Adreßräume sind Gegenstand der Abschnitte 4.5 und 4.6. Anwendungsadreßräume gängiger Betriebssysteme und Implementierungsaspekte werden in Abschnitt 4.7 besprochen.
4.1
Organisation von Adreßräumen aus Anwendungssicht
Im Adreßraum einer Anwendung müssen alle für die Programmausführung notwendigen Daten zur Verfügung gestellt werden. Man kann diese unmittelbar benötigten Informationen in 3 Bereiche untergliedern: • • •
Programmcode (Text) Datenbereich Laufzeitkeller (Stack)
4 Adreßräume
Code- oder Textbereich
Statischer Datenbereich Dynamischer Datenbereich (Heap)
Laufzeitkeller (Stack)
Der Code- oder Textbereich umfaßt die zur Programmausführung notwendigen Maschineninstruktionen. Der Datenbereich enthält alle notwendigen Variablen und speichert damit einen wesentlichen Anteil des Programmzustands. Außerdem benötigt jeder Thread oder Kontrollfluß, der innerhalb eines Adreßraums ausgeführt wird, einen Laufzeitkeller, in dem u.a. die jeweilige Unterprogrammverschachtelung gespeichert ist. Der Datenbereich selbst wird feiner unterteilt in einen statischen Datenbereich, der alle bereits zum Startzeitpunkt bekannten und u.U. vorinitialisierten Datenstrukturen enthält, und einen dynamischen Datenbereich (Heap), der alle während der Programmausführung dynamisch erzeugten Datenstrukturen aufnimmt. Letztere entstehen z.B. durch den Aufruf der Funktion malloc() bei C-Programmen oder des new-Operators in C++-Programmen. Eine dritte Gruppe von Datenbereichen ist meist in die Laufzeitkeller der Kontrollflüsse integriert und enthält alle beim Aufruf einer Prozedur benötigten lokalen Variablen (sogenannte automatische Variablen). Die Speicherung dieser Variablen auf dem Laufzeitkeller ist insbesondere bei der Realisierung rekursiver Prozeduraufrufe von Vorteil, da jede nicht beendete Prozeduraktivierung einen eigenen lokalen Variablensatz erfordert. Wachstum einzelner Adreßbereiche
Wachstum von Heap und Stack
Abb. 4-3 Wichtige Bereiche innerhalb eines Adreßraums
Für jeden Bereich muß von der Adreßraumverwaltung die Plazierung und Größe im Adreßraum festgelegt werden. Dabei muß bei dieser Plazierung ein eventuelles Größenwachstum einzelner Bereiche besonders berücksichtigt werden. Die Bereiche Programmcode und statische Daten sind in dieser Beziehung unproblematisch, da sich ihre Größe typischerweise nicht während der Programmausführung verändert. Im Unterschied dazu können der dynamische Datenbereich und der oder die Laufzeitkeller an Umfang erheblich zunehmen. Beim Heap geschieht dies u.U. mit jeder neuen Speicheranforderung seitens der Anwendung, wenn diese zwischenzeitlich keinen weiteren Speicher freigibt. Bei Laufzeitkellern hängen Größenschwankungen und Ausdehnung von der maximalen Tiefe der Prozedurschachtelung und dem Bedarf an prozedurlokalen Variablen ab.
4.1
Organisation von Adreßräumen aus Anwendungssicht
Die resultierende Adreßraumstruktur aus der Sicht des ausgeführten Programms ist in Abbildung 4-3 dargestellt. In der Regel werden Programm und statische Daten an einem Ende des Adreßraums plaziert. Die beiden dynamisch anwachsenden Bereiche werden mit einem maximalen Abstand voneinander im verbleibenden Adreßraum angeordnet, um ein möglichst unbeschränktes Wachstum dieser Bereiche zu gewährleisten. Die in der Abbildung gewählte Position der einzelnen Bereiche und die Wachstumsrichtung bei veränderlichen Bereichen sind für viele Systeme typisch und dienen o.B.d.A. als Grundlage für die nachfolgenden Abschnitte. Die konkrete Plazierung der Adreßbereiche wird durch das jeweils verwendete Betriebssystem und die zugrundegelegte Prozessorarchitektur bestimmt, z.B. legen viele Prozessoren bei Kelleroperationen eine bestimmte Wachstumsrichtung des Kellers automatisch fest und schränken damit die Plazierungsmöglichkeiten der Laufzeitkeller ein. Eine Anpassung an die Gegebenheiten einer konkreten Rechnerarchitektur ist meist einfach durchführbar. Mehrere Threads in einem Adreßraum Ein maximaler Abstand zwischen den dynamischen Daten und einem Laufzeitkeller erlaubt ein maximales Wachstum dieser beiden Bereiche innerhalb eines vorgegebenen Adreßraums. Die Situation wird etwas komplizierter, wenn mehrere Kontrollflüsse in einem Adreßraum unterstützt werden müssen. In diesem Fall wird der Kellerbereich typischerweise mehrfach unterteilt und jedem Kontrollfluß ein eigener Laufzeitkeller zugewiesen (siehe Abbildung 4—4). Der Abstand zwischen den einzelnen Kellern wird entweder standardmäßig vom System vorgegeben oder er wird vom Programmierer festgelegt. Eine exakte Bestimmung der maximalen Ausdehnung eines Laufzeitkellers ist in der Praxis meist unmöglich, d.h., der Abstand sollte deshalb möglichst groß gewählt werden. Dem steht die Forderung nach einer möglichst ökonomischen Nutzung des Adreßraums gegenüber, um insbesondere den Bereich für dynamische Daten im Wachstum nur minimal einzuschränken.
Ein Stack pro Thread
Abb. 4-4 Adreßraumorganisation bei mehreren Threads
4
Adreßräume
Überschneidung von Heap und Keller
Laufzeitfehler bei Überschneidungen von Heap und Stack
Unabhängig von der Anzahl der Laufzeitkeller muß eine Überschneidung zwischen mehreren Kellern oder zwischen dem »untersten« Laufzeitkeller und dem Bereich der dynamischen Daten grundsätzlich vermieden werden. Überschneiden sich diese Bereiche irgendwann während der Programmausführung entweder durch den Aufruf einer Prozedur oder durch das Anlegen einer weiteren, dynamischen Datenstruktur, so ist der zugeordnete Adreßraum zu klein, das Programm kann nicht ausgeführt werden. In der Realität ist der Abstand zwischen Heap und einem Laufzeitkeller in den meisten Fällen jedoch hinreichend groß, d.h., es existiert eine viele MByte große nicht referenzierte Lücke im Adreßraum. Geht man z.B. davon aus, daß alle vier genutzten Bereiche eines Adreßraums zusammen 20 MByte umfassen (die meisten Programme erreichen diese Größe bei weitem nicht), so ergibt sich bei einem 4 GByte großen Adreßraum je nach Betriebssystem eine Lücke von 2028 bis 4076 MByte. Im Fall mehrerer Laufzeitkeller wird dagegen die Wahrscheinlichkeit für einen zu kleinen Adreßraum durch den Zwang, den Abstand zwischen den einzelnen Kellern a priori festlegen zu müssen, überproportional erhöht. Unerkannte Überschneidungen haben in der Regel schwer zu lokalisierende indirekte Laufzeitfehler zur Folge. Wünschenswert sind also Maßnahmen, die den Eintritt einer solchen Überschneidung automatisch erkennen und die Programmausführung abbrechen. Bei heutigen Systemen werden Überschneidungen zwischen dynamischem Datenbereich und »unterstem« Laufzeitkeller in der Regel erkannt, ein Schutz der Keller untereinander ist jedoch in vielen Systemen noch nicht gewährleistet. Theoretisch kann ein absoluter Schutz für sogenannte Kellerüber- und -unterlaufe nur durch den Einsatz bestimmter Adressierungstechniken, die nicht jeder Prozessor anbietet, garantiert werden. In der Praxis kann aber auch in den verbleibenden Fällen die Wahrscheinlichkeit für den Eintritt eines solchen Fehlers hinreichend klein gehalten werden. Größe eines Adreßraums Heutige Prozessoren besitzen meist einen 32 oder 64 Bit breiten Adreßbus. Da der Prozessor bei der Programmausführung auf alle Instruktionen und Daten letztendlich über diesen Prozessorbus zugreift, steht jeder Anwendung potentiell ein 4 GByte oder bei 64 Bit breiten Adressen sogar ein 18x10 18 Byte großer Adreßraum zur Verfügung. Um das Wachstum des Programms nur minimal zu beschränken, beginnt der Programmbereich im Idealfall an der Adresse 0x00...00, die Bereiche der statischen und dynamischen Daten schließen sich un-
4.1
Organisation von Adreßräumen aus Anwendungssicht
mittelbar an, und der Keller wird am anderen Ende des Adreßraums plaziert - mit einem mehrere 100 MByte großen Abstand zum Heap. Bei sehr großen Programmen wird im Extremfall diese Lücke zwischen Heap und Stack vollständig benutzt, d.h., durch eine tiefe Prozedurverschachtelung und/oder umfangreiche statische und dynamische Datenbereiche werden von einem solchen Programm tatsächlich alle 4096 MByte im Adreßraum belegt. In der Realität ist eine Ausdehnung des Anwendungsadreßraums ohne zusätzliche Hilfsmittel jedoch nur innerhalb der Schranken des physischen Speicherausbaus möglich, durch den meist eine enge Grenze für das Adreßraumwachstum vorgegeben wird. Die Ausführung derart großer Programme setzt in diesem Fall sogar einen vollständig bestückten 32-Bit-Adreßraum voraus. Auch in Zeiten fallender Hardwarepreise ist ein so großer Speicherausbau teuer (ganz zu schweigen von einem vollständig bestückten 64-Bit-Hauptspeicher) und bis auf ganz wenige Einzelfälle auch völlig unnötig, da selbst ein sehr schneller Prozessor nicht auf jede Speicherzelle innerhalb des 32-Bit-Adreßraums mit hoher Frequenz zugreifen kann. Demgegenüber bieten moderne externe Speichersysteme wie z.B. Festplatten zwischen 8 und 80 GByte Speicherkapazität zu einem vergleichsweise geringen Preis von gegenwärtig ca. 12 DM pro GByte. Es liegt also nahe, Teile des Adreßraums größerer Anwendungen temporär auf externem Speicher auszulagern und den frei werdenden Speicherbereich zur Vergrößerung des Adreßraums oder zur Befriedigung von Speicheranforderungen anderer Anwendungen einzusetzen. Vorbedingung für den erfolgreichen Einsatz dieser Technik ist natürlich, daß der ausgelagerte Teil des Adreßraums längere Zeit nicht referenziert wird, um die erheblich höheren Zugriffszeiten auf externe Speichermedien zu kompensieren. Konzeptionell kann die Speicherauslagerung durch eine spezielle Abbildungstabelle realisiert werden, die für jede Speicherzelle angibt, ob sich der entsprechende Wert im Hauptspeicher befindet - also direkt zugreifbar ist - oder nicht. Zusätzlich muß im Fall der Auslagerung vermerkt werden, an welcher Stelle und auf welchem externen Medium sich der ausgelagerte Speicherinhalt befindet. Während der Programmausführung muß durch eine Spezialhardware bei jedem Speicherzugriff überprüft werden, wo sich die jeweils referenzierte Zelle gegenwärtig befindet. Im Fall der Auslagerung muß die aktuelle Instruktionsausführung vom Prozessor unterbrochen und die Systemsoftware beauftragt werden, den ausgelagerten Zelleninhalt wieder in den Hauptspeicher zu übertragen. Nach Abschluß dieses Einlagerungsvorgangs kann dann die unterbrochene Instruktionsbearbeitung durch den Prozessor wieder aufgenommen und der Adreßinhalt aus dem Hauptspeicher gelesen werden.
Lücke zwischen Heap und Stack
In Ausnahmefällen benötigen Anwendungen den gesamten virtuellen Adreßraum
Adreßraum um externen Speicher erweitern
4
Adreßräume
Adreßraumfragmentierung
Externe Fragmentierung
Unter dem Begriff Fragmentierung versteht man verschiedene Formen der Zerstückelung des noch freien und nutzbaren Teils des Adreßraums in kleinere Bereiche. Fragmentierung kann jedesmal dann entstehen, wenn eine neue Speicherplatzanforderung aus einer Menge noch freier Speicherbereiche mit einem einzigen zusammenhängenden Bereich befriedigt werden muß. Über viele einzelne Anforderungen hinweg kann eine Situation entstehen, in der eine weitere Anforderung bestimmter Größe nicht erfüllt werden kann, obwohl in der Summe ausreichend viel freier Speicher zur Verfügung steht. Man unterscheidet zwischen externer und interner Fragmentierung (siehe auch Abbildung 4-5). Bei der externen Fragmentierung wechseln sich benutzte und unbenutzte Speicherbereiche innerhalb eines Adreßraums ab. Im Fall eines Anwendungsadreßraums beschränkt sie sich auf den dynamischen Datenbereich und entsteht durch die unterschiedlichen Speicheranforderungs- und Freigabemuster. Zu einem bestimmten Zeitpunkt ist der Adreßraum soweit zerstückelt, daß größere Bereichsanforderungen nicht mehr erfüllt werden können. In bestimmten Fällen könnte diese Form der Fragmentierung durch ein meist zeitaufwendiges Zusammenschieben der benutzten Speicherbereiche vermieden werden. Diese Kompaktierung verbietet sich jedoch, wenn innerhalb der benutzten Speicherbereiche Verweise auf Absolutadressen von Instruktionen oder Daten enthalten sind und diese nicht erkannt werden können. In diesem Fall ist eine externe Fragmentierung nur durch den Einsatz einer zusätzlichen Abbildungstabelle möglich, die eine Umordnung der Speicheradressen gestattet. Dabei wird für jede Adresse im Anwendungsadreßraum vermerkt, an welcher Stelle sie sich tatsächlich im physischen Adreßraum befindet. Ein im Anwendungsadreßraum zusammenhängender Bereich kann damit auf mehrere freie, aber nicht zusammenhängende Lücken abgebildet werden.
Abb. 4-5 Externe und interne Fragmentierung
Interne Fragmentierung
Eine zweite, häufige Form der Fragmentierung ergibt sich, wenn der Speicher in Bereiche fester Größe untergliedert ist und Speicheranforderungen nur in Vielfachen dieser festen Grundgröße befriedigt wer-
4.1
Organisation von Adreßräumen aus Anwendungssicht
den können. Beispielsweise kann auf Festplatten Speicher nur blockweise belegt werden; je nach Plattentyp schwanken Blockgrößen zwischen 512 Byte und 4 bis 8 KByte. Selbst für die Speicherung eines einzigen Bytes muß in solchen Fällen ein ganzer Block belegt werden. Auch bei dieser internen Fragmentierung ergibt sich damit ein Verschnitt, der weder von der anfordernden Anwendung noch von der Speicherverwaltung genutzt werden kann. Zugriffscharakteristik Die einzelnen Adreßraumbereiche einer Anwendung unterscheiden sich auch sehr stark in ihrer Zugriffscharakteristik. Bis auf wenige, klar definierte Ausnahmen wird auf den Programmbereich einer Anwendung praktisch nur lesend zugegriffen. Programmierfehler, die eine Veränderung des Programmbereichs zur Folge haben, sind meist sehr schwer und verhältnismäßig spät zu erkennen. Ein entsprechender Schutz vor schreibendem Zugriff minimiert das Fehlerrisiko und ist daher in diesem Bereich des Adreßraums anstrebenswert. Umgekehrt befinden sich in den statischen und dynamischen Datenbereichen sowie im Keller Daten, auf die sowohl lesend als auch schreibend zugegriffen wird. Bei einer sehr restriktiven Auffassung über die Adreßraumnutzung sollte das Ausführen von Instruktionen in diesen Bereichen z.B. zum Schutz gegen bestimmte Formen von Computerviren unterbunden werden. Technisch setzt die Überwachung des Speicherzugriffs voraus, daß zusätzliche Informationsbits für jede Speicherzelle die erlaubten Zugriffsmodi festlegen (keine Zugriffserlaubnis, nur lesender Zugriff, lesender und schreibender Zugriff, ausführbare Instruktion). Die Inhalte dieser Schutzinformationen müssen von der Systemsoftware des Rechners verändert werden können, um den Speicher an die wechselnden Schutzanforderungen anpassen zu können. Gleichzeitig muß durch entsprechende Maßnahmen sichergestellt werden, daß für normale Anwendungsprogramme diese Schutzinformationen nur in kontrollierter Form geändert werden können. Während der Programmausführung muß außerdem Spezialhardware den einzelnen Zugriff des Prozessors auf seine Gültigkeit überprüfen. Bei einer Schutzverletzung muß die Systemsoftware des Rechners z.B. über eine Unterbrechung informiert werden, damit diese geeignete Folgemaßnahmen einleiten und ggf. den verantwortlichen Prozeß terminieren kann.
Nur lesender Zugriff auf Codebereich
Keine Instruktionen in den Datenbereichen
Überwachung jedes Speicherzugriffs
4
Adreßräume
Anwendungsforderungen Zusammenfassend können für einen Adreßraum aus Sicht der Anwendungsprogrammierung eine Reihe wichtiger Forderungen an dessen Realisierung gestellt werden:
• • • •
Homogener und zusammenhängender Adreßraum
Erkennen und Isolieren von Laufzeitfehlern
Homogene und zusammenhängende Adreßbereiche Größe des genutzten Adreßraums unabhängig von der Kapazität des physischen Adreßraums Erkennen fehlerhafter Zugriffe Erkennen von Überschneidungen zwischen Heap und Keller sowie zwischen mehreren Laufzeitkellern
Die ersten beiden Forderungen zielen auf eine Verbesserung der Adreßraumstruktur, die konfigurationsbedingte, technische Details vor dem Anwendungsentwickler verbergen soll. Eine homogene und zusammenhängende Adreßraumstruktur ermöglicht eine Programmentwicklung ohne das ansonsten notwendige Wissen über Position, Typ und Größe der referenzierbaren Speichermodule und E/A-Controller. Durch die zweite Forderung kann eine Anwendung einen Adreßraum nutzen, dessen Größe nicht durch die Kapazität des vorhandenen physischen Speichers beschränkt ist. Das frühzeitige Erkennen und Isolieren von Zugriffs- und Überschneidungsfehlern ist eine weitere wichtige Gruppe von Forderungen an die Adreßraumrealisierung. Durch eine entsprechend restriktive und geschützte Auslegung des Adreßraums soll versucht werden, für ansonsten schwer aufzudeckende Programmfehler den zeitlichen Abstand zwischen Fehlerursache und -Wirkung zu verkürzen. Aus Sicht der Systemsoftware steht primär der Schutz gegenüber fehlerhaften Anwendungen im Vordergrund. Es müssen Vorkehrungen getroffen werden, die einen Ausfall des gesamten Computersystems als Folge eines Anwendungsfehlers ausschließen. Kein ausreichender Schutz an dieser Stelle bedingt zumindest einen meist langwierigen Neustart des Computers. Die Konsequenzen können aber auch erheblich weitreichender sein, wenn durch den Fehler die Inhalte der dauerhaften Speicher verändert werden. Zum Beispiel können auf einer Platte wichtige Informationen zerstört oder sensible Daten über die Hardwarekonfiguration in batteriegepufferten Speicherbausteinen verändert werden, wenn eine fehlerhafte Anwendung ungeschützt auf kritische Bereiche außerhalb ihres Adreßraums zugreifen kann. Manche Systemsoftware verzichtet sogar auf diese elementare Form des Schutzes und setzt den Ausfall der Anwendung mit dem Ausfall des Gesamtsystems gleich. Zustätzliche Forderungen entstehen, wenn die Systemsoftware eines Rechners mehrere Adreßräume gleichzeitig unterstützt:
4.1
• • • •
Organisation von Adreßräumen aus Anwendungssicht
Schutz funktionstüchtiger Anwendungen gegenüber fehlerhaften Anwendungen Kontrollierbares und kontrolliertes Aufteilen der Speicherressourcen auf alle Anwendungen Speicherökonomie Minimale Fragmentierung
Essentiell ist bei der gleichzeitigen Unterstützung mehrerer Adreßräume deren gegenseitige Abschottung. Analog zum Schutz der Systemsoftware vor fehlerhaften Anwendungen, sollen auch andere funktionstüchtige Anwendungsprogramme geschützt werden. Ist dieser Schutz nicht gewährleistet, können fehlerhafte Programme den Adreßraum einer weiteren Anwendung verändern und damit Folgefehler in dieser auslösen. Indeterministische Fehler dieser Form sind schwer zu reproduzieren und ihre Lokalisierung ist meist extrem schwierig und langwierig. Bei mehreren gleichzeitig zu unterstützenden Adreßräumen müssen außerdem die vorhandenen Speicherressourcen auf diese aufgeteilt werden. Dabei muß berücksichtigt werden, daß der vorhandene physische Speicher meist nicht alle Anforderungen gleichzeitig befriedigen kann und daß Anwendungen zum Teil sehr unterschiedlichen Speicherbedarf haben. Während die meisten Anwendungen moderate Speicheranforderungen im Bereich weniger MByte stellen, gibt es vereinzelt Programme (z.B. im Bereich der KI oder der Bildverarbeitung), die den virtuellen 32-Bit-Adreßraum praktisch ausschöpfen. Eine den Anforderungen der einzelnen Anwendung entsprechende Zuteilung der Speicherressourcen und deren Kontrolle ist damit eine zentrale Aufgabe der Systemsoftware, um den reibungslosen Ablauf zu gewährleisten. Im Fall der speicherbasierten Ein- und Ausgabe obliegt der Systemsoftware in diesem Zusammenhang auch die exklusive Zuteilung bestimmter E/A-Geräte an einzelne Anwendungen. Zusätzlich sollte die Systemsoftware ökonomisch mit dem vorhandenen Speicher umgehen und alle gängigen Techniken einsetzen, um die durch dynamische Anforderungen bedingte Speicherfragmentierung und den tatsächlichen Speicherbedarf jeder Anwendung zu minimieren. Zum Beispiel können zusätzliche Einsparungen vorgenommen werden, wenn mehrere Anwendungen denselben Programmcode oder teilweise dieselben Funktionsbibliotheken verwenden. Durch geeignete Maßnahmen kann dafür gesorgt werden, daß dieser Code mehrfach in verschiedenen virtuellen Adreßräumen vorhanden ist, aber nur in einer Kopie Ressourcen des physischen Adreßraums belegt.
Gegenseitige Isolation der Adreßräume
Aufteilung der Speicherressourcen
Speicherökonomie
4
Adreßräume
Die Speicherabbildungstabelle Wie bereits an mehreren Stellen angeklungen ist, liegt der Schlüssel für eine erfolgreiche Umsetzung der meisten Anforderungen in einer zusätzlichen Abbildungstabelle, die bei jedem Speicherzugriff des Prozessors von einer Spezialhardware berücksichtigt und ausgewertet wird. Konzeptionell definiert diese Abbildungstabelle für jede Zelle im virtuellen Adreßraum der Anwendung die erlaubten Zugriffsmodi auf eine Adresse und den Ort des Adreßinhalts zum gegenwärtigen Zeitpunkt (siehe Abbildung 4-6). Die Ortsinformation selbst kann drei verschiedene Werte annehmen:
• •
•
Sie bezieht sich auf eine Position im physischen Speicher des Rechners (der Zelleninhalt ist in diesem Fall über eine normale Speicher Operation zugreifbar). Sie definiert die Position auf einem externen Speicher, auf den der Zelleninhalt ausgelagert wurde (der Zelleninhalt ist nur zugreifbar, nachdem er vom externen Speicher wieder geladen wurde). Die angegebene Adresse wird gegenwärtig nicht von der Anwendung genutzt (Undefinierte Adresse; der Zugriff führt zu einem Laufzeitfehler).
Abb. 4-6 Speicherabbildungstabelle
Es ist offensichtlich, daß nicht für jedes Byte im Anwendungsadreßraum ein Eintrag in dieser Abbildungstabelle vorhanden sein kann; die Tabelle wäre in diesem Fall mindestens um den Faktor k größer als der realisierte virtuelle Adreßraum (dabei sei k die Anzahl der benötigten Bytes pro Tabelleneintrag). Besondere Berücksichtigung muß auch der Aspekt finden, daß nicht bei jedem Speicherzugriff zusätzlich mehrere Zugriffe auf die Abbildungstabelle stattfinden können; die Ausführungsgeschwindigkeit von Programmen würde sonst ebenfalls minde-
4.2
Physischer Adreßraum
stens um den Faktor k reduziert werden. Viele der im nachfolgenden beschriebenen Adreßraumrealisierungen verfolgen das Ziel, die Größe der Tabelle und die Zugriffszeit substantiell zu verringern, ohne die Vorteile der zusätzlichen Adreßabbildung zu verlieren.
4.2
Physischer Adreßraum
Ein Großteil der Anwendungsforderungen, die im vorigen Abschnitt aufgestellt wurden, können durch den physischen Adreßraum allein nicht erfüllt werden. Eine homogene Adreßraumstruktur ergibt sich zum Beispiel nur dann, wenn der physische Adreßraum bereits homogen und zusammenhängend angelegt ist. Ohne zusätzliche Hardwareunterstützung können auch fehlerhafte Zugriffe auf die verschiedenen Adreßraumbereiche einer Anwendung praktisch nicht erkannt werden. Die theoretische Möglichkeit, vor der Ausführung jeder Instruktion zu prüfen, ob es zu einem Zugriffskonflikt kommt, scheidet aus Aufwandsgründen aus. Überschneidungen zwischen größenveränderlichen Bereichen des Adreßraums (Heap und Keller) können im Prinzip aufgedeckt werden. In diesem Fall muß bei der Vergrößerung eines solchen Bereichs immer überprüft werden, ob sich aus der Zuteilung eine Überschneidung ergibt. Wie im nachfolgenden dargestellt wird, sind jedoch einige Anforderungen zumindest in eingeschränktem Umfang erfüllbar. So können mit Hilfe entsprechender Unterstützungstechniken und unter Verwendung externer Speichermedien Anwendungen ausgeführt werden, die größer als der vorhandene physische Adreßraum sind. Auch der Schutz des zumindest von der Systemsoftware verwendeten Adreßraums vor dem fehlerhaften Zugriff durch Anwendungsprogramme kann mit Rückgriff auf vorhandene Möglichkeiten des Prozessors verhältnismäßig einfach erzwungen werden. Insgesamt bleiben die Möglichkeiten der direkten Nutzung des physischen Adreßraums aber weit hinter den Idealvorstellungen aus Anwendungssicht zurück, so daß ihr Einsatz auf bestimmte Einsatzgebiete beschränkt werden sollte. Ein solcher Einsatzbereich sind einfache industrielle Steuerungen, bei denen sich der zusätzliche Hardund Softwareaufwand zur Realisierung virtueller Adreßräume aus wirtschaftlichen Gründen meist nicht lohnt. Insbesondere wird für virtuelle Adreßraumtechniken zusätzlicher RAM-Speicher in Form der angesprochenen Abbildungstabelle benötigt, der sich aus Kostengründen bei Industriesteuerungen oft verbietet. Viele der aufgezählten Anwendungsforderungen sind im Bereich eingebetteter Systeme glücklicherweise auch nicht essentiell, so wird meist nur eine Anwendung ausgeführt; ein Schutz einzelner Anwendungen untereinander
Erkennen und Isolieren von Fehlern ist praktisch unmöglich
Anforderungen an Speicherverwaltung sind ungenügend umsetzbar
Einsatz bei einfachen Industriesteuerungen
4
MS-DOS
Adreßräume
entfällt damit. Außerdem können die benötigen Speicheranforderungen für diese Programme meist hinreichend genau bestimmt und statisch fixiert werden, so daß keine besonderen Anforderungen an die Laufzeitflexibilität der Adreßraumrealisierung gestellt werden müssen. Ein zweiter Einsatzbereich wird durch ältere aber weit verbreitete Betriebssysteme definiert, die Anwendungsprogrammen ausschließlich den physischen Adreßraum zur Verfügung stellen. Der bekannteste Vertreter dieser Gruppe von Betriebssystemen dürfte MS-DOS der Firma Microsoft sein. Hier steht ein maximal 640 KByte großer RAM-Bereich im physischen Adreßraum des Rechners jeweils nur einer Anwendung zur Verfügung. Schutz des Adreßraums der Systemsoftware
Schutz der Systemsoftware
Variante
Es ist mit verhältnismäßig einfachen Maßnahmen möglich, den von der Systemsoftware verwendeten Teil des physischen Adreßraums vor dem fehlerhaften Zugriff durch ein Anwendungsprogramm zu schützen. Das einfachste dieser Verfahren ist natürlich, alle unveränderlichen Teile der Systemsoftware im ROM-Bereich des physischen Adreßraums anzusiedeln. Eine Veränderung des SystemsoftwareCodes wird damit verhindert. Nachteilig an diesem Verfahren ist unter anderem, daß eine Aktualisierung oder ein Wechsel der Systemsoftware mit hohem Aufwand verbunden ist. Außerdem profitieren Zustandsinformationen der Systemsoftware, die im beschreibbaren Speicher liegen müssen, nicht von dieser Schutzmaßnahme. Eine zweite Möglichkeit besteht darin, Systemsoftware in einem Bereich des Adreßraums abzulegen, der nur im privilegierten Ausführungsmodus zugreifbar ist. Man macht sich in diesem Fall die Tatsache zunutze, daß die meisten Prozessoren die Ausführung von Instruktionen im privilegierten Modus über spezielle Statusleitungen nach außen signalisieren. Kombiniert man diese Statusinformation in geeigneter Weise mit der Adreßdekodierlogik des Rechners, können bestimmte Teile des physischen Adreßraums exklusiv der Systemsoftware zugeordnet werden. Umgekehrt kann die Systemsoftware selbst weiterhin auf den gesamten physischen Adreßraum zugreifen. Der Schutzmechanismus kann so ausgelegt werden, daß bei einem Zugriff auf diesen Speicherbereich im Normalmodus ein Interrupt ausgelöst wird. Da der Prozessor die Ausführung der Unterbrechungsroutine im privilegierten Modus beginnt, kann die Systemsoftware auf diese Schutzverletzung reagieren und das auslösende Anwendungsprogramm z.B. beenden.
4.2
Physischer Adreßraum
Anzahl unterstützter Anwendungsadreßräume Generell besteht die Möglichkeit, mehrere Anwendungen gleichzeitig im physischen Adreßraum zu unterstützen. Soll nur eine Anwendung unterstützt werden, steht dieser im Prinzip der gesamte physische Adreßraum zur Verfügung, abzüglich der von der Systemsoftware benötigten Teile. Bei mehreren Anwendungen muß der verbleibende physische Adreßraum unter allen Anwendungen aufgeteilt werden. Der nutzbare Adreßraum wird damit für jede Anwendung kleiner, und es wächst das Risiko einer Überschneidung bei den größenveränderlichen Speicherbereichen Heap und Stack. Aufgrund des fehlenden Schutzes der Anwendungen untereinander erhöht sich außerdem die Wahrscheinlichkeit, daß ein fehlerhaftes Programm auf Adreßbereiche einer anderen Anwendung zugreift und dort indeterministische Folgefehler herbeiführt. Bei der Unterstützung mehrerer Adreßräume entsteht beim Starten neuer Anwendungen Zusatzaufwand aufgrund notwendiger Relokationen, da Position und Größe des neuen Adreßraums erst unmittelbar vor dem Programmstart ermittelt werden können. Dieser Aufwand kann bei der Verwendung positionsunabhängigen Codes vermieden werden.
Gleichzeitig mehrere Adreßräume unterstützen
Relokation
Swapping Wenn keine besonderen Vorkehrungen getroffen werden, müssen alle zu einem bestimmten Zeitpunkt geladenen Anwendungsadreßräume vollständig in den physischen Speicher des Rechners passen. Wie bereits angesprochen, kann ohne spezielle Hard- und Softwareunterstützung keine für die Anwendung transparente Vergrößerung des Adreßraums über die Kapazität des physischen Speichers hinaus erzielt werden. Überschreiten die Speicherplatzanforderungen in der Summe trotzdem die vorhandenen Kapazitäten, müssen ein oder mehrere Anwendungsadreßräume vollständig auf einen Hintergrundspeicher (z.B. eine Festplatte) verdrängt und zu einem späteren Zeitpunkt wieder in den Hauptspeicher transferiert werden. Mit Hilfe dieses als Swapping bezeichneten Vorgangs wird ein Zeitmultiplexen des vorhandenen physischen Speichers unter den existierenden Anwendungsadreßräumen, die einzeln jeweils vollständig in den physischen Speicher passen müssen, erreicht. Optimierungskriterium für ein solches Multiplexverfahren ist eine gute Prozessorauslastung trotz der mit der Auslagerung verbundenen hohen Zugriffszeiten auf den Hintergrundspeicher. Man macht sich dabei zwei Eigenschaften besonders zunutze:
Temporäre Auslagerung ganzer Adreßräume
4
Adreßräume
1. Bei vielen Programmen wechseln sich längere Rechen- und Blockadephasen ab. 2. Hintergrundspeicher können in der Regel mittels DMA asynchron zum Hauptprozessor bedient werden.
Wechsel zwischen Prozessor- und E/A-Burst
Die erste Eigenschaft beschreibt ein empirisch ermitteltes, typisches Verhalten vieler Anwendungsprogramme. Während der auch als Prozessorbursts bezeichneten aktiven Rechenphasen führt der Prozessor Instruktionen der Anwendung aus. Die Rechenphasen werden immer wieder von Blockadephasen unterbrochen, in denen die Anwendung auf die Beendigung einer Ein- oder Ausgabeoperation wartet. Während dieser E/A-Bursts kann der Prozessor keine weiteren Instruktionen der auf das E/A-Ende wartenden Anwendung ausführen. Zusammen mit der zweiten Eigenschaft ergibt sich damit für SwappingVerfahren die Möglichkeit, die Auslagerung einer Anwendung in deren E/A-Burst zu verlagern. In der Regel erzielt man dabei mit nur einem unterstützten Anwendungsadreßraum keine ausreichende Prozessorauslastung, da das Potential an möglicher Parallelarbeit durch die asynchrone Bedienung des Hintergrundspeichers in diesem Fall gar nicht ausgeschöpft wird. Erst wenn zu einem Zeitpunkt mehrere Adreßräume unterstützt werden und diese unabhängig voneinander bei Bedarf ausgelagert werden können, sind akzeptable Werte für die Prozessorauslastung erreichbar. In diesem Fall kann der Prozessor während der Ein- oder Auslagerung eines Adreßraums einer anderen Anwendung zugeordnet werden. Techniken der Speicherüberlagerung In der Vergangenheit wurden vielfältige Methoden entwickelt, um Programme auszuführen, deren Speicherbedarf bereits alleine die vorhandene Kapazität des Hauptspeichers übertraf. Die Gründe dafür waren so unterschiedlich wie die eingesetzten Methoden. Beispielsweise waren die 16 Bit breiten Adreßbusse der frühen Mikroprozessoren für einige komplexe Programme der damaligen Zeit (z.B. Compiler und Datenbanksysteme) zu klein. Trotz der verhältnismäßig hohen Speicherkosten wurden daher hardwarebasierte Techniken entwikkelt, um mehr als 64 KByte bei einem auf 16 Bit begrenzten Adreßbus anzusprechen. Dieser Trend wurde lange Zeit durch den höheren Preisverfall bei Speicherbausteinen im Vergleich zum Preis der ersten 32-Bit-Prozessoren verstärkt.
4.2
Physischer Adreßraum
Abb. 4-7 Bank-Switching
Eine gängige Technik ist das sogenannte Bank-Switching. Bei diesem Verfahren wird ein festgelegter Teilbereich des physischen Adreßraums mehrfach überlagert (siehe Abbildung 4-7). Durch einen zusätzlichen parallelen Ausgang (Bankselektor) kann dabei programmgesteuert festgelegt werden, welcher Überlagerungsbereich (Bank) zum aktuellen Zeitpunkt im entsprechenden Fenster des physischen Adreßraums zugreifbar ist. Bank-Switching ist damit ein Verfahren, bei dem das ansonsten statische Einblenden der Speicherbausteine in den physischen Adreßraum des Rechners in Teilen dynamisch veränderbar bleibt. Eine softwaremäßige Realisierung des Bank-Switching-Verfahrens stellt die Overlay-Technik dar. Sie zielte vor allem darauf ab, die Überlagerungsbereiche durch den weitaus billigeren Hintergrundspeicher zu ersetzen. Overlay-Techniken sind eher dem Bereich Codeerzeugung bei der Übersetzung von Anwendungsprogrammen zuzuordnen; eine besondere Unterstützung seitens der Systemsoftware ist nicht notwendig. Der Entwickler teilt die Funktionen einer Anwendung in diesem Fall auf mehrere Overlays auf, von denen sich bei der Programmausführung jeweils nur eins im Hauptspeicher befindet. Der Compiler kann bei der Programmübersetzung mit einfachen Mitteln feststellen, ob bei einem Funktionsaufruf in ein anderes Overlay gewechselt wird. Ist dies der Fall, wird entsprechender Code zum Laden des notwendigen Overlays eingefügt, gefolgt vom eigentlichen Funktionsaufruf. Aufgrund der hohen Nachladezeiten ist es sinnvoll, die Unterteilung in mehrere Overlays so zu wählen, daß ein Wechsel möglichst selten vorkommt, d.h., daß viele Funktionsaufrufe im gleichen Overlay bleiben. Bei vielen Programmen können solche wechselwirkungsarmen Ausführungsphasen identifiziert werden. So bestehen z.B. Compiler selbst meist aus zwei Übersetzungsphasen, die häufig mittels der OverlayTechnik nacheinander in den Hauptspeicher geladen wurden. Da sich
Overlay-Technik
4
Adreßräume
die Überlagerung immer nur auf den reinen Programmcode bezieht der gesamte Programmzustand also für die gesamte Ausführungsdauer im Hauptspeicher verbleibt - muß das aktuell geladene Overlay im Gegensatz zu Swapping-Techniken beim Nachladen eines anderen Overlays nicht auf den Hintergrundspeicher zurückgeschrieben werden. Beispiel MS-DOS
MS-DOS
640-KByte-Grenze
Die Problematik der direkten Nutzung des physischen Speichers soll abschließend am Beispiel von MS-DOS kurz verdeutlicht werden (für eine detaillierte Diskussion siehe z.B. [Messmer 1995]). Die Entwicklung der ersten Version von MS-DOS im Jahre 1981 steht in unmittelbarem Zusammenhang mit der Einführung des ersten PCs durch IBM. Herz dieses Ur-PCs war der damalige 8088-Mikroprozessor der Firma Intel. Dieser Prozessor besitzt einen 20 Bit breiten Adreßbus und ist damit in der Lage, maximal 1 MByte zu adressieren. In der PC-Architektur mußte nun dieser 1 MByte große Adreßraum auf das Betriebssystem, speicherbasierte E/A-Geräte und den Speicherbereich für Anwendungsprogramme aufgeteilt werden. Sowohl Betriebssystem als auch alle Anwendungsprogramme werden nach dieser mehr oder weniger willkürlichen Aufteilung in den ersten 640 KByte des physischen Adreßraums untergebracht. Die verbleibenden 384 KByte bis zur 1-MByte-Grenze wurden für ROM-Speicher und verschiedene speicherbasierte E/A-Geräte wie z.B. dem Bildschirmspeicher reserviert. Solange 640 KByte für die Programmausführung ausreichen, entstehen mit dieser Aufteilung keine Probleme. Nachfolgende PC-Generationen besaßen schnell erheblich leistungsfähigere Prozessoren der Intel-Serien 80x86, die weit mehr als nur 1 MByte adressieren konnten (die heutigen Pentium-Prozessoren verfügen z.B. über einen 32 Bit breiten Adreßbus). Gleichzeitig stieg der Speicherbedarf vieler Anwendungen nicht zuletzt wegen der zunehmenden Verbreitung graphischer Bedienungsoberflächen drastisch an. Die 640-KByte-Grenze wurde damit sehr schnell erreicht. Obwohl im UNIX-Bereich virtuelle Speichertechniken bereits seit längerem gängig waren, wurde bei PCs wohl primär aus Kostengründen weiterhin darauf verzichtet. Statt dessen wurde eine Vielzahl an mehr oder weniger exotischen Techniken entwickelt, um zusätzlichen Speicher für Anwendungen zugreifbar zu machen. Ein weiteres Hindernis bei der Durchsetzung einer klaren Adreßraumarchitektur war auch der starke Druck der Abwärtskompatibilität, der erzwang, daß auch frühere PC-Programme auf neueren Rechnern direkt ausführbar bleiben. Entwickelt wurden u.a. Techniken, um unbenutzte Bereiche zwischen der 640-KByte- und der 1-MByte-Grenze nutzbar zu machen und eher
4.3
Segmentbasierter virtueller Adreßraum
als Adressierungstricks einzustufende Versuche, einen kleinen 64KByte-Bereich jenseits der 1-MByte-Schwelle (HIMEM) anzusprechen. Ein Speicherausbau bis zu 32 MByte wurde lange Zeit durch eine weitere Bank-Switching-Variante erreicht, bevor mit neueren Versionen, wie z.B. Windows 95, Anwendungen ein virtueller 32-BitAdreßraum zur Verfügung stand. Bei diesem als Expanded Memory (EMS) bezeichneten Verfahren werden innerhalb des unteren 1MByte-Bereichs des PCs vier Adreßfenster zu 16 KByte plaziert. Der erweiterte Speicher selbst wird ebenfalls in eine vom Ausbau abhängigen Anzahl von Seiten zu je 16 KByte unterteilt. In jedes der vier Adreßfenster kann durch ein spezielles Mapping Register (vergleichbar einem Bankselektor) jede beliebige Seite des erweiterten Speichers eingeblendet werden.
4.3
HIMEM
Expanded Memory
Segmentbasierter virtueller Adreßraum
Eine insbesondere im PC-Bereich gängige und einfache Variante virtueller Adreßraumtechniken basiert auf besonderen Prozessorregistern. Diese sogenannten Segmentregister (bei manchen Prozessoren auch als Basisregister bezeichnet) ermöglichen die Adressierung von Speicherzellen relativ zu einer Basisadresse. Zu diesem Zweck wird allgemein vor jeder Adreßreferenz der aktuelle Wert des Segmentregisters addiert; das Additionsergebnis definiert dann die effektive Speicheradresse. Im Fall von Intel wird damit ab dem 8088-Prozessor z.B. auch die Erweiterung des physischen Adreßraums von 64 KByte auf 1 MByte erreicht. Absolutadressen werden bei diesen Prozessoren und bei neueren Intel-Prozessoren in einem besonderen Kompatibilitätsmodus weiterhin als 16-Bit-Werte angegeben. Die Segmentregister sind ebenfalls 16 Bit breit. Um zu der 20 Bit breiten effektiven Adresse zu gelangen, wird der Inhalt des Segmentregisters konzeptionell mit dem Faktor 16 multipliziert (siehe Abbildung 4-8). Enthält ein Segmentregister zum Beispiel den Wert 0x13a0 und wird innerhalb des Segments die Adresse 0x023f angesprochen, so ergibt sich eine effektive Adresse 0x13a00 + 0x0023f = 0x13c3f. Praktisch wird diese Multiplikation ohne Zeitverzögerung durchgeführt, indem die einzelnen Bits des Segmentregisters um 4 Stellen verschoben addiert werden. Das Betriebssystem ist damit in der Lage, ältere Programme ohne jede Änderung in einem vergrößerten Adreßraum auszuführen, indem es vor dem eigentlichen Programmstart durch Setzen der Segmentregister die Anwendung im 1-MByte-Adreßraum geeignet plaziert.
Segmentregister
Segmentregister wirken implizit auf jeden Speicherzugriff
4
Adreßräume
Abb. 4-8 Bestimmung der Effektivadresse bei Verwendung von Segmentregistern
Zugriffstyp bestimmt das konkrete Segmentregister
Nachteile
Vorteile
Intel-Prozessoren stellen gleich mehrere Segmentregister zur Verfügung, die in Abhängigkeit vom Typ des jeweiligen Speicherzugriffs implizit bei der Ausführung der Maschineninstruktionen eingesetzt werden: Das Code-Segmentregister (CS-Register) wird beim Zugriff auf Instruktionen und PC-relativen Daten, das Data-Segmentregister (DSRegister) bei normalen Datenzugriffen und das Stack-Segmentregister (SS-Register) bei Kelleroperationen ausgewertet. Darüber hinaus steht Anwendungen ein weiteres ES-Register, bei neueren Prozessoren sogar zusätzlich die Register FS und GS, zur Verfügung. Segmente sind nach dem angegebenen Verfahren auf eine Größe von 64 KByte beschränkt. Beim Zugriff auf zusammenhängende Instruktionssequenzen und Datenstrukturen, die diese maximale Segmentgröße überschreiten, müssen die entsprechenden Segmentregister immer wieder neu geladen werden; ein zusätzlicher Aufwand, der die Gesamtleistung meßbar reduziert und der aufgrund von Beschränkungen oder Fehlern bei Sprachcompilern die Programmentwicklung häufig erschwert. Neben der Plazierung von Programmen in einem größeren Adreßraum erfüllen Segmentregister weitere Aufgaben. Die Unterscheidung zwischen verschiedenen Segmentregistern je nach Zugriffstyp erlaubt eine einfache Form des Schutzes - zumindest des Programmcodes. Die beschränkte Größe eines Segments schützt (abgesehen vielleicht von 64 KByte großen Segmenten, die definitiv zu klein sind) darüber hinaus bis zu einem gewissen Grad vor Überläufen, wenn die Basisadressen der einzelnen Segmente ausreichend weit auseinander liegen. Segmentregister sind damit eine primitive Variante virtueller Adressierungstechniken, die einen verhältnismäßig geringen zusätzlichen Hardwareaufwand erfordern. Basis- und Grenzregister Eine echte Überprüfung auf Gültigkeit beim Zugriff auf eine Speicheradresse findet bei der Verwendung von Segmentregistern nicht statt, da jede 16-Bit-Adresse innerhalb eines Segments als gültige Adresse aufgefaßt wird. Außerdem muß - zumindest bei Intel-Prozessoren -
4.3
Segmentbasierter virtueller Adreßraum
eine Anwendung mit der fest vorgegebenen 16-Bit-Größe der Segmente auskommen. Durch eine Erweiterung auf zwei Register können Segmente verschiedener Länge geeignet realisiert werden, das zusätzliche Register speichert in diesem Fall die aktuelle Segmentlänge. Abb. 4-9 Basis- und Grenzregister
Diese als Basis- und Grenzregister (siehe Abbildung 4-9) bekannte Technik faßt die Adressen innerhalb des Segments als Offsets relativ zur Anfangsadresse 0 auf. Bei jedem Speicherzugriff wird überprüft, ob ein Zugriff außerhalb des Segments stattfindet. Zu diesem Zweck speichert das Grenzregister die aktuelle Segmentlänge. Ist die angegebene Adresse kleiner als der Inhalt des Grenzregisters, so findet ein Zugriff innerhalb des Segments statt und der Wert des Basisregisters wird zur virtuellen Adresse addiert. Bei einem Zugriff außerhalb der Segmentgrenzen wird eine Ausnahmebehandlung ausgelöst und damit die Systemsoftware mit der weiteren Behandlung dieses Adreßfehlers beauftragt. Bei einem Basis- und Grenzregisterpaar kann der gesamte Adreßraum der jeweils aktiven Anwendung während der Ausführung geschützt werden. Mit jedem Kontextwechsel durch den Prozessor werden neue Werte für Basis- und Grenzregister geladen, um auf den jeweils aktuellen Adreßraum zugreifen zu können. Fehler wirken sich nur innerhalb des Adreßraums aus. Ein differenzierter Schutz innerhalb eines Adreßraums ist mit diesem Verfahren jedoch nicht möglich. Nachteilig ist außerdem, daß jedes Segment auf einen zusammenhängenden Speicherbereich im physischen Adreßraum abgebildet werden muß. Relokationen der Adreßräume im Speicher - z.B. zur Bereinigung externer Fragmentierung - sind jedoch prinzipiell möglich, da innerhalb des Segments relativ zur Basisadresse adressiert wird.
Einfacher Schutzmechanismus Nachteile
4
Adreßräume
Abb. 4-10 Segmenttabelle
Segmenttabelle
Segmentdeskriptor
Faßt man mehrere Basis- und Grenzregisterpaare zur Beschreibung von Segmenten zusammen, erhält man eine Segmenttabelle (siehe Abbildung 4-10). Auf die zu einem Zeitpunkt gültige Tabelle verweist ein spezielles Register im Prozessor. Das implizit bei jedem Speicherzugriff verwendete Segmentregister enthält in diesem Fall nicht die Basisadresse, sondern einen Index s, der auf den für die Adreßberechnung gültigen sogenannten Segmentdeskriptor in der Tabelle verweist. Alle Adressen werden weiterhin als Offset innerhalb des angesprochenen Segments verwendet. Mit jedem Speicherzugriff wird im Prinzip der entsprechende Segmentdeskriptor von der MMU aus dem Speicher geladen und ausgewertet. Bei gültigem Zugriff wird die im Deskriptor enthaltene Basisadresse zum Offset addiert und auf die angesprochene Speicherzelle zugegriffen. In allen anderen Fällen wird eine Ausnahmebehandlung im Hauptprozessor ausgelöst und der Speicherzugriff abgebrochen. In der Praxis wird der Deskriptor immer nur beim ersten Zugriff auf ein Segment tatsächlich geladen. Bei erlaubtem Segmentzugriff werden in der Regel Basisadresse und Segmentlänge in prozessorlokale Cache-Register kopiert, um nachfolgende Zugriffe zu beschleunigen. Segmente beim 80386-Prozessor Neben der Basisadresse und der Segmentlänge können in einem Segmentdeskriptor weitere Informationen gespeichert werden. Am Beispiel der Segmentdeskriptoren bei den Intel-Prozessoren 80386 und höher soll dies verdeutlicht werden. Jeder dieser Deskriptoren ist 8 Byte groß und speichert für jedes Segment im wesentlichen folgende Daten:
Bestandteile eines Segmentdeskriptors
• •
Basisadresse (32 Bit) Segmentlänge oder Limit (20 Bit)
4.3
• • • • •
Segmentbasierter virtueller Adreßraum
G-Bit (Granularitätsbit) P-Bit (Present-Bit) A-Bit (Accessed-Bit) Segmenttyp (5 Bit) Privilegierungsstufe (2 Bit) = DPL (Descriptor Privilege Level)
Mit der 32-Bit-Basisadresse kann das Segment an jeder Position im maximal 4 GByte großen physischen Adreßraum des Rechners plaziert werden. Die tatsächliche Segmentgröße wird durch das 20-BitLimit und das G-Bit bestimmt. Ist das G-Bit 0, wird die Segmentlänge in Einheiten zu einem Byte interpretiert. Die maximale Segmentgröße ist in diesem Fall 220 Byte = 1 MByte. Bei gesetzten G-Bit ist die Segmentgröße ein Vielfaches einer 4096 Byte großen Seite, mit einer resultierenden Maximalgröße von 232 Byte = 4 GByte pro Segment. Das PBit gibt an, ob sich ein Segment im Hauptspeicher des Rechners befindet oder auf einen Hintergrundspeicher ausgelagert wurde. Das A-Bit wird von der Hardware des Prozessors bei jedem Zugriff auf ein Segment automatisch gesetzt. Durch periodisches Prüfen und Zurücksetzen kann die Systemsoftware dieses Bit nutzen, um geeignete Auslagerungskandidaten zu bestimmen. Indirekt über den Segmenttyp werden die Zugriffsrechte auf ein Segment unterschieden, d.h., ob Leseund/oder Schreibzugriff zugelassen ist sowie ob ausführbare Instruktionen enthalten sind. Außerdem kann über den Segmenttyp die Wachstumsrichtung angegeben werden. Die Privilegierungsstufe DPL legt fest, welche Programme auf ein bestimmtes Segment zugreifen dürfen und welche nicht. Wie bereits in Kapitel 2 erläutert, unterscheiden Prozessoren der 80x86-Familie vier Privilegierungsstufen 0 bis 3. Die Privilegierungsstufe des aktuell ausgeführten Programms wird durch einen zusätzlichen Eintrag im Segmentregister CS vermerkt (CPL = Current Privilege Level) und bei jedem Zugriff auf ein anderes Segment überprüft. Ein direkter Zugriff wird nur gestattet, wenn die Privilegierungsstufe des ausgeführten Programms höher (wertmäßig kleiner) ist als die geforderte Stufe des zugegriffenen Segments, d.h wenn CPL < DPL. In allen anderen Fällen muß ein Zugriff auf höher privilegierte Segmente über besondere CallGates durchgeführt werden, die im wesentlichen einem auf mehrere Stufen erweiterten Trap-Mechanismus entsprechen (siehe dazu z.B. [Messmer 1995]). Der Prozessor selbst besitzt zwei sichtbare Register GDTR und LDTR, in denen letztendlich Verweise auf zwei aktuell gültige Segmenttabellen enthalten sind. Die globale Tabelle GDT (Global Descriptor Table) enthält Deskriptoren für Segmente von allgemeinem Interesse, z.B. Betriebssystemdienste und Funktionsbibliotheken. Diese Segmenttabelle ist für jedes Anwendungsprogramm zugreifbar; die
Zugriffsrechte
Globale Segmenttabelle (GDT)
4
Lokale Segmenttabelle (LDT)
Adreßräume
tatsächliche Nutzung der über sie zugreifbaren Segmente hängt von den jeweiligen Privilegierungsstufen ab. Die lokale Segmenttabelle LDT (Local Descriptor Table) beschreibt typischerweise die Segmente des Anwendungsadreßraums. In ihr können ein oder mehrere Codesegmente, die das Anwendungsprogramm beschreiben, mehrere Datensegmente sowie dynamisch wachsende Segmente für Heap und Laufzeitkeller vermerkt werden. Im Gegensatz zum GDTR wird das LDTR vom Betriebssystem bei jedem Adreßraumwechsel neu geladen. Beide Tabellen sind in ihrer Größe auf jeweils maximal 8192 Segmentdeskriptoren beschränkt. Zusammenfassung
Vorteile
Nachteile
Auf der Grundlage segmentbasierter Adressierungstechniken können die meisten der in Abschnitt 4.1 aufgestellten Forderungen realisiert werden. Die Segmentgröße ist nur durch die Adressierungsmöglichkeiten des Prozessors beschränkt. Jedes Segment kann einen zusammenhängenden Adreßbereich des physischen Adreßraums aufnehmen. Durch das Ein- und Auslagern ganzer Segmente können im Prinzip die Beschränkungen des physischen Speichers überwunden werden. Der wechselseitige Schutz der Anwendungsadreßräume untereinander einschließlich eines feingranularen Schutzes der einzelnen Bereiche innerhalb eines Adreßraums ist ebenso gewährleistet. Ordnet man dem dynamischen Datenbereich und jedem Laufzeitkeller ein eigenes Segment zu, können auch Überschneidungen zwischen diesen Bereichen ausgeschlossen werden, vorausgesetzt die den Segmenten zugeordneten Bereiche im physischen Adreßraum sind überlappungsfrei. Nachteilig an einem rein segmentbasierten Verfahren ist die resultierende externe Fragmentierung, da jedes Segment einen zusammenhängenden Speicherbereich im physischen Adreßraum benötigt. Aufgrund der Relokierbarkeit der Programme können jedoch alle Segmente an einem Ende des physischen Adreßraums zusammengeschoben werden. Dadurch entsteht wieder ein zusammenhängender Freispeicher maximaler Größe. Nach dem Zusammenschieben der Speicherbereiche müssen die Basisadressen in den jeweiligen Segmentdeskriptoren nur mit den neuen Positionen geladen werden. In der Praxis verbietet sich dieses Technik jedoch meist aufgrund ihres enormen Zeitaufwands. Problematisch ist auch der verhältnismäßig hohe Zeitaufwand beim Segmentwechsel. Bei Intel-Prozessoren kann z.B. das Laden eines einfachen Datums aus einem anderen Segment die Ausführungszeit für eine Instruktion um den Faktor 3 erhöhen. Je stärker also Segmente zur Realisierung eines feingranularen Schutzkonzepts ein-
4.4
Seitenbasierter virtueller Adreßraum
gesetzt werden, desto häufiger müssen Deskriptoren nachgeladen werden und desto größer ist der resultierende Overhead.
4.4
Seitenbasierter virtueller Adreßraum
Seitenbasierte Verfahren unterteilen einen Adreßraum in aufeinanderfolgende Seiten gleicher Größe. Für jede einzelne Seite können unter anderem Zugriffsrechte und die aktuelle Position des Seiteninhalts vermerkt werden. Analog zu Segmentdeskriptoren wird diese Information innerhalb einer Seitentabelle in Form sogenannter Seitendeskriptoren gespeichert. Im Gegensatz zu segmentbasierten Verfahren kommt diese Technik ohne besondere Register aus, d.h, Adressierungsarten und sichtbare Registerstruktur des Prozessors bleiben davon unberührt. Eine Adresse wird von der nachgeschalteten MMU als ein 2-Tupel aufgefaßt, das sowohl den Seitenanteil als auch den Offset innerhalb einer Seite für eine Speicherzelle festlegt (siehe Abbildung 4-11). Die Größe einer Seite ist immer eine Zweierpotenz. Zur Adressierung jeder Speicherzelle innerhalb einer Seite benötigt man deshalb k Bits, wenn 2 der Seitengröße entspricht. In der Regel wird dafür gesorgt, daß die MMU die niederwertigen k Bits der Adresse als Offset interpretiert. Die verbleibenden Bits des Adreßbusses legen dann die jeweilige Seite fest. Beispielsweise werden bei 1 KByte großen Seiten 10 Bit für das Offset benötigt. Besitzt der Prozessor einen 32 Bit breiten Adreßbus, können in diesem Fall maximal 22 Adreßleitungen zur Angabe der Seite verwendet werden.
Seitendeskriptor
Abb. 4-11 Aufbau einer seitenbasierten virtuellen Adresse
Der Seitenanteil einer virtuellen Adresse wird als Index innerhalb der Seitentabelle benutzt. Dabei wird der Anfang der aktuell gültigen Seitentabelle durch ein besonderes Register in der MMU (vergleichbar den Registern GDTR und LDTR als Zeiger auf Segmenttabellen bei Intel-Prozessoren) definiert. Da die Seitentabelle in einem zusammenhängenden Speicherbereich liegt und jeder Seitendeskriptor eine feste Größe hat, kann durch eine einfache Berechnung der Form
die Anfangsadresse des aktuellen Seitendeskriptors zu einer Adreßreferenz von der MMU ermittelt werden. Jeder Seitendeskriptor enthält die folgenden Informationen:
Seitentabelle
4
Bestandteile eines Seitendeskriptors
Kachel
Schutzbits
Referenced-Bit
Cache-Disable-Bit
Adreßräume
• • • • • • •
Seitenadresse Present-Bit (P-Bit) Schutzbits Referenced-Bit (R-Bit) Dirty-Bit (D-Bit) Cache-Disable-Bit (C-Bit) frei benutzbare Bits
Die Seitenadresse zusammen mit dem P-Bit gibt an, wo sich der Seiteninhalt gegenwärtig befindet. Ist das P-Bit gesetzt, definiert die Seitenadresse die Kachel mit dem Inhalt. Dabei werden als Kacheln typischerweise die Seiten des physischen Adreßraums bezeichnet. Bei nicht gesetztem P-Bit wurde der Seiteninhalt ausgelagert. In diesem Fall enthält die Seitenadresse in kodierter Form den jeweiligen Auslagerungsort z.B. auf Festplatte. Die Schutzbits geben analog zu den entsprechenden Informationen bei Segmentdeskriptoren an, in welcher Form auf die Speicherzellen einer Seite zugegriffen werden darf. Gewöhnlich wird dabei zwischen keinem, lesendem, schreibendem oder ausführendem Zugriff unterschieden. Die Schutzinformationen in einem Deskriptor beziehen sich immer auf alle Speicherzellen der Seite. Das Referenced-Bit wird von der MMU beim Zugriff auf eine Speicherzelle innerhalb der Seite gesetzt. Vom Betriebssystem können die R-Bits einer Seitentabelle durch zyklisches Überprüfen und Löschen dazu verwendet werden, innerhalb eines Zeitintervalls genutzte und ungenutzte Seiten zu identifizieren. Das Dirty-Bit gibt an, ob seit dem letzten Zurücksetzen durch die Systemsoftware schreibend auf eine Speicherzelle der Seite zugegriffen wurde. Das Cache-Disable-Bit wird benötigt, um die Zwischenspeicherung von Werten aus bestimmten Speicherzellen in den LI- und L2Caches des Rechners zu unterbinden. Die Information gilt für alle Speicherzellen innerhalb der Seite. Dieses Flag ist im wesentlichen beim Zugriff auf speicherbasierte E/A-Geräte notwendig, da hier eine transparente Zwischenspeicherung und ein zeitversetztes Zurückschreiben (Write Back) der Inhalte von E/A-Registern zu Inkonsistenzen und einem Fehlverhalten des Geräts führt. Beispielsweise wird bei einem über eine serielle RS232-Schnittstelle angeschlossenen Gerät das zuletzt empfangene Zeichen aus einem schnittstellenabhängigen E/A-Register entnommen. Durch aufeinanderfolgende Leseoperationen von demselben E/A-Register wird gewöhnlich eine vom Gerät gesendete Zeichenfolge empfangen. Würden die Zugriffe auf das E/ARegister über die Caches des Rechners laufen, könnten die zweite und alle weiteren Leseoperationen aus dem Cache befriedigt werden; ein Zugriff auf das eigentliche Gerät fände nicht mehr statt und nachfolgend eintreffende Zeichen gingen in diesem Fall verloren.
4.4
Seitenbasierter virtueller Adreßraum
Seitendeskriptoren enthalten außerdem meist eine bestimmte Anzahl von der MMU nicht benutzter Bits, die vom Betriebssystem verwendet werden können. Sie erlauben die Speicherung seitenspezifischer Zustandsinformation, insbesondere im Zusammenhang mit der Auslagerung von Seiten auf Hintergrundspeicher.
Freie Bits
Abb. 4-12 Einstufige Seitentabelle
Einstufige Seitentabelle Im einfachsten Fall existiert für jeden virtuellen Adreßraum eine Seitentabelle. Bei jedem Zugriff auf eine Adresse im virtuellen Adreßraum durch den Prozessor kann die angesprochene Seite p und das Offset d innerhalb der Seite identifiziert werden. Unter Verwendung der Anfangsadresse der aktuellen Seitentabelle und dem Seitenindex p wird der zugeordnete Seitendeskriptor geladen (siehe Abbildung 4-12). Ein besonders gekennzeichneter Seitendeskriptor (Nulldeskriptor) entspricht einem Adreßbereich, der gegenwärtig nicht von der Anwendung verwendet wird. Ein Zugriff auf eine Adresse in dieser Seite wird als Adreßfehler zurückgewiesen; der Prozessor beginnt mit der Ausführung einer entsprechenden Ausnahmebehandlungsroutine. Bei einem gültigen Seitendeskriptor werden die in den Schutzbits festgelegten Zugriffsrechte mit dem aktuellen Zugriffsmodus des Prozessors verglichen. Eine Zugriffsverletzung erzwingt ebenfalls eine entsprechende Ausnahmebehandlung im Prozessor, die MMU löst in diesem Fall die referenzierte Adresse nicht weiter auf. Ist der Zugriffsmodus des Prozessors in den Schutzbits enthalten, wird anhand des P-Bits überprüft, ob sich die Seite im physischen Speicher des Rechners befindet. Wenn ja - und in diesem Abschnitt gehen wir von P=l aus -, ergibt die Verknüpfung aus der im Deskriptor gespeicherten Seitenadresse und dem Offset d die effektive Adresse der Speicherzelle. Je nach C-Bit wird dann die Speicherzelle über die Caches oder unter Umgehung der Caches referenziert. Außerdem wird das R-Bit im zugehörigen Seitendeskriptor immer und das D-Bit im Fall eines Schreibzugriffs gesetzt.
Einstufige Adreßabbildung
Nulldeskriptor
4
Einstufige Seitentabellen sind zu groß
Adreßräume
Im Vergleich zu der in Abschnitt 4.1 eingeführten Abbildungstabelle kann durch dieses einstufige seitenbasierte Abbildungsverfahren viel Platz gespart werden. Die Einschränkung, Schutz- und Ortsinformationen auf der Basis ganzer Seiten festzulegen, ist in den meisten Fällen sogar eher von Vorteil, da der Verwaltungsaufwand erheblich reduziert und die Blockorientierung der Hintergrundspeicher im Auslagerungsfall besser berücksichtigt wird. Problematisch bei einem einstufigen Verfahren bleibt jedoch immer noch die Größe der Seitentabelle. Da durch die Plazierung von Heap und Laufzeitkeller immer Anfang und Ende eines Adreßraums verwendet werden, muß zwangsläufig der Speicherplatz für eine vollständig benutzte Seitentabelle angelegt werden. Bei einer typischen Größe von 8 Byte pro Seitendeskriptor, einer Seitengröße von z.B. 4 KByte (12 Bit Offset) und einem 32 Bit breiten virtuellen Adreßraum ergibt sich damit eine Größe von 8 MByte (8*2 32-12 =8*2 20 ). Dieser Wert ist für viele Systeme mit einem Speicherausbau im Bereich 16 bis 64 MByte immer noch viel zu hoch. Außerdem werden alle Seitendeskriptoren, die der Abbildung von virtuellen Adressen innerhalb der Lücke zwischen Heap und Laufzeitkeller dienen, nicht benötigt; d.h., bei einer Größe von 20 MByte für den von einer Anwendung verwendeten Teil des Adreßraums werden 99,5% des Platzes in der Seitentabelle verschwendet. Mehrstufige Seitentabelle
Größenreduktion durch mehrere Stufen
Seitentabellendeskriptor
Durch die Unterteilung in mehrere Abbildungsschritte kann die Größe der Seitentabelle weiter reduziert werden. Gängig sind Verfahren, die über zwei, maximal drei Abbildungsstufen gehen. Für jeden einzelnen Abbildungsschritt können eigene Seitentabellen eingesetzt werden, wobei die Größe der beschriebenen Seiten mit jedem Schritt kleiner wird. Die erste Seitentabelle unterteilt den gesamten virtuellen Adreßraum in verhältnismäßig wenige, aber sehr große Seiten. An jeder Stelle der Seitentabelle kann - analog zum einstufigen Verfahren - ein Seitendeskriptor stehen, wenn tatsächlich ein entsprechend großer Bereich im physischen Adreßraum dafür reserviert wurde. Wird dagegen nur ein verhältnismäßig kleiner Teil des Adreßbereichs einer so großen Seite von der Anwendung tatsächlich genutzt, kann auch ein sogenannter Seitentabellendeskriptor eingetragen werden. Ein Seitentabellendeskriptor verweist in diesem Fall auf eine weitere Seitentabelle, die den Adreßbereich der übergeordneten »Seite« ihrerseits in kleinere Seiten unterteilt. Auch in dieser Tabelle können Seitendeskriptoren und Seitentabellendeskriptoren gemischt vorkommen. Es entsteht eine baumartige Anordnung von Seitentabellen, mit großen Seiten an der Wurzel und der kleinsten unterstützten Seiten-
4.4
Seitenbasierter virtueller Adreßraum
große an den Blättern. Die Blätter dieser Baumstruktur sind immer konventionelle Seitendeskriptoren, die auf den Seiteninhalt im physischen Speicher oder auf Hintergrundspeicher verweisen. Jeder Seitentabellendeskriptor enthält im einzelnen folgende Informationen:
• • • • •
Typfeld Adresse der Seitentabelle (Größe der Seitentabelle - Limit) Present-Bit (P-Bit) frei benutzbare Bits
Da bei einem mehrstufigen Verfahren jeder Tabelleneintrag sowohl ein Seitendeskriptor als auch ein Seitentabellendeskriptor sein kann, müssen die beiden durch ein zusätzliches Typ-Bit für die MMU unterscheidbar sein (entsprechend enthält auch der Seitendeskriptor zusätzlich ein Typfeld zur Unterscheidung). Primärer Inhalt des Deskriptors ist die Adresse der entsprechenden Seitentabelle. In manchen Systemen kann über ein zusätzliches Limit-Feld die Seitentabelle analog zu Segmenten in der Länge begrenzt werden. Außerdem verfügen Seitentabellendeskriptoren ebenfalls über ein Present-Bit (P-Bit), das angibt, ob die Seitentabelle ausgelagert ist. In diesem Fall muß vor einer weiteren Adreßabbildung die benötigte Tabelle vom Hintergrundspeicher geladen werden. Zusätzliche freie Bits können vom Betriebssystem zur Speicherung von Seitentabellen-bezogenen Zustandsinformationen herangezogen werden. Technisch wird bei einem mehrstufigen Verfahren der Seitenanteil der virtuellen Adresse in Abhängigkeit von der Stufenanzahl weiter unterteilt. Stehen für den Seitenanteil insgesamt p Bits zur Verfügung, so wird bei L Stufen eine Unterteilung pl p2, ..., pL gesucht mit p=p 1 +p 2 +...+p L . In der Regel wird mit der Adreßabbildung ab dem höchstwertigen Bit begonnen. In diesem Fall definieren die obersten p1-Bits der Adresse den Seitenanteil der ersten Stufe. Die nächsten P 2 Bits definieren den Seitenanteil zweiter Stufe. Analog wird mit den verbleibenden Abbildungsstufen verfahren. Bei jedem Schritt definieren die restlichen Bits, d.h die tieferen Seitenanteile einschließlich des Offsets, den Versatz relativ zu dieser Seite.
Bestandteile eines Seitentabellendeskriptors
Aufbau einer mehrstufigen virtuellen Adresse
Abb. 4-13 Aufbau einer virtuellen Adresse mit drei
Ein Beispiel für den Adreßaufbau bei einem dreistufigen Abbildungsverfahren ist in Abbildung 4-13 dargestellt. Der 32-Bit-Adreßraum wird bei der ersten Stufe in 2 =128 Seiten zu je 32 MByte unterteilt. Ein Seitendeskriptor auf dieser Stufe verweist auf eine 32 MByte
Abbildungsstufen
4
Adreßräume
(232-7) große Seite, die zusammenhängend im physischen Speicher des Rechners abgelegt sein muß. Ein Seitentabellendeskriptor auf dieser ersten Stufe verweist dagegen auf eine weitere Seitentabelle, die den 32 MByte großen Adreßbereich wiederum in 27 =128 Seiten zu je 256 KByte unterteilt. Ein Seitendeskriptor auf der zweiten Stufe beschreibt damit eine 256 KByte große Seite im physischen Adreßraum. Im Fall eines zweiten Seitentabellendeskriptors wird in einer Seitentabelle der dritten Stufe ein 256 KByte großer Bereich nochmals in 128 Seiten zu je 2 KByte unterteilt. Auf der dritten Ebene können sich nur noch Seitendeskriptoren für 2 KByte große Seiten befinden; für die Adressierung einer einzelnen Speicherzelle innerhalb einer solchen Seite wird ein 11 Bit breites Offset benötigt. Abb. 4-14 Mehrstufige Seitentabelle
Mehrstufige Adreßabbildung
Geringe Größe
Der Vorgang der Adreßabbildung selbst verläuft sehr ähnlich zu einem einstufigen Verfahren, mit dem Unterschied weiterer Iterationen im Fall von Seitentabellendeskriptoren (siehe Abbildung 4-14). Bei Anlegen der Adresse bestimmt die MMU den entsprechenden Deskriptoreintrag über den Seitenanteil p1 in der aktuell gültigen Wurzeltabelle. Auch in diesem Fall verweist ein besonderes Register in der MMU auf den Anfang dieser Tabelle. Bei einem Nulldeskriptor wird der Abbildungsvorgang gestoppt, und der Prozessor beginnt mit einer entsprechenden Ausnahmebehandlung. Bei einem Seitendeskriptor (PD) wird in bekannter Weise verfahren: Befindet sich die Seite im Hauptspeicher (P-Bit) und ist der aktuelle Zugriff erlaubt, werden Basisadresse der Seite und Offset zur Ermittlung der effektiven Adresse konkatentiert. Im Fall eines Seitentabellendeskriptors (PTD) wird aus der angegebenen Tabelle der benötigte Deskriptor mittels des nächsten Seitenanteils p2 bestimmt. Je nach MMU können maximal bis zu vier Abbildungsstufen über Seitentabellendeskriptoren durchgeführt werden, bevor das Verfahren mit einem Null- oder Seitendeskriptor endet. Der wesentliche Vorteil mehrstufiger Verfahren liegt in der enormen Speicherersparnis bei der Darstellung typischer Adreßräume. Wenn größere Bereiche innerhalb des Adreßraums nicht genutzt werden, können Nulldeskriptoren nahe der Wurzel des Seitentabellen-
4.4
Seitenbasierter virtueller Adreßraum
baums große Teilbäume frühzeitig abschneiden; der entsprechende Speicherplatz wird gespart. Gerade die bei vielen Anwendungen charakteristische Nutzung von Anfang und Ende des Adreßraums mit der entsprechend großen Lücke zwischen Heap und Laufzeitkeller kann platzgünstig abgebildet werden. Wenn in einem Adreßraum z.B. nur das erste und das letzte Byte genutzt werden, benötigt eine wie oben beschriebene dreistufige Adreßabbildung über alle drei Stufen 5 KByte zur Speicherung der gesamten Seitentabelle. Dabei werden Seitentabellen- und Seitendeskriptoren mit einer Größe von 8 Byte veranschlagt. Bei 128 Einträgen pro Tabelle ergibt das eine Größe von 1 KByte für jede Seitentabelle; es werden eine Tabelle auf der ersten, zwei Tabellen auf der zweiten und weitere zwei Tabellen auf der dritten Stufe benötigt. Bei einer Seitengröße von 2 KByte wäre eine einstufige Seitentabelle 16 MByte groß. Translation Lookaside Buffer (TLB) Es ist offensichtlich, daß gerade mehrstufige Verfahren bei jedem Abbildungsvorgang eine größere Menge an Zusatzinformationen in Form von Deskriptoren aus dem Hauptspeicher laden müssen. Dadurch kann ein Zugriff auf eine Speicherzelle bei einer Deskriptorlänge von 8 Byte und L Abbildungsstufen um maximal 8L-Ladeoperationen verzögert werden, vorausgesetzt die benötigten Seitentabellen und die referenzierte Seite befinden sich im Hauptspeicher. Um den Vorgang der Adreßabbildung wesentlich zu beschleunigen, verwenden daher alle seitenbasierten MMUs einen besonderen Cache, den sogenannten Translation Lookaside Buffer TLB (siehe Abbildung 4-15). Der TLB speichert für eine bestimmte Menge an virtuellen Seiten die zugehörige Kachel im physischen Adreßraum. Bei jedem Zugriff auf eine virtuelle Adresse wird überprüft, ob sich die zugehörige virtuelle Seite und damit die Basisadresse der Seite im TLB befindet. Diese Suche geschieht assoziativ, d.h, alle Einträge werden gleichzeitig auf Übereinstimmung mit dem Seitenanteil der referenzierten Adresse verglichen. Wird der Eintrag gefunden, spricht man von einem TLB-Hit. In diesem Fall kann die Seitenadresse unter Umgehung der gesamten Adreßabbildung aus dem TLB gelesen und mit dem Offset verknüpft werden. Ist kein entsprechender Eintrag im TLB enthalten (TLBMiss), muß die Adreßabbildung einschließlich dem Laden aller Deskriptoren durchgeführt werden, um zur effektiven Speicheradresse zu gelangen. Aufgrund der Referenzlokalität vieler Programme ist es sinnvoll, diese Adreßabbildung daraufhin in den TLB zu übernehmen und dafür z.B. den am längsten nicht verwendeten Eintrag zu löschen.
Zeitverzögerungen durch die virtuelle Adreßabbildung
TLB speichert das Ergebnis virtueller Adreßabbildungen
TLB-Hit
TLB-Miss
4
Adreßräume
Abb. 4-15 TLB
Abschätzung der Trefferrate
Adreßraumwechsel
TLB-Temperatur
Die Trefferrate des TLB wächst mit der Anzahl der Einträge. In Abhängigkeit von der erzielten Trefferrate kann die mittlere Verzögerung ermittelt werden, die ein Speicherzugriff durch die Adreßabbildung erfährt. Für eine einstufige Seitentabelle gilt:
Dabei ist H die Trefferrate des TLB, tTLB der Zeitaufwand für die assoziative Suche im TLB und tPD der Zugriff auf den Seitendeskriptor. Bei modernen Prozessoren liegt die Trefferrate H aufgrund der Referenzlokalität im Bereich von 90% bis 98%. Nimmt man für tTLB einen Wert von 20 ns an und für tPD 100 ns (für das Laden von 2-32 Bit) ergibt sich für H=90% eine Verzögerung von 28 ns und für H=98% eine Verzögerung von 21.6 ns. Adreßraumwechsel sind für den TLB wie für jeden anderen Cache innerhalb des Rechners besonders kritisch. Ausgelöst wird ein solcher Wechsel durch eine neue Wurzelseitentabelle, deren Adresse in das entsprechende Register der MMU geladen wird. Da jeder TLB-Eintrag nur für den aktuellen virtuellen Adreßraum gültig ist, werden diese durch den Adreßraumwechsel invalidiert. Manche MMUs erkennen den Wechsel selbständig und löschen die TLB-Einträge automatisch. Bei anderen MMUs muß die Systemsoftware durch besondere Befehle die TLB-Einträge manuell zurücksetzen. Die Programmausführung beginnt nach dem Wechsel immer mit einem kalten TLB, dessen Einträge zuerst wieder vom Hauptspeicher nachgeladen werden müssen. Einzelne MMUs erlauben sogar das Sichern des TLB im Hauptspeicher und das Restaurieren des TLBInhalts beim Adreßraumwechsel, um möglichst schnell wieder zu einem heißen TLB zu gelangen. Eine zweite Möglichkeit besteht in der längerfristigen Reservierung von einzelnen Einträgen für bestimmte
4.4
Seitenbasierter virtueller Adreßraum
Adreßräume. Zu diesem Zweck werden virtuelle Adreßräume und TLB-Einträge mit einer Identifikation versehen. Bei jedem AdreßraumWechsel wird die Kennung des ab jetzt gültigen Adreßraums in einem Register der MMU gespeichert. Die assoziative Suche findet in diesem Fall auf der Basis des virtuellen Seitenanteils und der zwischengespeicherten Kennung statt, d.h, nur Einträge für den aktuell gültigen Adreßraum werden ausgewertet. Dieses zweite Verfahren ist besonders interessant, wenn ein TLB aufgrund einer besseren Ausnutzung der Chip-Fläche vom Hersteller vergrößert werden kann. Seitenbasierte Adressierung beim SPARC-Prozessor Die mehrstufige virtuelle Adreßabbildung soll am Beispiel der SPARCMMU noch einmal kurz verdeutlicht werden (siehe auch [Spare Int. 1992]). Das dreistufige Verfahren wandelt eine 32 Bit große virtuelle Adresse in eine 36 Bit große physische Adresse um (siehe Abbildung 4-16). Die kleinste unterstützte Seitengröße beträgt 4 KByte (12 Bit Offset). Abb. 4-16 Dreistufige Abbildung der SPARC-MMU
Seitentabellen- und Seitendeskriptoren sind jeweils 4 Byte groß. Ein Deskriptor kann damit in einem Speicherzugriff geladen werden. Die Größenreduktion wurde im wesentlichen durch Einschränkungen bei den Plazierungsmöglichkeiten von Seitentabellen und Seiten erreicht. Da der physische Adreßraum in Seiten zu 4 KByte unterteilt wird, reichen der MMU 24 Bit für die Angabe der Kachel mit dem Seiteninhalt. Die verbleibenden 12 Bit dienen u.a. der Speicherung der Zugriffsrechte und der verschiedenen Statusbits. Seitentabellen sind bei der SPARC-MMU in der ersten Stufe 1024 Bytes, ansonsten 256 Bytes groß; sie müssen an einer 256-Byte-Grenze beginnen. Der Zeiger auf die Seitentabelle ist in diesem Fall 30 Bit lang. Zwei Bits dienen bei beiden Deskriptoren zur Speicherung des Typs (ET = Entry Type). Ein Wert ET=01 identifiziert einen Seitendeskriptor, ET=10 einen Seitentabellendeskriptor. ET=00 ist für den Nulldeskriptor, d.h für nicht angelegten virtuellen Adreßraum, reserviert.
Seitengröße 4 KByte
4
Kontextinformation
Adreßräume
Der Aufbau der SPARC-MMU ist in Abbildung 4-17 schematisch dargestellt. Die 32 Bit große virtuelle Adresse VA wird von der MMU im Virtual Address Latcb zwischengespeichert. Im TLB kann - abhängig von der MMU-Version - eine bestimmte Anzahl an Seitendeskriptoren PD; zusammen mit der virtuellen Seiten- und Kontextinformation gespeichert werden. Die Kontextinformation entspricht dabei der erwähnten Kennzeichnung von Adreßräumen. Die assoziative Suche wird mit dem Seitenanteil vp der virtuellen Adresse und dem Inhalt des Context-Registers, das den aktuell gültigen Adreßraum identifiziert, durchgeführt. Bei einem Treffer wird aus der im Seitendeskriptor gespeicherten Kacheladresse und dem Offset innerhalb der Seite die effektive Adresse RA bestimmt. Bei einem TLB-Miss müssen die notwendigen Deskriptoren nachgeladen werden. Dabei zeigt das Context Table Pointer Register (CTPR) immer auf die aktuell gültige Wurzeltabelle.
Abb. 4-17 Schematischer Aufbau der SPARC-MMU
Fehlerregister der SPARC-MMU
Der Prozessor hat im privilegierten Modus lesenden und schreibenden Zugriff auf alle Steuerregister und alle TLB-Einträge. Neben dem CR und dem CTPR dienen das Fault Status- und das Fault AddressRegister zur genaueren Beschreibung von Fehlersituationen bei der Adreßabbildung. Das Fault Address-Register gibt immer die virtuelle Adresse an, bei der im Verlauf der Adreßabbildung ein Fehler entstanden ist. Im Statusregister wird der Fehlertyp vermerkt, d.h., ob z.B. eine Zugriffsverletzung aufgetreten ist, ein Nulldeskriptor eingetragen oder die benötigte Seite bzw. Seitentabelle ausgelagert war. Über das Control-Register kann der Prozessor Typ und Version der MMU erfragen, verschiedene Speicher- und Fehlerbehandlungsformen wählen und die gesamte virtuelle Adreßabbildung ein- oder ausschalten. Wie bei allen MMUs ist die virtuelle Adreßabbildung
4.4
Seitenbasierter virtueller Adreßraum
nach dem Einschalten des Rechners nicht aktiv. Die Systemsoftware muß während einer Initialisierungsphase virtuelle Adreßräume durch Anlegen der entsprechenden Seitentabellen erst einmal aufbauen. Danach kann durch einen privilegierten Befehl das entsprechende Bit im Control-Register gesetzt werden, um den virtuellen Adressierungsmechanismus zu aktivieren. Die TLB-Einträge müssen bei der SPARC-MMU explizit gelöscht werden. Dafür stellt die MMU einen sogenannten Flush-Befehl zur Verfügung. Über einen zusätzlichen Parameter kann die Reichweite dieses Befehls gesteuert werden. Neben dem Zurücksetzen aller TLBEinträge können alle Einträge eines bestimmten Kontextes oder selektiv einzelne Einträge auf einer bestimmten Abbildungsstufe gelöscht werden. Im letzten Fall muß dann zusätzlich die virtuelle Seitenadresse des zu löschenden Eintrags angegeben werden. Außerdem können TLB-Einträge über sogenannte Probe-Befehle prozessorgesteuert geladen werden.
Flush- und Probe-Befehle
Kombination Segment- und Seitenadressierung: 80386-Prozessor Segment- und seitenbasierte virtuelle Adressierungstechniken können auch kombiniert werden. Es gibt im wesentlichen zwei Kombinationsmöglichkeiten: •
•
Jedes Segment definiert einen eigenen seitenbasierten Adreßraum. In diesem Fall enthält jeder gültige Segmentdeskriptor einen Zeiger auf die zugehörige Seitentabelle erster Stufe. Alle Segmente werden in einem virtuellen Adreßraum verankert. Aus der im Segmentdeskriptor enthaltenen Basisadresse wird zusammen mit dem Offset innerhalb des Segments eine lineare virtuelle Adresse gebildet, die anschließend über ein ein- oder mehrstufiges seitenbasiertes Abbildungsverfahren in eine effektive Adresse umgewandelt wird.
Der 80386-Prozessor realisiert z.B. die zweite Variante. Alle Segmente der GDT und der LDT werden in einem 32 Bit großen virtuellen Adreßraum plaziert. Die in einem Deskriptor gespeicherte Basisadresse gilt innerhalb dieses 32-Bit-Adreßraums. Beim Zugriff auf eine Speicherzelle wird - wie bereits angesprochen - implizit über den im zugehörigen Segmentregister gespeicherten Selektor der benötigte Segmentdeskriptor aus einer der beiden Tabellen bestimmt. Die im Selektor gespeicherte Basisadresse und das Offset innerhalb des Segments werden anschließend addiert. Bei der rein segmentbasierten Adressierung endet hier der Abbildungsvorgang; das Additionsergebnis entspricht der effektiven Adresse im physischen Adreßraum.
80x86-Prozessoren bieten seitenbasierte virtuelle Adressierung innerhalb der Segmente
4
Adreßräume
Bei der Kombination der segment- und seitenbasierten Adressierung bildet das Additionsergebnis die virtuelle Ausgangsadresse für die seitenbasierte Abbildungsphase. Die MMU des 80386 unterstützt ein zweistufiges Verfahren. Der Seitenanteil jeder Stufe beträgt 10 Bit, d.h, jede Seitentabelle besitzt 1024 Einträge. Die kleinste unterstützte Seitengröße sind 4 KByte mit einem Offset von 12 Bit. Zusammenfassung
Vorteile
Nachteil
Auf der Grundlage seitenbasierter virtueller Adressierungstechniken können alle in Abschnitt 4.1 aufgestellten Forderungen erfüllt werden. Durch entsprechend belegte Seitentabellen kann ein zusammenhängender virtueller Adreßraum unabhängig von der Struktur und Zerstückelung des physischen Speichers angelegt werden. Im Gegensatz zu rein segmentbasierten Verfahren gibt es keine externe Fragmentierung. Statt dessen müssen Speicheranforderungen von Anwendungsadreßräumen immer durch ganze Seiten/Kacheln befriedigt werden. Für die Systemsoftware, die den physischen Adreßraum verwaltet, entsteht dadurch ein vernachlässigbares internes Fragmentierungsproblem. Der Schutz innerhalb eines Adreßraums auf der Grundlage von Zugriffsrechten für jede einzelne Seite ist ebenfalls in den meisten Situationen ausreichend. Lediglich beim Schutz einzelner Bereiche eines Adreßraums vor Überläufen sind segmentbasierte Verfahren leistungsfähiger, da diese bei einer geeigneten Segmentplazierung vollständigen Schutz gewährleisten. In einem rein seitenbasierten Verfahren können die einzelnen Bereiche ohne zusätzliche Informationen von der Speicherverwaltung nicht innerhalb eines virtuellen Adreßraums unterschieden werden. Das Erkennen von Überläufen muß in diesem Fall durch übergeordnete Mechanismen realisiert werden.
4.5
Dynamische Seitenersetzung
Die große Stärke seitenbasierter Verfahren kommt mit der dynamischen Aus- und Einlagerung von Seiten auf dem Hintergrundspeicher voll zum Tragen. Die Kernidee besteht darin, den Hauptspeicher als zusätzlichen (Level 3)-Cache für einen auf Hintergrundspeichern abgelegten virtuellen Speicher zu nutzen. Diese Auffassung entspricht einer Erweiterung der in Kapitel 2 eingeführten Speicherhierarchie. Konzeptionell werden Anwendungen auf dem großen aber verhältnismäßig langsamen virtuellen Speicher ausgeführt. Teile der Anwendungsadreßräume werden während der Ausführung lediglich im physischen Adreßraum zwischengespeichert. Technisch läßt sich die Cache-Verwaltung sehr einfach auf das in allen Deskriptoren vorhandene P-Bit zurückführen. Bei einem Treffer
4.5
Dynamische Seitenersetzung
befinden sich alle für die Adreßabbildung notwendigen Deskriptoren einschließlich des Seiteninhalts im Hauptspeicher. Der Prozessor kann in diesem Fall direkt auf den Inhalt einzelner Speicherzellen zugreifen. Stößt man im Verlauf der Adreßabbildung auf ein nicht gesetztes PBit, so handelt es sich um einen Cache Miss (siehe Abbildung 4-18). Der fehlende Teil muß in diesem Fall vom Hintergrundspeicher nachgeladen werden; unter Umständen müssen zu diesem Zweck die Inhalte anderer Kacheln wieder in den virtuellen Speicher zurückkopiert werden. Dem Prozessor wird die Notwendigkeit des Nachladens durch einen Seitenfehler-Interrupt signalisiert. Alle Informationen über den Fehlerursprung, ob z.B. eine Seitentabelle oder der Seiteninhalt nachgeladen werden muß, kann der Prozessor entweder aus entsprechenden Statusregistern der MMU erfahren (vgl. SPARC-MMU) oder - bei einer sehr engen Verzahnung von Prozessor und MMU - einem sogenannten Exception Frame auf dem privilegierten Laufzeitkeller entnehmen.
Seitenfehler-Interrupt
Abb. 4-18 Zugriff auf eine ausgelagerte Seite des Adreßraums (Seitenfehler)
Die Leistungsfähigkeit eines virtuellen Speichers hängt primär von der Wahrscheinlichkeit eines Seitenfehlers ab. Aufgrund der hohen Diskrepanz in der Zugriffzeit ist ein hohes Potential an Leistungssteigerung möglich, wenn durch eine entsprechend hohe Trefferrate die mittlere Zugriffszeit des virtuellen Speichers in der Nähe der Hauptspeicherzugriffszeit gehalten werden kann. Umgekehrt ist natürlich zu erwarten, daß gerade wegen der hohen Zugriffszeit auf den Hintergrundspeicher eine sehr hohe Trefferrate zur Kompensation notwendig ist. Die mittlere effektive Zugriffszeit tys auf den virtuellen Speicher kann wie folgt berechnet werden (dabei nehmen wir zur Vereinfachung nur ausgelagerte Seiten an, d.h, die Seitentabellen befinden sich alle im Hauptspeicher):
Dabei ist p die Seitenfehlerwahrscheinlichkeit, tHS die mittlere Zugriffszeit auf den Hauptspeicher im Fall eines Treffers und tSF die mitt-
Wahrscheinlichkeit eines Seitenfehlers
4
Adreßräume
lere Verzögerung für die Behandlung eines Seitenfehlers. Zur Bestimmung der mittleren Verzögerung im Fall eines Seitenfehlers ist es hilfreich, die einzelnen Phasen zu bestimmen (es wird davon ausgegangen, daß keine freie Seite mehr vorhanden ist): 1. Ausführung der Unterbrechnungsroutine (tUbr) 2. Auswahl und Auslagerung einer zu verdrängenden Seite (tAus) 3. Einlagern der referenzierten Seite vom Hintergrundspeicher (tEin 4. Aktualisierung der betroffenen Seitendeskriptoren (tAkt) 5. Wiederholung der unterbrochenen Instruktion (tWdh) Insgesamt ergibt sich damit für tSF:
Die dominanten Anteile sind das Aus- und Einlagern einer Seite, d.h
Da die Zeit für die Kommunikation mit dem Hintergrundspeicher, die im Bereich mehrerer Millisekunden liegt, auch gegenüber der Zugriffszeit auf den Hauptspeicher dominiert, ergibt sich für die effektive Zugriffszeit auf den virtuellen Speicher in Abhängigkeit von der Seitenfehlerwahrscheinlichkeit:
Bei einer mittleren Zugriffszeit auf den Hauptspeicher tHS=70 ns und einer mittleren Ein- und Auslagerungszeit von t A u s / E i n = 10 ms für eine Seite zwischen Haupt- und Hintergrundspeicher ergibt sich für die Seitenfehlerwahrscheinlichkeit:
Seitenfehler höchstens alle 2-3 Millionen Speicherzugriffe
Dabei definiert der Schwellwert k, welche prozentuale Verzögerung bei der effektiven Zugriffszeit auf den virtuellen Speicher im Vergleich zum mittleren Hauptspeicherzugriff noch tolerierbar sein soll. Ein Wert k=l.l bedeutet z.B. eine maximal um 10% über tHS liegende effektive Zugriffszeit auf den virtuellen Speicher. Die Einhaltung dieser Maximalverzögerung erfordert eine Seitenfehlerwahrscheinlichkeit p < 3.5e-7, d.h, höchstens alle 2,8 Millionen Speicherzugriffe darf ein Seitenfehler eintreten. Der extrem geringe Grenzwert für eine akzeptable Seitenfehlerrate ist eine unmittelbare Folge der im Millisekundenbereich liegenden Transferzeiten beim Zugriff auf den Hintergrundspeicher. Aus diesem Grund kann die Gesamtleistung des virtuellen Speichers durch eine Minimierung dieser Zeiten substantiell
4.5
Dynamische Seitenersetzung
verbessert werden. Ein wichtiger Aspekt ist dabei, Seitenauslagerungen soweit wie möglich einzusparen. Existiert von einer Seite, die als Auslagerungskandidat ausgewählt wurde, bereits eine Kopie auf dem Hintergrundspeicher und wurde die zugehörige Kachel im Hauptspeicher nicht verändert (Dirty-Bit des Seitendeskriptors nicht gesetzt), so kann auf die Auslagerung ganz verzichtet werden. Außerdem ist es sinnvoll, bei unterschiedlich schnellen Hintergrundspeichern die schnellste Platte zur Realisierung des virtuellen Speichers einzusetzen. Bereits kleine Verbesserungen bei der Latenz- und Übertragungszeit der verwendeten Platte können meßbare Verbesserungen der Gesamtleistung zur Folge haben.
Zeit für die Behandlung von Seitenfehlern verkürzen
Working-Set-Modell
In der Praxis kann die Seitenfehlerrate trotz des geringen Grenzwertes meist in einem akzeptablen Bereich gehalten werden. Ein entscheidender Grund dafür ist die bereits angesprochene Referenzlokalität vieler Programme. Besonders ausgeprägt ist sie bei Code, der aus prozedural strukturierten Anwendungsprogrammen generiert wurde. Für die prozeduralen Programmiersprachen Pascal und C wurde z.B. empirisch ermittelt, daß Zuweisungen' und Bedingungen mehr als 80% der in Programmen verwendeten Hochsprachenkonstrukte ausmachen ([Huck 1983], [Patterson und Sequin 1982]). Entsprechende Programme weisen ausgeprägte Zugriffs- und sequentielle Ausführungsmuster auf, aus denen sich eine hohe Referenzlokalität ableiten läßt. Dieses Verhalten wird durch die mehrfache Ausführung von Instruktionen innerhalb von Schleifen weiter verstärkt. Die Auswirkungen der Referenzlokalität werden auf der Ebene von ganzen Seiten noch deutlicher. Man betrachtet zu diesem Zweck sogenannte Referenzstrings, die das Zugriffsverhalten eines Programmes auf den Adreßraum wiedergeben. Dazu wird für jeden Speicherzugriff die virtuelle Seite vermerkt, in der er stattfand. Ein Beispiel für einen solchen Referenzstring ist: RS = 0 0 1 1 2 2 2 1 2 3 4 3 2 2 3 3 3 3 3 3 3 4 3 3 4 5 6 5 4 3 3 3 .... Von links nach rechts gelesen gibt der Referenzstring PS an der Position i (RS[i]) an, innerhalb welcher virtuellen Seite der i-te Speicherzugriff während der Programmausführung stattfand. Auf der Grundlage eines solchen Referenzstrings kann man nun für jeden Zeitpunkt t (dabei kann man die einzelnen Speicherzugriffe vereinfachend als einen diskreten Zeittakt auffassen) die letzten A Speicherzugriffe zurückverfolgen:
Referenzstring beschreibt Speicherzugriffsverhalten
4
Adreßräume
Diese Seitenmenge wird von Denning als Working-Set bezeichnet [Denning 1970]. Sie enthält bei einer geeigneten Wahl von A (ALS) praktisch über die gesamte Ausführungszeit des Programms eine im Vergleich zur Gesamtgröße des virtuellen Adreßraums geringe Seitenanzahl:
Lokalitätsmenge
Dieses durch ALS bestimmte Working-Set wird auch als Lokalitätsmenge bezeichnet. Anschaulich bedeutet dies, daß ein Programm über längere Zeiträume hinweg auf einer verhältnismäßig geringen Zahl von virtuellen Seiten arbeitet. Es ergibt sich eine minimale Seitenfehlerrate, wenn die Speicherverwaltung in der Lage ist, im physischen Adreßraum genügend Platz zur Speicherung der Lokalitätsmenge zu schaffen. Die Größe der Lokalitätsmenge ist dabei nicht konstant, d.h, aufgrund des Programmverhaltens werden zu bestimmten Zeitpunkten Seiten hinzugefügt oder entfernt. Für die Bestimmung eines Working-Sets ist die Wahl von A kritisch. Ist der Wert zu klein, d.h kleiner als die oben angesprochene Schranke A L S , wird die Lokalitätsmenge nicht voll erfaßt. Bei einem zu großen Wert besteht WS aus der Vereinigung mehrerer aufeinander abfolgender Lokalitätsmengen. Seitenverdrängungsverfahren
Welche Seiten sollen bei einem Seitenfehler verdrängt werden?
Seitenverdrängung und Lokalitätsmenge
Bei einem aufgetretenen Seitenfehler muß die neu referenzierte Seite vor der weiteren Befehlsausführung vom Hintergrundspeicher in eine Kachel des physischen Adreßraums kopiert werden. Wenn keine freie Kachel unmittelbar zur Verfügung steht, muß durch ein Seitenverdrängungsverfahren der benötigte Platz im physischen Adreßraum vorher geschaffen werden. In diesem Fall wird eine bereits belegte Kachel ausgewählt und die enthaltene Seite eines virtuellen Adreßraums auf den Hintergrundspeicher ausgelagert. Im Idealfall baut ein Verdrängungsverfahren auf der Referenzlokalität vieler Programme und der sich ergebenden Lokalitätsmenge auf. Solange sich die Lokalitätsmenge jedes Anwendungsprogramms im physischen Adreßraum des Rechners befindet, ist die Seitenfehlerrate sehr gering. Sie kann mit zusätzlich im Hautspeicher befindlichen Seiten nur geringfügig reduziert werden, da auf Seiten außerhalb der Lokalitätsmenge nur mit einer sehr geringen Wahrscheinlichkeit zugegriffen wird. Dagegen erhöht sich die Seitenfehlerrate überproportional, wenn die Lokalitätsmenge nicht vollständig im Hauptspeicher liegt. Auf der Grundlage der Working Set-Theorie ergibt sich damit für ein Seitenverdrängungsverfahren die Aufgabe, bevorzugt solche Seiten auszulagern, die nicht in der Lokalitätsmenge enthalten sind. Veränderungen an der Lokalitätsmenge sind in der Praxis jedoch schwer zu er-
4.5
Dynamische Seitenersetzung
mitteln, da jeder schreibende und lesende Zugriff auf eine virtuelle Adresse die Lokalitätsmenge beeinflussen kann und dementsprechend berücksichtigt werden muß. Aus diesem Grund stehen in der Praxis Verdrängungsverfahren im Vordergrund, die ohne Kenntnis der jeweiligen Lokalitätsmenge Auslagerungskandidaten im Hinblick auf eine minimale Seitenfehlerrate auswählen. Die Leistungsfähigkeit von Verdrängungsverfahren kann auf der Grundlage der bekannten Referenzstrings bestimmt werden. Bewertungsgrundlage ist die Gesamtanzahl an Seitenfehlern, die eine Verdrängungsstrategie bei der Abbildung des Referenzstrings auf eine konstante Kachelanzahl verursacht. Für alle Verfahren bildet das optimale Verdrängungsverfahren nach Belady [Belady 1966] eine Vergleichsbasis. Bei diesem Verdrängungsverfahren wird bei einem Seitenfehler die Seite verdrängt, die in der Zukunft am längsten nicht referenziert wird. Aus einsichtigen Gründen kann mit diesem Verfahren die Anzahl an Seitenfehlern minimiert werden. Es ist jedoch auch offensichtlich, daß dieses Verfahren in der Praxis nicht realisierbar ist; das notwendige Wissen über das zukünftige Programmverhalten liegt normalerweise nicht vor.
Optimale Verdrängung nach Belady
Abb. 4-19 Optimale Verdrängung
Das Verhalten dieses Verfahrens ist in Abbildung 4-19 graphisch dargestellt. Im oberen Teil der Abbildung befindet sich der Referenzstring in leicht abgewandelter Form. Dabei wurden Teilfolgen gleicher Seitenreferenzen jeweils zu einem Wert zusammengefaßt, da nur das erste Auftauchen einer neuen referenzierten Seite einen Seitenfehler auslösen kann. Unmittelbar nachfolgende Referenzen auf dieselbe Seite führen zu keinen weiteren Seitenfehlern, da sich die Seite im Hauptspeicher befindet. Unter dem Referenzstring ist jeweils von links nach rechts protokolliert, welche virtuellen Seiten sich bei einer konstanten Anzahl von 3 zur Verfügung stehenden Kacheln jeweils im Hauptspeicher befinden. Angegeben ist die Kachelbelegung nur bei Änderungen aufgrund von Seitenfehlern. Geht man davon aus, daß sich zum Zeitpunkt des Programmstarts keine einzige virtuelle Seite im Hauptspeicher befindet, geben die dargestellten Kachelbelegungen die Anzahl aufgetretener Seitenfehler wieder. Im Fall der optimalen Verdrängung nach Belady (siehe Abbildung 4-19) werden zuerst die virtuellen Seiten 7, 0 und 1 eingelagert. Beim
4
FIFO-Verdrängung
LRU-Verdrängung
Adreßräume
vierten Seitenfehler als Folge eines Zugriffs auf die virtuelle Seite 2 muß erstmals eine Seite verdrängt werden. Anschaulich wählt das Verfahren diejenige der eingelagerten Seiten aus, die im Referenzstring am längsten nicht mehr referenziert wird (in diesem Fall Seite 7). Analog wird mit jedem weiteren Seitenfehler verfahren. Ein einfaches Verdrängungsverfahren basiert auf dem FIFO-Prinzip (First-In-First-Out), d.h., es wird die Seite verdrängt, die sich am längsten im Hauptspeicher befindet. Dieses Verfahren ist nicht besonders leistungsfähig, da es die Lokalitätsmenge eines Programms nicht berücksichtigt. So werden über längere Zeiten hinweg häufig referenzierte Seiten, sogenannte Hot Spots, von diesem Verfahren verdrängt. Umgekehrt bleiben selten benutzte Seiten im Mittel genauso lange im Hauptspeicher wie häufig referenzierte Seiten. Zusätzlich weist das FIFO-Verfahren ein anomales Verhalten auf (Beladys Anomalie [Belady et al. 1969]), das bei wachsender Anzahl an verfügbaren Kacheln eine steigende Seitenfehlerrate zur Folge haben kann. Das Least-Recently-Used-Verfakren (LRU) kommt der optimalen Verdrängung nach Belady am nächsten. Dabei wird die Seite aus dem Hauptspeicher verdrängt, die in der Vergangenheit am längsten nicht benutzt wurde (siehe Abbildung 4-20). Das Verfahren garantiert bei Referenzlokalität eine nahe dem Optimum liegende Seitenfehlerrate, da die Wahrscheinlichkeit für eine Änderung der gegenwärtigen Lokalitätsmenge sehr klein ist.
Abb. 4-20 LRU-Verfahren
LRU-Verfahren praktisch nicht implementierbar
Das LRU-Verfahren ist leider aufwendig zu implementieren. Man muß sich im Prinzip für jede Seite den Zeitpunkt des letzten Speicherzugriffs in Form eines Zeitstempels merken. Im Verdrängungsfall wird die Seite mit dem ältesten Zeitstempel ausgelagert. Durch eine doppelt verkettete Liste, die alle Seiten nach aufsteigendem Zeitstempel sortiert, kann die Wahl des Verdrängungskandidaten in konstanter Zeit durchgeführt werden. Problematisch ist dabei, daß bei jedem schreibenden und lesenden Speicherzugriff - insbesondere auch bei der Mehrheit der Zugriffe ohne Seitenfehler - der Zeitstempel der referenzierten Seite und damit auch die Seitenverkettung aktualisiert werden müssen. Aufgrund dieses Aufwands hat das LRU-Verfahren keine große praktische Relevanz.
4.5
Dynamische Seitenersetzung
In den heute verbreiteten Betriebssystemen kommen statt dessen Näherungsverfahren zum Einsatz. Im wesentlichen handelt es sich um Verfahren, die auf eine Hardwareunterstützung z.B. in Form des RBits in einem Seitendeskriptor angewiesen sind. Im einfachsten Fall wird jeder Seite ein Zähler zugeordnet. In periodischen Abständen inkrementiert die Speicherverwaltung den Zähler jeder im letzten Intervall nicht referenzierten Seite (R-Bit nicht gesetzt). Für jede referenzierte Seite werden dagegen Zähler und R-Bit gelöscht. Verdrängt wird immer die Seite mit dem höchsten Zählerstand, da diese in der Vergangenheit am längsten nicht referenziert wurde. Ein anderes Verfahren nutzt das Referenced-Bit zur näherungsweisen Bestimmung des zurückliegenden Referenzstrings (siehe Abbildung 4-21) [Tanenbaum 1992]. Das Verfahren benötigt neben der Seitentabelle eine Zusatztabelle. In dieser Tabelle wird der zurückliegende Referenzstring in einem Ausschnitt ermittelt, dabei beschreibt jeder Eintrag den jeweiligen Referenzstring für eine bestimmte virtuelle Seite. Das Verfahren aktualisert den Referenzstring periodisch (z.B. alle 100 ms). Für jede Seite wird dazu der entsprechende Eintrag in der Zusatztabelle um eine Bitposition nach rechts verschoben und der aktuelle Wert des R-Bits im zugehörigen Seitendeskriptor links angehängt. Das R-Bit selbst wird daraufhin zurückgesetzt. Damit speichert jeder Eintrag, in welchen zurückliegenden k Intervallen eine Seite referenziert wurde. Faßt man nun im Verdrängungsfall jeden Eintrag als vorzeichenlose ganze Zahl auf, so wurde der Eintrag mit dem kleinsten Wert am längsten in der Vergangenheit nicht referenziert. Die zugehörige Seite wird in diesem Fall verdrängt.
LRU-Approximation
Variante
Abb. 4-21 Näherungsverfahren für LRU
Das ebenfalls verbreitete 2nd-Chance-Verfahren orientiert sich grundsätzlich am FIFO-Verfahren, verbessert dies aber durch Berücksichtigung von Seitenreferenzen durch eine Inspektion des R-Bits. Muß eine Seite verdrängt werden, so wird die Liste der im Hauptspeicher anwe-
2nd-Chance-Verfahren
4 Adreßräume
Clock-Algorithmus
senden Seiten in Richtung abnehmender Einlagerungsdauer so lange durchsucht, bis eine Seite mit R-Bit=0 gefunden wurde. Diese Seite wird dann verdrängt. Bei zwischenzeitlich referenzierten Seiten (RBit=l) wird lediglich das R-Bit zurückgesetzt, d.h., sie erhalten eine zweite Chance. Werden alle Seiten innerhalb eines FIFO-Durchlaufs erneut referenziert, so wird automatisch die Seite verdrängt, die zuletzt ein zweites Mal in die Schlange eingereiht wurde. Das Verfahren degeneriert in diesem Fall zum ursprünglichen FIFO-Verfahren. Die im UNIX-Bereich verbreiteten Clock-Algorithmen sind Implementierungsvarianten des 2nd-Chance-Verfahrens. Grund für diese Verfeinerungen ist die verhältnismäßig aufwendige Aktualisierung der FIFO-Schlange. Bei den Clock-Algorithmen wird die Schlange durch eine zirkuläre Liste ersetzt. Ein »Uhrzeiger« zeigt immer auf die gemäß FIFO-Ordnung älteste Seite. Im Verdrängungsfall wird der Uhrzeiger so lange um ein Element in der zirkulären Liste weitergeschaltet, bis eine Seite mit zurückgesetztem R-Bit gefunden ist. Seiten mit gesetztem R-Bit bekommen eine nächste Chance, d.h., ihr R-Bit wird wieder zurückgesetzt und der Uhrzeiger auf das nächste Element umgeschaltet. In der Praxis hat sich jedoch gezeigt, daß die Suche nach dem Verdrängungskandidaten bei einer großen Anzahl von Hauptspeicherkacheln zu langwierig ist.
Abb. 4-22 Clock-Algorithmus mit zwei Zeigern
Variante des Clock-Algorithmus
In einer weiteren Variante des Clock-Algorithmus werden aus diesem Grund zwei Zeiger verwendet, die einen konstanten Abstand innerhalb der zirkulären Liste besitzen (siehe Abbildung 4-22). Die Vergabe der zweiten Chance und die Auswahl des Verdrängungskandidaten wird auf die beiden Zeiger verteilt. Der »vordere« Zeiger setzt das R-Bit im Seitendeskriptor zurück. Der hintere Zeiger prüft das R-Bit. Bei zurückgesetztem Bit kann die Seite verdrängt werden. Bei gesetztem Bit werden beide Zeiger gleichzeitig um ein Element weitergeschaltet. Der Abstand zwischen den beiden Zeigern bestimmt, wie viele Schritte maximal durchgeführt werden müssen, um eine verdrängbare Seite zu finden. Bei geringem Abstand bleiben nur die am häufigsten referenzierten Seiten im Hauptspeicher; in dieser Form
4.5
Dynamische Seitenersetzung
wird der Algorithmus z.B. in BSD Unix [McKusik et al. 1996] eingesetzt. Bei maximalen Zeigerabstand entspricht diese Variante dem ursprünglichen Clock-Algorithmus. Seitennachschubverfahren Seitennach schubverfahren kopieren ein oder mehrere auf dem Hintergrundspeicher befindliche virtuelle Seiten in den Hauptspeicher des Rechners. Man kann grundsätzlich zwei verschiedene Nachschubverfahren unterscheiden: Demand-Paging und Pre-Paging. Beim Demand-Paging wird eine Seite nur aufgrund eines vorangehenden Seitenfehlers nachgeladen. Bei reinem Demand-Paging beginnt die Anwendungsausführung mit einem angelegten aber leeren Adreßraum. Ein Anwendungsprogramm baut sich in diesem Fall seine Lokalitätsmenge mit Beginn der Ausführung schrittweise auf. Charakteristisch für diese Vorgehensweise ist eine verhältnismäßig hohe Seitenfehlerrate während der Anlaufphase, die nach vollendeter Einlagerung des Working-Set auf ein Minimum zurückfällt. Beim PrePaging werden dagegen virtuelle Seiten bereits in den Hauptspeicher kopiert, ohne daß ein entsprechender Seitenfehler vorangegangen sein muß. Die Programmausführung beginnt in diesem Fall mit einem bereits teilweise gefüllten Adreßraum. Im Extremfall kann sogar der gesamte von der Anwendung genutzte Adreßraum eingelagert werden; in der Regel empfiehlt sich diese Vorgehensweise jedoch nicht, da Einlagerungen über die Lokalitätsmenge hinaus den Programmstart unnötig verzögern. In der Praxis wird häufig eine Kombination aus beiden Verfahren angewendet. Dabei wird mittels Pre-Paging der Anfang des Programmcodes und der Bereich der statischen Daten vom Hintergrundspeicher kopiert. Zusätzlich werden ein oder mehrere Seiten jeweils für Heap und Laufzeitkeller im Hauptspeicher reserviert. Dadurch wird eine erhöhte Seitenfehlerrate zu Beginn der Programmausführung vermieden. Weitere Einlagerungen finden dann gemäß DemandPaging als Reaktion auf Seitenfehler statt. Da Hintergrundspeicher beim sequentiellen Lesen mehrerer aufeinanderfolgender Blöcke besonders effizient arbeiten, werden vom Betriebssystem durch PrePaging häufig neben der gewünschten Seite auch weitere virtuelle Seiten vorab geladen. In welchem Umfang dies geschieht, hängt letztendlich von der Wahrscheinlichkeit ab, ob diese in naher Zukunft referenziert werden oder nicht. Da zusätzlich viele technische Details wie z.B. günstige Übertragungsmengen zwischen Platte und Hauptspeicher ebenfalls eine Rolle spielen, wird ein geeigneter Pre-Paging-Anteil in existierenden Systemen empirisch festgelegt.
Demand-Paging
Pre-Paging
Kombinierte Verfahren
4
Adreßräume
Kachelzuteilung
Unterschiedliche Anforderungen
Lokale Kachelzuteilung
Eine wichtige Aufgabe der Speicherverwaltung ist die Zuordnung von freien Kacheln im Hauptspeicher zu Anwendungsadreßräumen. Wird zu einem Zeitpunkt nur ein Adreßraum unterstützt, gestaltet sich diese Aufgabe recht einfach: Nach Abzug der von der Systemsoftware benötigten Speicherbereiche stehen alle verbleibenden Kacheln für den Anwendungsadreßraum zur Verfügung. Bei mehreren gleichzeitig unterstützten Adreßräumen müssen die vorhandenen Ressourcen dagegen geeignet auf alle verteilt werden. Die Speicherverwaltung muß also entscheiden, wie viele Kacheln sie für jeden einzelnen Anwendungsadreßraum reserviert. Dabei muß sie berücksichtigen, daß Anwendungen sehr unterschiedliche Speicheranforderungen stellen. Der Bedarf reicht von wenigen KByte für einfache Programme bis hin zu mehreren 100 MByte für Anwendungen aus den Bereichen Bildverarbeitung oder KI. Außerdem können jederzeit dynamisch neue Anwendungen und damit auch neue Adreßräume erzeugt werden. Damit diese neuen Adreßräume möglichst schnell aufgebaut werden können, sollte die Speicherverwaltung über einen ausreichenden Vorrat an freien Kacheln verfügen. Grundsätzlich kann zwischen einer lokalen und globalen Kachelzuteilung unterschieden werden. Eine lokale Kachelzuteilung basiert auf dem Working-Set-Modell. In diesem Fall werden für einen Adreßraum mindestens so viele Kacheln reserviert, daß die Lokalitätsmenge der zugehörigen Anwendung vollständig im Hauptspeicher gehalten werden kann. Die resultierende Seitenfehlerrate jeder Anwendung ist dann minimal. Tritt während der Programmausführung ein Seitenfehler auf, wird ebenfalls eine lokale Verdrängungsstrategie verfolgt und bevorzugt eine Seite desselben virtuellen Adreßraums verdrängt.
Abb. 4-23 Seitenfehlerrate in Abhängigkeit von der Kachelanzahl
Globale Kachelzuteilung
In der Praxis scheitert die lokale Kachelzuteilung an der hohen Frequenz, mit der die pro Adreßraum erzeugte Seitenfehlerrate bestimmt werden muß. Statt dessen wird bei einer globalen Kachelzuteilung die Seitenfehlerrate des Gesamtsystems gemessen (siehe Abbildung 4-23). Das Überschreiten einer oberen Schranke für die Seitenfehlerrate deu-
4.5
Dynamische Seitenersetzung
tet darauf hin, daß die Summe der Lokalitätsmengen aller Anwendungen nicht mehr in den Hauptspeicher paßt. Als Folge davon stehlen sich Anwendungen ständig wechselseitig Kacheln. Dieser Effekt ist so drastisch, daß er weitreichende Auswirkungen auf die Leistung des Gesamtsystems haben kann. Bei diesem als Seitenflattern oder Thrashing bezeichneten Phänomen werden ständig Seiten im virtuellen Adreßraum der Anwendung zwischen Hintergrund- und Hauptspeicher bewegt. Dabei kann es zum Stillstand des Gesamtsystems kommen, da der Hintergrundspeicher durch das permanente Ein- und Auslagern von Seiten zum Flaschenhals wird und damit letztendlich jede Anwendung blockiert. Das System reagiert in diesem Fall mit dem Auslagern ganzer Adreßräume, um den Speicherengpaß zu beheben. Bei einer Zuteilung, die auf Seitenfehlerraten basiert, können die bezüglich der Kachelanzahl unterbesetzten Adreßräume nicht direkt ermittelt werden, da die Lokalitätsmengen der einzelnen Anwendungen unbekannt sind. Sie wird daher in Verbindung mit einer globalen Verdrängungsstrategie angewendet, bei der alle Kacheln als Verdrängungskandidaten zur Verfügung stehen. Wird bei diesem Verfahren außerdem eine untere Schranke für die Seitenfehlerrate unterschritten, kann zusätzlicher freier physischer Adreßraum geschaffen werden, um neue Aufträge zu beginnen oder aufgrund von Thrashing unterbrochene Aufträge fortzusetzen. Es ist offensichtlich, daß auch im Fall einer lokalen Zuteilungsstrategie auf ein globales Verdrängungsverfahren zurückgegriffen werden muß, wenn im Fall eines Seitenfehlers mit dem Auslagern einer Seite die Lokalitätsmenge unterschritten wird.
Seitenflattern oder Thrashing ist ein systemweiter Effekt
Globale Verdrängungsstrategie
Dämon-Paging Eine gängige Technik, immer über einen ausreichenden Vorrat an freien Kacheln für die schnelle Reaktion auf weitere Speicher- und Adreßraumanforderungen zu verfügen, besteht in der Trennung von Seitenverdrängung und Seiteneinlagerung. Bei diesem als Dämon-Paging [McKusik et al. 1996] bezeichneten Verfahren versucht die Speicherverwaltung immer, eine vorab festgelegte Anzahl an Kacheln freizuhalten. Aus diesem Vorrat können Seitenfehler ohne eine vorherige Seitenverdrängung bedient und neue Adreßräume unmittelbar angelegt werden. Gleichzeitig wird ein spezieller Prozeß periodisch aktiviert. Dieser Dämon-Pager prüft, ob die vorgegebene Anzahl an freien Kacheln unterschritten wurde. In diesem Fall werden so lange Seiten auf der Grundlage der verwendeten Seitenverdrängungsstrategie auf den Hintergrundspeicher ausgelagert, bis die Anzahl an freien Kacheln den Schwellwert wieder überschreitet. Dämon-Paging wird z.B. in BSD UNIX eingesetzt. In diesem Betriebssystem wird der Dämon-Pager alle 250 ms aktiviert. Der Prozeß
Trennung von Seitenverdrängung und Seitenersetzung
4
Adreßräume
führt eine globale Verdrängung auf der Grundlage der 2-Zeiger-Variante des Clock-Algorithmus durch, bis der Schwellwert l o t s f ree an freien Kacheln überschritten wird. Dieser Schwellwert kann bei der Konfiguration des Betriebssystems auf einen bestimmten Prozentsatz des physischen Speicherausbaus festgesetzt werden; in der Praxis liegt der Wert bei ca. 2 5 % .
4.6
Swapping bei Thrashing-Gefahr
Auch beim Einsatz virtueller Adreßraumtechniken wird in bestimmten kritischen Situationen Swapping eingesetzt, d.h., ganze Adreßräume werden zeitweilig auf Hintergrundspeicher verdrängt. Aufgrund des hohen Aufwands wird Swapping im wesentlichen zur Vermeidung von akuten Speicherengpässen und der damit einhergehenden Gefahr des Seitenflatterns benutzt. Swapping kann aber auch eintreten, wenn eine bestimmte Anwendung plötzlich große Mengen an zusätzlichem Speicherbedarf benötigt. Bei der Auslagerung eines Adreßraums werden die im Hauptspeicher befindlichen virtuellen Seiten und in der Regel auch die zugehörigen Seitentabellen ausgelagert. Bei Systemen mit einem Dirty-Bit in den Seitendeskriptoren brauchen nur die veränderten Seiten zurückgeschrieben werden. Eine faire Strategie, welcher Adreßraum bei akutem Speicherplatzmangel ausgelagert wird, kann primär nur aufgrund der Informationen in der Prozeß Verwaltung getroffen werden. Wie bereits in Abschnitt 4.2 erläutert wurde, sollte die Auslagerung bevorzugt bei einer im I/O-Burst befindlichen Anwendung geschehen (siehe hierzu auch Kapitel 5). Aus Sicht der Speicherverwaltung sollte diese Entscheidung primär von der Adreßraumgröße bestimmt werden. Am sinnvollsten erscheint die Auslagerung der bzgl. der reservierten Kachelanzahl größten Adreßräume, um dadurch maximal viel Freispeicher zu schaffen.
4.7 Starke Verbreitung seitenbasierter Verfahren
Swapping ganzer Adreßräume
Implementierungsaspekte
Von allen virtuellen Adreßraumtechniken sind in der Praxis seitenbasierte Verfahren am weitesten verbreitet. Selbst wenn der Prozessor segmentbasierte Mechanismen zur Verfügung stellt, werden diese von vielen Betriebssystemen gegenwärtig nur selten genutzt. So setzen neuere Microsoft Windows-Betriebssysteme bei Intel-Prozessoren zwar die Kombination aus segment- und seitenbasierten Verfahren ein, aber in Anwendungsadreßräumen werden die Segmente in der Regel mit einer Basisadresse 0 und einer Segmentlänge von 4 GByte initialisiert (siehe auch [Oney 1996]). Der resultierende flache 32-BitAdreßraum wird in diesem Fall wie bei rein seitenbasierten Verfahren verwaltet.
4.7
Implementierungsaspekte
Abb. 4-24 Anwendungsadreßräume bei gängigen Betriebssystemen
Die 32 Bit großen virtuellen Adreßräume, die moderne Betriebssysteme Anwendungen zur Verfügung stellen, sind in vielen Bereichen ähnlich aufgebaut. Beispiele für Anwendungsadreßräume von einigen Betriebssystemen sind in Abbildung 4-24 schematisch dargestellt. Der weiße Bereich gibt jeweils den Teil des Adreßraums wieder, der von der Anwendung direkt genutzt werden kann. Dieser Bereich schwankt je nach Betriebssystem zwischen ca. 2 GByte und knapp 4 GByte. Bei allen Systemen wird ein unterschiedlich großer Adreßbereich am Anfang (Adressen 0 aufwärts) für jeglichen Zugriff gesperrt: Bei Windows 95 sind es 4096 Byte, d.h. genau 1 Seite, bei Windows NT 64 KByte und bei BSD Unix je nach eingesetztem Rechnertyp 4 bis 8 KByte. Durch diese Sperrung wird jeder Zugriff auf diesen Bereich und damit insbesondere das Dereferenzieren eines Nullzeigers, ein häufiger Programmierfehler, vom System abgefangen und die Programmausführung mit einer Fehlermeldung abgebrochen. In Windows 95 wird zusätzlich der Adreßbereich von 4 KByte bis 4 MByte von der Nutzung durch 32-Bit-Anwendungen ausgeklammert. In diesem Adreßbereich werden MS-DOS- und 16-Bit-Windows-Anwendungen ausgeführt, die von einem gemeinsamen physischen Adreßraum ausgehen. Im oberen Bereich eines virtuellen Adreßraums wird meist der Betriebssystemcode eingeblendet. Dadurch kann das Betriebssystem jederzeit ohne Adreßraumwechsel auf seinen Code zugreifen. Insbesondere wird damit der Wechsel in den privilegierten Modus beim Aufruf einer Betriebssystemfunktion erleichtert. Bei BSD Unix und bei Windows NT/2000 ist dieser Codebereich vor jeglichem Zugriff durch die Anwendung geschützt. In der Regel wird sogar explizit lesender Zugriff unterbunden, damit Anwendungen keine impliziten Annahmen
Dereferenzieren eines Nullzeigers
Systemcode im Anwendungsadreßraum
4
Keine vollständige Adreßraumisolation in Windows 9x
Adreßräume
über die interne Struktur des Betriebssystems machen können. Der überdimensionierte 2 GByte große Betriebssystembereich bei Windows NT ist primär auf eine Hardwarebeschränkung bei der Realisierung auf MlPS-Prozessoren zurückzuführen (siehe [Richter 1996]). Im obersten 1-GByte-Bereich von Windows 9x befinden sich alle wesentlichen Teile des Betriebssystems einschließlich aller Gerätetreiber. In dem darunterliegenden 1-GByte-Bereich werden außerdem alle allgemein genutzten Funktionsbibliotheken eingeblendet. Beide Bereiche können von jeder Anwendung aus Gründen der Abwärtskompatibilität, z.B. weil ältere Funktionsbibliotheken in diesem Adreßbereich auch Daten ablegen, sowohl gelesen als auch geschrieben werden. Dadurch ist der Schutz des Betriebssystems nicht vollständig gewährleistet; fehlerhafte Windows 9x-Applikationen können damit immer noch das gesamte System beeinflussen und zum Absturz bringen. Adreßraumerzeugung und -initialisierung Erzeugung und Initialisierung neuer Adreßräume sind wichtige Aufgaben der Speicherverwaltung. Bei der Erzeugung wird Speicherplatz für die Seitentabellen und für eine Teilmenge der virtuellen Seiten reserviert. Danach werden die Seitentabellen initialisiert und die verschiedenen Speicherbereiche des virtuellen Adreßraums initialisiert. Diese Initialisierung hängt in der Regel von der Angabe einer ausführbaren Datei ab, die das auszuführende Anwendungsprogramm in maschinenlesbarer Form enthält (siehe Abbildung 4-25).
Abb. 4-25 Adreßrauminitialisierung
4.7
Implementierungsaspekte
Die Datei selbst ist in mehrere Bereiche aufgeteilt, deren Größe und Position in einem Header am Anfang der Datei festgelegt sind. Das Maschinenprogramm liegt in der Regel in einer Form vor, die das direkte Einblenden einzelner Seiten innerhalb der Datei in den virtuellen Adreßraum der Anwendung ermöglicht. Dadurch wird das Dateisystem als erweiterter Teil des virtuellen Speichers aufgefaßt. Bei einem Seitenfehler wird der entsprechende Teil der Datei geladen. Eine Verdrängung erübrigt sich, da Programmcode in der Regel nicht verändert wird. Der Bereich der statisch allokierten Daten in der Datei wird in der Regel in den entsprechenden Bereich des virtuellen Adreßraums kopiert und damit initialisiert. Darüber hinaus kann eine ausführbare Datei Relokations- und Symbolinformationen enthalten. Relokationsinformationen erlauben das Verschieben von Daten und Instruktionen im virtuellen Adreßraum. Sie geben an, wo Daten und Instruktionen absolut adressiert werden und wie diese Adressen im Fall einer Verschiebung verändert werden müssen. Die Symboltabelle gibt Auskunft über die definierten und referenzierten Symbole in einem Programm. In einem ausführbaren Programm wird sie u.a. von Debuggern genutzt. Bei den meisten Betriebssystemen wird jeder Bereich des virtuellen Adreßraums nur in beschränktem Umfang angelegt. In der Regel wird z.B. nur ein Teil des Programmcodes unmittelbar in den Hauptspeicher geladen. Statische Daten werden aufgrund unterschiedlicher Initialwerte der Variablen zu Beginn meist vollständig angelegt. Für Heap und Laufzeitkeller werden in der Regel jeweils wenige Kacheln initial reserviert. Viele Betriebssysteme wie z.B. Windows 9x und Windows NT/ 2000 fassen die Erzeugung und Initialisierung eines neuen Adreßraums zu einer Funktion zusammen. Dabei wird im allgemeinen durch den Aufruf von Create ( Dateiname, ... )
ein neuer Adreßraum angelegt und mit den entsprechenden Informationen aus der angegebenen Programmdatei initialisiert. Im UNIX-Bereich werden Erzeugung und Initialisierung getrennt durchgeführt. Ein neuer Adreßraum wird durch den Aufruf der Funktion fork() erzeugt. Der erzeugte Adreßraum ist eine identische Kopie des ursprünglichen Adreßraums. Dabei wendet das System ein als Copy-on-Write bekanntes Verfahren [Fitzgerald und Rashid 1986] an, das eine extrem schnelle Erzeugung von Adreßraumkopien gestattet. Zu diesem Zweck werden effektiv nur die Seitentabellen des erzeugenden Adreßraums kopiert. Jede Seite des Adreßraums wird außerdem im zugehörigen Seitendeskriptor vor schreibendem Zugriff geschützt, der besondere Schutzzustand wird in einem der freien Bits des
Aufbau einer ausführbaren Programmdatei
Relokationsinformation und Symboltabelle
Adreßraumerzeugung durch den Aufruf von Create()
Adreßraumerzeugung durch den Aufruf von fork() Copy-on-Write
4
Adreßräume
Deskriptors vermerkt. Insgesamt entsteht ein zweiter virtueller Adreßraum, in dem jeder Seitendeskriptor auf dieselbe Kachel im Hauptspeicher oder im Auslagerungsfall auf dieselbe Position im Hintergrundspeicher zeigt. Solange keiner der beiden Adreßräume schreibend zugreift, verweisen beide Deskriptoren auf denselben Seiteninhalt. Erst mit der Ausführung von Schreiboperationen weichen die beiden Adreßräume voneinander ab. Da die Seiten zu Beginn schreibgeschützt sind, wird von der MMU ein Interrupt ausgelöst. Die Speicherverwaltung stellt in diesem Fall eine Kopie des zugehörigen Seiteninhalts her. Außerdem wird im betreffenden Seitendeskriptor die Seitenposition aktualisiert und anschließend in beiden Deskriptoren die ursprünglichen Zugriffsrechte wiederhergestellt. Durch den Aufruf einer zweiten Funktion exec () kann ein beliebiger Adreßraum mit dem neuen Inhalt eines ausführbaren Programms überladen werden (siehe dazu auch Kapitel 5). Überschneidungen zwischen Heap und Stack
Guards in WindowsBetriebssystemen
Viele Betriebssysteme nutzen mittlerweile seitenbasierte Schutztechniken zur Vermeidung von Überschneidungen zwischen den dynamischen Speicherbereichen eines Adreßraums. Dabei wird zwischen zwei veränderlichen Bereichen ein Teil des Adreßraums für jeden Zugriff gesperrt. Bei Windows NT wird zu diesem Zweck ein 4-KByte-Bereich gesperrt, bei Windows 9x sind diese sogenannten Guards jeweils 64 KByte groß. Im Fall von Windows 9x wird dabei jeder Laufzeitkeller am Anfang und Ende durch jeweils einen eigenen Guard geschützt. Verändert ein Adreßbereich seine Größe und über- oder unterschreitet dabei die vordefinierten Grenzen, so löst der Zugriff auf eine Adresse innerhalb eines Guards einen Interrupt durch die MMU aus. Nach einer Analyse der Fehlersituation wird das Betriebssystem in diesem Fall die Anwendung mit einer entsprechenden Fehlermeldung beenden. Leider sind mit diesem Verfahren nicht alle Überschneidungen erkennbar. Ist z.B. der bei einem Funktionsaufruf neu angelegte Stackbereich zur Speicherung von Parametern und funktionslokalen Variablen größer als der Guard, kann der nächste Adreßbereich hinter dem Guard u.U. ohne Fehlermeldung verändert werden. Durch einen großen Guard-Bereich wird lediglich die Wahrscheinlichkeit für einen solchen Fehlerfall reduziert.
4.7
Implementierungsaspekte
Überlappende Adreßräume Überlappende Adreßräume, wie sie z.B. zur Realisierung des in Kapitel 3 eingeführten Laufzeitmodells D notwendig sind, können mit virtuellen Adressierungstechniken leicht erzeugt werden. Dabei verweisen Seitendeskriptoren in verschiedenen Adreßräumen auf denselben Seiteninhalt (siehe Abbildung 4-26). Abb. 4-26 Gemeinsame Adreßbereiche
Die Schutzbits in den jeweiligen Seitendeskriptoren können zur Modellierung unterschiedlicher Zugriffsrechte eingesetzt werden, so kann z.B. in einem Adreßraum nur lesender Zugriff gestattet sein. In der Regel versucht man, den gemeinsamen Speicherbereich in allen Adreßräumen an derselben virtuellen Adresse zu plazieren. Dadurch können auch absolute Referenzen innerhalb des gemeinsamen Speicherbereichs von allen beteiligten Anwendungen korrekt interpretiert werden. Gemeinsam genutzte Funktionsbibliotheken Funktionsbibliotheken, die in mehreren Anwendungen benutzt werden, können als Spezialfall gemeinsam genutzter Adreßbereiche aufgefaßt werden. Bibliotheken dieser Form werden im Windows-Umfeld als dynamisch ladbare Bibliothek (DLL = Dynamic Link Library) und im UNIX-Bereich häufig als gemeinsame Bibliothek (Shared Library) bezeichnet. Die Bibliothek belegt in diesem Fall nur einmal physische Ressourcen des Systems. Der Programmbereich wird in jeden Anwendungsadreßraum, in dem eine bestimmte Bibliothek verwendet werden soll, lediglich an geeigneter Stelle eingeblendet. Dies funktioniert in einfacher Weise natürlich nur bei positionsunabhängigem Code. In diesem Fall kann dieselbe Bibliothek in verschiedenen Adreßräumen sogar an unterschiedlichen virtuellen Positionen plaziert werden. Enthält die Bibliothek absolute Referenzen, muß sie gezwungenermaßen in allen Adreßräumen an derselben virtuellen Adresse eingeblendet werden.
DLLs und Shared Libraries
4
Struktur dynamischer Bibliotheken
Adreßräume
Sinnvollerweise sollte der Code einer mehrfach eingeblendeten Bibliothek in jedem Adreßraum vor schreibendem Zugriff geschützt werden. Alternativ kann durch das Copy-on-Write-Verfahren zumindest sichergestellt werden, daß andere Adreßbereiche von schreibenden Änderungen unbeeinflußt bleiben. Aus Kompatibilitäts- und Platzgründen wird dieser Schutz z.B. bei Windows 9x jedoch nicht gewährleistet. Damit kann indirekt über dynamische Bibliotheken eine fehlerhafte Windows-Anwendung den Adreßraum anderer Anwendungen beeinflussen und dadurch schwer zu lokaliserende Laufzeitfehler auslösen. Dynamische Bibliotheken müssen besonders übersetzt und gebunden werden. Insbesondere müssen die Bibliotheken so ausgelegt werden, daß notwendige Datenstrukturen in jedem Adreßraum getrennt angelegt werden können. Umgekehrt können Referenzen auf Funktionen einer dynamischen Bibliothek nicht vor dem Startzeitpunkt aufgelöst werden. Das hat zur Folge, daß der Programmcode in einer ausführbaren Datei offene Verweise auf Bibliotheksfunktionen haben kann. Bei der Adreßrauminitialisierung werden in diesem Fall die notwendigen Bibliotheken in den Anwendungsadreßraum eingeblendet und die offenen Referenzen mit Hilfe der enthaltenen Symboltabelle nachträglich aufgelöst. Die meisten Betriebssysteme nutzen dieselbe Technik auch zum Einsparen von Speicherressourcen bei Anwendungsprogrammen. Werden mehrere Adreßräume mit derselben ausführbaren Programmdatei initialisert, reicht auch in diesem Fall die einmalige Reservierung von Speicherressourcen aus. Verankerung virtueller Adreßbereiche im Speicher
I/O-Locking
Gelegentlich muß die Auslagerung eines virtuellen Adreßbereichs unterbunden werden. Entsprechende Seiten oder Segmente werden durch das Setzen eines freien Bits im Seitendeskriptor markiert. Die zugeordneten Kacheln werden dann von den eingesetzten Verdrängungsverfahren ausgeklammert. Zum Beispiel werden virtuelle Speicherbereiche vom Betriebssystem nach dem Anstoßen von E/A-Operationen gesperrt. Diese auch als I/O-Locking bezeichnete Technik wird implizit vom Betriebssystem eingesetzt und ist für die Anwendung selbst transparent. Da die meisten Ein- und Ausgabeoperationen asynchron angestoßen und anschließend selbständig vom E/A-Controller mittels DMA durchgeführt werden, müssen die betroffenen Speicherbereiche bis zur Beendigung der E/A-Operation im Hauptspeicher verankert werden. Ein unbemerkter Austausch des Kachelinhalts als Folge einer Seitenverdrängung hätte ohne I/O-Locking fatale Folgen.
4.7
Implementierungsaspekte
Echtzeitaspekte Die Verankerung virtueller Adreßbereiche ist auch für die Einhaltung von Echtzeitanforderungen von Bedeutung. Echtzeitanwendungen, deren garantierte Antwortzeit in einem kritischen Bereich liegt, können ihre Zeitvorgaben nicht einhalten, wenn im Verlauf der Programmausführung unkontrolliert Teile des Adreßraums aus dem Hintergrundspeicher kopiert werden müssen. Bei dieser expliziten Form der Verankerung teilt die Anwendung dem System mit, welche virtuellen Adreßbereiche im Hauptspeicher von einer Seitenersetzung verschont werden sollen. Da die Verankerung virtueller Seiten die vorhandenen Speicherressourcen merklich reduzieren kann, setzen viele Betriebssysteme eine entsprechende Privilegstufe des Programms voraus. Ist diese nicht gegeben, ignoriert die Speicherverwaltung die Sperrung des Speichers. In POSIX.4 existieren mehrere Funktionen, die einer Anwendung das Sperren des gesamten Adreßraums oder bestimmter Adreßbereiche ermöglichen. Durch die Funktion mlockall(flags)
können alle momentan im Speicher befindlichen Seiten gesperrt werden (flags=MCL_CURRENT). Wird der Parameter flags dieser Funktion mit dem Wert MCL_CURRENT | MCL_FUTURE initialisiert, bleiben darüber hinaus alle zukünftig eingelagerten Seiten im Hauptspeicher verankert. Mit Hilfe der Funktionen mlock(Anfangsadresse,Länge)
und munlock{Anfangsadresse,Länge)
können Teile des virtuellen Adreßraums im Hauptspeicher verankert und wieder freigegeben werden. Die Parameter Anfangsadresse und Länge legen dabei den zu verankernden Adreßbereich fest. Da von der Speicherverwaltung nur ganze Seiten gesperrt werden können, wird ggf. auf Seitengrenzen gerundet. Explizites Sperren durch die Anwendung erfordert detaillierte Kenntnisse über den Aufbau des virtuellen Adreßraums und die Plazierung der einzelnen Speicherbereiche. So müssen in der Regel bestimmte Codebereiche zusammen mit Teilen des Anwendungszustands gesperrt werden. Außerdem müssen u.U. aufgerufene Bibliotheksfunktionen ebenfalls vor einer Verdrängung auf Hintergrundspeicher abgesichert werden, wenn diese aus einer kritischen Funktion heraus potentiell aufgerufen werden können.
Unerwartete Seitenfehler sind im Echtzeitfall oft kritisch
5
Threads
Threads (oder Kontrollflüsse) beschreiben die Aktivitäten in einem System. Sie können als virtuelle Prozessoren aufgefaßt werden, die jeweils für die Ausführung eines zugeordneten sequentiellen Programms in einem Adreßraum verantwortlich sind. Die Realisierung von Threads basiert auf dem Multiplexen der physischen Prozessoren. Innerhalb eines Adreßraums können je nach Laufzeitmodell ein oder mehrere Threads existieren (siehe Kapitel 3). Mit der Einrichtung eines neuen Adreßraums wird von der Systemsoftware immer ein erster Thread erzeugt, der seine Programmausführung an einer vordefinierten Startadresse aufnimmt. Bei einfachen Anwendungen reicht in vielen Fällen ein Thread pro Adreßraum aus. Dem Programmcode liegt in diesem Fall ein streng sequentielles Verarbeitungsmodell zugrunde. Bei komplexeren Anwendungen können auch mehrere Threads gleichzeitig in einem Adreßraum eingesetzt werden. Aus Sicht der Anwendungsentwicklung gibt es dafür im wesentlichen drei Gründe:
Sequentielles Verarbeitungsmodell
• Während einer E/A-Blockade eines Threads können weitere
Aufgaben der Anwendung von anderen Threads bearbeitet werden. • Bei Multiprozessorsystemen können Anwendungsaufgaben parallel auf mehreren Prozessoren bearbeitet werden. • Die Reaktionszeit der Anwendung auf Benutzereingaben kann verbessert werden. In vielen Fällen kann im Vergleich zum sequentiellen Verarbeitungsmodell die Gesamtausführungszeit der Anwendung und die Reaktionszeit z.B. auf Benutzereingaben reduziert werden. Dieses nebenläufige Verarbeitungsmodell gewinnt aufgrund dieser Vorteile und der wachsenden Verbreitung von geeigneten Thread-Realisierungen in Betriebssystemen und Laufzeitpaketen stark an Bedeutung. Darüber hinaus ist Nebenläufigkeit die natürliche Form der Programmierung, wenn innerhalb der Anwendung mehrere weitgehend entkoppelte Handlungsabläufe existieren. Klassische Beispiele für die nebenläufige Programmierung sind z.B. Anwendungen mit graphischer Bedienungsoberfläche. Insbeson-
Nebenläufiges Verarbeitungsmodell
5
Threads
dere wenn die Anwendung parallel zur graphischen Darstellung kontinuierliche Berechnungen durchführen muß, kann ein zweiter Thread die Interaktion mit dem Benutzer über die Bedienungsoberfläche in sinnvoller Weise koordinieren. Jeder Kontrollfluß für sich besitzt dadurch weiterhin einen klaren und eng an das sequentielle Verarbeitungsmodell angelehnten Aufbau. Während der erste Kontrollfluß praktisch ungehindert die notwendigen Berechnungen ausführt:
Schnelle Reaktion auf
wartet der zweite Kontrollfluß in einer Schleife auf eingehende Ereignisse des Graphiksystems - beispielsweise Benutzereingaben oder Änderungen bei der Fensteranordnung und -Sichtbarkeit - und führt die mit dem jeweiligen Ereignis verbundenen Anwendungsfunktionen
eingehende Ereignisse
aus: Thread 2: while (1) { e = ReceiveEvent(); ProcessEvent(e);
} Aufgrund der Nebenläufigkeit muß »lediglich« bei der Anwendungsprogrammierung darauf geachtet werden, daß gleichzeitige Zugriffe in den Funktionen Compute und ProcessEvent auf dieselben Datenstrukturen synchronisiert werden (siehe Kapitel 6). Unterbleibt diese Synchronisation, können selbst auf einem Monoprozessorsystem Dateninkonsistenzen und Programmfehler als Folge einer zeitlich verschränkten Ausführung beider Funktionen auftreten. Im Unterschied zur nebenläufigen Programmierung muß eine Anwendung mit nur einem Thread die kontinuierliche Berechnung periodisch unterbrechen, um eventuell eingetroffene Graphikereignisse entgegenzunehmen und zu beantworten. Dabei darf der Thread bei fehlenden Ereignissen des Graphiksystems in keinem Fall blockiert werden, da sonst auch die kontinuierliche Berechnung unterbrochen wäre: while (1) { /* Ein Berechnungsschritt */ ComputeStep(); if (QueryEvent()) { /* Ereignis angekommen? */ ProcessEvent(e);
} }
5 Threads
Mit wachsender Komplexität der Berechnung und der Oberflächenfunktionalität entsteht durch diese explizite Verzahnung schnell eine zerklüftete und schwer lesbare Programmstruktur. Nachteilig ist auch, daß z.B. die Reaktionszeit auf Benutzereingaben durch die Programmstruktur vorgegeben wird. Bei vergleichsweise kurzen Berechnungsschritten ist es beispielsweise nicht sinnvoll, in jedem Durchlauf ankommende Ereignisse entgegenzunehmen. Graphikereignisse treffen nicht in so hoher Frequenz ein, d.h., ein Großteil der QueryEventAufrufe ist bei einer Abfrage pro Durchlauf unnötig und verschwendet Prozessorleistung. Umgekehrt darf der zeitliche Abstand zwischen zwei Abfragen nicht zu groß werden, wenn die Reaktion auf Eingaben nicht zu lange verzögert werden soll. Ein Vorteil ergibt sich jedoch unmittelbar aus der sequentiellen Verarbeitung: Da sich aufgrund der Programmstruktur die Funktionen ComputeStep und ProcessEvent in der Ausführung wechselseitig ausschließen, erübrigt sich zumindest die im nebenläufigen Fall notwendige Synchronisation. Aus den oben angesprochenen Gründen ist es sinnvoll, bei der Anwendungsprogrammierung von einer unbeschränkten Anzahl an verfügbaren Threads auszugehen, d.h. von der tatsächlichen Anzahl an Prozessoren in einem Rechner zu abstrahieren. Bei der Anwendungsprogrammierung soll dabei im Idealfall jede potentiell vorhandene Nebenläufigkeit in Form eigenständiger Kontrollflüsse beibehalten und nicht durch nur einen Thread zwangsserialisiert werden. Die Vorteile rechtfertigen in vielen Fällen auch einen erhöhten Aufwand aufgrund notwendiger Synchronisationsmaßnahmen. Bereits auf einem Monoprozessorsystem kann die Anwendung durch das Ausnutzen von Blockadezeiten einzelner Threads profitieren. Auf einem Multiprozessorsystem ist häufig auch eine tatsächliche Leistungssteigerung erzielbar. Eine Tendenz zur Entwicklung nebenläufiger Programme ist in vielen Bereichen erkennbar, z.B. sind viele WWW-Server und -Browser mittlerweile multi-threaded, d.h., sie bestehen aus mehreren parallelen Kontrollflüssen. Auch im Windows-Bereich werden mit der Verfügbarkeit von Threads in der 32-Bit-Programmierschnittstelle (Win32-API) Anwendungen zunehmend durch mehrere Kontrollflüsse realisiert. Allgemein kann ein Thread als sogenannter virtueller Prozessor aufgefaßt werden. Teams mit mehreren Threads innerhalb eines Adreßraums entsprechen einer Gruppe virtueller Prozessoren, die über gemeinsamen Speicher miteinander kommunizieren (= virtueller Multiprozessor). Das gleiche gilt für Threads in verschiedenen Adreßräumen, aber mit einem überlappenden Speicherbereich (Laufzeitmodell D). Für eine nebenläufig programmierte Anwendung ist es dabei unerheblich, wie viele Prozessoren real vorhanden sind, d.h., ob es sich um ein Mono- oder Multiprozessorsystem handelt. Ist die Anzahl
Zerklüftete und schwer lesbare Programmstruktur
Anwendungsprogrammierer soll von unbeschränkter Prozessoranzahl ausgehen
Virtueller Prozessor
Virtueller Multiprozessor
5 Threads
Kontextwechsel
Scheduler
der in einer Anwendung benötigten virtuellen Prozessoren kleiner oder gleich der Anzahl vorhandener Prozessoren, kann im Prinzip eine l:l-Zuordnung durchgeführt werden. In diesem Fall wird jeder Thread von genau einem physischen Prozessor bearbeitet. Werden dagegen in der Anwendung mehr Threads erzeugt als physische Prozessoren vorhanden sind, muß von der Systemsoftware durch ein Zeitmultiplexverfahren implizit eine zeitlich versetzte Ausführung der Threads auf ein oder mehreren Prozessoren erzwungen werden. Zu diesem Zweck wird zu bestimmten Zeiten ein sogenannter Kontextwechsel durchgeführt, d.h., der Zustand des aktuell ausgeführten Threads wird gesichert und der vorher gespeicherte Zustand eines anderen Kontrollflusses wird vom Prozessor erneut übernommen. Dadurch wird mit jedem Kontextwechsel die Zuordnung virtueller Prozessor zu physischem Prozessor dynamisch verändert. Aufgabe des sogenannten Schedulers ist dabei, einen geeigneten Kandidaten aus einer Menge von potentiell ausführbaren Threads zu ermitteln. Insbesondere bei mehreren ausgeführten Anwendungen mit ein oder mehreren Threads kann der Scheduler in der Regel aus einem großen Vorrat den nächsten Kontrollfluß auswählen. Diese Wahl wird sehr stark von dem zugrundegelegten Optimierungsziel der Prozeßverwaltung beeinflußt, d.h., ob das System bei der Prozessorzuteilung eine hohe Auslastung der vorhandenen Hardwareressourcen oder im Fall von Echtzeitanwendungen die Einhaltung von Zeitvorgaben anstrebt. Im nachfolgenden Abschnitt 5.1 werden die zentralen Anforderungen an ein Thread-Konzept diskutiert. Zustandsmodelle für Threads sind Gegenstand von Abschnitt 5.2. Sie beschreiben relevante Thread-Zustände und bilden die Grundlage für verschiedene Schedulingstrategien: Monoprozessorstrategien (5.3), Scheduling mit Echtzeitgarantien (5.4) und Verfahren für Multiprozessoren (5.5). Abschließend werden in Abschnitt 5.6 gängige Programmierschnittstellen im Bereich Verwaltung und Scheduling von Threads einschließlich der entsprechenden POSIX-Schnittstelle und in Abschnitt 5.7 Implementierungsaspekte diskutiert.
5.1
Anforderungen
Die Forderung nach einer minimalen Einschränkung der Nebenläufigkeit bei der Anwendungsprogrammierung setzt ein einfach zu benutzendes, aber leistungsfähiges Thread-Konzept voraus. Wie bereits erwähnt, wird mit jedem neuen Adreßraum ein initialer Thread erzeugt. Diese Minimalunterstützung wird von jedem existierenden Betriebssystem zur Verfügung gestellt. Weitere von der Anwendung benötigte Threads können bei den meisten modernen Betriebssystemen bei Be-
5.1
Anforderungen
darf explizit erzeugt und über eine Startadresse mit einem sequentiellen Programm verknüpft werden. Natürlich ergibt sich aus Sicht der Anwendung für jeden neu erzeugten Thread der Wunsch, diesen möglichst dauerhaft an einen physischen Prozessor zu binden. Wenn genügend Prozessoren vorhanden sind, kann diese Prozessorzuordnung über die gesamte Lebensdauer des Threads aufrechterhalten werden. Threads werden in diesem Fall nur durch E/A-Operationen oder durch den Zwang zur Synchronisation mit anderen Kontrollflüssen blockiert. Eine ausreichende Auslastung der vorhandenen Hardwareressourcen, insbesondere der Prozessoren, ist in diesem Fall nur bei Anwendungen mit einem hohen Grad an Nebenläufigkeit gewährleistet. In der Praxis nutzen viele Anwendungen das Rechenpotential mehrerer Prozessoren noch nicht aus, d.h., die feste Zuordnung von ein oder mehreren Prozessoren zu einer Anwendung führt zu einer vergleichsweise geringen Auslastung der Hardwarressourcen. Außerdem übersteigt normalerweise die Gesamtanzahl der im System vorhandenen Threads die Prozessoranzahl um ein Vielfaches. Das Betriebssystem muß also Kompromisse bei der Prozessorzuordnung eingehen. Dadurch konkurrieren primär die Threads aller ausgeführten Anwendungen um das Betriebsmittel Prozessor. Bei einem hohen Parallelitätsgrad kann dies bereits innerhalb einer einzelnen Anwendung geschehen, d.h., mehrere Threads innerhalb eines Adreßraums stehen in einer direkten Konkurrenzsituation. In diesem Fall entsteht im Vergleich zur sequentiellen 1-Thread-Lösung ein verhältnismäßig geringer zusätzlicher Overhead aufgrund der durchgeführten Kontextwechsel.
Verhältnis Thread-Anzahl zu Prozessoranzahl
CPU- und I/O-Bursts Mit wachsender Thread- und fallender Prozessoranzahl verschärft sich der Wettbewerb um Prozessoren. Häufig existiert eine große Anzahl an Threads, die alle von der einzigen CPU eines Monoprozessorsystems ausgeführt werden müssen. Im PC- und Workstationbereich ist das der Normalfall. Durch die Zwangsserialisierung wird die absolute Ausführungsdauer der später ausgeführten Threads erheblich verlängert. Geht man z.B. von n Threads mit gleich langer Bearbeitungsdauer k aus, die unterbrechungsfrei hintereinander ausgeführt werden, so wird der erste Thread um 0, der zweite Thread um k, der i.-te Thread um (i-l)*k und der zuletzt ausgeführte Thread um die Zeitdauer (n-l)*k verzögert. Im Mittel ergibt sich für alle Threads eine Verzögerung, die proportional mit der Thread-Anzahl wächst:
Zwangsserialisierung bei einer CPU
5 Threads
CPU- und I/O-Bursts
CPU-Bursts sind meist kurz
In der Praxis wirkt der für viele Anwendungen typische Wechsel zwischen CPU- und I/O-Bursts subjektiv einer proportional mit der Thread-Anzahl wachsenden Verzögerung entgegen. Die obige Formel ergibt sich nämlich nur bei einer permanenten Belegung des Prozessors durch den Thread. Sinnvollerweise ordnet die Prozeßverwaltung aber nur dann einem Thread einen physischen Prozessor zu, wenn sich dieser im CPU-Burst befindet. Sobald der Thread eine E/A-Operation anstößt, können bis zur Beendigung dieser Operation keine weiteren Instruktionen sinnvoll ausgeführt werden. Die Prozessorzuordnung wird in diesem Fall mindestens bis zur Beendigung der E/A-Operation gelöst. Technisch findet ein Kontextwechsel statt, der die Ausführung des im I/O-Burst befindlichen Threads unterbricht und statt dessen den Prozessor mit der Ausführung eines rechenwilligen, d.h. im CPUBurst befindlichen zweiten Threads beauftragt. Rechenwillig kann z.B. ein Prozeß werden, dessen E/A-Operation in der Zwischenzeit beendet worden ist. CPU-Bursts sind verhältnismäßig kurz. In Abbildung 5-1 ist eine gemessene Verteilung der Dauer von CPU-Bursts dargestellt. Eine Häufung bei 2 ms Burst-Dauer ist klar erkennbar. Insgesamt sind über 90% der Bursts kürzer als 8 Millisekunden.
Abb. 5-1 Gemessene Verteilung bei der Dauer von CPU-Bursts
I/O-Bursts sind im Gegensatz zu CPU-Bursts meist erheblich länger. So werden z.B. bei einem Plattenzugriff bereits 8-10 ms zur Positionierung des Lese- und Schreibkopfs benötigt, und beim Lesen von Benutzereingaben können viele 100 ms zwischen zwei Eingaben (Drücken einer Taste) verstreichen. Setzt man voraus, daß genügend viele E/AOperationen von der Hardware asynchron ausgeführt werden können, ergibt sich für die mittlere Verzögerung eines Threads:
5.1
Anforderungen
mit t Burst gleich der mittleren Dauer eines CPU-Burst. Aufgrund der großen Zeitdiskrepanz zwischen CPU- und I/O-Burst ist die Verzögerung durch E/A-Operationen dominant. Der proportionale Verzögerungsfaktor bleibt in diesem Fall vielen Anwendungen und damit auch vielen Benutzern bis zu einer bestimmten Thread-Anzahl verborgen. Für die Prozeßverwaltung ergibt sich aus dem typischen Wechsel zwischen CPU- und I/O-Bursts ein leistungsfähiges Mittel zur erfolgreichen Umsetzung eines Thread-Konzepts. Der Aufruf jeder E/AOperation wird damit zu einem der zentralen Ansatzpunkte für Schedulingentscheidungen. In Abhängigkeit der maximalen Anzahl an autonom arbeitenden E/A-Controllern ist damit bereits auf einem Monoprozessorsystem ein beachtliches Maß an Nebenläufigkeit realisierbar. Vermeidung einer CPU-Monopolisierung Mit der Ausführung eines Threads durch einen physischen Prozessor übernimmt die zugehörige Anwendung bis zu einem gewissen Grad die Kontrolle über den Prozessor. Die Systemsoftware kann die Kontrolle auf drei verschiedene Arten zurückgewinnen:
• • •
bei Aufruf einer blockierenden E/A-Operation, durch die freiwillige Abgabe des Prozessors (Yielding) oder nach Eintreffen eines asynchronen Hardware-Interrupts.
Tritt keiner der drei Fälle ein, kann eine Anwendung im Extremfall auf einem Prozessor das Ausführungsmonopol erlangen. Die ersten beiden Möglichkeiten können z.B. durch eine fehlerhafte Anwendung, die sich in einer Endlosschleife befindet, leicht umgangen werden. Betriebssysteme nutzen in aller Regel die dritte Möglichkeit, um eine Monopolisierung des Prozessors durch eine fehlerhafte oder »böswillige« Anwendung zu verhindern. Da der Zugriff auf die Interruptmaske eine privilegierte Prozessorinstruktion ist, können Anwendungsprogramme nur indirekt über den Aufruf einer Betriebssystemfunktion daran Veränderungen vornehmen. Das Betriebssystem kann in diesem Fall sicherstellen, daß eine Monopolisierung jederzeit ausgeschlossen wird. Lediglich besondere Anwendungsprogramme, die im privilegierten Modus ausgeführt werden, können damit »erfolgreich« die dauerhafte Kontrolle über einen Prozessor erlangen. Privilegien dieser Art sollten aus diesem Grund die seltene Ausnahme bleiben. Technisch erlangt die Systemsoftware die Kontrolle über den Prozessor meist mit Hilfe sogenannter Timer-Bausteine. Diese werden so programmiert, daß sie periodisch oder nach einer vorgegebenen Zeit einen Interrupt auslösen. Mit jedem Timer-Interrupt wird damit die Prozeßverwaltung in die Lage versetzt, potentiell die Zuordnung
Timer-Interrupts sind ein sicheres Mittel gegen CPU-Monopolisierung
5 Threads
Preemptives Scheduling
Nichtpreemptives Scheduling
Thread : Prozessor durch einen Kontextwechsel zu verändern. Nutzt eine Prozeßverwaltung einen Timer-Interrupt in dieser Form, um einen mitten in der Ausführung befindlichen Thread vorzeitig zu unterbrechen, spricht man von preemptivem Scheduling. Neben Time-Interrupts kann eine Preemption auch durch andere asynchrone Unterbrechungen ausgelöst werden, wenn z.B. ein Interrupt die Fertigstellung einer E/A-Operation signalisiert und der damit wieder rechenbereit gewordene Thread den eben ausgeführten Kontrollfluß aufgrund einer Schedulingentscheidung ablöst. Im Gegensatz dazu kann bei einem nichtpreemptiven Scheduling nur durch die ersten beiden Möglichkeiten, d.h. beim Aufruf einer blockierenden Betriebssystemfunktion oder durch die explizite Abgabe des Prozessors, ein Kontextwechsel und damit die Ausführung eines anderen Threads herbeigeführt werden. Schedulingziel
Dialogbetrieb
Stapel- oder Batchbetrieb
Bei jedem Kontextwechsel muß die Prozeßverwaltung typischerweise aus einer Menge ausführbarer Threads einen geeigneten Kandidaten auswählen. Dabei zählt man zur Menge der ausführbaren Kontrollflüsse alle Threads, die nicht aufgrund einer E/A- oder Synchronisationsoperation blockiert sind. Insbesondere gehören auch alle Threads dazu, denen der Prozessor preemptiv entzogen worden ist. Die Auswahl des geeigneten Kandidaten wird vom Scheduler, einer zentralen Komponente der Prozeßverwaltung, vorgenommen. Das jeweils eingesetzte Schedulingverfahren hängt dabei letztendlich von dem übergeordneten Schedulingziel ab, das von der Prozeßverwaltung verfolgt wird. Schedulingziele selbst werden durch die geplante Betriebsform und die vorhandene Hardwarestruktur bestimmt. Ein weit verbreiteter Einsatzbereich ist der Dialogbetrieb. Das Schedulingziel ist in diesem Fall die besondere Unterstützung von interaktiven Anwendungen. Threads, die nach dem Aufruf einer blokkierenden E/A-Operation durch eine Benutzereingabe wieder ausführbar sind, werden vom Schedulingverfahren bevorzugt ausgewählt. Für den Anwender ist dies in der Regel durch eine schnelle Reaktion auf Benutzereingaben erkennbar. Im Unterschied dazu steht beim sogenannten Stapel- oder Batchbetrieb eine hohe Auslastung der vorhandenen Hardwareressourcen oder ein hoher Durchsatz an fertiggestellten Aufträgen im Vordergrund. Bei dieser früher weit verbreiteten Form der Offline-Verarbeitung existiert in der Regel keine direkte Verbindung mit den Benutzern über Terminals, eine besondere Reaktion auf Benutzereingaben ist dadurch unnötig. Eine spezielle Form der Prozeßverwaltung ist notwendig, wenn Anwendungen mit Echtzeitanforderungen ausgeführt werden sollen.
5.1
Anforderungen
In diesem Echtzeitbetrieb ist die Einhaltung von Zeitgarantien das vorrangige Schedulingziel, dem sich eine Optimierung der Hardwareauslastung oder der Reaktionszeit auf Benutzereingaben unterordnen muß. Charakteristisch für diese Betriebsform ist, daß Echtzeitanwendungen der Prozeßverwaltung zusätzliche Informationen über die maximale Laufzeit und die einzuhaltenden Fristen mitteilen müssen. Auf der Grundlage dieser Zusatzinformationen von allen Threads kann durch entsprechende Schedulingverfahren eine Verarbeitungsreihenfolge ermittelt werden, in der die Fristen aller Threads eingehalten werden (genügend Hardwareressourcen vorausgesetzt). Beim Hintergrundbetrieb werden Anwendungen ausgeführt, deren Fertigstellungszeitpunkt völlig unkritisch ist. Primäres Schedulingziel ist in diesem Fall, daß die Bearbeitung dieser Anwendungen den eigentlichen Rechenbetrieb nur minimal oder gar nicht beeinflußt. Meist wird diese Betriebsform in Kombination mit einem Dialog- oder Echtzeitbetrieb benutzt. Bei der Thread-Auswahl werden dabei zuerst dialogorientierte oder echtzeitkritische Threads ausgewählt, bevor verbleibende freie Zeitintervalle auf die Hintergrundaufträge aufgeteilt werden. Neben den spezifischen Eigenschaften des Einsatzgebietes hat die Anzahl vorhandener Prozessoren entscheidenden Einfluß auf die Schedulingstrategie. Bei nur einem verfügbaren Prozessor findet die bereits angesprochene Sequentialisierung aller Threads statt; bei jedem Kontextwechsel wird der entsprechend dem verfolgten Schedulingziel am besten geeignete Kandidat ausgewählt. Stehen mehrere Prozessoren zur Verfügung, können auf jeden Fall mehrere voneinander unabhängige Kontrollflüsse auf unterschiedlichen Prozessoren ausgeführt werden. Außerdem ergeben sich mehr Freiheitsgrade bei der Umsetzung des Schedulingziels, die jedoch nicht notwendigerweise eine Leistungssteigerung für eine nebenläufig programmierte Anwendung zur Folge haben. Nur wenn das Schedulingverfahren sicherstellen kann, daß mehrere Threads derselben Anwendung gleichzeitig auf mehreren Prozessoren ausgeführt werden, kann aufgrund der schnellen Interaktion innerhalb eines Adreßraums eine Beschleunigung erzielt werden.
Echtzeitbetrieb
Hintergrundbetrieb
Auswirkungen der Prozessoranzahl
Abb. 5-2 Kemel-Level-Threads (KL-Threads)
5
Threads
Thread-Typen KL-Thread
Je nach Realisierungsform unterscheidet man zwischen Kernel-LevelThreads (KL-Threads) und User-Level-Threads (UL-Threads). KLThreads werden im Kern durch Multiplexen der physischen Prozessoren realisiert (siehe Abbildung 5-2). Thread-Wechsel, d.h. die Umschaltung eines physischen Prozessors von einem KL-Thread auf einen anderen, erfordern daher immer die Übergabe der Kontrolle über den physischen Prozessor an den Betriebssystemkern. Ein vollständiger Thread-Wechsel durchläuft im allgemeinen die in Abbildung 5-3 dargestellten Schritte. Da die Threads KL-Thread 1 und KL-Thread 2 im allgemeinen in verschiedenen Adreßräumen operieren, muß als Teil des Kontextwechsels auch der Adreßraum gewechselt werden. Obwohl die unmittelbaren Zusatzkosten bei einem solchen Adreßraumwechsel lediglich aus ein oder zwei Instruktionen zum Neuladen der Segmenttabellen- oder dem obersten Seitentabellenregisters der MMU bestehen, sind die indirekten Folgekosten aufgrund der TLBTnvalidierung und der geringen anfänglichen Trefferrate kalter Caches z.T. erheblich.
Abb. 5-3 Thread-Wechsel
KL-Threads sind schwergewichtig
Aufgrund der relativ hohen Kosten, die bei KL-Threads durch ThreadWechsel im Kern entstehen, spricht man häufig auch von schwergewichtigen Threads. Die Kosten für den Thread-Wechsel können jedoch reduziert werden, wenn mehrere KL-Threads pro Adreßraum zugelassen sind und bei einem Thread-Wechsel vom Betriebssystemkern zunächst versucht wird, einen ablaufbereiten Thread im selben Adreßraum zu finden (siehe Abbildung 5-4). Damit kann die Invalidierung von TLB und Cache bis zu einem gewissen Grad vermieden werden. Eine gewisse Abkühlung von TLB und Caches läßt sich jedoch nicht vermeiden, da auch der Wechsel zu einem anderen Thread innerhalb desselben Adreßraums meist mit einem Wechsel der Lokalitätsmenge verbunden ist.
.,
5.1
Anforderungen
Abb. 5-4 Mehrere KL-Threads innerhalb eines Adreßraums
Bei der Unterstützung mehrerer KL-Threads innerhalb eines Adreßraums wird jedoch weiter kritisiert, daß ein Thread-Wechsel trotz des eingesparten Adreßraumwechsels immer den Aufruf einer Betriebssystemfunktion zur Folge hat. Da diese Funktionen aus Schutzgründen fast immer durch einen Trap-Aufruf realisiert werden, sind sie im Vergleich zu einem Prozeduraufruf innerhalb eines Adreßraums deutlich teurer. Der Mehraufwand ist im wesentlichen auf folgende Aspekte zurückzuführen: •
Trap-Instruktionen gehören zu den aufwendigen Instruktionen im Befehlssatz eines Prozessors. • Durch einen eventuell notwendigen Wechsel in den Adreßraum des Betriebssystems entstehen indirekte Folgekosten. • Die Ausführung allgemeiner Verwaltungsfunktionen des Betriebssystems erzeugt zusätzlichen Overhead. • Beim Aufruf der Betriebssystemfunktion kann potentiell ein Wechsel zu einem KL-Thread in einem anderen Adreßraum stattfinden. Abb. 5-5 Leichtgewichtige Thread-Realisierung (User-Level-Threads)
Zur Reduzierung der Kontextwechselkosten wurde aus diesem Grund eine leichtgewichtige Variante entwickelt, bei der Threads vollständig im Adreßraum der Anwendung realisiert werden. Diese als UserLevel-Thread (UL-Thread) bezeichnete Thread-Variante ist der Prozeßverwaltung des Kerns völlig unbekannt, benötigt aber einen Träger in Form eines KL-Threads (siehe Abbildung 5-5). Die Ausführungsin-
UL-Thread
5
UL-Threads sind leichtgewichtig
Coroutinen
Threads
tervalle des Träger-Threads werden in diesem Fall von einer anwendungsspezifischen Thread-Verwaltung auf die vorhandenen ULThreads aufgeteilt. Diese Verwaltungsfunktionen werden in der Summe als Thread-Package bezeichnet und befinden sich in Form einer Laufzeitbibliothek innerhalb des Anwendungsadreßraums. Kontextwechsel zwischen verschiedenen UL-Threads einer Anwendung sind damit im Aufwand mit einfachen Prozeduraufrufen vergleichbar; daraus ergibt sich auch der häufig verwendete Name Light-WeightThread. Technisch aufwendig ist die Vermeidung einer Blockade des Träger-Threads beim Aufruf einer potentiell blockierenden Betriebssystemfunktion durch den UL-Thread. Da in diesem Fall die Kontrolle durch das Betriebssystem u.U. in einen anderen Adreßraum verlagert wird, findet kein Wechsel der Kontrolle auf einen anderen UL-Thread innerhalb derselben Anwendung statt. Damit gehen wesentliche Vorteile der leichtgewichtigen Thread-Realisierung verloren. Einen Spezialfall von UL-Threads stellen sogenannte Coroutinen [Conway 1963] dar, wie sie von Programmiersprachen wie z.B. Simula zur Verfügung gestellt werden. In diesem Verfahren finden ebenfalls Kontextwechsel zwischen mehreren logischen Kontrollflüssen, die an einzelne Prozeduren der Anwendung gebunden werden, statt. Der Wechsel in den Kontext einer anderen Coroutine wird meist explizit durch den Aufruf einer entsprechenden Funktion eingeleitet. Die Schedulingreihenfolge ist dabei fest vorgegeben. Die verschiedenen Thread-Varianten haben einen großen Einfluß auf die Programmierung nebenläufiger Anwendungen. Sobald in der schwergewichtigen Variante mehreren Threads eigene Adreßräume zugeordnet werden, kann eine Kommunikation zwischen diesen Threads nur über Nachrichten oder über explizit angelegte, gemeinsame Speicherbereiche in den ansonsten disj unkten Adreßräumen stattfinden. Aufgrund der hohen Kosten der damit verbundenen Prozeßwechsel empfiehlt sich die Aufteilung in verhältnismäßig wenige Threads, die sich nach Möglichkeit bei der Bewerbung um einen Prozessor in größeren Zeitabständen abwechseln.
5.2
Zustandsmodelle
Die Unterscheidung zwischen verschiedenen Thread-Zuständen bildet die Entscheidungsgrundlage für die Auswahl eines geeigneten Kandidaten, der nach einem Kontextwechsel in den Besitz eines Prozessors kommt.
5.2
Zustandsmodelle
Abb. 5-6 Einfaches ThreadZustandsmodell
Einfaches Zustandsmodell In einem einfachen Zustandsmodell wird zwischen drei Thread-Zuständen unterschieden (siehe Abbildung 5-6):
•
•
•
Rechnend: Threads in diesem Zustand sind im Besitz eines physischen Prozessors. Bei Monoprozessorsystemen kann sich jeweils nur ein Thread in diesem Zustand befinden; bei Multiprozessorsystemen mit k Prozessoren sind höchstens k Threads rechnend. Blockiert: Blockierte Threads warten auf die Beendigung einer E/A-Operation oder den Eintritt einer bestimmten Synchronisationsbedingung. Zu jedem Zeitpunkt können beliebig viele Threads im Zustand Blockiert sein. Bereit: Threads in diesem Zustand sind potentiell ausführbar, aber nicht im Besitz eines physischen Prozessors. Auch in diesem Zustand können sich beliebig viele Threads befinden.
Thread-Zustände
Die im Zustandsmodell erlaubten Übergänge sind in Abbildung 5-6 durch Pfeile dargestellt:
• • •
•
add: Ein neu erzeugter Thread wird - u.U. nach dem Anlegen und Initialisieren des zugehörigen Adreßraums - dynamisch in die Menge der bereiten Threads aufgenommen. assign: Als Folge eines Kontextwechsels wird einem bereiten Thread der Prozessor zugeordnet. Der ausgewählte Thread wird damit ausgeführt. block: Beim Aufruf einer blockierenden E/A- oder Synchronisationsoperation sowie bei einem Seitenfehler wird einem rechnenden Thread der Prozessor entzogen. Der blockierte Thread wartet in diesem Fall auf die Beendigung der Operation oder die Einlagerung der fehlenden Seite. ready: Ein blockierter Thread wechselt nach Beendigung der angestoßenen Operation in den Zustand »Bereit«. Er bewirbt sich damit erneut um einen physischen Prozessor.
Zustandsübergänge
5
Threads
• •
resign: Einem rechnenden Thread wird der Prozessor z. B. aufgrund eines Timer-Interrupts vorzeitig entzogen. Der Thread bewirbt sich anschließend erneut um eine Prozessorzuteilung. retire: Ein aktuell rechnender Thread terminiert und gibt damit freiwillig den Prozessor ab; belegte Hard- und Softwareressourcen werden freigegeben.
Erweitertes Zustandsmodell Auswirkungen des Swapping auf das Zustandsmodell
Das einfache Zustandsmodell muß erweitert werden, wenn durch Swapping ganze Adreßräume und mit ihnen die darin enthaltenen Threads aufgrund eines Speichermangels ausgelagert werden können. Bis zur erneuten Einlagerung kann keiner der betroffenen Threads ausgeführt werden. Im einfachsten Fall besteht die Erweiterung aus einem zusätzlichen Zustand, der alle zu einem Zeitpunkt ausgelagerten Threads beinhaltet (siehe Abbildung 5-7; die aus Abbildung 5-6 bekannten Übergänge wurden aus Gründen der Übersichtlichkeit nicht dargestellt). In diesem ausgelagerten Zustand kann im Prinzip ein Übergang swap out aus jedem der drei Grundzustände existieren. Insbesondere wenn mehrere Threads innerhalb eines Adreßraums vorhanden sind, können durch die Auslagerung Threads aller drei Zustände davon betroffen sein. Umgekehrt existieren swap «'«-Übergänge vom Auslagerungszustand in die Zustände »Bereit« oder »Blockiert«. In welchen Zustand ein Thread jeweils konkret wechselt, hängt davon ab, ob er sich nach der Einlagerung erneut um einen Prozessor bewerben kann oder weiterhin auf die Beendigung einer E/A- oder Synchronisationsoperation warten muß.
Abb. 5-7 Erweitertes ThreadZustandsmodell
Umsetzung von Thread-Zustand und Zustandsmodell Prozeßkontrollblock (PCB)
Der Zustand eines Threads wird in den meisten Betriebssystemen durch einen sogenannten Prozeßkontrollblock (PCB) beschrieben (siehe Abbildung 5-8). Der PCB enthält alle für die Prozeßverwaltung notwendigen Informationen. Dazu gehören u.a.:
5.2
Zustandsmodelle
• Eine eindeutige Identifikation (PID = Process Identification) • • •
Bestandteile des PCB
Speicherplatz zur Sicherung des Prozessorzustands bei einem Kontextwechsel Informationen über den sogenannten Wartegrund im Fall eines blockierten Threads Adreßrauminformation, z. B. Verweis auf die oberste Seitentabelle Zustandsinformationen und Statistiken für das Scheduling
•
Im Hinblick auf die verschiedenen Thread-Realisierungen wird in existierenden Systemen zunehmend die Adreßrauminformation vom eigentlichen Thread-Zustand isoliert und in separaten Datenstrukturen gespeichert. In diesem Fall ergibt sich ein reduzierter Thread-Kontrollblock, der lediglich einen Verweis auf die zugehörige Adreßraumbeschreibung enthält. Aus Gründen der Übersichtlichkeit wird in den nachfolgenden Abschnitten einheitlich von einem PCB gesprochen und implizit unterstellt, daß bei der Unterstützung mehrerer KLThreads in einem Adreßraum und bei UL-Threads der entsprechende Teil des PCBs nicht vorhanden ist.
ThreadKontrollblock
Abb. 5-8 PCB und Zustandslisten
Das Zustandsmodell selbst kann durch drei bzw. vier Zeiger realisiert werden, die jeweils auf den Anfang der Rechnend-, Bereit- und Blokkiert-Liste sowie auf die Liste der ausgelagerten Threads zeigen. Innerhalb einer Zustandsliste werden die PCBs der zugehörigen Threads direkt untereinander verkettet, da ein Thread sich zu jedem Zeitpunkt in genau einem Zustand befindet.
Listenbasierte Realisierung des Zustandsmodells
5 Threads
Dispatcher und Scheduler Dispatcher führt Zustandsübergänge aus
Scheduler wählt nächsten auszuführenden Prozeß
Die Durchführung der Zustandsübergänge selbst ist Aufgabe des Dispatchers. Er stellt für alle Übergänge entsprechende Funktionen zur Verfügung, die von anderen Teilen der Prozeßverwaltung aufgerufen werden. Dabei muß sichergestellt sein, daß mit jedem Wechsel eines rechnenden Threads in die Listen der bereiten oder blockierten Threads durch den Aufruf der Funktion Assign ein geeigneter Kandidat zur weiteren Ausführung ausgewählt wird. Die Auswahl eines Threads aus einer Thread-Menge während des Zustandsübergangs wird von einem Scheduler vorgenommen. Aus Leistungsgesichtspunkten werden in einem konkreten System in der Regel zwei verschiedene Scheduler eingesetzt: •
•
Der Short-Term-Scheduler tritt beim assign-Übergang in Aktion und ist ausschließlich für die Prozessorzuteilung zwischen bereiten Threads zuständig. Der Long-Term-Scheduler trifft komplexere Schedulingentscheidungen und wählt z.B. bei einem akuten Speicherengpaß einen geeigneten Adreßraum für die Auslagerung aus.
Hauptgrund für die Trennung sind die Unterschiede in der Aufrufhäufigkeit und in den Effizienzanforderungen. Da der Short-Term-Scheduler bei jedem Kontextwechsel aufgerufen wird, steht nur ein extrem schmales Zeitfenster für die Auswahl zur Verfügung. Es ist daher sinnvoll, alle längerfristigen oder unkritischen Schedulingentscheidungen in den Long-Term-Scheduler zu verlagern.
5.3 Monoprozessor-Scheduling
Schedulingkriterien
Die Wahl der konkreten Schedulingstrategie wird sehr stark durch die vom Betriebssystem unterstützte Betriebsform beeinflußt. Da es eine Reihe sehr unterschiedlicher Schedulingverfahren gibt, müssen zusätzliche Kriterien zur Bewertung von Schedulingstrategien hinsichtlich ihrer Qualität und ihrer Eignung für eine bestimmte Betriebsform herangezogen werden. Im wesentlichen sind dies: • • •
CPU-Auslastung: Maß für die Auslastung eines Prozessors durch die Ausführung von Anwendungsinstruktionen Durchsatz: Anzahl der pro Zeiteinheit fertiggestellten Aufträge Turnaround: Zeit zwischen zwei Thread-Aktivierungen (Zeitintervall zwischen zwei aufeinanderfolgenden assign-Übergängen eines Threads)
5.3
• •
•
Monoprozessor-Scheduling
Wartezeit: Verweildauer in der Bereit-Liste, d. h. die Zeitdauer, in der einem rechenwilligen Thread kein physischer Prozessor zugeordnet wird Antwortzeit: Für interaktive Anwendung wichtige Zeitspanne zwischen der Ankunft z.B. einer Benutzereingabe und einer potentiellen Reaktion durch einen assign-Übergang des zugehörigen Threads Realzeit: Einhaltung der von Anwendungen vorgegebenen Realzeitgarantien
Abgesehen vom Realzeitkritierium, das erst in Abschnitt 5.4 diskutiert wird, können alle aufgezählten Kriterien zur Bewertung der nachfolgend vorgestellten Schedulingverfahren für Monoprozessorsysteme eingesetzt werden. Es ist offensichtlich, daß kein einzelnes Verfahren CPU-Auslastung und Durchsatz maximieren und gleichzeitig Turnaround, Warte- sowie Antwortzeit minimieren kann. Aus diesem Grand muß ein Schedulingverfahren in Hinblick auf die Betriebsform Kompromisse schließen. First-Come, First-Served (FCFS) Dieses nichtpreemptive Schedulingverfahren teilt einen Prozessor in der Reihenfolge des Auftragseingangs zu. Für FCFS gibt es eine einfache schlangen-basierte Realisierung, bei der mit jedem assignÜbergang der Thread am Kopf der als Schlange implementierten Bereit-Liste den Prozessor zugeordnet bekommt. Ein Kontextwechsel findet bei diesem Verfahren nur statt, wenn der aktuell rechnende Thread eine blockierende Betriebssystemfunktion aufruft oder den Prozessor freiwillig abgibt. Im ersten Fall wird der PCB des Threads nach Beendigung der blockierenden Operation wieder an das Ende der Bereit-Schlange angehängt. Bei einer freiwilligen Abgabe des Prozessors kommt der PCB sofort wieder an das Ende der Schlange; der Thread bewirbt sich also erneut um den Prozessor.
FCFS
Abb. 5-9 FCFS: Varianz bei der mittleren Wartezeit
Mit FCFS kann eine hohe CPU-Auslastung erzielt werden. Alle anderen Schedulingkriterien werden mit diesem Verfahren jedoch nicht optimiert. So hängen z.B. Turnaround, Warte- und Antwortzeit sehr stark von der konkreten Lastsituation ab und variieren entsprechend stark. In Abbildung 5-9 wird dies am Beispiel der mittleren Wartezeit verdeutlicht. Dargestellt sind die CPU-Bursts dreier Threads mit einer
Vor- und Nachteile
5
Threads
Länge von 24 oder 3 Zeiteinheiten. Ordnet FCFS den Prozessor zuerst dem Thread mit CPU-Burst 24 zu, ergibt sich eine mittlere Wartezeit von 17 ms. Umgekehrt ergibt sich eine mittlere Wartezeit von nur 3 ms, wenn zuerst die »kurzen« Threads ausgeführt werden. Abb. 5-10 Konvoi-Effekt
Nachteilig an FCFS ist außerdem, daß aufgrund eines sogenannten Konvoi-Effekts zwar die CPU-Auslastung hoch, die Auslastung des Gesamtsystems jedoch vergleichsweise gering sein kann. Dieser negative Effekt entsteht durch Threads mit langen CPU-Bursts in Kombination mit E/A-intensiven Threads (kurzer CPU-Burst). Die Auswirkungen des Effekts sind in Abbildung 5-10 am Beispiel eines rechenintensiven Threads PCPU u n d dreier E/A-intensiver Threads PJ/Q dargestellt. Man erkennt, daß während der Prozesssorbelegung durch PcPU keiner der E/A-intensiven Threads zum Zuge kommt, obwohl diese nur kurz den eigentlichen Prozessor belegen und meist sehr schnell eine blockierende E/A-Operation anstoßen. Dadurch wird das Potential nebenläufig ausführbarer E/A-Operationen nicht hinreichend gut ausgenutzt. Beispiel: Windows 3.x Kooperatives Scheduling
Eine Variante des FCFS-Verfahrens ist das bei Windows 3.x eingesetzte kooperative Scheduling, das in ähnlicher Form z.B. auch bei System 7 von Apple angewendet wird. Bei diesem Verfahren wird die Reihenfolge der Prozessorzuordnung »kooperativ« zwischen den aktuell ausgeführten Threads in der Form einer FCFS-basierten Ereignisschlange ermittelt. Die grundsätzliche Funktionsweise dieses Schedulingverfahrens wird in Abbildung 5-11 verdeutlicht. Im Zentrum steht eine für alle Anwendungen globale Ereignisschlange. Die primäre Quelle für die Erzeugung von Ereignissen stellt das Graphiksystem dar. Jede Benutzereingabe über die Maus oder die Tastatur sowie Änderungen am Status einzelner Fenster werden in Form von Ereignissen an das Ende der Schlange gestellt. Außerdem kann jede Anwendung zusätzlich
5.3
Monoprozessor-Scheduling
Ereignisse über die zentrale Schlange an andere Anwendungen versenden. Dabei ist jedes Ereignis in der Schlange an eine bestimmte Anwendung adressiert. Der Scheduler wählt die Anwendung für eine Prozessorzuteilung aus, die als Empfänger beim Ereignis am Kopf der Schlange eingetragen ist. Analog zu FCFS wird durch den Aufruf einer blockierenden Operation oder durch die freiwillige Prozessorabgabe der nächste Schedulingschritt initiiert. Abb. 5-11 Kooperatives Scheduling bei Windows 3.x
Die Nachteile dieses Verfahrens sind vielen Benutzern entsprechender Betriebssysteme bekannt. Eine vergleichsweise geringe Antwortzeit kann nur erreicht werden, wenn sich alle Anwendungen tatsächlich kooperativ verhalten und entsprechend häufig die Kontrolle an andere Anwendungen abgeben.
Nachteile
Shortest-Job-First (SJF) Dieses Verfahren führt die Prozessorzuteilung in der Reihenfolge wachsender CPU-Bursts durch, d.h., der Thread mit dem kleinsten nächsten CPU-Burst erhält den Prozessor. Bei mehreren Threads mit gleich langem nächsten CPU-Burst wird FCFS angewendet. Das SJFVerfahren versucht damit explizit den Konvoi-Effekt von FCFS zu umgehen und ermöglicht dadurch eine hohe Auslastung des Gesamtsystems. Darüber hinaus ist es beweisbar optimal bzgl. der Minimierung der Wartezeit. Das Verfahren ist nur bedingt realisierbar, da die Länge des nächsten CPU-Bursts a priori nicht bekannt ist. In der Praxis wird daher eine Approximation eingesetzt, die auf der Basis der gemessenen
SJF
Kein Konvoi-Effekt
SJF nur bedingt realisierbar
5
Threads
Länge des zurückliegenden Bursts und dessen Schätzwert einen exponentiellen Mittelwert für den nächsten Burst ermittelt:
Preemptive und nichtpreemptive Varianten
Dabei wird durch den Faktor a zwischen 0 und 1 bestimmt, welchen Einfluß zurückliegende Burstlängen auf die Schätzung haben. Werte nahe 1 ordnen der Vergangenheit nur einen geringen Stellenwert zu und sind besonders geeignet, wenn ein Thread eine hohe Varianz bei der Burstdauer aufweist. Für das SJF-Verfahren existieren preemptive und nichtpreemptive Varianten. Die beiden Varianten unterscheiden sich in ihrer Reaktion auf einen Thread Tj, der z.B. nach der Beendigung einer E/A-Operation während der Ausführung von T2 rechenbereit wird. Ist der geschätzte nächste CPU-Burst von Tj kürzer als die verbleibende Restzeit von T2, wird der Prozessor bei der preemptiven Variante dem Thread T2 unmittelbar entzogen und Tj zugeordnet. Im nichtpreemptiven Fall findet ein Kontextwechsel erst nach dem Aufruf einer blockierenden Operation oder der freiwilligen Abgabe des Prozessors durch T 2 statt. Round-Robin (RR)
RR
Zeitscheibe
Starke Verbreitung von RR
Round-Robin ist eine weit verbreitete preemptive Schedulingvariante, die eine gleichmäßige Aufteilung der verfügbaren Rechenzeit auf alle rechenwilligen Threads zum Ziel hat. Das Verfahren ordnet jedem rechenwilligen Thread ein definiertes Zeitquantum (auch Zeitscheibe genannt) zu. Nach einem Kontextwechsel ist ein Thread bis zum Ablauf dieses Zeitquantums oder bis zum Aufruf einer blockierenden Systemfunktion im Besitz eines Prozessors. Alle Prozesse im System werden in einer FIFO-Schlange verwaltet. Bei einem Kontextwechsel wird immer der am Kopf der Schlange befindliche Thread als nächstes ausgeführt. Ein aufgrund einer abgelaufenen Zeitscheibe unterbrochener Thread reiht sich an das Ende der Schlange ein und bewirbt sich damit erneut um einen Prozessor. Round-Robin kann damit als eine besondere preemptive Variante von FCFS-Scheduling angesehen werden. Round-Robin bildet die Grundlage für die meisten Multi-Taskingfähigen Betriebssysteme, bei denen die verfügbare Rechenleistung auf mehrere voneinander unabhängige Anwendungen möglichst gleichmäßig verteilt werden soll. Dieses Ziel wird aus einsichtigen Gründen auch erreicht, wenn alle Threads ihr Zeitquantum immer ausschöpfen (rechenintensive Threads). I/O-intensive Threads, die häufig vor Ablauf der Zeitscheibe eine blockierende Systemfunktion aufrufen und
5.3
Monoprozessor-Scheduling
damit vorzeitig den Prozessor entzogen bekommen, werden durch dieses Schedulingverfahren benachteiligt. Die konkrete Wahl des Zeitquantums ist für die Systemleistung kritisch. Eine zu kleine Zeitscheibe erhöht den »unproduktiven« Zeitaufwand (Anzahl Kontextwechsel). Im Extremfall wird nie die Nominalleistung des Systems erreicht, da aufgrund einer abgelaufenen Zeitscheibe ein erneuter Kontextwechsel stattfindet, bevor die Caches ihre Warmlaufphase überwunden haben. Bei steigender Zeitscheibe wird Round-Robin zunehmend zu einem reinen FCFS-Scheduling, da die Wahrscheinlichkeit für den Aufruf einer blockierenden Systemfunktion mit wachsendem Zeitquantum ansteigt. Analog erhöht sich die mittlere Wartezeit für einen Kontrollfluß und damit insbesondere die Reaktionszeit bei interaktiven Anwendungen. Typische Werte für die Zeitscheibe bei gängigen Systemen liegen im Bereich von 10 bis 20 Millisekunden.
Wahl des Zeitquantums ist kritisch
Prioritätsbasiertes Scheduling Bei dieser Form des Schedulings wird jedem Thread eine Priorität zugeordnet, die jede Schedulingentscheidung maßgeblich beeinflußt. Prioritäten werden innerhalb eines vom Betriebssystem vorgegebenen Intervalls zugewiesen, häufig wird dabei dem zahlenmäßig kleinsten Wert die höchste Priorität zugeordnet. Man unterscheidet zwischen statischen und dynamischen Verfahren. Bei einem statischen prioritätsbasierten Scheduling wird die Priorität jedes Threads zum Erzeugungszeitpunkt festgelegt. Dieser Wert kann im weiteren Verlauf nicht mehr verändert werden. Aufgrund der damit erzwungenen deterministischen Ordnung zwischen Threads werden statische Prioritäten häufig als Grundlage für Echtzeit-Scheduling eingesetzt. Bei einem dynamischen prioritätsbasierten Verfahren kann die zugewiesene Priorität vom Betriebssystem und in kontrollierter Weise auch vom Benutzer verändert werden. Zum Beispiel kann das SJF-Verfahren auch als Spezialfall eines dynamischen prioritätsbasierten Schedulings angesehen werden; die jeweilige Priorität eines Threads ist in diesem Fall umgekehrt proportional zur geschätzten Länge des nächsten CPU-Bursts. Bei beiden Schedulingvarianten wird bei einem Kontextwechsel ein Prozessor immer dem rechenbereiten Thread mit der höchsten Priorität zugeordnet. Dieser Thread bleibt bei der nichtpreemptiven Variante dieses Verfahrens so lange im Besitz des Prozessors, bis er eine blockierende Systemfunktion aufruft oder freiwillig den Prozessor abgibt. Im Fall des preemptiven Schedulings wird die aktuelle Prozessorzuordnung unmittelbar dann unterbrochen, wenn ein neuer Thread mit höherer Priorität erzeugt oder ein deblockierter Thread mit höherer Priorität erneut rechenbereit wird.
Statische Prioritäten
Dynamische Prioritäten
Preemptive und nichtpreemptive Varianten
5
Aushungerung (Starvation)
Aging
Threads
Bei einem reinen prioritätsbasierten Schedulingverfahren besteht die Gefahr der Aushungerung (Starvation). Zur Aushungerung von Threads mit niedriger Priorität kommt es, wenn die gesamte verfügbare Rechenleistung von Threads höherer Priorität ausgeschöpft wird. Im Extremfall kann es zu einer Monopolisierung eines Prozessors durch einen Thread kommen. Bei statischen Verfahren ist eine Aushungerung oder Monopolisierung nur durch eine Veränderung der Prioritätszuordnung und anschließendem Neustart zu beheben. Im Fall dynamischer Verfahren können auch sogenannte Aging-Techniken angewendet werden, bei denen in kontrollierter Weise Prioritäten von der Systemsoftware reduziert werden, um einer Aushungerung von Prozessen entgegenzuwirken. Multilevel-Scheduling
Kombination Echtzeitbetrieb und zeitunkritische Aufträge
Realisierung
In der Praxis werden häufig mehrere Schedulingverfahren miteinander kombiniert. Durch die Verknüpfung können Schedulingentscheidungen besser auf die jeweilige Betriebsform abgestimmt und mehrere Betriebsformen nebeneinander betrieben werden. Gängige Kombinationen sind eine gleichzeitige Unterstützung von Dialog- und Hintergrundbetrieb oder eine gleichzeitige Unterstützung von Echtzeitaufträgen und zeitunkritischen Aufträgen. In beiden Fällen werden primär alle rechenbereiten dialogorientierten oder zeitkritischen Aufträge ausgeführt. Nur wenn alle diese Aufträge abgearbeitet wurden, wird die eventuell verbleibende Rechenzeit auf unkritische Aufträge aufgeteilt. Auch beim prioritätsbasierten Scheduling müssen in der Praxis Multilevel-Techniken zum Einsatz kommen, um z.B. die Schedulingreihenfolge bei mehreren rechenbereiten Threads mit gleicher Priorität festzulegen. Die einfachste Form der Kombination besteht in einer Unterteilung der Bereit-Liste. Bei diesem Verfahren wird in jeder Teilliste eine andere Schedulingstrategie eingesetzt. Rechenaufträge werden - je nach der gewünschten Betriebsform - der entsprechenden Teilliste zugeordnet. Durch ein zusätzliches Auswahlverfahren wird bei einem Kontextwechsel aus mehreren nicht leeren Teillisten ein geeigneter Thread ausgewählt. Diese Auswahl kann z.B. durch eine Priorisierung der einzelnen Teillisten oder durch ein Zeitmultiplexen entsprechend den FCFS- oder RR-Verfahren gesteuert werden. Durch die Verwendung eines Multilevel-Schedulingverfahrens ist z.B. eine Unterscheidung zwischen zeitkritischen und -unkritischen Threads leicht umsetzbar: Die Bereit-Liste wird zu diesem Zweck in eine nichtpreemptive FCFS- und eine preemptive RR-Teilliste untergliedert. Innerhalb beider Teillisten können Threads entsprechend ihrer Priorität eingeordnet werden. Die im FCFS-Bereich nutzbaren Prioritäten liegen über
5.3
Monoprozessor-Scheduling
den RR-Prioritäten. Durch diese Zuordnung wird automatisch eine Wahl von zeitkritischen Threads aus der FCFS-Schlange priorisiert. Diese Form des Multilevel-Schedulings wird vom Prinzip her z.B. in POSIX.4-konformen Systemen eingesetzt. Hier können sich im Einzelfall die Prioritätsbereiche zwischen FCFS- und RR-Liste überschneiden. Feedback-Scheduling Diese Klasse von Schedulingverfahren berücksichtigt die Historie der Prozesse bei der Auswahl eines rechenwilligen Prozesses. Die Algorithmen beruhen darauf, die Schedulingkriterien periodisch oder bei bestimmten Ereignissen (z.B. Thread wird erneut rechenbereit) in Abhängigkeit vom aktuellen Systemzustand anzupassen. Ein gängiges Beispiel für einen Feedback-Mechanismus ist z.B. das bereits angesprochene Aging beim prioritätsbasierten Scheduling, um einer Aushungerung von Threads vorzubeugen. Auch SJF-Scheduling kann in diesem Zusammenhang als ein prioritätsbasiertes Feedback-Scheduling aufgefaßt werden.
Berücksichtigung der Vergangenheit
Muitilevel-Feedback-Scheduling In vielen Fällen werden im Zusammenhang mit Multilevel-Scheduling Techniken eingesetzt, die eine dynamische Anpassung der Schedulingkritierien im laufenden Betrieb vorsehen. Das Betriebssystem versucht damit, selbst feine Änderungen im Thread- oder Systemverhalten beim Scheduling zu berücksichtigen. Da die Einstufung eines Threads in diesem Fall von seinem früheren Verhalten und der aktuellen Situation des Gesamtsystems abhängig ist, werden diese Verfahren auch als Muitilevel-Feedback-Scheduling bezeichnet. Abb. 5-12 Multilevel-FeedbackScheduling
5 Threads
Eine gängige Variante dieses Schedulingverfahrens basiert auf dem RR-Verfahren und teilt die Bereit-Liste in mehrere Teillisten mit unterschiedlich langer Zeitscheibe sowie einer FCFS-Schlange (siehe Abbildung 5-12). Durch diese Verfeinerung kann die Benachteiligung I/Ointensiver Threads, wie sie bei einem einfachen RR-Scheduling auftritt, vermieden werden. Threads, die ihre Zeitscheibe aufbrauchen, d.h. denen der Prozessor mit Ablauf des Zeitquantums entzogen wird, kommen in eine Teilliste mit längerer Zeitscheibe aber geringerer Priorität (Feedback). Ruft ein Thread vor Ablauf der Zeitscheibe eine blockierende Systemfunktion auf oder gibt er den Prozessor freiwillig ab, verbleibt er in der jeweiligen Bereit-Liste. Zusätzliche Feedback-Mechanismen müssen eingesetzt werden, um geeignet auf eine erneute Veränderung im Burst-Verhalten zu reagieren, hierzu zählt die Verlagerung in Listen mit höherer Priorität bei kürzer werdenden CPU-Bursts. Diese Form des Feedback-Schedulings wird z.B. standardmäßig vom Linux-Betriebssystem zur Verfügung gestellt; Prozeßprioritäten bestimmen dabei für einen Thread direkt die jeweilige Teilliste. Beispiel: Scheduling in UNIX
Gute Auslastung der E/A-Geräte Kernprioritäten
Bevorzugung interaktiver Anwendungen
Eine andere Variante des Multilevel-Feedback-Schedulings wird in einigen auf BSD Unix basierenden Systemen eingesetzt (siehe Abbildung 5-13). Diese Betriebssysteme verfolgen mit dieser Schedulingstrategie zwei Ziele: erstens eine Bevorzugung dialogorientierter Anwendungen und zweitens eine möglichst hohe Auslastung schneller E/A-Geräte. Die Anzahl der Teillisten ist systemabhängig; BSD 4.4 bietet z.B. 128 unterschiedliche Prioritätsstufen an, die auf 32 einzelne Bereit-Listen aufgeteilt werden. Die Listen mit höherer Priorität (negative Werte in Abbildung 5-13) verwalten Threads, die durch den Aufruf einer Systemfunktion im Kern blockiert werden. Durch die hohe Priorität dieser Threads erreicht das Betriebssystem, daß Threads möglichst schnell nach Aufhebung der Blockadesituation den Kern verlassen. Dabei werden die höchsten Prioritäten für die schnellen E/A-Geräte eingesetzt, um diese möglichst gut auszulasten. Mit einer abgestuften Priorität werden langsamere Geräte wie z.B. Maus und Tastatur belegt. Kernprioritäten werden nur vom Betriebssystem vergeben, sie stehen Anwendungsprogrammen nicht unmittelbar zur Verfügung. Aus diesem Grund werden auch dialogorientierte Threads, die an Eingabegeräten warten, gegenüber normalen Threads bevorzugt behandelt. Für den Benutzer ist dies an einer schnellen Reaktion auf Benutzereingaben erkennbar. Werte oberhalb der Kernprioritäten (in Abbildung 5-13 Werte größer oder gleich 0) können für die Festlegung einer Rangordnung mit niedrigen Prioritäten zwischen rechenwilligen Anwendungsthreads eingesetzt werden.
5.3
Monoprozessor-Scheduling
Abb. 5-13 Multilevel-FeedbackScheduling bei UNIX
Der UNIX-Scheduler sucht von hoher Priorität zu niedriger Priorität die erste Liste, die mindestens einen rechenwilligen Thread enthält. Der erste Thread in dieser Liste bekommt den Prozessor zugeteilt. Die Bereit-Listen selbst werden über Round-Robin-Scheduling verwaltet, d.h., bei einem Kontextwechsel kommt der zuletzt ausgeführte Thread an das Ende der entsprechenden Liste. Um die Benachteiligung von dialogorientierten Anwendungen durch RR-Scheduling zu verhindern, verwendet der Scheduler zusätzlich ein Aging-Verfahren. Dabei wird in periodischen Abständen (bei BSD 4.4 alle 40 Millisekunden) die vergangene CPU-Auslastung jedes einzelnen Threads bestimmt und der Prioritätswert proportional zur ermittelten Auslastung erhöht (niedrige Priorität). Dadurch werden CPU-intensive Threads gegenüber I/O-intensiven Threads benachteiligt. Damit das System auf Veränderungen im Burst-Verhalten einzelner Threads reagieren kann, wird bei der Ermittlung der CPU-Auslastung durch einen Thread ein exponentieller Mittelwert berechnet. Vereinfacht fließt dabei »ältere« Information über die mittlere CPU-Auslastung nur zu einem bestimmten Prozentsatz in die Berechnung der aktuellen Auslastung ein [McKusik et al. 1996]:
Dabei gibt oc an, wie träge die Berechnung der Auslastung auf Änderungen reagiert. Für Werte nahe bei 0 werden Änderungen im BurstVerhalten nur sehr langsam vom Schedulingverfahren aufgegriffen, d.h., die Auslastung der letzten 40 Millisekunden (U -40ms ) wird im Gegensatz zur gemittelten Last der letzten Sekunde (U-1s) praktisch
Basis ist RR
Aging
5 Threads
nicht berücksichtigt. Ein Wert für a nahe bei 1 reagiert zwar schnell auf Änderungen, hebt aber die Benachteiligung I/O-intensiver Threads nicht auf. In der Praxis haben sich Werte im Bereich 0.9 bewährt. Beispiel: Windows 9x und Windows NT
Prioritätsklassen
Auch die Microsoft-Betriebssysteme Windows 9x und Windows NT verwenden ein prioritätsbasiertes Multilevel-Feedback-Scheduling. Beide Betriebssysteme unterscheiden insgesamt 31 verschiedene Prioritätswerte, die auf vier verschiedene Prioritätsklassen IDLE, NORMAL, HIGH und REALTIME aufgeteilt werden. Innerhalb jeder Prioritätsklasse kann ausgehend von einer Stufe NORMAL zusätzlich eine Feinabstufung um zwei Stufen nach oben (HIGHEST, ABOVEJSTORMAL) oder nach unten (BELOW_NORMAL, LOWEST) stattfinden. Threads der Klasse REALTIME besitzen die höchsten Prioritäten. Entsprechend vorsichtig sollte die Zuordnung von Werten aus dieser Klasse vorgenommen werden, da REALTiME-Threads selbst gegenüber der notwendigen Ausführung kritischer Betriebssystemfunktionen bevorzugt werden. Belegt ein solcher Thread für längere Zeit den Prozessor, werden eventuell geänderte Seiten im Platten-Cache nicht zurückgeschrieben oder die Aktualisierung der Mausposition findet nur sehr schleppend statt. Der Scheduler legt die Prozessorzuordnung auf der Grundlage von Prioritätsklasse und -stufe fest. Innerhalb einer Prioritätsstufe wird gemäß Round-Robin verfahren. Um die meist dialogzentrierten Anwendungen im Windows-Umfeld besonders zu unterstützen, erhöht der Scheduler automatisch die Priorität aller Threads einer Anwendung, wenn für diese Benutzereingaben anliegen. Da die Graphikoberfläche aus historischen Gründen fester Bestandteil des Betriebssystems ist, kann die Priorität der beteiligten Threads im Vergleich zu UNIXSystemen gezielter erhöht werden. Die Erhöhung wird, wenn keine weiteren Benutzereingaben anliegen, mit jeder Zeitscheibe reduziert, bis der Ausgangswert wieder erreicht wird.
5.4
Strikte Echtzeitsysteme
Echtzeit-Scheduling
Beim Echtzeit-Scheduling steht die Einhaltung von Zeitvorgaben im Vordergrund. Diesem Schedulingziel werden alle anderen Aspekte wie z.B. Maximierung der Betriebsmittelauslastung oder Unterstützung interaktiver Anwendungen untergeordnet. Die Zeitvorgaben werden immer von der jeweiligen Anwendung definiert. In vereinfachter Form gibt man dabei vor, wie lange die Anwendung im Maximalfall für die Reaktion auf einzelne Ereignisse benötigen darf. Man unterscheidet in der Praxis zwischen strikten und schwachen Echtzeitsystemen. Bei strikten Echtzeitsystemen bedeutet die Verlet-
5.4
Echtzeit-Scheduling
zung einer Zeitvorgabe meist eine Katastrophe, die unter allen Umständen zu vermeiden ist. Zum Beispiel ist es für eine Industriesteuerung katastrophal, wenn das Steuerungssystem beim Eintreffen eines Überdrucksignals von einem entsprechenden Sensor nicht rechtzeitig mit dem Öffnen eines Ventils reagiert, um einer Explosion vorzubeugen. Bei schwachen Echtzeitsystemen ist nach Möglichkeit die Überschreitung einer Zeitvorgabe ebenfalls durch geeignete Maßnahmen zu vermeiden, eine konkrete Verletzung hat jedoch keine katastrophalen Auswirkungen. Verspätete Reaktionen haben bei diesen Systemen eher einen störenden Charakter. Ein Beispiel dafür ist die Behandlung multimedialer Daten, bei denen Verzögerungen und Verschiebungen zwischen Ton- und Bildinformation bis zu einem gewissen Grad toleriert werden.
Schwache Echtzeitsysteme
Abb. 5-14 Sporadische und periodische Echtzeitaktivitäten
Formalisierung von Zeitvorgaben Die korrekte Behandlung von Echtzeitanwendungen setzt eine formale Beschreibung aller einzuhaltenden Zeitvorgaben voraus. Die Zeitanforderungen fließen in ihrer Gesamtheit in sehr unterschiedlicher Form in die jeweiligen Schedulingstrategien ein. Allgemein umfaßt eine Echtzeitanwendung eine Menge von Einzelaktivitäten. Jede Aktivität wird durch drei Kenngrößen charakterisiert (siehe Abbildung 5-14a): •
Bereitzeit (r, Ready Time): Frühestmöglicher Ausführungsbeginn einer Aktivität • Frist (d, Deadline): Spätester Zeitpunkt für die Beendigung einer Aktivität • Ausführungszeit (Ae, Execution Time): Worst-Case-Abschätzung für das zur vollständigen Ausführung der Aktivität notwendige Zeitintervall Die Bestimmung dieser drei Werte für jede Einzelaktivität ist essentiell, um mit Hilfe des Echtzeit-Schedulings alle Zeitvorgaben einer Anwendung einhalten zu können. Durch die Bereitzeit wird ausgedrückt, daß
Kenngrößen für Echtzeitaktivitäten
5 Threads
Problematik Worst-CaseAbschätzung der Ausführungszeit
Kenngrößen periodischer Aktivitäten
die Ausführung einer Aktivität vor diesem Zeitpunkt nicht begonnen werden darf, da z.B. notwendige Informationen vorher nicht vorliegen oder bestimmte Ressourcen noch nicht zur Verfügung stehen. Die Frist definiert die einzuhaltende Zeitvorgabe und die Ausführungszeit beschreibt den maximal notwendigen CPU-Bedarf. Aus einsichtigen Gründen gilt dabei Ae < d-r. In der Praxis ist eine Bestimmung der notwendigen Ausführungszeit aufgrund ihres Worst-Case-Charakters in vielen Fällen sehr aufwendig. Außerdem führt diese Abschätzung insgesamt zu einer sehr schlechten Auslastung des Gesamtsystems. Dies gilt insbesondere bei strikten Echtzeitsystemen, da hier genügend Rechenleistung vorhanden sein muß, um alle anfallenden Aktivitäten auch bei einem Worst-Case-Bedarf an Rechenzeit rechtzeitig fertigstellen zu können. In vielen Fällen sind Aktivitäten periodisch, z.B. werden bestimmte Zustände des technischen Systems häufig in festen Abständen abgefragt. Für periodische Aktivitäten hat sich in der Praxis eine Formalisierung über folgende Kenngrößen bewährt (siehe Abbildung 5-14b)i
• • • Dabei legt die Phase anschaulich die Bereitzeit innerhalb der Periode fest. Aus Periode und Phase können durch einfache Berechnungen Bereitzeit und Frist der k-ten Ausführung einer periodischen Aktivität ermittelt werden:
Die Ausführung jeder Aktivität k einer Echtzeitanwendung wird vom Scheduler zu einem bestimmten Zeitpunkt sk (Startzeit) eingeplant. In Abhängigkeit von dieser Startzeit ergibt sich damit zusammen mit der notwendigen Ausführungszeit eine Abschlußzeit ck. Im Fall eines nichtpreemptiven Schedulingverfahrens gilt für ck (im Extremfall):
Bei preemptiven Schedulingverfahren ergibt sich für die Abschlußzeit lediglich die Abschätzung:
5.4
Echtzeit-Scheduling
da u.U. die Ausführung anderer Aktivitäten zwischengeschoben wurden. In beiden Fällen werden Zeiten für notwendige Kontextwechsel und andere Betriebssystemfunktionen nicht berücksichtigt. Einige im nachfolgenden beschriebenen Schedulingverfahren gehen ausschließlich von periodischen Aktivitäten aus. In diesem Fall modelliert man häufig auch sporadische Einzelaktivitäten indem man eine Periodizität dieser Ereignisse unterstellt und statistisch abschätzt. Dabei muß die gewählte Periode gleich dem Zeitintervall sein, das minimal zwischen zwei Ausführungen dieser sporadischen Aktivität vergehen kann. In der Regel ergibt sich daraus eine weitere Verschlechterung der Systemauslastung. Die vorgestellte Formalisierung sporadischer und periodischer Echtzeitaktivitäten ist trotz ihrer Komplexität eine starke Vereinfachung realer Verhältnisse. Insbesondere bleiben komplexe Abhängigkeitsbeziehungen zwischen verschiedenen Aktivitäten z.B. in Form von Datenabhängigkeiten oder Synchronisationsbedingungen bestenfalls in den Werten für Bereitzeit und Ausführungszeit verborgen. Eine entsprechend detaillierte Modellierung der Zeitanforderungen ist zum Teil noch Gegenstand der Forschung; existierende Ansätze sind aufgrund ihrer Zeitkomplexität in der Praxis meist nicht anwendbar.
Sporadische Aktivitäten
Zeit- und ereignisgesteuerte Systeme Es gibt prinzipiell zwei verschiedene Systemmodelle, die bei der Ausführung von Echtzeitanwendungen eingesetzt werden können. Bei der zeitgesteuerten Ausführung werden alle Schedulingentscheidungen durch das Voranschreiten der Zeit ausgelöst. In diesem Fall reagiert der Scheduler ausschließlich auf eintreffende Timer-Interrupts. Die kleinste vorgegebene Periode einer Echtzeitaktivität ist ein ganzzahliges Vielfaches der Frequenz des Timer-Interrupts. Mit jedem »Tick« dieses Interrupts wird die Zeit fortgeschrieben und ggf. ein Kontextwechsel erzwungen. Der Zugriff auf E/A-Geräte findet direkt statt. Wie bereits angesprochen, geht man dabei prinzipiell von einem konfliktfreien Zugriff aus, da alle impliziten und expliziten Abhängigkeiten in der Formalisierung der Einzelaktivitäten bereits aufgelöst worden sind. Insbesondere werden Eingabegeräte wie z.B. Sensoren im Rahmen der Ausführung des Anwendungsprogramms explizit abgefragt (Polling). Der höhere Aufwand einer Interrupt-gesteuerten Geräteverwaltung ist unnötig und kann eingespart werden. Bei der ereignisgesteuerten Ausführung werden Schedulingentscheidungen durch externe und interne Ereignisse ausgelöst. Die Realisierung externer Ereignisse findet in der Regel auf der Grundlage von Interrupts statt und dient der Einbettung aller E/A-Geräte einschließlich der Sensoren und Aktoren zum technischen System. Interne Ereignisse werden durch andere Prozesse im System ausgelöst und werden
Zeitgesteuerte Echtzeitsysteme
Ereignisgesteuerte Echtzeitsysteme
5 Threads
Strikte Echtzeitsysteme sind meist zeitgesteuert
Schwache Echtzeitsysteme sind meist ereignisgesteuert
typischerweise über den Signalmechanismus, dem softwaretechnischen Gegenstück von Interrupts (siehe auch Kapitel 7), weitergegeben. In einer vereinfachenden Sichtweise kann die Eignung der beiden Systemmodelle in Einklang mit der konkreten Struktur der Echtzeitanwendung gebracht werden. Bei strikten Echtzeitsystemen sind alle Anforderungen a priori bekannt. Entsprechend einfach können sie daher auf ein zeitgesteuertes Systemmodell übertragen werden. Zeitgesteuerte Systeme können jedoch nur in sehr eingeschränktem Umfang dynamisch an sich ändernde Anforderungen angepaßt werden. In vielen Fällen unterscheidet man lediglich verschiedene Arbeitsmodi, zwischen denen zu bestimmten Zeitpunkten gewechselt wird. Die Reaktion auf dynamische Änderungen der Umgebung ist dagegen mit einem ereignisgesteuerten Systemmodell leicht zu bewerkstelligen. Da jedoch die Einhaltung aller Zeitvorgaben im Fall dynamischer Systemänderungen erheblich schwieriger nachzuweisen ist, wird dieses Systemmodell vorzugsweise bei Anwendungen mit schwachen Echtzeitanforderungen eingesetzt. Besonders kritisch an diesem Systemmodell ist außerdem, daß das Systemverhalten im Hochlastfall (viele gleichzeitig auftretende Ereignisse) schwer vorhersagbar ist. Statisches Offline-Scheduling
Komplexität
Offline-Scheduling ist meist beschränkt auf strikte Echtzeitsysteme
In besonders kritischen Fällen müssen Anwendungen mit strikten Echtzeitanforderungen einschließlich bestimmter Abhängigkeitsbeziehungen zwischen einzelnen Aktivitäten modelliert werden, damit das System in der vorgegebenen Zeit auf jede noch abzufangende katastrophale Situation reagiert. Dies gilt insbesondere, wenn von der Funktionstüchtigkeit des zu steuernden Systems Menschenleben abhängen, z.B. Echtzeitanwendungen im Bereich der Auto- oder Flugzeugsteuerung. Im allgemeinen Fall sind die dafür notwendigen Schedulingalgorithmen, die eine Einhaltung aller Zeitvorgaben garantieren, NP-vollständig, d.h., zum gegenwärtigen Zeitpunkt sind nur Algorithmen mit exponentiellem Zeitaufwand bekannt. Ein Einsatz dieser Algorithmen im laufenden Betrieb verbietet sich daher von selbst. Bei einem als Offline-Scheduling bezeichneten Verfahren wird das Scheduling in diesen kritischen Fällen zeitlich ganz vor die eigentliche Programmausführung gelegt. Eine hohe Laufzeit der Schedulingalgorithmen zur Ermittlung eines Plans, der alle Zeitvorgaben erfüllt, hat damit keinen negativen Einfluß auf das eigentliche Echtzeitsystem während der Ausführung. Ergebnis der Vorberechnung ist ein vollständiger Plan, der während der Programmausführung in Form einer Tabelle vorliegt. Das Schedulingverfahren selbst wird damit auf einen einfachen Tabellenzugriff reduziert, um in einer bestimmten Phase den
5.4
Echtzeit-Scheduling
nächsten auszuführenden Thread zu bestimmen. Meist wird OfflineScheduling in Verbindung mit einem zeitgesteuerten Systemmodell eingesetzt, d.h., mit jedem Tick wird über den Tabellenzugriff entschieden, ob ein Kontextwechsel durchgeführt werden muß und welcher Thread in diesem Fall einem Prozessor zugeordnet wird. OfflineScheduling setzt außerdem meist periodische Aktivitäten voraus; sporadische Aktivitäten müssen über ihre kleinste statistische Periode abgeschätzt werden. Earliest Deadline First (EDF) Bei diesem Schedulingverfahren wird ein Prozessor immer dem Thread (Aktivität) mit der am nächsten in der Zukunft liegenden Frist zugeordnet. Faßt man Fristen als Prioritäten auf, dann entspricht dieses Verfahren im Prinzip einem prioritätsbasierten Scheduling (siehe auch Seite 117). Beim EDF-Scheduling sind preemptive und nichtpreemptive Varianten möglich. Im nichtpreemptiven Fall bleibt eine Thread-Prozessor-Zuordnung bis zum Aufruf einer blockierenden Funktion oder bis zur freiwilligen Abgabe bestehen. Die Threads werden in der Reihenfolge der Fristen abgearbeitet. Ein Prozessor bleibt untätig (idle), solange die Bereitzeit des nächsten auszuführenden Threads noch nicht erreicht wurde. Die preemptive Variante führt einen Kontextwechsel durch, sobald ein Thread mit einer näher in der Zukunft liegenden Frist rechenbereit wird, z.B. weil nach dem Eintreffen eines Interrupts eine blockierende E/A-Operation beendet oder die Bereitzeit eines zweiten Threads mit kürzerer Deadline überschritten worden ist.
EDF
Preemptive und nichtpreemptive Varianten
Abb. 5-15 Beispiel für drei Echtzeitaktivitäten
Die nichtpreemptive Variante von EDF ist nicht optimal, d.h., das Verfahren findet nicht immer eine Abarbeitungsreihenfolge für alle Threads unter Einhaltung aller Zeitvorgaben, obwohl es eine solche Thread-Anordnung gibt. Ein Beispiel dafür ist in Abbildung 5-15 dargestellt. Die Prozesse P1 bis P3 werden gemäß EDF in der Reihenfolge ihrer Frist bearbeitet: P1, P2 und anschließend P3. P1 kann zum Zeitpunkt 0 begonnen und zum Zeitpunkt 2 beendet werden. P2 wird aufgrund der späten Bereitzeit erst zum Zeitpunkt 4 angefangen und zum Zeitpunkt 6 beendet. Im Zeitintervall 2 bis 4 ist der Prozessor daher
Nichtpreemptives EDF ist nicht optimal
5
Threads
untätig. Der frühestmögliche Beginn von P3 zum Zeitpunkt 6 verletzt dessen Frist, da er im schlimmsten Fall erst zum Zeitpunkt 10 beendet ist (siehe Abbildung 5-16a). Eine nichtpreemptive Abarbeitungsreihenfolge P1 (Zeitintervall 0-2), P3 (Zeitintervall 2-6), P2 (Zeitintervall 6-8) erfüllt dagegen alle Bereitzeiten und Fristen (siehe Abbildung 5-16b). Abb. 5-16 Nichtpreemptive Schedulingreihenfolgen mit und ohne Fristverletzung
Preemptives EDF
Abb. 5-17 Preemptive Schedulingreihenfolge nach EDF ohne Fristverletzung
Die preemptive EDF-Variante hat mehr Freiheitsgrade, da durch die Unterbrechbarkeit begonnener Aktivitäten eine Schedulingentscheidung nur auf die Menge der rechenbereiten Threads, d.h. Threads, deren Bereitzeit bereits erreicht oder überschritten wurde, beschränkt werden kann. Angewendet auf das Beispiel aus Abbildung 5-15 wird ebenfalls mit der Abarbeitung von P1 begonnen. Diese ist zum Zeitpunkt 2 beendet. Die preemptive Variante beginnt in diesem Fall jedoch mit der Ausführung von P3, dem (einzigen) rechenbereiten Thread mit kürzester Frist. Zum Zeitpunkt 4 muß diese Prozessorzuordnung unterbrochen werden, da Thread P2 mit einer noch kürzeren Frist rechenbereit wird. Nach der fristgerechten Beendigung von P2 zum Zeitpunkt 6 kann die Verarbeitung von P3 weitergeführt werden. Diese endet ebenfalls fristgerecht zum Zeitpunkt 8 (siehe Abbildung 5-17). Allgemein kann man beweisen, daß die preemptive Variante immer eine Abarbeitungsreihenfolge mit Einhaltung aller Zeitvorgaben findet, solange mindestens eine solche Reihenfolge tatsächlich existiert.
5.4
Echtzeit-Scheduling
Rate-Monotonic-Scheduling (RMS) Rate-Monotonic-Scheduling [Liu und Layland 1973] ist ein verbreitetes preemptives Verfahren im Echtzeitbereich, das ebenfalls auf statische Prioritäten abgebildet werden kann. Ähnlich wie beim EDF-Scheduling wird durch eine geeignete Interpretation des Prioritätsbegriffs eine implizite Abarbeitungsreihenfolge für alle Threads festgelegt, bei der alle Zeitvorgaben erfüllt werden können. RMS eignet sich nur zur Beschreibung periodischer Systeme, es kann jedoch ebenfalls durch eine statistische Abschätzung der kleinsten Periode auch für sporadische Ereignisse eingesetzt werden. RMS ordnet Prioritäten in Abhängigkeit von der Periode der einzelnen Aktivitäten zu; Aktivitäten mit der höchsten Frequenz (= kleinste Periode) wird dabei die höchste Priorität zugewiesen, Aktivitäten mit der geringsten Frequenz (= größte Periode) erhalten die tiefste Priorität. Diese Zuordnung steht im Widerspruch zu einer intuitiven Prioritätsfestlegung nach »Wichtigkeit« einer Aktivität, wie sie z.B. auch beim EDF-Scheduling durch die Frist der einzelnen Aktivitäten nahegelegt wird. Durch diese Zuordnung werden hochfrequente Aktivitäten minimal verzögert, so daß die Wahrscheinlichkeit einer Fristverletzung für diese Aktivitäten minimal ist. Das Verfahren führt jedoch zu einer stärkeren Zerstückelung niederfrequenter Aktivitäten aufgrund einer höheren Anzahl an Kontextwechseln zu Threads mit höherer Priorität. Neben der unproblematischen Abbildung der Zeitvorgaben einer Menge von Aktivitäten auf Prioritäten besitzt das Verfahren außerdem die günstige Eigenschaft, daß durch eine einfache Berechnung im Rahmen der Vorgaben sichergestellt werden kann, ob eine Echtzeitanwendung auf einer bestimmten Rechnerhardware ohne Fristverletzung ausgeführt werden kann oder nicht. Im einfachen Fall reicht zur Beantwortung dieser Frage die Überprüfung der Gesamtbelastung durch die Echtzeitanwendung aus. Dabei errechnet sich die Auslastung aus der Periode und der Ausführungszeit aller Aktivitäten A1 bis An:
Gilt für diesen Wert die Abschätzung (siehe dazu [Liu und Layland 1973]):
so liefert RMS beweisbar immer eine Ausführungsreihenfolge, bei der alle Zeitvorgaben eingehalten werden können. Umgekehrt bedeutet diese Abschätzung, daß bei einer geeigneten Auswahl der Hardware
RMS
Hohe Frequenz = Hohe Priorität
Einfache Handhabbarkeit
5 Threads
Hardwareauslastung mindestens 70% Kritische Instanz
maximal 30% der vorhandenen Ressourcen ungenutzt bleiben. Ist der konkrete Auslastungswert höher als die Schranke UM n , muß die Ausführbarkeit auf der Basis der sogenannten kritischen Instanz festgestellt werden. Dabei wird für einen Periodendurchlauf ab dem Zeitpunkt 0 ermittelt, ob alle Aktivitäten unter der Annahme einer Phase Ahk=0 für alle k fristgerecht durchgeführt werden können. Ist dies der Fall, dann gilt die fristgerechte Ausführung aller Aktivitäten auch für alle gültigen Phasenverschiebungen Ahk ungleich 0. Best Effort
Best-Effort
Ausreichend schnelle Hardware
Keine Garantien
In der Praxis sind Earliest-Deadline-First- oder Rate-Monotonic-Scheduling primär im Bereich strikter Echtzeitsysteme angesiedelt. Die schwierige Worst-Case-Abschätzung der Ausführungszeiten aller sporadischen und periodischen Aktivitäten unter Berücksichtigung relevanter Abhängigkeitsbeziehungen verhindert eine breite Verwendung. Außerdem reagieren diese Ansätze meist sensibel auf Programmänderungen, da die Erhöhung einzelner Ausführungszeiten meist Anpassungen der gesamten Programmstruktur nach sich zieht. Aus diesem Grund werden zumindest im Bereich schwacher Echtzeitsysteme häufig Betriebssysteme und Schedulingstrategien ohne besondere Echtzeitfähigkeiten eingesetzt. Man geht in diesem Fall von einer hinreichend leistungsfähigen Rechnerhardware aus, die eine Einhaltung von Zeitvorgaben auch in einer ungünstigen Lastsituation mit hoher Wahrscheinlichkeit gewährleistet kann (Best Effort). Die Tauglichkeit von Hardware, Betriebssystem und Anwendung wird dabei stichpunktartig unter künstlichen oder realen Lastbedingungen getestet. Es ist offensichtlich, daß diese Form der Echtzeitprogrammierung keine verbindlichen Garantien abgeben kann. Durch viele Unbestimmtheiten im Detailverhalten, durch Swapping oder durch Programmfehler ist die fristgerechte Ausführung kritischer Aktivitäten der Echtzeitanwendung letztendlich nicht sichergestellt. Beispiel: POSIX.4
Scheduling in POSIX.4
Bei POSIX.4-konformen Betriebssystemen wie z.B. Solaris von SUN, QNX oder Linux werden FCFS- und RR-Verfahren in Kombination mit Prioritäten in Form eines Multilevel-Schedulings eingesetzt. Dadurch ergibt sich insbesondere im Fall von FCFS eine preemptive Variante, die einem über FCFS eingeplanten Thread den Prozessor entzieht, sobald ein Prozeß mit höherer Priorität z.B. mit der Beendigung einer E/A-Operation rechenbereit wird. In der Summe unterscheiden sich damit beide Verfahren letztendlich nur in der maximalen Dauer der Prozessorzuordnung: Während bei der FCFS-Variante diese Zu-
5.5
Multiprozessor-Scheduling
Ordnung potentiell unbeschränkt lange andauern kann, wird sie bei der RR-Variante nach oben durch die Länge der Zeitscheibe begrenzt. Die in diesem Standard definierte Mischung von fest vorgegebenen Schedulern auf der Grundlage von FCFS und RR und einer optionalen, systemspezifischen Schedulingstrategie entspricht dem aktuellen Stand der Technik. Preemptives prioritätsbasiertes FCFS-Scheduling wird insbesondere im Echtzeitbereich eingesetzt, da EDF-Scheduling und RMS verhältnismäßig einfach darauf abgebildet werden können. Prioritätsbasiertes RR-Scheduling hat sich dagegen im Dialog- und Hintergrundbetrieb bewährt. In der Regel können damit auch Anwendungen mit schwachen Echtzeitanforderungen sinnvoll ausgeführt werden, solange die Zeitvorgaben entsprechend moderat sind (Best Effort). Besondere Verfahren stehen in Form des dritten Schedulers SCHED_OTHER zur Verfügung. Hier können je nach System z.B. explizite EDF-Verfahren mit entsprechend umfangreichen Informationen bereitgestellt werden. In Linux wird in diesem Fall z.B. das Standard-Schedulingverfahren mit dynamischen Prioritäten eingesetzt. Es sind jedoch auch gerade im Hinblick auf die Unterstützung von Echtzeitsystemen weitergehende Verfahren vorstellbar. Die Zusammenführung der drei Schedulingverfahren, die bei mehreren Threads in einem System koexistieren können, wird ebenfalls über den Prioritätsmechanismus erreicht. Konzeptionell gibt es eine Menge von Prioritätswerten, in denen die FCFS- und RR-Prioritäten eingebettet werden. Für beide Schedulingstrategien steht ein Intervall an nutzbaren Prioritäten zur Verfügung, das durch den Aufruf der Funktionen: int sched_get_priority_min ( int policy ) int sched_get_priority_max ( int policy )
ermittelt werden kann. Dabei definiert das Argument policy, für welches Prioritätsintervall der minimale oder maximale Wert abgefragt werden soll (z.B. SCHED_FIFO für das FCFS-Prioritätsintervall). In der Regel enthält das FCFS-Intervall Werte mit höherer Priorität als das RR-Intervall, damit die Ausführung von Echtzeitanwendungen sichergestellt werden kann.
5.5
Multiprozessor-Scheduling
Multiprozessorsysteme ermöglichen im Gegensatz zu Monoprozessorsystemen die echt parallele Ausführung mehrerer Threads. Auf den ersten Blick scheint die Verfügbarkeit mehrerer Prozessoren die Schedulingproblematik zu vereinfachen, da mehrere rechenwillige Threads in den Besitz eines Prozessors gelangen können und damit die Konkurrenz um nur einen Prozessor entschärft wird. Dies gilt insbesondere, wenn im System genügend »Rechenlast« in Form unabhängiger
Kombination Echtzeitund Dialog/Batchbetrieb
FCFS- und RR-Prioritäten
5 Threads
Abhängigkeiten schränken erreichbaren Leistungsgewinn ein
Anwendbarkeit von Monoprozessorstrategien
Threads vorhanden ist. Durch Abhängigkeiten zwischen den einzelnen Threads ist die höhere nominale Leistung eines Multiprozessorsystems jedoch häufig nicht voll ausschöpfbar. Abhängigkeiten können durch eine notwendige explizite Synchronisation zwischen mehreren Threads einer Anwendung (siehe auch Kapitel 6) oder implizit durch den Zugriff auf gemeinsame Ressourcen innerhalb der Systemsoftware (z.B. Zugriff auf dasselbe E/A-Gerät) entstehen. Ein Großteil der Kontrollflüsse ist dadurch zu einem bestimmten Zeitpunkt blockiert und die Anzahl an rechenwilligen Prozessen unterschreitet in vielen Fällen die vorhandene Prozessoranzahl. Eingeschränkt auf das Scheduling unabhängiger Threads ergeben sich bei einem Multiprozessorsystem keine wesentlichen Änderungen. Jede Monoprozessorstrategie kann auf einfache Weise auf mehrere Prozessoren übertragen werden. Die Verfügbarkeit mehrerer Prozessoren gleicht sogar häufig etwaige Nachteile einzelner Schedulingverfahren aus. So wird z.B. die Wahrscheinlichkeit für einen KonvoiEffekt beim FCFS-Scheduling reduziert, da Threads mit langem CPUBurst zwar weiterhin jeweils einen Prozessor dauerhaft belegen, aber I/O-intensiven Threads weitere Prozessoren als Ausweichkandidaten zur Verfügung stehen. Außerdem kann z.B. bei prioritätsbasierten Verfahren die Problematik mehrerer rechenwilliger Threads mit gleicher Priorität entschärft werden. Scheduling eng kooperierender Threads
Gleichzeitige Ausführung kooperierender Threads ist essentiell
Erheblich schwieriger wird das Scheduling abhängiger Threads mit dem Ziel, den maximalen Parallelitätsgrad zu erreichen und dadurch nebenläufig programmierten Anwendungen eine Leistungssteigerung zu ermöglichen. Von zentraler Bedeutung für die Realisierung dieses Schedulingziels ist die explizite Kennzeichnung eng kooperierender Threads durch die Anwendung und darauf aufbauend die gleichzeitige Zuordnung mehrerer Prozessoren an diese Thread-Gruppe. Nur in diesem Fall können die Threads unmittelbar und mit maximaler Geschwindigkeit über den gemeinsamen Speicher kommunizieren. Auch eventuell notwendige Blockadezeiten lassen sich dadurch auf ein absolutes Minimum reduzieren. Werden dagegen zwei oder mehrere dieser kooperierenden Threads zeitlich versetzt ausgeführt, ist die Ausführungsgeschwindigkeit einer nebenläufigen Anwendung im Extremfall vergleichbar mit der auf einem Monoprozessorsystem. Viele nebenläufige Anwendungen reagieren dabei bereits auf kleinste Verschiebungen bei den relativen Ausführungszeiten der interagierenden Threads mit deutlichen Leistungseinbußen; dies ist mit einer der Gründe, warum viele nebenläufige Anwendungen das Potential an Parallelarbeit auf den meisten multiprozessorfähigen Betriebssystemen nur selten ohne besondere Vorkehrungen ausschöpfen können.
5.5
Multiprozessor-Scheduling
Thread-Prozessor-Zuordnung Man unterscheidet bei Schedulingverfahren für Multiprozessoren außerdem zwischen einer statischen und dynamischen Thread-Prozessor-Zuordnung. Bei einer statischen Bindung bleibt eine einmal getroffene Zuordnung zwischen einem Thread und einem Prozessor bestehen, d.h., ein Thread wird in aufeinanderfolgenden Aktivierungen immer auf demselben Prozessor ausgeführt. In diesem Fall kann eine eventuell vorhandene Unterstützung zur Speicherung von Kontextinformationen bei MMU-TLB und Cache von der Systemsoftware genutzt werden. Mit zunehmender Größe dieser Zwischenspeicher können Cache-Einträge für einen gerade ausgeführten Thread auch nach einem Kontextwechsel bis zu einem gewissen Zeitpunkt im Cache verbleiben. Ist diese Kontextinformation bei der nächsten Ausführung des Threads noch im Zwischenspeicher enthalten, kann man die ansonsten notwendige Warmlaufphase der Caches verkürzen oder sogar ganz vermeiden. Bei einer dynamischen Zuordnung kann ein Thread bei jedem Kontextwechsel auf einem beliebigen anderen Prozessor des Systems weiter ausgeführt werden. Man spricht dann auch von einem symmetrischen Mehrprozessorbetrieb [Nehmer 1975]. Eine eventuelle Nutzung von Kontextinformation ist in diesem Fall meist unrealistisch, da die Wahrscheinlichkeit für eine identische Prozessorzuordnung in aufeinanderfolgenden Aktivierungen mit der Anzahl an Prozessoren immer kleiner wird. In besonders ungünstigen Fällen kann durch dieses Verfahren die Systemleistung aus Sicht einer Anwendung sogar unter das Niveau eines Monoprozessorsystems fallen, da nach jedem Kontextwechsel immer mit vollständig erkalteten Caches begonnen werden muß.
Statische Prozessorzuordnung
Dynamische Prozessorzuordn ung
Load Sharing Die einfachste Form des Multiprozessor-Schedulings verwendet eine zentrale Bereit-Liste für alle Prozessoren. Bei diesem auch als Load Sharing bezeichneten Verfahren wird bei jedem Kontextwechsel auf einem der Prozessoren der nächste auszuführende Thread aus der zentralen Liste ausgewählt. Da nur eine Liste vorhanden ist, kann im Prinzip jedes bekannte Schedulingverfahren für Monoprozessoren eingesetzt werden. Zur Vermeidung von Inkonsistenzen muß jedoch eine geeignete Koordinierung beim Zugriff und bei der Manipulation der Zustandslisten erfolgen. Im einfachsten Fall verwaltet dazu ein ausgezeichneter Prozessor alle Zustandslisten und führt entsprechende Operationen im Auftrag anderer Prozessoren aus. Viele aktuelle Betriebssysteme arbeiten nach diesem Verfahren, da es die schnelle
Entsteht in einfacher Form aus einem MonoprozessorScheduling
5 Threads
Hohe Thread-Last ist wichtig
Nachteile
Bereitstellung einer multiprozessorfähigen Version des Betriebssystems auf der Grundlage einer schon existierenden Version für Monoprozessorsysteme erlaubt. Die zweite Form der Konsistenzwahrung besteht in einer geeigneten Synchronisation der Listenzugriffe, die in diesem Fall von jedem Prozessor des Systems selbständig ausgeführt werden. Bei genügend vielen unabhängigen Threads kann mit diesem Verfahren eine gleichmäßige Verteilung der Rechenlast auf alle Prozessoren erreicht werden. Die gleichzeitige Ausführung kooperierender Threads auf unterschiedlichen Prozessoren wird bei diesem Verfahren aus einsichtigen Gründen in keinster Weise garantiert. Entsprechende Leistungssteigerungen für nebenläufig programmierte Anwendungen sind daher in der Regel auch nicht zu erwarten. Nachteilig an diesem Verfahren ist auch, daß es meist in Kombination mit einer dynamischen Thread-Prozessor-Zuordnung angewendet wird und eventuell vorhandene Unterstützung für Kontextinformation zur Vermeidung kalter Zwischenspeicher nicht eingesetzt werden kann. Außerdem wird durch dieses Verfahren die maximale Prozessoranzahl nach oben sehr stark beschränkt, da alle Prozessoren direkt oder indirekt über einen ausgezeichneten Prozessor auf die zentralen Listen zugreifen und damit die zentrale Thread-Verwaltung sehr schnell zu einem Flaschenhals wird [Nehmer 1977]. Gruppen-Scheduling
Explizite Gruppierung von Threads
Nachteile
Gruppen-Scheduling erlaubt die Kennzeichnung kooperierender Threads durch die Anwendung. Es gibt verschiedene Varianten dieses Verfahrens, die auch als Gang-Scheduling [Feitelson und Rudolph 1990], Group-Scheduling [Jones und Schwarz 1980] oder Co-Scheduling [Gehringer et al. 1987] bezeichnet werden. Allen Varianten gemeinsam ist die Behandlung einer Gruppe von kooperierenden Threads als eine Einheit. Zur Ausführung kommt eine solche ThreadGruppe nur bei genügend vielen »freien« Prozessoren, so daß in einem Schedulingschritt allen Threads innerhalb der Gruppe ein eigener Prozessor zugeordnet werden kann. Die gleichzeitige Ausführung kooperierender Threads ist damit gewährleistet. Nachteilig an diesem Verfahren ist ein in vielen Fällen ungünstiges Verhältnis zwischen vorhandener Prozessoranzahl und Größe der verschiedenen Thread-Gruppen. So können z.B. mehrere Prozessoren während einzelner Schedulingintervalle keinem Thread zugeordnet werden, da keine weitere Thread-Gruppe entsprechender Größe rechenwillig ist. In diesem Fall ergibt sich eine verminderte CPU-Auslastung für das Gesamtsystem. Besonders gering fällt die CPU-Auslastung aus, wenn z.B. nur größere Thread-Gruppen existieren, von
5.5
Multiprozessor-Scheduling
denen aufgrund der Gruppengröße nie zwei Gruppen gleichzeitig im Besitz von Prozessoren sind. Die resultierende Auslastung kann dann bis auf 50% fallen. Außerdem besteht die Schwierigkeit, mit wachsender Gruppengröße ausreichend viele freie Prozessoren zum Schedulingzeitpunkt bereitzustellen. Dadurch ergibt sich zwangsläufig eine Benachteiligung großer Thread-Gruppen, der durch zusätzliche Mechanismen entgegengewirkt werden muß. Dedizierte Prozessorzuordnung Bei dieser Form des Multiprozessor-Schedulings wird die Thread-Prozessor-Zuordnung für die gesamte Ausführungszeit der Anwendung getroffen. Auch bei diesem Verfahren wird durch die Gruppierung kooperierender Threads eine Vorgabe bezüglich der benötigten Prozessoranzahl getroffen. Entscheidet sich der Scheduler für eine bestimmte Thread-Gruppe, so stehen der zugehörigen Anwendung die zugeordneten Prozessoren bis zur Programmterminierung zur Verfügung. Solange die Gesamtanzahl an Threads innerhalb der Anwendung die Prozessoranzahl nicht übersteigt, kann jedem Thread ein dedizierter Prozessor zugewiesen werden. Durch die langfristige Thread-Prozessor-Bindung kann dieses Verfahren den in der Implementierung enthaltenen Parallelitätsgrad am besten ausschöpfen. Dem Verfahren liegt dabei die Annahme zugrunde, daß genügend viele Prozessoren zur Verfügung stehen und daß die Maximierung der CPU-Auslastung kein primäres Optimierungsziel mehr darstellt. Bei heutigen Multiprozessorsystemen ist diese Annahme nur in Einzelfällen gerechtfertigt, da die maximale Prozessoranzahl aufgrund vieler technischer Schranken auf 32 bis 64 Knoten begrenzt ist. Bei Parallelrechnersystemen mit vielen tausend Prozessoren, aber ohne gemeinsamen Speicher, ist dieses Schedulingverfahren jedoch gegenwärtig sehr weit verbreitet.
Feste Prozessorzuteilung für die gesamte Ausführungszeit
Maximaler Leistungsgewinn ist möglich
Viele Prozessoren sind wichtig
Activity Working Set Eine interessante Verallgemeinerung der Scheduling-Problematik für Multiprozessoren beschreibt die Activity Working Sef-Theorie [Gehringer et al. 1987]. Sie überträgt Aspekte der Working Set-Theorie aus dem Bereich der seitenbasierten virtuellen Speicherverwaltung auf Threads und Prozessoren. Prozessoren werden in dieser Analogie mit Kacheln im physischen Adreßraum eines Rechners gleichgesetzt, Threads mit Seiten virtueller Adreßräume. Dabei kann man kooperierende Threads mit verschiedenen Seiten desselben virtuellen Adreßraums identifizieren, während unabhängige Threads in dieser Analogie mit Seiten aus verschiedenen Adreßräumen gleichgesetzt werden
Analogie Working Set-Theorie
5
Gedankenmodell
Threads
können. Ein in Ausführung befindlicher Thread entspricht damit einer in einer Kachel eingelagerten virtuellen Seite. Die Problematik unbenutzter Prozessoren aufgrund ungünstiger Größen bei den Thread-Gruppen ist mit einer Speicherfragmentierung zu vergleichen. Auch hier unterscheidet man zwischen einer internen Fragmentierung, d.h., mehrere der zugeordneten Prozessoren sind blockiert oder zum aktuellen Zeitpunkt nicht aktiv, und einer externen Fragmentierung, bei der Prozessoren aufgrund ungünstiger Gruppengrößen ungenutzt bleiben. Von zentraler Bedeutung ist die Frage nach der Existenz einer Lokalitätsmenge bzgl. der Threads analog zu der Seitenlokalitätsmenge aufgrund der ausgeprägten Referenzlokalität vieler Awendungen. Die Größe dieser Menge bestimmt die benötigte Anzahl an Prozessoren, um eine möglichst gute Leistungssteigerung zu erzielen. Wenn die Analogie trägt, würden zusätzliche Prozessoren nur noch eine unwesentliche Steigerung der Leistung ermöglichen. Dagegen reduzieren zuwenig Prozessoren die Systemleistung drastisch und führen - wenn die Anwendung immer wieder neue Prozessoren anzufordern versucht - unter Umständen zu globalen Effekten, die mit dem Seitenflattern verglichen werden können. Zum gegenwärtigen Zeitpunkt besitzt die Activity Working SetTheorie den Stellenwert eines Gedankenmodells, mit dessen Hilfe über Analogieschlüsse interessante Lösungsansätze für die Scheduling-Problematik in Multiprozessorsystemen abgeleitet werden können. Eine praktische Anwendung der Theorie wird durch eine Reihe von Problemen behindert. Unter anderem setzt eine realistische Bewertung der Theorie Anwendungen mit einem hohen Parallelitätsgrad voraus, damit die Existenz einer Aktivitätsmenge und deren Lokalitätseigenschaften mit statistischen Methoden untermauert werden kann. Nur wenige Anwendungsfelder erfüllen zur Zeit diese Bedingung; viele dieser Anwendungen z.B. aus dem Bereich der Mehrgittersimulation besitzen zudem sehr reguläre Aktivitätsmuster, die nicht direkt verallgemeinert werden können.
5.6
Thread-Unterstützung durch APIs
Durch die verstärkte Unterstützung der nebenläufigen Programmierung in Form von Kernel- oder User-Level-Threads und durch die wachsende Anzahl an Anwendungen mit schwachen Echtzeitanforderungen haben sich die Programmierschnittstellen im Bereich Scheduling und Thread-Verwaltung stark gewandelt. Die angebotenen Funktionen lassen sich folgenden Gruppen zuordnen:
• • •
Erzeugung neuer Adreßräume mit initialem Thread Erzeugung zusätzlicher Threads innerhalb eines Adreßraums Wahl und Parametrisierung des Schedulers
5.6 Thread-Unterstützung durch APIs
• • •
Terminierung einzelner Threads Terminierung aller Threads Weitergabe des Terminierungsgrunds an andere Threads
Im nachfolgenden werden die einzelnen Funktionsgruppen am Beispiel der 32-Bit-Programmierschnittstelle Win32 von Windows 9x und Windows NT/2000 (siehe z.B. auch [Cohen und Woodring 1998], [Solomon und Russinovich 2000]) sowie für UNIX-ähnliche Systeme am Beispiel des entsprechenden POSIX-Standards vorgestellt ([Vahalia 1996], [Nichols et al. 1996]). Erzeugung neuer Adreßräume mit initialem Thread Beim Aufbau neuer Adreßräume mit einem initialem Thread dominieren die Speicheraspekte. Wie bereits in Kapitel 4 skizziert, können die notwendigen Adreßräume auf zwei Arten erzeugt werden: 1. Ein neuer, leer initialisierter Adreßraum wird mit dem auszuführenden Programmcode direkt überlagert (Create). 2. Ein neuer Adreßraum entsteht als identische Kopie auf der Basis von Copy-on-Write aus einem bereits vorhandenen Adreßraum (Fork). Die Win32-Programmierschnittstelle enthält Create-basierte Funktionen zum Aufbau neuer Adreßräume. Im Zentrum steht dabei die Funktion:
Win32
Boolean CreateProcess ( Pfadname, Argumente, ...)
Diese Funktion erzeugt einen initialen Adreßraum und überlagert ihn mit dem in einer Datei enthaltenen Programmcode. Der Name der Programmdatei wird durch den Parameter Pfadname festgelegt. Zusätzliche Argumente für die Anwendung werden durch den zweiten Parameter übermittelt. Weitere Parameter legen u.a. die Zugriffsrechte für den erzeugten Adreßraum und den initialen Thread, einen Normal- oder Debug-Modus für die Thread-Ausführung und die initialen Schedulingparameter fest. Der initiale Thread wird unmittelbar im Anschluß an die Adreßraumerzeugung ab einer festen Adresse, die sich aus dem jeweiligen Format der angegebenen Programmdatei ergibt, ausgeführt. An dieser Adresse befindet sich in der Regel eine Routine, die im wesentlichen System- und sprachspezifische Initialisierungen vornimmt. In einem letzten Schritt ruft diese Funktion je nach eingesetzter Programmiersprache eine bestimmte Anwendungsfunktion auf (z.B. main() im Fall von C- oder C++-Programmen). Im UNIX-Umfeld werden neue Adreßräume einschließlich des initialen Threads durch die Abspaltung vom Ausgangsadreßraum erzeugt. Die Abspaltung wird durch den im POSIX.l-Standard festgelegten System-Call fork() eingeleitet:
POSIX
5 Threads
Zusammen mit der Abspaltung des Adreßraums des Kindprozesses findet gleichzeitig eine Gabelung des Kontrollflusses statt, d.h., sowohl der aufrufende Thread als auch der neu erzeugte Thread führen die Programmausführung unmittelbar mit der Beendigung des forkAufrufs fort. Durch den Rückgabewert (im obigen Beispiel in der Variable cpid gespeichert) können die beiden Threads unterschieden werden. Der aufrufende Thread erhält als Rückgabewert die Prozeßidentifikation des neu erzeugten Threads (cpid = child pid), der neu erzeugte Thread erhält dagegen den Wert 0 als Ergebnis des f ork-Aufrufs. Die Übermittlung der eigenen Prozeßidentifikation an den Kindprozeß ist nicht nötig, da diese von jedem Thread durch den Aufruf der Funktion getpid() bei Bedarf ermittelt werden kann. Durch eine Verzweigung in Abhängigkeit von cpid kann sich anschließend die Programmausführung in Vater- und Kindprozeß unterscheiden. Zusätzlich zu fork() steht in UNIX-Systemen die Funktion execve ( Pfadname, Argumente, Environment )
mit verschiedenen darauf aufbauenden Varianten zur Verfügung, die eine Überlagerung des vorhandenen Adreßraums mit dem in Pfadname angegebenen Programmcode ermöglicht. Diese Funktion entspricht im wesentlichen dem CreateProcess() der Win32, mit dem Unterschied, daß der Adreßraum des aufrufenden Threads statt eines initial leeren Adreßraums überlagert wird. Auch hier wird die Programmausführung ab einer vorgegebenen Adresse mit einer systemund sprachspezifischen Initialisierung und der anschließenden Anwendungsausführung begonnen. Erzeugung zusätzlicher Threads innerhalb eines Adreßraums Win32
Durch weitere Funktionen können zusätzliche Threads innerhalb eines bereits vorhandenen Adreßraums erzeugt werden. Im Win32-Kontext geschieht dies durch den Aufruf der Funktion: HANDLE CreateThread ( Schutzattribute, Stackgröße, Startadresse, Parameter, ... )
5.6 Thread-Unterstützung durch APIs
Die Funktion gibt im Erfolgsfall einen Deskriptor (HANDLE) für den neu erzeugten KL-Thread zurück. Threads, die im Besitz eines anderen Thread-Deskriptors sind, können bestimmte Kernoperationen mit Bezug auf diesen Thread ausführen. Die erlaubten Rechte werden dabei durch die Schutzattribute festgelegt. Das zweite Argument gibt die von dem neu erzeugten Thread benötigte Größe des Laufzeitkellers an. Der neu erzeugte Thread beginnt ab der angegebenen Startadresse, meist die Anfangsadresse einer im Adreßraum enthaltenen Anwendungsfunktion, mit der Programmausführung. Zusätzlich kann ein einzelner 32-Bit-Parameter an den neu erzeugten Thread übergeben werden. Um auf diesen Wert zugreifen zu können, muß an der angegebenen Startadresse eine Funktion mit der entsprechenden Signatur definiert sein. Viele UNIX-Systeme bieten ebenfalls eigene KL-Thread-Realisierungen und entsprechende Zugriffsfunktionen an. Aufgrund der verschiedenen Dialekte wird auf eine Vorstellung der systemspezifischen Thread-Realisierungen verzichtet. Statt dessen wird die ThreadErweiterung Pthreads des POSIX-Standards (POSIX4.a) vorgestellt. Diese Erweiterung erlaubt die Modellierung von UL- und KLThreads. Viele UNIX-Systeme bieten Pthreads bereits, heute neben der proprietären Thread-Realisierung an oder werden in der näheren Zukunft diesen Standard unterstützen. Ein neuer Thread wird bei dieser Erweiterung mit dem Aufruf der folgenden Funktion erzeugt: in pthread_create ( pthread_t *tid, pthread_attr_t *attr, void * (*start)(void * ) , void *arg )
Im Zentrum steht die Angabe der vom neu erzeugten Thread auszuführenden Startfunktion start. Dem Thread kann ein 32 Bit großes Argument beliebigen Typs (arg) beim Start übergeben werden. Außerdem legt die Signatur der Startfunktion die Rückgabe eines 32 Bit großen Wertes bei der späteren Terminierung des Threads nahe. Der Rückgabewert der Funktion pthread_create() selbst gibt Aufschluß über den Erfolg der Thread-Erzeugung (0 = erfolgreich). Die Identifikation des neu erzeugten Threads wird über den Referenzparameter tid an den aufrufenden Thread zurückgegeben. Über die Attributstruktur attr können die Umsetzung in UL- oder KL-Threads gesteuert, die Größe und Position des Laufzeitkellers festgelegt und zusätzliche Schedulinginformationen angegeben werden. Wahl und Parametrisierung des Schedulers Die meisten Betriebssysteme verfolgen eine feste Schedulingstrategie, die aus Sicht der Anwendung nur über die vorhandenen Schedulingparameter wie z.B. Priorität beeinflußt werden kann. In Fällen wie
POSIX
5 Threads
Win32
dem kooperativen Scheduling von Windows 3.x sind sogar keine Schedulingparameter zu belegen. Multilevel-Schedulingverfahren lassen sich dagegen in eingeschränktem Umfang über die Schedulingparameter an unterschiedliche Anwendungsprofile (z.B. die Unterscheidung zwischen Dialog- und Hintergrundbetrieb) anpassen. Abgesehen von der Parameterbelegung und einer eventuellen Änderung dieser Werte im laufenden Betrieb sind jedoch keine weiteren Funktionen im Bereich Scheduling-API notwendig. Typische Vertreter einer solchen API mit festem Scheduler sind Win32-basierte Systeme und frühere UNIX-Versionen. Bei beiden Systemen werden Prioritäten für jeden Thread festgelegt, die vom Scheduler berücksichtigt werden. Im Fall von Win32 kann die Prioritätsstufe eines Threads bei dessen Erzeugung mit angegeben werden. Zusätzlich kann die Priorität jederzeit über einen entsprechenden Thread-Deskriptor (HANDLE) durch GetThreadPriority(HANDLE)
abgefragt und mit SetThreadPriority(HANDLE,Priority)
innerhalb der Prioritätsklasse verändert werden. Die Prioritätsklasse eines Threads selbst wird mit Hilfe der Funktion SetPriorityCLASS() verändert; die aktuelle Prioritätsklasse liefert die Funktion GetPriorityClass() . UNIX
POSIX
Bei UNIX-Systemen mit Multilevel-Feedback-Scheduling kann die Thread-Priorität bei der Erzeugung nicht angegeben werden. Der neu erzeugte Thread erbt die Priorität des erzeugenden, d.h. des fork()oder execve() -aufrufenden Threads. Dynamisch ist die Priorität ähnlich den Win32-Funktionen über getpriority() abruf- und änderbar. Die Multi-User-Fähigkeit von UNIX erlaubt dabei für normale Benutzerprozesse lediglich eine Reduzierung der Priorität (renice) und damit die Betonung einer Hintergrundcharakteristik. Die Erhöhung der Priorität ist nur bei sogenannten Superuser- oder RootThreads möglich. Der POSIX.4-Standard geht im Schedulingbereich erheblich weiter. Der Standard schreibt die beiden Schedulingstrategien FCFS und RR fest vor. Eine dritte proprietäre Schedulervariante kann von der Systemsoftware zusätzlich zur Verfügung gestellt werden. Die Schedulerzuordnung kann durch den Aufruf der Funktion int sched_setscheduler ( pid_t pid, int policy, struct sched_param *param )
5.6
Thread-Unterstützung durch APIs
gesetzt und dynamisch verändert werden. Das Argument pid bezeichnet den Thread, dessen Schedulingeigenschaften verändert werden sollen. Änderungsrechte werden auf UNIX-spezifische Gruppenstrukturen für Threads abgebildet; sie sind nicht Bestandteil des Standards. Über den Parameter p o l i c y wird FCFS-Scheduling ( S C H E D _ F I F O ) , RR-Scheduling (SCHED_RR) oder der proprietäre Scheduler (SCHED_OTHER) festgelegt. Über das dritte Argument param können die notwendigen Parameter an den Scheduler übergeben werden. Die FCFS- und RR-Scheduler benutzen nur das Feld sched_priority innerhalb dieses Records. Eine Erweiterung dieser Struktur um zusätzliche Parameter für SCHED_OTHER ist möglich; z.B. können auf diesem Weg zukünftige EDF-Realisierungen mit den notwendigen Werten für Bereitzeit, Frist und Ausführungszeit versorgt werden. Zusätzliche Funktion erlauben die Abfrage und die Änderung einzelner Teile der Schedulinginformation. Von besonderer Bedeutung im Hinblick auf das Scheduling ist auch die Funktion sched_yield(). Durch den Aufruf dieser Funktion gibt ein Thread explizit den Prozessor ab. Terminierung einzelner Threads Die Terminierung eines einzelnen Threads t kann auf zwei Arten geschehen: Sie kann erstens durch den Thread t selbst herbeigeführt oder zweitens über einen anderen Thread t' erzwungen werden. In beiden Fällen wird in diesem Abschnitt davon ausgegangen, daß der terminierte Thread nicht der letzte Thread innerhalb des Adreßraums ist. Die selbständige Terminierung eines Threads ist der einfachere Fall, wenn man davon ausgeht, daß dieser Thread vor der eigentlichen Terminierung alle belegten Systemressourcen einschließlich eventueller, von ihm angeforderter Sperren freigibt. Die eigenverantwortliche Terminierung kann implizit geschehen, indem das Ende der main()-Funktion im Fall von C oder der bei der Thread-Erzeugung angegebenen Startfunktion erreicht wird. Eine explizite Terminierung durch den Thread selbst ist auch über den Aufruf spezieller Funktionen möglich, z.B. über ExitThread() im Bereich Win32 oder pthreadexit() im Fall der Pthread-Erweiterung. Beiden Funktionen kann ein 32-Bit-Argument übergeben werden, das den genauen Grund der Terminierung wiedergibt. In den meisten Betriebssystemen gibt es darüber hinaus Funktionen, die es einem Thread erlauben, die Terminierung eines zweiten Threads zu erzwingen. Im Fall der Win32-Schnittstelle kann dies durch den Aufruf der Funktion TerminateThread ( HANDLE, ExitCode )
Eigenverantwortliche Terminierung
Terminierung anderer Threads Win32
5
POSIX
Threads
erfolgen. Dabei identifiziert HANDLE den zu terminierenden Thread und Exitcode entspricht dem Terminierungsgrund. Die erzwungene Terminierung eines Threads durch einen anderen Thread birgt potentiell die Gefahr der dauerhaften Belegung von Systemressourcen oder nicht auflösbarer Blockadesituationen aufgrund belegter Sperren. Dies ist z.B. bei der Win32-Funktion TerminateThread() der Fall, d.h., alle belegten Ressourcen und Sperren bleiben belegt. Insbesondere können im Bereich globaler dynamischer Bibliotheken (DLLs) Inkonsistenzen auftreten, wenn sich der terminierte Thread zu diesem Zeitpunkt in einer entsprechenden Bibliotheksfunktion befand. Die Pthread-Erweiterung bietet ebenfalls eine Funktion zur Terminierung anderer Threads an: int pthread_cancel ( pthread_t t )
Auch hier besteht die Möglichkeit der dauerhaften Belegung von Ressourcen und Sperren, wenn die Terminierung des angegebenen Threads t zu einem ungünstigen Zeitpunkt stattfindet. Der PthreadStandard geht an dieser Stelle jedoch weiter und unterscheidet drei Thread-Typen hinsichtlich ihrer Terminierbarkeit:
• • •
Der Thread ist nicht von außen terminierbar; Aufrufe der Funktion pthread_cancel () sind für diesen Thread wirkungslos. PTHRE'AD_CANCEL_DEFERRED: Die Terminierung eines Threads wird verzögert, bis dieser einen Terminierungspunkt (Cancellation Point) passiert. PTHREAD_CANCEIW\SYNCHRONOUS: Die Terminierung des Threads kann jederzeit erzwungen werden. PTHREAD_CANCEL_DISABLE:
Die Terminierbarkeit ist ein Attribut des jeweiligen Threads. Dadurch kann die versehentliche Terminierung kritischer Threads durch die Anwendung verhindert werden. Von besonderer Bedeutung ist die verzögerte Terminierung, bei der entsprechende Terminierungspunkte explizit durch den Aufruf von pthread_testcancel() definiert werden. Nur an diesen Punkten können entsprechende Threads von außen terminiert werden. Die Terminierung wird genau dann erzwungen, wenn zum Zeitpunkt des Aufrufs eine über pthread_cancel() initiierte Aufforderung vorliegt. Es wird implizit davon ausgegangen, daß eine Terminierung an einem solchen Punkt keine dauerhafte Belegung von Ressourcen zur Folge hat. Die Einhaltung dieser Bedingung obliegt jedoch der Anwendung, sie wird vom System nicht forciert.
5.6 Thread-Unterstützung durch APIs
Terminierung aller Threads Ein Adreßraum wird wieder freigegeben, wenn der letzte enthaltene Thread eigenverantwortlich terminiert. Dieser Thread muß nicht notwendigerweise mit dem initialen Thread bei der Adreßraumerzeugung übereinstimmen. Der angegebene Terminierungsgrund kann in diesem Fall je nach Betriebssystem an einen anderen Thread außerhalb des Adreßraums weitergegeben werden. Ein Thread hat außerdem die Möglichkeit, die Terminierung aller Threads eines Adreßraums und die anschließende Freigabe des Adreßraums selbst zu erzwingen. Die Win32-API stellt zu diesem Zweck die Funktion ExitProcess(ExitCode) zur Verfügung. Eine entsprechende Funktion wird in der Pthread-Erweiterung nicht angeboten, dasselbe Ergebnis wird im Fall von UNIX aber z.B. durch den Aufruf der Funktion exit() erreicht. In allen Fällen werden mit der Terminierung des letzten Threads innerhalb eines Adreßraums alle eventuell noch belegten Ressourcen vom Betriebssystem wieder freigegeben.
Win32
UNIX
Weitergabe des Terminierungsgrunds an andere Threads Die verschiedenen Varianten der Thread-Terminierung erlauben die Übergabe eines meist 32 Bit großen Parameters, der den genauen Grund der jeweiligen Terminierung wiedergibt. In vielen Fällen sind bestimmte Threads daran interessiert, über die Terminierung anderer Threads informiert zu werden und den konkreten Terminierungsgrund übermittelt zu bekommen. Je nach Betriebssystem gibt es verschiedene Realisierungsformen bei der Weitergabe des Terminierungsgrunds. Im UNIX-Umfeld geschieht dies innerhalb der Prozeßhierarchie, die über die Prozeßabspaltung auf der Grundlage des System-Calls fork() aufgebaut wird. Ein Vaterprozeß, oder - falls dieser bereits selbst terminiert ist - der bzgl. der Hierarchie übergeordnete Prozeß (Großvater) kann den Terminierungsgrund eines mit fork() erzeugten Kindprozesses über den Aufruf einer der Funktionen wait ( int *status ) waitpid ( int pid, int *status, int options )
erfahren. In beiden Fällen wird der aufrufende Prozeß blockiert, bis ein Kindprozeß oder im Fall von waitpid() bestimmte Kindprozesse (pid legt dabei fest, welche Prozesse in Frage kommen) terminiert sind. Der jeweilige Terminierungsgrund wird über den Zeiger satus zurückgegeben. In einer weiteren Variante wait3() kann der Zustand eines eventuell terminierten Prozesses auch nicht blockierend erfragt werden. Darüber hinaus existiert auch die Möglichkeit der asynchro-
UNIX
5 Threads
Zombie-Prozeß
POSIX
nen Benachrichtigung über eine Prozeßterminierung auf der Grundlage sogenannter Software-Interrupts (siehe auch Kapitel 7.4). Ein terminierter Prozeß nimmt innerhalb des Betriebssystems den Zustand eines sogenannten Zombie-Prozesses an, wenn der Terminierungsgrund vom Vaterprozeß noch nicht entgegengenommen wurde. Bis auf den PCB, der den Terminierungsgrund enthält, werden dabei alle belegten Systemressourcen (Adreßraum, geöffnete Dateien etc.) zum Zeitpunkt der Terminierung freigegeben. Der PCB selbst kann vom System erst mit der Abfrage des Terminierungsgrunds wiederverwendet werden. Aus historischen Gründen funktioniert der Mechanismus der Weitergabe von Terminierungsinformation in UNIX nur bei Prozessen, d.h. bei Adreßräumen mit jeweils genau einem Kontrollfluß. Bei UNIX-Systemen, die mehrere KL-Threads in einem Adreßraum unterstützen, oder bei UL-Threads kann daher nur der Terminierungsgrund bei der Auflösung des Adreßraums einschließlich aller enthaltener Threads an den Vaterprozeß übermittelt werden. Innerhalb eines Adreßraums definiert die Pthread-Erweiterung des POSIX-Standards eine Weitergabe von Terminierungsgründen an andere Threads desselben Adreßraums. Zu diesem Zweck muß ein Thread, der an der Terminierung eines anderen Threads und der Weitergabe des Terminierungsgrundes interessiert ist, die Funktion pthreadjoin ( pthread_t tid, void **status )
aufrufen. Der aufrufende Thread wird bis zur Terminierung des angegebenen Threads tid blockiert. Der Terminierungsgrund wird über den Zeiger status zurückgegeben. Analog zu UNIX wird der Terminierungsgrund eines Threads vom System zwischengespeichert, bis ein weiterer Thread innerhalb des Adreßraums diesen mittels pthread_join() entgegennimmt. Dieser Mehraufwand kann durch den Aufruf der Funktion pthread_detach ( pthread_t which )
Win32
eingespart werden, wenn eine Abfrage des Terminierungsgrundes in der Anwendung nicht vorgesehen ist. Auf die Terminierung entsprechend markierter Threads kann nicht mehr über phtread_join() gewartet werden. Ein explizites Verfahren wird auch bei Windows 95 und Windows NT angewendet. Jeder Kernel-Thread wird innerhalb des Betriebssystems über einen Thread-Deskriptor (HANDLE) identifiziert. Dieser Deskriptor wird vom Betriebssystem bei der Terminierung des Threads automatisch in einen sogenannten signalisierten Zustand versetzt. Andere Threads, die im Besitz des Deskriptors sind, können durch den Aufruf der Funktionen WaitForSingleObject ( HANDLE tid, Timeout ) WaitForMultipleObjects (...)
5.7
Implementierungsaspekte
blockierend auf die Terminierung eines Threads tid oder im Fall von WaitForMultipleObjects() auf die Terminierung eines Threads aus einer angegebenen Menge warten. Für die Wartezeit kann optional eine obere Schranke Timeout angegeben werden. Mit einem Wert Timeout=0 wird die Funktion nicht blockierend aufgerufen; bei einem Wert INFINITE ist die Wartzeit unbeschränkt. Die Signalisierung des Deskriptors als Folge der Thread-Terminierung befreit alle am Deskriptor blockierten Threads. Der Terminierungsgrund kann anschließend über die Funktion GetExitCodeThread ( HANDLE tid, long *status )
abgefragt werden. Da das Betriebssystem zusätzlich Buch darüber führt, welche Threads noch in Besitz eines bestimmten Deskriptors sind, kann der Terminierungsgrund auch zu einem späteren Zeitpunkt ermittelt werden. Außerdem funktioniert das Verfahren sogar über Adreßraumgrenzen hinweg, da alle Deskriptoren systemweit definiert sind. Im konkreten Fall wird der Kreis der Threads, die potentiell den Terminierungsgrund abfragen können, darauf reduziert, welcher Thread einen bestimmten Deskriptor übermittelt bekommen hat und welche Zugriffsrechte darauf definiert sind. Analog zu einzelnen Threads ermöglicht die Win32-Schnittstelle auch die Ermittlung von Terminierungsgründen, wenn Adreßräume mit allen darin enthaltenen Threads terminiert werden. Die Signalisierung und Synchronisation interessierter Threads findet in diesem Fall durch den Aufruf einer der Funktionen WaitForSingleObject() oder WaitForMultipleObject() unter Angabe des Prozeßdeskriptors statt. Den Terminierungsgrund selbst liefert die Funktion GetExitCodeProcess().
5.7
Implementierungsaspekte
Kontextwechsel-Problematik Jeder Kontextwechsel kostet Rechenzeit, die der Ausführung von Anwendungsprogrammen entzogen wird. Aus diesem Grund ist es ein wichtiges übergeordnetes Ziel, sowohl die Kosten eines einzelnen Wechsels als auch die Gesamtanzahl an durchgeführten Kontextwechseln zu minimieren. Eine Reduktion der Einzelkosten kann im wesentlichen durch eine handoptimierte Implementierung des eigentlichen Kontextwechsels erreicht werden. Der hohe Aufwand der manuellen Programmierung auf Maschinensprachebene amortisiert sich aufgrund der häufig durchgeführten Kontextwechsel bereits bei einer Einsparung von wenigen Instruktionen. Eine zweite wichtige Möglichkeit der Kosten-
Reduktion der Kosten eines Kontextwechsels
5 Threads
Reduktion der Anzahl an Kontextwechseln
reduktion besteht in der oben angesprochenen Bereitstellung unterschiedlicher Gewichtsklassen bei Threads. Die entscheidende Ansatzstelle für eine Reduktion der Gesamtkosten ist die Minimierung der Kontextwechselfrequenz. Im Prinzip geht es dabei um die Auswahl einer Schedulingstrategie, die bei vorgegebener Betriebsart und einem typischen Lastprofil die minimale Anzahl an durchgeführten Kontextwechseln aufweist. Die Bestimmung eines Schwellwerts für eine gerade noch akzeptable Anzahl an Kontextwechseln pro Zeitintervall wird daher von vielen Faktoren beeinflußt:
• • • Einfluß Registersatz
Einfluß TLB und Caches
Kontextinformation
Gesamtgröße der Registerstruktur eines Prozessors Struktur und Umfang des TLB Struktur und Größe der LI- und L2-Caches
Der Aufwand, einen Thread-Zustand zu sichern und zu aktualisieren, wächst proportional mit der Größe des Registersatzes eines Prozessors. Besonders kritisch ist dies bei sogenannten RISC-Prozessoren wie z.B. dem SPARC-Prozessor, die häufig über 32, 64 oder noch mehr Register verfügen. Geht man z.B. von 64 Registern zu je 32 Bit aus, so müssen bei einem Kontextwechsel jeweils 64-4 Byte = 256 Byte gelesen und geschrieben werden. Beim Sichern des aktuellen Registersatzes wird aus Gründen der Speicherkonsistenz meist auf den Hauptspeicher durchgeschrieben (Write Through). Umgekehrt ist die Wahrscheinlichkeit, daß sich der zu ladende Registersatz in einem der Caches befindet, äußerst gering. Insgesamt ergeben sich dadurch z.B. bei einer Speicherzugriffszeit von 60 ns pro 32 Bit minimale Kosten von 8 us für 2-64 Hauptspeicherzugriffe. Dabei wurde noch keine einzige Schedulingentscheidung getroffen. Im Vergleich dazu kann der Registerzustand bei Intel-Prozessoren ab dem 80386 mit jeweils 26 Speicherzugriffen zu je 32 Bit vollständig gesichert und geladen werden. Das ergibt eine minimale Zeit von 3.6 u.s für einen Kontextwechsel, die z.B. von dem Echtzeitbetriebssystem QNX einschließlich der Schedulingentscheidung knapp erreicht wird [QNX 1993]. Struktur und Größe von TLB und Caches haben ebenfalls einen indirekten Einfluß auf die Kontextwechselzeit und damit auf eine sinnvolle Kontextwechselfrequenz. So muß bei einem Adreßraumwechsel der Inhalt des TLB häufig vollständig invalidiert werden. Die sich ergebenden Zusatzkosten bestehen aus der Invalidierung des TLB selbst und aus der reduzierten Ausführungsgeschwindigkeit eines Threads nach einem Kontextwechsel aufgrund eines kalten TLB. In bestimmten Fällen kann auch selektiv nur ein Teil des TLB invalidiert werden, um die TLB-Einträge für den Betriebssystembereich zu erhalten. Besonders günstig wirkt sich ein TLB aus, der zu jedem Eintrag auch die Kontextidentifikation speichern kann (z.B. die SPARCMMU). In diesem Fall kommen keine weiteren Invalidierungskosten
5.7
Implementierungsaspekte
hinzu. Darüber hinaus besteht die Möglichkeit, daß bei einem hinreichend großen TLB noch frühere Einträge des nächsten Threads enthalten sind und die Ausführung mit einem warmen oder gar heißen TLB weitergeführt werden kann. Interessant sind auch MMU-Architekturen, die eine Speicherung des TLB-Inhalts als Teil der Zustandssicherung bei einem Kontextwechsel erlauben. In welchen Fällen sich diese explizite Speicherung gegenüber einem »Kaltstart« des TLB rentiert, hängt im wesentlichen von der Größe des TLB ab. Auch die Plazierung des L2-Caches in der Rechnerstruktur kann Auswirkungen auf die Kontextwechselzeit haben. Wie in Kapitel 4 diskutiert, kann ein L2-Cache vor oder nach der MMU positioniert werden. Im ersten Fall speichert der Cache virtuelle Adressen. Da sich diese Adreßabbildung bei einem Kontextwechsel ändert, müssen in diesem Fall alle L2-Einträge invalidiert werden. Handelt es sich zusätzlich um einen Write Back Cache, d.h., auch Schreiboperationen werden nicht unmittelbar auf den Hauptspeicher durchgeschrieben, muß spätestens mit dem Kontextwechsel der Abgleich der Speicherinhalte zwischen Cache und Hauptspeicher erzwungen werden. Ein Cache, der sich hinter der MMU befindet, speichert dagegen physische Adressen. Eine Invalidierung bestimmter Cache-Einträge ist in diesem Fall nur bei der Änderung der Kachelzuordnung oder beim Nachladen einer virtuellen Seite notwendig. Die Kontextwechselzeiten werden bei dieser Cache-Anordnung nicht erhöht.
Plazierung L2-Cache
Beispiel: Dispatcher für Monoprozessorsystem Threads werden im wesentlichen über den Dispatcher eines Betriebssystems realisiert. Der Thread-Zustand ist mit der Ausnahme rechnender Threads im jeweiligen PCB gespeichert. Bei den rechnenden Threads ist der dynamische Zustand im Prozessor und anderen Systemkomponenten gespeichert. Im nachfolgenden soll eine exemplarische Realisierung der Dispatcher-Funktionen skizziert werden. Dabei wird vereinfachend angenommen, daß es sich um ein Monoprozessorsystem handelt; entsprechender Synchronisationsbedarf beim gleichzeitigen Zugriff auf die jeweiligen Zustandslisten entfällt damit. Alle Dispatcher-Funktionen operieren auf den Prozeßzustandslisten, die aus einzelnen PCBs bestehen. Die Struktur eines PCBs wird dabei vereinfachend durch ein Record beschrieben: Realisierung PCB
Der PCB enthält alle notwendigen Felder zur Speicherung des Zustands. Dazu gehört z.B. ein Bereich r zur Sicherung des Register-
5 Threads
zustands. Die Größe dieses Feldes ist prozessorspezifisch und hängt von der Registeranzahl N_REG ab. Die tatsächliche Realisierung der Listen ist für das Verständnis der Dispatcher-Funktionen unnötig. Es wird daher von einem abstrakten Datentyp List ausgegangen, der im wesentlichen die Funktionen List.Put ( Element ) Element List.Get () Element List.Get ( Key )
Realisierung der Zustandslisten
zur Verfügung stellt. Mit Put kann ein Element zur angegebenen Liste hinzugefügt werden. Die Funktion Get entfernt entweder ein beliebiges Element aus der Liste (ohne Argument) oder ein Element mit der angegebenen Identifikation Key. In beiden Fällen ist das entfernte Element auch der Rückgabewert der Funktion. In der Praxis gibt es sehr unterschiedliche Realisierungsformen für diese Zustandslisten. Neben einfach- und doppelt-verketteten Listen werden häufig auch komplexere Strukturen wie z.B. Hash-Tabellen eingesetzt, um auch aufwendigere Schedulingverfahren effizient zu implementieren. Bei Multilevel-Verfahren werden außerdem zusätzliche Hilfsinformationen verwaltet, damit die Suche nach einer nichtleeren Teilliste in konstanter Zeit ausgeführt werden kann. Die Zustandslisten selbst bilden drei Instanzen des abstrakten Datentyps List mit PCB als dem Listenelement (alle Listen sind initial leer): List(PCB) rechnend = 0; List(PCB) bereit = 0; List(PCB) blockiert = 0;
Dispatcher-Funktionen
Die Dispatcher-Funktion Add fügt den PCB eines neuen Threads in die Bereit-Liste ein: Dispatcher.Add (NewPCB) { bereit.Put(NewPCB); }
Das Betriebssystem wird vor dem Aufruf von Add Speicherplatz für die Seitentabelle anlegen und diese entsprechend initialisieren. Darüberhinaus wird ein initialer Registerzustand festgelegt, bei dem PC und Stackpointer bereits auf die entsprechenden Anfangsadressen im virtuellen Adreßraum zeigen. Bei der Terminierung eines Threads wird der zugehörige PCB aus der Bereit-Liste entfernt: PCB Dispatcher.Retire (pid) { return bereit.Get(pid);
} Die Funktionen Assign, Resign, Ready und Block führen im wesentlichen entsprechende Umverteilungen eines PCBs innerhalb der drei
5.7
Implementierungsaspekte
Zustandslisten durch. Damit die Funktionen Resign und Block auch tatsächlich den vollständigen Prozessorzustand konsistent sichern können, müssen entsprechende Vorkehrungen beim Eintritt in den Kern stattfinden. Der Einfachheit halber wird im nachfolgenden angenommen, daß in beiden Fällen der korrekte Prozessorzustand unmittelbar nach dem Eintritt durch den Aufruf der Funktion MO.ContextSave(reg_save) in einem besonderen Speicherbereich reg_save gesichert wird:
Ein Kerneinthtt findet bei einer asynchronen Unterbrechung oder beim Aufruf eines System-Calls statt
Dispatcher.Assign () { PCB next; next = bereit.Get(); rechnend.Put(next); MO.ContextRestore(next->r);
} Dispatcher.Resign () { PCB current; current = rechnend.Get(); current->r = reg_save; bereit. Put (current) ,-
} Dispatcher.Ready (pid) { PCB p; p = blockiert.Get(pid); bereit.Put(p);
} Dispatcher.Block () { PCB seif; seif = rechnend.Get(); self->r = reg_save; blockiert.Put(seif);
Bei der angegebenen Realisierung wird vorausgesetzt, daß nach dem Verlassen einer Betriebssystemfunktion exakt ein Thread im Zustand »Rechnend« ist. Zur Erfüllung dieser Bedingung ist es notwendig, daß zu jedem beliebigen Zeitpunkt wenigstens ein Prozeß im Zustand »Bereit« ist. Diese Bedingung wird am einfachsten durch einen sogenannten Nullprozeß hergestellt, der nie in einen Wartezustand tritt und daher ständig einem Prozessor zugeordnet werden kann. Durch geeignete Schedulingmaßnahmen muß natürlich sichergestellt werden, daß dieser Nullprozeß nur dann rechnet, wenn kein anderer Thread rechenwillig ist. Die Forderung, nach Verlassen des Nukleus exakt einen Thread im Zustand »Rechnend« vorzufinden, schreibt zwingend vor, nach dem Entzug eines Prozessors durch die Funktionen Resign, Block und Retire immer die Funktion Assign auszuführen. Der Initialzustand des Systems wird durch eine besondere Startroutine hergestellt.
Nullprozeß
Nukleus
5 Threads
Da zu diesem Zeitpunkt in keiner Zustandsliste ein Thread vorgefunden wird, muß diese in der Regel einen ersten Urprozeß anlegen und dessen Ausführung durch Dispatcher.Add(...); Dispatcher.Assign();
initiieren. Im Verlauf des weiteren Bootvorgangs des Betriebssystems werden von diesem Urprozeß alle weiteren Systemprozesse und -Threads erzeugt. Auf der Grundlage des Dispatchers werden alle weiteren Funktionen des Betriebssystems, die potentiell einen Kontextwechsel zur Folge haben können, aufgebaut. Die Aktivierung einer Betriebssystemfunktion kann durch den Aufruf eines System-Calls in einem existierenden Thread oder durch einen Interrupt ausgelöst werden. Letztere signalisieren z. B. die Beendigung einer E/A-Operation-und führen damit zum Aufruf der Dispatcher-Funktion Ready. Timer-Interrupts können darüber hinaus bei einem preemptiven Schedulingverfahren die aktuelle Thread-Prozessor-Zuordnung durch den Aufruf von Resign aufheben. Realisierung von UL-Thread-Packages
setjmpO und longjmp()
yield()
Bei der Realisierung eines UL-Thread-Packages (ULTP) werden die notwendigen Verwaltungsinformationen (Zustandsinformationen und -listen) in den Anwendungsadreßraum verlagert. Die Zugriffsfunktionen einschließlich Scheduler und Dispatcher stehen der Anwendung in Form einer Laufzeitbibliothek zur Verfügung. Die Sicherung und Wiederherstellung des Registerzustands ist systemspezifisch. Im UNIXUmfeld können dafür die Funktionen setjmp() und longjmp() eingesetzt werden, die u.a. die Inhalte aller Register einschließlich PC und Stackpointer in besonderen Datenstrukturen ablegen. Stehen entsprechende Funktionen nicht zur Verfügung, muß der Registerzustand über gesonderte Assemblerroutinen gesichert und restauriert werden. Problematisch kann dabei im Einzelfall der Zugriff auf privilegierte Register wie z.B. Statuswort sein, der je nach Betriebssystem aus Schutzgründen durch einen Wechsel in den privilegierten Zustand realisiert werden muß. Die Microsoft-Betriebssysteme Windows 9x und Windows NT/2000 bieten z. B. besondere Funktionen, die lesenden und bis zu einem gewissen Grad auch schreibenden Zugriff auf privilegierte Register erlauben. Ein expliziter Thread-Wechsel kann in einem ULTP sehr einfach durch eine Funktion yield() realisiert werden. Der Zustand des aufrufenden Threads wird zuerst im zugehörigen PCB im Anwendungsadreßraum gespeichert. Anschließend wird analog zur Kernel-Reali-
5.7 Implementierungsaspekte
sierung ein neuer Thread ausgewählt und durch die Wiederherstellung der Registerinhalte ausgeführt. Bei einer nichtpreemptiven ThreadRealisierung müssen darüber hinaus blockierende System- und Bibliotheksaufrufe durch das ULTP abgefangen werden, damit die resultierende Blockade des Träger-Threads nicht alle Threads im Adreßraum betrifft. Statt des blockierenden Systemaufrufs muß auf eine asynchrone Variante zurückgegriffen werden, die den aufrufenden KLThread selbst nichtblockiert und die eine Beendigung der Operation über einen zusätzlichen Mechanismus (z.B. dem Signal-Mechanismus in UNIX) der Anwendung mitteilt. Allgemein wird also jeder blockierende System-Call und jede blockierende Bibliotheksfunktion X: x () { }
durch eine erweiterte Funktion xULT ersetzt: () { PCB current; Xasync(); current = rechnend.Get () ; blockiert.Put(current); Dispatcher.Assign() ;
} Über den angesprochenen zusätzlichen Kommunikationsmechanismus wird die Anwendung über die Beendigung der angestoßenen asynchronen Operation Xasync() informiert. In diesem Fall muß der blockierte Thread ermittelt und über den Aufruf einer Dispatcher.Ready()-ähnlichen Funktion wieder in die Bereit-Liste eingegliedert werden. Vereinfachend wurde in dem obigen Code-Beispiel angenommen, daß es zu einer blockierenden Funktion eine entsprechende nichtblockierende Funktion gibt. Inder Praxis ist diese Situation nur sehr selten gegeben. Für viele blockierende Funktionen gibt es in modernen Betriebssystemen zwar asynchrone Realisierungen, sie unterscheiden sich jedoch in ihrer Komplexität und in ihrem CodeUmfang sehr stark vom einfachen Funktionsaufruf. In anderen Fällen ist eine Blockade einfach nicht zu umgehen, d.h., blockierende Operationen haben den Stillstand aller UL-Threads in einem Adreßraum zur Folge. Aus diesem Grund werden in vielen ULTPs nur die wesentlichen blockierenden Operationen maskiert. Bei einer preemptiven UL-Thread-Realisierung muß zusätzlich ein zeitgesteuerter Signalisierungsmechanismus ähnlich dem Timer-Interrupt eingesetzt werden, um einen Thread-Wechsel innerhalb des Adreßraums herbeizuführen.
Behandlung blockierender Aufrufe
5 Threads
Umsetzung der POSIX.4-Threads Abbildung auf KLund/oder UL-Jhreads
Die Pthread-Erweiterung schreibt keine feste Realisierung als Kerneloder User-Level-Threads vor. Die konkrete Umsetzung hängt vielmehr von dem jeweiligen Wirtssystem ab, insbesondere, ob dieses mehrere Threads innerhalb eines Adreßraums zuläßt oder nicht. Das Spektrum an möglichen Umsetzungen wird von zwei Realisierungsformen begrenzt:
•
•
Bestimmte Funktionen dürfen nur betroffenen Thread blockieren
Standardisierungsprozeß ist noch nicht abgeschlossen
Pthread = KL-Thread Pthread = UL-Thread
Im ersten Fall wird natürlich vorausgesetzt, daß das Wirtssystem mehrere Threads innerhalb eines Adreßraums unterstützt. Jeder Pthread wird dann 1:1 auf einen KL-Thread abgebildet. Wettbewerb um Ressourcen findet bei dieser Realisierung auf der Ebene des Gesamtsystems statt (PTHREAD_SCOPE_SYSTEM). Bei der einfachen UL-Realisierung werden alle Pthreads auf genau einen KL-Thread abgebildet. Diese Variante kann praktisch auf jedem UNIX-System realisiert werden. Die Threads bewerben sich in diesem Fall innerhalb einer Anwendung um Ressourcen und Rechenzeit (PTHREAD_SCOPE_PROCESS). Bei der User-Level-Realisierung taucht das bereits angesprochene Problem auf, daß beim Aufruf einer blockierenden Betriebssystemfunktion u.U. nicht nur der aufrufende Thread, sondern auch der Träger-Thread blockiert wird. Dadurch findet indirekt ebenfalls ein Wettbewerb um Ressourcen auf Systemebene statt, und ein wesentlicher Vorteil der User-Level-Realisierung geht verloren. Der POSIX-Standard schreibt aus diesem Grund vor, daß bestimmte und häufig benutzte blockierende Funktionen wie z.B. pause(), wait(), write(), open(), read() oder sleep() auch im Fall von PTHREAD_SCOPE_PROCESS nur den betroffenen Thread blockieren. Der Scheduler der User-Level-Implementierung erhält damit die Möglichkeit, einem anderen Thread desselben Adreßraums den Träger-Thread zuzuordnen. POSIX.4-konforme Systeme mit einer Kernel-Level-Implementierung können der Anwendung für jeden Thread die Wahl der Schedulingebene (SCOPE_PROCESS oder SCOPE_SYSTEM) überlassen. Dadurch entstehen Mischformen, bei denen in einer Anwendung gleichzeitig beide Thread-Formen entsprechend ihren jeweiligen Vorteilen eingesetzt werden. Teil des POSIX.4-Standards ist auch die Nutzung von Multiprozessorsystemen in pthread-basierten Anwendungen. Eine bestimmte Menge an Prozessoren (Allocation Domain) wird dabei als Einheit bezüglich des Schedulings aufgefaßt. Aufgrund der Komplexität des Multiprozessor-Schedulings bei kooperierenden Threads ist der Standard an dieser Stelle noch sehr unspezifisch.
6
Speicherbasierte Prozeßinteraktion
Bei dieser Form der Prozeßinteraktion interagieren Prozesse über gemeinsam zugreifbare Speicherzellen. Die Voraussetzungen dafür sind grundsätzlich bei den im Kapitel 3 vorgestellten Laufzeit-Basismodellen A, C und D erfüllt. Eine elementare Aufgabe stellt das exklusive Sperren einer Speicherzelle durch konkurrierende Prozesse dar. Wir setzen zu diesem Zweck die beiden Operationen
Exklusives Sperren einer Speicherzelle
Sperren (SpeicherAdr) Freigabe (SpeicherAdr)
voraus. Prozesse, die sich um den Besitz derselben- Speicherzelle bewerben, benutzen diese Operationen gewöhnlich in einem kritischen Abschnitt wie untenstehend gezeigt:
Die zur Synchronisation benutzte Speicherzelle wird hier als Sperrflag bezeichnet. Es sei angenommen, daß das Sperrflag im Initialzustand frei ist. In ihrem Zusammenspiel müssen die Operationen Sperren() und Freigabe() garantieren, daß die folgende Invariante zu jedem Zeitpunkt erfüllt ist: Es existiert höchstens ein Besitzer eines Sperrflags, d.h., im kritischen Abschnitt darf sich höchstens ein Prozeß aufhalten.
Kritischer Abschnitt
Sperrflag
Sperren() und Freigabe ()
6 Speicherbasierte Prozeßinteraktion
Ein belegtes Sperrflag blockiert nachfolgende Prozesse
Das bedeutet, daß bei zeitlich überlappter Ausführung der Sperren-Operation auf dasselbe freie Sperrflag durch n Prozesse genau ein Prozeß die Operation erfolgreich beenden und damit in seinen kritischen Abschnitt eintreten kann; alle anderen n-1 Prozesse werden auf unbestimmte Zeit in der Operation verzögert. Das Sperrflag ist dann belegt. Durch die Ausführung der Freigabe-Operation wird ein in der Sperren-Operation verzögerter Prozeß fortgesetzt. Er kann damit in den kritischen Abschnitt eintreten und gilt bis zur Ausführung der Freigabe-Operation als neuer Besitzer des Sperrflags. Eine Freigabe-Operation setzt das Sperrflag wieder in den Zustand f r e i , wenn kein weiterer Prozeß den Eintritt in den kritischen Abschnitt wünscht. Um eine faire Behandlung aller Prozesse zu garantieren, sollten alle verzögerten Prozesse in der zeitlichen Reihenfolge bedient werden, in der sie die Sperren-Operation aufgerufen haben. In Abbildung 6-1 ist die synchronisierende Wirkung der Sperren-Operation am Beispiel zweier Prozesse dargestellt, die zeitgleich Zutritt zum kritischen Abschnitt suchen.
Abb. 6-1 Prozeßsynchronisation mittels kritischer
Beispiel: Zugriff auf exklusiv nutzbare Ressourcen
Eine naheliegende Anwendung der Operationen Sperren und Freigabe im Rahmen eines kritischen Abschnitts ist der exklusive Zugriff auf seriell benutzbare Ressourcen wie z.B. einen Drucker, wobei jede Ressource durch ein Sperrflag repräsentiert wird [Dijkstra 1968].
6
Speicherbasierte Prozeßinteraktion
Sperren(SpeicherAdr); /* SpeicherAdr repräsentiert eine Ressource *Benutzung der Ressource; Freigabe(SpeicherAdr);
Mit der Synchronisation von Zugriffen auf Ressourcen sind die Anwendungsmöglichkeiten kritischer Abschnitte jedoch keineswegs erschöpft. So stellen sie auch das geeignete Mittel dar, um den Zugriff von Prozessen auf gemeinsame Daten im Speicher zu synchronisieren. Das folgende Beispiel zeigt, warum hier eine Synchronisation der beteiligten Prozesse notwendig ist. Gegeben sei ein Platzbuchungssystem für Flüge. Alle Sitzplätze eines Flugzeugs sind in einem Feld Sitz
Platz[Anzahl];
organisiert, wobei Sitz eine Datenstruktur ist, die für jeden Sitzplatz den Status (frei oder belegt) und den Namen eines Passagiers im Falle der Sitzbelegung enthält: typedef struct{ enum Status{frei, belegt}; char Kunde[25] } Sitz;
In einer weiteren Variablen int Freiplätze = Anzahl;
wird die Anzahl der freien Plätze in einem Flugzeug gespeichert. Sie wird am Anfang mit einem positiven Wert initialisiert. Es sei nun angenommen, daß k Prozesse, die jeweils ein Terminal bedienen, auf die oben definierten Variablen gemeinsam zugreifen, um unabhängig voneinander Platzbuchungen vorzunehmen. Jeder Prozeß durchläuft dabei die folgenden Anweisungen: PROCESS { int I; LOOP { [1] Warte auf Signal von Terminal; [2] if(Freiplätze>0){ [3] I = SuchePlatz () ; [4] Platz [I] .Status = belegt; [5] Platz [I] .Kunde = ReadName() ; [6] Freiplätze --; [7] Print (I) ; [8] } else Print ("Flugzeug belegt");
} } Wir betrachten nun zwei Buchungsprozesse A und B, die zeitlich überlappt die Anweisungen 1-8 durchlaufen. Prozeß A möchte den Kunden K. Müller und Prozeß B den Kunden M. Zink einbuchen. Die Abbildung 6-2a zeigt die angenommene Ausgangslage. Die Abbildung
Beispiel: Platzbuchungssystem für Flüge
6
Zeitlich verzahnte Anweisungsfolgen von A und B
Inkonsistenter Datenbestand
Speicherbasierte Prozeßinteraktion
6-2b bis Abbildung 6-2d zeigen das Ergebnis bei verschiedenen zeitlichen Verzahnungen der Anweisungsfolgen von A und B, und zwar
b) A1...A8,B1...B8 c) B1...B8,A1...A8 d) A1, B1, A2, B2, A3, B3, A4, B4, A5, B5, A6, B6, A7, B7, A8, B8 Während die Ergebnisse von b) und c) korrekt sind, fand in d) offenbar eine Doppelbelegung des Sitzplatzes 7 statt, wobei die Buchung des Kunden K. Müller durch M. Zink überschrieben wurde. Der Zähler Freiplätze täuscht dagegen zwei zusätzliche Buchungen vor und stimmt mit der tatsächlichen Zahl freier Sitzplätze nicht mehr überein. Die Anweisungsfolge d) hat demnach einen inkonsistenten DatemSestand hinterlassen. Der Grund ist in dem nahezu gleichzeitigen Zugriff der Prozesse auf die Zustandsinformation des Platzes 7 zu suchen. Er führt in beiden Prozessen zu der (falschen) Schlußfolgerung, daß der Platz 7 noch frei ist.
Abb. 6-2 Sitzplatz-Belegungen eines Flugzeugs bei verschiedenen zeitlichen Überlappungen zweier Buchungsprozesse
Konsistenzprobleme der geschilderten Art sind immer dann zu erwarten, wenn Zugriffe auf gemeinsam benutzte Daten durch die beteiligten Prozesse unkoordiniert erfolgen. Für die korrekten Ergebnisse der Anweisungsfolgen b) und c) gibt es eine simple Erklärung: Unterstellt man, daß die Anweisungsfolge 18 einen korrekten Algorithmus darstellt (den man dann als Berechnung bezeichnet), dann hinterläßt jede Ausführung dieses Algorithmus einen konsistenten Folgezustand, vorausgesetzt der Initialzustand war konsistent. Jede Folge von Berechnungen muß dann ebenfalls in
6 Speicherbasierte Prozeßinteraktion
einem konsistenten Endzustand münden. Daraus folgt, daß durch die Serialisierung von unabhängigen Berechnungen, die auf dieselben Daten zugreifen, automatisch ein korrektes Ergebnis erzielt wird. Hoare [Hoare 1972] hat diese Überlegungen formalisiert und ein Axiom angegeben, das die Bedingungen für das Entstehen korrekter Ergebnisse bei parallelen Berechnungen definiert:
Das heißt, wenn I0 ein konsistenter Ausgangszustand und Il, I2 konsistente Folgezustände nach serieller Ausführung der Berechnungen a und b darstellen, dann dürfen bei beliebiger zeitlicher Überlappung von a und b nur die Endzustände Il oder I2 entstehen. Eine Möglichkeit zur Erreichung dieses Zieles ist die Zwangsserialisierung konkurrierender Berechnungen durch kritische Abschnitte. Ein kritischer Abschnitt hat dann den folgenden Aufbau:
Serialisierung
Zwangsserialisierung durch kritische Abschnitte
Sperren(SpeicherAdr); Berechnung; /* Zugriff auf die gemeinsamen Daten */ Freigabe(SpeicherAdr);
Das Sperrflag ist in diesem Fall ein Repräsentant der gemeinsamen Daten, auf die in konkurrierenden Berechnungen zugegriffen wird. Mit kritischen Abschnitten können jedoch nicht nur Konkurrenzsituationen zwischen Prozessen aufgelöst werden, sie bilden auch eine geeignete Basis zur Prozeßkooperation. Angenommen, es existieren zwei Berechnungen read() und write(), die auf einen Puffer, der den gemeinsam benutzten Datenbereich bildet, zugreifen. Zwei Prozesse A und B können dann über den Puffer mittels kritischer Abschnitte wie folgt kommunizieren:
Das Problem eines vollen Puffers in der write-Operation und leeren Puffers in der read-Operation wurde in der Darstellung bewußt ignoriert. In den nachfolgenden Abschnitten werden verschiedene Realisierungsformen der Operationen Sperren und Freigabe vorgestellt und
Kritische Abschnitte und Prozeßkooperation
6
Synchronisationsmechanismen
Speicherbasierte Prozeßinteraktion
Anwendungen diskutiert. Sie werden in der Literatur pauschal als Synchronisationsmechanismen bezeichnet, da sie primär zur Behandlung von Konkurrenzsituationen entwickelt wurden. In Abschnitt 6.1 werden Mechanismen behandelt, die mit Ausnahme atomarer Speicheroperationen keine weitere Unterstützung benötigen. Hardwaregestützte Synchronisation wird in Abschnitt 6.2 diskutiert. Der in Abschnitt 6.3 vorgestellte Semaphor-Mechanismus benötigt zusätzlich Betriebssystemunterstützung. Monitore (Abschnitt 6.4) stellen einen sprachgestützten Ansatz zur Synchronisation dar. In Abschnitt 6.5 wird abschließend eine Übersicht über die in verschiedenen Betriebssystemen angebotenen Möglichkeiten der speicherbasierten Prozeßinteraktion und der dabei einsetzbaren Synchronisationshilfsmittel gegeben.
6.1
Aktive Warteschleife (Busy Waiting)
Mechanismen auf der Basis atomarer Speicheroperationen
//
Die hier vorgestellten Lösungen setzen außer atomaren Speicheroperationen für das Lesen und Schreiben von Speicherzellen keine weitere Unterstützung voraus. Sie basieren auf der Idee, einen Prozeß so lange in einer aktiven Warteschleife in der Sperren-Operation zu verzögern, bis das Sperrflag freigegeben wird. Die unten aufgeführte inkorrekte Lösung zeigt, daß die Problematik schwieriger ist als zunächst vermutet. enum { frei, belegt } Sperrflag = frei; void Sperren () { while (Sperrflag == belegt) NOP; Sperrflag = belegt;
} void Freigabe () { Sperrflag = frei;
} Mehrere direkt aufeinanderfolgende Lesezugriffe auf das Sperrflag durch konkurrierende Prozesse haben zur Folge, daß sie alle das Sperrflag im Zustand frei antreffen. Sie belegen anschließend das Sperrflag und treten damit gleichzeitig in den kritischen Abschnitt ein. Die nachfolgend vorgestellten Algorithmen werden anhand zweier konkurrierender Prozesse A, B diskutiert. Eine Verallgemeinerung auf n Prozesse ist prinzipiell möglich ([Dijkstra 1968], [Peterson und Silberschatz 1985]).
6.1
Mechanismen auf der Basis atomarer Speicheroperationen
Rotierende Berechtigung Die Grundidee bei diesem Algorithmus basiert auf der vollständigen Vermeidung von Wettbewerbssituationen zwischen konkurrierenden Prozessen. In dem Sperrflag Turn wird zu diesem Zweck festgehalten, welcher Prozeß aktuell das Recht hat, in einen kritischen Abschnitt einzutreten. Die Eintrittsberechtigung wird jeweils am Ende eines kritischen Abschnitts an den nächsten Prozeß weitergereicht. Prozesse bilden damit einen logischen Ring, in dem das Eintrittsrecht unter den konkurrierenden Prozessen ständig zirkuliert. Für zwei Prozesse A, B können dann die Algorithmen für Sperren und Freigabe wie folgt beschrieben werden:
T u r n = Wer darf den kritischen Abschnitt betreten?
Processld Turn = A; void Sperren ( Processld Who ) { while (Turn != Who) NOP;
} void Freigabe ( Processld Who ) { if (Who == A) Turn = B eise Turn = A;
} Obwohl diese Implementierung die korrekte Synchronisation der beteiligten Prozesse gewährleistet, weist sie doch einige schwerwiegende Nachteile auf: 1. Die beteiligten Prozesse müssen bekannt und fest sein. 2. Durch den Algorithmus wird eine deterministische Reihenfolge festgelegt, in der die Prozesse in den kritischen Abschnitt eintreten dürfen. Dies widerspricht aber den Erfordernissen einer typischen Konkurrenzsituation. Nimmt ein Prozeß das an ihn weitergereichte Eintrittsrecht nicht wahr, dann können nachfolgende Prozesse mit einem vorliegenden Eintrittswunsch für lange Zeit verzögert werden.
Problematik: Prozeß nimmt Eintrittsrecht nicht wahr
Dekker-Algorithmus Dieser mündlich überlieferte Algorithmus [Dijkstra 1968] behält zwar das Prinzip der rotierenden Berechtigung bei, wendet es aber lediglich bei Vorliegen einer aktuellen Konkurrenzsituation unter den konkurrierenden Prozessen an. Ansonsten können Prozesse sofort in den kritischen Abschnitt eintreten. Zusätzlich zum Sperrflag Turn wird ein Feld benötigt, in dem jeder Prozeß sein Interesse an einen Eintritt in den kritischen Abschnitt bekundet.
Rotierende Berechtigung dient der Auflösung einer Konkurrenzsituation
6 Speicherbasierte Prozeßinteraktion
ProcessId Turn = A; enum { yes, no } Interest[2]; void Sperren ( Processld who ) { Processld Other; if (Who == A) Other = B else Other = A; while (true) { Interest[Who] = yes; repeat if (!Interest[Other]) goto fertig; unt if (Turn != Who); Interest[Who] = no; while (Turn == Other) NOP;
} fertig: ;
} void Freigabe ( Processld Who ) { if (Who == A) Turn = B else Turn = A; Interest[Who] = no;
} Zur Wirkung der Algorithmen hier noch einige Erläuterungen: Der Eintritt in den kritischen Abschnitt für einen Prozeß P ist nur möglich, wenn a) er sein Interesse bekundet hat und b) kein Interesse bei allen anderen Prozessen festgestellt wurde (Verlassen der äußeren Schleife über die goto-Anweisung).
T u r n gibt an, wer in einer Konkurrenzsituation den Vorzug erhält
Diese Eintrittsbedingung garantiert, daß alle potentiell konkurrierenden Prozesse von dem Interesse von P erfahren haben. Die Austrittsbedingung der inneren Schleife sorgt dafür, daß alle Prozesse ihr Interesse aufgeben, die in Turn die Identifikation eines anderen Prozesses vorfinden. Der Prozeß, der in Turn seine eigene Identifikation vorfindet, hält jedoch sein Interesse aufrecht: Er wird dadurch bevorzugt in den kritischen Abschnitt hineingelassen, sobald der im kritischen Abschnitt operierende Prozeß diesen freigegeben hat. In der abschließenden while-Schleife werden alle Prozesse verzögert, die in Turn nicht ihre eigene ProcessId finden. Durch Weiterreichen des Eintrittsrechts in der Freigabe-Operation wird damit immer nur ein Prozeß aus der while-Schleife entlassen, der damit als nächster den kritischen Abschnitt betritt. Peterson-Algorithmus In [Peterson und Silberschatz 1985] wird eine vereinfachte Version des Dekker-Algorithmus angegeben, die auf das Prinzip der rotierenden Berechtigung ganz verzichtet:
6.2 Hardwaregestützte Mechanismen
ProcessId Turn = A; enum { yes, no } Interest[2]; void Sperren ( Processld Who ) { Processld Other; if (Who == A) Other = B else Other = A; Interest[Who] = yes; Turn = Other; while (Interest[Other] and (Turn != Who)) NOP;
} void Freigabe ( Processld Who ) { Interest[Who] = no;
} Bei einer aktuellen Konkurrenzsituation der Prozesse soll derjenige als Sieger hervorgehen, der zuerst sein Interesse bekundet und die Variable Turn gesetzt hat. Bei zwei Prozessen A, B muß deshalb derjenige Prozeß in der while-Schleife verzögert werden, der Turn=Who bei bekundetem Interesse des anderen Prozesses feststellt (er hat dann offenbar Turn als letzter gesetzt).
Voraussetzung: Atomarität und Zwangssehalisierung einzelner Lese- und Schreiboperationen
Zusammenfassung Neben den bereits mehrfach aufgezählten Schwächen haben alle bisher diskutierten Algorithmen - wie bereits einleitend erwähnt - den Nachteil, daß sie nichtblockierende (d.h. aktive) Wartestellungen für ausgesperrte Prozesse realisieren und damit Prozessorzyklen nutzlos vergeuden.
6.2
Aktives Warten verschwendet nutzbare CPU-Zyklen
Hardwaregestützte Mechanismen
Eine denkbar einfache Realisierung der Operationen Sperren und Freigabe erhält man mit Unterstützung des Unterbrechungsmechanismus. Mittels zweier Prozessorinstruktionen Disable und Enable können Instruktionssequenzen zu einer unteilbaren (atomaren) Operation nach untenstehendem Muster zusammengefaßt werden:
Sperren von Interrupts
6
Atomarität durch Interruptsperren
Speicherbasierte Prozeßinteraktion
Die Atomarität der zwischen Disable . . . Enable eingeschlossenen Instruktionen wird durch eine Unterbrechungssperre garantiert, die lediglich durch Hardwarefehler außer Kraft gesetzt werden kann. Das Abprüfen und Modifizieren des Sperrflags kann nun unterbrechungsgeschützt durchgeführt werden: enum {frei, belegt} Sperrflag; void Sperren() { Disable; while (Sperrflag==belegt) { Enable; NOP's; Disable;
} Sperrflag=belegt; Enable;
} void Freigabet) { Sperrflag=frei;
} Problemkreise: Aktives Warten und Monopolisierung
Atomarer Leseund Schreibzyklus
Die vorgestellte Lösung ist jedoch nicht alleine wegen des aktiven Wartens für Anwendungsprozesse ungeeignet, sie bietet außerdem keinen Schutz gegen die Monopolisierung des physischen Prozessors. Bei Mehrprozessorsystemen versagt die vorgestellte Lösung ebenfalls, da die Unterbrechungssperre bei einem Prozessor nicht ausreicht, um den exklusiven Zugriff auf das Sperrflag durch verschiedene Prozessoren sicherzustellen. Eine spezielle Hardware-Instruktion Lock schafft hier Abhilfe. Sie erlaubt das atomare Abprüfen und Setzen des Sperrflags in zwei Speicherzyklen: typedef enum {frei, belegt} LockByte; LockByte Sperrflag; LockByte Lock() { LockByte Temp = Sperrflag; Sperrflag = belegt; return Temp;
} Damit können die Operationen Sperren und Freigabe wie folgt implementiert werden: void Sperren() { while (Lock() == belegt) NOP;
} void Freigabe() { Sperrflag = frei;
}
6.3
Betriebssystemgestützter Mechanismus: Semaphore
Diese Konstruktion wird auch als Spinlock bezeichnet. Spinlocks sollten grundsätzlich nur dann eingesetzt werden, wenn die kritischen Abschnitte aus wenigen ausgeführten Instruktionen bestehen und die Wartezeiten für Prozessoren damit kurz bleiben. In bestimmten Bereichen der Systemsoftware wie z.B. der Prozessorzuteilung bei Mehrprozessorsystemen ist die Verwendung von Spinlocks zwingend erforderlich. Auf Anwendungsebene sollten Spinlocks abgesehen von der Synchronisation eng gekoppelter Thread-Gruppen auf einem Multiprozessorsystem vermieden werden. Anwendungstaugliche Synchronisationsmechanismen blockieren die Verlierer einer Konkurrenzsituation und schalten den Prozessor auf einen ablaufbereiten Thread um. Auf diesen Grundgedanken basieren die im folgenden Abschnitt vorgestellten Mechanismen.
6.3
Spinlock
Schnelle Synchronisation bei Multiprozessoren
Betriebssystemgestützter Mechanismus: Semaphore
Betriebssystemunterstützung für Prozeßsynchronisation ist unumgänglich, wenn Wartesituationen durch Thread-Blockaden und nicht durch aktive Warteschleifen in den Prozessen realisiert werden sollen. Die Operationen Sperren und Freigabe müssen dann direkt auf die im Zustandsmodell definierten Thread-Zustände einwirken.
Blockierende Synchronisationsverfahren
Stellvertretend für verschiedene, in der Literatur vorgestellte Varianten blockierender Synchronisationsmechanismen, wird hier der Semaphor-Mechanismus detailliert behandelt. 6.3.1
Das Konzept
Der Semaphor-Mechanismus stammt von Dijkstra [Dijkstra 1968]. Sperrflags heißen dort in Anlehnung an Signalmasten - z.B. in der Schiffahrt oder in Eisenbahnnetzen - Semaphore. Die Operationen Sperren und Freigabe werden mit P (Passieren) bzw. V (Verlassen) bezeichnet und suggerieren den Eintritt in bzw. Austritt aus einem kritischen Abschnitt. Ein Semaphor besteht aus einem nicht negativ initialisierten Zähler und einer eventuell leeren Schlange mit Verweisen auf Threads, die Eintritt in den kritischen Abschnitt wünschen, aber aufgrund einer aktuellen Konkurrenzsituation vorübergehend blockiert wurden (siehe Abbildung 6-3). Die Operationen P und V lassen sich grob durch die untenstehenden Algorithmen beschreiben, für die atomare Realisierungen existieren müssen.
P- und V-Operation
6
Gemeint ist der dem Aufruf von P() folgende Zustand des Threads Ta .
Speicherbasierte Prozeßinteraktion
void P (Semaphor S) { Zähler(S)--; if (Zähler(S)<0) { Zustand des rechnenden Threads Ta speichern; Thread Ta blockieren; /*RECHNEND->BLOCKIERT*/ Schlange(S) = Schlange(S) bereiten Thread Tb wählen; /*BEREIT-> RECHNEND*/ Zustand von Tb laden;
} } void V(Semaphor S) { if (Zähler(S)<0) { Tb=S.First(); /*Top-Element aus S ausketten*/ Tb deblockieren; /*BLOCKTERT->BEREIT*/
} Zähler(S)++;
} Abb. 6-3 Struktur eines Semaphors
Funktionsweise von P und V
Jede p-Operation löst demnach bei einem Zählerstand des Semaphors kleiner oder gleich 0 eine Blockade des aktuellen Threads Ta und die zwangsläufige Umschaltung auf einen anderen ablaufbereiten Thread T b aus. Die V-Operation befreit bei negativem Zähler einen an dem Semaphor S blockierten Thread aus seiner Blockade und schickt ihn wieder in den Wettbewerb um Zuteilung eines Prozessors. Üblicherweise wird die mit einem Semaphor assoziierte Schlange durch eine FIFOStrategie verwaltet, d.h., blockierte Threads werden in der zeitlichen Reihenfolge bedient, in der sie Zugang zum kritischen Abschnitt gesucht haben. Anforderungen an den Echtzeitbetrieb können es jedoch erforderlich machen, von dieser Regel abzuweichen (siehe hierzu Abschnitt 6.3.4). Die Zustandswechsel »Rechnend«—>»Blockiert« und »Bereit«—> »Rechnend« für einen Thread werden begleitet durch ein Einfrieren der momentanen Registerinhalte bzw. deren Restaurierung. Der ifZweig der P-Operation beschreibt demnach einen vollständigen Pro-
6.3
Betriebssystemgestützter Mechanismus: Semaphore
zessorwechsel, während im if-Zweig der V-Operation lediglich die Deblockade eines blockierten Threads erfolgt. Aktuelle Implementierungen beider Funktionen müssen auch ihre logische Unteilbarkeit garantieren, sonst resultieren ähnliche Konsistenzprobleme wie in Abschnitt 6.1 diskutiert. Dazu werden in Abschnitt 6.3.3 verschiedene Möglichkeiten aufgezeigt. 6.3.2
Beispiele mit Semaphoren
Der Gebrauch des Semaphor-Mechanismus wird nachfolgend anhand einiger klassischer Synchronisations- bzw. Kommunikationsaufgaben demonstriert. 1. Beispiel: Einfacher kritischer Abschnitt Zwei Prozesse A und B konkurrieren um den Eintritt in einen kritischen Abschnitt. Es wird ein Semaphor benötigt, das zu 1 initialisiert wird.
Einfacher kritischer Abschnitt
2. Beispiel: Erzeuger-Verbraucher-System Zwei Prozesse kommunizieren über einen gemeinsamen Puffer. Der Erzeuger füllt den Puffer, während der Verbraucher den Pufferinhalt konsumiert. Die beiden Semaphore leer und voll werden benötigt, um zwischen den Prozessen den Eintritt der Ereignisse »Puffer ist leer« und »Puffer ist voll« zu melden. Anders als bei der Konstruktion kritischer Abschnitte führt in einer Erzeuger-Verbraucher-Konstellation der Erzeuger die V-Operation und der Verbraucher die P-Operation auf dasselbe Semaphor aus. Damit werden Synchronisationsereignisse produziert und konsumiert. Der untenstehende Algorithmus garantiert außerdem, daß sich Erzeuger und Verbraucher in der Benutzung des Puffers abwechseln.
Erzeuger-VerbraucherSystem
6
Speicherbasierte Prozeßinteraktion
3. Beispiel: Verwaltung gleichartiger Betriebsmittel
Betriebsmittelverwaltung
Zwei Prozeduren GetDisc und PutDisc regeln den Zugriff zu einem Pool gleichartiger Betriebsmittel, in unserem Fall Diskettenlaufwerke. Prozesse, die mittels GetDisc ein Diskettenlaufwerk anfordern, sollen nur dann in der Operation blockiert werden, wenn alle Laufwerke belegt sind. Zur Lösung des Problems wird ein Semaphor DiscSem benötigt, dessen Zähler mit der Anzahl n verfügbarer Diskettenlaufwerke initialisiert wird. Ein weiteres Semaphor Mutex dient dazu, die Auswahl eines beliebigen Laufwerks unter wechselseitigem Ausschluß von maximal n konkurrierenden Prozessen durchzuführen. Semaphore DiscSem=N, Mutex=l; enum {frei,belegt} Disc[N] = frei; int GetDisc { int i ; P(DiscSem); P(Mutex); i = l; while (Disc[i]==belegt) i + +; Disc[i]=belegt; V(Mutex); return i;
} void PutDisc(int i) { P(Mutex); Disc [i]=frei; V(Mutex); V(DiscSem) ;
} Ein Prozeß benutzt die Prozeduren GetDisc und PutDisc wie in einem kritischen Abschnitt:
6.3
Betriebssystemgestützter Mechanismus: Semaphore
4. Beispiel: Reader-Writer-Problem (1) Zwei Klassen von Prozessen bearbeiten einen gemeinsamen Datenbereich. Prozesse aus der einen Klasse - genannt die Writer - modifizieren den Datenbereich und beanspruchen deshalb exklusiven Zugriff. Prozesse aus der zweiten Klasse - genannt die Reader - lesen lediglich den Inhalt des Datenbereichs. Sie können diese Funktion parallel zu allen anderen Reader durchführen. Für dieses Problem existieren zwei Lösungen [Courtois et al. 1971].
Reader-Writer: Bevorzugung der Reader
int Readcount=0; Semaphor W, Mutex=l; PROCESS Reader { P(Mutex); Readcount++; if (Readcount==1) P(W); V(Mutex); Lese Daten; P(Mutex); Readcount--; if (Readcount==0) V(W); V(Mutex); } PROCESS Writer { P(W) ; Modifiziere Daten V(W) ; }
5. Beispiel: Reader-Writer-Problem (2) In der zweiten Lösung zu diesem Problem wird dem Schreiben Priorität über das Lesen gegeben: Sobald ein Writer Zutritt zum kritischen Abschnitt erlangt, wird er ihm zum frühestmöglichen Zeitpunkt gestattet. Nachfolgenden Reader wird deshalb der Eintritt in den kritischen Abschnitt erst dann wieder ermöglicht, wenn ein aktueller
Reader- Writer: Bevorzugung der Writer
6
Speicherbasierte Prozeßinteraktion
Lösung mit
Writer den kritischen Abschnitt verlassen hat und sich keine weiteren Writer um Zugriffe zum kritischen Abschnitt bewerben. Zur Lösung des Problems werden fünf Semaphore und zwei Zäh-
fünf Semaphoren
ler Readcount und Writecount zum Zählen der Reader und Writer
Semaphor R erzwingt die Writer-Bevorzugung
Mutex3 stellt sicher, daß maximal ein Reader einem Writer zuvorkommt
benötigt. Das Semaphor W hat dieselbe Bedeutung wie in der vorangegangenen Lösung: Es dient der Realisierung des kritischen Abschnitts, in dem die eigentlichen Datenzugriffe erfolgen. Durch die Semaphoren Mutex1 und Mutex2 wird dafür gesorgt, daß die Zähler Readcount und Writecount durch nur einen Prozeß zu einer Zeit modifiziert werden. Durch das Semaphor R wird die Priorität der Writer über die Reader realisiert: der durch P(R)...V(R) definierte kritische Abschnitt, um den sich Reader und Writer gleichermaßen bewerben, wird von einem Writer erst dann wieder freigegeben, wenn kein weiterer Writer Eintritt in den kritischen Abschnitt verlangt. Im anderen Falle wird das Exklusivrecht von Writer zu Writer weitergereicht. Durch das Semaphor Mutex3 schließlich wird erreicht, daß sich alle Reader nacheinander um den Eintritt in den kritischen Abschnitt bewerben. Bei einer aktuellen Wettbewerbssituation zwischen mehreren Reader und einem Writer wird durch Mutex3 sichergestellt, daß der Writer im ungünstigen Fall einem Reader den Vortritt lassen muß. Mit diesen Erläuterungen können wir die Lösung für das 2. Reader- Writer-Problem wie folgt angeben: int Readcount, Writecount=0; Sempaphor Mütexl, Mutex2, Mutex3, W,R=1;
PROCESS Reader { P(Mutex3); P(R) ; P(Mutexl); Readcount++; if (Readcount==l) P(W); V(Mutexl); V(R) ; V(Mutex3); Lese Daten; P(Mutexl); Readcount--; if (Readcount==0) V(W); V(Mutexl); } PROCESS Writer { P(Mutex2); Writecount++; if (Writecount==l) P(R); V(Mutex2), P(W) ;
6.3
Betriebssystemgestützter Mechanismus: Semaphore
Modifiziere Daten; V(W) ; P(Mutex2); Writecount--; if (Writecount==0) V(R); V(Mutex2);
}
6. Beispiel: Bedingte kritische Abschnitte Häufig taucht bei kritischen Abschnitten das Problem auf, daß der Eintritt von einer Bedingung abhängig gemacht wird, die als Prädikat über den durch den kritischen Abschnitt geschützten Daten definiert ist. Hoare [Hoare 1972] hat diese Situation durch eine Sprachkonstruktion erfaßt: with D when B do S;
wobei D die im kritischen Abschnitt bearbeiten Daten, B eine auf D definierte Eintrittsbedingung und S die Anweisungsfolge darstellen. Eine naive Lösung mittels eines Semaphors Mutex sähe wie folgt aus:
Bedingter kritischer Abschnitt
Semaphore Mutex=l; P(Mutex); while (!B) { V(Mutex); Nop; P(Mutex); } Datenzugriff; V(Mutex);
Dadurch, daß die Auswertung der Bedingung selbst im kritischen Abschnitt erfolgen muß, entsteht zwangsläufig eine aktive Warteschlange (Busy Waiting) - eine Situation, die eigentlich durch die blockierende P-Operation vermieden werden sollte. Im schlimmsten Fall können solche Situationen zum Aushungern (Starvation) aller anderen Prozesse führen, nämlich dann, wenn durch unfaires Thread-Scheduling ein Prozessorwechsel verhindert wird. Eine bessere Strategie ist es, einen Prozeß bei Nichterfüllung der Bedingung B so lange an einem zweiten Semaphor zu blockieren, bis die Daten durch einen anderen Prozeß modifiziert wurden. Erst dann liegt die Notwendigkeit für eine Neuauswertung der Bedingung vor. Die Lösung erfordert zwei Semaphore und einen Zähler. Durch das Semaphor Mutex wird der bedingte kritische Abschnitt betreten bzw. verlassen. Das Semaphor Condsem dient der Realisierung der Wartestellung von Prozessen, die ihre Bedingung nicht erfüllt vorfinden. Der
Aktives Warten
Nur ein anderer Prozeß kann zwischen zwei Überprüfungen die Bedingung herstellen
6
Speicherbasierte Prozeßinteraktion
Zähler waitcount enthält die Anzahl der Prozesse, die auf den Eintritt einer Bedingung warten. Semaphore Mutex=l, Condsem=0; int Waitcount=0; P(Mutex); while (!Bedingung) { Waitcount++; V(Mutex); P(Condsem); P(Mutex); } Datenzugriff; while (Waitcount>0) { Waitcount--; V(Condsem); } V(Mutex); Semaphorlösungen werden schnell komplex und sind daher fehleranfällig
Die Praxis im Umgang mit Semaphoren hat gezeigt, daß Lösungen sehr schnell komplex und damit fehleranfällig werden. In der Forschung wurde deshalb nach überzeugenden Alternativen zum Semaphor-Mechanismus gesucht, die leichter verständliche Lösungen für Synchronisationsprobleme hervorbringen. Eine Alternative stellt das Monitorkonzept dar, das in Abschnitt 6.4 beschrieben wird. 6.3.3
Bei KL-Threads sind die Pund V-Operationen Traps
Unteilbarkeit von P und V garantieren
Implementierungsaspekte
Ein Semaphor-Mechanismus entfaltet seine synchronisierende Wirkung durch adäquate Beeinflussung der Thread-Zustände. Es ist deshalb sinnvoll, ihn auf derselben Abstraktionsebene anzusiedeln wie die Thread-Realisierung, um die enge Verflechtung beider Mechanismen möglichst effizient zu bewerkstelligen. Das bedeutet, daß bei Threads, die durch Multiplexen physischer Prozessoren in einem nur über den Trap-Mechanismus zugänglichen Kern realisiert wurden (KL-Threads, vgl. Kapitel 5), auch der Semaphor-Mechanismus dort zur Verfügung gestellt werden sollte. Die in Abschnitt 6.3.1 skizzierten Operationen P und V sind dann Trap-Routinen. Die Traps werden durch Laufzeitroutinen aktiviert, die im Adreßraum der Prozesse liegen und mit den dort befindlichen Programmen zusammengebunden werden. Die Unteilbarkeit der Operationen p und v wird bei Monoprozessorsystemen durch die verdrängungsfreie Ausführung aller Prozesse realisiert, die gerade eine Trap-Routine im Kern abwickeln. In Mehrprozessorsystemen ist zusätzlich die Klammerung aller Code-
6.3 Betriebssystemgestützter Mechanismus: Semaphore
Sequenzen durch Spinlocks erforderlich, die im Kern auf gemeinsame Daten zugreifen. Die in den Algorithmen dargestellten Zustandswechsel werden im Kern durch einen Dispatcher realisiert, der das Multiplexen der physischen Prozessoren steuert und mit den Umschaltvorgängen auch das Einfrieren/Restaurieren aller Prozessorregister übernimmt (vgl. hierzu Kapitel 5). Handelt es sich dagegen um UL-Threads, dann wird der SemaphorMechanismus in einem Thread-Paket zusammen mit den Funktionen zur Erzeugung und Terminierung der Threads im Adreßraum der Prozesse bzw. Teams zur Verfügung gestellt (siehe Abbildung 6-4). Die Pund V-Operationen sind dann einfache Prozeduren, die die Thread-Synchronisation ohne Inanspruchnahme des Kerns durchführen.
Bei UL-Threads sind die Pund V-Operationen Teil des Thread-Packets
Abb. 6-4 Unterstützung von ULThreads durch ein ThreadPaket als Teil Adreßraumlokaler Laufzeitsysteme
Die Realisierung der Unteilbarkeit der P- und V-Operationen stellt in diesem Fall ein gewisses Problem dar und muß insbesondere mit der Situation fertig werden, die durch Thread-Preemption aufgrund von Interrupts wie z.B. Timer-Interrupts, entstehen kann (vgl. Kapitel 5). Es soll hier eine Lösung skizziert werden, die von genau einem kernbasierten Thread pro Adreßraum ausgeht. Sie basiert darauf, daß zu jedem Zeitpunkt feststellbar ist, ob ein Thread gerade eine Funktion des Adreßraum-lokalen Laufzeitsystems durchläuft. Durch Setzen eines Flags am Beginn jeder Laufzeitroutine und Rücksetzen vor dem Ende läßt sich diese Forderung leicht erfüllen. Wird nun ein Thread durch einen Interrupt - z.B. einen Timer-Interrupt - in den Exception-Modus versetzt, dann muß in der Signalbehandlungsroutine zunächst festgestellt werden, ob die Unterbrechung innerhalb des Laufzeitsystems geschah. Ist dies nicht der Fall, dann kann ohne Gefahr einer Konsistenzverletzung der aktive UL-Thread verdrängt »Rechnend«—>»Bereit« und auf einen neuen ablaufbereiten UL-
Unteilbarkeit von P und V im Fall von UL-Threads ist kritisch
Lösungsansatz bei nur einem KL-Träger-Thread pro Adreßraum
6
Speicherbasierte Prozeßinteraktion
Thread umgeschaltet werden »Bereit«—>»Rechnend«. Wurde der Thread jedoch im Laufzeitsystem unterbrochen, dann kann eine Konsistenzverletzung nicht ausgeschlossen werden. In der Signalbehandlungsroutine darf deshalb nur gespeichert werden, daß dieses Ereignis stattgefunden hat. Vor der Beendigung jeder Laufzeitroutine muß dann geprüft werden, ob Signale eingetroffen sind, die Preemption des UL-Threads bewirken sollen. Sofern dies der Fall ist, wird nun eine Umschaltung des UL-Threads eingeleitet. Damit dieser Vorgang in einer Signalbehandlung ohne Unterstützung des Kerns durchgeführt werden kann, muß der Zugriff auf die zum Interrupt-Zeitpunkt eingefrorenen Prozessorregister möglich sein. Dies läßt sich am einfachsten durch ihre Ablage auf dem Laufzeitkeller des Threads erreichen. 6.3.4
Erweiterungen für die Echtzeitverarbeitung
Bei Echtzeitanwendungen ist es wichtig, daß ablaufbereite Prozesse zeitgerecht abgewickelt werden. Sehr häufig dienen Prozeßprioritäten als Grundlage des Prozessorschedulings. Prozesse hoher Priorität haben dabei Vorrang vor Prozessen niedrigerer Priorität. Dieses Prinzip kann jedoch leicht verletzt werden, wenn die Prozesse um kritische Abschnitte konkurrieren. In Abbildung 6-5 ist eine Situation dargestellt, in der drei Prozesse P1, P 2 , P3 zeitlich versetzt über P(S) in einen kritischen Abschnitt eintreten wollen. Senkrechte Pfeile symbolisieren die Umschaltung des physischen Prozessors. Prozeß P1 hat die höchste, Prozeß P3 die niedrigste Priorität. Da P3 die p-Operation zuerst ausführt, gewinnt er das Rennen um den Eintritt in den kritischen Abschnitt. Aufgrund der zeitlichen Reihenfolge der P-Operationen wird als nächster Prozeß P2 den kritischen Abschnitt betreten, obwohl bereits bekannt ist, daß ein Prozeß mit höherer Priorität (P1) auf den Eintritt wartet. Abb. 6-5 Verletzung der Prozeßprioritäten durch kritische Abschnitte
6.4
Sprachgestützter Mechanismus: Monitore
Für den Echtzeitbetrieb ist es deshalb naheliegend, die Einreihung konkurrierender Prozesse in eine Semaphor-Schlange nicht an der zeitlichen Reihenfolge der Eintrittswünsche, sondern an der Prozeßpriorität zu orientieren. Damit geht zwar die Eigenschaft der Fairneß verloren, das Zeitverhalten des Systems wird jedoch planbarer. Damit sind jedoch noch nicht alle Probleme gelöst. Abbildung 6-6 zeigt eine Situation, in der ein Prozeß P2 einen Prozeß niedriger Priorität (P3) verdrängt, der in einem kritischen Abschnitt arbeitet. Das ist in der gezeigten Konstellation aber ungünstig, da ein Prozeß hoher Priorität (P1) auf den Eintritt in den kritischen Abschnitt wartet, nun aber durch P2 noch länger verzögert wird. Zur Vermeidung dieser Situation wird bei echtzeitfähigen Semaphor-Mechanismen oft das Prinzip der Prioritätsvererbung angewendet [Lampson und Redell 1980]. Es besagt, daß der im Besitz des kritischen Abschnitts befindliche Prozeß dynamisch die höhere Priorität des Prozesses erbt, der zu einem späteren Zeitpunkt den Eintritt in den kritischen Abschnitt sucht. Bei Verlassen des kritischen Abschnitts fällt der Prozeß wieder auf seine alte Priorität zurück. Damit ergibt sich ein gegenüber Abbildung 6-6 geänderter Verlauf der Prozeßabwicklung der zu einer Bevorzugung des Prozesses P1 führt (siehe Abbildung 6-7).
Einreihung in die Warteliste gemäß Priorität
Invertierung der Prioritäten
Prioritätsvererbung
Abb. 6-6 Zeitliche Verzögerung eines Prozesses P1 mit höherer Priorität in einem kritischen Abschnitt durch einen Prozeß P2
Abb. 6-7 Gegenüber Abb. 6-6 geänderter Ablauf der Prozesse durch Prioritätsvererbung von P1 an P3
6.4
Sprachgestützter Mechanismus: Monitore
Ausgehend von den Erfahrungen im Umgang mit dem SemaphorMechanismus haben sich Wissenschaftler in den 70er Jahren damit beschäftigt, für das Konzept des kritischen Abschnitts abstraktere Darstellungsformen zu entwickeln, die sich in Systemsprachen inte-
6
Sprachkonzept Monitor
Speicherbasierte Prozeßinteraktion
grieren lassen und eine übersichtlichere Strukturierung von Synchronisationsproblemen unterstützen. Breite Beachtung fand das MonitorKonzept, das in verschiedene Hochsprachen integriert wurde. Besonders hervorzuheben sind Erweiterungen der Sprachen Pascal [Brinch Hansen 1975], PL/I [Nehmer 1979] sowie die für Telekommunikationsanwendungen standardisierte Sprache CHILL [Sammer und Schwärtzel 1982]. Stellvertretend für alle sprachgestützten Synchronisationskonzepte wird deshalb das Monitor-Konzept nachfolgend vertieft behandelt. 6.4.1
Zusammenfassung der gemeinsamen genutzten Daten und der darauf
Das Konzept
Das Monitorkonzept wurde etwa gleichzeitig von Brinch Hansen [Brinch Hansen 1973] und Hoare [Hoare 1974] eingeführt. Es basiert auf der Idee, die in einem kritischen Abschnitt bearbeiteten Daten zusammen mit den darauf definierten Zugriffsalgorithmen in einer sprachlichen Einheit - dem Monitor - zusammenzufassen: MONITOR Monitorname (Parameter) Datendeklarationen; /* Gemeinsame Daten */
definierten Zugriffsfunktionen
ENTRY Funktionsnamel (Parameter) { Prozedurkörper } ENTRY Funktionsname2 (Parameter) { Prozedurkörper }
ENTRY FünktionsnameN (Parameter) { Prozedurkörper } INIT {Initialisierung} END
Die Initialisierung der Monitordaten erfolgt durch einmaligen Aufruf von Monitorname(aktuelle Parameter);
mit dem implizit der Initialisierungsteil durchlaufen wird. Prozesse benutzen den Monitor durch Aufrufe von Monitorprozeduren in der Form Monitorname.Funktionsname (aktuelle Parameter); Ähnlichkeiten zum Moduloder Klassenbegriff
Diese Konstruktion ist praktisch identisch mit dem Modulkonzept in Hochsprachen wie z.B. in MODULA2 oder dem Klassenbegriff objektorientierter Sprachen, erweitert es aber um eine Synchronisationssemantik. Sie besagt, daß sich Monitorprozeduren bei konkurrierendem Zugriff durch mehrere Prozesse wechselseitig ausschließen, d.h., Monitorprozeduren stellen kritische Abschnitte dar. Der erfolgreiche Aufruf einer Monitorprozedur ist gleichbedeutend mit der Sperre des Monitors, die bis zum Verlassen der Monitorprozedur bestehen bleibt.
6.4
Sprachgestützter Mechanismus: Monitore
Durch die konsequente Anwendung des Monitorkonzeptes resultieren gegenüber der Verwendung des Semaphor-Mechanismus zwei entscheidende Vorteile:
•
Gemeinsam durch mehrere Prozesse bearbeitete Daten werden explizit in der Programmstruktur sichtbar gemacht, da sie in Monitoren organisiert werden müssen. Die an der Schnittstelle bereitgestellten Monitorprozeduren definieren außerdem, welche Zugriffsalgorithmen zu den Monitordaten zulässig sind. Ein Umgehen der Prozedurschnittstelle des Monitors ist nicht erlaubt.
•
Monitore kapseln ebenso wie Module die gemeinsamen Daten ein. Solange die Prozedurschnittstelle eines Monitors nicht geändert wird, bleiben Änderungen der Monitor-internen Datenstrukturen und Algorithmen für Prozesse unsichtbar. Dieses bewährte Prinzip des information hiding begrenzt die Auswirkungen von lokalen Programmänderungen auf das gesamte Programm und erhöht damit die Änderungsfreundlichkeit der Programmstruktur.
Zur eleganten Formulierung bedingter kritischer Abschnitte können innerhalb eines Monitors sogenannte Condition-Variable deklariert werden:
Klare Programmstruktur
Kapselung der Datenstrukturen
Condition-Variable
Condition a;
Jede Condition-Variable steht für eine Bedingung (d.h. ein logisches Prädikat über den Monitordaten), die für die Fortsetzung eines Prozesses in einer Monitorprozedur erfüllt sein muß. Aus diesem Grunde verbirgt sich hinter einer Condition-Variablen eine einfache FIFOSchlange mit Zeigern auf Threads, die auf den Eintritt der Bedingung warten. Eine Wartestellung wird mittels der Operation Conditionvariable.WAIT
eingeleitet. Die Operation hat die folgende Wirkung: a) Der ausführende Thread wird blockiert. b) Ein Zeiger auf den Thread wird in die Schlange der ConditionVariablen aufgenommen. c) Unter den Zutritt suchenden Prozessen wird einer ausgewählt und sein Thread deblockiert. Existiert kein solcher Prozeß, dann wird der Monitor freigegeben.
Condition-Variablen stehen für anwendungsspezifische Bedingungen
Aufruf der W A I T ( ) -
Funktion, wenn die Bedingung für einen Prozeß nicht erfüllt ist
6
Monitor wird implizit verlassen
Aufruf der S I G N A L O -
Funktion, wenn ein Prozeß
Speicherbasierte Prozeßinteraktion
Die Monitorfreigabe erscheint zunächst unverständlich, ist aber notwendig, um anderen Prozessen die Möglichkeit zu geben, den Monitor zu betreten. Nur dadurch besteht die Chance, die Bedingungen irgendwann zu erfüllen, auf die Prozesse an Condition-Variablen warten. Vor Ausführung einer WAIT-Operation muß in einer Monitorprozedur ferner sichergestellt werden, daß sich die Monitordaten in einem konsistenten Zustand befinden. Wird durch die Ausführung einer Monitorprozedur eine Bedingung erfüllt, so wird dies mittels der Operation Conditionvariable.SIGNAL;
die Bedingung herstellt
signalisiert. Durch die SIGNAL-Operation wird bei nichtleerer Schlange der Condition-Variablen wenigstens ein Thread daraus entfernt und die Blockade dieses/dieser Threads aufgehoben. Da jedoch nur ein Prozeß zu einem Zeitpunkt in einem Monitor rechnen darf, stellt sich sofort die Frage, wer dies ist. Da der signalisierende Prozeß ja noch im Besitz des Monitors ist, gibt es offenbar verschiedene semantische Varianten für S I G N A L , die alle die Anforderungen erfüllen, daß a) wenigstens ein Thread die Blockade an der Condition-Variablen überwindet und b) nach Beendigung der SIGNAL-Operation höchstens ein Prozeß im Monitor rechnet. Signal-Varianten
Drei Varianten sollen anschließend näher behandelt werden. Sie weisen besondere Vorzüge hinsichtlich der Unterstützung bestimmter Anwendungsanforderungen auf. Signal-Variante I
Maximal 1 befreiter Prozeß, kein Besitzwechsel
Diese Variante beläßt den signalisierenden Prozeß im Monitorbesitz und veranlaßt, daß bei nichtleerer Schlange der Condition-Variablen ein Thread daraus entfernt wird, wobei der Prozeß allerdings gezwungen wird, sich erneut um den Zutritt zum Monitor zu bewerben. Signal-Variante II
Alle wartenden Prozesse befreien, kein Besitzwechsel
Diese Variante unterscheidet sich von Variante I nur dadurch, daß alle an der Condition-Variablen blockierten Threads aus ihrer Blockade befreit werden. Sie wird besonders vorteilhaft in Situationen eingesetzt, bei denen für mehrere Prozesse potentiell die Bedingungen für eine Fortführung nach einer Signalisierung eingetreten sind.
6.4 Sprachgestützter Mechanismus: Monitore
Signal-Variante III Diese Variante wurde von Hoare vorgeschlagen und basiert darauf, einen Besitzwechsel über den Monitor vom signalisierenden auf genau einen signalisierten Prozeß vorzunehmen, der seine Berechnung sofort im Monitor fortsetzen kann. Als Konsequenz dieser Strategie muß der signalisierende Prozeß den Monitor verlassen und sich um erneuten Zutritt bewerben. Der Vorteil dieser semantischen Variante liegt in der Gewißheit des signalisierten Prozesses, daß er nach seiner Befreiung aus der Blokkade die Bedingung für seine Fortführung vorfindet, da ja kein anderer Prozeß eine Möglichkeit hatte, zwischenzeitlich den Monitor zu betreten (vorausgesetzt, die Bedingung war bei Signalisierung erfüllt). Bei der Hoare'schen SIGNAL-Variante kann ein Prozeß deshalb mittels einer if-Anweisung den Eintritt in die Wartestellung einleiten:
Maximal 1 befreiter Prozeß, Besitzwechsel
Vorteile der Variante III
if (Bedingung=False) condvar.WAIT;
Da signalisierte Prozesse bei den SIGNAL-Varianten I und II diese Gewißheit nicht haben können (insbesondere kann ja auch der signalisierende Prozeß im Anschluß an die SIGNAL-Operation die Bedingung wieder invalidieren), muß dort die Einleitung einer Wartestellung mit einer while-Schleife beginnen: while (Bedingung=False) condvar.WAIT;
Das ist aber nicht weiter tragisch und hat zudem den Vorteil, daß fehlerhafte Signalisierungen nicht zu einer unbeabsichtigten Fortführung wartender Prozesse führen. Ein Nachteil der Variante III ist die hohe Anzahl von ThreadUmschaltungen im Zuge einer Signalisierung, die eine beträchtliche Minderung der Performanz bewirkt. So sind bis zur Fortsetzung des signalisierenden Prozesses zwei komplette Thread-Wechsel erforderlich. Da die SIGNAL-Anweisungen nach aller Erfahrung die letzten Anweisungen innerhalb einer Monitorprozedur bilden, erscheint dieser Aufwand besonders ungerechtfertigt, da der Monitor danach ohnehin freigegeben wird. Hoare [Hoare 1974] hat zur Lösung dieses Problems einige Realisierungsoptimierungen vorgeschlagen, auf die hier aber nicht näher eingegangen wird. Alle drei vorgestellten Varianten der SIGNAL-Operation teilen die Eigenschaft, daß sie bei leerer Warteschlange ohne Wirkung sind. Gelegentlich ist es nützlich, in einer Monitorprozedur den Zustand einer Condition-Variablen zu kennen. Dazu steht die Funktion int condvar.STATUS;
zur Verfügung, die im Rückgabeparameter die Länge der Schlange der betreffenden Gondition-Variablen bereitstellt.
Nachteil der Variante III
Signal auf einer leeren Condition-Variablen
6
Speicherbasierte Prozeßinteraktion
6.4.2
Beispiele mit Monitoren
Werden Monitore als Synchronisationshilfsmittel für die Zugriffssteuerung zu exklusiv benutzbaren Ressourcen verwendet, dann bietet es sich an, jede Ressource durch einen Monitor zu repräsentieren, dessen Prozeduren die erlaubten Zugriffsfunktionen definieren. Ein Monitor Disc zur Synchronisation von Plattenzugriffen könnte dann z.B. den unten gezeigten Aufbau haben: Beispiel: Betriebsmittelverwaltung
MONITOR Disc ENTRY Read (PlattenAdr,SpeicherAdr) {Lesevorgang durchführen} ENTRY Write (SpeicherAdr,PlattenAdr) {Schreibvorgang durchführen} INIT {Gerät in Initialzustand bringen} END
Die Lösung ist denkbar einfach, da sie ohne zusätzliche Monitorvariablen auskommt. Das Konzept der Condition-Variablen wird überhaupt nicht benötigt. Die Benutzung des Monitors durch Prozesse erfolgt einfach durch Aufrufe der Form Disc.Read(Von,Nach);
bzw. Disc.Write(Von,Nach);
Jeder Monitoraufruf realisiert dabei einen kritischen Abschnitt. Der wechselseitige Ausschluß der Monitorprozeduren garantiert dabei den exklusiven Zugriff zur Platte. Häufig ist die Benutzung einer Ressource jedoch nicht durch ein wohldefiniertes Zugriffsmuster wie Read oder Write beschreibbar. Die oben angegebene Lösung versagt bereits, wenn zwischen der Belegung und der Freigabe der Platte eine anwendungsabhängige Zahl von Plattenzugriffen liegt. In diesem Fall ist es nicht mehr sinnvoll, die Plattenzugriffe über entsprechende Monitorprozeduren in den Monitor zu verlegen. Vielmehr muß man sich dann darauf beschränken, im Monitor lediglich Funktionen zum Sperren und Freigeben der Ressource bereitzustellen, die eigentliche Benutzung der Ressource jedoch durch außerhalb des Monitors definierte Funktionen zu bewerkstelligen. Damit geht der eigentliche Vorteil des Monitorkonzeptes jedoch weitgehend verloren. Die nachfolgende Monitorrealisierung trägt diesem Gedankengang Rechnung. Wie bei allen weiteren Beispielen wurde die Semantik der Signal-Variante I unterstellt.
6.4 Sprachgestützter Mechanismus: Monitore
1. Beispiel: Verwaltung einer einzelnen Ressource MONITOR S i n g l e R e s o u r c e int b u s y ; /* b u s y = l : R e s s o u r c e frei b u s y = 0 : R e s s o u r c e b e l e g t */ Condition nonbusy; ENTRY A c q u i r e { w h i l e (busy<=0) n o n b u s y . W A I T ; busy- - ;
} ENTRY Release { busy++; nonbusy.SIGNAL
} INIT {busy=l} END
Sieht man einmal von den Bezeichnungen ab, dann beschreibt dieser Monitor exakt die P/V-Operationen auf ein Semaphor: Acquire stellt die P-Operation dar, Release die v-Operation und das Semaphor wird durch die Condition-Variable nonbusy und den Zähler busy nachgebildet. Prozesse benutzen den Monitor in der bekannten Konstruktion eines kritischen Abschnitts:
Damit ist der Nachweis erbracht, daß Monitore wenigstens die Leistungsfähigkeit des Semaphorkonzepts besitzen. Ihre eigentliche Stärke liegt in der einfachen Formulierung bedingter kritischer Abschnitte, die bereits im 1. Beispiel zum Ausdruck kommt: In der Acquire-Operation wird die Ausführung der Prozedur von der Bedingung abhängig gemacht, daß der Zähler busy>0 ist. Die Wiederholung einiger Beispiele aus dem Abschnitt 6.3 demonstriert den Umgang mit diesem nützlichen Konzept und beweist eindrucksvoll die verbesserte Verständlichkeit der resultierenden Algorithmen gegenüber den korrespondierenden Semaphorlösungen.
Semaphore können durch einen Monitor realisiert werden
6
Speicherbasierte Prozeßinteraktion
2. Beispiel: Verwaltung gleichartiger Betriebsmittel Verwaltung mehrerer
MONITOR DiscPool (int Anzahl) enum DiscStatus[N] {frei,belegt}; int busy; /* Zahl belegter Laufwerke */ Condition nonbusy;
Betriebsmittel
ENTRY int GetdiscO { int i ; i = l; while (busy==N) nonbusy.WAIT; while (DiscStatus[i]==belegt) i++; DiscStatus[i]=belegt; busy++; return (i);
} ENTRY PutDisc(int ActDisc) { DiscStatus[AtcDisc]=frei; busy--; nonbusy.SIGNAL;
} INIT { N=Anzahl; DiscStatus=frei; busy=0 END
3. Beispiel: Reader-Writer-Problem (1) Die untenstehende Lösung von Hoare setzt die Existenz von vier Monitorprozeduren StartRead, EndRead, StartWrite, EndWrite voraus. In der Variablen Readcount wird die Zahl der gleichzeitigen Readers und in der Variablen Writeflag ein potentieller Writer vermerkt. Die Condition-Variablen OkToRead und OkToWrite dienen der Realisierung von Wartestellungen für Prozesse, die auf die Erlaubnis zum Lesen bzw. Schreiben warten müssen. Reader- Writer-Problem: Reader-Bevorzugung
MONITOR ReadWrite int Readcount; //Zahl der Prozesse, die lesen oder wollen int Writeflag; Condition OkToRead, OkToWrite; ENTRY StartRead() { Readcount++; while (Writeflag==l) OkToRead. WAIT () ; OkToRead.SIGNAL();
} ENTRY EndRead() { Readcount--; if (Readcount==0) OkToWrite.SIGNAL();
} ENTRY StartWrite() { while((Readcount>0) | | (Writeflag==l)) { OkToWrite.WAIT{); Writeflag=l;
}
6.4
Sprachgestützter Mechanismus: Monitore
ENTRY EndWrite() { Writeflag=0; if(OkToWRead.Status>0) OktoRead.SIGNAL(); eise OktoWrite.SIGNAL();
} INIT { Readcount=0; WritefIag=0; } END
Die nachfolgende Lösung für das 2. Reader/Writer-Problem folgt dem Lösungsansatz für das 1. Reader/Writer-Problem und ist ebenso leicht verständlich. Die Komplexität der korrespondierenden Semaphorlösung wird nicht annähernd erreicht. 4. Beispiel: Reader-Writer-Problem (2) MONITOR ReadWrite { int Readcount, Writecount, Writeflag; Condition OkToRead, OkToWrite; ENTRY StartReadO { while (Writecount>0) OkToRead.WAIT(); Readcount++; OkToRead.SIGNAL();
} ENTRY EndRead() { Readcount--; if (Readcount==0) OkToWrite.SIGNAL 0 ;
} ENTRY StartWrite() { Writecount++; while ((Readcount>0)||(Writeflag==l)) OkToWrite.WAIT(); Writeflag=l;
} ENTRY EndWrite() { Writecount--; Writeflag=0; if (Writecount>0) OkToWrite.SIGNAL 0 ; else OktoRead.SIGNAL() ;
} INIT { Readcount=0; Writecount=0; WritefIag=0; } END
Prozesse benutzen den Monitor ReadWrite in der Form PROCESS R e a d e r {
PROCESS W r i t e r { ReadWrite.StartWrite(); Schreibzugriffe; ReadWrite.EndWrite();
ReadWrite.StartRead; Lesezugriffe; ReadWrite.EndRead();
}
}
Reader-Writer-Problem: Writer-Bevorzugung
6 Speicherbasierte Prozeßinteraktion
Aus der Sicht der Reader und Writer ist deshalb der Zugriff zu den gemeinsamen Daten genauso einfach wie mittels eines gewöhnlichen kritischen Abschnitts. 6.4.3
Semaphor-basierte Realisierung eines Monitors
M_UrgentCount speichert die Anzahl der am Semaphor M_urgent
Implementierungsaspekte
Da es sich beim Monitorkonzept um eine Sprachkonstruktion handelt, basieren alle Implementierungen auf der Übersetzung von Monitoren in eine primitivere Darstellungsform, in der die Synchronisation explizit enthalten ist. Die nachfolgend angegebenen Lösungen folgen einem Vorschlag von Hoare, Monitore mittels Semaphoren zu realisieren. Monitore werden dazu in Module übersetzt, in denen die Synchronisationen bei Monitoreintritt, Monitoraustritt sowie alle Operationen auf Condition-Variable durch Semaphor-Operationen ersetzt werden. Monitorein- und -austritt werden dabei über zwei Semaphore gesteuert, die bei der Compilierung für jeden Monitor getrennt erzeugt werden. In der untenstehend gezeigten übersetzten Version des Monitors M wurden die Semaphore mit M_Mutex und M_urgent bezeichnet. Über das Semaphor M_Mutex betreten alle Prozesse erstmalig den Monitor M. Prozesse, die bereits einmal im Monitorbesitz waren und ihn aufgrund einer Wait- oder Signal-Operation vorübergehend verlassen mußten, betreten dagegen den Monitor erneut über das Semaphor M_urgent. Prozesse, die am Semaphor M_Urgent warten, werden bevorzugt bedient. Mit diesen Erläuterungen nimmt der durch Compilierung von M resultierende Modul die folgende Gestalt an: MODULE M Datendeklarationen; Semaphor M_Mutex, M_Urgent; int M_UrgentCount ;
blockierten Prozesse ENTRY Funktionsnamel (Parameter) { P(M_Mutex); Anweisungsteil; if (M_UrgentCount>0) V(M_Urgent) else V(M_Mutex);
} Weitere Monitorprozeduren; INIT { M_Mutex=l; M_Urgent=0; M_UrgentCount=0; END
Jede Condition-Variable cond wird bei der Übersetzung durch ein Semaphor cond und einen zugehörigen Zähler condcount ersetzt: Semaphor-basierte Realisierung einer Condition-Variable
Condition cond: Semaphor cond = 0; int condcount = 0;
6.4
Sprachgestützter Mechanismus: Monitore
Die Operation cond.STATUS wird ersetzt durch einen Zugriff auf den zugehörigen Zähler condcount. Realisierungen der Operationen S I G N A L und W A I T sind abhängig von der jeweiligen semantischen Variante der Signal-Operation. Signal-Variante I: cond.SIGNAL: if (condcount>0) {v(cond); condcount--; M_UrgentCount++};
Umsetzung Signal-Variante I
cond.WAIT: condcount++; if (M_UrgentCount>0) V(M__Urgent); else V(M_Mutex); P (cond); P(M_Urgent); M_UrgentCount--;
Signal-Variante II: Die Realisierung der Wait-Operation ist identisch mit der Wait-Operation der Signal-Variante I, lediglich die Signal-Operation muß modifiziert werden: cond.SIGNAL: while(condcount>0) {V(cond); condcount- -; M_UrgentCount++;};
Umsetzung Signal-Variante II
Signal-Variante III: Die Realisierung der Signal-Variante III folgt dem Vorschlag von Hoare [Hoare 1974]. cond.WAIT: condcount++; if (M_UrgentCount > 0) V(M_Urgent); else V(M_Mutex); /* Prozeß verläßt Monitor */ P(cond); /* Prozeß betritt Monitor erneut */ condcount--;
Umsetzung Signal-Variante III
cond.SIGNAL: M_UrgentCount++; if (condcount > 0) { V(cond); P(M_Urgent); } M_UrgentCount--;
Der geführte Nachweis, daß sich Monitorkonzept und Semaphormechanismus ineinander überführen lassen verdeutlicht auch, daß beide Synchronisationshilfsmittel grundsätzlich gleich mächtig sind.
Semaphor und Monitor lassen sich ineinander überführen
6 Speicherbasierte Prozeßinteraktion
Effizientere Implementierungen sind möglich, wenn man auf die Benutzung des Semaphormechanismus verzichtet. Der interessierte Leser sei hier auf die entsprechende Spezialliteratur verwiesen ([Nehmer 1979], [Schmidt 1976]). 6.4.4
Berücksichtigung der Prozeßprioritäten
Erweiterung für Condition-Variable
Erweiterungen für Echtzeitverarbeitung
Grundsätzlich lassen sich die in Abschnitt 6.3 für Semaphore diskutierten Erweiterungen für die Echtzeitverarbeitung auch auf Monitore übertragen. Dies würde primär bedeuten, Prozesse bei Monitoreintritt prioritätsgerecht zu bedienen; die Kombination mit dem Prinzip der Prioritätsvererbung ist ebenfalls möglich. Für eine anwendungsgesteuerte Bedienung der an Condition-Variablen blockierten Prozesse hat Hoare vorgeschlagen, die Operationen Wait und Signal um einen Parameter zu erweitern: cond.WAIT(Priorität) cond.SIGNAL(Priorität)
In der Wait-Operation bestimmt der Parameter Priorität die Stellung des Prozesses in der Schlange der Condition-Variablen cond. Damit lassen sich anwendungsabhängige Prioritätsschemata realisieren. Der Parameter in der Signal-Operation ist optional und hat die Bedeutung eines Selektors: Er wählt den Prozeß aus der Schlange der Condition-Variablen aus, dessen Prioritätswert mit dem angegebenen Parameter übereinstimmt. Hoare hat die Nützlichkeit dieses Konzeptes anhand eines Monitors zur Realisierung eines einfachen Zeitdienstes demonstriert, wobei lediglich die Wait-Operation den Prioritätsparameter benötigt. Abbildung 6-8 zeigt die gesamte Anordnung. Ein Prozeß Timer, der durch einen periodischen Zeitgeber getriggert wird, ruft periodisch die Monitorprozedur Tick des Monitors Clock auf und aktualisiert damit eine Monitor-interne Zeitbasis. Prozesse, Clients genannt, benutzen den Weckdienst des Monitors Clock, indem sie die Monitorprozedur WakeMe aufrufen. Als Parameter wird der Prozedur die relative Weckzeit als Anzahl von Zeiteinheiten eines vorgegebenen Basisintervalls übergeben. Prozesse werden so lange im Monitor blokkiert, bis ihre Weckzeit abgelaufen ist. Der Monitor hat den folgenden Aufbau: MONITOR Clock int now; /*Absolutzeit*/ Condition wakeup; ENTRY WakeMe(int N) { int next; next=now+N;
6.5
Realisierungsbeispiele
while (now
} ENTRY Tick { now++; wakeup.SIGNAL;
} INIT {now=0} SEND Abb. 6-8 Organisation eines Weckdienstes durch einen Monitor Clock
Man beachte, daß die abschließende Signal-Operation in der Prozedur WakeMe gewährleistet, daß abgeschlossene Weckaüfträge mit identischer Weckzeit erkannt und behandelt werden. Neben den hier vorgestellten Erweiterungen für die Echtzeitverarbeitung wurden in der Literatur zahlreiche weitere Ergänzungen und Modifikationen diskutiert. Eine zusammenfassende Übersicht findet der Leser in [Andrews und Schneider 1983].
6.5
Realisierungsbeispiele
Grundlage für jede Form der speicherbasierten Prozeßinteraktion ist ein für alle beteiligten Threads gemeinsam zugreifbarer Speicherbereich (Shared Memory). Dieser Bereich ermöglicht eine unmittelbare Kommunikation zwischen mehreren Lhreads über einfache Lese- und Schreiboperationen. Gleichzeitig können in ihm die für eine Synchronisation der Threads notwendigen Datenstrukturen verankert werden. Drei Realisierungsvarianten sind gängig, die sich bezüglich der Größe des gemeinsamen Speicherbereichs in Relation zu den Adreßräumen der beteiligten Threads und den darauf definierten Zugriffsrechten unterscheiden:
• •
Mehrere kooperierende Threads innerhalb eines Adreßraums Expliziter Aufbau eines gemeinsamen Speichers zwischen ansonsten disj unkten Adreßräumen
Voraussetzung: Gemeinsam zugreifbarer Speicherbereich
6
Speicherbasierte Prozeßinteraktion
• Bereitstellung eines impliziten gemeinsamen Speicherbereichs durch die Systemsoftware Kommunikation über einfache Lese- und Schreiboperationen
In den ersten beiden Fällen können die kooperierenden Threads direkt durch einfache Lese- und Schreiboperationen Informationen über den gemeinsamen Speicher austauschen. Gleichzeitig werden die für den Aufbau der Synchronisationsprimitive notwendigen Datenstrukturen in diesem gemeinsam genutzten Speicherbereich abgelegt. Die auf den Primitiven definierten Zugriffsfunktionen werden typischerweise in Form einer Laufzeitbibliothek zur Verfügung gestellt, die zum Anwendungsprogramm dazugebunden werden muß. Bei einer Bereitstellung des gemeinsamen Speicherbereichs durch das Betriebssystem werden die notwendigen Zustandsinformationen und Datenstrukturen in einem gesonderten Speicherbereich des Kerns abgelegt, auf den alle Anwendungen durch den Aufruf privilegierter Systemfunktionen zugreifen können. Dadurch stehen einer Anwendung lediglich speicherbasierte Synchronisationsverfahren zur Verfügung. Eine speicherbasierte Kommunikation seitens der Anwendung mittels Lese- und Schreiboperationen ist in diesem Fall nicht möglich. In den nachfolgenden Abschnitten werden die im Bereich der speicherbasierten Prozeßinteraktion angebotenen Primitive am Beispiel des POSIX-Standards und der Win32-Programmierschnittstelle von Windows 9x und Windows NT vorgestellt. Expliziter Aufbau gemeinsamer Speicherbereiche Alle modernen Betriebssysteme bieten eine Reihe von Funktionen an, mit deren Hilfe gemeinsam nutzbare Speicherbereiche zwischen mehreren Prozessen angelegt werden können. Die richtige Verwendung dieser Funktionen setzt in vielen Fällen tiefergehende Systemkenntnisse voraus, da z.B. aus technischen Gründen Anfangsadresse und Größe des gemeinsamen Speicherbereichs in Einklang mit einer seitenbasierten Speicherverwaltung gebracht werden müssen. Außerdem ist in vielen Fällen zwingend notwendig, daß der gemeinsam genutzte Speicherbereich in allen beteiligten Prozessen die gleiche virtuelle Anfangsadresse besitzt, damit Zeiger innerhalb des Bereichs für alle zugreifenden Threads gültig sind. Darüber hinaus sind auch Funktionen notwendig, die den Aufbau eines solchen Speicherbereichs über Prozeßgrenzen hinweg unterstützen. Ein verbreiteter Lösungsansatz ist die Zuordnung und Identifikation über symbolische Namen. Dabei nutzen alle kooperierenden Prozesse einer Anwendung denselben symbolischen Namen für den koordinierten Aufbau des gemeinsamen Speicherbereichs.
6.5
Realisierungsbeispiele
In POSIX wird ein gemeinsam nutzbarer Speicher durch den Aufruf der Funktion
POSIX
int shm_open ( char *shared_memory_name, int flags, mode_t mode )
aufgebaut. Mit dem ersten Argument legt die Anwendung einen eindeutigen Namen für den gemeinsamen Speicherbereich fest. Über das Argument flags kann der Zugriffsmodus festgelegt werden: Mittels O_RDONLY wird z.B. nur lesender Zugriff auf den gemeinsamen Speicher zugelassen. In der Regel wird jedoch jeder kooperierende Prozeß durch Angabe von O_RDWR sowohl lesenden als auch schreibenden Zugriff anfordern. Ähnlich wie beim Öffnen von Dateien kann über dieses Argument zusätzlich gesteuert werden, wie zu verfahren ist, wenn der benannte Speicherbereich nicht vorhanden ist. Durch Setzen des Flags O _ C R E A T wird shm_open() in diesem Fall den angegebenen Speicher anlegen. Dabei werden über das Argument mode die Zugriffsrechte auf das neu angelegte Speicherobjekt festgelegt, d.h., es wird bestimmt, welche Prozesse lesend oder schreibend darauf zugreifen können. In der Regel wird ein ausgezeichneter Prozeß der Anwendung für das Anlegen und Initialisieren des gemeinsamen Speicherbereichs zuständig sein und das Flag O _ C R E A T angeben. Alle weiteren Prozesse öffnen anschließend über einfache shm_open() -Aufrufe den gemeinsamen Speicherbereich. Der Rückgabewert der Funktion ist im Erfolgsfall ein Deskriptor, den die Anwendung für weitere Funktionsaufrufe verwenden kann. Nach dem erstmaligen Öffnen eines gemeinsamen Speicherbereichs besitzt dieser eine Länge von 0 Byte. Die Funktion
Ähnlichkeiten zum Öffnen einer Datei
Größe des gemeinsamen Speicherbereichs festlegen
int ftruncate ( int shm_desc, off_t size )
erlaubt die Vergrößerung oder Verkleinerung eines angegebenen Speicherobjekts shm_desc auf die Gesamtgröße size. In einem weiteren Schritt muß jeder geöffnete gemeinsame Speicherbereich in den virtuellen Adreßraum eines Threads eingeblendet werden, damit anschließend über Lese- und Schreiboperationen darauf zugegriffen werden kann. In POSIX erfüllt die Funktion mmap() (memory map) u.a. auch diese Aufgabe: v o i d *mmap ( *void start_address, size_t length, int protection, int flags, int shm_desc, off t offset )
Gemeinsamen Speicherbereich in den virtuellen Adreßraum einblenden
6
Zugriffsrechte verändern
Gemeinsamen Speicherbereich auflösen
Speicherbasierte Prozeßinteraktion
Im wesentlichen blendet mmap() einen length Byte großen Bereich innerhalb des gemeinsamen Speicherobjekts shm_desc an der Adresse start_address in den virtuellen Adreßraum des aufrufenden Threads ein. Unter Umständen ist mmap() jedoch nicht in der Lage, den Speicherbereich gemäß den Vorgaben zu plazieren, da der angegebene virtuelle Adreßbereich bereits anderweit wie z.B. von Heap, Laufzeitkeller oder Programmcode belegt ist. Das System versucht in diesem Fall den Speicherbereich an einer anderen Stelle im virtuellen Adreßraum zu plazieren. Die Anwendung kann jedoch durch die Angabe des Flags M A P _ F I X E D bei flags erzwingen, daß die angegebene Startadresse verwendet oder der Aufruf von mmap() im Konfliktfall erfolglos abgebrochen wird. Zusätzlich muß bei einem gemeinsam genutzten Speicherbereich das Flag M A P _ S H A R E D definiert werden, das einen Zugriff auf den Bereich durch mehrere Prozesse zuläßt. Die Angabe dieser an sich redundanten Information ergibt sich aus der vielfältigen Einsetzbarkeit von mmap(). Die verbleibenden Argumente ermöglichen eine Verfeinerung der Zugriffsrechte innerhalb des von shm_open() vorgegebenen Rahmens (protection) und einen Versatz innerhalb des gemeinsamen Speicherbereichs beim Einblenden (offset). Letzterer wird im Fall eines gemeinsamen Speichers typischerweise 0 sein. Rückgabewert der Funktion mmap() ist im Erfolgsfall die Anfangsadresse des eingeblendeten Speicherbereichs im virtuellen Adreßraum. Im Fehlerfall wird ein Wert M A P _ F A I L E D zurückgegeben; die konkrete Fehlerursache ist dann in der prozeßglobalen Variablen errno hinterlegt. Nach dem Einblenden können die Zugriffsrechte von jedem Thread durch den Aufruf der Funktion mprotect() auf der Basis einzelner Seiten verändert werden. Dadurch kann z.B. ein Thread-Pakkage eine zufällige Veränderung sensibler Zustandsinformationen einschließlich bereitgestellter Synchronisationsprimitive verhindern. In diesem Fall wird ein schreibender Zugriff auf bestimmte Seiten des gemeinsamen Speicherbereichs nur innerhalb der entsprechenden Zugriffsfunktionen gestattet. Die Einblendung wird durch ein oder mehrere Aufrufe der Funktion munmap() rückgängig gemacht: int munmap ( void *begin, size_t length )
Dabei können einzelne Teilbereiche vor dem weiteren Zugriff gesperrt werden. Ab der virtuelle Adresse begin werden length Bytes ausgeblendet. Es ist jedoch zu beachten, daß das System immer nur ganze Seiten ein- oder ausblenden kann. Abschließend kann ein Thread durch den Aufruf der Funktion int close ( int shm desc )
6.5
Realisierungsbeispiele
den Verweis auf ein geöffnetes gemeinsames Speicherobjekt auflösen. Der gemeinsame Speicherbereich bleibt in diesem Fall weiterhin bestehen, der aufrufende Thread kann jedoch nicht mehr darauf zugreifen. Der Speicherbereich selbst kann mit Hilfe der Funktion int shm_unlink ( char *shared_memory_name )
gelöscht werden. Prozesse, die einen gültigen Deskriptor zu diesem Speicherobjekt haben, können weiterhin auf den gemeinsamen Speicher zugreifen. Mit dem Schließen der letzten Referenz auf dieses Speicherobjekt durch einen der beteiligten Threads wird jedoch der gemeinsame Speicherbereich aufgelöst. Im Fall der Win32-Schnittstelle von Windows 9x und Windows NT/2000 ist der Aufbau und die Nutzung eines gemeinsamen Speicherbereichs eng mit dem Einblenden von Dateien in den virtuellen Adreßraum (Memory Mapped Files) verbunden. Aus diesem Grund wird eine Diskussion der entsprechenden Funktionsschnittstelle auf das Kapitel 9 »Dateisysteme« verschoben.
Win32
Wechselseitiger Ausschluß Diese elementare Form der Prozeßsynchronisation wird in POSIXkonformen Systemen mit der Pthread-Erweiterung zur Verfügung gestellt. Die zugehörige Funktionsgruppe besitzt den Namenspräfix pthread_mutex_ (dabei steht mutex für »mutual exclusion«). Beim Anlegen eines für den wechselseitigen Ausschluß notwendigen MutexObjektes wird zwischen einer statischen und einer dynamischen Initialisierung unterschieden. Im statischen Fall wird einfach eine Variable pthread_mutex_t mutex_var = PTHREAD_MUTEX_INITIALIZER;
definiert und initialisiert. Die dynamische Initialisierung erlaubt eine Funktion pthread_mutex_init: pthread_mutex_t *m; m = (pthread_mutex_t *) malloc(sizeof(pthread_mutex_t)); pthread_mutex_init(m,NULL);
Der Standard unterscheidet zwischen Mutex-Objekten, die einen wechselseitigen Ausschluß (a) zwischen den Threads innerhalb eines Adreßraums (PTHREAD_PROCESS_PRIVATE) und (b) zwischen verschiedenen Prozessen ( P T H R E A D _ P R O C E S S _ S H A R E D ) garantieren. Im letztgenannten Fall muß die entsprechende Datenstruktur pthread_mutex_t in einem mit shm_open() explizit angelegten gemeinsamen Speicherbereich plaziert werden. Zusätzlich müssen die beteiligten Threads in einer geeigneten Form die Adresse des gemein-
POSIX.4a
Initialisierung
6
Speicherbasierte Prozeßinteraktion
samen Mutex-Objektes in Erfahrung bringen können. Die gewünschte Variante eines Mutex-Objektes kann von der Anwendung über ein Attribut festgelegt werden, so wird z. B. ein über Adreßraumgrenzen hinweg gültiges Mutex-Objekt m durch die Befehlsfolge pthread_mutex_t *m; pthread_mutex_attr_t m_attr; pthread_mutexattr_init(&m_attr); pthread_mutexattr_setshared(5cm_attr, PTHREAD_PROCESS_SHARED); pthread_mutex_init(m,&m_attr);
angelegt. Dabei wird eine entsprechende Plazierung und Initialisierung der Variablen m im gemeinsamen Speicherbereich vorausgesetzt. Ein kritischer Abschnitt selbst wird mit Hilfe der beiden FunktioBetreten und Verlassen eines kritischen Abschnitts
nen pthread_mutex_lock () und pthread_mutex_unlock () reali-
siert: pthread_mutex_t *m;
pthread_mutex_lock(m); // Kritischer Abschnitt pthread_mutex_unlock(m);
Zusätzlich bietet die Pthread-Erweiterung noch eine Funktion pthread_mutex_trylock ( pthread_mutex_t *mutex )
Win32
Adreßraum-Iokaler
an, mit der ein freies Mutex-Objekt analog zu pthread_mutex_lock() belegt werden kann. Im Unterschied zur Lock-Funktion liefert diese Funktion jedoch einen besonderen Rückgabewert und blockiert nicht, wenn ein bereits belegtes Mutex-Objekt angetroffen wird. Auch die Win32-Programmierschnittstelle unterscheidet zwischen einem wechselseitigen Ausschluß zwischen Threads innerhalb eines Adreßraums und zwischen verschiedenen Prozessen. Im Gegensatz zum POSIX-Standard gibt es jedoch für beide Fälle verschiedene Implementierungen. Innerhalb eines Adreßraums wird ein wechselseitiger Ausschluß durch das Anlegen einer entsprechenden Datenstruktur und den Aufruf zweier Funktionen sichergestellt: CRITICAL SECTION mutex;
kritischer Abschnitt EnterCriticalSection(&mutex); // Kritischer Abschnitt LeaveCriticalSection(Smutex);
6.5 Realisierungsbeispiele
Ein prozeßübergreifender wechselseitiger Ausschluß wird in der Win32-API ebenfalls über besondere Mutex-Objekte hergestellt. Angelegt wird ein solches Objekt mit Hilfe der Funktion: HANDLE CreateMutex ( Schutzattribute, Boolean initial_owner, char *mutex_name )
Im Erfolgsfall liefert die Funktion einen Deskriptor für das MutexObjekt. Neben den Schutzattributen, die eine Weitergabe des Deskriptors bei der Erzeugung weiterer Prozesse beschreiben, legen die verbleibenden Argumente fest, welchen symbolischen Namen das Mutex-Objekt besitzt (mutex_name) und ob der aufrufende Thread das Objekt bereits initial belegt hat (initial_owner). Über einen eindeutigen Namen können mehrere Prozesse Zugang zu einem Mutex-Objekt erhalten. Windows-Betriebssysteme unterscheiden bei fast allen über einen Deskriptor zugänglichen Kernobjekten einschließlich einem MutexObjekt zwischen einem signalisierten und einem nichtsignalisierten Zustand. Ein kritischer Abschnitt wird durch die bereits angesprochene Funktion WaitForSingleObject( HANDLE mutex, timeout )
Adreßraum-übergreifender kritischer Abschnitt
Prozeßblockade durch WaitForSingleObject
betreten. Diese Funktion blockiert den aufrufenden Thread, wenn sich das angegebene Mutex-Objekt mutex im nichtsignalisierten Zustand befindet. Findet der Aufrufer einen signalisierten Zustand vor, so wird lediglich das Mutex-Objekt automatisch in den nichtsignalisierten Zustand zurückgesetzt. Der aufrufende Thread wird in diesem Fall weiter fortgesetzt. Die optionale Angabe eines Zeitwertes timeout ermöglicht eine zeitlich beschränkte Blockade. Die Überschreitung einer angegebenen Zeitvorgabe wird durch einen gesonderten Rückgabewert der Anwendung mitgeteilt. Mit Hilfe der Funktion ReleaseMutex ( HANDLE m )
wird ein Mutex-Objekt erneut in den signalisierten Zustand versetzt. Durch diesen Zustandswechsel wird - falls vorhanden - ein durch den Aufruf von WaitForSingleObject() blockierter Thread wieder befreit und das entsprechende Mutex-Objekt zurückgesetzt. Insgesamt ergibt sich damit folgende Realisierung eines kritischen Abschnitts: HANDLE m; m = CreateMutex(NULL,False,"MyMutex"); WaitForSingleObject(m,INFINITE); // Kritischer Abschnitt ReleaseMutex(m);
Betreten und Verlassen eines kritischen Abschnitts
6
Speicherbasierte Prozeßinteraktion
Semaphore Die universell einsetzbaren Semaphore zur Synchronisation interagierender Prozesse werden praktisch von jeder Systemsoftware angeboten. Dies geschieht entweder im Anwendungsadreßraum im Fall von UL-Threads oder im Kernadreßraum bei KL-Threads. Im ersten Fall wird ein gemeinsamer Speicherbereich zwischen allen beteiligten Threads oder Prozessen vorausgesetzt. Die typischerweise auf einem Semaphor definierten Funktionen sind:
• • •
Smart Semaphor
Reentrant-Fähigkeit
Erzeugen und Löschen eines Semaphors P- und v-Operation Abfragefunktionen
Mit Hilfe der Abfragefunktionen kann z.B. ermittelt werden, ob ein Aufruf der P-Operation blockieren würde oder wie viele Prozesse aktuell blockiert sind. In den meisten Fällen bietet die Systemsoftware lediglich allgemeine Semaphore an, da binäre Semaphore leicht auf diese abgebildet werden können. Die angebotenen Semaphorrealisierungen bieten darüber hinaus meist die Möglichkeit, daß aufeinanderfolgende P-Operationen des im Besitz des Semaphors befindlichen Prozesses keine weiteren Blockaden nach sich ziehen. Diese auch als Smart Semaphor oder Friendly Semaphor bezeichnete Variante ermöglicht damit z.B. eine einfache Sicherung von Bibliotheksfunktionen vor Inkonsistenzen bei einem gleichzeitigen Zugriff durch mehrere Threads innerhalb eines Adreßraums. Eine solche reentrantfähige Bibliothek erlaubt den Wiedereintritt eines zweiten Threads durch den Aufruf einer Bibliotheksfunktion, obwohl bereits ein weiterer Thread eine Funktion derselben Bibliothek aufgerufen hat. Der Schutz wird durch einen wechselseitigen Ausschluß innerhalb der Bibliotheksfunktionen erreicht: Semaphor s(1); Bibliotheksfunktion f ( Argumente ) { P(s) ; // Kritischer Abschnitt f(...); V(s); }
Im obigen Programmbeispiel wird gleichzeitig auch der besondere Vorteil der Smart Semaphore verdeutlicht. Der rekursive Aufruf der Bibliotheksfunktion f() würde in einer normalen Semaphorrealisierung zwangsläufig zu einer Blockade führen. Im Fall eines Smart Semaphors kann der Thread, der im Besitz des Semaphors ist, beliebig weitere Bibliotheksfunktionen ohne Blockade aufrufen. Es sollte je-
6.5 Realisierungsbeispiele
doch darauf geachtet werden, daß jede verschachtelte P-Operation durch eine entsprechende v-Operation aufgehoben wird. Im POSIX-Umfeld werden Semaphore erst mit der Echtzeiterweiterung POSIX.4 standardisiert. Unabhängig davon bieten jedoch die meisten UNIX-Systeme eine Semaphorrealisierung an, die in vielen Fällen von der ursprünglich in UNIX System V angebotenen Funktionsschnittstelle abgeleitet ist. POSIX.4 unterscheidet zwischen unbenannten und benannten Semaphoren. Beide Varianten können sowohl innerhalb eines Prozesses als auch zwischen verschiedenen Prozessen eingesetzt werden. Sie unterscheiden sich lediglich darin, wie die beteiligten Threads Zugriff zu einem bestimmten Semaphor erhalten. Bei benannten Semaphoren wird - wie bereits angesprochen - ein symbolischer Namen definiert, über den alle interagierenden Threads auf das Semaphor zugreifen können. Im Fall von unbenannten Semaphoren muß die Adresse des Semaphors im Adreßraum oder im gemeinsam genutzten Speicherbereich über einen in der Anwendung implementierten Mechanismus an alle beteiligten Threads weitergegeben werden. Bezüglich der Funktionsschnittstelle unterscheiden sich die beiden Semaphorvarianten lediglich in ihrer Erzeugung und Auflösung. Die den P- und V-Operationen äquivalenten Funktionen können in beiden Fällen angewendet werden. Die Verwaltung benannter Semaphore geschieht über eine der Dateischnittstelle von UNIX sehr ähnlichen Funktionsschnittstelle: • • •
Erzeugung: sem_open Schließen: sem_close Löschen: sem unlink
POSIX
Benannte und unbenannte Semaphore
Semaphorfunktionen
Die entsprechende Funktionsschnittstelle für unbenannte Semaphore besteht lediglich aus den beiden Funktionen: • •
Erzeugung: sem_init Löschen: sem_destroy
In beiden Realisierungsvarianten sind die folgenden Funktionen aufrufbar: • P-Operation: sem_wait (sem_trywait) • V-Operation: sem_post Am Beispiel der allgemeineren Schnittstelle für benannte Semaphore soll deren Verwendung verdeutlicht werden. Die für die Erzeugung zuständige Funktion sem_open() besitzt die Signatur: sem_t *sem_open ( char *sem_name, int flags, mode_t mode, unsigned int value )
Semaphorerzeugung
6
Speicherbasierte Prozeßinteraktion
Das erste Argument sem_name definiert den symbolischen Namen des Semaphors. Das zweite Argument legt im wesentlichen mit flags
Die entsprechenden P- und V-Operationen
= O_CREAT
fest, ob ein Semaphor neu erzeugt werden soll, wenn kein Semaphor mit dem angegebenen Namen gefunden werden kann, oder ob die Funktion mit einer entsprechenden Fehlermeldung abbrechen soll. Im Fall der Erzeugung eines neuen Semaphors definiert mode die entsprechenden UNIX-Zugriffsrechte für andere Prozesse und value den initialen Semaphorwert. Im Erfolgsfall liefert die Funktion einen Zeiger auf ein Semaphorobjekt zurück. Eine P-Operation wird durch den Aufruf der Funktion sem_wait
(sem_t *s)
und eine V-Operation durch den Aufruf der Funktion sem_post
Win32
(sem_t *s)
ausgeführt. Die sem_trywait() -Funktion führt die P-Operation nur aus, wenn damit keine Blockade des aufrufenden Threads verbunden ist. Ansonsten gibt sie einen Wert -1 zurück, und die globale Variable errno besitzt den Fehlerwert E A G A I N . Die Win32-Realisierung eines Semaphors umfaßt die Funktionen:
• • • • •
Erzeugung und Öffnen: CreateSemaphore Offnen: OpenSemaphore P-Operation: WaitForSingleObject V-Operation: ReleaseSemaphore Schließen: CloseHandle
Am Beispiel einer exemplarischen Implementierung des Erzeugers in einem Erzeuger-Verbraucher-System soll die Verwendung der Funktionen skizziert werden: Beispiel: Implementierung des Erzeugers
HANDLE leer; HANDLE voll; leer = CreateSemaphore(NULL,1,1,"Leer"); voll = CreateSemaphore(NULL,0,1,"Voll"); Erzeuger: while (1) { WaitForSingleObject(leer,INFINITE); // Fülle Puffer ReleaseSemaphore(voll,1,NULL);
Ein Semaphor wird mit der Funktion CreateSemaphore() erzeugt. Das erste Argument definiert die Schutzattribute für nachfolgend erzeugte Prozesse. Die nächsten zwei Argumente legen den initialen und
6.5
Realisierungsbeispiele
maximalen Semaphorwert fest, d.h., das Semaphor l e e r wird mit 1 initialisiert und nimmt einen Maximalwert von 1 an, und das Semaphor voll wird mit 0 initialisiert und kann ebenfalls einen Maximalwert 1 annehmen. Über das letzte Argument kann dem Semaphor ein symbolischer Name zugewiesen werden, der in diesem Fall z.B. dem Verbraucher einen Zugang zu den notwendigen Semaphoren ermöglicht. Analog zu Mutex-Objekten unterscheiden Windows-Betriebssysteme auch bei einem Semaphor zwischen einem signalisierten und einem nichtsignalisierten Zustand. Der Aufruf der Funktion waitForSingleObject() entspricht dem Aufruf der P-Operation. Solange der Wert des Semaphors größer als 0 ist, verbleibt es im signalisierten Zustand und der entsprechende Aufruf von WaitForSingleObject() wird nicht blockiert. Analog dazu überführt der Aufruf von ReleaseSemaphore() das Semaphor nur dann in einen signalisierten Zustand, wenn der resultierende Wert größer als 0 ist. Das zweite Argument der ReleaseSemaphore()-Funktion ermöglicht die Inkrementierung des Semaphors um den angegeben Wert (typischerweise 1). Als drittes Argument kann optional ein Zeiger auf eine Long-Variable angegeben werden, in die das Betriebssystem den vorherigen Semaphorwert kopiert. Bei einer Angabe des Werts N U L L ist die Anwendung an dieser Information nicht interessiert.
Signalisierter und nichtsignalisierter Zustand
Condition-Variable und Events Aus einsichtigen Gründen ist das Monitorkonzept direkt an eine bestimmte Programmiersprache gebunden, die ein entsprechendes Sprachkonstrukt zur Synchronisation nebenläufiger Aktivitäten anbietet. So wird z.B. in der Telekom-Industrie mit CHILL ein Sprachstandard eingesetzt, der Monitore aufweist. Auch moderne Programmiersprachen wie z.B. JAVA [Gosling et al. 1996], die zunehmend dem Aspekt der nebenläufigen Programmierung in einem Adreßraum Rechnung tragen, verfügen über dieses Synchronisationsmittel. Dagegen wird von der Systemsoftware aufgrund der fehlenden Sprachbindung nur das Synchronisationsprimitiv Condition-Variable angeboten. In dieser von einem Monitor entkoppelten Form erlauben Condition-Variablen einer Anwendung eine vom Datenzustand abhängige Synchronisation kooperierender Prozesse. In der Pthread-Erweiterung des POSIX-Standards wird eine Condition-Variable im statischen Fall durch die Initialisierung einer Variablen in der Form pthread cond t cv = PTHREAD COND INITIALIZER;
Sprachbindung des Monitorkonzepts
Monitorkonzept in JAVA
POSIX Condition-Variable
6
Speicherbasierte Prozeßinteraktion
erzeugt. Analog zur Mutex-Realisierung gibt es auch die Möglichkeit der dynamischen Initialisierung mittels pthread_cond_init(). Die zentralen Zugriffsfunktionen auf eine Condition-Variable sind:
• Wait: pthread_cond_wait • Signal: pthread_cond_signal
• Signal-Varianten
Signal: pthread_cond_broadcast
Dabei befreit die Funktion pthread_cond_signal() höchstens einen an der Condition-Variablen blockierten Prozeß (Signal-Variante I), während pthread_cond_broadcast() alle blockierten Prozesse freigibt (Signal-Variante II). Die besondere Problematik der Bereitstellung einer Condition-Variablen ohne zugehöriges Monitorkonzept wird an der Signatur der Funktion pthread_cond_wait() deutlich: pthread_cond_wait ( pthread_cond_t *cv, pthread_mutex_t *mutex )
Verknüpfung mit einem Semaphor
Zur Vermeidung von Race-Conditions, d.h. zeitabhängiger Fehler aufgrund einer zeitlich verschränkten Ausführung kooperierender Prozesse, erzwingt die Pthread-Realisierung die Angabe eines zusätzlichen Mutex-Objekts. Korrekt angewendet, sichert dieser wechselseitige Ausschluß die Atomarität der kritischen Abschnitte. Im wesentlichen erfüllt das Mutex-Objekt damit die an ein Monitorsemaphor gekoppelten Forderungen, ohne daß diese wie im Fall einer sprachbasierten Lösung vom System erzwungen werden können. Beim Aufruf der Funktion pthread_cond_wait() wird vom System vorausgesetzt, daß der aufrufende Thread im Besitz des Mutex-Objektes ist, d.h. einen kritischen Abschnitt bereits betreten hat: pthread_mutex_t mutex = pthread_cond_t cv = ...
pthread_mutex_lock(Smutex); // Kritischer Abschnitt pthread_cond_wait(&cv,tmutex); pthread_mutex_unlock(&mutex);
Die Funktion pthread_cond_wait() blockiert den aufrufenden Thread und gibt anschließend das Mutex-Objekt durch einen impliziten Aufruf der Funktion pthread_mutex_unlock() wieder frei. Dadurch können andere Prozesse den kritischen Abschnitt erneut betreten. Dies entspricht dem Verlassen des zugehörigen Monitors und dem anschließenden Eintritt anderer Prozesse.
6.5
Realisierungsbeispiele
Analog zum Monitorkonzept muß auch der Aufruf einer signalisierenden Funktion innerhalb eines durch das Mutex-Objekt abgesicherten kritischen Abschnitts stattfinden: pthread_mutex_lock (&mutex); // Kritischer Abschnitt pthread_cond_signal(&cv);
Gemeinsame Verwendung von Condition-Variable und Semaphor
pthread_mutex_unlock(imutex);
Werden durch die signalisierende Funktion ein oder mehrere Prozesse aus ihrer Blockade befreit, so sorgt die Funktion pthread_cond_wait() automatisch für eine erneute Bewerbung um den kritischen Abschnitt durch den impliziten Aufruf von pthread_mutex_lock(). Im wesentlichen steht damit Sprachentwicklern eine Programmierschnittstelle zur Verfügung, die eine einfache und direkte Anbindung eines Monitorkonzepts auf die vorhandene Funktionsschnittstelle der Systemsoftware erlaubt. Ohne direkten Sprachbezug birgt jedoch die implizite und vom Anwendungsprogrammierer manuell einzuhaltende Kopplung zwischen Mutex-Objekt, Condition-Variable und den kritischen Abschnitten innerhalb der Zugriffsfunktionen die Gefahr vielfältiger und schwer zu lokalisierender Fehlerquellen. Die in der Win32-Programmierschnittstelle angebotenen Events kommen der Semantik einer Condition-Variablen am nächsten. Dieser Datentyp erlaubt ebenfalls die zustandsbezogene Blockierung und Signalisierung von Threads. Im Gegensatz zur POSIX-Realisierung ist jedoch keine explizite Verknüpfung mit einem Mutex-Objekt vorgesehen. Ein Event-Objekt wird mit Hilfe der Funktion CreateEvent() erzeugt:
Win32 Events
HANDLE CreateEvent( Schutzattribute, Boolean Manual, Boolean Initialstate, char *event_name)
Windows-Betriebssysteme unterscheiden zwischen manuell zurücksetzbaren Events (Manual-Reset Event) und automatisch zurücksetzbaren Events (Auto-Reset Event). Diese Eigenschaft bezieht sich auf den Übergang vom signalisierten Zustand in den nichtsignalisierten Zustand. Der konkrete Eventtyp wird über das zweite Argument Manual der Funktion CreateEvent() festgelegt. Das dritte Argument Initialstate bestimmt außerdem, ob sich das Event nach seiner Erzeugung im signalisierten (True) oder nichtsignalisierten (False) Zustand befindet. Die optionale Angabe eines symbolischen Namens event_name ermöglicht den Zugriff durch andere Prozesse.
Manuell und automatisch zurücksetzbare Events
6 Speicherbasierte Prozeßinteraktion
Event in den signalisierten Zustand versetzen
Die blockierende Wait-Funktion wird analog zu allen anderen Deskriptor-basierten Synchronisationsverfahren der Win32-Schnittstelle durch den Aufruf der Funktion WaitForSingleObject() und Angabe des gewünschten Event-Deskriptors ausgeführt. In beiden Varianten muß ein Event-Objekt explizit durch einen Thread der Anwendung in den signalisierten Zustand versetzt werden. In der Regel geschieht dies, wenn ein bestimmter Zustand wie z.B. »Puffer voll« erreicht worden ist: SetEvent ( HANDLE ev )
Manuelles Event in den nichtsignalisierten Zustand versetzen
Bei einem manuell zurücksetzbaren Event werden alle blockierten Threads daraufhin befreit und bewerben sich erneut um einen Prozessor. Der Aufruf von SetEvent() entspricht bei dieser Event-Variante daher der Signal-Variante II. Im signalisierten Zustand werden alle Threads, die WaitForSingleObject() aufrufen, bei einem manuellen Event nicht blockiert. Der signalisierte Zustand wird durch den manuellen Aufruf der Funktion ResetEvent ( HANDLE ev )
zurückgesetzt. Für den kombinierten Aufruf von SetEvent(), der Freigabe aller blockierten Threads und dem anschließenden Zurücksetzen durch ResetEvent() bietet die Programmierschnittstelle auch eine Kurzform an: PulseEvent ( HANDLE ev )
Ein automatisch zurücksetzbares Events verbleibt nach dem Aufruf der Funktion SetEvent() im signalisierten Zustand, bis ein bereits blockierter Thread direkt befreit werden kann oder der nächste WaitForSingleObject()-Aufruf auf dem Event-Deskriptor ausgeführt wurde. In beiden Fällen wird das Event anschließend automatisch in den nicht-signalisierten Zustand zurückgesetzt. Es entspricht damit in seiner Semantik der Signal-Variante I. Durch die »Speicherung« eines SetEvent()-Aufrufs bis zur Deblockade oder Fortführung eines an diesem Event interessierten Threads können bis zu einem gewissen Grad gefährliche Race-Conditions verhindert werden. Notwendige wechselseitige Ausschlußmaßnahmen bei kritischen Abschnitten müssen jedoch explizit von der Anwendung gelöst werden.
7
Nachrichtenbasierte Prozeßinteraktion
Bei nachrichtenbasierter Prozeßinteraktion tauschen Prozesse gezielt Informationen durch Verschicken und Empfangen von Nachrichten aus [Shatz 1984]. Die Prozesse sind zu diesem Zweck über ein Kommunikationssystem verbunden, das an seiner Schnittstelle wenigstens die Funktionen Send und Receive zur Verfügung stellen muß. Mittels
Senden und Empfangen von Nachrichten
Send (E,m)
wird eine Nachricht m für den Empfänger E an das Kommunikationssystem übergeben. Mit Receive (m)
entnimmt ein Empfänger E eine eventuell für ihn vorliegende Nachricht m aus dem Kommunikationssystem. Der Absender wird gewöhnlich in der Nachricht m codiert. Nachrichtenkommunikation ist immer dann die geeignete Prozeßinteraktionsform, wenn die beteiligten Prozesse in disjunkten Adreßräumen liegen und damit keine Möglichkeit haben, auf gemeinsamen Speicher zuzugreifen. Dies trifft auf die in Kapitel 3 vorgestellten Laufzeitmodelle B (nachrichtengekoppelte Prozesse) und C {nachrichtengekoppelte Teams) zu. Nachrichtenkommunikation ist die natürliche Form der Prozeßinteraktion in Rechnernetzen. Prozesse, die auf verschiedenen Rechnerknoten plaziert sind, müssen ein physisches Übertragungssystem benutzen, um miteinander in Kontakt zu treten. In Anlehnung an Cheriton [Cheriton 1984] wollen wir künftig mit einer Nachrichtentransaktion den gesamten Vorgang vom Abschicken einer Nachricht, über ihre Zustellung beim Empfänger, ihre Verarbeitung bis zur Quittierung gegenüber dem ursprünglichen Absender verstehen. Nur durch eine präzise Definition der Semantik einer Nachrichtentransaktion läßt sich genau festlegen, wann die mit dem Abschicken einer Nachricht ausgelösten Folgewirkungen endgültig abgeklungen sind. Der Begriff ist auch hilfreich, um verschiedene Synchronisationsformen zwischen Sender und Empfänger klarer zu definieren.
Nachrichtentransaktion
7
Empfangsreihenfolge von Nachrichten ist häufig unbestimmt
Nachrichtenverluste
Nachrichtenbasierte Prozeßinteraktion
Im Gegensatz zur speicherbasierten Prozeßinteraktion besteht bei nachrichtengekoppelten Prozessen nicht die Gefahr, daß prozeßlokale Daten in einen inkonsistenten Zustand geraten. Dafür gibt es einen einfachen Grund: Da der Empfänger über den Zeitpunkt der Annahme einer Nachricht entscheidet, kann er ihn immer so wählen, daß alle lokalen Daten in einem konsistenten Zustand sind. Dafür handelt man sich aber ein anderes Problem ein: Durch unbestimmte Nachrichtenlaufzeiten können kausal abhängige Nachrichten auf ihrem Weg vom Sender zum Empfänger vertauscht werden. Die Verarbeitung kausal abhängiger Nachrichten in verkehrter Reihenfolge kann auf Empfangsseite zu falschen Schlußfolgerungen führen. Dadurch wird zwar die prozeßlokale Datenkonsistenz nicht gefährdet, der globale Datenzustand (d.h. die Summe der prozeßlokalen Zustände) kann jedoch inkonsistent werden. Generell gilt, daß beim Eintreffen einer Nachricht der übermittelte Zustand auf Senderseite durch die unvermeidliche Nachrichtenlaufzeit schon wieder veraltet sein kann. Es ist deshalb grundsätzlich sehr schwer, durch Nachrichtenaustausch zwischen Prozessen ein konsistentes Gesamtbild über das Geschehen in einem System zu gewinnen. Nachrichten können auch gelegentlich verloren gehen. In einem Rechnerknoten spielt der Nachrichtenverlust zwar nur eine untergeordnete Rolle, in einem Rechnernetz dagegen können Verluste immer wieder vorkommen. Für die beteiligten Prozesse ist es sehr schwer herauszufinden, ob eine abgeschickte Nachricht verloren ging oder auf Grund unerwartet hoher Laufzeiten noch unterwegs ist. Eine falsche Schlußfolgerung kann zu fatalen Fehlern führen. In den nachfolgenden Abschnitten dieses Kapitels wird eine systematische Übersicht über das Gebiet der Nachrichtenkommunikation gegeben, wobei die besonderen Phänomene in verteilten Systemen (Nachrichtenlaufzeiten, Nachrichtenverlust) ignoriert werden. Diese Vereinfachung ist zulässig, da der Nachrichtentransport innerhalb eines Rechners durch Kopiervorgänge im Speicher erfolgt. Sie können in erster Näherung als zuverlässig und zeitlos angenommen werden. In Abschnitt 7.1 werden zunächst elementare Kommunikationsmodelle vorgestellt. Anhand der Realisierung eines Servers wird der Bedarf an weitergehender Unterstützung motiviert. Fortgeschrittene Kommunikationsmodelle werden in Abschnitt 7.2 diskutiert. Eine sprachbasierte Form der synchronen, auftragsorientierten Kommunikation, der RPC, wird in Abschnitt 7.3 vorgestellt. Signale stellen eine spezielle Ausprägung von Nachrichten dar, die in Abschnitt 7.4 diskutiert werden. Auf echtzeitspezifische Erweiterungen wird in Abschnitt 7.5 eingegangen. Implementierungsaspekte, die sich wiederum auf einen Rechnerknoten beschränken, werden in Abschnitt 7.6 diskutiert. Das
7.1
Elementare Nachrichtenkommunikationsmodelle
Kapitel schließt mit einer Übersicht über die Kommunikationsunterstützung im POSIX-Standard.
7.1
Elementare Nachrichtenkommunikationsmodelle
Ein einfaches Klassifikationsschema für Nachrichtenkommunikation orientiert sich an zwei Parametern:
• dem generellen Muster des Nachrichtenflusses zwischen den •
Prozessen, der zeitlichen Kopplung zwischen den Prozessen, die an einer Nachrichtentransaktion beteiligt sind.
Mittels zweier Modellparameter Kommunikationsmuster und Synchronität läßt sich dieser Sachverhalt erfassen. Elementare Kommunikationsmuster sind die Meldung bzw. der Auftrag. Eine Meldung ist eine Einweg-Nachricht vom Sender zum Empfänger. Die entsprechende Nachrichtentransaktion beginnt mit dem Versenden der Nachricht und endet mit ihrer Übergabe an den Empfänger. Zu einer Meldung gehört insbesondere nicht die anschließende Verarbeitung der Nachricht beim Empfänger. Ein Auftrag ist eine Zweiweg-Nachricht. Sie beginnt mit dem Versenden des Auftrags an den Empfänger und endet mit der Übergabe einer Erfolgsbestätigung (Quittung) über den durchgeführten Auftrag an den Sender. Dazwischen liegt die Auftragsbearbeitung auf Empfängerseite, die weitere Nachrichten zur Übertragung der Eingabeparameter bzw. zur Rückübertragung des Resultates auslösen kann. Im einfachsten Fall genügen für einen Auftrag zwei Nachrichten: die Auftragsnachricht und die Resultatnachricht, die gleichzeitig als Quittung dient. Durch den Empfang der Quittung erfährt der Sender in jedem Fall vom Abschluß der Nachrichtentransaktion. Ausbleibende Quittungen können mittels eines Timeouts erkannt werden und sind gleichbedeutend mit einer negativen Quittung, die auf Senderseite als nicht durchgeführter Auftrag interpretiert wird. Der mit dem Parameter Synchronität definierte Kopplungsgrad zwischen Prozessen bei der Durchführung einer Nachrichtentransaktion kann im einfachsten Fall die Werte asynchron und synchron annehmen. Bei asynchroner Nachrichtenkommunikation sind Sender und Empfänger einer Nachricht zeitlich voneinander entkoppelt. Das bedeutet insbesondere, daß ein Sender Nachrichtentransaktionen in schnellerer Folge für einen Empfänger erzeugen kann, als dieser in der
Meldung und Auftrag
Asynchrone Kommunikation (Non-blocking Send)
7
Synchrone Kommunikation (Blocking Send)
Nachrichtenbasierte Prozeßinteraktion
Lage ist, sie zu verarbeiten. Asynchrone Nachrichtenübertragung erfordert zwingend die Pufferung von Nachrichten auf dem Übertragungswege zwischen Sender und Empfänger. Die Parallelarbeit zwischen kommunizierenden Prozessen wird durch asynchrone Nachrichtentransaktionen praktisch nicht behindert. Synchrone Nachrichtenkommunikation erzwingt eine strikte Sequenz der Nachrichtentransaktionen zwischen den beteiligten Prozessen. Das bedeutet, daß ein Sender so lange in einer laufenden Nachrichtentransaktion blockiert wird, bis sie abgeschlossen ist. Durch synchrone Nachrichtenkommunikation wird zwar die Pufferung der Nachrichten entbehrlich, dafür werden die beteiligten Prozesse aber mehr oder weniger stark in ihrer potentiellen Parallelarbeit durch Sender-Blockaden eingeschränkt. Durch Kombination der Parameterwerte von Kommunikationsmuster und Synchronität werden vier verschiedene Kommunikationsmodelle definiert, die man in einer Matrix zusammenfassen kann (siehe Abbildung 7-1).
Abb. 7-1 Einfaches Klassifikationsschema für Nachrichtenkommunikation
Ihre genauere Semantik läßt sich anhand von Zeitlaufdiagrammen beschreiben. Asynchrone Meldung (Abbildung 7-2) Der Sender wird bei dieser Kommunikationsform lediglich bis zur Ablieferung der Meldung an das Kommunikationssystem blockiert. Die dafür benötigte Zeit kann man näherungsweise vernachlässigen. Der Sender wird damit nur geringfügig in seiner Unabhängigkeit gegenüber dem Empfänger eingeschränkt, wodurch ein Maximum an potentieller Parallelarbeit zwischen Sender und Empfänger gewahrt bleibt. Der Empfänger wird in einer Receive-Operation bis zum Eintreffen einer Nachricht blockiert.
7.1
Elementare Nachrichtenkommunikationsmodelle
Abb. 7-2 Asynchrone Meldung
Liegt die Nachricht bereits vor, dann kann der Empfänger ohne Verzögerung weiterarbeiten. Die zeitliche Entkopplung von Sender und Empfänger hat allerdings einen Preis: Sie setzt die Fähigkeit des Kommunikationssystems zur Pufferung von Nachrichten voraus.
Asynchrone Meldungen müssen gepuffert werden
Synchrone Meldung (Abbildung 7-3) Bei dieser Kommunikationsform sind Sender und Empfänger von Meldungen zeitlich derart gekoppelt, daß es einem Sender unmöglich ist, Meldungen in schnellerer Folge zu erzeugen, als diese auf Empfängerseite verarbeitet werden können. Dies wird durch Blockade des Senders bis zur Ablieferung der aktuellen Nachricht beim Empfänger erreicht ([Hoare 1978], [Liskov 1985]). Über die Ablieferung der Nachricht wird die Senderseite durch eine Quittungsnachricht informiert, die zur Aufhebung der Blockade des Senders führt. Eine interessante Realisierungsvariante dieses Modells sieht vor, daß Sender und Empfänger vor Austausch der eigentlichen Meldung Sende- und Empfangsbereitschaft herstellen. In diesem Fall braucht die Nachricht nirgends mehr gepuffert zu werden, sondern kann direkt vom Adreßraum des Senders in den Adreßraum des Empfängers übertragen werden. Man spricht auch von der Rendezvous-Technik. Das Erreichen des Rendezvous-Punktes definiert den Zeitpunkt, zu dem Senderund Empfängerseite wechselseitiges Einverständnis über das Vorliegen der Sende- und Empfangsbereitschaft erklärt haben.
Synchrone Meldung •• Rendezvous
7
Nachrichtenbasierte Prozeßinteraktion
Abb. 7-3 Synchrone Meldung
Synchroner Auftrag (Abbildung 7-4)
Starke Einschränkung bei der potentiellen Parallelarbeit
Remote Procedure Call (RPC)
Diese Kommunikationsform unterscheidet sich von einer synchronen Meldung dadurch, daß die Bearbeitung der Nachricht auf Empfängerseite Teil der Nachrichtentransaktion ist und mit der Versendung einer Resultatnachricht für den Sender abgeschlossen wird. Dafür wird neben den Funktionen Send() und Receive() eine Funktion Reply() benötigt. Die Übertragung von Eingabeparametern an den Empfänger und die Rückübertragung von Resultaten an den Sender können auch unabhängig von der Auftrags- und Resultatnachricht durch den Empfänger über spezielle Funktionen gesteuert werden (vgl. hierzu den VKern [Cheriton 1984]). Synchrone Aufträge schränken die potentielle Parallelarbeit zwischen Sender und Empfänger noch stärker ein als synchrone Meldungen, da die zeitliche Kopplung zwischen ihnen die Bearbeitung der Nachricht umfaßt. Besonders geeignet ist die Kommunikation mittels synchroner Aufträge in Client/Server-Systemen, bei denen die beteiligten Instanzen (Prozesse oder Teams) entweder die Rolle von Dienstanbietern (Servern) oder Dienstnutzern (Clients) spielen. Eine sprachbasierte Variante der synchronen, auftragsorientierten Kommunikation wird in Rechnernetzen durch das RPC-Konzept (Remote Procedure Call ('[Nelson 1981], [Birell und Nelson 1984]) unterstützt, auf das in Abschnitt 7.3 näher eingegangen wird.
7.1
Elementare Nachrichtenkommunikationsmodelle
Abb. 7-4 Synchroner Auftrag
Asynchroner Auftrag (Abbildung 7-5) Bei diesem Kommunikationsmodell wird die auftragsbezogene Kommunikation mit den Vorteilen der asynchronen Nachrichtenverarbeitung kombiniert. Auftrag und Resultat werden als Paar unabhängiger Meldungen verschickt, wobei zwischen ihnen eine logische Verbindung über eine eindeutige Auftragskennung hergestellt wird. Sie gestattet einem Empfänger, selektiv auf das Eintreffen eines bestimmten Resultats zu warten. Offenbar kann bei dieser Kommunikationsform der Sender mehrere Aufträge gleichzeitig in Bearbeitung durch einen oder mehrere Server haben. Umgekehrt ist es einem Server auch möglich, an mehreren Aufträgen gleichzeitig zu arbeiten. Asynchrone Aufträge sind als Kommunikationsform dann interessant, wenn auf Client-Seite die strikt sequentielle Verarbeitung durch synchrone Aufträge unakzeptabel ist, aber kein Mechanismus zur Erzeugung unabhängiger Threads zur Verfügung steht.
Selektives Warten auf das Resultat ist möglich
7
Nachrichtenbasierte Prozeßinteraktion
Abb. 7-5 Asynchroner Auftrag
Verfeinerungen dieses elementaren Klassifikationschemas für Nachrichtenkommunikation werden in [Liskov 1979] diskutiert. Wettstein [Wettstein 1993] wendet das Klassifikationsmerkmal Synchronität nicht auf die Nachrichtentransaktion, sondern getrennt auf Sender und Empfänger an. Damit lassen sich asynchrone und synchrone Formen des Nachrichtenversands und -empfangs beliebig kombinieren. Beispiele Anhand einiger elementarer Beispiele soll der Gebrauch der oben eingeführten Kommunikationsmechanismen nun demonstriert werden. Wir beschränken uns dazu auf eine Client/Server-Struktur gemäß Abbildung 7-6, in der n Clients den Dienst eines Servers in Anspruch nehmen. Die Clients verkehren gewöhnlich über Aufträge mit dem Server, so daß in den nachfolgenden Beispielen synchrone Aufträge als Kommunikationsform benutzt werden.
7.1
Elementare Nachrichtenkommunikationsmodelle
Abb. 7-6 Einfache n:1 -Auftragsbeziehung zwischen Clients und einem Server
1. Beispiel: Ressourcen-Pool
Der Server kapselt ein exklusiv benutzbares Betriebsmittel ein und stellt dafür die Funktionen Aquire, Release und Use zur Verfügung. Use repräsentiert dabei eine generische Funktion, mit der alle auf dem Betriebsmittel erlaubten Benutzungsarten gemeint sind. Clients benutzen den Server in der Form: Aquire(); Use(); Use(); Use(); Release();
/*Anforderung eines Betriebsmittels*/
Zugriff auf Betriebsmittel über Nachrichten
/Freigabe des Betriebsmittels*/
Bei der Transformation dieses Benutzungsmusters in synchrone Aufträge sei angenommen, daß die Benutzungsart (d.h. die auszuführende Funktion) und die Ein- und Ausgabeparameter in der Auftrags- bzw. Resultatnachricht kodiert werden. Unter Benutzung der Primitive Send und Receive nimmt deshalb das oben dargestellte Benutzungsmuster die folgende Gestalt an: Send(Serverld, ("Acquire",Parameter)); Send(Serverld, ("Use",Parameter)); Send(Serverld, ("Use",Parameter));
Send(Serverld, ("Use",Parameter)); Send(Serverld, ("Release",Parameter));
Eine einfache Realisierung für den Server zeigt der untenstehende skeletthafte Code: Server { Loop { Receive(); /*Übernahme des nächsten Auftrags*/ switch (Benutzungsart) { case "Acquire": if (Ressource==frei) Resultat=RessourceId; else Resultat="Ressourcen belegt' break; case "Use": Funktion durchführen; break;
Funktionen des Servers
7
Nachrichtenbasierte Prozeßinteraktion
case "Release": Ressource freigeben; break; } Reply(); /*Resultat an Sender schicken*/ } } Problematik: Alle Ressourcen sind belegt
Busy Waiting
Multi-Threaded Server (Server-Team)
Für den Fall, daß alle Ressourcen belegt sind, bleibt dem Server nichts anderes übrig, als den Auftrag mit einer negativen Quittung (Ressourcen belegt) zu beenden. Nur so wird gewährleistet, daß durch nachfolgende Aufträge Ressourcen potentiell freigegeben werden und damit die Engpaßsituation beseitigt wird. Der Client kann aber auf diese negative Rückmeldung nur mit periodischer Wiederholung reagieren. Wir haben damit wieder die unerwünschte Situation einer aktiven Warteschleife, ein Phänomen, auf das wir bereits bei der Diskussion speicherbasierter Interaktionsmechanismen gestoßen sind. Das Problem läßt sich ohne Einführung neuer Konzepte nur durch Abkehr von der sequentiellen Verarbeitung der Aufträge im Server lösen. Das kann z.B. dadurch geschehen, daß ein Server einen zur Zeit nicht bearbeitbaren Auftrag zurückstellt, bis die Voraussetzungen dafür erfüllt sind. Dafür handelt man sich aber unübersichtlichere Programmstrukturen ein, da im Server angefangene, aber nicht vollständig bearbeitete Aufträge verwaltet werden müssen. Eine wesentlich elegantere Möglichkeit zur Organisation eines Servers, der an mehreren Aufträgen zeitlich überlappt arbeiten kann, bietet das bereits bekannte ThreadKonzept. Der Server besteht im Ruhezustand lediglich aus einem Prozeß, dem Listener, der die Aufträge entgegennimmt. Nach Annahme eines Auftrags erzeugt der Listener einen Arbeitsprozeß Worker, der für die vollständige Abwicklung des Auftrags verantwortlich ist und danach terminiert. Die Worker in ihrer Gesamtheit repräsentieren die Anzahl und den momentanen Status gleichzeitig in der Bearbeitung befindlicher Aufträge im Server, ohne daß dafür ein zusätzlicher Implementierungsaufwand durch den Entwerfer eines Servers geleistet werden muß (siehe Abbildung 7-7).
Abb. 7-7 Serverorganisation für die gleichzeitige Bearbeitung mehrerer Aufträge durch unabhängige WorkerProzesse
Untenstehend sind die Algorithmen für den Listener und einen Worker beispielhaft dargestellt.
7.1
Elementare Nachrichtenkommunikationsmodelle
Listener { Loop { ReceiveO; /* Auftragsannahme */ Erzeugung eines Worker-Prozesses W; Übergabe des Auftrags an W; } }
Die Bearbeitung des Auftrags kann Blockaden einschließen
Worker { Übernahme des Auftrags vom Listener; Bearbeitung des Auftrags; R e p l y O ; /*Resultat an Client senden */ }
Noch eleganter wäre eine Lösung, die unbearbeitete Aufträge überhaupt nicht zum Server durchläßt. Sie basiert auf Mechanismen für den selektiven Nachrichtenempfang, auf die in Abschnitt 7.2 näher eingegangen wird. 2. Beispiel: Abhängige Server Abhängige Server entstehen durch Unteraufträge, die Server zur Erledigung ihrer Aufgaben an andere Server delegieren. Die damit zusammenhängende Problematik wird anhand eines Auskunfts-Servers diskutiert, der auf Anfrage Adressen ermittelt. Die Namen der Personen werden mit dem Auftrag als Eingabeparameter, z.B. durch eine Zeichenkette, übergeben. Es sei angenommen, daß der Auskunfts-Server an seiner Schnittstelle die Funktionen Eintragen (Name, Adresse) Austragen (Name, Adresse) Adresse=Anfrage(Name)
anbietet. Ein Teil der durch den Auskunfts-Server verwalteten Informationen wird nach dem LRU-Prinzip in einem lokalen Cache zwecks schnelleren Zugriffs gehalten. Der überwiegende Teil der Informationen liegt jedoch auf einem Hintergrundspeicher, der durch einen Datei-Server DS verwaltet wird (siehe Abbildung 7-8). Wir verwenden wieder das Kommunikationsmodell der synchronen Aufträge. Abb. 7-8 Ein Auskunfts-Server, der Unteraufträge an einen Datei-Server absetzt
7
Nachrichtenbasierte Prozeßinteraktion
Eine einfache Realisierung für den Server ist untenstehend gezeigt: Auskunftsserver { Loop { Receive(); /* Übernahme des nächsten Auftrags */ switch (Funktion) { case "Eintragen": /* Schreibauftrag an Datei-Server */ Send(DS,("write",Name,Adresse)); break; case "Austragen": /* Delete-Auftrag an Datei-Server */ Send(DS,("delete",Name,Adresse)); (Name,Adresse) im Cache löschen; case "Anfrage": if (Name im Cache) Resultat=Cache(Name); else { /* Adresse von DS lesen */ Send(DS,("read",Name,Adresse)); (Name,Adresse) im Cache speichern Resultat=Cache(Name);
} } Reply(); /* Adresse an Sender schicken */
} }
Lange Blockadezeiten bei der Auftragsbearbeitung wirken negativ auf die Server-Leistung
Multi-Threaded Server (Server-Team)
Offensichtlich wird die maximale Auskunftsrate im Server durch die Wahrscheinlichkeit von Plattenzugriffen stark eingeschränkt. Extrem ungünstig auf die Leistung wirkt sich die in Abbildung 7-9 gezeigte Situation aus: Die als nächste zu bearbeitende Anfrage löst einen Unterauftrag an den Datei-Server aus, der den Auskunfts-Server für die Dauer der Bearbeitung blockiert, obwohl alle nachfolgenden Anfragen sofort bearbeitet werden könnten. Es entstehen damit unnötige Totzeiten im Server. Wie im vorhergehenden Beispiel basieren Lösungen zur Vermeidung dieses Effektes auf der Fähigkeit eines Servers, an mehreren Aufträgen gleichzeitig zu arbeiten und damit Überholvorgänge zwischen Aufträgen zu ermöglichen. Bei einer sequentiellen Serverstruktur (1 Thread) kann die zeitlich überlappte Bearbeitung mehrerer Aufträge jedoch nur realisiert werden, wenn Blockaden des Auskunfts-Servers durch Unteraufträge an den Datei-Server ausgeschlossen werden. Das aber verlangt zwingend eine asynchrone Form der Auftragsabwicklung zwischen Auskunfts-Server und Datei-Server. Wesentlich eleganter läßt sich das geschilderte Problem auch hier durch eine Teamstruktur gemäß des Worker-Modells lösen, wobei dann die übersichtlichere Form der synchronen Auftragsbearbeitung beibehalten werden kann. Überholvorgänge zwischen Aufträgen regeln sich automatisch durch Blockaden derjenigen Worker-Prozesse, die einen Unterauftrag an den Datei-Server absetzen.
7.1
Elementare Nachrichtenkommunikationsmodelle
Abb. 7-9 Auftragssituation, die bei sequentieller Auftragsbearbeitung Totzeiten im Server auslöst
Fassen wir die Ergebnisse der bisher gewonnenen Erkenntnisse aus den beiden Beispielen noch einmal zusammen:
•
• •
•
In Client/Server-Systemen können bei sequentieller Auftragsbearbeitung in Servern aktive Warteschleifen und Totzeiten entstehen. Eine Vermeidung dieser unerwünschten Phänomene ist nur möglich, wenn Server in die Lage versetzt werden, gleichzeitig an mehreren Aufträgen zeitlich überlappt zu arbeiten. Die gleichzeitige Bearbeitung mehrerer Aufträge in einem Server erfordert entweder asynchrone Formen der Auftragsabwicklung oder eine Teamstruktur der Server (1 Auftrag = 1 Thread). Eine Teamstruktur im Server auf der Basis von Worker-Prozessen stellt die überzeugendste Lösung für das Problem dar.
Es ist das Verdienst von B. Liskov, diese Zusammenhänge erstmals erschöpfend beschrieben zu haben [Liskov 1985]. 3. Beispiel: Server als Fließband Bei einer Fließbandverarbeitung besteht ein Server aus n spezialisierten Funktionseinheiten, die jeweils eine wohldefinierte Teilaufgabe an einem Auftrag erledigen. Ein Auftrag wird der ersten Funktionseinheit übergeben und nach Erledigung seiner Teilaufgabe zusammen mit den Ergebnissen an die zweite Funktionseinheit weitergereicht. Die zweite Funktionseinheit verfährt genauso und reicht den Auftrag an die dritte Funktionseinheit weiter usw., bis der Auftrag schließlich die letzte Funktionseinheit durchlaufen hat, die die erfolgreiche Bearbeitung des Auftrags gegenüber dem Client mittels Reply() bestätigt. Ein Fließband-Server, der aus drei Funktionseinheiten besteht, ist in Abbildung 7-10 dargestellt. Offensichtlich handelt es sich bei der Fließbandverarbeitung um eine Form der Parallelverarbeitung, da im eingeschwungenem Zustand jede Funktionseinheit mit einem anderen Auftrag be-
Zusammenfassung
7
Nachrichtenbasierte Prozeßinteraktion
schäftigt ist, d.h., ein Server mit n Funktionseinheiten im Fließband kann an n Aufträgen gleichzeitig arbeiten. Abb. 7-10 Fließbandstruktur eines Servers
Zur Organisation eines Fließbandes ist es erforderlich, daß die erste Funktionseinheit den Auftrag übernimmt und die letzte den Auftrag durch Reply() abschließt. Die Funktionseinheiten untereinander müssen die Aufträge als Meldungen verschicken. Die auftragsorientierte Kommunikation ist für das Durchreichen der Aufträge zwischen den Funktionseinheiten ungeeignet. Für die drei Funktionseinheiten Fe1, Fe2, Fe3 ergibt sich damit der untenstehende Aufbau: Fe1 { LOOP { Receive(m); /* Auftragsübernahme */ Bearbeitungsschritt 1; /* Auftrag als Meldung weiterschicken*/ Send(Fe2,m'); } } Fe2 { LOOP { Receive(m'); /* Auftragsübernahme von Fe1 */ Bearbeitungsschritt 2; /* Auftrag als Meldung weiterschicken */ Send(Fe3,m''); } } Fe3 { LOOP Receive(m''); /* Auftragsübernahme von Fe2 */ Bearbeitungsschritt 3; /* Resultat an Client */ Reply (Client,Resultat); } }
Zusätzlicher Datenaustausch während der Auftragsbearbeitung
Das Beispiel zeigt, daß der Send() -Receive() -Reply() -Zyklus nicht immer ausreicht, um auftragsorientierte Kommunikationsstrukturen zu realisieren. Häufig müssen für die Durchführung eines Auftrags größere, aber im Umfang variable Datenmengen in beiden Richtungen zwischen Client und Server transportiert werden. Dieser Datentransport wird dann besser unabhängig von der Auftrags- und Resultatnachricht durch zusätzliche Funktionen gesteuert.
7.2
7.2
Erweiterungen elementarer Kommunikationsmodelle
Erweiterungen elementarer Kommunikationsmodelle
Zu den in Abschnitt 7.1 eingeführten Kommunikationsmodellen wurden zahlreiche Erweiterungen vorgeschlagen, die alle den Zweck verfolgen, komplexe Kommunikationsbeziehungen übersichtlicher zu gestalten. Die wichtigsten Erweiterungen werden in diesem Abschnitt vorgestellt. Ports Bisher wurde stillschweigend unterstellt, daß der Adressat einer Nachricht ein Adreßraum ist, wobei irgendein Prozeß in diesem Adreßraum eine Nachricht mittels Receive () entgegennehmen kann. Zu diesem Zweck muß jedem Adreßraum genau ein Puffer für neu eingetroffene, aber noch nicht an einen Prozeß abgelieferte Nachrichten zugeordnet werden (siehe Abbildung 7-11). Abb. 7-11 Server mit einem fest zugeordneten Nachrichtenpuffer
Ports bieten die Möglichkeit, von diesem starren Schema abzuweichen. Sie stellen Endpunkte einer Kommunikation dar und können nach Bedarf dynamisch eingerichtet und gelöscht werden [Liskov 1979]. Dazu stehen gewöhnlich die Funktionen PortId=CreatePort() DeletePort(Portld)
zur Verfügung. Mittels CreatePort() wird ein neuer Kommunikationsendpunkt eingerichtet und mit dem Adreßraum A verknüpft, aus dem die Funktion aufgerufen wurde. Externe Prozesse (d.h. Prozesse
Ports erhöhen die Flexibilität beim Nachrich tenempfang
7
Nachrichtenbasierte Prozeßinteraktion
aus anderen Adreßräumen) können nun Nachrichten an den Port in der Form Send(A.Portld,m)
schicken, sofern sie die notwendige Adreßinformation ( A , PortId) besitzen. Die Receive-Operation benötigt nun auch einen zusätzlichen Parameter zur Selektion des Ports: Receive(Port,m)
Selektiver Nachrichtenempfang durch den Einsatz von Ports
Abb. 7-12 Server mit Clientspezifischen Ports und Worker-Prozessen
Ports sind ein elegantes Hilfsmittel, um selektiven Nachrichtenempfang zu realisieren. So wäre es z.B. denkbar, daß ein Server Client-spezifische Ports nach Bedarf einrichtet und jedem Port einen eigenen Worker-Prozeß zuordnet, der sich speziell um den Auftrag (oder mehrere Aufträge) des zugeordneten Clients kümmert. Die Einrichtung dieser Ports kann davon abhängig gemacht werden, ob Clients in einer vorangestellten Authentisierung vom Server ihre Berechtigung zur Benutzung der Dienste erhalten haben. Abbildung 7-12 zeigt den Aufbau eines so strukturierten Servers. Im Ruhezustand ist kein Port eingerichtet, d.h., der Server kann nur über den fest mit seinem Adreßraum assoziierten Nachrichtenpuffer angesprochen werden. Jeder Client verkehrt mit dem Server nach untenstehendem Muster, wobei der Einfachheit halber synchrone Aufträge zur Kommunikation verwendet werden.
7.2
Erweiterungen elementarer Kommunikationsmodelle
Client { Send(Server,Authentisierungsnachricht); if (erfolgreich) { Send(Server.PortId,a 1 ); /* Auftrag 1 */ Send(Server.PortId,a 2 ); /* Auftrag 2 */
PortId wurde vom Server als Resultat der Authentisierung zurückgeschickt
Send(Server.PortId, a n ) ; /* Auftrag n */ Send(Server.PortId,Abmeldung); /* Port wird gelöscht */ } }
Der Prozeß Authentisierung im Server hat den Aufbau: Authentisierung { LOOP { Receive(Client,Authentisierungsnachricht) if(berechtigt) { PortId=CreatePort(); Erzeuge Worker-Prozeß; Übergebe Portld an Worker; Reply(Client,"PortId"); } else Reply(Client,"abgelehnt"); } }
Jeder Worker-Prozeß folgt dem Aufbau: Worker { Übernahme der PortId; LOOP { Receive(PortId,m); switch(Funktion) { case"Fl": F1 ausführen; Reply(Client,ok); break; case"F2": F2 ausführen; Reply(Client,ok); break; case"Abmelden": DeletePort(PortId); Reply(Client,"ok"); break; } } }
Der selektive Nachrichtenempfang über Ports stellt ein hilfreiches Instrument dar, um wirksame Schutzmechanismen zu etablieren. Jede PortId muß zu diesem Zweck allerdings von Servern und Clients geheim gehalten werden.
Schutz durch Geheimhalten der Portidentifikation
7 Nachrichtenbasierte Prozeßinteraktion
Kanäle Verbindungsorientierte Kommunikation Verbindungsaufbau vor der eigentlichen Kommunikation
Bisher wurde von einer verbindungslosen Kommunikation ausgegangen, d.h., eine Nachricht kann an einen Port geschickt werden, wenn seine Adresse bekannt ist. Bei einer verbindungsorientierten Kommunikation dagegen muß zuerst eine logische Verbindung - auch Kanal genannt - zwischen den Kommunikationspartnern etabliert werden, bevor mit dem Austausch von Information begonnen werden kann. Es bietet sich an, Kanäle zwischen Ports einzurichten (siehe Abbildung 7-13).
Abb. 7-13 Durch Kanäle verbundene Ports
Verbindungen können meist in beide Richtungen verwendet werden
Im allgemeinen Fall besitzt ein Kanal die Fähigkeit, bidirektional zu übertragen (Duplex-Verbindung), so daß Ports nicht mehr nur als Empfangsstellen für Nachrichten, sondern auch als Quellen dienen. Bei einer verbindungsorientierten Kommunikation können Clients und Server flexibel über den Austausch von Meldungen kommunizieren. Im Gegensatz zu den relativ starren Formen der in Abschnitt 7.1 behandelten auftragsorientierten Kommunikation können somit auf einfache Weise komplexere Auftragsbeziehungen realisiert werden, wie das untenstehende Beispiel zeigt:
7.2
Erweiterungen elementarer Kommunikationsmodelle
Die Einrichtung eines Kanals setzt das Durchlaufen des untenstehend dargestellten Protokolls zwischen Client und Server voraus:
Verbindungsaufbau
Die co««ec£-Nachricht wird an den Adreßraum des Servers geschickt und enthält die Identität des Clients sowie die global eindeutige Portadresse C l i e n t . Porte. Bei erfolgreicher Authentisierung richtet der Server einen neuen Port ein und verknüpft ihn mit C l i e n t .Porte. Nach Ankunft der Bestätigungsnachricht connected beim Client, die die Portidentifikation Server. Ports enthält, wird diese ihrerseits mit der Portadresse C l i e n t . Porte verknüpft. Nach der Verknüpfung ist der Kanal betriebsbereit, d.h., es können nun Meldungen von beiden Seiten mittels Send
(LocalPortId,m)
verschickt bzw. mit Receive
(LocalPortId,m)
empfangen werden. Für die Durchführung der Port-Verknüpfungen wird eine zusätzliche Funktion PortLink
(LocalPort,RemotePort)
benötigt, die Adreßinformation über einen externen Port in die den lokalen Port repräsentierende Datenstruktur einträgt. Verbindungsorientierte Kommunikation ist weit verbreitet und wird z.B. auch durch das TCP/IP-Protokoll unterstützt [Stevens 1994].
Senden und Empfangen
7 Nachrichtenbasierte Prozeßinteraktion
Ströme Nachrichtengrenzen bleiben nicht erhalten
Ströme stellen eine weitere Komfortsteigerung dar, indem sie auf Kanälen die tatsächlichen Nachrichtengrenzen verdecken. Abbildung 7-14 verdeutlicht das Prinzip. Ein Sender schickt über einen Kanal mittels Send () -Operationen drei Nachrichten an einen Empfänger. Die Nachrichten werden jedoch logisch zu einem durchgehenden Byte-Strom vereinigt, dem man auf Empfangsseite die Nachrichtengrenzen nicht mehr entnehmen kann. Ein Empfänger kann nun den Byte-Strom in Portionen verarbeiten, ohne sich an den ursprünglichen Nachrichtenlängen orientieren zu müssen. Pipes in UNIX [Bach 1986] und das Internet-Protokoll TCP realisieren das Konzept der Ströme.
Abb. 7-14 Kommunikation über Ströme
Gruppenkommunikation
Senden einer Nachricht an eine Empfängergruppe
Bisher wurden sowohl eine l:l-Kommunikation als auch eine n:lKommunikation betrachtet, die entweder die Kommunikation zwischen einem Sender/Empfänger-Paar (1:1) oder von n Sendern und einem Empfänger (n:l) unterstützen. Sie teilen die Eigenschaft, daß es genau einen Empfänger für eine Nachricht gibt. Eine l:n-Kommunikation erlaubt es dagegen, eine Nachricht mittels einer Send()-Operation an n Empfänger zu schicken. Die Empfänger sind zu diesem Zweck zu einer Gruppe zusammengeschlossen. Gruppenkommunikation ist hilfreich beim Verteilen allgemeiner Zustandsinformationen an viele Prozesse und stellt ein besonders wichtiges Konzept in verteilten Systemen dar.
7.3 Remote Procedure Call (RPC) RPC = Synchroner Auftrag
Prozeduraufruf über A dreßraumgrenzen
Das RPC-Konzept ist die sprachorientierte Variante der synchronen, auftragsorientierten Kommunikation. RPC-Systeme unterstützen den Prozeduraufruf über Adreßraumgrenzen hinweg, d.h., ein im Adreßraum A ablaufendes Programm kann Unterprogramme in anderen Adreßräumen aufrufen. Die Attraktivität des RPC-Konzeptes liegt darin, daß mit dem Prozedurkonzept vertraute Programmierer nicht
J
7.3
Remote Procedure Call (RPC)
umdenken müssen, wenn sie die Interaktion zwischen Programmen in disjunkten Adreßräumen organisieren. Da es prinzipiell unerheblich ist, ob die Adreßräume auf einem oder mehreren Rechnerknoten installiert sind, bietet sich der RPC-Mechanismus auch als elementares Kommunikationsmittel in verteilten Systemen an. Die grundlegenden Arbeiten zu RPC-Systemen gehen auf Nelson und Birell zurück ([Nelson 1981], [Birell und Nelson 1984]). Inzwischen haben RPC-Systeme einen hohen Reifegrad erreicht und werden als Laufzeitpakete auf den gängigen Betriebssystemen und Plattformen für verschiedene Sprachen angeboten [Corbin 1991]. Das grundlegende Prinzip wird durch Abbildung 7-15 verdeutlicht. Ein im Adreßraum A ablaufender Prozeß führt einen entfernten Prozeduraufruf auf eine Prozedur P durch, die im Adreßraum B liegt. Wie bei einem lokalen Prozeduraufruf wandert mit dem Aufruf die Kontrolle logisch von A nach B, wobei beim Aufruf Eingabeparameter für P von A nach B und Resultatparameter von B nach A transportiert werden müssen. Ein grundlegendes Problem, das jedes RPC-System lösen muß, ist die dynamische Bindung von P an den aufrufenden Prozeß zur Laufzeit und nicht - wie im lokalen Fall üblich - zur Übersetzungszeit.
Grundlegendes Prinzip
Abb. 7-15 Prinzip des Remote Procedure Call (RPC)
Abbildung 7-16 zeigt etwas genauer, wie diese Bindung hergestellt wird. Auf Client-Seite wird der Aufruf an die entfernte Prozedur rp() zunächst an einen lokalen Stellvertreter - den Client-Stub - gerichtet. Diese Stub-Prozedur ist, wie gewöhnlich, statisch mit dem Hauptprogramm gebunden. Ihre wesentliche Aufgabe besteht darin, die Eingabeparameter in eine Nachricht zu verpacken und einem Server zuzustellen, der die aufzurufende Prozedur enthält. In dem Server kümmert sich ein Stellvertreter des ursprünglichen Prozesses - der Server-Stub um die Behandlung der Nachricht. Die Parameter werden aus der Nachricht ausgepackt und die gewünschte Prozedur aufgerufen. Nach Rückkehr der Kontrolle aus der Prozedur rp werden die Resultate im Server-Stub wieder in eine Nachricht verpackt und dem Client-Stub zugeschickt. Dort werden sie wieder ausgepackt und mit der Rückkehr aus der Stub-Prozedur dem Hauptprogramm übergeben.
Client-Stub
Server-Stub
7
Nachrichtenbasierte Prozeßinteraktion
Abb. 7-16 Realisierungskonzept für RPC
Aufbau der Client-Stub
Damit folgen Client-Stub und Server-Stub dem untenstehenden einfachen Aufbau: rp(a) returns r { /* Client Stub */ Nachrichtenpuffer m, m=Einpacken(a); send (Server,m); /* Auftrag schicken */ /* Resultat empfangen */ receive(m); r=Auspacken(m) return r; }
Aufbau der Server-Stub
Fragestellungen
ServerStub { LOOP { receive(m); a=Auspacken(m); r=rp (a) ,m=Einpacken(r); send (client,m) } }
/* Auftrag übernehmen */ /* Aufruf der Prozedur rp */
Viele Fragen werden durch dieses einfache Schema noch nicht beantwortet:
• • • • •
Wer ist für das Schreiben der Stubs verantwortlich? Woher erfährt ein Client die Adressen der Server, an die er sieh für einen Aufruf wenden muß? Wann werden die Server eingerichtet/terminiert? Wie geht man mit Heterogenität um (d.h., Client und Server sind auf Rechnern unterschiedlicher Architektur plaziert)? Wie können sich Server gegen unauthorisierte Zugriffe abschirmen?
Für alle diese Fragen müssen leistungsfähige RPC-Systeme eine Lösung anbieten. Bevor der eigentliche Aufruf einer entfernten Prozedur erfolgen kann, gehen deshalb mehrere Schritte voraus: a) Für einen neu einzurichtenden Dienst mit den dazugehörenden Funktionen müssen die Stubs für die Client- und Server-Seite erzeugt werden. Dies geschieht üblicherweise über Generato-
7.3 Remote Procedure Call (RPC)
ren, die eine Beschreibung der Prozedurschnittstellen (Signaturen) als Eingabe erhalten [Corbin 1991]. Die Stubs müssen schließlich mit dem Client-Code (Client-Seite) und den entfernten Prozeduren (Server-Seite) zu einem ablauffähigen Gesamtsystem gebunden werden. b) Ein Namensdienst muß die Adressen der Server, die den Zugang zu einem Dienst eröffnen, verwalten und auf Anfrage bekanntgeben. c) In einer Authentisierungsphase muß sich ein Client vom zuständigen Server zunächst die Erlaubnis einholen, auf diesen Dienst zuzugreifen. Dies könnte z.B. durch Einrichtung eines Kanals zwischen Client und Server geschehen, an dessen Server-Seite eine neu erzeugte Instanz des Server-Stubs auf eingehende Aufträge wartet (siehe Abschnitt 7.2). d) In heterogenen Rechnernetzen muß in den Stubs zusätzlich die Transformation aller Daten in einen kanonischen Standard erfolgen, bevor sie in Nachrichten verschickt werden. Dazu stehen entsprechende Datenbeschreibungsstandards zur Verfügung wie z.B. XDR [Corbin 1991] und ASN.l [ISO 8824]. Liegen Client und Server auf demselben Rechnerknöten, dann sind einige aufwendige Algorithmen in den Stubs überflüssig. Mittlerweile sind hierfür leichtgewichtige RPC-Realisierungen entwickelt worden, die nahezu identische Performanz wie lokale Prozeduraufrufe aufweisen [Birell und Nelson 1984]. Natürlich ist man bestrebt, die Semantik eines entfernten Prozeduraufrufs dem eines lokalen Aufrufs weitgehend anzugleichen. Dies stößt aber auf prinzipielle Schwierigkeiten (siehe z.B. auch [Tanenbaum und Renesse 1988]): •
•
•
Die Parameterübergabe durch Call-by-Reference, die viele Programmiersprachen unterstützen, läßt sich nicht mit vertretbarem Aufwand realisieren, da die gerufene Prozedur mit Zeigern auf einen fremden Adreßraum wenig anfangen kann. Parameterlisten in Prozeduren, die das Prinzip der Typenstrenge verletzen, können von Stub-Generatoren nicht mehr behandelt werden (man denke z.B. an die printf()-Funktion in C). Liegen Prozeduren auf einem anderen Rechner, dann müssen Fehlerfälle wie z.B. der Ausfall dieses Rechners dem aufrufenden Programm gemeldet werden, damit es Ersatzmaßnahmen ergreifen kann. Entsprechende Return-Parameter sind beim lokalen Prozeduraufruf unbekannt.
Automatische Generierung der Stub-Prozeduren
Namensdienst für die Server-Adressen
Authentisierung
Datentypkonvertierungen in heterogenen Systemen
Client und Server auf einem Rechner
Probleme bei der RPC-Semantik
7 Nachrichtenbasierte Prozeßinteraktion
7.4
Signale erlauben den asynchronen Empfang von Daten
Anmelden der Empfangsroutine
Signale
Die in Abschnitt 7.1 eingeführten elementaren Kommunikationsmodelle basieren alle auf der Blockade des Empfängers, sofern zum Zeitpunkt der Ausführung einer Receive-Operation keine Nachricht vorliegt. Es gibt jedoch Situationen, die eine sofortige Reaktion des Empfängers beim Eintreffen einer Nachricht erforderlich machen auch dann, wenn zu diesem Zeitpunkt kein Prozeß auf diese Nachricht wartet. Das Konzept der Signale bietet eine Lösung für dieses Problem auf der Basis einer nichtblockierenden Receive-Operation. Signale sind sehr kurze Nachrichten (oftmals nur ein Speicherwort lang), die nach dem Eintreffen beim Empfänger sofort die Ausführung einer zuvor vereinbarten Routine auslösen. Die Receive-Operation nimmt dann die untenstehende Form an: Receive(Signalisierungsursache, HandlerAdresse)
(Signal-Handler)
Sie übergibt dem Kommunikationssystem lediglich die Adresse einer Routine, die beim Eintreffen eines bestimmten Signals aktiviert werden soll. Die Anzahl der Signalisierungsursachen ist gewöhnlich systemabhängig und umfaßt alle Hardwareausnahmen, Softwareausnahmen im Betriebssystemkern und explizit durch Anwendungen festgestellte Ausnahmesituationen. Mit Signal(Empfänger, Signalursache, Parameter)
Zusätzlicher Thread wird mit dem Signalempfang gestartet
kann ein Signal an einen beliebigen Empfänger geschickt werden. Wurde auf Empfängerseite keine Handlerroutine mittels Receive () vereinbart, dann tritt eine standardmäßige Ersatzbehandlung des Systems an ihre Stelle. Die konsequente Umsetzung dieses Konzeptes läßt sich am elegantesten durch Erzeugung eines neuen Threads mit jedem eintreffenden Signal bewerkstelligen. Der Thread wird mit der Handlerroutine verknüpft, die für die Signalursache zuständig ist. Außerdem werden ihm auf seinem Stack die Parameter übergeben, die mit der Signaloperation versandt wurden. Im allgemeinen Fall können durch das zeitlich unkoordinierte Eintreffen verschiedener Signale zum nahezu selben Zeitpunkt mehrere Threads mit einer Signalbehandlung pro Adreßraum beschäftigt sein. Die Beendigung einer Signalbehandlung ist gleichbedeutend mit einer Selbstterminierung des entsprechenden Threads. Leider läßt sich dieses Verfahren nur dann realisieren, wenn in einem Adreßraum die gleichzeitige Existenz mehrerer kernbasierter Threads unterstützt wird. Da das in früheren Systemen nur selten der Fall war, hat sich eine eingeschränkte Variante dieses Verfahrens breit durchgesetzt, die stark an das Unterbrechungskonzept auf Hard-
7.4
Signale
wareebene erinnert. Sie basiert darauf, Signale als Software-Interrupts aufzufassen, die einem bestimmten Prozeß zugeleitet werden. Trifft ein Signal bei einem Prozeß ein, dann wird dessen momentaner ThreadZustand im TCB gerettet und der Thread mit der Signal-Handlerroutine fortgesetzt. Für die Zeit der Signalbehandlung befindet sich der Prozeß in einem besonderen Ausführungsmodus (Signalmodus), der nur über eine weitere Funktion
Signale als SoftwareInterrupt
SignalReturn()
beendet werden kann. Sie setzt die Behandlung im Normalmodus in dem Zustand fort, der bei der Ankunft des Signals gerettet wurde (es sei denn, der gerettete Zustand wurde zwischenzeitlich überladen). In Abbildung 7-17 ist der prinzipielle Verlauf einer Signalbehandlung dargestellt. Der entscheidende Unterschied zu dem zuvor beschriebenen Verfahren ist die Begrenzung auf höchstens eine Signalbehandlung im Prozeß zu einem Zeitpunkt. Bei mehreren gleichzeitig eintreffenden Signalen wird ein Signal durchgelassen; alle anderen werden im Kern gepuffert und der Reihe nach abgearbeitet. Eine weitere unangenehme Begleiterscheinung dieser eingeschränkten Verfahrensvariante entsteht beim Zugriff auf Daten, auf die auch im Normalmodus zugegriffen wird. Da die Ausführung im Normalmodus und Signalmodus durch einen Thread (und nicht durch zwei unabhängige Threads) erfolgt, können zur Realisierung des wechselseitigen Ausschlusses keine Synchronisationsmechanismen wie z.B. Semaphore (vgl. Kapitel 6) eingesetzt werden. Träfe nämlich ein Signal zu einem Zeitpunkt ein, in dem gerade eine Sperre über ein Semaphor S gehalten wird, dann würde die Ausführung der P-Operation auf dieses Semaphor in der Signalbehandlung unausweichlich zu einer Verklemmung führen (siehe hierzu Kapitel 8). Abb. 7-17 Signalbehandlung mit einem Thread
Die primitivste Form einer nichtblockierenden Receive-Operation erhält man dadurch, daß sie bei fehlender Nachricht im Empfangspuffer sofort die Kontrolle an den Aufrufer zurückreicht. In einem ReturnCode wird dem Aufrufer mitgeteilt, ob eine Nachricht vorlag oder nicht. Die Receive-Operation kombiniert in diesem Fall den Test auf Vorliegen einer Nachricht mit ihrer Übernahme in den eigenen Adreßraum.
7
Nachrichtenbasierte Prozeßinteraktion
7.5
Einführen von Nachrichtenprioritäten
Echtzeitaspekte
Die Berücksichtigung von Anforderungen der Echtzeitverarbeitung an Nachrichtenkommunikationssysteme läßt sich relativ schnell abhandeln, da hierzu nur sehr wenige neue Gesichtspunkte ins Spiel kommen. Von einem erheblichen Wert ist die Möglichkeit der Zuordnung von Prioritäten zu Nachrichten. Nachrichten von hoher Priorität sollten 1. vom Kommunikationssystem bevorzugt transportiert werden, 2. nach Möglichkeit schneller transportiert werden und 3. beim Empfänger prioritätsgerecht in den Nachrichtenpuffer eingekettet werden. Insbesondere die dritte Maßnahme kann zu einer erheblich beschleunigten Bearbeitung einer Nachricht führen, da potentiell eine große Anzahl bereits für den Empfänger vorliegender Nachrichten überholt wird. Für Echtzeitprobleme besonders geeignet ist das Konzept der Signale, durch die unabhängig von der Bereitschaft des Empfängers die Annahme und Bearbeitung einer Nachricht erzwungen werden kann.
7.6
Kopiervorgänge bestimmen die Effizienz der Nachrichtenkommunikation
Implementierungsaspekte
Bei der Implementierung von Nachrichtenkommunikationssystemen innerhalb eines Rechners spielen Effizienzprobleme eine überragende Rolle. Der überwiegende Zeitbedarf für den Transport von Nachrichten entsteht durch das Anfertigen von Kopien im Speicher. In Abbildung 7-18 ist der Weg einer Nachricht vom Sender zum Empfänger grundsätzlich dargestellt. Mit dem Send-Aufruf wird ein Zeiger auf die Nachricht im Adreßraum des Senders an das Kommunikationssystem übergeben. Das Kommunikationssystem fordert daraufhin Speicherplatz für die Nachricht und einen zusätzlichen Nachrichtenkopf an, der der eigentlichen Nutznachricht vorangestellt wird und Adreß- und Kontrollinformation enthält. Der Nachrichtenkopf kann z.B. die Adresse des Absenders, eine eindeutige Nachrichtensequenznummer, die Empfängeradresse und die Nachrichtenpriorität enthalten. Nach Zuteilung des Speichers wird die Nachricht das erste Mal kopiert (vom Sender-Adreßraum in den Nachrichtenpuffer des Kommunikationssystems) und der Nachrichtenkopf initialisiert. Ist der Empfänger zur Übernahme dieser Nachricht bereit, dann wird der um den Nachrichtenkopf reduzierte Teil der Nachricht aus dem Kommunikationssystem in den Adreßraum des Empfängers kopiert. Damit wird die Nachricht wenigstens
7.6 Implementierungsaspekte
zwei Mal vollständig kopiert, bis sie den Empfänger erreicht hat (bei Übertragung einer Nachricht über Netze können weitere Kopiervorgänge durch die unvermeidbare Schnittstelle zwischen Rechner und Netzwerk notwendig werden). Abb. 7-18 Weg einer Nachricht vom Sender zum Empfänger
Der doppelte Kopieraufwand erscheint auf den ersten Blick unvermeidbar, ist es aber nicht. Folgende Optimierungen sind möglich: a) Bei asynchroner Kommunikation kommt man um eine Doppelkopie der Nachricht nicht herum, falls zum Zeitpunkt des Nachrichtenversands der Empfänger nicht übernahmebereit ist. Ist der Empfänger jedoch bereits in einer Receive-Operation blockiert, dann kann man sich die erste Kopie sparen: Die Nachricht kann direkt in den Empfänger-Adreßraum kopiert werden. b) Bei synchroner Kommunikation empfiehlt sich ein striktes Rendezvous-Protokoll: Wird erst die Sende- und Empfangsbereitschaft abgewartet, bevor der Nachrichtentransport beginnt, dann kann eine Nachricht immer direkt vom Adreßraum des Senders in den Adreßraum des Empfängers kopiert werden. c) Bei Adreßräumen, die durch virtuelle Adressierung unterstützt werden, kann auf doppeltes Kopieren grundsätzlich verzichtet werden. Es kann sogar vollständig auf das Kopieren verzichtet werden, wenn die übertragenen Nachrichteninhalte nur gelesen werden. Das bereits in Kapitel 4 angesprochene Copy-on-
Optimierungsmöglichkeiten
7 Nachrichtenbasierte Prozeßinteraktion
Copy-on-Write-Verfahren ist bei virtuellen Adreßräumen anwendbar
Write-Verfahren [Fitzerald und Rashid 1986] basiert darauf, anstelle der zu übertragenden Nachricht lediglich die Seitendeskriptoren in eine Nachricht zu verpacken und an den Empfänger zu schicken. Der schreibende Zugriff zu dem Speicherbereich im Adreßraum des Senders, der die Nachricht enthält, wird mit der Send-Operation schreibgesperrt. Mit einer Receive-Operation wird einem Empfänger lediglich der Speicherbereich der Nachricht in Form ihrer Seitentabellendeskriptoren an irgendeiner Stelle im Empfänger-Adreßraum eingeblendet (Voraussetzung ist, daß die relative Lage der Nachricht in bezug auf den Seitenanfang nicht verändert wird). Mit der ReceiveOperation wird gleichzeitig der Zugriff auf die nur virtuell empfangene Nachricht schreibgesperrt. Lesende Zugriffe auf die so übertragene Nachricht sind damit ohne einen physischen Kopiervorgang möglich. Ein Schreibzugriff auf Sender- oder Empfängerseite löst jedoch zwangsläufig einen Kopiervorgang für die betroffene Seite aus, wobei der entsprechende Seitendeskriptor angepaßt werden muß (er zeigt dann auf die frische Kopie der Seite). Natürlich hat ein Copy-on-Write-Verfahren einen zusätzlichen Overhead durch die zu verwaltenden Seiten im Shared-Modus zur Folge. Die Erfahrungen z.B. im Mach-System [Accetta et al. 1986] zeigen jedoch, daß dieser Aufwand unter dem Strich gerechtfertigt ist.
7.7 Nachrichtenkommunikation im POSIX-Standard
Nachrichtenkommunikation in POSIX. 1 Nachrichtenkommunikation in POSIX.4
Am Beispiel des POSIX-Standards soll in diesem abschließenden Abschnitt exemplarisch vorgestellt werden, wie die Unterstützung der Systemsoftware bei der Verwendung eines nachrichtenbasierten Kommunikationsmechanismus aussehen kann. Im Hinblick auf Nachrichtenkommunikation sind zwei Teile des Standards bedeutungsvoll. In POSIX. 1 wird durch die Standardisierung der bereits angesprochenen Signale und der für die Übertragung größerer Datenmengen geeigneten Pipes die Basis für eine prozeßübergreifende Kommunikation auf einem Rechner mittels Nachrichten gelegt. Der neuere Teil POSIX.4 enthält eine geeignete Systemunterstützung für Echtzeitanwendungen. Dieser Teil des Standards bietet zusätzlich zu POSIX. 1 leistungsfähigere Alternativen im Form von erweiterten Signalen und sogenannten Message Queues an. Bezüglich der in den vorherigen Abschnitten eingeführten Klassifikationskriterien lassen sich diese vier Verfahren folgendermaßen grob einordnen: • Signale: Verbindungslose, direkte Kommunikation; Asynchron; keine Übertragung zusätzlicher Daten möglich
7.7
Nachrichtenkommunikation im POSIX-Standard
• Erweiterte Signale: Verbindungslose, direkte Kommunikation; Asynchron; Übertragung von max. 32 zusätzlichen Datenbits
• Pipes: Verbindungsorientierte, synchrone Kommunikation •
über Ports; Unidirektional; Übertragung eines Datenstroms (ohne Erhalt der Nachrichtengrenzen) Message Queues: Verbindungsorientierte, synchrone Kommunikation über Ports; Unidirektional; Übertragung einzelner und in der Größe beschränkter Nachrichten; Nachrichtenprioritäten
Im nachfolgenden werden diese vier Kommunikationsverfahren einschließlich der wesentlichen Funktionen der Programmierschnittstelle kurz vorgestellt. Eine detaillierte Beschreibung, insbesondere die Kritik an den einfachen Verfahren und die daraus abgeleiteten Forderungen an die erweiterten Kommunikationsmechanismen in POSIX.4, ist z.B. in [Gallmeister 1995] zu finden. Signale Die in POSIX.l definierten Signale entsprechen im wesentlichen dem in Abschnitt 7.4 eingeführten Signalbegriff. Damit ein Prozeß Signale eines bestimmten Typs empfangen kann, muß er einen entsprechenden Signal-Handler definieren und bei der Systemsoftware anmelden. Die Anmeldung des Handlers geschieht mittels der Funktion: Anmelden des Signal-
int sigaction (
Handlers
int signo, struct
sigaction
*new_act,
struct sigaction *old_act
)
Das erste Argument signo bestimmt, für welchen Signaltyp die Reaktion auf eintreffende Signale festgelegt werden soll. In POSIX.l sind insgesamt 19 Signaltypen definiert, von denen lediglich 2 ( S I G U S R 1 und SIGUSR2) allgemein für die signalbasierte Kommunikation zwischen zwei Prozessen eingesetzt werden können. Alle anderen Signaltypen erfüllen Verwaltungs- und Fehlerbehandlungsfunktionen und sollten daher von der Anwendung nicht verwendet werden. Alle weiteren Informationen stehen in einer vom aufrufenden Prozeß initialisierten Datenstruktur new_act. Diese Struktur enthält die drei Elemente: struct sigaction void
sigset_t
sa_mask;
int sa_flags; }
{
(*sa_handler)(int);
7
Alternative Reaktionsmöglichkeiten auf Signale
Versenden von Signalen
Nachrichtenbasierte Prozeßinteraktion
Das erste Element sa_handler ist ein Zeiger auf die beim Eintreffen eines entsprechenden Signals aufzurufende Handler-Funktion. Aus der Deklaration wird ersichtlich, daß der Handler vom Betriebssystem mit einem Argument aufgerufen wird: dem Typ des Signals, das den Aufruf der Handler-Funktion ausgelöst hat. Alternativ kann beim Anmelden auch angegeben werden, daß dieser Signaltyp ignoriert ( S I G _ I G N ) oder eine vom Standard festgelegte Default-Aktion ( S I G _ D F L ) , wie z.B. die Terminierung des Prozesses, ausgelöst werden soll. Die Angabe einer Handler-Adresse erübrigt sich in diesem Fall. Diese beiden Reaktionsvarianten werden jedoch meist bei den Signaltypen mit Verwaltungs- und Fehlerbehandlungsfunktion eingesetzt. Das zweite Argument sa_mask legt fest, welche Signaltypen - einschließlich dem Typ des auslösenden Signals selbst - bei der Ausführung des Signal-Handlers blockiert sind. Mit dem letzten Argument sa_flags kann das Verhalten auf eintreffende Signale weiter spezifiert werden; dieses Element ist im Fall der Signaltypen S I G U S R 1 und SIGUSR2 ohne Relevanz. Wird ein gültiger Zeiger old_act übergeben, trägt das System die vorherige Art der Reaktion auf einen Signaltyp einschließlich der eventuell definierten Handler-Adresse ein. Ist der aufrufende Prozeß lediglich an dieser Information interessiert, gibt er für new_act entsprechend einen Wert N U L L an. Andere Prozesse können ein Signal an einen Prozeß pid mit der Funktion kill() senden - vorausgesetzt der sendende Prozeß ist im Besitz eines entsprechenden Senderechts: int kill (pid_t pid, int signo )
Der ungünstig gewählte Funktionsname kill ist historisch begründet, da einer der ersten Anwendungsbereiche für Signale in frühen UNIXSystemen die abnormale Terminierung eines Prozesses durch das Senden eines Signals vom Typ S I G T E R M war. Erweiterte Signale Kritik an den einfachen Signalen
Der in POSIX.l definierte einfache Signalbegriff hat im wesentlichen zwei Schwachpunkte: (1) nur zwei frei nutzbare Signaltypen SIGUSR1 und SIGUSR2 und (2) fehlende Möglichkeiten zur Übertragung zusätzlicher Daten. In POSIX.4 werden daher zusätzliche Signale definiert, die zahlenmäßig in einem Intervall SIGRTMIN
Mehr nutzbare Signale
SIGRTMAX
liegen. Der Standard schreibt vor, daß mindestens acht dieser Echtzeitsignale Anwendungen zur Verfügung stehen.
7.7
Nachrichtenkommunikation im POSIX-Standard
Außerdem kann ein Prozeß mit einem Echtzeitsignal auch einen beliebigen 32-Bit-Wert versenden. Die Berücksichtigung zusätzlicher kommunizierbarer Daten hat natürlich Auswirkungen auf die Anmeldung von Signal-Handlern und das Versenden der Signale. In der sigaction-Struktur steht zu diesem Zweck ein zusätzlicher Eintrag zur Verfügung, in dem die Adresse eines erweiterten Signal-Handlers sa_sigaction gespeichert werden kann:
Anmeldung des Handlers
(*sa_handler) (int);
sigset_t
32 Bit Nutzdaten
Änderungen bei der
struct sigaction { void
Übertragung von maximal
sa_mask;
int sa_flags; void (*sa sigaction)(int, siginfo_t *, void * ) ; }
Zu erkennen ist die geänderte Signatur eines erweiterten Handlers: Neben dem ersten Argument, das analog zu einem herkömmlichen Signal-Handler den Typ des auslösenden Signals enthält, tritt ein zweites Argument vom Typ siginfo_t für die Übergabe des gesendeten 32Bit-Wertes. Das dritte Argument wird im Standard lediglich aus Kompatibilitätsgründen aufgeführt; es hat bei der Kommunikation mittels Echtzeitsignalen keine weitere Bedeutung. Durch das Setzen des Flags S A _ S I G I N F O in sa_flags wird beim Anmelden eines Handlers für Echtzeitsignale durch den Aufruf der Funktion sigaction() sichergestellt, daß es sich um einen erweiterten Handler handelt und die entsprechende Anfangsadresse nicht im Eintrag sa_handler, sondern in s a _ s i g a c t i o n zu finden ist. Da die Funktion kill() für das Versenden von Signalen nicht in der Lage ist, weitere Daten entgegenzunehmen, definiert der POSIX.4Standard außerdem eine zusätzliche Funktion sigqueue
(pid_t pid,
int signo,
u n i o n s i g v a l value)
Versenden eines Signals mit Daten
die über das dritte Argument value das Versenden eines 32 Bit-Datenwertes an den angegebenen Prozeß pid ermöglicht. Pipes Pipes wurden bereits 1973 in einer der ersten UNIX-Versionen der Bell Labs, einem Vorläufer aller modernen UNIX-basierten Systeme, unterstützt [Pate 1996]. Sie bilden einen einfachen aber mächtigen unidirektionalen Kommunikationsmechanismus. Eine Pipe kann man sich als eine »Datenröhre« vorstellen. In das eine »Röhrenende« kann ein Prozeß durch den Aufruf der Funktion write(int pipe_desc,
char *msg,
int n_char)
In das eine Pipe-Ende wird geschrieben
7
Vom zweiten Pipe-Ende kann man lesen
Benannte und unbenannte Pipes
Beispiel mit einer
Nachrichtenbasierte Prozeßinteraktion
eine aus n_char vielen Zeichen bestehende Nachricht, die im Speicher des sendenden Prozesses ab der Adresse msg abgelegt ist, schreiben. Identifiziert wird die angesprochene Pipe, genauer das eine Ende der Pipe, durch den Deskriptor pipe_des. Ein zweiter Prozeß kann am anderen Röhrenende die gesendeten Daten durch den Aufruf der Funktion read(int pipe_desc, char *data, int max_char)
auslesen. Auch dieser Prozeß muß durch den Deskriptor pipe_desc angeben, von welcher Pipe er die Daten lesen möchte. Zusätzlich gibt er einen max_char Zeichen großen Puffer data an, in dem die empfangenen Daten abgelegt werden sollen. Pipes stellen einen synchronen Kommunikationsdienst dar, d.h., der empfangende Prozeß wird bis zur Ankunft von Daten blockiert. Die Nachrichtengrenzen werden dabei nicht notwendigerweise eingehalten. Es wird zwischen unbenannten und benannten Pipes (named pipes) unterschieden. Unbenannte Pipes setzen eine Prozeßerzeugungshierarchie voraus, wie sie sich in UNIX durch die Verwendung des System-Calls fork() bei der Erzeugung neuer Prozesse automatisch ergibt. In diesem Fall wird die Pipe von einem für die kommunizierenden Prozesse gemeinsamen Vaterprozeß erzeugt und die entsprechenden Pipe-Enden als Deskriptor an die jeweiligen Kindprozesse direkt weitergegeben. In vielen Fällen ist dabei der Vaterprozeß bereits einer der beiden kommunizierenden Prozesse. Erzeugung und Verwendung einer unbenannten Pipe soll an dem nachfolgenden Code-Fragment verdeutlicht werden: int pipe_ends[2];
unbenannten Pipe pipe(pipe_ends); if (forkO
!= 0) {
// Vaterprozeß write(pipe_ends[1],data,...); } else { // Kindprozeß read(pipe_ends[0],data.,...); }
Der System-Call pipe() erzeugt eine Pipe und gibt im zweielementigen Feld pipe_ends die Deskriptoren der jeweiligen Pipe-Enden zurück. Durch den anschließenden Aufruf der Funktion fork() wird ein Kindprozeß erzeugt, der auf das lesende Pipe-Ende (pipe_ends[0])
7.7
Nachrichtenkommunikation im POSIX-Standard
zugreift, während der Vaterprozeß durch Aufrufe der Funktion write() mit dem Deskriptor pipe_ends[1] Daten in die Pipe schreibt. Die Deskriptoren der Pipe-Enden können von einem Prozeß auch bei der Überlagerung des Adreßraums mit einer neuen Ausführungsvorschrift (exec()) weiterverwendet werden, solange der exec() aufrufende Prozeß diese nicht explizit schließt. Über benannte Pipes können zwei beliebige und nicht notwendigerweise in einer Erzeugungsbeziehung zueinander stehende Prozesse eine Kommunikationsverbindung aufbauen. Die beiden Kommunikationsteilnehmer müssen sich in diesem Fall auf einen gemeinsamen Namen für die Pipe einigen. Eine benannte Pipe wird mit Hilfe der Funktion mkfifo(char *name, modelt mode)
erzeugt. Übergeben wird der Pipe-Name name und eine Schutzinformation mode, die festlegt, welche Prozesse auf diese Pipe zugreifen dürfen und welche nicht. Jeder Prozeß kann unabhängig vom Kommunikationspartner ein Ende der benannten Pipe durch den Aufruf der Funktion int open(char *name, int flags, ...)
Erzeugung einer benannten Pipe
Öffnen einer benannten Pipe
öffnen und damit zugreifbar machen. Neben dem Pipe-Namen gibt der aufrufende Prozeß dabei im Argument f l a g s an, ob er auf das lesende ( O _ R D O N L Y ) oder schreibende (O_WRONLY) Ende der Pipe zugreifen möchte. Der open()-System-Call gibt im Erfolgsfall den Deskriptor des entsprechenden Pipe-Endes zurück. Der anschließende Zugriff findet über die bereits angesprochenen Funktionen read() und write() statt. Message Queues Message Queues sind eine weitere, komfortable Form der nachrichtenbasierten Prozeßkommunikation. Ihre Standardisierung fand vor dem Hintergrund der eingeschränkten Möglichkeiten bei der Verwendung von Pipes statt. Die wesentlichen Problemkreise bei einer Pipebasierten Kommunikation sind: 1. Die FIFO-Eigenschaft der Datenübertragung, 2. der Verlust der Nachrichtengrenzen und 3. das Fehlen jeglicher Abfragemöglichkeiten über den Pipe-Zustand. Dabei ist zu bemerken, daß die FIFO-Eigenschaft der Pipes, d.h., daß Daten in der Sendereihenfolge an den Empfänger übergeben werden, aus Sicht der Ablaufkonsistenz in der Mehrzahl der Fälle wünschens-
Kritik an Pipes
7 Nachrichtenbasierte Prozeßinteraktion
Out-of-band-Nachrichten
Erzeugen und/oder Öffnen
wert ist. Sie unterbindet jedoch die schnelle und bevorzugte Weiterleitung kritischer Daten mittels sogenannter Out-of-band-Nachrichten. Alle Kritikpunkte werden durch Message Queues gelöst. Sie stellen einen verbindungsorientierten, unidirektionalen Kommunikationsdienst dar, der unter Einhaltung der Nachrichtengrenzen Daten in FIFO-Ordnung von einem Sender an einen Empfänger übermittelt. Durch eine geeignete Priorisierung der Nachrichten kann die FIFOOrdnung jedoch bei Bedarf unterbrochen werden, um damit auch eine Verarbeitung von Out-of-band-Nachrichten zu gewährleisten. Eine Message Queue wird durch den Aufruf der Funktion mqd_t mq__open (
einer Message Queue
char *mq_name, int oflag, mode_t create_mode, struct mq_attr *attr )
erzeugt und/oder geöffnet. Das Argument mq_name gibt den Namen der gewünschten Message Queue an. Mit dem zweiten Argument oflag kann festgelegt werden, ob eine vorhandene Queue geöffnet oder eine noch nicht vorhandene Queue erzeugt ( O _ C R E A T ) und geöffnet werden soll. Das dritte Argument create_mode spielt lediglich bei der Erzeugung einer neuen Queue eine Rolle. Es legt in diesem Fall fest, welche Prozesse auf die Queue zugreifen dürfen. Über oflag wird außerdem bestimmt, ob ein Prozeß Nachrichten nur senden (O_WRONLY) oder nur empfangen (O_RDONLY) möchte. Durch Angabe von ORDWR kann ein Prozeß Nachrichten senden und empfangen. Das letzte Argument attr zeigt auf eine vom erzeugenden Prozeß initialisierte Datenstruktur Maximale
struct mq_attr { long mq_maxmsg;
Nachrichtenanzahl
long mq_msgsize;
und -große }
die im wesentlichen die maximale Anzahl zwischengespeicherter, d.h. gesendeter aber noch nicht empfangener Nachrichten (mq_maxmsg) und die maximale Größe einer einzelnen Nachricht (mq_msgsize) festlegt. Auf der Grundlage diese beiden Werte reserviert das Betriebssystem Puffer entsprechender Größe. Die Funktion liefert im Erfolgsfall einen Deskriptor vom Typ mqd_t zurück, über den in nachfolgenden Funktionsaufrufen auf die entsprechende Queue zugegriffen werden kann.
7.7 Nachrichtenkommunikation im POSIX-Standard
Die beiden zentralen Zugriffsfunktionen dienen dem Senden und Empfangen von Nachrichten: mq_send ( mqd_t mq,
Senden und Empfangen von Nachrichten
char *message, size_t message_length, unsigned int priority ) mq_receive ( mqd_t mq, char *buffer, size_t buffer_size, unsigned int *priority )
In beiden Fällen muß der Deskriptor mq der Queue angegeben werden, auf die zugegriffen werden soll. Zusätzlich zur Nachricht selbst (message, message_length) kann bei mq_send() eine Nachrichtenpriorität p r i o r i t y angegeben werden. Der jeweilige Wert bestimmt die Einordnung in die Nachrichtenschlange. Der Aufruf von mq_send() blockiert, wenn keine weitere Nachricht in der Schlange gespeichert werden kann. Nachrichten werden mittels mq_receive() empfangen. Diese Funktion blockiert den aufrufenden Prozeß bis zum Eintreffen einer Nachricht. Ist bereits eine Nachricht in der Queue enthalten, wird diese unmittelbar an den aufrufenden Prozeß übergeben. Bei mehreren zwischengespeicherten Nachrichten wird die Nachricht mit der höchsten Priorität an den Empfänger übergeben. Existieren mehrere Nachrichten der höchsten Priorität, wird gemäß FIFO-Ordnung verfahren. Im Argument priority wird zusätzlich die Priorität der gerade empfangenen Nachricht zurückgegeben. Der Rückgabewert der Funktion selbst entspricht der Größe der empfangenen Nachricht in Bytes. Auf eine Message Queue kann eine beliebige Anzahl an Prozessen sendend und empfangend zugreifen. In welcher Reihenfolge mehrere Sendeaufträge verschiedener Prozesse in die Queue eingetragen werden ist zufällig und hängt lediglich von deren Scheduling-Reihenfolge ab. Mit Hilfe der Funktionen mq_getattr() und mq_setattr() können die Eigenschaften der Nachrichtenschlange, d.h. maximale Nachrichtengröße und maximale Anzahl zwischengespeicherter Nachrichten, abgefragt und ggf. geändert werden. Zusätzlich liefert mq_getattr() die Anzahl zwischengespeicherter Nachrichten zum Zeitpunkt des Aufrufs.
Nachrichtenprioritäten
Zugriff und Änderung der Queue-Attribute
7
Nachrichtenbasierte Prozeßinteraktion
Mit Hilfe des Signal-Mechanismus kann auch bei Message Queues ein Nachrichtenempfang ohne Blockade realisiert werden. Zu diesem Zweck muß der empfangswillige Prozeß durch den Aufruf der Funktion mq_notify(mqd_t mq,
Signal bei Nachrichtenempfang
struct sigevent *sig_notify)
einen Signaltyp angeben, der bei Ankunft einer Nachricht ausgelöst werden soll und damit dem Prozeß einen blockade-freien Empfang der Nachricht durch den anschließenden Aufruf der Funktion mq_receive() ermöglicht. Das Signal wird nur ausgelöst, wenn keine anderen Prozesse auf den Empfang einer Nachricht an derselben Message Queue mq warten. Dabei ist jedoch zu beachten, daß es zwischen dem Empfang des Signals und dem Entgegennehmen der eigentlichen Nachricht in einem Prozeß p1 zu einer sogenannten Race-Condition kommen kann, wenn ein anderer Prozeß p2 vor dem Aufruf von mq_receive() durch pl selbst mq_receive() aufruft und die neu eingetroffene Nachricht damit entgegennimmt. Prozeß p1 wird dadurch im mq_receive() blockiert, obwohl die Ankunft des Signals das Vorhandensein einer Nachricht suggeriert.
8
Synchronisationsfehler
Synchronisationsfehler können in interagierenden Prozeßsystemen als Folge einer ausbleibenden oder inkorrekt verwendeten Synchronisation auftreten. Sie resultieren in einer Verletzung der Ablaufkonsistenz oder in einer sogenannten Verklemmung. Mit Verklemmung (Deadlock) bezeichnet man einen Zustand, in dem die beteiligten Prozesse wechselseitig auf den Eintritt von Bedingungen warten, die nur durch andere Prozesse in dieser Gruppe selbst hergestellt werden können. Als Folge dieser wechselseitigen Abhängigkeit bleiben die Prozesse für immer blockiert. Aus einer Verklemmung können Prozesse nur durch einen äußeren Eingriff befreit werden. Man spricht von einer totalen Verklemmung, wenn alle Prozesse eines betrachteten Systems davon betroffen sind. Die geschilderte Situation kann auch formal durch eine zirkuläre Wartebedingung beschrieben werden:
Sie besagt, daß jeder Prozeß P; aus der Menge der verklemmten Prozesse auf eine Bedingung B; wartet, die nur durch den Nachfolger in der Kette hergestellt werden kann. Da die Kette jedoch zu einem Zyklus geschlossen ist, kann letztendlich keine der Bedingungen hergestellt werden. Charakteristisch für Synchronisationsfehler ist ihre Zeitabhängigkeit: Sie treten nur bei einer bestimmten Aufeinanderfolge der Synchronisationsoperationen in Erscheinung und sind deshalb nicht im üblichen Sinn reproduzierbar. Aus diesem Grund können nicht alle Synchronisationsfehler durch systematisches Testen gefunden werden, und ihre Entdeckung bleibt oft dem Zufall überlassen. Trotzdem fand die Problematik zeitabhängiger Synchronisationsfehler bisher kein großes Echo im Bereich der Systemsoftware, da aufgrund der starken Verbreitung des klassisch sequentiellen Programmiermodells, in dem Fehler dieser Form nicht auftreten, dafür noch kein Bedarf bestand. Durch die wachsende Verbreitung leistungsfähiger nebenläufiger Programmiermodelle wird sich diese Situation ändern und zukünftige
Verklemmung (Deadlock)
Zirkuläre Wartebedingung
Synchronisationsfehler sind zeitabhängig und damit nichtdeterministisch
8
Synchronisationsfehler
Systemsoftware wird auch Unterstützungstechniken zum Aufdecken und Beheben zeitabhängiger Fehler enthalten. Aufgrund der fehlenden Reproduzierbarkeit bleibt jedoch die Vermeidung von Synchronisationsfehlern ein vorrangiges Ziel bei der Entwicklung nebenläufiger Anwendungen. Dazu ist es notwendig, ihre Ursachen genau zu studieren. In Abschnitt 8.1 werden zunächst die unterschiedlichen Erscheinungsformen zeitabhängiger Fehler anhand typischer Beispiele demonstriert. Anschließend werden in Abschnitt 8.2 zwei formale Modelle zur Beschreibung und Präzisierung von Verklemmungen als häufigste Folge zeitabhängiger Fehler eingeführt. In Abschnitt 8.3 werden dann die klassischen Algorithmen zur Erkennung und Vermeidung von Verklemmungen bei der Betriebsmittelvergabe behandelt. Abschließend wird in Abschnitt 8.4 kurz skizziert, wie die aktuelle Systemsoftware mit Verklemmungen umgeht.
8.1
Beispiele zeitabhängiger Fehler
Beispiel 1: Fehlerhafte kritische Abschnitte Drei Prozesse A, B und C bearbeiten zyklisch einen gemeinsamen Datenbereich durch Aufruf der kritischen Programmabschnitte PA, PB und P C :
Risiko inkonsistenter Datenzustände
Verklemmungsgefahr
Prozeß A sperrt den kritischen Abschnitt PA korrekt. Dagegen fehlt die V-Operation bei Prozeß B und die P-Operation bei Prozeß c. c tritt demnach unkoordiniert in seinen kritischen Abschnitt ein, während B den Zugriff zu den gemeinsamen Daten am Ende seines kritischen Abschnitts nicht freigibt. Prozeß B könnte in Abwesenheit von Prozeß C eine Verklemmung herbeiführen, da er nach einmaligem Durchlaufen des kritischen Abschnitts keinem weiteren Prozeß der Form A oder B erneuten Zugang zu den gemeinsamen Daten gewährt. Im Beisein von Prozeß C kann eine Verklemmung jedoch »erfolgreich« verhindert werden, indem dieser durch den Aufruf der V-Operation den kriti-
8.1
Beispiele zeitabhängiger Fehler
sehen Abschnitt stellvertretend für Prozeß B freigibt. In diesem Fall werden jedoch mit hoher Wahrscheinlichkeit Dateninkonsistenzen entstehen, da Prozeß C gleichzeitig mit A oder B auf die gemeinsamen Daten zugreifen kann. Beispiel 2: Gegenläufige Schachtelung kritischer Abschnitte Zwei Prozesse A und B treten mittels der Semaphore s1 und s2 in zwei geschachtelte kritische Abschnitte ein. Prozeß A beginnt seinen äußeren kritischen Abschnitt mittels s1, während B diesen mittels s2 betritt: Semaphor sl(l), s2(l); Prozeß A:
Prozeß B:
P(sl); // äußerer Abschnitt P(s2) ; // innerer Abschnitt V(s2) ; V(Sl) ;
P(s2); // äußerer Abschnitt P(sl) ; // innerer Abschnitt V(sl) ; V(s2) ;
Aus der lokalen Sicht jedes Prozesses wurden die kritischen Abschnitte korrekt realisiert. Wenn sich jedoch beide Prozesse gleichzeitig im äußeren kritischen Abschnitt aufhalten, d.h., wenn Prozeß A im Besitz von Semaphor s1 und B im Besitz von Semaphor s2 ist, entsteht zwangsläufig eine Verklemmung. Prozeß A verlangt nämlich durch den Aufruf P(s2) Eintritt in seinen inneren kritischen Abschnitt, bevor er Semaphor s1 freigibt. Prozeß B verlangt hingegen über P(s1) Eintritt in seinen inneren kritischen Abschnitt, bevor er selbst Semaphor s2 freigibt. Geschachtelte kritische Abschnitte treten im Zusammenhang mit allgemeinen Zuteilungsstrategien für Betriebsmittel häufig auf. Dabei kann ein einzelner kritischer Abschnitt als Belegen (P-Operation), Benutzen (kritischer Abschnitt) und Freigeben (V-Operation) eines Betriebsmittels aufgefaßt werden (siehe auch Kapitel 6). Bei geschachtelten kritischen Abschnitten fordert also ein Prozeß sukzessive mehrere Betriebsmittel an, bevor er sie wieder freigibt.
Verklemmungsgefahr
8
Synchronisationsfehler
Beispiel 3: Akkumulierende Belegung In diesem Beispiel belegen zwei Prozesse A und B mehrere Exemplare von einem bestimmten Betriebsmittel (z.B. mehrere Kacheln des Arbeitsspeichers). Die Anforderungen finden zeitlich versetzt statt. Vorausgesetzt wird die Existenz zweier Funktionen: Deskriptor Belegen (); Freigeben ( Deskriptor bm );
Beide Funktionen können - wie gezeigt - sowohl mit Semaphoren als auch mit Monitoren realisiert werden. Die Funktion Belegen () blokkiert den aufrufenden Thread, wenn keine weiteren Exemplare des Betriebsmittels mehr verfügbar sind. Der Rückgabewert dieser Funktion ist ein Deskriptor für das angegebene Betriebsmittel, der in nachfolgenden Funktionsaufrufen verwendet werden kann. Wir nehmen in diesem Beispiel einen Gesamtvorrat von 5 Exemplaren des Betriebsmittels an, die zu Beginn alle frei sind: Prozeß A:
Prozeß B:
BM1 = Belegen() BM1 = Belegen() BM2 = Belegen() BM2 = Belegen() BM3 = Belegen() BM3 = Belegen() BM4 = Belegen() BM4 = Belegen() Freigeben(BM1); Freigeben(BM2); Freigeben(BM3); Freigeben(BM4);
Verklemmungsgefahr
Freigeben(BM1) Freigeben(BM2) Freigeben(BM3) Freigeben(BM4)
Werden die Exemplare des Betriebsmittels zeitlich so angefordert, daß Prozeß A 3 Exemplare besitzt (BM1 bis BM3) und Prozeß B die ersten 2 Betriebsmittel belegt hat, dann ist das System verklemmt. Keiner der beiden Prozesse kann ohne äußere Intervention zu Ende geführt werden, da beide auf die Freigabe weiterer Betriebsmittelexemplare durch den jeweils anderen Prozeß warten. Man beachte, daß der Gesamtvorrat an Betriebsmittelexemplaren groß genug ist, um bei geeigneter Koordinierung die Anforderungen der beiden Prozesse jeweils einzeln zu befriedigen.
8.1
Beispiele zeitabhängiger Fehler
Beispiel 4: Dining-Philosopher-Problem Das Problem der essenden Philosophen ist ein anschauliches Beispiel für Verklemmungen in einem Prozeßsystem und für die Untersuchung verschiedener algorithmischer Ansätze, diesen Verklemmungen entgegenzutreten. In diesem Beispiel lebt eine beliebige Anzahl an Philosophen in einem Raum. Jeder Philosoph wechselt zwischen den beiden Zuständen Denken und Essen. In jedem dieser Zustände verbleibt er für eine unbestimmte aber endliche Zeit. Im Raum steht ein Tisch mit einer festen Anzahl von n Sitzplätzen und den zugehörigen Tellern, einer niemals zur Neige gehenden Reisschüssel und n Stäbchen (siehe Abbildung 8-1). Jeweils ein Stäbchen befindet sich links und rechts eines Sitzplatzes. Wenn ein Philosoph Hunger verspürt, sucht er sich an dem Tisch einen freien Sitzplatz. Ist ein solcher gefunden, muß der Philosoph anschließend in den Besitz des linken und rechten Stäbchens gelangen. Nur mit beiden Stäbchen kann gegessen werden.
Ein Philosoph braucht zwei Stäbchen zum Essen
Abb. 8-1 Tisch der Philosophen mit n Sitzplätzen (Tellern) und n Stäbchen
Gesucht wird eine algorithmische Lösung, in der keiner der Philosophen verhungert. Allgemein werden die Philosophen durch Prozesse modelliert. Semaphore eignen sich z.B. zur Beschreibung der Stäbchen: Ist ein Prozeß im Besitz eines bestimmten Semaphors, so entspricht das einem Philosophen mit dem entsprechenden Stäbchen in der Hand. In einer Ad-hoc-Lösung belegt jeder Philosoph Pk auf dem Sitzplatz k zuerst das linke (Position k) und dann das rechte Stäbchen (Position (k+1) mod n): Semaphor Stäbchen[n]; Essen (Platz k) { P(Stäbchen[k]); P(Stäbchen[(k+1) mod n ] ) ; // Guten Appetit V(Stäbchen[(k+1) mod n ] ) ; V(Stäbchen[k]); }
8
Verklemmungsgefahr Zum Beispiel nehmen »gerade« Philosophen zuerst das linke und »ungerade« zuerst das rechte Stäbchen
Synchronisationsfehler
Leider birgt diese Lösung die Gefahr einer Verklemmung, obwohl der geschachtelte kritische Abschnitt (der sicherstellt, daß maximal ein Philosoph mit zwei Stäbchen an einem Teller sitzt) nicht - wie in Beispiel 2 - gegenläufig belegt wird. Zu einer Verklemmung kommt es, wenn n Philosophen gleichzeitig in den Besitz des jeweils linken Stäbchens gelangen. Es gibt viele, zum Teil sehr interessante Lösungen, eine solche Verklemmung jederzeit auszuschließen. Beispiel 5: Zyklisches Erzeuger-Verbraucher-System In einem letzten Beispiel kommunizieren n Prozesse über Puffer gemäß Abbildung 5-2. Jeder Prozeß Pk wartet an einem Semaphor Vollk auf einen gefüllten Puffer von seinem Vorgänger. Bei gefülltem Puffer erfolgt die Bearbeitung der Daten aus dem Puffer des Vorgängers und die Ablage der Ergebnisse im Puffer des Nachfolgers. Dem nachfolgenden Prozeß wird schließlich über das Semaphor Voll k+1 der Pufferzustand »Voll« und über das Semaphor Leerk dem Vorgänger der Pufferzustand »Leer« gemeldet. Zu Beginn seien alle Puffer leer: Semaphor Voll 1 (0), ..., Voll n (0); Semaphor Leer 1 (1), ..., Leer n (1); Prozeß Pk: while (1) { P(Vollk) ; // Entleere Puffer k; V(Leer k ); P(Leer k+1 ); // Fülle Puffer k; V(Voll k + 1 ); }
Verklemmungsgefahr
Man erkennt sofort, daß alle Prozesse in der ersten P-Operation blokkieren, da offenbar kein Prozeß in dem Zyklus einen gefüllten Puffer vorfindet.
Abb. 8-2 Zyklisches ErzeugerVerbraucher-System
8.2
Formale Modelle
Verklemmungen stellen praktisch die einzige Form zeitabhängiger Fehler dar, die allgemein beschreibbar und formalisierbar ist. Dies liegt in der Stabilität einer Verklemmung begründet, die - einmal eingetreten - ohne äußere Einwirkungen nicht auflösbar ist. Dagegen entziehen sich Inkonsistenzen im Datenzustand eines Prozeßsystems auf-
8.2
Formale Modelle
grund fehlender oder fehlerhafter Synchronisationsmaßnahmen, die nicht in einer Verklemmung resultieren, einer allgemeinen Formalisierung, da sie zu einem beliebigen Fehlverhalten der beteiligten Prozesse führen. Aus diesem Grund beschränken sich die nachfolgenden Abschnitte auf eine Formalisierung des Begriffs Verklemmung. Vorgestellt wird ein allgemeiner Ansatz nach Holt und eine auf die Vergabe von Betriebsmitteln konzentrierte graphische Modellierung in der Form von Betriebsmittelbelegungsgraphen (Resource-AllocationGraph).
Formalisierung der Verklemmungsproblematik
Formales Modell nach Holt Holt [Holt 1972] hat auf der Grundlage eines einfachen Graphenmodells zur Beschreibung interagierender Prozeßsysteme den Zustand »Verklemmt« sowie weitere damit zusammenhängende Prozeßzustände präzise definiert. Er faßt zu diesem Zweck ein Prozeßsystem als Tupel auf. Dabei bezeichnet
Ein Prozeßsystem besteht aus einer Prozeß- und einer Zustandsmenge
eine Menge von Zuständen und
eine Menge von Prozessen. Jeder Prozeß P; ist durch Zustandsübergänge innerhalb der Menge definiert. Abb. 8-3 Beispiel für ein einfaches Prozeßsystem
In Abbildung 8-3 ist beispielsweise ein Prozeßsystem mit den Zuständen {S,T,U,V} und den Prozessen (Pi,P2) dargestellt. Kreise bezeichnen Zustände und gerichtete Kanten Operationen, die Zustandsübergänge herbeiführen. Die den gerichteten Kanten beigefügten Namen kennzeichnen den jeweiligen Prozeß, der für den Zustandsübergang verantwortlich ist.
Graphische Darstellung eines Prozeßsystems
8
Synchronisationsfehler
In diesem Modell beschreibt die Formulierung
Operation
einen Prozeß Pi, der den Zustand S in einen Folgezustand T überführt. Die Zustandsänderung von S nach T wird auch als Operation des Prozesses Pi bezeichnet. Operationen in dem Beispiel aus Abbildung 8-3 sind die folgenden:
Allgemein ist eine Folge von Funktionsübergängen der Form
äquivalent zu der Schreibweise:
Dieses Modell kann nun dazu benutzt werden, die Zustände blockiert, verklemmt und sicher von Prozessen zu definieren: Blockiert
Verklemmt
Sicher
•
Ein Prozeß P; ist im Zustand S blockiert, wenn kein Zustand T mit
•
existiert. Ein Prozeß Pi ist im Zustand S verklemmt, wenn er für alle Übergänge
•
im Zustand T blockiert ist. Ein Zustand S ist sicher, wenn für alle T mit
T in keinem Fall ein Verklemmungszustand ist.
Verklemmungszustände
Insgesamt läßt sich daraus ableiten, daß ein Prozeß immer dann verklemmt ist, wenn er aus einem blockierten Zustand nicht wieder befreit werden kann. In Abbildung 8-3 ist z.B. Prozeß P2 im Zustand S blockiert (aber nicht verklemmt) und in den Zuständen U und V verklemmt. Wenn S ein Zustand ist, in dem ein oder mehrere Prozesse verklemmt sind, bezeichnet man S auch als Verklemmungszustand. S ist
8.2
Formale Modelle
ein totaler Verklemmungszustand, wenn alle Prozesse in S verklemmt sind. In Abbildung 8-3 sind die Zustände U und V Verklemmungszustände; totale Verklemmungszustände existieren in diesem Beispiel dagegen nicht. Aus den oben angegebenen Definitionen lassen sich unmittelbar zwei Schlußfolgerungen ableiten: Angenommen es existiert ein Übergang
Wenn S ein Verklemmungszustand ist, so ist auch T ein Verklemmungszustand. Analog gilt für einen sicheren Zustand S, daß auch T ein sicherer Zustand ist. Verklemmung und Sicherheit in dem hier betrachteten Kontext sind also permanente Eigenschaften eines Prozeßsystems. Am Beispiel der gegenläufigen Schachtelung kritischer Abschnitte durch zwei Prozesse A und B (Beispiel 2) soll die Verwendung des Holtschen Zustandsmodells verdeutlicht werden (siehe Abbildung 8—4). Ein Zustandsübergang kennzeichnet in diesem Beispiel das erfolgreiche Durchlaufen einer P-Operation und resultiert in dem Besitz des Semaphors durch den entsprechenden Prozeß. Man erkennt, daß im Zustand S11 eine totale Verklemmung existiert.
Beispiel
Abb. 8-4 Holtsches Zustandsdiagramm für eine gegenläufige Schachtelung kritischer Abschnitte
An dem Beispiel wird ersichtlich, daß der Zustandsgraph bereits bei relativ einfachen Systemen recht komplex werden kann. Bei einer größeren Zahl beteiligter Prozesse sind Diagramme dieser Art aufgrund der kombinatorischen Anzahl an Systemzuständen nicht mehr beherrschbar. Der praktischen Anwendbarkeit dieses Verfahrens zur
Zustandsgraphen sind schnell komplex
8
Synchronisationsfehler
Analyse des dynamischen Verhaltens von Prozeßsystemen sind dadurch sehr enge Grenzen gesetzt. Resource-Allocation-Graph Verklemmungen bei der Zuteilung von Betriebsmitteln
Verklemmungen bei der Betriebsmittelzuteilung lassen sich anschaulich z.B. durch Betriebsmittelbelegungsgraphen darstellen [Peterson und Silberschatz 1985]. In dieser speziellen Graphenform werden drei Mengen unterschieden:
• • •
Anforderungspaar
Benutztpaar
Einzelne Exemplare eines Betriebsmittels werden durch Token dargestellt
Prozesse: P := {pl, p2, ..., pn} Betriebsmitteltypen: R := {rl, r2, ..., rm) Anforderungs- und Benutztrelation:
Ein Anforderungspaar (p,r) wird graphisch dargestellt als
mit der Bedeutung »Prozeß p fordert ein Exemplar des Betriebsmitteltyps r an«. Ein Benutztpaar (r,p) wird graphisch dargestellt als
mit der Bedeutung »Prozeß p ist im Besitz eines Exemplars des Betriebsmitteltyps r«. Die Anzahl der freien und belegten Exemplare jedes Betriebsmitteltyps wird durch schwarze Markierungen (Token) dargestellt. Ein Beispiel aus [Peterson und Silberschatz 1985] soll die Verwendung eines Resource-Allocation-Graphen verdeutlichen. Gegeben sind die drei Mengen P, R und E:
• • •
P={Pl> P 2 . P3) R={r1, r 2 , r 3 , r4} E={(p1,r1), (P 2 ,r 3 ), (r 1 ,p 2 ), (r 2 ,P 2 ), (r 2 ,P l ). (r3,P3)}
8.2 Formale Modelle
Abb. 8-5 Resource-Allocation-Graph ohne Zyklen
Der zugehörige Resource-Allocation-Graph ist in Abbildung 8-5 dargestellt. Er stellt eine Momentaufnahme dar, in der
• das Betriebsmittel r1 im Besitz von p2 ist und von p1 angefor-
• • •
dert wird, je ein Exemplar des Betriebsmittels vom Typ r2 im Besitz von pl und p 2 ist, das Betriebsmittel r3 im Besitz von p3 ist und von p2 angefordert wird, alle Betriebsmittel des Typs r4 frei sind.
Eine notwendige Bedingung für die Entstehung einer Verklemmung ist die Ausbildung eines Zyklus (geschlossener, gerichteter Kantenzug). Existiert für alle Betriebsmittel im Zyklus lediglich ein Exemplar, so kann mit Sicherheit auf eine Verklemmung geschlossen werden.
Zyklus ist notwendige Bedingung für Verklemmung
Abb. 8-6 Resource-Allocation-Graph mit Zyklen
Führt man z.B. in dem in Abbildung 8-5 dargestellten Graphen eine zusätzliche Kante (p 3 ,r 2 ) ein, so erhält man einen Graphen mit zwei Zyklen (siehe Abbildung 8-6):
8
Zyklus ist keine hinreichende Bedingung für Verklemmung
Synchronisationsfehler
Obwohl die notwendigen und hinreichenden Bedingungen für die Ausbildung einer Verklemmung nicht erfüllt sind (Betriebsmitteltyp r2 enthält mehr als ein Exemplar), liegt in beiden Fällen eine Verklemmung vor. Der offensichtliche Grund ist, daß die Prozesse p1 und p 2 , die jeweils ein Exemplar von r2 besitzen, beide Teil eines Zyklus sind und damit nicht aus eigenem Antrieb Betriebsmittel freigegeben können. In dem Graphen in Abbildung 8-7 ist eine Situation dargestellt, in der zwar ein Zyklus existiert, die Betriebsmittel aber auch von Prozessen außerhalb des Zyklus belegt sind (p 2 und p 4 ). In diesem Fall existiert keine Verklemmung, da der Zyklus durch Freigabe eines Exemplars des Typs r1 durch p2 oder des Typs r2 durch p4 aufgelöst wird.
Abb. 8-7 Beispiel eines ResourceAllocation-Graphs mit Zyklen
Grundsätzlich gilt, daß eine genauere Analyse der Situation erforderlich ist, wenn bei einem vorhandenen Zyklus Betriebsmitteltypen mit mehr als einem Exemplar vorkommen. Eine Verklemmung liegt nur dann vor, wenn die am betrachteten Zyklus unbeteiligten Prozesse (die Exemplare von im Zyklus liegenden Betriebsmitteltypen besitzen) selbst in einem anderen Zyklus verklemmt sind.
8.3
Erkennungs- und Vermeidungsalgorithmen
Die zirkuläre Wartebedingung als allgemeines Kriterium für die Ausbildung von Verklemmungen wurde von Coffmann [Coffmann et al. 1971] für das Gebiet der Betriebsmittelzuteilung präzisiert. Danach müssen die folgenden Bedingungen erfüllt sein, damit sich eine Verklemmung einstellt: Vier gleichzeitig geltende Bedingungen für eine Verklemmung
1. Die belegten beziehungsweise angeforderten Betriebsmittel können nur exklusiv von jeweils einem Prozeß benutzt werden. 2. Prozesse sind bereits im Besitz von Betriebsmitteln, während sie weitere anfordern.
8.3
Erkennungs- und Vermeidungsalgorithmen
3. Belegte Betriebsmittel können nicht zwangsweise entzogen werden. 4. Es existiert eine zirkuläre Kette von Prozessen, in der jeder Prozeß ein oder mehrere Betriebsmittel besitzt, die vom nächsten Prozeß im Zyklus angefordert werden. Sobald auch nur eine dieser vier Existenzbedingungen außer Kraft gesetzt wird, können Verklemmungen zuverlässig vermieden werden. Aufbauend auf dieser Erkenntnis sind die folgenden Betriebsmittelzuteilungsstrategien vorgeschlagen worden: •
Zuteilung der Betriebsmittel in einer vorher festgelegten linearen Reihenfolge. • Zuteilung aller benötigten Betriebsmittel zu einem Anforderungszeitpunkt. • Zwangsweiser Entzug (bzw. freiwillige Rückgabe) aller bereits belegten Betriebsmittel, sofern eine Anforderung nicht erfüllt werden kann. • Zuteilung von Betriebsmitteln nur dann, wenn der resultierende Zustand sicher ist.
Möglichkeiten, eine Verklemmungsbedingung aufzuheben
Die letzte Methode setzt voraus, daß eine Verklemmung eindeutig entdeckt werden kann. Dazu wurde ebenfalls von Coffmann ein Algorithmus angegeben, der nachfolgend beschrieben wird. Algorithmus zum Erkennen einer Verklemmung B={bm1, bm 2 , ..., bmk} sei die Menge der verschiedenen Betriebsmittel. Jeder Wert bmi gibt die Anzahl der insgesamt verfügbaren Exemplare des Betriebsmitteltyps ri an. Es existieren insgesamt n Prozesse. Zu einem beliebigen Zeitpunkt t bezeichne
• •
pi,j die Anzahl von Betriebsmitteln des Typs rj, die dem Prozeß Ti zugeteilt sind und qi,j die Anzahl von Betriebsmitteln des Typs rj, die vom Prozeß Ti zusätzlich angefordert werden.
Die Werte pi,j können zu einer Zuteilungsmatrix P=(pi,j) zusammengefaßt werden, in der jeder Zeilenvektor Pi alle Betriebsmittel enthält, die dem Prozeß Ti gegenwärtig zugeteilt sind. Analog dazu bilden die Werte qi,j eine Anforderungsmatrix Q=(qi,j), in der jeder Zeilenvektor Qi alle Betriebsmittel enthält, die der Prozeß Ti zusätzlich anfordert. Durch einen Vektor V=(vl, v 2 ,..., vk) werden außerdem die für jeden Betriebsmitteltyp momentan noch verfügbaren Exemplare definiert, wobei vj kleiner oder gleich bmj sein muß.
Zuteilungsmatrix
Anforderungsmatrix
8 Synchronisationsfehler
Offenbar gilt:
Der skizzierte Algorithmus weist eine Verklemmung nach, indem er versucht, eine Anordnung für die n Prozesse zu finden, in der jeder Prozeß mit den restlichen, noch freien Betriebsmitteln und den freigegebenen Betriebsmitteln seines Vorgängers seinen persönlichen Bedarf befriedigen und deshalb zu Ende geführt werden kann: Boolean Erkennen (P,Q,V) { W := V; Stop := False; for i:=l to n { if (P[i] = (0,0, ...,0)) Markiere P [i] ; } do { if (Unmarkierte Zeile u mit Q[u]<= W { /* Anforderungen von Prozeß u erfüllbar */ Markiere P[u]; W := W+P[u]; } else Stop := True; } until Stop; if (Unmarkierte Zeilen in P) return True else return False; }
Unmarkierte Zeilen geben ein oder mehrere Verklemmungszyklen wieder
Der Test auf Erfüllbarkeit durch den Vergleich der Zeilenvektoren Q[u]<W entspricht einem paarweisen Vergleichen der Vektorelemente. Eine Verklemmung existiert genau dann, wenn nach Abbruch des Algorithmus unmarkierte Zeilen von P übrig bleiben (Rückgabewert ist in diesem Fall True). Die unmarkierten Zeilen korrespondieren exakt zu den Prozessen, die in ein oder mehreren zirkulären Wartebedingung verklemmt sind. Die Funktionsweise des Algorithmus soll an einem einfachen Beispiel verdeutlicht werden. Es existieren 5 Prozesse T1 bis T5 und 3 Betriebsmitteltypen mit einem Vorrat von bm 1 =12, bm 2 =8 und bm 3 =40. Die aktuelle Belegungsmatrix P und die Anforderungsmatrix Q haben folgende Inhalte:
8.3
Erkennungs- und Vermeidungsalgorithmen
Beispiel für ein verklemmtes Prozeßsystem
Daraus ergibt sich für den Verfügbarkeitsvektor V = (2 1 9), da z.B. bm 1 =12-2-0-l-5-2=2. Im nachfolgenden Ablaufprotokoll werden die insgesamt drei Schleifendurchläufe mit den Inhalten der wichtigsten Datenstrukturen dargestellt. Sterne hinter einzelnen Zeilenvektoren von P kennzeichnen bereits markierte Prozesse:
Nach der dritten Iteration muß der Algorithmus abbrechen, da er die Anforderungen der Prozesse T1 und T4 nicht erfüllen kann. Beispielsweise fordert T1 noch die Betriebsmittel Qi=(7 3 10) an, vom ersten Betriebsmittel sind jedoch wegen W=(5 7 24) nur 5 verfügbar.
8
Synchronisationsfehler
Algorithmus zum Vermeiden von Verklemmungen
Vermeidungsalgorithmus auf Entdeckungsalgorithmus abbilden
Betriebsmittelzuteilung nur gewähren, wenn dadurch keine Verklemmung entsteht
Der Algorithmus zur Entdeckung von Verklemmungen kann in einem Algorithmus zur Vermeidung von Verklemmungen eingesetzt werden, um nur solche Betriebsmittelzuteilungen zuzulassen, die einen sicheren und damit verklemmungsfreien Systemzustand nach sich ziehen. Der nachfolgend skizzierte Algorithmus geht auf Haberman [Haberman 1969] zurück. Er setzt die Kenntnis der Maximalanforderungen an Betriebsmitteln für jeden Prozeß voraus, die in einer Matrix A festgehalten werden. Die Grundidee des Algorithmus basiert darauf, eine aktuelle Betriebsmittelanforderung auf der Basis von P, Q und V scheinbar durchzuführen und in dem fiktiven Folgezustand P', Q', V' festzuhalten. Auf P', Q', V' wird nun der bekannte Algorithmus zur Erkennung von Verklemmungen angewendet, wobei vom schlimmsten Fall ausgegangen wird, d.h., daß alle Prozesse den Rest an Betriebsmitteln bis zu ihrem Maximalbedarf anfordern. Stellt der Algorithmus eine Verklemmung fest, dann wird die Betriebsmittelzuteilung unterlassen und der Prozeß blockiert. Diese Behandlung der Prozesse stellt sicher, daß es immer wenigstens einen Prozeß geben muß, dessen Maximalanforderungen erfüllbar sind. Damit existiert eine Zuteilungsreihenfolge unter den Prozessen, die eine Ausführbarkeit jedes Prozesses mit den noch nicht zugeteilten Betriebsmitteln und den belegten Betriebsmitteln seines Vorgängers garantiert. Im einzelnen hat der Vermeidungsalgorithmus folgenden Aufbau: Boolean Vermeiden (P,A,V,qx) { for i:=1 to n { if (i=x) P'[x] := P [x] +qx; else P'[x] := P[x] ;
} Q':= A-P'; V := V-qx; Deadlock := Erkennen(P',Q',V') if (Deadlock) return False; else return True;
} Er prüft einen eventuellen Verklemmungszustand bei Zuteilung der von einem Prozeß Tx aktuell angeforderten Betriebsmittel qx. Ein Rückgabewert False weist auf einen möglichen Verklemmungszustand hin; das System wird in diesem Fall die Zuteilung nicht durchführen und den Prozeß blockieren. Obwohl dieser Algorithmus weniger Restriktionen auferlegt als die zuvor behandelten Verfahren zur Vermeidung von Verklemmun-
8.4
Realisierungsbeispiele
gen, hat er sich aus einsichtigen Gründen in der Praxis nicht durchgesetzt:
• Die Kenntnis des Maximalbedarfs an Betriebsmitteln kann in • • •
8.4
den meisten Fällen nicht vorausgesetzt werden. In dynamischen Prozeßumgebungen kann die schritthaltende Modifikation der Matrizen P, Q und A aufwendig werden. Die zeitliche Dauer der Prozeßblockaden aufgrund versagter Betriebsmittelzuteilung ist nicht kalkulierbar. Der Algorithmus ist pessimistisch; in den meisten Fällen würde bei einer Zuteilung trotz der potentiellen Gefahr keine Verklemmung stattfinden.
Kritik am Vermeidungsalgorithmus
Realisierungsbeispiele
Wie bereits mehrfach angeklungen, sind die bekannten Verfahren zum Erkennen und Vermeiden von Verklemmungen in der Praxis nicht einsetzbar. Entsprechend gering ist das Angebot, das Betriebssysteme im Hinblick auf Verklemmungen gegenwärtig bereitstellen. Aufgrund der starken Vorherrschaft des sequentiellen Programmiermodells gab es in der Vergangenheit dafür auch keine dringende Notwendigkeit. Darüber hinaus konnte die latente Verklemmungsgefahr bei der Prozessor-, Speicher- und Gerätezuteilung von der Systemsoftware u.a. durch eine Virtualisierung und die Bereitstellung logischer Geräte gelöst werden. Durch die damit eingeführte Indirektionsstufe war es möglich, einem Prozeß ohne dessen Wissen physische Betriebsmittel in kritischen Situationen zu entziehen und damit eine der vier notwendigen Bedingungen für das Entstehen einer Verklemmung außer Kraft zu setzen. Durch die wachsende Verbreitung des nebenläufigen Programmiermodells wird innerhalb der Anwendung die Kooperation und Konkurrenz zwischen mehreren Prozessen sichtbar. Aus diesem Grund wird in der Praxis der Aspekt Verklemmung eine wichtigere Rolle einnehmen, als dies in der Vergangenheit der Fall war. Ein Indiz dafür mag sein, daß im POSIX.4-Standard u. a. für die potentiell blokkierende P-Operation auf ein Semaphor int sem_wait ( sem_t *s )
ein Rückgabewert EDEADLK vorgesehen ist. Die Systemsoftware gibt diese Fehlermeldung zurück, wenn sie irgendeine Form von Erkennungsalgorithmus ausführt und der aktuelle Aufruf der P-Operation zu einer Verklemmung führen würde. Gegenwärtig wird diese Erkennung jedoch von keinem System durchgeführt.
Verklemmungen haben in der Vergangenheit praktisch keine Rolle gespielt
POSIXA-Standard sieht Rückgabe einer Verklemmungswarnung bei blockierenden Operationen vor
8
Synchronisationsfehler
Im übrigen verschiebt diese Form der Informationsübergabe an die Anwendung nur das Problem: Da die Fehlermeldung EDEADLK in keinem heutigen System auftaucht, werden die wenigsten Anwendungen geeignete Vorkehrungen in der folgenden Form treffen: ret = sem_wait(s); if (ret == EDEADLK) { // Alternative } else { // Kritischer Abschnitt }
sondern meistens mittels sem_wait(s); // Kritischer Abschnitt
unkoordiniert den kritischen Abschnitt betreten werden. Statt einer permanenten Verklemmung sind damit beliebige Ablaufinkonsistenzen vorprogrammiert.
9
Dateisysteme
Dateisysteme dienen der dauerhaften Speicherung von Programmen und Daten. Diese dauerhafte oder persistente Speicherung unterscheidet sich grundsätzlich von der Datenspeicherung durch RAM-Bausteine, wie sie im Hauptspeicher des Rechners stattfindet. Zum Einsatz kommen magnetische und optische Schreib- und Leseverfahren, die im Gegensatz zum Hauptspeicher den Datenerhalt auch nach dem Ausschalten der Versorgungsspannung sicherstellen. Die eingesetzten Speichermedien sind meist mit einer besonderen Beschichtung versehene Platten oder Bänder, auf die mechanisch über magnetische und/oder optische Lese- und Schreibköpfe zugegriffen wird.
Dauerhafte Speicherung von Daten
Abb. 9-1 Schematicher Aufbau eines Festplattenlaufwerks
Ein gängiges Beispiel sind sogenannte Festplattenlaufwerke, bei denen ein oder mehrere magnetisch beschichtete Platten mit hoher Geschwindigkeit (gegenwärtig zwischen 5400 und ca. 10000 Umdrehungen pro Minute) rotieren (siehe Abbildung 9-1). Die Lese- und Schreibköpfe können mit Hilfe einer entsprechend dimensionierten Mechanik auf einer bestimmten Stelle über der Platte positioniert werden. Aufgrund der Plattenrotation greifen die Köpfe dabei auf jeweils einen konzentrischen Kreis, dem sogenannten Zylinder, zu. Die Plattenelektronik kann beim Schreiben innerhalb eines Zylinders binäre Daten in eine unterschiedliche magnetische Polarisierung der Plattenbeschichtung umwandeln. Analog wird die Magnetisierung der einzelnen Bitpositionen eines Zylinders, wenn sie sich unterhalb des Lese-
Funktionsweise einer Festplatte
9
Weitere externe Speichersysteme
Zugriffszeiten sind meist im Bereich Millisekunden
Kapazität ist meist im Bereich GByte
Dateisysteme
kopfs befinden, eine entsprechende Polarisierung des Kopfs bewirken, die eine anschließende Rückwandlung der binären Daten möglich macht. Neben magnetischen Festplatten existiert eine große Vielfalt an nicht flüchtigen Speichersystemen. Weitere gängige magnetische Verfahren sind z.B. Diskettenlaufwerke oder Magnetbänder in sehr unterschiedlichen Ausführungen. Bei optischen Verfahren werden unterschiedliche Reflektions- oder Polarisierungseigenschaften der Beschichtung für die Speicherung binärer Daten eingesetzt. Beim lesenden Zugriff z.B. in einem CD-ROM- oder DVD-Laufwerk wird ein Laser auf die CD gerichtet und das reflektierte Licht von einer Photodiode empfangen und ausgewertet. Ein schreibender Zugriff ist bei CD-ROM analog zur ROM-Halbleitertechnologie nicht unmittelbar möglich. Bei wiederbeschreibbaren optischen Systemen (CD-R, CDRW etc.) wird auch der schreibende Zugriff von einem Laser vorgenommen, der jedoch im Vergleich zum Leselaser mit einer höheren Energie arbeitet. Es sind jedoch auch Hybridtechniken z.B. in der Form sogenannter MO-Laufwerke (magneto-optische Laufwerke) im Einsatz, bei denen der Schreibvorgang magnetisch durchgeführt wird und die unterschiedlichen Magnetisierungszustände der Beschichtung optisch wahrgenommen und damit gelesen werden. Neben der nicht flüchtigen Speicherung der Daten ist allen diesen Systemen gemeinsam, daß sie im Vergleich zum Hauptspeicher eine um mehrere Größenordnungen höhere Zugriffszeit haben. Bei einer Festplatte muß z. B. beim Zugriff auf ein bestimmtes Datum zuerst der Kopf über dem entsprechenden Zylinder positioniert werden. Im Extremfall muß der Kopf dabei über die gesamte Platte bewegt werden. Die Zeit für diese Positionierung liegt bei gängigen Platten im Bereich 5 bis 14 ms; gefolgt von einer ca. 1 ms langen Ausschwingzeit des Kopfes. Sobald die gewünschten Daten sich unterhalb des Kopfes befinden, kann dann der Zugriff beginnen. Im schlimmsten Fall muß die Elektronik jedoch eine Plattenumdrehung warten, wenn sich die Daten unmittelbar vor dem Zugriff gerade unter dem Kopf befunden haben. Diese dritte Wartezeit hängt lediglich von der Rotationsgeschwindigkeit ab: Bei einer Platte mit 5400 Umdrehungen pro Minute sind das maximal 11 ms. Insgesamt ergibt sich bei modernen Platten eine mittlere Zugriffszeit zwischen 7 und 12 Millisekunden. Aus einsichtigen Gründen ist die Zugriffszeit erheblich niedriger, wenn größere Datenmengen bewegt werden und diese innerhalb eines Zylinders oder zumindest auf benachbarten Zylindern einer Platte gespeichert werden können. Der schlechten Zugriffszeit externer Speichersysteme steht eine vergleichsweise hohe Kapazität und ein sehr gutes Preis-Leistungsverhältnis gegenüber; letzteres wird in DM pro KByte, MByte oder GByte aus-
9.1
Anforderungen
gedrückt. So kosten 4 GByte Hauptspeicher, aufgebaut aus gängigen dynamischen Speicherbausteinen, Anfang 2001 zwischen 4000 und 10000 DM. Für 4-GByte-Platz auf einer modernen Festplatte müssen dagegen nur noch ca. 70 DM investiert werden. Die Kapazitäten einzelner Platten reichen von 1 GByte bis hin zu 80 GByte. Auch bei optischen Systemen sind mit der Einführung der sogenannten DVD (digital versatile disk) Kapazitäten bis 17 GByte erreichbar, bei einem Preis der in naher Zukunft nicht weit oberhalb heutiger CD- und CD-R-Laufwerke liegen wird. Die zentrale Aufgabe des Dateisystems ist es daher, die besonderen Eigenschaften externer Speichermedien optimal umzusetzen und Anwendungsprogrammen einen effizienten Zugriff auf die persistent gespeicherten Daten zu ermöglichen. Jedes Dateisytem führt zu diesem Zweck zwei wesentliche Abstraktionen ein: Datei und Verzeichnis. Dateien bilden die Behälter für die persistente Speicherung jeglicher Information einschließlich dem Code ausführbarer Programme. Sie werden über einen Namen angesprochen. Das Dateisystem bietet besondere Zugriffsfunktionen für Dateien an, die der Zugriffscharakteristik externer Speicher Rechnung tragen. Dadurch können in vielen Fällen die langen Zugriffszeiten vor der Anwendung verborgen werden. Verzeichnisse sind besondere, vom Dateisystem selbst verwaltete Dateien, die eine Strukturierung externer Speichermedien erlauben. Dadurch wird es möglich, die sehr großen Speicherkapazitäten geordnet auf viele zehntausend Dateien zu verteilen. Im nachfolgenden Abschnitt 9.1 sollen die Anforderungen an ein Dateisystem herausgearbeitet werden. Im Vordergrund steht dabei, wie Programmanforderungen und die Charakteristika externer Speicher aufeinander abgestimmt werden können. In Abschnitt 9.2 werden Dateien und die darauf definierten Zugriffsfunktionen vorgestellt. Abschnitt 9.3 beschäftigt sich mit dem Aufbau von Verzeichnisstrukturen. Ein schichtenbasiertes Architekturmodell für eine vergleichende Betrachtung verschiedener Dateisysteme ist Gegenstand von Abschnitt 9.4. Im abschließenden Abschnitt 9.5 werden Realisierungsaspekte diskutiert.
9.1
Datei und Verzeichnis
Anforderungen
Die Grundanforderungen an ein Dateisystem sind offensichtlich: Einem Prozeß muß der lesende und/oder schreibende Zugriff auf eine Datei gewährt werden können. Der Zugang zu einer einzelnen Datei findet dabei in der Regel über ihren Namen statt. Darüber hinaus muß das Dateisystem einem oder mehreren Benutzern die Erzeugung und Verwendung von Dateien gestatten. Neben den dafür notwendigen
Orundanforderungen
9
Dateisysteme
Ordnungs- und Strukturierungsverfahren muß ein Dateisystem dabei speziell im Mehrbenutzerbetrieb auch den Zugriff auf Dateien kontrollieren. Benutzer müssen festlegen können, welche Dateien in ihrem Besitz sind und welche anderen Benutzer in welcher Form darauf zugreifen dürfen. Diese sogenannten Zugriffsrechte müssen überprüfund änderbar sein. Auf der Grundlage dieser Rechte soll auch der lesende oder schreibende Zugriff eines Prozesses vom Dateisystem entweder gewährt oder zurückgewiesen werden. Unterschiedliche Lese- und Schreibgranulate
Blockorientierter Zugriff
Transfer ganzer Blöcke verbessert effektive Zugriffszeit
Prefetching
Aufgrund der hohen Zugriffszeiten bei externen Speichermedien ist ein kleines Lese- und Schreibgranulat, d.h. das Lesen und Schreiben von wenigen Bytes, nicht sinnvoll. Daher bilden sogenannte Blöcke die kleinste Übertragungseinheit zwischen externem Speicher und Hauptspeicher. Typische Blockgrößen liegen im Bereich 512 Byte bis 4 KByte. Ein blockorientierter Datentransfer hat zusätzlich den Vorteil, daß die Zugriffszeiten nach dem Lesen des ersten Bytes eines Blockes drastisch fallen. So muß - wie bereits angesprochen - ein FestplattenController im Mittel mehrere Millisekunden warten, bis das erste Byte des gewünschten Blockes gelesen oder geschrieben werden kann. Die Übertragungsgeschwindigkeit für die restlichen Bytes eines Blockes hängt jedoch nur von der Umdrehungsgeschwindigkeit und der pro Zylinder gespeicherten Anzahl an Bytes ab. Bei einer mit 5400 Umdrehungen pro Minute rotierenden Platte und einer Zylindergröße von z.B. 170 KByte beträgt die Zugriffszeit für alle restlichen Bytes eines Blockes jeweils 63 ns; dies entspricht der Zugriffszeit gängiger dynamischer Speicherbausteine wie sie zum Aufbau eines Hauptspeichers eingesetzt werden. Es ist offensichtlich, daß mit wachsender Blockgröße die effektive Zugriffszeit auf den externen Speicher drastisch reduziert werden kann. Gleichzeitig wächst jedoch der Anteil an Daten, die vom aufrufenden Prozeß primär nicht angefordert wurden. Wie hoch der resultierende Leistungsgewinn oder -verlust effektiv ist, hängt von der erzielbaren Trefferrate ab. Bei einem sequentiellen Dateizugriff wird durch große Blöcke die Zugriffsgeschwindigkeit aus Sicht der einzelnen Anwendung verbessert. Solange nicht andere Anwendungen zusätzliche Blöcke anfordern, ist es sogar kostengünstiger, mittels eines sogenannten Prefetching mehrere schnell erreichbare Folgeblöcke der Datei vorab zu laden. Der für die einzelne Anwendung in diesem Fall erzielte Leistungsgewinn muß vom Dateisystem jedoch gegenüber den potentiellen Leistungseinbußen für andere Prozesse abgewogen werden, deren Lese- und Schreibaufträge deswegen verzögert ausgeführt werden.
9.1
Anforderungen
Im Gegensatz zu der festen Blockgranularität zwischen externem Speicher und dem Hauptspeicher definieren sich Anwendungen ihre Zugriffsgranularität selbst. In vielen Fällen werden unterschiedliche Datenmengen in aufeinanderfolgenden Dateizugriffen gelesen oder geschrieben. Häufig sind es sogar nur wenige Bytes, wenn z.B. nur ein einzelnes Zeichen oder ein 4 Byte Integerwert von der Anwendung angefordert wird. Eine zentrale Aufgabe des Dateisystems muß es daher sein, die Anwendungsanforderungen automatisch an die Blockgranularität anzupassen und den resultierenden Mehraufwand durch geeignete Caching-Strategien zu minimieren.
Anwendungen definieren ihr Zugriffsgranulat selbst
Charakteristiken der Dateinutzung Analog zu den Charakteristiken externer Speicher liegt vielen Dateisystemen eine Nutzungscharakteristik von Dateien zugrunde. Ziel des Dateisystems ist es dabei, die häufigste Form der Dateinutzung optimal auf die Eigenschaften externer Speicher abzustimmen (»make the common case fast«). Wie Dateien danach typischerweise genutzt werden, wurde empirisch ermittelt:
• • • •
Dateien sind meist klein (wenige KByte). Dateien werden häufiger gelesen, seltener geschrieben und noch seltener gelöscht. Der sequentielle Zugriff ist dominant. Dateien werden selten von mehreren Programmen oder Personen gleichzeitig benutzt.
Viele Dateisysteme bauen noch heute auf dieser Nutzungscharakteristik auf und optimieren ihre Zugriffsfunktionen entsprechend. Diese Systeme erlauben z.B. den schnellen Zugriff auf die einzelnen Positionen einer kleineren Datei, während der Zugriff auf den Großteil einer viele MByte großen Datei zum Teil erheblich verzögert werden kann. Gleichzeitig wird in vielen Systemen nur der sequentielle Zugriff auf die Datei besonders unterstützt. Eine wahlfreier Zugriff auf die Daten einer Datei hat in solchen Fällen meist eine außergewöhnlich hohe Verzögerung des zugreifenden Programmes zur Folge. Unklar ist bei der Dominanz des sequentiellen Zugriffs auch, inwieweit es sich hierbei um ein Henne-Ei-Problem handelt. Aufgrund der inhärent einfachen Optimierung des sequentiellen Zugriffs auf externen Speicher werden viele Programme ihr Zugriffsverhalten zwangsweise danach ausgerichtet haben. Andere mögliche Formen der Dateinutzung standen damit lange Zeit im Hintergrund.
Dateien sind meist klein und werden deshalb vorzugsweise gelesen
9
Dateisysteme
Multimedia
Multimediale Daten erfordern hohe Speicherkapazitäten...
... und eine hohe Übertragungsbandbreite
Neuere Untersuchungen würden bei der Nutzungscharakteristik von Dateien ein verändertes Bild ergeben: Beispielsweise hat die zunehmende Digitalisierung audiovisueller Medien (Audio- und Videodaten) neben den weiterhin vorhandenen kleinen Dateien zu Dateigrößen im Bereich von mehreren hundert MByte bis hin zu mehreren GByte geführt. So benötigt beispielsweise eine Audiodatei, die Toninformation in CD-Qualität speichert, pro Minute Ton ca. 10 MByte Speicherkapazität. Bei Videodaten ist der Speicherbedarf leicht ein bis zwei Größenordnungen höher. Nicht jedes Dateisystem ist in der Lage, derart große Dateien zu speichern. Unberücksichtigt bleiben auch meist Echtzeitaspekte, als Folge kann es z. B. bei der Wiedergabe einer Audio- oder Videodatei aufgrund der Nichtlinearität bei der Zugriffszeit leicht dazu kommen, daß die notwendig gleichmäßige Zugriffsgeschwindigkeit über die gesamte Länge oft nicht gewährleistet werden kann. Ein »Ruckein« der Wiedergabe ist in solchen Fällen nicht vermeidbar. Problematisch ist in leistungskritischen Einzelfällen auch die maximale Übertragungsbandbreite, die ein Dateisystem einer Anwendung beim lesenden und schreibenden Zugriff auf eine Datei zur Verfügung stellt. In einfach aufgebauten Systemen wird sie letztendlich durch die Bandbreite zum externen Speicher beschränkt. Bei modernen Festplatten liegt die über längere Zeiträume maximal mögliche Übertragungskapazität im Bereich 2 bis 15 MByte pro Sekunde. Anwendungen, die höhere Übertragungskapazitäten benötigen, sind auf dafür besonders ausgelegte Platten- und Dateisysteme angewiesen. Zum Beispiel ist bei einer unkomprimierten Videoaufzeichnung im Format 1024x768 bei 3 Byte pro Pixel und 50 Bildern pro Sekunde eine Übertragungskapazität von 112,5 MByte pro Sekunde notwendig (abgesehen davon, daß diese Datei bereits nach 1 Minute eine Größe von 6,5 GByte erreichen würde). Einblendtechniken und wahlfreier Zugriff auf persistente Daten
Persistente Objekte
Die Dominanz sequentiellen Zugriffsverhaltens auf Dateien ist im Licht aktueller Nutzungsformen ebenfalls zu relativieren. Dies soll an einem Beispiel verdeutlicht werden. So ist es im Fall der objektorientierten Programmierung naheliegend, zwischen persistenten und nichtpersistenten Objekten zu unterscheiden. Während sich die Lebensdauer nichtpersistenter Objekte lediglich auf die Programmlaufzeit beschränkt, ist diese bei persistenten Objekten aufgrund der Sicherung des Objektzustands auf externem Speicher potentiell unbegrenzt. Bei Dateisystemen, die auf diese Nutzungsform noch nicht vorbereitet
I
9.2
Dateien
sind, muß die Sicherung explizit von der Anwendung durch eine sogenannte Serialisierung erreicht werden. Dabei wird der Objektzustand beim ersten Zugriff z.B. beim Aufruf einer Objektmethode aus einer Datei sequentiell ausgelesen und die entsprechende Objektinstanz schrittweise aufgebaut. Analog wird der eventuell veränderte Zustand spätestens bei Programmbeendigung wieder seriell auf die Datei zurückgeschrieben. Im Einzelfall bietet die Laufzeitumgebung der Anwendung unterstützende Funktionen für diese Serialisierung an. Zum Beispiel stellt die MFC-Bibliothek (Microsoft Foundation Classes), ein objektorientiertes Framework für Win32-Anwendungen mit graphischer Bedienungsoberfläche, entsprechende Serialisierungsunterstützung zur Verfügung [Prosise 1996]. Neuere Dateisysteme erlauben die Sicherung eines persistenten Objektzustands ohne den zeitraubenden Umweg über die Serialisierung. Mit Hilfe sogenannter Ein- und Ausblendtechniken können dabei Ausschnitte einer Datei, die persistente Objektzustände speichert, im virtuellen Adreßraum einer Anwendung sichtbar gemacht und bei Änderungen jederzeit wieder auf den externen Speicher zurückgeschrieben werden. Dadurch sind Objektzustände direkt sicherbar. Die unvorherbestimmte Aufrufreihenfolge einzelner Methoden eines Objekts durch ein oder mehrere Kontrollflüsse innerhalb eines Adreßraums setzt jedoch einen effizienten Zugriff auf den Objektcode und damit auf verschiedene Abschnitte der für die Speicherung der persistenten Objekte verwendeten Datei voraus. Dabei besteht die Hoffnung, daß - ähnlich wie bei der Realisierung virtuellen Speichers - die hohe Zugriffszeit auf externen Speicher durch die Referenzlokalität sequentieller Kontrollflüsse und darauf abgestimmter Caching-Strategien des Dateisystems weitgehend kompensiert werden kann.
9.2
Serialisierung von Objektzuständen auf eine Datei
Ein- und Ausblendtechniken ersetzen Serialisierung
Dateien
Dateien bilden in einem Dateisystem die Behälter für die dauerhafte Speicherung beliebiger Information. Auch die restliche Systemsoftware greift in vielen Fällen auf Dateien zurück. So wird beim Starten eines neuen Prozesses der zugehörige Programmcode einer Datei entnommen. Im Einzelfall wird auch die Auslagerung von Seiten des virtuellen Adreßraums über das Dateisystem auf einer sogenannten Auslagerungsdatei (z.B. Windows 9x) vorgenommen. Benutzer und Anwenderprogramme speichern ebenfalls sehr unterschiedliche Informationen in Dateien. Eine Datei kann z.B. den Lagerbestand, einen Brief, den Text eines Buches, Bilder, audiovisuelle Daten oder Programm-Quelltexte enthalten. Der Interpretation des Dateiinhalts in dieser Beziehung sind keine Grenzen gesetzt. In fast al-
Dateien sind Behälter für beliebige Information
9 Dateisysteme
len Systemen wird eine Datei als eine Folge von Bytes aufgefaßt. Eine Datei beginnt mit dem Byte 0 und endet in Abhängigkeit von ihrer Länge n mit einem Byte n-1. Öffnen einer Datei Dateien müssen vor dem Zugriff geöffnet werden
POSIX
Vor dem ersten lesenden oder schreibenden Zugriff muß ein Prozeß eine Datei öffnen. Das Dateisystem wird dadurch in die Lage versetzt, die Datei auf dem externen Speicher zu lokalisieren und interne Datenpuffer für den anschließenden Dateizugriff zu initialisieren. In entsprechenden Systemen wird darüber hinaus geprüft, ob der aufrufende Prozeß die für den gewünschten Dateizugriff notwendigen Rechte besitzt. In POSIX.l-konformen Systemen wird eine Datei mittels des Systemaufrufs open() geöffnet (siehe z.B. auch [Stevens 1992]): int open ( const char *filename, int flags, mode_t mode )
Mit dem ersten Argument filename gibt der aufrufende Prozeß an, welche Datei zu öffnen ist. Mit dem zweiten Argument flags wird u.a. die Art des Zugriffs festgelegt:
• • •
Dateideskriptor
O_RDONLY: Nur lesender Zugriff O_WRONLY: Nur schreibender Zugriff O_RDWR: Lesender und schreibender Zugriff
Die Datei wird nur dann erfolgreich geöffnet, wenn die gewünschte Zugriffsart durch die vom Besitzer der Datei festgelegten Zugriffsrechte gestattet ist. Über das Argument flags können weitere Eigenschaften der Dateinutzung durch den Prozeß bestimmt werden. So wird z.B. durch den Wert O_APPEND eine Datei so geöffnet, daß geschriebene Daten an ihr Ende angehängt werden. Über den Wert O_CREAT kann gesteuert werden, wie sich das Dateisystem verhalten soll, wenn die angegebene Datei beim Offnen noch nicht existiert. Bei Angabe von O_CREAT wird die Datei angelegt, ansonsten bricht die Funktion mit einer Fehlermeldung ab. Die neu erzeugte Datei erhält die im dritten Argument mode angegebenen Zugriffsrechte. Als Besitzer der Datei wird die Benutzerkennung des erzeugenden Prozesses eingetragen. Kann die Datei erfolgreich geöffnet werden, so liefert die Funktion einen sogenannten Dateideskriptor zurück, über den in nachfolgenden Funktionsaufrufen auf die Datei zugegriffen werden kann. Der Wert -1 wird dagegen im Fehlerfall zurückgegeben.
9.2
Dateien
Das Öffnen einer Datei oder das Erzeugen einer neuen Datei geschieht in der Win32-Programmierschnittstelle von Windows 9x und Windows NT/2000 über die Funktion CreateFile(): HANDLE CreateFile ( LPCSTR filename, DWORD access, DWORD shared_mode, LPSECURITY_ATTRIBUTES secatt, DWORD creation, DWORD flags, HANDLE template_file )
Da die Funktion CreateFile() sehr viele unterschiedliche Aufgaben erfüllt, können vom aufrufenden Kontrollfluß entsprechend viele Argumente angegeben werden. Über das erste Argument filename wird wieder der Dateiname der zu öffnenden Datei angegeben. Das Argument access legt den Zugriffsmodus fest: GENERIC_READ für einen lesenden, GENERIC_WRITE für einen schreibenden und GENERIC_READ | GENERIC_WRITE
für lesenden und schreibenden Zugriff. Das dritte Argument shared_mode legt fest, ob ein gleichzeitiger Zugriff durch andere Prozesse gestattet ist. Der Wert 0 öffnet die Datei exklusiv für den aufrufenden Prozeß; die Versuche anderer Prozesse, diese Datei zu öffnen, scheitern in diesem Fall. Werden diesem Argument die Werte FILE_SHARED_READ oder FILE_SHARED_WRITE zugewiesen, so werden weitere Versuche, diese Datei zu öffnen, nur zugelassen, wenn der zweite Prozeß lediglich Lesezugriff bzw. Schreibzugriff anfordert. Durch das Argument creation kann der aufrufende Prozeß beeinflussen, wie beim Öffnen einer nicht vorhandenen und beim Erzeugen einer bereits vorhandenen Datei reagiert werden soll:
• • • • •
CREATE_NEW: Eine neue Datei wird erzeugt; der Funktionsaufruf wird fehlerhaft beendet, falls die Datei bereits existiert. CREATE_ALWAYS: Die Datei wird immer neu erzeugt; eine bereits vorhandene Datei gleichen Namens wird gelöscht. CREATE_EXISTING: Eine vorhandene Datei wird geöffnet; es wird eine Fehlermeldung zurückgegeben, falls die Datei nicht existiert. OPEN_ALWAYS: Die Datei wird geöffnet oder zuerst erzeugt und anschließend geöffnet. TRUNCATE_EXISTING: Eine vorhandene Datei wird geöffnet und auf 0 Byte Länge gesetzt; es wird eine Fehlermeldung zurückgegeben, falls die Datei nicht existiert.
Win32
9
Zugriffsoptimierung mit Anwendungshilfe
Dateisysteme
Über das Argument flags können eine Reihe von Eigenschaften beim Zugriff auf die Datei eingestellt werden. Eine detaillierte Erörterung aller Möglichkeiten würde hier den Rahmen sprengen (siehe dafür z.B. [Richter 1999]. Von besonderem Interesse sind jedoch die folgenden Einstellungen: FILE_FLAG_SEQUENTIAL_SCAN
oder FILE FLAG RANDOM ACCESS
Dateideskriptor
Durch diese Werte charakterisiert der aufrufende Prozeß, ob auf den Dateiinhalt sequentiell oder wahlfrei zugegriffen werden soll. Das Dateisystem ist auf der Grundlage dieser Zusatzinformationen in der Lage, eine entsprechende Optimierung des Dateizugriffs vorzunehmen. Analog zur Funktion open() in POSIX.l gibt die Funktion CreateFile() einen Dateideskriptor zurück, falls die angegebene Datei erfolgreich geöffnet werden konnte. Über diesen Deskriptor kann der aufrufende Prozeß anschließend auf den Dateiinhalt zugreifen. Schließen einer Datei
Freigabe belegter Ressourcen
Es ist nicht nur guter Programmierstil, eine nicht mehr benötigte Datei wieder zu schließen. Das Dateisystem kann dadurch reservierte Ressourcen wie z.B. Datenpuffer wieder freigeben. In Systemen, die einen exklusiven Zugriff auf Dateien ermöglichen, wird durch das Schließen der Zugang zu dieser Datei auch für andere Prozesse wieder ermöglicht. Es hat in der Regel keine negativen Auswirkungen, wenn ein Prozeß das Schließen einer nicht mehr benötigten Datei unterläßt. Belegte Betriebsmittel wie z.B. Dateien werden von der Systemsoftware mit der Terminierung des belegenden Prozesses immer implizit freigegeben. Im Fall der Unterlassung wird lediglich der Dateizugang für andere Prozesse unnötig lange unterbunden und eine eventuell mögliche Parallelarbeit damit verhindert. Dateien werden durch den Aufruf einer entsprechenden Systemfunktion unter Angabe des Dateideskriptors der nicht mehr benötigten Datei geschlossen. In POSIX.l-konformen Systemen geschieht dies mittels der Funktion int close ( int fd )
Die Win32-Schnittstelle bietet zu diesem Zweck die Funktion CloseHandle ( HANDLE fd )
an.
9.2
Dateien
Schreibinstruktionen sowohl sequentiell als auch wahlfrei darauf zugegriffen werden. Die Realisierung eingeblendeter Dateien ist für das Dateisystem nicht weiter schwer, da praktisch die gesamte Funktionalität von der Systemsoftware bereits zur Realisierung des virtuellen Adreßraums implementiert wird. Das Dateisystem bestimmt lediglich einen hinreichend großen Bereich im virtuellen Adreßraums, der den gewünschten Dateiausschnitt oder die vollständige Datei aufnehmen kann. Entsprechend große freie Adreßbereiche befinden sich in der Regel zwischen Heap und Stack. Die Seitentabellendeskriptoren für diesen virtuellen Adreßbereich werden anschließend initialisiert und zeigen auf die entsprechenden Blöcke der einzublendenden Datei. Analog zu virtuellen Speichertechniken können einzelne Blöcke bereits durch ein Prefetching in den Hauptspeicher geladen werden. In der Regel wird das Dateisystem jedoch lediglich den virtuellen Adreßbereich für einen Zugriff freigeben und die eigentlichen Blockinhalte als Folge eines Seitenfehlers beim ersten Zugriff nachladen. Aus dieser Form der Realisierung ergibt sich zwangsläufig, daß immer nur Vielfache ganzer Blöcke einer Datei eingeblendet werden können. Ein einzublendender Bereich, der nicht auf einer Blockgrenze beginnt oder keine entsprechende Länge aufweist, wird vom Dateisystem meist stillschweigend auf Blockgrößen erweitert. Auch Dateien, die zum Lesen und Schreiben geöffnet wurden, können eingeblendet werden. Veränderte Blöcke werden aus Effizienzgründen meist zu einem späteren Zeitpunkt, auf jeden Fall aber beim Schließen der Datei auf den externen Speicher zurückgeschrieben. In kritischen Fällen kann die Anwendung das Zurückschreiben veränderter Blöcke auch zu einem bestimmten Zeitpunkt erzwingen. Die Einblendung einer Datei ist für eine Anwendung vergleichsweise einfach. Die Vorgehensweise soll anhand des nachfolgenden Code-Fragments am Beispiel der Win32-Programmierschnittstelle skizziert werden: HANDLE fh, fmh; DWORD len; LPVOID addr; fh = CreateFile( filename, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); DWORD highlen; len = GetFileSize(fh,Shighlen); fmh = CreateFileMapping( fh, NULL, PAGE_READONLY, 0,0, NULL); addr = MapViewOfFile(fmh,FILE_MAP_READ,0,0,0);
Zugunsten der Übersichtlichkeit wurde in diesem Beispiel auf das Abfangen von Fehlerfällen verzichtet. In dem Beispiel wird eine Datei
Virtuelle Speichertechniken bilden die Basis für eingeblendete Dateien
Win32
9 Dateisysteme
Sehr große Dateien können stückweise eingeblendet werden
POSIX
filename vollständig in den virtuellen Adreßraum eingeblendet. Dabei soll lediglich lesender Zugriff gestattet sein. Analog zum klassischen Dateizugriff wird die Datei vor dem Einblenden geöffnet (createFile). Über den Dateideskriptor fh wird in späteren Funktionsaufrufen auf die Datei zugegriffen. Da die Datei vollständig eingeblendet werden soll, wird mittels GetFileSize zuerst die aktuelle Dateilänge (len) bestimmt. Anschließend wird über den Aufruf der Funktion CreateFileMapping() das Einblenden vorbereitet und mittels MapViewOf File() die Datei tatsächlich in den Adreßraum eingeblendet. Der Rückgabewert addr speichert die Anfangsadresse der Datei im virtuellen Adreßraum, d.h., an der virtuellen Adresse addr+i befindet sich das Byte i der eingeblendeten Datei. In der Regel wählt das Dateisystem den virtuellen Adreßbereich und damit die Anfangsadresse aus. Über die Funktion MapViewOfFileEx() kann der aufrufende Prozeß einen bestimmten Bereich des Adreßraums auch vorgeben. Die Funktion wird versuchen, dem Vorschlag zu folgen. Ist dies aus Platzgründen nicht möglich, weil zumindest Teile des vorgeschlagenen Adreßbereichs bereits belegt sind, terminiert die Funktion mit einer Fehlermeldung. Anzumerken ist, daß das angegebene Code-Fragment in dieser Form nur mit Dateigrößen bis maximal ca. 3 GByte funktioniert. Man kann mit Hilfe von MapViewOfFile() jedoch auch jeweils einen ca. 3 GByte großen Ausschnitt einer insgesamt maximal 264 Byte großen Datei in den Adreßraum einblenden. Auf die gesamte Datei muß dann in mehreren Schritten zugegriffen werden. Dabei wird bei jedem Aufruf von MapViewOfFile() ein anderes Offset innerhalb der Datei angegeben. Die Speichereinblendung kann durch den Aufruf der Funktion UnmapViewOfFile() aufgehoben werden. Werden keine weiteren Einblendungen der Datei mehr vorgenommen, können durch die Freigabe des Deskriptors fmh mittels closeHandle(fmh) alle für Einblendungen der angegebenen Datei reservierten Ressourcen freigegeben werden. Im POSIX-Standard ist die Funktion mmap() nicht nur für das Einrichten eines gemeinsamen Speicherbereichs zwischen mehreren Adreßräumen zuständig (siehe auch Kapitel 6), sondern auch für das Einblenden von Dateien: caddr_t mmap ( caddr_t addr, size_t len, int prot, int flag, int fd, off t offset )
9.3
Verzeichnisse
Der Dateideskriptor fd legt die gewünschte Datei fest. Es sollen len Bytes beginnend ab der Position offset innerhalb der Datei eingeblendet werden. Dabei muß offset in den meisten Fällen ein ganzes Vielfaches der Seitengröße sein, damit immer ein Vielfaches ganzer Seiten in den virtuellen Adreßraum eingeblendet werden kann. Der erste Parameter addr ist 0, wenn dem aufrufenden Prozeß die konkrete Plazierung innerhalb des virtuellen Adreßraums egal ist. Ansonsten gibt er die gewünschte Anfangsadresse an. Auch hier gilt, daß der Wert von addr ein Vielfaches der Seitengröße sein muß. Wird eine Anfangsadresse vorgegeben, muß zusätzlich das Argument flags auf den Wert MAP_FIXED gesetzt werden. Das Argument prot legt fest, welche Zugriffsrechte innerhalb der eingeblendeten Datei gelten sollen. Es kann zwischen lesendem (MAP_READ), schreibendem (MAP_WRITE), ausführendem (MAP_EXEC) und gar keinem Zugriff (MAP_NONE) unter-
schieden werden. Die Speichereinblendung kann in POSIX durch den Aufruf der Funktion munmap() beendet werden.
9.3
Verzeichnisse
Verzeichnisse {directory) oder Ordner (folder) erlauben eine hierarchische Strukturierung des externen Speichers. Ein Verzeichnis kann weitere Verzeichnisse, sogenannte Unterverzeichnisse {subdirectory), und einfache Dateien enthalten. Auf einfache Dateien können Anwendungsprogramme nach dem Öffnen über read() und write() oder über eine Speichereinblendung zugreifen. Die in einem Verzeichnis enthaltenen Unterverzeichnisse und Dateien besitzen Namen, die relativ zu diesem Vaterverzeichnis (parent directory) eindeutig sind. Ein ausgezeichnetes Verzeichnis, das sogenannte Wurzelverzeichnis (root directory), steht an oberster Stelle. Dieses Wurzelverzeichnis besitzt als einziges kein übergeordnetes Vaterverzeichnis. Durch diese rekursive Schachtelung von Verzeichnissen entsteht eine baumartige Struktur (siehe Abbildung 9-3). Die Blätter des Baums sind einfache Dateien oder leere Verzeichnisse, die keine weiteren Unterverzeichnisse und Dateien enthalten.
Hierarchische Strukturierung durch Verzeichnisse
Vaterverzeichnis Wurzelverzeichnis
9
Dateisysteme
Abb. 9-3 Baumartige Verzeichnisstruktur
Vollständige Dateinamen
Der vollständige und eindeutige Name einer Datei oder eines Verzeichnisses entsteht durch eine Aneinanderreihung aller Verzeichnisnamen beginnend beim Wurzelverzeichnis. Er endet mit dem eigentlichen Dateinamen. Durch ein spezielles Zeichen werden dabei die einzelnen Verzeichnisnamen getrennt. In UNIX-Systemen wird dafür das Zeichen »/« eingesetzt; in den Windows-Betriebssystemen übernimmt das Zeichen »\« diese Funktion. Beispiele für die vollständigen oder sogenannten absoluten Dateinamen verschiedener in Abbildung 9-3 dargestellter Dateien sind: /home/s/c. /home/s/leer /usr/bin/cc
Dabei ist der vollständige Dateiname /usr/bin/cc zusammengesetzt aus einem Pfad /usr/bin und dem eigentlichen Dateinamen cc. Anzumerken ist, daß es sich bei den in der Abbildung 9-3 dargestellten Dateien /home/n/a und /home/s/a um unterschiedliche Dateien handelt, obwohl der eigentliche Dateiname a identisch ist. Links
Links verweisen auf Dateien oder Verzeichnisse
Die streng hierarchische Struktur einer rekursiven Verzeichnisschachtelung kann in den meisten Dateisystemen durch sogenannte Links ergänzt werden. Links sind keine eigenständigen Dateien oder Verzeichnisse, sondern sie verweisen lediglich auf eine andere Datei oder ein anderes Verzeichnis. Dadurch können einzelne Dateien oder ein ganzer durch ein Verzeichnis definierter Teilbaum an mehreren Stellen in der gesamten Verzeichnisstruktur erscheinen. Die betroffenen Dateien und Verzeichnisse sind weiterhin nur einmal physisch vorhanden.
9.3
Verzeichnisse
Abb. 9-4 Links in einer Verzeichnisstruktur
Ein Beispiel für die Verwendung von Links ist in Abbildung 9—4 dargestellt. Der Link von /bin/x verweist auf /d1/d3. Dadurch gibt es zwei unterschiedliche Pfade für die Datei c: /dl/d3/c und /bin/x/c. Das gleiche gilt für die Datei d und den ganzen Teilbaum von Verzeichnis o. Ein entsprechender Link kann in einem UNIX-System mit Hilfe des Befehls In -s /dl/d3 /bin/x
angelegt werden. Laufwerksbuchstaben vs. Mounting In der Regel besitzt ein Rechner mehrere Festplatten und weitere externe Speichersysteme. Zusätzlich sind Platten partitionierbar, d.h., die Gesamtkapazität einer Platte kann aus administrativen Gründen in mehrere sogenannte Partitionen unterteilt werden. Aus Sicht des Dateisystems entspricht jede Partition einer eigenständigen Platte. In den meisten Dateisystemen ist jede einzelne Platte, CD-ROM oder Diskette in sich abgeschlossen, d.h., sie enthält ein eigenes Wurzelverzeichnis und alle von dieser Wurzel erreichbaren Unterverzeichnisse und Dateien müssen sich auf demselben Medium befinden. Das Dateisystem steht damit vor der Problematik, Anwendungsprogrammen und Benutzern Zugang zu allen Platten und weiteren externen Speichern zu gewähren. Die Betriebssysteme Windows 9x und Windows NT führen zu diesem Zweck sogenannte Laufwerksbuchstaben ein, die einem vollständigen Dateinamen vorangestellt werden müssen, damit der Ort der Datei oder des Verzeichnisses eindeutig festgelegt ist. So befindet sich z. B. die Datei C:\Windows\System\Kernel32.dll
auf der Platte mit dem Laufwerksbuchstaben C. Dabei werden standardmäßig die Buchstaben A und B für Diskettenlaufwerke reserviert.
Unterteilung einer Platte in Partitionen
Verwaltung mehrerer Platten und Partitionen
Laufwerksbuchstaben in Win32-Systemen
9
Dateisysteme
Platten und andere externe Speichergeräte werden anschließend aufsteigend beginnend mit C in der Reihenfolge numeriert, wie sie beim Starten des Dateisystems in einem Rechner vorgefunden werden. Obwohl diese Technik im wesentlichen nur Nachteile bringt, z.B. können sich bei Windows 95 die vollständigen Dateinamen durch Hinzunahme einer weiteren Platte aufgrund einer Umnumerierung bei den Laufwerksbuchstaben plötzlich ändern, wird sie von Microsoft wohl primär aus Gründen der Abwärtskompatibilität beibehalten. Bei Windows NT 4.0 können die Laufwerksbuchstaben mittlerweile fixiert werden und in der aktuellen Version Windows 2000 ist es möglich, den Zugriff auf mehrere Platten und externe Speicher auch ohne Laufwerksbuchstaben zu realisieren. Abb. 9-5 Zusammenfügen zweier Platten durch Mounting
Aufbau einer Gesamthierarchie in UNIX
Mounting
UNIX-Systeme verfolgen von Anfang an einen anderen Weg. Hier werden die Wurzelverzeichnisse oder Unterverzeichnisse verschiedener Platten durch das sogenannte Mounting in Beziehung zueinander gesetzt. Es entsteht eine einzige große Verzeichnisstruktur, die alle Platten und externe Speicher umfaßt. In seiner Funktionsweise ähnelt das Mounting dem Herstellen eines besonderen Links zwischen einem beliebigen Verzeichnis einer ersten Platte und dem Wurzelverzeichnis oder einem anderen Unterverzeichnis einer zweiten Platte. Das Wurzelverzeichnis der ersten Platte wird damit zum Wurzelverzeichnis für beide Platten. Eine ausgezeichnete Hauptplatte {root partition) definiert dadurch das Wurzelverzeichnis für Verzeichnisse und Dateien aller angeschlossenen externen Speicher. Ein Beispiel für das Mounten von Platten ist in Abbildung 9-5 dargestellt. Das Wurzelverzeichnis der Platte 2 wird über den Pfad /a mit der ersten Platte verbunden. Über den Pfad /a sind damit alle Verzeichnisse und Dateien der Platte 2 von Platte 1 aus erreichbar. In der Regel werden durch das Mounting von Wurzelverzeichnissen immer ganze Platten untereinander verknüpft. In manchen Fällen ist es jedoch sinnvoll, nur einen bestimmten Teilbaum einer Platte über
9.3
Verzeichnisse
eine andere Platte zugreifbar zu machen. In Abbildung 9-5 hätte auch eine Verknüpfung vom Pfad /a der ersten Platte zum Unterverzeichnis /a2 der zweiten Platte hergestellt werden können. In diesem Fall ist lediglich der durch /a2 definierte Teilbaum der zweiten Platte über /a erreichbar. Alle anderen Verzeichnisse von Platte 2 einschließlich /a1 bleiben damit von der ersten Platte aus nicht zugreifbar. Arbeitsverzeichnis Angesprochen werden Dateien immer über ihren vollständigen Namen. Das Dateisystem speichert daher für jeden Prozeß ein sogenanntes Arbeitsverzeicbnis {working directory), um die eindeutige Benennung einer gewünschten Dateien zu vereinfachen. Jeder nicht absolute Dateiname, d.h. ein Name, der in UNIX nicht mit einem »/«-Zeichen oder bei Windows 9x und Windows NT nicht mit einem Laufwerksbuchstaben beginnt, wird relativ zu diesem Arbeitsverzeichnis interpretiert. Ist das aktuelle Arbeitsverzeichnis z.B.
Arbeitsverzeichnis
Dateinamen relativ zum Arbeitsverzeichnis
/home/dpunkt/buch/Systemsoftware
so reicht die Angabe von kapii/dok.fm5 zur Benennung der Datei mit dem absoluten Dateinamen /home/dpunkt/buch/systemsoftware/kapll/dok.fm5
aus. In den meisten Systemen kann sogar über die Zeichenfolge »./« wie z.B. in ./kapll/dok.fm5
explizit auf das Arbeitsverzeichnis Bezug genommen werden. Windows 9x und Windows NT speichern für jedes Laufwerk ein eigenes Arbeitsverzeichnis, d.h., die Dateiangaben sind jeweils relativ zum Arbeitsverzeichnis des aktuellen Laufwerks. Dateien auf anderen Laufwerken müssen in diesem Fall immer absolut angegeben werden. Durch besondere Befehle kann das aktuelle Arbeitsverzeichnis erfragt oder verändert werden. In einem UNIX-System kann ein Anwendungsprogramm z.B. das aktuelle Arbeitsverzeichnis über die Funktion getcwd() (get current working directory) erfragen und über die Funktion chdir() ändern. Home-Verzeichnis Zusätzlich zum aktuellen Arbeitsverzeichnis wird in Multi-User-Systemen für jeden Prozeß auch die Wurzel eines ausgezeichneten Dateiteilbaums gespeichert, in dem der jeweilige Benutzer primär seine Dateien ablegt. Dieses Home-Verzeichnis wird innerhalb eines Dateinamens
Home-Verzeichnis
9
Dateisysteme
meist durch das Symbol »~« charakterisiert. Ist zum Beispiel das Home-Verzeichnis des Benutzers dpunkt = /home/dpunkt, so bezieht sich die relative Pfadangabe -/buch/Systemsoftware auf die Datei/home/dpunkt/buch/systemsoftware. Abb. 9-6 Ausschnitt aus einem Dateisystem
Navigation über den Dateinamen
»..« bezeichnet das übergeordnete Verzeichnis (Vaterverzeichnis)
Viele Dateisysteme erlauben auch die Verwendung eines besonderen Operators in einem Dateinamen, der - unabhängig vom konkreten Namen - auf das übergeordnete Verzeichnis verweist. Meist wird diese positionsabhängige Benennung des übergeordneten Verzeichnisses durch die Zeichenfolge »..« charakterisiert. Dadurch ist ein Navigieren innerhalb der Verzeichnisstruktur insbesondere in relativen Pfadangaben möglich: Ausgehend von einer bestimmten Position im Pfadnamen geht man durch die konkrete Angabe eines Verzeichnisses eine Stufe nach unten, während man durch die Angabe von »..« in der Verzeichnishierarchie eine Stufe nach oben geht. Diese Navigation soll am Beispiel des in Abbildung 9-6 dargestellten Ausschnitts eines Dateisystems verdeutlicht werden. Wenn das aktuelle Arbeitsverzeichnis beispielsweise den Wert / c / d besitzt, dann verweist ../e
auf die Datei /c/e und ../../a
auf das Verzeichnis /a. Traversieren von Verzeichnisstrukturen in Programmen Das Dateisystem realisiert Verzeichnisse ebenfalls mit Hilfe von Dateien. Verzeichnisse werden jedoch besonders behandelt und können in der Regel nur vom Dateisystem selbst geändert werden. Anwendungsprogramme können ein Verzeichnis daher meist nicht wie eine
9.3
Verzeichnisse
normale Datei öffnen und anschließend durch Lese- und Schreiboperationen oder durch eine Speichereinblendung darauf zugreifen. Diese Einschränkung ist auch sinnvoll, da Prozessen die interne Struktur eines Verzeichnisses unbekannt ist und eine Offenlegung dieser Informationen die Anpassung und Änderung von Verzeichnisinformationen erschweren würde. Dateisysteme bieten daher zusätzliche Funktionen an, mit deren Hilfe Anwendungen auf Verzeichnisse und die Namen der enthaltenen Unterverzeichnisse und Dateien zugreifen können. In POSIX.Inkonformen Systemen findet dieser Zugriff z.B. über die Funktionen
• • •
Verzeichnisse können nicht wie normale Dateien geöffnet werden
POSIX
DIR *opendir ( char *pathname ) struct dirent *readdir ( DIR *dirp ) int closedir ( DIR *dirp )
statt. Ein Verzeichnis pathname wird durch den Aufruf der Funktion opendir() geöffnet. Rückgabewert dieser Funktion ist ein Zeiger auf eine DIR-Struktur, die mit jedem Aufruf von readdir() aktualisiert wird. Der interne Aufbau dieser Datenstruktur ist für den aufrufenden Prozeß irrelevant; sie speichert lediglich, welche Unterverzeichnisse und Dateien von der Funktion readdir() dem Prozeß gegenüber bereits aufgelistet wurden. Wesentlich ist dagegen der Rückgabewert aufeinanderfolgender Aufrufe von readdir(). Der zurückgegebene Zeiger verweist auf eine Datenstruktur dirent, die im wesentlichen den Namen einer Datei oder eines Unterverzeichnisses von pathname enthält: structdirent { char d_name[NAME_MAX+1]; }
Die Konkatenation von pathname, »/« und einem von readdir() zurückgegebenen Dateinamen bezeichnet damit ein Unterverzeichnis oder eine enthaltene Datei. Dateiattribute Zu jeder Datei und zu jedem Verzeichnis speichert das Dateisystem eine Reihe von Attributen. Zu diesen sogenannten Dateiattributen gehören u.a. Informationen über:
• • • • •
• •
Typ (Datei oder Verzeichnis) Speicherort (z.B. welche Platte und an welcher Position) Größe Anzahl der Links Erzeugungs- sowie letzter Zugriffs- und Änderungszeitpunkt Besitzrechte Zugriffsrechte
Dateiattribute speichern u.a. Typ, Größe, Besitzund Zugriffsrechte für eine Datei
9
Dateisysteme
Die Besitz- und Zugriffsrechte bestimmen, welche Prozesse und damit welche Benutzer auf eine Datei oder auf die in einem Verzeichnis enthaltenen Dateien und Unterverzeichnisse zugreifen dürfen.
9.4
Schichtenmodell
Ein Dateisystem kann logisch in drei Schichten unterteilt werden, die zunehmend höhere Abstraktionen für die Speicherung persistenter Daten anbieten. Im einzelnen handelt es sich - von unten nach oben um die Schichten: • • • Grundfunktionen der drei Dateisystemschichten
Datenträgerorganisation Blockorientiertes Dateisystem Dateiverwaltung
Die Datenträgerorganisation realisiert eine hardwareunabhängige Schnittstelle zu allen externen Speichern. Primär wird hier lediglich ein ganzer Datenträger wie z.B. eine Festplatte organisiert. Auf der Ebene des blockorientierten Dateisystems findet erstmals eine Aufteilung der Blöcke eines Datenträgers auf einzelne Dateien statt. Die Schicht Dateiverwaltung bildet dann den eigentlichen Zugang zum Dateisystem. Sie realisiert Verzeichnisse und ermöglicht damit die hierarchische Strukturierung externer Speicher. Eine zweite wichtige Aufgabe ist die Bereitstellung von Dateizugriffsfunktionen, die unabhängig vom Blockgranulat der tieferen Schichten und der verwendeten Datenträger sind. Schicht 1: Datenträgerorganisation Die wesentliche Aufgabe dieser Schicht ist die logische Durchnumerierung aller Blöcke eines Datenträgers (siehe Abbildung 9-7). Dadurch können die Blöcke eines externen Speichers unabhängig von der technischen Realisierung eines Datenträgers einheitlich von den höheren Schichten des Dateisystems adressiert werden.
Abb. 9-7 Aufsteigende Numerierung der Blöcke eines Datenträgers
Für die Numerierung der Blöcke eines Datenträgers wird meist eine ganze Zahl mit einer festen Anzahl an Bits verwendet. Bei n Bits ergibt sich damit eine maximale Blocknummer 2 n -l und in Verbindung mit der Blockgröße eine maximale Größe für jeden Datenträger. Externe Speicher mit einer höheren Gesamtkapazität können nicht mehr als
9.4
Schichtenmodell
Einheit verwaltet werden. In diesem Fall ist eine Partitionierung notwendig, bei der ein Datenträger in mehrere logische Datenträger unterteilt wird, die jeweils die maximal vom Dateisystem unterstützte Datenträgergröße nicht überschreiten. Das auf MS-DOS aufbauende Dateisystem FAT, das auch in der Ursprungsversion von Windows 95 eingesetzt wird, verwendet lediglich maximal 16 Bit für eine Blocknummer. Bei einer ursprünglichen Blockgröße von 512 Byte können damit externe Speicher mit bis zu 32 MByte Speicherkapazität angesprochen werden. Aufgrund der Fixierung der internen Datenstrukturen auf 16-Bit-Blockgröße mußten in Windows 95 für den Einsatz großer Platten mehrere Blöcke zu sogenannten Clustern zusammengefaßt werden, die jeweils über eine logische 16-Bit-Blocknummer angesprochen werden. Die Clustergröße wächst mit der Größe des Datenträgers und erreicht einen Wert von 32 KByte zur Adressierung eines 2047 MByte großen Datenträgers. Da ein Cluster die kleinste Zuteilungseinheit im Dateisystem von Windows 95 darstellt, entsteht bei den heute gängigen Plattengrößen eine nicht zu unterschätzende interne Fragmentierung, da selbst eine Datei mit nur einem Byte Dateiinhalt immer ein 32768 Byte großes Cluster belegt. Erst die inoffizielle Nachfolgeversion Windows 95B und damit auch Windows 98 führten eine mit der Original-DÖS-FAT inkompatible FAT32 ein, die 32 Bit große Blocknummern besitzt. Bei einer minimalen Clustergröße von 4 KByte können dadurch Datenträger mit einer Kapazität von mehreren TByte adressiert werden. Aufbauend auf der Blocknumerierung stellt diese Schicht des Dateisystems Lese- und Schreiboperationen zur Verfügung. Darüber hinaus kann sie die Formatierung eines Datenträgers durchführen oder ggf. koordinieren. Im wesentlichen werden bei dieser Formatierung zusätzliche Datenstrukturen initialisiert, die eine effiziente Verwaltung der freien und defekten Blöcke eines Datenträgers (Bad Blocks) ermöglichen. Letztere sind praktisch auf jedem größeren Datenträger vorhanden. Im Laufe des Betriebs können aufgrund von Verschleißerscheinungen oder einer fehlerhaften Oberfläche weitere defekte Blöcke dazukommen. Das Dateisystem verwaltet diese Liste der defekten Blöcke, um jeden weiteren Zugriff darauf zu verhindern. In einer gängigen Realisierung werden freie und defekte Blöcke in zusammenhängenden Bitvektoren auf dem Datenträger selbst gespeichert (siehe Abbildung 9-8). Diese Realisierung erlaubt den gleichzeitigen Test von 16, 32 oder 64 Bitpositionen mit Hilfe der Logikoperatoren des Prozessors.
Nur 216 Blocknummern bei DOS-FAT
Verwaltung freier und defekter Blöcke
9
Dateisysteme
Abb. 9-8 Verwaltung der freien und defekten Blöcke
Superblock
Insgesamt ergibt sich eine Organisation für einen Datenträger wie sie in Abbildung 9-9 dargestellt ist. Ein sogenannter Superblock verwaltet alle essentiellen Informationen über den Datenträgeraufbau. Dazu gehört die Größe des Datenträgers, die Blockgröße sowie die Positionen der Bitvektoren für die freien und defekten Blöcke. Aus Sicherheitsgründen wird der Superblock mehrfach, verteilt über den gesamten Datenträger, repliziert.
Abb. 9-9 Organisation des Datenträgers
Schicht 2: Blockorientiertes Dateisystem Blockorientierte Dateien
Abb. 9-10 Zusammenhängende und verteilte Belegung
Diese zweite Schicht teilt den vorhandenen Speicherplatz eines logisch durchnumierten Datenträgers auf mehrere Dateien auf. In dieser Ebene besitzen Dateien jedoch nur einen internen Namen. Außerdem existieren keine hierarchischen Verzeichnisstrukturen, d.h., alle Dateien sind in einer flachen Struktur unmittelbar über den internen Dateinamen ansprechbar. Jede Datei besteht aus einer Menge von Blökken. Die Blöcke werden relativ zum Dateianfang numeriert. In der Regel geschieht bereits auf dieser Ebene die Verwaltung der meisten Dateiattribute wie Position auf dem physischen Datenträger, Größe, Besitz- und Zugriffsrecht sowie Erzeugungs-, Zugriffs- und Änderungszeitpunkte.
9.4
Schichtenmodell
Das blockorientierte Dateisystem bietet im wesentlichen folgende Funktionen auf Dateien an:
• • • •
Erzeugen und Löschen Vergrößern und Verkleinern Öffnen und Schließen Lesen und Schreiben von Blöcken
Bei der Plazierung aufeinander folgender Dateiblöcke kann meist zwischen einer zusammenhängenden Belegung (Clustering oder Koallokation) und einer verteilten Belegung gewählt werden (siehe Abbildung 9-10). Die zusammenhängende Belegung setzt einen entsprechend großen zusammenhängenden Bereich auf dem Datenträger voraus. Dabei wird angenommen, daß die Zugriffszeiten auf logisch aufeinanderfolgende Blöcke eines Datenträgers verhältnismäßig schnell durchgeführt werden können. Nachteilig an einer ausschließlich zusammenhängenden Belegung ist eine u.U. hohe externe Fragmentierung und der damit verbundene hohe Verwaltungsaufwand. Außerdem ist ein Dateiwachstum zeitaufwendig durch ein Verschieben nachfolgender Dateien realisierbar. Bei einer verteilten Belegung wird eine externe Fragmentierung verhindert. Die interne Fragmentierung hängt in beiden Fällen von der Blockgröße ab; abgesehen von Problemfällen wie Windows 9x ist diese jedoch meist moderat. Die verteilte Belegung hat außerdem den Vorteil, daß eine beliebige Vergrößerung und Verkleinerung von Dateien ohne nennenswerten Aufwand durchgeführt werden kann. Nachteilig an dieser Belegungsform ist ein prinzipiell höherer Verwaltungsaufwand. Im Vergleich zum Clustering sind außerdem die Zugriffszeiten auf die einzelnen Blöcke z.T. höher. In modernen Dateisystemen wird aus diesen Gründen meist eine Kombination beider Verfahren eingesetzt. Dabei wird standardmäßig von einer verteilten Belegung ausgegangen. Bei Dateien mit zeitkritischem Zugriff kann eine Anwendung im Einzelfall explizit eine zusammenhängende Belegung der Blöcke vorschlagen oder fordern. In manchen Systemen wie z.B. Windows 9x kann durch die explizite Durchführung einer sogenannten Defragmentierung eine zusammenhängende Speicherung aller Blöcke jeder Datei eines Datenträgers erreicht werden. Je nach Größe des Datenträgers ist diese Defragmentierung u.U. sehr zeitaufwendig, da ein Großteil der Blöcke einer Platte gelesen und an anderer Stelle wieder zurückgeschrieben wird.
Logisch aufeinanderfolgende Blöcke physisch aufeinanderfolgend ablegen (Clustering)
Fragmentierung
Defragmentierung
Abb. 9-11 Aufbau eines Datenträgers aus Sicht des blockorientierten Dateisystems
9
Dateisysteme
Insgesamt entsteht in dieser Schicht des Dateisystems die in Abbildung 9-11 dargestellte logische Struktur eines Datenträgers. In der Regel wird eine bestimmte Menge an sogenannten Dateideskriptoren reserviert, die alle relevanten Dateiattribute speichern. Außerdem verweisen die Deskriptoren auf zusätzliche Datenstrukturen, die eine Abbildung von einer logischen Blocknummer innerhalb der Datei auf die entsprechende Blocknummer des Datenträgers ermöglichen. Die Wurzel enthält alle notwendigen Verwaltungsinformationen, wie z.B. die reservierte Anzahl an Deskriptoren sowie Position der Deskriptortabelle. Sie ist meist in den Superblock integriert. Schicht 3: Dateiverwaltung Aufbauend auf einer blockorientierten Realisierung von Dateien wird in der dritten Schicht des Dateisystems zwischen verschiedenen Dateitypen unterschieden. Von entscheidender Bedeutung ist dabei die Bereitstellung von Verzeichnissen zur hierarchischen Organisation des Datenträgers. Dateien werden auf dieser Ebene über Namen identifiziert. Das Zugriffsgranulat wird von der Anwendung festgelegt. Die Abbildung auf blockbasierte Lese- und Schreiboperationen übernimmt die Dateiverwaltung. Insgesamt realisiert diese Schicht die in den Abschnitten 9.2 und 9.3 beschriebene Funktionalität.
9.5
Realisierungsaspekte
Anhand einiger Beispiele für Dateisysteme werden in diesem Abschnitt verschiedene Realisierungsaspekte und Implementierungsvarianten diskutiert. Beispiel: Klassische UNIX-Dateisysteme Der Aufbau eines Datenträgers unter UNIX entspricht dem in Abschnitt 9.4 vorgestellten Schichtenmodell. Durch die explizite Separation in aufeinander aufbauende Stufen ist ein Dateisystementwickler in der Lage, neuartige externe Speicher transparent und mit verhältnismäßig wenig Aufwand in ein bestehendes Dateisystem zu integrieren. Die Realisierung von CD-ROM- und Netzwerk-Dateisystemen konnte davon unmittelbar profitieren.
9.5
Realisierungsaspekte
Abb. 9-12 Organisation einer Festplatte unter UNIX
Eine für UNIX-Systeme typische Organisation von Festplatten, dem mit Abstand am häufigsten verwendeten externen Speicher, ist in Abbildung 9-12 dargestellt. Den in Abschnitt 10.4 vorgestellten Dateideskriptoren entsprechen in UNIX die sogenannten I-Nodes, die alle relevanten Dateiattribute einschließlich einer Indexstruktur für die Lokalisierung der einzelnen Dateiblöcke enthalten. Diese Blockverkettung wird in Abbildung 9-13 noch einmal detaillierter wiedergegeben. Die Position der ersten 10 Blöcke einer Datei werden direkt im Deskriptor gespeichert. Dadurch ist ein unmittelbarer und effizienter Zugriff auf kurze Dateien oder die ersten Blöcke einer größeren Datei möglich. Für größere Dateien wird ein eigener Block für die Speicherung weiterer Verweise reserviert. Auf diesen Indexblock zeigt der einfach indirekte Zeiger im Deskriptor. Bei einer Blockgröße von z.B. 1 KByte und einer 32 Bit großen Blocknummer können in einem solchen Indexblock Verweise auf 256 weitere Dateiblöcke gespeichert werden. Insgesamt sind damit Dateien mit maximal 266 KByte Größe darstellbar (10 KByte für die ersten 10 Blöcke und 256 Blöcke zu 1 KByte über den ersten Indexblock). Für noch größere Dateien ist eine zweifach indirekte Organisation notwendig, d.h., ein Indexblock verweist auf maximal 256 weitere Indexblöcke, die selbst auf jeweils 256 Dateiblöcke zeigen. Bleibt man bei 1 KByte Blockgröße und 32 Bit für die Blocknummer, so können Dateien bis 65,5 MByte indiziert werden. Mit Hilfe einer dreifachen Verkettung können sogar Dateien bis maximal 16 GByte dargestellt werden.
I-Nodes
Schneller Zugriff auf kleine Dateien
9
Dateisysteme
Abb. 9-13 Mehrfach indirekte Blockverkettung einer UNIX-Datei
Logbasierte Dateisysteme
Erhöhung der Übertragungsbandbreite durch zeitverschränkte Ansteuerung mehrerer Platten RAID
Striping
In klassischen Dateisystemen müssen sich Dateien meist vollständig auf einem Datenträger befinden. Dadurch ergeben sich Beschränkungen bei der maximalen Dateigröße sowie bei der maximalen Übertragungsbandbreite zwischen Datenträger und Hauptspeicher. Um diese Größen- und Kapazitätsgrenzen zum Beispiel für das Aufzeichnen digitaler Videos mit hoher Aufzeichnung aufzuheben, werden zunehmend mehrere Platten parallel verwendet. Auf die einzelnen Platten kann über entsprechend leistungsfähige Bussysteme zeitverschränkt zugegriffen werden. Neben der Erhöhung des Durchsatzes kann damit auch eine höhere Verfügbarkeit erzielt werden; vorausgesetzt es wird genügend redundante Information auf den verschiedenen Platten gespeichert, um verlorengegangene Dateiinformationen wieder zu restaurieren. Auf der Hardwareebene hat sich in diesem Bereich der sogenannte RAID-Ansatz [Redundant Array of Independent Disks) durchgesetzt [Patterson et al. 1987]. RAID-Systeme erscheinen gegenüber dem Dateisystem wie eine herkömmliche Platte; lediglich Speicherkapazität, Übertragungsbandbreite und Verfügbarkeit dieser »Platte« sind außergewöhnlich hoch. Es werden 6 sogenannte RAID-Levels unterschieden:
•
RAID-0: Zur Steigerung der Übertragungsbandbreite werden Dateien auf mehrere Platten verteilt (Striping)
•
RAID-1: Spiegelung der Daten; Erhöhung der Ausfallsicherheit; keine Verbesserung bzgl. der Speicher- und Übertragungskapazität
•
RAID-2: RAID-1 mit einfacher Fehlererkennung
9.5
•
Realisierungsaspekte
RAID-3 bis RAID-5: Verteilte Speicherung der Daten und der Redundanzinformation
Auch auf der Ebene der Dateisysteme kann diese verteilte Form der Datenspeicherung über mehrere unabhängige Platten umgesetzt werden. So bietet z. B. Windows NT 4.0 in seinem Dateisystem NTFS [Solomon 1998] die Möglichkeit, Daten eines Datenträgers auf einem zweiten zu spiegeln (Mirroring). Die erhöhte Ausfallsicherheit geht dabei auf Kosten der Systemleistung, da jeder Schreibauftrag an beide Platten gesendet werden muß. Wenn die Systemarchitektur an dieser Stelle genügend Parallelarbeit zuläßt, z.B. weil die beiden Platten über zwei verschiedene Busse mit dem Hauptspeicher verbunden sind, ist der resultierende Overhead jedoch meist moderat. Single-Level-Store Die enge Verzahnung von Dateisystem und virtuellem Speicher bildet auch die Grundlage für eine weitere, neuartige Sichtweise auf die persistente Datenhaltung. In diesem Konzept bildet die Gesamtheit aller an einen Rechner angeschlossenen externen Speicher (u.U. ergänzt um den Speicher von weiteren Rechern, die über ein Kommunikationsnetzwerk erreichbar sind) den eigentlichen »Hauptspeicher« des Systems, dem Single-Level-Store. LI- und L2-Cache sowie der interne Hauptspeicher des Systems bilden lediglich Caches, die auf der Grundlage einer hohen Trefferrate die hohen Zugriffszeiten auf externen Speicher kompensieren sollen. Im wesentlichen wird hier die Integration von Ein- und Ausblendtechniken in die restliche Systemsoftware weiter vorangetrieben. Dabei wird jedem Speicherobjekt ein fester Adreßbereich in einem großen virtuellen Adreßraum zugeordnet, der unterstützt durch zusätzliche Schutztechniken - von allen Anwendungen gemeinsam genutzt wird. Aus Effizienzgründen kann die Lebensdauer einzelner Speicherobjekte weiterhin beschränkt und z.B. mit der Programmlaufzeit verknüpft werden. Ein wesentlicher Vorteil des Single-Level-Store liegt in der Bereitstellung eines einzigen Funktionssatzes für den Zugriff auf persistente und nichtpersistente Daten. Demgegenüber bietet die klassische Systemsoftware zwei sehr unterschiedliche Funktionssätze für den Zugriff auf persistente (einfache Lese- und Schreiboperationen) und nichtpersistente Daten (besondere Dateioperationen) an. Außer in speziellen Forschungssystemen [Chase et al. 1994] hat dieses Konzept für ein integriertes Dateisystem jedoch noch keine große Verbreitung gefunden.
Hauptspeicher ist der Cache des externen Speichers
10 Ein-und Ausgabe
In Kapitel 2 wurde die Ein- und Ausgabe als Teil der Rechnerarchitektur betrachtet und der Zugang zu peripheren Geräten an der Hardwareschnittstelle diskutiert. Anwendungen benutzen periphere Geräte jedoch üblicherweise nicht direkt, sondern wenden sich zu diesem Zweck an die Systemsoftware. Sie leistet die folgende Unterstützung bei der Gerätenutzung:
• • • • •
Abstrahierung von Gerätedetails Kapselung von sporadischen Gerätefehlern Koordinierung der Gerätezugriffe bei konkurrierenden Anwendungen Schutz von Standardgeräten wie Druckern und Platten vor unerlaubten Zugriffen Virtualisierung physischer Geräte durch Speicher- und Zeitmultiplex
Die Abstrahierung von Gerätedetails wie physischen Adressen, Statusund Kontrollregistern etc. erhöht den Komfort in der Gerätenutzung und verbessert die Softwareportabilität durch deren weitgehende Geräteunabhängigkeit beim Austausch von Geräten. Gelegentliche Gerätefehler, die z.B. durch fehlerhafte Übertragungen zwischen Gerät und Hauptspeicher Zustandekommen, sollten durch die Systemsoftware nach Möglichkeit maskiert werden - z.B. durch Wiederholung der Übertragung. Greifen mehrere Anwendungen potentiell konkurrierend auf Geräte zu, ist eine Koordinierung der Zugriffe unverzichtbar. Grundsätzlich eignen sich hierfür die in Kapitel 6 eingeführten Synchronisationstechniken. Bei Standardgeräten wie Platten und Druckern ist in aller Regel der Zugriff nur über höherwertige Dienste wie Dateiverwaltung und Druckdienst zulässig. Das Unterlaufen der entsprechenden Schnittstellen in der Systemsoftware muß unterbunden werden. Durch Virtualisierung können die physisch bedingten Beschränkungen von Geräten teilweise aufgehoben werden. Ein Beispiel für Gerätevirtualisierung stellen Fenstersysteme dar, die aus einem physi-
Funktionen der Geräteverwaltung
10 Ein- und Ausgabe
Integration neuer Geräte
sehen Terminal n virtuelle Terminals durch Multiplexen von Bildschirm, Tastatur und Maus erzeugen. Der indirekte Zugriff zur Gerätewelt über Systemsoftware steht jedoch in Konflikt zu manchen Anwendungen, die mit eigenen Geräten kommunizieren müssen. So wird in Echtzeitanwendungen zur Steuerung technischer Prozesse die Standardperipherie um Spezialgeräte wie Sensoren, Aktuatoren und Timer erweitert, die unter voller Kontrolle der Anwendungssoftware stehen müssen. Gefragt sind deshalb Konzepte, die die nachträgliche Integration solcher Geräte in die Systemsoftware und ihre sichere Bedienung über entsprechende Schnittstellen unterstützen. Eine besondere Herausforderung stellt dabei die Integration neuer Geräte zur Laufzeit dar, ohne daß der Betrieb eingestellt werden muß. So darf in einem automatisierten Fertigungsbetrieb die Produktion nicht angehalten werden, nur weil ein neuer Roboter installiert wird. In Abschnitt 10.1 werden zunächst die grundlegenden Konzepte der betriebssystemgestützten Ein- und Ausgabe eingeführt. Konkrete Realisierungen müssen Rücksicht auf die zugrundeliegende E/AArchitektur des Rechners nehmen. Am Beispiel von UNIX und Windows 9x wird in Abschnitt 10.2 demonstriert, wie existierende Betriebssysteme die Ein- und Ausgabe unterstützen.
10.1 Konzepte Aus Anwendungssicht ist die Vorstellung attraktiv, daß jedes physische Gerät in der Systemsoftware spiegelbildlich durch ein abstraktes Gerät repräsentiert wird (siehe Abbildung 10-1). Abstrakte Geräte besitzen einen Zustand und eine gerätetypabhängige Anzahl von Operationen, mit denen das Gerät betrieben werden kann. Abb. 10-1 Abstrakte Geräte als Repräsentanten physischer Geräte
10.1
j£3
Konzepte
Systemintern werden abstrakte Geräte häufig durch einen Gerätekontrollblock (DCB = device control block) und einen Gerätetreiber realisiert (siehe Abbildung 10-2). Im DCB wird die gesamte Anschluß- und Zustandsinformation für das korrespondierende Gerät wie E/A-Portadressen, Interruptnummer, Betriebszustand etc. gespeichert. Der Gerätetreiber realisiert alle auf dem Gerät definierten Operationen und enthält auch die Routinen zur Behandlung der geräteabhängigen Unterbrechungen.
Gerätekontrollblock (DGB)
Abb. 10-2 Realisierung eines abstrakten Gerätes
Aus Anwendersicht sind an der Schnittstelle zur Systemsoftware wenigstens die folgenden Funktionen bereitzustellen: Deviceld = OpenDevice (Gerätename, Betriebsparameter) CloseDevice (Deviceld) Read (Deviceld,From,To,Länge) Write (Deviceld,From,To,Länge)
Durch OpenDevice() wird ein vorhandenes Gerät für die anschließende Nutzungsphase geöffnet. Über den zurückgereichten Parameter DeviceId wird der Gerätezugriff in nachfolgenden Lese-/Schreibvorgängen beschleunigt. Mit closeDevice() wird eine Nutzungsphase mit dem Gerät beendet. Das Gerät wird in einen Grundzustand versetzt und evtl. dynamisch angelegte Zustandsinformation freigegeben. Durch den Aufruf von Read() und Write() wird ein Datenblock vom Gerät in den Hauptspeicher gelesen bzw. vom Hauptspeicher auf das Gerät geschrieben. Welche Geräte durch Anwendungen ansprechbar sind, wird in der Regel während der Systemkonfigurierung entschieden. Durch eine parametergesteuerte Prozedur werden Treiber und Anschlußinformationen für alle Geräte ausgewählt, die dem System angehören sollen. Ein Einbringen neuer Geräte und Treiber bleibt Systemspezialisten vorbe-
Grundfunktionen
10 Ein- und Ausgabe
halten. Dieses Einbringen zusätzlicher Geräte zur Laufzeit - ggf. auch durch Anwender - könnte durch Erweiterung des oben dargestellten Konzepts um zwei weitere Funktionen erreicht werden. CreateDevice DeleteDevice
(Gerätename,Treiber,Anschlußinfo) (Gerätename)
Durch CreateDevice() wird ein neues Gerät definiert und für den späteren Betrieb der Treiber sowie die physische Anschlußinformation an die Systemsoftware übergeben. Mittels DeleteDevice() wird ein Gerät aus der Systemssoftware ausgetragen. Obwohl diese Erweiterung plausibel erscheint, ist sie in der Praxis äußerst problematisch, da in diesem Fall zusätzlicher Code in die Systemsoftware zur Laufzeit integriert wird (man spricht auch von function shipping), der fehlerhaft sein kann. Das Problem wird noch dadurch verschärft, daß Treiber häufig im privilegierten Modus ablaufen müssen, da sie direkt auf die Hardwareschnittstelle der Geräte zugreifen. Damit sind auch alle Hardwareschutzmechanismen wirkungslos. Fehler im Treiber können deshalb beliebigen Schaden in der Software anrichten. Es existieren zwei grundsätzlich verschiedene Ansätze zur Lösung des Problems. Universalgeräte
Universeller Treiber
Das Konzept der Universalgeräte geht auf Cheriton [Cheriton 1987] zurück und stellt den Versuch dar, eine generische Gerätearchitektur mit einem universellen Treiber zu definieren, der durch Parameterbelegung auf einen bestimmten Gerätetyp instanziiert werden kann. Die Menge aller durch Parameterbelegungen modellierbaren Geräteausprägungen definiert ein Spektrum, das im Idealfall alle realen Gerätetypen enthält. Cheritons Universalgeräte besitzen eine blockorientierte Speicherstruktur und können durch die folgenden Parameter spezialisiert werden: -
Gerätetyp /* Ein , Aus , EinAus */ Blocklänge AnzahlBlöcke Zugriffsart /* Random, Sequentiell, Stream */
Durch die Parameter Blocklänge und AnzahlBlöcke wird die Größe und Struktur des geräteinternen Speichers definiert. Mit dem Parameter Gerätetyp wird ein Universalgerät auf Eingabe (z.B. Tastatur), Ausgabe (z.B. Drucker) oder ein Ein- und Ausgabe fixiert. Der Parameter Zugriffsart dient dazu, die Zugriffsmöglichkeiten auf den geräteinternen Speicher einzuschränken. Die Random-Zugriffsart gestattet den Zugriff auf jeden Block über einen frei positionierbaren
10.1
Konzepte
Blockzeiger. Bei der Zugriffsart Sequentiell kann lediglich sequentiell auf die Blöcke zugegriffen werden, wobei der Blockzeiger im Initialzustand auf den Block 0 zeigt. Bei der Zugriffsart Stream besteht der geräteinterne Speicher aus einem Fenster, das mit jeder E/A-Operation in einer Richtung durch einen Ein- oder Ausgabestrom bewegt wird. Ein Rücksetzen des Speicherfensters auf einen früher gelesenen oder geschriebenen Block ist unmöglich. Offensichtlich lassen sich mit diesem generischen Gerätemodell klassische Geräte recht gut nachbilden. So wird durch die Parameterbelegung Gerätetyp = EinAus Blocklänge = 2 04 8 Byts Anzahl Blöcke = 1048576 Zugriffsart = Random
eine 2-GB-Platte modelliert. Auf Universalgeräte kann mittels der Operationen Open() Close() Read() Write() Position()
ähnlich wie auf Dateien zugegriffen werden, wobei durch Parameterbelegung vorgenommene Spezialisierungen Einschränkungen im Gebrauch der Operationen zur Folge haben können. So ergibt es keinen Sinn, auf eine Tastatur zu schreiben. Bei der Stream-orientierten Verarbeitung ist ferner die Positionierung des Blockzeigers nur in Vorwärtsrichtung möglich. Der Vorteil des Konzepts der Universalgeräte besteht darin, daß für alle durch das Modell unterstützten Geräteausprägungen ein universeller Gerätetreiber bereitgestellt werden kann. Die Integration von potentiell fehlerhaftem Treibercode in das Betriebssystem ist damit ausgeschlossen. Der Ansatz versagt jedoch, wenn das reale Gerät Eigenschaften aufweist, die in der generischen Gerätearchitektur nicht berücksichtigt wurden. Geräteserver Dem Konzept der Geräteserver liegt die Idee zugrunde, abstrakte Geräte in eigenständigen Adreßräumen auf Anwenderebene zu verwalten (siehe Abbildung 10-3). Damit der Treiber im Geräteserver ablauffähig wird, muß ihm durch die Systemsoftware das Zugriffsrecht auf die mit dem abstrakten Gerät assoziierte physische Geräteschnittstelle erteilt werden.
Eigenständiger Geräteserver
10
Ein-und Ausgabe
Abb. 10-3 Kapselung abstrakter Geräte in Geräteservern
Es sei angenommen, daß dafür die Funktion DevPtr = ConnectDevice(Geräteadr,Int_Adr)
Eintragen einer Interruptroutine
Zugriff auf E/A-Register
Interruptweiterleitung
zur Verfügung steht. Sie überprüft die Zugriffsberechtigung des Adreßraumes auf das Gerät, trägt die Adresse int_Adr einer Unterbrechungsroutine ein, die für die Behandlung der E/A-Interrupts zuständig ist, in eine interne Tabelle der Systemsoftware ein und reicht in DevPtr die Adresse desjenigen Speicherbereichs im Server-Adreßraum zurück, in den die Kontroll- und Statusregister des Geräts eingeblendet wurden. Durch Zugriff auf die Kontroll- und Statusregister kann das Gerät anschließend direkt angesprochen werden. E/A-Interrupts werden vom Kern in Signale an den zuständigen Geräteserver transformiert und lösen dort die Aktivierung der zugehörigen Interruptroutine aus (vgl. Kapitel 7). Die Schnittstellen zu Geräteservern sind so zu gestalten, daß sie von Hardwaredetails der Geräte, die sie einkapseln, weitgehend abstrahieren. Das Konzept der Geräteserver garantiert, daß
•
•
nur auf das Gerät zugegriffen werden kann, auf das der Geräteserver die Zugriffsberechtigung besitzt, und fehlerhafte Treiberprogramme durch ihre Kapselung in eigenständigen Adreßräumen keinen Schaden an anderen Systemsoftware-Komponenten anrichten können.
10.2
Einbettung der E/A in das Dateisystem
Geräteserver lassen sich besonders effizient in Rechnern mit virtueller Adressierung und einer speicherbasierten E/A-Schnittstelle implementieren (siehe E/A-Architekturvarianten, S. 16). Durch die Funktion ConnectDevice() werden die das Gerät repräsentierenden Kontrollund Statusregister in den Adreßraum des Geräteservers eingeblendet, verankert und der Cache für die betreffende Speicherseite ausgeschaltet. Der durch ConnectDevice() zurückgereichte Parameter ist der Zeiger auf diesen Speicherbereich, der anschließend direkt mit MoveInstruktionen bearbeitet werden kann. Zeitaufwendige Traps in den Kern werden damit völlig überflüssig. Bei Systemen mit dediziertem E/A-Bus läßt sich ein zeitraubender Umweg über den Kern nicht vermeiden, da nur dort die privilegierten E/A-Instruktionen ausgeführt werden dürfen. Dieser Umweg kann z.B. dadurch realisiert werden, daß DevPtr auf einen geschützten Bereich des Kerns verweist. Bei einem Zugriffsversuch durch den Geräteserver wird ein Schutzfehler ausgelöst, der die Regie an den Kern übergibt. Dort kann die Zugriffsberechtigung überprüft und die E/AOperation ausgeführt werden. Voraussetzung für dieses Verfahren ist jedoch ein vertrauenswürdiger Kern mit hinreichender Schutzinformation, so daß die Zugriffsberechtigung sicher durchgeführt werden kann (vgl. Kapitel 13).
Problematik dedizierter E/A-Busse
10.2 Einbettung der E/A in das Dateisystem Die Realisierung abstrakter Geräte und die Definition einer generischen Gerätearchitektur ist ein charakteristisches Merkmal für die meisten Betriebssysteme aus dem UNIX-Umfeld. Der Zugriff auf praktisch jedes E/A-Gerät findet dabei über die Funktionen des Dateisystems statt. Jedes physische und abstrakte Gerät wird durch einen Namen im Dateisystem repräsentiert. In vielen UNIX-Systemen werden beispielsweise alle Geräte unter dem Teilbaum /dev verwaltet. Der »Dateiname« charakterisiert den jeweiligen Typ von E/A-Gerät, wie z.B.:
• • •
• •
/dev/ttya: Physische serielle Schnittstelle /dev/ptty01 ... /dev/pttyXY: Abstrakte serielle Schnittstellen (die maximale laufende Nummer ist konfigurationsabhängig) /dev/fd0 ... /dev/fdX: Diskettenstationen /dev/sd0 ... /dev/sdX: Festplatten /dev/le0 ... /dev/lex: Netzkarten
Zusätzlich wird bei Geräten in UNIX-Systemen zwischen einem zeichenorientierten und einem blockorientierten Zugriff unterschieden.
Gerätezugriffe in UNIX erfolgen primär über das Dateisystem
10
Ein-und Ausgabe
Das Zugriffsgranulat ist dabei Teil der Dateiinformation. Entsprechend werden die Geräte im Dateisystem in Anlehnung an das Zugriffsgranulat auch als character special file oder block special file bezeichnet. Vor dem eigentlichen Zugriff auf ein Gerät muß dieses analog zu einer Datei geöffnet werden. Neben dem Namen des zu öffnenden Geräts wird dabei auch festgelegt, ob auf das Gerät lesend und/oder schreibend zugegriffen werden soll. Verfügt der öffnende Prozeß über die notwendigen Rechte, so kann er anschließend über die entsprechenden Lese- und Schreiboperationen darauf zugreifen. Für die Zeitdauer der Benutzung werden für diese »Dateien« meist die Besitzrechte des zugreifenden Prozesses eingetragen. Ein Gerät sollte nach dem Zugriff wieder geschlossen werden. Das Gerät wird damit wieder für andere Interessenten zugänglich. Die Nutzung eines Geräts auf dieser Ebene erfordert viel Sachkenntnis. Aus diesem Grund werden meist nur serielle oder parallele Schnittstellen unmittelbar durch Öffnen des entsprechenden Gerätenamens genutzt. Vielmehr greifen dedizierte Geräteserver auf die physischen E/A-Geräte zu. Diese Server stellen Anwendungen dann abstrakte Geräte zur Verfügung, die leichter zu handhaben sind als die physischen Gegenstücke.
10.3 Dedizierte Geräte-APIs
Spezielle Programmierschittstelien für einzelne Geräte
Höhere Performanz Beispiel 3D-Graphikkarten
In Microsoft-Betriebssystemen wird beginnend mit Windows 95 ein anderer Weg verfolgt. Hier existiert für praktisch jedes E/A-Gerät eine eigenständige Programmierschnittstelle. Diese Spezialisierung geht in Einzelfällen so weit, daß E/A-Controller gleichen Typs aber unterschiedlicher Hersteller jeweils eigene APIs zur Verfügung stellen. In der Regel wird jedoch für jeden Gerätetyp eine standardisierte minimale Programmierschnittstelle bereitgestellt. Dies ist z.B. beim Zugriff auf Disketten, Festplatten und CD-ROM-Laufwerken der Fall. Auch im Bereich der Graphikkarten wurden Standards wie z.B. VGA, SVGA, XVGA - die alle eine bestimmte Maximalauflösung, Farbtiefe und Wiederholfrequenz definieren - festgelegt, die von praktisch allen Graphikkarten verstanden werden. Im Windows-Umfeld wird in vielen Fällen jede spezielle Eigenschaft eines E/A-Controllers durch ein dediziertes API einer Anwendung unmittelbar zur Verfügung zu stellen. Das am häufigsten zitierte Argument ist dabei der Performanz-Aspekt, d.h., eine Anwendung, die diese Eigenschaft nutzt, wird schneller ausgeführt. Besonders offensichtlich ist das gegenwärtig im Bereich der sogenannten SD-Beschleunigung. Entsprechend ausgerüstete Graphikkarten besitzen zu-
10.3
Dedizierte Geräte-APIs
sätzliche Prozessoren für die schnelle Plazierung, Bewegung und Sichtbarkeitsberechnung von Flächenstücken (z.B. Dreiecke oder Polygone) im dreidimensionalen Raum. Zusätzlich können diese Flächen meist mit einem bestimmten Muster (Textur) bedeckt werden; ggf. sind auch Filtereffekte wie z.B. Nebel, die die Sichtbarkeit weiter entfernter 3D-Objekte beeinflussen, parallel dazu berechenbar. Gegenwärtig greifen besonders gerne Spiele auf die 3D-Eigenschaften einer Graphikkarte zurück, um eine ruckelfreie Bildwiederholfrequenz und eine ansprechende visuelle Darstellung zu erreichen. Für viele dieser Spiele gibt es sogenannte Patches, die an eine bestimmte 3D-Hardware z.B. den Voodoo-Graphikkarten der Firma 3dfx oder die Riva TNToder Geforce-Chipsätze von nVidia angepaßt sind. Microsoft versucht dieser immer spezialisierteren Bindung zwischen Anwendungen (insbesondere Spiele) und Hardware entgegenzuwirken. Damit trotz einer standardisierten Programmierschnittstelle alle in Form spezieller Hardware vorhandenen Funktionen mit minimalen Leistungseinbußen zugreifbar sind, wurden die sogenannten DirectX-Schnittstellen (direkter Zugriff) geschaffen [Bargen und Donelly 1998]. Mittlerweile gibt es Schnittstellen für Zeichenoperationen (DirectDraw), 3D-Operationen (Direct3D), Audio (DirectSound), Eingabegeräte wie z.B. Maus, Joystick oder sogenannte Force-Feedback-Joysticks (DirectInput) und die Koordination sogenannter Multiplayerspiele (DirectPlay). Jede dieser Programmierschnittstellen erhebt den Anspruch, alle relevanten Aspekte zu modellieren und zugreifbar zu machen. Entsprechend umfangreich sind die resultierenden Funktionssätze ausgefallen. In jeder DirectX-Komponente wird zu Beginn ermittelt, welche der bereitgestellten Funktionen direkt von einer im System vorhandenen Spezialhardware ausgeführt werden kann und welche Funktionen mangels dieser Unterstützung vom Hauptprozessor emuliert werden müssen.
DirectX
11 Schutz
Der Schutz gespeicherter Information vor Diebstahl, unerwünschter Manipulation und Verletzung der Vertraulichkeit ist ein zentrales Anliegen in allen Mehrbenutzersystemen. In Abschnitt 11.1 wird ein allgemeines Schutzkonzept auf Basis einer Schutzmatrix eingeführt. Beispielhaft wird in Abschnitt 11.2 das Schutzkonzept in UNIX erläutert und die Beziehung zur Schutzmatrix aufgezeigt.
11.1 Die Schutzmatrix Grundsätzlich sollte der Schutz so organisiert werden, daß dadurch der gewollte Austausch sowie die gemeinsame Nutzung von Programmen und Daten zwischen Nutzern nicht unnötig eingeschränkt werden. Diese Forderung verlangt nach einem flexiblen Schutzkonzept, das die differenzierte Austeilung von Zugriffsrechten auf alle durch Systemsoftware verwalteten Objekte unterstützt. Unter den zahlreichen in der Literatur vorgestellten Schutzkonzepten ([Landwehr 1981], [Graham und Denning 1972]) hat das von Lampson ([Lampson 1969], [Lampson 1973]) eingeführte Konzept der Schutzmatrix eine besondere Popularität erlangt. Die Schutzmatrix (siehe Abbildung 11-1) verknüpft Schutzdomänen (das sind die Zeilen der Matrix) mit allen zugreifbaren Objekten im System (das sind die Spalten der Matrix). In dem Kreuzungspunkt einer Domäne und eines Objektes können Zugriffsarten eingetragen sein. Sie definieren, in welcher Form auf dieses Objekt im Kontext der betreffenden Schutzdomäne zugegriffen werden darf. Damit repräsentiert die Schutzmatrix den globalen Schutzzustand eines Systems.
Schutzmatrix verknüpft Schutzdomänen mit Objekten
Zugriffsarten
11
Schutz
Abb. 11-1 Schutzmatrix nach Lampson
Subjekt = Prozeß in einer Schutzdomäne
Die Einbettung der Schutzmatrix in ein Schutzsystem ist in Abbildung 11-2 veranschaulicht. Ein wesentlicher Aspekt ist die Verknüpfung jedes Prozesses mit einer Schutzdomäne, durch die dessen Handlungsspielraum festgelegt wird. Ist P ein Prozeß und D eine Schutzdomäne, dann wird das Paar (P,D)=S als Subjekt bezeichnet. Subjekte sind demnach im Kontext einer Schutzdomäne agierende Prozesse. Jedes Subjekt wendet sich mit einem Zugriffswunsch (Di, y1, ak) an den Schutzmonitor, wobei Di eine Schutzdomäne, y1 ein Objekt und ak eine Zugriffsart auf das Objekt definiert. Der Schutzmonitor prüft anhand seiner intern gespeicherten Schutzmatrix, ob in der Zeile von Di ein Zugriffsrecht (y1, ak) existiert. Bei positivem Ausgang der Prüfung wird der Zugriff zugelassen, andernfalls wird ein Schutzalarm (protection violation) ausgelöst und der Zugriff unterdrückt.
Abb. 11-2 Schutzsystem auf der Basis der Schutzmatrix
Bei dem in Abbildung 11-2 dargestellten System wird unterstellt, daß • Subjekte die ihnen zugeordnete Schutzdomäne nicht selbständig ändern können,
11.1
• • •
Die Schutzmatrix
Subjekte in keinem Falle auf Objekte unter Umgehung des Schutzmonitors zugreifen können, der Schutzmonitor vertrauenswürdig ist, der Schutzmonitor in bezug auf den Schutz besondere Privilegien genießt, die ihn ermächtigen, die gewünschten Zugriffe auf die Objekte zu veranlassen.
Die Zuordnung einer Schutzdomäne zu jedem Prozeß geschieht in einer allen Zugriffsversuchen vorangeschalteten Authentisierung, in der die hinter einem Prozeß stehende Benutzeridentität abgeprüft wird. Zu diesem Zweck wendet sich jeder Prozeß an eine vertrauenswürdige Instanz - z.B. den Schutzmonitor -, die vertrauliche Informationen über alle Benutzer im System verwaltet, etwa in Form von Paßwörtern. Anonyme Prozesse, die noch ohne Schutzdomäne sind, übergeben dem Schutzmonitor Namen und Paßwort des Benutzers, in dessen Auftrag sie handeln. Erkennt der Schutzmonitor die Identität an, dann veranlaßt er den Kern, einen Verweis auf die zugeordnete Schutzdomäne im PCB des Prozesses zu speichern (siehe Abbildung 11-3). Danach kann der Prozeß fortgesetzt werden.
Authentisierung = Prüfung der Identität
Abb. 11-3 Authentisierung von Prozessen
Übergabe von Benutzername und Paßwort Verknüpfung des Prozesses P mit einer Schutzdomäne D Fortsetzung des Prozesses
Auch bei der Authentisierung wird die Sonderstellung des Schutzmonitors deutlich: Nur er ist befugt, durch Aufruf einer entsprechenden Kernfunktion die Zuordnung von Schutzdomänen zu Prozessen zu manifestieren bzw. zu ändern, gewöhnlichen Prozessen bleibt dieses Recht dagegen verwehrt. Damit wird auch die Vertrauenswürdigkeit des Kerns implizit unterstellt.
11
Schutz
Vereinfachend soll für alle nachfolgenden Ausführungen eine 1:1Beziehung zwischen Schutzdomänen und Benutzern angenommen werden, d.h., Schutzdomänen können eindeutig durch eine Userid identifiziert werden. Die meisten Schutzsysteme machen diese Einschränkung und ersparen sich damit komplexe Mechanismen zur Verwaltung der Schutzmatrix. Eine direkte Implementierung des Schutzsystems gemäß Abbildung 11-2 ist aus den folgenden Gründen nicht zweckmäßig: a) Die Schutzmatrix ist typischerweise dünn besetzt. b) Ein zentraler Schutzmonitor, über den sämtliche Zugriffe abgewickelt werden, stellt einen potentiellen Flaschenhals dar. c) In einer Client/Server-Architektur werden Objekte durch Server verwaltet, d.h., der Schutzmonitor hat in der Regel keinen direkten Zugriff auf die Objekte.
Capability-Listen
Zur Vermeidung des Problems a) wurde vorgeschlagen, die Schutzmatrix entweder zeilenweise oder spaltenweise zu speichern. Bei zeilenweiser Zusammenfassung entstehen Capability-Listen der Form Ci = {c1, c 2 ,..., cn}
Zugriffskontrollisten
wobei jedes ck = (yk, ak) ein Zugriffsrecht in der Domäne i darstellt. Capability-Listen sind aus logischer Sicht Prozessen mit einer bestimmten UserId zugeordnet. Die Speicherung von Capability-Listen in einer Client/Server-Architektur ist jedoch problematisch. Natürlicher ist die spaltenweise Zusammenfassung in Zugriffskontrollisten der Form Z 1 = {z l ,z 2 ,...,z n } wobei jedes zk = (dk, ak) ein Kontrollrecht über das Objekt 1 darstellt, Zugriffskontrollisten werden in natürlicher Weise in dem Server gespeichert, der das entsprechende Objekt beherbergt. Der Schutzmonitor von Abbildung 11-2 wird als verteilte Struktur gemäß Abbildung 11-4 in jedem Server realisiert. Unklar ist, wie bei dieser verteilten Struktur die Authentisierung der Prozesse geschieht. Sie kann nicht mehr durch die Schutzmonitore erfolgen, da sonst alle Server auch die Sonderprivilegien des ursprünglich zentralen Schutzmonitors erben müßten. Da die Server aber im allgemeinen nicht als vertrauenswürdig einzustufen sind, kann diese Konsequenz nicht akzeptiert werden.
11.1
Die Schutzmatrix
Abb. 11-4 Modifiziertes Schutzsystem in einer Client/ServerUmgebung
Eine Lösung des Problems kann in Anlehnung an UNIX erfolgen: Der vertrauenswürdige Kern erzeugt bei der Systeminitialisierung einen ersten Prozeß, den Urprozeß. Der Urprozeß erbt vom Kern die Vertrauenswürdigkeit und damit das Privileg, Kindprozessen bei deren Erzeugung eine Schutzdomäne in Form einer UserId zuzuordnen. Das Privileg der Vertrauenswürdigkeit wird auf einfache Weise durch eine für diesen Zweck reservierte Userld, z.B. Userld = 0, realisiert. Sie verkörpert den allmächtigen Benutzer - im UNIX-Jargon den Superuser. Der Urprozeß, der seine Userld an alle Kinder vergibt, erzeugt nun für jedes Terminal einen Login-Prozeß, der die Authentisierung nach folgendem Muster durchführt: PROCESS Login { LOOP { Benutzeraufforderung; Read (Benutzername,Paßwort); Userld = Paßwortsuche (Benutzername,Paßwort); if (UserId<>-1) { Create Process (Shell, Userld, . . . ); Wait();
} else Fehlerbehandlung;
} } In Abbildung 11-5 ist die Erzeugungshierarchie dargestellt. Sie zeigt, daß ab der Ebene der Shell-Prozesse der Sonderstatus der Prozesse aufhört, d.h., von Shell-Prozessen und deren Kindern können nur noch Nachfolger erzeugt werden, die im Kontext eines gewöhnlichen Benutzers ablaufen.
Urprozeß
Superuser
Der Prozeß wartet bis zur Terminierung des ShellProzesses
11 Schutz
Abb. 11-5 Prozeßerzeugungsbaum
Alle in Abbildung 11-5 dargestellten Login-Prozesse müssen prinzipiell auf die gesamte Paßwortliste zugreifen können. Diese Liste enthält Tupel der Form (Benutzername, Paßwort, Userld, Shell-Programm) und sollte auf einem persistenten Speichermedium abgelegt werden. Es ist deshalb ratsam, die Paßwortliste in einem Dateisystem zu speichern und so zu schützen, daß nur die privilegierten Prozesse darauf zugreifen können. Das Dateisystem muß dann allerdings selbst vertrauenswürdig sein. In UNIX ist die Voraussetzung erfüllt, da das Dateisystem Teil des vertrauenswürdigen UNIX-Kerns ist.
11.2 Schutz in UNIX Schutz basiert auf Dateien
Der Schutz in UNIX basiert auf dem Schutz von Dateien. Dieser Ansatz ist konsequent, da benutzergesteuerte Aktivitäten nur durch Dateizugriffe ausgelöst werden können. Grundlage des Dateischutzes in UNIX ist eine rudimentäre Form von Zugriffskontrollisten, die für jede Datei bei deren Erzeugung automatisch angelegt werden. Die Zugriffskontrollisten sind rudimentär, weil sie Zugriffsrechte lediglich nach drei Benutzerkategorien unterscheiden können:
• Besitzer einer Datei (groupId)
• •
Verwandten des Besitzers (groupId) alle anderen Prozesse
Die Verwandtschaft eines Prozesses mit dem Besitzer einer Datei wird durch eine Gruppenidentifikation (groupId) definiert. Sie ist wie die
11.2
Schutz in UNIX
ownerId Bestandteil jeder Dateibeschreibung. Prozesse sind fest mit einer bestimmten UserId verknüpft. Greift nun ein Prozeß auf eine Datei zu, mit dessen Besitzer er einer gemeinsamen Gruppe angehört, dann gelten die Zugriffsrechte der Gruppe. Für jede Datei sind nun drei Zugriffsrechte (Leserecht, Schreibrecht, Ausführungsrecht) pro Benutzerkategorie vorgesehen, die durch den Besitzer beliebig ein- oder ausgeschaltet werden können. Der Schutz jeder Datei kann damit auf 9 Schutzbits beschränkt werden (siehe Abbildung 11-6). Abb. 11-6 Schutzbits für UNIXDateien
r Leserecht (1 = ja, 0 = nein) w Schreibrecht (1 = ja, 0 = nein) x Ausführungsrecht (1 = ja, 0 = nein)
Beim Zugriff auf Verzeichnis-Dateien wurde die Semantik der Schutzbits leicht modifiziert, um das ungehinderte Navigieren durch einen UNIX-Dateibaum einzuschränken (siehe Abbildung 11-7). Abb. 11-7 Bedeutung der Schutzbits in UNIX
Das oben geschilderte Verfahren setzt voraus, daß jeder UNIX-Prozeß mit einer unverfälschbaren UserId verknüpft ist. Der durch den UNIX-Kern gestartete Urprozeß - auch als Init-Prozeß bezeichnet erhält die Identität des Superusers. Für ihn sind alle Schutzrechte ausgeschaltet. Er erzeugt für jedes Terminal einen Terminalprozeß. Durch Code-Überladen wandelt sich jeder Terminalprozeß in einen Authentisierungsprozeß, der die Login-Prozedur steuert. Bis zu diesem Zeitpunkt sind alle Terminalprozesse noch mit dem Superuser verknüpft und besitzen deshalb auch uneingeschränktes Zugriffsrecht auf alle
11 Schutz
SetUserId-Bit und SetGroupId-Bit
Dateien. Bei erfolgreicher Authentisierung überlädt sich der Terminalprozeß ein weiteres Mal mit der gültigen Shell und erhält eine neue UserId zugeordnet. Von diesem Zeitpunkt an greifen bei allen Dateizugriffen die UNIX-Schutzmechanismen. Bei der Ausführung fremder Programme (Ausführungsrecht = 1) entsteht sehr häufig das Problem, daß auf interne Dateien des Programmbesitzers zugegriffen werden muß, für die der Programmbenutzer aber keinerlei Zugriffsrechte besitzt. Zu diesem Zweck existieren neben den bereits erläuterten 9 Schutzbits zwei weitere, das SetUserId-Bit und das SetGroupId-Bit. Wird nun in einem UNIX-Prozeß ein fremdes Programm ausgeführt (Ausführungsrecht = 1), bei dem außerdem das SetUserId-Bit (SetGroupId-Bit) gesetzt ist, dann gelten für die Dauer der Programmausführung die Schutzrechte des Programmbesitzers (der Gruppe). Auf diese Weise ist der Zugriff auf ansonsten unzugängliche Dateien möglich. In UNIX wird von dem SetUserId-Bit häufig Gebrauch gemacht, z.B. beim Zugriff auf die Paßwortdatei, die im Besitz des Superusers ist und für alle Zugriffe fremder Benutzer gesperrt ist. Der indirekte Zugriff über das Programm passwd ist bei gesetztem SetUserId-Bit mit einem Wechsel in den Schutzkontext des Superuser gekoppelt, in dem der Zugriff zur Paßwortdatei erlaubt ist.
12 Zugang zur Systemsoftware
Die Funktionalität der Systemsoftware steht Anwendungsprogrammen und Benutzern zur Verfügung. Im Fall eines Anwendungsprogramms wird der Funktionsumfang durch eine Vielzahl an Programmierschnittstellen, den sogenannten APIs (Application Programmers Interface), definiert. Die Signatur der Funktionen, d.h. der Funktionsname, Anzahl und Typ der Parameter sowie der Typ eines Rückgabewertes, werden in entsprechenden Header-Dateien zusammengefaßt. Auf diese Header-Dateien greift der Programmierer im Verlauf der Anwendungsentwicklung zurück. Die Header-Dateien werden anschließend zusammen mit dem Anwendungsprogramm übersetzt (bei den Programmiersprachen C und C++ mittels der Directive #include), damit der syntaktisch korrekte Aufruf der Funktion überprüft werden kann. Die Funktionen selbst stehen dem Anwendungsprogramm zur Laufzeit in Form von Bibliotheken zur Verfügung. Im Gegensatz zu Anwendungsprogrammen wird Benutzern der Zugang zu einem Betriebssystem durch eigens dafür geschaffene Programme ermöglicht, die aus historischen Gründen häufig auch als Shell bezeichnet werden. Eine Shell »umhüllt« den Betriebssystemkern und definiert damit für den Benutzer eine Bedienungsschnittstelle der Systemsoftware. Der Zugang selbst wird über die vorhandenen Einund Ausgabegeräte hergestellt: traditionell sind dies Tastatur, Maus und Bildschirm. In modernen Systemen gewinnen jedoch auch andere Geräte wie Mikrophon (sprachbasierte Anweisungen an das Betriebssystem), Lautsprecher (Ausgabe von Sprach- und Toninformation) oder z.B. auch sogenannte Datenhandschuhe (3-dimensionale Eingabe) eine wachsende Bedeutung als Teil der Bedienungsschnittstelle. Man unterscheidet bei einer Shell analog zu jedem anderen Anwendungsprogramm zwischen einer textuellen und einer graphischen Ein- und Ausgabeschnittstelle. Bei einer textuellen Shell bildet Text die Grundlage für die Interaktion zwischen Benutzer und Betriebssystem. Der Benutzer gibt Kommandos in textueller Form über die Tastatur ein, und das Betriebssystem antwortet in der Regel durch eine Textausgabe auf dem Bildschirm. Aufgrund fehlender Hardwareunterstützung waren textuelle Shells lange Zeit die einzig mögliche Bedienungsschnittstelle der Systemsoftware. Ein typisches Beispiel für die Ver-
Zugang für Programme über API
Zugang für Benutzer über Shell
Textuelle Shell
12
Zugang zur Systemsoftware
wendung einer textuellen Shell ist in Abbildung 12-1 dargestellt. Nach Eingabe des Kommandos »ls -l« gibt hier die im UNIX-Umfeld verbreitete Korn-Shell Informationen für alle Dateien im Verzeichnis »F:/The Books/Part l« aus. Angezeigt werden neben dem Dateinamen Zugriffsrechte, Größe und Datum der letzten Änderung. Abb. 12-1 Auflistung eines Verzeichnisses mit Hilfe einer textuellen Shell
Graphische Shell
Abb. 12-2 Verzeichnisstruktur in einer graphischen Shell
Graphische Shells stellen mittlerweile eine gängige Erweiterung und Aufwertung der Bedienungsschnittstelle moderner Systemsoftware dar. In der Regel erlauben sie weiterhin den Systemzugang über eine konventionelle textuelle Shell. Darüber hinaus werden aber wesentliche Teile der Bedienungsschnittstelle auch graphisch dargestellt. Beispielsweise gibt Abbildung 12-2 die in Abbildung 12-1 textuell dargestellte Verzeichnisstruktur in der graphischen Shell von WindowsBetriebssystemen wieder. In dieser Darstellungsform sind lediglich die Dateinamen und Unterverzeichnisse abgebildet; der Dateityp wird durch ein spezifisches Icon angezeigt.
12
Zugang zur Systemsoftware
Kommandos können in einer graphischen Shell häufig durch eine scheinbar direkte Manipulation der dargestellten Objekte z.B. durch einen Mausklick oder durch eine sogenannte Drag&Drop-Operation angestoßen werden. Ein verbreitetes Beispiel für eine solche Drag&Drop-Operation ist das Löschen einer Datei, indem ein die Datei repräsentierendes Graphiksymbol (Icon) mittels der Maus auf einen Mülleimer (z.B. in Abbildung 12-2 rechts unten) gezogen (Drag) und dort losgelassen (Drop) wird. Textuelle und graphische Shells ergänzen sich gegenseitig. Textuelle Zugänge erlauben in vielen Fällen eine gegenüber graphischen Shells einfachere Automatisierung wiederkehrender Abläufe. Außerdem können Kommandofolgen in vielen Fällen kompakter, d.h. mit weniger Eingaben, formuliert werden. Dabei muß man jedoch voraussetzen, daß der Benutzer die Kommandos und deren Syntax hinreichend gut beherrscht. Da jede Shell häufig ihre eigene Kommandosyntax definiert und diese in vielen Fällen wenig intuitiv ist, ist dies jedoch auch der am häufigsten aufgezählte Nachteil der textuellen Shells: Der Benutzer muß einen hohen Voraufwand zum Erlernen und Beherrschen der jeweiligen Kommandosyntax investieren. Bei graphischen Shells ist dagegen die Bedeutung vieler Drag&Drop-Operationen intuitiv klar; der Anwender kann in diesem Fall viele Kommandos direkt und ohne Einhaltung einer vorgegebenen Syntax ausführen. In manchen Fällen führt die vom Systementwickler unterstellte Semantik einer Drag&Drop-Operation jedoch zu weit: So kann z.B. in der graphischen Shell der Macintosh-Betriebssysteme eine CD aus dem CD-Laufwerk entfernt werden, indem das CD-Icon in den Mülleimer gezogen wird. Die Schwierigkeit, für jedes Kommando eine sinnvolle »graphische Metapher« zu definieren, ist ein Nachteil dieser Realisierungsform einer Shell. Graphische Shells haben daher meist nicht die Mächtigkeit einer textuellen Shell. Ein weiterer Nachteil liegt darin, daß automatische Abläufe bei graphischen Shells im Vergleich zu den Möglichkeiten programmiersprachenähnlicher Skriptsprachen bei textuellen Shells nur schwer zu formulieren sind. Insgesamt sind textuelle Shells weniger gut als Bedienungsschnittstelle für Laien und »einfache« Endanwender eines Systems geeignet. Dieser Anwendergruppe wird der Systemzugang über graphische Shells wegen der in vielen Fällen intuitiven Semantik der Drag& Drop-Operationen eher zusagen. Experten und Systemadministratoren favorisieren dagegen aufgrund der Automatisierbarkeit und der Kompaktheit der Kommandos die textuelle Form des Systemzugangs. In diesem Kapitel soll ein Überblick über die notwendigen Funktionen einer Betriebssystem-Shell gegeben werden. Die Funktionen werden am Beispiel textueller und graphischer Shells für verschiedene
Drag&Drop
Vergleich textuelle und graphische Shell
9
Funktionsweise eines Dateipuffers
POSIX
Wahlfreier Zugriff
Dateisysteme
Die für den lesenden und schreibenden Zugriff notwendigen Informationen werden vom Dateisystem in einer eigenen Datenstruktur gespeichert, die jedem geöffneten Dateideskriptor zugeordnet wird (siehe Abbildung 9-2). Diese Datenstruktur enthält neben der Ortsinformation, die Aufschluß über den physischen Aufenthaltsort der Datei auf einem der externen Speichermedien gibt, einen Puffer zur Zwischenspeicherung von Daten. Der Dateipuffer stellt eine Kopie eines bestimmten Dateiausschnitts dar. Die Position dieser Kopie innerhalb der Datei speichert ein zusätzlicher Zeiger. Jede Lese- und Schreiboperation wird aus dem Puffer bedient, der immer einen den Dateizeiger umfassenden Ausschnitt der Datei enthält. Bevor der Dateizeiger bei der Ausführung einer dieser Operationen den gepufferten Bereich verläßt, schreibt das Dateisystem eventuell geänderte Daten in die Datei zurück und lädt anschließend den nächsten Dateiausschnitt in den Puffer. Übertragen werden dabei immer Vielfache der festgelegten Blockgröße. Ohne weiteres Zutun ergibt sich damit für die Anwendung ein sequentieller Dateizugriff. In POSIX. 1 kann der Dateizeiger durch die Funktion int lseek ( int fd, off_t offset, int whence )
auch explizit von der Anwendung gesetzt werden. Wie das zweite Argument of f s e t dabei in die Neuberechnung des Dateizeigers (dz) einfließt, hängt vom Argument whence ab:
• •
Win32
SEEK_SET: dz := offset (absolut) SEEK_CUR: dz := dz + offset (relativ)
Die Inhalte des Dateipuffers werden durch diese Operation in der Regel überschrieben. Dabei kann es zu den bereits angesprochenen Leistungseinbußen kommen, wenn der wahlfreie Zugriff auf die Datei überwiegt. Die Win32-Schnittstelle bietet für den Zugriff auf die Datei die Funktionen ReadFile() und WriteFile() an. Signatur und Semantik entsprechen im wesentlichen den POSIX.l-Funktionen. Das Win32-Gegenstück zur Funktion lseek() heißt SetFilePointer(). Auch bei dieser Funktion kann der Dateizeiger sowohl relativ als auch absolut gesetzt werden. Speichereinblendung von Dateien
Memory Mapped Files
Bei einer alternativen Form des Dateizugriffs wird eine Datei oder Teile davon in den virtuellen Adreßraum eines Prozesses eingeblendet (Memory Mapped Files). Nachdem der Inhalt der Datei dadurch im virtuellen Adreßraum sichtbar wird, kann über einfache Lese- und
12.1 Start neuer Prozesse
fault eine Datei a.out erzeugen. Das abschließende Argument c_prog.c benennt die zu übersetzende Datei. Die Shell liest die Kommandozeile als Ganzes ein und ermittelt die durch sogenannte »White Spaces« (u.a. Leerzeichen und Tabulatorzeichen) getrennten Optionen und Argumente. Diese werden von der Shell nach dem Aufruf der Funktion fork() bei der Überlagerung des neuen Adreßraums mit dem Code des auszuführenden Programms durch execve() an die Anwendung, in diesem Fall einem C-Compiler, übergeben: execve ( char *pfad, char **argumente, ...)
Die Anwendung selbst greift auf die Optionen und Argumente je nach Sprachumgebung unterschiedlich zu. In C implementierte Anwendungsprogramme definieren zu diesem Zweck zwei Parameter bei der Funktion main() : int main ( int arge, char **argv ) { }
Der erste Parameter argc spezifiziert die Anzahl an Optionen und Argumenten. Der zweite Parameter argv entspricht einem Array von Zeigern, die jeweils auf eine Option oder ein Argument in Form einer Zeichenkette verweisen. Die Anwendung kann in diesem Fall durch eine Feldindizierung argv[0] bis argv[argc-l] darauf zugreifen. Im obigen Beispiel sind dies die Argumente (argc = 5): argv[0] argv[l] argv[2] argv[3] argv[4]
= "cc" = "-g" = "-o" = "c_prog" = "c_prog.c"
Die Übergabe von Kommandozeilenoptionen und -argumenten findet in den Win32-basierten analog zur UNIX-Realisierung statt. Die von der Shell eingelesenen Parameter werden ebenfalls in Form eines Zeigerfeldes aufbereitet und bei der Erzeugung der neuen Anwendung mittels CreateProcess ( Pfad, Argumente, ...)
an die Anwendung übergeben. Während jedoch Kommandozeilenoptionen im UNIX-Bereich traditionell durch ein vorangestelltes Minuszeichen gekennzeichnet werden, nutzen die Betriebssysteme von Microsoft zu diesem Zweck das Zeichen »/« (z.B. die kompakte Auflistung aller Dateien eines Verzeichnisses mittels dir /p /w).
12 Zugang zur Systemsoftware
Environment-Variablen
PATH
Environment-Variablen bilden in vielen Systemen eine weitere Form der Informationsübergabe an Anwendungsprogramme. Der Benutzer kann innerhalb einer Shell eine neue Environment-Variable deklarieren (meist reicht die Initialisierung der Variablen) oder den Wert einer bereits vorhandenen Environment-Variablen verändern. Die Syntax der Variablendeklaration, Zuweisung und Änderung ist shell-spezifisch. Beispielsweise wird in UNIX die Environment-Variable PATH im Fall der Korn-Shell durch die Anweisung PATH="$PATH;/usr/bin"
um den Eintrag »/usr/bin« ergänzt. Dabei wird durch den Ausdruck $PATH in dieser Shell auf den vorherigen Wert der Variablen zugegriffen. Das Setzen von Environment-Variablen in Windows 9x ist dem sehr ähnlich, so wird z.B. eine Initialisierung der Pfadvariablen PATH mittels des Kommandos SET PATH=D:\MKSNT;D:\JDK\BIN;C:\WINDOWS
erreicht. Environment-Variablen bleiben bis zur Terminierung der Shell oder bis zu ihrer expliziten Löschung durch den Benutzer gültig. Anwendungsprogramme können mit Hilfe bestimmter Funktionen und unter Angabe des Variablennamens auf die für sie gültigen Environment-Variablen zugreifen. In der Regel können Environment-Variablen lediglich Zeichenketten sparen. Anwendungen, die mit Hilfe dieser Variablen auch andere Datentypen speichern wollen, müssen die notwendigen Konvertierungen selbst vornehmen. Jede Shell verknüpft mit bestimmten Environment-Variablen eine feste Semantik. So wird in vielen Fällen mittels der Variablen PATH festgelegt, in welchen Verzeichnissen eines Systems nach gültigen Programmen gesucht werden soll. Liegt z.B. ein C-Compiler in einem durch diese Variablen definierten Verzeichnis, reicht die Angabe des einfachen Namens für die Anwendungsausführung. In allen anderen Fällen muß neben dem Namen der Datei auch der absolute oder relative Pfad innerhalb des Dateisystems vom Benutzer explizit angegeben werden (z.B. /usr/bin/cc). Ein- und Ausgabeumlenkung
stdin, stdout und stderr
Die textbasierte Ein- und Ausgabe von Programmen kann in vielen Shells von Tastatur und Bildschirm umgelenkt werden. Dabei wird vorausgesetzt, daß die Anwendung Eingaben von einer sogenannten Standardeingabe (stdin) liest und Ausgaben auf einer entsprechenden Standardausgabe (stdout) ausgibt. Zusätzlich bieten die meisten
12.1 Start neuer Prozesse
Systeme eine zweite Ausgabemöglichkeit für den Fehlerfall (stderr). Anwendungsprogramme selbst greifen auf diese standardisierten Einund Ausgabekanäle meist wie auf normale Dateien zu, z.B. kann ein C-Programm durch den Aufruf der Funktion fprintf(stdout,"Hello, World\n");
die Zeichenkette »Hello, World« ausgeben (die Kurzform printf() gibt sogar implizit auf stdout aus). Die Ein- und Ausgabeumlenkung findet von einer oder auf eine Datei statt. Durch die Umlenkung der Eingabe liest ein Anwendungsprogramm Eingaben von einer Datei anstelle der Tastatur. Im Fall der Ausgabeumlenkung werden Programmausgaben in eine Datei geschrieben; sie erscheinen nicht auf dem Bildschirm. Ob eine Umlenkung der Ein- und/oder Ausgabe vorgenommen wurde, ist für das betroffene Anwendungsprogramm im allgemeinen nicht direkt erkennbar. Syntaktisch wird eine Umlenkung in den meisten Shells im Anschluß an den Aufruf des Programms (einschließlich der Optionen und Argumente) angegeben. So wird z.B. durch die Kommandozeile
E/A-Umlenkung von oder auf eine Datei
X < a
das Programm X mit einer Eingabeumlenkung gestartet. X erhält seine Eingaben von der Datei a, die Ausgaben finden weiterhin auf dem Bildschirm statt. Analog wird mit X > b
eine Ausgabeumlenkung vorgenommen. In diesem Fall werden die Ausgaben von x in die Datei b geschrieben. Die Eingabe findet über die Tastatur statt. Natürlich kann sowohl die Eingabe als auch die Ausgabe eines Programms umgelenkt werden: X < a > b
Pipes Auf die bereits in Kapitel 7 eingeführten Pipes kann in den meisten Shells unmittelbar zugegriffen werden. Sie bilden eine einfache, aber mächtige Verallgemeinerung der Ein- und Ausgabeumlenkung. Die Umlenkung findet dabei nicht zwischen Programm und Datei, sondern zwischen mehreren Programmen statt. So wird durch eine Pipe der Form X | Y
die Ausgabe von Programm X direkt in die Eingabe des zweiten Programms Y umgelenkt. Die Umsetzung der Pipe in einer Shell erfordert
E/A-Umlenkung zwischen Programmen
12
Zugang zur Systemsoftware
einen entsprechenden Kommunikationsmechanismus zwischen verschiedenen Adreßräumen. Ein einfacher unidirektionaler Zeichenstrom reicht jedoch zur Realisierung aus. UNIX-Shells setzen unmittelbar auf dem System-Call pipe() auf. Eine einfaches Beispiel für eine Pipe »X | Y« gibt nachfolgendes C-Code-Fragment wieder: Beispielumsetzung
int fd[2];
»X I Y« if (fork() = = 0) { pipe(fd); if (fork() > 0) { // Prozeß X dup2(fd[l],STDOUT_FILENO); execve ("X",...); } else { // Prozeß Y dup2(fd[0],STDIN_FILENO); execve ("Y",...); } }
Der skizzierte Code laufe im Kontrollfluß der Shell ab. Aus Gründen der Übersichtlichkeit wurde auf die Überprüfung von Fehlerfällen verzichtet. Durch den ersten Aufruf der Funktion fork() spaltet die Shell einen neuen Kontrollfluß ab, der das Programm X ausführen wird. Der zweite Aufruf von fork() bereitet einen weiteren Prozeß für die Ausführung von Y vor. Der System-Call pipe() erzeugt die Pipe; dabei kann über fd[0] lesend und fd[1] schreibend darauf zugegriffen werden (siehe auch Kapitel 7). In Prozeß X wird durch den Aufruf von dup2() der Dateideskriptor für stdout auf die zu beschreibende Seite der Pipe gesetzt; analog stdin in Y auf die zu lesende Seite. Anschließend wird in beiden Fällen der Adreßraum mit dem Code für X und Y mit der entsprechenden Programmdatei überlagert. Die Shell nutzt dabei die Eigenschaft des execve()-Aufrufs, daß geöffnete Dateideskriptoren weiterhin geöffnet bleiben. Einer direkten und transparenten Kommunikation zwischen X und Y steht damit nichts mehr im Weg. Die Mächtigkeit des Pipe-Mechanismus ist eine der wesentlichen Gründe für den großen Erfolg von UNIX, insbesondere bei Experten und Systemadministratoren. Durch die Kaskadierung vieler Einzelprogramme, die jedes für sich eine klar umrissene Funktion realisieren, können komplexe Funktion zusammengesetzt werden. Pipes können auch in Kombination mit der einfachen Ein- und Ausgabeumlenkung über Dateien verwendet werden. Es ist jedoch zu beachten, daß jedes Programm jeweils nur eine Standardeingabe und eine Standardausgabe besitzt. Kommandozeilen der Form X < a | Y < b
werden daher von jeder Shell mit einer Fehlermeldung abgewiesen.
12.2
Prozeßverwaltung
12.2 Prozeßverwaltung Aus dem Start neuer Prozesse ergibt sich für den Benutzer die Notwendigkeit, zu jedem Zeitpunkt die aktuell im System befindlichen Prozesse zu verwalten. Die Kontrolle über diese Prozesse ermöglicht ebenfalls die Shell. Welche Prozesse kontrollierbar sind, hängt u.a. vom Betriebssystemtyp ab. Bei sogenannten Single-User-Systemen wie z.B. Windows 9x können vom Benutzer in der Regel alle im System befindlichen Prozesse verwaltet werden. Bei Multi-User-Systemen ist die Verwaltung aus einsichtigen Gründen lediglich auf die dem jeweiligen Benutzer zugeordneten Prozesse beschränkt. Dabei werden Eigentümerinformationen und Besitzrechte analog zu Dateien auf Prozesse definiert und für jeden Prozeß im PCB gespeichert. Jeder vom Benutzer im Verlauf einer Sitzung erzeugte Prozeß wird mit dessen Benutzerkennung versehen. Technisch wird nach einer erfolgreichen Anmeldung, d.h. nach Eingabe einer gültigen.Nutzerkennung und einem korrekten Paßwort, eine sogenannte Login-Shell erzeugt, über die alle weiteren Prozesse des Anwenders im Verlauf der Sitzung gestartet werden. Diese Login-Shell überträgt automatisch die Eigentümerinformationen auf die erzeugten Folgeprozesse. Eine Veränderung dieser Informationen kann nur durch einen ausgezeichneten Superuser (Root) vorgenommen werden. Dieser Superuser kann auch in einem Multi-User-System in der Regel auf alle Prozesse im System Einfluß nehmen. Der Funktionsumfang der Prozeßverwaltung hängt auch davon ab, ob es sich um ein Single-Tasking- oder Multi-Tasking-System handelt. Bei einem Single-Tasking-System kann zu einem Zeitpunkt nur ein Programm ausgeführt werden. Die Prozeßverwaltung gestaltet sich entsprechend einfach, d.h., die Shell und ein vom Benutzer gestartetes Programm wechseln sich in der Prozessornutzung ab. Der Benutzer benötigt lediglich einen zusätzlichen Mechanismus, um Programme vorzeitig abzubrechen, z.B. um eine Endlosschleife zu beenden. Bei einem Multi-Tasking-System kann dagegen jeder Benutzer (bei einem Single-User/Multi-Tasking-System wie z.B. Windows 9x und Windows NT maximal ein Benutzer) mehrere Prozesse starten und verwalten.
Single- und Multi-UserSysteme
Single- und Multi-TaskingSysteme
Vorder- und Hintergrundbearbeitung Bei der Verwaltung mehrerer Prozesse muß vom Benutzer festgelegt werden, welcher Prozeß im Vordergrund bearbeitet werden soll, d.h. im Besitz der zentralen Ein- und Ausgabegeräte ist. Nur dieser Prozeß kann z.B. erfolgreich Eingaben von der Tastatur lesen und Ausgaben auf dem Bildschirm wiedergeben. Alle anderen, im Hintergrund aus-
Vordergrund
Hintergrund
12 Zugang zur Systemsoftware
geführten Prozesse, werden bei einem Zugriff auf die Standardeingabe und Standardausgabe blockiert. Primär wird der Bearbeitungsmodus durch den Benutzer beim Starten eines Prozesses festgelegt. Die Vordergrundverarbeitung ist in einer UNIX-Shell meist der Normalfall, d.h., eine Kommandozeile der Form CC -O X X.C
führt den C-Compiler im Vordergrund aus. Der Prozeß könnte auf Eingaben von der Tastatur warten. Ausgaben, wie z.B. Warnungen und Fehlermeldungen, erscheinen unmittelbar auf dem Bildschirm. Die Shell selbst ist bis zur Beendigung des Übersetzungslaufs blockiert. Eine Hintergrundbearbeitung wird - zumindest bei UNIX-Systemen - meist durch ein abschließendes »&«-Zeichen signalisiert: CC -O X X.C &
Die Shell bleibt weiterhin im Vordergrund, und der Benutzer kann erneut Eingaben tätigen. Parallel dazu wird der Compiler ausgeführt, solange dieser keine Ein- und Ausgabe verwendet. Jeder Versuch dieses Prozesses, auf die Standardeingabe oder -ausgäbe zuzugreifen, hat eine unmittelbare Blockierung zur Folge. Abb. 12-3 Bearbeitungsmodi eines Prozesses X
Job-Control
Der Bearbeitungsmodus kann vom Benutzer auch dynamisch geändert werden. Jede Multi-Tasking-fähige Shell bietet zu diesem Zweck neben der Auflistung der aktuell im Vorder- und Hintergrund ausgeführten Prozesse weitere Funktionen an. So kann jederzeit der aktuell im Vordergrund befindliche Prozeß blockiert und anschließend in den Hintergrund gebracht werden. Außerdem können im Hintergrund ausgeführte oder blockierte Prozesse in den Vordergrund versetzt werden und damit Zugang zu Standardein- und -ausgäbe erhalten. Am Beispiel der csh in UNIX werden in Abbildung 12-3 die verschiedenen Modi und die notwendigen Befehle dargestellt. Unterschieden werden die Zustände V (Vordergrund), Suspend (unterbrochener Vordergrundprozeß), H E / A (E/A-blockierte Hintergrundprozesse) und HRun (im Hintergrund ausgeführte Prozesse). Der Übergang von H E / A oder H R u n nach V wird durch die Eingabe %i er-
12.2 Prozeßverwaltung
reicht. Wobei i die laufende Nummer des gewünschten Prozesses in der Ausflistung der Hintergrundprozesse (Befehl jobs) ist. Im Vordergrund suspendierte Prozesse können durch die Eingabe des Kommandos fg erneut im Vordergrund oder durch die Eingabe von bg im Hintergrund fortgeführt werden. Prozeßterminierung
Vorder- und Hintergrundprozesse können vom Benutzer auch vorzeitig terminiert werden. Eine besondere Tastensequenz wie z.B. Control-C terminiert in einer UNIX-Shell den im Vordergrund ausgeführten Prozeß. Hintergrundprozesse werden mittels des UNIX-Befehls
Terminierungssignal in UNIX
kill Signal pid
terminiert. Dabei wird der vorhandene Signal-Mechanismus eingesetzt und dem zu terminierenden Prozeß über die Prozeßidentifikation pid z.B. ein nicht maskierbares Signal SIGKILL gesendet. Als Prozeßidentifikation kann auch %i angegeben werden. In diesem Fall wird der in der Auflistung aller Hintergrundaufträge mit i nummerierte Prozeß terminiert. In Windows-Betriebssystemen können Prozesse mit Hilfe des sogenannten Task Managers vorzeitig terminiert werden. Diesen Manager kann der Benutzer durch gleichzeitiges Drücken der Tasten Control+Alt+Delete aufrufen. Der gewünschte Prozeß wird anschließend mit der Maus ausgewählt und durch Drücken des entsprechenden Buttons terminiert.
Task Manager
Prozeßgruppen und Sessions Neben der Identifikation einzelner Prozesse erlaubt die moderne Systemsoftware auch die Zusammenfassung mehrerer Prozesse zu Prozeßgruppen und diese zu sogenannten Sessions. Dabei gelten meist einfache n:l-Beziehungen, d.h., jeder Prozeß ist Mitglied in genau einer Prozeßgruppe und jede Prozeßgruppe ist Teil genau einer Session. Alle verwaltungstechnischen Funktionen können sowohl auf einzelne Prozesse als auch auf Prozeßgruppen oder Sessions angewendet werden. Dadurch werden viele Verwaltungsaufgaben erheblich vereinfacht. So können in UNIX Signale auch an Prozeßgruppen versendet werden. Jede UNIX-Shell nutzt diese Funktionalität, um z.B. drei über zwei Pipes verknüpfte Prozesse als Einheit (Auftrag, Job) aufzufassen und zu verwalten. Entsprechende Befehle beim Wechsel zwischen Hintergrund und Vordergrund wirken sich dadurch auf alle Prozesse des Auftrags aus. Auch die vorzeitige Terminierung der gesamten Prozeßgruppe wird dadurch direkt unterstützt. Analog dazu erlauben Ses-
Zusammenfassung von Prozessen zu Gruppen und Sessions
12
Zugang zur Systemsoftware
sions die einfache Terminierung aller Prozesse eines Benutzers, wenn dieser das System verläßt (logout).
12.3 Zugang zum Dateisystem Ein Vielzahl an Shell-Funktionen und Dienstprogrammen erlaubt die Verwaltung von Dateien und Verzeichnissen durch den Benutzer. Dazu gehören Befehle zum Anlegen und Löschen von Dateien und Verzeichnissen, zum Kopieren oder Verschieben einzelner Dateien oder ganzer Teilbäume sowie Funktionen zum Ändern der Dateiattribute. Der Benutzer kann in allen Fällen absolute und in Beziehung zum Arbeits- und Home-Verzeichnis auch relative Dateinamen verwenden. Die konkrete Ausführbarkeit der Funktionen hängt von den Rechten des Benutzers ab. Ein Benutzer kann z.B. eine Datei nur löschen oder an einen anderen Ort kopieren, wenn die entsprechenden Rechte vorliegen.
Abb. 12-4 Ausschnitt einer komplexen Verzeichnisstruktur im Explorer von Windows 95
Die anschauliche Darstellung einer Verzeichnis- und Dateistruktur einschließlich der Navigation darin ist die entscheidende Stärke einer graphischen Shell. In Abbildung 12-2 wurde bereits der übersichtliche Aufbau eines Verzeichnisses dargestellt. Durch einen Doppelklick mit der Maus können weitere Unterverzeichnisse geöffnet werden. Durch besondere Icons können auch das aktuelle Arbeitsverzeichnis und Home-Verzeichnis angezeigt werden. Selbst komplexere Verzeichnisbäume lassen sich z. B. mittels einer z. B. im Explorer von Windows 9x eingesetzten Baumsicht (siehe Abbildung 12-4) noch übersichtlich wiedergeben. Ähnliche graphische Darstellungen gibt es mittlerweile
12.4
Batch- und Skript-Dateien
für alle modernen Betriebssysteme einschließlich UNIX (hier z. B. als Teil des CDE = Common Desktop Environment, einem Standard für die graphische Bedienungsoberfläche von UNIX-Systemen).
12.4 Batch- und Skript-Dateien Die meisten textuellen Shells erlauben auch die Automatisierung von Abläufen in Form von sogenannten Batch- oder Skript-Dateien. In diesen Dateien stehen ein oder mehrere Befehlszeilen, wie sie auch manuell vom Benutzer eingegeben werden können. Darüber hinaus kann der Kontrollfluß innerhalb einer Batch- und Skript-Datei in Analogie zu Programmiersprachen z.B. über bedingte Anweisungen und Schleifen verändert werden. Dabei kann auf Kommandozeilenoptionen, Argumente und zusätzliche Variablen zurückgegriffen werden. Die Operatoren und Funktionen sind im Gegensatz zu allgemeinen Programmiersprachen auf das Einsatzgebiet einer Shell zugeschnitten. So kann z.B. getestet werden, ob eine bestimmte Datei oder ein bestimmtes Verzeichnis vorhanden ist oder ob ein Zugriff nach einem bestimmten Zeitpunkt stattfand. Die Möglichkeiten der Batch- und Shell-Programmierung sind vielfältig. Aufgrund der unterschiedlichen Syntax ist jedoch eine längere Einarbeitung notwendig, bevor die Funktionen effektiv genutzt werden können. Darüber hinaus sind Shells an ein bestimmtes Betriebssystem gebunden, so daß Nutzer mehrerer Betriebssysteme häufig wechseln müssen. Zusätzlich zu den eingebauten Programmiermöglichkeiten einer Shell stehen seit einigen Jahren auch neue Programmiersprachen wie z.B. Perl [Wall et al. 1996] Python [Rossum 1993], oder Java [Gosling et al. 1996] zur Verfügung, die ebenfalls den Bereich der Batch- und Skript-Programmierung abdecken können. Diese Sprachen wurden einschließlich ihrer Funktionsbibliotheken mittlerweile auf verschiedene Betriebssysteme portiert, insbesondere existieren Implementierungen für alle gängigen UNIX-Systeme und Windows-Betriebssysteme.
Shell-Programmierung
13 Implementierungsaspekte für Systemsoftware
Aus Anwendersicht ist die Vorstellung einer Laufzeitplattform naheliegend, auf der alle Anwendungen abgewickelt werden. Sie bildet die für Anwendungen zugängliche Schnittstelle zu einer abstrakten Maschine, die alle zur Laufzeit benötigten Mechanismen enthält und damit Anwendungen einen komfortablen Zugang zur Rechnerhardware eröffnet (siehe Abbildung 13-1). Anwenderprogramme und abstrakte Maschine haben disjunkte, gegeneinander geschützte Adreßräume. Das Anfordern einer Leistung von der abstrakten Maschine durch Anwenderprogramme ist nur über einen Trap-Befehl möglich, der die Kontrolle an eine hardwaremäßig festgelegte Einsprungstelle innerhalb der abstrakten Maschine überträgt und dabei den Prozessor in den privilegierten Ausführungsmodus umschaltet.
Laufzeitplattform = abstrakte Maschine
Abb. 13-1 Laufzeitsystem als abstrakte Maschine
Die ersten allgemein verwendbaren Betriebssysteme, die Mitte der 50er Jahre entwickelt wurden, folgten exakt diesem Ansatz. Sie waren mit der in Abbildung 13-1 dargestellten abstrakten Maschine identisch. Man sprach auch von einem monolithischen Ansatz, da die interne Struktur der abstrakten Maschine von außen unsichtbar und unbeeinflußbar war; modulare Gliederungskonzepte steckten noch in den Kinderschuhen. Probleme mit diesem Ansatz entstehen dann, wenn die Plattform für eine Anwendung nicht die passenden Mechanismen anbietet. Die dann unvermeidlichen Änderungen oder Erweiterungen der abstrak-
Monolithische Systeme
13
Implementierungsaspekte für Systemsoftware
ten Maschine sind ohne Detailkenntnisse des internen Programmaufbaus schwierig und können in aller Regel nur durch Systemspezialisten vorgenommen werden. Einen Ausweg aus diesem Dilemma bieten Systeme mit einer Schichtenstruktur (siehe Abbildung 13-2). Abb. 13-2 Laufzeitsystem als Hierarchie abstrakter Maschinen
Geschichtete Systeme
Anstelle einer abstrakten Maschine wird die Laufzeitplattform für Anwendungen durch eine Hierarchie aufeinander aufbauender abstrakter Maschinen realisiert, wobei Anwendungen lediglich Zugriff zur obersten abstrakten Maschine haben. Die Vorteile geschichteter Systeme sind in mehreren experimentellen Betriebssystementwicklungen ([Dijkstra 1968a], [Habermann et al. 1976], [Liskov 1972]) gründlich untersucht worden. Zum einen wird durch die Schichtenstruktur eine Systemfamilie begründet (siehe Abbildung 13-3), d.h., ein neues Familienmitglied (und damit eine neue Plattform für Anwendungen) kann mit vergleichsweise geringem Aufwand auf irgendeiner existierenden abstrakten Maschine aufgesetzt werden. Zum anderen wird durch die Schichtenstruktur aber auch das Problem der Systemkomplexität deutlich entschärft: Unter der Voraussetzung, daß die Schnittstellen präzis und in sich konsistent festgelegt wurden, kann jede abstrakte Maschine unabhängig von anderen erstellt werden. Findet ein Anwender nicht die passende Plattform, dann muß eine neue abstrakte Maschine
13
Implementierungsaspekte für Systemsoftware
entwickelt werden. Obwohl diese Aufgabe durch die Definition klarer Schnittstellen erleichtert wird, erfordert sie doch in aller Regel Spezialisten, die sich mit der Systemfamilie gut auskennen. Abb. 13-3 Durch Schichtung gebildete Systemfamilie
Offene, d. h. durch den Anwender leicht zu ergänzende Plattformen müssen dagegen die Fähigkeit besitzen, Erweiterungen, Ersetzungen oder Modifikationen auf der Abstraktionsebene des Anwenders zu unterstützen. Diesem Gedankengang trägt die Client/Server-Architektur Rechnung, die bereits in Kapitel 3 vorgestellt wurde. Das gesamte Laufzeitsystem besteht aus den folgenden drei Komponenten (siehe Abbildung 13-4):
• • •
Laufzeitbibliotheken im Adreßraum der Anwendungen Server in eigenen Adreßräumen Systemkern (kurz: Kern)
Die für Anwendungen sichtbare Plattform wird durch die Schnittstelle zur Adreßraum-lokalen Laufzeitbibliothek definiert. Im Extremfall werden alle an der Plattform angebotenen Dienstleistungen durch die Laufzeitbibliothek oder Server erbracht. Die Funktionalität des Kerns reduziert sich in diesem Fall auf das reine Vermitteln zwischen Leistungsanforderern (Clients) und Leistungsanbietern (Server). Die Offenheit einer Client/Server-Architektur ist faktisch unbeschränkt. Ein Anwender kann jede gewünschte Funktionalität herstellen: durch
• • •
Verwendung existierender Laufzeitbibliotheken und StandardServer, Erweiterung existierender Laufzeitbibliotheken und Einbringen neuer Server.
Client/Server-basierte Systeme
13
Implementierungsaspekte für Systemsoftware
Entscheidend ist, daß diese Tätigkeiten auf der Abstraktionsebene der Anwendung stattfinden und deshalb keine systeminternen Spezialkenntnisse von dem Anwender erfordern. Abb. 13-4 Client/Server-Architektur
Regeln für die Funktionsaufteilung
Für die Aufteilung der Gesamtfunktionalität eines Laufzeitsystems auf Kern, Laufzeitbibliothek und Server gelten die folgenden Regeln:
•
•
•
Der Kern muß sowohl alle Mechanismen für die Erzeugung und Terminierung von Laufzeitobjekten (Adreßräume, Threads) als auch einen elementaren Schutzmechanismus enthalten, der die Monopolisierung physischer Betriebsmittel (Prozessor, Speicher und Peripheriezugang) durch Laufzeitobjekte unterbindet. Kerne, die lediglich diese Funktionen anbieten, sind minimal ([Assenmacher 1996], [Liedtke 1995]). Man spricht dann auch von Minikernen (die in der Literatur häufig zu findenden Begriffe Mikrokern, Nanokern, Pikokern kennzeichnen Entwicklungsstadien auf dem Wege zu minimalen Kernen). Die Verlagerung von Dienstleistungen in Server ist immer dann angebracht, wenn es sich hierbei um allgemeine Dienstleistungen von anwendungsneutralem Charakter handelt. Datei-Server sind ein typisches Beispiel. Die Verlagerung von Dienstleistungen in Adreßraum-lokale Laufzeitbibliotheken ist dann sinnvoll, wenn - sie stark anwendungsabhängig sind oder - Effizienzgründe eine Verlagerung in Server ausschließen.
13
Implementierungsaspekte für Systemsoftware
Die erste Generation von Mikrokernen, zu denen V [Cheriton 1988], Amoeba [Muhender und Tanenbaum 1986], Mach [Acetta et al. 1986] und Chorus [Zimmermann et al. 1981] gehören, zielte primär auf die Bereitstellung einer universellen Plattform für Betriebssysteme. Insbesondere sollten existierende Betriebssysteme wie UNIX oder VMS (sog. native operating Systems) mit geringem Aufwand auf dieser Plattform realisierbar sein. Abbildung 13-5 zeigt die resultierende Systemarchitektur. Der Plattformgedanke kommt in dieser Architektur zweimal zur Anwendung. Mikrokerne stellen eine Plattform für die Konstruktion verschiedener Betriebssystemausprägungen dar (operating system Personalities). Die dort verfügbaren Mechanismen müssen entweder so elementar oder hochgradig parameterisiert sein, daß sich alle gängigen Betriebssystemausprägungen leicht darin realisieren lassen.
Beispiele für Mikrokerne
Abb. 13-5 Mikrokern-basierte Systemarchitektur
Durch Laufzeitbibliotheken im Adreßraum jeder Anwendung wird in einem zweiten Schritt die eigentliche Betriebssystemausprägung - z.B. die UNIX-Schnittstelle - nachgebildet. Selbstverständlich können Funktionsteile auch auf separate Server ausgelagert werden. Mikrokerne stellen damit eine Softwaretechnologie für die leichte Migration existierender Betriebssysteme auf neue Hardwaretechnologien dar. Die den Mikrokernen zugrundeliegende Idee ist nicht neu. Mit dem Systemkern CP 67 (später VM) hat IBM für die Rechnerfamilie IBM 360/370/390 eine Plattform für Betriebssysteme geschaffen [Borden et al. 1989]. Im Gegensatz zum Mikrokern-Ansatz waren die durch VM unterstützten Abstraktionen jedoch nicht rechnerneutral, sondern auf die Hardwarearchitektur der IBM-Rechnerfamilie zugeschnitten. Anstelle von Abstraktionen für Adreßräume, Threads und Kommunikation unterstützt VM virtuelle IBM-Rechner, die aus virtuellem Speicher, virtuellem Prozessor und virtuellen Peripheriegeräten bestehen. Jede virtuelle Maschine kann mit einem eigenen Betriebs-
VM
13
Implementierungsaspekte für Systemsoftware
System geladen werden. Bei der VM-Entwicklung stand der Wunsch nach Koexistenz mehrerer Betriebssysteme auf einem Wirtrechner im Vordergrund des Interesses. Kernbasierte Systemarchitekturen stellen heute den Stand der Technik dar. Welche Funktionalität ein Kern idealerweise besitzen sollte, ist jedoch aus wissenschaftlicher Sicht noch nicht eindeutig geklärt. Der Trend zu Minikernen ist unverkennbar, obwohl Effizienzprobleme häufig zu Kompromissen zwingen. Kritisch ist insbesondere die erhöhte Anzahl an Kontextwechseln durch die Verteilung der Gesamtfunktionalität auf viele Server, die in bestimmten Konstellationen Leistungseinbußen verursachen können [Chen et al. 1996]. Aus diesem Grund erlauben neuere Versionen kernbasierte Architekturen wie z.B. Mach [Accetta et al. 1986] und Chorus [Zimmermannetal. 1981] die spätere Reintegration leistungskritischer Server in den Adreßraum des Kerns. Auch in dem an sich eher monolithischen Linux-System wird ein vergleichbarer Strukturierungsansatz mit den sogenannten Kernelmodulen verfolgt (siehe z.B. auch [Bovet und Cesati 2001]). In den nachfolgenden Abschnitten werden die wichtigsten Entwicklungslinien für Kerne kurz beleuchtet.
13.1 Speichereinbettung der Kerne Kerne müssen aus Schutzgründen sicher gegen unkontrollierte Zugriffe auf ihren Speicher abgeschottet werden. Diese Anforderung läßt sich auf verschiedene Weise erfüllen: a) Der Kern wird in einem eigenen virtuellen Adreßraum untergebracht. b) Der Kern wird in einem gegen jeden Zugriff geschützten physischen Speicherbereich untergebracht, wobei im Kern real adressiert wird. c) Der Kern wird in jeden virtuellen Adreßraum eingeblendet, wobei den Prozessen alle Zugriffsrechte entzogen werden (siehe Abbildung 13-6). Abb. 13-6 Einblendung des Kerns in jeden Adreßraum
13.2
Serielle versus nebenläufige Kerne
Alle drei Verfahren erzwingen, daß der Eintritt in den Kern nur über einen Hardware-Trap erfolgen kann, der die Kontrolle an eine vorbestimmte Adresse des physischen Speichers transferiert. Der dort abgelegte Trap-Handler überprüft die Gültigkeit des Aufrufs und aktiviert die gewünschte Kernfunktion. Aus Effizienzgründen ist der Variante c der Vorzug zu geben, da Traps keinen Wechsel des Adreßraums nach sich ziehen. Erst wenn im Kern ein Kontextwechsel (Umschaltung auf einen anderen Prozeß) ansteht, muß auch der Adreßraum gewechselt und damit die MMU-Register inkl. Cache umgeladen werden.
13.2 Serielle versus nebenläufige Kerne Kerne sind im allgemeinen nur seriell benutzbar, d.h., zu einem Zeitpunkt kann immer nur ein Kernaufruf bearbeitet werden. Diese Organisationsform hat den Vorteil, daß innerhalb des Kerns keine Maßnahmen zur Thread-Synchronisation notwendig sind, da sich immer nur ein Thread zu einem Zeitpunkt im Kern aufhalten kann. Eine einfache Realisierung dieses Prinzips basiert auf der Ununterbrechbarkeit des Kerns. Für diesen Fall zeigt die Abbildung 13-7 einen vollständigen Zyklus vom Aufruf bis zur Rückkehr des Threads in die Anwendung.
Nur eine Aktivität im Kern
Abb. 13-7 Ablauf eines Kernaufrufs bei einem seriellen Kern
In der Praxis scheitert diese einfache Realisierung jedoch an zwei Forderungen. •
Kernaufrufe sind häufig blockierend, d.h., der Thread wird im Kern blockiert, beispielsweise bei der Initialisierung eines E/AVorgangs, und erst mit dem Eintreffen des E/A-Ende-Interrupts wieder fortgesetzt. In der Zwischenzeit erfolgt eine Pro-
Nachteile
13
Implementierungsaspekte für Systemsoftware
•
zeßumschaltung, die zur erneuten Kernaktivierung durch einen anderen Prozeß führen kann. Damit entsteht grundsätzlich die Gefahr des Zugriffs auf inkonsistente Daten, die blokkierte Prozesse auf ihrem Weg durch den Kern hinterlassen. Asynchrone Interrupts von Geräten, Timern und dergleichen will man möglichst unverzögert annehmen, auch wenn sich der Prozeß gerade im Kern aufhält. Auch hier birgt die Umschaltung im Kern auf die Interruptroutine die Gefahr, daß sich die Daten gerade in einem inkonsistenten Zustand befinden.
Beide Probleme lassen sich relativ einfach lösen, wenn folgende Regeln beachtet werden:
•
•
Blockierende Kernoperationen müssen sicherstellen, daß vor Eintritt in die Blockade ein konsistenter Kernzustand erreicht wurde. Die Umschaltung auf einen anderen Prozeß ist dann unkritisch. Die durch asynchrone Interrupts ausgelöste Unterbrechungsverarbeitung darf im Kern arbeitende Threads niemals verdrängen (keine Thread-Umschaltung). Außerdem dürfen in der Unterbrechungsverarbeitung keine Daten angefaßt werden, auf die potentiell während einer Trap-Bearbeitung zugegriffen wird.
Diese auch in UNIX angewendeten Maßnahmen versagen jedoch bei symmetrischen Multiprozessor-Systemen. Der gleichzeitige Eintritt in den Kern durch Threads, die jeweils einen unabhängigen physischen Prozessor besitzen, kann grundsätzlich nicht verhindert werden. Eine elegantere und besser strukturierte Lösung für das oben geschilderte Problem bieten nebenläufige Kerne, die gleichzeitige Kernaktivierungen durch Threads zulassen. Bei ihnen wird der Kern in zwei Schichten untergliedert (siehe Abbildung 13-8):
• •
dem Nukleus höhere Kernfunktionen
13.2
Serielle versus nebenläufige Kerne
Abb. 13-8 Ablauf eines Kernaufrufs bei einem nebenläufigen Kern
Im Nukleus, der als eine Art Minimalkern aufgefaßt werden kann, sind lediglich der Prozessormultiplexer und ein elementarer Mechanismus zu Thread-Synchronisation - z.B. Semaphore - angesiedelt. Alle höheren Kernfunktionen werden oberhalb des Nukleus durch Threads realisiert. Lediglich für den Nukleus muß die Serialisierbarkeit aller an ihn gerichteten Aufrufe (Traps oder asynchrone Interrupts) gefordert werden. Diese Forderung läßt sich durch Unterbrechungsschutz und bei Mehrprozessorsystemen durch das zusätzliche Hilfsmittel von Spin-Locks (vgl. Kapitel 6) leicht lösen. Oberhalb der Nukleus-Ebene konkurrieren Threads um den Zugriff auf Datenstrukturen des Kerns und stützen sich zu diesem Zweck auf die Synchronisationsfunktionen des Nukleus. Abbildung 13-8 zeigt den Ablauf eines Kernaufrufes bei einer nebenläufigen Kernarchitektur. Jeder Trap lenkt die Kontrolle zwangsweise in den Nukleus um und hat entweder die Bedeutung eines Kernaufrufs (Kern-Call) oder einer Rückkehr in die Anwendung (Kern-Return). Der Nukleus wirkt demnach lediglich als Schaltstelle zwischen Anwendung und Kern und führt die notwendigen Kontextwechsel wie z.B. Umlegen des PCs auf die zuständige Routine im Kern und die Umschaltung in den privilegierten Modus durch. Insgesamt stellt sich allerdings die Frage, ob die höherwertigen Funktionen des Kerns unbedingt dort angesiedelt sein müssen. Dies ist nur für solche Funktionen zu bejahen, die im privilegierten Modus ablaufen müssen. Alle anderen Funktionen können ebenso gut in die
Nukleus
13
Implementierungsaspekte für Systemsoftware
Laufzeitbibliotheken der Anwenderadreßräume oder separate Server ausgelagert werden. Die Vorteile dieser Strategie wurden bereits diskutiert.
13.3 Kerne ohne E/A-Unterstützung Verlagerung der E/A in Server
Externer Pager
Die Auslagerung der gesamten E/A-Unterstützung aus dem Kern ist bei einer speicherbasierten E/A-Architektur der Hardware möglich. Geräte werden in diesem Fall durch ihre Status- und Kontrollregister repräsentiert, die über normale MO VE-Instruktionen wie gewöhnliche Speicherzellen angesprochen werden. Eine differenzierte Zuweisung der Zugriffsberechtigung zu Geräten kann durch die Einblendung der Geräteregister in die virtuellen Adreßräume von Anwenderprozessen und/oder Geräteservern vorgenommen werden. Auf die Vorteile dieser Organisationsform für Geräte wurde bereits früher hingewiesen. Da die Unterbrechungen zwangsläufig eine Umschaltung des Prozesses in den privilegierten Modus auslösen, muß die Primärbehandlung von Unterbrechungen jedoch immer im Kern erfolgen. Im allgemeinen ist dann nur noch die Weiterleitung des Interrupts inkl. Statusinformation an den Prozeß notwendig, der für das Gerät verantwortlich ist. Dazu eignen sich asynchrone Formen des Nachrichtenversands oder Mechanismen für die Behandlung von Exceptions und Signalen (vgl. Kapitel 7). Die konsequente Auslagerung aller E/A-Aktivitäten aus dem Kern hat zur Folge, daß die Verwaltung virtueller Adreßräume nicht mehr geschlossen im Kern abgewickelt werden kann. So muß die Ein- und Auslagerung von Seiten zwischen Arbeitsspeicher und Platte bei Paging-Systemen durch einen externen Pager erfolgen (siehe Abbildung 13-9). Der Kern weist Prozessen aufgrund von Seitenfehlern Kacheln im Arbeitsspeicher zu und informiert den externen Pager darüber, der daraufhin die Einlagerung der Seiten und den Re-Start der betroffenen Prozesse veranlaßt. Unterschreitet die Anzahl freier Kacheln eine vorgegebene untere Schranke, dann wählt der Kern geeignete Verdrängungskandidaten aus, sperrt den Zugriff zu den korrespondierenden Seiten und informiert den externen Pager, der daraufhin die Auslagerung der Seiten auf die Platte veranlaßt. Die Attraktivität von externen Pagern kommt besonders in verteilten Systemen zur Geltung. Dort muß nämlich nicht mehr jeder Rechner seine eigene Paging-Verwaltung besitzen. Vielmehr reicht es aus, einen externen Paging-Server im Netz zu etablieren, der für alle anderen Rechner die Seitenverwaltung auf der Platte übernimmt. Mach [Accetta et al. 1986] ist ein bekannter Kern, der das Konzept von externen Pagern verwirklicht hat.
13.4
Nichtblockierende Kerne
Abb. 13-9 Externer Pager
1 2 3 4 5 6
Seitenfehler Kachelzuteilung Einlagerungswunsch Einlagerung einer Seite Bestätigung Re-Start des Prozesses
Insgesamt hat die Auslagerung der Ein-/Ausgabe aus dem Kern den Vorteil, daß dieser nur noch wenige Konfigurationsparameter besitzt und damit ohne großen Aufwand für ein beabsichtigtes Einsatzgebiet generiert werden kann. Auf der Serverebene ist eine übersichtliche Geräteverwaltung auf der Basis unabhängiger Geräteserver leicht möglich, wobei auch das dynamische Einbringen neuer Geräte zur Laufzeit keine großen Probleme aufwirft. Dazu muß lediglich die folgende Prozedur durchlaufen werden: 1. Erzeugung eines neuen Geräteservers 2. Authentisierung des neuen Geräteservers gegenüber dem Kern 3. Bei erfolgreicher Authentisierung: Einblenden der Geräteregister in den virtuellen Adreßraum des Geräteservers
13.4 Nichtblockierende Kerne Nichtblockierende Kerne zeichnen sich dadurch aus, daß sie keine Funktionen enthalten, die Threads blockieren. Die Umschaltung der physischen Prozessoren zwischen Threads wird in diesen Kernen ausschließlich aufgrund freiwilliger Prozessorabgabe oder Zeitscheibenende ausgelöst. Nichtblockierende Kerne dürfen deshalb weder blockierende E/A-Operationen noch Synchronisations- und Kommu-
Keine Thread-Blockaden im Kern
13
Implementierungsaspekte für Systemsoftware
nikationsfunktionen enthalten, die eine potentielle Blockade von Threads nach sich ziehen. Das durch nichtblockierende Kerne realisierte Zustandsmodell für Threads reduziert sich dann auf zwei Zustände (siehe Abbildung 13-10) Abb. 13-10 Vereinfachtes ThreadZustandsmodell bei nichtblockierenden Kernen
Vorteile
Abb. 13-11 Realisierung von ULThreads durch zweistufigen Prozessormultiplex
Der Grund, weshalb nichtblockierende Kerne trotz ihres reduzierten Funktionsumfangs blockierenden Kernen überlegen sind, hängt mit den Implikationen für eine Implementierung von UL-Threads zusammen (vgl. hierzu Kapitel 5). UL-Threads basieren auf einem Zeitmultiplex von KL-Threads durch ein Laufzeitpaket im Adreßraum von Prozessen bzw. Teams. Durch UL-Threads wird eine zweistufige Multiplexhierarchie der physischen Prozessoren begründet. Abbildung 13-11 zeigt nochmal das grundlegende Prinzip. Die physischen Prozessoren werden im Kern durch den Dispatcher verwaltet, der ein Zeitmultiplex unter allen KL-Threads - oft auch als virtuelle Prozessoren (VPs) bezeichnet - realisiert.
13.5
Minimalkerne
Transparent für den Kern werden in den Adreßräumen von Prozessen oder Teams virtuelle Prozessoren im Zeitmultiplex auf UL-Threads verteilt. Dabei kann nun die folgende Situation entstehen:
Integration von UL-Threads
a) Ein UL-Thread ruft eine Funktion seiner Adreßraum-lokalen Taufzeitbibliothek auf. b) In der aufgerufenen Taufzeitroutine wird die Unterstützung des Kerns benötigt, d.h., mittels eines Traps wird eine Kernfunktion aktiviert. Wird der virtuelle Prozessor im Kern blockiert, dann bleiben alle bereiten UT-Threads, für die der virtuelle Prozessor zuständig ist, für die Dauer der Blockade ohne Prozessorzuteilung. An dieser Situation wird deutlich, daß das Konzept der UTThreads unverträglich mit blockierenden Kernen ist. Nichtblockierende Kerne belassen dagegen den virtuellen Prozessor, der einen Kernaufruf initiiert hat, in aller Regel im Besitz des physischen Prozessors. Von dieser Regel wird nur dann abgewichen, wenn der virtuelle Prozessor wegen Mangels an einem bereiten UT-Thread freiwillig den physischen Prozessor abgibt oder der Kern aufgrund einer abgelaufenen Zeitscheibe auf einen anderen bereiten virtuellen Prozessor umschaltet.
13.5 Minimalkerne Minimalkerne (oder kurz Minikerne) stellen lediglich die unbedingt notwendigen Mechanismen eines Kerns bereit und verlagern alle übrigen Funktionen in unabhängige Server oder in Laufzeitbibliotheken der Adreßräume. Nach Assenmacher [Assenmacher 1996] kann die Funktionalität von Minikernen auf die folgenden Mechanismen beschränkt werden:
• •
• •
Authentisierung Prozessorzuteilung Kachelzuteilung (ohne Paging) primitive Kommunikation
Durch die jeweiligen Mechanismen der Prozessorzuteilung und Kachelzuteilung werden elementare Abstraktionen für virtuelle Prozessoren und Adreßräume bereitgestellt. Die Mechanismen müssen Fairneß garantieren und damit die Monopolisierung von Prozessor und Speicher durch eine Anwendung sicher unterbinden. Dies kann durch Zuordnung von Zeitscheiben bei der Prozessorvergabe und den Kachelentzug bei Speicherengpässen geschehen. Durch einen Authentisierungsmechanismus wird die Basis für den über Zugriffsberechti-
Minimale Kernfunktionalität
13
Implementierungsaspekte für Systemsoftware
gungen geschützten Zugriff auf ein externes Gerät gelegt. Nur Prozesse, die sich erfolgreich beim Kern identifizieren, erhalten die Geräteregister in ihren Adreßraum eingeblendet. Eine primitive Form von Kommunikationsmechanismus müssen Minikerne bereitstellen, um den Informationsaustausch zwischen disjunkten Adreßräumen zu bewerkstelligen und Interruptsignale (z.B. E/A-Interrupts) vom Kern an Geräteserver weiterzuleiten. Aus den oben dargestellten Gründen sollten Minikerne keine blockierenden Aufrufe enthalten. Deshalb kommen bei Kommunikationsprimitiven nur asynchrone Formen von Send/Receive in Betracht. Die Forschung auf dem Gebiet der Minikerne hat noch nicht zu einem Konsens über einen kanonischen Funktionssatz geführt [Liedtke 1995]. Der Wert von Minikernen liegt in ihrer Universalität begründet. Durch ihre Beschränkung auf das unbedingt Notwendige maximieren sie ihre Einsetzbarkeit für Anwendungen mit sehr unterschiedlichen Anforderungen an die darunterliegende Plattform.
Glossar
Adreßraum Eine über eindeutige Adressen zugreifbare Menge von Speicherzellen. Man unterscheidet zwischen einem
und einem . Aktive Warteschleife Ein Thread wartet in einer Schleife aktiv auf den Eintritt einer Bedingung, die nur durch einen zweiten Thread oder durch ein externes Ereignis erfüllt werden kann (Busy Waiting). Application Programming Interface
siehe
Asynchrone Unterbrechung Ein externes Ereignis, das die aktuelle Programmausführung unterbricht (Interrupt). Auftrag Kommunikationsmuster, bei dem ein eine Auftragsnachricht an einen <Setver> sendet und das Ergebnis der Auftragsbearbeitung in Form eines sogenannten Replys erhält. Ausführungsmodus Der Ausführungsmodus eines Prozessors bestimmt die Ausführbarkeit bestimmter Instruktionen und den Zugriff auf einzelne Register. Man unterscheidet zwischen einem privilegierten Ausführungsmodus> und einem . Ausführungszeit Zeitbedarf für die Ausführung einer Befehlsfolge. Neben dem Messen der Ausführungszeit ist im Kontext von <Echtzeitsystemen> die Angabe einer maximalen Ausführungszeit für eine Echtzeitaktivität von besonderer Bedeutung für das Echtzeitscheduling. Aushungerung Rechenwilligen Prozessen wird durch die Anwendung einer meist prioritätsbasierten Schedulingstrategie der Prozessor dauerhaft entzogen, da z.B. Prozesse mit höherer Priorität kontinuierlich bevorzugt werden (Starvation). Ausnahme Die zuletzt ausgeführte Instruktion wurde mit einem Fehler beendet (Synchrone Unterbrechung). Authentisierung
Überprüfung der Identität.
Betriebssystem Monolithisches System, bei dem auf den überwiegenden Teil des Funktionsangebots durch zugegriffen wird. Die
Glossar
monolithische Struktur kann von Anwendungen nur in sehr eingeschränktem Umfang erweitert und angepaßt werden. Block Menge von Daten bestimmter Länge. Typischerweise findet die Übertragung von Daten zwischen dem Hauptspeicher und einem externen Speicher oder Netzadapter in Blöcken statt. Busy Waiting
siehe
Cache Einem Speicher vorgeschalteter schneller Zwischenspeicher, der Kopien zurückliegender Speicherzugriffe enthält. Ein Cache soll die hohe Zugriffszeit auf den nachfolgenden Speicher verbessern. Die Qualität des Caches hängt von der erzielbaren Trefferwahrscheinlichkeit ab. Bei einem Treffer oder kann ein Speicherzugriff bereits durch den schnellen Cache erfüllt werden. Bei einem muß auf den langsamen nachgeschalteten Speicher zugegriffen werden. Die ausgeprägte vieler Programme hat eine hohe Trefferrate zur Folge. Cache Hit
siehe
Cache-Kohärenz Bei der Verwendung von Caches, insbesondere in einem Multiprozessorsystem, entstehen ein oder mehrere Kopien einer Speicherzelle. Caches sind kohärent, wenn zu keinem Zeitpunkt unterschiedliche Inhalte derselben Speicherzelle in verschiedenen Caches gespeichert werden. Cache Miss
siehe
Capability-Liste tes Subjekt.
Enthüllt alle Zugriffsrechte auf Objekte für ein bestimm-
Client Ein Client ist Teil eines . Ein Client wendet sich aktiv mit einem an einen <Server>. Client/Server-System Organisationsform für Systeme. In einer Client/Server-basierten <Systemsoftware> wird der überwiegende Teil des Funktionsangebots durch dedizierte <Server> zur Verfügung gestellt, die von und Servern selbst in Anspruch genommen werden. Die Adreßraum-übergreifende Kommunikation zwischen Client und Server wird in diesem Fall von einem <Mikrokern> realisiert. Clustering
siehe
Codebereich Teil des , in dem sich die auszuführenden Instruktionen befinden. Condition-Variable Prozesse warten an einer Condition-Variablen innerhalb eines <Monitors> auf den Eintritt einer bestimmten Bedingung. Andere Prozesse, die zwischenzeitlich im Besitz eines Monitors sind,
Glossar
können den Eintritt entsprechender Bedingungen signalisieren und damit evtl. blockierte Prozesse befreien. Datei Einheit der Speicherung beliebiger Daten auf einem . Datenbereich Teil des Adreßraums, in dem sich die statischen und eines Programms befinden. Deadline
siehe
Deadlock
siehe
Deskriptor Eine nicht weiter aufschlüsselbare Datenstruktur, die dem Besitzer des Deskriptors bestimmte Zugriffe auf ein Objekt erlaubt. Zum Beispiel wird beim Öffnen einer Datei ein Dateideskriptor an den aufrufenden zurückgegeben, der den nachfolgenden Zugriff auf diese Datei ermöglicht. Directory
siehe
Dirty-Bit (D-Bit) Teil eines <Seitendeskriptors>. Die entsprechende Seite wurde seit dem letzten Zurücksetzen des Bits verändert. Dispatcher Der Dispatcher realisiert die Übergänge im . Die Dispatcher-Funktionen für sind Teil des . Dispatcher-Funktionen für sind meist in Form einer realisiert. Dynamische Daten In der Größe veränderlicher Bereich, in dem die von einem Thread dynamisch zur Laufzeit angeforderten Speicherbereiche z. B. zum Aufbau einer Liste oder eines Binärbaums verwaltet werden. E/A-Adreßbereich Adressen im oder virtuellen Adreßraum>, über die ein <E/A-Controller> angesprochen werden kann. E/A-Bus Ein nebem dem Prozessorbus eigenständiger Bus, über den ein <E/A-Bus-Controller> den Zugriff auf <E/A-Controller> ermöglicht. E/A-Bus-Controller Ein spezieller E/A-Controller, der einen <E/A-Bus> zur Verfügung stellt und damit den Zugriff auf ein oder mehrere weitere <E/A-Controller> erlaubt. E/A-Controller Eine gesonderte Hardware, die zwischen einem Computer und einem bestimmten E/A-Gerät vermittelt. Wesentliche Aufgaben des Controllers sind z.B. die Pegelwandlung oder die Information eines E/A-Bus-Controllers oder des Prozessors über eingetroffene Daten. E/A-Regis-ter Zugreifbare Speicherzellen eines <E/A-Controllers>, über die ein Controller konfiguriert (Kommandoregister), der Status abge-
Glossar
fragt (Statusregister) und Ein- oder Ausgabedaten übertragen (Datenregister) werden können. Echtzeitsystem Anwendung, bei der Zeitvorgaben eingehalten werden müssen. Man unterscheidet zwischen <strikten Echtzeitsystemen> und <schwachen Echtzeitsystemen>. Eingeblendete Datei Eine wird in den einer Anwendung eingeblendet. Auf den Dateiinhalt kann durch einfache Speicheroperationen zugegriffen werden (Memory Mapped File). Entfernter Prozeduraufruf (RPC) Im Kontext dieses Buches der Adreßraum-übergreifende Aufruf einer Prozedur. Gängiger Vertreter der auftragsbasierten Nachrichtenkommunikation in einem . Ereignisgesteuerte Ausführung Die Ausführung von Echtzeitaktivitäten wird durch Ereignisse angestoßen. Ereignisse können durch andere Aktivitäten und z.B. durch Sensoren ausgelöst werden. Externe Fragmentierung Durch eine Folge von Speicheranforderungen wird der Vorrat an freiem Speicher soweit zerstückelt, daß zu einem bestimmten Zeitpunkt eine weitere Anforderung nicht befriedigt werden kann, obwohl in der Summe hinreichend viel Platz vorhanden wäre. Fragmentierung Bei der Zuteilung eines Speicherbereichs aus einem Vorrat an freiem Speicher entsteht meist eine oder <externe Fragmentierung;». Frist
Zeitpunkt, bis zu dem eine Echtzeitaktivität beendet sein muß.
Gemeinsamer Adreßraum Ein , der von mehreren gemeinsam genutzt wird. Sonderfall eines gemeinsamen Speicherbereichs;». Gemeinsamer Speicherbereich Mehrere verfügen über einen teilweise oder vollständig gemeinsamen Speicherbereich. Alle beteiligten Threads können über Lese- und Schreiboperationen darauf zugreifen (Shared Memory). Heap
siehe
Hintergrund Ein wird im Hintergrund ausgeführt, wenn er nicht im Besitz der primären E/A-Geräte Tastatur und Bildschirm ist. Eine E/A hat in diesem Fall die Blockade des Prozesses zur Folge. Interne Fragmentierung Die Speicherverwaltung erfüllt eine Anforderung mit einem größeren Speicherbereich. Die zusätzlichen Speicherzellen können vom anfordernden Thread und von der Speicherverwaltung nicht verwendet werden.
Glossar
Interrupt Ein externes Ereignis, das die aktuelle Programmausführung unterbricht (Asynchrone Unterbrechung). Interruptmaskierung Bei einem maskierten Interrupt findet kein Aufruf der beim Eintritt eines entsprechenden Interrupts statt. Maximal ein Interrupt wird vom Prozessor gespeichert und zu einem späteren Zeitpunkt, z. B. bei der Rücknahme der Maskierung, ausgelöst. Interrupts können durch eine privilegierte Instruktion freigeschaltet oder maskiert werden. Kachel Teil des , der zur Speicherung einer virtuellen <Seite> eingesetzt wird. Bei seitenbasierten Verfahren werden meist wesentliche Teile des Speichers in Kacheln gleicher Länge unterteilt. Keller
siehe
Kellerregister Ein Prozessorregister, das auf das oberste bzw. unterste Element des aktuellen zeigt. Kern Geschützter Teil der Systemsoftware. Ein Kern wird ausschließlich über und <synchrone Unterbrechung e n (z.B. durch einen ) betreten. Kernaufruf <Synchrone Unterbrechung> z.B. in Form des Trap-Befehls. Die Ausführung wird im privilegierten Modus> an einer bestimmten Stelle im aufgenommen. Parameterübergabe und Rückgabe des Resultats wird meist von einer Bibliotheksfunktion übernommen und an einen normalen Unterprogrammsprung angepaßt (System-Call). KL-Thread
Thread-Realisierung im .
Koallokation Benachbarte Speicherung von Daten, z.B. zusammenhängende Plazierung der einer auf einem und einem . Kontextwechsel Wechsel in der -Ausführung. Der Zustand des aktuell ausgeführten Threads wird im gesichert. Anschließend wird der Zustand des nächsten auszuführenden Threads wiederhergestellt. Laufzeitbibliothek Menge von Bibliotheksfunktionen, die von der Anwendung aufgerufen werden können. Bibliotheken können zu einer Anwendung statisch, d. h. vor der Programmausführung, oder dynamisch,
Glossar
d.h. unmittelbar beim Start oder beim Aufruf einer Bibliotheksfunktion, gebunden werden. Laufzeitkeller Ein Laufzeitkeller speichert die Aufrufverschachtelung und die damit verbundenen lokalen Variablenzustände eines . Je nach Prozessortyp können Laufzeitkeller von hohen zu niedrigen oder von niedrigen zu hohen Adressen wachsen. Laufzeitmodell Ein Laufzeitmodell ist eine Modellvariante, die durch eine bestimmte Funktionsmenge und Systemarchitektur beschrieben wird. Für ein Laufzeitmodell existieren verschiedene Ausprägungen, die sich z.B. bezüglich der enthaltenen Systemdienste und der jeweiligen Spezialisierung unterscheiden. Laufzeitplattform Die Funktionsmenge der <Systemsoftware> bildet die Laufzeitplattform. Die Laufzeitplattform basiert auf einem bestimmten . Laufzeitroutine
Teil einer
Logischer Adreßraum
siehe
Lokalitätsmenge Teilmenge des <Working-Sets> eines Threads. Wenn die virtuellen <Seiten> der Lokalitätsmenge im Hauptspeicher eingelagert sind, ergibt sich eine minimale <Seitenfehlerrate> für diesen . Meldung Einfaches , bei dem ein Sender eine Nachricht an einen Empfänger sendet. Memory Management Unit (MMU) Hardwarekomponente für die effiziente Umsetzung virtueller Speichertechniken. Die MMU kann als eigener Chip realisiert oder auf dem Prozessorchip plaziert sein. Memory Mapped File
siehe <eingeblendete Datei>
Mikrokern Ein , der eine geringe bzw. minimale Funktionsmenge realisiert. MMU
siehe <Memory Management Unit>
Monitor Ein sprachbasiertes Synchronisationsmittel, bei dem die gemeinsam genutzten Daten und die darauf definierten Zugriffsfunktionen zu einer Einheit zusammengefaßt werden. Die Monitorfunktionen schließen sich in der Ausführung wechselseitig aus. Monolithisches System Eine klassische Betriebssystemstruktur, bei der ein einziger großer alle wesentlichen Funktionen der bereitstellt.
Glossar
Multiprozessorsystem Ein System mit mehreren Arbeitsprozessoren. Man unterscheidet zwischen einem asymmetrischen System, in dem ein ausgezeichneter Prozessor alle wesentlichen Systemfunktionen ausführt, und einem symmetrischen System, in dem die Systemfunktionen gleichmäßig auf alle Prozessoren verteilt sind. Nachricht Einheit der Kommunikation zwischen zwei , die über keinen gemeinsamen Speicher verfügen. Nachrichtengekoppelte Prozesse (Laufzeitmodell B) Ein , in dem alle beteiligten nur durch den Austausch von kommunizieren. Nachrichtengekoppelte Teams (Laufzeitmodell C) Das umfassendste . Innerhalb eines kommunizieren die beteiligten über den gemeinsamen Adreßraum>. Die Teamübergreifende Kommunikation findet ausschließlich über statt. Nachrichtentransaktion Gesamter Vorgang vom Abschicken einer bis zu ihrer Verarbeitung und anschließenden Quittierung durch den Empfänger. Nichtpreemptives Scheduling Ein Schedulingverfahren, bei dem einem rechnenden der zugeordnete Prozessor nur entzogen werden kann, wenn eine blockierende Operation aufgerufen oder der Prozessor freiwillig abgegeben wird (yielding). Normalmodus Eingeschränkter , in dem eine privilegierte Instruktion nicht ausgeführt und auf privilegierte Register nicht zugegriffen werden kann. Bei einer Schutzverletzung wird eine s y n chrone Unterbrechung> ausgelöst. Partition Zusammenhängender Teil einer Festplatte, der von der <Systemsoftware> meist als eigenständige logische Festplatte behandelt wird. PCB
siehe
Periode Legt die Frequenz einer immer wiederkehrenden Echtzeitaktivität fest. Persistenter Speicher Ein Speicher, der seine Inhalte dauerhaft speichert. Die Basis bilden persistente Speichermedien wie z. B. Festplatte und darauf aufbauende Speicherdienste. Physischer Adreßraum Die über den Adreßbus des Prozessors direkt zugreifbare Speicher- und E/A-Hardware. Eine eventuell vorhandene <Memory of Management Unit> ist in diesem Fall ausgeschaltet.
Glossar
Preemptives Scheduling Ein Schedulingverfahren, bei dem einem rechnenden der zugeordnete Prozessor entzogen werden kann, wenn eine blockierende Operation aufgerufen, der Prozessor freiwillig abgegeben (yielding) oder durch eine osynchrone Unterbrechung> ein zweiter Prozeß mit höherer Priorität rechenbereit wird. Ein typisches Beispiel für eine Preemption ist ein Thread-Wechsel aufgrund eines Timer-Interrupts (z.B. Round-Robin-Scheduling). Present-Bit (P-Bit) Teil eines <Seitendeskriptors>, <Seitentabellendeskriptors> oder <Segmentdeskriptors>. Bei gesetztem Bit ist der angesprochene Bereich im Hauptspeicher zugreifbar. Bei nicht gesetztem P-Bit befindet sich der betreffende Teil auf einem nach seiner »Wichtigkeit«. Die Priorität bestimmt die Schedulingreihenfolge bei mehreren rechenbereiten Threads. Prioritätsinversion Unerwünschte Situation im Echtzeitbetrieb, bei der ein Prozeß mit hoher durch einen Prozeß mit niedriger Priorität blockiert wird, z.B. weil der Prozeß mit niedriger Priorität im Besitz eines <Semaphors> ist, auf das auch der Prozeß mit hoher Priorität blockierend zugreift. Eine Inversion kann durch <prioritätsvererbende> Synchronisationsverfahren vermieden werden. Prioritätsvererbung Zeitlich beschränkte Prioritätserhöhung eines Threads mit niedriger Priorität zur Vermeidung einer
Privilegierter Modus , in dem alle Instruktionen ausgeführt und auf alle Register des Prozessors zugegriffen werden kann. In den privilegierten Modus gelangt man nur durch eine . Programmierschnittstelle Eine Beschreibung der Funktionsmenge eines Dienstes, die von einem Programmierer bei der Nutzung dieses Dienstes in Anspruch genommen werden kann. Die Programmierschnittstelle liegt in einer von der jeweiligen Programmiersprache abhängigen Form vor, in der alle Funktionen durch Angabe von Namen und Signatur sowie die Datentypen für Argumente und Rückgabewerte eindeutig definiert werden. Programmzähler Ein ausgezeichnetes Prozessorregister, das immer auf die Adresse der nächsten auszuführenden Instruktion zeigt. Prozeß
mit mindestens einem .
Prozeßkontrollblock Datenstruktur, die alle relevanten Informationen zur Verwaltung von und speichert. Auf den PCB greifen im wesentlichen und <Scheduler> zu.
Glossar
Prozessorzustandsregister Ein Prozessorregister, das verschiedene Zustandsinformationen wie z.B. das Ergebnis der letzten arithmetisch-logischen Operation speichert. Bei vielen Prozessoren werden über dieses Register auch der aktuelle und Informationen über wiedergegeben. Reentrantfähiger Code Kann von mehreren gleichzeitig ausgeführt werden. Eventuell notwendige Zustandsinformationen werden pro Thread gespeichert. Der Zugriff auf kritische Abschnitte wird synchronisiert. Referenced-Bit (R-Bit) Teil eines <Seitendeskriptors>. Auf die entsprechende Seite wurde seit dem letzten Zurücksetzen des Bits lesend oder schreibend (siehe dazu auch ) zugegriffen. Referenzlokalität Eigenschaft eines . Die Ausführung sequentieller Instruktionsfolgen und die Schleifenhäufigkeit in prozeduralen Programmen hat eine hohe Referenzlokalität zur Folge, bei der in einer längeren Folge von Speicherzugriffen vergleichsweise wenige unterschiedliche Adressen referenziert werden. Die ist eine Ausprägung der Referenzlokalität auf Seitenebene. Registersatz Menge aller Prozessorregister. Die vollständige Sicherung und Restaurierung des Registersatzes ist ein zentrales Element eines . Remote-Procedure-Call (RPC)
siehe <Entfernter Prozeduraufruf>
Scheduler Teil der <Systemsoftware>, der aus einer Menge rechenwilliger Threads den nächsten auszuführenden Kandidaten auf der Grundlage einer konkreten Schedulingstrategie auswählt. Die Notwendigkeit einer Auswahl ist immer bei einem Assign-Übergang durch den gegeben. Schwaches Echtzeitsystem Echtzeitsystem, bei dem die Verletzung einer Zeitvorgabe () keine schwerwiegenden Folgen hat. Trotzdem ist eine Fristverletzung nach Möglichkeit zu vermeiden. Segment Zusammenhängender virtueller Adreßbereich. Der segmentbasierte Speicherzugriff findet implizit über ein <Segmentregister> statt. Bei einem Speicherzugriff wird in diesem Fall die Adresse als Offset relativ zum Segmentanfang aufgefaßt. Segmentdeskriptor Charakterisiert ein <Segment>. Enthält u.a. die Anfangsadresse des Segments im und die Segmentlänge. Segmentregister Das Segmentregister speichert im einfachen Fall die Anfangsadresse des Segments im . Alternativ
Glossar
kann ein Segmentregister den Index für einen <Segmentdeskriptor> in einer <Segmenttabelle> enthalten. Segmenttabelle
Eine indizierbare Menge von <Segmentdeskriptoren>.
Seite Bei einem seitenbasierten Verfahren wird der in Seiten gleicher Länge unterteilt. Adreßabbildung, Ein- und Auslagerung sowie Schutz findet auf der Basis einzelner Seiten statt. Seitendeskriptor Teil einer <Seitentabeüe>. Der Seitendeskriptor speichert alle relevanten Informationen zu einer <Seite> im , u.a. den Aufenthaltsort der Seite in Abhängigkeit des sowie Schutzinformationen. Seitenfehler Die <Memory Management Unit> löst einen Seitenfehler (spezielle <synchrone Unterbrechung>) aus, wenn im Zuge der virtuellen Adreßabbildung der ermittelte <Seitendeskriptor> ein nicht gesetztes Present-Bit aufweist. Die fehlende Seite muß in diesem Fall vor einer Wiederholung des Speicherzugriffs in den Hauptspeicher eingelagert werden. In mehrstufigen Verfahren wird auch bei einer ausgelagerten <Seitentabelle> ein Seitenfehler ausgelöst. Seitenfehlerwahrscheinlichkeit Wahrscheinlichtkeit, daß beim Zugriff auf eine virtuelle Adresse ein <Seitenfehler> eintritt. Seitenflattern Systemweiter Effekt, der eintritt, wenn sich die aller Threads nicht im Hauptspeicher befindet. Dadurch werden <Seitenfehler> mit einer hohen Frequenz ausgelöst und bringen damit das Gesamtsystem zum Stillstand (Thrashing). Seitennachschubverfahren Im wesentlichen unterscheiden sich die Seitennachschubverfahren bezüglich des Zeitpunktes, an dem eine virtuelle <Seite> in den Hauptspeicher eingelagert wird. Beim Demand-Paging wird eine Seite mit der ersten Referenz als Folge eines <Seitenfehlers> nachgeladen. Beim Pre-Paging werden einzelne Seiten vor einem ersten Zugriff vorab geladen. Seitentabelle Eine indizierbare Menge von <Seitendeskriptoren>. Im Fall eines mehrstufigen Verfahrens eine indizierbare Menge von <Seitendeskriptoren> und <Seitentabellendeskriptoren>, die ihrerseits auf nachfolgende Seitentabellen verweisen. Der Index eines Deskriptors in der Tabelle bestimmt die Position und Länge des entsprechenden Speicherbereichs im zugehörigen . Seitentabellendeskriptor Teil einer mehrstufigen <Seitentabelle>. Verweist auf eine weitere <Seitentabelle>. Darüber hinaus enthält der Deskriptor meist ein , das in diesem Fall angibt, ob sich die entsprechende Seitentabelle im Hauptspeicher befindet oder ausgelagert ist.
Glossar
Seitenverdrängungsverfahren Ein Verfahren, das aus einer Menge von in des Hauptspeichers befindlichen virtuellen <Seiten> potentielle Kandidaten für eine Auslagerung bestimmt. Idealerweise versucht ein Seitenverdrängungsverfahren diejenige virtuelle Seite zu ermitteln, die in der Zukunft am längsten nicht referenziert wird. Die Wahl eines geeigneten Verfahrens hat einen entscheidenden Einfluß auf die <Seitenfehlerrate>. Semaphor
Einfaches speicherbasiertes Synchronisationsprimitiv.
Server Ein Server ist Teil eines . Ein Server wartet passiv auf eintreffende eines . Nach Auftragsbearbeitung wird das Auftragsergebnis in Form eines Replys an den zurückgesendet. Shared Memory
siehe gemeinsamer Speicherbereiche
Shell Dienstprogramm, das Benutzern den Zugang zu der <Systemsoftware> ermöglicht. Speicherabbildungstabelle Konzeptionell wird die Abstraktion eines durch eine Speicherabbildungstabelle erreicht, die jede Adresse des .auf eine Adresse im abbildet oder auf den Aufbewahrungsort auf einem verweist. Zusätzlich können die Zugriffsrechte festgelegt werden. Die Realisierbarkeit einer Speicherabbildungstabelle scheitert an ihrer Größe. In der Praxis werden Vereinfachungen in Form von <Segmenttabellen> und <Seitentabellen> eingesetzt. Speicherbasierte E/A <E/A-Controller> und <E/A-Bus-Controller> werden konzeptionell als spezielle Speicherbausteine aufgefaßt und in den des Rechners integriert. Auf die <E/A-Register> wird in diesem Fall über normale Lese- und Schreiboperationen zugegriffen. Speichergekoppelte Prozesse (Laufzeitmodell A) Mehrere kooperieren innerhalb eines gemeinsamen Adreßraums>. Starvation
siehe
Striktes Echtzeitsystem Ein Echtzeitsystem, bei dem die Verletzung einer Zeitvorgabe () katastrophale Folgen haben kann. Swapping mangel.
Auslagerung ganzer bei akutem Speicher-
Synchrone Unterbrechung Die zuletzt ausgeführte Instruktion wurde mit einem Fehler beendet (Ausnahme). Synchrone Unterbrechungen
Glossar
können durch besondere Instruktionen explizit ausgelöst werden, z.B. in Form eines . System-Call
siehe
Systemsoftware Konfigurierbare und erweiterbare Menge von Systemdiensten, die einer Anwendung die einfache und von technischen Details befreite Nutzung eines Rechnersystems ermöglicht. Die Funktionalität der Systemsoftware basiert auf einem bestimmten und steht der Anwendung in Form einer zur Verfügung. Team Mehrere kooperierende , die über einen gemeinsamen Adreßraum> verfügen. Thrashing
siehe <Seitenflattern>
Thread Abstraktion eines physischen Prozessors. In dieser Rolle ist der Thread Träger einer sequentiellen Aktivität, die durch die Ausführung einer ihm zugeordneten Instruktionsfolge (Programm) bestimmt ist. Die Ausführung findet im Kontext eines statt. Einem Thread wird durch den zeitweise oder dauerhaft ein physischer Prozessor zugeordnet. Translation Lookaside Buffer (TLB) innerhalb der <Memory Management Unit>. Der TLB speichert das Ergebnis zurückliegender Adreßabbildungen. Aufgrund der ausgeprägten vieler kann dadurch die für notwendige Adreßabbildung beschleunigt werden. Bei jedem Zugriff auf den virtuellen Adreßraum wird zuerst überprüft, ob das Abbildungsergebnis bereits im TLB vorliegt. Wenn ja, liegt ein TLB-Hit vor und die Adreßabbildung kann ohne weitere Speicherzugriffe ausgeführt werden. Bei einem TLB-Miss müssen im Zuge der Adreßabbildung die notwendigen <Segmentdeskriptoren>, <Seitentabellendeskriptoren> und <Seitendeskriptoren> von der MMU geladen werden. Der eigentliche Speicherzugriff verzögert sich dadurch. Überlappende Adreßräume (Laufzeitmodell D) oder mit ansonsten disj unkten verfügen über einen gemeinsamen Speicherbereiche Die Kooperation der beteiligten findet innerhalb des gemeinsamen Speicherbereichs über normale Lese- und Schreiboperationen statt. UL-Thread Leichtgewichtige -Realisierung im der Anwendung. Alle notwendigen Funktionen wie z.B. der ULThread-Scheduler sind Teil einer . Unterbrechung Prozessorinterne und -externe Ereignisse, die die Ausführung der aktuellen Instruktionssequenz unterbrechen können. Man unterscheidet in Abhängigkeit einer zeitlichen Beziehung zur Ausführung
Glossar
der aktuellen Instruktion zwischen <synchronen Unterbrechungen;-, die aufgrund der aktuellen Instruktionsausführung entstehen, und « s y n chronen Unterbrechungen;», die zufällig zum Zeitpunkt der aktuellen Instruktionsausführung auftreten. Unterbrechungsroutine Im privilegierten Modus> ausgeführte, meist kurze Instruktionssequenz, die beim Auftreten einer ausgeführt wird. Die Unterbrechungsroutine ist meist Teil der <Systemsoftware>. Verklemmung Eine zirkuläre Wartebedingung zwischen mehreren . Die betroffenen Threads können sich aus einer Verklemmung nicht selbständig befreien. Verzeichnis Spezielle, vom Dateisystem verwaltete . Verzeichnisse enthalten Verweise auf normale Dateien und weitere Unterverzeichnisse. Die große Speicherkapazität persistenter Speicher kann dadurch hierarchisch strukturiert werden. Virtuelle Maschine Die Funktionen der können als Funktionen eines virtuellen Rechners, d.h. einer virtuellen Maschine, aufgefaßt werden. Virtueller Adreßraum Abstraktion des . Dabei können viele technischen Beschränkungen wie z.B. die maximale Größe eines Adreßraums praktisch aufgehoben werden. Durch virtuelle Adreßräume können auch Schutzkonzepte verwirklicht und insbesondere Anwendungen isoliert werden. Zum Einsatz kommen segmentund seitenbasierte Verfahren, die mit Hilfe der <Memory Management Unit> umgesetzt werden. Virtueller Prozessor Abstraktion eines physischen Prozessors. Dabei können viele Beschränkungen, wie z.B. die Anzahl in einer Anwendung nutzbarer Prozessoren, aufgehoben werden. Virtuellen Prozessoren wird durch und <Scheduler> zeitweise ein realer Prozessor zugeordnet. Vordergrund Ein im Vordergrund befindlicher ist im Besitz der primären E/A-Geräte Tastatur und Bildschirm. Wartegrund Teil eines . Der Wartegrund identifiziert eindeutig das Ereignis, auf das ein Thread blockierend wartet. Die Wartegrund-Information erlaubt die erneute Eingliederung eines blockierten Threads in die Menge der rechenwilligen Threads, wenn das entsprechende Ereignis eingetreten ist. Typische Wartegründe sind E/A-Operationen und blockierende Synchronisationsoperationen. Working-Set Definitionsgemäß die Menge der virtuellen <Seiten>, die zu einem Zeitpunkt t durch die zurückliegenden A Speicherzugriffe referenziert wurden. Das Working-Set ist für einen bestimmten Wert A mi-
Glossar
nimal und umfaßt in diesem Fall genau die eines Threads. Zeitgesteuerte Ausführung Die Ausführung von Echtzeitaktivitäten wird ausschließlich durch das Voranschreiten der Zeit in Form eines periodischen Timer-Interrupts ( und die erlaubten Zustandsübergänge. Das Zustandsmodell wird von und <Scheduler> umgesetzt.
Abkürzungen
ALU
Arithmetic-Logical Unit
API
Application Programming Interface oder Application Programmers Interface
BIOS
Basic Input Output System
CD
Compact Disc
CD-R
Compact Disc Recordable (Einmal beschreibbare CD)
CD-RW
Compact Disc Read/Write (Mehrfach beschreibbare CD)
CPL
Current Privilege Level
CPU
Central Processing Unit
CS
Code Segment
DCB
Device Control Block
DLL
Dynamic Link Library
DMA
Direct Memory Access
DOS
Disc Operating System
DPL
Descriptor Privilege Level
DS
Data Segment
DSM
Distributed Shared Memory
DVD
Digital Versatile Disc
EDF
Earliest Deadline First
EISA
Enhanced Industry Standard Architecture
EMS
Expanded Memory System
FAT
File Allocation Taste
FCFS
First-Come, First-Served
FIFO
First-In, First-Out
GDT
Global Descriptor Table
GDTR
Global Descriptor Table Register
GSM
Global System for Mobile Communication
Abkürzungen
HIMEM
High Memory
ISA
Industry Standard Architecture
KL
Kernel Level
LDT
Local Descriptor Table
LDTR
Local Descriptor Table Register
LRU
Least-Recently Used
MFC
Microsoft Foundation Classes
MMU
Memory Management Unit
MS-DOS
Microsoft Disc Operating System
NTFS
Microsoft Windows NT File System
PC
Program Counter (Programzähler)
PCB
Process Control Block
PCI
Peripheral Component Interconnect
PID
Process Identification
PSW
Processor Status Word (Prozessorzustandsregister)
RAID
Redundant Array of Independend Disks
RAM
Random-Access Memory
RISC
Reduced Instruction Set Computer
RMS
Rate Monotonie Scheduling
ROM
Read-Only Memory
RPC
Remote Procedure Call
RR
Round-Robin
SCSI
Small Computer System Interface
SJF
Shortest-Job-First
SP
Stack Pointer (Kellerregister)
SS
Stack Segment
TLB
Translation Lookaside Buffer
UL
User Level
VM
Virtual Machine
Literaturhinweise
[Accetta et al. 1986] M. Accetta, R. Baron, W. Balasky, D. Golub, R. Rashid, A. Tevanian, M. Young: Mach: A New Kernel Foundation for UNIX Development, in Summer USENIX Conference, Atlanta, 1986, pp. 93-112 [Andrews und Schneider 1983] G. R. Andrews, F. B. Schneider: Concepts and Notations for Concurrent Programming, ACM Computing Surveys Vol. 15, No. 1, 1983, pp. 3-43 [Assenmacher 1996] H. Assenmacher: Ein Architekturkonzept zum Entwurf flexibler Betriebssysteme, Dissertation am Fachbereich Informatik, Universität Kaiserslautern, Februar 1996 [Bach 1986] M. J. Bach: The Design ofthe UNIX Operating System, Prentice Hall, Englewood Cliffs, New Jersey, 1986 [Bargen und Donelly 1998] B. Bargen, P. Donelly: Inside DirectX, Microsoft Press, Redmond, Washington, 1998 [Belady 1966] L. A. Belady: A Study of Replacement Algorithms for a Virtual Storage Computer, IBM Systems Journal, Vol. 5, Nr. 2, 1966, pp. 78-101 [Belady et al. 1969] L. A. Belady, R. A. Nelson, G. S. Shedler: An Anomaly in Space-Time Characteristics of Certain Programs Running in a Paging Machine, CACM, Vol. 12, Nr. 6, 1969, pp. 349-353 [Ben-Ari 1990] M. Ben-Ari: Principles of Concurrent and Distributed Programming, Prentice Hall, Englewood Cliffs, New Jersey, 1990 [Birell und Nelson 1984] A. D. Birell, J. Nelson: Implementing Remote Procedure Calls, ACM Trans, on Computer Systems, Vol. 2, No. 1, 1984, pp. 39-59 [Borden et al. 1989] T. L. Borden, J. P. Hennessy, J. W. Rymarczyk: Multiple Operating Systems on one Processor Complex, IBM Systems Journal, Vol. 28, Nr. 1, 1989, pp. 104-123
Literaturhinweise [Bovet und Cesati 2001] D. P. Bovet und M. Cesati: Understanding the Linux Kernel, O'Reilly & Associates Inc. Sebastopol, 2001 [Brinch Hansen 1973] P. Brinch Hansen: Operating System Principles, Prentice Hall, Englewood Cliffs, New Jersey, 1973 [Brinch Hansen 1975] P. Brinch Hansen: The Programming Language Concurrent Pascal, IEEE Trans, on Software Engineering, Vol. 1, No. 2, 1975 [Chase et al. 1994] J. S. Chase, H. M. Levy, M. J. Feeley, E. D. Lazowska: Sharing and Protection in a Single-Adress-Space Operating System, ACM Transactions on Computer Systems Vol. 12, No. 4, 1994, pp. 271-307 [Chen et al. 1996] J. B. Chen, Y. Endo, K. Chan, D. Mazieres, A. Dias, M. Seltzer, M. D. Smith: The Measured Performance of Personal Computer Operating Systems, ACM Transactions on Computer Systems, Vol. 14, Nr. 1, 1996, pp. 3^10 [Cheriton 1984] D. Cheriton: The V-Kernel-A Software Base for Distributed Systems, IEEE Software 19, 1984, pp. 19-42 [Cheriton 1987] D. R. Cheriton: UIO: A Uniform I/O System Interface for Distributed Systems, ACM Trans, on Computer Systems, Vol. 5, 1987, No. 1, pp. 12-16 [Cheriton 1988] D. Cheriton: The V Distributed System, CACM 31, 1988, pp. 314-333 [Coffmann et al. 1971] E. G. Coffmann, M. J. Elphick, A. Shoshani: System Deadlocks, ACM Computing Surveys,Vol. 3, Nr. 2, 1971, pp. 67-78 [Cohen und Woodring 1998] A. Cohen, M. Woodring: Win32 Multithreaded O'Reilly & Associates Inc., Sebastopol, 1998
Programming,
[Conway 1963] M. Conway: Design of a separable Transition-Diagram Compiler, CACM, Vol. 6, 1963, pp. 396-408 [Corbin 1991] J. R. Corbin: The Art of Distributed Applications-Programming Techniques for Distributed Applications, Springer-Verlag, Berlin, 1991 [Courtois et al. 1971] P. J. Courtois, F. Heymans, D. L. Parnas: Concurrent Control with »Readers« und »Writers«, CACM 14, 1971, pp. 667-668
Literaturhinweise [Denning 1970] P. J. Denning: Virtual Memory, ACM Computing Surveys Vol. 2, No. 2, 1970, pp. 153-189 [Dijkstra 1968] E. W. Dijkstra: Cooperating Sequential Process, in F. Gennuys (Ed.) »Programming Languages», Academic Press, N. Y., 1968 [Dijkstra 1968a] E. W. Dijkstra: The Structure of the THE-Multiprogramming System, CACM 11, 1968, pp. 341-346 [Feittelson und Rudolph 1990] D. Feitelson, L. Rudolph: Mapping and Scheduling in a Shared Parallel Environment Using Distributed Hierarchical Control, Proc. 1990 Int. Conf. on Parallel Processing [Fitzerald und Rashid 1986] R. Fitzerald, R. F. Rashid: The Integration of Virtual Memory Management and Interprocess Communication in Accent, ACM Trans, on Computer Systems, Vol. 4, No. 2, 1986, pp. 147-177 [Fleisch und Popek 1989] B. D. Fleisch, G. J. Popek: Mirage: A Coherent Distributed Shared Memory Design, Proc. 12th ACM Symposium on Operating Systems Principles (Litchfield Park, Arizona), Operating Systems Review 23 (5), 1989, pp. 211-223 [Gallmeister 1995] B. O. Gallmeister: POSIX.4 - Programming for the real world, O'Reilly & Associates Inc., Sebastopol, 1995 [Gehringer et al. 1987] E. Gehringer, D. Siewiorek, Z. Segall: Parallel Processing: The Cm* Experience, Digitial Press, Bedford, MA, 1987 [Gosling et al. 1996] J. Gosling et al.: Java Programming Eanguage, SunSoft Press, 1996 [Graham und Denning 1972] G. S. Graham, P. J. Denning: Protection - Principles and Pracitice. In: Proc. 1972 AFIPS Spring Joint Computer Conference, Vol. 40, 1972 pp. 417-429 [Habermann 1969] A. N. Habermann: Prevention of System Deadlocks, CACM, Vol. 12, 1969, pp. 373-377 [Habermann et al. 1976] A. N. Habermann, L. Flou, L. Coopricher: Modularization and Hierarchy in a Family of Operating Systems, CACM 19, 1976, pp. 266-272 [Halsall 1992] F. Halsall: Data Communications, Computer Networks and Open Systems, Addison-Wesley Publishing Company, Reading, MA, 1992
Literaturhinweise [Hennessy und Patterson 1990] J. Hennessy, D. Patterson: Computer Architecture: A Quantitative Approach, Morgan Kaufmann, Palo Alto, CA, 1990 [Herrtwich und Hommel 1994] R. G. Herrtwich, G. Hommel: Nebenläufige Programme, Springer-Verlag, Berlin, 1994 [Hoare 1972] C. A. R. Hoare: Towards a Theory of Parallel Programming. In: Operating System Techniques, Hoare (Ed.), Academic Press, N.Y., 1972, pp. 61-71 [Hoare 1974] C. A. R. Hoare: Monitors: An Operating System Structuring Concept, CACM Vol. 17, 1974, pp. 549-557 [Hoare 1978] C. A. R. Hoare: Communicating Sequential Processes, CACM 21, 1978, pp. 666-677 [Holt 1972] R. C. Holt: Some Deadlock Properties of Computer Systems, ACM Computing Surveys,Vol. 4, Nr. 3, 1972, pp. 179-196 [Huck 1983] T. Huck: Comparative Analysis of Computer Arcbitectures, Technischer Bericht 83-243, Stanford University [ISO 8824] Abstract Syntax Notation 1 (ASN. 1) ISO Standard 8824 [Jones und Schwarz 1980] S. Jones, P. Schwarz: Experience Using Multiprocessor Systems - A Status Report, Computing Surveys, Juni 1980 [Lampson 1969] B. W. Lampson: Dynamic Protection Structures, Proc. 1969 AFIPS Fall Joint Computer Converence, Vol. 35, 1969, pp. 27-38 [Lampson 1973] B. W. Lampson: Protection, ACM SIGOPS Operating Systems Review, Vol. 8, 1974, pp. 18-24 [Lampson und Redeil 1980] B. W. Lampson, D. D. Redell: Experience with Processes and Monitors in Mesa, C A C M Vol. 2 3 , N o . 2, pp. 105-117, 1980 [Landwehr 1981] C. E. Landwehr: Formal Models for Computer Security, ACM Computing Surreys,Vol. 13, No. 3, 1981, pp. 247-278 [Liedtke 1995] J. Liedtke: On Micro-Kernel Construction, 15th ACM Symposium on Operating Systems Principles (Copper Mountain Resort, Colorado), Operating Systems Review Vol. 29, No. 5, 1995, pp. 237-250
üteraturhinweise [Liskov 1972] B. Liskov: The Design of the VENUS Operating System, CACM 15, 1972, pp. 144-149 ;
[Liskov 1979] B.Liskov, Primitives for Distributed Computing, Proc. of the 7th ACMSymposium on Operating Systems Principles, Asilomar, 1979, pp. 33-42
'
[Liskov 1985] B. Liskov: Limitation of Syncbronous Communication with Static Process Structure in Languages for Distributed Computing, Techn. Report CMU-CS-85-168, Carnegie-Mellon-University 1985 [Liu und Layland 1973] C. L. Liu, J. W. Layland: Scheduling Algorithms for Multiprogramming in a Hard Real-Time Environment, Journal of the ACM, Vol. 20, Nr. 1, 1973, pp. 46-61 [McKusik et al. 1996] M. K. McKusik, K. Bostic, M. J. Kareis, J. S. Quarterman, The Design and Implementation of the 4.4 BSD Operating System, AddisonWesley, Reading, MA., 1996 [Messmer 1995] H.-P. Messmer: PC-Hardwarebuch, Addison-Wesley, Bonn, 1995 [Mullender und Tanenbaum 1986] S. J. Mullender, A. S. Tanenbaum: The Design of a Capability-Based Distributed Operating System, The Computer Journal, Vol. 29, Nr. 4, 1986, pp. 289-300 [Nehmer 1975] J. Nehmer: Dispatcher Primitives for the Construction of Operating System Kernels, Acta Informatica 5, pp. 237-255 [Nehmer 1977] J. Nehmer: An Experimental Study of Processor Thrashing in Multiprocessor Systems, Computing 18, 1977, pp. 185-197 [Nehmer 1979] J. Nehmer: The Implementation of Concurrency for a PL/Tlike Language, Software-Practice and Experience Vol. 9, No. 12, 1979, pp. 1043-1057 [Nelson 1981] B. J. Nelson: Remote Procedure Call, Technischer Bericht CSL-81-9, Xerox Palo Alto Research Center, Palo Alto, CA., 1981 [Nichols et al. 1996] B. Nichols, D. Buttlar, J. P. Farrell: Pthreads Programming, O'Reilly & Associates Inc., Sebastopol, 1996 [Nitzberg und Lo 1991] B. Nitzberg, V Lo: Distributed Shared Memory: A Survey of Issues and Algorithms, IEEE Computer 24, No. 8, 1991, pp. 52-60
Literaturhinweise [Oney 1996] W. Oney: Systems Programming for Windows 95, Microsoft Press, Redmond, Washington, 1996 [Pate 1996] Steve D. Pate, UNIX Internais, Addison-Wesley, Reading, MA., 1996 [Patterson et al. 1987] D. A. Patterson, G. Gibson, R. H. Katz: A Case for Redundant Arrays of Inexpensive Disks (RAID), Technischer Bericht UCB/CSD 87/391, University of California, Berkeley [Patterson und Sequin 1982] D. Patterson, C. Sequin: A VLSI RISC, IEEE Computer, September 1982 [Peterson und Silberschatz 1985] J. L. Peterson, A. Silberschatz: Operating System Concepts, AddisonWesley, 2nd ed., Reading, MA., 1985 [Prosise 1996] Jeff Prosise, Programming Windows 95 with MFC, Microsoft Press, Redmond, Washington, 1996 [QNX 1993] QNX System Architecture, QNX Software Systems [Richter 1996] J. Richter: Advanced Windows, Microsoft Press, Redmond, Washington, 1996 [Richter 1999] J. Richter: Programming Applications for Microsoft Windows, Microsoft Press, Redmond, Washington, 1999 [Rossum 1993] G. van Rossum: An Introduction to Python for UNIX/C Programmers, Proc. NLUUG (Niederländische Unix User Group) [Sammer und Schwärtzel 1982] W. Sammer, H. Schwärtzel: CHILL - Eine moderne Programmiersprache für die Systemtechnik, Springer-Verlag, Berlin, 1982 [Shatz 1984] S. M. Shatz: Communication Mechanisms for Programming Distributed Systems, IEEE, Computer, Vol. 17, No. 6, 1984, pp. 21-29 [Schmidt 1976] H. A. Schmidt: On the Efficient Implementation of Conditional Critical Regions an the Construction of Monitors, Acta Informatica 6, 1976, pp. 227-249 [Solomon 1998] D. A. Solomon: Inside Windows NT, 2nd ed., Microsoft Press, Redmond, Washington, 1998
Literaturhinweise [Solomon und Russinovich 2000] D. A. Solomon, M. E. Russinovich: Inside Microsoft Windows 200(f, 3rd ed., Microsoft Press, Redmond, Washington, 2000 [Spare Int. 1992] The SPARC Architecture Manual, Version 8, Prentice Hall, Englewood Cliffs, New Jersey, 1992 [Stallings 1993] W. Stallings: Computer Organization and Architecture, Macmillan, London, 3rd edition, 1993 [Stevens 1992] W. Richard Stevens: Advanced Programming in the UNIX Environment, Addison-Wesley, Redmond, MA., 1992 [Stevens 1994] W. Richard Stevens: TCP/IP Illustrated, Volume 1 - The Protocols, Addison-Wesley, Redmond, MA., 1994 [Tanenbaum 1990] A. S. Tanenbaum: Structured Computer Organization, Prentice-Hall, Englewood Cliffs, New Jersey, 1990 [Tanenbaum 1992] A. S. Tanenbaum: Modern Operating Systems, -2nd ed., Prentice-Hall, Englewood Cliffs, New Jersey, 1992 [Tanenbaum und Renesse 1988] A.S. Tanenbaum, R. van Renesse: A Critique ofthe Remote Procedure Call Paradigm, in: Research into Networks and Distributed Applications, R. Speth (Ed.), Elsevier Science Publishers,1988, pp. 775-783 [Vahalia 1996] U. Vahalia: UNIX Internais - The new frontiers, Prentice-Hall, Englewood Cliffs, New Jersey, 1996 [Wall et al. 1996] L. Wall, R. L. Schwartz, T. Christiansen: Programming Perl, 2nd ed., O'Reilly & Associates Inc., Sebastopol, 1996 [Wettstein 1993] H. Wettstein: Systemarchitektur, Carl Hanser Verlag, München, 1993 [Zimmermann et al. 1981] H. Zimmermann, J. S. Banino, A. Caristan, M. Guillemont, G. Morisset: Basic Concepts for the Support of Distributed Systems: the Chorus Approach, Proc. 2nd ICDCS, pp. 60-66
Index
2nd-Chance-Verfahren 83 640-KByte-Grenze 54, 58
A Abhängige Server 209 Ablaufkonsistenz 235 Absoluter Dateiname 268 Abstrakte Maschine 315 Abstrakte Rechnerarchitektur 22 Abstraktes Gerät 284 Activity Working Set 135 Adreßabbildung - Segmentregister 60 - Seitenbasiert 65 - Seitenbasiert, Mehrstufig 70 Adreßbus 5 Adressierungsarten 7 Adreßraum 30, 3 1 , 4 1 -erzeugung 90 -große 46 -initialisierung 90 Aging-Techniken 118 Akkumulierende Belegung 238 Aktivitätsträger 30 Algorithmus zum Erkennen einer Verklemmung 247 Algorithmus zum Vermeiden von Verklemmungen 250 ALU 6 Anforderungsmatrix 247 Anforderungspaar 244 Antwortzeit 113 API 301 Arbeitsverzeichnis 271 Argument 304 Arithmetisch-logische Einheit 6
Asymmetrisches Multiprozessorsystem 21 Asynchrone Kommunikation 201 Asynchrone Meldung 202 Asynchrone Unterbrechung 8 Asynchroner Auftrag 205 Auftrag 201 Ausführungsmodus 9 Ausführungszeit 123 Aushungerung 118 Ausnahme 8 Authentisierung 295 Automatisch zurücksetzbares Event 197 Automatische Variable 44 Auto-Reset-Event 197
B Bad Block 275 Bank-Switching 57 Basisregister 60 Batchbetrieb 104 Batch-Datei 313 Bediengeräte 14 Bedienungsschnittstelle 301 Bedingte kritische Abschnitte (Semaphor) 169 Benutztpaar 244 Bereit-Liste 111 Bereitzeit 123 Best-Effort-Scheduling 130 Betriebsmittelverwaltung -Monitor 177, 179,180 - Semaphor 166 BIOS 11 Block 256
Index
Blockgranularität 257 BIockiert-Liste 111 Blockorientierte Geräte 18 Blockorientiertes Dateisystem 274, 276 Block-Special-File 290 BSD Unix 89, 120, 121 Busmaster-Fähigkeit 18
C Cache 11, 146 - Hit 12 - Line 13 - Miss 12 -D-Bit 66 -Kohärenz 21 -Temperatur 13, 106 Capability 296 CD-R 254 CD-ROM 254 CD-RW 254 Character-Special-File 290 c h d i r O 271 Client 317 Client-Stub 219 Client/Server-Architektur 317 Clock-Algorithmus 84 C l o s e D e v i c e () 285 c l o s e d i r ( ) 273 C l o s e H a n d l e O 194,262 c l o s e O 188,262 Cluster 275 Clustering 277 Condition-Variable 175, 195 Conditionvariable.SIGNAL() 176, 184 Conditionvariable.WAIT() 175, 184 C o n n e c t D e v i c e ( ) 288 Copy-on-Write 91, 94, 137, 225 Coroutinen 108 Co-Scheduling 134 CPU -Auslastung 112 -Burst56, 101, 117 -Monopolisierung 103
C r e a t e D e v i c e ( ) 286 C r e a t e E v e n t 0 197 C r e a t e F i l e M a p p i n g ( ) 265 C r e a t e F i l e O 261, 265 C r e a t e M u t e x O 191 C r e a t e P o r t ( ) 213 C r e a t e P r o c e s s O 137,304,305 C r e a t e S e m a p h o r e ( ) 194 C r e a t e T h r e a d O 138 C r e a t e O 91, 137
D Dämon-Paging 87 Datei 255, 259 -attribut 273 -nutzung 257 -System 37, 253 -Verwaltung 274, 278 -Zugriff über Read und Write 263 Daten-Cache 13 Datenbereich 43 Datenbus 6 Datenhaltung 28, 37 Datenträgerorganisation 274 D-Bit 66 DCB 285 Deadline 123 Deadlock 235 Dedizierte Geräte-API 290 Dedizierte Prozessorzuordnung 135 Deferred Write 12 Deferred-Write-Cache 21 Dekker-Algorithmus 159 D e l e t e D e v i c e ( ) 286 D e l e t e P o r t ( ) 213 Demand-Paging 85 Dereferenzieren eines Null-Zeigers 89 Device-Control-Block 285 Dialogbetrieb 104 Dienstprogramm 304 Dining-Philosopher-Problem 239 Directory 267 DirectX 291 Disable-Instruktion 161 Diskettenlaufwerk 254
Index
Dispatcher 112 Dispatcher für Monoprozessorsystem 147 D i s p a t c h e r . A d d ( ) 148 D i s p a t c h e r . A s s i g n ( ) 149 D i s p a t c h e r . B l o c k t ( ) 149 D i s p a t c h e r . R e a d y ( ) 149, 151 D i s p a t c h e r . R e s i g n ( ) 149 D i s p a t c h e r . R e t i r e ( ) 148 DLL 93 DMA 18, 56 Drag&Drop-Operation 303 DSM-Konzept 34 dup2 () 308 Durchsatz 112 Dynamisch ladbare Bibliothek 93 Dynamische Seitenersetzung 76 Dynamischer Datenbereich 44
Erkennen einer Verklemmung 247 Erweitertes Signal 227, 228 Erweitertes Zustandsmodell 110 Erzeuger-Verbraucher-System (Semaphor) 165 Event 195, 197 Exception 8 Execution Time 123 e x e c v e ( ) 138,305,308 exec() 92,304 E x i t P r o c e s s () 143 E x i t T h r e a d O 141 e x i t () 143 Explizite Prozeßinteraktion 33 Explorer 312 Externe Fragmentierung 48 Externe Speicher 14 Externer Pager 324
E
F
Echtzeitanwendung 38 Echtzeitbetrieb 105 Echtzeit-Scheduling 122 EDF-Scheduling 127 Ein- und Ausgabe (E/A) 283 - Register 15 - Adreßbereich 15 - Burst 56 - B u s 16, 289 - Bus-Controller 17 - Controller 15, 41, 94 - Datenregister 16 - Einbettung in das Dateisystem 289 - Geräte 14 - Kommandoregister 15 - Statusregister 15 -umlenkung 306 Einblenden 42 Einblendtechniken 258 Einfaches Zustandsmodell 109 EISA-Bus 18 EMS 59 Enable-Instruktion 161 E n t e r C r i t i c a l S e c t i o n ( ) 190 Environment-Variable 306 Ereignisgesteuerte Ausführung 125
FAT 275 FAT32 275 FCFS-Scheduling 113,132 Feedback-Scheduling 119 Fehlerhafte kritische Abschnitte 236 Festplatte 253 FIFO-Verdrängungsverfahren 82 Folder 267 f o r k ( ) 91, 137,143,304,308 Formales Modell nach Holt 241 Fragmentierung 48, 277 F r e i g ä b e t ) 153,155,157,158, 159, 160, 161, 162 Friendly Semaphor 192 Frist 123 f t r u n c a t e ( ) 187
G Gang-Scheduling 134 GDT 63 GDTR 63, 65 Gegenläufige Schachtelung kritischer Abschnitte 237 Gemeinsame Bibliothek 93 Gerätemanagement 28, 37 Geräteserver 287
Index
g e t c w d ( ) 271 G e t E x i t C o d e T h r e a d ( ) 145 G e t F i l e S i z e ( ) 265 G e t P r i o r i t y C l a s s ( ) 140 g e t p r i o r i t y ( ) 140 G e t T h r e a d P r i o r i t y ( ) 140 Globale Kachelzuteilung 86 Globale Verdrängungsstrategie 87 Graphische Shell 302 Grenzregister 60 Größenwachstum 44 Gruppenkommunikation 218 Gruppen-Scheduling 134 Guard 92
H HandlungsVorschrift 30 Header-Datei 301 Heap 44, 46 Hintergrundbetrieb 105 Hintergrundverarbeitung 309 Home-Verzeichnis 271
I Icon 302 Implizite Prozeßinteraktion 33 Instruktion-Cache 13 Instruktions-Pipeline 19 Instruktionssatz 6 Interne Fragmentierung 48 Interrupt 8, 288 Interruptpriorität 17 ISA-Bus 18 I/O-Burst 101 I/O-Locking 94
K Kachel 77 Kachelzuteilung 86 Kanal 216 Kellerregister 6 Kern 39, 317, 318 Kern ohne E/A-Unterstützung 324 Kernel-Level-Thread 106 kill 311
k i l l ( ) 228,229 KL-Thread 106, 107, 170 Koallokation 277 Kombination Segment- und Seitenadressierung 75 Kommandozeilenoption 304 Konkurrenz 32 Konkurrenzsituation 157 Kontextwechsel 100, 104, 117, 145 Kontrollfluß 97 Konvoi-Effekt 132 -FCFS-Schedulingll4 Kooperation 32 Kooperatives Scheduling 114 Kritische Instanz 130 Kritischer Abschnitt 153, 237 - Semaphor 165
L Laufwerksbuchstabe 269 Laufzeitbibliothek 317 Laufzeitfehler 41 Laufzeitkeller 43, 44, 45, 46 Laufzeitmodell 27, 29, 30 Laufzeitmodell A 34 Laufzeitmodell B 35, 199 Laufzeitmodell C 35, 199 Laufzeitmodell D 36, 99 Laufzeitplattform 315 Laufzeitsystem 30 LDT64 LDTR 63, 65 L e a v e C r i t i c a l S e c t i o n ( ) 190 Level-1-Cache 13 Level-2-Cache 14 Light-Weight-Thread 108 Link 268 L i s t . G e t ( ) 148 L i s t . P u t ( ) 148 Load-Sharing-Scheduling 133 Lock() 162 Logbasierte Dateisysteme 280 Login-Prozeß 297 Login-Shell 309 Logischer Adreßraum 42 Lokale Kachelzuteilung 86
Index
Lokale Variable 44 Lokalitätsmenge 80 l o n g j m p ( ) 150 Long-Term-Scheduler 112 LRU82 - Näherungsverfahren 83 - Verdrängungsverfahren 82 l s e e k ( ) 264 L2-Cache 147
M Magnetband 254 m a i n ( ) 141,305 Manuell zurücksetzbares Event 197 MapViewOfFile() 265 Mehrbenutzerbetrieb 27 Memory Management Unit 42, 67, 70, 72, 92, 133, 147 Memory Mapped File 264 Message Queue 227, 231 MFC-Bibliothek 259 Mikrokern 40, 318,319 Minimalkern 327 Mirroring 281 m k f i f o ( ) 231 m l o c k a l l () 95 m l o c k ( ) 95 mmap() 187,266 MMU 42, 67, 70, 72, 92, 133, 147 MO-Laufwerk 254 Monitor 174 Monitor-Implementierung 182 Monitorprozedur 174 Monolithischer Ansatz 315 Monopolisierung 30, 118 Monoprozessor-Scheduling 112 Mounting 270 m p r o t e c t ( ) 188 m q _ g e t a t t r ( ) 233 m q _ n o t i f y ( ) 234 mq_open() 232 m g _ r e c e i v e ( ) 233 mq_send() 233 m q _ s e t a t . t r () 233 MS-DOS 54, 58, 275 Multilevel-Feedback-Scheduling 119 Multilevel-Scheduling 118
Multimedia 258 Multiprozessor-Scheduling 131 Multiprozessorsysteme 20 Multi-Tasking-System 309 Multi-User-System 309 m u n l o c k ( ) 95 raunraap() 188 MO 22 - Maschine 23 M O . C o n t e x t R e s t o r e () 23 M O . C o n t e x t S a v e ( ) 23 MO.DefaultlSR() 24 MO.InBlock() 25 MO . I n t e r r u p t D i s a b l e () 24 M O . I n t e r r u p t E n a b l e ( ) 24 MO.In() 25 MO.OutBlockO 25 MO.Out() 25 MO.RaiseO 24 M O . R e g i s t e r l S R O 24 M O . R e g i s t e r R e s t o r e ( ) 23 M O . R e g i s t e r S a v e O 23 M O . U n r e g i s t e r l S R O 24
N Nachricht 199 Nachrichtengekoppelter Prozeß 35 Nachrichtengekoppeltes Team 35 Nachrichtenkommunikation 199 Named Pipe 230 Nanokern 318 Navigation über den Dateiname 272 Nebenläufiger Kern 322 Nebenläufiges Verarbeitungsmodell 97 Nebenläufigkeit 19, 20, 101 Netzadapter 14 Nichtblockierender Kern 325 Nichtpreemptives Scheduling 104 Normalmodus 9 NTFS281 Nukleus 149, 322, 323 Nulldeskriptor 67 Nullprozeß 149 Null-Zeiger 89 Nutzungscharakteristik von Dateien 257
Index
O Offline-Scheduling 126 Öffnen einer Datei 260 OpenDevice() 285 opendir() 273 OpenSemaphore() 194 o p e n ( ) 231,260 Operating System Personality 319 Operation 242 Optimale Verdrängungsverfahren nach Belady 81 Out-of-band-Nachricht 232 Overlay-Technik 57
P Parent Directory 267 Partition 269 PATH-Variable 306 P-Bit 66, 69, 76 PCB 110, 309 PCI-Bus 18 Periode 124 Persistente Speicherung 253 Peterson-Algorithmus 160 Phase 124 Physischer Adreßraum 10, 41, 53 PID 111 Pikokern318 Pipe 227, 229, 307 p i p e ( ) 230,308 Polling 27 P-Operation 163 Port 213 POSIX 186,260,262, 263, 266,273 POSIX.4 95, 130, 140, 187, 193, 226, 228 POSIX.4a 137, 139, 142, 144, 152, 189, 195 Preemptives Scheduling 104 Prefetching 256 Pre-Paging 85 Prioritätsbasiertes Scheduling 117 - Dynamisch 117 -Statisch 117 Prioritätsvererbung 173 Privilegierte Instruktion 27
Privilegierter Modus 9 Privilegierungsstufe 63 Process Identification 111 Programm 6 Programmcode 43 Programmzähler 6 Prozeß 30 -erzeugungsbaum 298 -gruppe 311 -interaktion 28, 30, 32 -kontrollblock 110 -kooperation 32, 157 -management 28, 30 -Synchronisation 32 -terminierung 311 -Verwaltung 309 Prozessor 5, 6 -burst 56 -zustandsregister 7 Pthread 139, 142, 152, 189, 195 p t h r e a d _ c a n c e l ( ) 142 pthread_cond_broadcast() 196 p t h r e a d _ c o n d _ s i g n a l ( ) 196 p t h r e a d _ c o n d _ w a i t ( ) 196 p t h r e a d _ c r e a t e ( ) 139 p t h r e a d _ d e t a c h ( ) 144 p t h r e a d _ e x i t ( ) 141 p t h r e a d _ j o i n ( ) 144 pthread_mutexattr_init() 190 pthread_mutexattr_setshared () 190 p t h r e a d _ m u t e x _ i n i t () 189, 190 pthread__mutex_lock () 190, 196 p t h r e a d _ m u t e x _ t r y l o c k ( ) 190 p t h r e a d _ m u t e x _ u n l o c k ( ) 190, 196 p t h r e a d _ t e s t c a n c e l ( ) 142 P u l s e E v e n t ( ) 198
Q Quittung 201
Index
R RAID-Ansatz 280 RAM 10 R-Bit 66 r e a d d i r ( ) 273 Reader-Writer-Problem -Monitor 180, 181 - Semaphor 167 R e a d F i l e ( ) 264 Ready Time 123 r e a d ( ) 230,263,285 Realzeit 113 R e c e i v e O 199,214,222 Rechnend-Liste 111 Reentrantfähig 192 Referenzlokalität 12, 19 Referenzstring 79 Registersatz 6 R e l e a s e M u t e x O 191 R e l e a s e S e m a p h o r e ( ) 194, 195 r e n i c e 0 140 R e s e t E v e n t () 198 Resource-Allocation-Graph 244 Ressourcen-Pool 207 RISC-Prozessor 146 RM-Scheduling 129 ROM 11 Root Directory 267 Root Partition 270 Rotierende Berechtigung 159 RPC 218 RR-Scheduling 116 Rücksprungadresse 8
S Schachtelung kritischer Abschnitte 237 Scheduler 100, 112 Scheduling - Best-Effort 130 - Echtzeit 122 - EDF 127 -FCFS 113,132 -Feedback 119 - Gang 134 - Gruppen 134
-Kooperativ 114 - Load-Sharing 133 - Monoprozessor 112 -Multilevel 118 - Multilevel-Feedback 119 - Multiprozessor 131 - Nichtpreemptives 104 - Offline 126 - Preemptives 104 - Prioritätsbasiertes 117 - RM 129 - R R 116 -SJF 115 -Strategie 112 - UNIX 120 - Windows 122 -ziel 104 sched_get_priority_max() 131 sched_get_priority_min() 131 s c h e d _ s e t s c h e d u l e r ( ) 140 s c h e d _ y i e l d ( ) 141 Schichtenmodell 274 Schichtenstruktur 316 Schließen einer Datei 262 Schutz -alarm 294 -domäne 293 - in UNIX 298 -matrix 293 -monitor 295 -system 294 -Verletzung 49 Schwaches Echtzeitsystem 123 Schwergewichtiger Thread 106 SCSI-Bus 18 Segment -basierter virtueller Adreßraum 59 -deskriptor 62 -deskriptor, Intel 80x86 62 -register 59 -tabelle 62 Seiten -basierter virtueller Adreßraum 65
Index
-deskriptor 65 -flattern 87 -nachschubverfahren 85 -verdrängungsverfahren 80 Seitenfehler - Interrupt 77 -rate 86 -Wahrscheinlichkeit 77 Seitentabelle 65 - Einstufig 67 - Mehrstufig 68 -deskriptor 68 Semaphor 163, 192 s e m _ c l o s e ( ) 193 s e m _ d e s t r o y ( ) 193 s e m _ i n i t ( ) 193 sem_open() 193 s e m p o s t ( ) 193, 194 s e m _ t r y w a i t ( ) 194 s e m _ u n l i n k ( ) 193 s e m _ w a i t ( ) 193, 194,251 Send() 199,214 Sequentielles Verarbeitungsmodell 97 Serialisierung 259 Serieller Kern 321 Server 39, 317 Server-Pipeline 211 Server-Stub 219 Session 311 S e t E v e n t ( ) 198 S e t F i l e P o i n t e r ( ) 264 SetGroupId-Bit 300 s e t j m p ( ) 150 S e t P r i o r i t y C l a s s ( ) 140 S e t T h r e a d P r i o r i t y ( ) 140 SetUserld-Bit 300 Shared Library 93 Shared Memory 185 Shell 301 shm_open() 187 shm__unlink() 189 Short-Term-Scheduler 112 s i g a c t i o n O 227,229 Signal 222, 226, 227 S i g n a l R e t u r n ( ) 223 Signal-Variante I 176, 183
Signal-Variante II 176, 183 Signal-Variante III 177, 183 S i g n a l () 222 s i g q u e u e ( ) 229 Single-Level-Store 38, 281 Single-Tasking-System 309 Single-User-System 309 SJF-Scheduling 115 Skript-Datei 313 Smart Semaphor 192 Snoopy Cache 21 SPARC-MMU 73 Speicherabbildungstabelle 52 Speicherauslagerung 47 Speicherbasierte Ein- und Ausgabe 16,289 Speichereinbettung eines Kerns 320 Speichereinblendung von Dateien 264 Speichergekoppelter Prozeß 34 Speicherhierarchie 13 Speicherpyramide 14 S p e r r e n d 153,155,157,158, 159, 160, 161, 162 Sperrflag 153 Sporadisches Ereignis 125 Sprungbefehl 7 Stack 43 Standardausgabe 306 Standardeingabe 306 Stapelbetrieb 104 Start neuer Prozesse 304 Startzeit 124 Starvation 118 Statischer Datenbereich 44 Steuerbus 5 Steuerwerk 6 Striktes Echtzeitsystem 122 Striping 280 Strom 218 Subdirectory 267 Subjekt 294 Superblock 276 Superuser 297, 309 Swapping 55, 88, 110 Symmetrisches Multiprozessorsystem 21
Index
Synchrone Kommunikation 201 Synchrone Meldung 203 Synchrone Unterbrechung 8 Synchroner Auftrag 204 Synchronisation 153 Synchronisationsfehler 235 Synchronisationsmechanismen 158 Synchronität 201 Systembedienung 28 Systemkern 317
UNIX System V 193 UNIX-Dateisystem 278 UNIX-Scheduling 120 UNIX-Shell 308 Unterbrechungen 8 Unterbrechungsroutine 9 Unterbrechungssprungtabelle 9 Unterprogrammsprung 8 Unterverzeichnis 267 Urprozeß 297 User-Level-Thread 106
T Task Manager 311 Team 34 T e r m i n a t e T h r e a d ( ) 141 Textuelle Shell 301 Thrashing 87 Thread 31, 97 -Kontrollblock 111 - Package 108 -Prozessor-Bindung 135 - Prozessor-Zuordnung 133 Timer-Interrupt 103 TLB 71, 146 - Hit 71 - Invalidierung 106 - Miss 71 - Trefferrate 72 Totaler Verklemmungszustand 243 Trap 40, 170, 315 Trap-Befehl 8 Traversieren von Verzeichnisstrukturen 272 Turnaround 112
V Vaterverzeichnis 267 Verankerung virtueller Adreßbereiche 94 Verbindung 216 Verklemmung 235 Verklemmungsbedingungen 246 Verklemmungszustand 242 Vermeiden von Verklemmungen 250 Verzeichnis 255, 267 Virtuelle Adresse 65 - Seitenbasiert, Mehrstufig 69 Virtuelles Gerät 283 Virtueller Adreßraum 42 Virtueller Multiprozessor 99 Virtueller Prozessor 99 Virtueller Speicher 31, 76 VM 319 Von-Neumann-Architektur 5 V-Operation 163 Vordergrundverarbeitung 309
W U Überlappende Adreßräume 36, 93 Überschneidung 46 Übertragungsbandbreite 258 UL-Thread 106, 107, 171 UL-Thread-Package 150 Unbenannte Pipe 230 Undefinierte Adresse 41 Universalgerät 286 UNIX 143,150,152,229,271,278, 289,298,304,310
Wahlfreier Zugriff auf persistente Daten 258 WaitForMultipleObjects() 144 W a i t F o r S i n g l e O b j e c t ( ) 144, 191,194, 195, 198 w a i t p i d ( ) 143 w a i t ( ) 143 w a i t 3 ( ) 143 Wartezeit 113 Wartezyklen 11
Index
Wechselseitiger Ausschluß 189 Windows NT/2000 88, 90, 91, 92, 122,137,186,191,195,261, 269,281,304,309,311 Windows 3.x 114 Windows 9x 88, 90, 91, 92, 94, 122, 137, 186, 191, 195,261,269, 275,290,302,304,309,311, 312 Windows-Scheduling 122 Win32-API 99, 137, 140, 141, 143, 144, 186, 189, 190,194, 197, 259, 261, 262, 264, 265, 305 Working Directory 271 Working-Set 85 Working-Set-Modell 79 W r i t e F i l e ( ) 264 Write Through Cache 12, 21 w r i t e ( ) 229,263,285 Wurzelverzeichnis 267, 270
Y y i e l d ( ) 150
Z Zeichenorientierte Geräte 18 Zeitabhängigkeit 235 zeitgesteuerte Ausführung 125 Zeitquantum 116 Zeitscheibe 116 Zeitvorgabe (Formalisiert) 123 Zirkuläre Wartebedingung 235 Zombie-Prozeß 144 Zugang zum Dateisystem 312 Zugriffsart 293 Zugriffscharakteristik 49 Zugriffsgranularität 257 Zugriffskontrolliste 296 Zugriffsrecht 294 Zugriffszeit externer Speicher 254 Zustand - Blockiert 242 - Sicher 242 - Verklemmt 242 Zustandsmodell 108 Zuteilungsmatrix 247 Zwangsserialisierung 157 Zyklisches Erzeuger-VerbraucherSystem 240 Zylinder 253
Jürgen Nehmer • Peter Sturm
Systemsoftware - Grundlagen moderner Betriebssysteme Die Betriebssysteme alter Prägung mit ihrer monolithischen Struktur und einer fest vorgegebenen Funktionalität sind im Aussterben begriffen. Heute kommen an ihrer Stelle kernbasierte Laufzeitsysteme mit einem offenen, leicht erweiterbaren Funktionssatz zum Einsatz, bei denen die Grenze zwischen Anwendung und Betriebssystem verschwindet. Mit dem Begriff »Systemsoftware« tragen die Verfasser dieser Entwicklung Rechnung und verbinden damit auch ein Architekturkonzept, das die Bereitstellung maßgeschneiderter Laufzeitsysteme für Anwendungen gestattet. Dieses Lehrbuch vermittelt Organisations- und Architekturprinzipien moderner Betriebssysteme und deckt den Vorlesungsstoff entsprechender Veranstaltungen im Informatikstudium ab. Dabei fließen die umfassenden Erfahrungen der Autoren aus ihrer langjährigen Lehrtätigkeit an der Hochschule ein. Anhand zahlreicher Beispiele wird in dieser zweiten, aktualisierten Auflage die Realisierung dieser Prinzipien in aktuellen Betriebssystemen wie Unix, Linux und Windows gezeigt. Das Buch wendet sich an Leser, die an einer fundierten, praxisorientierten Einführung in die Architektur und Funktionsweise moderner Betriebssysteme interessiert sind.
ISBN 3-89864-115-5