Grundlagen der Informatik I Tafel -- Manuskript Studiengang Wirtschaftsingenieurwesen Wintersemester 2013/2014 Autor: Prof. Dr.-Ing. habil. Hans-Joachim Böhme HTW Dresden, Fakultät Informatik/Mathematik Friedrich-List-Platz 1, 01069 Dresden E-Mail: [email protected] Hinweis: Die Rechte am Dokument liegen ausschließlich beim Verfasser. Inhaltsverzeichnis 1 Grundlegender Überblick................................................................................................... 4 1.1 Was ist Informatik/Informationsverarbeitung ............................................................ 4 1.2 Was ist Information? .................................................................................................. 5 1.3 Was ist ein Bit? .......................................................................................................... 7 1.4 Datei und Speichergrößen .......................................................................................... 9 2 Informationsdarstellung ................................................................................................... 11 2.1 Text........................................................................................................................... 11 2.1.1 Der ASCII-Code............................................................................................... 11 ASCII-Erweiterungen....................................................................................................... 12 ASCII-Probleme............................................................................................................... 13 2.1.2 Unicode ............................................................................................................ 14 2.1.3 UTF-8 ............................................................................................................... 14 2.1.4 Zeichenketten ................................................................................................... 15 2.2 Zahlendarstellung ..................................................................................................... 16 2.2.1 Binärdarstellung ............................................................................................... 16 2.2.2 Hexadezimaldarstellung ................................................................................... 17 2.2.3 Umwandlung zwischen verschiedenen Zahlensystemen ................................. 17 Exkurs: Division mit Rest ................................................................................................ 17 2.2.4 Die Zweierkomplementdarstellung .................................................................. 18 Berechnung des Zweierkomplements wird in der Übung behandelt! .............................. 19 Standardformate für Zweierkomplementzahlen............................................................... 19 2.3 Darstellung logischer Werte..................................................................................... 19 3 Rechnerorganisation – Teil 1 ........................................................................................... 21 3.1 Von-Neumann-Architektur ...................................................................................... 21 John von Neumann........................................................................................................... 21 Nachteile der von-Neumann-Architektur......................................................................... 22 3.2 Merkmale und Komponenten der Von-Neumann - Rechnerarchitektur .................. 23 3.2.1 Zentrale Recheneinheit / Central Processing Unit ........................................... 23 3.2.2 Speicher............................................................................................................ 24 3.2.3 Interne Datenwege / Busse............................................................................... 24 3.2.4 Ein-/Ausgabeeinheit ......................................................................................... 25 3.3 Mainboard ................................................................................................................ 25 3.3.1 Systembus......................................................................................................... 25 3.3.2 BIOS................................................................................................................. 26 3.3.3 CPU .................................................................................................................. 28 Cache Speicher................................................................................................................. 28 Cache Hierarchie .............................................................................................................. 29 Prozessor-Cache ............................................................................................................... 29 Prozessor-Hersteller ......................................................................................................... 30 3.3.4 Arbeitsspeicher / RAM (Random Access Memory) ........................................ 30 Speichertypen ................................................................................................................... 31 Technische Realisierung .................................................................................................. 32 3.3.5 Northbridge ...................................................................................................... 32 3.3.6 Southbridge ...................................................................................................... 33 3.3.7 PCI-Bus ............................................................................................................ 34 3.3.8 Accelerated Graphics Port (AGP) .................................................................... 35 3.3.9 PCI-Express...................................................................................................... 36 3.3.10 IDE (ATA/ATAPI) .......................................................................................... 37 Konfiguration von EIDE-Geräten .................................................................................... 39 1 ATAPI - Advanced Technology Attachment with Packet Interface................................ 39 3.3.11 S-ATA (Serial ATA)........................................................................................ 40 S-ATA II (Serial ATA II)................................................................................................. 41 3.3.12 USB (Universal Serial Bus) ............................................................................. 41 Technischer Überblick ..................................................................................................... 42 Übertragungsgeschwindigkeiten ...................................................................................... 43 3.4 Speicher-Hierarchie.................................................................................................. 44 3.5 Funktionell-struktureller Aufbau der CPU............................................................... 44 3.6 Einteilung von Rechnerarchitekturen nach Flynn.................................................... 50 Harvard-Architektur ................................................................................................................. 51 4 Betriebssysteme................................................................................................................ 53 4.1 Die ISA-Ebene ......................................................................................................... 53 4.2 Assemblerprogrammierung ...................................................................................... 53 Assemblersprache – Maschinensprache und Assembler.................................................. 54 4.3 Von der Hardware zum Betriebssystem................................................................... 56 4.4 Aufgaben des Betriebssystems................................................................................. 57 4.4.1 Dateiverwaltung ............................................................................................... 58 4.4.2 Prozessverwaltung............................................................................................ 63 4.4.3 Speicherverwaltung.......................................................................................... 66 5 Algorithmen und Programmierung .................................................................................. 70 5.1 Einführung................................................................................................................ 70 5.2 Übersicht der Programmiersprachen........................................................................ 71 5.3 Definition von Programmiersprachen ...................................................................... 72 5.4 Klassifikation von Programmiersprachen ................................................................ 72 5.5 Vom Programm zur Maschine ................................................................................. 74 5.5.1 Virtuelle Maschinen ......................................................................................... 74 5.5.2 Interpreter ......................................................................................................... 76 5.6 Programmentwicklung ............................................................................................. 77 5.6.1 Analyse............................................................................................................. 77 5.6.2 Entwurf............................................................................................................. 78 Algorithmen als Lösung einer Spezifikation.................................................................... 80 Terminierung.................................................................................................................... 81 5.6.3 Implementierung .............................................................................................. 81 5.6.4 Test ................................................................................................................... 82 5.6.5 Dokumentation ................................................................................................. 82 5.7 Elementare Aktionen in Programmiersprachen ....................................................... 83 5.8 Programmierumgebungen ........................................................................................ 84 5.9 Datentypen und Variablen........................................................................................ 84 5.9.1 Datentypen ....................................................................................................... 84 5.9.2 Variablen .......................................................................................................... 84 5.9.3 Primitive Datentypen........................................................................................ 86 5.9.4 Zusammengesetzte Datentypen........................................................................ 87 5.9.5 Variablen und Speicher .................................................................................... 89 5.9.6 Deklaration von Variablen ............................................................................... 89 5.9.7 Initialisierung von Variablen............................................................................ 90 5.9.8 Typkorrekte Ausdrücke.................................................................................... 90 5.9.9 Typfehler .......................................................................................................... 91 5.10 Der Kern imperativer Sprachen................................................................................ 91 5.10.1 Zuweisungen .................................................................................................... 92 5.10.2 Kontrollstrukturen ............................................................................................ 93 Die if-Anweisung ............................................................................................................. 94 2 Die if else-Anweisung...................................................................................................... 96 Die switch-Anweisung ..................................................................................................... 96 Die while-Schleife............................................................................................................ 99 Die for-Schleife .............................................................................................................. 101 Die do while-Schleife..................................................................................................... 103 Die break-Anweisung..................................................................................................... 105 Die continue-Anweisung................................................................................................ 106 5.11 Datenstrukturen ...................................................................................................... 106 5.11.1 Stacks ............................................................................................................. 107 Stackoperationen ............................................................................................................ 108 Anwendungsbeispiel für einen Stack ............................................................................. 108 5.11.2 Queues............................................................................................................ 110 Anwendung von Queues ................................................................................................ 110 5.11.3 Listen.............................................................................................................. 111 Einfach verkettete Listen................................................................................................ 111 Doppelt verkettete Listen ............................................................................................... 112 5.12 Bäume..................................................................................................................... 113 Beispiele für Bäume ....................................................................................................... 114 5.12.1 Binärbäume .................................................................................................... 115 5.12.2 Traversierungen.............................................................................................. 117 5.13 Suchalgorithmen..................................................................................................... 117 5.13.1 Lineare Suche................................................................................................. 118 5.13.2 Binäre Suche .................................................................................................. 119 5.14 Komplexität von Algorithmen ............................................................................... 120 5.15 Sortieralgorithmen.................................................................................................. 121 5.15.1 BubbleSort...................................................................................................... 121 3 1 Grundlegender Überblick 1.1 Was ist Informatik/Informationsverarbeitung Der Begriff Informatik leitet sich von dem Begriff Information her. Er entstand in den 60er Jahren. Informatik ist die Wissenschaft von der maschinellen Informationsverarbeitung. Die englische Bezeichnung für Informatik ist Computer Science, also die Wissenschaft, die sich mit Rechnern beschäftigt. Wenn auch die beiden Begriffe verschiedene Blickrichtungen andeuten, bezeichnen sie dennoch das Gleiche. Die Spannweite der Disziplin Informatik ist sehr breit, und demzufolge ist das Gebiet in mehrere Teilgebiete untergliedert. 1. Technische Informatik Die Technische Informatik beschäftigt sich mit der Hardware, d.h. mit der Konstruktion von Rechnern, Rechnerkomponenten (Prozessoren und Speicherchips) und Peripheriegeräten (Druckern, Bildschirmen). Sie stellt also die Grundlage der maschinellen Informationsverarbeitung bereit. Die Hardwareentwicklung erfolgt aber immer auch im Hinblick auf das spätere Anwendungsgebiet (Steuert ein Prozessor eine Raumfähre oder eine XBox?) Die Grenzen zur Elektrotechnik sind fließend. 2. Praktische Informatik Im Gegensatz zur Technischen Informatik beschäftigt sich die Praktische Informatik mit der Software, die auf der Hardware ausgeführt werden soll, also im weitesten Sinne mit den Programmen, die die Hardware steuern. Ein klassisches Anwendungsgebiet der Praktischen Informatik ist der Compilerbau. Ein Compiler übersetzt Programme, die in einer Programmiersprache (Java, C, C++,...) geschrieben sind, in die stark von den Eigenheiten des jeweiligen Systems abhängende Maschinensprache (Assembler). Ein compiliertes Programm läuft nur auf dem System, für das es compiliert wurde. Windowsprogramme laufen also nicht unter Unix, ohne neu compiliert zu werden. 3. Theoretische Informatik Die Theoretische Informatik beschäftigt sich mit den abstrakten mathematischen und logischen Grundlagen aller Teilgebiete der Informatik. Theorie und Praxis sind in der Informatik enger verwoben als in vielen anderen Diziplinen, theoretische Erkenntnisse sind schneller und direkter einsetzbar. Durch die theoretischen Arbeiten auf dem Gebiet der formalen Sprachen und der Automatentheorie zum Beispiel hat man das Gebiet des Compilerbaus (-> Praktische Informatik) heute sehr gut im Griff. In Anlehnung an die theoretischen Erkenntnisse sind praktische Werkzeuge entstanden (Programme), mit denen ein großer Teil des Compilerbaus automatisiert und vereinfacht werden kann. Früher dauerte die Konstruktion eines einfachen 4 Compilers ca. 25 Bearbeiter-Jahre. Heute erledigen Studenten vergleichbare Aufgaben im Rahmen eines Praktikums! (Vorweg zur Frage: "Was nützt mir die Automatentheorie?". Aber dazu später mehr...) 4. Angewandte Informatik Die Angewandte Informatik beschäftigt sich mit dem Einsatz von Rechnern in den verschiedensten Bereichen unseres Lebens. Es gibt eine enge Verknüpfung zwischen Angewandter Informatik und den anderen Teilbereichen. So gilt es z.B. neue Hardware mit erweiterten Möglichkeiten (-->Technische Informatik) in Verbindung mit aus dem Zusammenspiel von Theoretischer und Praktischer Informatik entstandenen Werkzeugen einer sinnvollen, neuen Anwendung zuzuführen (z.B. Organizer, Palmtops oder Tablet-PC's, für die Lagerhaltung, auf der Baustelle, oder für den Paketboten). Dies gilt auch für die Anwendung von Computersystemen in speziellen Fachbereichen der Wissenschaft: In der HKI: Anwendung auf alle (v.a. historischen) Quellen. Bessere Nutzbarmachung von z.B. alten Handschriften durch Text-Markup und Datenbanken. In der Archäologie: Anwendung von Computersystemen zur Rekonstruktion antiker Bauten z.B. als 3D-Modell. .... 1.2 Was ist Information? Information und Daten Die verschiedenen Teilbereiche der Informatik beschäftigen sich mit unterschiedlichen Ebenen der Informationsverarbeitung. Um zu beschreiben, wie Informationen im Computer verarbeitet werden, müssen wir uns zunächst auf eine Erklärungsebene festlegen. Wir beginnen mit der niedrigsten Ebene, der Ebene der Nullen und Einsen. Wir beschäftigen uns also zunächst damit, wie Informationen im Rechner durch Nullen und Einsen repräsentiert werden können. Die so repräsentierten Informationen nennen wir Daten. Die Repräsentation muss derart gewählt werden, dass man aus den Daten auch wieder die repräsentierte, ursprüngliche Information zurückgewinnen kann. Diesen Prozess der Interpretation von Daten nennt man auch Abstraktion. 5 Zur Verdeutlichung: Wenn man nur eine Folge von Bits betrachtet, kann noch keine Information daraus gewonnen werden. Eine Folge von Bits oder Bytes hat für sich genommen keine Bedeutung. Erst wenn man weiß, wie diese Bytes zu interpretieren sind, erschließt sich deren Bedeutung und damit eine Information! Die Bedeutung (oder der Informationsgehalt) von Daten ergibt sich erst aus der Kenntnis der benutzten Repräsentation. (--> mp3 im Texteditor? jpg im Texteditor?). Diese Interpretation von Daten nennt man Abstraktion. Definitionen Daten (Singular: Datum) sind eine Folge von digitalen Zeichen (--> Bits). Eine Information ist ein Datum mit inhaltlicher Bedeutung. Eine Nachricht ist eine Zeichenfolge, die Informationen vermittelt. Ein Signal ist die physikalische Darstellung von Nachrichten oder Daten. Oder anders ausgedrückt: Information ist der abstrakte Gehalt (Bedeutungsinhalt, Semantik) eines Dokuments, einer Aussage, o. ä. Repräsentation ist die äußere Form der Darstellung (konkrete Form) Kommunikation und Sprache Information wird übermittelt durch Kommunikation. Kommunikation bedient sich immer einer Sprache. Die Information wird in einer Sprache kodiert. Jede Sprache hat: ein Alphabet (der endliche Symbolsatz) eine Grammatik (Regeln zur Formung von Zeichenketten, Worten, Aussagen) eine Syntax ("Rechtschreibung" der Worte bzw. Aussagen) eine Semantik (Bedeutung des Gesagten) 6 Repräsentation von Daten Das Alphabet, das der Computer verwendet, um Daten zu repräsentieren besteht aus Nullen und Einsen, aus Bits. 1.3 Was ist ein Bit? Ein Bit (aus dem Englischen für binary digit) ist die kleinstmögliche Einheit der Information. Ein Bit ist die Informationsmenge in einer Antwort auf eine Frage, die zwei Möglichkeiten zulässt: ja oder nein wahr oder falsch schwarz oder weiß hell oder dunkel naß oder trocken links oder rechts usw. Zu einer solchen Frage lässt sich immer eine Codierung festlegen. Da es zwei mögliche Antworten gibt, reicht ein Code mit zwei Zeichen, ein sogenannter binärer Code. Man benutzt dazu die Zeichen: 0 oder 1 Eine solche Codierung ist deswegen nötig, weil die Information technisch dargestellt werden soll. Man bedient sich dabei z.B. elektrischer Ladungen: 0 = ungeladen 1 = geladen oder elektrischer Spannungen 0 = 0 Volt 1 = 5 Volt oder Magnetisierungen 0 = unmagnetisiert 1 = magnetisiert Bitfolgen Lässt eine Frage mehr als zwei Antworten zu, so enthält die Beantwortung der Frage mehr als ein Bit an Information. Die Frage etwa, aus welcher Himmelsrichtung, Nord, Süd, Ost oder West, der Wind weht, lässt 4 mögliche Antworten zu: 0 0 = Nord 7 0 1 = Ost 1 0 = Süd 1 1 = West --> Offensichtlich gibt es genau 4 mögliche Folgen von 2 Bit. Mit 2 Bit können wir also Fragen beantworten, die 4 mögliche Antworten zulassen. Angenommen, wir lassen als Antworten auf die obige Frage auch noch die Zwischenrichtungen Nordost, Nordwest, Südost und Südwest zu, haben wir insgesamt 8 mögliche Antworten. Mit einem zusätzlichen Bit, also insgesamt mit 3 Bit können wir alle 8 möglichen Antworten darstellen. 0 0 0 = Nord 0 0 1 = Ost 0 1 0 = Süd 0 1 1 = West 1 0 0 = Nordost 1 0 1 = Nordwest 1 1 0 = Südost 1 1 1 = Südwest Nibbels, Bytes und Worte Ein Rechner ist viel besser als der Mensch in der Lage, mit langen Folgen von Bits umzugehen. Für uns wird das Ganze schnell unübersichtlich: 01001111011000010110110001101100 Übersichtlicher wird das Ganze, wenn wir die Bitfolge oben in kleinere Vierer-Pakete unterteilen: 0100 1111 0110 0001 0110 1100 0110 1100 Eine solche Gruppe von 4 Bits nennt man auch ein Halb-Byte oder Nibble. Wenn ein Rechner Daten liest oder schreibt, wenn er mit Daten operiert, gibt er sich nie mit einzelnen Bits ab. Dies wäre im Endeffekt viel zu langsam. Stattdessen arbeitet er immer nur mit Gruppen von Bits, entweder mit 8 Bits, 16 Bits, 32 Bits oder 64 Bits. man spricht dann von 8-Bit-Rechnern, 16-Bit-Rechnern, 32-Bit-Rechnern (z.B. AMD AthlonXP, Intel Pentium 3 ) oder von 64-Bit-Rechnern (AMD Athlon64, Athlon X2 64, Opteron, Intel Core 2 Duo). In Wahrheit gibt es aber auch Mischformen: Rechner, die etwa intern mit 32-Bit-Blöcken rechnen, aber immer nur Blöcke zu 64-Bits lesen oder schreiben. Stets jedoch ist die Länge eines Bitblocks ein Vielfaches von 8. Eine Gruppe von 8 Bit nennt man daher auch ein Byte. 4 Bits = Halb-Byte / Nibble 8 Bits = Byte 8 2 Bytes = Wort Für eine Gruppe von 2, 4 oder 8 Bytes sind auch die Begriffe Wort, Doppelwort und Quadwort in Gebrauch, allerdings ist die Verwendung dieser Begriffe uneinheitlich! Bei einem 16-Bit-Rechner bezeichnet man eine 16-Bit Größe (2 Bytes) als Wort, ein Byte ist dann ein Halbwort. Bei einem 32-Bit-Rechner steht Wort auch für eine Gruppe von 4 Bytes. 1.4 Datei und Speichergrößen Dateigrößen von weniger als hundert Byte sind äußert selten, meist bewegen sich die Dateigrößen in Bereichen von Tausenden oder gar Millionen von Bytes. Es bietet sich daher an, dafür die von Gewichts- und Längenmaßen gewohnten Präfixe kilo (für tausend) und mega- (für million) zu verwenden. Andererseits wäre es günstig, beim Umgang mit binären Größen auch die Faktoren durch Zweierpotenzen 2, 4, 8, 16, ... auszudrücken. Da trifft es sich sehr gut, dass die Zahl 1000 sehr nahe bei einer Zweierpotenz liegt, nämlich: Die Abkürzungen für die in der Informatik gebräuchlichen Größenfaktoren sind daher: Die obigen Maßeinheiten haben sich auch für die Angabe der Größe des Hauptspeichers und anderer Speichermedien durchgesetzt. 1 GB = 1024 MB RAM Wer allerdings schon einmal eine Festplatte gekauft hat, wird sich vielleicht gewundert haben, dass seine schöne neue 250 GB Festplatte de facto nur ca. 232 GB groß ist. Wie dieser Umstand entsteht, zeigt die folgende Rechnung: 9 10 2 Informationsdarstellung Wie bereits im Vorangegangenen ausgeführt, bezeichnen wir die Repräsentation von Information durch Nullen und Einsen als Daten. Im Folgenden wird nun die Darstellung von Texten, logischen Werten, Zahlen und Programmen durch Daten erläutert. 2.1 Text Um Texte in einem Rechner darzustellen, codiert man Alphabet und Satzzeichen in Bitfolgen. Mit einem Alphabet von 26 Kleinbuchstaben, ebenso vielen Großbuchstaben, einigen Satzzeichen wie etwa Punkt, Komma und Semikolon und Spezialzeichen wie "+", "&", "%" hat eine normale Tastatur eine Auswahl von knapp 100 Zeichen. Die Information, wo ein Zeilenumbruch stattfinden oder wo ein Text eingerückt werden soll, codiert man ebenfalls durch spezielle Zeichen. Solche Sonderzeichen, dazu gehören das CR-Zeichen (von engl. carriage return = Wagenrücklauf) und das Tabulatorzeichen Tab, werden nie ausgedruckt, sie haben beim Ausdrucken lediglich die entsprechende steuernde Wirkung. Sie heißen daher auch Steuerzeichen oder nicht-druckbare Zeichen. --> Exkurs zu Codierung: Eine Codierung ist eine Abbildungsvorschrift (oder Funktion), die jedem Zeichen eines Zeichenvorrats (Urbildmenge) eindeutig ein Zeichen oder eine Zeichenfolge aus einem möglicherweise anderen Zeichenvorrat (Bildmenge) zuordnet. Den Vorgang des Übersetzens eines Zeichens oder einer Zeichenfolge der Urbildmenge in die Bildmenge bezeichnet man als Codierung oder Verschlüsselung. Der umgekehrte Vorgang heißt Decodierung oder Entschlüsselung. 2.1.1 Der ASCII-Code Für alle oben aufgezählten Zeichen unseres Alphabets, inklusive der Steuerzeichen kommen wir mit 7 Bits aus, das ergibt 128 verschiedene Möglichkeiten. Man muss also nur eine Tabelle erstellen, mit der jedem Zeichen ein solcher Bitcode zugeordnet wird. Dazu nummeriert man die 128 gewählten Zeichen einfach durch und stellt die Nummer durch 7 Bit binär dar. Die heute fast ausschließlich gebräuchliche Nummerierung ist die so genannte ASCII-Codierung. ASCII steht für "American Standard Code for Information Interchange". Sie berücksichtigt einige Systematiken wie: die Kleinbuchstaben sind in alphabetischer Reihenfolge durchnummeriert, 11 die Großbuchstaben sind in alphabetischer Reihenfolge durchnummeriert, die Ziffern 0 bis 9 stehen in der natürlichen Reihenfolge. Hier exemplarisch einige Zeichen mit Ihrem ASCII-Code: Zeichen A B Z a b z ? ASCII 65 66 90 97 98 122 63 Zeichen 0 1 9 CR + = ASCII 48 49 57 13 43 45 61 Die 128 ASCII-Zeichen entsprechen den Bytes 0000 0000 bis 0111 1111. Eine Datei, die nur ASCII-Zeichen enthält, also Bytes, deren erstes Bit 0 ist, nennt man ASCII-Datei. Oft versteht man unter einer ASCII-Datei auch einfach eine Textdatei, selbst wenn Codes aus einer ASCII-Erweiterung verwendet werden. ASCII-Erweiterungen Bei der ASCII-Codierung werden nur die letzten 7 Bits eines Byte genutzt. Das erste Bit verwendete man früher als Kontrollbit für die Datenübertragung. Es wurde auf 0 oder 1 gesetzt, je nachdem ob die Anzahl der 1-en an den übrigen 7 Bitpositionen gerade (even) oder ungerade (odd) war. Die Anzahl der 1-en in dem gesamten Byte wurde dadurch immer gerade (even parity). Wenn nun bei der Übertragung ein kleiner Fehler auftrat, d.h. wenn in dem übertragenen Byte genau ein Bit verfälscht wurde, so konnte der Empfänger dies daran erkennen, dass die Anzahl der 1-en ungerade war. Bei der Verwendung des ASCII-Codes zur Speicherung von Texten und auch als Folge der verbesserten Qualität der Datenübertragung wurde dieses Kontrollbit überflüssig. Daher lag es nahe, nun alle 8 Bit zur Zeichenkodierung zu verwenden. Somit ergab sich ein weiterer verfügbarer Bereich von ASCII 128 bis ASCII 255. Da der ursprüngliche ASCII-Zeichensatz in den USA entwickelt wurde, enthält er natürlich keine Umlaute. Der IBM-PC nutzt diese zusätzlichen freien Codes von 128 - 255 zur Darstellung von sprachspezifischen Zeichen wie z.B. "ä" (ASCII 132), "ö" (ASCII 148), "ü" (ASCII 129) und einigen Sonderzeichen anderer Sprachen. Dieser erweiterte ASCII-Code wird "Extended ASCII" oder auch ANSI (American National Standard Institute) genannt. Der ANSI-Zeichensatz wurde von der ISO (International Organization for Standardization) standardisiert unter ISO 6937-2. 12 Leider ist auch die Auswahl der sprachspezifischen Sonderzeichen bei weitem nicht ausreichend für die vielfältigen Symbole fremder Schriften. Daher wurden von der ISO (International Organization for Standardization) verschiedene ASCIIErweiterungen (Codepages) normiert: ISO 8859-1 = Latin-1 (Erweiterung für Westeuropa) ISO 8859-2 = Latin-2 (Erweiterung für Osteuropa) ASCII-Probleme Werden zwischen zwei Rechnern, die nicht dieselben ASCII-Erweiterungen einsetzen, Daten ausgetauscht, werden die Zeichen der Erweiterungen nicht korrekt dargestellt. Ein Problem ist das z.B. bei Emails, wenn der Sender die deutsche ASCIIErweiterung verwendet, die auch die Umlaute wie ä, ö, ü und das ß umfasst und der Empfänger die ASCII-Erweiterung für z.B. Portugal, die diese speziellen Zeichen nicht enthält, dafür aber andere: Dort, wo eigentlich die deutschen Sonderzeichen der ASCII-Erweiterung stehen sollten, tauchen dann sonderbare Grafikzeichen oder Symbole auf. Dieses Phänomen ist wahrscheinlich jedem schon einmal begegnet. Es könnte auch sein, dass der Sender eine länderspezifische Erweiterung benutzt (8-Bit breit) und der Empfänger den ASCII-Code (7-Bit breit!). In diesem Fall geht sogar das komplette 1. Bit jedes Zeichens verloren, weil der Empfänger dieses als Korrekturbit behandelt. --> Ein Lösungsansatz ist, die zu sendenden Dateien (z.B. Bilder oder Programme) vorher in ASCII-konforme Dateien zu konvertieren, d.h. in Dateien, die nur noch aus "druckbaren ASCII-Zeichen" bestehen. Dies kann z.B. mit dem Tool "UUencode" vorgenommen werden. Das Gegenstück, das die Datei wieder in ihren Urzustand versetzt heißt "UUdecode". Allerdings ist dies heutzutage nicht mehr ganz so problematisch, da man auch das folgende Verfahren für das Versenden von Dateien per Email verwenden kann und dies in den meisten Fällen schon tut, ohne davon zu wissen: MIME (Multipurpose Internet Mail Extensions). MIME ist ein Standard-Verfahren, das es erlaubt, beliebige Dateien als Anhang per Mail zu verschicken. Die Übertragung der Dateien erfolgt binär, also ohne Umwandlung in ASCII. Das oben für Emails geschilderte Phänomen ist bestimmt jedem auch schon bei Webseiten begegnet, also bei HTML-Dateien. Die Lösung für dieses Problem ist die Darstellung der betroffenen Sonderzeichen (im Deutschen die Umlaute und das 'ß') als sog. Entities, hier eine kleine Auswahl: ä = &auml; Ä = &Auml; ö = &ouml; Ö = &Ouml; 13 ß = &szlig; 2.1.2 Unicode Nachdem sich das Hantieren mit diversen, länderspezifischen ASCII-Codepages unterschiedlicher Bit-Breite (7-Bit US-ASCII und 8-BIT Extended ASCII oder ANSICode) bei der weltweiten Datenübertragung als sehr lästig und unschön erwiesen hat, entstand in den letzten Jahren ein neuer Standard, der versucht, sämtliche relevanten Zeichen aus den unterschiedlichsten Kulturkreisen in einem universellen Code zu vereinigen. Dieser neue Code heißt Unicode und verwendet eine 16-Bit Codierung, kann also maximal 65.536 Zeichen darstellen. Länderspezifische Zeichen wie die deutschen Umlaute gehören ebenso selbstverständlich zum Unicode-Zeichensatz wie kyrillische, arabische, japanische, chinesische und tibetanische Schriftzeichen. Die ersten 128 Zeichen sind identisch mit dem ASCII-Code, die folgenden 128 sind identisch mit dem ISO-Latin 1-Code. Herkömmliche Programmiersprachen lassen meist keine Zeichen zu, die über den ASCII-Code hinausgehen. Java erlaubt als erste der großen Programmiersprachen die Verwendung beliebiger Unicode-Zeichen. Unicode ist als UCS (Universal Character Set) bzw. als ISO-10646 vom Unicode-Konsortium und der ISO standardisiert worden. In der Definition von UCS geht die ISO allerdings noch einen Schritt weiter als das Unicode-Konsortium: Es werden sowohl eine 16-Bit Codierung (UCS-2) als auch eine 31-Bit Codierung (UCS-4) festgelegt. Die Codes von UCS-2 werden als basic-multilingual plane (BMP) bezeichnet, beinhalten alle bisher definierten Codes und stimmen mit Unicode überein. Codes, die UCS-4 ausnutzen sind für potenzielle zukünftige Erweiterungen vorgesehen. Die Speicherung von Texten im Unicode benötigt zwei (UCS-2) bis viermal (UCS-4) soviel Speicherplatz, wie im ASCII-Code. Dies liegt daran, dass ein Zeichen im UCS-2 Format mit 16 Bit statt mit 8 Bit codiert wird, d.h. selbst Zeichen, die auch im alten ASCII-Standard enthalten sind, werden mit 16-Bit codiert, von denen die ersten 9-Bit Nullen sind, also keine Information enthalten! Aus diesem Grund hat sich eine weitere Codierung etabliert, die etwas ökonomischer mit Speicherplatz umgeht: UTF-8. 2.1.3 UTF-8 UTF steht für UCS Transformation Format, wodurch betont wird, dass es sich lediglich um eine andere Codierung von UCS bzw. Unicode handelt. Neben UTF-8 existieren unter anderem noch die Formate UTF-7, UTF-16 sowie UTF-32, die aber nur geringe Bedeutung erlangt haben, weshalb wir uns hier auf die Darstellung von UTF-8 beschränken. UTF-8 ist eine Mehrbyte-Codierung, bzw. ein Code von variabler Bit-Breite. 14 ASCII-Zeichen werden mit 1 Byte codiert, in dem das erste Bit immer Null ist: 0xxx xxxx Jedes Byte, das mit einer 1 beginnt, gehört zu einem aus mehreren Bytes bestehenden UTF-8 Code: 110x xxxx 10xx xxxx = 2 Byte Code Besteht ein UTF-8 Code aus n ≥ 2 Bytes beginnt das erste Byte (Startbyte) mit n Einsen, gefolgt von einer Null und jedes n-1 folgende Byte mit der Bitfolge 10 (siehe Beispiel oben). Ein 3-Byte Code sieht also folgendermaßen aus: 1110 xxxx 10xx xxxx 10xx xxxx Mit den 16 noch verfügbaren Bits können alle 16-Bit UCS-2 Codes dargestellt werden. UTF-8 codierte Dateien sind also voll abwärtskompatibel zur 7-Bit ASCII Vergangenheit und vergrößern den Umfang von Dateien aus dem amerikanischen und europäischen Bereich gar nicht oder nur unwesentlich. Diese Eigenschaften haben dazu geführt, dass diese Codierungsmethode der de facto Standard bei der Verwendung von Unicode geworden ist. Bei den Webseiten des Internets wird UTF-8 immer häufiger verwendet - alternativ dazu, können, wie bereits geschildert, in HTML-Dateien Sonderzeichen durch sog. Entities umschrieben werden. 2.1.4 Zeichenketten Zur Codierung eines fortlaufenden Textes fügt man einfach die Codes der einzelnen Zeichen aneinander. Eine Folge von Textzeichen heißt auch Zeichenkette oder engl. string. Der Text "WING WS 2010" wird also durch folgende Zeichenfolge repräsentiert: W,I,N,G, ,W,S, ,2,0,1,0 Jedes dieser Zeichen, einschließlich der Leerzeichen " " ersetzen wir durch seine dezimale Nummer in der ASCII-Tabelle und erhalten: 87 73 78 71 32 87 83 32 50 48 49 48 Als binäre Bitfolge sieht das Ganze dann so aus: 01010111 | 01001001 | 01001110 | 01000111 | 0100000 | 01010111 01010011 | 00100000 | 00110010 | 00110000 | 0110001 | 00110000 15 2.2 Zahlendarstellung Wie alle bisher diskutierten Informationen werden auch Zahlen durch Bitfolgen dargestellt. Wenn eine Zahl wie z.B. "4711" (und welche andere Zahl könnten wir in Köln als Beispiel wählen?) mitten in einem Text vorkommt, etwa in einem Werbeslogan, so wird sie, wie der Rest des Textes, als Folge ihrer ASCIIZeichencodes gespeichert, d.h. als Folge der ASCII-Zeichen für "4", "7", "1" und "1". Dies wären hier die ASCII-Codes mit den Nummern 52, 55, 49, 49. Eine solche Darstellung ist aber für Zahlen, mit denen man arithmetische Operationen durchführen möchte, unpraktisch und verschwendet unnötig Platz (8-Bit pro Ziffer, das wären hier schon 4 Byte). Man kann Zahlen viel effizienter durch eine umkehrbar eindeutige (ein-zu-eins) Zuordnung zwischen Bitfolgen und Zahlen kodieren. Wenn wir nur Bitfolgen einer festen Länge N betrachten, können wir damit 2 hoch N viele Zahlen darstellen. Gebräuchlich sind N = 8, 16, 32 oder 64. Man repräsentiert durch die Bitfolgen der Länge N dann: die natürlichen Zahlen von 0 bis 2N - 1 die ganzen Zahlen von -2N-1 bis 2N-1 - 1 ein Intervall der reellen Zahlen mit begrenzter Genauigkeit 2.2.1 Binärdarstellung Will man nur positive ganze Zahlen (natürliche Zahlen) darstellen, so kann man mit n Bits Zahlen von 0 bis 2N - 1, das sind 2N viele, abdecken. Die Zuordnung der Bitfolgen zu den natürlichen Zahlen geschieht so, dass die Bitfolge der Binärdarstellung der darzustellenden Zahl entspricht. Die natürlichen Zahlen nennt man in der Informatik auch vorzeichenlose Zahlen (unsigned), und die Binärdarstellung heißt demzufolge auch vorzeichenlose Darstellung. Um die Idee der Binärdarstellung zu verstehen, führen wir uns noch einmal das gebräuchliche Dezimalsystem (Zehnersystem) vor Augen. Man kann die einzelnen Ziffern einer Dezimalzahl auch als Koeffizienten von Zehnerpotenzen schreiben. Deutlicher wird das an folgendem Beispiel: Für das Binärsystem (Zweiersystem) hat man anstelle der Ziffern 0...9 nur die beiden Ziffern 0 und 1 zur Verfügung, daher stellen die einzelnen Ziffern einer Binärzahl die Koeffizienten der Potenzen von 2 dar. Die Bitfolge 1101 hat daher den Zahlenwert: 16 Mit drei Binärziffern können wir die Zahlenwerte von 0 bis 7 darstellen: Mit 4 Bits können wir analog die 16 Zahlen von 0 bis 15 erfassen, mit 8 Bits die 256 Zahlen von 0 bis 255, mit 16 Bits die 65.536 Zahlen von 0 bis 65.535 und mit 32 Bits die 4.294.967.296 von 0 bis 4.294.967.295 2.2.2 Hexadezimaldarstellung Neben dem Dezimalsystem und dem Binärsystem ist in der Informatik auch das Hexadezimalsystem in Gebrauch. Es handelt sich um ein Zahlensystem zur Basis 16. Man benötigt 16 verschiedene Ziffern und benutzt dazu 0...9 und A...F. Um zu verstehen, wieso sich gerade das 16er System anbietet, erinnern wir uns an die sog. Nibbles oder Halb-Bytes. Dies sind Bitfolgen der Länge N=4. Wie wir im letzten Abschnitt gesehen haben, kann man mit 4 Bits 16 verschiedene Zahlen darstellen: 0000=0 0100=4 1000=8 1100=C 0001=1 0101=5 1001=9 1101=D 0010=2 0110=6 1010=A 1110=E 0011=3 0111=7 1011=B 1111=F 2.2.3 Umwandlung zwischen verschiedenen Zahlensystemen Die Umwandlung einer Binär- oder Hexadezimalzahl in das Dezimalsystem ist einfach. Wir müssen nur die Ziffern mit den entsprechenden Potenzen von 2 oder 16 multiplizieren und die Ergebnisse aufsummieren. Exkurs: Division mit Rest Bevor wir uns nun der etwas schwierigeren Wandlung von Dezimalzahlen in andere Zahlensysteme zuwenden, lohnt es sich, eine wichtige zahlentheoretische Beobachtung vorweg zu schicken: --> Wenn wir eine natürliche Zahl z durch eine andere natürliche Zahl d≠0 teilen, erhalten wir einen Quotienten q und einen Rest r. Es gilt dann offensichtlich der Zusammenhang: z=qxd+r mit 0 ≤ r < d 17 In der Informatik bezeichnet man die Operation des exakten Dividierens mit div und die Operation, die zwei Zahlen den Divisionsrest zuordnet, mit mod. Beispielsweise gilt: 39 div 8 = 4 und 39 mod 8 = 7 Die Probe ergibt auch hier: 39 = 4 x 8 + 7. Die obige Gleichung können wir unter Zuhilfenahme der Operatoren div und mod allgemein schreiben als: z = (z div d) x d + (z mod d) 2.2.4 Die Zweierkomplementdarstellung Die Zweierkomplementdarstellung ist die gebräuchliche interne Repräsentation ganzer positiver und negativer Zahlen. Sie kommt auf sehr einfache Weise zu Stande. Wir erläutern Sie zunächst für den Fall N = 4. Mit 4 Bits kann man einen Bereich von 24 = 16 ganzen Zahlen abdecken. Den Bereich kann man frei wählen, also z.B. die 16 Zahlen von -8 bis +7. Man zählt nun von 0 beginnend aufwärts, bis man die obere Grenze +7 erreicht, anschließend fährt man an der unteren Grenze -8 fort und zählt aufwärts, bis man die Zahl -1 erreicht hat. Auf diese Weise erhält man nun folgende Zuordnung von Bitfolgen zu ganzen Zahlen: 1000 = -8 1001 = -7 1010 = -6 1011 = -5 1100 = -4 1101 = -3 1110 = -2 1111 = -1 0000 = 0 0001 = 1 0010 = 2 0011 = 3 0100 = 4 0101 = 5 0110 = 6 0111 = 7 Jetzt sieht man auch den Grund, wieso der Bereich von -8 bis +7 gewählt wurde und nicht etwa der Bereich von -7 bis +8: 18 Bei dem mit 0 beginnenden Hochzählen wird bei der achten Bitfolge zum ersten Mal das erste Bit zu 1. Springt man also ab der 8. Bitfolge in den negativen Bereich, so hat man die folgende Eigenschaft: Bei den Zweierkomplementzahlen stellt das erste Bit das Vorzeichen dar. Das Zweierkomplement hat zudem den Vorteil, dass die '0' nur einmal vorkommt. Darstellbar sind mit dem Zweierkomplement die Zahlen von -2N-1 bis 2N-1-1. Zweierkomplementzahlen werden auch als signed binary numbers oder signed integers bezeichnet. Berechnung des Zweierkomplements wird in der Übung behandelt! Standardformate für Zweierkomplementzahlen Prinzipiell kann man beliebige Zahlenformate vereinbaren, in der Praxis werden fast ausschließlich Zahlenformate mit 8, 16, 32 oder 64 Bits eingesetzt. In den meisten Programmiersprachen gibt es vordefinierte ganzzahlige Datentypen mit unterschiedlichen Wertebereichen. Je größer das Format, desto größer ist natürlich der erfasste Zahlenbereich. Einige typische Formate sind z.B.: Wertebereich -27 ... 27-1 oder -128 ... 127 -215 ... 215-1 oder -32.768 ... 32.767 -231 ... 231-1 oder -2.147.483.648 ... 2.147.483.647 -263 ... 263-1 oder -9.223.372.036.854.775.808 ... 9.223.372.036.854.775.807 Bytes Java C++ 8 Bit char byte 16 Bit short int/short 32 Bit int 64 Bit long int/long ---- Arithmetische Operationen im Binärsystem werden in den Übungen ausführlich behandelt! Gleiches gilt für die Darstellung von rationalen und reellen Zahlen! 2.3 Darstellung logischer Werte Logische Werte sind die Wahrheitswerte Falsch und Wahr (engl. true oder false). Sie werden meist durch die Buchstaben T und F abgekürzt. Auf diesen logischen Werten sind die Booleschen Verknüpfungen NOT (Negation), 19 AND (logisches Und), OR (logisches Oder) und XOR (exklusives Oder) durch die folgenden Verknüpfungstafeln festgelegt: NOT F T T F OR F T ---------------F T F T T T AND F T F F F T F T XOR F ---------------F F T T T T F Beispiele: ¬F = T T AND T = T F OR F = F T XOR F = T Da es nur zwei Wahrheitswerte gibt, könnte man diese durch die beiden möglichen Werte eines Bits darstellen, z.B. durch 0=F und 1=T. Da aber ein Byte die kleinste Einheit ist, mit der ein Computer operiert, spendiert man meist ein ganzes Byte für einen Wahrheitswert. Eine gängige Codierung ist: F = 0000 0000 T = 1111 1111 20 3 Rechnerorganisation – Teil 1 3.1 Von-Neumann-Architektur John von Neumann Computer der ersten Generation waren riesige Geräte. So bestand zum Beispiel der ENIAC (1943), ein aus Mitteln der amerikanischen Armee finanziertes Projekt, aus 18.000 Vakuumröhren und 1500 Relais, wog 30 Tonnen und verbrauchte 140 KW an Strom. Programmiert wurde der ENIAC durch Einstellen von bis zu 6000 mehrstelligen Schaltern und durch Anschluss unzähliger Buchsen mit einem wahrhaften Wald an Steckbrückenkabeln. Im Jahre 1944 legte John von Neumann, ein ehemaliges Mitglied des ENIACProjektes, ein Architektur-Konzept für einen speicherprogrammierten Universalrechner vor. Von Neumann fiel auf, dass die Programmierung von Computern mit Unmengen von Schaltern und Kabeln langsam, mühsam und unflexibel war. Er erkannte, dass man das Programm mit den Daten im Speicher des Computers in digitaler Form darstellen konnte, und er erkannte, dass man die schwerfällige, serielle Dezimalarithmetik des ENIAC, bei der jede Ziffer mit zehn Vakuumröhren (1 On und 9 Off) dargestellt wird, durch parallele Binärarithmetik ablösen kann. Von-Neumann-Architektur Sein erster Entwurf ist heute als Von-Neumann-Maschine oder als Von-NeumannArchitektur bekannt. Wie weitsichtig und visionär sein Konzept war, wird daran deutlich, dass auch aktuellen Computern noch diese Architektur zugrunde liegt. Eine Von-Neumann-Maschine weist die folgenden wichtigen Merkmale auf: 1. Ein programmgesteuerter Rechner besteht aus a. zentraler Recheneinheit (CPU = Central Processing Unit) mit einer ALU (Arithmetical Logical Unit) und einer Steuereinheit b. Speicher c. Ein- und Ausgabe-Einheiten und d. den internen Datenwegen (Busse) 2. Die Zahlen werden im Rechner binär dargestellt. 3. Die Struktur des Rechners ist unabhängig von dem zu bearbeitenden Problem (--> Universalrechner). Die verschiedenen Aufgaben werden durch entsprechende Programme gelöst. 21 4. Programme und von diesen benötigte Daten werden in einem gemeinsamen Speicher abgelegt. Die Speicherplätze sind gleichlang und werden über Adressen einzeln angesprochen. 5. Befehle geben nur die Speicheradresse an, wo die Daten abgelegt sind, und nicht die Daten selbst. Die bedeutendste Neuerung in der damaligen Zeit war von Neumanns Idee, das Programm und die Daten zuerst in denselben Speicher zu laden und dann auszuführen. Bis dahin war das Programm noch hardwaremäßig verschaltet oder wurde über Lochstreifen schrittweise eingelesen und sofort (streng sequentiell) bearbeitet. Nun war es möglich: 1. Sprünge einzuführen, sowohl auf vorhergehende wie spätere Programmsequenzen, und 2. Programmcode während des Programmablaufes zu modifizieren (allerdings eine sehr riskante, fehleranfällige Möglichkeit!). Von Neumann erreicht also mit seinem Konzept, daß der Rechner selbstständig logische Entscheidungen treffen kann. Damit ist der Übergang vom starren Programmablauf zur flexiblen Programmsteuerung oder, anders ausgedrückt, von der Rechenmaschine zur Datenverarbeitungsanlage vollzogen. Nachteile der von-Neumann-Architektur Die von Neumann Architektur hat aber auch einige Nachteile: 1. Im Speicher kann man Befehle und Daten anhand des Bitmusters nicht unterscheiden. 22 2. Im Speicher kann man variable und konstante Daten nicht unterscheiden. 3. Bei falscher Adressierung können Speicherinhalte verändert werden, die nicht geändert werden dürfen, wie z.B. Befehle und Konstanten (Eine Bitänderung bei einem Befehl erzeugt einen ganz anderen Befehl!) 4. Da Daten und Befehle im Speicher gehalten werden, wird die Verbindung und Datenübertragung zwischen CPU und Speicher über den Systembus zum sog. Von-Neumann-Flaschenhals. Jeglicher Datenverkehr von und zur CPU wird über den internen Bus abgewickelt, dessen Transfergeschwindigkeit langsamer ist, als die Verarbeitungsgeschwindigkeit der CPU. Dieses Problem versucht man in modernen PC's durch die Verwendung von schnellem Cache-Speicher, der meist in der CPU integriert ist, abzuschwächen. 3.2 Merkmale und Komponenten der Von-Neumann Rechnerarchitektur 3.2.1 Zentrale Recheneinheit / Central Processing Unit Die Zentrale Recheneinheit oder CPU stellt das "Gehirn" im System dar. Sie besteht aus Recheneinheit und Steuereinheit. Ein geschicktes Zusammenspiel der Steuereinheit mit der Recheneinheit sorgt dafür, dass nacheinander die Befehle geholt und dann ausgeführt werden. Für arithmetische Operationen, wie z.B. der Addition, oder logische Operationen, wie z.B. AND und NOT, ist die Recheneinheit zuständig. Die Recheneinheit hat eine feste Verarbeitungsbreite, z.B. 8, 16, 32 oder 64 Bit. Man spricht dann von einem 8, 16, 32 oder 64-Bit System. Recheneinheit oder ALU Die Aufgabe der Recheneinheit besteht in der Bearbeitung der Daten, besonders dem Ausführen von arithmetischen und logischen Operationen. Steuereinheit Zur wichtigsten Aufgabe der Steuereinheit gehört die Koordination der zeitlichen Abläufe im Rechner. Dazu muss die Steuereinheit die Befehle aus dem Speicher holen, entschlüsseln und deren Ausführung steuern. Die Steuereinheit besteht aus Befehlsregister, Befehlsdecoder, Speicheradressregister und Befehlszähler. 1. Im Befehlsregister (IR=Instruction Register) befindet sich jeweils der aktuell zu bearbeitende Befehl. Das Befehlsregister ist ein spezielles CPU-Register, das vom Anwender nicht adressierbar ist. 2. Der Befehlsdecoder entschlüsselt den Befehl und erzeugt die zur Ausführung notwendigen Hardware-Steuersignale 3. Im Speicheradressregister (MAR=Memory Adress Register) steht die Adresse des nächsten auszuführenden Befehls oder die Adresse eines Datenwortes, falls zur Ausführung eines Befehls ein Datenwort vom Speicher geholt bzw. in den Speicher gebracht werden muss 23 Der Befehlszähler (PC=Program Counter) übernimmt den Wert des Speicheradressregisters und erhöht ihn entsprechend der Befehlslänge. Bei einem linearen Befehlsablauf kann das Speicheradressregister diesen Wert als Adresse für den folgenden Befehl übernehmen; bei einem Sprung ist die neue Adresse im Befehl selbst angegeben. 3.2.2 Speicher Der Speicher eines von Neumann Rechners besteht aus einer Vielzahl von Speicherzellen. Jede Speicherzelle kann ein Bit speichern. Man faßt jeweils mehrere Zellen zu einer Speicherzeile oder einem Speicherwort zusammen (z.B. bei einem 32-Bit System ergeben 32 Bit ein Speicherwort) Um ein Speicherwort in einem Speicher ablegen und auch wiederfinden zu können, zählt man die Speicherplätze vom Speicheranfang bis zum Ende durch, und zwar byteweise. Diese Zahlen nennt man Adressen. Man braucht also so viele Adressen, wie der Speicher Bytes enthält. 3.2.3 Interne Datenwege / Busse Innerhalb eines von Neumann-Rechners entstehen Daten in verschiedenen Hardware-Komponenten und werden dann in anderen Komponenten benötigt. Der Transport dieser Daten erfolgt auf den sog. internen Datenwegen. Diese wollen wir im Folgenden näher betrachten. Die wichtigsten Busse sind 1) der Datenbus, 2) der Adressbus und 3) der Steuerbus 1. Der Datenbus wird normalerweise parallel übertragen, d.h. bei einem 32-Bit System besteht der Datenbus aus 32-Leitungen. Der Datenbus überträgt die Daten, die über den Adressbus angefordert werden. 2. Der Adressbus besteht aus den Adressleitungen, deren Anzahl vom Adressbereich der CPU abhängt. Ein typischer Wert sind heute 32-Leitungen, um mit 32 Bit bis zu 4 GByte adressieren zu können. Über den Adressbus werden nur Adressen übertragen 3. Der Steuerbus ist der Teil des Bussystems, welcher die Steuerung des Busses bewerkstelligt. Hierzu zählen unter anderem die Leitungen für die Interrupt-Steuerung, Buszugriffssteuerung, der Taktung (falls ein Bustakt erforderlich ist), Reset- und Statusleitungen. 24 3.2.4 Ein-/Ausgabeeinheit Eingabe-/Ausgabewerk steht im Konzept des Von-Neumann-Rechners für alle die Komponenten, die den Computer mit "draußen" verbindet. "Draußen" bedeutet dabei nicht „außerhalb des Gehäuses", sondern "nicht mehr zu dem eigentlichen, universellen Von Neumann Rechner-Konzept gehörend". In einem modernen PC verkörpert der Prozessor das Rechenwerk und die Steuereinheit, die Northbridge mit dem RAM den Speicher bzw. das Speicherwerk und der Rest, angefangen mit der Southbridge, bildet das Eingabe/Ausgabewerk. Eine Grafikkarte, für die allermeisten Rechner notwendiges Bauteil zur Kommunikation mit dem Benutzer, ist bereits "draußen" – und heutzutage bereits ein spezialisierter Rechner für sich. Zu dem Ein-/Ausgabewerk gehören also z.B. der PCI-Bus für Einsteckkarten ebenso wie die verschiedenen Schnittstellen für Peripheriegeräte und Festplatten. 3.3 Mainboard Zentraler Bestandteil des Rechners ist das Mainboard (auch: Hauptplatine oder Motherboard). Auf dem Mainboard sind die einzelnen Bauteile wie die CPU (Hauptprozessor), der Arbeitsspeicher (RAM), der Chip mit dem BIOS sowie verschiedene Steckplätze und Schnittstellenbausteine montiert. Es bietet Steckplätze für Grafikkarten (AGP oder PCI-Express) Für PCI-Steckkarten (Soundkarte, Netzwerkkarte usw.) Außerdem beherbergt das Mainboard den sog. Chipsatz. Der Chipsatz besteht in der Regel aus Northbridge und Southbridge. 3.3.1 Systembus Die oben genannten Erweiterungskarten stecken in den bereits erwähnten Steckplätzen des Mainboards. 25 Diese Steckplätze sind untereinander und mit der Hauptplatine durch einen sog. Bus (auch Systembus genannt) verbunden. Hierbei handelt es sich um eine Serie paralleler Datenleitungen, über die Daten zwischen Prozessor und Peripheriegeräten ausgetauscht werden. Busse gibt es mit unterschiedlich vielen Leitungen und mit unterschiedlichen Protokollen (oder Standards) zur Übertragung von Daten über diese Leitungen. Die Leistungsfähigkeit eines Busses wird an der Anzahl von Bytes gemessen, die in einer Sekunde übertragen werden können. Moderne Busse schaffen einige hundert MByte pro Sekunde. Da an dem Bus viele unabhängig und gleichzeitig arbeitende Peripheriegeräte angeschlossen sind, muss auch der Buszugang durch einen Controller (den Buscontroller) geregelt werden. Die Standards für Technik und Datenübertragung des Busses haben sich in den letzten Jahren sehr verändert. Einige dieser Standards heißen: ISA (Industry Standard Architecture) EISA (Extended ISA) PCI (Peripheral Component Interconnect) AGP (Accelerated Graphics Port) (Ist strenggenommen kein Bus, da man nur genau ein Gerät anschließen kann) PCI-Express Mit den Standards ändern sich auch die Steckplätze. Bis vor kurzem hatten aktuelle Mainboards einen AGP-Steckplatz für Grafikkarten. Der AGP Steckplatz ist derzeit aber bereits fast durch seinen leistungsfähigeren Nachkommen PCI-Express x16 ersetzt worden. 3.3.2 BIOS Das Akronym BIOS steht für Basic Input Output System. Das BIOS ist heutzutage in einem sogenannten Flash-EEPROM Baustein gespeichert. EEPROM steht für Electrically Erasable Programmable Read-Only Memory, d. h. elektrisch löschbarer, programmierbarer Nur-Lese-Speicher. Im diesem BIOS-Chip sind grundlegende und sehr elementare Hilfsprogramme zur Ansteuerung der verschiedenen Hardwarekomponenten wie Tastatur, Maus, Festplatte und Grafikcontroller abgelegt. Die Programme werden auch als Interrupts (engl. für Unterbrechungen) bezeichnet, weil Sie je nach Prioritätenstufe andere gleichzeitig laufende Programme unterbrechen dürfen. 26 Jeder Tastendruck, jede Mausbewegung, führt zum Aufruf eines Interrupts. Dabei werden die Parameter des aufgetretenen Ereignisses ermittelt und es wird ggf. darauf reagiert. Der Boot-Vorgang Zusätzlich enthält das BIOS Programme, die nach dem Einschalten des Rechners ausgeführt werden. Dazu gehört eine Prüfung, welche Geräte angeschlossen sind, ein Funktionstest des Hauptspeichers und das Laden des Betriebssystems von Festplatte, Netzwerk, Diskette oder CD-Rom. Dies ist ein mehrstufiger Prozess ein Programm im BIOS lädt und startet ein Ladeprogramm (loader) Dieses Ladeprogramm lädt das eigentliche Betriebssystem. Diesen Vorgang nennt man booten (von engl. bootstrapping = Schuh schnüren. Eine andere Geschichte behauptet, booten stamme aus den Geschichten von Baron Münchhausen, der sich in der englischen Fassung nicht an den eigenen Haaren, sondern an den eigenen Schnürsenkeln=boot straps aus dem Sumpf gezogen hat.) Verbindung zwischen Betriebssystem und Hardware Das BIOS stellt also (gemeinsam mit den Treibern) gewissermaßen die Brücke zwischen der angeschlossenen Hardware und dem Betriebssystem dar. 27 3.3.3 CPU Die CPU (oder auch Prozessor) ist das Kernstück eines Computers. Er dient der Verarbeitung von Daten, die sich in Form von Bytes im Speicher des Rechners befinden. Zwei wesentliche Bestandteile der CPU sind Register und ALU (arithmetical logical unit). Die ALU ist eine komplizierte Schaltung, welche die eigentlichen mathematischen und logischen Operationen ausführen kann. Register sind extrem schnelle Hilfsspeicherzellen, die direkt mit der ALU verbunden sind. Die ALU-Operationen erwarten Ihre Argumente in bestimmten Registern und liefern ihre Ergebnisse wieder in Registern ab. Auch der Datentransfer vom Speicher zur CPU läuft durch die Register. Üblicherweise sind diese Register 32 oder 64 Bit breit. Man spricht dann von einem 32-Bit oder einem 64 Bit Prozessor, meint damit aber mehrere, eigentlich zu unterscheidende Aspekte: D: Die Anzahl der Datenbits, die von dem verwendeten Prozessor in einem arithmetischen Befehl verknüpft werden können. (Breite der Register) S: Die Anzahl der Bits, die von einem LOAD- oder STORE-Befehl gleichzeitig zwischen dem Speicher und einem CPU-Register transportiert werden können (s. Datenbus). A: Die Anzahl der Bits, die zur Adressierung verwendet werden können. (s. Adressbus) Cache Speicher Der Cache-Speicher der CPU ist ein schneller Puffer-Speicher, in welchen ständig Kopien des Hauptspeichers vorgehalten werden, die möglicherweise als nächstes von der CPU benötigt werden. Die Ziele beim Einsatz eines Caches sind eine Verringerung der Zugriffszeit bzw. eine Verringerung der Anzahl der Zugriffe auf den zu cachenden Speicher. Ein weiterer eher nebensächlicher Effekt beim Einsatz von Caches ist die verringerte Bandbreitenanforderung an die nächsthöhere Speicherebene der Speicherhierarchie. Dadurch, dass oftmals der Großteil der Anfragen vom Cache beantwortet werden kann (sog. Cache-Hit), sinkt die Anzahl der Zugriffe und damit die Bandbreitenanforderung an den zu cachenden Speicher. Ein moderner 28 Mikroprozessor ohne Cache würde selbst mit unendlich kleiner Zugriffszeit des Hauptspeichers dadurch ausgebremst, dass nicht genügend Speicherbandbreite zur Verfügung steht, weil durch den Wegfall des Caches die Anzahl der Zugriffe auf den Hauptspeicher und damit die Anforderung an die Speicherbandbreite stark zunehmen würde. Bei CPUs kann der Einsatz von Caches somit zum Verringern des Von-NeumannFlaschenhals der Von-Neumann-Architektur beitragen. Die Ausführungsgeschwindigkeit von Programmen kann dadurch im Mittel enorm gesteigert werden. Ein Nachteil von Caches ist das schlecht vorhersagbare Echtzeitverhalten, da die Ausführungszeit eines Befehls aufgrund von Cache-Misses nicht immer konstant ist. Cache Hierarchie Da Cache-Speicher zwar sehr schnell, aber auch sehr teuer ist, ist es (aus kostengründen) nicht ohne weiteres möglich, einen Cache zu bauen, der gleichzeitig sowohl groß als auch schnell ist. Man kann aber mehrere Caches verwenden - z. B. einen kleinen schnellen und einen großen langsameren Cache (der aber immer noch Größenordnungen schneller ist als der zu cachende Speicher). Damit kann man die beiden konkurrierenden Ziele von geringer Zugriffszeit und Cachegröße bei vertretbaren Kosten realisieren. Existieren mehrere Caches, so bilden diese eine Cache-Hierarchie, die Teil der Speicherhierarchie ist. Die einzelnen Caches werden als Level-1 bis Level-n durchnummeriert (kurz: L1, L2 usw.). Die niedrigste Nummer bezeichnet hierbei den Cache mit der kleinsten Zugriffszeit welcher also als erstes durchsucht wird. Enthält der L1 Cache die benötigten Daten nicht, so wird der nächste (meist etwas langsamere, aber größere) Cache (also der L2) durchsucht usw. Das geschieht solange, bis die Daten entweder in einem Cache-Level gefunden oder alle Caches ohne Erfolg durchsucht wurden (Cache Miss). In diesem Fall muss auf den verhältnismäßig langsamen Speicher zugegriffen werden. Moderne CPUs haben meist zwei oder drei Cache-Levels. Mehr als drei ist eher unüblich. Festplatten haben nur einen Cache. Prozessor-Cache Bei CPUs kann der Cache direkt im Prozessor integriert, d.h. On-Die (bei aktuellen Prozessoren bis zu 4MB L2-Cache) oder extern auf der Hauptplatine oder auf der Prozessorplatine (z.B. beim Pentium II - Slot1) platziert sein. Je nach Ort des Caches arbeitet dieser mit unterschiedlichen Taktfrequenzen: Der L1 ist fast immer direkt im Prozessor integriert und arbeitet daher mit dem vollen 29 Prozessortakt, also meist mit mehreren Gigahertz. Ein externer Cache hingegen wird oftmals nur mit einigen hundert Megahertz getaktet. Gängige Größen für L1-Caches sind 4 bis 256 KB und für den L2 256 bis 4096 KB. Bei aktuellen CPUs ist der L1- und der L2-Cache direkt auf dem Prozessor-Kern (Die) integriert. Prozessor-Hersteller Die beiden wichtigsten Prozessor-Hersteller sind Intel (z.B. Pentium 4, Core 2 Duo u.a.) und AMD (Athlon XP, Athlon64, Athlon64 X2 u.a.). Jeder Hersteller verwendet mittlerweile einen eigenen Prozessor-Sockel: Bei Intel ist der aktuellste Sockel zur Zeit der Sockel 775. In diesen Sockel passen Pentium 4, Pentium D, und Core 2 Duo Prozessoren. Die aktuellsten Sockel bei AMD sind zur Zeit der Sockel 939 und der neue Sockel AM2. In den Sockel 939 passen Athlon64, Athlon64 X2 und Opteron Prozessoren. Auch für den Nachfolger, den Sockel AM2, gibt es passende Athlon64 und Athlon64 X2 Modelle. 3.3.4 Arbeitsspeicher / RAM (Random Access Memory) Im Hauptspeicher/Arbeitsspeicher/RAM eines Computers werden Programme und Daten abgelegt. Die Daten werden von den Programmen bearbeitet. Der Inhalt des RAM's ändert sich ständig - insbesondere dient der Arbeitsspeicher nicht der permanenten Speicherung von Daten. Fast immer ist er aus Speicherzellen aufgebaut, die ihren Inhalt beim Abschalten des Computers verlieren. Der Arbeitsspeicher ist also ein flüchtiges oder volatiles Speichermedium. Beim nächsten Einschalten werden alle Bits des Arbeitsspeichers auf ihre Funktionsfähigkeit getestet und dann auf 0 gesetzt. Die Bits des Arbeitsspeichers sind byteweise organisiert. Jeder Befehl kann immer nur auf ein ganzes Byte zugreifen, um es zu lesen, zu bearbeiten oder zu schreiben. Den 8 Bits, die ein Byte ausmachen, kann noch ein Prüfbit beigegeben sein. Mit dessen Hilfe überprüft der Rechner ständig den Speicher auf Fehler, die z.B. durch eine Spannungsschwankung oder einen Defekt entstehen können. Diese Fehlerüberprüfung funktioniert nach dem selben Prinzip wie das Prüfbit beim ASCII-Code. die Speicherhardware setzt und liest das Prüfbit automatisch. Meist setzt diese das Prüfbit so, dass die Anzahl aller Einsen in dem gespeicherten Byte zusammen mit dem Prüfbit geradzahlig wird (even parity), daher heißt das 30 Prüfbit auch Parity-Bit. Wird ein Bit durch einen Fehler oder Defekt verändert, erkennt das die Speicherhardware daran, daß die Anzahl der Einsen ungerade wird. Diese sog. ECC-Fehlerkorrektur wird vorwiegend im Server-Bereich eingesetzt. Die Verwendung von Prüfbits verliert allerdings seit einiger Zeit wegen der höheren Zuverlässigkeit der Speicherbausteine an Bedeutung. Speichertypen Es gibt grundsätzlich zwei Typen von Speicherzellen: Dynamisches RAM oder DRAMs und Statisches RAM oder SRAMs. DRAMs speichern die elektrische Ladung in Kondensatoren, die periodisch wieder aufgefrischt werden müssen, da sich die gespeicherte Information aufgrund von Leckströmen mit der Zeit verflüchtigt. Aus diesem Grund sind DRAMs langsamer (aber dafür billiger) als SRAMs und werden hauptsächlich für den Hauptspeicher verwendet. SRAMs haben die Fähigkeit, ihre Inhalte zu behalten, solange der Strom fließt: Sekunden, Minuten, Stunden oder gar Tage. SRAMs sind sehr schnell, die typischen zugriffszeiten betragen nur wenige Nanosekungen. Aus diesem Grund sind werden sie häufig als Cache-Speicher der Ebene 2 (Second Level Cache oder L2 Cache) verwendet. Cache Speicher stellt eine Möglichkeit dar, das Problem des Von-Neumann Flaschenhalses zu mildern. Der schnelle Cache steht dem Prozessor als Puffer zur Verfügung. In diesem Speicher wird immer ein gewisser Teil des Arbeitsspeichers vorgehalten, so daß die Datenübertragung vom und zum Prozessor beschleunigt wird, da die Lese- und Schreibzugriffe beim Cache schneller sind, als beim Hauptspeicher. Für Arbeitsspeicher kommt heutzutage eine verbesserte Version des DRAM zum Einsatz: SDRAM (Synchronous Dynamic RAM), dessen Taktrate optimal auf die CPU abgestimmt ist. DDR-SDRAM (Double Data Rate) stellt eine Weiterentwicklung von SDRAM dar. Bei DDR-SDRAM wird im Vergleich zu SDRAM innerhalb eines Taktes die doppelte Menge an Daten übertragen. 31 Technische Realisierung Technisch wird der Arbeitsspeicher heutiger Computer aus speziellen Bauelementen, den Speicherchips aufgebaut. Diese werden nicht mehr einzeln, sondern als Speichermodule angeboten. Dabei sind jeweils mehrere Einzelchips zu sogenannten SIMM- oder DIMMModulen (single/dual inline memory module) zusammengefasst. Das sind kleine Mini-Platinen mit 72 Pins auf einer Seite der Steckverbindung (SIMM) oder mit 168 Pins, d.h. je 84 Pins auf beiden Seiten (DIMM), auf denen jeweils 8 oder 9 gleichartige Speicherchips sitzen. Gängige Taktfrequenzen für DDR-SDRAM sind: DDR-266 mit 133 MHz, d.h. 266 MHz effektivem Takt (2,1 GByte/s DDR-333 mit 166 MHz, d.h. 333 MHz effektivem Takt (2,7 GByte/s) DDR-400 mit 200 MHz, d.h. 400 MHz effektivem Takt (3,2 GByte/s) DDR2-1066 mit 533 MHz , d.h. 1066 MHz effektivem Takt 3.3.5 Northbridge Die Northbridge ist in modernen Computersystemen für den Hochgeschwindigkeits-Datenaustausch verantwortlich. So enthält in den meisten PC-Systemen die Northbridge unter anderem den Speichercontroller, der den Datentransfer zwischen CPU und Hauptspeicher verwaltet. Der Speichercontroller ist über den sog. Frontsidebus (FSB) an die CPU gekoppelt. Außerdem synchronisiert der Chip der Northbridge noch den Datentransfer und die Datensteuerung zwischen CPU und AGP- und/oder PCI-Express Grafikkarte. In neueren Systemen ist der Speichercontroller nicht mehr Bestandteil der Northbridge, sondern direkt in den Prozessor integriert (z.B. Athlon64). Auf diese Weise wird der Datentransfer zwischen CPU und Speicher zusätzlich beschleunigt, da die "externe" Verbindung zwischen CPU und Northbridge (FSB) wegfällt, wodurch die entstehenden Latenzen beim Speicherzugriff deutlich reduziert werden können. Die Northbridge ist über den PCI-Bus, über PCI-X (schnellere 64Bit Variante von PCI) , oder über eine andere proprietäre Schnittstelle wie z.B. Hypertransport an die Southbridge angebunden. 32 Northbridge und Southbridge bilden zusammen den Chipsatz. 3.3.6 Southbridge Neben der Northbridge ist die Southbridge der wichtigste Bestandteil des Chipsatzes. Während die Northbridge die Verbindung zwischen CPU und Arbeitsspeicher darstellt ist die Southbridge für den Datentransfer und die Datensteuerung zwischen peripheren Geräten (PCI-Steckkarten, IDE-Laufwerke wie Festplatten und CD/DVD-ROM) zuständig. Die Southbridge kontrolliert neben den angeschlossenen Laufwerken auch die Maus und die Tastatur, sowie verschiedene über USB angeschlossene Geräte, wie z.B. Drucker, Scanner usw. Außerdem sind heutzutage oft Peripheriegeräte bereits in die Southbridge integriert, wie z.B. OnBoard-Soundlösungen und LAN-Schnittstellen. In moderneren Systemen kann die Southbridge sogar direkt in die Northbridge integriert sein, so z.B. bei nVidia's nForce4 Chipsatz. Die Bezeichnungen Northbridge und Southbridge resultieren aus der räumlichen Anordnung der jeweiligen Chips auf dem Mainboard. Dreht man die Platine so, daß der Prozessorsockel oben ist, liegt die Northbridge 33 näher an der CPU, also "im Norden der Platine", während die Southbridge weiter "südlich" im unteren Teil der Platine zu finden ist. 3.3.7 PCI-Bus PCI steht für Peripheral Component Interconnect und dient folgerichtig zur Verbindung von Peripheriegeräten mit dem Chipsatz (Northbridge+Southbridge) und dem Prozessor. Der PCI-Bus ist Industriestandard und fester Bestandteil von IBM-kompatiblen PC's, Apple Macintoshs. Der PCI-Bus System wurde von Intel im Jahr 1990 entwickelt, um den zu langsam gewordenen ISA- und dessen Nachfolger, den EISA-Bus abzulösen. Der ursprüngliche PCI-Bus übertrug 32 Bit pro Zyklus und lief mit 33 Mhz (d.h. einer Zykluszeit von 30 ns). Die Bandbreite betrug insgesamt 133 Mbyte/s. 1993 wurde der PCI 2.0 eingeführt, und 1995 kam PCI 2.1 heraus. PCI 2.2 hat Eigenschaften, die ihn für mobile Computer geegnet machen (vorwiegend solche zur Einsparung von Batteriestrom). Mittlerweile liegt PCI 2.3 vor. Der PCI-Bus läuft mit bis zu 66 Mhz und handhabt 64 Bit-Transfers. Somit hat er eine Bandbreite von 528 Mbyte/s. Ursprünglich waren auch Grafikkarten über den PCI-Bus ins System eingebunden. Allerdings reichte die maximale Bandbreite des Busses mit den steigenden Anforderungen der sich rasch weiterentwickelnden 3D-Grafik nicht mehr aus, nicht zuletzt deswegen, weil sich aufgrund der Bus-Struktur, mehrere Geräte die Bandbreite teilen müssen. 34 Als Lösung des Bandbreitenproblems für Grafikkarten wurde deswegen der Accelerated Graphics Port - kurz AGP - entwickelt. 3.3.8 Accelerated Graphics Port (AGP) 3D-Grafik ist aus keinem Anwendungsbereich mehr weg zu denken. Ob Forschung, Unterhaltung oder Multimedia, alles setzt auf 3D-Visualisierung. Echtzeit-3D Grafik benötigt jedoch große Datenmengen. Diese Datenmengen waren irgendwann zu groß, um vom bereits leicht betagten PCI-Bus bewältigt zu werden, der sich im Laufe der Zeit als Engpass für die Grafikausgabe herausstellte. Vor allem deshalb, weil der PCI ein Bus ist, den sich mehrere Erweiterungskarten und evtl. die Southbridge (die ebenfalls über den PCI-Bus mit der Northbrdige verbunden war) teilen müssen. Mitte des Jahres 1996 begann ein Konsortium von Mainboard- und Grafikkartenherstellern unter der Führerschaft von Intel mit der Entwicklung des AGP. Der Accelerated Graphics Port ist weder ein Ersatz für den PCI-Bus noch eine Erweiterung. Zudem ist er strenggenommen auch kein Bus. Er ist eine einzelne Punkt-zu-Punkt-Verbindung zwischen Grafikkarte und Chipsatz (i.e. der Northbridge) Der AGP ist ein spezielles Interface für die Kopplung von Prozessor, Hauptspeicher und Grafikkarte. Um auch Speicherzugriffe an der CPU vorbei zu ermöglichen (und so für Entlastung des Prozessors zu sorgen), bekommt der AGP vom System-BIOS exklusiv einen bestimmten Speicherbereich im Hauptspeicher zugewiesen. Dieser Speicher wird in der AGP-Spezifikation AGP-Aperture genannt. Allerdings sind Lese- und Schreibzugriffe auf den Hauptspeicher bedeutend langsamer als auf den lokalen Speicher der Grafikkarte Der AGP hat eine Breite von 32 Bit und einen Takt von 66 Mhz und somit ursprünglich eine Bandbreite von 266 MBytes. In weiteren Ausbaustufen der AGP-Spezifikation wurden die Übertragungsraten weiter angehoben: AGP 2x --> 532 MBytes/s AGP 4x --> 1,06 GByte/s AGP 8x --> 2,1 GByte/s Mit der neuesten Version AGP 8x ist das Ende der Fahnenstange erreicht, da sich, auf Grund von Timing-Problemen bei hohen Taktungen, die durch die parallele Datenübertragung entstehen, und dem dadurch zunehmend komplizierter werdenden Platinendesign, die Geschwindigkeit nicht weiter steigern lässt. 35 Dazu kommt noch, dass heutige Grafikkarten über so viel eigenen Speicher verfügen, dass sie nur noch verhältnismäßig selten auf den Hauptspeicher zugreifen müssen. Im Vergleich zum Grafikspeicher ist der Arbeitsspeicher des PCs relativ langsam. AGP war als schnelle Punkt-zu-Punkt-Verbindung zum Arbeitsspeicher sinnvoll, solange schnelles VRAM (Video RAM oder Grafikspeicher) auf der Grafikkarte übermäßig teuer war, was heute nicht mehr der Fall ist. Seit Mitte 2006 wurden kaum noch neue Hauptplatinen mit Unterstützung für den AGP vorgestellt. Stattdessen verwenden die meisten Hersteller den schnelleren PCIExpress-Port auf ihren Hauptplatinen. 3.3.9 PCI-Express PCI-Express (Peripheral Component Interconnect Express) (Abk. PCIe od. PCI-E) ist der Nachfolger von PCI und AGP und bietet eine höhere Datenübertragungsrate. PCIe ist im Gegensatz zum PCI-Bus kein paralleler Bus, sondern eine serielle Punkt-zu-Punkt-Verbindung. Die Datenübertragung erfolgt über sogenannte Lanes (dt. Spuren, Wege), wobei jede Lane aus einem Leitungspaar für das Senden und einem zweiten Paar für das Empfangen besteht. Trotz dieses völlig anderen physischen Aufbaus ist PCIe softwareseitig voll kompatibel zu PCI, sodass weder Betriebssysteme, noch Treiber, noch Anwendungsprogramme angepasst werden müssen. PCIe ist vollduplexfähig und arbeitet mit einer Taktrate von 1,25 GHz DDR (effektiven 2,5 GHz). Daraus berechnet sich die Datenrate einer Lane zu max. 250 MByte/s pro Richtung, da für ein 8 Bit-Paket ein 10-Bit Paket übertragen wird, in dem 2 Bit für 36 Verwaltungszwecke eingesetzt werden. Zählt man beide Richtungen zusammen erhält man sogar ca. 500 MByte/s (zum Vergleich: der Standard-PCI Bus mit 32-Bit Busbreite bei 33 MHz erreicht nur maximal 133 MByte/s). Verwendet man nur eine Lane, spricht man von PCIe x1. Durch Koppelung mehrerer Lanes kann man die Datenrate erhöhen, etwa x2 mit 2 Lanes bis zu x32 mit 32 Lanes. Die PCI-SIG (Special Interest Group) plant darüberhinaus zukünftig auch Versionen mit 500 und 1000 MByte/s pro Lane. Im Endnutzerbereich wird PCIe x1 als Ersatz für den PCI-Bus und PCIe x16 zur Anbindung einer Grafikkarte verwendet (PCI-Express For Graphics, PEG), was somit auch den AGP überflüssig macht. PEG oder PCIe x16 bietet eine Bandbreite von 4 GByte/s in jede Richtung, also insg. 8 GByte/s. Die Slots sind außerdem abwärts kompatibel, d.h. eine x4 Karte kann z.B. auch in einen x8 Slot gesteckt werden, die überzähligen vier Lanes werden dann nicht genutzt. Umgekehrt ist dies momentan nur bei SLI (Scalable Link Interface) üblich. Denn obwohl die Slots für die Grafikkarten die Größe von x16 Slots haben, werden beim Einsatz von zwei Grafikkarten die 16 Lanes auf beide Slots verteilt, so daß jede Grafikkarte nur 8 Lanes zur Verfügung hat, falls das Mainboard bzw. der darauf verbaute Chipsatz keine vollwertigen x16 Slots für beide Grafikkarten bereitstellt. 3.3.10 IDE (ATA/ATAPI) IDE bedeutet Integrated Drive Electronics. Im Unterschied zu den Festplatten der ersten Generation, ist der FestplattenController nicht mehr auf einer zusätzlichen Steckkarte untergebracht, sondern in die Festplatte integriert. Die Weiterentwicklung von IDE ist EIDE (Extended IDE). Die (E)IDE-Schnittstelle bezeichnet man oft auch als ATA-Schnittstelle (Advanced Technology Attachment), allerdings ist EIDE und ATA nicht dasselbe: (E)IDE definiert den Anschluß der Laufwerke, wie Pinbelegung, Stecker, Kabel und elektrische Signale. ATA definiert das Protokoll, anhand dessen die Daten über die Leitungen übertragen werden. Der Datentransfer zwischen Peripherie und Hauptspeicher kann entweder über die CPU oder über das sogenannte Bus Mastering ausgeführt werden. 37 Bus Mastering ist das effektivere Verfahren, weil durch direkten Speicherzugriff (Direct Memory Access: DMA) die CPU entlastet wird. Für den Datentransfer gibt es zwei Protokolltypen: 1. 2. Den älteren PIO-Modus (Programmed Input/Output) den neueren UDMA-Modus (Ultra Direct Memory Access) Beim PIO-Modus ist der Prozessor für jeden Lese- und Schreibvorgang verantwortlich. Der UDMA-Modus kann über den DMA-Controller direkt auf den Arbeitsspeicher zugreifen. So kann der Prozessor sich um andere Aufgaben kümmern. Das gesamte System läuft schneller. Modus IDE (ATA-1) PIO 0 IDE (ATA-1) PIO 1 IDE (ATA-1) PIO 2 IDE Multiword-DMA 0 IDE Multiword-DMA 1 IDE Multiword-DMA 2 E-IDE (Fast ATA-2) PIO 3 E-IDE (Fast ATA-2) PIO 4 Bandbreite 3,33 MByte/s 5,22 MByte/s 8,33 MByte/s 4,16 MByte/s 13,33 MByte/s 16,66 MByte/s 11,11 MByte/s 16,66 MByte/s Einführung 1989 1994 Der Ultra-DMA-Modus (Ultra-ATA) unterstützt höhere Datenübertragungsraten und besitzt eingebaute Sicherheitsmechanismen. Zusätzlich wird die Belastung des Prozessors bei der Datenübertragung durch das sog. Bus-Mastering reduziert. Das Bus-Mastering ist ein Datentransfer-Verfahren für die Übertragung von Daten und Befehlen, bei dem der Host-Controller direkt auf dem Arbeitsspeicher zugreift, ohne den Prozessor zu belasten. Für alle Ultra-ATA-Festplatten (133/100/66) wird ein UDMA-Kabel benötigt. Dieses Flachbandkabel hat 80 Leitungen. 40 für den Datenverkehr und 40 für die Erdung. Der Ultra-DMA-Standard 133 ist abwärtskompatibel. An diesen Controllern lassen sich auch andere Ultra-ATA-Festplatten (66 und 100) betreiben. Modus Bandbreite Einführung 16,66 MByte/s 1996 Ultra-DMA 0 (ATA-16 / ATA-3) 25,0 MByte/s Ultra-DMA 1 (ATA-25) 33,33 MByte / sek. 1997 Ultra-DMA 2 (ATA-33 / ATA-4) 44,4 MByte / sek. Ultra-DMA 3 (ATA-44) 1999 Ultra-DMA 4 (Ultra-ATA-66 / ATA-5) 66,66 MByte / sek. Ultra-DMA 5 (Ultra-ATA-100 / ATA-6)99,99 (100) MByte / sek.2000 2001 Ultra-DMA 6 (Ultra-ATA-133 / ATA-7)133 MByte / sek. 38 Konfiguration von EIDE-Geräten An einen (E)IDE-Strang können maximal 2 Geräte angeschlossen werden. Das Master-Gerät wird am Kabelende angesteckt. Der Jumper sollte hinten am Gerät auf Master (M) gesteckt sein. Ein zweites Gerät kommt an den zweiten Stecker, in der Mitte des Kabels. Dies wird als Slave betrieben. Entsprechend sollte der Jumper (S) gesteckt sein. Bei der Verwendung von Cable Select (CS) muss bei beiden Geräten entsprechend der Jumper gesteckt sein. Die Position bzw. die Betriebsart wird in diesem Fall über eine (nicht) durchverbundene Ader im Flachbandkabel eingestellt. In manchen Gerätekonstellationen funktioniert diese Automatik nicht. Deshalb ist angeraten in jedem Fall die manuelle Einstellung vorzunehmen. Bei nur zwei Geräten ist das auch kein Problem. Die Software-Konfiguration von Festplatten und Laufwerken wird im BIOS vorgenommen. Dort stellt man den EIDE-Anchluß auf Auto-Detect (Autoerkennung) ein. Erst wenn diese Einstellung fehlschlägt, dann kommt man um das manuelle Eintragen der Festplatten oder Laufwerks-Parameter nicht herum. Im Regelfall ist das nicht notwenig. ATAPI - Advanced Technology Attachment with Packet Interface ATAPI bedeutet Advanced Technology (AT) Attachment with Packet Interface und ist ein Erweiterung des Befehlssatzes ATA zum Anschluss eines CD-RomLaufwerks oder eines anderen Wechsel-Laufwerks (z. B. ZIP-Drive) an die EIDESchnittstelle. Im aufkommenden Multimedia-Zeitalter wurden Computer mit CD-Rom-Laufwerken als Wechsel-Massenspeicher ausgestattet. Mit dem Laufwerk wurde auch eine CDRom-Controller-Steckkarte in den Computer eingebaut. Jeder CD-Rom Hersteller lieferte seine eigene, speziell für seine eigenen CD-Rom-Laufwerke, eine ControllerKarte mit. Als Zwischenstufe wurden Soundkarten mit den 3 bis 4 wichtigsten Schnittstellen onboard ausgestattet. Auf die Dauer, und mit dem einsetzenden CD-Rom-Boom, wurde das aber zu teuer. Außerdem waren die proprietären Schnittstellen nicht schnell genug, um die Aufgaben in einem Multimedia-Computer erledigen zu können. Die Lösung war, CD-Rom-Laufwerke entweder an den SCSI-Bus oder die EIDESchnittstelle anzuschließen. Für die billigen Consumer-Computer wurde die EIDESchnittstelle gewählt. Dabei gab es aber ein Problem: Für jedes Gerät, das an der EIDE-Schnittstelle angeschlossen ist, wird ein fester Laufwerksbuchstabe vergeben. Und somit ist es kein Wechsel-Laufwerk mehr. Der CD-Rom-Wechsel würde einen Neustart des Computers nach sich ziehen. Aus diesem Grund wurde der ATAPI-Befehlssatz entwickelt um CD-ROMs über die EIDE-Schnittstelle steuern zu können. Das BIOS ist sogar in der Lage das Betriebssystem von einer CD-ROM oder DVD zu booten. 39 CD- oder DVD-Laufwerke werden nun ohne einen speziellen Treiber erkannt und ins System eingebunden. Für ältere Modelle mußte man immer einen zusätzlichen Treiber für die Controller-Karte installieren. 3.3.11 S-ATA (Serial ATA) Schnittstellen für Massenspeicher waren bisher immer in paralleler Ausführung. Mit zunehmender Übertragungsgeschwindigkeit ergeben sich technische Schwierigkeiten, die für die Übertragungsrate eine obere Grenze setzen: Bei parallelen Bussen wird es bei höheren Übertragungsraten immer schwieriger, den Datenfluss auf allen Leitungen synchron zu halten Je länger ein Kabel ist, desto eher treten Laufzeitdifferenzen zwischen den parallelen Signale auf. Dies kann nur durch eine kürzere Übertragungsstrecke oder eine geringere Taktrate verhindert werden. Zusätzliche Masseleitungen führen zu dicken und unflexiblen Flachband- oder Rundkabeln, die die Luftströmung innerhalb eines Computers verhindert. Serial-ATA hat sich aus dem älteren ATA, auch IDE genannten, Standard entwickelt. Zu Gunsten der Leistungsfähigkeit entschied man sich, von einem parallelen Busdesign zu einem bit-seriellen Bus überzugehen, d. h., dass die Daten seriell übertragen werden (Bit für Bit) und nicht, wie bei den alten ATAStandards, in 16-Bit-Worten. Mit 150 MByte/s hat S-ATA direkt an die parallele ATA-Schnittstelle (P-ATA) mit 133 MByte/s angeknüpft. Die Serial-ATA-Schnittstelle unterstützt 1,5 GBit/s bei einer Nettodatenrate von ca. 150 MByte/s. Um die Kompatibilität zu gewährleisten werden die parrallel vorliegenden Daten mit Wandlern in serielle Datenströme konvertiert. Die hohe Integrationsdichte und die extrem schnelle interne Verarbeitungsgeschwindigkeit in integrierten Schaltungen erlauben die Wandlung in nahezu Echtzeit. Gegenüber seinem Vorgänger besitzt S-ATA drei Hauptvorteile: 1. höhere Datentransferrate, 2. vereinfachte Kabelführung und 3. Hot-Plug-Fähigkeit zum Austausch von Datenträgern im laufenden Betrieb (im Serverbereich wichtig). Seit der Einführung von Serial-ATA wird der bisherige ATA-Standard häufig als Parallel ATA (P-ATA) bezeichnet, um Verwechslungen zu vermeiden. Auf neueren Hauptplatinen findet man zu den üblichen zwei P-ATA-Steckplätzen zusätzlich zwei bis acht S-ATA-Anschlüsse für Festplatten. 40 S-ATA nutzt auf der Link-Layer-Schicht (Kabel) eine Punkt-zu-Punkt Verbindung. Jedes Gerät hat also seinen eigenen Anschluss. S-ATA ist nicht auf Festplatten beschränkt, mittlerweile gibt es z. B. auch SATABandlaufwerke, DVD-Laufwerke und -Brenner. S-ATA II (Serial ATA II) S-ATA II bietet eine maximale Geschwindigkeit von 300 MByte/s und ist damit theoretisch doppelt so schnell wie S-ATA. Das "theoretisch" bezieht sich darauf, daß die Geschwindigkeit zur Zeit nicht von der Datenübertragung begrenzt ist, sondern durch die Festplattenmechanik. Festplatten mit 10.000 Umdrehungen in der Minute (U/min) liefern rund 75 MByte/s an Daten. Die Schnittstellengeschwindigkeit reicht also auch für die Zukunft locker aus. S-ATA II bietet einige zusätzliche Features im Vergleich zu S-ATA: Native Command Queuing (NCQ) ist eine Technologie für Festplatten, die die Geschwindigkeit verbessert. Sie ermöglicht, dass mehrere Anfragen gleichzeitig an die Festplatte abgesetzt werden und diese dann selbst entscheidet, welche Anfrage sie zu erst abarbeitet. Durch die Vermeidung unnötiger Kopfbewegungen kann so der Durchsatz und vor allem die Latenz verbessert werden. Das Laufwerk selbst, der Controller und der Treiber müssen Command Queuing unterstützen, um es zu nutzen. (Weitere Informationen zu NCQ auf TomsHardware.de) eSATA (external SATA) für externe Laufwerke, maximale Kabellänge 2m. eSATA verwendet andere, stärker abgeschirmte Kabel, um externe Geräte vor elektromagentischen Störungen zu schützen. So soll die sichere Übertragung über 2m ermöglicht werden. Port Multiplier Über diesen Mechanismus kann der Punkt-zu-Punkt-Datenstrom auf mehrere Geräte aufgeteilt werden. Bis zu 15 Geräte lassen sich an einem SATA-II-Port betreiben. Bei 4 Geräten mit ca. 70 MByte/s würden 3 G/Bit/s ausreichen, um alle Geräte ohne Geschwindigkeitsverlust bedienen zu können. 3.3.12 USB (Universal Serial Bus) Der Universal Serial Bus (USB) ist ein Bussystem zur Verbindung eines Computers mit Zusatzgeräten. Ein USB-Anschluss belegt wenig Platz und kann einfache Geräte wie Mäuse, Telefone oder Tastaturen mit Strom versorgen. 41 Mit USB ausgestattete Geräte können im laufenden Betrieb miteinander verbunden werden (Hot-Plugging), angeschlossene Geräte und deren Eigenschaften können automatisch erkannt werden. Moderne Computer haben meist zwei bis sechs USB-Schnittstellen. Stehen zu wenig USB-Anschlüsse zur Verfügung, kann man über preiswerte Hubs bis zu 127 USBGeräte an einer Schnittstelle betreiben. USB eignet sich für viele Geräte wie Drucker, Scanner, Webcams, Maus, Tastatur, aber auch Dongles, sowie USB-Kaffeewärmer und USB-Weihnachtsbäume;o). Einige Geräte sind überhaupt erst mit USB entstanden, wie z.B. USBSpeichersticks. Seit der Einführung der USB-2.0-Spezifikation sind relativ hohe Datenübertragungsraten möglich, wodurch sich der USB zum Anschluss weiterer Gerätearten wie Festplatten, TV-Schnittstellen und Foto-Kameras eignet. USB wurde entwickelt um eine einheitliche Schnittstelle für PC-Peripheriegeräte anzubieten. Er ersetzt zunehmend ältere serielle und parallele Anschlüsse, aber auch PCIBussysteme (z.B. bei externen Soundkarten). Technischer Überblick USB ist ein bitserieller Bus, die einzelnen Bits des Datenpaketes werden also nacheinander übertragen. Die Datenübertragung erfolgt differentiell über zwei verdrillte Leitungen, eine überträgt das Datensignal unverändert und die andere das invertierte Signal. Der Signalempfänger bildet die Differenzspannung beider Signale; der Spannungshub zwischen 1- und 0-Pegeln ist dadurch doppelt so groß. Dies erhöht die Übertragungssicherheit, unterdrückt Gleichtaktstörungen und verbessert nebenbei die elektromagnetische Verträglichkeit. Zwei weitere Leitungen dienen, falls nötig, zur Stromversorgung der angeschlossenen Geräte. Durch Verwendung von vier Adern in einem Kabel können diese dünner und preiswerter als bei parallelen Schnittstellen ausgeführt werden. Eine hohe Datenübertragungsrate ist mit relativ geringem Aufwand zu erreichen, da nicht mehrere Signale mit identischem elektrischen und zeitlichen Verhalten übertragen werden müssen. Ähnlich wie bei S-ATA dürfen die Kabel eine bestimmte Länge nicht überschreiten. USB-Kabel dürfen eine maximale Länge von 5m haben, viele Geräte funktionieren mit längeren Kabeln nicht richtig. Die Bus-Spezifikation sieht einen zentralen Host-Controller (dem sog. MASTER) vor, der die Koordination der angeschlossenen Peripherie-Geräte (den sog. SlaveClients) übernimmt. Daran können bis zu 127 verschiedene Geräte angeschlossen werden. 42 An einen USB-Port kann immer nur ein einzelnes USB-Gerät angeschlossen werden. Wenn an einen Host mehrere Geräte angeschlossen werden sollen, muss deshalb ein Verteiler (Hub) für die Kopplung dieser Geräte sorgen. Durch den Einsatz von Hubs entstehen Baumstrukturen, die alle im Hostcontroller enden. Der USB ersetzt die älteren PC-Schnittstellen RS232 (seriell), Gameport, die Centronics-Schnittstelle (paralleler Drucker-Anschluss) sowie die PS/2Schnittstelle für Tastatur und Maus. Im Vergleich zu diesen bietet USB deutlich höhere Datenübertragungsraten. Trotz seines Namens – Universal Serial Bus – ist der USB kein physischer Datenbus. Bei einem solchen werden mehrere Geräte parallel an eine Leitung angeschlossen. Die Bezeichnung „Bus“ bezieht sich auf die logische Vernetzung, die tatsächliche elektrische Ausführung erfolgt nur mit Punkt-zu-Punkt-Verbindungen. Übertragungsgeschwindigkeiten USB 1.x bietet folgende Übertragungsgeschwindigkeiten: o o 1,5 Mbit/s (Low Speed), 12 Mbit/s (Full Speed) Ab USB 2.0 erlaubt USB mit bis zu o 480 Mbit/s (High Speed) Daten zu übertragen. Diese Übertragungsraten basieren auf dem Systemtakt der jeweiligen USB-Geschwindigkeit und stellen die physikalisch mögliche Datenübertragungsrate dar. Die tatsächlich nutzbare Datenrate liegt – z. B. durch Protokolloverhead – darunter; bei aktuellen Systemen in der Größenordnung 320 Mbit/s. Wird die Schnittstelle eines Geräts mit „USB 2.0“ angegeben, heißt das nicht unbedingt, dass dieses Gerät auch die High-Speed-Datenrate von 480 MBit/s anbietet. Standpunkt der Anbieter ist dabei, dass ein USB-2.0-kompatibles Gerät grundsätzlich jede der drei Geschwindigkeiten benutzen kann und die 2.0-Kompatibilität in erster Linie bedeutet, dass die neueste Fassung der Spezifikation eingehalten wird. 480 MBit/s dürfen also nur erwartet werden, wenn ein Gerät mit dem Logo „Certified USB Hi-Speed“ ausgezeichnet ist. Seit 2008 erfolgt Entwicklung von USB 3.0 mit 4,8 Gbit/s. Seit 2010 zunehmend Geräte verfügbar, die diesen Standard unterstützen. 43 3.4 Speicher-Hierarchie 3.5 Funktionell-struktureller Aufbau der CPU Aufbau des Zentralprozessors als vereinfachte funktionale Darstellung 44 3.5.1.1 Leitwerk (Steuerwerk) Das Leitwerk nimmt Koordinationsfunktionen für den Prozessor wahr und stellt den Kern der CPU dar. Es steuert den Ablauf des Befehls- und Datenflusses und bestimmt mit seinem Taktgeber die Verarbeitungsgeschwindigkeit. Es besteht aus logischen Schaltungen und Registern. Das Leitwerk ist für die Übertragung von Anweisungen aus dem Arbeitsspeicher zuständig, decodiert diese und führt sie aus. Von den zahlreichen Registern eines Prozessors gehören folgende zum Leitwerk: Befehlszähler, Befehlsregister und Statusregister. Der Befehlszähler (Synonym: Programmzähler (programm counter)) enthält jeweils die Adresse des nächsten zur Ausführung anstehenden Befehles. Zu Beginn der Programmabarbeitung wird der Befehlszähler mit der Anfangsadresse (= Adresse des ersten Befehls) geladen. Das Leitwerk holt von der Adresse, die im Befehlszähler enthalten ist, diesen Befehl zur Verarbeitung ab. Nach vollzogener Befehlsinterpretation wird der Befehlszähler um die Länge des gerade übernommenen Befehls erhöht, d.h. um die entsprechende Zahl von Arbeitsspeicheradressen weitergezählt. Dadurch ergibt sich normalerweise die Adresse des Folgebefehls, der damit aus dem Arbeitsspeicher (bzw. aus dem Cache) geholt und verarbeitet werden kann. Ist der Normalfall, bei dem die Befehle des Programms in aufeinander folgenden Stellen des Arbeitsspeichers stehen, nicht gegeben, so muss eine Modifikation des Befehlszählers erfolgen. Dies ist z.B. bei Schleifen der Fall, bei denen zu bereits vorher verarbeiteten Befehlen zurück gesprungen wird. Bei derartigen Sprungoperationen wird der Befehlszähler nicht auf den im Arbeitsspeicher nächstfolgenden Befehl eingestellt, sondern er wird mit der Zieladresse des Sprungbefehls geladen. Der Befehl, der aus der durch den Befehlszähler adressierten Speicherstelle gelesen wurde, wird im Befehlsregister (instruction register) gespeichert. Das Befehlsregister enthält also genau den Befehl, der im Moment ausgeführt wird. Der Befehlsdekodierer entschlüsselt die im Operationsteil angegebene Bitkombination und setzt diese in Steuersignale um. Die erzeugten Signale und die errechneten Operandenadressen werden je nach Befehlstyp an die für die Ausführung des Befehls zuständigen Teile des Rechenwerks, des Leitwerks, den Arbeitsspeicher, den I/O-Prozessor usw. weitergeleitet. Der Status, in dem sich ein Programm befindet, wird in einem Statusregister angegeben. Beim Mehrprogrammbetrieb bedient der Prozessor abwechselnd in Zeitabschnitten verzahnt mehrere Programme, sodass bei der Unterbrechung eines laufenden Programms vermerkt werden muss, wo nach der Wiederaufnahme des Programms fortgesetzt werden soll. Zu diesem Zweck wird der erreichte Befehlszählerstand in einem Statusregister sichergestellt. Es gibt noch weitere Unterbrechungsgründe, wie z.B. Hardware- oder Softwarefehler. In Abhängigkeit vom jeweiligen Unterbrechungsereignis ergreift das Betriebssystem die erforderlichen Maßnahmen. Das Leitwerk enthält noch eine Reihe weiterer Spezialregister, auf die hier aber nicht eingegangen werden soll. Das Leitwerk liest und interpretiert Befehl für Befehl. Die abgegebenen Steuersignale dienen zur Steuerung der verschiedenen Register und Addierwerke (im 45 Rechenwerk), der peripheren Geräte usw. Abgesehen von einigen wenigen Befehlen, die z.B. die Dateneingabe von Eingabeeinheiten in den Arbeitsspeicher oder die Ausgabe der verarbeiteten Daten aus dem Arbeitsspeicher zu Ausgabeeinheiten veranlassen, findet die Befehlsausführung im Wesentlichen im Zentralprozessor (Rechenwerk) statt. Während der Ausführungsphase werden zum Beispiel Operanden aus dem Arbeitsspeicher geholt, Daten miteinander verknüpft, Ergebnisse in den Arbeitsspeicher geschrieben usw. 3.5.1.2 Rechenwerk (ALU – Arithmetic Logic Unit) Während das Leitwerk sich um die koordinierte Abarbeitung der Befehle kümmert, führt das Rechenwerk sie letztendlich aus. Das Rechenwerk erhält vom Leitwerk die auszuführenden Rechenoperationen und die bereitgestellten Daten, führt die Rechenoperation aus und liefert die Ergebnisse wieder an das Leitwerk zurück. Die Rechenoperationen werden in arithmetische (bspw. Addition, Subtraktion, Multiplikation, Bitverschiebeoperationen, Vergleichsoperationen) und logische Operationen (bspw. UND, ODER, NICHT) unterteilt. Die Operanden der Rechenoperationen sind meist Worte (2 Byte, 16 Bit), die als duale Werte interpretiert werden. Zu den Grundelementen der Rechenoperationen gehören die Additionen, die durch Addierschaltungen realisiert werden. Multiplikationen können bspw. darauf aufbauend durch fortgesetzte Addition realisiert werden. Bei vielen Prozessoren können weitere Operationen durch Mikroprogrammierung hinzugefügt werden. Während die Rechenwerke für ganzzahlige Werte (integer) traditionell Bestandteil des Prozessorchips waren, wurden lange Zeit die Rechenwerke für Gleitkommaoperationen als eigene Gleitkommaeinheiten (floating point unit) in separaten Chips integriert. Die heutigen Mikroprozessoren haben integrierte Gleitkommaeinheiten, wobei diese auch in unterschiedlicher Form für Skalar- oder Vektoroperationen bereitgestellt werden. Die Verarbeitungsbreite des Rechenwerks gibt die Größe der Operanden in Bit an. Eine höhere Verarbeitungsbreite bedeutet dabei, dass in einem Rechenschritt größere Datenmengen verarbeitet werden können. Heutige Prozessoren verfügen über eine Verarbeitungsbreite von 32/64 Bit für Operationen mit ganzen Zahlen (integer arithmetic), während die Gleitkommaeinheit mit 128 Bit oder mehr (abhängig vom Prozessortyp) arbeitet. Nachdem die prinzipiellen Bestandteile der Zentraleinheit bekannt sind und deren Funktionsweise erläutert wurde, sollen im Folgenden die einzelnen Stufen der Befehlsabarbeitung am Beispiel des Von-Neumann-Zyklus zusammengefasst werden. 3.5.1.3 Von-Neumann-Zyklus 46 Noch einmal zur Erinnerung: Die zwei Aufgaben der CPU sind die Befehlsausführung und die Ablaufsteuerung. Gemäß diesen Aufgaben besteht sie aus einem Rechenwerk/Datenprozessor und einem Leitwerk/Befehlsprozessor. Die Aufgabe des Datenprozessors besteht in der "klassischen" Verarbeitung von Daten, d.h. dem Ausführen von Berechnungen. Dazu enthält er die ArithmetischLogische-Einheit (ALU) sowie (mindestens) drei Register zur Aufnahme von Operanden. Bei den Registern handelt es sich um den Akkumulator, ein Multiplikator-Register (MR) (z.B. zur Aufnahme von Multiplikationsergebnissen) und ein Link-Register (LR) (zur Aufnahme z.B. eines Additionsübertrages), welche beide als Akkumulator-Erweiterung angesehen werden können, sowie das Memory Buffer Register (MBR), über welches die Kommunikation mit dem Speicher abgewickelt wird. Während der Akkumulator ein allgemeines Register ist, welches im Prinzip für jede im Rahmen eines Programms anfallende Aufgabe verwendet werden kann, sind alle anderen Register spezielle Register, welche alle eine spezielle Funktion besitzen und ausschließlich für diese verwendet werden können. Die Aufgabe des Befehlsprozessors besteht darin, Befehle zu entschlüsseln und deren Ausführung zu steuern. Dazu kann er sich folgender Register bedienen: 1) Der aktuell bearbeitete Befehl befindet sich im Befehlsregister (Instruction Register (IR)). 2) Die Adresse des Speicherplatzes, welcher als nächstes anzusprechen ist, ist im Memory Adress Register (MAR) abgelegt. 3) Die Adresse des nächsten auszuführenden Befehls wird im Befehlszähler (Program Counter (PC)) gespeichert. Die Entschlüsselung eines Befehls erfolgt durch einen separaten Befehls-Decodierer, die Steuerung der Ausführung schließlich durch die Operationensteuerung. Oftmals wird diese auch separat als Steuerwerk bezeichnet. Die nachfolgende Abbildung 12 zeigt die genannten Funktionseinheiten und verdeutlicht, welche Einheiten miteinander kommunizieren. Die Bearbeitung eines speziellen Problems erfolgt gemäß einem Programm, also anhand einer Folge von Instruktionen. Vor Beginn der Bearbeitung steht dieses zusammen mit den Daten, die es benötigt, im Speicher. Daraus leiten sich die wichtigsten Merkmale des Von-NeumannRechners ab: 47 Zentralprozessor mit Daten- und Befehlsprozessor als vereinfachte funktionale Darstellung 1) Zu jedem Zeitpunkt führt die CPU genau einen Befehl aus. Dieser Befehl kann (höchstens) einen Datenwert bearbeiten (SISD = Single Instruction, Singe Data) 2) Alle Speicherworte (d.h. Inhalte der Speicherzellen) sind als Daten, Befehle oder Adressen verwendbar. Die jeweilige Verwendung eines Speicherinhaltes richtet sich nach dem momentanen Kontext. 3) Da also Daten und Programme nicht in getrennten Speichern untergebracht werden, besteht grundsätzlich keine Möglichkeit, die Daten vor ungerechtfertigtem Zugriff zu schützen. Von Neumann Zyklus - Ausführung von Instruktionen Der Prozeß der Befehlsverarbeitung bei Von-Neumann-Rechnern wird VonNeumann-Zyklus genannt und besteht aus den folgenden fünf nacheinander ablaufenden Teilschritten: 1) FETCH 2) DECODE 3) FETCH OPERANDS 4) EXECUTE 5) UPDATE PROGRAM COUNTER (PC) zu 1) Beim FETCH-Schritt wird aus dem Speicher der nächste zu bearbeitende Befehl geholt. Dazu wird der Inhalt von PC nach MAR gebracht und der Inhalt dieser 48 Adresse über das MBR aus dem Speicher geholt und im IR gespeichert. (Da in der Fetch-Phase nur Befehle verarbeitet werden, geht der Rechner automatisch davon aus, dass es sich bei der geholten Bitfolge um einen Befehl handelt.) zu 2) Beim DECODE-Schritt wird der geholte Befehl durch den Decodierer in Schaltinstruktionen für die Hardware aufgelöst. zu 3) Bei FETCH-OPERANDS werden nun die Operanden, also die Werte, die durch den Befehl verändert werden sollen bzw. die als Parameter verwendet werden, also etwa die beiden Operanden einer Addition, aus dem Speicher geholt zu 4) Bei EXECUTE wird die Operation von der ALU ausgeführt zu 5) Bei UPDATE PROGRAM COUNTER wird der Befehlszähler erhöht, damit der Rechner weiß, an welcher Stelle des Programms er sich gerade befindet. Dies geschieht parallel zum DECODE und FETCH OPERANDS. Bei EXECUTE kann der PC wieder verändert werden (z.B. durch einen Sprungbefehl). Anschließend kann der Zyklus von vorn beginnen und der nächste Schritt des Programms kann ausgeführt werden. Bedingte und unbedingte Sprünge Eine Folge von Befehlen stellt ein Programm für einen Rechner dar. Ausgeführt werden die Befehle eines Programms im Allgemeinen in der Reihenfolge, in der sie (hintereinander) im Speicher abgelegt sind (und welche durch den Programmierer bestimmt wird). Dazu wird im UPDATE-PROGRAM-COUNTER-Schritt der Inhalt des PC, der die Adresse des nächsten auszuführenden Befehls angibt, lediglich um eins (bzw. um die Anzahl der Bytes, die der aktuelle Befehl benötigt) erhöht. Eine Ausnahme bilden (bedingte oder unbedingte) Sprungbefehle (z.B. bei Schleifenenden oder Unterprogramm-Sprüngen). In diesen Fällen ist der Inhalt des PC neu zu laden. 49 Die Beschreibung der Fetch-Phase lässt sich wie folgt zusammenfassen: PC --> MAR MAR --> MBR MBR --> IR decodiere IR falls (kein Sprungbefehl) dann {stelle Operanden bereit; PC = PC + 1; } sonst { PC = Sprungziel-Adresse; } Inhalt des PC ins MAR Inhalt des MAR ins MBR Inhalt des MBR ins IR Befehls-Decodierung Updaten des PC Setzen des PC auf Sprungziel 3.6 Einteilung von Rechnerarchitekturen nach Flynn Flynn teilt Rechnerarchitekturen nach der Art der Befehlsausführung in die vier folgenden Kategorien ein: SISD Ein nach dem SISD-Prinzip (Single Instruction Single Data) aufgebauter Rechner kann Befehle ausschließlich sequentiell, also nacheinander, abarbeiten. In einem Bearbeitungsschritt wird auf das entsprechende Datenelement bzw. den Operanden genau ein Befehl angewendet. Die Von-Neumann-Architektur folgt diesem Prinzip und stellt damit einen SISDRechner dar. SIMD Ein nach dem SIMD-Prinzip (Single Instruction Multiple Data) aufgebauter Rechner kann einen Befehl mehr oder weniger simultan auf verschiedene Datenelemente bzw. Operanden anwenden. Entsprechende Prozessoren werden auch als Vektorprozessoren (vector processor, array processor) oder Vektorrechner bezeichnet. Mit einem SIMD-Rechner können Berechnungen mit Vektoren und Matrizen sehr effizient realisiert werden, was besonders für die grafische Datenverarbeitung, die Bildverarbeitung und die Verarbeitung von Multimedia-Datenströmen wichtig ist. Der erste Vektorrechner war der Gray-Supercomputer in den 70er Jahren. Heute findet man SIMD-Instruktionen bei vielen gängigen Rechnerarchitekturen, wie bspw. die MMX, SSE, SSE2, SSE3 bei Intel-Prozessoren oder 3DNow! bei AMDProzessoren. MMX ... Multimedia Extension für ganzzahlige Werte SSE ... Streaming SIMD Extension für Gleitkommawerte 50 MIMD Ein nach dem MIMD-Prinzip (Multiple Instruction Multiple Data) aufgebauter Rechner kann gleichzeitig mehrere Befehle auf mehreren Datenelementen bzw. Operanden ausführen. Entsprechende Architekturen finden bei Mehrprozessorsystemen Anwendung, bei denen die einzelnen Prozessoren unabhängig voneinander Berechnungen ausführen können. Im weiteren Sinne sind praktisch alle PCs, die Hilfs- bzw. Spezialprozessoren verwenden, Rechnersysteme, die nach dem MIMD-Prinzip aufgebaut sind. Eine aktuelle Entwicklung stellen Prozessoren mit mehrfachen Prozessorkernen (z.B. dual core) dar. Hier werden innerhalb des eigentlichen Prozessorchips mehrere Prozessoren emuliert, sodass das damit für das Betriebssystem ein Mehrprozessorsystem vorliegt. Die Klasse der MIMD-Architekturen lässt sich noch weiter hinsichtlich der Kopplung der Zentralprozessoren unterteilen. Bei einem eng gekoppelten Mehrprozessorsystem greifen meist wenige Zentralprozessoren auf einen gemeinsamen Arbeitsspeicher zu (shared memory). Die Prozessoren befinden sich physisch innerhalb desselben Rechners und benutzen einen gemeinsamen Kommunikationskanal (z.B. Bus), um auf den Arbeitsspeicher zuzugreifen. Ein lose gekoppeltes Mehrprozessorsystem besteht aus Prozessoren, die jeweils über einen eigenen lokalen Speicher verfügen. Man spricht auf von verteilten Speicherstrukturen (distributed memory). Die Kommunikation erfolgt hier typischerweise durch Nachrichten (messages), die meist über ein Rechnernetz versendet werden. Daher rührt auch der Begriff des nachrichtengekoppelten Mehrprozessorsystems. Harvard-Architektur Die Harvard-Architektur bezeichnet in der Informatik ein Schaltungskonzept zur Realisierung besonders schneller CPUs und Signalprozessoren. Der Befehlsspeicher ist physisch vom Datenspeicher getrennt und beide werden über getrennte Busse angesteuert. Der Vorteil dieser Architektur besteht darin, dass Befehle und Daten gleichzeitig geladen, bzw. geschrieben werden können. Bei einer klassischen VonNeumann-Architektur sind hierzu mindestens zwei aufeinander folgende Buszyklen notwendig. Zudem sorgt die physikalische Trennung von Daten und Programm dafür, dass bei Softwarefehlern kein Programmcode überschrieben werden kann. Nachteilig ist allerdings, dass nicht benötigter Datenspeicher nicht als Programmspeicher genutzt werden kann. Die Harvard-Architektur wurde zunächst überwiegend in RISC-Prozessoren konsequent umgesetzt. Moderne Prozessoren in Harvard-Architektur sind in der Lage, parallel mehrere Rechenwerke gleichzeitig mit Daten und Befehlen zu füllen. Bei Signalprozessoren der C6x-Familie von Texas Instruments ist dies beispielsweise für bis zu acht Rechenwerke möglich. 51 Ein weiterer Vorteil der Trennung ist, dass die Datenwortbreite (die kleinste adressierbare Einheit) und Befehlswortbreite unabhängig festgelegt werden kann. Damit kann auch, wenn erforderlich, die Effizienz des Programmspeicherbedarfs verbessert werden, da sie nicht direkt von den Datenbusbreiten abhängig ist, sondern ausschließlich vom Befehlssatz. Dies kann z.B. in eingebetteten Systemen oder kleinen Microcontroller-Systemen von Interesse sein. Als besonders bekannte Vertreter sollten hier auch die Produkte der Firma Microchip Technology Inc. erwähnt werden, die ebenso auf dieser Architektur aufbauen (PICmicro). Ebenso basieren die Mikrocontroller der AVR-Reihe von Atmel auf der Harvard Architektur. Eine bedeutende Erweiterung der Harvard-Architektur wurde von der amerikanischen Firma Analog Devices Anfang der 1990er Jahre durch die Einführung der SHARC(Super-Harvard-Architecture)-Technologie vorgenommen, bei der die genannten Speichersegmente als Dual-Port-RAMs ausgeführt sind, die kreuzweise zwischen den Programm- und Daten-Bussen liegen. Viele moderne Prozessoren verwenden eine Mischform aus Harvard- und vonNeumann-Architektur, bei der innerhalb des Prozessorchips Daten und Programm voneinander getrennt verwaltet werden, eigene Caches und MMUs haben und über getrennte interne Busse laufen, extern jedoch in einem gemeinsamen Speicher liegen. Einer der ersten bedeutenden Prozessoren, die die Harvard-Architektur intern einsetzten, war der Motorola 68030. 52 4 Betriebssysteme 4.1 Die ISA-Ebene Bevor wir uns die Betriebssystemebene anschauen, werfen wir einen kurzen Blick darauf, wie durch Software auf die Maschine oder Hardware zugegriffen werden kann. Die Sprache der ISA-Ebene wird gebildet aus der Instruktionsmenge, die von der Maschine, oder genauer von der CPU und den dazugehörigen HardwareSchaltungen ausgeführt werden kann. Diese Instruktionsmenge nennt man auch Mikroprogramm. Das Mikroprogramm besteht aus Mikrobefehlen. Das sind Bitfolgen, die wie andere Daten auch in einem Speicher abgelegt werden können. Ein solcher Mikrobefehlsspeicher ist Teil der CPU. Er ist als ROM (Read Only Memory) ausgeführt, d.h. er kann nur gelesen, aber nicht verändert werden. Ansonsten ist das ROM wie jeder andere Speicher aufgebaut, insbesondere besitzt es ein Adressregister, in dem die Adresse eines Speicherwertes abgelegt wird, und ein Datenregister, in dem der dort befindliche Datenwert zurückgegeben wird. Mikrobefehle sind jedoch für den Menschen nicht besonders gut lesbar. Die Vorstellung, größere Programme in Mikrocode programmieren zu müssen, ist sehr abschreckend: Als Programmierer sollte man sich nicht damit plagen müssen, Schalter in Datenwegen zu betätigen, Daten mühsam via Adress- und Datenregister aus dem Speicher zu lesen, Code-Adressen in Code-Adress-Register zu schreiben öder ähnliche lästige Dinge festzulegen. Die Details der Benutzung der Busse sollen dem Programmierer ebenfalls verborgen (erspart) bleiben. Daher benötigen Programmierer eine Maschinensprache, die besser lesbar ist (so genannte mnemonische Befehle enthält) und eine abstraktere Sicht der CPU bietet. Eine solche Sprache ist die Assemblersprache. 4.2 Assemblerprogrammierung Eine Assemblersprache ist eine Sammlung von Befehlen, die dem Programmierer für den direkten Zugriff auf die CPU zur Verfügung steht. Eine reine Assemblersprache produziert von jeder Anweisung genau eine Maschineninstruktion. Anders ausgedrückt: Es gibt eine Eins-zu-EinsEntsprechung zwischen den Maschineninstruktionen und den Anweisungen im Assemblerprogramm. 53 Assemblersprache ist also eine symbolischere Form der Maschinensprache, d.h. sie verwendet symbolische Namen und Adressen anstelle von binären. Somit lässt sich in Assembler wesentlich leichter programmieren als in Maschinensprache. Ein weiterer Vorteil in der Verwendung von Assembler liegt darin, dass man in Assembler eine sehr genaue Kontrolle über die Ausführungszeiten der verschiedenen Befehle hat. Man kann zeitkritische Programmteile sehr effizient in Aktionen der CPU umsetzen. Trotzdem ist Assemblerprogrammierung sehr schwierig und nichts für Leute mit schwachen Nerven. Außerdem dauert das Schreiben eines Programms in Assemblersprache viel länger als in einer Hochsprache, nicht zuletzt, weil auch das Debugging viel länger dauert. Außerdem ist die Wartung und Pflege eines Assemblerprogramms sehr viel aufwendiger. Zusammengefasst gibt es also zwei gute Gründe für die Verwendung von Assembler: 1. Direkter Hardwarezugriff auf alle Funktionen der CPU und der angeschlossenen Geräte. 2. Assemblerprogramme sind meist kompakter und schneller, als Programme einer Hochsprache. Als Faustregel kann man sagen, dass 10% des Programm 90% der Ausführungszeit in Anspruch nehmen! Eine empfehlenswerte Vorgehensweise ist demnach, zunächst ein Programm in einer höheren Programmiersprache zu entwickeln, anschließend die zeitkritischen Stellen, oder die Stellen, die direkten Hardwarezugriff erfordern, zu identifizieren und diese dann gezielt in Assemblersprache umzuschreiben. Jede Computerarchitektur hat ihre eigenen Instruktionen, so dass sich die Assemblersprachen unterscheiden. Das heißt, ein Assemblerprogramm ist nur auf dem System, für das es geschrieben wurde, lauffähig. Assemblersprache – Maschinensprache und Assembler Jeder Maschinenbefehl besteht zunächst aus einer Bitfolge. In reiner Maschinensprache hat jeder Befehl eine Nummer, OpCode genannt. Die Bedeutung der einzelnen OpCodes müsste man im Grunde jedes Mal in einer Tabelle nachschlagen. Deshalb gibt es eine deutlich lesbarere Form von Maschinenbefehlen und ihren Opcodes: Die Assemblersprache oder kurz Assembler (Assembler bezeichnet außerdem ein Programm, das Assemblerbefehle in Maschinensprache umwandelt). 54 Es gibt für jeden Maschinenbefehl eine Assembleranweisung. Diese Anweisung hat den Vorteil, dass man sich die Abkürzungen um einiges leichter merken kann, als den OpCode dieses Befehls. Die folgende Abbildung zeigt einige Assemblerbefehle sowie deren entsprechende Maschinensprachebefehle: Glücklicherweise besitzt Assembler noch mehr Fähigkeiten, als "nur" für einen Assemblerbefehl den entsprechenden Maschinenbefehl aus einer Tabelle zu holen. Der Assembler erlaubt, symbolische Namen für Speicherplätze (Variablen), symbolische Sprungadressen (Labels) und Daten (Konstanten) zu verwenden. Außerdem steht ein einfaches Prozedurkonzept zur Verfügung, das es erlaubt, häufig verwendete Programmteile über einen zugewiesenen Namen zu referenzieren. Darüber hinaus sorgen so genannte Makros für übersichtlicheren Code, da sie es erlauben, komplexe Programmteile durch einen kurzen Namen zu ersetzen und diese an beliebigen Stellen im Programm aufzurufen. Der Unterschied zu Prozeduren besteht darin, dass der Code für eine Prozedur nur einmal im Programm existiert, ein Aufruf verzweigt an diese Stelle. Bei Makros wird der kurze Name des Makros jedes Mal durch den wirklichen Programmtext ersetzt (-->Expansion), d.h. der Code existiert hier mehrmals. 55 Die obige Abbildung zeigt ein kleines Assembler-Programm, das die Zahlen von 1...N aufaddiert. Zunächst wird hier der Inhalt der Speicheradresse 0 in Register A geladen und B mit dem Wert 0 initialisiert. Ab dem mit der Marke nochmal gekennzeichneten Befehl wird A zu B addiert und A um eins heruntergezählt. Ist A ≠ 0 wird zur Marke nochmal gesprungen und der Code erneut ausgeführt. 4.3 Von der Hardware zum Betriebssystem Bisher haben wir die Hardware und die Möglichkeiten der Datenrepräsentation behandelt. Ohne Programme ist beides aber nutzlos. Die Programme, die einen sinnvollen Betrieb eines Rechners erst möglich machen, nennt man Software. Man kann verschiedene Schichten von Software identifizieren. Sie unterscheiden sich durch ihren Abstand zum menschlichen Benutzer bzw. zur Hardware des Computers. Zur Verdeutlichung stellen wir uns zunächst einen "blanken" Computer, d.h. eine CPU auf einem Motherboard mit Speicher und Verbindung zu Peripheriegeräten wie Drucker und Laufwerken, aber ohne jegliche Software vor. Die CPU kann in dieser Situation nicht viel mehr als: Speicherinhalte in Register laden Registerinhalte im Speicher ablegen Registerinhalte logisch oder arithmetisch verknüpfen mit IN- und OUT-Befehlen Register in Peripheriegeräten lesen und schreiben. In Zusammenarbeit mit den Peripheriegeräten (Tastatur, Bildschirm, Laufwerke, Soundkarte) kann man auf diese Weise bereits: ein Zeichen von der Tastatur einlesen ein Zeichen an einer beliebigen Position des Textbildschirms ausgeben einen Sektor einer bestimmten Spur der Diskette oder Festplatte lesen oder schreiben einen Ton einer bestimmten Frequenz und Dauer erzeugen. Alle diese Tätigkeiten bewegen sich auf einer sehr niedrigen Ebene, z.B. auf der ISA-Ebene oder der darunter liegenden Mikroarchitekturebene. Wollte man einen Rechner auf dieser Basis bedienen, müsste man sich genauestens mit den technischen Details jedes einzelnen der Peripheriegeräte auskennen. Niemand würde einen Rechner benutzen, wenn er sich bei jedem Tastendruck überlegen müsste, wie ein auf der Tastatur eingegebenes Zeichen in die CPU gelangt und anschließend auf dem Bildschirm als Text dargestellt wird. Gar nicht auszumalen, wenn dann auch noch alle Zeichen des bereits auf dem 56 Bildschirm dargestellten Textes verschoben werden müssten, um dem eingefügten Zeichen Platz zu machen! Zwischen dem von einem Anwender intuitiv zu bedienenden Rechner und den Fähigkeiten der Hardware klafft also eine gewaltige Lücke. Diese Lücke füllt das Betriebssystem aus. 4.4 Aufgaben des Betriebssystems Ein Betriebsystem ist ein Programm, das dem Benutzer und den Anwendungsprogrammen elementare Dienste bereitstellt. Der Nutzer eines Betriebssystems ist nicht notwendigerweise ein Programmierer, sondern möglicherweise jemand, der vom funktionieren des Rechners keine Ahnung hat. Für einen solchen Benutzer präsentiert sich der Rechner über das Betriebssystem. Die Dienste, die es bereitstellt, sind das, was der Rechner in den Augen eines solchen Nutzers kann. Seitdem Rechner in viele Bereiche unseres Lebens Einzug gehalten haben, gibt es immer mehr Menschen, die mit einem Computer arbeiten müssen. Daher muss das Betriebssystem immer einfacher zu benutzen sein. Erst den graphischen Betriebssystemsoberflächen (GUI = Graphical User Interface) ist es zu verdanken, dass heute jeder einen Rechner irgendwie bedienen kann und dass es verhältnismäßig leicht ist, mit einem bisher unbekannten Programm zu arbeiten, ohne vorher umfangreiche Handbücher wälzen zu müssen. Der Rechner mit seinen Peripheriegeräten stellt eine Fülle von Ressourcen zur Verfügung, auf die Benutzerprogramme zugreifen. Zu diesen Ressourcen gehören: CPU (Rechenzeit) Hauptspeicher Plattenspeicherplatz interne Geräte (Erweiterungskarten, Onboard-Sound u.ä.) externe Geräte (Drucker, Scanner, etc.) Die Verwaltung dieser Ressourcen ist eine schwierige Aufgabe, da viele Benutzer und deren Programme auf diese Ressourcen gleichzeitig zugreifen wollen. Die zentralen Bestandteile eines Betriebssystems sind entsprechend seiner zentralen Aufgaben also die Dateiverwaltung Prozessverwaltung Speicherverwaltung 57 4.4.1 Dateiverwaltung Eine wichtige Aufgabe des Betriebssystems ist die Dateiverwaltung. Damit ein Benutzer sich nicht darum kümmern muss, in welchem Bereich der Festplatte noch Platz ist, um den gerade geschriebenen Text zu speichern, oder wo die Version von gestern gespeichert war, stellt das Betriebssystem das Konzept der Datei als Behälter für Daten aller Art zur Verfügung. Die Übersetzung von Dateien und ihren Namen in bestimmte Bereiche der Festplatte nimmt das Dateisystem als Bestandteil des Betriebssystems vor. Moderne Dateisysteme sind hierarchisch aufgebaut. Mehrere Dateien können in einem Ordner (folder) zusammengefasst werden. Andere Bezeichnungen für Ordner sind Katalog, Verzeichnis, Unterverzeichnis (directory, subdirectory). Da Ordner sowohl Dateien als auch andere Ordner enthalten können, entsteht eine hierarchische (baumähnlich verzweigte) Struktur. In Wirklichkeit ist ein Ordner eine Datei, die Namen und einige Zusatzinformationen von anderen Dateien enthält. Dateien können sich auf allen Stufen befinden. Von oben gesehen beginnt die Hierarchie mit einem Wurzelordner (root directory), dieser enthält wieder Dateien und Ordner, und so fort. Der linke Teil der folgenden Abbildung zeigt einen Dateibaum, wie er unter Windows XP dargestellt wird. Das Wurzelverzeichnis ist F. Dieser enthält unter anderem die Ordner COMEDY, HÖRSPIELE, MUSIK. Der Ordner MUSIK enthält den Unterordner MP3 und dieser wiederum diverse weitere Unterordner. In der rechten Hälfte des Bildes sieht man die Dateien, die sich im Unterordner "Frank Sinatra - My Way" befinden. 58 4.4.1.1 Dateinamen und Pfade Jede Datei erhält einen Namen, unter der sie gespeichert und wiedergefunden werden kann. Der Dateiname ist im Prinzip beliebig, er kann sich aus Buchstaben, Ziffern und einigen erlaubten Sonderzeichen zusammensetzen. Allerdings hat sich als Konvention etabliert, Dateinamen aus zwei Teilen, nämlich dem eigentlichen Namen und der Erweiterung zu bilden. Beide Bestandteile werden durch einen Punkt "." voneinander getrennt. Die letzte Abbildung zeigte die Datei "01-My Way.mp3". Anhand des Namens macht man den Inhalt der Datei kenntlich, anhand der Erweiterung die Art bzw. das Format des Inhalts. Von letzterem ist nämlich abhängig, mit welchem Programm die Datei geöffnet werden kann. In diesem Falle zeigt die Erweiterung".mp3", dass es sich um eine Datei handelt, die z.B. mit dem Programm "Winamp" geöffnet werden kann. Obwohl theoretisch auch Dateinamen ohne Dateierweiterung möglich sind, ist es sinnvoll, sich an die Konvention zu halten, da auch die Anwenderprogramme von diesem Normalfall (default) ausgehen. Ein Anwendungsprogramm wie z.B. Word wird beim ersten Abspeichern einer neuen Datei als default die Endung ".doc" vorgeben. Es kann vorkommen, dass zwei Dateien, die sich verschiedenen Ordnern befinden, den gleichen Namen besitzen. Dies ist kein Problem, da das Betriebssystem eine Datei auch über ihre Lage im Dateisystem identifiziert. Diese Lage ist in einer baumartigen Struktur wie dem Dateisystem immer eindeutig durch den Pfad bestimmt, den man ausgehend von der Wurzel traversieren muss, um zu der gesuchten Datei zu gelangen. Den Pfad kennzeichnet man durch die Folge der dabei traversierten Unterverzeichnisse. Der Pfad zum Verzeichnis der letzten Abbildung ist demnach: 59 o F:\MUSIK\MP3\Frank Sinatra - My Way - The Best of\ Man erkennt, dass die einzelnen Unterordner durch das Trennzeichen "\" (backslash) getrennt werden. In den Betriebssystemen der UNIX-Familie (LINUX, SunOS, Fedora) wird stattdessen der "/" (slash) verwendet. Der Pfad, zusammen mit dem Dateinamen (incl. der Erweiterung), muss eine Datei eindeutig kennzeichnen. Die MP3-Datei aus unserem Beispiel hat also den vollständig qualifizierten Dateinamen F:\MUSIK\MP3\Frank Sinatra - My Way - The Best of\01-My Way.mp3 4.4.1.2 Das Dateisystem Viele Speichergeräte arbeiten blockweise, indem sie Daten als Blöcke fester Größe (je nach Dateisystem zwischen 512 Bytes und 64 KBytes) in Bereiche eines Datenträgers speichern. Die Hardware bietet somit dem System eine Menge von Blöcken an, die eindeutig adressierbar sind. Das Betriebssystem verwaltet alle diese Blöcke in einem Dateisystem und enthält Routinen zum Lesen und Schreiben von Blöcken für die jeweils verwendeten Gerätetypen, die so genannten Gerätetreiber. Das vom Betriebssystem verwaltete Dateisystem erspart dem Benutzer so das Hantieren mit lästigen Details, wie z.B. das Ausrichten des Schreib-/Lesekopfes einer Festplatte beim Zugriff auf eine Datei. Für einen Benutzer ist es viel einfacher und intuitiver, seine Daten in Dateien (files) zu organisieren. Eine Datei entspricht intuitiv einer Akte und diese können in Ordnern zusammengefasst werden. Jede Datei hat einen Namen und einen Inhalt. Dieser kann aus einer beliebigen Folge von Bytes bestehen. Das Betriebssystem muss eine Übersetzung zwischen den von der Hardware angebotenen Blöcken und den vom Benutzer gewünschten Dateien gewährleisten. Das Dateisystem, als zuständiger Teil des Betriebssystems, verwaltet eine Datei als Folge von Blöcken. Selbst wenn sie nur ein Byte enthält, verbraucht eine Datei mindestens den Speicherplatz eines Blockes. 60 Die Dateien eines Dateisystems werden in speziellen Dateien organisiert, den sog. Ordnern oder Verzeichnissen. In einem Ordner oder auch Verzeichnis findet sich für jede in ihm enthaltene Datei ein Eintrag mit Informationen folgender Art: Dateiname (dazu gehört ggf. auch die Erweiterung) Dateityp (Normaldatei, ausführbare Datei, Verzeichnisdatei) Länge in Bytes zugehörige Blöcke (meist reicht ein Verweis auf den ersten Block der Datei) Zugriffsrechte (Besitzer, ggf. Passwort, Schreib- und Leserechte) Datum (Erstellung, Änderung) Unter Windows ist jedes Laufwerk die Wurzel eines eigenen Dateibaums, die mit einem "Laufwerksbuchstaben" benannt wird. Unter UNIX sind die Dateisysteme aller Laufwerke Unterbäume eines globalen Dateibaums, dessen Wurzel root heißt. Ein systemweites Dateisystem hat Vorteile, wenn viele Festplatten vorhanden sind und der Benutzer gar nicht wissen will, auf welchen Geräten sich die Daten befinden. Es hat aber Nachteile, wenn Geräte mit auswechselbaren Datenträgern (z.B. Disketten oder CD-ROMS) betrieben werden, da bei jedem Medienwechsel entsprechende Teile des Gesamt-Katalogs geändert werden müssen. Außerdem muss das Betriebssystem eine Pseudodatei verwalten, die aus allen freien (also noch verfügbaren) Blöcken des Datenträgers besteht. Eine weitere Pseudodatei besteht aus allen Blöcken, die als unzuverlässig gelten, weil ihre Bearbeitung zu Hardwareproblemen geführt hat. Diese werden dann nicht mehr für Dateien genutzt. Es gibt also: belegte Blöcke freie Blöcke unzuverlässige Blöcke Während der Bearbeitung der Dateien ändern sich die Listen dynamisch. 4.4.1.3 Datei-Operationen Das Dateisystem bietet dem Anwenderprogramm mindestens folgende Operationen zur Verwaltung von Dateien: "Neu": Anlegen einer noch leeren Datei in einem bestimmten Verzeichnis; die Parameter dieser Operation sind Dateiname und Dateityp. "Löschen": Die Datei wird entfernt und damit unzugänglich. Meist wird hier aber nur der Verweis auf den ersten Block der Datei gelöscht, so dass man die Datei häufig wiederherstellen kann. 61 "Kopieren": Dabei kann implizit eine neue Datei erzeugt oder eine bestehende überschrieben oder verlängert werden. "Umbenennen": Änderung des Dateinamens oder anderer Verzeichniseinträge einer Datei. "Verschieben": Die Datei wird aus einem Verzeichnis entfernt und einem anderen hinzugefügt. Um eine Datei bearbeiten zu können, muss man sie vorher öffnen. Dabei wird eine Verbindung zwischen der Datei und ihrem Verzeichnis, zwischen der Zugriffsmethode (lesen/schreiben) und einem Anwenderprogramm, welches den Zugriff veranlasst hat, hergestellt. Nach dem Bearbeiten muß die Datei wieder geschlossen werden. Wenn beispielsweise ein Anwender eine Textdatei editiert, dann muss das Betriebssystem aus dem Dateinamen die Liste der Blöcke bestimmen, in denen der Datei-Inhalt gespeichert ist. Meist ist dazu nur die Kenntnis des ersten Blockes notwendig. Dieser enthält dann einen Verweis auf den nächsten Block uns so fort. Wird die editierte Datei gespeichert, so müssen möglicherweise neue Blöcke an die Liste angehängt werden oder einige Blöcke können entfernt und der Liste der freien Blöcke (s. Dateisystem) übergeben werden. Das Betriebssystem muss also Operationen des Anwendungsprogramms wie z.B. "Datei lesen" und "Datei speichern" umsetzen in elementare Operationen, sog. Systemaufrufe (system calls), die die Hardware ausführen kann. Dazu gehören: Lesen eines oder mehrerer Blöcke Schreiben eines oder mehrerer Blöcke Die Systemaufrufe Neu oder Löschen führen lediglich dazu, dass Blöcke der Liste der freien Blöcke entnommen, oder zurückgegeben werden. Der Inhalt der Blöcke muß nicht gelöscht werden. Aus diesem Grund können "gelöschte" Dateien mit spezieller Software häufig wieder hergestellt werden, da sie sich noch physisch auf der Festplatte befinden. Was gelöscht wurde, war lediglich der Verweis auf den ersten Block der Datei. Sobald man den Anfang wiederfindet, kann man die Datei lesen. Dies funktioniert aber nur, solange keine Blöcke der Datei von anderen Dateien verwendet und damit überschrieben wurden. Das Dateisystem verwaltet eine Datei als Folge von Blöcken. Eine Datei, die nur ein Byte enthält, verbraucht mindestens den Speicherplatz eines Blockes. Für ein Anwenderprogramm wie einen Texteditor besteht eine Datei aus einer Folge von Bytes (!) . Der Texteditor muss in der Lage sein, ein bestimmtes Byte zu schreiben, zu löschen oder an einer bestimmten Stelle ein Byte einzufügen. Die Repräsentation einer Datei als eine solche Folge von Bytes ist Aufgabe des 62 Betriebssystems. Es stellt daher Systemaufrufe zur Verfügung, um Dateien byteweise zu lesen und zu schreiben. Selbst der Programmierer des Texteditors muss nichts von der blockweisen Organisation der Dateien wissen. Für ihn ist die Abbildung von der Bytefolge in die Blockfolge unsichtbar. 4.4.2 Prozessverwaltung Ein auf einem Rechner ablauffähiges oder im Ablauf befindliches Programm, zusammen mit all seinen benötigten Ressourcen wird zusammenfassend als Prozess oder Task bezeichnet. Auf einem Rechner mit nur einer CPU ist es nicht wirklich möglich, dass mehrere Prozesse gleichzeitig laufen. Wenn man allerdings mehrere Prozesse abwechselnd immer für eine kurze Zeit (einige Millisekunden) arbeiten lässt, so entsteht der Eindruck, als würden diese Prozesse gleichzeitig laufen. Die zur Verfügung stehende Zeit wird in kurze Intervalle unterteilt. In jedem Intervall steht die CPU einem anderen Prozess zur Verfügung. Dazwischen findet ein Taskwechsel statt, wobei der bisherige Prozess suspendiert und ein anderer Prozess (re-) aktiviert wird. Ein Prozess kann sich, nachdem er gestartet wurde, in verschiedenen Zuständen befinden: Nach dem Start ist er zunächst rechenbereit und wartet auf die Zuteilung von Prozessorzeit. Wird er dann vom Betriebssystem zur Ausführung ausgewählt, ist er rechnend. Ist er nach einer bestimmten Zeit nicht beendet, wird er vom Betriebssystem suspendiert und ein anderer Prozess erhält den Prozessor zugeteilt. Unser Prozess ist dann erneut rechenbereit. Es kann aber auch der Fall eintreten, dass der Prozess auf eine Ressource wartet (z.B. auf einen Drucker oder auf eine Dateioperation). Dann wird er blockiert und erst wieder als rechenbereit eingestuft, falls das Signal kommt, dass die benötigten Ressourcen bereitstehen. In der Zwischenzeit können die andern Prozesse den Prozessor nutzen. 63 Abb.: Zustandsdiagramm eines Prozesses Hat ein Prozessor genau eine CPU, so ist höchstens ein Prozess zu jedem Zeitpunkt aktiv. Dieser wird dann als laufender Prozess bezeichnet. 4.4.2.1 Bestandteile eines Prozesses Ein Prozess ist eine Instanz eines in Ausführung befindlichen Programms. Jeder Prozess bekommt einen eigenen Adressbereich zugewiesen. Wenn er deaktiviert wird, müssen alle notwendigen Informationen gespeichert werden, um ihn später im exakt gleichen Zustand reaktivieren zu können. Zu diesen Informationen gehören: der Programmcode des Prozesses (bzw. dessen Programms) seine im Arbeitsspeicher befindlichen Daten der Inhalt der CPU-Register einschließlich des Befehlszählers eine Tabelle aller geöffneten Dateien mit ihrem aktuellen Bearbeitungszustand Abb.: Speicherabbild eines Prozesses Wenn ein Prozess unterbrochen werden soll, muss der Inhalt der CPU-Register in den vorgesehenen Adressbereich gerettet werden. Wenn ein wartender Prozess aktiviert werden soll, muss der Inhalt der Register, so wie er bei seiner letzten Unterbrechung gesichert wurde, wieder geladen werden. 64 4.4.2.2 Threads Jeder Prozess besitzt seinen eigenen Speicherbereich. Demgegeüber sind Threads (Thread = Faden) Prozesse, die keinen eigenen Speicherbereich besitzen. Man nennt sie daher auch leichtgewichtige Prozesse (lightweight process). Der Laufzeit-Overhead zur Erzeugung und Verwaltung von Threads ist deutlich geringer als bei Prozessen. Gewöhnlich laufen innerhalb eines Prozesses mehrere Threads ab, die den gemeinsamen Speicherbereich nutzen. Threads sind in den letzten Jahren immer beliebter geworden. Moderne Sprachen, wie Java, haben Threads in die Sprache integriert. Computerspiele nutzen Threads zunehmend, um die einzelnen Kerne von MehrkernProzessoren besser auszulasten. So kann z.B. in einem Thread von einem CPUKern die Spielphysik berechnet werden, während ein anderer die KI (Künstliche Intelligenz) der Computergegner berechnet. 4.4.2.3 Multitasking a) Prioritätsabhängige Verwaltung / kooperatives Multitasking Für die Verwaltung der Prozesse hat das Betriebssystem verschiedene Möglichkeiten. Die einfachste Methode besteht darin, Prozessen eine Priorität zuzuordnen und dem jeweils bereiten Prozess höchster Priorität die CPU zu überlassen, so lange bis dieser Prozess die CPU nicht mehr benötigt oder ein anderer Prozess höherer Priorität bereit ist. Eine allein durch Prioritäten gesteuerte Verwaltung begünstigt also den Prozess mit höchster Priorität. Zeitpunkt und Dauer der Ausführung von Prozessen geringerer Priorität sind nicht vorhersehbar. Diese Form des Multitasking ist das von älteren Windows-Versionen und Mac OS bis Version 9 bekannte kooperative Multitasking. Dabei ist es jedem Prozess selbst überlassen, wann er die Kontrolle an das Betriebssystem zurückgibt. Dies hat den Nachteil, dass Programme, die nicht kooperieren, bzw. die Fehler enthalten, das gesamte System zum Stillstand bringen können. b) Zeitscheibenverfahren / präemptives Multitasking Eine bessere Methode besteht darin, die Zuteilung des Betriebsmittels CPU nicht allein von der Priorität eines Prozesses abhängig zu machen, sondern jedem bereiten Prozess eine sogenannte Zeitscheibe zuzuteilen. Diese Methode nennt man Zeitscheibenverfahren oder präemptives Multitasking. Das Betriebssystem führt eine neue CPU-Zuteilung durch, wenn der laufende Prozess entweder o eine bestimmte Zeit gerechnet hat, oder 65 o wenn er auf ein Ereignis warten muss. Ein solches Ereignis könnte die Ankunft einer Nachricht von einem anderen Prozess sein oder die Freigabe eines Betriebsmittels wie Laufwerk, Drucker, Modem, das der Prozess zur Weiterarbeit benötigt. Je nach System werden als Dauer einer Zeitscheibe 1 Millisekunde, 10 Millisekunden oder mehr gewählt. Die meisten Betriebssysteme führen allerdings nur eine sehr einfache Zeitscheibenzuteilung für die bereiten Prozesse durch. Dieses Verfahren wird round robin (Ringelreihen) genannt und besteht darin, alle Prozesse, die bereit sind, in einer zyklischen Liste anzuordnen. Die bereiten Prozesse kommen in einem bestimmten Turnus an die Reihe und erhalten jeweils eine Zeitscheibe. Prozesse, die nicht mehr bereit sind, werden aus der Liste entfernt, solche, die soeben bereit geworden sind, werden an einer bestimmten Stelle in der Liste eingehängt. Die Verwaltung der Prozesse erfolgt durch einen Teil des Betriebssystems, den sogenannten Scheduler, der selbst als einer der Prozesse in der Liste betrieben wird. Er wird einmal pro Rundendurchlauf aktiv und kann den Ablauf der anderen Prozesse planen. 4.4.3 Speicherverwaltung Eine der Aufgaben des Betriebssystems besteht in der Versorgung der Prozesse mit dem Betriebsmittel Arbeitsspeicher. Dabei sollen Prozesse vor gegenseitiger Beeinträchtigung durch fehlerhafte Adressierung gemeinsam benutzter Speicherbereiche geschützt werden. Die wesentlichen Verfahren zur Speicherverwaltung sind: o o Paging Swapping 4.4.3.1 Virtueller Speicher Vor langer Zeit stießen Programmierer zum ersten Mal auf das Problem, dass die Programme zu groß für den verfügbaren Speicher wurden. Normalerweise lösten Sie dieses Problem, indem sie die Programme in mehrere Teile aufspalteten, so genannte Overlays. Zunächst wurde Overlay 0 ausgefügrt, das dann, sobald es fertig war, das nächste Overlay 1 aufrief, welches dann Overlay 2 lud, usw. Die einzelnen Overlays wurden auf der Festplatte gespeichert und nach Bedarf vom Betriebssystem dynamisch ein- und ausgelagert . Obwohl die Overlays vom Betriebssystem verwaltet wurden, mußte der Programmierer das Programm selbst aufteilen. Große Programme in kleine, modulare Teile aufzuspalten, war eine zeitaufwändige 66 und langweilige Arbeit. Es dauerte also nicht lange, bis sich jemand etwas ausdachte, um auch diese Aufgabe durch den Computer erledigen zu lassen. Die Methode wurde als virtueller Speicher bekannt. Die Grundidee dahinter war, zu erlauben, dass der Programmcode, die Daten und der Stack zusammen größer sind als der verfügbare Hauptspeicher. Das Betriebssystem hält die Teile des Programms, die gerade gebraucht werden, im Hauptspeicher und den Rest auf der Festplatte. Beispielsweise kann man ein 16 MB großes Programm auf einer 4 MB-Maschine ausführen, indem man die 4 MB, die im Speicher liegen, zu jedem Zeitpunkt sorgfältig auswählt und Teile des Programms nach Bedarf ein- und auslagert. 4.4.3.2 Swapping Eine weitere Methode zur Speicherverwaltung ist das so genannte Swapping. Hierbei befinden sich zu einem gegebenen Zeitpunkt einige Segmentmengen im Speicher. Bei einem Aufruf eines Segmentes, das sich momentan nicht im Speicher befindet, wird dieses geholt. Ist kein Platz dafür vorhanden, müssen eines oder mehrere Segmente zuerst auf die Platte ausgelagert werden. In dieser Hinsicht ist das Swapping von Segmenten mit dem Paging vergleichbar: Segmente werden nach Bedarf geholt oder ausgelagert. Die Segmentierung wird beim Swapping aber anders als beim Paging implementiert. Swapping hat hier einen entscheidenden Nachteil: Seiten (--> Paging) haben eine feste Größe, Swapping-Segmente nicht. 67 Die Folgen dieses Nachteils werden deutlich, wenn wir die obige Abbildung betrachten. Sie zeigt das Abbild eines physikalischen Speichers, der anfangs fünf Segmente enthält. Man betrachte, was passiert, wenn in (a) Segment 1 ausgelagert und das kleinere Segment 7 an seine Stelle platziert wird. Dies zeigt (b). (b) Zwischen Segment 7 und Segment 2 liegt ein ungenutzter Bereich, also ein Loch. (c) Segment 4 wird durch Segment 5 ersetzt (d) Segment 3 wird durch Segment 6 ersetzt. Nachdem das System eine Weile lief, wird der Speicher in Stücke aufgeteilt, von denen einige Segmente und andere Löcher enthalten. Dieses Phänomen nennt man externe Fragmentierung, weil außerhalb der Segmente, in den dazwischen liegenden Löchern, Platz verschwendet wird. Diese externe Fragmentierung wird beim Paging vermieden, weil sowohl die Seiten des virtuellen Speichers, als auch die Seitenrahmen des physikalischen Speichers die gleiche Größe haben. 4.4.3.3 Paging Das als virtueller Speicher beschriebene Konzept basiert auf der Trennung zwischen dem (technisch möglichen) Adressraum eines Computers (bei einem 32Bit-System sind das 4 GByte) und den tatsächlich verfügbaren Speicherstellen (z.B. 1 GByte RAM). Man unterscheidet also den virtuellen Adressraum und den physikalischen Adressraum. Beim Paging wird der virtuelle Adressraum in eine Reihe von Seiten gleicher Größe gegliedert. Ebenso wird der physikalische Adressraum in Segmente aufgeteilt, die genauso groß sind wie die Seiten. Jedes Stück Arbeitsspeicher kann also genau eine Seite aufnehmen. Diese Segmente des Arbeitsspeichers (physikalischer Speicher) nennt man deshalb auch Seitenrahmen. Das Speicherabbild eines Prozesses wird also in einzelne Seiten aufgeteilt. Nur die wirklich benötigten Speicherseiten müssen im Arbeitsspeicher geladen sein, während ein Prozess läuft. Ein Prozess, der gestartet werden soll, muss dem Betriebssystem mitteilen, wie viel virtueller Hauptspeicher benötigt wird. Für diesen virtuellen Speicherbereich muß ein Schattenspeicher in einem speziellen Bereich des Dateisystems reserviert werden. Diese Paging Area liegt auf der Festplatte (das Pagefile unter Windows). 68 Ihre Größe begrenzt letztlich den virtuellen Hauptspeicher, der jedem Prozess zugeordnet werden kann. Prozesse können den theoretisch adressierbaren Bereich von 4 GB also nur verwenden, wenn die Festplatte groß genug ist. 4.4.3.4 MMU (Memory Management Unit) Wenn eine nicht geladene Seite adressiert wird, muss dieser Vorgang von der Hardware entdeckt werden, dies übernimmt die so genannte MMU oder Memory Management Unit, bei heutigen Rechnern Bestandteil der CPU, die dann die virtuelle Adresse (die der nicht geladenen Seite) auf die physische Adresse abbildet. Die angeforderte Seite wird dann durch das Betriebssystem von der Festplatte in den Arbeitsspeicher geladen. 4.4.3.5 Ablauf Wenn nun eine Adresse angefragt wird, die sich nicht im Hauptspeicher befindet, geschieht folgendes: 1) Der Inhalt des Arbeitsspeichers wird auf die Platte gespeichert 2) Die Speicherseite mit der gewünschten Adresse wird auf der Platte ausfindig gemacht. 3) Die gefundene Speicherseite wird in den Arbeitsspeicher geladen. 4) Die Abbildung der virtuellen Adressen auf die physikalisch vorhandenen wird angepasst, da die Speicherseite nicht mehr auf der Platte liegt, sondern im Hauptspeicher. 5) Die Ausführung wird fortgesetzt, als wäre nichts Ungewöhnliches passiert. Die Zuordnung von virtuellen Adressen zu physikalischen Adressen wird über die sogenannte Seitentabelle verwaltet. 69 5 Algorithmen und Programmierung 5.1 Einführung Die Programmierung ist ein Teilgebiet der Informatik, das sich im weiteren Sinne mit Methoden und Denkweisen bei der Lösung von Problemen mit Hilfe von Computern und im engeren Sinne mit dem Vorgang der Programmerstellung befasst. Unter einem Programm versteht man dabei eine in einer speziellen Sprache verfasste Anleitung zum Lösen eines Problems durch einen Computer. Programme werden auch unter dem Begriff Software subsumiert. Konkreter ausgedrückt ist das Ziel der Programmierung bzw. Softwareentwicklung, zu gegebenen Problemen Programme zu entwickeln, die auf Computern ausführbar sind und die Probleme korrekt und vollständig lösen, und das möglichst effizient. Die hier angesprochenen Probleme können von ganz einfacher Art sein, wie das Addieren oder Subtrahieren von Zahlen oder das Sortieren einer gegebenen Datenmenge. Komplexere Probleme reichen von der Erstellung von Computerspielen oder der Datenverwaltung von Firmen bis hin zur Steuerung von Raketen. Von besonderer Wichtigkeit für ein systematisches Programmieren ist die Herausarbeitung der fundamentalen Konzepte einer Programmiersprache. Eine Programmiersprache ist eine zum Formulieren von Programmen geschaffene künstliche Sprache. Die Anweisungen, die wir dem Computer geben, werden als Text formuliert, man nennt jeden solchen Text ein Programm. Der Programmtext wird nach genau festgelegten Regeln formuliert. Diese Regeln sind durch die Grammatik einer Programmiersprache festgelegt. Im Gegensatz zur Umgangssprache verlangen Programmiersprachen das exakte Einhalten der Grammatikregeln. Jeder Punkt, jedes Komma hat seine Bedeutung, selbst ein kleiner Fehler führt dazu, dass das Programm als Ganzes nicht verstanden wird. In frühen Programmiersprachen standen die verfügbaren Operationen eines Rechners im Vordergrund. Diese mussten durch besonders geschickte Kombinationen verbunden werden, um ein bestimmtes Problem zu lösen. Moderne höhere Programmiersprachen orientieren sich stärker an dem zu lösenden Problem und gestatten eine abstrakte Formulierung des Lösungswegs, der die Eigenarten der Hardware, auf der das Programm ausgeführt werden soll, nicht mehr in Betracht zieht. Dies hat den Vorteil, dass das gleiche Programm grundsätzlich auf unterschiedlichen Systemen ausführbar ist. 70 5.2 Übersicht der Programmiersprachen 71 5.3 Definition von Programmiersprachen Programmiersprachen sind sehr exakte künstliche Sprachen zur Formulierung von Programmen. Sie dürfen keine Mehrdeutigkeiten bei der Programmerstellung zulassen, damit der Computer das Programm auch korrekt ausführen kann. Bei der Definition einer Programmiersprache müssen deren Lexik, Syntax, Semantik und Pragmatik definiert werden: Lexik: Die Lexik einer Programmiersprache definiert die gültigen Zeichen zw. Wörter, aus denen Programme der Programmiersprache zusammengesetzt sein dürfen. Syntax: Die Syntax einer Programmiersprache definiert den korrekten Aufbau der Sätze aus gültigen Zeichen bzw. Wörtern, d.h. sie legt fest, in welcher Reihenfolge lexikalisch korrekte Zeichen bzw. Wörter im Programm auftreten dürfen. Semantik: Die Semantik einer Programmiersprache definiert die Bedeutung syntaktisch korrekter Sätze, d.h. sie beschreibt, was passiert, wenn bspw. bestimmte Anweisungen ausgeführt werden. Pragmatik: Die Pragmatik einer Programmiersprache definiert ihren Einsatzbereich, d.h. sie gibt an, für welche Arten von Problemen die Programmiersprache besonders gut geeignet ist. 5.4 Klassifikation von Programmiersprachen Eine durchaus berechtigte Frage wäre: Wieso gibt es eigentlich nicht nur eine einzige Programmiersprache, mit der alle Programmierer arbeiten? Da Programmiersprachen anders als natürliche Sprachen, die sich über Jahrhunderte hinweg entwickelt haben, ja künstlich definiert werden müssen, hätte man sich doch von Anfang an auf eine einheitliche Programmiersprache festlegen können. Eine mögliche Klassifizierung unterscheidet so genannte niedere Maschinensprachen (maschinennahe Programmiersprachen) und höhere problemorientierte Programmiersprachen. Maschinensprachen ermöglichen die Erstellung sehr effizienter Programme. Sie sind jedoch abhängig vom speziellen Computertyp. Dahingegen orientieren sich die höheren Programmiersprachen nicht so sehr an 72 den vom Computer direkt ausführbaren Befehlen, sondern eher an den zu lösenden Problemen. Sie sind für Menschen verständlicher und einfacher zu handhaben. Ein weiterer Grund für die Existenz der vielen verschiedenen Programmiersprachen liegt in der Tatsache, dass die zu lösenden Probleme nicht alle gleichartig sind. So werden häufig neue Programmiersprachen definiert, die speziell für bestimmte Klassen von Problemen konzipiert sind. Den höheren Programmiersprachen liegen bestimmte Konzepte zugrunde, mit denen die Lösung von Problemen formuliert wird. Im Wesentlichen lassen sich hier fünf Kategorien - oder auch Programmierparadigmen genannt - unterscheiden: Imperative Programmiersprachen: Programme bestehen aus Folgen von Befehlen (BASIC, PASCAL, MODULA2). Funktionale Programmiersprachen: Programme werden als mathematische Funktionen betrachtet (LISP, MIRANDA). Prädikative/deklarative Programmiersprachen: Programme bestehen aus Fakten (gültige Tatsachen) und Regeln, die beschreiben, wie aus gegebenen Fakten neue Fakten hergeleitet werden können (PROLOG). Regelbasierte Programmiersprachen: Programme bestehen aus "Wenn-Dann-Regeln"; wenn eine angegebene Bedingung gültig ist, dann wird eine angegebene Aktion ausgeführt (OPS5). Objektorientierte Programmiersprachen: Programme bestehen aus Objekten, die bestimmte (Teil-)Probleme lösen und zum Lösen eines Gesamtproblems mit anderen Objekten über Nachrichten kommunizieren können (SMALLTALK). Nicht alle Programmiersprachen können eindeutig einer dieser Klassen zugeordnet werden. So ist bspw. LOGO eine funktionale Programmiersprache, die aber auch imperative Sprachkonzepte besitzt. Java und C++ können als imperative objektorientierte Programmiersprachen klassifiziert werden, denn Java und C++-Programme bestehen aus kommunizierenden Objekten, die intern mittels imperativer Sprachkonzepte realisiert werden. Programmiersprachen einer Kategorie unterscheiden sich häufig nur in syntaktischen Feinheiten. Die grundlegenden Konzepte sind ähnlich. Von daher ist es im Allgemeinen nicht besonders schwierig, eine weitere Programmiersprache zu erlernen, wenn man bereits eine Programmiersprache derselben Kategorie beherrscht. Anders verhält es sich jedoch beim Erlernen von Programmiersprachen anderer Kategorien, weil hier die zugrunde liegenden Konzepte stark voneinander abweichen. 73 5.5 Vom Programm zur Maschine Programme, die in einer höheren Programmiersprache geschrieben sind, können nicht unmittelbar auf einem Rechner ausgeführt werden. Sie sind anfangs in einer Textdatei gespeichert und müssen erst in Folgen von Maschinenbefehlen übersetzt werden. Maschinenbefehle sind elementare Operationen, die der Prozessor des Rechners unmittelbar ausführen kann. Sie beinhalten zumindest Befehle, um Daten aus dem Speicher zu lesen elementare arithmetische Operationen auszuführen Daten in den Speicher zu schreiben die Berechnung an einer bestimmten Stelle fortzusetzen (Sprünge) Die Übersetzung von einem Programmtext in eine Folge solcher einfacher Befehle (auch Maschinenbefehle oder Maschinencode genannt), wird von einem Compiler durchgeführt. Das Ergebnis ist ein Maschinenprogramm, das in einer als "ausführbar" (executable) gekennzeichneten Datei gespeichert ist. Eine solche ausführbare Datei muss noch von einem Ladeprogramm in den Speicher geladen werden und kann erst dann ausgeführt werden. Ladeprogramme sind im Betriebssystem enthalten, der Benutzer weiß oft gar nichts von deren Existenz. So sind in den Betriebssystemen der Windows-Familie ausführbare Dateien durch die Endung ".exe" oder ".com" gekennzeichnet. Tippt man auf der Kommandozeile den Namen einer solchen Datei ein und betätigt die Eingabetaste, so wird die ausführbare Datei in den Hauptspeicher geladen und ausgeführt. 5.5.1 Virtuelle Maschinen Die Welt wäre einfacher, wenn sich alle Programmierer auf einen Rechnertyp und eine Programmiersprache einigen könnten. Man würde dazu nur einen einzigen Compiler benötigen. Die Wirklichkeit sieht anders aus. Es gibt (aus gutem Grund) zahlreiche Rechnertypen und noch viel mehr verschiedene Sprachen. Fast jeder Programmierer hat eine starke Vorliebe für eine ganz bestimmte Sprache und möchte, dass seine Programme auf möglichst jedem Rechnertyp ausgeführt werden können. Bei n Sprachen und m Maschinentypen würde dies, wie in Abbildung 21 dargestellt, n x m viele Compiler erforderlich machen. 74 Abbildung 21: Kombination/Zuordnung von Programmiersprachen und Rechnern Schon früh wurde daher die Idee geboren, eine virtuelle Maschine V zu entwerfen, die als gemeinsames Bindeglied zwischen allen Programmiersprachen und allen konkreten Maschinensprachen fungieren könnte. Diese Maschine würde nicht wirklich gebaut, sondern man würde sie auf jedem konkreten Rechner emulieren, d.h. nachbilden. Für jede Programmiersprache müsste dann nur ein Compiler vorhanden sein, der Code für V erzeugt. Statt n x m vieler Compiler benötigte man jetzt nur noch n Compiler und m Implementierungen von V auf den einzelnen Rechnertypen, insgesamt also nur n + m viele Übersetzungsprogramme - ein gewaltiger Unterschied: Abbildung 22: Idee einer universellen virtuellen Maschine Leider ist eine solche virtuelle Maschine nie zu Stande gekommen. Neben dem Verdacht, dass ihr Design eine bestimmte Sprache oder einen bestimmten Maschinentyp bevorzugen könnte, stand die begründete Furcht im Vordergrund, dass diese Zwischenschicht die Geschwindigkeit der Programmausführung beeinträchtigen könnte. 75 Außerdem verhindert eine solche Zwischeninstanz, dass spezielle Fähigkeiten eines Maschinentyps oder spezielle Ausdrucksmittel einer Sprache vorteilhaft eingesetzt werden können. In Zusammenhang mit einer festen Sprache ist das Konzept einer virtuellen Maschine jedoch mehrfach aufgegriffen worden, z.B. bei der objektorientierten Sprache Java. Ein Java-Compiler übersetzt ein in Java geschriebenes Programm in einen Code für eine virtuelle Java-Maschine. Auf jeder Rechnerplattform, für die ein Emulator dieser virtuellen Java-Maschine verfügbar ist, ist das Programm dann lauffähig. Weil man also bewusst auf die Ausnutzung besonderer Fähigkeiten der jeweiligen Hardware verzichtet, wird die Sprache plattformunabhängig. Abbildung 23: Reale virtuelle Maschine 5.5.2 Interpreter Ein Compiler übersetzt immer einen kompletten Programmtext in eine Folge von Maschinenbefehlen, bevor die erste Programmanweisung ausgeführt wird. Ein Interpreter dagegen übersetzt immer nur eine einzige Programmanweisung in ein kleines Unterprogramm aus Maschinenbefehlen und führt dieses sofort aus. Anschließend wird mit der nächsten Anweisung genauso verfahren. Interpreter sind einfachen zu konstruieren als Compiler, haben aber den Nachteil, dass ein Befehl, der mehrfach ausgeführt wird, jedes Mal erneut übersetzt werden muss. Grundsätzlich können fast alle Programmiersprachen compilierend oder interpretierend implementiert werden. Trotzdem gibt es einige, die fast ausschließlich mit Compilern arbeiten. Dazu gehören Pascal, Modula, COBOL, Fortran, C und C++. Andere, darunter BASIC, APL, LISP und Prolog, werden überwiegend interpretativ bearbeitet. Sprachen wie Java und Smalltalk beschreiten einen Mittelweg zwischen compilierenden und interpretierenden Systemen - das Quellprogramm wird in Code für die virtuelle Java bzw. Smalltalk-Maschine, den so genannten Bytecode, 76 compiliert. Dieser wird dann von der virtuellen Maschine interpretativ ausgeführt. Damit ist die virtuelle Maschine nichts anderes als ein Interpreter für Bytecode. 5.6 Programmentwicklung Ziel der Programmierung ist die Entwicklung von Programmen, die gegebene Probleme korrekt und vollständig lösen. Ausgangspunkt der Programmentwicklung ist also ein gegebenes Problem, Endpunkt ist ein ausführbares Programm, das korrekte Ergebnisse liefert. Den Weg vom Problem zum Programm bezeichnet man auch als Problemlöse- oder Programmentwicklungsprozess oder kurz Programmierung. Der Problemelöseprozess kann in mehrere Phasen zerlegt werden, die in der Regel nicht streng sequentiell durchlaufen werden. Durch neue Erkenntnisse, aufgetretene Probleme und Fehler wird es immer wieder zu Rücksprüngen in frühere Phasen kommen. 5.6.1 Analyse Eine Analyse ist eine vollständige, detaillierte und eindeutige Spezifikation eines Problems. In der Analysephase wird das zu lösende Problem bzw. das Umfeld des Problems genauer untersucht. Insbesondere folgende Fragestellungen sollten bei der Analyse ins Auge gefasst und auch mit anderen Personen diskutiert werden: Ist die Problemstellung exakt und vollständig beschrieben? Was sind mögliche Initialzustände bzw. Eingabewerte (Parameter) für das Problem? Welches Ergebnis wird genau erwartet, wie sehen der gewünschte Endzustand bzw. die gesuchten Ausgabewerte aus? Gibt es Randbedingungen, Spezialfälle bzw. bestimmte Zwänge (Constraints), die zu berücksichtigen sind? Lassen sich Beziehungen zwischen Initial- und Endzuständen bzw. Eingabeund Ausgabewerten herleiten? Erst wenn alle diese Fragestellungen beantwortet sind und eine exakte Problemspezifikation vorliegt, sollte in die nächste Phase verzweigt werden. Es hat sich gezeigt, dass Fehler, die aus einer nicht ordentlich durchgeführten Analyse herrühren, zu einem immensen zusätzlichen Arbeitsaufwand in späteren Phasen führen können. Deshalb sollte in dieser Phase mit größter Sorgfalt gearbeitet werden. 77 5.6.2 Entwurf Nachdem in der Analysephase das Problem in einer Spezifikation genau beschrieben worden ist, geht es darum, einen Lösungsweg zu entwerfen. Dies geschieht in der Entwurfsphase. Da die Lösung von einem Rechner durchgeführt wird, muss jeder Schritt exakt vorgeschrieben sein. Wir kommen zu folgender Begriffsbestimmung: Ein Algorithmus ist eine detaillierte und explizite Vorschrift zur schrittweisen Lösung eines Problems. Im Einzelnen beinhaltet diese Definition: Die Ausführung des Algorithmus erfolgt in einzelnen Schritten. Jeder Schritt besteht aus einer einfachen und offensichtlichen Grundaktion. Zu jedem Zeitpunkt muss klar sein, welcher Schritt als nächster auszuführen ist. Der Entwurfsprozess kann im Allgemeinen nicht mechanisch durchgeführt werden, vor allen Dingen ist er nicht automatisierbar. Vielmehr kann man ihn als kreativen Prozess bezeichnen, bei dem Auffassungsgabe, Intelligenz und vor allem Erfahrung des Programmierers eine wichtige Rolle spielen. Diese Erfahrung kann insbesondere durch fleißiges Üben erworben werden. Für den Entwurf eines Algorithmus können die folgenden Ratschläge nützlich sein: Informieren Sie sich über möglicherweise bereits existierende und verfügbare Lösungen für vergleichbare Probleme und nutzen Sie diese. Schauen Sie sich nach allgemeineren Problemen um und überprüfen Sie, ob Ihr Problem als Spezialfall des allgemeinen Problems betrachtet werden kann. Versuchen Sie, das (komplexe) Problem in einfachere Teilprobleme aufzuteilen. Wenn eine Aufteilung möglich ist, wenden Sie den hier skizzierten Programmentwicklungsprozess zunächst für die einzelnen Teilprobleme an und setzen dann die Teillösungen zu einer Lösung für das Gesamtproblem zusammen. Eine Möglichkeit zur grafischen Darstellung von Algorithmen sind Flussdiagramme. Sie haben den Vorteil, unmittelbar verständlich zu sein. Der Nachteil ist, dass sie für komplexere Algorithmen schnell unübersichtlich werden. Flussdiagramme setzen sich zusammen aus folgenden Symbolen: 78 Abbildung 24: Elemente von Flussdiagrammen Beispiel für einen Algorithmus Bei dem folgenden Beispiel geht es um die Lösung des Problems, die Summe aller natürlichen Zahlen bis zu einer vorgegebenen Natürlichen Zahl n zu berechnen. Mathematisch definiert ist also die folgende Funktion f zu berechnen: Das folgende Flussdiagramm stellt einen Algorithmus zur Lösung des obigen Problems dar. Die Aktionen, die in den Rechtecken dargestellt sind, werden als elementare Handlungen des Rechners verstanden, die nicht näher erläutert werden müssen. In unserem Falle handelt es sich dabei um so genannte Zuweisungen, bei denen ein Wert berechnet und das Ergebnis gespeichert wird. So wird z.B. durch die Zuweisung "erg = erg + i" der Inhalt der durch erg und i bezeichneten Speicherplätze addiert und das Ergebnis in dem Speicherplatz erg abgelegt. 79 Abbildung 25: Flussdiagramm zum Beispielalgorithmus Algorithmen als Lösung einer Spezifikation Eine Spezifikation beschreibt also ein Problem, ein Algorithmus gibt eine Lösung des Problems an. Ist das Problem durch ein Paar {P} {Q} aus einer Vorbedingung P und einer Nachbedingung Q gegeben, so schreiben wir: o {P} A {Q}, falls der Algorithmus A die Vorbedingung P in die Nachbedingung Q überführt. Genauer formuliert bedeutet dies: Wenn der Algorithmus A in einer Situation gestartet wird, in der P gilt, dann wird, wenn A beendet ist, Q gelten. In diesem Sinne ist ein Algorithmus eine Lösung einer Spezifikation. Man kann eine Spezifikation als eine Gleichung mit einer Unbekannten ansehen: Zu der Spezifikation {P} {Q} ist ein Algorithmus X gesucht mit {P} X {Q}. 80 Nicht jede Spezifikation hat eine Lösung. So verlangt {M<0} {x = log M}, den Logarithmus einer negativen Zahl zu finden. Diese Spezifikation kann also nicht gelöst werden. Wenn eine Spezifikation aber eine Lösung hat, dann gibt es immer unendlich viele Lösungen. So ist jeder Algorithmus, der das gewünschte Ergebnis liefert - ganz egal, wie umständlich er dies macht - eine Lösung für unsere Spezifikation. Terminierung In einer oft benutzten strengeren Definition des Begriffes Algorithmus wird verlangt, dass ein solcher nach endlich vielen Schritten terminiert, also beendet ist. Diese Forderung steht aber vor folgenden Schwierigkeiten: Manchmal ist es erwünscht, dass ein Programm bzw. ein Algorithmus nicht von selber abbricht. Ein Texteditor, ein Computerspiel oder ein Betriebssystem soll im Prinzip unendlich lange laufen können. Es ist oft nur schwer oder überhaupt nicht feststellbar, ob ein Algorithmus in endlicher Zeit zum Ende kommen wird. Verantwortlich dafür ist die Möglichkeit, Schleifen zu bilden, so dass dieselben Grundaktionen mehrfach wiederholt werden. 5.6.3 Implementierung Der Entwurf eines Algorithmus sollte unabhängig von einer konkreten Programmiersprache erfolgen. Die anschließende Überführung des Algorithmus in ein in einer bestimmten Programmiersprache verfasstes Programm wird als Implementierung bezeichnet. Anders als der Entwurf eines Algorithmus ist die Implementierung in der Regel ein eher mechanischer Prozess. Die Implementierungsphase besteht selbst wieder aus zwei Teilphasen: Editieren: Zunächst wird der Programmcode mit Hilfe eines Editors eingegeben und in einer Datei dauerhaft abgespeichert. Compilieren: Anschließend wird de rProgrammcode mit Hilfe eines Compilers auf syntaktische Korrektheit überprüft und - falls keine Fehler vorhanden sind - in eine ausführbare Form (ausführbares Programm) überführt. Liefert der Compiler eine Fehlermeldung, muss in die Editierphase zurückgesprungen werden. Ist die Compilation erfolgreich, kann das erzeugte Programm ausgeführt werden. Je nach Sprache und Compiler ist die Ausführung entweder mit Hilfe des Betriebssystems durch den Rechner selbst oder aber durch die Benutzung eines Interpreters möglich. 81 5.6.4 Test In der Testphase muss überprüft werden, ob das entwickelte Programm die Problemstellung korrekt und vollständig löst. Dazu wird das Programm mit verschiedenen Initialzuständen oder Eingabewerten ausgeführt und überprüft, ob es die erwarteten Ergebnisse liefert. Man kann eigentlich immer davon ausgehen, dass Programme nicht auf Anhieb korrekt funktionieren, was zum einen an der hohen Komplexität des Programmentwicklungsprozesses und zum anderen an der hohen Präzision liegt, die die Formulierung von Programmen erfordert. Insbesondere die Einbeziehung von Randbedingungen wird von Programmieranfängern häufig vernachlässigt, so dass das Programm im Normalfall zwar korrekte Ergebnisse liefert, in Ausnahmefällen jedoch versagt. Genauso wie der Algorithmusentwurf ist auch das Testen eine kreative Tätigkeit, die viel Erfahrung voraussetzt und darüber hinaus ausgesprochen zeitaufwendig ist. Im Durchschnitt werden ca. 40% der Programmentwicklungszeit zum Testen und Korrigieren verwendet. Auch durch noch so systematisches Testen ist es in der Regel nicht möglich, die Abwesenheit von Fehlern zu beweisen. Es kann nur die Existenz von Fehlern nachgewiesen werden. Aus der Korrektheit des Programms für bestimmte überprüfte Initialzustände bzw. Eingabewerte kann nicht auf die Korrektheit für alle möglichen Initialzustände bzw. Eingabewerte geschlossen werden! 5.6.5 Dokumentation Parallel zu den eigentlichen Programmentwicklungsphasen sollten alle Ergebnisse dokumentiert, d.h. schriftlich festgehalten werden. Die Dokumentation besteht also aus: einer exakten Problemstellung, einer verständlichen Beschreibung der generellen Lösungsidee und des entwickelten Algorithmus, dem Programmcode sowie einer Erläuterung der gewählten Testszenarien und Protokollen der durchgeführten Testläufe Außerdem sollten weitergehende Erkenntnisse, wie aufgetretene Probleme oder alternative Lösungsansätze, in die Dokumentation mit aufgenommen werden. Die Dokumentation dient dazu, dass andere Personen bzw. der Programmierer selbst, auch zu späteren Zeitpunkten, das Programm noch verstehen bzw. den Programmentwicklungsprozess nachvollziehen können, um z.B. mögliche Erweiterungen oder Anpassungen vornehmen oder die Lösung bei der Bearbeitung vergleichbarer Probleme wieder verwenden zu können. 82 5.7 Elementare Aktionen in Programmiersprachen Wir haben bisher noch nicht erklärt, welche "elementaren Aktionen" wir voraussetzen, wenn wir Algorithmen formulieren. In der Tat sind hier eine Reihe von Festlegungen denkbar. Wir könnten zum Beispiel in einem Algorithmus formulieren, wie man ein bestimmtes Gericht zubereitet. Die Grundaktionen wären dann einfache Aufgaben, wie etwa "Prise Salz hinzufügen", "umrühren" und "zum Kochen bringen". Der Algorithmus beschreibt dann, ob, wann und in welcher Reihenfolge diese einfachen Handlungen auszuführen sind. In einer Programmiersprache kann man Speicherzellen für Datenwerte mit Namen kennzeichnen. Diese nennt man auch Variablen. Man darf den Inhalt einer Variablen lesen oder ihr einen neuen Wert zuweisen. Der vorher dort gespeicherte Wert geht dabei verloren, man sagt, er wird überschrieben. Eine Grundaktion besteht jetzt aus drei elementaren Schritten: einige Variablen lesen die gelesenen Werte durch einfache Rechenoperationen verknüpfen das Ergebnis einer Variablen zuweisen. Eine solche Grundaktion heißt Zuweisung. In Java und C++ wird sie als x = y geschrieben. Dabei ist x eine Variable, das Zeichen "=" ist der Zuweisungsoperator und die rechte Seite y kann ein beliebiger (arithmetischer) Ausdruck sein, in dem auch Variablen vorkommen können. Es handelt sich nicht um eine Gleichung, denn die Variablen, die auf der rechten Seite des Zuweisungszeichens vorkommen, stehen für den alten Wert und die Variable auf der linken Seite für den neuen Wert nach der Zuweisung. Am besten man ignoriert die Ähnlichkeit des Zuweisungsoperators mit dem Gleichheitszeichen und spricht es als "erhält" aus: o "x erhält (den Wert) y" für x = y. Hat man erst einmal einige nützliche Algorithmen programmiert, kann man diese in anderen Programmen benutzen - oder " aufrufen" - und wie eine elementare Aktion behandeln. Dazu muss man sie nur mit einem Namen versehen und kann danach diesen Namen anstelle des Algorithmus hinschreiben. Einige solcher zusätzlicher Aktionen, in Java und C++ Methoden bzw. Funktionen genannt, sind bei allen Sprachen bereits "im Lieferumfang" enthalten. So ist die Funktion printf standardmäßig in C++ enthalten. Ihre Wirkung ist die Ausgabe von Werten in einem Terminalfenster. Ein Aufruf, wie etwa printf ("Hallo Welt!"), ist also auch eine elementare Aktion. 83 5.8 Programmierumgebungen Für fast alle Sprachen gibt es heute "integrierte Entwicklungsumgebungen" (integrated development environment - IDE), die alle zur Programmerstellung notwendigen Werkzeuge beinhalten: o o o einen Editor zum Erstellen und Ändern eines Programmtextes, einen Compiler bzw. Interpreter zum Ausführen von Programmen, einen Debugger für die Fehlersuche in der Testphase eines Programms. Kern dieser Systeme ist immer ein Texteditor zum Erstellen des Programmtextes. Dieser hebt typischerweise nicht nur die Schlüsselworte der Programmiersprache farblich hervor, er markiert auch zugehörige Klammerpaare und kann auf Wunsch den Programmtext auch übersichtlich formatieren. Klickt man auf den Namen einer Variablen oder einer Funktion, so wird automatisch deren Definition im Programmtext gefunden und angezeigt. Soll das zu erstellende Programm zudem eine moderne graphische Benutzeroberfläche erhalten, so kann man diese mit einem GUI-Editor erstellen, indem man Fenster, Menüs, Buttons und Rollbalken mit der Maus "zusammenklickt", beliebig positioniert und anpasst. 5.9 Datentypen und Variablen 5.9.1 Datentypen Daten sind die Objekte, mit denen ein Programm umgehen soll. Man muss verschiedene Sorten von Daten unterscheiden, je nachdem, ob es sich um Wahrheitswerte, Zahlen, Geburtstage, Texte, Bilder, Musikstücke oder Videos handelt. Alle diese Daten sind von verschiedenem Typ, insbesondere verbrauchen Sie unterschiedlich viel Speicherplatz und unterschiedliche Operationen sind mit ihnen durchführbar. So lassen sich zwei Geburtstage oder zwei Bilder nicht addieren, wohl aber zwei Zahlen. Andererseits kann ein Bild komprimiert werden, bei einer Zahl macht dies aber keinen Sinn. Zu einem bestimmten Typ von Daten gehört also immer auch ein charakteristischer Satz von Operationen, um mit diesen Daten umzugehen. Jede Programmiersprache stellt eine Sammlung von Datentypen samt der zugehörigen Operationen bereit und bietet zugleich Möglichkeiten, neue Datentypen zu definieren. 5.9.2 Variablen Eine Variable in einer Programmiersprache ist eine benannte Speicherstelle im Arbeitsspeicher des Rechners. Über den Variablennamen kann der Programmierer auf die entsprechende Speicherzelle zugreifen. 84 Eine Variable hat vier Kennzeichen: Variablennamen Datentyp Wert Adresse (der Speicherzelle) Der Datentyp ist der Bauplan für eine Variable. Der Datentyp legt fest, welche Operationen auf einer Variablen möglich sind und wie die Darstellung (Repräsentation) der Variablen im Speicher des Rechners erfolgt. Mit der Darstellung wird festgelegt, wie viele Bytes die Variable im Speicher einnimmt und welche Bedeutung ein jedes Bit dieser Darstellung hat. Variablen braucht man, um in ihnen Werte abzulegen. Eine Variable ist eine veränderliche Größe - ihr Wert kann also in ihrem Speicherbereich nach Bedarf verändert werden. Der Wert einer Variablen muss der Variablen in der Regel explizit zugewiesen werden. Es gibt aber auch Fälle, bei denen von der Programmiersprache aus eine Variable in impliziter Weise mit einem Wert vorbelegt wird. Ein solcher Vorbelegungs-Wert wird als Default-Wert oder Standardwert bezeichnet. Wird einer Variablen weder explizit, noch defaultmäßig - d.h. durch Vorbelegung - ein Wert zugewiesen, so ist ihr Wert undefiniert. Da im Arbeitsspeicher die Bits immer irgendwie ausgerichtet sind, hat jede Variable automatisch einen Wert, auch wenn ihr noch kein definierter Wert zugewiesen wurde. Ein solcher Wert ist jedoch rein zufällig und führt zu einer Fehlfunktion des Programms. Daher darf es der Programmierer nicht versäumen, den Variablen die gewünschten Startwerte (Initialwerte) zuzuweisen, d.h. die Variable zu initialisieren. Variablen liegen während der Programmausführung in Speicherzellen des Arbeitsspeichers. Die Speicherzellen des Arbeitsspeichers sind durchnummeriert. Die Nummern der Speicherzellen werden Adressen genannt. Eine Variable kann natürlich mehrere Speicherzellen einnehmen. Abbildung 26: Variable im Arbeitsspeicher 85 Die Variable aus der obigen Abbildung belegt die Speicherzellen mit den Adressen 5 und 6 und hat den Wert 3. Über den Namen der Variablen kann man ihren Wert aus den Speicherzellen auslesen und verändern. 5.9.3 Primitive Datentypen Im Folgenden wollen wir die wichtigsten Datentypen, die auch in den meisten Programmiersprachen vorhanden sind, vorstellen. Manche dieser Typen wie z.B. die der ganzen oder der reellen Zahlen, umfassen theoretisch unendlich viele Werte. Die meisten Programmiersprachen schränken daher die verfügbaren Werte auf verschieden große endliche Bereiche ein. Dies hat zur Folge, dass bei der Überschreitung dieser Bereiche Fehler auftreten können, die sich je nach Anwendungsfall mehr oder weniger katastrophal äußern können. 5.9.3.1 Datentypen für Boolesche Werte Der einfachste Datentyp besteht aus den booleschen Werten true und false. Man bezeichnet diesen Datentyp mit dem englischen Ausdruck boolean (Java) oder mit bool (C++). Der benötigte Speicherplatz für diesen Datentyp beträgt in Java 2 Bytes und in C++ 1 Byte. Für die Durchführung von logischen Operationen stehen in Java und C++ folgende Operationen zur Verfügung: Operator (Java/c++) & / && | / || ^/ !/! Verwendung op1 & op2 op1 | op2 op1 ^ op2 !op1 Operation AND OR XOR NOT 5.9.3.2 Datentypen für ganze Zahlen Zur Darstellung ganzer Zahlen ohne Nachkommastelle gibt es o o in Java die Datentypen byte, short, int und long und in C++ short int, int und long int Als elementare Operationen auf diesen Datentypen gelten die arithmetischen Operationen +, -, *, /. In Java sind alle ganzzahligen Datentypen vorzeichenbehaftet, wohingegen in C++ dies durch die Schlüsselwörter signed und unsigned angegeben wird. In Java ergeben sich folgende ganzzahlige Datentypen: 86 Datentyp byte short int long Größe in Bytes 1 2 4 8 Wertebereich -27...27-1 -215...215-1 -231...231-1 -263...263-1 5.9.3.3 Datentypen für reelle Zahlen Datentypen für reelle Zahlen dienen der Speicherung von Zahlen mit Nachkommastellen. In Java stehen hierzu die Typen float und double und in C++ float, double und long double zur Verfügung. Als elementare Operationen auf diesen Datentypen gelten ebenfalls die arithmetischen Operationen +, -, *, /. In Java gibt es die folgenden Gleitkommatypen: Datentyp float double long double (C++) Größe in Bytes 4 8 10 Wertebereich -3,4x10 bis +3,4x1038 -1,7x10308 bis +1,7x10308 -1,1x104932 bis +1,1x104932 38 5.9.3.4 Datentypen für Zeichen Sowohl in Java als auch in C++ wird ein Zeichen durch den Datentyp char (engl. character) dargestellt. Während C++ dazu die 256 ASCII-Zeichen verwendet, stellt Java bereits den gesamten UNICODE-Zeichensatz zur Verfügung. Zur Darstellung eines Zeichens werden daher in Java 2 Byte und in C++ 1 Byte benötigt. 5.9.4 Zusammengesetzte Datentypen Die bisher besprochenen Datentypen sind in einem gewissen Sinne atomar, d.h. ihre Werte sind nicht weiter in Bestandteile zerlegbar. Zusammengesetzte Datentypen bestehen aus einer Menge gleichartiger Datentypen. 5.9.4.1 Strings Als erstes Beispiel eines zusammengesetzten Typs betrachten wir Strings. Ein String ist eine Zeichenkette und besteht daher aus einer Folge von Zeichen. Strings kann man direkt als Stringliterale angeben. Dazu schließt man eine beliebige Folge von Zeichen in besondere Begrenzungszeichen ein. Java benutzt dafür doppelte Anführungszeichen: o "Ich bin ein Java-Stringliteral" 87 Durch Aneinanderhängen (Konkatenieren) zweier Strings s1 und s2 entsteht ein neuer String s1+s2. Auf die einzelnen Zeichen eines Strings kann man zugreifen, Java bietet dafür die Methode s1.charAt(i) an. i ist dabei eine Variable, die die Position des gewünschten Zeichens im String angibt. Mit s.length() kann man die Länge des Strings anzeigen lassen. s.indexOf("test") liefert die Position des Strings "test" innerhalb des Strings s oder 1, wenn der gesuchte String nicht enthalten ist. 5.9.4.2 Arrays Ein Array ist ein Objekt, das aus Komponenten (Elementen) zusammengefasst ist, wobei jedes Element eines Arrays vom selben Datentyp sein muss: int int int int Ein Array aus 5 Elementen des Datentyps integer int In Java können Arrays aus Elementen eines primitiven Datentyps oder aus Elementen eines Referenztyps bestehen. Ein Element eines Arrays kann auch selbst wieder ein Array sein. Dann entsteht ein mehrdimensionales Array. Im Folgenden werden zunächst eindimensionale Arrays betrachtet. Die Länge oder Größe eines Arrays legt die Anzahl der Elemente des Arrays fest. Die Länge muss als Wert immer eine ganze positive Zahl haben. Ist laenge die Länge des Arrays, so werden die Elemente von 0 bis laenge-1 durchgezählt. Die Nummer beim Durchzählen wird als Index des Arrays bezeichnet. Über den Index kann man auf ein Element zugreifen. Der Zugriff auf das i-te Element des Arrays mit dem Namen arrayName erfolgt durch arrayName[i-1] (weil die Indizierung mit 0 beginnt!). Der Vorteil eines Arrays gegenüber mehreren einfachen Variablen ist, dass Arrays sich leicht mit Schleifen bearbeiten lassen, da der Index einer Array-Komponente eine Variable sein und als Laufvariable in einer Schleife benutzt werden kann. char-Arrays in C++ Arrays aus Elementen des Typs char nehmen in C++ eine Sonderstellung ein. Einerseits kann man in ihnen - in Analogie zu Arrays aus Elementen eines der anderen Datentypen - einzelne Datenobjekte vom Typ der Arrayelemente ablegen, also etwa Zeichen oder auch kleine ganze Zahlen, andererseits dienen sie aber auch zur Speicherung eines speziellen Typs von Datenobjekten: Strings. In C++ ist ein String eine Folge von Zeichen, die in einem Array aus Elementen vom Typ char gespeichert ist und mit dem Nullzeichen '\0' 88 terminiert ist. Fehlt das Nullzeichen, hat man lediglich einzelne Zeichen in einem charArray vor sich, jedoch noch keinen String. Wegen des zusätzlichen abschließenden Nullzeichens belegt ein String stets ein Byte mehr Platz im Speicher, als die eigentliche Zeichenfolge Zeichen hat. 5.9.5 Variablen und Speicher Um mit einem Computer Algorithmen zu definieren, ist es notwendig, Zwischenwerte zu speichern und gespeicherte Zwischenwerte für die weitere Berechnung zu verwenden. Für die Speicherung von Werten steht der Hauptspeicher zur Verfügung. Es wäre aber mühsam, wenn sich der Programmierer darum kümmern müsste, an welcher Stelle im Speicher ein Zwischenwert steht, wie viele Bytes (etwa im Falle einer Gleitpunktzahl) dazu gehören, welche Speicherplätze noch frei sind etc. Daher bieten alle Programmiersprachen das Konzept der Variablen an. Aus der Sicht des Programmierers sind Variablen Behälter für Werte eines bestimmten Datentyps. Der Compiler sorgt dafür, dass zur Laufzeit eines Programms für alle Variablen Speicherplatz reserviert ist und zwar soviel, wie für die Aufnahme von Werten des jeweiligen Datentyps benötigt wird. Er setzt automatisch jeden Bezug (Referenz) auf eine Variable in die entsprechende Hauptspeicheradresse um. Programmierer können mit Variablen so umgehen wie mit Werten des Datentyps. Man kann mit ihnen rechnen wie mit Unbestimmten in der Mathematik. Wenn ein Ausdruck, wie z.B. x*(y+1/y) ausgerechnet wird, so werden für die Variablen x und y immer die Werte eingesetzt, die sich zur Zeit an den ihnen zugewiesenen Speicherplätzen befinden. 5.9.6 Deklaration von Variablen In den meisten höheren Programmiersprachen müssen Variablen vor ihrer ersten Benutzung deklariert werden. Dies bedeutet, dass man dem System mitteilen muss, welche Variablen man benötigt und von welchem Datentyp die Werte sein sollen, die in der Variablen gespeichert werden sollen. Generell gilt (und das nicht nur bei Variablennamen), dass C++ und Java zwischen Groß- und Kleinschreibung unterscheiden: o o o o o int a, b, c; int A, B, C; boolean test; float x, y; char c; 89 Da nicht alle Sprachen solche Variablendeklarationen verlangen, stellt sich die Frage, welchen Vorteil eine solche Deklaration mit sich bringt. Zunächst hilft eine Deklaration dem Compiler, weil er zur Compilierungszeit bereits weiß, wie viel Speicherplatz er reservieren muss. Für die obige Java-Deklaration ergibt sich ein Speicherplatzbedarf von: 3x4 Byte + 3x4 Byte + 1 Byte + 2x4 Byte + 2 Byte = 35 Byte. Zweitens kann der Compiler bereits festlegen, wo die Variablen (relativ zueinander) im Hauptspeicher angeordnet werden sollen. In dem Programm kann er dann jede Erwähnung einer Variablen bereits durch eine Referenz auf den entsprechenden Speicherplatz ersetzen. Dies führt zu einer Zeitersparnis für jeden Variablenzugriff. Vor allem aber hilft die Deklaration dem Programmierer - viele Fehler, die aus einer falschen Benutzung von Variablen entstehen, werden bereits zur Compilierungszeit erkannt. 5.9.7 Initialisierung von Variablen Bevor zum ersten Mal ein Wert in einer Variablen gespeichert wurde, ist der darin befindliche Wert (wie bereits ausgeführt) vom Zufall bestimmt. Daher ist es sinnvoll, jede Variable möglichst frühzeitig mit einem Ausgangswert zu versehen, d.h. sie zu initialisieren. Manche Programmiersprachen initialisieren Variablen automatisch mit einem Standardwert (engl. Default), die meisten überlassen dies aber dem Programmierer. C++ und Java bieten die Möglichkeit, Variablen gleichzeitig zu deklarieren und optional auch mit einem Anfangswert zu initialisieren: o o o o int a,b,c = 6; boolean test = false; double pi = 3.14; char c = 'p'; 5.9.8 Typkorrekte Ausdrücke Variablen bezeichnen Werte von bestimmten Datentypen. Die allgemeinste Form, einen Wert zu bezeichnen, erlaubt neben Variablen und Elementen der Datenstruktur auch Operationszeichen. Eine solche Konstruktion nennt man (typkorrekten) Ausdruck, in der Mathematik auch wohlgeformten Term. Konstanten und Variablen sind demzufolge Ausdrücke. Wenn mehrere Ausdrücke durch ein Operationszeichen verknüpft werden, ist die resultierende Konstruktion wieder ein Ausdruck. Bedingung dabei ist, dass nur Ausdrücke passender Sorten verknüpft werden Nehmen wir z.B. an, dass wir in Java folgende Variablen deklariert haben: 90 o o o o o int a,b,c = 6; double pi = 3.14; double t = 3.00; char ch = 'c'; char d = 'd'; Dann sind folgende Ausdrücke Beispiele für typkorrekte Ausdrücke: o o a + 2 * (b + c) || nur ganze Zahlen t = pi + t || nur double-Werte Nicht typkorrekt ist dagegen: o a + ch || Addition von Zahl und Zeichen = sinnlos) a = b + pi || Zuweisung von double zu Integer führt zu Genauigkeitsverlusten 5.9.9 Typfehler Die meisten gängigen Programmiersprachen sind statisch getypt, was bedeutet, dass der Typ jedes Ausdrucks bereits zur Compilierzeit, also vor der Ausführung des Programms, feststehen muss. Viele Fehler können daher frühzeitig erkannt werden, weil sie bewirken, dass Ausdrücke inkompatibler Typen verknüpft werden. Hätte man sich z.B. vertippt und "length(s+1)" geschrieben, statt "length(s)+1", so würde der Compiler bereits den Fehler erkennen, denn wenn s als String deklariert ist, macht s+1 keinen Sinn, und wenn s als Integer deklariert ist, ist die Länge von s+1 ebenfalls sinnlos. Ist eine Sprache nicht statisch getypt oder gar völlig untypisiert, so würde der obige Fehler erst zur Laufzeit oder gar nicht auffallen. Egal ob s einen Integer oder einen String darstellt, er ist durch eine Bitfolge repräsentiert - und length arbeitet im Endeffekt auf Bitfolgen. Es käme also irgendetwas heraus bei length(s+1), nur wäre es vermutlich nicht das was man eigentlich wollte. Wird mit diesem sinnlosen Ergebnis weitergerechnet, wird es möglicherweise nicht einmal als falsch erkannt! 5.10 Der Kern imperativer Sprachen Programmiersprachen erweitern die Möglichkeiten, die Datenstrukturen durch ihre Operationen bieten, um die Fähigkeit, Zwischenwerte zu speichern und später wieder zu verwenden. Zwischenwerte werden dabei in Variablen abgespeichert. 91 Die jeweilige Belegung einer Variablen mit konkreten Werten nennen wir Speicherzustand oder kurz Speicher. Eine Berechnung besteht dann aus einer Folge von elementaren Aktionen, die immer wieder einen Ausdruck auswerten das Ergebnis speichern Solche elementaren Aktionen werden Zuweisung genannt. Da der Wert eines Ausdruckes von den darin enthaltenen Variablen, oder genauer von den in diesen Variablen gespeicherten Zwischenwerten, abhängig ist, können sich durch geschickte Kombination von Zuweisungen komplexe Berechnungen ergeben. Ein Programm enthält somit einfache oder zusammengesetzte Anweisungen an die Maschine, die insgesamt eine gezielte Veränderung des Speicherzustandes herbeiführen. Solche Anweisungen nennt man auch Befehle und spricht von befehlsorientierten oder imperativen Sprachen. 5.10.1 Zuweisungen Zuweisungen sind die einfachsten Anweisungen zur Veränderung des Speichers. Sie bewirken die gezielte Veränderung des Wertes einer einzigen Variablen. Eine Zuweisung besteht aus einer Variablen v und einem Ausdruck t, die durch ein Zuweisungszeichen verbunden sind: v=t (wir gehen hier davon aus, dass "=" der Zuweisungsoperator ist (wie in Java oder C++). Das ist aber nicht zwangsläufig der Fall: In PASCAL ist ":=" der Zuweisungsoperator.) Eine solche Zuweisung wird ausgeführt, indem der Ausdruck t ausgewertet wird und der resultierende Wert anschließend in v gespeichert wird. Dabei wird der alte Wert von v überschrieben (siehe nachfolgendes Beispiel): 92 5.10.2 Kontrollstrukturen Das Wesen einer imperativen Programmiersprache besteht darin, Folgen von Anweisungen zu neuen Anweisungen zu gruppieren. Wie bei Zuweisungen ist der Netto-Effekt solcher zusammengesetzter Anweisungen eine Veränderung des Speicherinhalts. Selbst eine Bildschirmausgabe ist letztlich Ausdruck einer Speicherveränderung, denn sie beruht auf einer Veränderung des Bildschirmspeichers, der im Textmodus für jede Bildschirmposition ein Zeichen, sowie einen Farb- und Helligkeitswert enthält. Die Möglichkeiten, die eine Programmiersprache anbietet, um gezielt und kontrolliert Anweisungen zu neuen komplexeren und abstrakteren Anweisungen zusammenzusetzen, nennt man Kontrollstrukturen. Der Begriff Kontrollstrukturen fasst spezielle Anweisungen zusammen, die der Steuerung (Kontrolle) des Programmflusses dienen. Sie ermöglichen es dem Programmierer, nicht nur streng sequentiell ablaufende Programme zu schreiben, sondern auch solche, in denen Anweisungen unter bestimmten Bedingungen übersprungen oder wiederholt ausgeführt werden können, wenn die Logik des betreffenden Programms dies erfordert. Die Kontrollstrukturen in Java und C++ lassen sich in drei Gruppen einteilen: Auswahl- oder Selektionsanweisungen Machen die Ausführung von Anweisungen vom Erfülltsein einer Bedingung abhängig und reduzieren so die generelle Ausführbarkeit einer Anweisung auf ihre Ausführbarkeit in lediglich einer bestimmten Auswahl von Fällen. Wiederholungs- oder Iterationsanweisungen Gestatten es, eine oder mehrere Anweisungen beliebig oft auszuführen. Sprung- oder Kontrollanweisungen Bewirken Sprünge - vorwärts oder rückwärts - im Programm (wonach das Programm mit der Ausführung der Anweisungen fortfährt, die sich an der Stelle befinden, zu der gesprungen wurde). Mit diesen drei Typen von Kontrollstrukturen werden wir uns in den nächsten Abschnitten beschäftigen. 5.10.2.1 Auswahlanweisungen Es gibt drei Auswahlanweisungen in Java und C++, die mit den Schlüsselwörtern if, if else und switch bezeichnet sind. 93 Die if-Anweisung Die if-Anweisung stellt den einfachsten Fall einer bedingten Anweisung dar: Ist die Bedingung erfüllt, werden die betreffenden Anweisungen ausgeführt. Wenn nicht, findet keinerlei Operation statt. Die if-Anweisung gestattet die Ausführung einer oder mehrer Anweisungen nur dann, wenn die Überprüfung einer zuvor formulierten Bedingung ergeben hat, dass diese erfüllt ist. Gibt es nur eine Anweisung, die von dieser Bedingung abhängig ist, kann man die ifAnweisung mit folgender Syntax ausdrücken: if (ausdruck) anweisung Sollen mehrere Anweisungen von der Bedingung abhängig sein, müssen diese als Block in geschweiften Klammern notiert werden: if (ausdruck) { anweisung1 anweisung2 anweisung3 ... } Die Bedingung der if-Anweisung ist durch den Syntaxteil ausdruck gegeben: Ergibt ausdruck den Wahrheitswert TRUE, werden die abhängigen Anweisungen ausgeführt. Ist ausdruck FALSE, gelangt keine der abhängigen Anweisungen zur Ausführung. 94 Abbildung 27: Ist ausdruck TRUE, werden die abhängigen Anweisungen ausgeführt. Ist ausdruck FALSE, wird das Programm mit der auf die ifAnweisung folgenden Anweisung fortgesetzt. Wird das untenstehende Programmstück ausgeführt, wird die Meldung "a ist größer als b" auf den Bildschirm ausgegeben, da der Wert von a in der Tat größer als der von b ist, und somit die Bedingung a > b = TRUE ausgewertet wird. int a = 2; int b = 1; if ( a > b) cout << "a ist größer als b"; 95 Ändert man jedoch den Wert von b mit b = a von 1 in 2, sind die Werte der beiden Variablen gleichgroß und somit die Bedingung a > b nicht erfüllt, also FALSE. In diesem Falle unterbleibt die Ausgabe auf den Bildschirm. Die if else-Anweisung Sollen auch, wenn die Bedingung nicht erfüllt ist, bestimmte Operationen ausgeführt werden, verewndet man die if else-Anweisung, die sich von der einfachen ifAnweisung durch das Vorhandensein konkreter Alternativanweisungen für diesen Fall unterscheidet. Es kommt nicht selten vor, dass eine Situation eintritt, in der nicht nur für den Fall, dass die Bedingung einer if-Anweisung TRUE ist, irgendwelche Operationen stattfinden sollen, sondern auch dann, wenn die Bedingung FALSE ist. Die passende Kontrollstruktur für derartige Fälle ist die if else-Anweisung. Gibt es nur eine abhängige Anweisung, wird folgende Syntax verwendet: o Sollen mehrere Anweisungen von der Bedingung abhängig sein, müssen diese wieder als Block in geschweiften Klammern notiert werden: o if (ausdruck) anweisung else anweisung if (ausdruck) { anweisung } else { anweisung } Ist ausdruck TRUE, werden die Anweisung(en) im so genannten if-Zweig der if else-Anweisung ausgeführt, andernfalls diejenigen des else-Zweiges (siehe nächste Abbildung) Die switch-Anweisung Die switch-Anweisung realisiert eine so genannte Mehrfachauswahl, bei der Aktionen nicht nur in einem oder zwei, sondern auch in einer größeren Anzahl von Fällen stattfinden können. Die switch-Anweisung realisiert eine Auswahl zwischen beliebig vielen Alternativen und ist insofern mit einer geschachtelten if else-Anweisung vergleichbar. Diese so genannte Mehrfach-Auswahl wird gemäß der folgenden Syntax durchgeführt: 96 Der Ausdruck nach dem Schlüsselwort switch muss von ganzzahligem Typ sein und wird der Reihe nach mit jeder der ebenfalls ganzzahligen und voneinander verschiedenen Konstanten verglichen, die Bestandteil der so genannten caseLabels im Block der switch-Anweisung sind (ein Label ist eine Art Kennung oder Markierung für einen bestimmten Programmpunkt). Stimmt der Wert von ausdruck mit dem Wert einer der case-Konstanten überein, wird das Programm mit den Anweisungen fortgesetzt, die unmittelbar auf das entsprechende case-Label folgen, wobei es auch möglich ist, dass ein Label keine Anweisungen besitzt. Gibt es keine Übereinstimmung von ausdruck mit einer der case-Konstanten, werden die Anweisungen nach dem default-Label ausgeführt, sofern ein solches überhaupt in der betreffenden switch-Anweisung enthalten ist (Die eckigen Optionalitäts-Klammern in der Syntax bedeuten, dass etwas nicht zwingend vorgeschrieben ist. Jede switch-Anweisung darf maximal einen default-Zweig besitzen, der nicht unbedingt als letzter Zweig notiert werden muß, sondern an einer beliebigen Position im switch-Block stehen kann). Stimmt ausdruck mit keiner der case-Konstanten überein und gibt es keinen default-Zweig, wird keine der im switch-Block enthaltenen Anweisungen ausgeführt und das Programm mit der auf die switch-Anweisung folgenden Anweisung fortgesetzt. 97 Abbildung 28: Switch-Anweisung Das nachfolgende Programmfragment soll eine Anwender-Eingabe in der Variablen direction aufnehmen, die aus einem der Buchstaben l, r, o, u für die Richtungen links, rechts, oben und unten bestehen darf. Anschließend soll ausgegeben werden, welche Richtung der Anwender gewählt hat: 98 5.10.2.2 Die Wiederholungsanweisungen Die Notwendigkeit, bestimmte Verarbeitungsschritte mehrfach hintereinander auszuführen, ergibt sich bei der Entwicklung von Programmen ausgesprochen häufig. Ein Programm soll z.B. bis 100 zählen und dabei jeden gezählten Wert ausgeben. Natürlich denkt man bei derartigen Aufgaben gewöhnlich nicht daran, einhundert einzelne Anweisungen zu codieren, sondern vertraut vielmehr stillschweigend darauf, dass die gewählte Programmiersprache für solche Fälle etwas Passendes bereithält. In der Tat verfügen Java und C++ über drei Wiederholgungs- oder Iterationsanweisungen: die while-Schleife die for-Schleife die do while-Schleife Mit diesen Schleifen lassen sich eine oder mehrere Anweisungen beliebig oft ausführen, ohne das man diese Anweisungen jedes Mal neu hinschreiben muss. Wiederholungsanweisungen werden aufgrund ihres zirkulären Ablaufs auch als Schleifen (loops) bezeichnet. Allen drei Schleifentypen in Java und C++ ist gemeinsam, dass die (wiederholte) Ausführung von Anweisungen ähnlich wie bei den Auswahlanweisungen vom Erfülltsein einer Bedingung abhängt. Die while-Schleife Die while-Anweisung hat die folgende Syntax: while (ausdruck) anweisung Sollen mehrere Anweisungen innerhalb einer while-Schleife wiederholt werden, wird wie üblich mit geschweiften Klammern ein Block gebildet: while (ausdruck) { anweisung1 anweisung2 anweisung3 anweisung4 } Die while-Anweisung führt die in ihr enthaltenen Anweisungen aus, wenn die Auswertung von ausdruck, also die Bedingung für die Ausführung der Schleife TRUE ist. Anschließend wird ausdruck erneut überprüft und die Ausführung der Anweisungen wiederholt, wenn ausdruck immer noch TRUE ist. 99 Dieses Prozedere von Überprüfen der Laufbedingung für die Schleife und anschließendem Ausführen der in der Schleife enthaltenen Anweisungen wird so lange fortgesetzt, bis die Auswertung von ausdruck nicht mehr TRUE zurückliefert. Liefert die Auswertung der Schleifenbedingung FALSE zurück, werden die Anweisungen in der Schleife nicht (mehr) ausgeführt. Ergibt bereits die erste Auswertung der Schleifenbedingung den Wert FALSE, werden die Anweisungen in der Schleife überhaupt nicht ausgeführt! Die while-Schleife ist also eine so genannte abweisende Schleife, da evtl. die in Ihr enthaltenen Anweisungen kein einziges Mal ausgeführt werden, wenn die Bedingung nicht erfüllt ist (siehe Abbildung 29). Das nachfolgende Programmstück erzeugt die Ausgabe: 1 2 3 4 5 6 7 8 9 10: Dies kommt auf die folgende Weise zustande: Da i zunächst den Wert 1 hat, ist die Schleifenbedingung erfüllt (TRUE) und die Anweisungen im Rumpf der Schleife werde ausgeführt. Das Resultat ist die Ausgabe des aktuellen Werts von i und die Inkrementierung der Variablen auf den Wert 2. Da 2 kleiner ist als 11, ergibt auch die erneute Auswertung der Schleifenbedingung TRUE. Nach diesem Prinzip wird die Schleife insgesamt zehnmal durchlaufen. Danach hat i aufgrund der Inkrementierung im 10. Durchgang den Wert 11 und die nächste Auswertung der Schleifenbedingung ergibt daher FALSE, da i nun nicht mehr kleiner ist als 11. Als Folge davon werden die Anweisungen des Schleifenrumpfs nicht mehr ausgeführt, die Schleife wird beendet und das Programm fährt mit der auf die whileSchleife folgenden Anweisung fort. 100 Abbildung 29: While-Schleife Die for-Schleife Die for-Anweisung hat die folgende Syntax: for (initialisierung;abbruchbedingung;inkrementierung) anweisung Soll mehr als eine Anweisung im Schleifenrumpf iteriert werden, wird wie üblich mit geschweiften Klammern ein Block gebildet: 101 for (initialisierung;abbruchbedingung;inkrementierung) { anweisung1 anweisung2 anweisung3 anweisung4 ... } Die for-Schleife in Java und C++ kann auch als so genannte Zählschleife bezeichnet werden. Sie wird meist verwendet, wenn die Anzahl der Schleifendurchläufe im Vorhinein feststeht, wobei man eine so genannte Laufvariable verwendet, um die Anzahl der Schleifendurchläufe zu zählen. Die for-Schleife besteht aus folgenden vier Teilen: 1. aus einem Initialisierungsteil, der vor Betreten der Schleife ausgeführt wird und in dem die Laufvariable(n) einen Wert bekommt(en); 2. aus einer Abbruchbedingung, die jedes Mal vor Betreten der Schleife geprüft wird; 3. aus einem Inkrementierungsteil, der am Ende jedes Schleifendurchlaufs ausgeführt wird und zum Beispiel die Laufvariable erhöhen kann; 4. aus dem Schleifenrumpf. Abbildung 30: For-Schleife Das folgende Programmstück erzeugt die Ausgabe 12 17 76 99: 102 Das kommt folgendermaßen zustande: i wird zunächst mit dem Wert 0 initialisiert. Anschließend wird in der Bedingung überprüft, ob i kleiner ist, als die Länge des Arrays a (Länge = 4). Ergibt die Auswertung der Bedingung TRUE, wird die Anweisung im Schleifenrumpf ausgeführt, d.h. der Wert an der Indexposition i des Arrays wird ausgegeben und i wird inkrementiert (im Schleifenkopf). Nach diesem Prinzip wird die Schleife insgesamt 4x durchlaufen. Danach hat i aufgrund der Inkrementierung den Wert 4, ist damit also nicht mehr kleiner als die Länge des Arrays und somit ergibt die darauf folgende Auswertung der Schleifenbedingung FALSE. Als Folge davon werden die Anweisungen des Schleifenrumpfs nicht mehr ausgeführt, die Schleife endet und das Programm fährt mit der auf die forAnweisung folgenden Anweisung fort. Die do while-Schleife Im Unterschied zu den beiden anderen Wiederholungsanweisungen führt die die do while-Schleife zunächst die in ihr enthaltenen Anweisungen aus und wertet dann erst die Schleifen-Bedingung aus! Die do while-Schleife ist also eine so genannte annehmende Schleife. Sie hat die folgende Syntax: do anweisung while (ausdruck) Sollen mehrere Anweisungen in der Schleife ausgeführt werden, müssen diese mit geschweiften Klammern in einem Block geklammert werden: do { anweisung1 103 anweisung2 anweisung3 anweisung4 ... } while (ausdruck) Der Umstand, dass ausdruck (die Bedingung der Schleife) erst am Ende der do while-Schleife ausgewertet wird, hat zur Folge, dass die Anweisung(en) in der Schleife - im Unterschied zur for- oder while-Schleife - in jedem Fall mind. einmal ausgeführt werden, unabhängig davon, ob ausdruck, d.h. die Bedingung TRUE ist oder nicht. Ergibt die Überprüfung von ausdruck den Wert TRUE, werden die Anweisungen in der Schleife erneut ausgeführt. Ist der Wert von ausdruck FALSE, endet die Schleife und das Programm fährt mit der auf die do while-Schleife folgenden Anweisung fort. Abbildung 31: Do-While-Schleife Das folgende Programmstück nimmt zunächst im do-Block in der Variablen x eine Benutzereingabe auf, addiert dann die Variablen x und y und speichert das Ergebnis in der Variablen x: 104 Anschließend wird überprüft, ob der Wert von x ungleich 0 ist. Ist dies der Fall, wird der do-Block erneut ausgeführt. Nach diesem Prinzip wird der do-Block solange ausgeführt, bis der Benutzer den Wert 0 eingibt. Dann ist x = 0 und die darauf folgende Auswertung der Schleifenbedingung ergibt FALSE. Die Schleife endet und das Programm fährt mit der auf die do while-Schleife folgenden Anweisung fort. 5.10.2.3 Sprunganweisungen Sprunganweisungen verlagern die Ausführungskontrolle in einem Programm von einer Stelle an eine andere (ohne dass dazu eine besondere Bedingung erfüllt sein müßte). Die Sprache C++ verfügt über vier Sprunganweisungen, zu denen die break-, die continue-, die goto- und die return-Anweisung gehören. In Java existiert die goto-Anweisung nicht. Mit Ausnahme der return-Anweisung führen Sprunganweisungen nur einen Transfer der Ausführungskontrolle innerhalb ein- und derselben Funktion durch. Die return-Anweisung überträgt die Kontrolle von einer aufgerufenen Funktion zurück an die aufrufende Funktion (oder das Betriebssystem). Wir werden im folgenden exemplarisch lediglich die break- und die continueAnweisung betrachten. Die break-Anweisung Die break-Anweisung kann ausschließlich innerhalb einer switch-Anweisung oder innerhalb einer der drei Schleifenarten auftreten und hat die folgende Syntax: break; Innerhalb eines case-Zweigs einer switch-Anweisung bewirkt die break-Anweisung das sofortige Ende der switch-Anweisung und den Transfer der Ausführungskontrolle zu der auf die switch-Anweisung unmittelbar folgende Anweisung. Eine break-Anweisung in einer Schleife beendet die Schleife und übergibt die Programmkontrolle an die Anweisung, die unmittelbar auf die Schleife folgt. 105 In dem obigen Beispiel zur switch-Anweisung bewirkt die break-Anweisung, dass nach der Ausführung einer case-Anweisung die switch-Anweisung verlassen wird, ohne die darauf folgenden case-Klauseln auszuführen. Die continue-Anweisung Die continue-Anweisung hat die folgende Syntax: continue; Sie kann ausschließlich in Wiederholungsanweisungen (Schleifen) verwendet werden. Sie bewirkt dort den Abbruch eines einzelnen Schleifendurchgangs (der unmittelbar umgebenden Schleife). In einer while- oder do while-Anweisung fährt die Programmausführung danach mit dem nächsten Schleifendurchgang fort, d.h. genauer: mit der erneuten Überprüfung der Schleifenbedingung. In einer for-Schleife wird zuvor noch der Inkrementierungsteil ausgeführt. while (ausdruck_1) { if (ausdruck_2) continue; } 5.11 Datenstrukturen In der Anfangszeit des Programmierens kam den verwendeten Daten nur eine zweitrangige Bedeutung zu. Der Schwerpunkt lag auf der Formulierung sauberer Algorithmen. Daten waren schlicht Bitfolgen, die von dem Algorithmus manipuliert wurden. Ein Programmierer hat aber stets eine Interpretation der Bitfolgen, d.h. eine bestimmte Abstraktion im Auge: Gewisse Bitfolgen entsprechen Zahlen, andere stellen Wahrheitswerte, Zeichen oder Zeichenketten dar. Die ersten erfolgreichen höheren Programmiersprachen gestatteten daher eine Typdeklaration. Variablen konnten als Integer, Boolean, usw. deklariert werden. Der Compiler reservierte automatisch den benötigten Speicherplatz und überprüfte auch noch, dass die auf die Daten angewandten Operationen dem Datentyp angemessen waren. Eine Addition etwa einer Integer-Größe zu einem Wahrheitswert wurde als fehlerhaft zurückgewiesen. Dies erleichterte es dem Programmierer, Denkfehler früh zu erkennen und zu beheben. 106 Als Programme größer und unübersichtlicher wurden und daher möglichst in einzelne Teile zerlegt werden sollten, erschien es sinnvoll, Daten und Operationen, die diese Daten manipulieren, als Einheit zu sehen und auch als abgeschlossenen Programmteil formulieren und compilieren zu können. Ein solcher Programmteil, im Allgemeinen Modul genannt, konnte von einem Programmierer erstellt werden und dann anderen in compilierter Form zur Verfügung gestellt werden. Es stellt sich die Frage, was man den Benutzern des Moduls mitteilen soll. Damit diese einen Gewinn aus der geleisteten Arbeit ziehen können, sollten sie nicht mit den Interna des programmierten Moduls belästigt werden, sie sollten nur wissen, was man damit anfangen kann. Ein konkretes Beispiel wären Kalenderdaten: Ein Benutzer sollte lediglich wissen, wie man damit umgehen kann, nicht aber, wie die Kalenderdaten intern repräsentiert sind. Aus der Kenntnis der Repräsentation könnte sogar eine Gefahr erwachsen: Eine Änderung des Moduls könnte Programmen, die von einer speziellen internen Repräsentation ausgehen, den Garaus machen. Die Forderung, dem Anwender das (und nur das) mitzuteilen, was das Modul als Funktionalität anbietet, nicht aber Interna der Implementierung, wird in dem Schlagwort "information hiding" (oder Kapselung von Information) zusammengefasst. In einer neueren Programmiersprache wie Java werden Datenstrukturen und darauf operierende Methoden in Modulen zusammengefasst, die als Klassen bezeichnet werden. Als Datenstruktur definieren wir demnach nicht nur die Daten selbst, sondern auch die Operationen, die darauf ausgeführt werden können. Im Folgenden werden wir uns mit einigen der wichtigsten Datenstrukturen befassen. 5.11.1 Stacks Ein Stack ist ein abstrakter Datentyp, bei dem Elemente eingefügt und wieder entfernt werden können. Derartige Datentypen, die als Behälter für Elemente dienen, fasst man oft unter Oberbegriffen wie Container oder Collection zusammen. Unter den Behälter-Datentypen zeichnen sich Stacks dadurch aus, dass immer nur auf dasjenige Element zugegriffen werden kann, das als letztes eingefügt wurde. Dafür gibt es das Schlagwort: LIFO = Last-In First-Out. 107 Das englische Wort Stack kann man mit Stapel übersetzen. Dabei liegt es nahe, an einen Stapel von Tellern oder Tabletts zu denken (--> z.B. die "Tellerspender" in der Mensa) bei dem man auch immer nur auf das oberste Element zugreifen kann dasjenige also, welches als letztes auf den Stapel gelegt wurde. In einem Stack s sind Elemente x eines beliebigen, aber festen Datentyps gespeichert. "Speichern" heißt hier Ablegen auf dem Stack und "Entfernen" heißt Entnehmen des obersten Elements. Stackoperationen Die fundamentalen Stackoperationen, die auf einem Stack s angewendet werden können, sind (die Bezeichnungen für diese Operationen können je nach Programmiersprache variieren!): push(x,s) legt ein Element x auf den Stack s top(s) liefert das zuletzt auf den Stack s gelegte Element pop(s) entfernt das zuletzt auf den Stack s gelegte Element isEmpty(s) gibt an, ob der Stack s leer ist Abbildung 32: Stack und Stack-Operationen Anwendungsbeispiel für einen Stack Ein wichtiges Anwendungsbeispiel für die Datenstruktur Stack ist die Auswertung von arithmetischen Ausdrücken. 108 Jeder arithmetische Ausdruck in normaler Schreibweise kann in eine PostfixNotation umgewandelt werden, bei der sich die Operatoren stets rechts von den Operanden befinden. Dabei kommt man gänzlich ohne Klammern aus. Bei einer Auswertung von links nach rechts bezieht sich ein Operator immer auf die unmittelbar links von ihm stehenden Werte. Beispielsweise wird aus dem arithmetischen Ausdruck (2+4)2/(16-7) der PostfixAusdruck: 2 4 + 2 16 7 - / Dieser kann mittels eines Stacks von links nach rechts abgearbeitet werden: Ist das gelesene Datum ein Operand, so wird es mit push auf den Stack gelegt. o Ist das gelesene Datum ein n-stelliger Operator, dann wird er auf die obersten n Elemente des Stacks angewandt. Das Ergebnis ersetzt diese n Elemente. o Abbildung 33 zeigt die 9 Schritte zur Berechnung des obigen Ausdrucks: Abbildung 33: Beispiel für Stack-Operationen Jeder Compiler wandelt Ausdrücke bei der Compilierung in Postfix-Ausdrücke um. 109 5.11.2 Queues Ähnlich einem Stack ist eine Queue ein Behälter, in den Elemente eingefügt und nur in einer bestimmten Reihenfolge wieder entnommen werden können. Einfügung eines Elements - "enQueue" - erfolgt an einem, Entfernung - "deQueue" - an dem anderen Ende der Queue. Dies bewirkt, dass man immer nur auf das Element zugreifen kann, das am längsten im Behälter ist (FIFO = First in-First Out). Abbildung 34: Queue Anwendung von Queues Queues werden oft eingesetzt, um ansonsten unabhängige Prozesse miteinander kommunizieren zu lassen, bzw. um kooperierende Prozesse zu entkoppeln. Üblicherweise produziert ein Prozess eine Folge von Daten, während der zweite Prozess diese Daten entgegennimmt und weiter verarbeitet. Der erste Prozess heißt dann Erzeuger oder Produzent (engl. producer) und der zweite heißt Verbraucher oder Konsument (engl. consumer). Man verwendet eine Queue, in die der Produzent die erzeugten Daten ablegt (enQueue) und aus dem der Konsument die benötigten Daten entnimmt (deQueue). Die Queue dient der zeitlichen Entkopplung - der Erzeuger kann weiter produzieren, auch wenn der Verbraucher die Daten noch nicht alle entgegengenommen hat, und der Verbraucher hat Daten, mit denen er weiterarbeiten kann, auch wenn der Erzeuger für die Produktion gewisser Daten gelegentlich eine längere Zeit benötigt. Beispiele für eine solche Erzeuger-Verbraucher Situation sind: Die Druckerwarteschlange unter Windows ist eine Queue für Druckaufträge. Erzeuger sind die Druckprozesse unterschiedlicher Programme (im Netzwerk) - Verbraucher ist der Druckertreiber. Die Queue dient hier zur zeitlichen Entkopplung. 110 Ähnlich legt der Festplatten-Controller als Produzent die gelesenen Daten in einer Queue ab - dort kann das Betriebssystem sie abholen. Beim Schreiben auf die Platte vertauschen Produzent und Konsument die Rollen. Queues können auch zur logischen Entkopplung von Prozessen dienen. So gibt es z.B. in UNIX eine Sammlung nützlicher kleiner Programme, welche einen Input in einen Output transferieren. Diese Tools nennt man Pipes. Mithilfe einer Pipe, die nichts anderes ist, als eine Queue, kann man die Ausgabe eines Programms mit der Eingabe des nächsten verknüpfen. 5.11.3 Listen Eine Liste ist eine Folge von Elementen, in der an beliebiger Stelle neue Elemente eingefügt oder vorhandene Elemente entfernt werden können. Im Gegensatz dazu darf man bei Stacks und Queues nur am Anfang oder am Ende Elemente einfügen oder entnehmen. Listen sind also allgemeinere Datenstrukturen als Stacks und Queues, in der Tat kann man sowohl Stacks als auch Queues mithilfe von Listen realisieren. Die Spezifikation eines abstrakten Datentyps Liste bietet viele Variationsmöglichkeiten. Es stellen sich unter anderem folgende Fragen: Welche Operationen sollen dazugehören? Wie sollen wir uns auf bestimmte Elemente in der Liste beziehen? Wir entscheiden uns hier dafür, Listenelemente anhand ihrer Position in der Liste anzusprechen - wenn die Liste n Elemente hat, dann nummerieren wir die Elemente von 1 bis n durch. Dazu benötigen wir: eine Operation laenge, um festzustellen, wie viele Elemente die Liste L hat, eine Methode Inhalt, die zu einer gültigen Position p mit 1 <= p <= laenge(L) den Inhalt des Elements an der Position p findet und eine Operation suche, die die Position p eines Elements mit Inhalt e bestimmt, sofern es denn ein solches gibt. Einfach verkettete Listen Wir implementieren eine Liste als eine Folge von Elementen. Jedes Element wird zum Teil einer Kette. Zu diesem Zweck trägt jedes Element nicht nur einen Inhalt e, sondern auch einen Zeiger next, der auf das folgende Element der Liste verweist. Manch einem mag die Analogie eines Elements zu einem Eisenbahnwaggon hilfreich 111 sein. Jeder Waggon hat einen Inhalt und eine Kupplung, an die der nächste Waggon gehängt werden kann. Auf diese Weise lassen sich beliebig lange Züge - Listen - zusammenstellen: Abbildung 35: Liste Um die ganze Liste inspizieren zu können, müssen wir nur ihr erstes Element finden. Dessen next-Zeiger führt uns auf das folgende Element, dessen next-Zeiger wiederum auf das dritte und so weiter. Das letzte Glied der Kette erkennen wir daran, dass sein next-Zeiger auf null verweist. Von einem Element aus kann man die Liste immer nur den next-Zeigern folgend in einer Richtung durchlaufen. Um danach wieder in die Liste einsteigen zu können, benötigen wir einen festen Zeiger anfang auf das erste Element der Liste. Hilfszeiger auf innere Elemente nennt man Cursor. Viele Operationen auf Listen beginnen an dem durch einen Cursor bezeichneten Element. Abbildung 36: Einfach verkettete Liste Doppelt verkettete Listen In einer einfach verketteten Liste sind die Elemente direkt nach dem Cursor einfach erreichbar. Will man allerdings auf das Element vor dem Cursor zugreifen, so muss die Liste vom Anfang her komplett durchlaufen werden! Durch die Richtung der Verkettung ergibt sich eine Asymmetrie im Aufwand des Listendurchlaufes. Spendiert man jedem Element noch einen Zeiger auf seinen Vorgänger, so stellt sich für den Aufwand des Listendurchlaufs die Symmetrie wieder her. Eine solche Implementierung (mit oder ohne Cursor) nennt man doppelt verkettete Liste. 112 Abbildung 37: Doppelt verkettete Liste 5.12 Bäume Bäume gehören zu den fundamentalen Datenstrukturen der Informatik. In gewisser Weise kann man sie als mehrdimensionale Verallgemeinerung von Listen auffassen. In Bäumen kann man nicht nur Daten, sondern auch relevante Beziehungen der Daten untereinander, wie Ordnungs- oder hierarchische Beziehungen, speichern. Daher eignen sich Bäume besonders, gesuchte Daten rasch wieder aufzufinden. Ein Baum besteht aus einer Menge von Knoten (Punkten), die untereinander durch Kanten (Pfeile) verbunden sind. Führt von Knoten A zu Knoten B eine Kante, so schreiben wir dies als A --> B und sagen A ist Vater/Mutter von B oder B ist Sohn/Tochter von A. Einen Knoten ohne Söhne nennt man ein Blatt oder terminalen Knoten. Alle anderen Knoten heißen innere Knoten. Ein Pfad von A nach B ist eine Folge von Knoten und Kanten, die von A nach B führen: A --> X1 --> X2 --> ... --> B Die Länge des Pfades definieren wir als die Anzahl der Knoten (0 oder mehr). Gibt es einen Pfad von A nach B, so ist B ein Nachkomme von A und A ein Vorfahre von B. Ein Baum muss folgende Bedingung erfüllen: Es gibt genau einen Knoten ohne Vater. Dieser Knoten heißt Wurzel (root). Jeder andere Knoten ist ein Nachkomme der Wurzel und hat genau einen Vater. 113 Weitere wichtige Eigenschaften eines Baumes ergeben sich hieraus automatisch. Dazu gehören: o o o Es gibt keinen zyklischen Pfad. Von der Wurzel gibt es zu jedem anderen Knoten genau einen Pfad. Die Nachkommen eines beliebigen Knotens K zusammen mit allen ererbten Kanten bilden wiederum einen Baum, den Unterbaum mit Wurzel K. Letzteres ist eine wichtige kennzeichnende Eigenschaft, die zeigt, dass Bäume rekursiv definiert werden können: Rekursive Baumdefinition: Ein Baum ist leer oder er besteht aus einer Wurzel W und einer leeren oder nichtleeren Liste B1, B2, ..., Bn von Bäumen. Von W zur Wurzel Wi von Bi führt jeweils eine Kante. Hinweis: Ein Objekt (oder eine Definition) heißt rekursiv, wenn es sich selbst als Teil enthält oder mithilfe von sich selbst definiert ist. Da Bäume hierarchische Strukturen sind, lässt sich jedem Knoten eine Tiefe zuordnen. Diese definieren wir als die Länge des Pfades von der Wurzel zu diesem Knoten. Die Tiefe eines Baumes definieren wir als 0, falls es sich um einen leeren Baum handelt, andernfalls als das Maximum der Tiefen seiner Knoten. Abbildung 38: Baum als Datenstruktur Beispiele für Bäume Viele natürliche hierarchische Strukturen sind Bäume. Dazu gehören unter anderem: 114 Stammbäume Knoten sind Frauen, Kanten führen von einer Mutter zu jeder ihrer Töchter. Die Wurzel (im abendländischen Kulturkreis) ist Eva. Dateibäume Knoten sind Dateien oder Verzeichnisse, Kanten führen von einem Verzeichnis zu dessen direkten Unterverzeichnissen oder Unterdateien. Die Wurzel heißt häufig C: oder "/". Listen Listen sind Bäume, bei denen jeder Knoten höchstens einen Nachfolger hat. Bäume stellt man gewöhnlich grafisch dar, indem jeder Knoten durch einen Kreis und jede Kante durch eine Linie dargestellt wird. Dabei wird ein Vater-Knoten immer über seinen Söhnen platziert, so dass die Wurzel der höchste Punkt ist. 5.12.1 Binärbäume Im Allgemeinen können Baumknoten mehrere Söhne haben. Ein Binärbaum ist dadurch charakterisiert, dass jeder Knoten genau zwei Söhne hat. Eine rekursive Definition ist: Ein Binärbaum ist leer oder besteht aus einem Knoten - Wurzel genannt - und zwei Binärbäumen, dem linken und dem rechten Teilbaum. Abbildung 39: Binärbaum (I) Binärbäume sind also 2-dimensionale Verallgemeinerungen von Listen. Ähnlich wie die Elemente einer Liste kann man in den Knoten eines Baumes beliebige Informationen speichern. Im Unterschied zu den Listenelementen enthält ein Knoten eines Binärbaumes zwei 115 Zeiger, einen zum linken und einen zum rechten Teilbaum. Abbildung 40: Binärbaum (II) Eine wichtige Anwendung von Bäumen, insbesondere auch von Binärbäumen, ist die Repräsentation von arithmetischen Ausdrücken. Innere Knoten enthalten Operatoren, Blätter enthalten Werte oder Variablennamen. Einstellige Operatoren werden als Knoten mit nur einem nichtleeren Teilbaum repräsentiert. In der Baumdarstellung sind Klammern und Präzedenzregeln überflüssig. Erst wenn wir einen "zweidimensionalen" Baum eindimensional (als String) darstellen wollen, sind Klammern notwendig. Beachten Sie, dass das Vorzeichen "-" und das Quadrieren einstellige Operatoren sind. Im Baum stellen wir sie durch "+/-" bzw. ()2 dar: Abbildung 41: Binärbaum zur Repräsentation eines arithmetischen Ausdruckes 116 5.12.2 Traversierungen Listen konnten wir auf natürliche Weise von vorne nach hinten durchlaufen - bei Bäumen können wir ähnlich einfach von der Wurzel zu jedem beliebigen Knoten gelangen, sofern wir seine "Baumadresse" kennen. Um den Vorgänger eines Elementes e in einer Liste zu finden, mußten wir am Anfang einsteigen und nach rechts laufen, bis wir zu einem Element z gelangten für das galt: z.next = e. Um in einem Baum den Vater eines bestimmten Knotens zu finden, haben wir es schwerer - wir müssen an der Wurzel einsteigen und in jedem Schritt raten, ob wir nach rechts oder nach links gehen sollen. Schlimmstenfalls müssen wir den ganzen Baum durchsuchen. Spätestens hier erhebt sich die Frage, wie wir systematisch alle Knoten eines Baumes durchlaufen - oder traversieren - können. Traversieren bedeutet, jeden Knoten eines Baumes systematisch zu besuchen. Diese Operationen sind für lineare Listen aufgrund ihrer Definition trivial, doch für Bäume gibt es eine Reihe verschiedener Möglichkeiten. Diese unterscheiden sich vor allem hinsichtlich der Reihenfolge, in der die Knoten besucht werden. Die wichtigsten Vorgehensweisen sind Pre-Order, Post-Order, InOrder und Level-Order, auf die hier aber nicht detailliert eingegangen werden soll. 5.13 Suchalgorithmen Gegeben sei eine Sammlung von Daten. Wir suchen nach einem oder mehreren Datensätzen mit einer bestimmten Eigenschaft. Dieses Problem stellt sich zum Beispiel, wenn wir im Telefonbuch die Nummer eines Teilnehmers suchen. Zur raschen Suche nutzen wir aus, dass die Einträge geordnet sind, z.B. nach Name, Vorname, Adresse. Wenn wir Namen und Vornamen wissen, finden wir den Eintrag von Herrn Sommer sehr schnell durch binäres Suchen: Dazu schlagen wir das Telefonbuch in der Mitte auf und vergleichen den gesuchten Namen mit einem Namen auf der aufgeschlagenen Seite. Ist dieser zufällig gleich dem gesuchten Namen, so sind wir fertig. Ist er in der alphabetischen Ordnung größer als der gesuchte Name, brauchen wir für den Rest der Suche nur noch die erste Hälfte des Telefonbuchs zu berücksichtigen. Wir verfahren danach weiter wie vorher, schlagen also bei der Mitte der ersten Hälfte auf und vergleichen wieden den gesuchten mit einem gefundenen Namen und so fort. 117 Dieser Algorithmus heißt binäre Suche. Bei einem Telefonbuch mit ca. 1000 Seiten Umfang kommen wir damit nach höchstens 10 Schritten zum Ziel, bei einem Telefonbuch mit 2000 Seiten nach 11 Schritten. Noch schneller geht es, wenn wir die Anfangsbuchstabenmarkierung auf dem Rand des Telefonbuchs ausnutzen. Wenn wir umgekehrt eine Telefonnummer haben und mithilfe des Telefonbuchs herausbekommen wollen, welcher Teilnehmer diese Nummer hat, bleibt uns nichts anderes übrig, als der Reihe nach alle Einträge zu durchsuchen. Diese Methode heißt lineare Suche. Bei einem Telefonbuch mit 1000 Nummern müssen wir im Schnitt 500 Vergleiche durchführen, im schlimmsten Fall sogar alle 1000. Müssen wir diese Art von Suche öfters durchführen, so empfiehlt sich eine zusätzliche Sortierung (einer Kopie) des Telefonbuchs nach der Rufnummer, so dass wir wieder binär suchen können. 5.13.1 Lineare Suche Allgemein lässt sich das Suchproblem wie folgt formulieren: Suchproblem: In einem Behälter A befindet sich eine Reihe von Elementen. Prüfe, ob Element e in A existiert, das eine bestimmte Eigenschaft p(e) besitzt. "Behälter" steht hier allgemein für Strukturen wie: Arrays, Dateien, Mengen, Listen, Stacks, Queues, etc. Wenn nichts näheres über die Struktur des Behälters oder die Platzierung/Sortierung der Elemente bekannt ist, dann müssen wir den folgenden Algorithmus anwenden: Entferne der Reihe nach Elemente aus dem Behälter, bis dieser leer ist oder ein Element mit der gesuchten Eigenschaft gefunden wurde. Schlimmstenfalls müssen wir den ganzen Behälter durchsuchen, bis wir das gewünschte Element finden. Hat dieser N Elemente, so werden wir bei einer zufälligen Verteilung der Daten erwarten, nach ca. N/2 Versuchen das gesuchte Element gefunden zu haben. In jedem Fall ist die Anzahl der Zugriffe proportional zur Elementzahl. Arrays als Behälter werden in den folgenden Such- und Sortier-Algorithmen eine besondere Rolle spielen. In Java ist die Indexmenge eines Arrays a mit n = a.length Elementen stets das Intervall [0...n-1]. Besonders für die rekursiven Algorithmen wird es sich als günstig herausstellen, wenn wir sie so verallgemeinern, dass sie nicht nur ein ganzes Array, sondern auch einen Abschnitt (engl. slice) eines Arrays sortieren können. 118 5.13.2 Binäre Suche Wenn auf dem Element-Datentyp eine Ordnung definiert ist und die Elemente entsprechend ihrer Ordnung in einem Array gespeichert sind, dann nennt man das Array geordnet oder sortiert. Für die Suche in solchen sortierten Arrays können wir die binäre Suche einsetzen, wie wir sie aus dem Telefonbuch-Beispiel kennen. Dazu sei x das gesuchte Element. Wir suchen also nach einem Index i mit a[i] = x. Wie bei der Namenssuche im Telefonbuch wollen wir den Bereich, in dem sich das gesuchte Element noch befinden kann, in jedem Schritt auf die Hälfte verkleinern. Dazu verallgemeinern wir das Problem dahingehend, dass wir i in einem beliebigen Indexbereich [min...max] des Arrays a suchen, angefangen mit min = 0 und max = n-1. Mit anderen Worten: Wenn das gesuchte Element x überhaupt in dem Array vorhanden ist, dann muss es (auch) im Abschnitt [min...max] zu finden sein. Davon ausgehend, dass das gesuchte Element überhaupt in a enthalten ist, wählen wir im ersten Schritt einen Index m, mit min ≤ m ≤ max. Anschließend gehen wir folgendermaßen vor: Falls x = a[m] gilt, sind wir fertig; Wir geben m als Ergebnis zurück. falls x < a[m], suche weiter im Bereich min...m-1, setze also max = m-1 falls x > a[m], suche weiter im Bereich m+1...max, setze also min = m + 1 Für den Index m zwischen min und max nimmt man am besten einen Wert nahe der Mitte, also z.B. m = (min + max) / 2. Auf diese Weise halbiert sich in jedem Schritt der noch zu betrachtende Bereich, und damit der Aufwand für die Lösung des Problems. Wenn der Bereich min...max aus n Elementen besteht, können wir den Bereich höchstens log2(n)-mal halbieren. Um 1000 Elemente zu durchsuchen, genügen also log2(1000) = 10 Vergleiche. Wir illustrieren die Methode mit 19 Elementen: Abbildung 42: Binäre Suche 119 5.14 Komplexität von Algorithmen Unter der Komplexität eines Algorithmus versteht man grob seinen Bedarf an Ressourcen in Abhängigkeit vom Umfang der Inputdaten. Die wichtigsten Ressourcen sind dabei die Laufzeit und der Speicherplatz. Lineare Suche Wenn wir davon ausgehen, dass die lineare Suche mit einer while-Schleife bewerkstelligt wird, hängt der Zeitbedarf im Wesentlichen davon ab, wie oft die whileSchleife durchlaufen wird. Dabei nehmen wir an, dass das gesuchte Element vorhanden ist, und unterscheiden drei mögliche Fälle: Best case: Im günstigsten Fall wird ein Element e mit p(e) beim ersten Versuch gefunden. Die Schleife wird nur einmal durchlaufen. Average case: Im Schnitt kann man davon ausgehen, dass das Element etwa nach der halben Maximalzahl von Schleifendurchläufen gefunden wird. Worst case: Im schlimmsten Fall wird das Element erst beim letzten Versuch oder überhaupt nicht gefunden. Die Maximalzahl der Schleifendurchläufe ist durch die Anzahl der Elemente begrenzt. Binäre Suche Bei der binären Suche benötigen wir im worst case log2(n) Schleifendurchläufe, bzw. rekursive Aufrufe - im average case kann man sich überlegen, dass man im vorletzten Schritt, also nach log2(n)-1 Schritten erwarten kann, das Element zu finden. Wir können die Anzahl der Schleifendurchläufe also folgendermaßen tabellieren: lineare Suche binäre Suche Best Case 1 1 Average Case n log2(n)-1 Worst Case n log2(n) Der genaue Zeitaufwand im worst case für die lineare Suche in einem Behälter mit n Elementen setzt sich zusammen aus einer Initialisierungszeit CI und aus dem Zeitbedarf für die while-Schleife, den wir mit Cw x n ansetzen können, wobei Cw die Zeitdauer eines Schleifendurchlaufes bedeutet. Wir erhalten für den Zeitbedarf tL(n) der linearen Suche also die Formel: tL(n) = CI + CW * n Zum Vergleich berechnen wir den Zeitbedarf tB(n) für die binäre Suche. Auch hier haben wir eine konstante Initialisierungszeit KI und eine konstante Zeit KW für jeden Schleifendurchlauf, bzw. für jeden rekursiven Aufruf. Dies ergibt: tB(n) = KI + KW * log2(n) 120 Offensichtlich ist die binäre Suche schneller - egal welchen Wert die einzelnen Konstanten haben. Wenn n nur groß genug ist, wird tL(n) größer als tB(n) sein. Allgemein interessiert uns beim Laufzeitvergleich verschiedener Algorithmen nur das Verhalten "für große" n. Auf diese Weise können wir die Güte von Algorithmen beurteilen, ohne die genauen Werte der beteiligten Konstanten zu kennen. 5.15 Sortieralgorithmen Viele Sortieralgorithmen übernehmen Strategien, die Menschen bereits im täglichen Leben anwenden - zum Beispiel beim Sortieren von Spielkarten. Wenn wir Kartenspieler beim aufnehmen einer "Hand" beobachten, können wir unter anderem folgende "Algorithmen" beobachten: Der Spieler nimmt eine Karte nach der anderen auf und sortiert sie in die bereits aufgenommenen Karten ein. Dieser Algorithmus wird InsertionSort genannt. Der Spieler nimmt alle Karten auf, macht eine Hand daraus und fängt jetzt an, die Hand zu sortieren, indem er benachbarte Karten solange vertauscht, bis alle in der richtigen Reihenfolge liegen. Dieser Algorithmus wird BubbleSort genannt. Bei Kartenspielen, bei denen die Karten zunächst aufgedeckt auf dem Tisch liegen können: Der Spieler nimmt die jeweils niedrigste der auf dem Tisch verbliebenen Karten auf und kann sie in der Hand links (oder rechts) an die bereits aufgenommenen Karten anfügen. Dieser Algorithmus wird SelectionSort genannt. Die oben genannten Algorithmen gehören zu den einfachen Sortierstrategien. Es gibt aber noch schnellere Algorithmen, die zudem auf eleganten mathematischen Ideen beruhen. Dazu gehören HeapSort, QuickSort und MergeSort mit einer Laufzeit von n*log(n). Aufgrund der Komplexität dieser Algorithmen werden wir uns im Folgenden auf BubbleSort als Beispiel eines Sortieralgorithmus beschränken. Bei Interesse an anderen Sortieralgorithmen sei hier auf die Literaturangaben zum Thema verwiesen, besonders auf: Sedgewick, Robert: Algorithmen. AddisonWesley, 2002. 5.15.1 BubbleSort Dieser Algorithmus sortiert z.B. ein Array von Datensätzen durch wiederholtes Vertauschen von Nachbarfeldern, die in falscher Reihenfolge stehen. Dies wiederholt man so lange, bis das Array vollständig sortiert ist. Dabei wird das Array in mehreren Durchgängen von links nach rechts durchwandert. Bei jedem Durchgang werden alle Nachbarfelder verglichen und ggf. vertauscht. Nach dem 1. Durchgang hat man folgende Situation: Das größte Element ist ganz rechts. 121 Alle anderen Elemente sind zwar zum Teil an besseren Positionen (also näher an der endgültigen Position), im Allgemeinen aber noch unsortiert. Die folgende Abbildung illustriert den ersten Durchlauf: Abbildung 43: Erster Durchlauf beim Sortieralgorithmus BubbleSort Das Wandern des größten Elements ganz nach rechts kann man mit dem aufsteigen von Luftblasen in einem Aquarium vergleichen: Die größte Luftblase (engl. bubble) steigt nach oben (bzw. nach rechts ;o). Nach dem ersten Durchgang ist das größte Element also an seiner endgültigen Position. Für die restlichen Elemente müssen wir nun den gleichen Vorgang anwenden. Nach dem zweiten Durchgang ist auch das zweitgrößte Element an seiner endgültigen Position. Dies wiederholt sich für alle restlichen Elemente mit Ausnahme des letzten. In unserem konkreten Beispiel sind spätestens nach 14 Durchgängen alle Elemente an ihrer endgültigen Position, folglich ist das Array geordnet. Wenn wir uns das Ergebnis der einzelnen Durchgänge anschauen, stellen wir fest, daß der Sortiervorgang nicht erst nach dem 14ten Durchgang beendet ist, sondern bereits nach dem 10ten. Dies liegt daran, dass sich, wie oben schon erwähnt, bei jedem Durchlauf auch die Position der noch nicht endgültig sortierten Elemente verbessert. Wir können zwar den ungünstigsten Fall konstruieren, bei dem tatsächlich n-1 122 Durchgänge benötigt werden, im Allgemeinen können wir aber BubbleSort bereits nach einer geringeren Anzahl von Durchgängen abbrechen - im günstigsten Fall, wenn die Daten bereits nach dem 1. Durchgang sortiert sind. Abbildung 43: Vollständiger Durchlauf beim Sortieralgorithmus BubbleSort 123