199 103 16MB
German Pages 371 [384] Year 1990
Programmierung komplexer Systeme 5 Herausgegeben von Fevzi Belli und Hinrich E. G. Bonin
Ulrich Hoffmann
Einführung in die systemnahe Programmierung Anwenderprogramme und Datenstrukturen
W Walter de Gruyter G Berlin · New York 1990 DE
Dr. Ulrich Hoffmann Professor fur Systemprogrammierung und DV-Systeme an der Fachhochschule Nordostniedersachsen, Lüneburg
Das Buch enthält 136 Abbildungen.
CIP-Kurztitelaufnahme der Deutschen Bibliothek
Hoffmann, Ulrich Einführung in die systemnahe Programmierung: Anwenderprogramme und Datenstrukturen / Ulrich Hoffmann. - Berlin ; New York : de Gruyter, 1990 (Programmierung komplexer Systeme ; 5) ISBN 3-11-012466-1 NE: GT
Θ
Gedruckt auf säurefreiem Papier, das die US-ANSI-Norm über Haltbarkeit erfüllt.
© Copyright 1990 by Walter de Gruyter & Co., D-1000 Berlin 30. Dieses Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Jede Verwertung außerhalb der engen Grenzen des Urheberrechtsgesetzes ist ohne Zustimmung des Verlages unzulässig und strafbar. Das gilt insbesondere für Vervielfältigung, Übersetzungen, Mikroverfilmungen und die Einspeicherung und Verarbeitung in elektronischen Systemen. Druck: WB-Druck GmbH, Rieden am Forggensee. - Bindung: Heinz Stein, Berlin. Einbandentwurf: Johannes Rother, Berlin. -Textillustrationen: David Andersen, Berlin.
Vorwort
Üblicherweise assoziiert man noch heute mit „systemnaher" Programmierung umfangreiche Assemblerprogramme. Grund ist einerseits die intensive Interaktion dieser Programme mit der jeweiligen Hardware und andererseits die traditionell hierfür geschaffene Nomenklatur der „Bit- und Byte-Welt". Das systemnahe Programmieren lebt von und mit dem Architekturmodell des vorgesehenen Zielrechners. Auch effiziente Anwendungsprogramme bedingen eine umfassende Kenntnis der jeweiligen Rechnerarchitektur. Der heutige massenhafte Einsatz von Mikrorechnern bewirkt eine Renaissance der systemnahen Programmierung, genannt „Mikroprogammierung". Bewährte Konzepte und Erfahrungen aus der System- und systemnahen Programmierung von Großrechnern („Mainframes") und Prozeßrechnern sind auch für die Anwendungsprogrammierung auf Mikrorechnern nützlich. Ein Ziel des vorliegenden Buches ist die Vermittlung einer integrierten Sicht dieser anscheinend (noch) getrennten „Welten" der Mikro-, Mini- und Großrechner. Ein Schwerpunkt dabei ist die zweckmäßige Strukturierung von Programmen und Daten, sowie z.B. die eingehend erläuterten Mechanismen zur Modulverknüpfung und zur Parameterübergabe. Das Buch vermittelt die Konzepte systemnaher Programmierung exemplarisch anhand der beiden Rechnerarchitekturen SIEMENS 7.500 (bzw. IBM/ 370) und INTEL 80386. Die Beispiele, insbesondere aus der kommerziellen Programmierung, sind auch in den höheren problemorientierten Sprachen COBOL und Pascal formuliert. Es ist ein Verdienst dieses Buches, die modernen Konzepte der Objektorientierung in Verbindung mit den klassischen Konzepten leicht verständlich darzustellen. Damit wird auch eine „goldene Brücke" zu Konzepten des Software-Engineering tragfahig gebaut. Insgesamt verdeutlicht dieses Buch das Ziel der vorliegenden Reihe „Programmierung komplexer Systeme". Es trägt dazu bei, die Konstruktion komplexer Programme aus systemnaher Sicht rationell und planbar zu gestalten. Paderborn/Lüneburg, August 1990
Fevzi Belli Hinrich E. G. Bonin
Inhalt 1 Einleitung
1
2 Die klassische Rechnerarchitektur 2.1 Beispiel der Rechnerarchitektur eines Großrechners 2.2 Beispiel der Rechnerarchitektur eines Mikrocomputers
4 11 27
3 Aspekte der Programmerstellung 3.1 Übersetzen und (statisches) Binden von Programmen 3.2 Laufzeit-Layout des virtuellen Adreßraums 3.3 Dynamische Bindemethoden 3.4 Mehrfach-benutzbarer Code 3.5 Units in TURBO-Pascal
37 41 59 66 71 80
4 Datenobjekte 4.1 Datenobjekte in ASSEMBLER 4.2 Datenobjekte in Pascal 4.3 Datenobjekte in der objektorientierten Programmierung
87 95 96 108
5 Interne Datendarstellung 5.1 Datentyp Zahl 5.2 Datentyp Zeichenkette 5.3 Datentyp Binärwert 5.4 Datentyp Adresse
133 137 148 149 150
6 Datendefinitionen in ASSEMBLER 6.1 Einfache Datenobjekte 6.2 Strukturierte Datenobjekte 6.3 Selbstdefinierte Datentypen 6.4 Typkompatibilität
154 155 165 179 207
7 Datendefinitionen in höheren Programmiersprachen 7.1 Ordinal-, Real- und Stringtypen 7.2 Strukturierte Datentypen 7.3 Pointertypen 7.4 Prozedurtypen 7.5 Typkompatibilität
210 211 214 220 225 226
8 Programmverknüpfung 8.1 Unterprogramme 8.2 Methoden der Parameterübergabe an Unterprogramme
229 231 239
8.3 Eine standardisierte Implementierung der Parameterübergabe an Unterprogramme 8.4 Programmverknüpfung bei statischen Programmiersprachen 8.5 Stackorientierte Sprachen 8.6 Programmverknüpfung bei stackorientierten Programmiersprachen 9 Mechanismen zur Programmverknüpfung mit Diensten des Betriebssystems 9.1 Betriebssystemdienste 9.2 Anschluß an ein ASSEMBLER-Programm 9.3 Anschluß an ein Programm in einer höheren Programmiersprache
248 255 268 275 284 288 292 297
10 Speicherauszug
299
11 Anwendungsorientierte Datenstrukturen in der systemnahen Programmierung 11.1 Lineare Datenstrukturen 11.2 Nichtlineare Datenstrukturen 11.3 Realisierung von Dateien in Form eines B*-Baums
308 314 321 344
12 Literatur
363
Stichwortverzeichnis
366
Abbildungsverzeichnis Abb. 2-1: Schichtenmodell eines Rechnersystems Abb. 2-2: Schematischer Aufbau einer Rechenanlage Abb. 2.1-1: Funktionszustandsübergänge (SIEMENS 7.500) Abb. 2.1-2: Registersätze (SIEMENS 7.500) Abb. 2.1-3: Virtuelle Adressen (SIEMENS 7.500) Abb. 2.1-4: Adreßumsetzung (SIEMENS 7.500) Abb. 2.1-5: Speicherklassen im ΒS2000 Abb. 2.2-1: Registersatz (INTEL 80386) Abb. 2.2-2: Adreßumsetzung INTEL 80386) Abb. 2.2-3: Privilegstufen (INTEL 80386) Abb. 2.2-4: Gate-Mechanismus (INTEL 80386) Abb. 3-1: Beispiel: Übersetzung einer COBOL-Anweisung Abb. 3-2: Beispiel: Übersetzung einer ASSEMBLER-Anweisung Abb. 3-3: HOL, Assembler-Sprache, Maschinensprache Abb. 3.1-1: Externe Referenzen Abb. 3.1-2: Zugriff auf externe Referenzen Abb. 3.1-3: Adressierung in einem Modul (ASSEMBLER) Abb. 3.1-4: Adressierung in einem Modul (ASSEMBLER) Abb. 3.1-5: Objektmoduln vor und nach dem Binden Abb. 3.1-6: Unterprogramme des Laufzeitsystems COBOL85 Abb. 3.2-1: Aufrufe externer Programme Abb. 3.2-2: Laufzeit-Layout: virtueller Adreßraum (Pascal) Abb. 3.3-1: Statisches Binden Abb. 3.3-2: Dynamisches Binden Abb. 3.4-1: Parallel mehrfach-benutzbarer (reentrant) Code Abb. 3.4-2: Sharing auf Seitenebene beim Paging Abb. 3.4-3: Sharing auf Segmentebene beim Paging Abb. 3.5-1: Syntaktischer Aufbau einer Unit in TURBO-Pascal Abb. 3.5-2: Abhängige Units (Beispiel) Abb. 4-1: Charakterisierung eines Datenobjekts Abb. 4-2: Datenobjekt (Pascal-ASSEMBLER) Abb. 4-3: Datenobjekt (COBOL-ASSEMBLER) Abb. 4.2-1: Syntaktische Form eines Pascal-Programms Abb. 4.2-2: Syntaktische Form einer Pascal-Prozedur Abb. 4.2-3: Blockstruktur eines Pascal-Programms (Beispiel) Abb. 4.2-4: Gültigkeitsbereich von Bezeichnern (Beispiel) Abb. 4.2-5: Lebensdauer von Datenobjekten (Beispiel) Abb. 4.3-1: Blöcke (Beispiel) Abb. 4.3-2: Objekte in der objektorienten Programmierung Abb. 4.3-3: Statische Vererbung Abb. 4.3-4: Virtuelle Methoden Abb. 4.3-5: Virtuelle Methodentabelle Abb. 5-1: Byte-Inhalt Abb. 5-2: Struktur des virtuellen Adreßraums Abb. 5.1-1: Festpunktzahlen in einem Wort Abb. 5.1-2: Zusammenhang zwischen Exponent u. Charakteristik
4 5 14 16 21 24 26 29 32 34 36 38 40 40 45 49 52 54 56 59 61 64 67 70 73 76 79 82 85 89 92 95 98 98 100 104 106 109 117 123 126 127 134 136 138 143
Abb. 5.1-3: Datentyp Gleitpunktzahl Abb. 5.1-4: Datentypen Zahl Abb. 5.2-1: EBCDI-Code Abb. 5.4-1: Datentyp Adresse Abb. 6.1-1: DC-Anweisung in ASSEMBLER Abb. 6.1-2: Bedeutung der Typangabe in der DC-Anweisung Abb. 6.1-3: Definition von Datenobjekten (Beispiel) Abb. 6.2-1: Strukturiertes Datenobjekt in ASSEMBLER Abb. 6.2-2: Fehlerhafte Definition eines str. Datenobjekts Abb. 6.2-3: CNOP-Anweisung Abb. 6.2-4: Tiefer strukturiertes Datenobjekt in ASSEMBLER Abb. 6.2-5: Speicherbereich POOL (Beispiel) Abb. 6.2-6: Strukturierung in ASSEMBLER (Forts. Abb. 6.2-4) Abb. 6.2-7: Bitkombinationen in ASSEMBLER (Beispiel) Abb. 6.3-1: Pseudoabschnitte in ASSEMBLER Abb. 6.3-2: Verwendung eines Pseudoabschnitts (Beispiel) Abb. 6.3-3: Operationen auf verketteten Datenobjekten Abb. 6.3-4: Condition Code Abb. 6.3-5: Der BC-Befehl in ASSEMBLER Abb. 6.3-6: Basisoperationen auf einer Adreßverweiskette Abb. 6.3-7: Tabellenverarbeitung in ASSEMBLER (Beispiel) Abb. 7.1-1: Aufzählungstyp Abb. 7.1-2: Teilbereichstyp Abb. 7.2-1: RECORD-Typ in Pascal (Beispiel) Abb. 7.3-1: Belegung des Heaps (Beispiel) Abb. 7.5-1: Kompatibilitätsregeln von Datentypen (Pascal) Abb. 8.1-1: Unterprogrammaufrufe mit Parametern Abb. 8.1-2: Prozedurspezifikation in COBOL Abb. 8.1-3: Prozedurspezifikation in Pascal Abb. 8.2-1: Graphische Darstellung eines Datenobjekts Abb. 8.2-2: call-by-reference-Methode Abb. 8.2-3: call-by-reference-Parameter als Aktualparameter Abb. 8.2-4: call-by-value-Methode Abb. 8.2-5: call-by-result-Methode Abb. 8.2-6: call-by-value-result-Methode Abb. 8.2-7: Parameterübergabemethoden (Beispiel) Abb. 8.3-1: Standardparameterübergabe Abb. 8.3-2: ASSEMBLER-Unterprogramm mit Parametern Abb. 8.4-1: Aufbau der Savearea (Standardlink-Konvention) Abb. 8.4-2: Aufruffolge mit Standardlink-Konvention Abb. 8.4-3: ASSEMBLER-Realisierung des Standardlinks Abb. 8.4-4: Standardlink (Beispiel) Abb. 8.4-5: Rekursiver Prozeduraufruf (bei Standardlink) Abb. 8.5-1: Stackbelegung bei einer Aufruffolge Abb. 8.5-2: Stackbelegung bei einer rekursiven Prozedur Abb. 8.6-1: Layout eines Stackeintrags Abb. 8.6-2: Programmverknüpfung (stackorientierte Sprache) Abb. 9-1: Busy Waiting
144 147 149 151 157 160 163 166 167 168 169 171 173 177 181 183 189 194 195 200 205 213 213 217 224 228 233 234 238 240 240 241 243 244 244 247 249 254 257 259 263 266 267 272 274 279 282 285
Abb. 9-2: Realisierung des Busy-Waitings in ASSEMBLER Abb. 9.2-1: SVC in ASSEMBLER (Beispiel) Abb. 9.2-2: Systemmakro in ASSEMBLER (Beispiel) Abb. 10-1: Aufrufhierarchie (Beispiel) Abb. 10-2: Speicherauszug (Beispiel) Abb. 10-3: Aufbau des PC-Eintrags im Dump Abb. 10-4: IW-Eintrag im Dump Abb. 10-5: Verkettung der Saveareas Abb. 11-1: Anwendung des abstrakten Datentyps stack Abb. 11-2: Eine Realisierung des abstrakten Datentyps stack Abb. 11-3: Altern. Realisierung des Datentyps stack Abb. 11-4: Verwendung abstrakter Datentypen Abb. 11.1-1: Operation show für Stack-Implementationen Abb. 11.1-2: Schlange als rückwärts-verkettete Liste Abb. 11.2-1: Binärer Suchbaum Abb. 11.2-2: Delete_bin in einem binären Suchbaum Abb. 11.2-3: Binärer Suchbaum nach einem delete_bin- Aufruf Abb. 11.2-4: Prioritätsschlange als binärer Suchbaum Abb. 11.2-5: Operation show auf einem Binärbaum Abb. 11.2-6: Breitensuche in einem Binärbaum Abb. 11.2-7: AVL-Baum (Beispiel) Abb. 11.2-8: Komplexität bei binären Suchbäumen Abb. 11.3-1: Knoten eines B*-Baums Abb. 11.3-2: Überlaufbehandlung eines Datenblocks Abb. 11.3-3: Überlaufbehandlung eines Indexblocks Abb. 11.3-4: Unterlaufbehandlung eines Datenblocks Abb. 11.3-5: Beispiel eines B*-Baums Abb. 11.3-6: Mengengerüst eines B*-Baums
287 296 297 300 303 304 305 307 310 311 312 314 317 321 325 326 327 334 337 341 342 343 347 350 352 355 359 360
1 Einleitung
1
1 Einleitung Die vorliegende Einfährung in die systemnahe Programmierung führt im Untertitel die Begriffe Anwenderprogramme und Datenstrukturen. Damit werden die Schwerpunkte angesprochen, nämlich die Behandlung der maschinennahen Darstellung von Datenobjekten, die gemäß (durch die verwendete Programmiersprache) vorgegebener oder selbstdefinierter Datentypen strukturiert sind die Darstellung intern verwendeter Mechanismen zur Umsetzung der Programmverknüpfung getrennt übersetzter Programme die Darstellung spezieller Methoden der systemnahen Programmierung und der dabei vorkommenden Datenstrukturen. Ziel des Buches ist nicht die Entwicklung von Systemsoftware wie Betriebssysteme einschließlich der dazugehörigen Dienstprogramme, Datenbankmanagementsysteme, Transaktionsmonitore, Compiler (vgl. dazu [DAL88], [DON84]) usw. Es vermittelt die systemnahen Kenntnisse, die in der Anwendungsprogrammierung von Bedeutung sind. Einige grundlegende Mechanismen in Betriebssystemen werden in diesem Zusammenhang behandelt. Mit dem Begriff der systemnahen Programmierung wird die Programmierung in einer Assembler-Sprache, also in einer auf einen speziellen Maschinentyp abgestellten Sprache, assoziiert. Systemnahe Programme erfordern eben den direkten Zugriff auf die darunterliegende Hardware, und Zeit- und Speichereffizienz sind dabei grundlegende Faktoren. Seitdem systemnahe Software mit Hilfe geeigneter höherer Programmiersprachen formuliert wird, geht die Bedeutung der Assembler-Programmierung auch in der Systemprogrammierung zurück. Folglich konzentriert sich der Assembler-Anteil im vorliegenden Buch auf die Darstellung von Datenstrukturen; einige wichtige Assembler-Befehle zur Formulierung grundlegender Algorithmen werden jedoch behandelt. Insofern liefert das Buch einen Beitrag zur Einführung in die Assembler-Programmierung. Ein Ziel höherer Programmiersprachen mit Sicht auf den Anwendungsprogrammierer besteht darin, ihn nicht mit maschinennahen Details der Programmierung zu belasten. Trotzdem sollte sich ein Anwendungsprogrammierer gelegentlich mit diesen maschinennahen Details befassen: Dabei kommt es weniger auf die Realisierung eines Programmablaufs in der jeweiligen AssemblerSprache an, als vielmehr auf die Kenntnis der Umsetzung von Datenstrukturen,
2
1 Einleitung
die in höheren Programmiersprachen vom Anwender definiert werden, in maschinennahe Darstellungsform. In der Regel gibt es viele Realisierungsmöglichkeiten dieser Umsetzung, und im Einzelfall hängt das Ergebnis vom jeweiligen Sprachübersetzer oder der verwendeten Hardwareumgebung ab. Die hier beschriebenen Verfahren stellen insofern Beispiele dar, die hinreichend allgemein und verallgemeinerbar sind. Letztlich soll ihre Kenntnis dazu beitragen, effiziente Anwendungsprogramme zu schreiben. In der Testphase eines Programmsystems wird auch der Anwendungsprogrammierer mit dem Lesen eines Speicherauszugs konfrontiert, und er muß wissen, wie die definierten Datenstrukturen in maschinensprachliche Formulierungen umgesetzt wurden. Die Notwendigkeit, gelegentlich Anwendungen in unterschiedlichen Programmiersprachen zu realisieren, erfordert das Wissen über Programmverknüpfungs- und Parameterübergabemechanismen bzw. über die festgelegten Konventionen. Das Buch stellt eine Verbindung zwischen Datendefinitionen, Datenstrukturen und Programmverknüpfungen in höheren Programmiersprachen und den darunterliegenden maschinennahen Implementierungen und Methoden her. Es zeigt, daß auch in maschinennaher Programmierung ein hoher Grad der Datenstrukturierung möglich ist. Wichtige Informationsquellen für diese Verbindungen sind im allgemeinen die Handbücher der jeweilig verwendeten Programmiersprache für den jeweiligen Rechner. Es bestehen Abhängigkeiten zwischen Compiler-(versionen) der Programmiersprache und Maschinentypen bzw. Betriebssystemen. Das Buch stellt die wesentlichen Prinzipien dar, die verschiedenen Implementierungen zugrunde liegen. Ausgehend von dieser Zielsetzung ist es erforderlich, sich mit rechnerinternen Details der Informationsdarstellung und ansatzweise mit dem logischen Aufbau eines Rechners und seiner Komponenten zu beschäftigen. Als Beispiel für eine Großrechnerarchitektur werden hierzu Bytemaschinen vom Typ der Systemfamilie SIEMENS 7 500 (mit einem zu den Serien IBM /360, IBM /370 usw. ähnlichen Instruktionssatz) gewählt. Beispiele für Datenstrukturierungen in einer maschinenorientierten Sprache sind in ASSEMBLER, der Assembler-Sprache dieser Rechnerfamilien, formuliert. Gelegentlich wird auf Mikroprozessoren, in diesem Fall dem INTEL 80386, eingegangen.
1 Einleitung
3
Zur Formulierung von Datenstrukturen in höheren Programmiersprachen wird in den Beispielen Pascal verwendet, gelegentlich auch COBOL. Pascal wurde aufgrund der weiten Verbreitung dieser Sprache gewählt. Die Klarheit des Sprachkonzepts (und damit eine gewisse Ästhetik) und die gute Lesbarkeit der Quellprogramme unterscheidet Pascal von anderen Programmiersprachen. Einschränkungen bezüglich des Sprachumfangs entfallen weitgehend bei Wahl eines geeigneten Pascal-Dialekts. COBOL wurde aufgrund der Häufigkeit gewählt, in der diese Sprache in kommerziellen Anwendungen eingesetzt wird. Die Darstellung von Programmverknüpfungsmechanismen, von Parameterübergabeverfahren und die Interpretation von Speicherauszügen erfolgt anhand von ASSEMBLER-Beispielen bzw. an Beispielen in höheren Programmiersprachen (Pascal als Vertreter stackorientierter Sprachen und COBOL als Vertreter statischer Sprachen). Konzepte der objektorientierten Programmierung und die Realisierung abstrakter Datentypen werden mit Hilfe des Pascal-Dialekts TURBO-Pascal (ab Version 5.5) erläutert. Das Buch ist kein Lehrbuch einer speziellen Programmiersprache. Es zeigt allgemeingültige sprachübergreifende Methoden. Die Programmiersprachen dienen lediglich der exakten Formulierung dieser Mechanismen. Pascal- oder COBOL-Kenntnisse werden beim Leser nur in geringem Umfang vorausgesetzt. Zum Verständnis des Buchinhalts genügen Programmiererfahrungen im Umfang einer Einführungsveranstaltung zum Informatikstudium. ASSEMBLERKenntnisse werden vom Leser nicht erwartet, sondern werden anhand zahlreicher ausführlich beschriebener Beispiele vermittelt. Das gleiche gilt für spezielle Hardware- und Betriebssystemkenntnisse. Der Inhalt des Buches beruht auf Vorlesungen, die der Verfasser seit mehreren Jahren in den Fächern Systemprogrammierung und Datenverarbeitungssysteme an der Fachhochschule Nordostniedersachsen Lüneburg in der Ausbildung für Wirtschaftsinformatiker im Hauptstudium hält. Das Buch ist nicht nur Wirtschaftsinformatikstudenten nützlich, sondern richtet sich an jeden praxisorientierten Informatiker.
2 Die klassische Rechnerarchitektur
4
2 Die klassische Rechnerarchitektur Ein Rechnersystem läßt sich in Form eines Schichtenmodells darstellen. Die dabei verwendete Terminologie ist in der Literatur nicht einheitlich; es sollen hier die in Abbildung 2-1 aufgeführten Begriffe verwendet werden. Die Hardware des Rechners ist den meisten Anwendern bis auf die Bedienung bestimmter Eingabe- und Ausgabegeräte wie Tastatur, Bildschirmgeräte oder Drucker gar nicht direkt zugänglich. Auch ein Systemprogrammierer kommuniziert weniger mit einem Rechner als vielmehr mit dem um die Hardware gelegten Betriebssystem. Die Funktionen der in Abbildung 2-1 gezeigten inneren Schichten werden in diesem Kapitel erläutert, wobei besonderes Gewicht auf die Hardwarearchitektur und grundlegende Funktionen eines Betriebssystems gelegt wird. Anwendungsprogrammierer
Systemprogrammierer
Abbildung 2-1: Schichtenmodell eines Rechnersystems
5
2 Die klassische Rechnerarchitektur
Die Hardware klassischer konventioneller Rechner orientiert sich an der vottNeumann-Architektur, deren Schema in Abbildung 2-2 dargestellt ist. Diese Aussage gilt insbesondere für die in kommerziellen Anwendungen vorkommenden Rechner. Zu dieser klassischen Rechnerarchitektur alternative Rechnerarchitekturen, wie man sie z.B. bei Parallelrechnern, Datenflußrechnern, systolischen Feldern usw. findet, weichen aufgrund ihrer Zielsetzung mehr oder weniger von diesem Konzept ab. Die Funktionen der einzelnen Komponenten und ihr Zusammenspiel bilden den Inhalt der folgenden Abschnitte.
Peripherie externer Speicher, Massenspeicher
Arbeitsspeicher (Hauptspeicher) Eingabeeinheit
Ausgabeeinheit
ZZ1
Rechanrafllsler
Anzeigenregister
Steuerung
Prozessor, C P U
Die Pfeile deuten die Datenpfade an
Zentraleinheit
Abbildung 2-2: Schematischer Aufbau einer Rechenanlage Der Arbeitsspeicher (Hauptspeicher) besteht aus fortlaufend numerierten Speicherplätzen. Die Nummer des Speicherplatzes ist seine (physikalische) Adresse. Ein Speicherplatz ist die kleinste ansprechbare Einheit, d.h. eine Adresse identifiziert einen Speicherplatz als Ganzes und nicht etwa Teile davon. Jeder Speicherplatz hat dieselbe Größe; üblicherweise ist diese Größe ein Byte bestehend aus 8 Bits. Speicherplätze enthalten Daten oder (Maschinen-) Instruktionen. Eine Instruktion belegt im allgemeinen mehrere hintereinanderliegende Speicherplätze. Die Anzahl zusammenhängender Speicherplätze für
6
2 Die klassische Rechnerarchitektur
Daten hängt von deren Typ ab. Die Interpretation eines Speicherplatzinhalts als Daten bzw. Instruktionen, also Programmteile, wird vom jeweiligen Programm bzw. der Steuerung des Prozessors bewerkstelligt. Die Aufgabe des Prozessors (CPU, central processing unit) besteht in der Ausführung des im Arbeitsspeicher enthaltenen Programms. Dieses ist in Form von Maschineninstruktionen (Maschinenbefehlen) geladen. Die Steuerung liest und interpretiert nacheinander die Instruktionen und führt sie aus. Dabei werden bei der Ausführung einer Anweisung meist Daten aus dem Arbeitsspeicher in ein oder mehrere Rechenregister geladen, über arithmetische oder logische Operationen miteinander verknüpft und die Resultate in Rechenregistern oder Speicherzellen des Arbeitsspeichers abgelegt. Die Adressen, an denen die Daten einer Operation stehen oder an die die Ergebnisse der Operation im Arbeitsspeicher abgelegt werden, heißen Operandenadressen. Die Operandenadressen für eine Operation werden je nach Rechner- und Befehlstyp durch den Inhalt von Rechenregistern oder speziellen Adressierungsregistern, durch Speicherplatzinhalte oder durch direkte Zahlenangaben bestimmt. Adressierungstechniken werden im folgenden noch genauer behandelt. Ein klassischer konventioneller von-Neumann-Rechner verfügt über verschiedene Typen von Instruktionen: Befehle für arithmetische und logische Verknüpfung von Operanden Transportbefehle für Inhaltsübertragungen zwischen Arbeitsspeicherplätzen bzw. Rechenregistern Befehle für den Vergleich von Inhalten einzelner Arbeitsspeicherplätze bzw. von Inhalten der Rechenregister Sprungbefehle zur Änderung des rein sequentiellen Ablaufs der Befehlsausführung Befehle zum Laden und zur Auswertung der Anzeigenregister (siehe unten) privilegierte Befehle, die für Anwendungsprogramme normalerweise nicht zur Verfügung stehen, da sie den momentanen Hardwarezustand der Anlage entscheidend dadurch verändern, daß sie Inhalte von Anzeigenregistern modifizieren
2 Die klassische Rechnerarchitektur
7
privilegierte Eingabe- und Ausgabebefehle für die Vermittlung des Verkehrs mit der Peripherie spezielle maschinenabhängige Sonderbefehle. Die Anzeigenregister enthalten Informationen über die Ausführung einer Operation den aktuellen Zustand des Prozessors. Beispielsweise wird das Ergebnis des wertmäßigen Vergleichs zweier Operanden durch Setzen einer definierten Bitkombination in einem dieser Register angezeigt. Diese Anzeigen können vom Programm ausgewertet werden, und es kann je nach Anzeige verschieden reagieren. Einige typische Anzeigen führen zu einem Programmabbruch durch den Prozessor. Sie beschreiben dann einen "irregulären" Zustand des Prozessors, der aufgrund der letzten Befehlsausführung entstand. Zusammenfassend geben die Anzeigen Auskunft über: das Ergebnis des wertmäßigen Vergleichs von Operanden das Ergebnis von arithmetischen und logischen Operationen, z.B. ob das Resultat einer Addition kleiner, größer oder gleich Null ist arithmetische Zahlenbereichsüberläufe als Ergebnis einer Operation Datenformatfehler bei arithmetischen Operationen Divisionsfehler (z.B. Division durch Null) während einer Operation den Versuch, eine dem Rechner nicht bekannte (nicht-entschlüsselbare) oder dem Anwender zur Zeit nicht-erlaubte (privilegierte) Operation auszuführen ungültige, aber trotzdem verwendete Operandenadressen. Bei einigen dieser Situationen ist es möglich, den automatischen Programmabbruch zu verhindern, indem diese Absicht in weiteren Anzeigenregistern per Programm durch sogenannte Masken (das sind spezielle fest definierte Zeichenkombinationen) festgehalten wird. Eine Maske gibt an, auf welche Anzeige sofort, erst später oder gar nicht reagiert werden soll.
2 Die klassische Rechnerarchitektur
8
Ein besonders zu erwähnendes Anzeigenregister ist das Befehlszählerregister (Befehlszähler, instruction count, program count), das jeweils die Adresse des nächsten auszuführenden Befehls enthält. Dieses Register wird bei der Ausführung eines Befehls von der Steuerung mit der Adresse des logisch nächsten Befehls geladen. Der sequentielle Ablauf der Befehlsausführung kann durch Sprungbefehle oder Interrupts (Unterbrechungen) beeinflußt werden: Ein Sprungbefehl bewirkt, daß die Adresse eines Befehls, der im Hauptspeicher nicht unmittelbar auf den letzten Befehl folgt, in das Befehlszählerregister geladen wird und daß das Programm dann an dieser Adresse fortgesetzt wird. Diese Änderung des rein sequentiellen Ablaufs erfolgt also aufgrund der Programmlogik und kann als eine zum Programmablauf synchrone Operation angesehen werden. Die Änderung des Inhalts des Befehlszählerregisters ist einem Anwender nur indirekt durch die Verwendung eines Sprungbefehls und nicht etwa durch ein direktes Laden dieses Registers möglich. Ein Interrupt dagegen wird seltener durch die Logik eines laufenden Programms als durch ein externes Ereignis hervorgerufen. Der Zeitpunkt, an dem der Interrupt auftritt, ist vom Programm nicht vorhersehbar. Ein Interrupt stellt daher ein zum Programmablauf asynchrones Ereignis dar. Prinzipiell hat ein Interrupt die gleiche Wirkung wie ein Sprungbefehl mit der Ausnahme, daß die auf den Interrupt folgenden Befehle durch spezielle Interruptroutinen des Benutzers oder als Teil des Betriebssystems festgelegt sind und nicht zum normalen Programmablauf gehören. Nach Behandlung des Interrupts wird das unterbrochene Programm meist an der Unterbrechungsstelle fortgesetzt. Durch Sonderbefehle kann aus der Programmlogik heraus ein Interrupt erzeugt werden. Dieser Interrupt übergibt die Kontrolle an das Betriebssystem, das direkten Zugriff auf alle Rechnerkomponenten hat. Die möglichen Unterbrechungsursachen sind für jeden Rechnertyp fest definiert. Dabei kann es sich beispielsweise handeln um einen von der Hardware der Peripherie erzeugten Interrupt, z.B. Rückmeldung einer simultan zum Prozessor ausgeführten Ein- oder Ausgabeoperation einen vom Betriebssystems des Rechners erzeugten Interrupt, z.B. als Mitteilung über die Beendigung einer Betriebssystem-Dienstleistung einen vom Programm erzeugten Interrupt
2 Die klassische Rechnerarchitektur
9
einen von einem Zeitgeber erzeugten Interrupt einen Hardwarefehler. Insgesamt werden der Typ und die Arbeitsweise eines Prozessors charakterisiert durch die Anzahl, die Größe und den Verwendungszweck von Rechen- und Anzeigenregistern den Befehlsvorrat die Adressierungstechnik des Arbeitsspeichers die Operationsgeschwindigkeit die Unterbrechungsmöglichkeiten und -behandlung. Die Eingabeeinheit, die Ausgabeeinheit und externe Massenspeicher sind über genormte Schnittstellen angeschlossen. Bei Großrechnern erfolgt dieser Anschluß meist über die sogenannten Kanäle oder E/A-Systeme, die selbständige Rechner darstellen und simultan zur Zentraleinheit operieren. Bei der Busarchitektur (z.B. bei Personalcomputern) werden die peripheren Geräte direkt an den internen Bus angeschlossen. Die gegenseitige physikalische Anpassung eines Geräts an die standardisierte Schnittstelle übernehmen gerätespezifische Controller. Mit Hilfe von Treiberprogrammen als Teil des Betriebssystems werden die Geräte über den jeweiligen Controller angesteuert. Im Einzelfall kann sich die Rechnerarchitektur von dem in Abbildung 2-2 dargestellten Konzept unterscheiden. Meist wird der Arbeitsspeicherzugriff durch zusätzliche Hardwareeinrichtungen unterstützt, da hier ein kritischer Performance-Engpaß eines Rechners liegt. Beispielsweise können vor Ausführung einer Maschineninstruktion der Steuereinheit bereits weitere Befehle bereitgestellt werden (Befehls-Pipelining). Die Adressierung des Arbeitsspeichers erfolgt häufig nicht direkt, sondern es wird eine Adreßumrechnung zwischengeschaltet, die durch schnelle Speichereinheiten (Cache-Speicher) zwischen Prozessor und Arbeitsspeicher unterstützt wird. Zusätzliche Komponenten wie komplexe Bussysteme für Daten und Adressen, mehrfache Prozessoren und Prozessoren für spezielle Aufgaben (z.B. für die Gleitpunktarithmetik), zusätzliche Arbeitsspeichersteuerungen usw. erhöhen die Leistungsfähigkeit eines Rechners.
10
2 Die klassische Rechnerarchitektur
Die prinzipiellen Funktionen der um die Hardware eines Rechnersystems liegenden Schichten (vgl. Abbildung 2-1) lassen sich wie folgt beschreiben. Der Betriebssystemkern verwaltet die einzelnen Komponeneten der Hardware wie Prozessor, Arbeitsspeicher, Peripheriegeräte, logische Dateien (den einzelnen Peripheriegeräten zugeordnete Namen) und Prozesse. Er den stellt den weiter außen liegenden Schichten eine einheitliche Schnittstelle zur Hardware bereit und liefert Statusinformationen über die Hardware, die über definierte Softwareschnittstellen angesteuert werden kann. Funktionen zur Prozeßkommunikation und Prozeßsynchronisation sind ebenfalls Teil des Betriebssystemkerns. Typische Teile des Betriebssystemkerns sind Treiber für Prozessor, Hauptspeicher, Kanäle, Peripheriegeräte, Programme für das Prozeßmanagement, die Adreßraumverwaltung (siehe auch Kapitel 2.1 und 2.2) und zur Auslastungsoptimierung der Hardware. Die Betriebssystem-Basissoftware verwaltet komplexe logische Funktionsbereiche oberhalb des Betriebssystemkerns und ist wie der Betriebssystemkern selbst hardware-abhängig. Ein allen Anwendern direkt zugänglicher Teil des Betriebssystems, der Kommandointerpreter zur Analyse und Ausführungsinitialisierung eines Benutzerkommandos, kann als Teil der Betriebssystem-Basissoftware angesehen werden. Weitere typische Teile sind Auftragsverwaltung (Jobmanagement) bei Großrechnern, Dateimanagement, Datenkommunikationssystem, Transaktionssystem und Systemadministrationssystem (Accounting). Die Basissoftware umfaßt eine Menge systemnaher anwendungsneutraler Programme, die im wesentlichen Hilfsmittel zur Programmentwicklung darstellen. Dazu gehören Binder, Übersetzungsprogramme für Programmiersprachen, Datenbankverwaltungssysteme, Transaktionsmonitore, Texteditoren, Dienstprogrammen zur Datensicherung und -archivierung, Sortierprogramme und Analyseprogramme zur Interpretation eines Speicherauszugs. Die Anwendungsprogramme lassen sich als anwendungsbezogene systemneutrale Softwareteile charakterisieren. Zwei Beispiele für Rechnerarchitekturen, auf die im Buch Bezug genommen wird, werden näher betrachtet. Sie unterscheiden sich in den Registersätzen und in der Adressierung und Verwendung des Arbeitsspeichers. Entsprechend werden sie mit vollständig unterschiedlichen Betriebssystemen betrieben. Das erste
2.1 Beispiel der Rechnerarchitektur eines Großrechners
11
Beispiel eines Prozessors aus der SIEMENS 7'500-Systemfamilie ist typisch für einen Großrechner. Die rechnerinterne Informationsdarstellung, die Adressierungsarten und die Möglichkeiten der Datenstrukturierung bei maschinennaher Programmierung wird im folgenden in den Beispielen hauptsächlich an diesem Rechnertyp gezeigt. Die Sprachelemente der zugehörigen Assembler-Sprache ASSEMBLER werden, soweit es erforderlich ist, erklärt. Das zweite Beispiel des INTEL-80386-Prozessors steht für heutige Mikroprozessoren z.B. in Personalcomputern.
2.1 Beispiel der Rechnerarchitektur eines Großrechners Stellvertretend für Großrechner werden in diesem Kapitel Komponenten eines typischen Rechners aus der SIEMENS 7500-Systemfamilie skizziert (vgl. [SIE87], [STI88]). Die interne Datenrepräsentation bei diesem Rechnertyp entspricht derjenigen der IBM-Großrechner. Insbesondere stimmen die Assembler-Sprachen überein, die für die Großrechner dieser Hersteller eingesetzt werden. Ein Rechner der SIEMENS 7 500-Systemfamilie wird mit einem UniversalBetriebssystem betrieben, dem BS2000 ([DED87]). An einigen Stellen wird für das korrekte Funktionieren des Prozessors eines Rechners dieser Systemfamilie vorausgesetzt, daß das Betriebssystem Tabellen aufbaut und pflegt. Die entsprechenden Funktionen hierzu sind sämtlich im Betriebssystemkern des BS2000 angesiedelt. Die SIEMENS 7500-Systemfamilie umfaßt Rechner bestehend aus unterschiedlichen Komponenten. Die verschiedenen Prozessoren unterscheiden sich z.B. in der Zahl und Technologie der einzelnen Hardwarekomponenten, im physikalischen Aufbau der Steuerung, in der Breite der Datenwege und in der Verarbeitungsgeschwindigkeit der einzelnen Instruktionen. Je nach Typ gibt es Einfach- oder Mehrfachprozessoren, physikalisch mehrfach vorhandene Arbeitsspeicher (Multiprozessortechnik) oder eine Reihe von Hardwarekomponenten, die den Zugriff auf den Arbeitsspeicher beschleunigen. Eine physikalische Adresse besteht aus 24, 31 oder 32 Bits. In den logischen Funktionen eines Prozessors, der Menge der Register, der Instruktionssätze und der Adressierungstechnik des Arbeitsspeichers gibt es keine wesentlichen Unterschiede.
12
2 Die klassische Rechnerarchitektur
Die Zentraleinheit wird für wichtige Funktionen durch einen zusätzlichen Serviceprozessor unterstützt. Seine Aufgabe besteht im Rücksetzen, Starten und Initialisieren der CPU im Fall eines Systemabsturzes und zum Anhalten und Wiederstarten des eigenen oder anderer Zentralprozessoren, zur Konfiguration von Hardwarekomponenten sowie zur Sicherstellung von Diagnoseinformationen für Prozessoren. Außerdem stellt er verschiedene Zeitgeber zur Verfügung. Die Zentraleinheit verarbeitet Maschinenbefehle, die sich in einer Privilegierungsstufe unterscheiden. Privilegierte Befehle ermöglichen im wesentlichen schreibende und lesende Zugriffe auf die Anzeigenregister und verändern so den momentanen logischen Zustand des Prozessors. Nicht-privilegierte Befehle verwenden Inhalte von Rechenregistern oder beziehen sich auf Operanden im Arbeitsspeicher. Die Anzeigen, die durch einen nicht-privilegierten Befehl direkt veränderbar sind, beschreiben Masken zur Behandlung arithmetischer Überlaufsituationen. Bei einigen Prozessortypen der Rechnerfamilie, die Multiprocessing zulassen, gibt es zudem semi-privilegierte Befehle', sie dienen in erster Linie zur Unterstützung hardwaremäßig vorhandener mehrfacher Adreßräume; ihre Ausführung bedarf definierter Privilegierungsstufen. Grundsätzlich unterscheidet man zwei Maschinenzustände des Prozessors: den Anwenderzustand und den Systemzustand. Die Unterscheidung richtet sich danach, welche Maschinenbefehle (alle oder nur nicht-privilegierte Befehle) zur Ausführung erlaubt sind. Der jeweilige Maschinenzustand wird in einem Anzeigenregister festgehalten und kann von einem Programm nur durch privilegierte Befehle geändert werden. Im Systemzustand sind alle Maschinenbefehle gültig, insbesondere die privilegierten Befehle, die Befehle zur Ein- und Ausgabe und die Befehle, die Arbeitsspeicherschutzschlüssel und Zeitgeberwerte setzen und ändern. Das Betriebssystem des Rechners läuft im Systemzustand, da es direkten Zugriff auf alle Rechnerkomponenten haben muß. Im Anwenderzustand sind nur die nicht-privilegierten Befehle gültig. Die Verwendung eines privilegierten Befehls im Anwenderzustand oder eines semi-privilegierten Befehls bei unzureichender Privilegierungsstufe im Anwenderzustand führt zu einem Interrupt. Ein Programm im Anwenderzustand
2.1 Beispiel der Rechnerarchitektur eines Großrechners
13
kann nicht von sich aus in den Systemzustand wechseln, da hierzu ein privilegierter Befehl notwendig ist, der sich auf ein Anzeigenregister bezieht. Um trotzdem Funktionen auszuführen, die nur im Systemzustand bereitstehen (z.B. Ein- und Ausgabeoperationen, Starten eines Zeitgebers oder Interprozeßkommunikation), kann aus dem Anwenderzustand mit Hilfe des nicht-privilegierten Maschinenbefehls SVC (SUPERVISOR CALL) die Kontrolle an das Betriebssystem übergeben werden, das dann einen entsprechenden Auftrag ausführt. Das Betriebssystem kann dazu auch privilegierte Befehle einsetzen. Vor der Rückkehr in das Anwenderprogramm wird dann in den vorigen Maschinenzustand zurückgeschaltet. Die Unterscheidung von System- und Anwenderzustand findet man bei vielen Rechnerfamilien auch anderer Hersteller. Eine SIEMENS 7500-Familienspezifische Eigenschaft ist die Unterscheidung von Funktionszuständen des Prozessors: Der Prozessor befindet sich zu jedem Zeitpunkt in genau einem von vier Funktionszuständen, die mit PI, P2, P3 und P4 bezeichnet werden. Das Diagramm der Funktionszustände und ihrer Übergänge zeigt Abbildung 2.1-1. Die Zustände PI und P2 steuern den normalen Programmablauf: Im Zustand PI werden Anwendungsprogramme (im Anwender- oder Systemzustand) bearbeitet; im Zustand P2 laufen die Interrupt-Bearbeitungsroutinen. Die Zustände P3 und P4 dienen der Interrupt-Analyse: Der Zustand P3 analysiert programmbezogene Interrupts, der Zustand P4 Interrupts, die durch definierte Maschinenfehler erzeugt wurden. Bei den meisten festgestellten Maschinenfehlern läuft der Prozessor auf einen Haltezustand. Das Umschalten zwischen den Funktionszuständen erfolgt automatisch durch einen Interrupt oder durch den privilegierten Befehl PC (WECHSELN FUNKTIONSZUSTAND). Bei einem Interrupt, der im Funktionszustand PI auftaucht, oder bei einem Interrupt im Funktionszustand P2, also während einer Interrupt-Behandlung selbst, wechselt der Prozessor automatisch in den Funktionszustand P3 oder P4 (bei Maschinenfehler). Interrupts im Zustand P3 werden als Maschinenfehler interpretiert und führen in den Zustand P4. Die Rückkehr in den unterbrochenen Zustand erfolgt durch den privilegierten Befehl PC (WECHSELN FUNKTIONSZUSTAND). Ein Programm, das im Anwenderzustand läuft, kann nicht von sich aus den Funktionszustand wechseln, da der dazu notwendige Maschinenbefehl privilegiert ist. Es kann jedoch durch eine Betriebssystemaufruf (SVC-Befehl) einen Interrupt erzeugen und so z.B. die Kontrolle an das Betriebssystem abgeben.
2 Die klassische Rechnerarchitektur
14
f P1 [ \
Λ
Anwendungsprogramm
'
( I \ \ \
P2 Λ
/ Ι /
InterruptBearbeitungsroutine
I \
I I
/
' L ·
P3 Τ Analyse eines programmbezogenen Interrupts
P4 Λ
( | I
J /
m
Analyse eines Maschinenfehiers
1 \ \
] j /
\
Übergang aufgrund eines Interrupts Übergang aufgrund des Befehls W E C H S E L N FUNKTIONSZUSTAND
Abbildung 2.1-1: Funktionszustandsübergänge (SIEMENS 7 500) Jeder Funktionszustand verfügt hardwaremäßig über einen eigenen Registersatz (Rechen- und Anzeigenregister). Insbesondere hat jeder Funktionszustand ein eigenes Befehlszählerregister und kann daher unabhängig von den anderen Funktionszuständen Programme im Prozessor steuern und ausführen. Durch diese Unabhängigkeit können die verschiedenen Interrupts effektiv analysiert und bearbeitet werden, ohne daß vor der Ausführung einer Interrupt-Analyse bzw. -Bearbeitungsroutine oder bei der Rückkehr ins unterbrochene Programm Informationen aus den Registern im Arbeitsspeicher zwischengespeichert zu werden brauchen. Der Prozessor wird jeweils durch die Register des aktuellen Funktionszustands gesteuert. Inhalte von Registern inaktiver Funktionszustände liegen außerhalb der Zugriffsmöglichkeit eines Funktionszustands, d.h. sie beeinflussen den Prozessor nicht, insbesondere können sie auch nicht verändert werden. Eine Ausnahme bildet eine Menge von allen Funktionszuständen gemeinsamen Registern, die sowohl Rechenregister für die Gleitpunktarithmetik als auch spezielle Register der Zeitgeberfunktionen und weitere Spezialregister umfaßt.
2.1
Beispiel der Rechnerarchitektur eines Großrechners
15
Wichtige Rechen- und Anzeigenregister der einzelnen Funktionszustände sind in Abbildung 2.1 -2 wiedergegeben. Die allen Funktionszuständen gemeinsamen Register sind im unteren Teil dargestellt. Wie man sieht, verfügen die Funktionszustände P3 und P4 über eingeschränkte Registersätze. Die einzelnen Register unterscheiden sich in ihrer Größe. Die meisten Register umfassen 32 Bits, die Register für die Gleitpunktarithmetik 64 Bits. Für ein Anwendungsprogramm im Anwenderzustand sind allein die Register des Funktionszustands PI zusammen mit der Menge der allen Funktionszuständen gemeinsamen Register "sichtbar". Die Register der Funktionszustände P2, P3 und P4 dienen ja der effektiven Interrupt-Analyse und -Behandlung, im Grunde also der Prozessorsteuerung. RO R1 R2 R3 R4 R5 R5 R7 RS R9 R10 R11 R12 R13 R14 R15
P1
RO R1 R2 R3 R4 R5 R5 R7 R8 R9 R10 R11 R12 R13 R14
P2
8
IMR1 IMR2 PIFR ERCR
—iSpezial-j —'jjggister^ röaister!
gemeinsame Register aller Funktionszustände
s
3 ISR
IMR1 IMR2 PIFR ERCR [
ε
%
_| PCR
ISR
l
R7
R15
| PCR
P4
P3
R11 R12 R13 R14 R15
1
R8 R9 R10 R11
ι 5>' 5 J R15
]PCR
]PCR
] ISR
] ISR
IMR1 IMR2
IMR1 IMR2
-fSpezial-j -1j register register,
Register für Gleitpunktarithmetik FRO FR2
FR4 FR6
Zeitgeberund Spezialregister
16
2 Die klassische Rechnerarchitektur
Erläuterungen Register
Funktion
Name des Registers
Rn, 015 Stellen
Klassentest-Tabelle für Test auf ALPHABETIC-LOWER
Umwandlung von und nach Gleitpunkt Division von Dezimalzahlen > 15 Stellen
Klassentest-Tabelle für Test auf ALPHABETIC-UPPER
INITIALIZE-Anweisung
Klassentest-Tabelle für Test auf NUMERIC (mit Vorzeichen)
INSPECT-Anweisung Klassentest-Tabelle für Test auf NUMERIC OCCURS DEPENDING (rekursiv) Multiplikation von Dezimalzahlen > 1 5 Stellen
Klassentest bei Datenfeldern > 256 Byte oder Variablen
MOVE für numerisch druckaufbereitete Felder
UNSTRING-Anweisung
Deeditierender MOVE
Vergleich für Felder variabler Länge/Adresse oder > 256 Byte
Überprüfung von Tabellengrenzen MOVE ALL >literal< REPORT-WRITER-Steuermodul Auffüllen für Felder > 256 Byte bei MOVE SEARCH-ALL-Anweisung STRING-Anweisung
MOVE für Felder variabler Länge/Adresse oder > 256 Byte Potenzierung mit Gleitpunktzahlen
3. Programm-Manager: COBOL85 Programm-Manager
Abbildung 3.1-6: Unterprogramme aus dem Laufzeitsystem des COBOL85Compilers (SIEMENS 7 500) 3.2 Laufzeit-Layout des virtuellen Adreßraums Abbildung 3.2-1 zeigt ein Beispiel einer Aufrufreihenfolge von externen Unterprogrammen UPROl, UPR02 und UPR03 aus einem externen Programm HPRO. Es soll angenommen werden, daß die Größen der Objektmoduln der Größen der dargestellten Kästchen entsprechen. Das Durchlaufen der einzelnen Programme wird durch die gestrichelten Pfeile und der Aufruf eines Programms durch das Schlüsselwort CALL dargestellt. UPR03 greift auf (globale) Daten zu, die in UPRO1 bzw. in einem Objektmodul UPR04 definiert und dort als von
60
3 Aspekte der Programmerstellung
externen Programmen zugreifbare Daten gekennzeichnet sind (EXT_REF_1 in UPROl bzw. EXT_REF_4 in UPR04). Für das Beispiel ist es irrelevant, ob es sich um einen lesenden oder einen schreibenden Zugriff handelt. Das Schlüsselwort USE soll diesen Zugriff realisieren. Andere externe Referenzen als die dargestellten sollen nicht vorkommen. Die Reihenfolge, in der der Binder die Objektmoduln zusammenbindet, wird durch die Folge der benötigten externen Referenzen bestimmt. Selbstverständlich wird ein Objektmodul nur einmal angebunden, auch wenn er mehrmals über eine externe Referenz angesprochen wird. Erzeugt der Binder aus den Objektmoduln ein ablauffähiges Programm, indem er alle Objektmoduln linear hintereinander bindet, so kann ein "sehr großes" Programm entstehen. Solange der verfügbare virtuelle Adreßraum ausreicht, funktioniert dieses Hintereinanderbinden. Dies gilt insbesondere dann, wenn mit Hilfe der Verfahren zur Arbeitsspeicherverwaltung nur die Teile in den Arbeitsspeicher geladen werden, auf die aktuell zugegriffen wird (wie beim Paging oder der Segmentierung). Bei anderen Arbeitsspeicherverwaltungen, z.B. wie sie im PC-Betriebssystem MS-DOS implementiert ist, wird immer das gesamte gebundene Programm geladen. Ein sehr großes Programm paßt dann eventuell nicht in den Arbeitsspeicher. Bei einer derartigen Arbeitsspeicherverwaltung bietet es sich an, das Programm in Form einer Überlagerungsstruktur aus Overlays zu binden: Ein Overlay ist ein Programmteil bestehend aus einem oder mehreren Objektmoduln, das mit anderen Overlays während der Laufzeit ein und denselben Teil des virtuellen Adreßraums, jedoch zu unterschiedlichen Zeitpunkten, belegt. Man kann sagen, daß ein Overlay parallel mit anderen Overlays läuft, parallel nämlich zu denjenigen, mit denen es sich (zur Laufzeit) denselben Teil des Adreßraums teilt. Erst wenn während des Programmlaufs Code aus einem Overlay verwendet wird, lädt die Laderoutine des Rechners das Overlay in den Arbeitsspeicher, wobei eventuell andere parallele Overlays überschrieben werden. Im Beispiel der Abbildung 3.2-1 wird das Objektmodul UPR02 nicht gleichzeitig mit dem Code der Objektmoduln UPROl, UPR03 und UPR04 benötigt, da sie keine aufeinander bezogenen Referenzen enthalten. Es ist also möglich, die Objektmoduln so zu binden, daß UPR02 dieselben virtuellen Adressen belegt wie UPROl, UPR03 und UPR04. Diese Adreßbelegung findet natürlich zu unterschiedlichen Zeitpunkten während der Laufzeit statt. Während Code in UPROl abgearbeitet wird, werden die anderen Objektmoduln UPR02, UPR03
3.2 Laufzeit-Layout des virtuellen Adreßraums
61
und UPR04 nicht angesprochen. Es würde sich also anbieten, UPRO1 als jeweils zu UPR02, UPR03 und UPR04 paralleles Overlay zu definieren. Daß es so nicht funktioniert, hat folgende Gründe: Während der Laufzeit wird von UPR03 auf Code in UPROl, nämlich auf EXT_REF_1, zugegriffen; zur Laufzeit von UPR03 wird wegen des Zugriffs auf EXT_REF_4 in UPR04 das Objektmodul von UPR04 benötigt. Die Overlay-Struktur kann also nur UPR02 zu der Menge der Moduln, die aus UPROl, UPR03, und UPR04 besteht, parallelisieren. Das Objektmodul von HPRO muß ständig verfügbar sein, da sich eine Rücksprungadresse (aus UPROl, UPR02 bzw. UPR03) auf HPRO bezieht.
τ CALL UPROl ι
CALL UPR02 CALL UPROS r USE LXT ΒΓΓ 1 USE EXT_REP_4
ι Durchlauffolge ] l der Moduln ι I
linear gebundene Moduln als Overlaystruktur gebundene Moduln
Abbildung 3.2-1: Aufrufe externer Programme
62
3 Aspekte der Programmerstellung
Der Entwurf einer Overlay-Struktur bedingt, daß parallele Overlays keinen Bezug aufeinander nehmen. Programmteile, die während der Laufzeit zu einem Zeitpunkt Adreßreferenzen aufeinander haben, können nicht als parallele Overlays definiert werden, sondern müssen linear hintereinander auf unterschiedliche Adressen gebunden werden. Als Konsequenz ergibt sich, daß möglichst alle Objekte, auf die aus externen Moduln Bezug genommen wird, in dem Modul liegen sollten, der zur Laufzeit ständig geladen bleiben muß. Im Beispiel ist das das Modul von HPRO, und bei Verlagerung der durch EXT_REF_1 bzw. EXT_REF_4 bezeichneten Datenobjekte nach HPRO könnten alle anderen Moduln parallel in der Overlaystruktur angebunden werden. Setzt ein Rechner den virtuellen Adreßraum mittels Paging in physikalische Arbeitsspeicheradressen um, ist die Overlaytechnik natürlich nur von untergeordneter Bedeutung. Die Größe eines Programms ist zur Laufzeit irrelavant, da ja nur einzelne gleichlange, von der Programmstruktur unabhängige Seiten geladen werden. Bei einer Speicherverwaltung mittels Segmentierung kann es durchaus notwendig werden, einzelne Segmentmengen als zueinander parallele Overlays zu definieren, da ein Segment zur Laufzeit immer als ganzes in den Arbeitsspeicher (dessen Größe eventuell nicht ausreicht, um alle nacheinander gebundenen Segmente zusammen aufzunehmen) geladen werden muß. Eine Overlaystruktur ist ein typisches Beispiel eines Laufzeit-Layouts des virtuellen Adreßraums, also der Belegung der virtuellen Adressen durch ein Programm während seines Ablaufs. Dieses Layout wird vom Anwender definiert. Eine andere durch die charakteristischen Eigenschaften der verwendeten Programmiersprache bestimmte Aufteilung des Adreßraums während der Laufzeit beruht auf der folgenden Klassifizierung der Programmiersprachen. Man kann die Menge der üblichen Programmiersprachen grob in drei Klassen nach ihrem Laufzeitverhalten unterteilen (vgl. z.B. [GHE89], [WIL88]): Statische Sprachen: Die Speicheranforderung eines Programms einer statischen Programmiersprache kann bestimmt werden, bevor das Programm gestartet wird. Insbesondere ist der Speicherplatz aller Daten zu Beginn der Laufzeit reserviert. Statische Sprachen erlauben keine rekursiven Programmaufrufe, d.h. Unterprogramme, die sich selbst aufrufen. Beispiele dieser Sprachklasse sind COBOL und FORTRAN.
3.2 Laufzeit-Layout des virtuellen Adreßraums
63
Stackorientierte Sprachen: Ein Programm einer stackorientierten Programmiersprache verwendet Daten, die erst zur Laufzeit des Programms angelegt werden (dynamische Daten), und setzt dazu eine spezielle als Stack bezeichnete Datenstruktur ein. Die Typbeschreibung dieser Daten ist zur Übersetzungszeit bekannt. Stackorientierte Sprachen erlauben rekursive Programmaufrufe. Beispiele dieser Sprachklasse sind Pascal, C und Ada. Dynamische Sprachen: Objekte eines Programms einer dynamischen Programmiersprache werden erst zur Laufzeit durch ihre Verwendung definiert, und zwar wird auch der Datentyp der Daten jetzt erst bestimmt. Der benötigte Speicherplatzbedarf ist also abhängig vom jeweiligen Programmablauf und in der Regel nicht vorhersehbar. In diese Sprachklasse fallen funktionale Programmiersprachen wie LISP und APL oder logikgeprägte Sprachen wie PROLOG. "Klassische" Programmiersprachen fallen in die erste und zweite Kategorie, auch wenn stackorientierte Sprachen einige dynamische Merkmale aufweisen. Beispielsweise erlaubt der Sprachdialekt TURBO-Pascal (ab Version 5.5, vgl. [HEI88]) eine objektorientierte Programmiertechnik, ein charakteristisches Merkmal dynamischer Sprachen. Die Speicherbelegung (des virtuellen Adreßraums) zur Laufzeit bei statischen und stackorientierten Sprachen soll an Beispielen kurz skizziert werden; auf Aspekte dynamischer Sprachen, wie sie beispielsweise auch in TURBO-Pascal vorzufinden sind, wird in Kapitel 4.3 eingegangen. Bei einer statischen Programmiersprache hat der Binder aus den einzelnen Objektmoduln ein ablauffähiges Programm erzeugt, das genau aus der Gesamtheit aller angesprochenen Objektmoduln besteht und bei dem alle externen Referenzen aufgelöst sind. Die innere Struktur (Code, Reservierungen und Vorbesetzungen für definierte Daten usw.) der einzelnen Objektmoduln ist prinzipiell im ablauffähigen Programm genauso vorhanden, wie sie der Anwender im Quellcode der Moduln festgelegt hat. Alle während der Laufzeit benötigten Reservierungen für Daten liegen fest. Eventuell sind einige Programmteile, die der Anwender nicht explizit definiert hat, aus dem Laufzeitsystem der Programmiersprache angebunden worden; diese Programmteile realisieren Sprachkonstrukte, die nicht unmittelbar in Objektcode übersetzt wurden (vgl. Abbildung 3.1-6). Es erfolgt bei Ablauf des Programms lediglich, je nach
64
3 Aspekte der Programmerstellung
Rechnertyp, noch eine Adreßumsetzung von virtuellen auf physikalische Adressen, die der Prozessor des Rechners vornimmt, die aber außerhalb der Einflußmöglichkeiten des Anwenders liegen. Das Bild zur Laufzeit, das nach der Übersetzung und dem Binden eines Programms einer stackorientierten Sprache entstanden ist, unterscheidet sich häufig grundlegend vom Quellcode. Der Sprachübersetzer erzeugt eventuell aus einem Quellprogramm mehrere Objektmoduln, und zur Laufzeit werden weitere Bereiche im virtuellen Adreßraum reserviert und zugeordnet. Das typische Laufzeit-Layout des virtuellen Adreßraums bei der Pascal-Programmierung ist in Abbildung 3.2-2 dargestellt. Hierbei wird ein Pascal-Programm mit Namen prog, das interne Unterprogramme, Datendefinitionen und den Aufruf eines externen Unterprogramms EXTUPRO enthält, vom Pascal-Compiler übersetzt. Das externe Unterprogramm wird ebenfalls mit dem erforderlichen Sprachübersetzer übersetzt. Der Binder bindet neben den erzeugten Objektmoduln weitere Moduln des Laufzeitsystems von Pascal an; dabei handelt es sich um vorübersetzte, in einer Bibliothek abgelegte Routinen zur Implementierung verwendeter Pascal-Sprachkonstrukte (vgl. auch Kapitel 3.1). Codebereich
statischer Datenberelch
dynamischer Datenbereich
Moduln des PascalLaufzeitsystems Laufzeit-Layout
Abbildung 3.2-2: Laufzeit-Layout des virtuellen Adreßraums bei der PascalProgrammierung
3.2 Laufzeit-Layout des virtuellen Adreßraums
65
Das ablauffähige Programm besteht aus folgenden unterscheidbaren Bereichen: Der Codebereich enthält alle ausführbaren Anweisungen des Programms prog und aller internen Prozeduren (aus prog) und externen Prozeduren (EXTUPRO und Prozeduren des Laufzeitsystems). Ausserdem sind hier alle definierten Konstanten (CONST-Deklarationen) in den Code eingearbeitet (ein Pascal-Compiler verarbeitet üblicherweise Konstantendefinitionen, indem er sie an den entsprechenden Stellen direkt in den Code einsetzt). Der statische Datenbereich enthält alle im Hauptprogramm prog definierten Variablen, sofern ihr Speicherplatzbedarf bereits zur Übersetzungszeit bekannt ist. Über Pointer definierte (dynamische) Variablen gehören nicht dazu, wohl aber die Pointer selbst, die auf derartige Daten verweisen. Sogenannte typisierte Konstanten des Hauptprogramms, durch eine CONST-Deklaration vereinbarte Konstanten also, die mit Werten initialisiert werden und in einigen Pascal-Dialekten erlaubt sind, liegen ebenfalls im statischen Datenbereich, da sie wie Variablen behandelt werden. Der dynamische Datenbereich ist in zwei Teile unterteilt: in den Heap und den Stack. Die Gesamtgröße beider Bereiche läßt sich durch Compileroptionen festlegen; das Größenverhältnis beider Teilbereiche zueinander verändert sich dynamisch während der Laufzeit. Dabei wächst im allgemeinen der Heap von niedrigen zu hohen Adressen und der Stack in umgekehrter Richtung. Falls sich die Bereichsgrenzen während der Laufzeit überschneiden sollten, kommt es zu einem Laufzeitfehler mit Programmabbruch. Heap und Stack sind komplett unterschiedlich organisiert und werden durch Routinen des Laufzeitsystems verwaltet. Der Heap enthält alle während der Laufzeit durch die PascalStandardprozedur New eingerichtete Variablen, die über Pointer angesprochen werden (vgl. Kapitel 7.3). Der Stack dient der Aufnahme aller lokalen Variablen einer Prozedur (siehe Kapitel 4.2): Dazu gehören im wesentlichen alle Variablen, die innerhalb der Prozedur deklariert werden. Variablen, die in einer eingebetteten Prozedur deklariert werden, sind lokal zu dieser eingebetteten Prozedur.
66
3 Aspekte der Programmerstellung
Eine Prozedur wird im allgemeinen von mehreren Stellen aus aufgerufen, wobei ihr vom Aufrufer jeweils wertmäßig unterschiedliche Sätze an Daten übergeben werden. In Abhängigkeit von diesen übergebenen Werten ermittelt die Prozedur "Rückgabewerte" und gibt sie an den Aufrufer zurück (der Mechanismus der Parameterübergabe ist Thema der Kapitel 8.1 und 8.2). Zur Aufnahme dieser zum jeweiligen Aufruf gehörenden aktuellen Werte (Aktualparameter, aktuelle Parameter) sind Variablen vorgesehen, die zu den lokalen Variablen der Prozedur zählen. Sie heißen Formalparameter (formale Parameter) der Prozedur und liegen ebenfalls im Stack. Die Liste der Formalparameter einer Pascal-Prozedur wird im Prozedurkopf spezifiziert. Außerdem werden im Stack bei einem Unterprogrammsprung die Rücksprungadresse zum Aufrufer abgelegt und Sicherstellungsbereiche für Registerinhalte eingerichtet. Eventuell erforderliche Speicherplätze für Zwischenergebnisse bei arithmetischen Operationen können ebenfalls im Stack liegen. Ein Stackeintrag für eine Prozedur, d.h. die Reservierung der beschriebenen Daten, erfolgt erst, wenn die Prozedur während der Laufzeit auch wirklich aufgerufen wird. Die Organisationsform des Stacks wird in den Kapiteln 8.5 und 8.6 behandelt. Das hier beschriebene Layout zur Laufzeit ist nur als prinzipiell und beispielhaft zu betrachten. Je nach Sprachdialekt, eingesetztem Betriebssystem und verwendetem Rechner gibt es Abweichungen.
3.3 Dynamische Bindemethoden Die bisher besprochene Methode des Bindens wird als statisches Binden bezeichnet. Das Ergebnis des Bindevorgangs ist ein "fertiges" ablauffähiges Programm, bei dem alle Referenzen bezogen auf einen virtuellen Adreßraum aufgelöst sind. Dieses Programm braucht zur Ausführung nur noch in den Arbeitsspeicher geladen zu werden, wobei eine rechnerabhängige Adreßumsetzung stattfindet. Ein Nachteil des statischen Bindens besteht darin, daß eine über eine externe Referenz angesprochenen Prozedur auch dann angebunden wird, wenn sie während der Laufzeit gar nicht ausgeführt wird. Allein die Angabe der externen Referenz veranlaßt den Binder, die Prozedur anzubinden und die externe Referenz aufzulösen.
3.3 Dynamische Bindemethoden
67
Ein weiterer Nachteil ist darin zu sehen, daß alle möglicherweise aufzurufenden Prozeduren explizit schon zum Bindezeitpunkt bekannt sein müssen. Das folgende Beispiel erläutert die Situation (vgl. auch [IAC88]): PRODEDURE transact;
TRANSACT
PROCEDURE transl; EXTERNAL;
. ·· ·
TRANS1
PROCEDURE transn; EXTERNAL;
VAR transaction: STRING; BEGIN
TRANSn
REPEAT READ (transaction) { Lesen der Programmidentifizierung }; CASE transaction OF { Aufruf des jeweiligen Programms } 'TRANSI': transl; 'TRANSn': transn; ELSE { Fehlerbehandlung } ... END { CASE }; UNTIL ... { Beenden von transact } END {transact};
Abbildung 3.3-1: Statisches Binden Ein Steuerprogramm (z.B. innerhalb eines modular aufgebauten Anwendungssystems) namens TRANSACT empfängt über eine Kommunikationsschnittstelle den Namen eines Programms. Erlaubte Programmnamen sind TRANS 1, ..., TRANSn. Das Programm TRANSi (l ( n l o g 2 η - 1 .
< l,441og2(n +2) wie beim vollkommen höhenbalancierten binären Suchbaum
Abbildung 11.2-8: Komplexität bei binären Suchbäumen In praktischen Anwendungsfällen besteht ein Element der Datenstruktur häufig aus einem Datensatz, der über den Wert eines Primärschlüsselfelds eindeutig identifiziert wird und eine Reihe weiterer meist umfangreicher Komponenten beinhaltet. Die Datenstruktur stellt in diesem Fall eine (phsyikalische) Datei dar. Bei einer großen Anzahl von Datensätzen, auf die die Operationen einer Prioritätsschlange angewendet werden sollen und die auf einem externen Speichermedium liegen, ist es im allgemeinen nicht angebracht, diese Datei in Form eines binären Suchbaums zu organisieren. Jeder Zugriff auf einen Datensatz, der dann in einem Knoten des binären Suchbaums liegt, erfordert einen Datentransfer zwischen Arbeitsspeicher und Peripherie. Der hierbei zu veranschlagende Zeitaufwand ist selbst im Idealfall des vollkommen höhenbalancierten binären Suchbaums in der Regel nicht akzeptabel. Aus den Formeln in Abbildung 11.2-8 wird ersichtlich, daß im Idealfall des vollkommen höhenbalancierten binären Suchbaums bei einer Datei mit beispielsweise 20 000 Datensätzen im Durchschnitt ca. 13,3 und bei 200 000 Datensätzen im Durchschnitt 16,6 (vergleich-
344
11 Anwendungsorientierte Datenstrukturen in der systemnahen Programmierung
sweise sehr zeitaufwendige) Plattenzugriffe erforderlich sind, um zu einem Datensatz zu gelangen. In der Praxis ist dieser Wert unakzeptabel. Es sind daher andere Realisierungsformen für Dateien angebracht. Von den in der Literatur (vgl. z.B. [AH074], [MEH88], [OLL89]) beschriebenen Vorschlägen ist das Konzept des B*-Baums zur Realisierung von Dateien von hoher praktischer Relevanz; es wird zur Speicherung von Datensätzen in ISAM-Dateien des BS2000 und VSAM-Dateien im IBM-Betriebssystem MVS eingesetzt (vgl. [KOC87] und [BOH81]).
11.3 Realisierung von Dateien in Form eines B*-Baums Die Elemente der Datenstruktur sind Datensätze, die über den Wert eines Primärschlüsselfelds key als eindeutiger Ordnungsbegriff jeweils identifiziert werden. Im folgenden soll der übrige Teil eines Elements mit dem Namen data bezeichnet werden, d.h. ein Element hat den Datentyp e n t r y _ t y p = RECORD key : ... { P r i m ä r s c h l ü s s e l }; d a t a : ... { ü b r i g e r D a t e n t e i l } END;
Nach einem bestimmten Algorithmus werden Datensätze auf Blöcke fester Länge verteilt. Dabei kann ein Block mehr als einen Datensatz aufnehmen; er braucht aber nicht vollständig gefüllt zu sein. Die Blocklänge orientiert sich an physikalischen Gegebenheiten der Datenspeicherungsform auf einem externen Speichermedium. Um an die Informationen eines Datensatzes zu gelangen, muß der komplette Block, der diesen Datensatz enthält, mit einem externen Speicherzugriff in den Arbeitsspeicher übertragen werden; anschließend werden die einzelnen Blockinformationen extrahiert. Neben den Blöcken, die Datensätze enthalten, gibt es Blöcke, die Primärschlüsselwerte von Datensätzen und Verweise auf andere Blöcke enthalten. Unter einem Verweis ist jetzt die Blocknummer bzw. Blockadresse bezogen auf das externe Speichermedium des angesprochenen Blocks zu verstehen. Diese Blöcke dienen der Organisation des schnellen Zugriffs auf die Datensätze. Die Gesamtheit der Blöcke bildet eine Datei, die im allgemeinen auf einem externen Speichermedium liegt. Eine so organisierte Datei enthält Blöcke mit Datensätzen und Blöcke mit Primärschlüsselwerten und Verweisen.
11.3 Realisierung von Dateien in Form eines B"-Baums
345
Jeder Block stellt den Knoten eines Baums dar. Die Verweise auf andere Blöcke bilden die Kanten. Die dabei entstehende Struktur heißt B*-Baum. Sie garantiert durch weitere Festlegungen, daß jeder Pfad von der Wurzel zu einem Blatt gleichlang ist. Natürlich kann ein B*-Baum kein Binärbaum sein, d.h. ein Knoten wird maximal mehr als zwei Nachfolger haben. Formal läßt sich ein B*-Baum mit Parametern u und ν wie folgt definieren: Alle Blätter haben denselben Rang, d.h. sie sind alle in derselben Anzahl von Kanten von der Wurzel entfernt Die Wurzel ist ein Blatt oder hat mindestens 2 Nachfolger Jeder Knoten außer den Blättern und der Wurzel hat mindestens u+1 und höchstens 2u+l Nachfolger; falls die Wurzel kein Blatt ist, hat sie mindestens 2 und höchstens 2u+l Nachfolger Jeder Knoten außer den Blättern enthält s key-Werte, wenn er s+1 Nachfolger hat; die key-Werte sind aufsteigend sortiert Jedes Blatt enthält, wenn es nicht die Wurzel ist, mindestens ν und höchstens 2v Datensätze, die nach aufsteigenden key-Werten sortiert sind; ist die Wurzel ein Blatt, so enthält sie höchstens 2v Datensätze. Der hier zur Speicherung einer Datei entstehende B*-Baum enthält zwei Typen von Knoten, die im folgenden auch Blöcke genannte werden: Ein Datenblock ist ein Blatt des B*-Baums und enthält mehrere Datensätze vom Datentyp entry_typ und zusätzliche Verwaltungsinformationen zur Organisation des Zugriffs auf die einzelnen Datensätze innerhalb des Blocks; die Datensätze sind nach aufsteigenden keyWerten sortiert Ein Indexblock ist ein Knoten des B*-Baums, der Nachfolgerknoten besitzt; er enthält Verweise auf andere Knoten und key-Werte; die key-Werte sind aufsteigend sortiert; auch ein Indexblock beinhaltet zusätzliche Verwaltungsinformationen, die die schnelle Ausführung der Zugriffsalgorithmen unterstützen. Die Parameter u und ν beschreiben einen Mindestfüllungsgrad der Indexblöcke bzw. der Datenblöcke.
11 Anwendungsorientierte Datenstrukturen in der systemnahen Programmierung
346
Zum schnelleren Navigieren durch den B*-Baum sind die Knoten im Baum vorwärts und rückwärts verkettet, d.h. ein Knoten enthält neben den (Vorwärts-) Verweisen auf seine Nachfolgerknoten einen zusätzlichen (Rückwärts-) Verweis auf seinen Vorgängerknoten. Außerdem sind alle Knoten desselben Rangs als doppelt-verkettete Liste miteinander verknüpft. Den Aufbau der Knoten eines B*-Baums zeigt Abbildung 11.3-1. Ein B*-Baum stellt die Realisierung einer Prioritätsschlange dar, wobei die Knoten desselben Rangs zusätzlich jeweils eine Liste mit doppelter Verkettung bilden. Die Implementation der Operationen init, insert, delete, member, min und show sind aufgrund ihres Umfangs hier nur informell beschrieben und an einem Beispiel erläutert.
t anz vorg last next
k1
data,
k2
datag
k,
data,
Datenblock
Feldname
Inhalt
anz
gegenwärtige Anzahl an Datensätzen im Datenblock (= t mit v2v+l Datensätze, so gilt für die benötigte Anzahl Α (η) der Blocktransfers: log π -log(2v) iog(2u+1)
logn-loggy) log(2a+1)
+ / ·
Im allgemeinen belegen die key-Werte und die Verweise weniger Speicherplatz als die kompletten Datensätze. Daher kann man u>v wählen. Besteht ein Datensatz beispielsweise aus 200 Bytes, wobei die key-Komponente 20 Bytes belegt, so können bei einer Blockgröße von 2048 Bytes 10 Datensätze in einem Datenblock untergebracht werden, d.h. ν = 5. Für die anz-Komponente bzw. einen Verweis auf einen Block seien jeweils 4 Bytes zu veranschlagen. Ein Indexblock belege ebenfalls 2048 Bytes; dabei ist er gefüllt, wenn er 2u =84 key-Werte enthält. Bei 20 000 Sätzen gilt dann für die Höhe h des B*-Baums 2,71