2.3 Funktionen, die den Filedeskriptor verwenden
Eine große Anzahl von Funktionen verwendet einen Filedeskriptor. In diesem Abschnitt finden Sie eine Beschreibung der wichtigsten Funktionen und einen Überblick zu weniger verwendeten oder erst in einem späteren Kapitel benötigten Funktionen.
2.3.1 Datei öffnen – open()
Es gibt zwei Versionen der Funktionen open(), um eine Datei zu öffnen oder eine neue Datei anzulegen. Hier die Syntax dazu:
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int open(const char *pfadname, int flags);
int open(const char *pfadname, int flags, mode_t zugriffsrechte);
Als Pfadangabe müssen Sie den absoluten oder relativen Pfad der Datei angeben, die geöffnet werden soll. Da der Prototyp in Wirklichkeit korrekt open(const char *, int ...) lautet, kann je nach Situation das dritte Argument auch wegfallen.
Hinweis In der prozessspezifischen Dateitabelle werden dabei die entsprechenden Einträge gemacht und initialisiert. Z. B. wird für die Datei die Position des aktuellen Schreib- oder Lesevorgangs mit 0 vorbelegt (Ausnahme: das O_APPEND Flag). Dies ist für den Programmierer nicht unbedingt wichtig zu wissen, aber falls es Sie interessiert, was intern vorgeht, soll dies nicht unerwähnt bleiben.
|
Der Systemaufruf open() liefert als Rückgabewert den Index (Filedeskriptor) des neu angelegten Eintrags in der prozessspezifischen Dateitabelle zurück. Die Einträge 0, 1 und 2 sind dabei bereits beim Start des Programms mit denen des Elternprogramms vorbelegt (siehe 2.1, Filedeskriptor). Im Falle einer Shell ist dies der Terminal, sofern keine Dateiumleitung vorgenommen wurde.
Die maximale Anzahl geöffneter Filedeskriptoren für einen Prozess ist mit der symbolischen Konstante OPEN_MAX, die in der Headerdatei <limits.h> definiert ist, festgelegt. OPEN_MAX ist hierbei das Minimum an Filedeskriptoren, die das Betriebssystem bzw. die C-Bibliothek bereitstellt. Es können natürlich über weiterreichende Funktionen wie ulimit(), rsetlimit() diese Limits erhöht werden, während OPEN_MAX immer gleich bleibt. Weiterhin kann man mit diesen Funktionen die Zahl auch kleiner als OPEN_MAX werden lassen; jene Einstellungen gelten dann auch für Subprogramme (Kindprozesse).
Konnte der Systemaufruf open() die Datei nicht öffnen, wird der Wert -1 zurückgeliefert. Benötigen Sie weitere Informationen, welcher Fehler aufgetreten ist, so sollten Sie die globale Variable errno, die sich in der Headerdatei <errno.h> befindet, auswerten. Diesen Fehlerwert können Sie anhand unterschiedlicher Konstanten, die sich ebenfalls in dieser Headerdatei befinden, identifizieren. Oder besser noch, Sie lassen sich die Fehlermeldung mit der Funktion perror() (stdlib.h) oder strerror() (string.h) im Klartext ausgeben (mehr dazu etwas später).
Hinweis Die Fehlervariable errno wird in der Regel von allen wichtigen Systemcalls (sofern nicht anders in den Manual Pages »ausgeschildert«) gesetzt und kann daher auch bei allen diesen Funktionen verwendet werden.
|
Bei der Verwendung von flags können mit dem Systemaufruf open() folgende Angaben eingesetzt werden:
Tabelle 2.3
Bearbeitungsflags beim Öffnen einer Datei
Flag
|
Bedeutung
|
O_RDONLY
|
Öffnen der Datei zum Lesen
|
O_WRONLY
|
Öffnen der Datei zum Schreiben
|
O_RDWR
|
Öffnen der Datei zum Lesen und Schreiben
|
Diese drei Flags können nicht miteinander kombiniert werden und schließen sich gegenseitig aus. Eins dieser Flags, das Sie ausgewählt haben, können Sie aber mit dem bitweisen ODER (|) mit den nachfolgenden Werten kombinieren.
Tabelle 2.4
Weitere Flags für den Systemaufruf open()
Flag
|
Bedeutung
|
O_CREAT
|
Existiert die Datei noch nicht, wird diese mit den als dritten Parameter angegebenen Zugriffsrechten (minus aktuelle umask) erzeugt. Existiert die Datei bereits, so hat O_CREAT keinen weiteren Effekt.
|
O_APPEND
|
Die Datei wird zum Schreiben am Ende geöffnet. Der Schreib-/Lesezeiger wird vor jeder Schreiboperation auf die aktuelle Dateigröße gesetzt.
|
O_EXCL
|
Wird dieses Flag zusammen mit O_CREAT verwendet, kann die Datei nicht geöffnet werden, wenn diese bereits existiert. Mit diesem Flag können Sie verhindern, dass keine parallelen Schreibzugriffe auf eine Datei gemacht werden.
|
O_TRUNC
|
Existiert diese Datei, wird sie komplett geleert (auf 0 Bytes Länge gesetzt). Dies geschieht auch, wenn die Datei nicht zum Schreiben geöffnet wurde (sinnvoll mit O_EXCL).
|
O_SYNC
|
Jeder Schreibvorgang auf das Medium wird direkt ausgeführt, und es wird gewartet, bis der Schreibvorgang komplett beendet wurde. Dieses Flag setzt den Pufferungsmechanismus außer Kraft. O_SYNC wird nicht von POSIX.1 unterstützt, wohl aber von SVR4.
|
O_NONBLOCK
|
Falls der Pfadname der Name eines FIFO oder einer Gerätedatei ist, wird der Prozess beim Öffnen und bei nachfolgenden I/O-Operationen nicht blockiert. Dieses Flag zeigt seine Wirkung erst bei einer Pipe oder bei nicht blockierenden Sockets, was später behandelt wird.
|
O_NOCTTY
|
Falls der Pfadname der Name eines Terminals ist, so sollte dieser nicht der neue Kontrollterminal des Prozesses werden, sofern der aktuelle Prozess kein Kontrollterminal besitzt.
|
Der dritte Parameter des Systemaufrufs open() ist optional und wird nur dann ausgewertet, wenn das Flag O_CREAT gesetzt ist. Mit diesem Parameter können Sie die Zugriffsrechte auf eine Datei erteilen. Auch für die Zugriffsrechte können Sie eine oder mehrere Konstanten, verknüpft mit dem bitweisen ODER, verwenden. Hierzu ein Überblick über die Konstanten, die Sie für die Zugriffsrechte verwenden können.
Tabelle 2.5
Überblick über die Konstanten für Zugriffsrechte mit open()
Konstante
|
Darstellung in ls
|
Bedeutung
|
S_ISUID
|
[--S------]
|
set-user-ID Bit
|
S_ISGID
|
[-----S---]
|
set-group-ID Bit
Für Dateien ohne S_IXGRP führt dies dazu, dass die Datei dem »mandatory locking« (Linux only) unterliegen. (Siehe später)
|
S_ISVTX
|
[--------T]
|
sticky Bit (saved-text Bit)
Hat unter Linux für Dateien keine Bedeutung mehr.
|
S_IRUSR
|
[r--------]
|
read (user; Leserecht für Eigentümer)
|
S_IWUSR
|
[-w-------]
|
write (user; Schreibrecht für Eigentümer)
|
S_IXUSR
|
[--x------]
|
execute (user; Ausführungsrecht für Eigentümer)
|
S_IRWXU
|
[rwx------]
|
read, write, execute (user; Lese-, Schreib-, Ausführungsrecht für Eigentümer)
|
S_IRGRP
|
[---r-----]
|
read (group; Leserecht für Gruppe)
|
S_IWGRP
|
[----w----]
|
write (group; Schreibrecht für Gruppe)
|
S_IXGRP
|
[-----x---]
|
execute (group; Ausführungsrecht für Gruppe)
|
S_IRWXG
|
[---rwx---]
|
read, write, execute (group; Lese-, Schreib-, Ausführungsrecht für Eigentümer)
|
S_IROTH
|
[------r--]
|
read (other; Leserecht für alle anderen Benutzer)
|
S_IWOTH
|
[-------w-]
|
write (other; Schreibrecht für alle anderen Benutzer)
|
S_IXOTH
|
[--------x]
|
execute (other; Ausführungsrecht für alle anderen Benutzer)
|
S_IRWXO
|
[------rwx]
|
read, write, execute (other; Lese-, Schreib-, Ausführungsrecht für alle anderen Benutzer)
|
Hierzu folgt jetzt ein Listing mit dem Systemaufruf open(). Das zweite Argument in der Kommandozeile wird hierbei der Name der neuen Datei.
/* make_file.c */
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd. h.>
int main(int argc, char **argv) {
// Zugriffsrechte für die neue Datei: -rw-rw-r--
mode_t mode = S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH;
const char *new_file;
int fd; // Filedeskriptor
// Alle Zugriffsrechte der Einschränkungsmaske erlauben
umask(0);
// Zweites Argument der Kommandozeile auswerten
if(argv[1] == NULL) {
fprintf(stderr, "Usage: %s datei_zum_oeffnen\n",
*argv);
return EXIT_FAILURE;
}
new_file = argv[1];
/*--------------------------------------------------
Neue Datei erzeugen (O_CREAT)
zum Schreiben (O_WRONLY)
falls Datei existiert, nicht erzeugen (O_EXCL)
Zugriffsrechte der Datei erteilen (modus)
--------------------------------------------------*/
fd = open(new_file, O_WRONLY | O_EXCL | O_CREAT, mode);
if(fd == -1) {
perror("Fehler bei open ");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Hier das Programm im Einsatz:
$ gcc -o make_file make_file.c
./make_file test
$ ls -l test
-rw-rw-r-- 1 Juergen users 0 Oct 14 02:23 test
$ ./make_file test
Fehler bei open : File exists
Hinweis Sofern Sie mit der oktalen Schreibweise der Rechtevergabe vertraut sind, können Sie natürlich diese als Alternative verwenden. Z. B.:
fd = open( new_file, O_WRONLY | O_EXCL | O_CREAT, 0644);
|
Außerdem sollten Sie noch wissen, dass bei Verwendung des Flags O_CREAT die Zugriffsrechte nicht unbedingt gewährt werden müssen, da die Einschränkungsmaske die Vergabe von Rechten verhindern kann. Aus diesem Grund wurde mithilfe der Funktion umask() die Maske zur Wegnahme von Rechtebits auf 0 gesetzt, womit alle Zugriffsrechte in dieser Maske erlaubt werden.
Einschränkungsmaske setzen und abfragen – umask()
Diese Funktion hat jetzt hier zwar nichts mit den Filedeskriptoren am Hut, aber wird recht häufig gerade vor einem open()- oder dem gleich folgenden creat()-Aufruf verwendet. Mit dieser Funktion können Sie die Einschränkungsmaske für einen aktuellen Prozess erfragen oder neu setzen.
#include <sys/stat.h>
#include <sys/types.h>
mode_t umask(mode_t new_mask);
Mit dieser Funktion legen Sie die Einschränkungsmaske fest, die beim Anlegen einer neuen Datei (oder auch eines neuen Verzeichnisses) nicht zu vergeben bzw. zu löschen ist, selbst wenn diese explizit von Funktionen wie open() oder creat() mit dem modus-Argument gefordert wurde. Für das Argument mask können alle Konstanten bis auf S_ISUID, S_ISGID und S_ISVTX verwendet werden, die Sie mit open() für die Zugriffsrechte verwenden konnten (siehe Tabelle 2.5). Mehrere Werte verknüpfen Sie auch hier mit dem bitweisen ODER-Operator. Im Beispiel zuvor wurde die Einschränkungsmaske auf 0 gesetzt. Somit können hiermit vom Prozess alle Zugriffsrechte für eine Datei gesetzt werden. Natürlich lässt sich auch die oktale Darstellungsweise verwenden.
Wenn Sie z. B. für die Maske mittels umask() den (oktalen) Wert 0077 verwenden würden, hätten Sie folgende Einschränkungsmaske:
---rwxrwx
Damit ist es gegeben, dass beim Setzen der Rechte, unabhängig von den geforderten Rechten, nur die Lese-, Schreib- und Ausführungsrechte des Eigentümers möglich wären. Das heißt, Sie können nur die Zugriffsrechte setzen, die von der Einschränkungsmaske nicht verwendet wurden; in diesem Falle wäre das Resultat rwx------ (0700). Würden Sie z. B. die Maske 0133 setzen, so hätten Sie folgende Einschränkungsmaske vergeben:
--x-wx-wx (0133)
Hiermit ist es nicht möglich, dass der Eigentümer die Datei ausführen kann und die Gruppe und alle anderen Benutzer an ein Schreib- und Ausführungsrecht kommen werden. Beispiel einer solchen Berechnung:
--x-wx-wx (0133) //Einschränkungsmaske von umask()
rw-r--r-- (0644) //Mögliche Rechtevergabe
-------------------
rwxrwxrwx (0777) //Gesamtsumme der Rechtevergabe immer 777
Standardmäßig wird meistens die Einschränkungsmaske 022 vergeben. Es ist aber auch möglich, mit dem Shell-Builtin-Kommando umask die eigene Einschränkungsmaske zu ändern. Innerhalb des o. g. Listings z. B. würde die neu gesetzte umask von 0 nur während der Ausführung des Programms (und der Unterprozesse) gültig. Dazu kann man z. B. einen entsprechenden umask-Aufruf in einer Startup-Datei wie .profile eintragen, so dass beim Start einer entsprechenden Shell die Einschränkungsmaske automatisch gesetzt wird.
2.3.2 Anlegen einer neuen Datei – creat()
In älteren UNIX-Versionen gab es das Flag O_CREAT noch nicht, daher wurde folgende Funktion zum Anlegen einer neuen Datei verwendet:
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int creat(const char *pfadname, mode_t zugriffsmodus);
Auch diese Funktion gibt bei Erfolg den Wert des neuen Filedeskriptors zurück, und bei einem Fehler wird -1 zurückgeliefert.
Da mit creat() dasselbe Ziel erreicht wird wie mit der Funktion open() und den Flags O_WRONLY | O_CREAT | O_TRUNC, ist diese Funktion eigentlich überflüssig. Außerdem kann eine mit creat() erzeugte Datei erst mal nur beschrieben werden. Wollen Sie aus der Datei lesen, müssen Sie diese erst mit close() schließen und anschließend mit open() wieder öffnen. Richten Sie weiterhin das Augenmerk auf O_TRUNC. Somit ist open() gegenüber creat() nicht nur aufgrund seiner Flexibilität der Vorzug zu geben.
2.3.3 Datei schließen – close()
Wenn Sie Ihre Arbeit mit dem Filedeskriptor beendet haben, sollten Sie diesen mit der Funktion close() wieder schließen. Bei solchen Anwendungen, wie Sie es im Beispiel von open() oben gesehen haben, ist es natürlich nicht unbedingt notwendig, dass Sie den Filedeskriptor schließen, da bei Beendigung eines Prozesses automatisch alle Filedeskriptoren geschlossen werden. Bei dauerhaft laufenden Programmen kann es aber schon mal zu Engpässen mit den maximal geöffneten Filedeskriptoren kommen. Wenn die Grenze von OPEN_MAX-Dateien, die gleichzeitig geöffnet wurden, erreicht ist, sollten Sie nicht benötigte Filedeskriptoren freigeben. Die Syntax dazu lautet:
#include <unistd. h.>
int close(int fd);
Konnte der Filedeskriptor ordentlich geschlossen werden, liefert close() den Wert 0 zurück, bei einem Fehler wird -1 zurückgegeben. Wurde für fd ein falscher bzw. ungültiger Filedeskriptor verwendet, besitzt errno den Wert EBADF.
2.3.4 Schreiben von Dateien – write()
Mit dem Systemaufruf write() können Zeichen in einer geöffneten Datei geschrieben werden. Voraussetzung, dass mit write() auch in eine Datei geschrieben wird, ist, dass diese (logischerweise) auch zum Schreiben geöffnet wurde. Hier die Syntax:
#include <unistd. h.>
ssize_t write(int fd, void *puffer, size_t anzahl_bytes);
Mit write() werden anzahl_bytes Zeichen, ab der Adresse puffer, in die Datei geschrieben, die mit dem Filedeskriptor fd zuvor mit open() geöffnet wurde. Als Rückgabewert wird entweder die Anzahl der erfolgreich geschriebenen Zeichen oder bei einem Fehler -1 zurückgegeben. Natürlich erweitert die Funktion write() auch den Schreib-/Lesezeiger um die Anzahl der erfolgreich geschriebenen Bytes.
Hinweis Da mit dem primitiven Datentypen size_t nur nicht negative Werte aufgenommen werden können, hat POSIX.1 den primitiven Typ ssize_t für vorzeichenbehaftete Werte eingeführt.
|
Hier ein Beispiel, das die Funktion write() im Einsatz demonstriert:
/* write_file.c */
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd. h.>
int main(int argc, char **argv) {
/* Zugriffsrechte für die neue Datei: -rw-rw-r-- */
mode_t mode = S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH;
ssize_t size;
int fd; /* Filedeskriptor */
char *str;
/* Alle Zugriffsrechte der Einschränkungsmaske erlauben */
umask(0);
/* Zweites Argument der Kommandozeile auswerten */
if(argc < 2) {
fprintf(stderr, "Usage: %s datei_zum_oeffnen\n",
*argv);
return EXIT_FAILURE;
}
if(argc < 3) { str = "Keine Angaben gemacht\n"; }
else { str = argv[2]; }
size = strlen(str);
/*----------------------------------------------
Neue Datei erzeugen (O_CREAT)
zum Schreiben (O_WRONLY)
Daten am Ende hinzufügen (O_APPEND)
Zugriffsrechte der Datei erteilen (modus)
----------------------------------------------*/
fd = open(argv[1], O_WRONLY | O_APPEND | O_CREAT, mode);
if(fd == -1) {
perror("Fehler bei open()");
return EXIT_FAILURE;
}
if(write(fd, str, size) != size)
perror("Fehler bei write()");
return EXIT_SUCCESS;
}
Das Programm im Einsatz:
$ gcc -o write_file write_file.c
$ ./write_file test
$ cat test
Keine Angaben gemacht
$ ./write_file test Blopp
$ cat test
Keine Angaben gemacht
Blopp
In diesem Beispiel wird immer der Text ans Ende der Datei geschrieben (O_APPEND). Als Text kommt entweder das dritte Argument in der Kommandozeile in Frage oder der konstante String, der auf den dummy verweist.
Probleme mit der Funktion write() können auch auftreten, weil nicht alle Schreiboperationen direkt auf den Datenträger gemacht werden müssen, außer es wurde das Flag O_SYNC verwendet. Obwohl write() ein Systemaufruf ist, werden die Daten im Falle einer Datei nochmals in einen separaten Pufferbereich innerhalb des Kernels gelegt, bevor sie schließlich auf den Datenträger geschrieben werden. Ein Multitasking-Betriebssystem hat nämlich noch andere Dinge zu tun, als sich sofort um die Ausführung Ihres Programms zu kümmern. Erst wenn der Systemkern Zeit hat, wendet er sich dem Puffer zu und schreibt entsprechende Daten in die Datei. Stürzt aber das System in der Zeit ab (was bei Linux weniger der Fall ist), wenn sich die Daten noch im Puffer befinden, sind diese weg und nicht mehr zu retten. Alternativ kann auch ein ganzes Dateisystem im synchronen Modus gemountet werden, siehe Manual Page von mount(1); praktisch heißt das, dass jedem open()-Aufruf implizit O_SYNC zugesteckt wird. Es gibt sogar ein O_ASYNC-Flag, um auf einem synchronen Dateisystem einige Dateien als asynchron zu markieren.
Tritt beim Schreiben ein Fehler auf, ist es nicht leicht zu bestimmen, was denn falsch gelaufen ist. Ein Rückgabewert von 0 und größer sagt nicht aus, ob auch wirklich alle Daten angekommen sind. In unserem Beispiel eben wurde Folgendes verwendet:
if(write(fd, str, size) != size)
fprintf(stderr, "Fehler bei write ...\n");
Das scheint im Großen und Ganzen auch in Ordnung zu sein. Was ist aber, wenn die Daten in str so umfangreich sind oder nicht so viele Daten auf einmal geschrieben werden können (wie das bei den Sockets häufig der Fall ist)? Dann wird der Schreibvorgang mit einer Fehlermeldung abgebrochen. Für solch einen Fall sollten (müssen) Sie sich rüsten. Es wird empfohlen, write() immer in einer Schleife zu verwenden, die so lange ausgeführt wird, bis alle Daten geschrieben wurden. Ich habe hierzu eine Wrapper-Funktion für write() geschrieben, die wie folgt aussieht:
#include <unistd. h.>
ssize_t write2(int fd, const void *buffer, size_t count) {
while (count > 0) {
ssize_t geschrieben = write(fd, buffer, count);
if (geschrieben == -1)
return -1;
count -= geschrieben;
buffer += geschrieben;
}
return 0;
}
Sie können diese Wrapper-Funktion gerne in das Beispiel zuvor einbauen. Diese Funktion kehrt erst zurück, wenn alle Bytes geschrieben wurden oder ein Fehler bei write() auftritt.
Hinweis Wrapper-Funktionen können Sie sich wie einen Strumpf vorstellen, der über die Originalfunktion gezogen wird. Natürlich darf eine solche Funktion nicht denselben Namen wie das Original haben (sie darf natürlich schon, nur wird sich GCC definitiv darüber ärgern).
|
Wollen Sie außerdem absolut sichergehen, dass write() alle Daten mit einem Rutsch in die Datei schreibt, müssen Sie das Flag für synchrones Schreiben (O_SYNC) verwenden. Dadurch machen Sie sich allerdings die Zeitvorteile der Funktion write() zunichte (allerdings auch abhängig vom Speichermedium). Der Zeitaufwand für das Schreiben erhöht sich dadurch drastisch. Letztendlich müssen Sie entscheiden, ob nun Sicherheit oder Schnelligkeit wichtiger ist.
2.3.5 Lesen von Dateien – read()
Mit dem Systemaufruf read() können Zeichen aus einer geöffneten Datei gelesen werden. Voraussetzung natürlich ist auch hier, dass mit read() nur aus einer Datei gelesen werden kann, wenn diese (logischerweise) auch zum Lesen geöffnet wurde. Hier die Syntax:
#include <unistd. h.>
ssize_t read(int fd, void *puffer, size_t anzahl_bytes);
Damit werden anzahl_bytes aus der geöffneten Datei vom Filedeskriptor fd in die Adresse von puffer kopiert. Auch hier ist, wie schon bei write(), der Rückgabewert die Anzahl der erfolgreich gelesenen Bytes und im Fehlerfall -1. Steht der Lesezeiger bereits am Dateiende und der Systemaufruf read() wird verwendet, wird in diesem Fall der Wert 0 zurückgeliefert – was keinen Fehler darstellt! Es ist ebenfalls kein Fehler, wenn weniger Zeichen gelesen wurden, als mit anzahl_bytes angegeben ist, was z. B. passieren kann, wenn wir uns kurz vor dem Dateiende befinden.
Auch bei der Funktion read() wird ein Puffermechanismus verwendet, der dafür sorgt, dass der Lesevorgang sehr schnell abgewickelt wird. Das drückt sich darin aus, dass bereits Gelesenes sofort nochmals gelesen werden kann, ohne dass dafür z. B. die CD noch mal angedreht werden müsste. Dieser Puffer (cachet Inodes) hat den Vorteil, dass nicht nur beim Lesen mittels read() ein Geschwindigkeitsgewinn entsteht, sondern auch beim wiederholten Ausführen eines – sagen wir – 20 MB schweren Programms von einem langsamen (oder künstlich verlangsamten) Gerät.
Die Anzahl von Bytes, die read() auf einmal lesen soll, hängt sehr stark von der Geschwindigkeit des Lesevorgangs des Mediums ab. Man spricht dabei auch von Blockgrößen. Als optimale Blockgröße haben sich Speicherblöcke mit 512, 1024 bis hin zu 8192 Bytes erwiesen. Natürlich hängt dies auch von der Menge der Daten ab, die es zu lesen gilt. Es macht durchaus Sinn, die Effizienz der optimalen Größe zu ermitteln. Denn je kleiner die Blockgröße, desto mehr read()-Aufrufe müsste man tätigen – und ein Wechsel von Userspace zu Kernelspace kostet viel Zeit. Das liegt zwar im Mikrosekunden-Bereich, aber – die Masse macht´s. Für übliche Diskettenlaufwerke, die mit durchschnittlich 29,7 KB/s lesen, reicht somit eine kleine Blockrate (1024 aufwärts), bei z. B. externen USB-Festplatten, die knapp 20 MB/s hergeben, kann man auch schon mal eine Blockgröße von 64 KB oder noch mehr nehmen (kleiner als der auf der Festplatte angegebene Cache sollte sie auf jeden Fall bleiben).
Folgendes Beispiel kopiert die Datei, die Sie als zweites Argument in der Kommandozeile angegeben haben, in die neu erzeugte Datei, die Sie als drittes Argument angegeben haben.
/* cpy_file.c */
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd. h.>
#define BLOCKSIZE 64 // bytes
// write2() aus obigem Codeblock übernehmen
static ssize_t write2(int, const void *, size_t);
int main(int argc, char **argv) {
// Zugriffsrechte für die neue Datei: -rw-rw-r--
mode_t mode = S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH;
char buffer[BLOCKSIZE];
int fd_r, fd_w; // Filedeskriptoren
int n;
// Alle Zugriffsrechte der Einschränkungsmaske erlauben
umask(0);
if(argc < 3) {
fprintf(stderr, "> %s quelldatei zieldatei\n", *argv);
return EXIT_FAILURE;
}
fd_r = open(argv[1], O_RDONLY);
fd_w = open(argv[2], O_WRONLY | O_EXCL | O_CREAT, mode);
if(fd_r == -1 || fd_w == -1) {
perror("Fehler bei open()");
return EXIT_FAILURE;
}
while((n = read(fd_r, buffer, BLOCKSIZE)) > 0)
if(write2(fd_w, buffer, n) < 0)
perror("Fehler bei write()");
return EXIT_SUCCESS;
}
Und hier das Programm in Aktion:
$ gcc -o cpy_file cpy_file.c
$ ./cpy_file cpy_file.c cpy_file.bak
$ cat cpy_file.bak
/* cpy_file.c */
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd. h.>
#define BLOCKSIZE 64
int main (int argc, char* argv[])
...
Bei diesem Beispiel wurde für die Blockgröße des Puffers eine Größe von 64 Bytes gewählt, womit Lese- und Schreiboperationen durchgeführt werden. Beim letzten Aufruf von read() wird man wohl keinen ganzen Block mehr lesen können. Daher wurde auch der Wert der gelesenen Zeichen in der Variable n gespeichert, damit auch nur diese Anzahl erfolgreich gelesener Zeichen vom Puffer wieder in die Zieldatei kopiert werden kann.
2.3.6 Schreib-/Lesezeiger positionieren – lseek()
Jede Datei, die Sie öffnen, besitzt auch einen Schreib-/Lesezeiger. Nach jedem Schreib- oder Lesevorgang wird dieser Zeiger um die Anzahl der geschriebenen oder gelesenen Bytes weitergesetzt. In der Regel befindet sich dieser Positionszeiger beim Öffnen einer Datei, sei dies nun zum Schreiben oder zum Lesen, anfangs immer auf 0; abgesehen davon natürlich, wenn die Datei mit dem Flag O_APPEND geöffnet wurde.
Die Parameter der Funktion lseek() entsprechen dabei denselben wie denen der Funktion fseek(), die in der höheren Ebene verwendet wird.
#include <sys/types.h>
#include <unistd. h.>
off_t lseek(int fd, off_t position, int indikator);
Vom dritten Parameter (indikator) hängt es ab, wie der zweite Parameter interpretiert wird. Dabei haben Sie drei Möglichkeiten zur Auswahl.
Tabelle 2.6
Mögliche Angaben für indikator
Konstante
|
Bedeutung
|
SEEK_SET
|
Den Schreib-/Lesezeiger vom Anfang der Datei um abstand Bytes versetzen. abstand darf dabei keine negative Zahl sein.
|
SEEK_CUR
|
Der Schreib-/Lesezeiger wird relativ zur aktuellen Position der Datei um abstand Bytes versetzt. abstand darf dabei sowohl eine positive als auch eine negative Zahl sein, wobei negative Zahlen einem Zurücksetzen (Richtung Anfang der Datei) entsprechen.
|
SEEK_END
|
Den Schreib-/Lesezeiger vom Ende der Datei um abstand Bytes »Richtung Dateiende« versetzen. abstand darf dabei sowohl eine negative als auch eine positive Zahl sein. Letzteres wird man aber wohl kaum in der Praxis finden, denn was befindet sich schon hinter EOF? Beachten Sie außerdem, dass lseek() im Gegensatz zu fseek() nicht den EOF-Status bereinigen kann – wozu fseek() gerne verwendet wird! Es gibt nämlich keinen EOF-Status bei den Low-Level-Funktionen.
|
Der erste Parameter fd ist der Filedeskriptor der geöffneten Datei, wo Schreib- und Lesezeiger versetzt werden sollen. Wenn bei diesem Systemaufruf alles glatt verlief, wird die aktuelle neue Position des Schreib- und Lesezeigers zurückgegeben. Bei einem Fehler wird -1 zurückgeliefert.
Der primitive Datentyp off_t wurde vom Datentyp long abgeleitet. Somit kann der maximale Wert der Positionierung auf einmal 2 GB betragen.
Hinweis Auf manchen 32-Bit-Systemen (Linux auf jeden Fall), die das Large Files System (LFS) unterstützen, ist es möglich, mit der Funktion open() und dem Flag O_LARGEFILE Dateien zu öffnen, die größer sind als die o. g. 2 GB. Wird dieses Flag gesetzt, ist der größte Wert, der dargestellt werden kann, ein Objekt vom Typ off64_t (primitiver Datentyp, der gewöhnlich den Typen long long repräsentiert).
|
Man kann auch Dateien größer als 2 GB ohne O_LARGEFILE öffnen, nur können dann die meisten Operationen nur bis 32 Bit ausgeführt werden.
Hierzu ein Beispiel, das eine (Text-)Datei, die Sie als zweites Argument in der Kommandozeile angeben, rückwärts ausgibt. Mithilfe von lseek() wird der Schreib-/Lesezeiger zuerst ans Dateiende, eine Position vor EOF, gesetzt. Anschließend wird mit dem Systemaufruf read() ein Zeichen eingelesen und mit auf dem Bildschirm ausgegeben. Damit der Text rückwärts ausgegeben wird, muss die Position des Zeigers immer um eine Stelle bis zum Dateianfang (pos == 0) zurückgezählt werden. Ist der Dateianfang erreicht, wird die Funktion beendet. Hier das Beispiel:
/* backward.c */
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd. h.>
static void bwread(int fd) {
int rv, nl = 0;
char ch;
lseek(fd, -1, SEEK_END);
while((rv = read(fd, &ch, 1)) == 1) {
if(ch == '\n' && !nl) { ++nl; }
else { putc(ch, stdout); }
if(lseek(fd, -2, SEEK_CUR) < 0) { break; }
}
if(nl) { putc('\n', stdout); }
if(rv == 0) { fprintf(stderr, "EOF unerwartet ...\n"); }
else if(rv == -1) { perror("Fehler bei read()"); }
return;
}
int main(int argc, char **argv) {
int fd;
if(argc < 2) {
fprintf(stderr, "Syntax: %s datei\n", *argv);
return EXIT_FAILURE;
}
if((fd = open(argv[1], O_RDONLY)) < 0) {
perror("open");
return EXIT_FAILURE;
}
bwread(fd);
return EXIT_SUCCESS;
}
Und hier das Programm im Einsatz:
$ cat > test.txt
Pinguine können nicht fliegen.
Sie sind das Produkt eines misslungenen Experiments
von M$. Aber der Tag der Abrechnung naht ;-)
STRG+D
$ gcc -o backward backward.c
$ ./backward test.txt
)-; than gnunhcerbA red gaT red rebA .$M nov
stnemirepxE nenegnulssim senie tkudorP sad dnis eiS
.negeilf thcin nennök eniugniP
Das Beispiel berücksichtigt übrigens auch die Position des Newline-Zeichens. Beachten Sie auch, dass hier der Schreib-/Lesezeiger um eine Position vor dem Dateiende platziert wurde. Hätten Sie hierfür 0L angegeben, würde read() den Wert 0 zurückgeben, und es würde EOF unerwartet ... ausgegeben werden.
2.3.7 Duplizieren von Filedeskriptoren – dup() und dup2()
In manchen Fällen wird es nötig sein, dass Sie einen Filedeskriptor duplizieren müssen. Ein Beispiel hierfür wäre, wenn der Elternprozess mit einem Kindprozess Daten austauschen will und der Kindprozess durch einen neuen Prozess mit einer exec*()-Funktion überlagert wird. In solch einem Fall, ohne dup() oder dup2(), würde das close-on-exec-Flag gesetzt werden. Wird dieses Flag gesetzt, werden alle Filedeskriptoren ungültig (da vom neuen Prozess überlagert) – sprich, sie sind nicht mehr vorhanden. Die Kommunikation zwischen dem Eltern- und Kindprozess wäre hiermit auch beendet. Duplizieren Sie hingegeben mit dup() oder dup2() einen Filedeskriptor, wird das close-on-exec-Flag gelöscht, und der neu überlagerte Prozess kann diese/n duplizierten Filedeskriptor/en zur Kommunikation verwenden. Mehr zum close-on-exec-Flag in Kürze. Sollte Ihnen das hier Beschriebene noch nicht so klar oder gänzlich unklar sein, ist dies noch nicht so wichtig. Mehr dazu finden Sie in den nächsten Kapiteln zu den Prozessen und den Interprozesskommunikationen.
Für solche und eventuell weitere Anwendungsfälle stehen die Funktionen dup() und dup2() zur Verfügung.
#include <unistd. h.>
int dup(int fd);
int dup2(int fd, int fd2);
Als Parameter wird der Funktion dup() der Filedeskriptor übergeben, der dupliziert werden soll. Im Falle eines Erfolges erhält man als Rückgabewert die Nummer des Filedeskriptors, der nun auf dieselbe Datei verweist. Bei der Nummer handelt es sich um die kleinste nicht negative Zahl, die noch nicht für einen anderen Filedeskriptor vergeben wurde. Schlägt der Aufruf von dup() fehl, wird -1 zurückgegeben.
Beim Aufruf der Funktion dup2() hingegen wird der Wert des Zieldeskriptors als zweiter Parameter (fd2) angegeben. Wurde fd2 bereits zuvor geöffnet, wird dieser zuvor noch geschlossen.
Bei beiden Funktionen zeigt der neu zurückgegebene Filedeskriptor auf denselben Dateitabelleneintrag, worin sich u. a. der Schreib-/Lesezeiger befindet. Somit kann man auf einen Deskriptor lseek() anwenden und danach von dem anderen (von der neuen Position) lesen.
Hierzu ein Anwendungsbeispiel der beiden Funktionen:
/* dup_fd.c */
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd. h.>
static void test_write(const char *str) {
write(STDOUT_FILENO, str, strlen(str));
return;
}
int main(int argc, char **argv) {
mode_t mode = S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH;
int fd1, fd2, fd3;
// Alle Zugriffsrechte der Einschränkungsmaske erlauben
umask(0);
fd1 = open("file1", O_WRONLY | O_CREAT, mode);
fd2 = open("file2", O_WRONLY | O_CREAT, mode);
if(fd1 == -1 || fd2 == -1) {
perror("Fehler bei open()");
return EXIT_FAILURE;
}
test_write("Zeile 1\n");
dup2(fd1, STDOUT_FILENO);
test_write("Zeile 2\n");
dup2(fd2, STDOUT_FILENO);
test_write("Zeile 3\n");
test_write("Zeile 4\n");
dup2(fd1, STDOUT_FILENO);
test_write("Zeile 5\n");
dup2(fd2, STDOUT_FILENO);
test_write("Zeile 6\n");
test_write("Zeile 7\n");
close(fd1);
close(fd2);
fd3 = dup(STDERR_FILENO);
write(fd3, "Schwerer Fehler ...\n",
strlen("Schwerer Fehler ...\n"));
return EXIT_SUCCESS;
}
Und hier das Programm im Einsatz:
$ gcc -o dup_fd dup_fd.c
$ ./dup_fd
Zeile 1
Schwerer Fehler ...
$ cat file1
Zeile 2
Zeile 5
$ cat file2
Zeile 3
Zeile 4
Zeile 6
Zeile 7
In diesem Beispiel wurde vorwiegend der Filedeskriptor für die Standardausgabe dupliziert. Somit wurde die Standardausgabe jeweils in eine der beiden geöffneten Dateien umgeleitet. Am Ende wurde auch noch die Standardfehlerausgabe dupliziert, und mithilfe ihres Filedeskriptors wurde auch eine Ausgabe darauf gemacht.
2.3.8 Ändern oder Abfragen der Eigenschaften eines Filedeskriptors – fcntl()
Zum Ändern oder Abfragen einer offenen Datei können Sie die Funktion fcntl() verwenden. Damit lassen sich folgende Anwendungsbeispiele auf eine geöffnete Datei realisieren:
|
Setzen oder Abfragen der Flags im Dateitabelleneintrag |
|
Setzen oder Abfragen von Einträgen in der Prozesstabelle |
|
Setzen oder Abfragen von Record Lockings (Sperren) |
|
Duplizieren von Filedeskriptoren |
Anhand dieser Aufzählungspunkte wird klar, wie universell diese Funktion ist. Hierzu die Syntaxbeschreibung:
#include <sys/types.h>
#include <unistd. h.>
#include <fcntl.h>
int fcntl( int fd, int kommando ... );
Als erstes Argument geben Sie den Filedeskriptor an, an dem Sie entsprechende Einstellungen oder Veränderungen vornehmen wollen. Das zweite Argument ist das Kommando, was Sie mit dem entsprechenden Filedeskriptor anstellen wollen. Hierfür gibt eine Reihe von symbolischen Konstanten, die anschließend ausführlich beschrieben werden. Das dritte Argument wird nur dann ausgewertet, wenn Sie vorhaben, einen Filedeskriptor zu duplizieren oder wenn Sie die Einstellung einer offenen Datei verändern wollen. Der Rückgabewert von fcntl() ist vom Kommando abhängig, aber im Fehlerfall wird wie gewöhnlich -1 zurückgeliefert.
Jetzt wie versprochen zu den einzelnen Kommandos von fcntl() und deren Bedeutung und Anwendungsbeispiele.
F_DUPFD
Verwenden Sie als Kommando F_DUPFD, so wird der Filedeskriptor fd dupliziert. Bei Erfolg liefert in diesem Fall die Funktion fcntl() den kleinstmöglichen positiven Wert des neuen Filedeskriptors zurück.
int old_fd, new_fd;
new_fd = fcntl(fd, F_DUPFD, 0);
Dieser neue Filedeskriptor besitzt denselben Dateitabelleneintrag wie fd (sprich hier lseek(), dort read()), verwendet aber innerhalb der Prozesstabelle einen eigenen Eintrag für die Filedeskriptor-Flags, wobei das close-on-exec-Bit gelöscht ist. Bei gelöschtem close-on-exec-Bit bleibt der Filedeskriptor auch bei einem exec*()-Aufruf bestehen. Dies ist übrigens dieselbe Funktionalität wie beim Systemaufruf mit dup2().
F_GETFD
Mit F_GETFD können Sie überprüfen, ob das close-on-exec-Bit gesetzt ist oder nicht. Dies ist momentan auch das einzige Flag des Filedeskriptors, das Sie überprüfen können. Die symbolische Konstante dazu lautet FD_CLOEXEC. Ist das Flag gesetzt, liefert fcntl() als Rückgabewert 1 zurück, ansonsten wird bei einem Fehler -1 und, wenn nicht gesetzt, 0 zurückgeliefert.
if( fcntl ( fd, F_GETFD, FD_CLOEXEC ) == 1 ) {
/* Close-on-exec-Flag ist gesetzt */
} else {
/* nicht gesetzt */
}
Close-on-exec-Flag
Wenn das Flag nicht gesetzt ist (Standard), bleiben alle bereits geöffneten Filedeskriptoren bei einem durch einen exec()-Aufruf neu gestarteten Programm offen. Ist das Flag gesetzt, werden die entsprechenden Filedeskriptoren beim exec()-Aufruf geschlossen. Wenn z. B. ein privilegiertes Programm einen neuen Prozess erzeugt, sollten Sie aus sicherheitsrelevanten Gründen alle Filedeskriptoren – auch Verzeichnisse, IPC-Handles und Sockets – schließen. Um hierbei auf Nummer sicher zu gehen, sollten Sie das close-on-exec-Flag für die sicherheitsrelevanten Filedeskriptoren direkt nach dem Öffnen setzen (natürlich mit Vorbehalt).
Weiterhin ist das close-on-exec-Flag sinnvoll für Anwendungen, die generell nicht wollen, dass ihre geöffneten Dateien nur irgendwie an Subprozesse weitergegeben werden. Als Beispiel hierfür bietet sich der so genannte »PID-Lock« an, wo eine Datei in /var/run/name erstellt wird, die PID eingeschrieben wird und offen bleibt. Führt man besagtes Programm nochmals aus, so kann die zweite Instanz überprüfen, ob es /var/run/name gibt und, wenn ja, ob auch der Prozess mit der darin enthaltenen PID z. Z. läuft. Aber zurück zum Thema: Man möchte i. d. R. nicht, dass eine Subshell oder sonstige Anwendungen durch fork() und/oder exec() diesen Lock umgehen, indem die Datei schon geöffnet ist.
F_SETFD
Wie auch schon bei F_GETFD, können Sie mit F_SETFD die Filedeskriptor-Flags setzen. Als mögliches drittes Argument kommen auch hier nur FD_CLOEXEC zum Setzen oder !FD_CLOEXEC zum Löschen des Flags in Frage.
if(fcntl(fd, F_GETFD, FD_CLOEXEC) == 0) {
// Close-on-exec ist nicht gesetzt
if(fcntl(fd, F_SETFD, FD_CLOEXEC) > 0) {
// Close-on-exec wurde gesetzt
}
}
F_GETFL
Mit dem Kommando F_GETFL können Sie die File Status Flags erfragen. Folgende Statusmöglichkeiten können Sie hierbei abfragen:
Tabelle 2.7
File Status Flags, die mit F_GETFL abgefragt werden können
Modus
|
Bedeutung
|
O_RDONLY
|
nur lesen
|
O_WRONLY
|
nur schreiben
|
O_RDWR
|
lesen und schreiben
|
O_APPEND
|
zum Schreiben ans Dateiende öffnen
|
O_NONBLOCK
|
kein Blockieren bei FIFOs und Gerätedateien
|
O_SYNC
|
nach jedem Schreiben auf die Beendigung des physikalischen Schreibvorgangs warten
|
O_ASYNC
|
asynchroner I/O (nur bei Linux und BSD) – Gegenteil zu O_SYNC
|
Wenn Sie den Rückgabewert von F_GETFL haben wollen, müssen Sie das mit fcntl() ermittelte Flag durch die symbolische Konstante O_ACCMODE filtern:
flag = fcntl(fd, F_GETFL, 0);
modus = flag & O_ACCMODE;
if(modus == O_RDWR) {
// Mit diesem Filedeskriptor ist Lesen und Schreiben möglich
} else if(modus == O_RDONLY) {
// Mit diesem Filedeskriptor ist nur Lesen möglich
} else if(modus == O_WRONLY) {
// Mit diesem Filedeskriptor können Sie nur schreiben
}
F_SETFL
Mit diesem Kommando können Sie folgende File Status Flags mit dem dritten Argument von fcntl() setzen: O_APPEND, O_NONBLOCK, O_SYNC, O_ASYNC. Um eines dieser File Status Flags zu setzen, geht man wie folgt vor:
if(fcntl(fd, F_SETFL, O_APPEND) > 0) {
// Filestatus-Flag O_APPEND gesetzt
} else {
// Filestatus-Flag O_APPEND konnte nicht gesetzt werden
}
F_GETOWN
Hierbei liefert fcntl() die PID oder PGID des Prozesses, der im Augenblick die Signale SIGIO und SIGURG empfängt.
F_SETOWN
Damit legen Sie mit dem dritten Argument die PID oder PGID des Prozesses fest, der die Signale SIGIO und SIGURG empfängt. Ein positiver Wert legt die PID, ein negativer die GID fest.
Jetzt ist es an der Zeit, ein Listing zu erstellen, das Ihnen einige dieser Kommandos in ihrer Anwendung zeigen soll:
/* play_fd.c */
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd. h.>
int main(int argc, char **argv) {
int fd, fdup;
int flags, modus;
if(argc < 2) {
fprintf(stderr, "Usage: %s DATEI\n", *argv);
exit(EXIT_FAILURE);
}
if((fd = open(argv[1], O_RDONLY)) == -1) {
fprintf(stderr, "Konnte %s nicht öffnen!\n", argv[1]);
exit(EXIT_FAILURE);
}
if((fdup = fcntl(fd, F_DUPFD, 0)) != -1)
printf("Fildeskriptor erfolgreich dupliziert\n");
/* Wir überprüfen die Flags von fd und setzen,
* falls noch nicht gesetzt, FD_CLOEXEC.
*/
if((fcntl(fd, F_GETFD, FD_CLOEXEC)) == 1)
printf("Close-on-exec-Bit ist gesetzt\n");
else if((fcntl(fd, F_SETFD, FD_CLOEXEC)) < 0)
printf("Konnte close-on-exec-Bit nicht setzen\n");
else
printf("Close-on-exec-Bit wurde gesetzt\n");
// Wir überprüfen die Filestatus-Flags von fd
flags = fcntl(fd, F_GETFL, 0);
modus = flags & O_ACCMODE;
if(modus == O_RDWR)
printf("Datei zum Lesen und Schreiben geöffnet\n");
else if(modus == O_RDONLY)
printf("Datei nur zum Lesen geöffnet\n");
else if(modus == O_WRONLY)
printf("Datei nur zum Schreiben geöffnet\n");
/* Wir überprüfen weitere Statusflags von fd und
* ändern, falls nötig, die Flags.
*/
if(modus == O_APPEND)
printf("Wir können am Dateiende schreiben\n");
modus |= O_APPEND;
if(fcntl(fd, F_SETFL, modus) < 0)
printf("Konnte Attribut O_APPEND nicht setzen\n");
if(modus == O_APPEND)
printf("O_APPEND gesetzt\n");
flags = fcntl(fd, F_GETFD, 0);
modus = flags & O_ACCMODE;
if(modus == O_NONBLOCK) {
printf("O_NONBLOCK gesetzt\n");
} else {
modus |= O_NONBLOCK;
fcntl(fd, F_SETFL, modus);
if(modus == O_NONBLOCK)
printf("O_NONBLOCK erfolgreich gesetzt\n");
else
printf("Konnte O_NONBLOCK nicht setzen\n");
}
return EXIT_SUCCESS;
}
So könnte die Ausgabe aussehen:
$ gcc -o play_fd play_fd.c
$ ./play_fd test.txt
Filedeskriptor erfolgreich dupliziert
close-on-exec-Bit wurde gesetzt
Datei nur zum Lesen geöffnet
...O_APPEND gesetzt...
...konnte O_NONBLOCK nicht setzen...
2.3.9 Record Locking – Sperren von Dateien einrichten
Wenn zwei oder mehrere Prozesse gleichzeitig auf eine Datei schreibend zugreifen wollen, führt dies unwiderruflich zu Problemen. Meistens kann man dabei mit einem Datensalat rechnen. Ein einfaches Beispiel, das dieses Problem demonstriert:
/* trash.c */
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd. h.>
int main(void) {
int fd = open("file", O_WRONLY | O_CREAT | O_TRUNC, 0666);
pid_t pid;
int x, y;
if(fd == -1) {
perror("open()");
exit(EXIT_FAILURE);
}
if((pid = fork()) == -1) {
perror("fork()");
exit(EXIT_FAILURE);
} else if(pid > 0) { // Elternprozess
srand(time(NULL) ^ getpid());
for(x = 0; x < 10; ++x) {
usleep(rand() % 10 * 10000);
write(fd, "x", 1);
write(STDOUT_FILENO, "x", 1);
}
wait(NULL);
printf("\n");
} else { // Kindprozess
srand(time(NULL) ^ getpid());
for(y = 0; y < 10; ++y){
usleep(rand() % 10 * 10000);
write(fd, "X", 1);
write(STDOUT_FILENO, "X", 1);
}
}
return EXIT_SUCCESS;
}
Das Programm bei der Ausführung:
$ gcc -o trash trash.c
$ ./trash
$ cat file
XxxXxXxXxXXxXxxXxXxX
Es ist vorerst gar nicht so wichtig, dass Sie die einzelnen Funktionen des Programms verstehen. Hierbei wird mit dem Systemaufruf fork() ein zweiter Prozess gestartet. Zu diesem Thema werden Sie später noch eine Menge erfahren. Wichtig ist nur, dass hier zwei Prozesse gleichzeitig in die Datei file schreiben. Ein Prozess schreibt dabei kleine x-Buchstaben und der andere große X in die Datei. Bei beiden Prozessen wurde eine Zeitverzögerung mit usleep() eingebaut. An der Ausgabe der Datei file können Sie feststellen, dass hier weder Zucht noch Ordnung herrschte. Denn anstatt
XxxXxXxXxXXxXxxXxXxX
hätte die Datei file eigentlich folgenden Inhalt haben sollen:
xxxxxxxxxxXXXXXXXXXX
//oder
XXXXXXXXXXxxxxxxxxxx
Stellen Sie sich nun vor, dass wären Daten in einer Datenbank gewesen! Um solche Inkonsistenzen in gemeinsam benutzten Dateien zu vermeiden, gibt es einen POSIX-Mechanismus namens Record Locking.
Mit einem solchen Record Lock ist es möglich, einen beliebigen (zusammenhängenden) Teil einer Datei (natürlich auch eine komplette Datei) zu blockieren. Dabei gibt es so genannte »Shared« und »Exclusive« Locks (Sperren).
|
Shared Locks – Mehrere Prozesse sperren Teile einer oder der kompletten Datei und greifen z. B. auf gleiche Dateibereiche lesend zu. Erst wenn hierbei die letzte Sperre entfernt wurde, ist ein Exclusive Lock möglich, beide Locking-Typen schließen sich somit aus. |
|
Exclusive Locks – Ein Prozess sperrt einen Teil oder eine komplette Datei. Auf den nun gesperrten Bereich ist kein weiterer Lock mehr möglich, der Prozess kann nun schreibend zugreifen. |
Sie haben generell zwei Möglichkeiten (Funktionen) zum Erstellen eines Dateilocks:
|
fcntl() – Dies ist ein BSD-konformer Lock-Mechanismus, der hier auch etwas umfangreicher behandelt wird. |
|
lockf() – (SYSV- und POSIX-konform) Dies ist letztendlich eine einfache, aber häufig ausreichende Schnittstelle zu fcntl(). |
Advisory Locking und Mandatory Locking
Alle eben benannten Möglichkeiten, eine Datei zu sperren, gehören zu den so genannten Advisory Locks (freiwilliges Sperrverfahren; advise = avisieren/benachrichtigen; hier beraten/vorschlagen). Bei diesem Sperrverfahren versucht jeder Prozess, vor dem Zugriff auf einen bestimmten Bereich einer Datei diesen zu sperren. Existiert für diesen Bereich bereits eine Sperre, scheitert der Versuch, die Sperre zu setzen. Der Prozess wartet dann, bis dieser gesperrte Bereich wieder freigegeben wird. Das heißt auch, dass ein für die Datei bearbeitender Prozess selbstständig dafür Sorge tragen muss, dass es für den Fall, dass er schreibend zugreifen will, keinerlei Locks auf diese Datei gibt. Solange sich der Programmierer bei der Erstellung an diesen Test hält, sind alle anderen Aktionen der Programme auf die gesperrte(n) Datei(en) unproblematisch – sprich, diese Art von Sperren hängen im großen Maße von der Disziplin des Programmierers ab.
Auf der anderen Seite gibt es noch das Mandatory Locking (verbindliches Sperrverfahren). Der Unterschied zum Advisory Locking ist, dass hier nach dem Setzen einer verbindlichen Sperre durch einen Prozess ein Zugriff durch einen anderen Prozess mittels open(), read() oder write() zu einem Fehler führt. Mandatory Locking (auch als Kernel Locking bekannt) ist natürlich die ideale Alternative. Das Setzen einer solchen Sperre wird beim Mandatory Locking über ein spezielles Setzen des SGID-Bits (in der Kommandozeile mit chmod g+s-x myfile) der Dateiattribute realisiert. Das Mandatory Locking ist im Gegensatz zum Advisory Locking erheblich aufwändiger und damit logischerweise auch langsamer. Das Mandatory Locking ist bei Linux-Versionen allerdings erst ab einer Kernel-Version von 2.3.x implementiert. Ebenfalls vorhanden ist dieses Sperrverfahren unter Sun OS 4.1.x, Solaris 2.x, HP-UX 9.x-11.x und IRIX 6.5.
Mandatory Locking unter Linux verwenden
Sofern Sie das anschließende Beispiel unter Linux mit einem verbindlichen Sperrverfahren ausführen wollen, müssen Sie sicherstellen, dass hier das Mandatory-Attribut für die Partition aktiviert ist, in der sich die Datei befindet. Hierzu muss praktisch nur ein Eintrag in /etc/fstab verändert werden. In der vierten Spalte der entsprechenden Partition muss man nur die mand-Option einfügen und dem Kommando mount direkt übergeben. Z. B. sieht ein Eintrag bei mir wie folgt aus:
# mount
/dev/hda5 on / type ext2 (rw)
Ändern Sie nun diese Zeile wie folgt um:
/dev/hda5 on / type ext2 (rw,mand)
Um diese Änderungen wirksam zu machen, müssen Sie diese Partition eventuell neu mounten (für die root-Partition wird eventuell die Option -n nötig):
# mount -o remount /dev/hda5
oder auch:
# mount / -o remount
Dieser Zusatzaufwand sollte generell nur unter Linux nötig sein, da viele andere Systeme dieses Locken schon von Haus aus unterstützen.
Sperren von Dateien mit fcntl() einrichten
Der Sinn von Sperren ist es ja, dass immer nur ein Prozess in eine Datei schreiben darf. Dies wird realisiert, indem eine Schreibsperre eingerichtet wird. Sobald der Schreibvorgang beendet wurde, sollte diese Schreibsperre auch wieder aufgehoben werden. All dies lässt sich mit der Funktion fcntl() realisieren, die zusätzlich als drittes Argument einen Zeiger auf die Struktur flock bekommt:
int fcntl( int fd, int kommando, ...
/* struct flock *flockzeiger */ );
Die Struktur flock ist wie folgt aufgebaut:
struct flock {
short l_type; /* F_RDLCK (Lesesperre) */
/* F_WRLCK (Schreibsperre) */
/* F_UNLCK (Sperre aufheben) */
off_t l_start; /* relatives Offset in Byte, */
/* abhängig von l_whence */
short l_whence /* SEEK_SET,SEEK_CUR,SEEK_END */
off_t l_len; /* Größe der Speicherung in */
/* Bytes,0=Sperren bis Dateiende */
pid_t l_pid; /* wird bei F_GETLK zurückgegeben */
};
Der im flock-Record mit l_type übergebene Locking-Typ ist somit immer entweder F_RDLCK für Shared Locks oder F_WRLCK für Exclusive Locks. Wird versucht, auf eine bereits exklusiv gesperrte Datei einen weiteren Lock zu setzen, kann man durch den open()-Befehl selbst entscheiden, ob man auf das Ende des blockierenden Locks warten will oder den Befehl mit einem Fehler abbrechen möchte (bei Advisory Locking).
Wollen Sie z. B. die ganze Datei zum Lesen oder Schreiben sperren, was in der Regel am häufigsten zutreffen wird, müssen Sie die Strukturvariablen l_start und l_whence an den Anfang der Datei setzen:
flockzeiger.l_start = 0;
flockzeiger.l_whence = SEEK_SET;
Wenn Sie jetzt noch eine geregelte Sperre immer bis zum Dateiende haben wollen, müssen Sie nur die Strukturvariablen l_len auf 0 setzen, die für eine Sperre bis zum Dateiende steht:
flockzeiger.l_len = 0;
Die Datei ist somit immer bis zum Dateiende gesperrt, unabhängig davon, ob jetzt neuer Inhalt am Ende angefügt wurde.
Jetzt zu den möglichen Angaben der Kommandos, die als zweites Argument an die Funktion fcntl() übergeben werden.
F_GETLK
Mit F_GETLK können Sie überprüfen, ob bereits eine Sperre für die Datei (an einer gewissen Position) spezifiziert wurde. Diese Einstellung wird mithilfe des Filedeskriptors und des Kommandos F_GETLK mit der Funktion fcntl() an eine Adresse vom Typ struct flock übergeben. Anschließend können Sie die Strukturvariable l_type auf folgende symbolische Konstanten auswerten:
Tabelle 2.8
Konstanten, mit denen die Strukturvariable l_type belegt sein kann
Konstante
|
Bedeutung
|
F_UNLCK
|
keine Sperre vorhanden
|
F_RDLCK
|
Lesesperre
|
F_WRLCK
|
Schreibsperre
|
In der Praxis sieht das dann in etwa so aus:
struct flock sperre = {
.type = F_RDLCK, .start = 0, .whence = SEEK_SET, .len = 0
};
fd = open(datei, O_CREAT | O_WRONLY, 0666);
// Sperre abfragen
fcntl(fd, F_GETLK, &sperre);
// Abfrage auswerten
if(sperre.l_type == F_UNLCK) {
// Datei nicht gesperrt
} else if(sperre.l_type == F_RDLCK) {
// Lesesperre
} else if(sperre.l_type == F_WRLCK) {
// Schreibsperre
}
Anschließend werden Sie sehen, wie man mit F_SETLK Sperren setzen kann. Natürlich ist es auch möglich, Sperren an verschiedenen Stellen zu positionieren. Um einen solchen Lock mit F_GETLK wiederzubekommen, geht man folgendermaßen vor: Man bereitet einen struct flock so vor, als ob dieser für F_SETLK verwendet werden soll (folgt gleich), d. h. inklusive l_type, l_start und l_whence, und gibt diese für F_GETLK an, womit natürlich geprüft wird, ob bei »l_start/l_whence« ein l_type vorliegt.
F_SETLK
Mit dem Kommando F_SETLK können Sie eine Lese- oder Schreibsperre einrichten und auch wieder entfernen. Einrichten können Sie eine Sperre, indem Sie zuerst die Strukturvariablen von flock mit entsprechenden Werten initialisieren und anschließend als drittes Argument mit dem Funktionsaufruf fcntl() verwenden. Gelingt es nicht, eine Sperre einzurichten, bricht fcntl() ab und setzt die Fehlervariable errno auf EACCES oder EAGAIN.
In der Praxis sieht das Einrichten einer Sperre so aus:
sperre.l_start = 0;
sperre.l_whence = SEEK_SET;
sperre.l_len = 0;
sperre.l_type = F_RDLCK;
if(fcntl(fd, F_SETLK, &sperre) == 0) {
// Lesesperre gesetzt
}
sperre.l_start = 0;
sperre.l_whence = SEEK_SET;
sperre.l_len = 0;
sperre.l_type = F_WRLCK;
if(fcntl(fd, F_SETLK, &sperre) == 0) {
// Lesesperre gesetzt
}
sperre.l_type = F_UNLCK;
if(fcntl(fd, F_SETLK, &sperre) == 0) {
// Sperre aufgehoben
}
/* Oder im C99-Stil: ------------------------ */
struct flock sperre = {
.l_start = 0,
.l_whence = SEEK_SET,
.l_len = 0,
.l_type = F_RDLCK,
};
if(fcntl(fd, F_SETLK, &sperre) == 0) {
// Lesesperre gesetzt
}
F_SETLKW
Mit diesem Kommando wird der Prozess so lange suspendiert, bis er die geforderte Sperre einrichten kann. Es handelt sich hierbei um die blockierende Version des Kommandos F_SETLK.
Hinweis Wenn Sie eine Datei mit einer Schreibsperre (F_WRLCK) versehen wollen, muss die Datei auch zum Schreiben geöffnet (O_WRONLY oder O_RDWR) werden. Und umgekehrt (F_RDLCK), wenn Sie einer Datei eine Lesesperre anhängen wollen, muss diese auch im Lesemodus (O_RDONLY oder O_RDWR) geöffnet werden. Das bedeutet auch, dass Sie nie zwei unterschiedliche Sperrtypen auf ein bestimmtes Byte festlegen können.
|
Hierzu jetzt ein Listing, das die Sperren in Dateien demonstriert. In diesem Beispiel wurden drei Funktionen geschrieben. Eine, die den Status (status()) der Sperre überprüft, eine weitere, die eine Schreibsperre einrichtet (locki()), und noch eine dritte, welche die Schreibsperre wieder auflöst (unlock()).
/* sperre.c */
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd. h.>
#define FNAME "locki.lck"
static void status(struct flock *lock) {
printf("Status: ");
switch(lock->l_type) {
case F_UNLCK:
printf("F_UNLCK (Sperre aufgehoben)\n");
break;
case F_RDLCK:
printf("F_RDLCK (PID: %d) (Lesesperre)\n",
lock->l_pid);
break;
case F_WRLCK:
printf("F_WRLCK (PID: %d) (Schreibsperre)\n",
lock->l_pid);
break;
}
}
static void unlock(int fd, struct flock *lock) {
memset(lock, 0, sizeof(struct flock));
lock->l_type = F_UNLCK; /* Sperre aufheben */
if(fcntl(fd, F_SETLK, lock) < 0)
perror("Fehler: fcntl(fd, F_SETLK, F_UNLCK)");
else
status(lock);
}
static void locki(int fd, struct flock *lock) {
memset(lock, 0, sizeof(struct flock));
lock->l_type = F_WRLCK; // Schreibsperre
lock->l_start = 0; // kein Offset
lock->l_whence = SEEK_SET; // vom Anfang der Datei
lock->l_len = 0; // bis zum Ende der Datei
lock->l_pid = getpid(); // Prozessnummer der Sperre
if(fcntl(fd, F_SETLK, lock) < 0)
perror("Fehler: fcntl(fd, F_SETLK, F_WRLCK)");
else
status(lock);
}
int main(int argc, char **argv) {
struct flock lock;
char puffer[100];
int fd, n;
fd=open(FNAME, O_WRONLY | O_CREAT | O_APPEND, S_IRWXU);
memset(&lock, 0, sizeof(struct flock));
fcntl(fd, F_GETLK, &lock);
status(&lock);
printf("\nEingabe machen: ");
fflush(stdout);
while((n = read(STDIN_FILENO, puffer, 100)) > 1) {
locki(fd, &lock);
write(fd, puffer, n);
unlock(fd, &lock);
}
close(fd);
return EXIT_SUCCESS;
}
Das Programm bei seiner Ausführung:
$ gcc -o sperre sperre.c
$ ./sperre
Eingabe machen: Hallo Welt
F_WRLCK (pid: 1072) (Schreibsperre)
F_UNLCK (Sperre aufgehoben)
Hallo nochmals
F_WRLCK (pid: 1072) (Schreibsperre)
F_UNLCK (Sperre aufgehoben)
ENTER
Entscheidend sind in diesem Listing folgende Zeilen:
while((n = read(STDIN_FILENO, puffer, 100)) > 1) {
locki(fd, &lock);
write(fd, puffer, n);
unlock(fd, &lock);
}
Nachdem die Daten mit read() eingelesen wurden, wird erst eine Schreibsperre mit locki() eingerichtet. Somit kann im Augenblick kein anderer Prozess auf die Datei locki.lck schreibend zugreifen. Im nächsten Schritt kann also mit write() in diese Datei geschrieben werden. Nach dem Schreibvorgang kann die Datei wieder mit unlock() zum Schreiben für andere Prozesse freigegeben werden. Um Ihnen die Sperre in der Ausführung zu demonstrieren, bauen Sie jetzt nochmals am besten hinter der Funktion write() ein getchar() ein:
write(fd,puffer,n);
getchar();
Übersetzen Sie das Programm, und starten Sie es jetzt erneut:
$ gcc -o sperre sperre.c
$ ./sperre
Eingabe machen: Eine sinnvolle Zeile mehr in diesem Buch
F_WRLCK (pid: 1079) (Schreibsperre)
Jetzt wartet das Programm wegen dem getchar() auf das Drücken der (ENTER)-Taste. Dem Wunsch kommen Sie jetzt nicht nach, sondern öffnen eine weitere Konsole und starten das Programm erneut:
$ ./sperre
Eingabe machen: Was einem so einfällt
Fehler bei : fcntl(fd, F_SETLK, F_WRLCK)(Permission denied)
ENTER
Fehler : fcntl(fd, F_SETLK, F_UNLCK) (Permission denied)
$ cat > locki.lck
Mal mit cat versuchen, in locki zu schreiben
cat: write error: Permission denied
Sie sehen, dass Sie keinen Zugriff auf die Datei locki.lck haben, solange der Prozess mit der PID 1079 den Schreibschutz nicht aufhebt. Wechseln Sie also wieder in die andere Konsole, und drücken Sie (ENTER). Jetzt wird der Schreibschutz wieder aufgehoben, und in die Datei locki.lck kann wieder geschrieben werden.
Das Programm ist natürlich wenig anwenderfreundlich, denn anstatt zu versuchen, einfach einen Schreibschutz zu verhängen, sollten Sie in der Praxis schon zuvor den Status abfragen, bevor Sie irgendwelche Maßnahmen treffen.
Hinweis Sofern Sie bei dem Beispiel nicht Mandatory Locking verwendet haben, wurden mit den Advisory Locks schwache Sperren verwendet. Bei diesen schwachen Sperren findet keine Überprüfung statt, ob die Systemfunktionen open(), read() und write() bei Ihrer Ausführung durch eine Sperre verhindert werden konnten. In diesem Fall liegt der weitere Programmablauf bezüglich der Sperren in Ihren Händen.
|
Wollen Sie sich noch tiefgründiger mit diesem Thema befassen, finden Sie auf der Buch-CD noch zwei Kapitel extra dazu. Und zwar: Sperren bis zum Dateiende und Deadlocks.
Record Locking – Sperren einrichten mit lockf()
Eine etwas einfachere Methode als über fcntl() bietet die SYSV/POSIX-Funktion lockf(). Unter Linux ist lockf() zwar nur eine Schnittstelle zu fcntl(), aber der Standard gibt keine Relation zwischen den beiden vor. Mit lockf() ist es nicht möglich (so sehe ich das), zwischen Read und Write Locks zu unterscheiden; hier gibt es nur einen Typ: »Exclusive Locks«. Mit diesen kann man Segmente einer Datei für einen Prozess exklusiv sperren, das funktioniert aber nur so lange, wie alle Prozesse, die auf die Datei zugreifen, lockf() bzw. Exclusive Locks verwenden. Ein kleines Anwendungsbeispiel:
#include <unistd. h.>
lseek(fd, start, SEEK_SET);
lockf(fd, F_LOCK, 0);
Wobei start den flockzeiger.l_start und die 0 den flockzeiger.l_len aus obigem fcntl()-Unterkapitel entspricht. Mehr dazu entnehmen Sie bitte aus der Manual Page.
2.3.10 Multiplexing E/A – select()
Manchmal ist es nötig, mehrere offene Filedeskriptoren (Kanäle) abzufragen, ob Daten angekommen sind. Ein normales read() würde in diesem Fall nicht funktionieren, da dies das Programm so lange blockieren würde, bis eine Eingabe gelesen wurde. Sie könnten zwar read() im nicht blockierenden Modus (O_NONBLOCK) aufrufen und jeden Filedeskriptor einzeln überprüfen, ob Daten eingetroffen sind (was i. d. R. sehr CPU-intensiv ist), oder aber Sie verwenden die Funktion select(), die extra dazu konzipiert wurde.
Ein Beispiel in der Praxis wäre ein Server, der mehrere Verbindungsendpunkte über ein FIFO oder eine named pipe gleichzeitig bedienen soll – oder gar ein Webserver, wie dieser im Kapitel zur Netzwerkprogrammierung auch erstellt wird. Würden Sie hier z. B. blockierende Funktionen verwenden, kann es recht ineffizient sein und ein wenig dauern, bis Daten eintreffen. Und was ist mit den Verbindungsendpunkten (Filedeskriptoren), an denen schon längst Daten angekommen sind?
Hierzu die Syntax der Funktion select() und die dazugehörenden Makros:
#include <sys/select.h>
int select(int mp_fd, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);
Die Funktion select() wartet bei einer Reihe von Filedeskriptoren darauf, dass sich der Zustand von mindestens einem ändert. Dabei wird zwischen drei unabhängigen Mengen von Filedeskriptoren unterschieden:
|
readfds – Es wird geprüft, ob Zeichen zum Lesen angekommen sind. |
|
writefds – Es wird geprüft, ob Zeichen geschrieben werden können. |
|
exceptfds – Es wird geprüft, ob ein außergewöhliches Ereignis vorgekommen ist. |
Die Makros werden zur Bearbeitung der drei Mengen verwendet. Vor jedem Aufruf von select() muss die Menge mit FD_ZERO() gelöscht werden. Einen Filedeskriptor zur Menge können Sie mit FD_SET() hinzufügen und mit FD_CLR()entfernen.
Nachdem Sie alle zu überwachenden Deskriptoren den gewünschten Mengen hinzugefügt haben, können Sie die Funktion select() aufrufen. Diese ändert die Mengen, so dass nur noch die Deskriptoren darin vorhanden sind, von denen entweder gelesen oder zu denen geschrieben werden kann bzw. wo etwas passiert ist. Wollen Sie wissen, ob ein Deskriptor noch in der Menge vorhanden ist, können Sie dies mit dem Makro FD_ISSET() abfragen.
Neben den Mengen werden zwei weitere Parameter an select() übergeben:
|
n – Hier geben Sie die höchste Nummer (plus 1) des in der Menge vorhandenen Filedeskriptors an. Dabei sind alle drei Mengen gemeint. Es schadet nicht, eine höhere Nummer anzugeben (z. B. gleich OPEN_MAX), jedoch braucht der Kernel ein paar Ticks länger, um die größere Anzahl an Deskriptoren zu überprüfen. Verfallen Sie daher nicht darauf, nur OPEN_MAX zu verwenden. |
|
timeout – Damit geben Sie an, wie lange select() warten soll, bis sich einer der Deskriptoren geregt hat. Geben Sie hierbei NULL an, blockiert select() so lange, bis dies passiert ist. Geben Sie für beide Werte der Struktur timeval 0 an, kehrt select() nach dem Überprüfen aller vorhandenen Filedeskriptoren sofort wieder zurück. Hiermit wird praktisch echtes Polling ohne Blockieren erreicht. Die dritte Möglichkeit ist gegeben, wenn Sie eine oder beide Strukturvariablen mit einem Wert initialisieren. select() kehrt dann zurück, wenn die mit timeout angegebene Zeit abgelaufen ist oder wenn ein Filedeskriptor aus der Menge bereit für eine Ein-/Ausgabe ist (oder select() durch ein Signal unterbrochen wurde). |
Bei Erfolg gibt select() die Anzahl der Filedeskriptoren zurück, deren Status sich geändert hat. Wenn ein Fehler aufgetreten ist, wird -1 zurückgegeben und errno entsprechend gesetzt. Die Mengen und timeout befinden sich dann in einem undefinierten Zustand (sind z. B. nur teilweise gefüllt), auf deren Inhalt sollte man sich also bei einem Fehler nicht mehr verlassen. (timeout soll nach select() bis zu einer potenziellen Wiederbelegung für den nächsten select()-Aufruf nicht mehr gelesen werden. Siehe Manual –Page.) Wird hingegen 0 zurückgegeben, bedeutet dies, dass kein Filedeskriptor für die Ein-/Ausgabe bereit ist. 0 tritt auch auf, wenn der Timeout abgelaufen ist, bevor ein Filedeskriptor bereit war.
Hinweis Bei richtiger Anwendung kann mit der Funktion select() ein einfaches Multiplexing erzeugt werden. Dies ist ansonsten nur möglich, wenn man noch weitere Prozesse (mit fork()) oder Threads startet.
|
Ein tiefgründiges Listing hierzu würde dem Leser, der noch nicht mit dem Thema vertraut ist, bis zum jetzigen Kapitel ein wenig zu viel abverlangen. Hierauf wird im Kapitel der Netzwerkprogrammierung noch eingegangen. Das folgende Beispiel demonstriert, wie Sie die Standardeingabe »pollen« können. Dabei blockiert select() so lange, bis Sie (nach einer Eingabe) die (ENTER)-Taste gedrückt haben. Wenn die (ENTER)-Taste nicht binnen fünf Sekunden gedrückt wurde, bricht der eingerichtete Timeout select() ab. Hier das Listing:
/* poll_stdin_time.c */
#include <sys/types.h>
#include <sys/time.h>
#include <stdio.h>
#include <unistd. h.>
static int poll_stdin_time(int sekunden) {
struct timeval timeout;
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(STDIN_FILENO, &read_fds);
timeout.tv_sec = sekunden;
timeout.tv_usec = 0;
return select( STDIN_FILENO + 1, &read_fds, NULL,
NULL, &timeout );
}
/* oder im C99-Standard:
static int poll_stdin_time(int sekunden) {
struct timeval timeout={.tv_sec = sekunden, .tv_usec = 0};
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(STDIN_FILENO, &read_fds);
return select( STDIN_FILENO + 1, &read_fds, NULL,
NULL, &timeout );
}
*/
int main(void) {
int ret;
printf("Warte 5 Sek. auf Eingabe (ENTER-Taste)!\n");
ret = poll_stdin_time(5);
if(ret == 0)
printf("Zeit abgelaufen\n");
else if(ret == 1)
printf("Danke!!!\n");
else
perror("select()");
return 0;
}
Das Listing bei der Ausführung:
$ gcc -o poll_stdin_time poll_stdin_time.c
$ ./poll_stdin_time
Warte 5 Sek. auf Eingabe (ENTER-Taste)!
ENTER
Danke!!!
$ ./poll_stdin_time
Zeit abgelaufen
2.3.11 Unterschiedliche Operationen – ioctl()
Der ioctl()-Systemaufruf ist eine Funktion, die für viele Arten von E/A-Operationen (und nicht nur dafür) einsetzbar ist. Hier die Syntax dazu:
#include <unistd. h.> /* SVR4 */
#include <sys/ioctl.h> /* Linux, BSD */
int ioctl( int fd, int operation ...);
Als erstes Argument geben Sie den Filedeskriptor an, auf den die Operation durchgeführt werden soll, die als zweites Argument angegeben wird. Schlägt der Systemaufruf fehl, gibt ioctl() den Wert -1 zurück. Zu den wirklich unzählig vielen Operationen möchte ich Sie vorerst auf die Man-Pages (ioctl_list(2)) verweisen. Auf die Funktion ioctl() wird noch intensiver im Kapitel zu den Devices eingegangen.
Sie können mit dieser Funktion praktisch auf einfachste Weise auf die Hardware im System zugreifen, ohne direkt auf Hardware-Ebene programmieren zu müssen. Hierzu ein einfaches Beispiel, das demonstriert, wie Sie auf Ihr CD-ROM-Laufwerk zugreifen und die CD auswerfen (Eject) können.
/* openCD.c */
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd. h.>
#include <linux/cdrom.h>
/* Bitte anpassen, falls notwendig */
#define CDROM "/dev/cdrom"
int main(int argc, char **argv) {
int fd, rc;
if((fd = open(CDROM, O_RDONLY)) < 0) {
fprintf(stderr, "Konnte %s nicht öffnen\n",CDROM);
return 1;
}
if((rc = ioctl(fd, CDROMEJECT)) != 0) {
perror("Konnte CD-ROM nicht auswerfen\n");
return 1;
}
close(fd);
return 0;
}
Im Beispiel wird davon ausgegangen, dass sich das CD-ROM-Laufwerk an einem IDE-Anschluss befindet (/dev/hdc ). Sofern Sie über einen SCSI- oder sonstigen Anschluss das CD-ROM-Laufwerk betreiben, passen Sie die symbolische Konstante bitte Ihren Bedürfnissen an.
Hinweis ioctl() ist nur Bestandteil von SVR4 und BSD, nicht aber von POSIX.1.
|
2.3.12 Lesen und Schreiben mehrerer Puffer – writev() und readv()
Spezielle Lese- und Schreibfunktionen stehen Ihnen mit den POSIX-Funktionen readv() und writev() zur Verfügung. Mit diesen Funktionen ist es möglich, mehrere nicht zusammenhängende Puffer auf einmal zu schreiben oder zu lesen. Es liegt somit auf der Hand, dass ein einzelner writev()- oder readv()-Aufruf wesentlich schneller ist als zwei oder mehrere write()- oder read()-Aufrufe.
Das Prinzip dieser Funktion ist recht einfach und würde sich auch relativ leicht selbst nachbilden lassen. Beim Lesen mit writev() werden Daten in einem speziellen Puffer (genauer in der Struktur iovec) gesammelt und dann mit einem Rutsch mit writev() in eine Datei geschrieben.
Ähnlich ist dies auch bei der Funktion readv(). Hierbei werden die einzelnen Daten aus einer Datei gelesen und nacheinander auf die einzelnen Puffer verteilt. Hierzu die Syntax dieser beiden Funktionen:
#include <sys/uio.h>
ssize_t readv( int fd, const struct iovec *iov,
int anz_iov );
ssize_t writev( int fd, const struct iovec *iov,
int anz_iov );
Als erstes Argument übergeben Sie diesen Funktionen einen geöffneten Filedeskriptor. Das zweite Argument ist die Adresse eines Arrays, genauer eines Strukturarrays mit dem Datentyp struct iovec, der wie folgt aussieht:
struct iovec {
void *iov_base; // Anfangsadresse des Puffers
size_t iov_len; // Länge des Puffers
};
Mit dieser Struktur ist es dann möglich, Daten unterschiedlichen Typs (da void*) und Länge auf einmal zu schreiben bzw. zu lesen.
Beide Funktionen geben die Anzahl erfolgreich gelesener bzw. geschriebener Bytes zurück. Bei einem Fehler wird -1 zurückgeliefert. Erreicht readv() das Dateiende oder sind keine Daten mehr zum Lesen vorhanden, wird 0 zurückgegeben.
Hierzu ein einfaches Beispiel, das demonstriert, wie Sie den Puffer mit Daten füllen und auf einmal in eine Datei schreiben können.
/* write_vec.c */
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd. h.>
#define MAX 3 // Anzahl der Elemente
int main(int argc, char **argv) {
mode_t mode = S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH;
const char *str[] = {"Ein Teststring\n", "Noch einer\n",
"Der Letzte im Bunde\n"};
struct iovec vec[MAX];
ssize_t length = 0;
int fd, i;
//unlink("test");
umask(0);
fd = open("test", O_WRONLY | O_CREAT | O_EXCL, mode);
if(fd == -1) {
perror("open()");
return EXIT_FAILURE;
}
// Einzelne Daten in die Struktur legen
for(i = 0; i < MAX; ++i) {
vec[i].iov_base = (void *)str[i];
vec[i].iov_len = strlen(str[i]);
length += strlen(str[i]);
}
// Jetzt in die Datei schreiben
if(writev(fd, vec, MAX) != length) {
perror("writev()");
return EXIT_FAILURE;
}
close(fd);
return EXIT_SUCCESS;
}
Das Programm bei seiner Ausführung:
$ gcc -o write_vec write_vec.c
$ ./write_vec
$ cat test
Ein Teststring
Noch einer
Der Letzte im Bunde
2.3.13 Übersicht zu weiteren Funktionen, die den Filedeskriptor verwenden
Die Low-Level-Funktionen im Kapitel zuvor wurden zwar schon recht flott durchgenommen, aber jetzt ist es an der Zeit, noch ein wenig mehr auf das Gaspedal zu treten. Die wichtigsten kennen Sie jetzt zumindest ansatzweise und wissen, wozu diese zu verwenden sind – was auch zur Voraussetzung der weiteren Kapitel nötig ist. Aber schließlich wollen Sie noch mehr lernen, als nur die trockene Theorie zu pauken. Außerdem hat Linux noch eine ganze Menge mehr zu bieten.
fsync()
Verwenden Sie beim Systemaufruf open() das Flag O_SYNC, wird bei jedem Schreiben aller sich noch in Puffercaches befindlichen Daten auf die Beendigung des Schreibvorgangs gewartet. Gleiches gilt für nachträgliches Setzen des Flags O_SYNC mit fcntl(). Wollen Sie allerdings nicht, dass auf jeden Schreibvorgang gewartet wird, sondern nur dann, wenn Sie es wollen (z. B. bei extrem kritischen Daten), können Sie die Funktion fsync() verwenden. Damit wird nur zum Zeitpunkt des Aufrufs einmal so verfahren, vorausgesetzt, Sie haben die Datei nicht im O_SYNC-Modus geöffnet. Hierzu die Syntax der Funktion:
#include <unistd. h.>
int fsync(int fd) ;
Die Funktion fsync() benötigt als Argument den Filedeskriptor, auf dem die Aktion ausgeführt werden soll. Ist der Aufruf erfolgreich, wird 0 zurückgegeben, ansonsten bei einem Fehler -1.
ftruncate()
Um eine Datei zu beschneiden, steht Ihnen die Funktion ftruncat() zur Verfügung.
#include <sys/types.h>
#include <unistd. h.>
int ftruncate( int fd, off_t cut );
Damit wird die Datei mit dem Filedeskriptor auf cut Bytes beschnitten. Hat die Datei mehr als cut Bytes, wird diese Datei gekürzt. Der Rest der Datei ist somit nicht mehr verfügbar. Hat die Datei weniger als cut Bytes, ist das Verhalten systemabhängig. In SVR4 z. B. wird die Datei um cut Bytes verlängert. Bei BSD hingegen passiert in diesem Fall gar nichts. Linux füllt die Datei mit Null-Bytes auf (also ähnlich wie bei SVR4), jedoch beansprucht eine solch erweiterte Datei (noch) keinen weiteren Festplattenplatz:
ftruncate(fd, 1048576 * 100);
Angenommen, man würde eine Datei unter 100 MB mit diesem Befehl bearbeiten, so würde »ls« die 100 MB anzeigen, »du« jedoch immer noch den tatsächlichen verbrauchten Platz. Dies ist möglich, da per Definition ein Erweitern über ftruncate() mit Null-Bytes gepaddet wird. Da Sie dies wissen, können Sie diese Bytes auch einsparen, bis sie mit etwas anderem gefüllt werden. Diese Technik nennt sich »Sparse Files« (die einzige Möglichkeit, eine »478-GB«-Datei auf einem 20-GB-Datenträger anzulegen). Dabei können solche »Lücken« nicht nur am Dateiende existieren, z. B. durch:
/* fangen wir mit irgendeiner 0-bytigen Datei an */
ftruncate(fd, 0);
ftruncate(fd, 1048576 * 100);
lseek(fd, 1048576, SEEK_SET);
putc('x', fd);
Somit hätten Sie von 0–1048575 und von 1048577 (in Wirklichkeit 1048576 + ein bisschen weiter) bis zum Dateiende eine Lücke.
Hinweis Oben genannte 478 GB sind natürlich Wahnsinn und sollten allein dem Zweck dienen, die Funktionalität von ftruncate() zu erläutern. Kids, don't do this at home, denn auch eine Datei von tatsächlicher Größe 0 belegt ein paar administrative Inodes, die beim Löschen erst durchsucht und analysiert werden müssen.
|
Bei einem Fehler liefert diese Funktion -1 und bei Erfolg 0 zurück.
Der Aufruf der Funktion im Stile
ftruncate(fd , 0);
kommt einem Aufruf der Funktion open() mit dem Flag O_TRUNC gleich. In beiden Fällen wird der Inhalt der Datei gelöscht. ftruncate() ist jedoch vorzuziehen, wenn die Datei nur einmal geöffnet werden kann (z. B. aus speziellen Locking-Gründen oder Race Conditions).
Mit Race Conditions ist z. B. gemeint, wenn man ein Gerät (wie /dev/dsp) nur einmal öffnen kann (gilt nur für OSS, nicht ALSA), man es aber nicht riskieren kann, den fd zu verlieren, weil andere potenzielle Anwendungen open()versuchen.
truncate()
Nebst ftruncate() gibt es auch noch truncate(), das als ersten Parameter statt eines Deskriptors einen Dateinamen erwartet.
fstat() und fchmod(), chmod()
Das Erfragen von Dateiattributen können Sie mit der Funktion fstat() realisieren. Die Funktion wird in einem extra vorgesehenen Kapitel (Kapitel 3) behandelt. Es wird dann zwar die Version der höheren Ebene verwendet, aber außer dem ersten Argument, was bei der niedrigeren Ebene ein Filedeskriptor und bei der höheren Ebene ein Pfadname ist, besteht dabei kein Unterschied. Hierzu die Syntax der Funktion:
#include <sys/types.h>
#include <sys/stat.h>
int fstat(int fd, struct stat *sb);
Verlief der Funktionsaufruf erfolgreich, wird 0, ansonsten bei einem Fehler -1 zurückgegeben.
Um die Zugriffsrechte einer Datei zu ändern, stehen Ihnen die Funktionen fchmod() und chmod() zur Verfügung.
#include <sys/types.h>
#include <sys/stat.h>
int fchmod(int fd, mode_t mode);
int chmod(const char *name, mode_t mode);
Damit ändern Sie die Zugriffsrechte auf dem offenen Filedeskriptor, die Sie mit mode angeben. Dabei können Sie zum Verändern der Zugriffsrechte dieselben Konstanten und dieselbe Vorgehensweise verwenden, die Sie bereits bei der Funktion open() (Abs. 2.3.1) kennen gelernt haben. Beim erfolgreichen Verändern der Rechte wird 0, ansonsten bei einem Fehler -1 zurückgegeben. chmod() funktioniert genauso wie fchmod(), nur wird als erstes Argument der Dateiname (eventuell mitsamt Pfad) anstatt eines Filedeskriptors angegeben.
Hinweis Natürlich dürfen echte chmod’er anstatt der symbolischen Konstanten auch die oktale Version der Zugriffsrechte verwenden, z. B.: fchmod(fd, 0644)
|
fileno()
Benötigen Sie zu einem offenen FILE-Stream einen Filedeskriptor, können Sie die Funktion fileno() verwenden:
#include <stdio.h>
int fileno(FILE *fp);
Bei erfolgreicher Ausführung von fileno() erhalten Sie den Integer-Filedeskriptor zum Stream. fp. fileno() ist z. B. erforderlich, falls eine Datei mit fopen() geöffnet wurde, um den Stream für Funktionen einzusetzen, die einen Filedeskriptor benötigen.
Bei fileno() handelt es sich natürlich um eine Funktion der höheren Ebene (High Level).
fdopen()
Die Funktion fdopen() ist das Gegenstück der Funktion fileno(). Damit erhalten Sie aus einem geöffneten Filedeskriptor einen FILE-Stream. Die Syntax hierzu:
#include <stdio.h>
FILE *fdopen(int fd, const char *modus);
Als modus, wie die Datei geöffnet wird, können dieselben Modi wie bei der Funktion open() verwendet werden.
fdopen() wird gewöhnlich auf Filedeskriptoren verwendet, die von Funktionen, die Pipes und Kommunikationskanäle in Netzwerken einrichten, zurückgegeben werden. Das kommt daher, weil einige Funktionen (open(), dup(), dup2(), fcntl(), pipe() ...) in Netzwerken nichts mit Streams anfangen können und Filedeskriptoren benötigen. Um aber wieder aus Deskriptoren einen Stream (FILE-Zeiger) zu erzeugen, ist die Funktion fdopen() erforderlich.
Im Gegensatz zu fileno(), der »nur« den FD-Integer zurückgibt, erstellt fdopen() ein neues FILE-Objekt, weshalb man fdopen() nur einmal aufrufen sollte, da es ja die Parallelfunktion zu fopen() ist.
|