Neulich war mal die Frage nach dem "Ringbuffer" bzw. "Ringpuffer" aktuell, als es um eine Snake Programmiererei am PET ging ( siehe dort Selbstgeschriebene Spiele für die Commodore PET/CBM-Reihe ).
Da sich verschiedentlich Leute schwer damit taten, hier der Versuch einer kurzen Einführung dazu.
Wozu benötigt man soetwas überhaupt ??
Eigentlich benötigt man es nicht, zumindest rein theoretisch sollte es auch ganz ohne solche Konstrukte gehen, aber da Computer nunmal immer chronsich zu langsam waren (und sind) und zudem die Abstimmung der einzelnen Teile eines Rechners sich so erheblich vereinfacht, gibt es eben das Konzept des "Buffers" / "Puffers".
Das besondere am Rinbuffer ist eben nur, daß er in der Vorstellung als ein "Ring" gedacht werden kann. Rein praktisch ist er das natürlich eher nicht, er wird nur so aufgebaut, als wäre er "rund".
Aber erstmal was zu den Puffern.
Diese dienen dazu Daten - irgendwelcher Art - aufzunehmen und für einen relativ kurzen Zeitraum zwischenzuspeichern. Meist sind es auch eher wenige Daten, die so vorgehalten werden - von einigen wenigen Bytes bis zu ein paar Kilobytes. Lediglich bei Grafiken und Datenbanken kann das deutlich größere Werte annehmen. Etwa wenn ein Bild in einer Grafikkarte erst komplett aufgebaut wird und erst nachdem es fertig ist, als Anzeige freigeschaltet und sichtbar wird, dann aber eben komplett. In so einem Fall spricht mn von einem "Double Buffering", da man zweimal den Speicherplatz für die Anzeige bereitstellen können muß - einmal für das angezeigte Bild und dann für den Buffer, in dem gerade das kommende nächste Bild aufgebaut wird.
Noch größere Buffer sind z.B. ganze Server, die Teile des Internets laden und dann lokal zur Verfügung stellen, ohne daß jemand, der dort zugreift, die Daten direkt aus dem Netz laden muß. Das geht dann i.a. schneller und ist natürlich besonders bei z.B. Seiten mit vielen PDFs o.ä. durchaus sinnvoll.
Aber zurück zu den kleineren "Buffern".
Prinzipiell ist ein Buffer nichts anderes als ein freies Stück Speicherbereich. Dieses kann an sich auch irgendwo liegen - sinnigerweise aber natürlich eher im RAM als auf einer Diskette oder Platte, einfach weil RAM schneller ist.
Auf einem 8Bit Homecomputer z.B. als eine reservierte Speichereinheit von z.B. $100 Größe. Also etwa von Adresse $1f00 bis Adresse $1fff. Man hat damit also Platz von $00 beginnend bis zu $ff am Ende , wenn man nur die hinteren beiden Stellen anschaut. Und das sind dann genau 256 Zahlen, die man darin speichern kann.
Man sieht aber schon zwei wichtige Sachen
- der Buffer hat einen Anfang , d.h. eine Startadresse
- der Buffer hat ein Ende , d.h. eine Endadresse
beide zusammen definieren, wie groß der Buffer maximal ist
- der Buffer ist erstmal "linear" und die Speicherstellen sind quasi durchnummeriert von 0 ( $00 ) bis $ff ( 255 )
Möchte man nun Daten in so einen kleinen Speicherbereich schreiben, gibt es dafür verschiedene Ansätze.
Einer wäre etwa, daß man es sich einfach macht und darin eine Kopie von einer anderen Stelle des Speichers anlegt. Dann werden etwa die Werte von z.B. Adresse $1000 bis $10ff dahineinkopiert. Oder etwa der Bereich, in dem der Zeichensatz liegt, und man kann dann die Werte der ASCII Zeichen ändern und - sofern der Videochip das unterstützt - von der neuen Adresse bei $1f00 ausgeben.
Allerdings: Das ist NICHT unbedingt das, was gemeint ist, wenn es um "Buffer" aus der Abteilung "Ringbuffer" geht, auch wenn bei dem Beispiel natürlich durchaus was gebuffert wird ( der Zeichensatz nämlich ).
Der Unterschied ist, daß ein "echter Buffer" eine dynamische Struktur ist - v.a., was die Daten anbetrifft. Er wird nämlich benutzt um mit minmalem Aufwand ankommende Daten in ihn hineinzuschreiben, um sie ein klein wenig später direkt und unverändert wieder aus ihm auszulesen. Dabei erhalten die Daten ständig eine neue Position im Buffer, je nachdem wie voll dieser bereits ist.
Ein ankommendes Byte kann also im leeren Buffer an Position Null ( $00 ) landen, oder, wenn der Buffer schon halbvoll ist, die Adresse in der Mitte ( bei $80 ) zugewiesen bekommen.
Damit das funktioniert muß der Buffer natürlich "wissen", wo genau ein neues Byte gespeichert werden kann. Er benötigt also Information darüber, an welcher Stelle genau das nächste Byte gebuffert werden soll. Und genau dafür gibt es einen sogenannten Schreib-Zeiger ( Write-Pointer ). Das ist also einfach eine besondere (Speicher-)Stelle, in der man sich den nächsten freien beschreibbaren Platz notiert.
Bei einem leeren Buffer liegt dieser Write-Pointer natürlich erstmal auf Adresse $00.
Kommt nun ein neues Datenbyte an , dann kann dieses direkt an dieser Stelle in den Buffer geschrieben werden.
Damit ist Platz $00 dann "besetzt", was automatisch dazu führt, daß das nächste freie Plätzchen jetzt die $01 wäre - und dementsprechend muß natürlich der Write-Pointer entsprechend auf diese neue Postion gesetzt werden. Also Write-Pointer=Write-Pointer+1 .
LDX $Write-Pointer
LDA $neues-Datenbyte
STA $BufferAnfang,X
INX
... (evtl. mehrmals (loop) )
STX $Write-Pointer
( wäre so die Minimalvariante in 6502 Assembler )
Das Spiel kann man nun eine Weile machen und dadurch alle ankommenden Datenbytes "puffern" - und zwar maximal solange bis der Puffer voll ist. D.h. in dem Beispiel ist bei $ff Ende, Schluß, Fin.
Was dann passiert, kommt drauf an, was man so drumherum programmiert hat. Man könnte etwa den Buffer automatisch vergrößern. Oder man gibt eine Fehlermeldung aus, daß keine Bytes mehr angenommen werden können. Am Besten aber dimensioniert man die Maximalgröße des Buffers so, daß dieser Umstand gar nicht auftreten kann - weil der Buffer immer schnell genug wieder geleert wird.
Die Fehlermeldung eines Buffers kennt JEDER Benutzer eines Rechners, zumindest als ambitionierte Altcomputer-Bastler.
Und zwar in akustischer Form.
Sie tritt nämlich immer dann auf, wenn man den Tastaturbuffer überlastet hat und durch zuviele schnelle Tastendrücke den Buffer gefüllt hat. Dann wird i.a. ja nicht "intelligent" darauf reagiert und der Buffer vergrößert, sondern so ein DOS PC (z.B.) macht dann einfach einen "BEEP" - und teilt damit mit, daß er nicht gewillt ist, so schnell Tastendrücke anzunehmen. Die Fehlerbearbeitung wird so quasi elegant an den User delegiert - der gefälligst ein bißchen Geduld mitzubringen hat.
Interessant ist nun v.a. aber, wie der Puffer seine Daten wieder loswird !
Im Fall einer Tastatur ist das relaitv klar. Die Tastendrücke sollen in der gleichen Reihenfolge, wie sie in den Buffer gekommen sind, da auch wieder rausgelesen werden. Das heißt also, daß das erste geschriebene Byte auch als erstes wieder ausgelesen werden muß. Also im Beispiel die Adresse $00 , weil da ja zuerst geschrieben wurde. Und dann die $01 ... und so weiter.
Damit das schön klappt, benötigt man noch ein zweites Element - nämlich einen Pointer zum Lesen , den Read-Pointer. Dieser merkt sich also die Speicherstelle an der das nächste Byte ausgelesen werden kann.
Wir haben also jetzt
einen Buffer mit Anfang und Ende , der linear durchadressiert ist
und dazu zwei extra Speicherstellen irgendwo anders, in denen man sich die Postionen für Lesen bzw. Schreiben merkt ( eben Read- und Write-Pointer ).
Und damit kann man nun verschiedene Sachen machen.
Die Variante von eben - nämlich, daß man Daten aufsteigend da hineinschreibt, den Write-Pointer immer um 1 erhöht und wenn dann mal genug Zeit ist, den Lese-Pointer benutzt und bei $00 beginnend dann in einem schnellen Rutsch alle ( bzw. möglichst viele ) Datenbytes wieder ausliest - ist nur eine Möglichkeit.
Bei dieser würde man solange Daten auslesen, bis man den Write-Pointer "eingeholt" hat. An dieser Position weiß man dann, daß der Buffer komplett gelesen ist.
Und hier ist es dann auch sinnvoll, den Write-Pointer ( und den Lese-Pointer natürlich auch ) wieder auf den Startwert zurückzusetzen.
Dabei ist es noch nicht einmal nötig, die Daten aus dem Speicher zu entfernen. Diese können auch einfach im Buffer stehen bleiben. Allerdings, wenn es um Sicherheit geht, sind die dann auch von da auslesbar ! Dürfte aber bei Snake Spielen keine Rolle spielen ...
So eine Struktur hat ja eine wichtige Eigenschaft - die Daten die zuerst im Buffer landen, werden als erste auch wieder ausgelesen. Nämlich $00 wird zuerst geschrieben, dann wird $01 geschrieben, dann $02 usf. Der Lesevorgang beginnt aber ebenfalls bei $00 und läuft über $01, $02 usw.
Nach dieser Eigenschaft heißt eine derartige Form von Buffer: FIFO
das steht für " first in - first out " und beschreibt einfach nur, was da also passiert mit den gepufferten Bytes.