Die Posix-Programmierschnittstelle (Übersicht)

(Portable Operating System Interface for Unix)


a) File I/O (ungepuffert)

b) Files and Directories

c) Standard I/O Library (gepuffert)

d) System Data Files (z.B. /dev/null à wenn hier hineingeschrieben wird, verschwindet das

Geschriebene à „Papierkorb“

/etc/passwd )

e) Environment of an Unix Process

f) Process Groups/Relationships

g) Signals

h) Terminal I/O

i) Daemon Process

j) Advanced I/O

k) Interprocess Communication

l) Advanced IPC

m) Pseudo Terminal

  1. Process Control


File I/O (a)

Folgende Funktionen werden oft Funktionen zur ungepufferten Ein- /Ausgabe benannt, da jeder Aufruf von read() oder write() einen Systemaufruf in den Kern auslöst.


Grundthema: Pathnames <=> Filedescriptor

Notwendig aus Performance-Gründen: rein theoretisch könnte man bei jedem Systemaufruf, z.B. read() oder write(), einen Pfadnamen angeben.

Ebenso könnte jeder I/O-Systemaufruf etwa in Form eines Plattenzugriffs ausgeführt werden, heute Platten sind aber um Größenordnungen langsamer als Hauptspeicher-Zugriffe.

Lösung: man unterscheidet zwischen den (statischen) Daten auf der Platte <=> dynamische Pufferung im Hauptspeicher.

Nachteil: bei Stromausfall eventuell Daten weg!

Zugriff erfolgt über ein Handle (Filedescriptor), das zur Laufzeit erzeugt (begrenzte Gültigkeitsdauer => unabhängiger Namensraum) wird und eine Datei identifiziert.

(in Unix: nicht negative Ganzzahl).


Zwei verschiedene Prozesse können durch ihre jeweils eigenen Filedescriptoren dieselbe Datei zur selben Zeit öffnen! => Synchronisationsproblem

Derselbe Prozess kann aber auch zwei verschiedene Filedescriptoren besitzen, auf dieselbe Datei verweisen.


Vordefinierte Filedescriptoren: (siehe <unistd.hh)


#define STDIN_FILENO 0 -|

#define STDOUT_FILENO 1 |- <unistd.h>

#define STDERR_FILENO 2 -|


  1. int open (const char* pathname,

int oflag,

/*mode_t mode*/);

Werte für oflag:


min. eins:


O_RDONLY


O_WRONLY

(nur für das eine Filehandle, nicht für die Datei)

O_RDWR




beliebig:


