Systemprogrammierung - Teil 1Input/Outputvon Wolfgang Hetzler Systemprogrammierung gilt als schwierig, da ihre Inhalte dem Beutzer sehr oft abstrakt gegenüber treten im Gegensatz zu praktischen Aufgabenstellungen, deren Sinn plastisch vor Augen steht. Das diese Abstraktheit nicht sein muß und die Systemcalls eine wohldefinierte Schnittstelle zum Kernel darstellen, will diese Serie zeigen. Dieser Artikel leitet eine Serie zum Thema Systemprogrammierung mit Linux ein. Ich werde versuchen, diese Thema in einer Weise zu handeln, die es ermöglicht, aufgrund der gegebenen Anstöße sich selbständig vertiefend in diese Thema einzuarbeiten - Systemprogrammierung ist wie jegliche Form von Programmierung Erziehung zur Mündigkeit oder Learning by Doing It. Ich hoffe, es wird kein lustloser Prozeß. Als Programmiersprache habe ich C++ gewählt, wobei ich überwiegend den prozeduralen Teil dieser Sprache für mein Thema bevorzuge. Alle Beispiele sollten bei Interesse nachvollziehbar sein, auf umfangreichere Behandlung weise ich jeweils in der Literaturangabe hin, ansonsten empfehle ich das Studium der mannigfaltigen Source-Dateien, die Kernelquellen selber sind absolut ungeeignet, denn die Systemcalls, die wir behandeln, sind Schnittstelle zum Kernel, letzterer realisiert erstere. Schwerpunkte der SerieLinux verfügt über eine Vielzahl von Systemcalls, die sich alle thematisch gruppieren lassen, einige sind Exoten, die sich einer Kategorie entziehen. Viele Systemcalls haben ihre Tradition in System V, andere kommen von BSD, Posix definiert zusätzlich einige, Linux hat aber auch solche, die keine Entsprechung in anderen Systemen haben. Ich behandele die Systemcalls themenspezifisch, die Herkunft ist eher uninteressant. Daraus ergeben sich für mich folgende Schwerpunkte:
Es wird sich zeigen lassen, daß diese Themen die aktuelle Szene beherrschen, sie sind "State of the Art" und natürlich im Umfeld von Linux zu finden, was einen Linuxer nicht weiter erstaunen wird. Zum besseren Verständnis dieser Themen ziehe ich Beispielprogramme und/oder beispielhafte Implementierungen von vorhandenen Kommandos heran. Eins möge mir der gestandene Systemprogrammierer und die kritische Leserin dabei verzeihen: Ich verzichte in den Beispielprogrammen weitestgehend auf Fehlerabfragen, nicht, weil mir keine Fehler passieren, sondern weil diese Abfragen die Lesbarkeit eines Programmes drastisch reduzieren - man/frau sieht den Wald vor lauter Bäumen nicht. Nun und für alle weiteren Folgen zur Sache. Die Behandlung der Systemcalls im Kontext der ProgrammierspracheAlle Systemcalls bewirken einen Kontextswitch, d.h. der aufrufende Prozeß wechselt vom Userlevel in den Kernellevel, dies geschieht bei allen Kernelvarianten für PCs durch einen Interrupt 0x80, die Parameter des Interrupts bestimmen den konkreten Systemcall. Die Aufrufe selber sind als Interface in Form von C-Funktionen in der Standardbibliothek angelegt, somit unterscheidet sich ein Systemcall in der Oberfläche nicht von einer normalen C-Funktion und gleichzeitig können alle Aspekte der Systemprogrammierung auf der Ebene von C bzw. C++ vorgenommen werden. Übersicht über die aktuellen Systemcalls dieser FolgeDie Input/Output - Mechanismen reduzieren sich auf fünf elementare Systemcalls, die öfters auch als low-level Funktionen bezeichnet werden:
mit ihren Prototypen int open(char *, int, int); int close(int); int read(int,char *,int); int write(int,char *,int); int lseek(int,int,int); Beschreibung der Systemcalls mit BeispielenOpen und CloseDer Systemcall open öffnet den Zugriff auf eine Datei, übergeben wird der Name und der gewünschte Zugriff, der dritte Parameter erlaubt das Setzen von Zugriffsrechten, open liefert einen Dateideskriptor zurück, der in den weiteren Systemcalls als Paramter die Datei identifiziert. Der Deskriptor selber ist ein Index in einer internen Tabelle des Systems, die der Kernel für alle Zugriffe und Manipulationen benutzt. Wir schreiben also (siehe Listing 1, wo wir, wie auch im folgenden keine Rückgabewerte liefern.):
Das kleine Programm aus Listing 1 versucht die Datei zu öffnen, deren Name beim Aufruf des Programms übergeben wird, gelingt das nicht, liefert open wie alle Systemcalls -1 als allgemeinen Fehlerindikator zurück, im korrekten Fall wird die Datei über den Systemcall close wieder geschlossen, close erwartet lediglich den Dateideskriptor. Das Makro O_RDONLY gibt den Zugriffsmodus an - ausschließlich Lesen (siehe dazu die Makros aus der Datei fcntl.h, die ich im Zusammenhang mit dem Aufruf fcntl behandle). Der Systemcall open kreiert nicht die Datei, falls sie nicht existiert: der Zugriffsparameter muß das regeln . Wir schreiben
Es wird versucht, die Datei zum Lesen/Schreiben zu öffnen, gelingt das nicht, wird sie mit den Permissions 0644 angelegt, die Zugriffsmodi werden dabei mit einem logischen oder verknüpft: Das Ganze ist also ein kleines Programm zum Anlegen von Dateien (analog zum touch-Befehl). Read und WriteLesen und Schreiben sind im Handling unproblematisch, sie gehen sowohl auf geblockte als auch ungeblockte Dateien, Transfermenge sind Bytes ohne Ansehen und Würde, dies verführt auch zu der fehlerhaften Behauptung, es handelt sich um low - level I/0, es sind aber Systemcalls. Wir schreiben ein kleines Beispielprogramm, das eine Datei kopiert:
Read versucht die angegebene Menge Bytes in den Puffer buffer zu transferieren, die effektive Anzahl wird vom Aufruf zurückgeliefert, deshalb sollten stets nur count Bytes weggeschrieben werden. Die Anzahl von Bytes in der gewünschen Menge = 1024 entspricht der Blockgröße von Linux bezüglich geblockter Dateien, ein Lesen unter dieser Grenze ist für geblockte Dateien unsinnig, da mehr Systemcalls als nötig in Anspruch genommen werden. Dasselbe gilt natürlich auch für write, allerdings kann der letzte Block auch weniger gültige Bytes enthalten, deshalb kann ich nicht einfach stur 1024 wegschreiben. Read und Write beeinflussen den kernelinternen Offsetwert, der um die jeweiligen Bytemenge geändert wird. Das nächste Beispiel ist etwas näher an der Realität; es implemtiert eine einfache Variante des Kommandos dd, das ja bekanntlich Dateien dumpt (Z.B. dd if=boot.img of=/dev/fd0). Sende- und Empfangsdateien können sowohl normale als auch Gerätedateien sein, die Anzahl der Blöcke kann bei mir als ein dritter Paramter übergeben werden, ist dies nicht der Fall, wird bis EOF gelesen, zum Abschluß wird die Anzahl der verarbeiteten Blöcke auf dem Bildschirm ausgegeben. Zunächst das Listing 4:
Der Aufruf von dd kann in folgenden Formen erfolgen: dd /dev/fd0 /tmp/otto der Inhalt der Floppy wird nach /tmp/otto kopiert dd /tmp/otto /dev/fd0 die Datei /tmo/otto wird auf die Floppy kopiert dd /dev/fd0 /tmp/otto 10 es werden von der Floppy 10 Blöcke nach /tmp/otto kopiert In main werden die Dateien geöffnet, das kann jederzeit auch eine Gerätedatei sein, anschließend wird gecheckt, ob die Anzahl der zu kopierenden Blöcke angegeben worden ist und danach in Abhägigkeit entweder do_it oder do_it_until_eof aufgerufen. Der Rest sollte selbsterklärend sein. LseekDer Systemcall lseek erlaubt das Positionieren in Dateien. Linux interpretiert ja die Dateien als Streams, d.h. als eine endliche Folge von Bytes, lseek erlaubt, eine relative Adresse in dieser Datei anzugeben, die anschließend vom Kernel als neue Offset-Position übernommen wird. Der Aufruf von lseek erfolgt in der Form: lseek(fd,offset,where); wobei fd der Dateideskriptor und offset die neue relative Adresse ist, die in Abhängikeit vom dritten Parameter interpretiert wird: where = 0 offset wird vom Start interpretiert Eine kleine Anwendung soll wieder den Zusammenhang verdeutlichen. In Anlehnung an das Kommando rdev, das für ein Kernelimage root-flags, ram-size, vga-mode und root-device setzen kann, schreibe ich ein kleines Programm, das Teile dieser Werte ausgibt (siehe Listing 5):
Die Informationen liegen im Kernel ab Offset 504 dezimal vor, daher muß ich zunächst auf diese Position gehen. Jeweils 2 folgende Bytes enthalten die Informationen, die mit jedem read geholt werden und anschließend auf dem Bildschirm ausgegeben werden. Das hier jeweils nur 2 Bytes gelesen werden, kann vernachläßigt werden, da ein blockorientiertes Lesen hier unsinnig ist. Ein letztes Beispiel zeigt, wie eine Datei in einer bestimmten Blockgröße angelegt werden kann, die Anzahl der Blöcke wird dabei als Eingabeparameter übergeben (Siehe Listing 6):
Es wird auf das Ende mit dem entsprechenden offset positioniert und ein Block weggechrieben, dies ist erforderlich, um die Datei auch wirklich in der gegebenen Größe zu produzieren Für große Dateien existiert unter Linux auch eine lseek Variante mit dem Namen llseek, die als offset einen long long int bastelt. Alle genannten Systemcalls operieren als Schnittstelle zum Linux spezifischen virtuellen File System (VFS), die konkrete Implementierung liegt beim entsprechenden Handler des konkreten Dateiensystem: Linux unterstützt ja bekanntlich sehr viele. Die genannten Systemcalls sind Bestandteil und notwendige Voraussetzung aller highlevel I/O - Funktionen der Bibliotheken von C und C++ sowie natürlich auch aller anderen Programmiersprachen. Die nächste Folge beschäftigt sich mit Systemcalls rund um Dateien und Datei-Systeme.
Copyright © 1997 Linux-Magazin Verlag |