Langsam nähern wir uns dem Ende des technischen Teils dieser Reihe. Das letzte mal wurde das Zweierkomplement erklärt und dargelegt, wie Subtraktion im Rechner funktioniert. Zusätzlich haben wir gesehen, wie ein einfaches Rechenwerk arbeitet; eine wichtige Grundlage der Funktionsweise sind hier die Steuersignale. Und deren Herkunft wollen wir heute klären.
Zuerst gibt es allerdings etwas Theorie.
Die Grundlage der Steuersignale sind sogenannte Instruktionen; eine Instruktion ist eine Bitkette, die eine bestimmte durch den Prozessor auszuführende Aufgabe codiert, zum Beispiel “addiere den Wert 5 auf den im Akkumulator gespeicherten Wert” (wir erinnern uns: der Akkumulator war ein spezielles Register, welcher Ergebnisse und Operanden für das Rechenwerk verwaltet). Eine Instruktion besteht in der Regel aus einem Opcode, welcher die eigentliche Operation (z.B. “addiere”) beschreibt, sowie einer optionalen Anzahl von Operanden (also etwa die zu addierenden Zahlen). Operanden können feste Werte sein, aber auch bestimmte Register beschreiben oder lediglich eine Adresse auf eine bestimmte Speicherzelle enthalten (dazu später noch etwas); die Interpretation der Operanden hängt immer vom Opcode ab. Die Länge einer Instruktion und der einzelnen Bestandteile muss übrigens nicht fest sein, sondern kann auch variabel sein, je nach zugrundeliegender Prozessorarchitektur.
Ein kurzes (fiktives) Beispiel: die Instruktion “0110110101” kann etwa in die Bestandteile 0110, 11 und 0101 unterteilt werden, wobei “0110” der Opcode ist und zum Beispiel die Anweisung “addiere einen festen Wert zu dem in einem Register gespeicherten Wert” codiert, “11” das zu benutzende Register (zum Beispiel den Akkumulator) beschreibt und “0101” schließlich den festen Wert, hier eine 5, codiert. Zusammengenommen ergibt die Bitkette also die Instruktion “addiere den Wert 5 auf den im Akkumulator gespeicherten Wert”.
Die Abarbeitung von Instruktionen durch das sogenannte Steuerwerk im Prozessor erfolgt bei der Von-Neumann-Architektur (welche die Grundlage der meisten modernen Rechner bildet) nach dem Von-Neumann-Zyklus. Dieser besteht üblicherweise aus 4 Phasen:
- Fetch: die nächste auszuführende Instruktion wird aus dem Speicher geladen und im sogenannten Instruktionsregister gespeichert
- Decode: die Instruktion im Instruktionsregister wird dekodiert, also grob gesagt in ihre Bestandteile “zerlegt”; konkret werden hier unter anderem die Steuersignale aus der Instruktion extrahiert
- Fetch Operands: eventuelle Operanden aus dem Speicher werden in die entsprechenden Register geladen
- Execute: die Anweisung wird ausgeführt
Soweit zur Theorie. Die klingt zwar immer recht schön, lässt aber offen, was nun in den einzelnen Phasen genau passiert. Schauen wir also einmal etwas genauer hinein; dazu benötigen wir noch einige neue Bauelemente, die jeweils an der entsprechenden Stelle näher beschrieben werden.
Phase 1: Fetch
Bevor eine Instruktion verarbeitet werden kann, muss sie erst einmal zur Verfügung stehen, da der Prozessor nur mit Werten arbeiten kann, die in einem seiner Register vorliegen. Jetzt ist die Anzahl der Register eines Prozessors in der Regel sehr begrenzt; es stehen auf keinen Fall genügend zur Verfügung, um viele Instruktionen (welche zusammen übrigens ein komplettes Programm ergeben!) aufzunehmen. Für die Speicherung von Instruktionen existiert im Prozessor ein separates Register (in modernen Rechnern sogar mehrere davon), welches Instruktionen speichert, das sogenannte Instruktionsregister. Die Instruktionen selbst stehen woanders, in der Regel im Cache oder im Arbeitsspeicher.
Ohne jetzt allzu sehr ins Detail zu gehen: der Cache ist ein kleiner, sehr schneller Speicher, der sich physikalisch in der Nähe des Prozessors (oder sogar in ihm) befindet und unmittelbar benötigte Daten aufnimmt. Je nach Nähe zum Prozessor werden verschiedene Cache-Level unterschieden; so haben moderne Prozessoren in der Regel einen Level-1-Cache, der direkt im Prozessor selber integriert ist, im gleichen Takt wie dieser läuft und eine Größe im Bereich von bis zu wenigen hundert Kilobyte hat. Dazu gibt es in der Regel noch mindestens einen Level-2-Cache außerhalb des Prozessors, der langsamer arbeitet und bis zu einigen Megabyte groß sein kann. Irgendwo weiter hinten in der Speicherhierarchie findet sich dann schließlich der Arbeitsspeicher, der langsamer als die Caches arbeitet, dafür aber auch bedeutend größer ist (momentan sind etwa 4 Gigabyte relativ üblich). Irgendwo in einem Speicher dieser Hierarchie steht auch die nächste Instruktion, die der Prozessor bearbeiten soll, und die gilt es zu finden. Hierfür steht ein separates Register zur Verfügung, der sogenannte Program Counter (PC). Dieser enthält die Adresse der nächsten Instruktion (in Form einer Bitkette), anhand derer die betreffende Instruktion im Speicher gefunden und ins Instruktionsregister geladen werden kann.
Üblicherweise hören Beschreibungen der Funktionsweise eines Prozessors an dieser Stelle auf, aber im Interesse der Vollständigkeit wollen wir hier doch einmal ein wenig tiefer in die Materie schauen; das Laden der Instruktion aus dem Speicher in ein Register ist nämlich an sich ein interessanter Vorgang.
Nehmen wir einmal an, die Instruktion wird aus dem Level-1-Cache geladen (was in der Regel auch der Fall ist). Der Cache ist aus sogenannten SRAM-Zellen aufgebaut; das sind im Grunde Flipflops mit einer speziellen Steuerelektronik. Untenstehende Abbildung (entnommen aus der Wikipedia) zeigt eine derartige Zelle, die ein einzelnes Bit speichern kann.
Die Zelle besitzt die 3 Anschlüsse WL (Word Line), BL und BL. WL dient zur Aktivierung der Zelle – solange an WL 0 anliegt, geben BL und BL eine 0 aus. Soll nun der Wert des in der Zelle gespeicherten Bits ausgelesen werden, wird WL auf 1 geschaltet, woraufhin an BL der gespeicherte Wert (0 oder 1) anliegt (an BL entsprechend der invertierte Wert). Zum Schreiben eines Wertes in die Zelle reicht es, den gewünschten Wert an BL anzulegen (an BL wiederum entsprechend den invertierten Wert) und WL anschließend wieder auf 1 zu setzen (für die technisch Versierteren: damit das funktioniert, muss die an BL/BL angelegte Spannung größer sein als die in den Transistoren gespeicherte, um den aktuellen Wert zu überschreiben). Schaltet man mehrere dieser Zellen in einer zweidimensionalen Matrix (also einem Gitter) zusammen, so kann man damit auch größere Werte speichern – und so sind auch Caches aufgebaut. Üblicherweise hat das Gitter dabei so viele Spalten, wie ein Byte Bits hat (meistens 8, aber nicht immer) und entsprechend so viele Zeilen, wie Bytes im Cache gespeichert werden sollen. Über das Ansprechen einer bestimmten Zeile kann demzufolge ein Byte aus dem Cache ausgelesen werden. Ein Cache mit 5 Bytes, die jeweils aus 4 Bit bestehen, könnte also folgendermaßen aussehen; die einzelnen Bytes werden jeweils über die Eingänge b1 bis b5 angesprochen, wobei die einzelnen Bits der Zeilen an den Ausgängen o1 bis o4 anliegen (die invertierten Ausgänge der SRAM-Zellen habe ich einmal weggelassen):
Bleibt noch zu klären, wie denn nun eine bestimmte Zeile angesprochen werden kann. Hierfür wird in der Regel ein sogenannter Decoder benutzt (den wir nachher auch noch einmal benötigen werden). Vereinfacht gesagt ist ein Decoder ein Bauelement, welches ein Signal in ein anderes umwandelt. Das wichtigste dabei ist, dass das Eingangs- und das Ausgangssignal nicht die gleiche Länge haben müssen – ein Decoder kann also etwa eine Bitkette (die ja nichts anderes ist als ein Signal) der Länge 2 in eine Bitkette der Länge 4 überführen. Hierbei können natürlich nie sämtliche Bit-Kombinationen an den Ausgängen erreicht werden (bei einem Eingangssignal mit 2 Bit sind eben nur insgesamt 4 Kombinationen möglich), aber das benötigt man auch gar nicht. Da der Aufbau eines Decoders in der Regel von der Signaltransformation abhängt, die er durchführen soll, spare ich mir diesmal das interaktive Bildchen; nur so viel: er lässt sich mit einfachen Grundbauelementen wie UND-Gattern und Invertern bauen. Im Folgenden der schematische Aufbau eines beispielhaften Decoders mit einem 2-Bit-Signal als Eingang und einem 4-Bit-Signal als Ausgang, gleich mit Wertetabelle (Quelle ist mal wieder die Wikipedia):
An dieser Schaltung lässt sich auch gleich sehen, wie ein Decoder helfen kann, einzelne Zeilen in einem Cache anzusprechen, indem er nämlich ein Eingangssignal (die Adresse) immer so transformiert, dass an lediglich einem Ausgang eine 1 anliegt (ein Decoder muss nicht zwangsläufig so arbeiten – die Ausgaben können beliebige Bitketten sein). Mit obigem Decoder könnten 4 Bytes adressiert werden, bei mehr Eingängen dann auch entsprechend mehr Bytes. Formal ausgedrückt: bei einer Adressbreite von n Bits können 2n Bytes adressiert werden; bei den heute üblichen Rechnern mit 32 Bit wären das 232, also etwas mehr als 4 Milliarden Bytes – das entspricht 4 Gigabyte (deswegen bringt es übrigens auch nichts, in einen Rechner mit einem 32-Bit-Betriebssystem mehr als 4 Gigabyte RAM einzubauen – sie können schlicht nicht vollständig adressiert werden).
Damit sind eigentlich die wichtigsten Informationen für die Fetch-Phase verfügbar. Fassen wir sie also noch einmal zusammen:
- Die im Program Counter im Steuerwerk gespeicherte Adresse wird an den Decoder des Caches übermittelt (das geschieht übrigens über den sogenannten Adressbus – eine Datenleitung im Computer, die einfach mehrere getrennte Bauelemente miteinander verbindet und so auch die Datenübermittlung über größere Entfernungen ermöglicht).
- Der Decoder wandelt das Adresssignal derart um, dass eine einzelne Zeile im Cache aktiviert wird.
- Die nun am Ausgang vom Cache anliegende Bitkette wird (wieder über einen Bus, diesmal den Datenbus) zurück an das Steuerwerk übermittelt, wo sie als Instruktion im Instruktionsregister gespeichert wird.
- Als letztes wird noch der Program Counter aktualisiert, so dass er auf die nächste Instruktion im Speicher verweist.
Phase 2: Decode
Die größte Magie ist jetzt eigentlich schon vorbei, spannender wird es nicht mehr. In der zweiten Phase wird die nun im Instruktionsregister geladene Instruktion durch einen Decoder im Steuerwerk geschleust (wobei das Register seine gespeicherten Werte zum Beispiel einfach direkt an den Decoder ausgebe könnte), der sie in ihre relevanten Bestandteile (also den Opcode und eventuelle Adressen, Register-Identifizierer und konkrete Werte) zerlegt. Der Decoder hier arbeitet genauso wie der im Cache, nur dass diesmal eben keine einzelne Bitkette mit nur jeweils einer einzelnen 1 darin entsteht, sondern mehrere, variablere Bitketten. Nach dem Ende der zweiten Phase ist also bekannt, was gemacht werden soll und welche Daten dafür zu benutzen sind.
Phase 3: Fetch Operands
Genauso, wie in der ersten Phase die Instruktion aus dem Speicher geladen wurde, werden in der dritten Phase eventuell benötigte Daten geladen; die Adressen hierfür wurden in der vorherigen Phase ja aus der Instruktion extrahiert, sind also bekannt. Das Vorgehen ist hierbei identisch zu dem in Phase 1, lediglich mit dem Unterschied, dass die geladenen Daten in anderen Registern gespeichert werden.
Phase 4: Execute
In der letzten Phase werden schließlich alle gesammelten Daten vereint und die geladene Instruktion ausgeführt. Vereinfacht ausgedrückt ist ein Prozessor dabei so verdrahtet, dass durch den gespeicherten Opcode automatisch die passenden Steuersignale an die benötigten Teile des Prozessors gesendet werden; wenn wir an das Rechenwerk aus dem letzten Teil denken, könnte durch den Opcode also das Steuersignal für den Addier-/Subtrahiereingang generiert werden.
Zusammenfassung
Wenn man sich jetzt den ganzen Prozessor einfach um noch viel mehr (Rechen-)Funktionen angereichert vorstellt, hat man ein ziemlich gutes Bild davon, wie so ein Computer tief im Inneren funktioniert. Auch moderne Prozessoren funktionieren nach dem in diesem und den letzten Kapiteln beschriebenen Prinzip. Natürlich sind sie weitaus komplexer in der Hinsicht, dass sie mehr Register, ein viel größeres Rechenwerk und insgesamt über mehr Funktionalität verfügen, aber das Prinzip ist wirklich das gleiche.
Damit haben den technischen Teil der Arbeitsweise von Computern geklärt. Wir haben die theoretischen Grundlagen des dualen Zahlensystems geklärt; wir haben besprochen, wie aus Transistoren einfache logische Gatter aufgebaut werden können; anschließend wurde gezeigt, wie mit Hilfe dieser logischen Gatter zunächst einzelne Bits miteinander verrechnet und gespeichert werden können; danach haben wir uns mit der Speicherung von ganzen Bitketten beschäftigt und komplexere Rechenoperationen besprochen; heute schließlich wurde erklärt, wie all die vorher vorgestellten Methoden zusammenwirken und einen Prozessor formen.
Wenn im Kommentarbereich keine besonderen Wünsche über weitere technische Bestandteile eines Rechners, die hier noch besprochen werden sollen, geäußert werden, würde ich diese Reihe hiermit für (erfolgreich) beendet erklären und mich in Zukunft der Programmierung von Computern zuwenden. Wir haben schließlich zwar geklärt, wie ein Computer Instruktionen verarbeiten kann, aber woher diese Instruktionen letzten Endes kommen, ist noch offen – und hier kommt der Programmierer ins Spiel. Aber dazu in Zukunft mehr.
Kommentare (12)