Atomar {

O_APPEND

positioniert ans Ende, arbeitet atomar!

(atomar für alle Schreiboperationen, jede OP hängt hinten an, kein Überschreiben!)

à Bsp.: Logfile mehrerer Daemons. Alle schreiben parallel in die gleiche Datei. Die eine OP muss intern abgeschlossen sein, bevor die nächste startet.

O_CREAT

erstellt, falls nicht existiert

O_EXCLC

liefert Fehler, wenn Datei bereits existiert => atomar!

(nur wenn auch die Option O_CREATE angeben wurde)

O_TRUNC

löscht Dateiinhalt (Länge wird auf 0 gesetzt) => atomar

(falls die Datei existiert und erfolgreich zum Schreiben ohne Lesezugriff oder Lesen und Schreiben geöffnet wird)

O_NONBLOCK

Spezialmodus (nicht blockierend, I/O läuft asynchron im Hintergrund)

O_SYNC

Synchrones „Durchschreiben,“Aufrufer wird garantiert blockiert bis Daten geschrieben.


è liefert den niedrigsten noch nicht verwendetet Filedescriptor zurück, das wird auch von älteren Programmen ausgenutzt.

êReturncode < 0 bedeutet Fehler!


  1. int creat (const char* pathname,

mode_t mode);

entspricht::

open(pathname, O_WRONLY|O_CREAT|O_TRUNC, mode);

Ein Nachteil dieser Funktion ist, dass Dateien nur zum Schreiben geöffnet werden.

Besser:

open(pathname, O_RDWR|O_CREAT|O_TRUNC, mode);

(Lesen und Schreiben | Datei erstellen, wenn nicht existiert | Länge auf 0 setzten)


  1. int close (int filedes);


Beim Schließen einer Datei werden auch alle Datensatzsperren aufgehoben, die ein Prozess in der Datei vorgenommen hat. Wenn ein Prozess beendet wird, schließt der Systemkern automatisch alle geöffneten Filedescriptoren.

  1. off_t lseek (int filedes,
    off_t offset,
    int whence);

Werte für whence: (whence: von wo aus gerechnet?)

SEEK_SET à der Wert des Parameters offset wird zum Dateianfang addiert.

SEEK_CUR à der Wert des Parameters offset (positiv od. negativ) wird zur aktuellen Position addiert

SEEK_END à der Wert des Parameters offset (positiv od. negativ) wird zur Dateigröße addiert


Verschiebt den Positionszeiger auf eine andere Stelle. Grundidee: jeder Filedescriptor besitzt einen eigenen Positionszeiger, aber Achtung, davon gibt es Ausnahmen (siehe spätere Folie)!

ê Man darf hinter EOF positionieren, anschließendes write() erzeugt ein Loch“ => sparse file

(nicht geschrieben Bytes in der Datei werden als 0 gelesen)


  1. ssize_t read (int filedes,
    void* buff,

size_t nbytes);


Es wird am aktuellen Datei-Offset mit dem Lesen begonnen (siehe lseek()). Vor der Rückgabe des Ergebnisses, wird der Offset-Wert um die Anzahl der gelesenen Bytes erhöht.

Rückgabe: Anzahl gelesener Bytes, 0 = EOF, -1 Fehler


  1. ssize_t write (int filedes,
    void* buff,

size_t nbytes);


Der Rückgabewert hat in der Regel den selben Wert wie der Parameter nbytes. Ist dies nicht der Fall, ist ein Fehler aufgetreten (z.B. Platte voll). Nach erfolgreichem Schreiben wird der aktuelle Datei-Positionszeiger um die Anzahl der geschriebenen Byte erhöht. Wenn die Option O_APPEND im Aufruf der Funktion open()verwendet wird, wird der aktuelle Datei-Offset vor jedem Schreiben auf das Dateiende gesetzt.

  1. int dup (int filedes);

int dup2 (int filedes,

int filedes2);

Ein bestehender Filedescriptor lässt sich mit diesen beiden Funktion duplizieren. dup() liefert den niedrigsten noch nicht verwendetet Filedescriptor zurück, bei dup2() können wir den Wert des neuen Filedescriptor mit filedes2 festlegen; falls der durch filedes2 bezeichnete Descriptor bereits geöffnet ist, wird er zuerst geschlossen. Dabei zeigt der neue Filedescriptor auf den selben Eintrag in der Dateitabelle wie der mit dem Parameter fildes angebende Filedescriptor. Jeder Descriptor ist jedoch mit einer eigenen Gruppe von Filedescriptor-Flags ausgestattet.



  1. int fcntl (int filedes,
    int cmd,
    ... );
    Ändert Eigenschaften des Filedescriptors (teilweise implementierungsspezifisch). Zum Beispiel das Close-On-Exec Flag, das einen Descriptor beim Starten eines neuen Programms durch exec() schließt.

  2. int ioctl (int filedes,
    int request,
    ... );
    Ändert Eigenschaften des Dateninhalts, vor allem bei Geräten, oft stark geräteabhängig.

Files and Directories (b)


  1. int stat ( char* pathname,

struct stat* buf);

Diese Funktion gibt eine Datenstruktur mit Informationen über die durch den Parameter pathname bezeichnete Datei aus.


int fstat ( int filedes,
struct stat* buf);

Diese Funktion ermittelt Informationen über die Datei, die unter dem Filedescriptor filedes bereits geöffnet ist. Das f bei fstat steht für den Filedescriptor!

int lstat (const char* pathname,

struct stat* buf);

Diese Funktion ähnelt der Funktion stat; wenn es sich aber bei der bezeichneten Datei jedoch um einen symbolischen Verweis handelt, gibt sie Informationen über den Verweis und nicht über die Datei!
à Symlink

struct stat {
mode_t st_mode; Dateityp und Modus (Zugriffsberechtigungen)
ino_t st_ino; i-node number (Dateisystem-interne Seriennummer)
dev_t st_dev; device number (filesystem) [Gerätenummer (Dateisystem)]
dev_t st_rdev; device number for special files
nlink_t st_nlink; number of links (Anzahl der Hardlink-Verweise)
uid_t st_uid; user ID of owner
gid_t st_gid; group ID of owner
off_t st_size; size in bytes for regular files (Größe in Bytes)
time_t st_atime; time of last access (letzter Zugriff: read())
time_t st_mtime; time of last modification (letzte Änderung: write())
time_t st_ctime; time of last status change (Dateistatusänderung: chown(), chmod())
long st_blksize; best I/O block size (geeignetste I/O-Blockgröße)
long st_blocks; number if 512-byte blocks allocated (Anzahl der zugewiesenen 512 Byte großen Blöcke)
};


Dateitypen (Dateitypmakros aus <sys/stat.h>):

Regular File S_ISREG()

Die Interpretation des Inhalts einer regulären Datei wird der Anwendung überlassen. Dem Systemkern ist es also egal, ob die Dateien Text- oder Binärdaten enthalten.

Directory File S_ISDIR()

Verzeichnis. Früher (im Ur-Unix der 70er Jahre) war das intern eine Datei, die den Namen von anderen Dateien und Zeiger (inode-Nummern) auf die inodes dieser Dateien enthielt. Heute sind Verzeichnisse intern als eigenständige Datenstrukturen realisiert.

Character Device S_ISCHR()

Zeichenorientierte Gerätedateien, z.B. serielle Schnittstellen, Tastatur, etc. Ein Spezial-Dateityp, der für bestimmt Gerätetypen des Systems verwendet wird.

Block Device S_ISBLK()

Blockorientierte Dateien. Ein Spezial-Dateityp, der für gewöhnliche bei Festplatten- oder Diskettenlaufwerken verwendet wird.

Unix war eines der ersten Betriebssysteme, das Geräte wie »normale« Dateien behandelt. Die Geräte sind üblicherweise im Verzeichnis /dev versammelt.

FIFO (named pipe) S_ISFIFO()

Ein Pseud-Dateityp, der für die Kommunikation zwischen Prozessen verwendet wird.

Socket S_ISSOCK()

Ein Dateityp, der für die Netzwerkkommunikation zwischen Prozessen verwendet wird.

Symlinks S_ISLINK()

Symbolische Verweise (symlink im Fachjargon). Ein Pseudo-Dateityp, der auf den Namen einer anderen Datei verweist.



Verwendung der Makros:

[…]

stat (/home/user/test, &buf)

if (S_ISREG(buf.st_mode)

cout << “regular file!”;

if (S_ISDIR(buf.st_mode)

cout << “directory!”;

[…]


  1. int access (const char* pathname,
    int mode);

mode-Konstanten:

R_OK Leseberechtigung prüfen

W_OK Schreibberechtigung prüfen

X_OK Ausführungsberechtigung prüfen

F_OK Vorhandensein prüfen


Diese Funktion überprüft die Zugriffsberechtigung anhand der tatsächlichen Benutzernummer und der tatsächlichen Gruppennummer. Testet ob Operation möglich (mit read userid). Diese Funktion ist anzuwenden, wenn man vor einem open()-aufruf wissen möchte, ob die richtigen Rechte gesetzt sind.

  1. mode_t umask (mode_t cmask);
    Setzt default Permissions eines Prozesses für das Erzeugen neuer Dateien und Verzeichnisse.
    Bei den Funktionen open() und creat() können mit dem Parameter mode die Zugriffsberechtigungen gesetzt werden. Bits, die in der Bitschutzmaske gesetzt sind, werden mit dem Parameter mode für die Datei deaktiviert.

    Beispiel:

[..]

umask (S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);


creat (“test”, S_IRUSR | S_IWUSR | S_IRGRP |

S_IWGRP | S_IROTH | S_IWOTH);

[…]

Die Bits für Lesen (Gruppe, Andere) und Schreiben (Gruppe, Andere) werden dadurch beim Aufruf von creat() deaktiviert => die Datei test hat nur die Rechte für Lesen (Benutzer) und Schreiben (Benutzer)


  1. int chmod (const char* pathname,
    mode_t mode);
    int fchmod ( int filedes,
    mode_t mode);

Diese Funktionen können die Zugriffsberechtigungen bei bestehenden Dateien ändern. chmod bearbeitet die mit dem Parameter pathname bezeichnete Datei, während die Funktion fchmod eine bereits geöffnete Datei bearbeitet.

  1. int chown (const char* pathname,
    uid_t owner
    gid_t group);

    int fchmod ( int filedes,
    uid_t owner,
    gid_t group);

    int lchown (const char* pathname,
    uid_t owner,
    gid_t group);

Diese Funktionen erlauben uns, die Benutzernummer und die Gruppennummer von Dateien zu verändern.

chown => pathname

fchown => Bereits geöffnete Datei.

lchown => Der Eigentümer des Verweises wird verändert, nicht die Datei auf die er verweist!

  1. int truncate (const char* pathname,
    int ftruncate ( int filedes,
    off_t length);

    Verkürzt Dateiinhalt. Hierbei werden Daten vom Ende der Datei abgeschnitten. Bestehende Dateien werden auf length Byte verkürzt. War die vorherige Größe der Datei größer als length, kann auf die Daten hinter length nicht mehr zugegriffen werden. Ist die Dateigröße kleiner als length, ist das Ergebnis systemabhängig.
    (Siehe open (…, O_RDWR | O_TRUNC |…)
    à Sonderfall, der eine Datei auf 0 verkürzt!)

  2. int link (const char* existingpath,
    const char* newpath);
    Erzeugt Hardlinks. Beim Hardlink existieren mehrere Namen im Namensraum des Dateisystems, die auf die gleiche inode-Nummer und damit auf den identischen Datei-Inhalt verweisen.

  3. int unlink (const char* pathname);
    entfernt den
    angegebenen Namen aus dem Namensraum. Falls weitere Hardlinks bestehenbleiben, wird der Inhalt der Datei dadurch nicht gelöscht. Der von der Datei eingenommene Speicherplatz auf der Platte wird erst dann freigegeben, wenn kein Hardlink mehr vorhanden ist, der darauf verweist, und wenn ausserdem keine Filedescriptor mehr offen ist.

    ê funktioniert auch mit geöffneten Dateien!

    Anwendung bei Temp-Dateien, die dann automatisch beim Beenden der Anwendung gelöscht werden und nicht als Leichen übrig bleiben (z.B. kill -9). Der Datei-Inhalt wird nach dem Aufruf von unlink nicht gelöscht, da er immer noch von der Anwendung geöffnet ist. Der Inhalt darf sogar überschrieben und vergrössert werden. Nur wenn die Anwendung mit der Funktion close() die Datei schließt oder terminiert (in diesem Fall schließt der Systemkern alle geöffneten Dateien) wird die Datei gelöscht. Falls pathname einen symbolischen Verweis bezeichnet, bezieht sich die Funktion unlink auf den Verweis und nicht auf die Datei.


    int rename (const char* oldname,
    const char* newname);
    ê atomar, während der Änderung sind parallele Namensänderungen gesperrt!

    Es gibt drei Fälle zu unterscheiden:

  1. Falls oldname eine Datei bezeichnet, die kein Verzeichnis ist, wird die Datei umbenannt. In diesem Fall kann newname, falls dieser Parameter angegebenen wird, kein Verzeichnis bezeichnen. Falls der mit newname angegebene Name existiert (kein Verzeichnis darstellt), wird er entfernt und oldname wird in newname umbenannt. Wir müssen in dem Verzeichnis, das oldname enthält, und in dem Verzeichnis, das newname enthält, über ein Zugriffsberechtigung zum Schreiben verfügen da wir beide Verzeichnisse verändern.

  2. Falls oldname ein Verzeichnis bezeichnet, wird das Verzeichnis umbenannt. Falls der mit newname angegebene Name existiert, muss er ein leeres Verzeichnis bezeichnen. Wenn newname existiert (und ein leeres Verzeichnis ist), wird es entfernt und oldname wird in newname umbenannt. Verzeichnisse können auch nur dann umbenannt werden, wenn newname nicht dieselbe Pfadangebe wie oldname enthält. Bsp: /usr/foo => /usr/foo/testdir funktioniert nicht!

  3. Ein Sonderfall liegt vor, wenn oldname und newname dieselbe Datei bezeichnen. In diesem fall kehrt die Funktion fehlerfrei zurück und nimmt keine Änderungen vor.


  1. int symlink (const char* actualpath,
    const char* sympath);
    int readlink (const char* pathname,
    char* buff,
    int bufsize);


Im Unterschied zu den Hardlinks, die direkt auf eine inode einer Datei zeigen, stellen symbolische Verweise indirekte Zeiger auf Datei-Namen dar. Mit der Einführung von symbolischen Verweisen wollte man Beschränkungen von Hardlinks umgehen:

    1. bei Hardlinks ist es normalerweise erforderlich, dass der Verweis und die inode sich im selben Dateisystem befinden.

    2. Früher konnte der Supervisor mit Hilfe »schmutziger Tricks« Hardlinks auf Verzeichnisse definieren, heute geht das generell nicht => Gefahr von Endlos-Rekursion, wenn in einem Unterbaum ein Hardlink-Directory auf einen Vaterknoten zeigen würde.

Symbolische Verweise werden normalerweise dazu verwendet, um eine Datei oder eine ganze Verzeichnisstruktur virtuell an eine andere Position im System zu verlagern bzw. dort zu spiegeln.


symlink(): Der symbolischer Verweis wird auch dann erstellt, wenn die mit actualpath angegebenen Datei nicht existiert. Da die Funktion open() symbolische Verweise zurückverfolgt, brauchen wir ein Verfahren, mit dem wir den Verweis selbst öffnen und den im Verweis enthaltenen Namen lesen können. Die Funktion readlink() erfüllt diese Anforderungen. Wenn die Funktion fehlerfrei ausgeführt wird, gibt sie die Anzahl der gelesenen Bytes in buf aus. Die Daten des symbolischen Verweises, die in buf zurückgegeben werden, sind nicht nullterminiert.

Folgende Funktionen verfolgen einen symbolischen Verweis zurück, d.h. der übergebene Pfadname (an die Funktion) bezieht sich auf die Datei, auf die der Verweis zeigt:

access()

chdir()

chmod()

creat()

chown() (implementierungsabhängig)

  1. int mkdir (const char* pathname,
    mode_t name);
    int rmdir (const char* pathname);


mkdir() erstellt ein neues, leeres Verzeichnis. Bei den im mode angegebenen Zugriffsberechtigungen sollte darauf geachtet werden, dass zumindest eines der Ausführungsbits aktiviert ist, um den Zugriff auf Dateinamen innerhalb des Verzeichnisses zu ermöglichen.

rmdir() funktioniert nur dann, wenn das das zu löschende Verzeichnis leer ist. Sonderproblem: falls ein anderer Prozess auf das Verzeichnis zugreift (z.B. mit chdir() dorthin gewechselt hat), darf der Speicherplatz des Verzeichnisses nicht sofort entfernt werden. Eine interessante und meines Wissens von den Standards nicht geregelte Frage ist, was passieren soll, wenn dieser Prozess in dem eigentlich gelöschten Verzeichnis neue Dateien oder gar rekursiv weitere Verzeichnisse erstellen will. Bei meiner Implementierung des Dcache habe ich das Problem so gelöst, daß so etwas möglich ist, wobei diese sogenannten »Zombie«-Dateien bzw. »Zombie«-Verzeichnisse automatisch rekursiv deallokiert werden, sobald der letzte Verwender seine dynamischen Referenzen aufgegeben hat (analog zum unlink() bei geöffneten regulären Dateien).

  1. DIR * opendir (const char* pathname);
    struct dirent * readdir (DIR * dp);
    void rewinddir (DIR * dp);
    int closdir (DIR * dp);

DIR ist eine interne Struktur, in der die vier Funktionen die Informationen über das Verzeichnis, das sie gerade lesen, speichern. Die Funktion opendir() gibt einen Zeiger auf eine Struktur vom Typ DIR zurück, der dann von anderen drei Funktionen eingelesen wird. opendir() initialisiert auch Werte so, dass mit dem ersten Aufruf von readdir() der erste Eintrag im Verzeichnis gelesen wird. Die Reihenfolge der Einträge innerhalb der Verzeichnisse ist implementierungsabhängig. Für gewöhnlich sind Einträge nicht alphabetisch sortiert.


Die Struktur dirent ist implementierungsabhängig, enthält aber mindestens diese zwei Einträge:

struct dirent{
ino_t d_ino; // Inode-nummemmer
char d_name; // nullterminierter Dateiname
};


  1. int chdir (const char* pathname);
    int fchdir ( int filedes);


Wir können über einen Aufruf der Funktionen das aktuelle Arbeitsverzeichnis des aufrufenden Prozesses verändern. Wir können das neue Arbeitsverzeichnis entweder durch einen pathname oder durch einen Filedescriptor(filedes) bezeichnen. In dem Arbeitsverzeichnis eines Prozesses geht die Suche nach relativen Pfadnamen (Pfadnamen, die nicht mit einem „/“ beginnen) aus.

  1. void sync (void);
    int fsync (int filedes);


Traditionelle Unix-Implementierungen verfügen über einen Cache-Puffer im Systemkern, der bei den meisten Festplatten-I/O-Operationen verwendet wird. Wenn wir mit der Funktion write() Daten schreiben, kopiert der Systemkern die Daten normalerweise in einen seiner Puffer und fügt sie in die I/O-Warteschlange ein. Man nennt diesen Vorgang verzögertes Schreiben.

Der Systemkern schreibt schließlich sämtliche in den Puffern enthaltenen Datenblöcke auf die Festplatte. Normalerweise geschieht dies, wenn der Puffer für andere Datenblöcke benötigt wird. Mit den Funktionen sync() und fsync() wird die Konsistenz zwischen dem tatsächlichen auf der Festplatte vorhanden Dateisystem und dem Inhalt des Cache-Puffers sichergestellt.


Die Funktion sync reiht sämtliche modifizierten Pufferdaten in die Warteschlange zum Schreiben ein (und kehrt systemabhängig eventuell zurück, nicht so bei Linux). Sie wartet nicht unbedingt, bis die tatsächlichen I/O-Operation ausgeführt wird.

Die Funktion sync() wird normalerweise alle 30 Sekunden von einem Systemdämon (update) aufgerufen. Damit wird sichergestellt, dass die Datenpuffer des Systemkerns regelmäßig gesichert werden (Schadensbegrenzung bei Stromausfall).


Die Funktion fsync() bearbeitet nur eine Datei (die durch den Filedescriptor fildes angegeben wird) und wartet, bis die tatsächliche I/O-Operation ausgeführt wurde, bevor sie zurückkehrt! Beachte das Flag O_SYNC beim öffnen (open()) einer Datei à synchrones Schreiben, hier wartet der Prozess, bis die Datei wirklich geschrieben wurde. Anwendung der Funktion ist bei Datenbanken zu finden.


ê Reflektiert an der Schnittstelle die Tatsache, dass im Kernel Puffer vorhanden sind!


Environment of a Unix Process (e)

  1. Hauptprogramm-Interface
    int main (int argc,
    char* argv[]);

Wenn ein C-Programm vom Systemkern gestartet wird, wird vor dem Aufruf der Funktion main() eine spezielle Startroutine aufgerufen. In der ausführbaren Datei ist diese Startroutine als Startadresse des Programms verzeichnet. Diese Startroutine liest Werte vom Systemkern (die Befehlszeilenargumente und die Umgebungsvariablen) und setzt die Programmvariablen so, dass die Funktion main(), wie oben gezeigt, aufgerufen wird.

  1. Terminierung:
    normale

anormale


Unabhängig davon, wie Prozesse beendet werden, führt der Systemkern schlussendlich denselben Code aus. Er schließt alle geöffneten Descriptoren des Prozesses, gibt den Speicher frei, usw.

Process Control (f)

Erstellung neuer Prozesse, die Ausführung von Programmen und die Beendigung von Prozessen. Es gibt einige besondere Prozesse:

Nummer 0: Scheduler

Nummer 1: init

Nummer 2: Pagedaemon


  1. Prozessidentifikation (PID)

Jedem Prozess ist eine eindeutige (nichtnegative Ganzzahl) Prozessnummer zugeordnet.


pid_t getpid (void)

Prozessnummer des aufrufenden Prozesses (Kindprozess).


pid_t getppid (void)

Nummer des dem aufrufendem Prozess übergeordneten Prozesses (Elternprozess).


ê Beachte, dass bei den Funktionen kein Rückgabewert für den Fehlerfall definiert ist!

  1. Kreieren neuer Prozesse

Es gibt nur eine Weise, auf die der Systemkern Prozesse erzeugen kann, nämlich indem ein bestehender Prozess die Funktion fork aufruft (dies gilt nicht für besondere Prozesse wie den Swapper, init und den Pagedaemon).


pid_t fork (void)

Rückgabewerte: 0 an Kindprozess, Vaterprozess bekommt PID des Kindes zurück.


Der neue, von fork() kreierte Prozess, wird Kindprozess genannt. Diese Funktion wird nur einmal aufgerufen, liefert jedoch zwei Rückgabewerte. Die Rückgabewerte unterscheiden sich nur dadurch, dass dem Kindprozess immer der Wert 0, dem Elternprozess dagegen die Prozessnummer des neuen Kindprozesses übergeben wird. Die Prozessnummer des Kindprozesses wird von dem Elternprozess deshalb übergeben, da Prozesse über mehrere Kindprozesse verfügen können. Daher gibt es auch keine Funktion, mit der ein Prozess die Prozessnummer seiner Kindprozesse ermitteln kann. fork() übergibt dem Kindprozess den Wert 0, weil jeder Prozess nur ein Elternprozess haben kann. Der Kindprozess kann daher auch immer mit dem Aufruf der Funktion getppid() die Prozessnummer seines Elternprozesses ermitteln.

Der Kindprozess ist eine Kopie des Elternprozesses. Der Kindprozess erhält beispielsweise eine Kopie vom Datenbereich, Stack und Heap des Elternprozesses.


ê Es handelt sich hier um eine Kopie des Speicherbereichs à er wird nicht gemeinsam benutzt!


Viele der heutigen Implementierungen kopieren die Daten, den Heap und den Stack des Elternprozesses nicht vollständig, da nach einem Aufruf von fork() oft ein exec()-Aufruf folgt. Statt dessen verwendet man copy-on-wirte (bei Schreibversuch kopieren). Diese Speicherbereiche werden vom Eltern- und Kindprozess gemeinsam benutzt und werden vom Systemkern schreibgeschützt. Wenn einer der beiden Prozesse versucht den Bereich zu verändern, erstellt der Systemkern nur eine Kopie des betreffenden Speicherbereichs.

Eine Eigenschaft von fork() ist auch, dass alle im Elternprozess geöffneten Descriptoren im Kindprozess dupliziert werden (gleiches Ergebnis wie wenn man die Funktion dup() für jeden Descriptor aufruft). Eltern- und Kindprozess greifen bei jedem geöffneten Descriptor gemeinsam auf den zugehörigen Dateitabelleneintrag zu.



Wenn sowohl Kind- als auch Elternprozess zum selben Dateidescriptor schreiben, und nicht in irgendeiner Weise synchronisiert werden, werden ihre Ausgaben vermischt!


Der Kindprozess erbt zusätzlich folgende Eigenschaften des Elternprozess:

Unterschiedlich bei Eltern- und Kindprozess sind:

  1. pid_t wait (int* statloc); (wartet, bis irgendein Kindprozess terminiert)
    pid_t waitpid (pid_ pid,
    int* statloc,
    int options);


Wenn ein Prozess normal oder anormal terminiert, wird der Elternprozess vom Systemkern durch das SIGCHLD -Signal darüber benachrichtet. Das Signal muss nicht beachtet werden!

Diese Funktionen können folgendes bewirken:


Im einzelnen:


Wenn ein Kindprozess bereits beendet ist und den Status eines Zombies hat, kehrt wait() sofort zurück und gibt den Status des Kindprozesses aus. Ansonsten wird der aufrufende Prozess so lange blockiert, bis ein Kindprozess terminiert.

  1. int excel (const char* pathname,
    char* arg 0,
    ...,
    (char*)01);
    int execle ( ...
    char* const envp[]);
    int execlp (const char* filename,
    ... ); Unterschied: sucht in $Path
    int execv (const char* pathname,
    char* const argv[]);
    int execve (const char* pathname
    char* const argv[],
    char* const envp[]);
    int execvp (const char* filename,
    char* const argv[]);

    if (!fork()) {
    execl(„/bin/ls“,“ls“,“-l“,NULL);
    };


Mit der Funktion fork() wird ein neuer Prozess kreiert, der dann über den einer exec()-Funktion ein anderes Programm startet. Wenn ein Prozess eine der exec()-Funktionen aufruft, wird dieser Prozess vollständig durch das neue Programm ersetzt, und das neue Programm beginnt die Ausführung mit seiner main()-Funktion. Die Prozessnummer wird hierbei nicht verändert, da kein neuer Prozess erzeugt wird. exec() ersetzt einfach den bestehenden Prozess (sein Textsegment, Datensegment, Heap und Stack) durch ein ganz neues von der Festplatte geladenes Programm.


Funktion

pathname

filename

Parameterliste

argv[]

environ

envp[]

execl



execlp




execle




execv




execvp




execve




Kennbuschstabe


p

l

v


e


p: Der Funktion muss ein Dateiname übergeben werden. Weiter sucht die Funktion in dem von der Umgebungsvariablen PATH angegebenen Suchpfad nach der Datei sucht.

l: Zeigt an, dass die Funktion eine Werteliste (char* arg) als Parameter erwartet.

v: Parameterangabe in Form eines Vektors (argv[]) wird erwartet.

e: Weist daraufhin, dass die Funktion nicht die aktuelle Umgebung berücksichtigt, sondern ein Array im Format envo[] liest, das den Suchpfad definiert.


Nach einem exec-Aufruf verändert sich die Prozessnummer nicht. Das neue Programm erbt jedoch noch weitere Eigenschaften vom aufrufenden Prozess:


Beispiel:

[…]

pid = fork();

if (pid == 0) {

execle (“home/user/program”, “program”,

“myarg1”, “myarg2”, (char *) 0, env_init);

[…]

if (pid == 0) {

execlp (“program”, “program”, “only_1_arg”, (char *) 0,

env_init);

[…]



Advanced I/O (j)

  1. File and Record Locking (Datensatzsperren)
    …die Fähigkeit eines Prozesses, andere Prozesse davon abzuhalten, einen bestimmten Bereich einer Datei zu verändern, während der erste Prozess in diesem Bereich liest oder schreibt (à „Bereichssperren“).


int fcntl (int filedes,
int cmd,
/* struct flock xptr /*);


Diese Funktion kann jeden beliebigen Bereich einer Datei sperren, die ganze Datei ebenso gut wie ein einzelnes Byte!


cmd ist:


struct flock {
short l_type; /* F_RDLCL, F_WRLCK, F_UNLCK */
off_t l_start; /* Offset in Byte relativ zu l_whence */
short l_whence2; /* SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_len; /* Länge in Byte; 0 bedeutet Sperren bis EOF */
pid_t l_pid; /* nur bei F_GETLK */ Outputparam.
}

Die Struktur (flock)beschreibt:


Implizite Vererbung und Freigabe von Sperren:

  1. Zu einer Sperre gehören ein Prozess und eine Datei (pid / filedes):

  1. Sperren werden nie über einen fork()-Befehl an einen Kindprozess vererbt. Wenn also der Elternteil eine Sperre einrichtet und dann fork aufruft, wird der Kindprozess hinsichtlich der eingerichteten Sperre als fremder Prozess betrachtet. Der Kindprozess muss fcntl() aufrufen, um für alle mit fork() vererbten Descriptoren seine eignen Sperren einzurichten. Das ist auch Sinnvoll, denn schließlich sind Sperren dafür gedacht, zu verhindern, dass mehrere Prozesse gleichzeitig dieselbe Datei schreiben.

  2. Über exec() können Sperren an ein neues Programm vererbt werden.


ê Locks gehören zu einem (pid / filedes)-Paar werden aber bei dup() und fork() nicht vererbt!
bei exec() werden sie vererbt!
ê Locking ist „advisory“, d.h. hängt von Kooperationsbereitschaft ab!

  1. Multiplexing von I/O

Wenn man aus zwei Filedescriptoren abwechselnd lesen möchte wird die Multiplex-I/O-Methode verwendet. In diesem Fall können wir für keinen dieser Descriptoren ein blockierendes read() verwenden, weil unter dem einem Descriptor Daten auftauchen können, während der andere durch ein read() blockiert ist. Dazu erstellen wir eine Liste mit den Filedescriptoren, die uns interessieren und rufen eine Funktion auf, die erst zurückkehrt, wenn einer der Filedescriptoren für I/O bereit ist. Die Funktion teilt bei der Rückkehr mit, welche Descriptoren bereit sind. Dadurch wird ein permanentes Abfragen (read) (auch Polling genannt) verhindert, bei dem die Descriptoren abwechselnd auf Daten überprüft werden.

int select ( int maxfd,
fd_set* readfds,
fd_set* writefds,
fd_set* exeptfds,
struct timeval* tvptr);

Die Parameter der select()-Funktion teilen dem Systemkern folgendes mit:


Bei der Rückkehr von select() liefert der Systemkern folgende Informationen:


Mit Hilfe dieser Infos können wir die geeigneten I/O-Funktionen aufrufen (für gewöhnlich read() oder write()) und wissen dabei, dass die Funktion nicht blockieren wird.


Wie Lange wollen wir warten?


struct timeval {
long tv_seconds; //Sekunden
long tv_usec; //und Mikrosekunden
};

tvptr == NULL:

unendliches warten. Die Rückkehr erfolgt, wenn einer der angegebenen Descriptoren bereit ist oder wenn ein Signal abgefangen wird.

sonst:

Timeout, evtl. auch gar nicht warten.


Rückgabewerte:

-1 : Es ist ein Fehler aufgetreten, oder ein Signal wurde abgefangen

0 : keine Descriptoren sind bereit (à Timeout wurde erreicht)

>0 : Anzahl der Descriptoren die bereit sind.



Die Funktion poll() ähnelt der Funktion select(), hat aber eine andere Programmierschnittstelle. poll() ist mit dem Datenstrom verknüpft, obwohl man die Funktion mit jedem Descriptor verwenden kann.


int poll (struct pollfd fdarray[],
unsignedlong nfds, int timeout);


Anstatt für jede Bedingung (Lesen, Schreiben, Fehler) eine Descriptorengruppe zu initialisieren, wie wir das bei select() getan haben, initialisieren wir bei poll() ein Array mit poolfd-Strukturen, wobei jedes Element eine Descriptornummer und die Bedingung angibt, für die wir uns bei diesem Descriptor interessieren.


struct pollfd {
int fd; /* Descriptor prüfen, wenn < 0, ignorieren */
short events; /* Die interessierenden Ereignisse auf fd */ short revents; /* Die eingetretenen Ereignisse auf fd */
};



nfds:

Anzahl der Elemente in fdarray.
timeout:

= = INFTIM : unbegrenzt warten (<stropts.h> = -1)

= = 0 : überhaupt nicht warten

> 0 : timeout in Millisekunden warten

  1. Scatter read, Gatherwrite


Die Funktionen readv() und writev() ermöglichen es, in einem einzigen Funktionsaufruf aus unzusammenhängenden Puffern zu lesen und in sie zu schreiben.

ssize_t readv ( int filedes,
struct iorec iov[],
int iovent);

ssize_t writev( int filedes,
struct iorec iov[],
int iovent);


Die Anzahl der Elemente im Array iov wird durch iovcnt angegeben.

Rückgabewert beider Funktionen:

Anzahl der gelesenen oder geschriebenen Bytes, -1 Fehler.



struct iorec { // „Puffert” Zugriffe
void* iov_base; // Startadresse des Puffers
size_t io_len; // Puffergröße
};


writev() sammelt die Ausgabedaten in den Puffern der Reihenfolge iov[0], iov[1] bis iov[iovent-1]. Die Funktion liefert die Anzahl der geschriebenen Bytes – normalerweise sollte sie mit der Summe alle Pufferlängen übereinstimmen.

readv() verteilt in der selben Reihenfolge die Daten auf die einzelnen Puffer. Die Funktion füllt immer erst einen Puffer, bevor sie sich den nächsten vornimmt, readv() liefert die Gesamtzahl der gelesenen Bytes. Der Rückgabewert beträgt Null, wenn keine weiteren Daten mehr verfügbar sind und die Funktion auf das Dateiende stößt.

  1. Memory Mapped I/O (I/O über ein Speicherabbild)


Bei der Ein- /Ausgabe über ein Speicherabbild (memory mapped I/O) wird eine Festplattendatei auf einen Puffer im Arbeitsspeicher abgebildet. Wenn wir Daten aus dem Puffer holen, wird der entsprechende Bereich der Datei gelesen, und wenn wir Daten in den Puffer schreiben, werden die entsprechenden Bytes automatisch in die Datei geschrieben. Wir können damit I/O-Operationen ausführen, ohne read() oder write() aufzurufen.

Wenn wir von dieser Möglichkeit Gebrauch machen wollen, müssen wir den Systemkern anweisen, eine bestimmte Datei auf einen Bereich im Arbeitsspeicher abzubilden. Dazu dient die Funktion mmap.


caddr_t mmap (caddr_t addr,
size_t len,
int prot,
int flag,
int filedes,
off_t offs);


Der Datentyp caddr_t wird meistens als char * definiert. Im Parameter addr können wir angeben, an welcher Adresse der abgebildete Bereich beginnen soll. Normalerweise sollte addr auf Null gesetzt werden, damit das System bei der Wahl der Startadresse freie Hand hat. Die Funktion gibt die Startadresse des abgebildeten Bereichs zurück.

filedes ist der Descriptor der Datei, die abgebildet werden soll. Die Datei muss zuvor geöffnet worden sein. len steht für die Anzahl der abzubildenden Bytes und off repräsentiert den Datei-Offset der abzubildenden Bytes.

Der Parameter prot spezifiziert den Zugriffsschutz auf den abgebildeten Bereich.





1Nullpointer

2woher, von wo [wens]

1