Systemnahe Software (Systemnahe Software II)

Größe: px
Ab Seite anzeigen:

Download "Systemnahe Software (Systemnahe Software II)"

Transkript

1 ii Systemnahe Software (Systemnahe Software II) F. Schweiggert, A. Borchert, M. Grabert und J. Mayer 24. Mai 2005 Fakultät Mathematik u. Wirtschaftswissenschaften Abteilung Angewandte Informationsverarbeitung Vorlesungsbegleiter (gültig ab Sommersemester 2005) UNI VERSITÄ T ULM DOCENDO CURA NDO SCIENDO Hinweise: Auf eine detaillierte Unterscheidung zwischen BSD Unix und System V Unix wird hier verzichtet. Die enthaltenen Beispiel-Programme wurden zum großen Teil unter Linux entwickelt und sind weitestgehend unter Solaris getestet (für konstruktive Hinweise sind die Autoren dankbar). Die Beispiele sollen jeweils gewisse Aspekte verdeutlichen und erheben nicht den Anspruch von Robustheit und Zuverlässigkeit. Man kann alles anders und besser machen. Details zu den behandelten bzw. verwendeten System-Calls sollten jeweils im Manual bzw. den entsprechenden Header-Files nachgelesen werden. Sehr nützliche Hinweise zur Verwendung des C-Compilers finden sich unter der von Dr. Andreas Borchert erstellten Seite i

2 iv INHALTSVERZEICHNIS Inhaltsverzeichnis 1 Das Unix-Prozess-Subsystem Theorie der Prozesse Prozess-Baum Prozessmanagement Synchrone und asynchrone Prozesse CPU-intensive Prozesse Prozesszustände und Zustandsübergänge Virtueller Adressraum Das proc-filesystem Prozesse unter Unix System Call fork System Call exit System Call exec System Call wait n-damen-problem mit Prozessen Datei-Umlenkung Eine kleine Shell (tinysh) Allgemeines Anforderungen Grundlegender Ablauf Einschub: stralloc.h, libowfat-bibliothek Einlesen der Kommandozeile readline() Zerlegen in Tokens tokenizer() Hauptprogramm unserer Shell Bootstrapping klassisch Der init-prozess Signale Einführung Signalbehandler Reaktion auf Signale: signal() Wecksignale mit alarm() Das Versenden von Signalen Reaktion auf Signale: sigaction() Die Zustellung von Signalen Signale als Indikatoren für terminierte Prozesse Signalbehandlung in einer (Mini-)Shell Überblick der Signale aus dem POSIX-Standard Inter-Prozess-Kommunikation (IPC) Einführung IPC - Client-Server Beispiel System Calls dup(), dup2() Unnamed Pipes Client-Server mit Unnamed Pipes Standard I/O Bibliotheksfunktion Über Standarddeskriptoren in Pipe schreiben / lesen Termination von Pipeline-Verbindungen Wie die Shell eine Pipeline macht SIGPIPE Netzwerk-Kommunikation Übersicht Ethernet Internetworking - Konzept & Grundlegende Architektur Transport-Protokolle Ports UDP Berkeley Sockets Grundlagen Ein erstes Beispiel: Timeserver an Port Die Socket-Abstraktion Die Socket-Programmierschnittstelle Vorbemerkungen Überblick/Einordnung Erzeugung eines Socket Benennung eines Socket Aubau einer Kommunikationsverbindung Client-Beispiel: Timeclient für Port Überblick: Gebrauch von TCP-Ports Der Datentransfer Terminierung einer Kommunikationsverbindung Verbindungslose Kommunikation Feststellen gebundener Adresse Socket-Funktionen im Überblick Konstruktion von Adressen Socket-Adressen Socket-Adressen der UNIX-Domäne Socket-Adressen in der Internet-Domäne Byte-Ordnung Spezifikation von Internet-Adressen Hostnamen Lokale Hostnamen und IP-Adressen Portnummern und Dienste Netzwerk-Programmierung Client/Server Vorbemerkungen concurrent server iterative server echo-server und echo-client Erste Implementierungen iii

3 INHALTSVERZEICHNIS v vi INHALTSVERZEICHNIS Headerfiles TCP-Verbindung - Concurrent Server UDP-Verbindung - Iterative Server TCP-Verbindung in der UNIX Domain Modifikation der ersten Implementierung Anmerkungen Verbesserte Implementierungen Zeilenorientierter Echo-Server Anhang 219 Literatur 221 Abbildungsverzeichnis 223 Beispiel-Programme 225 Index 225

4 2 KAPITEL 1. DAS UNIX-PROZESS-SUBSYSTEM Jeder Prozess besitzt eine Prozessnummer (PID). als eindeutige Identifikation, die einen Index in die Prozesstabelle darstellt. In dieser Tabelle verwaltet das Betriebssystem die wichtigsten Daten aller Prozesse. Mit der Funktion int getpid() kann ein Prozess seine eigene PID abfragen. Kapitel 1 Das Unix-Prozess-Subsystem 1.1 Theorie der Prozesse Die Ausführung eines Programms heißt Prozess. Der Kontext eines Prozesses ist seine Ausführungsumgebung. Dazu gehören der Adressraum, in dem u.a. der Programmtext (als Maschinencode) und die Daten untergebracht sind ein Satz Maschinenregister einschließlich der Stackverwaltung (Stack-Zeiger, Frame- Zeiger) und dme PC der Program Counter verweist auf die nächste auszuführende Instruktion weitere Statusinformationen, die vom Betriebssystem verwaltet werden wie z.bsp. offene I/O-Verbindungen Die Kombination aus Adressraum und Statusinformationen wird bei Unix als Prozess bezeichnet. Zu einem Prozess gehoeren mindestens ein Satz Maschinenregister einschließlich einem PC (program counter). Es können aber auch mehrere sein; in diesem Fall spricht man von Threads: es existieren mehrere Ausführungsfäden, die parallel abgearbeitet werden. Unix verwaltet allerdings für einzelne Threads auch Statusinformationen, so dass bei einem Prozess auch manchmal von einer Rechtegemeinschaft gesprochen wird. Ausführbare Programme sind z.b.: vom Compiler generierter Objektcode ( a.out ) Datei mit einer Folge von Shell-Kommando s chmod+x) Das Betriebssystem verwaltet eine endliche Menge von Prozessen und versucht, die vorhandenen Resourcen (Speicher, Rechenzeit, I/O-Operationen) fair auf die einzelnen Prozesse zu verteilen. Ein Prozess folgt bei der Ausführung einer genau festgelegten Folge von Anweisungen, die in sich abgeschlossen sind. Schutzmechanismen des Betriebssystems und der Hardware verhindern, dass ein Prozess auf Anweisungen oder Daten außerhalb seines Adressraums zugreift. Die gesamte Kommunikation mit anderen Prozessen (IPC Interprocess Communication) oder mit dem Kernel muss über System Calls erfolgen. (Fast) alle Prozesse entstehen aus einem bereits existierenden Prozess durch Aufruf von int fork(). (Fast) jeder Prozess hat einen Erzeuger (siehe oben: fork()), der sog. parent process Dessen PID kann ein Prozess mit der Funktion int getppid() erhalten. Programm 1.1: Prozess-ID abfragen (pids.c) 1 # include < stdio.h> 2 # include < stdlib.h> 3 # include < unistd.h> 4 5 int main () { 6 7 printf ("My pid is %d, that of my parent ist %d\n", 8 getpid (), getppid ()); 9 exit (0); 10 } Ausführung: spatz$ echo $$ # PID der Shell 4223 spatz$ gcc -Wall -o pids pids.c spatz$ pids My pid is 4236, that of my parent ist 4223 spatz$ Anm.: Die Shell-Variablen $ enthält die PID der aktuellen Shell wird einer Shell-Variablen ein $ vorangestellt, so wird diese von der Shell durch ihren Wert substituiert. Jeder Prozess gehört zu einer Prozessgruppe, die ebenfalls durch eine kleine positive Integerzahl identifiziert wird (process group ID). Ist die process ID eines Prozesses gleich der process group ID, so wird dieser Prozess als process group leader bezeichnet. Mit kill (Shell-Kommando und auch System Call) können Signale an alle Prozesse einer process group gesandt werden. Bestimmung der process group ID: Funktion int getpgrp(); in BSD4.3: int getpgrp(int pid); ist das Argument pid 0, liefert diese Funktion die process group ID des aktuellen Prozessen, ansonsten die der Prozessgruppe des über pid angegebenen Prozesses. Die Zuordnung zu einer Prozessgruppe kann geändert werden: unter System V: int setpgrp(); damit wird der aufrufende Prozess zum process group leader; als process group ID wird die process ID geliefert. unter 4.3.BSD: int setpgrp(int pid, int pgrp); ist pid 0, so ist der aktuelle Prozess gemeint, ansonsten muss der angesprochene Prozess dieselbe effektive user ID wie der aktuelle Prozess haben oder der aktuelle Prozess muss Superuser Privilegien haben. Jeder Prozess kann Mitglied einer terminal group sein, die selbst wieder über eine positive ganze Zahl (terminal group ID) identifiziert wird. Diese ist die process ID des process group 1

5 1.1. THEORIE DER PROZESSE 3 leader, der das Terminal geöffnet hat - typischerweise die login shell. Dieser Prozess wird control process für dieses Terminal genannt. Jedes Terminal hat nur einen control process. Die terminal group ID identifiziert das Kontroll-Terminal für jede Prozessgruppe. Das Kontroll- Terminal wird benutzt, um Signale abzuarbeiten (von der Tastatur aus erzeugt, oder wenn die login shell terminiert). 4 KAPITEL 1. DAS UNIX-PROZESS-SUBSYSTEM 1.2 Prozess-Baum Hierarchie und Vererbung (siehe Abbildung 1.1 parent process Wenn der Prozess, der gleichzeitig Kontroll-Prozess eines Terminals und Prozessgruppenführer ist (typischerweise die login shell), ein exit zum Beenden aufruft, so wird das hangup Signal an alle Prozesse dieser Gruppe gesandt. Das Kontrollterminal wird automatisch referenziert durch /dev/tty. Ein Dämon-Prozess hingegen ist ein Hintergrundprozess ohne kontrollierendes Terminal. (z.b. httpd) pid1 = fork() (pid1 > 0) child process (pid1 == 0) Terminierung eines Prozesses: Prozesse können sich jederzeit mit exit() beenden und dabei einen Statuswert (Exit-Status oder Beendigungsstatus) im Bereich von 0 bis 255 angeben. In C-Programmen kann die exit-funktion auch implizit aufgerufen werden: ein return in der main-funktion führt zu einem entsprechenden exit, wenn das Ende der textitmain-funktion erreicht wird, so entspricht dies einem exit(0). pid2 = fork() (pid2 > 0) child process (pid2 == 0) Ein Exit-Wert 0 deutet eine erfolgreiche Terminierung an, andere Werte, so EXIT_FAILURE, werden als Misserfolg gewertet (allerdings Programm abhängig: egrep liefert 1, wenn keine Treffer vorliegen). Diese Konventionen orientieren sich zwar an UNIX, sind aber auch Bestandteil von ISO/IEC 9899:1999 (C99-Standard). Abbildung 1.1: Prozess-Hierarchie Vererbung: Prozesse vererben Teile ihrer Umgebung an ihre Kindprozesse: die Standard-Dateiverbindungen den aktuellen Katalog Umgebungsvariablen Auch die Regeln für die Weitergabe von Informationen folgen streng der hierarchischen Vererbungslehre. Elternprozesse können ihren Kindern Teile ihrer Umgebung als Kopie mitgeben. Die umgekehrte Richtung ist nicht möglich. Prozesse können Daten untereinander nur über IPC- Mechanismen (interprocess communication) austauschen. Eine Ausnahme bildet der Beendigungsstatus, den ein Prozess bei seiner Beendigung an seinen Erzeugerprozess zurückgeben kann (exit() wait()). Auf Shell-Eeben kann der Exit-Status des zuletzt terminierten Programm über die Variable? abgefragt werden: spatz$ egrep stdio *.c longrun.c:# include <stdio.h> pids.c:# include <stdio.h> spatz$ echo $? 0 spatz$ egrep stdin *.c swg@spatz:~/skripte/soft2.05/1/progs> echo $? 1 spatz$

6 1.3. PROZESSMANAGEMENT Prozessmanagement Kommando ps - Ausgabe der Prozesstabelle Das Kommando ps gibt alle wesentlichen Informationen über die aktuelle Prozesshierarchie ( Prozesstabelle) aus. Je nach Option (und Betriebssystemversion) besteht die Ausgabe von ps aus mehr oder weniger Informationen zu den einzelnen Prozessen. Die Ausgabe ist, wie bei ls, in tabellarischer Form. spatz$ ps -o pid,comm,ppid,tty,stat,start_time PID COMMAND PPID TT STAT START 3875 bash 3866 pts/0 Ss 09: gv 3875 pts/0 S 09: gs 3934 pts/0 S 15: ps 3875 pts/0 R+ 15:26 spatz$ Bedeutung: PID COMMAND PPID TT STAT die eindeutige Prozessnummer (process id) der zum Prozess gehörende Kommandoname die Prozessnummer des Erzeugers Name des Terminals (Windows), an dem der Prozess gestartet wurde Prozessstatus R für Running (runnable, on run queue, S für Sleeping, T für Traced/Stopped, Z für Zombie (tot, aber nicht vom Vater beerdigt), s für Session Leader, + für Mitglied in der im Vordergrund laufenden Prozessgruppe Mehr dazu siehe in den jeweiligen Manualseiten! Kommando top Darstellung der CPU-intensiven Prozesse man top Kommando kill Prozesse abbrechen Mit dem Kommando kill werden Signale an Prozesse verschickt. Es gibt Signale, von Prozessen ignoriert werden können. Nicht ignoriert werden können das Signal mit der Nummer 9 (SIGKILL gewaltsames Beenden und das Signal SIGSTOP Anhalten. Siehe signal.h, man kill oder kill -l). Auf diesem Weg lassen sich Hintergrundprozesse abbrechen, falls sie blockiert sind oder ungewöhnlich viel CPU -Zeit verbrauchen (Endlosschleife?). Ein blockiertes Terminal lässt sich durch ein kill-kommando für die betreffende Login-Shell, von einem anderen Terminal aus, im Normalfall wieder in einen benutzbaren Zustand zurückbringen. Die Login-Shell ist in der Spalte COMMAND als -sh oder schlicht als - aufgeführt. Als gewöhnlicher Benutzer darf man nur seine eigenen Prozesse mit kill beenden. Als Super-User darf man alle Prozesse beenden; von dieser Möglichkeit sollte man aber nur dann Gebrauch machen, wenn man sich über die Konsequenzen im klaren ist! 6 KAPITEL 1. DAS UNIX-PROZESS-SUBSYSTEM 1.4 Synchrone und asynchrone Prozesse Vordergrundverarbeitung In der Regel verarbeitet die Shell Kommandos synchron. Sie startet ein Kommando ( erzeugt Prozess) und erst nach dessen Beendigung meldet sie sich mit dem Prompt (beginnt sie mit dem nächsten Kommando). Der Erzeugerprozess wartet auf das Ende seines Kindprozesses. Hintergrundverarbeitung Kommandos, deren Abarbeitung länger dauert (etwa in der Statistik oder Numerik), blockieren bei synchroner Abarbeitung den interaktiven Betrieb. Schließt ein &-Zeichen das Kommando ab, wartet die Shell (Erzeugerprozess) nicht auf das Ende des gerade gestarteten Prozesses. Sie ist statt dessen sofort bereit, das nächste Kommando entgegenzunehmen. Fußangeln Hintergrundkommandos, die von der Standardeingabe lesen wollen, erhalten das Signal SIGTTIN der Prozess wird angehalten, existiert aber weiterhin! Damit wird verhindert, dass sich ein Vordergrundprozess (meistens die Shell selbst) und mehrere aktive Hintergrundprozesse um die Eingabe vom Terminal streiten. 1 # include < stdio.h> 2 # include < stdlib.h> 3 4 int main () { 5 char buf [256]; 6 puts ("Eingabe: " ); 7 fgets ( buf, 255, stdin ); 8 printf ("gelesen: %s\n", buf ); 9 exit (0); 10 } Programm 1.2: Von stdin lesen (read-stdin.c) Mit dem Kommando fg kann ein via SIGTTIN angehaltener Prozess in den Vordergrund geholt werden: spatz$ gcc -Wall read-stdin.c spatz$ a.out & Eingabe: [2] 5150 [2]+ Stopped a.out spatz$ ps -o pid,comm,ppid,tty,stat PID COMMAND PPID TT STAT 3875 bash 3866 pts/0 Ss 5150 a.out 3875 pts/0 T 5153 ps 3875 pts/0 R+ spatz$ fg 2 a.out Und jetzt? gelesen: Und jetzt? spatz$

7 1.5. CPU-INTENSIVE PROZESSE 7 Am.: Die in eckigen Klammern angegebene Zahl bezeichnet einen sog. Job: mehrere zu einer Einheit zusammengefasste Prozesse (z.bsp. in einer Pipeline). Ein beliebiges Hintergrundkommando terminiert automatisch mit der Terminalsitzung, aus der es gestartet wurde: es erhält das SIGHUP-Signal, voreingestellte Reaktion darauf ist Termination. Dies lässt sich für Programme mit extrem langer Laufzeit auch beim Start verhindern: nohup kommando & 1.5 CPU-intensive Prozesse Prozesse laufen in einer bestimmten Prioritätsklasse diese wird vom Erzeuger-Prozess geerbt. Die Priotitätsstufen gehen von -20 (höchste Stufe bis 19 niedrigste Stufe). Mit dem Kommando nice bzw. der Funktion nice() kann die Priorität eines Prozesses verändert (herabgestuft) werden! Die Erhöhung der Priorität verlangt Privilegien! Programm 1.3: Priorität verändern (priority.c) 1 # include < unistd.h> 2 # include < stdio.h> 3 # include < stdlib.h> 4 5 int main () { 6 printf ( "My priority is %d!\n", nice (0)); 7 perror ( "nice" ); 8 printf ( "And now it s 5 points decreased to %d!\n", nice(5)); 9 perror ( "nice" ); 10 printf ( "And now it s 8 points decreased to %d!\n", nice(8)); 11 perror ( "nice" ); 12 printf ( "Let s try to increase it by 5 to %d!\n", nice( 5)); 13 perror ( "nice" ); 14 exit (0); 15 } 8 KAPITEL 1. DAS UNIX-PROZESS-SUBSYSTEM Ausführung von Programm 1.3 als root: My priority is 0! nice: Success And now it s 5 points decreased to 5! nice: Success And now it s 8 points decreased to 13! nice: Success Let s try to increase it by 5 to 8! nice: Success Üblicherweise nutzt man das Kommando nice beim Start eines Programmes: spatz$ nice -n 3 a.out My priority is 3! nice: Success And now it s 5 points decreased to 8! nice: Success And now it s 8 points decreased to 16! nice: Success Let s try to increase it by 5 to -1! nice: Permission denied spatz$ Ausführung von Programm 1.3: My priority is 0! nice: Success And now it s 5 points decreased to 5! nice: Success And now it s 8 points decreased to 13! nice: Success Let s try to increase it by 5 to -1! nice: Permission denied

8 1.6. PROZESSZUSTÄNDE UND ZUSTANDSÜBERGÄNGE Prozesszustände und Zustandsübergänge system call 1 10 KAPITEL 1. DAS UNIX-PROZESS-SUBSYSTEM Switch wählt der Scheduler den Prozess mit der größten Priorität (Zur Erinnerung: Kommando nice!) aus der Ready-Liste (alle Prozesse im Zustand (3) Ready to Run ) aus und befördert ihn in den Zustand (2). Der Scheduler teilt ihm die CPU zu und bringt ihn damit zur Ausführung. exit() 9 sleep 2 reschedule return preempt Virtueller Adressraum Bei einfachen Prozessoren gibt es keinen Unterschied zwischen physischen und virtuellen Speicheradressen und damit auch keine scharfe Trennlinie zwischen Betriebssystem und Anwendung. Dies macht nicht nur die Speicheraufteilung unübersichtlich, es führt auch dazu, dass ein Absturz einer Anwendung, beispielsweise hervorgerufen durch einen ungültigen Zeigerzugriff, das gesamte System beeinträchtigen kann. wakeup 4 swap out 3 swap in swap out enough memory 8 fork() Bessere Prozessoren besitzen eine Speicherverwaltung (memory management unit, abgekürzt MMU), die die Einrichtung virtueller Speicherumgebungen ermöglicht. Anwendungen verwenden dann virtuelle Speicheradressen, die mittels einer vom Betriebssystem konfigurierten Funktion in physische Speicheradressen abgebildet werden. 6 5 wakeup not enough memory virtuelle Adresse Abbildung 1.2: Prozess Zustandsmodell Abbildungstabelle der MMU 1 Ausführung im User Mode (Prozess arbeitet in seinem Adressraum) 2 Ausführung im Kernel Mode (Prozess arbeitet im Kernel-Adressraum) 3 Prozess wartet auf Zuteilung der CPU durch Scheduler 4 Prozess schläft im Hauptspeicher, wartet auf bestimmtes Ereignis, das ihn aufweckt 5 Prozess ist ready to run, aber ausgelagert - muss vom Swapper (Prozess 0) in Hauptspeicher geholt werden, bevor der Kern ihn zuteilen kann 6 Prozess schläft im Sekundärspeicher (ausgelagert) 7 Prozess geht vom Kernel Mode in User Mode, aber der Kern supendiert ihn und vollzieht einen Kontext-Switch (anderer Prozess kommt zur Ausführung (bis auf Kleinigkeiten identisch mit 3) 8 Prozess neu erzeugt (Startzustand für jeden Prozess - ausgenommen Prozess 0) 9 Prozess hat exit -Aufruf ausgeführt, existiert nicht weiter, hinterlässt aber Datensatz (Exit- Code, Statistik-Angaben) für Vater-Prozess - Endzustand Übergänge System Calls und Kernel-Serviceroutinen, sowie äußere Ereignisse (Interrupts durch Uhr, HW, SW,...) können Zustandswechsel bei den Prozessen bewirken. Ein Context Switch zwischen Prozessen ist nur beim Zustandswechsel des aktiven Prozesses von Zustand (2) Running in Kernel Mode nach (4) Asleep möglich. Beim Context physische Adresse Abbildung 1.3: Virtuelle und physische Adressen Abbildung 1.3 zeigt schematisch, wie typischerweise virtuelle Adressen in physische Adressen abgebildet werden. Um die Abbildung effizient (in der Hardware) durchführen zu können, wird der gesamte Speicher in Form von Seiten organisiert. Typische Seitengrößen liegen zwischen 4 und 64 Kilobyte. Entsprechend lassen sich virtuelle Adressen in zwei Teile zerlegen: Der niedrigwertige Teil spezifiziert nur die Position innerhalb einer Seite, der höherwertige gibt die Seitennummer an. In einer Tabelle der MMU können nun die Adressen physischer Seiten den Seitennummern virtueller Adressen zugeordnet werden. Mehrere Abbildungstabellen können parallel nebeneinander existieren: Eine für das Betriebssystem selbst und eine für jede Anwendung. Ein Wechsel lässt sich effizient realisieren, indem ein spezielles Hardware-Register verwendet wird, das auf die derzeitig gültige Abbildungstabelle verweist.

9 1.7. VIRTUELLER ADRESSRAUM 11 Die Abbildungstabellen einer MMU bieten typischerweise noch Platz für Zugriffsrechte. So kann geregelt werden, welche Seiten aus der Sicht einer gegebenen Abbildungstabelle vorhanden, lesbar, schreibbar und ausführbar sind. Die Gesamtheit aus Abbildungstabelle und Zugriffsrechten wird als virtueller Adressraum bezeichnet. Kommt es bei einem Zugriff zu einem Fehler durch die MMU, kann das Betriebssystem entscheiden, ob es sich dabei um einen Fehler der Anwendung handelt oder ob es den Zugriff nach einigen Manipulationen doch noch ermöglicht. 12 KAPITEL 1. DAS UNIX-PROZESS-SUBSYSTEM Prozess Tabelle Viele Tricks sind auf diese Weise möglich: Zwei oder mehr Adressräume können gemeinsam auf die gleiche Bibliothek zugreifen. Hierbei ist es sinnvoll, für den gemeinsamen Bereich nur Lese- und Ausführungsrechte einzuräumen. für alle Prozess zusammen resident Teile eines belegten Adressraumes können auf die Platte ausgelagert werden. Bei einem Zugriff kommt es zuerst zu einem Fehler der MMU, der dann vom Betriebssystem abgefangen wird, um den Bereich wieder von Platte einzulesen und zur Verfügung zu stellen. Kopien von Speicherbereichen werden verzögert angefertigt, indem der zunächst noch gemeinsam gehaltene Bereich mit einem Schreibschutz versehen wird. Sobald eine Schreiboperation erfolgt, wird die betroffene Seite dupliziert und bei beiden Kopien werden dann die Schreibrechte wieder zurückgegeben. je Prozess System Daten Kernel Adressraum (Programm ) Text auslagerbar (swappable) Daten Benutzer Adressraum Abbildung 1.4: Prozess-Kontext Der Kernel unterteilt den virtuellen Adressraum eines Prozesses in logische (abstrakte) Bereiche (regions) zusammenhängender Adressen; diese werden als verschiedene Objekte behandelt, die als Ganzes geschützt (protected) bzw. geteilt (shared) werden können Text, Daten, Stack bilden typischerweise separate Bereiche Der Kernel unterhält eine Tabelle mit Verweisen auf die aktiven Bereiche (u.a. Information, wo diese im physischen Speicher liegen) Jeder Prozess unterhält eine private per process region table (z.b. in der u area) Context (Abb. 1.4, S. 12) Der UNIX-Jargon unterteilt den Context in drei disjunkte Bereiche: Text, User Data und System Data. Davon hält der Kernel die für ihn wichtigen Daten ( System- Daten ) an zwei definerten Plätzen in seinem Adressraum - in der Prozesstabelle und in der User Area ( per process data region ) Text und User-Daten Bereich bilden den User Adressraum. Einträge in der Prozesstabelle: Prozesszustand (siehe obiges Beispiel bg.c: Zustand T) Information, wo der Prozess und die u area im Haupt-/Sekundärspeicher liegen sowie über Speicherbedarf ( context switch) Verwandtschaftsverhältnisse scheduling Parameter diverse Uhren ( time) u.a.m. Mehr dazu: man ps! Einträge in der u area - Reale und effektive UserID Context Switch Ein Context Switch findet statt, wenn der Scheduler beschließt, dass ein anderer Prozess zur Ausführung kommen soll. Der Kernel hält den laufenden Prozess an, sichert dessen Context und lädt den Context des nächsten Prozesses. Bei jedem Wechsel sichert der Kernel soviel Informationen, dass er später zu dem unterbrochenen Prozess zurückkehren und dessen Ausführung fortsetzen kann. Für den Prozess bleibt die Unterbrechung völlig transparent. Ein Context Switch kann nur stattfinden, während der Prozess im Kernel Mode arbeitet. Ein Wechsel des Execution Mode ist kein Context Switch. - Vektor zur Signalverarbeitung - Hinweis auf login terminal - Fehlereintrag für Fehler bei einem Systemaufruf - Rückgabewert von Systemaufrufen - Aktueller Katalog, Aktuelle Wurzel - User File Descriptor Table - u.a.m.

10 1.8. DAS PROC-FILESYSTEM KAPITEL 1. DAS UNIX-PROZESS-SUBSYSTEM 1.8 Das proc-filesystem Das Verzeichnis /proc ist ein Pseudo-Dateisystem, das als Schnittstelle zu den Datenstrukturen des Kernels dient und den direkten Zugriff auf /dev/kmem erspart. Mehr dazu: man proc

11 16 KAPITEL 2. PROZESSE UNTER UNIX UFDT u area user stack open Files current dir Kapitel 2 data Kernel Stack KIT Prozesse unter Unix shared text 2.1 System Call fork user stack u area open Files current dir changed root Syntax # include <sys/types.h> # include <unistd.h> pid_t fork (void) /* create new processes */ /* returns PID and 0 on success or -1 on error */ data Kernel Stack Abbildung 2.1: Ein neuer Prozess und sein Erzeuger Beschreibung Der System Call fork erzeugt einen neuen Prozess, indem er den Context des ausführenden Prozesses dupliziert siehe Abb. 2.1 Der neu erzeugte Prozess wird als Kind-Prozess (child process), der aufrufende als Erzeugeroder Vater- oder Eltern-Prozess ( parent process) bezeichnet (die weibliche Bezeichnung Mutter -Prozess ist wenig gebräuchlich!). fork wird einmal aufgerufen und kehrt im Erfolgsfall zweimal zurück: der Aufrufer erhält als Rückgabewert die PID des erzeugten Prozesses, der Kindprozeß erhält 0; im Fehlerfall (z.b. bereits zuviele Prozesse erzeugt) kehrt er einmal mit -1 zurück. 15

12 2.1. SYSTEM CALL FORK 17 Beispiel Programm 2.1: Ein neuer Prozess mit fork() (fork1.c) 1 #include < stdio.h> 2 #include < unistd.h> 3 #include < stdlib.h> 4 5 int main() 6 { 7 int pid = fork (); 8 9 if ( pid < 0 ) { 10 perror ( "fork() can t fork a child" ); 11 exit (1); 12 } if ( pid > 0 ) { 15 printf ("Parent: created process %d, my pid is %d\n", 16 pid, ( int ) getpid ()); 17 } 18 else / pid == 0 / { 19 printf ( "Child: after fork, my pid is %d\n", (int) getpid ()); 20 printf ( "Child: my parent is %d\n", (int) getppid ()); 21 } printf (" finished (PID = %d)\n", getpid ()); 24 exit ( 0 ); 25 } Ausgabe: Parent: created process 4851, my pid is 4850 finished (PID = 4850) Child: after fork, my pid is 4851 Child: my parent is 4850 finished (PID = 4851) 18 KAPITEL 2. PROZESSE UNTER UNIX Oder so: Erzeuger schläft ein bisschen, bevor er terminiert Programm 2.2: fork() zum zweiten (fork2.c) 1 #include < stdio.h> 2 #include < unistd.h> 3 #include < stdlib.h> 4 5 int main() 6 { 7 int pid ; 8 switch ( pid = fork ()) { 9 case 1: 10 perror ( "fork() can t fork a child" ); 11 exit (1); 12 case 0: 13 printf ( "Child: after fork, my pid is %d\n", (int) getpid ()); 14 printf ( "Child: my parent is %d\n", (int) getppid ()); 15 break; 16 default : 17 sleep (5); 18 printf ( "Parent: created process %d, my pid is %d\n", 19 pid, ( int ) getpid ()); 20 break; 21 } 22 printf ( "%d: finished\n", getpid ()); 23 exit (0); 24 } Ausgabe: Child: after fork, my pid is 4870 Child: my parent is : finished Parent: created process 4870, my pid is : finished Der Vater-Prozess terminiert hier (rein zufällig ) vor dem Sohn, der Init-Prozess erbt den verlorenen Sohn. Das Geschehen ist nicht deterministisch, hängt u.a. von der Systemauslastung ab!

13 2.1. SYSTEM CALL FORK 19 Vererbt werden: real user ID real group ID effective user ID effective group ID process group ID terminal group ID root directory current working directory signal handling settings file mode creation mask Unterschiede: process ID parent process ID eigene File Deskriptoren (Kopie) Zeit bis zu einem ggf. gesetzten alarm ist beim Kind auf 0 gesetzt Gebrauch: 1. Ein Prozess erzeugt von sich selbst eine Kopie, so dass diese die eine oder andere Operationen ausführt ( Server). 2. Ein Prozess will ein anderes Programm zur Ausführung bringen - der Kind-Prozess führt via exec ein neues Programm aus (überlagert sich selbst) ( Shell) 20 KAPITEL 2. PROZESSE UNTER UNIX 2.2 System Call exit Der eigentliche SystemCall ist _exit(): Syntax: #include <unistd.h> void _exit(int status); Meist verwendet man eine entsprechende Bibliotheksfunktion, die auch noch etwas aufräumt: Syntax #include <stdlib.h> void exit( int status ) /* terminate process */ /* does NOT return */ /* 0 <= status < 256 */ Beschreibung Mit dem System Call exit beendet ein Prozess aktiv seine Existenz. Von diesem System Call gibt es keine Rückkehr in den User Mode. Unterscheide: System Call _exit und C-Bibliotheks-Funktion exit C-Funktion leert erst alle Puffer und ruft dann den System Call auf. Der Kernel gibt den User Adressraum (Text und User-Daten) des Prozesses frei, sowie auch einen Teil des System-Daten Bereichs (User Area). Übrig bleibt nur der Eintrag in der Prozesstabelle, der den Beendigungsstatus und die Markierung des Prozesses als Zombie enthält, bis ihn der init-prozess erbt und abräumt. Beispiel: Programm 2.3: Beenden mit exit() (exit1.c) 1 #include < stdio.h> 2 #include < unistd.h> 3 #include < stdlib.h> 4 5 int main () { 6 printf ("This is some text to write on stdout"); 7 exit (0); 8 } Programm 2.4: Beenden mit _exit() (exit2.c) 1 #include < stdio.h> 2 #include < unistd.h> 3 #include < stdlib.h> 4 5 int main () { 6 printf ("This is some text to write on stdout"); 7 _exit (0); 8 } Manchmal ist es praktisch, selbst vor dem (selbst gewollten) Beenden noch einiges zu erledigen: Funktion atexit()

14 2.2. SYSTEM CALL EXIT 21 Programm 2.5: Beenden mit eigenem Aufräumen (exit3.c) 1 #include < stdio.h> 2 #include < unistd.h> 3 #include < stdlib.h> 4 5 void do_on_exit (void){ 6 printf ( "That s all BaBa\n"); 7 } 8 void bye(void ) { 9 printf ( "That was all, folks\n"); 10 } int main(int argc, char argv ) { 13 if ( ( atexit ( do_on_exit ) == 0 ) && ( atexit (bye ) == 0)){ 14 printf (" functions to be called at normal termination set\n"); 15 printf ("================================================\n"); 16 } 17 if ( argc == 1 ) 18 printf (" exit 1\n" ), exit (1); 19 if ( argc == 2 ) 20 printf (" exit 2\n" ), exit (2); 21 printf ( "end of text reached\n"); 22 exit (0); 23 } spatz$ gcc -Wall exit3.c spatz$ a.out functions to be called at normal termination set ================================================ exit 1 That was all, folks That s all -- BaBa spatz$ a.out functions to be called at normal termination set ================================================ end of text reached That was all, folks That s all -- BaBa spatz$ 22 KAPITEL 2. PROZESSE UNTER UNIX 2.3 System Call exec Syntax #include <unistd.h> extern char ** environ; int execl( char *path, /* path of program file */ char *arg0, /* 1st argument (cmd name) */ char *arg1,..., /* 2nd,... argument */ char *argn, /* last argument */ (char *) 0 ); int execlp( char *filename, /* name of program file */ char *arg0, char *arg1,... char *argn, (char *) 0 ); int execle( char *path, /*path of program file */ char *arg0, char *arg1,... char *argn, (char *) 0, char **envp) /* pointer to environment */ ); int execv( char *path, /* path of program file */ char *argv[]; /* pointer to array of argument */ ); int execvp( char *filename, /* name of program file */ char *argv[]; /* pointer to array of argument */ ); int execve( char *path, /* path of program file */ char *argv[], /* pointer to array of argument */ char *envp[], /* pointer to environment */ ); /* all return with -1 on error only */ Beschreibung Die exec - System Calls überlagern im Context des ausführenden Prozesses den Text und den User-Daten Bereich mit dem Inhalt der Image-Datei. Anschließend bringen sie die neuen Instruktionen zur Ausführung.

15 2.3. SYSTEM CALL EXEC 23 Zusammenhang 24 KAPITEL 2. PROZESSE UNTER UNIX Vererbung bei exec(): execlp(file, arg,...,0) create argv execvp(file,argv) convert file to path execl(path,arg,..., 0) create argv execv(path,argv) add envp Abbildung 2.2: Die exec()-familie execle(path, arg,...,0,envp) create argv execve(path,argv,envp) system call 1. Die drei Funktionen in der oberen Reihe enthalten jedes Kommando-Argument als separaten Parameter, der NULL-Zeiger ( (char *) 0 -!!!) schließt die variable Anzahl ab (kein argc!). Die drei Funktionen in der unteren Reihe fassen die Kommando-Argumente in einen Parameter argv zusammen, das Ende wird entsprechend wieder durch den NULL-Zeiger definiert. 2. Die zwei Funktionen in der linken Spalte definieren die Programm-Datei nur durch den Datei-Namen; diese wird über die Einträge in der Umgebungsvariable PATH gesucht (konvertiert in vollen Pfadnamen). Falls diese nicht gesetzt ist, wird als default-suchpfad :/bin:/usr/bin genommen. Enthält das Argument path einen slash, so wird die Variable PATH nicht verwendet. 3. Bei den vier Funktion in den beiden linken Spalten wird das Environment über die externe Variable environ an das neue Programm übergeben. Die beiden Funktionen in der rechten Spalte spezifizieren explizit eine Environment-Liste (muß ebenfalls mit einem NULL- Zeiger abgeschlossen sein). process ID parent process ID process group ID terminal group ID time left until an alarm clock signal root directory current working directory file mode creation mask real user ID real group ID file locks Mögliche Anderungen mit exec(): set user ID / set group ID - bit der neuen Datei gesetzt effective user ID auf user ID des Besitzers der Programm-Datei effective group ID auf group ID des Besitzers der Programm-Datei Signale: Terminieren bleibt Terminieren Ignorieren bleibt Ignorieren Speziell abgefangene Signale werden wegen des Überlagerns auf Terminieren gesetzt

16 2.3. SYSTEM CALL EXEC 25 Beispiel: Programm 2.6: Neues Programm ausführen (exec1.c) 1 #include < stdio.h> 2 #include < unistd.h> 3 #include < stdlib.h> 4 5 void exec_test () / execl version / { 6 printf ( "The quick brown fox jumped over " ); 7 fflush ( stdout ); 8 execl ( "/bin/echo", "echo", "the", "lazy", "dogs.", 9 (char ) 0 ); 10 perror ( " execl can t exec /bin/echo" ); 11 } int main () { 14 int pid ; pid = fork (); 17 if ( pid < 0) { 18 perror ("fork() can t fork" ); 19 exit (1); 20 } 21 if ( pid > 0) 22 printf ("PP: Parent PID: %d / Child PID: %d\n", 23 ( int ) getpid (), pid ); 24 else / pid == 0 / { 25 printf ("CP: Child PID: %d\n", (int) getpid () ); 26 exec_test (); 27 } 28 exit (0); 29 } Ausgabe: CP: Child-PID: 4881 The quick brown fox jumped over PP: Parent-PID: 4880 / Child-PID: 4881 the lazy dogs. Läßt man in Programm 2.6 den Funktionsaufruf fflush() weg (siehe Programm 2.7, S. 26), so passiert folgendes: 26 KAPITEL 2. PROZESSE UNTER UNIX Programm 2.7: Programm 2.6 ohne fflush() (exec2.c) 1 #include < stdio.h> 2 #include < unistd.h> 3 #include < stdlib.h> void exec_test () / execl version / { 7 printf ( "The quick brown fox jumped over " ); 8 // fflush ( stdout ); 9 execl ( "/bin/echo", "echo", "the", "lazy", "dogs.", 10 (char ) 0 ); 11 perror ( " execl can t exec /bin/echo" ); 12 } int main () { 15 int pid ; pid = fork (); 18 if ( pid < 0) { 19 perror ("fork() can t fork" ); 20 exit (1); 21 } 22 if ( pid > 0) 23 printf ("PP: Parent PID: %d / Child PID: %d\n", 24 ( int ) getpid (), pid ); 25 else / pid == 0 / { 26 printf ("CP: Child PID: %d\n", (int) getpid () ); 27 exec_test (); 28 } 29 exit (0); 30 } Erläuterung: Wenn in eine Datei oder in eine Pipe geschrieben wird, erledigt printf() die Ausgabe gepuffert (in Blöcken zu 512 Bytes), Bei der Ausgabe ans Terminal (s.o.) kann die Ausgabe auch dann bruchstückhaft (oder gar nicht) erscheinen, wenn Zeilenpufferung stattfindet. Normalerweise führt dies zu keinen Problemen; der letzte Puffer wird automatisch entleert, wenn der Prozess endet. In obigem Beispiel war der Prozess noch nicht beendet, als der exec-aufruf erfolgte der im Benutzerdatensegment liegende Puffer wurde überlagert, bevor er entleert werden konnte. the lazy dogs. PP: Parent-PID: 4914 / Child-PID: 4915

17 2.3. SYSTEM CALL EXEC 27 1 # include < stdio.h> 2 # include < unistd.h> 3 4 int main () { 5 char argv [4]; 6 argv [0] = "my ls"; 7 argv [1] = "?"; 8 argv [2] = ".c" ; 9 argv [3] = NULL; 10 execvp ( " ls ", argv ); 11 perror ( "exevp"); 12 return 1; 13 } Programm 2.8: Argumentvektor übergeben (exec3.c) Für das Programm 2.10 brauchen wir das Programm 2.9 zur Ausgabe des Environment: 1 # include < stdio.h> 2 3 int main () { 4 extern char environ ; 5 while( environ!= NULL) 6 printf ("%s\n", environ ++); 7 return 0; 8 } Programm 2.9: Environment ausgeben (env.c) 28 KAPITEL 2. PROZESSE UNTER UNIX Bei den Nicht-exec?e() -Varianten wird die Umgebung über environ übergeben (siehe Programm # include < stdio.h> 2 # include < unistd.h> 3 4 int main () { 5 extern char environ ; 6 char envp [2]; 7 envp [0] = "x=100"; 8 envp [1] = NULL; 9 environ = envp; 10 execl ("./env", "env", NULL); 11 perror ( " execl " ); 12 return 1; 13 } Programm 2.11: Umgebung ändern bei execl() (exec5.c) 1 # include < stdio.h> 2 # include < unistd.h> 3 4 int main () { 5 char envp [2]; 6 envp [0] = "x=1"; 7 envp [1] = NULL; 8 execle ( "./env", "env", NULL, envp); 9 perror ( " execle " ); 10 return 1; 11 } Programm 2.10: Umgebung ändern (exec4.c) Übersetzen und Ausführen: spatz$ gcc -Wall -o env env.c spatz$ gcc -Wall exec4.c spatz$ a.out x=1 spatz$ echo $x # und jetzt? spatz$

18 2.4. SYSTEM CALL WAIT System Call wait Syntax #include <sys/types.h> #include <sys/wait.h> pid_t wait( int * status ); pid_t waitpid(pid_t pid, int *status, int options); Beschreibung von wait() (zu waitpid() siehe Manuals) Falls der Aufrufer von wait() keinen Kind-Prozess erzeugt hat, kehrt wait() mit -1 zurück. Kehrt zurück, falls ein Kind bereits vorher terminierte Ansonsten blockiert wait() (wird vom Kernel suspendiert), bis einer der erzeugten Prozesse terminiert. In diesem Fall liefert wait() die PID des terminierten Kind-Prozesses. Über das Argument von wait() erhält der Prozess Informationen über den terminierten Kind-Prozess (Abb. 2.3) Argument von exit() 0x00 0x00 Signalnummer core Flag Termination durch exit() Signal 30 KAPITEL 2. PROZESSE UNTER UNIX Ein erstes Beispiel mit wait(): Programm 2.12: Auf Kind-Prozess warten (wait1.c) 1 #include <sys/types. h> 2 #include <sys/wait. h> 3 #include < unistd.h> 4 #include < stdlib.h> 5 #include < stdio.h> 6 7 int main () { 8 pid_t pid ; 9 10 if ( ( pid = fork () ) < 0 ) 11 perror ("fork" ), exit (1); if ( pid == 0) { // Child 14 printf ("Child my pid is %d\n", (int) getpid()); 15 sleep (10); 16 return 0; 17 } else { 18 // Parent 19 int status, signal_nr, exit_st ; 20 pid_t child_pid = wait(& status ); 21 if ( child_pid == 1) 22 perror ("wait" ), exit (2); 23 signal_nr = status & 0 x3f ; 24 exit_st = ( status» 8) & 0 xff ; 25 printf ("Parent child with pid %d finished\n", (int) child_pid ); 26 printf ("SignalNr: %d ExitSt: %d\n", signal_nr, exit_st ); 27 return 0; 28 } 29 } Makros erleichtern die Dekodierung: (aus /usr/include/sys/wait.h) Signalnummer 0x7F Prozess angehalten (nur in Spezialfällen) WIFEXITED(status): liefert TRUE, wenn der Kindprozess normal terminierte (freiwilliges exit() oder _exit() bzw. Rückkehr aus der main()-funktion (siehe Programm 2.13, 31) NB: Signal-Nummern sind größer 0! Abbildung 2.3: Information über Kind-Prozess WIFSIGNALED(status): liefert TRUE, wenn der Kindprozess durch ein nicht abgefangenes Signal zur Termination kam WEXITSTATUS(status: liefert den durch exit() oder return in der main()-funktion gesetzten Exit-Status kann nur benutzt werden, wenn WIFEXITED(status) TRUE lieferte WTERMSIG(status: liefert die Nummer des Signals, durch das der Kindprozess zur Termination kam kann nur benutzt werden, wenn WEXITSTATUS(status) TRUE lieferte

19 2.4. SYSTEM CALL WAIT 31 Programm 2.13: Auf Kind-Prozess warten (forkandwait.c) 1 #include < stdio.h> 2 #include < stdlib.h> 3 #include < unistd.h> 4 #include <sys/wait. h> 5 6 int main () { 7 pid_t child, pid ; 8 int stat ; 9 10 child = fork (); 11 if ( child == 1) { 12 perror ("unable to fork" ); exit (1); 13 } 14 if ( child == 0) { / child process / 15 srand ( getpid ()); char randval = rand (); 16 printf ("Child is going to exit with %d\n", randval & 0xFF); 17 exit ( randval ); 18 } / parent process / 21 pid = wait(& stat ); 22 if ( pid == child ) { 23 if ( WIFEXITED(stat)) { 24 printf ( " exit code of child = %d\n", WEXITSTATUS(stat)); 25 } else { 26 printf ( " child terminated abnormally\n"); 27 } 28 } else { 29 perror ("wait" ); 30 } 31 return 0; 32 } waitpid(): pid_t waitpid(pid_t pid, int * status, int options); Parameter pid: < 1 auf alle Kind-Prozesse, warten, deren Prozessgruppen-ID gleich dem Betrag von pid ist 1 auf alle Kind-Prozesse warten (wie wait()) 0 auf alle Kind-Prozesse warten, deren Prozessgruppen-ID gleich der der Aufrufers ist > 0 auf den Kindprozess mit der angegebenen pid warten 32 KAPITEL 2. PROZESSE UNTER UNIX WUNTRACED: auch bei gestoppten Kindprozessen zurückkehren Ansonsten wird der dritte Parameter einfach auf 0 gesetzt! Beispiel mit waitpid() Programm 2.14: Warten mit waitpid() (waitpid1.c) 1 #include <sys/types. h> 2 #include <sys/wait. h> 3 #include < unistd.h> 4 #include < stdlib.h> 5 #include < stdio.h> 6 7 int main () { 8 pid_t pid ; 9 10 if ( ( pid = fork () ) < 0 ) 11 perror ("fork" ), exit (1); if ( pid == 0) { 14 printf ("Child my pid is %d\n", (int) getpid()); 15 sleep (10); 16 printf ("Child going to terminate!\n"); 17 return 0; 18 } else { 19 // Parent 20 pid_t child_pid ; 21 int status ; 22 if (( child_pid = waitpid ( pid, & status, WUNTRACED )) < 0) 23 perror ("wait" ), exit (2); 24 // WUNTRACED: return for children which are stopped, and 25 // whose status has not been reported printf ("Parent child with pid %d finished, ", (int) child_pid ); 28 if WIFEXITED(status) 29 printf ("with exit value %d\n", WEXITSTATUS(status) ); 30 if WIFSIGNALED(status) 31 printf ("caused by signal %d\n", WTERMSIG(status) ); 32 if WIFSTOPPED(status) 33 printf ("not really, is stopped by %d and now dead!\n", 34 WSTOPSIG(status) ); 35 printf ("Parent going to terminate!\n"); 36 return 0; 37 } 38 } Aufgaben des Kernel, wenn ein Prozess exit() aufruft: Parameter options: bitweises ODER folgender Konstanten WNOHANG: nicht blockieren, falls kein Kindprozess terminierte Wartet der Erzeuger-Prozess mit wait(), so wird er von der Termination des (eines) Kind- Prozesses benachrichtigt (wait() kehrt zurück). Im Argument von wait() wird der exit-status (das Argument in exit()) geliefert, als Rückgabewert die PID des Kind-Prozesses.

20 2.4. SYSTEM CALL WAIT 33 Wartet der Erzeuger-Prozess nicht, so wird der Kind-Prozess als Zombie-Prozess markiert. Der Kernel gibt dessen Ressourcen frei, bewahrt aber den exit-status (Beendigungsstatus) in der Prozesstabelle auf. Sind process ID, process group ID, terminal group ID des terminierenden Prozesses alle gleich, so wird das Signal SIGHUP an jeden Prozess mit gleicher process group ID gesandt. Wenn der Erzeuger vor dem Kind-Prozess terminiert: Die PPID der Kind-Prozesse wird auf 1 gesetzt, der init-prozess erbt die Waisen in Programm 2.15 wird ein Waisenkind erzeugt. Übersetzung und Ausführung liefert: spatz$ gcc -Wall orphan.c spatz$ a.out Hi, my parent is 5174 spatz$ My parent is now 1 spatz$ Der init-prozess terminiert nie, und wenn doch, so wäre es im Multiuser-Betrieb nicht mehr möglich, dass sich weitere Benutzer anmelden. In 4.3BSD würde es zu einem automatischen reboot kommen. 34 KAPITEL 2. PROZESSE UNTER UNIX abzusetzen; der exit-status der Kindprozesse wird bei deren Termination nicht weiter aufbewahrt. Programm 2.16, S. 34, demonstriert die Erzeugung eines Zombies. Der neu erzeugte Prozess verabschiedet sich sofort mit exit(), während der übergeordnete Prozess mit Hilfe eines sleep()- Aufrufes sich für 60 Sekunden zur Ruhe legt. Während dieser Zeit verbleibt der Unterprozess im Zombie-Status, wie das ps-kommando belegt: spatz$ genzombie & 4792 [1] 4791 spatz$ ps -f UID PID PPID C STIME TTY TIME CMD swg :27 pts/3 00:00:00 bash -i swg :27 pts/3 00:00:00 genzombie swg :27 pts/3 00:00:00 [genzombie] <defunct> swg :28 pts/3 00:00:00 ps -f spatz$ ps -f # 1 Minute spaeter UID PID PPID C STIME TTY TIME CMD swg :27 pts/3 00:00:00 bash -i swg :28 pts/3 00:00:00 ps -f [1]+ Done genzombie spatz$ Programm 2.15: Ein Prozess wird zum Waisenkind (orphan.c) 1 #include < stdio.h> 2 #include < stdlib.h> 3 #include < unistd.h> 4 5 int main () { 6 pid_t child ; 7 child = fork (); 8 if ( child == 1) { 9 perror ("fork" ); exit (1); 10 } 11 if ( child == 0) { 12 printf ("Hi, my parent is %d\n", (int) getppid ()); 13 sleep (5); 14 printf ("My parent is now %d\n", (int) getppid ()); 15 } 16 sleep (3); 17 exit (0); 18 } Programm 2.16: Erzeugen eines Zombie-Prozesses (genzombie.c) 1 #include < stdio.h> 2 #include < stdlib.h> 3 #include < unistd.h> 4 5 int main () { 6 pid_t child = fork (); 7 if ( child == 1) { 8 perror ("fork" ); exit (1); 9 } 10 if ( child == 0) exit (0); 11 / parent : / 12 printf ( "%d\n", child ); 13 sleep (60); 14 exit (0); 15 } Wenn ein Prozess mit exit() terminiert, so wird dem Erzeuger das Signal SIGCLD (SIGCHLD) zugeleitet (default-reaktion: ignorieren). In System V ist es möglich, dass ein Prozess verhindert, dass seine Kind-Prozesse zu Zombies werden; dazu hat er nur den Aufruf signal(sigcld, SIG_IGN)

21 2.5. N-DAMEN-PROBLEM MIT PROZESSEN n-damen-problem mit Prozessen 36 KAPITEL 2. PROZESSE UNTER UNIX Start Ein etwas trickreicheres Beispiel ist in Programmtext 2.18 zu finden. Beim n-damen-problem geht es darum, n Damen auf einem n n Schachbrett so unterzubringen, dass sie sich gegenseitig nicht bedrohen. Nach den üblichen Schachregeln bedroht ein Dame horizontal, vertikal und diagonal (siehe Abb. 2.4). (0,0) (0,1) (1,0) (1,1) (1,0) (1,1) (0,2) (0,3) (2,0) (2,1) Abbildung 2.5: n-damen-problem: der Lösungsbaum int row, col; Bit: Abbildung 2.6: n-damen-problem: Zeilen-/Spaltenbedrohung 6 7 Abbildung 2.4: n-damen-problem: das Schachbrett Diagonalen von links unten nach rechts oben: Werte von 0 bis 2 (n 1) (Variable diags2) Diagonalen von links oben nach rechts unten: Werte von (n 1) bis +(n 1), ins Positive mit (n 1) transformiert: Werte von 0 bis 2 (n 1) (Variable diags1) Die Bedrohungssituation kann leicht festgestellt werden: Sei (i, j) eine mit einer Dame zu besetzende Position. Bedroht wären dadurch: Zeile i Spalte j alle Felder (k, l) mit (1) k l = i j (Diagonale von links oben nach rechts unten) (2) k + l = i + j (Diagonale von links unten nach rechts oben) Meist wird das Problem im Backtracking-Verfahren gelöst, bei dem rekursiv alle Varianten durchprobiert werden (siehe Programm 2.17, S. 36). Klappt es mit einem Weg nicht, werden die bereits gesetzten Damen wieder sukzessive abgebaut, bis sich andere bislang noch nicht getestete Varianten eröffnen (Abb. 2.5, S. 36). Zunächst soll zum Verständnis die klassische Lösung betrachtet werden: Der gesamte Stand auf dem n n Schachbrett (im folgenden n = 8) wird in einer Reihe globaler Variablen verwaltet. Da es nur um die Feststellung geht, ob z.b. eine Zeile belegt ist oder nicht, kann man Bitmaps verwenden ist das i-te Bit auf 1, so ist die i-te Zeile / Spalte bedroht (siehe Abb. 2.6, S. 36). Die Bedrohungssituation in den beiden Diagonalen kann ebenfalls in einer Integer-Zahl (Bitmap) geführt werden: Dies ergibt dann das Programm 2.17, S. 36. Anzumerken bleibt, dass von den 92 Lösungen bei n = 8 sehr viele symmetrisch sind. Programm 2.17: 8-Damen klassisch (classicqueens.c) 1 #include < stdio.h> 2 3 #define INSET(member,set ) ((1«( member))&set) 4 #define INCL(set,member ) (( set ) = 1«( member)) 5 #define EXCL(set,member ) (( set ) &= ( ~(1«( member ))) ) 6 #define size struct positions { int row, col ; } pos[ size ]; / queen positions / 9 int rows = 0; / bitmaps of [0.. n 1] / 10 int diags1 = 0; / bitmap of [0..2 ( n 1)] used for row col+n 1 / 11 int diags2 = 0; / bitmap of [0..2 ( n 1)] used for row+col / 12 / rows( i ) == 1 means : line i is threatened 13 diags1 ( i j+size 1) == 1 : diag through ( i, j ) from left up 14 to right down is threatened 15 diags1 ( i+j ) == 1 : diag through ( i, j ) from left down to 16 right up is threatened 17 / void printpos () { 20 int j = 0; 21 printf (" (next) positioning :\n" ); 22 printf ("Row : Col\n"); 23 for ( j = 0; j < size ; j ++ ) 24 printf ("%3d : %3d\n", pos[j].row, pos[ j ]. col );

22 2.5. N-DAMEN-PROBLEM MIT PROZESSEN } void try ( int i ){ 28 int j ; 29 for ( j = 0; j < size ; j++){ 30 if (!( INSET(j,rows)) && 31!( INSET((i+j ), diags2 )) && 32!( INSET((i j+size 1), diags1 )) 33 ) { 34 pos [ j ]. row = j ; pos[ j ]. col = i ; 35 INCL(rows,j ); INCL(diags2, i+j ); INCL(diags1, i j+size 1); 36 if ( i < size 1 ) 37 try ( i +1); 38 else 39 printpos (); 40 EXCL(rows,j ); EXCL(diags2,i+j ); EXCL(diags1,i j+size 1); 41 } 42 } 43 } int main (){ 46 try (0); 47 return 0; 48 } Diese sequentielle Vorgehensweise lässt sich auch parallelisieren. Auf der ersten Reihe auf dem Schachbrett gibt es n mögliche Positionen für die erste Dame. Entsprechend können n Prozesse erzeugt werden, die sich jeweils um einen Teilbaum (Abb. 2.5, S. 36) kümmern. Das Verfahren kann auch weiter mit Parallelisierung fortgesetzt werden. Bei größeren Brettgrößen stößt dies jedoch rasch an die Grenze der Prozesstabelle, so dass die vorgestellte Lösung nicht wirklich skalierbar ist (und auf den Unterrichtsrechnern auf keinen Fall ausprobiert werden sollte). Um das Problem zu vereinfachen, soll das Programm nur die Zahl der gefundenen Lösungen ermitteln. Eine Ausgabe der gefundenen Lösungen ist nicht trivial, da hier eine Synchronisierung stattfinden müsste: wenn viele Prozesse konkurrierend versuchen, Ausgabe zu produzieren, würde dies zu einer wilden Textmischung führen. Über den Exit-Wert kann für nicht zu große Werte von n bequem die Zahl der gefundenen Lösungen eines Teilbaumes zurückgeliefert werden, so dass der übergeordnete Prozess die Gelegenheit hat, die Einzelresultate zusammenzuzählen. Zur Prozesslösung: 38 KAPITEL 2. PROZESSE UNTER UNIX Falls eine weitere Dame gefahrlos gesetzt werden kann, dann wird diese Variante einem Prozess überlassen, der in Zeile 26 neu erzeugt wird. Dieser setzt die Dame innerhalb der Datenstruktur in den Zeilen 31 bis 35. Wenn damit size Damen gesetzt sind, ist eine Lösung gefunden worden und der Unterprozeß signalisiert dies mit einem exit(1) in Zeile 36. Ansonsten gibt es einen Sprung zur Schleife auf Zeile 21, die dann die nächste Zeile zu besetzen versucht. Die Anweisungen ab Zeile 41 werden von allen Prozessen ausgeführt mit Ausnahme derjenigen, die entweder bei fork() scheitern oder selbst eine Lösung gefunden haben. Die Aufgabe besteht hier darin, all die Zahlen der gefundenen Lösungen der einzelnen Unterprozesse zu addieren. Dies erledigt die while-schleife auf Zeile 43, die wait() solange aufruft, bis -1 zurückgeliefert wird, d.h. alle Unterprozesse berücksichtigt worden sind. Die Exit-Werte werden in Zeile 47 einfach aufaddiert. Es findet nur eine zusätzliche Überprüfung statt, ob der Unterprozess normal terminierte und selbst keine Probleme mit der Erzeugung weiterer Unterprozesse hatte. So ein Problem wird mit einem Exit-Wert von 255 in Zeile 28 signalisiert. Dieses Beispiel ist nicht zur Nachahmung geeignet, da es sehr schnell am Mangel an zur Verfügung stehender Einträge in der Prozesstabelle scheitert. Dennoch ist der Ansatz brauchbar, wenn es darum geht, auf einer Mehrprozessor-Maschine die vorhandenen Ressourcen auszunutzen. Allerdings macht es dann nicht Sinn, mehr Prozesse zu erzeugen als tatsächlich Prozessoren vorhanden sind. Ein pragmatischer Ansatz könnte es sein, die erste Stufe der Rekursion (also hier die erste Reihe auf dem Schachbrett) zu parallelisieren und ansonsten sequentiell weiterzuarbeiten. Elegant wird der Programmtext jedoch durch so einen Mischansatz nicht. Ein weiterer Problempunkt ist die Rückgabe gefundener Lösungen. Dies geht entweder nur unter Verwendung von Dateien (leicht zu programmieren, jedoch nicht sehr effizient) oder der direkten Kommunikation zwischen den Prozessen (effizienter, jedoch leider nicht einfach zu programmieren). Für eine Dateiausgabe könnte der folgende Text zwischen Zeile 35 und 36 eingefügt werden: if (col == size) { char file[32]; FILE * fp; sprintf(file,"%d",getpid()); fp = fopen(file,"a"); fprintf(fp,"process %d:\n", getpid()); for (i=0; i< size; i++) fprintf(fp,"row %d / col %d\n", pos[i].row, pos[i].col); } Der gesamte Stand auf dem n n Schachbrett (im folgenden wieder n = 8) wird in einer Reihe lokaler Variablen von main() verwaltet. Abgesehen von col wird keine dieser Variablen im ersten Prozess modifiziert. Stattdessen erfolgen sämtliche Änderungen nur bei Unterprozessen, die dank der Magie des fork()- Aufrufes auf einer Kopie davon arbeiten. Die entscheidende Schleife beginnt in Zeile 21. Auf den Reihen 0 bis nofqueens 1 sind die Damen bereits gesetzt. Als nächstes ist nun eine Dame auf Reihe nofqueens zu plazieren. Dazu geht die Schleife sämtliche Spaltenpositionen durch und überprüft in den Zeilen 22 bis 25, ob die einzelnen Varianten im Konflikt zu den bereits plazierten Damen stehen. Weitere Minuspunkte ergeben sich aus der Verwendung einer goto-anweisung und gemeinsamer Programmpfade nach dem fork().

23 2.5. N-DAMEN-PROBLEM MIT PROZESSEN 39 Programm 2.18: Rekursion mit Unterprozessen (forkqueens.c) 1 #include < stdio.h> 2 #include < stdlib.h> 3 #include <sys/wait. h> 4 #include < unistd.h> 5 #define INSET(member,set ) ((1«( member))&set) 6 #define INCL(set,member ) (( set ) = 1«( member)) 7 8 int main () { 9 const int size = 4; / square size of the board / 10 struct { int row, col ; } pos[ size ]; / queen positions / 11 / a queen on ( row, col ) threatens a row, a column, and 2 diagonals ; 12 rows and columns are characterized by their number (0.. n 1), 13 the diagonals by row col+n 1 and row+col, 14 (n is a shorthand for the square size of the board ) 15 / 16 int rows = 0, cols = 0; / bitmaps of [0.. n 1] / 17 int diags1 = 0; / bitmap of [0..2 ( n 1)] used for row col+n 1 / 18 int diags2 = 0; / bitmap of [0..2 ( n 1)] used for row+col / 19 int row ; int col = 0; setnextqueen : for ( row = 0; row < size ; ++row ) { 22 if ( INSET(row, rows )) continue; 23 if ( INSET(col, cols )) continue; 24 if ( INSET(row col + size 1, diags1 )) continue; 25 if ( INSET(row + col, diags2 )) continue; 26 int child = fork (); 27 if ( child == 1) { 28 perror ( "fork" ); exit (255); 29 } 30 if ( child == 0) { 31 INCL(rows, row ); INCL(cols, col ); 32 INCL(diags1, row col + size 1); 33 INCL(diags2, row + col ); 34 pos[row ]. row = row ; pos[row ]. col = col ; 35 ++col ; / set next queen in next col / 36 if ( col == size ) exit (1); / solution found / 37 goto setnextqueen ; 38 } 39 } / count the results of all children / 42 int stat ; pid_t child ; int nofsolutions = 0; 43 while (( child = wait(& stat )) > 0) { 44 if (! WIFEXITED(stat)) continue; 45 int exitval = WEXITSTATUS(stat); 46 if ( exitval == 255) exit (255); 47 nofsolutions += exitval ; 48 } 49 exit ( nofsolutions ); 50 } 40 KAPITEL 2. PROZESSE UNTER UNIX 2.6 Datei-Umlenkung Viele Unix-Kommandos sind als Filter konzipiert: wenn nichts anderes über die Kommandozeile angegeben ist, so lesen sie von der Standardeingabe (0), schreiben auf die Standardausgabe (1) und setzen ihre Fehler- / Diagnoseausgaben auf Standarderror (2) sofern dies jeweils Sinn macht. Programm 2.19 zeigt eine einfache Anwendung: wir setzen den Filedeskriptor 0 auf eine Datei und starten dann ein Programm, das von 0 liest. Übersetzung und Ausführung: spatz$ gcc -Wall umlenkung1.c spatz$ a.out # include <stdio.h> # include <fcntl.h> # include <unistd.h> spatz$ Programm 2.19: Über 0 aus einer Datei lesen (umlenkung1.c) 1 # include < stdio.h> 2 # include < fcntl.h> 3 # include < unistd.h> 4 # include < stdlib.h> 5 # include <sys/wait. h> 6 7 int main () { 8 pid_t pid ; 9 if ( ( pid = fork () ) < 0 ) 10 perror ("fork" ), exit (1); 11 if ( pid == 0 ) { 12 close (0); 13 if ( open("umlenkung1.c", O_RDONLY) < 0) 14 perror ("open" ), exit (2); 15 execlp ("head", "head", " 3", NULL); 16 perror ("exec" ), exit (3); 17 } else { 18 wait (0); 19 return 0; 20 } 21 }

24 2.6. DATEI-UMLENKUNG 41 Das geht natürlich auch ausgabeseitig, wie Programm 2.20 (S. 41) zeigt: 42 KAPITEL 2. PROZESSE UNTER UNIX 2.7 Eine kleine Shell (tinysh) spatz$ gcc -Wall umlenkung2.c spatz$ a.out Ein kleiner Text auf 2 Zeilen spatz$ cat XXX Ein kleiner Text auf 2 Zeilen spatz$ Programm 2.20: Über 1 in eine Datei schreiben (umlenkung2.c) 1 # include < stdio.h> 2 # include < fcntl.h> 3 # include < unistd.h> 4 # include <sys/types.h> 5 # include <sys/wait. h> 6 7 int main () { 8 pid_t pid ; 9 if ( ( pid = fork () ) < 0 ) { 10 perror ("fork" ); 11 return 1; 12 } 13 if ( pid == 0 ) { 14 close (1); 15 if ( open("xxx", O_WRONLY O_CREAT O_TRUNC, 0666) < 0) { 16 perror ("open"); 17 return 2; 18 } 19 execlp (" cat ", " cat ", NULL); 20 perror ("exec" ); 21 return 3; 22 } else { 23 wait (0); 24 return 0; 25 } 26 } Allgemeines Wenn man in der Shell ein Kommando startet (oder auf einer grafischen Oberfläche ein Programm durch Anklicken startet), so läuft die traditionelle Erzeugung eines Prozesses ab: Erzeuge einen neuen Prozess mit einem gegebenen Programmtext. (Die Kombination aus fork() und exec(). Einrichtung der Umgebung für den neuen Prozess. Start des neuen Prozesses. Shell fork() wait() Umgebung einrichten exec() Anwendung exit() Abbildung 2.7: Start einer Anwendung von der Shell Die Trennung in fork() und exec() eröffnet die Möglichkeit, dass der Programmtext des übergeordneten Prozesses direkt im neu erzeugten Prozess die Umgebung vorbereitet, die dann bei exec() von dem gleichen Prozess mit dem neuen Programmtext vorgefunden wird. Wie Abbildung 2.7 zeigt, wird genau dies von den UNIX-Shells genutzt, um die Umgebung für Anwendungen einzurichten. Der übergeordnete Prozess der Shell wartet dann normalerweise mit wait() auf das Ende des untergeordneten Prozesses Anforderungen Jede Eingabezeile ist entweder leer oder enthält genau ein Kommando. Die einzelnen Worte (Token) sind durch Leerzeichen getrennt. Ein-/Ausgabe-Umlenkung von / auf Dateien ist möglich. Dazu dienen spezielle Worte: beginnt ein Wort mit einem <, so sind die darauf folgenden Zeichen der Name der Eingabedatei; beginnt ein Wort mit >, so sind die darauf folgenden Zeichen der Name der Ausgabedatei sollte die Datei bereits existieren, wird sie auf Länge 0 verkürzt existiert die Datei noch nicht, wird sie angelegt;

25 2.7. EINE KLEINE SHELL (TINYSH) 43 beginnt ein Wort mit >>, so sind die darauf folgenden Zeichen der Name der Ausgabedatei; das Schreiben erfolgt am Ende der Datei; existiert die Datei noch nicht, wird sie angelegt; Es wird der Exit-Status des Programmes ausgegeben, falls dieser ungleich 0 ist. Wenn das Programm nicht gestartet werden kann, so soll der Exit-Status 255 ausgegeben werden. Es wird kein Signal-Handling durchgeführt ; es gibt keine Unterstützung von Hintergrundkommandos, Pipelines, Builtin-Kommandos oder Shell-Variablen Grundlegender Ablauf Prompt ausgeben Kommandozeile einlesen readline() Zerlegen in Worte tokenizer() 44 KAPITEL 2. PROZESSE UNTER UNIX Einschub: stralloc.h, libowfat-bibliothek Es ist zwar sehr einfach möglich mittels calloc() bzw. strdup() einen dynamischen String anzulegen, jedoch bieten die Funktionen strcpy(), strcat(), sprintf(), etc. keine Möglichkeit der Überprüfung, ob die allozierte Länge nicht überschritten wird was nicht zuletzt an der fehlenden Größeninformation bei C-Arrays liegt. Es bietet sich also die Verwendung der Bibliothek libowfat ( an, die von Felix von Leitner nach einem Vorbild von Dan J. Bernstein nachprogrammiert und unter die GPL (GNU General Public License) gestellt wurde. Diese Bibliothek ist bei uns lokal unter /usr/local/diet installiert. Sie kann sehr leicht auch unter Linux installiert werden: herunterladen und entpacken, make aufrufen (GNUmakefile ist enthalten), im GNUmakefile den prefix anpassen (z.b. auch auf /usr/local/diet) und danach make install aufrufen. Bei C-Arrays fehlt im Wesentlichen die Größeninformation. Dies behebt die folgende Datenstruktur für Strings in der stralloc-bibliothek: typedef struct stralloc { char s ; / Zeichen des Strings ( i.a. ohne \0 am Ende ) / unsigned int len ; / Laenge des Strings ( len <= a ) / unsigned int a ; / Laenge des Arrays s / } stralloc ; I/O Umlenkung vorbereiten fassign() Argumentvektor aufbauen Hinter s verbirgt sich das Zeichenarray, das im Allgemeinen nicht nullterminiert ist. Dies hat den Vorteil, dass Strings auch das Null-Byte enthalten können. Die Komponente len enthält die momentane Länge des Strings und a ist die momentante Länge des Arrays s; also gilt folglich immer len a. Da C lokale Variablen nicht automatisch initialisiert, müssen diese jeweils von Hand initialisiert werden: stralloc sa = {0}; Warten Abbildung 2.8: Ablaufprinzip der tinysh Kommando ausführen Man beachte, dass durch obige Initialisierung nicht nur sa. s, sondern auch sa. len und sa. a auf 0 initialisiert werden. Im Folgenden sind die wesentlichen Funktionen der stralloc-bibliothek kurz beschrieben. Der Rückgabewert dieser Funktionen ist bei Erfolg 1 und sonst 0. int stralloc_ready(stralloc* sa,unsigned int len) Stellt sicher, dass genügend Platz für len Zeichen vorhanden ist. int stralloc_readyplus(stralloc* sa,unsigned int len) Stellt sicher, dass genügend Platz für weitere len Zeichen vorhanden ist. int stralloc_copys(stralloc* sa,const char* buf) Kopiert den nullterminierten String buf nach sa. int stralloc_copy(stralloc* sa,const stralloc* sa2) Kopiert sa2 nach sa.

26 2.7. EINE KLEINE SHELL (TINYSH) 45 int stralloc_cats(stralloc* sa,const char* in) Hängt den nullterminierten String buf an sa an. int stralloc_cat(stralloc* sa,stralloc* in) Hängt sa2 an sa an. int stralloc_0(stralloc* sa) Hängt ein Null-Byte an sa an. void stralloc_free(stralloc* sa) Gibt den von sa belegten Speicher wieder frei also den Speicher von sa >s (und nicht den Speicher für die stralloc-struktur) Einlesen der Kommandozeile readline() Damit können wir die Funktion readline() zum Einlesen der Kommandozeile implementieren. Programm 2.21 (S. 45) definiert die Schnittstelle, Programm 2.22 (S. 45) enthält die Implementierung. Programm 2.21: tinysh: readline() Schnittstelle (tinysh/sareadline.h) 1 #ifndef SA_READLINE_H 2 #define SA_READLINE_H 3 4 #include < stdio.h> 5 #include < stralloc. h> 6 int readline (FILE fp, stralloc sa ); 7 8 #endif Programm 2.22: tinysh: readline() Implementierung (tinysh/sareadline.c) 1 / 2 Read a string of arbitrary length from a 3 given file pointer. LF is accepted as terminator. 4 1 is returned in case of success, 0 in case of errors. 5 afb 4/ / 7 8 #include < stralloc. h> 9 #include < stdio.h> int readline (FILE fp, stralloc sa ) { 12 if (! stralloc_copys ( sa, "" )) return 0; // FALSE 13 for (;;) { 14 if (! stralloc_readyplus ( sa, 1)) return 0; // FALSE 15 if ( fread ( sa >s + sa >len, sizeof (char ), 1, fp ) <= 0) 16 return 0; // FALSE 17 if ( sa >s[sa >len] == \n ) break; 18 ++sa >len; 19 } 20 return 1; // TRUE 21 } 46 KAPITEL 2. PROZESSE UNTER UNIX Mit der C-Funktion size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); für binären Stream-Input werden nmemb Datenelemente mit je size Bytes Länge vom Stream stream gelesen und in ptr abgelegt. Rückgabewert ist die Anzahl der gelesenen Elemente. Ein kleines Testprogramm zeigt 2.23, S. 46. Programm 2.23: tinysh: readline() Test (tinysh/test-readline.c) 1 #include < stdio.h> 2 #include < stdlib.h> 3 #include " sareadline.h" 4 5 void print ( stralloc sa ) { 6 int i ; 7 for ( i = 0; i < sa >len; i++) 8 putchar ( sa >s[i ]); 9 } int main () { 12 stralloc line = {0}; 13 printf ( "Eingabe: " ); 14 readline ( stdin, & line ); 15 printf ( "Gelesen: " ); 16 print (& line ); 17 puts ("" ); 18 exit (0); 19 } Übersetzung und Ausführung: spatz$ gcc -Wall -I /usr/local/diet/include/ \ -L /usr/local/diet/lib/ sareadline.c test-readline.c -lowfat swg@spatz:~/skripte/soft2.05/2/progs/tinysh> a.out Eingabe: aaa bbb >xxx >>zzz Gelesen: aaa bbb >xxx >>zzz spatz$

27 2.7. EINE KLEINE SHELL (TINYSH) Zerlegen in Tokens tokenizer() Der Wortzerleger tokenizer() arbeitet auf einer als stralloc repräsentierten Zeichenkette und generiert eine Liste vom Typ strlist, die auf die einzelnen Wörter verweist. Dabei wird ein Umkopieren der Wörter vermieden. Stattdessen verweisen die Zeiger in die originale Zeichenkette und die Leerzeichen werden in Nullbytes verwandelt, um als Begrenzer dienen zu können. Abbildung 2.9 zeigt die resultierende Datenstruktur an einem Beispiel. len a len input s 7 30 tokens list 4 allocated 8 c p 0 x 0 y 0 Abbildung 2.9: Datenstruktur zur Wortzerlegung Die Datenstruktur für tokens ist ähnlich der in der stralloc-bibliothek aufgebaut, die Struktur wie die notwendigen Funktionen sind in 2.24, S. 48, definiert und in 2.25, S. 49 implementiert KAPITEL 2. PROZESSE UNTER UNIX Programm 2.24: tinysh: Datenstruktur für den tokenizer() Schnittstelle (tinysh/strlist.h) 1 / 2 Data structure for dynamic string lists that works 3 similar to the stralloc library. 4 Return values : 1 if successful, 0 in case of failures. 5 afb 4/ / 7 8 #ifndef STRLIST_H 9 #define STRLIST_H typedef struct strlist { 12 char list ; 13 unsigned int len ; / # of strings in list / 14 unsigned int allocated ; / allocated length for list / 15 } strlist ; / assure that there is at least room for len list entries / 18 int strlist_ready ( strlist list, unsigned int len ); / assure that there is room for len additional list entries / 21 int strlist_readyplus ( strlist list, unsigned int len ); / truncate the list to zero length / 24 int strlist_clear ( strlist list ); / append the string pointer to the list / 27 int strlist_push ( strlist list, char string ); 28 #define strlist_push0 ( list ) strlist_push (( list ), 0) / free the strlist data structure but not the strings / 31 int strlist_free ( strlist list ); #endif

28 2.7. EINE KLEINE SHELL (TINYSH) 49 Programm 2.25: tinysh: Datenstruktur für den tokenizer() Implementierung (tinysh/strlist.c) 1 / 2 Data structure for dynamic string lists that works 3 similar to the stralloc library. 4 Return values : 1 if successful, 0 in case of failures. 5 afb 4/ / 7 #include < stdlib.h> 8 #include " strlist.h" 9 10 / assure that there is at least room for len list entries / 11 int strlist_ready ( strlist list, unsigned int len ) { 12 if ( list > allocated < len ) { 13 unsigned int wanted = len + ( len»3) + 8; 14 char newlist = ( char ) realloc ( list >list, 15 sizeof (char ) wanted ); 16 if ( newlist == 0) return 0; 17 list >list = newlist ; 18 list > allocated = wanted; 19 } 20 return 1; 21 } / assure that there is room for len additional list entries / 24 int strlist_readyplus ( strlist list, unsigned int len ) { 25 return strlist_ready ( list, list >len + len ); 26 } / truncate the list to zero length / 29 int strlist_clear ( strlist list ) { 30 list >len = 0; 31 return 1; 32 } / append the string pointer to the list / 35 int strlist_push ( strlist list, char string ) { 36 if (! strlist_ready ( list, list >len + 1)) return 0; 37 list >list [ list >len++] = string ; 38 return 1; 39 } / free the strlist data structure but not the strings / 42 int strlist_free ( strlist list ) { 43 free ( list >list ); list >list = 0; 44 list > allocated = 0; 45 list >len = 0; 46 return 1; 47 } 50 KAPITEL 2. PROZESSE UNTER UNIX Das Programm für den tokenizer() hierzu besteht wieder aus Headerfile (2.26, S. 50) und Implementierung (2.27, S. 50) Programm 2.26: tinysh: tokenizer() Schnittstelle (tinysh/tokenizer.h) 1 #ifndef TOKENIZER_H 2 3 #define TOKENIZER_H 4 #include < stralloc. h> 5 #include " strlist.h" 6 int tokenizer ( stralloc input, strlist tokens ); 7 8 #endif Programm 2.27: tinysh: tokenizer() Implementierung (tinysh/tokenizer.c) 1 / 2 Simple tokenizer : Take a 0 terminated stralloc object and return a 3 list of pointers in tokens that point to the individual tokens. 4 Whitespace is taken as token separator and all whitespaces within 5 the input are replaced by null bytes. 6 afb 4/ / 8 9 #include < ctype.h> 10 #include < stdlib.h> 11 #include < stralloc. h> 12 #include " strlist.h" 13 #include " tokenizer. h" int tokenizer ( stralloc input, strlist tokens ) { 16 char cp ; 17 int white = 1; strlist_clear ( tokens ); 20 for ( cp = input >s; cp && cp < input >s + input >len; ++cp ) { 21 if ( isspace ( cp )) { 22 cp = \0 ; white = 1; continue; 23 } 24 if (! white ) continue; 25 white = 0; 26 if (! strlist_push ( tokens, cp )) return 0; 27 } 28 return 1; 29 }

29 2.7. EINE KLEINE SHELL (TINYSH) 51 Ein kleines Test-Programm zeigt 2.28, S. 51. Programm 2.28: tinysh: tokenizer() Test (tinysh/test-tokenizer.c) 1 #include < stdio.h> 2 #include < stdlib.h> 3 #include " sareadline.h" 4 #include " strlist.h" 5 #include " tokenizer. h" 6 7 int main () { 8 stralloc line = {0}; 9 printf ( "Eingabe: " ); 10 readline ( stdin, & line ); strlist tokens = {0}; 13 stralloc_0 (& line ); / required by tokenizer () / 14 if (! tokenizer (& line, & tokens )) printf ("ERROR\n"); 15 if ( tokens. len == 0) printf ("No Tokens\n"); 16 for ( int i = 0; i < tokens. len ; ++ i ) 17 printf ("Token %d: %s \n", i,tokens. list [ i ]); 18 exit (0); 19 } Übersetzung und Ausführung: spatz$ gcc -Wall -std=c99 -I /usr/local/di et/include/ \ -L /usr/local/diet/lib/ \ sareadline.c strlist.c tokenizer.c test-tokenizer.c -lowfat spatz$ a.out Eingabe: aaa >bbb >> >>xxx " " Token 0: aaa Token 1: >bbb Token 2: >> Token 3: >>xxx Token 4: " Token 5: " spatz$ 52 KAPITEL 2. PROZESSE UNTER UNIX Hauptprogramm unserer Shell Programm 2.29: tinysh: Hauptprogramm (tinysh/tinysh.c) 1 #include < fcntl.h> 2 #include < stdio.h> 3 #include < stdlib.h> 4 #include < unistd.h> 5 #include <sys/wait. h> 6 #include " sareadline.h" 7 #include " strlist.h" 8 #include " tokenizer. h" 9 10 / 11 assign an opened file with the given flags and mode to fd 12 / 13 void fassign ( int fd, char path, int oflags, mode_t mode ) { 14 int newfd = open( path, oflags, mode); 15 if ( newfd < 0) { 16 perror ( path ); exit (255); 17 } 18 if ( dup2(newfd, fd ) < 0) { 19 perror ("dup2" ); exit (255); 20 } 21 close (newfd ); 22 } int main () { 25 stralloc line = {0}; 26 while ( printf ( "%% "), readline ( stdin, & line )) { 27 strlist tokens = {0}; 28 stralloc_0 (& line ); / required by tokenizer () / 29 if (! tokenizer (& line, & tokens )) break; 30 if ( tokens. len == 0) continue; 31 pid_t child = fork (); 32 if ( child == 1) { 33 perror ( "fork" ); continue; 34 } 35 if ( child == 0) { 36 strlist argv = {0}; / list of arguments / 37 char cmdname = 0; / first argument / 38 char path ; / of output files / 39 int oflags ; for ( int i = 0; i < tokens. len ; ++ i ) { 42 switch ( tokens. list [ i ][0]) { 43 case < : 44 fassign (0, & tokens. list [ i ][1], O_RDONLY, 0); 45 break; 46 case > : 47 path = & tokens. list [ i ][1]; 48 oflags = O_WRONLY O_CREAT; 49 if ( path == > ) { 50 ++path ; oflags = O_APPEND;

30 2.7. EINE KLEINE SHELL (TINYSH) } else { 52 oflags = O_TRUNC; 53 } 54 fassign (1, path, oflags, 0666); 55 break; 56 default : 57 strlist_push (&argv, tokens. list [ i ]); 58 if ( cmdname == 0) cmdname = tokens. list [ i ]; 59 } 60 } 61 if ( cmdname == 0) exit (0); 62 strlist_push0 (&argv ); 63 execvp (cmdname, argv. list ); 64 perror (cmdname); 65 exit (255); 66 } / wait for termination of child / 69 int stat ; 70 pid_t pid = wait(& stat ); 71 if ( pid == child ) { 72 if ( WIFEXITED(stat)) { 73 int code = WEXITSTATUS(stat); 74 if ( code && code!= 255) { 75 printf ("terminated with exit code %d\n", code); 76 } 77 } else { 78 printf ("terminated abnormally\n"); 79 } 80 } else { 81 perror ( "wait" ); 82 } 83 } 84 } 54 KAPITEL 2. PROZESSE UNTER UNIX Dazu noch ein kleines Makefile: CC = gcc CFLAGS = -Wall -std=c99 -I /usr/local/diet/include/ \ -L /usr/local/diet/lib/ OBJ = tinysh.o sareadline.o strlist.o tokenizer.o tinysh: $(OBJ) $(CC) $(CFLAGS) -o tinysh $(OBJ) -lowfat tinysh.o: tinysh.c sareadline.h strlist.h tokenizer.h $(CC) $(CFLAGS) -c tinysh.c sareadline.o: sareadline.h sareadline.c $(CC) $(CFLAGS) -c sareadline.c strlist.o: strlist.h strlist.c $(CC) $(CFLAGS) -c strlist.c tokenizer.o: tokenizer.h tokenizer.c strlist.h $(CC) $(CFLAGS) -c tokenizer.c.phony: clean realclean clean: rm -f $(OBJ) core realclean: clean rm -f tinysh Stattdessen hätten wir auch das Makefile-Template (siehe Titelseite) verwenden können! 2.8 Bootstrapping klassisch Henne oder Ei? Alle Aktionen in einem UNIX System erfolgen durch Prozesse. Für alle Prozesse besteht eine Erzeuger-Kindprozeß-Beziehung. Bleibt die Frage, woher kommt der erste Prozess, die Wurzel dieses Prozessbaums? Bootstrapping Das Starten eines Betriebssystems heißt Bootstrapping. Für UNIX fallen darunter alle Aktionen vom Stromeinschalten bis zum Erreichen des stabilen Zustands in dem Prozess 1 läuft. Anschließend können alle weiteren Aktionen mit Prozessen und nach den durch die System Calls festgelegten Regeln erfolgen. Phase 0 Hardware Das Einschalten der Stromversorgung bewirkt verschiedene Aktionen, die sehr von der Hardware und der Architektur des Rechners abhängen. Alle haben aber das gleiche Ziel: Selbsttest und Grundinitialisierung der einzelnen Komponenten. Die Hardware befindet sich danach in einem definierten Startzustand.

31 2.8. BOOTSTRAPPING KLASSISCH 55 Phase 1: First Stage Boot Die Hardware verfügt über einige festeingebaute Routinen, die in der Lage sind, einen Boot-Block von der Platte zu lesen und zur Ausführung zu bringen. Der Boot-Block enthält ein loader - Programm. Dieses Programm muss mit der Console kommunizieren können und bereits über das UNIX File System Bescheid wissen. Falls der Platz im Boot-Block für ein Programm mit diesen Fähigkeiten nicht ausreicht, muß das minimale Boot Loader Programm ein weiteres, größeres Loader Programm laden und ausführen können. Phase 2: Second Stage Boot Aufgabe des loader - Programms ist es, den Kernel, meist die Datei /unix, von der Platte in den Hauptspeicher zu laden und zu starten. Anschließend beginnt UNIX zu leben. Loader Programme sind meist trickreiche Assemblerprogramme. Im Boot-Block können nur wenige Anweisungen untergebracht werden, die müssen aber komplexe, Hardwarenahe Aufgaben erledigen. 56 KAPITEL 2. PROZESSE UNTER UNIX 2.9 Der init-prozess user Id 0 Operationen: BSD init führt das Shell-Skript /etc/rc aus. Dabei werden u.a. einige Daemon-Prozesse gestartet. init entnimmt der Datei /etc/ttys, welche Terminals für den Multiuser-Betrieb aktiviert werden müssen. 2. System V init liest die Datei /etc/inittab, in der spezifiziert ist, was zu tun ist. Zum normalen Multiuser-Betrieb wird die Datei /etc/rc ausgeführt; dieses Programm startet die meisten Daemon-Prozesse. Nach dessen Beendigung werden wie in /etc/inittab definiert die angeschlossenen Terminals aktiviert. (mehr dazu in einem Administrator Reference Manual). Phase 3: Standalone init process ID = 1 Sobald der UNIX Kernel gestartet wurde, übernimmt er die gesamte Kontrolle über den Rechner. Der Kernel kann jetzt alles weitere aus eigener Kraft erledigen (Standalone). fork() fork() fork() In seiner Startphase setzt er die Interruptvektoren im low mem, initialisiert die Speicherverwaltungs-Hardware, baut seine Tabellen (Prozess-, Open File-, Inode-, usw.) auf führt eine mount-ähnliche Operation für das root File System aus... Nun fehlt noch etwas Magie, um den ersten Prozess zu erschaffen. Die Prozessverwaltung generiert in ihrer Startphase den Prozess 0 von Hand. Dieser Prozess, meist swapper genannt, besteht nur aus dem System-Daten Bereich: Slot 0 in der Prozesstabelle plus per process data region. Er besitzt keinen Text oder User-Daten Bereich. Dafür existiert er während der gesamten Systemlaufzeit und ist für das Scheduling zuständig. Er benötigt hierfür nur Instruktionen und Daten aus dem Kernel-Adressraum. Da Prozess 0 eigentlich kein echter Prozess ist, erschafft der Kernel auch Prozess 1 manuell. Soweit als möglich benutzt oder imitiert der Kernel hier bereits den fork Mechanismus, um den Prozess 1 vom Prozess 0 abzuspalten. Prozess 1 erhält jedoch einen ganz regulären Context und kann anschließend vom Scheduler als normaler Prozess zur Ausführung gebracht werden. Sein hand crafted Text Bereich enthält einzig die Anweisung execl( /etc/init, init, 0 ) Nach dem exec System Call läuft im Context des Prozesses 1 das Programm /etc/init und Prozess 1 heißt nun init - Prozess. init init init getty wartet auf login Name exec() getty login exec() exec Arg.: Login Name wartet auf Passwort Abbildung 2.10: Der Init-Prozess getty login /bin/sh exec() exec() exec() getty setzt die Übertragungsgeschwindigkeit des Terminals, gibt irgendeine Begrüßungsformel aus und wartet auf Eingabe eines login-namens. Login-Name eingegeben exec(/bin/login) login sucht den eingegebenen Login-Namen in der Passwortdatei und fordert die Eingabe eines Passwortes. Alle bis hier ausgeführten Programme (init, getty, login) laufen als Prozesse mit user ID und effective user ID 0 (superuser) - mit dem System Call exec ändert sich die process ID nicht. Danach wird das current working directory auf den entsprechenden Eintrag für das login directory aus der Passwortdatei gesetzt.

32 2.9. DER INIT-PROZESS KAPITEL 2. PROZESSE UNTER UNIX Group ID und user ID (in dieser Reihenfolge) werden via setgid und setuid wie in der Passwortdatei definiert gesetzt. Über exec wird das in der Passwortdatei spezifizierte Programm gestartet (falls keine Angabe: /bin/sh). Falls der sich anmeldende Benutzer nicht der Superuser ist (login-name meist root): setgid und setuid reduzieren Prozessprivilegien beide System Calls sind dem Superuser vorbehalten (daher diese Reihenfolge)

33 60 KAPITEL 3. SIGNALE Kapitel 3 Signale 3.1 Einführung Signale werden für vielfältige Zwecke eingesetzt. Sie können verwendet werden, um den normalen Ablauf eines Prozesses für einen wichtigen Hinweis zu unterbrechen, um die Terminierung eines Prozesses zu erbitten oder zu erzwingen und um schwerwiegende Fehler bei der Ausführung zu behandeln wie z.b. den Verweis durch einen invaliden Zeiger. Signale ersetzen keine Interprozesskommunikation, da sie fast keine Informationen mit sich führen. In Abhängigkeit von der jeweiligen Systemumgebung gibt es mehr oder weniger fest definierte Signale, die über natürliche Zahlen identifiziert werden. Der ISO-Standard für die Programmiersprache C definiert eine einfache und damit recht portable Schnittstelle für die Behandlung von Signalen. Hier gibt es neben der Signalnummer selbst keine weiteren Informationen. Der IEEE Standard (POSIX) bietet eine Obermenge der Schnittstelle des ISO-Standards an, bei der wenige zusätzliche Informationen (wie z.b. die Angabe des invaliden Zeigers) dabei sein können und der insbesondere eine sehr viel feinere Kontrolle der Signalbehandlung erlaubt. Signale können von verschiedenen Parteien bzw. unter unterschiedlichen Bedingungen ausgelöst werden: 1. System Call kill(): damit kann ein Prozess sich oder einem anderen Prozess ein Signal senden (führt nicht notwendig zur Termination) int kill(int pid, int sig); Der sendende Prozess muss entweder ein superuser-prozess sein oder der sendende und empfangende Prozess müssen dieselbe effektive userid haben. Ist das pid-argument 0, so geht das Signal an alle Prozesse in der Prozess-Gruppe des Senders Ist das pid-argument -1 und der Sender nicht der Superuser, so geht das Signal an alle Prozesse, deren real user ID gleich der effective user ID des Senders ist Ist das pid-argument -1 und der Sender der Superuser, so geht das Signal an alle Prozesse ausgenommen die Superuser-Prozesse (i.a. PID s 0 oder 1) Ist das pid-argument negativ, aber ungleich -1, so geht das Signal an alle Prozesse, deren Prozess-Gruppen-Nummer gleich dem Absolutbetrag von pid ist Ist das sig-argument 0, so wird nur eine Fehler-Prüfung durchgeführt, aber kein Signal geschickt (z.b. zur Prüfung der Gültigkeit des pid-arguments) 2. Kommando kill: nutzt den System Call kill() (s.o. und man) 3. Tastatureingaben, z.b.: ctrl-c (oder delete) beendet einen im Vordergrund laufenden Prozess ( SIGINT), genauer alle Prozesse in der Kontrollgruppe dieses Terminals - vom Kernel verschickt ctrl-\) erzeugt SIGQUIT ctrl-z hält einen im Vordergrund laufenden Prozess an ( SIGSTOP) kann mit SIGCONT fortgesetzt werden 4. Hardware-Bedingungen, z.b.: Gleitpunkt-Arithmetik-Fehler (Floating Point Exception) (SIGFPE) Adressraum-Verletzungen (Segmentation Violation) (SIGSEGV) 5. Software-Bedingungen, z.b.: Schreiben in eine Pipe, an der kein Prozess zum Lesen hängt (SIGPIPE). Typisch für die Auslösung durch das Betriebssystem ist die Terminalschnittstelle unter UNIX. Diese wurde ursprünglich für ASCII-Terminals mit serieller Schnittstelle entwickelt, die nur folgende Eingabemöglichkeiten anboten: Einzelne ASCII-Zeichen, jeweils ein Byte (zusammen mit etwas Extra-Kodierung wie Prüfund Stop-Bits). Ein BREAK, das als spezielles Signal repräsentiert wird, das länger als die Kodierung für ein ASCII-Zeichen währt. Ein HANGUP, bei dem ein Signal wegfällt, das zuvor die Existenz der Leitung bestätigt hat. Dies benötigt einen weiteren Draht in der seriellen Leitung. Diese Eingaben werden auf der Seite des Betriebssystems vom Terminal-Treiber bearbeitet, der in Abhängigkeit von den getroffenen Einstellungen die eingegebenen Zeichen puffert und das Editieren der Eingabe ermöglicht (beispielsweise mittels BACKSPACE, CTRL-u und CTRL-w) und bei besonderen Eingaben Signale an alle Prozesse schickt, die mit diesem Terminal verbunden sind. 59

34 3.2. SIGNALBEHANDLER 61 Ziel war es, dass im Normalfall ein BREAK zu dem Abbruch oder zumindest der Unterbrechung der gerade laufenden Anwendung führt. Und ein HANGUP sollte zu dem Abbruch der gesamten Sitzung führen, da bei einem Wegfall der Leitung keine Möglichkeit eines regulären Abmeldens besteht. Heute sind serielle Terminals rar geworden, aber das Konzept wurde dennoch beibehalten. Zwischen einem virtuellen Terminal (beispielsweise einem xterm) und den Prozessen, die zur zugehörigen Sitzung gehören, ist ein sogenanntes Pseudo-Terminal im Betriebssystem geschaltet, das der Sitzung die Verwendung eines klassischen Terminals vorspielt. Da es BREAK in diesem Umfeld nicht mehr gibt, wird es durch ein beliebiges Zeichen ersetzt wie beispielsweise CTRL-c. Wenn das virtuelle Terminal wegfällt (z.b. durch eine gewaltsame Beendigung der xterm-anwendung), dann gibt es weiterhin ein HANGUP für die Sitzung. Auf fast alle Signale können Prozesse, die sie erhalten, auf dreierlei Weise reagieren: Voreinstellung: Terminierung des Prozesses. Ignorieren. Bearbeitung durch einen Signalbehandler. Es mag harsch erscheinen, dass die Voreinstellung zur Terminierung eines Prozesses führt. Aber genau dies führt bei normalen Anwendungen genau zu den gewünschten Effekten wie Abbruch des laufenden Programms bei BREAK (die Shell ignoriert das Signal) und Abbau der Sitzung bei HANGUP. Wenn ein Prozess diese Signale ignoriert, sollte es genau wissen, was es tut, da der Nutzer auf diese Weise eine wichtige Kontrollmöglichkeit seiner Sitzung verliert. Sinnvoll ist es natürlich, eine Anwendung mit einem Signalbehandler zu ergänzen, wenn dadurch Datenverluste bei einer Terminierung vermieden werden können. 3.2 Signalbehandler 3.3 Reaktion auf Signale: signal() Ein Prozess kann eine Funktion definieren, die bei Eintreten eines bestimmten Signals ausgeführt werden soll (signal handler). Signale (außer SIGKILL und SIGSTOP) können ignoriert werden. System Call signal: #include <signal.h> int (*signal (int sig, void (*func) (int))) (int); /* ANSI C signal handling */ 62 KAPITEL 3. SIGNALE Also: signal ist eine Funktion, die einen Zeiger auf eine Funktion zurückliefert, die selbst eine int zurückliefert (die bisherige Reaktion auf das Signal oder bei Fehler SIG_ERR) Das erste Argument ist die Signalnummer (Makro aus signal.h), das zweite (func) ist Zeiger auf eine Funktion, die void liefert (die neue Reaktion auf das Signal) Wegen der detaillierteren Reaktionsmöglichkeit und einiger anderen Fußangeln ist die Funktion sigaction() vorzuziehen! Programm 3.1 demonstriert die Behandlung des Signals SIGINT. Als Signalbehandler operiert die Funktion signal_handler(). Signalbehandler erhalten als Argument eine ganze Zahl, worüber sie die Nummer des Signals erhalten, das sie gerade bearbeiten. Einen Rückgabewert gibt es nicht. Programm 3.1: Behandlung eines SIGINT-Signals (sigint.c) 1 #include < signal.h> 2 #include < stdio.h> 3 #include < stdlib.h> 4 5 volatile sig_atomic_t signal_caught = 0; 6 7 void signal_handler ( int signal ) { 8 signal_caught = signal ; 9 } int main () { 12 if ( signal (SIGINT, signal_handler ) == SIG_ERR) { 13 perror ("unable to setup signal handler for SIGINT"); 14 exit (1); 15 } 16 printf ( "Try to send a SIGINT signal!\n"); 17 int counter = 0; 18 while (! signal_caught ) { 19 for ( int i = 0; i < counter ; ++ i ) 20 ; 21 ++counter ; 22 } 23 printf ( "Got signal %d after %d steps!\n", signal_caught, counter ); 24 } Erläuterungen zu Programm 3.1 (S. 62): Der Signalbehandler signal_handler() setzt nur eine globale Variable auf den Wert des erhaltenen Signals (Zeile 8). Die Verwendung der Speicherklasse volatile und des Datentyps sig_atomic_t wird später diskutiert.

35 3.3. REAKTION AUF SIGNALE: SIGNAL() 63 main() richtet auf Zeile 12 mit der Funktion signal() für das Signal mit der Nummer SIGINT die Funktion signal_handler() als Reaktion auf dieses Signal ein. Alternativen zu diesem Funktionszeiger wären SIG_DFL für die Prozessterminierung oder SIG_IGN für das Ignorieren. Im Erfolgsfalle liefert signal() die frühere Einstellung zurück, ansonsten SIG_ERR. Die Schleife in Zeile 18 bricht ab, wenn sich der Wert von signal_caught ändert. Dies kann in diesem Beispiel nur durch den Signalbehandler passieren. Wenn dies geschieht, wird auf Zeile 22 die Nummer des eingetroffenen Signals ausgegeben und das Programm beendet. So könnte ein Aufruf dieses Programms aussehen, wenn das Versenden des Signals SIGINT durch den Terminaltreiber durch die Eingabe von CTRL-c recht schnell geschieht: spatz$ sigint Try to send a SIGINT signal! ^CGot signal 2 after steps! spatz$ Die 2 ist dabei die Nummer des Signals SIGINT: thales$ grep SIGINT /usr/include/sys/iso/signal_iso.h #define SIGINT 2 /* interrupt (rubout) */ thales$ 64 KAPITEL 3. SIGNALE lokale Variablen zu verwenden, mit volatile deklarierte Variablen zu benutzen und Funktionen aufzurufen, die sich an die gleichen Spielregeln halten. Letzteres schließt insbesondere die Verwendung von Ein- und Ausgabe innerhalb eines Signalbehandlers aus. Der ISO-Standard nennt nur abort(), _Exit() 1 und signal() als zulässige Bibliotheksfunktionen. Beim POSIX-Standard werden noch zahlreiche weitere Systemaufrufe genannt. Auf den Manualseiten von Solaris wird dies dokumentiert durch die Angabe Async-Signal-Safe bei MT-Level 2 Zum Datentyp sig_atomic_t: ganzzahliger Typ Lese- und Schreiboperationen laufen garantiert atomar ab Datentyp, den der verwendete Prozessor mit einer einzigen ununterbrechbaren Instruktion laden oder speichern kann Bei allen anderen Datentypen ist es nicht ausgeschlossen, dass mitten in einer Zuweisung ein asynchrones Signal eintreffen kann und der zugehörige Signalbehandler dann eine partiell modifizierte Variable vorfindet. Zur Speicherklasse volatile: Leider sind mit der Bearbeitung von Signalen große Probleme verbunden. Wenn ein optimierender Übersetzer den Programmtext 3.1 (S. 62) analysiert, könnten folgende Punkte auffallen: Die Schleife in den Zeilen 18 bis 21 ruft keine externen Funktionen auf. Innerhalb der Schleife wird signal_caught nirgends verändert. Daraus könnte vom Übersetzer der Schluss gezogen werden, dass die Schleifenbedingung nur zu Beginn einmal überprüft werden muss. Findet der Eintritt in die Schleife statt, könnte der weitere Test der Bedingung ersatzlos wegfallen. Analysen wie diese sind für heutige optimierende Übersetzer Pflicht, um guten Maschinen-Code erzeugen zu können. Es wäre also fatal, wenn darauf nur wegen der Existenz von asynchron aufgerufenen Signalbehandlern verzichtet werden würde. Um beides zu haben, die fortgeschrittenen Optimierungstechniken und die Möglichkeit, Variablen innerhalb von Signalbehandlern setzen zu können, wurde in C die Speicherklasse volatile eingeführt. Damit lassen sich Variablen kennzeichnen, deren Wert sich jederzeit ändern kann selbst dann, wenn dies aus dem vorliegenden Programmtext nicht ersichtlich ist. Entsprechend gilt dann auch in C, dass alle anderen Variablen, die nicht als volatile klassifiziert sind, sich nicht durch magische Effekte verändern dürfen. Daraus folgt, dass korrekte Signalbehandler in ihren Möglichkeiten stark eingeschränkt sind. So ist es nur zulässig, 1 _Exit() unterlässt im Vergleich zu exit() sämtliche Aufräumarbeiten. 2 MT steht hier für Multi-Threading, da dabei ähnliche Probleme auftreten, wenngleich in einem noch größeren Umfang.

36 3.4. WECKSIGNALE MIT ALARM() Wecksignale mit alarm() Zu den Signalen, den der POSIX-Standard definiert, gehört auch SIGALRM, das sich als Wecksignal verwenden lässt. Der Wecker wird mit alarm() unter Angabe einer relativen Weckzeit in Sekunden gestellt. Am Ende dieser Frist kommt es zum Eintreffen des Signals SIGALRM. Programmtext 3.2 (S. 65) zeigt, wie alarm() verwendet werden kann, um eine Operation zeitlich zu befristen. Die Funktion timed_read() arbeitet genauso wie read(), wobei jedoch die Wartezeit auf die gegebene Zahl von Sekunden begrenzt wird. Wenn die Zeit verstreicht, ohne dass eine Eingabe erfolgte, wird 0 zurückgeliefert. Programm 3.2: read-operation mit Zeitlimit (tread/tread.c) 1 / 2 Timed read operation. timed_read () works like read () but returns 0 3 if no input was received within the given number of seconds. 4 / 5 6 #include < signal.h> 7 #include < unistd.h> 8 #include "tread.h" 9 10 static volatile sig_atomic_t time_exceeded = 0; static void alarm_handler ( int signal ) { 13 time_exceeded = 1; 14 } int timed_read ( int fd, void buf, size_t nbytes, unsigned seconds ) { 17 if ( seconds == 0) return 0; 18 / setup signal handler and alarm clock but 19 remember the previous settings : 20 / 21 void ( previous_handler )( int ) = signal (SIGALRM, alarm_handler); 22 if ( previous_handler == SIG_ERR) return 1; time_exceeded = 0; 25 int remaining_seconds = alarm( seconds ); 26 if ( remaining_seconds > 0) { 27 if ( remaining_seconds <= seconds ) { 28 remaining_seconds = 1; 29 } else { 30 remaining_seconds = seconds ; 31 } 32 } 33 int bytes_read = read ( fd, buf, nbytes ); 34 / restore previous settings / 35 if (! time_exceeded ) alarm (0); 36 signal (SIGALRM, previous_handler ); 37 if ( remaining_seconds ) alarm( remaining_seconds ); 38 if ( time_exceeded ) return 0; 39 return bytes_read ; 40 } 66 KAPITEL 3. SIGNALE Was geschieht, wenn mehrere Wecksignale nebeneinander eingerichtet werden? Da alarm() nur die Verwaltung einer einzigen Weckzeit unterstützt, ist eine kooperative Vorgehensweise notwendig. Dies wird dadurch erleichtert, dass alarm() bei der Einrichtung einer neuen Weckzeit entweder 0 zurückgibt (vorher war keine Weckzeit aktiv) oder eine positive Zahl von Sekunden als Restzeit zum vorher eingestellten Wecksignal. Entsprechend wird auf Zeile 24 in Programmtext 3.2 (S. 65) in der Variablen remaining_seconds die alternative Weckzeit notiert und später auf Zeile 36 findet eine Wiedereinsetzung statt, nachdem zuvor auf Zeile 29 die verbleibende Restzeit neu berechnet worden ist. Analog wird in Zeile 21 der frühere Signalbehandler für SIGALRM in der Variablen previous_handler gesichert, so dass er in Zeile 35 wieder restauriert werden kann. Bei der Restaurierung ist es wichtig, dass kein Fenster offenbleibt, das zum Verlust einer Signalbehandlung führt oder den alten Signalbehandler auf das noch nicht eingetretene Signal für das von timed_read() eingerichtete Zeitlimit reagieren lässt. Wenn der alte Signalbehandler SIG_DFL war, also die Voreinstellung, dann würde das sogar unerwartet zur Terminierung unseres Prozesses führen. Deswegen wird in Zeile 34 der Wecker zuerst deaktiviert, wenn er bislang sein Signal noch nicht von sich gegeben hat. Danach kann in Zeile 35 der alte Signalbehandler eingesetzt werden, ohne dass wir Gefahr laufen, daß er sofort verwendet wird. In Zeile 36 wird dann der Wecker neu aufgesetzt, falls er vor dem Aufruf von timed_read() eingeschaltet war. Es bleibt hier noch anzumerken, dass es noch bessere Wege gibt, Leseoperationen mit Zeitbeschränkungen zu versehen. Es empfiehlt sich hier entweder die Verwendung der Systemaufrufe poll() oder select() oder der Einsatz asynchroner Lesetechniken. Dennoch ist die Verwendung von alarm() sinnvoll, da es genügend Operationen gibt, bei denen es keine Variante mit Zeitbeschränkung gibt.

37 3.5. DAS VERSENDEN VON SIGNALEN Das Versenden von Signalen Programm 3.3: Versenden eines Signals an den übergeordneten Prozess (killparent.c) 1 #include < stdio.h> 2 #include < stdlib.h> 3 #include < unistd.h> 4 #include < signal.h> 5 #include <sys/wait. h> 6 #include <sys/types. h> 7 8 void sigterm_handler ( int signo ) { 9 const char msg [] = "Goodbye, cruel world!\n"; 10 write (1, msg, sizeof msg 1); 11 _exit (1); 12 } int main () { 15 if ( signal (SIGTERM, sigterm_handler ) == SIG_ERR) { 16 perror (" signal " ); exit (1); 17 } pid_t child = fork (); 20 if ( child == 0) { 21 if ( kill ( getppid (), SIGTERM) < 0 ) { 22 perror (" kill " ); exit (1); 23 } 24 exit (0); 25 } 26 int wstat ; 27 wait(&wstat ); 28 exit (0); 29 } Der ISO-Standard für C sieht nur eine Funktion raise() vor, die es erlaubt, ein Signal an den eigenen Prozess zu versenden. Im POSIX-Standard kommt die Funktion kill() hinzu, die es erlaubt, ein Signal an einen anderen Prozess zu verschicken, sofern die dafür notwendigen Privilegien vorliegen. Programmtext 3.3 zeigt ein Beispiel, bei dem ein neu erzeugter Prozess ein Signal an den Erzeuger sendet. Programm 3.4 (S. 68) dreht den Spiess um: der Erzeuger schickt ein Signal an das erzeugte Kind und untersucht nach dessen Termination (in Kommentaren ist beim Kindprozess das Setzen des Signalbehandlers ausgeblendet) dessen Exit-Status. 68 KAPITEL 3. SIGNALE Programm 3.4: Versenden eines Signals an den erzeugten Prozess (killchild.c) 1 #include < stdio.h> 2 #include < stdlib.h> 3 #include < unistd.h> 4 #include < signal.h> 5 #include <sys/wait. h> 6 #include <sys/types. h> 7 8 void sigterm_handler ( int signo ) { 9 const char msg [] = "Goodbye, cruel world!\n"; 10 write (1, msg, sizeof msg 1); 11 _exit (1); 12 } int main () { 15 pid_t child = fork (); 16 if ( child == 0) { if ( signal (SIGTERM, sigterm_handler ) == SIG_ERR) { 19 perror ( " signal " ); exit (1); 20 } sleep (10); 23 exit (0); 24 } 25 if ( kill ( child, SIGTERM ) < 0 ) { perror ( " kill " ); exit (1); } 26 int wstat ; 27 wait(&wstat ); 28 int sig = wstat & 0177; 29 int exitnumber = ( wstat» 8) & 0377; 30 if ( sig ) 31 printf ("Child terminated with signal %d\n", sig); 32 else 33 printf ("Child terminated with exit number %d\n", exitnumber); 34 exit (0); 35 } Übersetzung und Ausführung ohne Signalbehandler beim Kind: spatz$ gcc -Wall killchild.c spatz$ a.out Child terminated with signal 15 spatz$ Übersetzung und Ausführung mit Signalbehandler beim Kind: spatz$ a.out Goodbye, cruel world! Child terminated with exit number 1 spatz$

38 3.5. DAS VERSENDEN VON SIGNALEN 69 Zur Abfrage des Exit-Codes wie auch der Signalnummer im Exit-Status, wie er über wait() zurückgeliefert wird, sei nochmals auf Abb. 2.3 (S. 29) verwiesen. Trotz ihres geringen Informationsgehalts dienen Signale gelegentlich zur Kommunikation. So gibt es eine weitverbreitete Konvention, dass bei langfristig laufenden Diensten SIGHUP das erneute Einlesen der Konfiguration veranlasst und SIGTERM eine geordnete Terminierung einleiten soll. Gelegentlich sind für Dienste auch Reaktionen für SIGUSR1 und SIGUSR2 definiert. So wird der Apache-Webserver beispielsweise bei SIGUSR1 veranlasst, bei nächster Gelegenheit auf sanfte Weise neu zu starten. Anders als bei SIGHUP kommt es dann nicht zur Unterbrechung aktueller Netzwerkverbindungen. Der Systemaufruf kill() erfüllt aber auch noch einen weiteren Zweck. Bei einer Signalnummer von 0 wird nur die Zulässigkeit des Signalversendens überprüft. Programmtext 3.5 (S. 69) demonstriert, wie dies dazu verwendet werden kann, um die Existenz eines Prozesses zu überprüfen. Programm 3.5: Verwendung von kill() zur Überprüfung der Existenz eines Prozesses (lookfor.c) 1 #include <errno.h> 2 #include < signal.h> 3 #include < stdio.h> 4 #include < stdlib.h> 5 #include < unistd.h> 6 7 int main(int argc, char argv ) { 8 char cmdname = argv++; argc; 9 if ( argc!= 1) { 10 fprintf ( stderr, "Usage: %s pid\n", cmdname); 11 exit (1); 12 } / convert first argument to pid / 15 char endptr = argv [0]; 16 pid_t pid = strtol ( argv [0], & endptr, 10); 17 if ( endptr == argv [0]) { 18 fprintf ( stderr, "%s: integer expected as argument\n", 19 cmdname); 20 exit (1); 21 } if ( kill ( pid,0) == 0 ) 24 printf ("Process %d exists!\n", pid ); 25 else 26 printf ("Process %d doesn t exist!\n",pid ); if ( errno == ESRCH) exit (0); 29 perror (cmdname); exit (1); 30 } 70 KAPITEL 3. SIGNALE Übersetzung und Ausführung von Programm 3.5 (S. 69): spatz$ gcc -Wall lookfor.c spatz$ ps -uswg PID TTY TIME CMD 3976? 00:00:02 fvwm 4015? 00:00:00 ssh-agent 4018? 00:00:00 FvwmTheme 4030? 00:00:02 xterm 4031? 00:00:00 FvwmButtons 4034? 00:00:00 xeyes 4036? 00:00:00 xclock 4037? 00:00:00 FvwmPager 4039 pts/0 00:00:00 bash 4057? 00:00:06 xterm 4059 pts/1 00:00:00 bash 4075 pts/1 00:00:00 myxtel 4170 pts/1 00:00:01 gv 4208 pts/1 00:00:02 gs 4690? 00:00:00 xterm 4692 pts/2 00:00:00 bash 5002 pts/ 2 00:00:00 ps spatz$ a.out 4690 Process 4690 exists! a.out: Success spatz$ a.out 5002 Process 5002 doesn t exist! spatz$ Gelegentlich kommt es vor, dass Prozesse nur auf das Eintreffen eines Signals warten möchten und sonst nichts zu tun haben. Theoretisch könnte ein Prozess dann in eine Dauerschleife mit leerem Inhalt treten (auch busy loop bezeichnet), aber dies wäre nicht sehr fair auf einem System mit mehreren Prozessen, da dadurch Rechenzeit vergeudet würde. Abhilfe schafft hier der Systemaufruf pause(), der einen Prozess schlafen legt, bis ein Signal eintrifft. Programm 3.6 demonstriert das Warten auf ein Signal anhand eines virtuellen Ballspiels zweier Prozesse.

39 3.5. DAS VERSENDEN VON SIGNALEN 71 Programm 3.6: Virtuelles Ballspiel zweier Prozesse (pingpong/pingpong.c) 1 #include < stdio.h> 2 #include < stdlib.h> 3 #include < unistd.h> 4 #include <sys/types. h> 5 #include < signal.h> 6 7 #define PINGPONGS static volatile sig_atomic_t sigcount = 0; void sighandler ( int sig ) { 12 ++sigcount ; 13 if ( signal ( sig, sighandler ) == SIG_ERR) _exit (1); 14 } static void playwith ( pid_t partner, int start ) { 17 int i ; 18 if ( signal (SIGUSR1, sighandler ) == SIG_ERR) { 19 perror (" signal SIGUSR1"); exit (1); 20 } 21 / give our partner some time for preparation / 22 if ( start ) sleep (1); 23 / start the ping pong game / 24 if ( start ) sigcount = 1; 25 for ( i = 0; i < PINGPONGS; ++i) { 26 if (! sigcount ) pause (); 27 printf ("[%d] send signal to %d\n", (int) getpid (), ( int ) partner ); 28 if ( kill ( partner, SIGUSR1) < 0) { 29 printf ( "[%d] %d is no longer alive\n", (int) getpid (), ( int ) partner ); 30 return ; 31 } 32 sigcount; 33 } 34 printf ( "[%d] finishes playing\n", ( int ) getpid ()); 35 } int main () { 38 pid_t parent = getpid (); 39 pid_t child = fork (); if ( child < 0) { 42 perror ("fork" ); exit (1); 43 } 44 if ( child == 0) { 45 playwith ( parent, 1); 46 } else { 47 playwith ( child, 0); 48 } 49 exit (0); 50 } 72 KAPITEL 3. SIGNALE Erläuterungen zu Programm 3.6 (S. 71): In Zeile 39 wird ein weiterer Prozess als Spielpartner erzeugt. Beide Prozesse rufen anschließend playwith() auf, um das Spiel durchzuführen. Der neu erzeugte Prozess hat zuerst den virtuellen Ball und darf mit dem Spiel beginnen. Der Besitzer des virtuellen Balles sendet in Zeile 28 den Ball mit Hilfe des Signals SIGUSR1 an den Partner und legt sich anschließend in Zeile 26 mittels pause() schlafen, um auf das Wiedereintreffen des Balls zu warten, das wiederum durch SIGUSR1 signalisiert wird. Die Variable sigcount in Zeile 9 repräsentiert die Zahl der Bälle, die sich im Augenblick im Besitz des Prozesses befinden. Diese Zahl wird von dem Signalbehandler sighandler() in Zeile 12 hochgezählt, wenn ein SIGUSR1-Signal eintrifft und in Zeile 32 wieder heruntergezählt, nachdem das SIGUSR1-Signal an den Partner verschickt wurde. Zu Beginn setzen beide Spieler in Zeile 18 sighandler() als Signalbehandler ein. Derjenige, der mit dem Spiel beginnt, wartet dann in Zeile 22 noch eine Sekunde, um zu vermeiden, dass ein Signal geschickt wird, bevor der Spielpartner die Gelegenheit hatte, seinen Signalbehandler aufzusetzen. Zu beachten ist dabei, dass der Signalbehandler in Zeile 13 sich selbst wieder einsetzt, da der POSIX-Standard nicht festlegt, ob diese Einstellung nach Eintreffen eines Signals erhalten bleibt. signal() gehört mit zu den vom POSIX-Standard genannten Funktionen, die auch innerhalb eines Signalbehandlers aufgerufen werden dürfen.

40 3.6. REAKTION AUF SIGNALE: SIGACTION() Reaktion auf Signale: sigaction() Für eine genauere Behandlung eintreffender Signal bietet POSIX (jedoch nicht ISO-C) den Systemaufruf sigaction() an. #include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); Datentyp Feldname Beschreibung void(*) (int) sa_handler Funktionszeiger (wie bisher) void(*) (int, siginfo_t*, void*) sa_sigaction alternativer Zeiger auf einen Signalbehandler, der mehr Informationen zum Signal erhält sigset_t sa_mask Menge von Signalen, die während der Signalbehandlung dieses Signals zu blockieren sind int sa_flags Menge von Boolean-wertigen Optionen Tabelle 3.1: Felder der struct sigaction Während bei signal() zur Spezifikation der Signalbehandlung nur ein Funktionszeiger genügte, kommen bei der struct sigaction, die sigaction() verwendet, die in Tabelle 3.1 genannten Felder zum Einsatz. Programm 3.8 zeigt ein einfaches Beispiel, ein Testprogramm ist 3.9. Zunächst wird für eine bestimmte Zeiteinheit das Signal, das durch ctrl-c erzeugt wird, abgefangen, danach wird der alte Signalbehandler (Termination) wieder eingesetzt. Übersetzung und Ausführung (auf SuSE Linux, gcc 3.3.3): spatz$ make gcc -std=c99 -Wall -g -D_POSIX_C_SOURCE=200112L -D EXTENSIONS =1 -D_Exit=_exit -I/usr/local/diet/include -c -o show.o show.c gcc -std=c99 -Wall -g -D_POSIX_C_SOURCE=200112L -D EXTENSIONS =1 -D_Exit=_exit -I/usr/local/diet/include -c -o sign.o sign.c gcc -L/usr/local/diet/lib show.o sign.o -lowfat -o show spatz$ show Break? go asleep for 5 sec ^C signal handler: SIGNAL = >2< ^C ^C ^\ Quit spatz$ Die (eingefügten) Zeichen ^C und ^\ sollen die erzeugten Signale wiedergeben: nach dem ersten ^C meldet sich der Signalbehandler mit signal handler: SIGNAL = >2<, die weiteren 74 KAPITEL 3. SIGNALE ^Cs werden die nächsten 10 Sekunden zurückgehalten, während das ^\ während der Abwicklung des Signalbehandlers (sleep(10) zugestellt wird (Funktion sigemptyset() in Zeile 18 des Programms 3.8 auf S. 74): durch das Leeren der Komponente newact.sa_mask wird erreicht, dass während der Ausführung der installierten Signalbehandlungsfunktion mit Ausnahme des im Argument sig angegebenen Signals keine weiteren Signale von der Zustellung durch den Systemkern zurückgehalten werden (deswegen kommt ja auch unser ^\ sofort durch!) 1 #include < signal.h> 2 3 #ifndef SIGN_H 4 #define SIGN_H 5 6 typedef void ( Sigfunc )( int ); 7 Sigfunc ignoresig ( int ); 8 Sigfunc entrysig ( int ); 9 #endif Programm 3.7: Header-File zu 3.8 (sigaction1/sign.h) Programm 3.8: Erstes Beispiel zu sigaction() (sigaction1/sign.c) 1 # include "sign.h" 2 # include < stdio.h> 3 # include < unistd.h> 4 5 void myignore( int sig ){ 6 printf ( " signal handler: SIGNAL = >%d<\n",sig); 7 sleep (10); 8 return ; 9 } struct sigaction newact, oldact ; Sigfunc ignoresig ( int sig ) { 14 static int first = 1; // static : bleibt erhalten! 15 newact. sa_handler = myignore; 16 if ( sigemptyset (&newact.sa_mask ) < 0) 17 return SIG_ERR; 18 / Durch diese Initialisierung der Komponente sa_mask mit 19 der leeren Menge wird bewirkt, dass waehrend der Aus 20 fuehrung der installierten Signalbehandlungsfunktion 21 mit Ausnahme des im Argument sig angegebenen Signals 22 keine weiteren Signale von der Zustellung durch den 23 Systemkern zurueckgehalten werden 24 / if ( first ) { 27 first = 0; 28 if ( sigaction ( sig, &newact, & oldact ) < 0) 29 return SIG_ERR; 30 else 31 return oldact. sa_handler ; 32 } else { 33 if ( sigaction ( sig, &newact, NULL) < 0) 34 return SIG_ERR; 35 else

41 3.6. REAKTION AUF SIGNALE: SIGACTION() return NULL; 37 } 38 } Sigfunc entrysig ( int sig ) { 41 if ( sigaction ( sig, & oldact, NULL) < 0 ) 42 return SIG_ERR; 43 else 44 return NULL; 45 } Programm 3.9: Testprogramm zu 3.8 (sigaction1/show.c) 1 # include < stdio.h> 2 # include < unistd.h> 3 # include < stdlib.h> 4 # include "sign.h" 5 6 int main () { 7 int sleep_time ; 8 / install reaction / 9 if ( ( ignoresig (SIGINT) == SIG_ERR) ) { 10 perror (" ignoresig " ); 11 exit (1); 12 } 13 printf ( "Break?\n"); 14 while (1) { / loop forever / 15 sleep_time = 5; 16 do { 17 printf ( "go asleep for %d sec\n", sleep_time ); 18 sleep_time = sleep ( sleep_time ); 19 } while( sleep_time!= 0); 20 ; 21 printf ("And now?\n"); 22 / restore reaction / 23 if ( ( entrysig (SIGINT) == SIG_ERR) 24 ( entrysig (SIGQUIT) == SIG_ERR) ) { 25 perror ( " entrysig " ); 26 exit (2); 27 } 28 } 29 exit (0); 30 } 76 KAPITEL 3. SIGNALE Mit der Funktion sigaddset() können zu der Menge newact.sa_mask gezielt Signale hinzugenommen werden. Dies zeigt Programm 3.10 auf S. 76, eine kleine Modifikation von Programm Programm 3.8 (S. 74). Programm 3.10: Modifikation von 3.8 (sigaction2/sign.c) 1 # include "sign.h" 2 # include < stdio.h> 3 # include < unistd.h> 4 5 void myignore( int sig ){ 6 printf ( " signal handler: SIGNAL = >%d<\n",sig); 7 sleep (10); 8 return ; 9 } struct sigaction newact, oldact ; Sigfunc ignoresig ( int sig ) { 14 static int first = 1; // static : bleibt erhalten! 15 newact. sa_handler = myignore; 16 if ( sigemptyset (&newact.sa_mask ) < 0) 17 return SIG_ERR; 18 if ( sigaddset (&newact.sa_mask, 3) < 0) 19 return SIG_ERR; 20 if ( first ) { 21 first = 0; if ( sigaction ( sig, &newact, & oldact ) < 0) 24 return SIG_ERR; 25 else 26 return oldact. sa_handler ; 27 } else { 28 if ( sigaction ( sig, &newact, NULL) < 0) 29 return SIG_ERR; 30 else 31 return NULL; 32 } 33 } Sigfunc entrysig ( int sig ) { 36 if ( sigaction ( sig, & oldact, NULL) < 0 ) 37 return SIG_ERR; 38 else 39 return NULL; 40 }

42 3.6. REAKTION AUF SIGNALE: SIGACTION() 77 Ausführung: 78 KAPITEL 3. SIGNALE 3.7 Die Zustellung von Signalen spatz$ show Break? go asleep for 5 sec ^Csignal handler: SIGNAL = >2< ^\ Quit spatz$ Die vorangegangenen Beispiele werfen die Frage auf, wie UNIX bei der Zustellung von Signalen vorgeht, wenn der Prozess zur Zeit nicht aktiv ist, gerade ein Systemaufruf für den Prozess abgearbeitet wird oder gerade ein Signalbehandler aktiv ist. Unmittelbar nach der Eingabe von ^C wurde ein ^\ gegeben es dauert allerdings einige Sekunden (Ablauf von sleep(10) im Signalbehandler) bis ^\ zugestellt wird und logischerweise zur Termination führt (Quit). Mehr zu sigaction() im nächsten Abschnitt! Vom ISO-Standard für C wird in dieser Beziehung nichts festgelegt. Der POSIX-Standard geht jedoch genauer darauf ein: 3 Wenn ein Prozess ein Signal erhält, wird dieses Signal zunächst in den zugehörigen Verwaltungsstrukturen des Betriebssystems vermerkt. Signale, die für einen Prozess vermerkt sind, jedoch noch nicht zugestellt worden sind, werden als anhängige Signale bezeichnet. Wenn mehrere Signale mit der gleichen Nummer anhängig sind, ist nicht festgelegt, ob eine Mehrfachzustellung erfolgt. Es können also Signale wegfallen. Nur aktiv laufende Prozesse können Signale empfangen. Prozesse werden normalerweise durch die Existenz eines anhängigen Signals aktiv aber dieses kann auch längere Zeit in Anspruch nehmen, wenn dem zwischenzeitlich mangelnde Ressourcen entgegenstehen. Für jeden Prozess gibt es eine Menge blockierter Signale, die im Augenblick nicht zugestellt werden sollen. Dies hat nichts mit dem Ignorieren von Signalen zu tun, da blockierte Signale anhängig bleiben, bis die Blockierung aufgehoben wird. Der POSIX-Standard legt nicht fest, was mit der Signalbehandlung geschieht, wenn ein Signalbehandler aufgerufen wird. Möglich ist das Zurückfallen auf SIG_DFL (Voreinstellung mit Prozessterminierung) oder die temporäre automatische Blockierung des Signals bis zur Beendigung des Signalbehandlers. Alle modernen UNIX-Systeme wählen die zweite Variante. Dies lässt sich aber gemäß dem POSIX-Standard auch erzwingen, indem die umfangreichere Schnittstelle sigaction() anstelle von signal() verwendet wird. Allerdings ist sigaction() nicht mehr Bestandteil des ISO-Standards für C (aber eben des POSIX- Standards!). UNIX unterscheidet zwischen unterbrechbaren und unterbrechungsfreien Systemaufrufen. Zur ersteren Kategorie gehören weitgehend alle Systemaufrufe, die zu einer längeren Blockierung eines Prozesses führen können. Ist ein nicht blockiertes Signal anhängig, kann ein unterbrechbarer Systemaufruf aufgrund des Signals mit einer Fehlerindikation beendet werden. errno wird dann auf EINTR gesetzt. Dabei ist zu beachten, dass der unterbrochene Systemaufruf nach Beendigung der Signalbehandlung nicht fortgesetzt wird, sondern manuell erneut gestartet werden muss. Dies kann leider zu unerwarteten Überraschungseffekten führen, weil insbesondere auch die stdio-bibliothek keinerlei Vorkehrungen trifft, Systemaufrufe automatisch erneut aufzusetzen, falls es zu einer Unterbrechung kam. Dies ist eine wesentliche Schwäche sowohl des POSIX-Standards als auch der stdio-bibliothek und ein Grund mehr dafür, auf die Verwendung der stdio in kritischen Anwendungen völlig zu verzichten. 3 Siehe Signal Concepts, im Web unter

43 3.7. DIE ZUSTELLUNG VON SIGNALEN 79 Für die genauere Regulierung der Signalbehandlung bietet POSIX (jedoch nicht ISO-C) wie bereits erwähnt den Systemaufruf sigaction() an. Ein wesentlicher Unterschied zwischen sigaction() und signal() besteht bereits darin, dass per Voreinstellung das Signal, das eine Signalbehandlung auslöst, während der Bearbeitung automatisch blockiert wird. Ferner findet (solange nichts gegenteiliges in den Optionen angegeben wurde) keine implizite Veränderung des Signalbehandlers auf SIG_DFL nach der Signalbehandlung statt. Bei signal() ist dies ebenfalls möglich, jedoch nicht garantiert. In neueren UNIX- Version (ab System V.3) wurde der Signalmechanismus erweitert: man kann nun in einem Programm mit sighold(sig) einen kritischen Abschnitt beginnen und mit sigrelse(sig) (sigrelease, s.u.) abschliessen tritt in diesem Abschnitt das Signal sig auf, so wird es bis zur Bendigung des Abschnitts zurückgehalten (siehe z.b. Programm 3.11: Verlust von Signalen (sigfire/sigfire.c) 1 #include < signal.h> 2 #include < stdio.h> 3 #include < stdlib.h> 4 #include < unistd.h> 5 6 static const int NOF_SIGNALS = 1000; 7 static volatile sig_atomic_t received_signals = 0; 8 static volatile sig_atomic_t terminated = 0; 9 10 static void count_signals ( int sig ) { received_signals ; 12 } void termination_handler ( int sig ) { 15 terminated = 1; 16 } int main () { 19 sighold (SIGUSR1); sighold (SIGTERM); pid_t child = fork (); 22 if ( child < 0) { 23 perror ("fork" ); exit (1); 24 } 25 if ( child == 0) { 26 struct sigaction action = {0}; 27 action. sa_handler = count_signals ; 28 if ( sigaction (SIGUSR1, &action, 0)!= 0) { 29 perror ( " sigaction " ); exit (1); 30 } 31 action. sa_handler = termination_handler ; 32 if ( sigaction (SIGTERM, &action, 0)!= 0) { 33 perror ( " sigaction " ); exit (1); 34 } 80 KAPITEL 3. SIGNALE 35 sigrelse (SIGUSR1); sigrelse (SIGTERM); 36 while (! terminated ) pause (); 37 printf ("[%d] received %d signals\n", 38 ( int ) getpid (), received_signals ); 39 exit (0); 40 } sigrelse (SIGUSR1); sigrelse (SIGTERM); 43 for ( int i = 0; i < NOF_SIGNALS; ++i) { 44 kill ( child, SIGUSR1); 45 } 46 printf ( "[%d] sent %d signals\n", 47 ( int ) getpid (), NOF_SIGNALS); 48 kill ( child, SIGTERM); wait(0); 49 } Programm 3.11 demonstriert den möglichen Verlust von Signalen trotz umfangreicher Vorsichtsmaßnahmen (hierbei werden spezielle ISO-C Funktionen wie verwendet. Das Experiment besteht hier im Versenden von 1000 SIGUSR1-Signalen, die beim Empfänger nachgezählt werden. In den Zeilen 26 bis 30 setzt der neu erzeugte Prozess die Funktion count_signals() als Signalbehandler für SIGUSR1 auf. Dabei wird hier sigaction() anstelle von signal() verwendet. Dies garantiert, dass während der Bearbeitung des Signals SIGUSR1 weitere eintreffende SIGUSR1-Signale aufgeschoben und nicht sofort in verschachtelter Form bearbeitet werden. Auf diese Weise ist die Atomizität des Hochzählens der Variable received_signals garantiert. Zu bedenken ist dabei, dass der Datentyp sig_atomic_t selbst nur die Atomizität einer einzelnen Lese- oder Schreiboperation gewährleistet. Bei einem Inkrement liegt jedoch eine Leseund eine Schreib-Operation vor. Zwischen diesen Operationen wäre eine Unterbrechung wegen einer Signalbehandlung denkbar. In diesem Beispiel versendet der übergeordnete Prozess 1000 Signale und der neu erzeugte Prozess zählt die eingetroffenen Signale. Wann darf der übergeordnete Prozess davon ausgehen, dass der neu erzeugte Prozess seinen Signalbehandler fertig aufgesetzt hat, so dass er mit dem Zählen beginnen kann? Wenn der übergeordnete Prozess zu früh Signale versendet, während noch die voreingestellte Reaktion für SIGUSR1 eingerichtet ist, würde dies nur zur vorzeitigen Terminierung des erzeugten Prozesses führen. Diese Problematik lässt sich vermeiden, indem die Signale, für die der erzeugte Prozess Behandler aufsetzt, vor der Prozesserzeugung blockiert werden. Das geht am einfachsten mit der Funktion sighold() (auf Zeile 19), die zum POSIX- Standard gehört. Zu jedem Prozess unterhält das Betriebssystem eine Menge blockierter Signale. Mit sighold() wird das angegebene Signal zu der Menge hinzugefügt. Entscheidend ist hier, dass die Menge der blockierten Signale an den neu erzeugten Prozess vererbt wird. So können nach dem fork() in aller Ruhe auf den Zeilen 26 bis 34 die Signalbehandler für SIGUSR1 und SIGTERM aufgesetzt werden, bevor auf Zeile 35 diese Signale wieder mit Hilfe von sigrelse() (eine unglückliche Abkürzung von signal release) wieder aus der Menge der blockierten Signale entfernt werden. Auch der übergeordnete Prozess nimmt die beiden Signale wieder heraus auf der Zeile 42. Wie wird das Experiment beendet? Da, wie das Experiment zeigen soll, Signale verloren gehen können, sollte der erzeugte Prozess nicht darauf warten, bis NOF_SIGNALS eingetroffen sind. Stattdessen wird SIGTERM vom übergeordneten Prozess an den erzeugten Prozess verwendet, um das Ende des Experiments zu signalisieren. Hier sind einige Läufe des Experiments, die demonstrieren, wie sehr die Werte voneinander abweichen können:

44 3.7. DIE ZUSTELLUNG VON SIGNALEN 81 doolin$ sigfire [22073] sent 1000 signals [22074] received 264 signals doolin$ sigfire [22075] sent 1000 signals [22076] received 227 signals doolin$ sigfire [22077] sent 1000 signals [22078] received 481 signals doolin$ sigfire [22079] sent 1000 signals [22080] received 136 signals doolin$ Wenn anstelle von nur SIGUSR1 zwei Signale SIGUSR1 und SIGUSR2 abwechselnd verwendet werden, können höhere Werte erzielt werden, wobei der Erfolg keinesfalls garantiert ist: doolin$ sigfire2 [22142] sent 1000 signals [22143] received 495 signals doolin$ sigfire2 [22144] sent 1000 signals [22145] received 462 signals doolin$ sigfire2 [22146] sent 1000 signals [22147] received 468 signals doolin$ sigfire2 [22151] sent 1000 signals [22152] received 688 signals doolin$ Dieses Experiment untermauert die Regel, dass einzelne Signale zuverlässig zugestellt werden, während es bei dem Mehrfachen Eintreffen des gleichen Signals zu Verlusten kommen kann. In der Praxis zeigen sich jedoch Signalverluste nur bei härteren Rahmenbedingungen, sei es durch ein explizites Dauerfeuer (wie in diesem Experiment) oder durch eine hohe Belastung der Maschine. 82 KAPITEL 3. SIGNALE 3.8 Signale als Indikatoren für terminierte Prozesse Wenn ein von einem Prozess P erzeugter Prozess K terminiert, so wird P das Signal SIGCHLD zugestellt. Die voreingestellte Reaktion (SIG_DFL) darauf ist Ignorieren (siehe bei Linux z.b. man -S7 signal). In Programm 3.12 (S. 82) werden nacheinander drei Prozesse erzeugt, die jeweils eine zufällig gewählte Zeit (zwischen 0 und 4 Sekunden) warten und dann mit einem zufälligen Exit-Status (zwischen 0 und 255) terminieren (diese Zufälligkeit ist zunächst ohne Bedeutung). Der Erzeuger legt sich 10 Sekunden schlafen und gibt danach seine und seiner Abkömmlinge Einträge in der Prozesstabelle aus: euklid$ show Create child processes: I m child 1 with PID = 4241! signal handler: SIGNAL = >17< I m child 2 with PID = 4242! signal handler: SIGNAL = >17< I m child 3 with PID = 4243! signal handler: SIGNAL = >17< PID TTY TIME CMD 4233 pts/3 00:00:00 bash 4240 pts/3 00:00:00 show 4241 pts/3 00:00:00 show <defunct> 4242 pts/3 00:00:00 show <defunct> 4243 pts/3 00:00:00 show <defunct> 4244 pts/3 00:00:00 ps signal handler: SIGNAL = >17< Parent: going to exit! euclid$ Auch hier ist wie oben bereits beschrieben zu beachten, dass einzelne Signale zuverlässig zugestellt werden, während es bei dem Mehrfachen Eintreffen des gleichen Signals zu Verlusten kommen kann. Programm 3.12: Prozesse, auf die der Erzeuger nicht wartet (sigchld/show.c) 1 # include < stdio.h> 2 # include < stdlib.h> 3 # include < unistd.h> 4 # include < signal.h> 5 6 typedef void ( Sigfunc )( int ); 7 8 void myignore( int sig ){ 9 printf ( " signal handler: SIGNAL = >%d<\n",sig); 10 return ; 11 } struct sigaction newact, oldact ; Sigfunc ignoresig ( int sig ) {

45 3.8. SIGNALE ALS INDIKATOREN FÜR TERMINIERTE PROZESSE newact. sa_handler = myignore; 17 if ( sigemptyset (&newact.sa_mask ) < 0) 18 return SIG_ERR; 19 newact. sa_flags = 0; 20 if ( sigaction ( sig, &newact, & oldact ) < 0) 21 return SIG_ERR; 22 else 23 return oldact. sa_handler ; 24 } int main () { 27 int pid [3], i = 3; 28 if ( ignoresig (SIGCHLD) == SIG_ERR) { 29 perror (" ignoresig " ); 30 exit (1); 31 } printf ( "Create child processes:\n" ); 34 while(i >0) { 35 switch(pid[3 i]= fork ()) { 36 case 1: perror ("fork" ); 37 exit (1); 38 case 0 : printf ("I m child %d with PID = %d!\n", 4 i, (int) getpid()); 39 srand ( getpid ()); sleep (rand () % 5); 40 exit (( char ) rand ()); 41 default : 42 sleep (1); 43 } 44 i ; 45 } 46 sleep (10); 47 switch( fork ()) { 48 case 1: perror ("fork" ); 49 exit (1); 50 case 0: 51 execlp ("ps", "ps", " l", NULL); 52 default : 53 sleep (1); 54 } 55 sleep (1); 56 printf ( "Parent: going to exit!\n" ); 57 exit (0); 58 } 84 KAPITEL 3. SIGNALE Das lässt sich leicht verändern. der Erzeuger bekommt schließlich Nachricht über die Termination eines erzeugten Prozesses. Wenn er auf das Signal einen Signalbehandler mit einem schlichten wait() einrichtet, wird der Eintrag des eben beendeten Kindprozesses dadurch abgeräumt (siehe Programm 3.13, S. 84): euclid$ show Create child processes: I m child 1 with PID = 4364! I m child 2 with PID = 4365! signal handler: SIGNAL = >17< Process 4365 terminated with Status 225 signal handler: SIGNAL = >17< Process 4364 terminated with Status 248 I m child 3 with PID = 4366! signal handler: SIGNAL = >17< Process 4366 terminated with Status 154 Process table: PID TTY TIME CMD 4356 pts/3 00:00:00 bash 4363 pts/3 00:00:00 show 4367 pts/3 00:00:00 ps signal handler: SIGNAL = >17< Process 4367 terminated with Status 0 Parent: going to exit! euclid$ Programm 3.13: Prozesse, auf die der Erzeuger ohne zu blockieren wartet (sigchld1/show.c) 1 # include < stdio.h> 2 # include < stdlib.h> 3 # include < unistd.h> 4 # include < signal.h> 5 # include <sys/wait. h> 6 7 typedef void ( Sigfunc )( int ); 8 9 void myignore( int sig ){ 10 int status, pid ; 11 printf ( " signal handler: SIGNAL = >%d<\n",sig); 12 pid = wait(& status ); 13 printf ( "Process %d terminated with Status %d\n", pid, (status» 8) & 0377); 14 return ; 15 } struct sigaction newact, oldact ; Sigfunc ignoresig ( int sig ) { 20 newact. sa_handler = myignore; 21 if ( sigemptyset (&newact.sa_mask ) < 0) 22 return SIG_ERR; 23 newact. sa_flags = 0; 24 if ( sigaction ( sig, &newact, & oldact ) < 0) 25 return SIG_ERR;

46 3.8. SIGNALE ALS INDIKATOREN FÜR TERMINIERTE PROZESSE else 27 return oldact. sa_handler ; 28 } int main () { 31 int pid [3], i = 3; 32 if ( ignoresig (SIGCHLD) == SIG_ERR) { 33 perror (" ignoresig " ); 34 exit (1); 35 } printf ( "Create child processes:\n" ); 38 while(i >0) { 39 switch(pid[3 i]= fork ()) { 40 case 1: perror ("fork" ); 41 exit (1); 42 case 0 : printf ("I m child %d with PID = %d!\n", 4 i, (int) getpid()); 43 srand ( getpid ()); sleep (rand () % 5); 44 exit (( char ) rand ()); 45 default : 46 sleep (1); 47 } 48 i ; 49 } 50 sleep (10); 51 switch( fork ()){ 52 case 1: perror ("fork" ); 53 exit (1); 54 case 0: 55 printf ("Process table: \n" ); 56 execlp ("ps", "ps",null); 57 default : 58 sleep (1); 59 } 60 printf ( "Parent: going to exit!\n" ); 61 exit (0); 62 } 86 KAPITEL 3. SIGNALE Diese Lösung ist einfach, aber nicht allzu sicher. Das Problem ist, dass bei einer zeitgleichen Terminierung mehrerer Prozesse es wiederum zu Verlusten des SIGCHLD-Signals kommen kann. Somit kann man sich nicht darauf verlassen, dass für jeden terminierten Prozess der Signalbehandler genau einmal aufgerufen wird. Deswegen empfiehlt es sich, den Status aller bereits terminierter Prozesse abzurufen. Dies wird mit waitpid() unter der Verwendung der Option WNOHANG erreicht. Diese verhindert ein Blockieren des Systemaufrufs waitpid() und somit kann waitpid() gefahrlos solange aufgerufen werden, bis waitpid() 0 oder einen negativen Wert zurückliefert. Programm 3.14: Prozesse, auf die der Erzeuger ohne zu blockieren wartet (sigchld2/show.c) 1 # include < stdio.h> 2 # include < stdlib.h> 3 # include < unistd.h> 4 # include < signal.h> 5 # include <sys/wait. h> 6 # include <sys/types.h> 7 8 typedef void ( Sigfunc )( int ); 9 10 void myignore( int sig ){ 11 int status, pid ; 12 printf ( " signal handler: SIGNAL = >%d<\n",sig); 13 while (( pid = waitpid (( pid_t) 1, & status, WNOHANG)) > 0) { 14 printf ("%d terminated with %d\n", pid, (status» 8) & 0377); 15 }; 16 return ; 17 } struct sigaction newact, oldact ; Sigfunc ignoresig ( int sig ) { 22 newact. sa_handler = myignore; 23 if ( sigemptyset (&newact.sa_mask ) < 0) 24 return SIG_ERR; 25 newact. sa_flags = 0; 26 if ( sigaction ( sig, &newact, & oldact ) < 0) 27 return SIG_ERR; 28 else 29 return oldact. sa_handler ; 30 } int main () { 33 int pid [3], i = 3; 34 if ( ignoresig (SIGCHLD) == SIG_ERR) { 35 perror (" ignoresig " ); 36 exit (1); 37 } printf ( "Create child processes:\n" ); 40 while(i >0) { 41 switch(pid[3 i]= fork ()) { 42 case 1: perror ("fork" ); 43 exit (1);

47 3.8. SIGNALE ALS INDIKATOREN FÜR TERMINIERTE PROZESSE case 0 : printf ("I m child %d with PID = %d!\n", 4 i, (int) getpid()); 45 srand ( getpid ()); sleep (rand () % 5); 46 exit (( char ) rand ()); 47 default : 48 sleep (1); 49 } 50 i ; 51 } 52 sleep (10); 53 switch( fork ()){ 54 case 1: perror ("fork" ); 55 exit (1); 56 case 0: 57 printf ("Process table: \n" ); 58 execlp ("ps", "ps",null); 59 default : 60 sleep (1); 61 } 62 printf ( "Parent: going to exit!\n" ); 63 exit (0); 64 } Um die Prozesse und deren Status zu erfassen, sind umfangreichere Datenstrukturen erforderlich. Wenn auf gemeinsame Datenstrukturen von mehreren Seiten in asynchroner Form zugegriffen werden kann, dann sind die zugreifenden Programmbereiche sogenannte kritische Regionen. Nur durch den gegenseitigen Ausschluss wird verhindert, dass die betroffene Datenstruktur durch einen ungünstigen Unterbrechungszeitpunkt inkonsistent wird. Da sigaction() anstelle von signal() verwendet wird, ist bereits sichergestellt, dass der Signalbehandler nicht mehrfach in verschachtelter Form aufgerufen wird. Somit müssen nur alle verbliebenen Programmbereiche, die auf die gleiche Datenstruktur zugreifen, in sighold() und sigrelse() geklammert werden. Dies soll aber hier nicht weiter behandelt werden! 88 KAPITEL 3. SIGNALE 3.9 Signalbehandlung in einer (Mini-)Shell Die mit dem Programmtext 2.29 vorgestellte einfache Shell kümmerte sich nicht um die Signalbehandlung. Dies kann zu überraschenden Effekten führen, wenn der Versuch unternommen wird, aufgerufene Prozesse beispielsweise mit SIGINT zu unterbrechen: euclid$ tinysh % cat >OUT some input... ^Ceuclid$ Hier wurde zunächst cat aufgerufen und nach der ersten eingegebenen Zeile CTRL-c eingegeben, welches bei den aktuellen Einstellungen zu einem SIGINT an alle Prozesse der aktuellen Sitzung führte. Zur aktuellen Sitzung gehört jedoch nicht nur das gerade laufende cat-kommando, sondern natürlich auch tinysh. Da tinysh keinerlei Vorkehrungen traf, wurde es genauso wie cat einfach terminiert, weil dies die voreingestellte Reaktion ist. Wäre tinysh die Login-Shell gewesen, wäre damit die gesamte Sitzung beendet. Hier in diesem Beispiel wurde SIGINT offensichtlich von der normalen Shell ignoriert. Signalbehandlung einer Shell: Wenn ein Kommando im Vordergrund läuft, muss die Shell die Signale SIGINT und SIGQUIT ignorieren (sie soll ja schließlich weiterlaufen). Wenn ein Kommando im Hintergrund läuft, müssen für diesen Prozess SIGINT und SIGQUIT ignoriert werden. Wenn die Shell ein Kommando einliest, sollten SIGINT und SIGQUIT die Neu-Eingabe des Kommandos ermöglichen. Bezüglich SIGHUP muss nichts unternommen werden. Die Worte auf der Kommandozeile werden jetzt differenzierter betrachtet. Zulässige Symbole (token) der Kommandozeilen-Sprache sind: > (T_GT) Ausgabeumlenkung - danach muss ein Dateiname kommen >> (T_GTGT) Ausgabeumlenkung zum Anfügen - danach muss ein Dateiname kommen < (T_LT) Eingabeumlenkung - danach muss ein Dateiname kommen 2> (T_TWO_GT) Umlenkung der Diagnose-Ausgabe - danach muss ein Dateiname kommen & (T_AMP) Ausführung des Kommandos im Hintergrund - die darauf folgenden Zeichen bis zum Zeilenende werden hier ignoriert Die Erkennung der verschiedenen Symbole (token) beschreibt der in Abb. 3.1 dargestellte Automat. Die Syntax-Analyse der Kommandozeile wird wie in Abb. 3.2 (S. 89) dargestellt durchgeführt. Anm.: In Abb. 3.2, S. 89 sind Fehlerzustände nicht dargestellt; von den vier Arten der I/O- Umlenkung >, >>, 2>, < ist jeweils maximal eine zulässig!

48 3.9. SIGNALBEHANDLUNG IN EINER (MINI-)SHELL KAPITEL 3. SIGNALE space \n > out(t_nl) & > out(t_amp) < > out(t_lt) <,>,&,\n,space > ungetc; out(t_word) else > word[i]=c; i++ Signal Handler einrichten LOOP FOREVER Prompt ausgeben > > > out(t_gtgt) else > ungetc; out(t_gt); NEUTRAL 2 TWO_GT else > word[0] = c; i++ > > out(t_two_gt) else > ungetc; word[0]= 2 ; WORD i++ Kommandozeile lesen > Token erkennen und Syntax prüfen Neuen Prozess erzeugen ggf. I/O Umlenkung vornehmen GTGT falls im Hintergrund: Signal Handler einrichten Abbildung 3.1: Token-Erkennung via exec Kommando starten ggf. Warten T_WORD Abbildung 3.3: Ablauf der MidiShell T_LT start Modularisierung: T_WORD T_WORD main.c: infile expected T_GT T_GTGT T_TWO_GT termination.h cmd.h sign.c T_NL T_AMP outfile expected termination.c exit-status ausgeben cmd.c Syntax-Analyse I/O-Umlenkung ggf. Signalhandler Kommando starten sign.h Signal-Handler command background command defs.h gettoken.h Abbildung 3.2: Syntax-Analyse der Kommandozeile grundlegende gettoken.c Vereinbarungen Token holen Abbildung 3.4: Struktur der MidiShell Ablauf-Schema:

49 3.9. SIGNALBEHANDLUNG IN EINER (MINI-)SHELL 91 Die Programme im Einzelnen: Programm 3.15: Midi-Shell: Grundlegende Vereinbarungen (midishell/defs.h) 1 / Grundlegende Vereinbarungen : / 2 3 typedef enum {FALSE, TRUE} BOOLEAN; 4 5 / Token Symbole: / 6 typedef enum {T_WORD, T_GT, T_GTGT, T_TWO_GT, T_AMP, 7 T_LT, T_NL, T_EOF, T_ERR} TOKEN; 8 9 #define BADFD 2 10 #define MAXARG #define MAXWORD #define MAXFNAME 256 Programm 3.16: Midi-Shell: Schnittstelle zur Tokenbestimmung (midishell/gettoken.h) 1 #ifdef GET_H 2 #include < stdio.h> 3 #include "defs.h" 4 #else 5 / lexikalische Analyse der Kommandozeile / 6 extern TOKEN gettoken(char word ); 7 extern void skip_line (); 8 #endif Programm 3.17: Midi-Shell: Schnittstelle zum Signalbehandler (midishell/sign.h) 1 #ifdef SIGN_H 2 #include < signal.h> 3 #include <sys/wait. h> 4 #include < unistd.h> 5 6 typedef void ( Sigfunc )( int ); 7 8 #else 9 10 #define SIGN_H 11 / avoid multiple includes / 12 #include < signal.h> typedef void ( Sigfunc )( int ); Sigfunc ignoresig ( int ); 17 / ignore interrupt and avoid zombies 18 just for midishell ( parent ) 19 / 20 Sigfunc ignoresig_bg ( int ); 21 / ignore interrupt 22 just for execution of background commands 23 / 24 Sigfunc entrysig ( int ); 25 / restore reaction on interrupt / 26 #endif 92 KAPITEL 3. SIGNALE Programm 3.18: Midi-Shell: Schnittstelle zur Kommandoausführung (midishell/cmd.h) 1 #ifdef CMD_H 2 #include < stdio.h> 3 #include < strings.h> 4 #include < fcntl.h> 5 #include <errno.h> 6 #include < unistd.h> 7 #include < stdlib.h> 8 #include "defs.h" 9 #include "gettoken.h" 10 #include "sign.h" 11 #else extern void redirect ( int srcfd, char srcfile, 14 int dstfd, char dstfile, 15 int errfd, char errfile, 16 BOOLEAN append, BOOLEAN bckgrnd); 17 / I /O redirection / extern int invoke ( int argc, char argv [], 20 int srcfd, char srcfile, 21 int dstfd, char dstfile, 22 int errfd, char errfile, 23 BOOLEAN append, BOOLEAN bckgrnd); 24 / invoke () execute simple command / extern TOKEN command(int waitpid); 27 / collect a simple command from stdin 28 by calling gettoken () do redirection 29 if necessary by redirect () and execute 30 command by invoke () 31 / #endif Programm 3.19: Midi-Shell: Schnittstelle zur Ausgabe des Exitstatus (midishell/termination.h) 1 #ifdef TERM_H 2 3 #include < stdio.h> 4 #define lowbyte (w ) (( w) & 0377) 5 #define highbyte (w) lowbyte (( w )» 8) 6 #define MAXSIG #else 9 void statusprt ( int status ); 10 #endif

50 3.9. SIGNALBEHANDLUNG IN EINER (MINI-)SHELL 93 Programm 3.20: Midi-Shell: main-funktion Start (midishell/main.c) 1 #include < stdio.h> 2 #include < stdlib.h> 3 #include < string.h> 4 #include < unistd.h> 5 #include <sys/wait. h> 6 7 #include "defs.h" 8 #include "sign.h" 9 #include "cmd.h" 10 #include "termination.h" int main () { 13 char prompt; 14 int pid, status ; 15 TOKEN term; if ( ( ignoresig (SIGINT) == SIG_ERR) 18 ( ignoresig (SIGCHLD) == SIG_ERR)) { 19 perror (" ignoresig " ); 20 exit (1); 21 } prompt = "midish> "; while (1) { 26 printf ("%s", prompt ); 27 term = command(&pid); 28 if ( term == T_ERR) { 29 continue; 30 } 31 if ( ( term!= T_AMP) && (pid!= 0) ) { 32 / wait for foreground process 33 if fg process terminates 34 the signal handler will handle the exit status 35 ( will do his wait!) and 36 the following waitpid will return with 1! 37 / 38 waitpid ( pid,& status,0); 39 } 40 } 41 printf ( "\n\n"); 42 exit (0); 43 } 94 KAPITEL 3. SIGNALE Programm 3.21: Midi-Shell: Tokenbestimmung (midishell/gettoken.c) 1 #define GET_H 2 #include "gettoken.h" 3 4 void skip_line () { 5 int c ; 6 while ( ( c = getchar ())!= \n ); 7 } 8 9 / lexikalische Analyse der Kommandozeile / TOKEN gettoken(char word ) { 12 int c ; 13 char w; 14 enum {NEUTRAL, TWO_GT, GTGT, INWORD } state = NEUTRAL; w = word; 17 while ( ( c= getchar () )!= EOF ) { 18 switch ( state ) { case NEUTRAL: 21 switch ( c ) { 22 case & : 23 / read rest from line : / 24 skip_line (); 25 return ( T_AMP); 26 case < : 27 return ( T_LT); 28 case \n : 29 return ( T_NL); 30 case : 31 case \t : 32 continue; 33 case > : 34 state = GTGT; 35 continue; 36 case 2 : 37 state = TWO_GT; 38 continue; 39 default : 40 state = INWORD; 41 w++ = c; 42 continue; 43 } 44 case GTGT: 45 if ( c == > ) 46 return ( T_GTGT); 47 ungetc (c, stdin ); 48 return ( T_GT); 49 case TWO_GT: 50 if ( c == > ) 51 return ( T_TWO_GT); 52 w++ = 2 ;

51 3.9. SIGNALBEHANDLUNG IN EINER (MINI-)SHELL ungetc (c, stdin ); 54 state = INWORD; 55 continue; 56 case INWORD: 57 switch ( c ) { 58 case & : 59 case < : 60 case > : 61 case \n : 62 case : 63 case \t : 64 ungetc (c, stdin ); 65 w = \0 ; 66 return ( T_WORD); 67 default : 68 w++ =c; 69 continue; 70 } 71 } 72 } 73 return ( T_EOF); 74 } 96 KAPITEL 3. SIGNALE Programm 3.22: Midi-Shell: Kommandoausführung (midishell/cmd.c) 1 #include < string.h> 2 #define CMD_H 3 #include "cmd.h" 4 5 / Redirection of I /O: 6 after redirect the caller has file descriptor 0 7 for input, descriptor 1 for output, and 2 for stderr! 8 / 9 static void redirect ( int srcfd, char srcfile, 10 int dstfd, char dstfile, 11 int errfd, char errfile, 12 BOOLEAN append, BOOLEAN bckgrnd) { 13 int flags, fd ; 14 / we expect for srcfd : 15 0: nothing to do, 2: redirect 0 ( stdin ) to file 16 ( 1 indicates error ) we expect for dstfd : 19 1: nothing to do, 2: redirect 1 ( stdout ) to file 20 (with respect to parameter append we expect for errfd : 23 2: nothing to do 24 2: redirect 2 ( stderr ) to file 25 / if ( ( srcfd == 0) && bckgrnd ) { 28 strcpy ( srcfile, "/dev/null"); 29 / / dev / null > 30 there is nothing to read, only EOF 31 a background command couldn t get any 32 input from stdin ; 33 / 34 srcfd = BADFD; 35 / so redirect 0 to srcfile 36 set to / dev/ null above 37 / 38 } if ( srcfd!= 0) { 41 / 0 should point to file for input 42 / 43 if ( close (0) == 1) 44 perror (" close " ); 45 else if ( open( srcfile, O_RDONLY, 0) == 1) { 46 fprintf ( stderr, "can t open %s\n", srcfile ); 47 exit (1); 48 } 49 } / now file is referenced by file descriptor 0 52 /

52 3.9. SIGNALBEHANDLUNG IN EINER (MINI-)SHELL / now the same for std_output / 55 if ( dstfd!= 1) { 56 / output to file (>,» ( dstfd == 2) 57 / 58 if ( close (1) == 1) 59 perror (" close " ); 60 else { 61 flags = O_WRONLY O_CREAT; 62 if (! append ) / > file / 63 flags = O_TRUNC; 64 else 65 flags = O_APPEND; 66 if ( open( dstfile, flags, 0666) == 1) { 67 / open returns the smallest 68 free file descriptor 69 / 70 fprintf ( stderr, "can t create %s\n", dstfile ); 71 exit (1); 72 } 73 } 74 } 75 / now the same for std_error / 76 if ( errfd!= 2) { 77 / output to file 78 / 79 if ( close (2) == 1) 80 perror (" close " ); 81 else { 82 flags = O_WRONLY O_CREAT O_TRUNC; if ( open( errfile, flags, 0664) == 1) { 85 / open returns the smallest 86 free file descriptor 87 / 88 fprintf ( stderr, "can t create %s\n", errfile ); 89 exit (1); 90 } 91 } 92 } for ( fd =3; fd < 20; fd ++) ( void) close ( fd ); 95 / the caller now only needs 0,1,2!!! / 96 } / invoke () execute simple command 99 in a new process 100 / 101 static int invoke ( int argc, char argv [], 102 int srcfd, char srcfile, 103 int dstfd, char dstfile, 104 int errfd, char errfile, 105 BOOLEAN append, BOOLEAN bckgrnd) { 106 / uses redirect () / 98 KAPITEL 3. SIGNALE 107 int pid ; / empty commandline??? / 110 if ( argc == 0 ) 111 return (0); 112 switch ( pid = fork () ) { 113 case 1: 114 fprintf ( stderr, "Can t create new process\n"); 115 return (0); 116 case 0: 117 / CHILD / redirect ( srcfd, srcfile, dstfd, dstfile, errfd, errfile, 120 append, bckgrnd ); / restore reaction on interrupt and quit??? 123 not necessary, but could be as follows : 124 if (! bckgrnd ) 125 entrysig (SIGINT); 126 / / install signal handler for background : / 129 if ( bckgrnd ) { 130 if ( ignoresig_bg (SIGINT) == SIG_ERR) { 131 perror ("ignorsig_bg SIGINT"); 132 exit (1); 133 } 134 } execvp ( argv [0], argv ); 138 / this shouldn t be reached / 139 fprintf ( stderr, "can t execute %s\n", argv [0]); 140 exit (1); 141 default : 142 / PARENT / 143 if ( srcfd > 0 && close ( srcfd ) == 1) 144 perror (" close src" ); 145 if ( dstfd > 1 && close ( dstfd ) == 1) 146 perror (" close dst" ); 147 if ( errfd > 2 && close ( errfd ) == 1) 148 perror (" close error" ); if ( bckgrnd ) 151 printf ("%d\n", pid ); 152 return ( pid ); 153 } 154 } TOKEN command(int waitpid) { 157 / int waitpid : perhaps we have to wait 158 for the command return value : T_NL or T_AMP on success, T_ERR on error

53 3.9. SIGNALBEHANDLUNG IN EINER (MINI-)SHELL uses : gettoken (), invoke () 163 / TOKEN token, term; 166 int argc, srcfd, dstfd, errfd, pid ; 167 char argv[maxarg+1]; 168 char srcfile [MAXFNAME+1]; 169 char dstfile [MAXFNAME+1]; 170 char errfile [MAXFNAME+1]; 171 char word[maxword], malloc(); 172 BOOLEAN append; argc = 0; srcfd = 0; dstfd = 1; errfd = 2; 175 / defaults / while (1) { 178 switch ( token = gettoken (word )) { 179 case T_WORD: 180 if ( argc == MAXARG) { 181 fprintf ( stderr, "Too many args\n"); 182 break; 183 } 184 if (( argv[ argc]= malloc ( strlen (word)+1))==null) { 185 fprintf ( stderr, "Out of arg memory\n"); 186 break; 187 } 188 strcpy ( argv[ argc ], word ); 189 argc++; 190 continue; 191 case T_LT: 192 if ( srcfd!= 0) { 193 fprintf ( stderr, "syntax error: EXTRA <\n"); 194 skip_line (); 195 return T_ERR; 196 } 197 if ( gettoken ( srcfile )!= T_WORD) { 198 fprintf ( stderr, "syntax error: Illegal <\n"); 199 skip_line (); 200 return T_ERR; 201 } 202 / we have to redirect 0 to a file / 203 srcfd = BADFD; 204 continue; 205 case T_GT: 206 case T_GTGT: 207 if ( dstfd!= 1) { 208 fprintf ( stderr, "syntax error: EXTRA > or»\n"); 209 skip_line (); 210 return T_ERR; 211 } 212 if ( gettoken ( dstfile )!= T_WORD) { 213 fprintf ( stderr, "syntax error: Illegal > or»\n"); 214 skip_line (); 100 KAPITEL 3. SIGNALE 215 return T_ERR; 216 } 217 dstfd = BADFD; 218 append = ( token == T_GTGT); 219 continue; 220 case T_TWO_GT: 221 if ( errfd!= 2) { 222 fprintf ( stderr, "syntax error: EXTRA 2>\n"); 223 skip_line (); 224 return T_ERR; 225 } 226 if ( gettoken ( errfile )!= T_WORD) { 227 fprintf ( stderr, "syntax error: Illegal 2>\n"); 228 skip_line (); 229 return T_ERR; 230 } 231 errfd = BADFD; 232 continue; case T_AMP: 235 case T_NL: 236 term = token ; / one simple command is read / 239 argv[ argc ] = NULL; / Eingabe von > file allein : loeschen / anlegen 242 einer Datei : 243 / 244 if ( ( argc == 0 ) && ( dstfd == BADFD) && (!append)) { 245 dstfd = open( dstfile, O_WRONLY O_CREAT O_TRUNC, 0664); 246 close ( dstfd ); 247 return T_NL; 248 } pid = invoke ( argc, argv, srcfd, srcfile, dstfd, 251 dstfile, errfd, errfile, 252 append, term == T_AMP); 253 waitpid = pid ; while ( argc >= 0) 256 free (argv[ argc ]); 257 return ( term ); 258 case T_EOF: 259 printf ("\n\n"); 260 exit (0); 261 default : exit (1); / not reached! / 262 } 263 }; / end of while (1) / 264 / if reached, then error : / 265 return T_ERR; 266 }

54 3.9. SIGNALBEHANDLUNG IN EINER (MINI-)SHELL 101 Programm 3.23: Midi-Shell: Signalbehandler (midishell/sign.c) 1 #define SIGN_H 2 3 # include < stdio.h> 4 # include "sign.h" 5 6 void shell_handler ( int sig ){ 7 if ( ( sig == SIGCHLD) (sig == SIGCLD)) { 8 int status ; long gone; 9 gone = waitpid (0, & status, WNOHANG); 10 if ( gone <= 0 ) return ; 11 printf ("Terminated: %ld with ", gone); 12 if ( status & 0177) 13 printf (" Signal %d\n", status & 0177); 14 else 15 printf (" exit status: %d\n", ( status» 8)&0 xff ); 16 } 17 return ; 18 } struct sigaction newact, oldact ; Sigfunc ignoresig ( int sig ) { 23 static int first = 1; 24 newact. sa_handler = shell_handler ; 25 if ( first ) { 26 first = 0; 27 if ( sigemptyset (&newact.sa_mask ) < 0) 28 return SIG_ERR; 29 newact. sa_flags = 0; 30 newact. sa_flags = SA_RESTART; 31 if ( sigaction ( sig, &newact, & oldact ) < 0) 32 return SIG_ERR; 33 else 34 return oldact. sa_handler ; 35 } else { 36 if ( sigaction ( sig, &newact, NULL) < 0) 37 return SIG_ERR; 38 else 39 return NULL; 40 } 41 } struct sigaction newact_bg, oldact_bg ; Sigfunc ignoresig_bg ( int sig ) { 46 newact_bg. sa_handler = SIG_IGN; 47 if ( sigemptyset (&newact_bg.sa_mask ) < 0) 48 return SIG_ERR; 49 newact_bg. sa_flags = 0; 50 newact_bg. sa_flags = SA_RESTART; 51 if ( sigaction ( sig, &newact_bg, & oldact_bg ) < 0) 52 return SIG_ERR; 102 KAPITEL 3. SIGNALE 53 else return oldact_bg. sa_handler ; 54 } Sigfunc entrysig ( int sig ) { 57 if ( sigaction ( sig, & oldact, NULL) < 0 ) 58 return SIG_ERR; 59 else return NULL; 60 } Programm 3.24: Midi-Shell: Termination (midishell/termination.c) 1 #define TERM_H 2 #include "termination.h" 3 4 static char sigmsg [] = { 5 "", 6 "Hangup", "Interrupt ", "Quit", 7 " Illegal instruction ", "Trace trap", 8 "IOT instruction", "EMT instruction", 9 " Floating point exception", 10 " Kill ", "Bus error", 11 "Segmentation violation", 12 "Bad arg to system call", 13 "Write on pipe", "Alarm clock", 14 "Terminate signal", 15 "User signal 1", "User signal 2", 16 "Death of child", "Power fail" 17 }; void statusprt ( int status ) { 20 int code ; if ( lowbyte ( status ) == 0) { 23 // normal termination 24 code = highbyte ( status ); 25 printf (" Exit code %d\n", code); 26 } else { 27 if (( code = ( status & 0177)) <= MAXSIG) 28 printf ( "%s", sigmsg[ code ]); 29 else 30 printf ( " Signal# %d", code ); 31 if (( status & 0200) == 0200) 32 printf ( " core dumped"); 33 printf ("\n" ); 34 } 35 }

55 3.9. SIGNALBEHANDLUNG IN EINER (MINI-)SHELL #include < unistd.h> 2 #include < stdlib.h> 3 4 int main () { 5 sleep (20); 6 exit (0); 7 } Programm 3.25: Midi-Shell: Testprogramm 1 (midishell/sleepwell.c) Programm 3.26: Midi-Shell: Testprogramm 2 (midishell/read-something.c) 1 #include < stdio.h> 2 #include < stdlib.h> 3 4 int main () { 5 int n; 6 printf ("Give number:"); 7 if ( scanf ("%d", &n ) == 1) { 8 printf ("got: %d\n", n ); 9 exit (0); 10 } else { 11 exit (1); 12 } 13 } Das Makefile: # eine kleine Shell - die midishell midish: main.o cmd.o sign.o termination.o gettoken.o gcc -Wall -o midish main.o cmd.o sign.o termination.o gettoken.o # ausfuehrbares Programm: midish gcc -Wall -o sleepwell sleepwell.c # sleepwell: ein Dauerlaeufer gcc -Wall -o read-something read-something.c # read-something: lies von stdin main.o: main.c defs.h sign.h cmd.h termination.h gcc -Wall -c main.c cmd.o: cmd.c cmd.h gettoken.h defs.h sign.h gcc -Wall -c cmd.c gettoken.o: gettoken.c gettoken.h defs.h gcc -Wall -c gettoken.c sign.o: sign.c sign.h gcc -Wall -c sign.c termination.o: termination.c termination.h gcc -Wall -c termination.c 104 KAPITEL 3. SIGNALE Ausführung: hypatia$ make gcc -Wall -c main.c gcc -Wall -c cmd.c gcc -Wall -c sign.c gcc -Wall -c termination.c gcc -Wall -c gettoken.c gcc -Wall -o midish main.o cmd.o sign.o termination.o gettoken.o # ausfuehrbares Programm: midish gcc -Wall -o sleepwell sleepwell.c # sleepwell: ein Dauerlaeufer gcc -Wall -o read-something read-something.c # read-something: lies von stdin hypatia$ midish midish>ps PID TTY TIME CMD 212 tty1 00:00:00 bash 846 pts/0 00:00:00 bash 895 pts/0 00:00:00 midish 896 pts/0 00:00:00 ps midish>sleepwell # Eingabe von ctrl-c Interrupt midish>sleepwell & 898 midish>ps PID TTY TIME CMD 212 tty1 00:00:00 bash 846 pts/0 00:00:00 bash 895 pts/0 00:00:00 midish 898 pts/0 00:00:00 sleepwell 899 pts/0 00:00:00 ps midish> # ctrl-c midish>ps PID TTY TIME CMD 212 tty1 00:00:00 bash 846 pts/0 00:00:00 bash 895 pts/0 00:00:00 midish 898 pts/0 00:00:00 sleepwell 900 pts/0 00:00:00 ps midish>read-something Give number:5 got: 5.PHONY: clean realclean clean: rm -f *.o core realclean: rm -f *.o core midish sleepwell read-something

56 3.9. SIGNALBEHANDLUNG IN EINER (MINI-)SHELL 105 midish>read-something & 902 Give number:midish>5 can t execute 5 Exit code 1 midish>cat < text dies ist ein Text midish>cat < text > new > very_new syntax error: EXTRA > or >> midish>cat -? cat: invalid option --? Try cat --help for more information. Exit code 1 midish>cat -? 2> err Exit code 1 midish>cat err cat: invalid option --? Try cat --help for more information. midish> # ctrl-d hypatia$ 106 KAPITEL 3. SIGNALE 3.10 Überblick der Signale aus dem POSIX-Standard Signal Voreinstellung Beschreibung SIGABRT A Process abort signal. SIGALRM T Alarm clock. SIGBUS A Access to an undefined portion of a memory object. SIGCHLD I Child process terminated, stopped, or continued. SIGCONT C Continue executing, if stopped. SIGFPE A Erroneous arithmetic operation. SIGHUP T Hangup. SIGILL A Illegal instruction. SIGINT T Terminal interrupt signal. SIGKILL T Kill (cannot be caught or ignored). SIGPIPE T Write on a pipe with no one to read it. SIGQUIT A Terminal quit signal. SIGSEGV A Invalid memory reference. SIGSTOP S Stop executing (cannot be caught or ignored). SIGTERM T Termination signal. SIGTSTP S Terminal stop signal. SIGTTIN S Background process attempting read. SIGTTOU S Background process attempting write. SIGUSR1 T User-defined signal 1. SIGUSR2 T User-defined signal 2. SIGPOLL T Pollable event. SIGPROF T Profiling timer expired. SIGSYS A Bad system call. SIGTRAP A Trace/breakpoint trap. SIGURG I High bandwidth data is available at a socket. SIGVTALRM T Virtual timer expired. SIGXCPU A CPU time limit exceeded. SIGXFSZ A File size limit exceeded. Voreinstellung T A I S C Beschreibung Abbruch des Prozesses. Bei dem bei wait() zurückgelieferten Status ist WIFSIGNALED wahr und über WTERMSIG lässt sich das Signal ermitteln. Analog zu T. Hinzu kommt möglicherweise noch die Erzeugung eines Speicherauszugs (in der Datei core). Letzteres lässt sich mit WCOREDUMP untersuchen. Das Signal wird ignoriert. Der Prozess wird gestoppt. Der Prozess wird fortgesetzt. Tabelle 3.2: Im POSIX-Standard genannte Signale (Quelle: Die Tabelle 3.2 liefert einen Überblick aller vom POSIX-Standard genannten Signale. Einzelne Implementierungen können noch weitere Signale unterstützen. Die Signale lassen sich dabei in mehrere Gruppen aufteilen: Programmierfehler: SIGBUS, SIGFPE, SIGILL, SIGSEGV und SIGSYS. Ressourcenverbrauch: SIGVTALRM, SIGXCPU und SIGXFSZ.

57 3.10. ÜBERBLICK DER SIGNALE AUS DEM POSIX-STANDARD KAPITEL 3. SIGNALE Prozeßkontrolle: SIGCONT, SIGKILL, SIGSTOP, SIGTERM und SIGTRAP. Sitzungskontrolle: SIGHUP, SIGINT, SIGQUIT und SIGTSTP. Ereignis-Indikatoren: SIGALRM, SIGCHLD, SIGPIPE, SIGPOLL, SIGPROF, SIGTTIN, SIGTTOU, SIGURG, SIGUSR1, SIGUSR2 und SIGVTALRM.

58 110 KAPITEL 4. INTER-PROZESS-KOMMUNIKATION (IPC) Netzwerk-Kommunikation Kapitel 4 Das UNIX-IPC-Konzept lässt sich orthogonal erweitern von der lokalen Kommunikation zwischen Prozessen innerhalb eines Systems auf Netzwerk-Kommunikation zwischen Prozessen, die auf verschiedenen System laufen. Vor allem die Entwicklungsarbeiten der University of California at Berkeley brachte hier einige neue Ansätze zur Interprocess Communication in UNIX ein. Inter-Prozess-Kommunikation (IPC) User Process UserProcess 4.1 Einführung KERNEL KERNEL Jeder UNIX-Prozess besitzt seinen eigenen Context. Innerhalb eines Prozesses können die verschiedenen Moduln über Parameter und Rückgabewerte bei Funktionsaufrufen oder über globale Variablen Daten austauschen. Wollen jedoch zwei eigenständige Prozesse Daten miteinander austauschen, so kann dies nur über den Kernel via System Calls erfolgen. Denn der Kernel verhindert unkontrollierte Übergriffe eines Prozesses in den Adressraum eines anderen Prozesses. Abbildung 4.2: IPC auch über Rechnergrenzen Für die Prozesse kann es völlig transparent sein, wo der jeweilige Partnerprozess abläuft. user process user process 4.2 IPC - Client-Server Beispiel Der Client liest einen Dateinamen von stdin ein und schreibt ihn in den IPC-Kanal. Anschliessend wartet er auf die Reaktion des Servers. KERNEL Abbildung 4.1: IPC nur über den Kernel Der Server liest einen Dateinamen von dem IPC-Kanal und versucht die Datei zu öffnen. Gelingt es dem Server, die Datei zu öffnen, kopiert er ihren Inhalt in den IPC-Kanal. Lässt sich die Datei nicht öffnen, schickt der Server eine Fehlermeldung über den IPC-Kanal. Der Client wartet auf Daten am IPC-Kanal, er liest sie von dort und schreibt sie nach stdout. Konnte der Server die Datei öffnen, zeigt der Client so den Dateiinhalt an, sonst kopiert der Client die Fehlermeldung durch. filename stdin filename Bei diesem Konzept müssen beide Prozesse explizit der Kommunikation zustimmen. Der UNIX- Kernel bietet mit seinen Interprocess Communication Facilities nur die Möglichkeit zur Kommunikation an. file contenst or error message stdout client server file file contents or error message Abbildung 4.3: Client-Server-Beispiel Die beiden gestrichelten Pfeile zwischen dem Server und dem Client entsprechen dem jeweiligen Interprocess Communication Kanal. 109

59 4.3. SYSTEM CALLS DUP(), DUP2() KAPITEL 4. INTER-PROZESS-KOMMUNIKATION (IPC) 4.3 System Calls dup(), dup2() Im Zusammenhang mit unnamed pipes ist der Systemaufruf dup nützlich: #include <unistd.h> int dup(int oldfd) /*duplicate file descriptor*/ int dup2(int oldfd, int newfd) /*newfd is closed before (if open)*/ /* returns new file descriptor or -1 on error */ write fd user process read fd dup verdoppelt einen bestehenden Filedeskriptor und liefert als Resultat einen neuen Filedeskriptor (mit der kleinsten verfügbaren Nummer), der mit der gleichen Datei oder der gleichen Pipe verbunden ist. Beide File Deskriptoren haben denselben Positionszeiger. Damit kann z.b. ein Filedeskriptor mit der Nummer 0 erhalten werden, falls dieser vorher geschlossen wurde. Falls so mit exec ein Programm ausgeführt wird, das von Filedeskriptor 0 liest, kann es so dazu gebracht werden, aus einer Pipe zu lesen. Ähnlich kann so der Filedeskriptor 1 manipuliert werden. Kernel pipe flow of data 4.4 Unnamed Pipes Abbildung 4.4: Unnamed Pipe - erster Schritt System Call pipe() #include <unistd.h> int pipe( int pipefd[2] ) /* create a pipe */ /* pipefd[2]: file descriptors */ /* returns 0 on success or -1 on error */ VORSICHT! Der Kernel synchronisiert Prozesse, die in Pipes schreiben oder aus Pipes lesen Producer / Consumer Modell. Sollte hier in dem Ein-Prozess-Beispiel ein read() oder write System Call blockieren, entsteht ein Deadlock, denn der blockierte Prozess kann den befreienden, komplementären write() oder read() System Call nicht absetzen. Kommunikation zwischen verwandten Prozessen Durch einen fork() System Call entstehen zwei eigenständige, aber verwandte Prozesse, die insbesondere die gleichen I/O-Verbindungen besitzen (siehe Abb. 4.5, S. 112). Pipes sind der älteste IPC-Mechanismus. Seit Mitte der 70er Jahre existieren sie auf allen Versionen und Arten von UNIX. Eine Pipe besteht aus einem unidirektionalen Datenkanal. Zwei File Deskriptoren repräsentieren die Pipe im User Prozess. Der System Call pipe() kreiert die Pipe, er liefert die beiden Enden als File Deskriptoren über sein Vektorargument an den Prozess. Dabei ist pipefd[1] das Ende zum Schreiben, pipefd[0] das Ende zum Lesen (siehe Abb. 4.4, S. 112) write fd parent process read fd fork Kernel write fd child process read fd In dieser Konstellation lässt sich die Pipe nur als Zwischenspeicher für Daten außerhalb des User Adressraums benutzen. pipe flow of data Abbildung 4.5: Unnamed Pipe zweiter Schritt Schließt nun ein Prozess sein Lese-Ende und der andere Prozess sein Schreib-Ende, so entsteht ein unidirektionaler Kommunikationspfad zwischen den beiden Prozessen (Abb. 4.6, S. 113).

60 4.4. UNNAMED PIPES KAPITEL 4. INTER-PROZESS-KOMMUNIKATION (IPC) parent process child process Realisierung: write fd Kernel pipe flow of data Abbildung 4.6: Unnamed Pipe dritter Schritt Wiederholen die Prozesse die fork, pipe() und close System Calls, entstehen längere Pipelines. Dieses Datenverarbeitungs-Prinzip ist untrennbar mit UNIX verbunden. Beispiel: Pipe zwischen zwei Prozessen Das Programm kreiert eine pipe und einen zweiten Prozess, dem diese beiden pipe-deskriptoren verebt werden. Der Erzeugerprozess schreibt Text in die Pipe und wartet auf das Ableben des Kindprozesses. Der Kindprozess liest Text aus der Pipe, schreibt ihn nach stdout und beendet seine Ausführung. Folgende Schritte sind der Reihe nach auszuführen: 1. Parent führt pipe() aus 2. Parent führt fork() aus 3. Child schließt sein Schreib-Ende der Pipe und wartet an seinem Lese-Ende der Pipe. 4. Parent schließt sein Lese-Ende der Pipe, schreibt Text in sein Schreib-Ende der Pipe, und führt wait() für sein Kind aus. 5. Child liest von seinem Lese-Ende, gibt gelesenen Text aus und terminiert. 6. Parent hat auf Child gewartet und kann jetzt auch terminieren. read fd Programm 4.1: Einfache Kommunikation via Pipe (pipe.c) 1 # include < stdio.h> 2 # include < unistd.h> 3 # include < stdlib.h> 4 # include <sys/wait. h> 5 6 # define RD_FD 0 7 # define WR_FD int main () { 10 int childpid, gone, pipefd [2]; if ( pipe ( pipefd ) < 0 ) { 13 perror ("pipe" ); exit (1); 14 } switch( childpid = fork () ){ 17 case 1: 18 perror ("fork" ); exit (1); 19 case 0: / child : / { 20 char buf [ 128 ]; int nread ; close ( pipefd [ WR_FD ] ); / read from pipe and copy to stdout / 25 nread = read ( pipefd [ RD_FD ], buf, sizeof ( buf )); 26 write (1, buf, nread ); 27 break; 28 } default : / parent : / close ( pipefd [ RD_FD ] ); 33 write ( pipefd [ WR_FD ], "hello world\n", 12 ); do / wait for child / { 36 if ( ( gone = wait ( ( int ) 0 )) < 0 ) { 37 / no interest for exit status / 38 perror ("wait" ); exit (2); 39 } 40 } while ( gone!= childpid ); 41 } 42 exit (0); 43 }

61 4.5. CLIENT-SERVER MIT UNNAMED PIPES Client-Server mit Unnamed Pipes Bidirektional Durch eine Pipe fließen Daten nur in genau eine Richtung. Zur Realisierung unseres Client-Server Beispiels benötigen wir aber einen bidirektionalen Kommunikationskanal. Wir müssen dazu zwei Pipes kreieren und eine Pipe für jede Richtung konfigurieren. stdout file content file content Vorgehen: parent process (client) stdin filename filename pipe_1 pipe_2 filename child process (server) Abbildung 4.7: Client-Server mit bidirektionaler Kommunikation 1. Pipe1 und Pipe2 kreieren 2. fork() ausführen 3. Linker Prozess (Erzeuger) schließt Lese-Ende von Pipe1 und Schreib-Ende von Pipe2 4. Rechter Prozess (Kind) schließt Schreib-Ende von Pipe1 und Lese-Ende von Pipe2 Die Realisierung zeigen die Programme 4.2 (S. 116), 4.4 (S. 117), 4.6 (S. 118), file content 116 KAPITEL 4. INTER-PROZESS-KOMMUNIKATION (IPC) Programm 4.2: Hauptprogramm zum Client/Server-Beispiel (cli-srv/main.c) 1 # include < stdio.h> 2 # include < stdlib.h> 3 # include <unistd. h> 4 # include <sys/wait.h> 5 6 # include " client.h" 7 # include "server.h" 8 9 # define RD_FD 0 10 # define WR_FD int main () { 13 int childpid, gone, pipe_1 [2], pipe_2 [2]; if ( pipe ( pipe_1 ) < 0 pipe ( pipe_2 ) < 0 ) { 16 perror ("pipe (): can t creat pipes" ); exit (1); 17 } 18 if ( ( childpid = fork ()) < 0 ) { 19 perror ("fork" ); exit (1); 20 } if ( childpid > 0 ) { // client / parent 23 printf ( " Client/Parent: my pid is %d\n", 24 ( int ) getpid () ); close ( pipe_1 [ RD_FD ] ); close ( pipe_2 [ WR_FD ] ); client ( pipe_2 [ RD_FD ], pipe_1 [ WR_FD ] ); / wait for child / 31 do { 32 if ( ( gone = wait ( ( int ) 0 )) < 0 ) { 33 perror ("wait" ); exit (2); 34 } 35 } while ( gone!= childpid ); printf ( " Cli/Par: server/child %d terminated\n", gone ); 38 printf ( " Cli/Par: going to exit\n" ); } else { // server / child 41 printf ( "Server/Child: after fork, my pid is %d\n", 42 ( int ) getpid () ); close ( pipe_1 [ WR_FD ] ); close ( pipe_2 [ RD_FD ] ); server ( pipe_1 [ RD_FD ], pipe_2 [ WR_FD ] ); printf ( "Server/Child: going to exit\n" ); 49 } 50 exit ( 0 ); 51 }

62 4.5. CLIENT-SERVER MIT UNNAMED PIPES 117 Programm 4.3: Client (cli-srv/client.h) 1 / 2 read line ( filename ( from stdin, write it to IPC channel, 3 copy text ( file content ) from IPC channel to stdout 4 / 5 6 # ifndef CLIENT_H 7 # define CLIENT_H 8 extern void client ( int readfd, int writefd ); 9 #endif Programm 4.4: Client (cli-srv/client.c) 1 # define CLIENT_H 2 # include " client.h" 3 4 # include < stdio.h> 5 # include < unistd.h> 6 # include < stdlib.h> 7 # include < string.h> 8 9 # define BUFSIZE void client ( int readfd, int writefd ) { 12 char buf [ BUFSIZ ]; 13 int n; / read filename from stdin, write it to IPC channel / 16 printf ("give filename: " ); / not safe!!! / if ( fgets ( buf, BUFSIZ, stdin ) == ( char ) 0 ) { 19 fprintf ( stderr, " Client : filename read error" ); 20 exit (1); 21 } n = strlen ( buf ); 24 if ( buf [ n 1 ] == \n ) n ; / zap NL / if ( write ( writefd, buf, n )!= n ) { 27 perror ("write" ); exit (2); 28 } / read data from IPC channel, write it to stdout / 31 while ( ( n = read ( readfd, buf, BUFSIZ )) > 0 ) 32 if ( write ( 1, buf, n )!= n ) { 33 perror ("write" ); exit (3); 34 } if ( n < 0 ) { 37 fprintf ( stderr, "read (): Client : can t read from IPC channel" ); 38 exit (4); 39 } 40 } 118 KAPITEL 4. INTER-PROZESS-KOMMUNIKATION (IPC) Programm 4.5: Server (cli-srv/server.h) 1 / 2 read filename from IPC channel, open this file, 3 copy data from file to IPC channel. 4 / 5 # ifndef SERVER_H 6 # define SERVER_H 7 extern void server ( int readfd, int writefd ); 8 # endif Programm 4.6: Server (cli-srv/server.c) 1 #include < stdio.h> 2 #include < unistd.h> 3 #include < stdlib.h> 4 #include < string.h> 5 #include < fcntl.h> 6 7 #define BUFSIZE void server ( int readfd, int writefd ) { 10 char buf [ BUFSIZ ]; 11 int n, fd ; / read filename from IPC channel / 14 if ( ( n = read ( readfd, buf, BUFSIZ )) < 0 ) { 15 perror ("read" ); exit (1); 16 } 17 buf [ n ] = \0 ; / try to open file / if ( ( fd = open ( buf, O_RDONLY )) < 0 ) { 22 / Format and send error mesg to client / 23 char errmesg [ BUFSIZ ]; (void) sprintf ( errmesg, "Server: can t open infile (%. s)\n", 26 BUFSIZ/2, buf ); 27 n = strlen ( errmesg ); 28 if ( write ( writefd, errmesg, n )!= n ) { 29 perror ( "write" ); exit (2); 30 } 31 return; 32 } while ( ( n = read ( fd, buf, BUFSIZ )) > 0 ) 35 if ( write ( writefd, buf, n )!= n ) { 36 perror ("write" ); exit (3); 37 } 38 if ( n < 0 ) { 39 fprintf ( stderr, "Server: can t read file " ); 40 exit (3); 41 } 42 }

63 4.6. STANDARD I/O BIBLIOTHEKSFUNKTION Standard I/O Bibliotheksfunktion popen() und pclose() #include <stdio.h> FILE *popen( char * cmd, char * mode ) /* create a pipe to a cmd: cmd: cmd to be executed mode: read from or write to pipe/cmd returns file pointer on success or NULL on error */ int pclose( FILE * fp ) /* close pipe with cmd */ /* returns exit status of cmd or -1 on error */ Beschreibung Die Funktion popen() aus der Standard I/O Bibliothek kreiert eine unidirektionale (!) Pipe und einen neuen Prozess, der von der Pipe liest oder in die Pipe schreibt. In dem neuen Prozess startet eine Shell und führt die mitgegebene Kommandozeile cmd (unter Berücksichtigung von PATH) aus. Die Pipe wird abhängig vom Argument mode (entweder r oder w!) so konfiguriert, dass sie stdout oder stdin des erzeugten Kommandos mit dem aufrufenden Prozess verbindet. Der aufrufende Prozess erhält sein Pipe-Ende als File Pointer von popen. 120 KAPITEL 4. INTER-PROZESS-KOMMUNIKATION (IPC) Beispiel: Programm 4.7: Beispiel mit popen(), pclose() (popen/popen.c) 1 #include < stdio.h> 2 #include < stdlib.h> 3 4 #define BUFFER_SIZE int main () { 7 8 char buf [ BUFFER_SIZE ]; 9 FILE fp ; if ( ( fp = popen ( "/bin/pwd", "r" )) == ( FILE ) 0 ) { 12 perror ("popen()" ); 13 exit (1); 14 } if ( fgets ( buf, BUFFER_SIZE, fp ) == (char ) 0 ) { 17 fprintf ( stderr, " fgets error" ); 18 exit (2); 19 } printf ( "The current working directory is:\n" ); 22 printf ( "\t%s", buf ); / pwd inserts newline / pclose ( fp ); 25 exit (0); 26 } pclose() schließt eine mit popen() geöffnete I/O-Verbindung ab (NICHT fclose()). Die Funktion blockiert bis das Kommando terminiert und liefert den Exit-Status des Kommandos zurück.

64 4.7. ÜBER STANDARDDESKRIPTOREN IN PIPE SCHREIBEN / LESEN Über Standarddeskriptoren in Pipe schreiben / lesen Programm 4.8: 0/1 auf Pipe (pipe-2.c) 1 #include < stdio.h> 2 #include < unistd.h> 3 #include < stdlib.h> 4 int main () { int pid ; int pfd [2]; 5 pipe ( pfd ); 6 pid = fork (); 7 8 if ( pid < 0) { perror ("fork" ); exit (1);} 9 10 if ( pid == 0) { / CHILD / 11 char buf [128]; int n; close (0); dup(pfd [0]); if ( ( n = read (0, buf, 15)) < 0 ) { 16 perror ("read" ); exit (1); 17 } else { 18 buf [n ] = \0 ; 19 printf (" child : read > %s <\n", buf); 20 exit (0); 21 } 22 } else { / PARENT / close (1); dup(pfd [1]); if ( write (1, "I m your parent", 15) < 0) { 27 perror ("write" ); exit (2); 28 } 29 exit (0); 30 } 31 } 122 KAPITEL 4. INTER-PROZESS-KOMMUNIKATION (IPC) 4.8 Termination von Pipeline-Verbindungen Programm 4.9: Termination? (pipe-3.c) 1 #include < stdio.h> 2 #include < unistd.h> 3 #include < stdlib.h> 4 5 int main () { 6 int pid ; int pfd [2]; 7 8 pipe ( pfd ); 9 pid = fork (); if ( pid < 0) { perror ("fork" ); exit (1); } if ( pid == 0) { / CHILD / 14 char buf [128]; int n; close (0); dup(pfd [0]); while ( ( n = read (0, buf,7)) > 0) { 19 buf [n ] = \0 ; 20 printf (" child : read > %s <\n", buf); 21 } 22 exit (0); 23 } else { / PARENT / 24 close (1); 25 dup(pfd [1]); 26 if ( write (1, "I m your parent", 15) < 0) { 27 perror ("write" ); exit (2); 28 } 29 exit (0); 30 } 31 }

65 4.8. TERMINATION VON PIPELINE-VERBINDUNGEN 123 Übersetzung und Ausführung: 124 KAPITEL 4. INTER-PROZESS-KOMMUNIKATION (IPC) 4.9 Wie die Shell eine Pipeline macht spatz$ gcc -Wall pipe-3.c spatz$ a.out spatz$ child: read > I m you < child: read > r paren < child: read > t < spatz$ ps PID TTY TIME CMD 5692 pts/3 00:00:00 bash 5705 pts/3 00:00:00 a.out 5706 pts/3 00:00:00 ps spatz$ kill 5705 spatz$ ps PID TTY TIME CMD 5692 pts/3 00:00:00 bash 5707 pts/3 00:00:00 ps spatz$ So terminiert alles: Programm 4.10: Termination! (pipe-4.c) 1 // Termination!!! 2 #include < stdio.h> 3 #include < unistd.h> 4 #include < stdlib.h> 5 6 int main () { 7 int pid ; int pfd [2]; 8 pipe ( pfd ); pid = fork (); 9 if ( pid < 0) { perror ("fork" ); exit (1); } if ( pid == 0) { / CHILD / 12 char buf [128]; int n; 13 close (0); dup(pfd [0]); close ( pfd [1]); / < / while ( ( n = read (0, buf,7)) > 0) { 18 buf [n ] = \0 ; 19 printf (" child : read > %s <\n", buf); 20 } 21 exit (0); 22 } else { / PARENT / 23 close (1); dup(pfd [1]); 24 if ( write (1, "I m your parent", 15) < 0) { 25 perror ("write" ); exit (2); 26 } 27 exit (0); 28 } 29 } Programm 4.11: Pipelining der Shell (pipe-5.c) 1 / ls l wc l / 2 #include < stdio.h> 3 #include < unistd.h> 4 #include < stdlib.h> 5 6 int main () { 7 int pid1, pid2 ; int pfd [2]; 8 9 pipe ( pfd ); pid1 = fork (); 12 if ( pid1 < 0) { perror ("fork" ); exit (1); } 13 if ( pid1 == 0) { / CHILD 1: wc l / close (0); dup(pfd [0]); 16 close ( pfd [1]); / without this line??? / 17 execlp ( "wc", "wc", " l", NULL); 18 perror ( "exec" ); exit (2); } else { / PARENT / pid2 = fork (); 23 if ( pid2 < 0) { perror ("fork" ); exit (3); } 24 if ( pid2 == 0) { / CHILD 2: ls l / close (1); dup(pfd [1]); 27 execlp (" ls ", " ls ", " l", NULL); 28 perror ("exec" ); exit (4); 29 } else { / PARENT / 30 close ( pfd [1]); / Why? / 31 close ( pfd [0]); / nice! / 32 / terminate, don t wait / 33 exit (0); 34 } 35 } 36 }

66 4.10. SIGPIPE KAPITEL 4. INTER-PROZESS-KOMMUNIKATION (IPC) 4.10 SIGPIPE Das Kommando xlsfonts produziert sehr große Ausgaben, mehr als auf einmal in eine Pipeline passt diese werden in eine Pipeline gelenkt über das Kommando head lesen wir ein bisschen heraus und terminieren dann was passiert mit xlsfonts??? Programm 4.12: Schreiben in eine Pipe ohne Leseende (pipe-6.c) 1 #include < stdio.h> 2 #include < unistd.h> 3 #include < stdlib.h> 4 #include <sys/wait. h> 5 int main () { 6 int pid1, pid2 ; int pfd [2]; 7 pipe ( pfd ); 8 pid1 = fork (); 9 if ( pid1 < 0) { perror ("fork" ); exit (1); } 10 if ( pid1 == 0) { / CHILD 1 fuer head 2 / 11 close (0); dup(pfd [0]); 12 close ( pfd [1]); / without this line? / 13 execlp ( "head", "head", " 2", NULL); 14 perror ( "exec" ); exit (2); 15 } else { / PARENT / 16 pid2 = fork (); 17 if ( pid2 < 0) { perror ("fork" ); exit (3); } 18 if ( pid2 == 0) { / CHILD 2 fuer xlsfonts / 19 close (1); dup(pfd [1]); 20 close ( pfd [0]); / without this line??? / 21 execlp (" xlsfonts ", " xlsfonts ", NULL); 22 perror ("exec" ); exit (4); 23 } else { / PARENT / 24 int status ; 25 close ( pfd [0]); close ( pfd [1]); / without? / 26 / wait for second child / 27 waitpid ( pid2, & status, 0); 28 / terminated because of Signal? / 29 if ( status & 0x7F ) { 30 printf ("terminated by signal %d\n", status & 0x7F); 31 } 32 exit (0); 33 } 34 } 35 } Übersetzung und Ausführung: spatz$ gcc -Wall pipe-6.c spatz$ a.out -adobe-courier-bold-o-normal m-60-iso adobe-courier-bold-o-normal m-60-iso terminated by signal 13 spatz$

67 128 KAPITEL 5. NETZWERK-KOMMUNIKATION Grundlegendes herstellerunabhängiges Konzept: DIN/ISO-OSI-Referenzmodell Kapitel 5 OSI - Open Systems Interconnection Netzwerk-Kommunikation 7 Sender Application D a t e n s t r o m H Paket i Anwendungs schicht Empfänger 5.1 Übersicht 6 5 Presentation Session H H Darstellungs schicht Sitzungs schicht Netzwerk 4 Transport H Transport schicht räumlich verteiltes System von Rechnern, Steuereinheiten und Peripheriegeräten, verbunden mit Datenübertragungseinrichtungen 3 Network H Vermittlungs schicht aktive / passive Komponenten 2 Data Link H T Sicherungs schicht Unterscheidung nach Ausbreitung globales Netz (GAN global area network) Internet, EUNet, VNET (IBM), u.a. Weitverkehrsnetz (WAN wide area network) DATEV-Netz, DFN (Deutsches Forschungsnetz) lokale Netze (LAN local area network, WLAN wireless LAN) innerhalb eines Unternehmens; i.a. mehrere, die selbst wieder vernetzt sind Front-end-Lan (Netz innerhalb einer Abteilung / Instituts) Backbone-LAN (Verbindung von Front-end-LAN s Netzwerktopologie physikalische / logische Verbindung der Rechner im Netz (Stern, Ring, Bus) Protokolle Regeln (Vereinbarungen), nach denen Kommunikationspartner (Rechner) eine Verbindung aufbauen, die Kommunikation durchführen und die Verbindung wieder abbauen 1 Physical Medium Bitfolge Abbildung 5.1: ISO-OSI-Referenzmodell Bitübertragungs schicht Medium 1. Bitübertragungsschicht Regelung aller physikalisch-technischer Eigenschaften der Übertragungsmedien zwischen den verschiedenen End-/Transitsystemen Darstellung von Bits via Spannungen, Stecker, Sicherungsschicht Sicherung der Schicht 1 gegen auf den Übertragungsstrecken auftretenden Übertragungsfehler (elektromagnetische Einflüsse) z.b. Prüfziffern, parity bits 3. Vermittlungsschicht Adressierung der Zielssysteme über das (die) Transitsystem(e) hinweg sowie Wegsteuerung der Nachrichten durch das Netz Flusskontrolle zwischen End- und Transitsystemen (Überlastung von Übertragungswegen und Rechnern / Transitsystemen, faire Verteilung der Bandbreite) 127

68 5.1. ÜBERSICHT Transportschicht Stellt die mithilfe der Schichten hergestellten Endsystemverbindungen für die Anwender zur Verfügung z.b. Abbildung logischer Rechnernamen auf Netzadressen 5. Kommunikationssteuerungsschicht Bereitsstellung von Sprachmitteln zur Steuerung der Kommunikationsbeziehung (session) Aufbau, Wiederaufnahme nach Unterbrechung, Abbau 6. Datendarstellungsschicht Vereinbarungen bzgl. Datenstrukturen für Datentransfer 7. Anwendungsschicht Berücksichtigung inhaltsbezogener Aspekte (Semantik) Quasistandard TCP/IP Transmission Control Protocol / Internet Protocol Process / Application File Transfer: FTP simple mail transf.prot. (SMTP) terminal emulation telnet protocol network manag. simple network manag. prot Host to host TCP User Datagram Prot.UDP address internet prot. internet control Internet layer resolution message prot Ethernet, IEEE 802, Arcnet, X.25 network access or local network lay twisted pair, Koaxial,Glasfaser KAPITEL 5. NETZWERK-KOMMUNIKATION Kopplung von Netzen Kopplungseinheiten zur Verbindung von Netzen (internetworking units) Adressumwandlung, Wegewahl (routing), Flusskontrolle, Fragmentierung und Wiederzusammenfügung von Datenpaketen, Zugangskontrolle, Netzwerkmanagement Repeater Verstärker; Empfangen, Verstärken, Weitersenden der Signale auf Bitübertragungsschicht; die zu verbindenden Netze müssen identisch sein; Verbindung von Netzsegmenten Bridge Verbindung von Netzen mit unterschiedlichen ÜBertragungsmedien, aber mit gleichem Schichtaufbau; operiert auf Sicherungsschicht Router operiert auf Vermittlungsschicht Gateway Verknüpfung von Netzen, die in Schicht 3 (und aufwärts) unterschiedliche Struktur aufweisen Hub Eine Art Multiplexer, der das Eingangssignal sternförmig an die angeschlossenen Geräte weiterleitet (keine eigene Adresse); reduziert den Verkabelungsaufwand Switch spezieller Hub der Unterschied ist die Arbeitsweise: ein Hub empfängt ein Paket und sendet es einfach an alle Ports weiter, ein Switch hingegen schickt ein Paket nur an den Port weiter der das Paket auch benötigt Vorteil eines Switches: wesentlich höhere Geschwindigkeit, die durch das intelligente Routing erreicht wird Dabei übernehmen weiter unten stehende Geräte teilweise die Funktionalität darüberstehender Geräte! OSI TCP/IP Abbildung 5.2: TCP/IP Protokoll Implementierungen

69 5.1. ÜBERSICHT KAPITEL 5. NETZWERK-KOMMUNIKATION Übertragungsmedien STP Kupferleiter (siehe z.b. de.wikipedia.org/wiki/) Twisted-Pair-Kabel: Kabeltypen, bei denen die beiden Adern eines Adernpaares miteinander verdrillt (auch verseilt oder verdreht) sind; durch die Verdrillung jeweils einer Datenleitung mit einer Masseleitung ist die Datenübertragung weniger störanfällig; Sie bestehen grundsätzlich aus Ader: Kunststoffisolierter Kupferleiter Paar: Je zwei Adern sind zu einem Paar (englisch pair) verdrillt Seele: Bezeichnet die vier miteinander verseilten Paare Kabelmantel: Umfasst die Seele. Besteht aus PVC oder halogenfreiem Material Zusätzlich zu den Aderpaaren können weitere Elemente im Kabel vorhanden sein: Beidrähte als elektrische Masseleitung, Fülladern aus Kunststoff zum Ausfüllen von Hohlräumen zwischen den Paaren oder Trennelemente aus Kunststoff um die Paare auseinander zu halten. Twisted-Pair-Kabel gibt es in zwei- und vierpaariger Ausführung. Bei aktuellen Netzwerkinstallation werden fast nur vierpaarige Kabel verwendet. Man unterscheidet UTP (Unshielded Twisted Pair): Kabel mit ungeschirmten Paaren und ohne Gesamtschirm (siehe Abb. 5.3, S. 131) STP (Shielded Twisted Pair): Die Adernpaare sind mit einem metallischem Schirm (meist eine Alu-kaschierte Kunststofffolie) umgeben (siehe Abb. 5.4, S. 132) S/UTP (Screened Unshielded Twisted Pair): Aufbau wie bei UTP, jedoch mit zusätzlicher Gesamtschirmung um die Seele S/STP (Screened Shielded Twisted Pair): Aufbau wie bei STP, jedoch mit zusãd tzlicher Gesamtschirmung um die Seele (Abb. 5.5, S. 132) UTP Adernisolierung Paar Paarschirm Kabelmantel Abbildung 5.4: Shielded-Twisted-Pair-Kabel S/STP Koaxialkabel, kurz: Koax-Kabel Kupferkabel Adernisolierung Paar Paarschirm Gesamtschirm Kabelmantel Abbildung 5.5: Screened Shielded-Twisted-Pair-Kabel bestehen aus einem isolierten Innenleiter (auch Seele genannt), der von einem in konstantem Abstand um den Innenleiter angebrachten Außenleiter umgeben ist. Üblicherweise ist diese Ummantelung ebenfalls nach außen isoliert (Abb. 5.6, S. 132 Ausnutzung eines breiten Frequenzspektrums für parallele Übertragung Abbildung 5.6: Koaxial-Kabel Kupferleiter Aderisolierung Paar Kabelmantel Abbildung 5.3: Unshielded-Twisted-Pair-Kabel Twisted-Pair-Kabel sind billig, einfach zu verlegen; geringe Bandbreite, geringe Abhörsicherheit, hohe Störanfälligkeit

70 5.1. ÜBERSICHT 133 Glasfaser (Lichtwellenleiter) Die Faser besteht aus einem Kern, einem Mantel und einer Beschichtung. Der lichtführende Kern dient zum Übertragen des Signals. Ein Standard für lokale Computernetze, der auf Glasfaserkabeln aufbaut, ist z. B. das Fiber Distributed Data Interface (FDDI). 134 KAPITEL 5. NETZWERK-KOMMUNIKATION Zugangsverfahren wer darf wann senden? a) über strenge Vorschrift wird festgelegt, wer wann senden darf b) jeder sendet wie er will, bis Fehler auftritt, der dann korrigiert wird Drahtlose Übertragung (Funk, Infrarotwellen) Wireless LAN (WLAN) bezeichnet ein drahtloses lokales Funknetz-Netzwerk, wobei meistens ein Standard der IEEE Familie gemeint ist arbeiten meistens im sog. Infrastruktur-Modus, bei der eine oder mehrere Basisstationen (Wireless Access Points) die Kommunikation zwischen den Clients organisieren; der Datentransport läuft immer über die Basisstation(en) Bei einem Infrastruktur-Netzwerk wird über einen zentralen Knotenpunkt (Access Point) die Kommunikation der einzelnen Endgeräte ermöglicht, die sich jeweils mit ihrer MAC-Adresse und/oder IP-Adresse am Knoten anmelden müssen Die MAC-Adresse (Media Access Control) ist die Hardware-Adresse eines jeden Netzwerkgerätes (Netzwerkkarte, Switches), die zur eindeutigen Identifikation des Geräts im Netzwerk dient. ad a) Token-Verfahren (stark vereinfacht) ein besonderes Bitmuster (Token) kursiert im Netz senden darf der, der es besitzt das Token wird an das Ende der Sendung angefügt ad b) CSMA/CD-Verfahren carrier sense multiple access with collision detection viele beteiligte Sender (multiple access) vor dem Senden in den Kanal horchen (carrier sense) wenn frei, senden, sonst warten während des Sendens den Kanal prüfen, ob andere senden, um Kollissionen zu erkennen (collision detection) wenn Kollision, müssen alle Sender abbrechen; jeder wartet eine zufällig gewählte Zeitspanne und wiederholt Sendevorgang Sender mit der kürzesten Zeitspanne gewinnt

71 5.2. ETHERNET Ethernet ca XEROX PARC Standardisiert 1978 von XEROX, INTEL, DEC Schicht 2 Kabel rein passiv elektronische Komponenten (Netzwerkkarte): Transceiver (Übertragen/Empfangen in/von Ethernet) host interface (Rechner-Bus) 136 KAPITEL 5. NETZWERK-KOMMUNIKATION Frame Format: (siehe auch Abb. 5.1, S. 128) Preamble Destination Source Type Data Abbildung 5.7: Ethernet Frame Format CRC (Cyclic Redundancy Check) Eigenschaften: alle teilen sich einen Kanal (BUS) broadcast - alle Transceiver hören alles, host interface stellt fest, ob Nachricht für diesen Rechner gedacht ist best-effort delivery - Bemühensklausel : ob Sendung wohl ankommt? CSMA/CD-Zugangsverfahren Adressierung: Netzwerkkarte (host interface) bildet Filter, alle Pakete werden dahin weitergeleitet, nur die dem Transceiver entsprechenden Pakete (Hardware-Adressen) werden an Rechner weitergeleitet Preamble: zur Synchronisation der Knoten, alternierende 0/1-Folge Type: welches Protokoll (für BS)? self-identifying 5.3 Internetworking - Konzept & Grundlegende Architektur bislang: ein Netz (ein physikalisches Netz) - z.b. Token Ring, Bus Adressen von Hosts waren physikalische Adressen (MAC-Adressen) jetzt: Adresse eines Rechners: 48 Bit Integer, vom Hersteller auf Interface festgelegt, von IEEE gemanaged MAC-Adresse Adresstypen: physische Adresse einer Schnittstelle network broadcast address (alle Bits auf 1, an alle ) multicast broadcast (Teilmengen broadcast) Betriebssystem initialisiert Schnittstelle: welche Adressen sollen erkannt werden? Netz über Netzen über Netzen über... über physikalischen Netzen notwendig: Abstraktion von zugrundeliegenden physikalischen Netzen Ansatz 1: spezielle (Applikations-) Programme, die aus der Heterogenität der physikalischen Netze / Hardware eine softwaremäßige Homogenität herstellen Ansatz 2 (Abb. 5.8, S. 137): Verbergen von Details im Betriebssystem des jeweiligen Rechners (layered-system architecture, Schichtenmodell der Protokolle)

72 5.3. INTERNETWORKING - KONZEPT & GRUNDLEGENDE ARCHITEKTUR KAPITEL 5. NETZWERK-KOMMUNIKATION Frage: Muss ein Gateway alle in allen Netzen erreichbare Rechner kennen (ein Super-Computer)? application level Antwort: Nein! OS Abstraktion gateways route packets based on destination network, not on destination host Benutzer-Sicht: ein Internet als ein großes, virtuelles oder logisches Netzwerk physical level Routing: Abbildung 5.8: Layered System Architecture Finden eines optimalen Weges von einem Rechner A (in einem beliebigen lokalen Netz) zu einem Rechner B (in einem beliebigen lokalen Netz) Optimal: kostengünstigster und / oder kürzester und / oder schnellster Weg? Grundlegende Internet Architektur abhängig vom Routing-Protokoll und somit Einstellungssache des Routers Internet-Adressen Netz 1 (Token-Ring: 8-Bit-Adressen) GATEWAY globale Identifikation von hosts im (in ihrem) Netz Internet-Protocol (IP) NAME (was) ADRESSE (wo) abnehmende Abstraktion der Identifikation Adressen von Rechnern in Netz-2 aus Sicht der Rechner aus Netz-1??? Netz 2 <net_id,host_id> (Ethernet - 48-Bit-Adressen) ROUTE, PFAD (wie dorthin) Abbildung 5.10: Abstraktion der Identifikation Abbildung 5.9: Internet Architektur IP-Adresse In a TCP/IP internet, computers called gateways provide all interconnections among physical networks logische Adresse eindeutih im gesamten Internet, ausgenommen die privaten Subnetze Integer, die das Routing unterstützen: IPv4 32-Bit (siehe Abb. 5.11, S. 139) / IPv6 128 Bit

73 5.3. INTERNETWORKING - KONZEPT & GRUNDLEGENDE ARCHITEKTUR KAPITEL 5. NETZWERK-KOMMUNIKATION 0 netid hostid Class A 1 0 netid hostid Class B Diese Aufteilung war sehr sehr starr und wurde 1996 durch das Konzept der Netzmaske verallgemeinert (s.u.): CIDR Classless Inter-Domain Routing als Übergang zu IPv6 mit 128-Bit-Adressen. Damit spielt es keine Rolle mehr, welcher Netzklasse eine IP-Adresse angehört. Subnetzmaske netid muliticast id hostid reserved Abbildung 5.11: 32-Bit IP-Adresse in Version 4 lesbare Adressen ( punktiertes Dezimalformat): (dotted decimal notation) Class C Class D Class E Subnetz-Masken werden eingesetzt, um die starre Klassenaufteilung der IP-Adressen in Netze und Rechner flexibel an die tatsächlichen Gegebenheiten anzupassen Die Grenze zwischen den Bits der Netz- und der Rechneradresse wird verschoben. Dadurch erhöht man zwar die Zahl der möglichen Netze, verringert aber gleichzeitig die Anzahl der jeweiligen Rechner. Diese neuen vielen kleinen Netze werden als Subnetze bezeichnet Die Einrichtung von Subnetzen macht es möglich, viele völlig verschiedene und weit entfernte Netze miteinander zu verbinden, da jedes Subnetz seine eindeutige Adresse bekommt und somit vom IP-Router adressierbar wird Ein Subnetz wird dadurch definiert, dass die IP-Adresse mit einer sogenannten Subnetz- Maske verknüpft wird: Ist ein Bit in der Subnetz-Maske gesetzt, wird das entsprechende Bit der IP-Adresse als Teil der Netzadresse angesehen Ist ein Bit in der Subnetz-Maske nicht gesetzt, dann wird das entsprechende Bit der IP-Adresse als Teil der Rechneradresse benutzt (thales.mathematik.uni ulm.de) Abbildung 5.12: IP-Adresse: dotted decimal form Aus dem in Abb (S. 139) beschriebenem Adressaufbau resultieren die in Abb (S. 139) dargestellten Adressräume für die einzelnen Klassen. Class A: Class B netid hostid netid hostid 1.x.x.x bis 126.x.x.x 126 Subnetze x bis x Hosts x.x bis x.x Subnetze x.x.0.1 bis x.x Hosts Class C netid x bis x.x hostid x.x.x.1 bis x.x.x.254 Abbildung 5.13: IP-Adressklassen in Version Subnetze 254 Hosts Die einzelnen IP-Router der Subnetze können ihre IP-Adresse auch wieder mit einer Subnetz- Maske verknüpfen, um weitere Subnetze zu erzeugen Beispiele: a) Netzmaske: ( ) Die Subnet-Maske geht bis zum 11.ten Bit, d.h. die ersten 11 Bit einer IP-Adresse bezeichnen das Netzwerk, die restlichen den Host in diesem Netzwerk. b) Netzmaske: ( ) Hier geht die Subnet-Maske bis zum 30. Bit, d.h. die ersten 30 Bit einer IP-Adresse bezeichnen das Netzwerk, der Rest die Rechner im jeweiligen Netzwerk. Statt der Netzmaske verwendet man heute einen Präfix, der angibt, bei welchem Bit die Aufteilung in Netz- und Rechneradresse erfolgt: Subnetzmaske 32-Bit-Wert Präfix / / / / /26 Wird in der IP-Konfiguration einer Netzwerkstation IP-Adresse und Subnetzmaske manuell eingegeben erfolgt die Schreibweise separat in Form von / (IP-Adresse / Subnetzmaske) oder / 24 (IP-Adresse / Präfix).

74 5.3. INTERNETWORKING - KONZEPT & GRUNDLEGENDE ARCHITEKTUR 141 Adressenvergabe: Die Zugehörigkeit eines Netzwerkinterfaces zu einem Adressraum wird durch bestimmte administrative Behörden geregelt in Deutschland DE-NIC, weltweit INTER-NIC. Ausnahme hiervon bilden die sog. privaten Adressräume einer jeden Klasse, welche für private Netzwerke, die nicht (unmittelbar) am Internet teilnehmen, reserviert sind. Sie können natürlich auch am Internet teilnehmen, z.b. über das NAT-Protokoll ( Network Address Translation) Reservierte Adressen (privat, nicht zur direkten Anbindung an das Internet: bis , bis , bis (RFC 1918) Routing RFC1983 Internet Users Glossary: Routing ist der Prozess der Auswahl der richtigen Schnittstelle und des nächsten Hops (Router) für (Daten-) Pakete, die weitergeleitet werden sollen. Bedingungen an Router: Die entsprechenden Routing-Protokolle müssen aktiv sein Das Zielzetz muss bekannt sein oder eine Alternative, die zum Zielnetz führen kann Der Router muss seine aktive Schnittstelle in Richtung Zielnetz auswählen Routing-Protokolle: Protokolle, die die Wegwahl durch spezielle Routing-Algorithmen ermöglichen 142 KAPITEL 5. NETZWERK-KOMMUNIKATION Autonome Systeme (AN Die Netzwerke, die das Internet bilden, werden Autonome Systeme (AS) (manchmal findet sich auch der Begriff Domain) genannt; sie stellen eine Zusammenfassung zentral adminstrierter Netze dar und werden über eine 16-Bit-Zahl in IPv4 / 32 in IPv6 (AS-Nummer) identifiziert. Der Betreiber eines solchen autonomen Systems lässt sich von seiner regional zuständigen Vergabestelle für IP-Adressen (s.u.) einen Block von IP-Adressen zuteilen, mit denen er sein Netzwerk adressiert. Für die in Europa ansässigen Internet-Dienstleister ist die zuständige Vergabestelle RIPE (Réseaux IP Européen), weltweit ist es die IANA (Internet Assigned Numbers Authority). Die AS-Nummer ermöglicht es demzufolge, verschiedene Subnetze eines Netzbetreibers zu einem Block zusammenzufassen. Diese Zusammenfassung dient vor allem dem Zweck, das Routing (die Wegewahl) auf oberster Ebene, also zwischen autonomen Systemen, entscheidend zu vereinfachen. Nicht jedes autonome System muss den Weg für alle einzelnen Subnetze kennen, sondern jedes autonome System muss die Möglichkeit haben, bei Bedarf durch Abfragen festzustellen, zu welchem autonomen System die gewünschte IP-Adresse gehört, um dann die Datenübertragung an das entsprechende autonome System weiterzuleiten, das die Ziel-IP-Adresse beinhaltet. Algorithmen Distance Vector Routing (Bellman-Ford) Bestimmung der Anzahl der Hops (Sprünge) in einem Pfad, um den kürzesten Weg von Quelle zu Ziele zu finden jeder Router überträgt seine vollständige Routing-Tabelle bei jedem Update an seinen Nachbarn jedem Router sind nur die Kosten zu jedem Ziel bekannt und der dafür notwendige nächste Knoten bekannt benutzt bei RIP es werden nur Tabellen mit Informationen, die die Weiterleitung von Benutzerinformationen unterstützen, an andere Router weitergegeben (keine Benutzerinformationen) Austausch der Routing-Tabellen darf Netz nicht übermäßig beanspruchen Routing-Tabellen sollen überschaubar bleiben Beispiele: RIP Routing Information Protocol IGRP Interior Gateway Routing Protocol (von Cisco) OSPF Open Shortest Path First BGP Border Gateway Protocol Unterscheidung nach Einsatzgebieten: innerhalb der eigenen Netzwerke zwischen Netzen Link State Routing jeder Router überträgt seine Routing Informationen (Kosten / Last zur Erreichung seine Nachbarn) an alle Router im Netz (Link State Broadcast) Jeder Router kennt gesamt Netztopologie lokale Bestimmung des kürzesten Weges anhand des Dijkstra Algorithmus benutzt bei OSPF

75 5.3. INTERNETWORKING - KONZEPT & GRUNDLEGENDE ARCHITEKTUR 143 TCP/IP 144 KAPITEL 5. NETZWERK-KOMMUNIKATION Internet Datagram - basic transfer unit Application Services grober Aufbau (Abb. 5.15, S. 144): Reliable Transport Service TCP Datagram Header Datagram Data Area Connectionless packet delivery Service IP Abbildung 5.15: IP Datagramm - grober Aufbau Abbildung 5.14: TCP/IP-Schichtenmodell 1. Concept of Unreliable Delivery Auslieferung von Paketen ist nicht garantiert (may be lost, duplicated, delayed, delivered out of order service will not detect nor inform sender or receiver) 2. Connectionless Jedes Paket wird losgelöst von den anderen behandelt (evt. auch anders gerouted) 3. best-effort delivery bemühe mich um Auslieferung genauer ist dies in Abb. 5.16, S. 144 dargestellt Regeln, wie Host s und Gateway s Pakete verarbeiten sollen, wie und wann Fehlermeldungen produziert werden sollen, Bedingungen für das Wegwerfen von Paketen Version Header Length Time to live (TTL) Identification Service Type Protocol IP Options (if any) Flags Source IP Address Destination IP Address Total Length Header Checksum Fragmentation offset Padding DATA IP Internet Protocol: DATA Regeln, die den unreliable, connectionless, best-effort delivery - Mechanismus definieren 1. basic unit of data transfer durch ein TCP/IP-Internet 2. enthält routing-funktion (Auswahl des Pfades) Abbildung 5.16: IP Datagram - im Detail

76 5.3. INTERNETWORKING - KONZEPT & GRUNDLEGENDE ARCHITEKTUR 145 TCP/IP Internet Layering Model Conceptual Layer Application Transport Internet Network Interface (data link) Hardware Objects Passed Between Layers Messages or Streams Transport Protocol Packets IP Datagrams Abbildung 5.17: TCP/IP Layering Model Network-Specific Frames 146 KAPITEL 5. NETZWERK-KOMMUNIKATION jedem Paket wird zusätzliche Information beigefügt (welche Appl.?) Prüfsumme Internet Layer Erstellen der IP Datagrams (weiter-) senden der Datagrams auspacken der Datagrams (auf Zielmaschine) Übergabe an das richtige Transport-Protokoll (Zielmaschine) ICMP Network Interface Layer übernimmt Datagrams und schickt sie auf spezifischem Netz weiter Application Layer Auf der obersten Ebene starten Benutzer Anwendungsprogramme, die auf im Netz verfügbare Dienste zugreifen wollen. Ein Anwendungsprogramm interagiert mit dem / den Transport-Protokoll(en), um Daten zu senden / empfangen. Jedes Anwendungsprogramm wählt die benötigte Transport-Art, z.b. eine Folge individueller Botschaften (messages) oder einfach eine Folge von Bytes. Das Anwendungsprogramm übergibt diese Daten in der verlangten Form an die Transport-Ebene zur Auslieferung. Transport Layer Die Hauptaufgabe dieser Schicht besteht darin, die Verbindung zur Kommunikation zwischen zwei Anwendungsprogrammen bereitzustellen (end-to-end-communication). Regulieren des Informationsflusses Bereitsstellen eines zuverlässigen Transports Sicherstellen, dass Daten korrekt und in Folge ankommen Warten auf Empfangsbestätigung des Empfängers erneutes Senden verlorengegangener Pakete Aufteilen des Datenstroms in kleine Stücke (packets) Übergeben jeden Paketes mit Zieladresse für die nächste Schicht i.a. arbeiten viele Applikation mit der Transport-Schicht (Senden, Empfangen)

77 5.4. TRANSPORT-PROTOKOLLE Transport-Protokolle Ports Internet Protokolle adressieren Host s Adressierung von Applikationen (letztliches Ziel) innerhalb des Zielrechners? Unterstellt seien multiprocess-systeme als Zielrechner (z.b. UNIX-Rechner). Prozess als letztliches Ziel? Prozesse werden dynamisch erzeugt und terminiert, Sender kann Prozesse auf anderen Maschinen nicht identifizieren Dienste (sprich Funktionen, Leistungen) auf anderen Rechnern sind das Ziel, unabhängig von welchem Prozess (welchen Prozessen) diese realisiert sind! Jede Maschine hat eine Menge sog. Protokoll- Port s (abstrakter Endpunkt), identifiziert durch positive Integer. Das jeweilige Betriebssystem bietet Mechanismus an, mit dem Prozesse Ports spezifizieren und nutzen können. Die meisten Betriebssysteme unterstützen synchronen Zugriff auf Ports. Wenn dabei eine Prozess Daten von einem Port lesen will, noch keine Daten da sind, wird er solange blockiert, bis (genügend) Daten eingetroffen sind. Ports sind typischerweise gepuffert. Ziel-Adresse: (IP Adresse + Port-Nummer) Jede Meldung enthält neben destination port auch source port (z.b. zum Antworten). 148 KAPITEL 5. NETZWERK-KOMMUNIKATION keine Empfangsbestätigungen (Acknowledgement) Kein Ordnen ankommender Meldungen keinerlei Feedback UDP messages können verloren gehen, dupliziert werden, ungeordnet ankommen, schneller ankommen als verarbeitet werden! Anwendungsprogramme, die auf UDP aufbauen, müssen selbst für Zuverlässigkeit sorgen. Source Port ist optional, wenn nicht genutzt, so NULL LENGTH Anzahl Bytes (octets) im UDP Datagram inkl. Header. CECKSUM ebenfalls optional; wenn keine Prüfsumme berechnet, so NULL. NB: IP berechnet keiner Prüfsumme über den Datenteil! Berechnung der Prüfsumme (wie bei IP): Daten werden in 16-Bits-Einheiten aufgeteilt, Summe über 1-er-Komplement wird gebildet, davon wieder 1-er-Komplement gebildet; ist Prüfsumme identisch Null, so wird davon das 1-er-Komplement gebildet (alles auf 1); unproblematisch, da es bei 1-er-Komplement zwei Darstellungen des Zahl Null gibt: alle Bits auf 0 oder alle auf 1! Port 1 Port 2 Port 3 UDP: Demultiplexing UDP Based on Port UDP Datagram arrives IP Layer UDP Source Port UDP Message Length UDP Destination Port UDP Checksum Abbildung 5.19: UDP-Demultiplexing Data Data Abbildung 5.18: UDP - Format The User Datagram Protocol (UDP) provides unreliable connectionless delivery service using IP to transport messages between machines. It adds ability to distinguish among multiple destinations within a given host computer

78 5.4. TRANSPORT-PROTOKOLLE 149 Port-Nummern: 150 KAPITEL 5. NETZWERK-KOMMUNIKATION Beispiel einer TCP/IP Sitzung an Port 80 (Webserver): einige zentral vergeben (well-known port numbers, universal assignment) dynamic binding approach Port ist nicht global bekannt; wenn ein Programm einen Port benötigt, wird von der Netzwerk- Software einer bereitgestellt. Um eine Port-Nummer auf einem anderen Rechner zu erfahren, wird eine entsprechende Anfrage gestellt und beantwortet. Decimal Keyword UNIX Keyword Description Reserved 7 ECHO echo Echo 9 DISCARD discard Discard 11 USERS systat Active Users 13 DAYTIME daytime Daytime 37 TIME time Time 42 NAMESERVER name Host Name Server 43 NICNAME whois Who Is? 53 DOMAIN nameserver Domain Name Server 67 BOOTPS bootps Bootstrap Protocol Server 68 BOOTPC bootpc Bootstrap Protocol Client 69 TFTP tftp Trivial File Transfer 111 SUNRPC sunrpc Sun Microsystems RPC 123 NTP ntp Network Time Protocol snmp SNMP net protocol... thales$ telnet 80 Trying Connected to Escape character is ^]. GET /sai/swg/ HTTP/1.0 # <-- Eingabe # <-- noch ein <return>, dann folgt Ausgabe des Servers HTTP/ OK Date: Tue, 18 May :05:22 GMT Server: Apache/ (Unix) Last-Modified: Tue, 18 May :04:47 GMT ETag: "b40a3-10fe-40a9d1af" Accept-Ranges: bytes Content-Length: 4350 Connection: close Content-Type: text/html <HTML> <TITLE>Prof. Dr. Franz Schweiggert</TITLE> <BODY> <H1>Prof. Dr. Franz Schweiggert</H1>... <ADDRESS><A HREF="/sai/swg/">Franz Schweiggert</A>, 15. Mai 2004 </ADDRESS> </BODY> </HTML> Connection to closed by foreign host. thales$

79 152 KAPITEL 6. BERKELEY SOCKETS application Kapitel 6 Berkeley Sockets socket system calls socket system call implementation socket layer functions protocol layer TCP/IP, UNIX, XNS user kernel 6.1 Grundlagen device driver API (application program interface) zu Kommunikations-Protokollen Abbildung 6.1: Vereinf. Modell der Implementierung von Sockets unter BSD In UNIX: Berkeley sockets und System V Transport Layer Interface (TLI) Beide Schnittstellen wurden für C entwickelt Die Implementierungen der Internet-Protokolle und der Socket-Schnittstelle wurden erstmals 1982 in der Version 4.1c der Berkeley Software Distributions (BSD) an der Universität von Kalifornien in Berkeley integriert. Mit der Version 4.2BSD war 1983 das erste Unix-System mit Netzwerkunterstützung verfügbar. Der Zugriff einer Anwendung auf die Funktionen der Socketschicht (socket layer functions) im Betriebssystemkern erfolgt über die Systemaufrufe der Socket-Schnittstelle. Hier findet die Umsetzung der protokollunabhängigen Operationen in die protokollspezifischen Implementierungen der darunterliegenden Protokollschicht (protocol layer) statt. Die Auswahl des konkreten Kommunikationsprotokolls wird bei der Erzeugung eines Socket über die Spezifikation der Domäne festgelegt. In der Protokollschicht (protocol layer) sind die Implementierungen der vom System unterstützen Protokollfamilien zusammengefaßt. Die darunterliegende Datenübertragungsschicht enthält die Implementierungen der Gerätetreiber (device driver) zur Datenübertragung über verschiedene Medien oder auch systeminterne Mechanismen. Die beiden zentralen Abstraktionen der Socket-Schnittstelle sind der Socket und die Kommunikationsdomäne. Aus Sicht der Anwendung repräsentiert ein Socket einen Kommunikationsendpunkt eines Benutzerprozesses innerhalb der gewählten Kommunikationsdomäne. Die Domäne spezifiziert die Protokollfamilie und definiert allgemeine Eigenschaften und Vereinbarungen wie z.b. die Adressierung des Kommunikationsendpunktes. Durch die Abstraktion Socket und Kommunikationsdomäne wird eine Trennung zwischen den Funktionen der Socket-Schnittstelle und den im System implementierten Kommunikationsprotokollen erreicht. Dem Anwender steht somit eine einheitliche Schnittstelle zur Verfügung, die verschiedene Netzwerkprotokolle und lokale Interprozesskommuniktaionsprotokolle gleichermaßen unterstützt. Die Implementierung ist vollständig im Betriebssystem integriert (siehe Abb. 6.1, S. 152). 151

80 6.2. EIN ERSTES BEISPIEL: TIMESERVER AN PORT Ein erstes Beispiel: Timeserver an Port KAPITEL 6. BERKELEY SOCKETS Übersetzung und Ausführung: Programm 6.1: Timeserver an Port (timserv.c) 1 #include <netdb.h> 2 #include < netinet /in.h> 3 #include < stdio.h> 4 #include < strings.h> 5 #include <sys/ socket.h> 6 #include <sys/time. h> 7 #include < unistd.h> 8 #include < stdlib.h> 9 #define TPORT int main () { 11 struct sockaddr_in addr, client_addr ; 12 size_t client_add_len = sizeof ( client_addr ); 13 int sfd, fd ; int optval = 1; bzero (&addr, sizeof ( addr )); // mit 0en fuellen addr. sin_family = AF_INET; // TCP/IP Verbindung 18 addr. sin_port = htons (TPORT);// Port eintragen, Network Byte Order if (( sfd = socket (AF_INET, SOCK_STREAM, 0)) <0) // socket erzeugen 21 perror ("socket" ), exit (1); if ( setsockopt ( sfd,sol_socket, SO_REUSEADDR, &optval, sizeof(optval))<0) 24 perror (" setsockopt" ), // rasche Wiederzuweisung ermoeglichen 25 exit (1); if ( bind ( sfd, ( struct sockaddr ) & addr, sizeof ( addr )) <0) 28 perror ("bind" ), exit (1); if ( listen ( sfd, SOMAXCONN) <0) 31 perror (" listen " ), exit (1); while (( fd = accept ( sfd, ( struct sockaddr ) & client_addr, 34 & client_add_len )) >=0) { 35 time_t clock ; char tbuf ; 36 time(& clock ); // aktuelle Uhrzeit holen 37 tbuf = ctime(& clock ); // in String schreiben 38 write ( fd, tbuf, strlen ( tbuf )); // in Clientverbindung schreiben 39 close ( fd ); 40 } 41 exit (0); // seldomly reached 42 } thales$ make -f make_timserv gcc -o timserv -Wall timserv.c -lsocket -lnsl thales$ timserv & [1] thales$ telnet thales Trying Connected to thales. Escape character is ^]. Tue May 18 11:36: Connection to thales closed by foreign host. thales$ exit

81 6.3. DIE SOCKET-ABSTRAKTION Die Socket-Abstraktion Der Socket repräsentiert als zentrale Abstraktion einen Kommunikationsendpunkt eines Benutzerprozesses innerhalb der gewählten Kommunikationsdomäne. Letztere wiederum ist eine weitere Abstraktion, die allgemeine Kommunikationseigenschaften der vom System bereitgestellten Mechanismen für die Prozesskommunikation mit Sockets zusammenfaßt. Dazu zählen beispielsweise der zu verwendende Namensraum und die damit verbundene Art der Adreßspezifikation bei der Benennung eines konkreten Socket. Die Kommunikation zwischen je zwei Sockets verläuft normalerweise immer innerhalb derselben Domäne. 156 KAPITEL 6. BERKELEY SOCKETS Die wichtigsten Socket-Typen unter Unix: STREAM: Ein Stream Socket stellt ein Kommunikationsverfahren bereit, das einen bidirektionalen, kontrollierten und verlässlichen Datenfluss garantiert, d.h. alle transferierten Datenpakete kommen in derselben Reihenfolge vollständig und ohne Duplikate beim Empfänger an (vgl. in der Semantik zu bidirektionale Unix-Pipes). In BSD-Unix sind Pipes auf diese Weise implementiert. DGRAM: Ein Datagram Socket stellt ebenfalls ein bidirektionales Kommunikationsverfahren bereit, aber ohne Flusskontrolle und ohne Garantie, dass gesendete Pakete den Empfänger erreichen, dass die Reihenfolge erhalten beleibt oder dass Duplikate ankommen. Sie sind zudem im Datenvolumen beschränkt. Datensatzgrenzen bleiben beim Datentransfer allerdings erhalten. Die in der Regel von allen Unix-Systemen unterstützten und heute hauptsächlich verwendeten Kommunikationsdomänen sind die Unix-Domäne für die Prozesskommunikation auf dem lokalen System und die Internet-Domäne für die Prozesskommunikation nach den Internet- Standard-Protokollen. Die innerhalb der Internet-Domäne miteinander kommunizierenden Prozesse können sehr wohl auch auf demselben lokalen System laufen; hierzu bietet jedoch die Verwendung der Unix-Domäne entscheidende Vorteile durch erweiterte protokoll-spezifische Eigenschaften und durch bessere Performance. RAW: Ein Raw Socket stellt eine allgemeine Schnittstelle zu den meist der Transportschicht zugrundeliegenden Kommunikationsprotokollen bereit, welche die Socket-Abstraktion unterstützen. Dieser Socket-Typ ist normalerweise Datagram-orientiert, obwohl die exakte Charakteristik von der Kommunikationssemantik des konkreten Protokolls abhängt. In der Internet-Protokollfamilie kann mit Raw Sockets beispielsweise direkt das Internet Protocol (IP), das ICMP (Internet Control Message Protocol) oder das IGMP (Internet Group Management Protocol) verwendet werden. Socket-Typen: 6.4 Die Socket-Programmierschnittstelle In der Socket-Abstraktion entsprechen die Socket-Typen den für die Anwendung jeweils sichtbaren Kommunikationsverfahren (verbindungslos - verbindungsorientiert) Vorbemerkungen Zusätzlich definiert ein Socket-Typ spezielle Eigenschaften eines Protokolls wie z.b. fehlerfreie Datenübertragung, Flusskontrolle, Einhaltung von Datensatzgrenzen). Letztlich wird über den Socket-Typ die Kommunikationssemantik definiert. Wie bei den Domänen gilt: Prozesskommunikation nur zwischen Sockets vom selben Typ! Ein Ziel bei der Entwicklung der Socket-Schnittstelle war, die bestehenden Systemaufrufe des Unix-I/O-Systems weitestgehend auch für die Netzwerkkommunikation zu nutzen (orthogonale Erweiterung). Aufgrund wesentlicher Unterschiede in der Semantik von File-I/O und Netzwerk- I/O konnte die Abstraktion Everything is a file nicht befriedigend erweitert werden. Als Kompromiss wurden deshalb insgesamt 17 neue Systemaufrufe für die Kommunikation mit Sockets bereitgestellt. In BSD-basierten Systemen sind Sockets vollständig im Betriebssystem realisiert und somit erfolgt der Zugriff auf die Socket-Schnittstelle vollständig über System Calls. In SVR4-basierten Systemen sind Sockets auf der Basis des Streams-Subsystems implementiert. Die Socket-Funktionen sind dabei entweder als Bibliotheks-Funktionen unter Verwendung der Streams-Systemaufrufe oder auch im Systemkern mit einer Systemaufrufschnittstelle realisiert. Für die Anwendungsprogrammierung ist dies in der Regel irrelevant. Sockets werden in gleicher Weise wie Dateien über Deskriptoren realisiert. Viele Systemaufrufe des I/O-Subsystems (wie z.b. read() oder write()) können so auch auf Sockets angewandt werden.

82 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE Überblick/Einordnung 158 KAPITEL 6. BERKELEY SOCKETS Kommunikations-Domäne: Zuverlässige vs. nicht zuverlässige Verbindung: Internet Domain: (bidirektionale) Kommunikation zwischen Rechnern Berkeley TCP/IP oder UDP/IP Adressierung: das 5er-Tupel Sockets API SOCK_STREAM SOCK_DGRAM (Protocol, SendAddr, SendPort, RecvAddr, RecvPort) Unix Domain: (bidirektionale) Kommunikation auf einem (Unix-) Rechner ISO/OSI 4 TCP reliable UDP unreliable = IP + Port + Checksum 3er-Tupel: (Protocol, local-pathname, foreign-pathname) 3 IP (unreliable) / ARP Server Client 2 1 Ethernet Verkabelung / Protokoll Twisted Pair, Glasfaser, Koaxial socket() socket() Abbildung 6.2: Sockets Überblick bind() Server Client Server Client listen() socket() socket() socket() socket() accept() connect() bind() bind() bind() recv() send() listen() send() recv() accept() connect() Abbildung 6.4: Kommunikation in der Unix-Domain read() write() recvfrom() sendto() write() read() sendto() recvfrom() Abbildung 6.3: API-Aufrufe

83 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE Erzeugung eines Socket 160 KAPITEL 6. BERKELEY SOCKETS Benennung eines Socket Die Erzeugung einer Socket-Instanz erfolgt mit der Funktion socket(): # include <sys/types.h> # include <sys/socket.h> int socket( int domain, int type, int protocol ) Mit socket() werden die internen, den Socket repräsentierenden Datenstrukturen allokiert und initialisiert. In diesem Zustand kann der Socket als unbenannter Socket bezeichnet werden. Solange der Socket noch mit keiner Adresse verknüpft ist, kann der Socket noch nicht von fremden Prozessen angesprochen werden und somit auch keine Kommunikation stattfinden. Die Benennung eines Socket erfolgt durch Zuweisung einer Adresse an den Socket: Die zu verwendende Domäne wird über den Parameter domain angegeben. Die zu verwendende Kommunikationssemantik wird über den Parameter type angegeben. Das konkrete Kommunikationsprotokoll kann über den Parameter protocol angegeben werden. Wird hier 0 angegeben (also nichts spezifiziert), so selektiert das System eine geeignetes Protokoll passend zu den ersten beiden Parametern. Rückgabewert ist ein Deskriptor, der den erzeugten Socket referenziert und in allen weiteren darauf operierenden Funktionen benutzt wird. Für den Parameter domain sind in sys/socket.h Konstanten definiert: AF_UNIX (auch als PF_UNIX) für die Unix-Domäne, AF_INET (auch als PF_INET) für die Internet-Domäne. AF steht für address family, PF für protocol family. Für den Parameter type sind in sys/socket.h ebenfalls Konstanten definiert: SOCK_STREAM für den Socket-Typ STREAM, SOCK_DGRAM für DGRAM und SOCK_DGRAM für RAW. Der Rückgabewert 1 signalisiert einen Fehler: EPROTONOSUPPORT, falls das spezifizierte Protokoll nicht unterstützt wird ENOPROTOTYPE, falls der Socket-Typ innerhalb der gewählten Domäne unzulässig ist oder nicht unterstützt wird; weitere Fehler können aufgrund systeminterner Ressourcenknappheit oder mangelnden Zugriffsrechten entstehen. Beispiel: int sd = socket(pf_inet, SOCK_STREAM, 0); Damit wird ein Stream-Socket der Internet-Domäne erzeugt, welches das voreingestellte Transportprotokoll (hier TCP) als das dem Socket zugrundeliegende Kommunikationsprotokoll verwendet. Socket-Adresse in der Internet-Domäne: <protocol,local-address,local-port,foreign-address,foreign-port> Ein Halb-Tupel <protocol,address,port> definiert dabei jeweils einen Kommunikationsendpunkt, foreign bezieht sich auf den zukünftigen Kommunikationspartner. Socket-Adressen in der Unix-Domäne: Hier werden die Kommunikationsverbindungen über Pfadnamen idenitifiziert, also über Tupel der Form <protocol,local-pathname,foreign-pathname> Mit der Funktion bind() wird ein Kommunikationsendpunkt (ein Halbtupel, s.o.) festgelegt: # include <sys/types.h> # include <sys/socket.h> int bind( int sd, /*socket descriptor*/ struct sockaddr * address, /*address*/ int addresslen /*length of address*/ ); Aufgrund der meist unterschiedlichen Adressformate der einzelnen Domänen sind Socket-Adressen als eine Folge von Bytes variabler Länge mittels einer generischen Datenstruktur anzugeben. Alle Funktionen der Socket-Schnittstelle, die Adressen verwenden, referenzieren diese nur über diese generische Datenstruktur, die in sys/socket.h wie folgt definiert ist: struct sockaddr { u_char sa_len; /*total address length */ u_char sa_family; /*address family */ char sa_data[14]; /*protocol-specific address*/ }; Die Komponente sa_len gibt die gesamte Länge der Socket-Adresse in Bytes an.

84 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE 161 sa_family bezeichnet die Socket-Adressfamilie, die dem Adressformat der verwendeten Kommunikationsdomäne entspricht. sa_data enthält die ersten 14 Bytes der Adresse selbst. Damit wird die Länge der tatsächlichen Adresse nicht beschränkt! Anm.: Dieser Punkt ist sehr implementierungsspezifisch und wird mit dem Übergang auf IPv6 geändert werden müssen. Beispiel für die Initialisierung und Zuweisung einer Adresse in der Unix-Domäne (Pfadname /tmp/my_socket): 162 KAPITEL 6. BERKELEY SOCKETS Prinzip: # include <netinet/in.h> int sd; struct sockaddr_in sin_addr; /*defined in <netinet.h>*/ /* create a socket: */ sd = socket(pf_inet,sock_stream,0); /* Initialize the address:... */ /* Bind the name to the socket: */ bind(sd, (struct sockaddr *)&sin_addr,sizeof(sin_addr)); # include <sys/un.h> # include <string.h> int sd; struct sockaddr_un sun_addr; /* Create a socket in the UNIX domain: */ sd = socket(pf_unix,sock_stream,0); /*socket address *defined in <sys/un.h> */ /* Initialize the socket address: */ (void) memset(&sun_addr,0,sizeof(sun_addr)); (void) strcpy(sun_addr.sun_path,"/tmp/my_socket"); sun_addr.sun_family = AF_UNIX; sun_addr.sun_len = (u_char) ( sizeof(sun_addr.sun_len) + sizeof(sun_addr.sun_family) + sizeof(sun_addr.sun_path) + 1); /* bind the address to the socket: */ bind(sd,(struct sockaddr *)&sin_addr, sizeof(sun_addr)); Benennung einer Adresse in der Internet-Domäne: Hier ist aus Rechneradresse und Portnummer eine Netzwerkadresse zu konstruieren Dazu gibt es eine ganze Reihe noch zu besprechender Bibliotheksfunktionen

85 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE Aubau einer Kommunikationsverbindung Der Aufbau einer Kommunikationsverbindung zwischen zwei nicht notwendigerweise verschiedenen Prozessen verläuft in der Regel asymmetrisch, wobei ein Prozess als Client, der andere als Server bezeichnet wird. Der Server stellt normalerweise einen Dienst zur Verfügung, wartet also auf die Inanspruchnahme dieses Dienstes durch einen anderen Prozess, den Client; entsprechend werden die Kommunikationsendpunkte als passiver bzw. aktiver Kommunikationsendpunkt bezeichnet. Server Process Client Process 164 KAPITEL 6. BERKELEY SOCKETS Socket durchzuführen, da es automatisch vom Betriebssystem vorgenommen wird, sofern der Socket zum Zeitpunkt der Verbindungsaufnahme noch unbenannt ist. Anm.: Die Socket-Adresse des Kommunikationspartners wird in der Socket-Terminologie auch als Peer-Adresse bezeichnet. Beispiel: # include <netinet/in.h> socket() socket() int sd; struct sockaddr_in sin_addr; /* for address of the server*/ bind() listen() accept() recv() send() connect() send() recv() sd = socket(pf_inet, SOCK_STREAM,0); /* Initialize the socket address of the server (see below)*/ /*connect to the specified server:) connect(sd, (struct sockaddr *)&sin)addr, sizeof(sin_addr)); Der Client wird durch die Ausführung von connect() solange blockiert, bis entweder die Kommunikationsverbindung vom Server vervollständigt wurde (somit erfolgreich hergestellt wurde) oder bis ein Fehler auftritt. Bevor der Server Verbindungsanforderungen entgegen nehmen kann, muss sein erzeugter (socket()) und benannter (bind()) Socket als passiver Kommunikationsendpunkt gekennzeichnet werden (als Bereitschaft, Verbindungen zu akzeptieren) - dazu dient die Funktion listen(): Abbildung 6.5: Verbindungsorientierte Client-Server-Kommunikation Die Benennung des Server-Socket spezifiziert dabei die eine Hälfte einer möglichen Kommunikationsverbindung; die Vervollständigung wird durch einen Client initiiert, der aktiv die Verbindung zu einem Server anfordert und dabei die bekannte Adresse des Servers spezifiziert und seine eigene (implizit) mitliefert. Dazu dient die Funktion connect(): # include <sys/types.h> # include <sys/socket.h> int connect( int sd, /*client s socket descriptor*/ struct sockaddr * address, /* server s address*/ int adresslen ); Das zweite und dritte Argument ist analog zu den enstprechenden Parametern von bind() auf Server-Seite zu verstehen. Der Server hat kein explizites bind() zur Adresszuweisung an seinen # include <sys/socket.h> int listen( int sd, int backlog ); Der Parameter backlog spezifiziert die Länge einer Warteschlange des passiven Socket, in der Verbindungsanforderungen von Clients solange gehalten werden, bis sie vom Server explizit akzeptiert werden. Dies erfolgt durch die Funktion accept(): # include <sys/types.h> # include <sys/socket.h> int accept( int sd, struct sockaddr * address, int * addresslen );

86 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE 165 Der Parameter sd ist der Deskriptor des benannten, passiven Socket. Die Argumente address und addresslen sind Wert-/Resultat-Parameter: sie müssen mit einer Variablen der domänen-spezifischen Socket-Adresse bzw. deren Länge initialisiert werden. Nach erfolgreichem Funktionsaufruf enthalten sie die Socket-Adresse des Client bzw. deren tatsächliche Länge (also die Peer-Adresse). Ist der Server an der Peer-Adresse nicht interessiert, so ist im Parameter addresslen der Nullzeiger anzugeben, der Parameter address bleibt dabei unberücksichtigt. accept() blockiert, bis eine Verbindungsanforderung eines Clients in der Warteschlange des passiven Sockets zur Verfügung steht. Als Resultat liefert accept() - sofern keine Fehler aufgetreten ist - einen Socket-Deskriptor zurück, der einen neuen Socket referenziert; dieser repräsentiert den Kommunikationsendpunkt für die nun fertige neue Verbindung. 166 KAPITEL 6. BERKELEY SOCKETS Akzeptieren einer Kommunikationsverbindung in der UNIX-Domäne: int sd, /*server socket descriptor*/ conn_sd; /*connection socket desc. */ struct sockaddr_un client_addr; /*client socket address */ int client_len; /*length of client address*/ /* * sd = socket(); bind(sd,...); listen(sd,...); */ client_len = sizeof(client_addr); conn_sd = accept(sd, (struct sockaddr *) &client_addr, &client_len); server process passive socket connect() client process connection socket Akzeptieren einer Kommunikationsverbindung in der Internet-Domäne: int sd, /*server socket descriptor*/ conn_sd; /*connection socket desc. */ struct sockaddr_in client_addr; /*client socket address */ int client_len; /*length of client address*/ /* * sd = socket(); bind(sd,...); listen(sd,...); */ accept() client_len = sizeof(client_addr); conn_sd = accept(sd, (struct sockaddr *) &client_addr, &client_len); connection socket send(), recv() Abbildung 6.6: Aufbau einer verbind.-orient. Client/Server-Kommunikation

87 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE Client-Beispiel: Timeclient für Port Programm 6.2: Timeclient für Port (timcli.c) 1 #include <netdb.h> 2 #include < netinet /in.h> 3 #include < stdio.h> 4 #include < strings.h> 5 #include <sys/ socket.h> 6 #include < unistd.h> 7 #include < stdlib.h> 8 9 #define TPORT int main(int argc, char argv) 12 { struct sockaddr_in addr ; 13 struct hostent hp; 14 int fd ; 15 int nbytes ; 16 char buf [BUFSIZ]; if ( argc!=2) 19 printf ("usage: %s hostname\n", argv[0]), 20 exit (1); if (!( hp = gethostbyname ( argv [1]))) // IP Adresse des Servers holen 23 fprintf ( stderr, "unknown host: %s\n", argv[1]), 24 exit (1); bzero (&addr, sizeof ( addr )); // mit 0en fuellen 27 addr. sin_family = AF_INET; // TCP/IP Verbindung 28 addr. sin_port = htons (TPORT);// Port eintragen, Network Byte Order 29 // Serveraddresse eintragen 30 bcopy(hp >h_addr, &addr. sin_addr, hp >h_length); if (( fd = socket (AF_INET, SOCK_STREAM, 0)) <0) 33 perror ( "socket" ), // Socket erzeugen 34 exit (1); if ( connect ( fd, ( struct sockaddr ) & addr, sizeof (addr )) <0) 37 perror ("connect" ), // an Socketport verbinden 38 exit (1); 39 // vom Socket lesen 40 while (( nbytes = read ( fd, buf, sizeof ( buf ))) >0) 41 printf ("%. s", nbytes, buf ); close ( fd ); 44 exit (0); 45 } 168 KAPITEL 6. BERKELEY SOCKETS Überblick: Gebrauch von TCP-Ports server server client process table tcp ports well known CMD UID PID timserv httpd netscape Unix Operating System liest / schreibt liest / schreibt liest / schreibt Abbildung 6.7: Ports und Prozesse in Unix Wie erfahre ich, welche Prozesse aktuell einen bestimmten Port belegen? lsof list open files (and ports lsof grep TCP grep 4711 ports, only superuser registered ports, some are still free free / dynamic ports

88 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE 169 Beispiel: 170 KAPITEL 6. BERKELEY SOCKETS httpd: netscape liest Webseite netscape: gethostbyname(" // get IP-Addr. server fd = socket(af_inet,sock_stream,0); sin_port = htons(8); sin_family = AF_INET; connect(fd, [to hsot port 80]); write(fd, "GET /HTTP/1.0\m\n", 16); read(fd,buf, sizeof(buf)); thales (client) processes ports netscape temp. conn. temp. network connection ulm.de (server) ports processes const 80 httpd fd = socket(af_inet, SOCK_STREAM,0); sin_port = htons(80); sin_family = AF_INET; bind(fd, [Internetdomain; port 80]); listen(fd, SOMAXCON); // Maximale Anzahl in Queue nfd = accept(fd,...); read(nfd, buf, sizeof(buf)); write(nfd,"<html><body>... ", 586); thales (client) processes ports netscape temp temp. network connection ulm.de (server) ports processes bind 80 const httpd Abbildung 6.9: Server httpd antwortet Abbildung 6.8: netscape als Client httpd (HTTP Dämon, Webserver-Prog.) auf erhält Anfrage von netscape auf thales.mathematik.uni-ulm.de

89 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE 171 struct sockaddr 172 KAPITEL 6. BERKELEY SOCKETS Der Datentransfer zentraler Informationsträger für bind(), accept() und connect() bei AF_INET: struct sockaddr_in bei AF_UNIX: struct sockaddr_un wichtigster Inhalt, wenn Komponente sin_family auf AF_INET gesetzt: sin_port = Portnummer sin_addr = IP-Adresse Server / Client (bei accept) wer braucht was? bind(): braucht Infos für Server (Protokoll + Port) accept(): schreibt Infos über Client (Protokoll + IP-Adresse + Port) connect(): braucht Infos über Server (Protokoll + IP-Adresse + Port) Wie erfahre ich, welcher Rechner von welchem Port aus sich an meinen Server angedockt hat? accept(fd, (struct sockaddr *) & cli_addr,...) accept() schreibt Infos über den Client in die Struktur cli_addr printf("port: %d\n", ntohs(cli_addr.sin_port)); ntohs() konvertiert Network-Byte-Order in Host-Byte-Order printf("host: %s\n", inet_ntoa(cli_addr.sin_addr)); inet_ntoa() konvertiert IP-Adresse in String (dotted decimal form) Nachdem zwischen Client und Server eine Kommunikationsverbindung aufgebaut ist, kann ein Datentransfer stattfinden. Dazu können in gewohnter Weise die System Calls des UNIX-I/O- Systems benutzt werden: read(), readv() (read from multiple buffers), write(), writev() (write into multiple buffers). Die Socket-Schnittstelle stellt 3 weitere Funktionspaare zur Verfügung, die die Semantik der UNIX-I/O-Funktionen um Socket- und protokollspezifische Eigenschaften und Mechanismen erweitern. send(), recv() # include <sys/types.h> # include <sys/socket.h> ssize_t send ( int sd, void * buf, size_t len, int flags); ssize_t recv ( int sd, void * buf, size_t len, int flags); Ist bei beiden Funktion für flags der Wert 0 angegeben, so sind sie identisch zu write() und read(). Die Spezifikation bestimmter Methoden des Datentransfers oder das Versenden / Empfangen von Kontrollinformationen kann durch entsprechende flag-werte aktiviert werden: MSG_OOB Die spezifizierten Daten sollen mit hoher Priorität gesendet bzw. empfangen werden. Solche Daten werden im Kontext von Sockets als out-of-band-daten (OOB-Daten) bezeichnet. Sie werden im Vergleich zu normalen Daten auf einem logisch unabhängigen Kanal gesendet. Bei OOB-Daten kann zudem der übliche Pufferungsmechanismus umgangen werden. Dieser Mechanismus unterliegt allerdings Beschränkungen und ist nur für wenige, verbindungsorientierte Protokolle realisiert. MSG_PEEK Auf der Seite des Empfängers wird hiermit spezifiziert, dass gepufferte Daten nur gelesen, aber nicht konsumiert werden sollen. Der nächste Lesezugriff liefert dieselben Daten noch einmal zutücl. MSG_WAITALL Der Lesezugriff soll solange blockieren, bis die angeforderte Datenmenge insgesamt zur Verfügung steht. MSG_DONTWAIT Ausführung der Operation im nicht-blockierenden Modus. MSG_DONTROUTE Ausgehende Datenpakete werden ohne Berücksichtigung einer Routing- Tabelle mur in das lokal angeschlossene Netzwerk gesendet. Dies ist nur für spezielle Diagnoseprogramme interessant.

90 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE 173 MSG_EOR Die Flagge end-of-record markiert das logische Ende eines Datensatzes; die Daten werden dabei mit zusätzlicher Kontrollinformation versendet. Sie kann aber nur verwendet werden, wenn das zugrundeliegende Kommunikationsprotokoll das Konzept der Datensatz-Übermittlung unterstützt. MSG_EOF Hiermit wird das Ende der Datenübertragung markiert und als Kontrollinformation zusammen mit den angegebenen Daten übertragen. Die für den Datentransfer bereitgestellten Flaggen sind zumeist auf spezielle Kommunikationsprotokolle beschränkt und auch nur definiert, wenn die diese Protokolle auf dem System implementiert sind. Die protokollunabhängigen Flaggen sind MSG_PEEK, MSG_WAITALL und MSG_DONTWAIT; die beiden letzteren stehen nur in neueren Implementierungen zur Verfügung! sendto(), recvfrom() # include <sys/types.h> # include <sys/socket.h> ssize_t sendto ( int sd, void * buf, size_t len, int flags, struct sockaddr * address, int addresslen); ssize_t recvfrom ( int sd, void * buf, size_t len, int flags, struct sockaddr * address, int * addresslen); Diese Funktionen stehen für den Datentransfer mit verbindungslosen Kommunikationsverfahren zur Verfügung. Dabei ist die Angabe der Sender- und Empfänger-Adresse in jedem Datenpaket erforderlich, da keine virtuelle Verbindung zwischen den Partnern besteht. Die Angabe der Adressen (Parameter address und addresslen) sind wie bei den Funktionen connect() bzw. accept() anzugeben. Läßt man diese Parameter weg, so entsprechen diese Funktionen den obigen Funktionen send() und recv(). 174 KAPITEL 6. BERKELEY SOCKETS Terminierung einer Kommunikationsverbindung Das Schliessen und die damit verbundene Freigabe der systeminternen Ressourcen erfolgt mit der Funktion close() auf den entsprechenden Socket-Deskriptor. Bei Prozess-Termination werden die Socket-Verbindungen in gleicher Weise wie die Datei- oder terminal-verbindungen geschlossen. In verbindungsorientierten Kommunikationsprotokollen, die ja einen verlässlichen Datentransfer garantieren, versucht das System beim Schliessen eines Socket für eine gewisse Zeit evt. noch ausstehende, zwischengepufferte Daten zu transferieren. Dies lässt sich durch entsprechende Operationen ändern. Eine Verbindung zwischen zwei Sockets ist voll-duplex; dies bedeutet, dass Sende- und Empfangskanal logisch voneinander unabhängig sind. Mit der Funktion shutdown() lässt sich die Verbindung auch nur in einer Richtung terminieren. Dies wird typischerweise bei verbindungsorientierten Protokollen vom Sender dazu verwendet, dem Empfänger das Ende der Eingabe anzuzeigen. Die Empfängerseite bleibt für den Datentransfer weiterhin geöffnet. Das Schliessen der Empfängerseite bewirkt, dass noch nicht konsumierte, zwischengepufferte Daten wie auch alle zukünftig noch eintreffenden Daten verworfen werden. shutdown() # include <sys/socket.h> int shutdown ( int sd, int how ); Der Wert von how: 0 Leseseite (Empfängerseite) wird geschlossen 1 Schreibseite (Senderseite) wird geschlossen 2 Beide Seiten werden geschlossen Verbindungslose Kommunikation sendmsg(), recvmsg() # include <sys/types.h> # include <sys/socket.h> ssize_t sendmsg ( int sd, struct msghdr * msg, int flags ); ssize_t recvmsg ( int sd, struct msghdr * msg, int flags ); Diese beiden Funktionen stellen die allgemeinste Schnittstelle dar und erweitern die Funktionalität der obigen Funktionen. Mehr dazu siehe z.b. im Manual! In diesem Fall läuft die Kommunikation nach einem symmetrischen Modell, auch wenn ggf. einer der Prozesse die Funktion des Servers, der andere die des Clients einnehmen kann (aber es findet kein Verbindungsaufbau statt). Die Adressen der Kommunikationspartner werden also nicht über eine virtuelle Kommunikationsverbindung festgelegt; in jedem Datenpaket muss stattdessen die Empfängeradresse beigefügt werden. Die Datenpakete werden bei verbindungsloser Kommunikation oft auch als Datagramme und die Kommunikationsendpunkte als Datagram Sockets (Socket-Typ: SOCK_DGRAM) bezeichnet. Soll der Socket mit einer bestimmten lokalen Adresse benannt werden, so muss die Zuweisung der Adresse über die Socket-Funktion bind() vor dem ersten Datentransfer stattfinden; ansonsten erfolgt die benennung des lokalen Socket beim Senden des ersten Datagrams implizit durch das Betriebssystem.

91 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE 175 Versenden von Daten mit gleichzeitiger Spezifikation der Empängeradresse: Funktionen sendto() und sendmsg() Empfang von Daten mit gleichzeitiger Gewinnung der Absenderadresse: Funktionen recvfrom() und recvmsg() Ein Datentransfer zwischen zwei Datagram-Sockets kann nur dann stattfinden, wenn beide Kommunikationsendpunkte explizit über die Funktion bind() oder implizit über die Funktionen sendto() oder sendmsg() benannt sind; ansonsten werden die gesendeten Datagramme verworfen! Da verbindungslose Kommunikation auch unzuverlässige Datenzustellung bedeutet, sollte man sich in Client/Server-Anwendungen die gesendeten Datagramme vom Empfänger bestätigen lassen, sofern auf eine Anfrage keine Daten vom Empfänger erwartet werden. Ist ein kontrollierter und zuverlässiger Datentransport nötig, so müssen dies die Anwendungen in diesem selbst regeln! Prinzip der Kommunikation zwischen zwei Datagram Sockets: 176 KAPITEL 6. BERKELEY SOCKETS Auf Datagram Sockets kann die Funktion connect() angewandt werden; damit wird jedoch keine Kommunikationsverbindung aufgebaut, sondern die dabei spezifizierte Adresse dem Socket als Empfängeradresse zugewiesen, an die alle folgenden Datagramme zu senden sind. Ausserdem werden dann an diesem Socket nur Datagramme empfangen, deren Adresse mit der in connect() angegebenen Adresse übereinstimmt. Damit erübrigt sich die Identifizierung der empfangenen Datagramme. Da hiermit sowohl die Sender- wie Empfängeradresse festgelegt sind, können zum Datentransfer auch die Funktionen send() und recv() bzw. die UNIX-I/O-Systemaufrufe verwendet werden. In den Funktionen sendto() und sendmsg() sollten die Socket-Adressen aus Gründen der Portabilität unspezifiziert bleiben. Die Empfängeradresse kann durch einen erneuten Aufruf von connect() jederzeit geändert werden sowie eine Beziehung durch Angabe einer ungültigen Adresse gelöscht werden Feststellen gebundener Adresse Manchmal ist es notwendig, die lokal oder entfernt gebundene Socket-Adresse allein anhand des Socket Deskriptors festzustellen; dazu dienen die Funktionen getsockname() und getpeername(), die als Resultat die lokale bzw. entfernt gebundene Adresse zurückliefern: server process socket() bind() client process socket() bind() # include <sys/socket.h> int getsockname ( int sd, struct sockaddr * address, int * addresslen ); int getpeername ( int sd, struct sockaddr * address, int * addresslen ); recvfrom() sendto() Die Parameter sd, address, addresslen sind dabei in gleicher Weise wie in der Funktion accept() zu spezifizieren! sendto() recfrom() Abbildung 6.10: Aufbau einer verb.-losen Client/Server-Kommunikation Die Funktion getsockname() ist insbesondere dann nützlich, wenn die lokale Socket-Adresse vom System zugewiesen wurde. Das Feststellen der entfernt gebundenen Adresse mit der Funktion getpeername() ist dann notwendig, wenn ein Prozess diese Adresse benötigt und allein den Socket Deskriptor einer bereits akzeptierten Kommunikationsverbindung erhält und somit keinen Zugriff auf die Peer-Adresse besitzt. Dies ist beispielsweise bei durch den Internet Superserver inetd gestarteten Server-Prozessen der Fall. Der Server-Prozess benennt den erzeugten Datagram Socket mit einer nach aussen hin bekannten Adresse. Die explizite Benennung des Client-Socket mit bind() ist optional und i.d.r. nicht erforderlich, da nur der Server die Adresse des Kommunikationspartners als Empfängeradresse für die zu sendende Rückantwort benötigt! Wichtig ist in diesem Zusammenhang nur, dass die Kommunikationsverbindung innerhalb der gewählten Domäne eindeutig ist; bei der impliziten Benennung eines Socket durch das Betriebssystem ist dies gewährleistet!

92 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE Socket-Funktionen im Überblick Aufbau Server Client socket(): Erzeugen eines unbenannten Socket socketpair(): Erzeugen eines Paares von miteinander verbundenen Sockets, siehe Manual bind(): Zuweisung einer lokalen Adresse an einen unbenannten Socket listen(): Einen Socket auf Verbindungsanforderungen vorbereiten accept(): Eine Verbindungsanforderung akzeptieren connect(): Eine Verbindung zu einem Socket anfordern Empfangen read(): Daten einlesen readv(): Daten in mehrere Puffer einlesen recv(): Daten einlesen und Angabe von Optionen recvfrom(): Daten einlesen, optional Senderadresse empfangen, Angabe von Optionen recvmsg(): Daten in mehrere Puffer einlesen, optional Senderadresse und Kontrollinformationen empfangen, Angabe von Optionen Senden write(): Daten senden writev(): Daten aus mehreren Puffern senden send(): Daten senden und Angabe von Optionen sendto(): Daten an die spezifizierte Empfängeradresse senden, Angabe von Optionen sendmsg(): Daten aus mehreren Puffern senden, und Kontrollinformationen an den spezifizierten Empfäger senden, Angabe von Optionen Ereignisse select(): Multiplexen und auf I/O-Bedingungen warten, siehe Manual 178 KAPITEL 6. BERKELEY SOCKETS Terminieren shutdown(): Eine Verbindung in eine oder beide Richtungen terminieren close(): Eine Verbindung terminieren und Socket schliessen Administration getsockname(): Feststellen der lokal gebundenen Socket-Adresse getpeername(): Feststellen der entfernt gebundenen Socket-Adresse setsockopt(): Ändern von Socket- und Protokoll-Optionen, siehe Manual getsockopt(): Auslesen von Socket- und Protokoll-Optionen, siehe Manual fcntl(): Ändern der I/O-Semantik, siehe Manual ioctl(): Verschiedene Socketoperationen, siehe Manual 6.5 Konstruktion von Adressen Adressen für die Lokalisierung eines Dienstes auf einem nicht notwendig entfernten System sind protokoll-spezifisch und setzen sich in der Internet-Domäne aus einer Rechneradresse und einer den Dienst identifizierenden Portnummer zusammen. Anwendungen spezifizieren den anzufordernden Dienst i.r.g. über einen Namen statt über Rechneradresse plus Portnummer, so z.b. den WWW-Server auf dem Rechner dessen bereitgestellter Dienst mit http bezeichnet wird. Namen lassen sich schliesslich leichter merken als numerische Adressen, man erreicht dadurch auch eine gewisse Unabhängigkeit (Änderung von Adresse und Portnummer unter Beibehaltung des Namens). Für die Konvertierung von Namen in Adressen bzw. Portnummern, für die Konstruktion und Manipulation von Netzwerkadressen sowie für die Lokalisierung eines Rechners gibt es eine Reihe von Bibliotheksfunktionen, die allerdings nicht Bestandteil der Socket-Schnittstelle sind Socket-Adressen Alle Funktionen der Socket-Schnittstelle, die auf Socket-Adressen operieren, verwenden eine generische Socket-Adressstruktur. Damit wird die Unabhängigkeit der Socket-Schnittstelle von den konkreten Implementierungen der bereitgestellten Kommunikationsprotokolle erreicht. Die Interpretation der Socket-Adressen erfolgt in den protokollspezifischen Funktionen, die bei der Erzeugung einer Socket-Instanz durch die Domäne festgelegt sind.

93 6.5. KONSTRUKTION VON ADRESSEN 179 Deklaration der Socket-Adressstruktur in auf 4.3BSD basierenden Betriebssystemen: struct sockaddr { u_short sa_family; /* address family */ char sa_data[14]; /* protocol-specific address */ } Die Komponente sa_family enthält das Adressformat. Die Komponente sa_data maximal die ersten 14 Bytes der protokollspezifischen Adresse. Dies ist ein Implementierungsdetail der Socket-Schnittstelle und beschränkt nicht die Länge der protokollspezifischen Adresse. Die Implementierung von Netzwerkprotokollen stellt mit Blick auf die Performance viele Anforderungen an die Speicherverwaltung des Betriebssystems, so z.b. die Handhabung von Datenpuffern unterschiedlicher Länge, das einfache Hinzufügen / Entfernen von Kopfdaten oder die Datenübergabe zwischen den eigenverantwortlichen Funktionen der verschiedenen Netzwerkschichten. Auf BSD-Systemen ist für die Kommunikation mit Sockets eine spezielle Speicherverwaltung implementiert, die sogenannten memory buffers; dabei wird versucht, Kopieroperationen der Datenpakete zu minimieren. SVR6 basierende Systeme verwenden i.d.r. die Mechanismen des Streams-Subsystems. Die Schnittstellen zwischen den Schichten des Socket-Modells sind zwar wohldefiniert, die Grenzen in der Implementierung sind eher fließend, da die einer Schicht zugrundeliegenden Datenstrukturen oft auch den Funktionen der darüberliegenden Schicht zugänglich sind. So sind das Adressformat und die ersten 14 Bytes der protokollspezifischen in der Socket-Schicht bekannt, sie sind aber auch der Anwendungsschicht bekannt (Abhängigkeit der Anwendung von den konkreten Kommunikationsprotokollen!). Bezüglich der Kompatibilität und Portabilität entsteht eine weitere Abhängigkeit dadurch, dass die generische Socket-Adresse eine Datenstruktur des Betriebssystemkerns ist und dass sich diese Struktur ab der Version 4.3BSD-Reno (und aller darauf aufbauenden Systeme) wie folgt geändert hat: struct sockaddr { u_char sa_len; /* total address length */ u_char sa_family; /* address family */ char sa_data[14]; /* protocol specific address */ } Die Komponente sa_len enthält die Länge der protokollspezifisichen Adresse in Bytes; die Gesamtlänge der Datenstruktur ist unverändert 16 Bytes! Die Hinzunahme dieser Komponente ist für die systeminterne Implementierung notwendig, damit Adressen variabler Länge protokollunabhängig behandelt werden können. Die Anwendung hat davon keinen echten Vorteil, da die Längeninformation Argument der entsprechenden Socket-Funktionen ist, somit bereits verfügbar ist. 180 KAPITEL 6. BERKELEY SOCKETS Socket-Adressen der UNIX-Domäne Hier werden für die Benennung von Sockets UNIX-Pfadnamen benutzt; die folgende Socket- Adressstruktur ist in sys/un.h definiert: struct sockaddr_un { u_char sun_len; /*total address length incl. nullbyte*/ u_char sun_family; /*address family AF_UNIX */ char sun_path[104]; /*path name */ } In der Komponente sun_len ist die Länge der konkreten Socket-Adresse in Bytes anzugeben; diese ergibt sich aus der Länge der beiden Komponenten sun_len und sun_family sowie der Stringlänge des Pfadnamens in sun_path plus 1 (terminierendes Null-Byte!). Die Komponente sun_family bezeichnet die Adressfamilie bzw. das Adressformat und ist stets mit der Konstanten AF_UNIX zu initialisieren. Die Dimension der Komponente sun_path mit 104 Bytes ist systemabhängig; in den meisten UNIX-Systemen ist die maximale Länge eines Pfadnamens auf 1024 Bytes (Konstante MAX_PATH in limits.h) festgelegt. In Socket-Implementierungen, die auf dem Streams System basieren, können Sockets mit Pfadnamen bis zu dieser Länge benannt werden. Werden in Socket-Implementierungen die Adressstrukturen der Version 4.3BSD-Reno verwendet, so ist die maximale Pfadlänge durch den Datentyp u_char der Komponente sun_len auf 256 Bytes beschränkt! Die Socket-Adressen sollten einer sicheren Konvention folgend immer mit Null-Bytes initialisiert werden. Die Verwendung von absoluten Pfadnamen ist in einigen System erforderlich. Initialisierung: struct sockaddr_un addr; (void) memset(&addr, 0, sizeof(addr)); (void) strcpy(addr.sun_path, "/tmp/my_socket"); addr.sun_family = AF_UNIX; # ifdef HAVE_SOCKADDR_SA_LEN addr.sun_len = (u_char) (sizeof(addr.sun_len) + sizeof(addr.sun_family) + strlen(addr.sun_path) +1); # endif Die sehr systemnahe Implementierung von Netzwerkanwendungen mittels der Socket-Schnittstelle hat einige Nachteile, die sich insbesondere auf die Portabilität auswirken. Darauf soll im Folgenden jedoch nicht weiter eingegangen werden (Fragen und Lösungen diesbezüglich wurden in der Dissertation von M. Etter behandelt).

94 6.5. KONSTRUKTION VON ADRESSEN Socket-Adressen in der Internet-Domäne 182 KAPITEL 6. BERKELEY SOCKETS sockaddr{} Hier ist die Socket-Adressstruktur in netinet/in.h wie folgt definiert: len family d a t a struct in_addr { u_long s_addr; /* 32 bit netid/hostid */ }; struct sockaddr_in { u_char sin_len; /*total address length (16 bytes)*/ u_char sin_family; /*address family AF_INET */ u_short sin_port; /*16 bit port number */ struct in_addr sin_addr; /*IP address */ char sin_zero[8];/*unused */ }; Die Datenstruktur in_addr enthält nur die Komponente s_addr, in der eine 32 Bit lange Adresse des Internetprotokolls IP in Netzwerkbyteordnung gespeichert wird. Die Komponente sin_len der Datenstruktur sockaddr_in ist immer sizeof(struct sockaddr_in) = 16 Bytes. Die Adressfamilie in Komponente sin_family ist immer AF_INET. 1 len (16) Byte-Ordnung AF_INET 1 14 Bytes sockaddr_in{} port addr in_addr{} addr.s_addr 4 Bytes zero \0 \0 \0 \0 \0 \0 \0 \0 Abbildung 6.11: Organisation der Adress-Struktur sockaddr_in Die Anordnung von Bytes bei Mehr-Byte-Grössen erfolgt nicht bei allen Computersystemen in der gleichen Reihenfolge. Für die Speicherung einer 2-Byte-Grösse gibt es zwei Möglichkeiten: Die Komponente sin_port ist eine 16 Bit lange Port-Nummer in Netzwerkbyteordnung, die zusammen mit der Internet-Adresse sin_addr einen Kommunikationsendpunkt eindeutig definiert. Die Komponente sin_zero ist aus Gründen der Portabilität mit Null-Bytes zu initialisieren und dient lediglich zur Ausweitung der Datenstruktur auf die Länge der generischen Socket-Adressstruktur sockaddr (siehe Abb. 6.11, S. 182). das niederwertige Byte liegt an der Startadresse (Little-Endian-Anordnung) das höherwertige Byte liegt an der Startadresse (Big-Endian-Anordnung) Bei 4-Byte-Grössen können zusätzlich noch die 2-Byte-Grössen unterschiedlich angeordnet sein! Für die implizite Selektierung einer geeigneten lokalen IP-Adresse oder auch Portnummer durch das System existieren jeweils eine ausgezeichnete IP-Adresse und eine Portnummer mit Sonderbedeutung: die IP-Adresse 0 ( INADDR_ANY) bzw in punktiertem Dezimalformat sowie die Portnummer 0. Bei der Spezifikation der Komponenten sin_addr.s_addr und sin_port mit diesen sog. Wildcards wird die lokale IP-Adresse nach einem erfolgreichen Verbindungsaufbau entsprechend der ein- / ausgehenden Netzwerkschnittstelle automatisch vom System festgelegt und bei der Benennung eines Socket eine frei Portnummer aus dem Bereich der kurzlebigen Portnummern gewählt. Für den Austausch protokollspezifischer Daten werden in den Internet-Protokollen nur 2- und 4-Byte-Integerwerte im Big-Endian-Format verwendet (network byte order), die Bit-Ordnung selbst ist ebenfalls in diesem Format! Die Implementierungen von Netzwerkprotokollen sind somit auf jedem System dafür verantwortlich, dass protokollspezifische Daten in Netzwerkbyteordnung transferiert werden, d.h. die Daten sind von der Byteordnung des Computersystems (host) in die Netzwerkbyteordnung zu transferieren, sofern sich die Anordnungen unterscheiden. Zur Konvertierung von 2-Byte-Grössen (short) und 4-Byte-Grössen (long) gibt es folgende Funktionen (Makros): u_short htons(u_short hostshort); /*host-to-network short*/

95 6.5. KONSTRUKTION VON ADRESSEN 183 u_long htonl(u_long hostlong); /*host-to-network long*/ u_short ntohs(u_short netshort); /*network-to-host short*/ u_long ntohl(u_long netlong); /*network-to-host long*/ Die protokollspezifischen Mehrbytegrössen, die bei Internet-Protokollen zu spezifizieren sind, sind die Internet-Adresse und die Portnummer in sockaddr_in (Komponenten sin_addr.s_addr und sin_prt) Spezifikation von Internet-Adressen Die 32-Bit-IP-Adressen (IPv4) werden meist in der sog. punktiertes Dezimalformat (dotted decimal notation) angebenen; diese entsteht dadurch, dass byte-weise Dezimalzahlen gebildet werden, die durch Punkt getrennt sind (thales.mathematik.uni ulm.de) 184 KAPITEL 6. BERKELEY SOCKETS inet_aton() # include <sys/socket.h> # include <netinet/in.h> # include <arpa/inet.h> int inet_aton ( char * ipaddr; /*dotted decimal notation */ struct in_addr * in_addr; /*result: 32-bit-IP-address*/ ); Rückgabewert ist 1 im Erfolgsfall, 0 sonst! Konvertierung einer 32-Bit-IP-Adresse in dotted-decimal notation: Funktion inet_ntoa() # include <sys/socket.h> # include <netinet/in.h> # include <arpa/inet.h> char * inet_ntoa ( struct in_addr inaddr); Beispiel für eine Anwendung: struct sockaddr_in addr; if(inet_aton(" ", &addr.sin_addr)) (void) printf("ip-address: %s\n", inet_ntoa(addr.sin_addr)); Abbildung 6.12: dotted-decimal notation Jeder Host in einem IP-Netzwerk ist über eine IP-Adresse eindeutig identifiziert. Ist ein Host an mehrere IP-Netze angeschlossen (multi-homed host), so muss dieser für jedes angeschlossene Netz eine IP-Adresse besitzen. Über einen Alias-Mechanismus können einem Host auch mehrere IP-Adressen zugeordnet werden. Zur Manipulation und Konvertierung von IP-Adressen gibt es wieder einige Bibliotheksfunktionen, die im Headerfile arpa/inet.h definiert sind. Umwandlung eines als String in dotted-decimal notation vorliegende IP-Adresse in eine 32- Bit-IP-Adresse: Funktion inet_addr() # include <arpa/inet.h> unsigned long inet_addr ( char * ipaddr); Rückgabewerte ist eine 32-Bit-IP-Adresse oder im Fehlerfall die Konstante INADDR_NONE (0xffffffff), die allerdings einer gültigen Broadcast-Adresse entspricht. Zu beachten ist auch, dass der Rückgabewert unsigned long und nicht struct in_addr. Dies behebt die folgende Funktion:

96 6.5. KONSTRUKTION VON ADRESSEN Hostnamen Die Beziehung zwischen Hostnamen und IP-Adressen werden in der Internet-Domäne jeweils in der Datenstruktur hostent repräsentiert, die als Resultat der Funktionen gethostbyname() und gethostbyaddr() geliefert wird. Diese Funktionen zur Adress-Resolution werden als Resolver bezeichnet. Die Struktur hostent (in netdb.h definiert): struct hostent { char * h_name; /*official name of host */ char ** h_aliases; /*alias list */ int h_addrtype; /*host address type (address family)*/ int h_length; /*length of address */ char ** h_addr_list; /*address list from name server */ } # define h_addr h_addr_list[0] /*address for backward compatibility*/ Diese Datenstruktur beschreibt den offiziellen Namen des Rechners in der Komponente h_name, eine Liste seiner öffentlichen Alias-Namen in h_aliases, den Adress-Typ in h_addrtype, die Länge einer Adresse in h_length und eine Liste der IP-Adressen (in Netzwerkbyteordnung) in h_addr_list. Falls es sich bei dem Rechner um einen multi-homed host handelt oder Alias-Adressen definiert sind, so enthält die Liste der IP-Adressen entsprechend viele Elemente. Aus Kompatibilitätsgründen verweist die Makrodefinition h_addr auf das erste Element dieser Liste. Die Funktionen gethostbyname() und gethostbyaddr(): # include <netdb.h> struct hostent * gethostbyname (char * name); struct hostent * gethostbyaddr ( char * addr, int len, int type ); In der Funktion gethostbyaddr() ist die protokollspezifische Adresse in Netzwerkbyteordnung, deren Länge und der Adress-Typ anzugeben. In der Internet-Domäne sind als Parameter eine IP-Adresse, deren Länge, zu erhalten als sizeof(struct in_addr), und die Konstante AF_INET anzugeben. Die Spezifikation von Hostnamen erfolgt entweder über einfache Namen wie beispielsweise thales oder über absolute Namen wie thales.mathematik.uni-ulm.de.. Ein absoluter Namen wird auch als Fully Qualified Domain Name (FQDN) bezeichnet; diese müssen mit einem Punkt enden, der die Wurzel des hierarchisch geordneten Namensraums bezeichnet. Relative Hostnamen werden abhängig von der administrativen Konfiguration des Systems mit Hilfe der auf dem lokalen System voreingestellten Namensdomäne vervollständigt. In Benutzeranwendungen wird der abschliessende Punkt bei absoluten Namen meist weggelassen. 186 KAPITEL 6. BERKELEY SOCKETS Programm 6.3: Hostnamen ermitteln (hostent.c) 1 / hostent. c : print hostent / 2 3 # include < stdio.h> 4 # include < stdlib.h> 5 # include <netdb.h> 6 # include <sys/ socket.h> 7 # include < netinet /in.h> 8 # include <arpa/inet.h> 9 10 void print_hostent ( char host ) { 11 struct hostent hp; (void) printf ( "%s:\n", host ); 14 if ( ( hp = gethostbyname ( host )) ) { 15 char ptr ; (void) printf (" Offizieller Host Name: %s\n", hp >h_name); for ( ptr = hp >h_aliases ; ptr && ptr ; ptr++) 20 (void) printf (" alias: %s\n", ptr ); if ( hp >h_addrtype == AF_INET) 23 for ( ptr = hp >h_addr_list ; ptr && ptr ; ptr++) 24 (void) printf (" Adresse: %s\n", 25 inet_ntoa ( (( struct in_addr ) ptr ))); 26 } else 27 (void) printf (" > Kein Eintrag!\n"); (void) printf ( " \n"); 30 } int main(int argc, char argv ) { 33 int i ; 34 for ( i = 1 ; i < argc ; i++) 35 print_hostent (argv[ i ]); 36 exit (0); 37 } thales$ gcc -o hostent -Wall -lxnet hostent.c thales$ hostent thales turing thales: Offizieller Host-Name: thales alias: ftp alias: www alias: pop alias: glueck alias: adi Adresse: turing: Offizieller Host-Name: turing alias: loghost

97 6.5. KONSTRUKTION VON ADRESSEN 187 alias: mailhost Adresse: thales$ Die Beziehungen zwischen Hostnamen und Internet-Adressen werden in der verteilten Datenbank des Domain Name System (DNS) verwaltet; die darin enthaltenen Informationen sind über sogenannte Nameserver zugänglich. Alternativ dazu werden Informationen über Hostnamen und Internet-Adressen auf dem lokalen System in der Datei /etc/hosts oder über den Network Information Service (NIS) bereitgestellt. Die unterschiedlichen Möglichkeiten der Adress- Resolution zeigt die folgende Abbildung: 188 KAPITEL 6. BERKELEY SOCKETS Offizieller Host-Name: thales.mathematik.uni-ulm.de Adresse: Lokale Hostnamen und IP-Adressen Besteht bereits eine Verbindung, so können der lokale Hostname und die lokale IP-Adresse mit Hilfe der Socket-Funktion gethostname() und der Resolver-Funktion gethostbyaddr() ermittelt werden. local hosts database NIS application resolver resolver configuration gethostbyxyz() local name server internet Abbildung 6.13: Methoden der Adress-Resolution other name servers Die auf eine Anfrage gelieferten Informationen variieren aufgrund der verschiedenen Zugangsverfahren und auch unterschiedlichen Organisationen der Datenbanken. Die Zugangsverfahren hängen auch von der Implementierung wie der administrativen Konfiguration des Resolvers ab. Die Resolver-Funktionen liefern auf jeden Fall den offiziellen Hostnamen und eine IP-Adresse in der Struktur hostent zurück. Bei Verwendung lokaler Mechanismen liefern einige Systeme jedoch nur einfache und keine absoluten Hostnamen zurück. Die wesentlichen Unterschiede zeigen sich bei Aliasnamen und Aliasadressen. Wird beispielsweise die Host-Tabelle oder NIS verwendet, erhält man genau eine Adresse und alle Aliasnamen. Werden die Informationen über Nameserver angefordert, so erhält man eventuell Aliasadressen und höchstens einen Aliasnamen, sofern es sich bei demi in der Anfrage spezifizierten Hostnamen um einen Aliasnamen handelt; dazu noch einmal obiges Programm: Die Funktion gethostname(): # include <unistd.h> int gethostname(char * name, size_t namelen); Programm 6.4: Hostnamen bestimmen (gethost.c) 1 # include < stdio.h> 2 # include < unistd.h> 3 4 int main () { 5 6 char name[20]; 7 if ( gethostname (name,20) == 0 ) 8 (void) printf ( "Hostname: %s\n", name); 9 return 0; 10 } Die maximale Länge eines Hostnamens ist auf den meisten Systemen über die Konstante MAXHOSTNAMELEN im Headerfile sys/param.h festgelegt. Das Kommando uname: thales$ uname -a SunOS thales 5.9 Generic_ sun4u sparc SUNW,Sun-Fire-V240 thales$ hostent www #using NIS www: Offizieller Host-Name: thales alias: ftp alias: www alias: pop alias: glueck alias: adi Adresse: thales$ hostent #using DNS

98 6.5. KONSTRUKTION VON ADRESSEN 189 Die Funktion uname(): Dazu ist im Headerfile sys/utsname.h folgende Datenstruktur definiert: struct utsname { char sysname[sys_nmln]; /*operating system name */ char nodename[sys_nmln]; /*node name (host name) */ char release[sys_nmln]; /*operating system release level*/ char version[sys_nmln]; /*operating system version level*/ char machine[sys_nmln]; /*hardware type */ } Die Funktion selbst ist int uname( struct utsname * buf); Beispiel: Programm 6.5: Funktion uname() (uname.c) 1 # include < stdio.h> 2 # include <sys/utsname.h> 3 4 int main (){ 5 6 struct utsname buf ; 7 if ( uname(&buf ) >= 0 ) { 8 (void) printf ("Host: %s\nos: %s\nrelease: %s\n", 9 buf. nodename, buf.sysname, buf. release ); 10 (void) printf ("Version: %s\nhardware: %s\n", 11 buf. version, buf.machine ); 12 return 0; 13 } else { return 1; } 14 } 190 KAPITEL 6. BERKELEY SOCKETS Portnummern und Dienste In der Internet-Protokollfamilie werden in den Protokollen der Transportschicht (TCP und UDP) 16 Bit lange Portnummern für die Identifizierung eines Dienstes bereitgestellt. Diese Portnummern werden von der IANA ( Internet Assigned Numbers Authority) in drei Bereiche eingeteilt: well-known ports (0 1023), registered ports ( ) und dynamic and/or private ports ( ). Der aktuelle Stand ist in der Datei ftp://ftp.isi.edu/in-notes/iana/assigments/port-numbers verfügbar. Well-known ports identifizieren bekannte Internet-Dienste. So wird in allen TCP/IP-Implementierungen dem Telnet-Server telnetd die TCP-Portnummer 23 und dem TFTP-Server tftpd (Trivial File Transfer Protocol) die UDP-Portnummer 69 zugewiesen, sofern dieses Anwedungsprotokoll unterstützt wird und die entsprechenden Netzwerkanwendungen auf dem System bereitgestellt sind. Auf UNIX-Systemen werden die Portnummern aus dem Bereich als reservierte Portnummern bezeichnet und können nur von Prozessen mit Superuser-Privilegien zur Benennung von Sockets verwendet werden. Die sog. well-known ports belegen hier die Portnummern und die Portnummern sind für Client-Anwendungen mit Supreuser-Privilegien reserviert, die eine reservierte Portnummer als Bestandteil der Client/Server-Authentifizierung benötigen. Beispiele dafür sind rlogin und rsh. Von der IANA nicht verwaltet werden Dienste, die registrierte Portnummern verwendet (lediglich als Konvention aufgelistet). So sind z.b. die Portnummern für einen X Window Server für beide Protokolle (allerdings derzeit nur TCP verwendet) registriert. Nach Konvention binden X-Server für passive Sockets die Portnummern x, wobei x die Nummer des Displays angibt. Wengleich die registrierten Portnummern frei verfügbar sind zur Benennung eines Socket beliebig genutzt werden können, ist dies keinesfalls zu empfehlen. Über die dynamischen und privaten Portnummern, häufig auch als kurzlebige Portnummern (ephemeral ports) bezeichnet, wird von der IANA nichts ausgesagt. Sie sind für eine implizite Benennung eines Socket durch das Betriebssystem reserviert. Die meisten UNIX-Systeme binden heute noch die Nummern für kurzlebige Portnummern; damit können maximal 3977 Sockets (typischerweise für Client-Anwendungen) zu einem Zeitpunkt implizit benannt sein. Solaris-Betriebssysteme verwenden kurzlebige Portnummern aus dem Bereich Die Beziehungen zwischen Portnummern und den offiziellen Namen der Dienste sowie deren Aliasnamen werden in der Datei /etc/services oder einer entsprechenden NIS-Tabelle definiert. Dazu ist im Headerfile netdb.h folgende Struktur definiert: struct servent { char * s_name; /*official service name */ char ** s_aliases; /*alias list */ int s_port; /*port number (network byte order*/ char * s_proto; /*protocol to use */ }

99 6.5. KONSTRUKTION VON ADRESSEN 191 Die Zugriffsfunktionen: 192 KAPITEL 6. BERKELEY SOCKETS Übersetzung aud Ausführung: getservbyname() # include <netdb.h> struct servent * getservbyname( char * name, char * protocol ); thales$ gcc -Wall -o getserv -lxnet getserv.c thales$ getserv telnet tcp Offizieller Name des Dienstes: telnet Port-#: 23 Protokoll: tcp thales$ getservbyport() # include <netdb.h> struct servent * getservbyport( int port; char * protocol ); Programm 6.6: Port und Protokoll eines Dienstes (getserv.c) 1 # include < stdio.h> 2 # include < stdlib.h> 3 # include <netdb.h> 4 # include < netinet /in.h> 5 6 void print_servent ( char name, char protocol ) { 7 struct servent sp ; 8 9 if ( ( sp = getservbyname (name, protocol )) ) { 10 char ptr ; (void) printf (" Offizieller Name des Dienstes: %s\n", 13 sp >s_name); for ( ptr = sp > s_aliases ; ptr && ptr ; ptr++) 16 (void) printf (" Alias: %s\n", ptr ); (void) printf (" Port #: %d\n", 19 ntohs ( ( u_short ) sp >s_port )); 20 (void) printf (" Protokoll: %s\n", sp >s_proto ); 21 } else 22 (void) printf ("Kein Eintrag!\n" ); 23 } int main(int argc, char argv ) { 26 if ( argc!= 3 ) { 27 fprintf ( stderr, "Usage: %s service protocol\n", argv [0]); 28 exit (1); 29 } else 30 print_servent ( argv [1], argv [2]); 31 exit (0); 32 }

100 194 KAPITEL 7. NETZWERK-PROGRAMMIERUNG Kapitel 7 Netzwerk-Programmierung 7.1 Client/Server Vorbemerkungen In diesem Abschnitt sollen einige der zuletzt dargestellten Konzepte und Funktionen an einer einfachen Client/Server-Implementierung exemplarisch dargestellt werden. Für die Implementierung des Servers gibt es im Prinzip zwei Möglichkeiten: Der Server arbeitet die ankommenden Anforderungen sukzessive ab (iterative server). Der Server arbeitet die ankommenden Anforderungen parallel ab (concurrent server) concurrent server Server forkt, neuer Prozess bearbeitet Anforderung 1 / 2 simple error handling 3 / 4 5 int sockfd, newsockfd ; 6 7 if ( ( sockfd = socket (... ) ) < 0 ) { 8 perror ( "socket error" ); 9 exit (1); 10 } 11 if ( bind ( sockfd,...) < 0 ) { 12 perror ( "bind error" ); 13 exit (1); 14 } Programm 7.1: Concurrent Server (conc-srv.c) 15 if ( listen ( sockfd, 5) < 0 ) { 16 perror ( " listen error" ); 17 exit (1); 18 } while (1) { 21 newsockfd = accept ( sockfd,...); / blockiert / 22 if ( newsockfd < 0 ) { 23 perror ("accept error" ); 24 exit (1); 25 } 26 switch ( pid = fork () ) { 27 case 1: 28 perror ( "fork error" ); 29 exit (1); 30 case 0: 31 close ( sockfd ); 32 / verarbeite Anforderung : / 33 doit ( newsockfd ); 34 exit (0); 35 default : 36 close ( newsockfd ); 37 break; 38 } 39 } iterative server Programm 7.2: Iterative Server (iter-srv.c) 1 / Iterative Server : iter_srv. c 2 simple error handling 3 / 4 5 int sockfd, newsockfd ; 6 7 if ( ( sockfd = socket (... ) ) < 0 ) { 8 perror ( "socket error" ); 9 exit (1); 10 } 11 if ( bind ( sockfd,...) < 0 ) { 12 perror ( "bind error" ); 13 exit (1); 14 } 15 if ( listen ( sockfd, 5) < 0 ) { 16 perror ( " listen error" ); 17 exit (1); 18 } while (1) { 21 newsockfd = accept ( sockfd,...); / blockiert / 22 if ( newsockfd < 0 ) { 193

101 7.2. ECHO-SERVER UND ECHO-CLIENT perror ("accept error" ); 24 exit (1); 25 } 26 / verarbeite Anforderung : / 27 doit ( newsockfd ); 28 close ( newsockfd ); 29 } 7.2 echo-server und echo-client stdin stdout CLIENT line line Abbildung 7.1: echo: Client/Server SERVER Client liest eine Zeile (Folge von Zeichen bis zu einem newline!) von stdin und übergibt diese an den Server Server liest die Zeile über seine Netzwerk-Eingabe und gibt sie über seine Netzwerk-Ausgabe an den Client zurück (echo) Der Client liest die Zeile von der Socket Schnittstelle, gibt sie an stdout aus und terminiert. 7.3 Erste Implementierungen 196 KAPITEL 7. NETZWERK-PROGRAMMIERUNG #define SERV_UDP_PORT #define SERV_TCP_PORT / must not conflict with 19 any other TCP server s port 20 / 21 #define SERV_HOST_ADDR " " 22 / " ": it s thales / / " " / 25 / local host / #define MAXLINE 256 Programm 7.4: Include-Anweisungen für UnixDomain (ux-stream/unix.h) 1 / unix.h 2 3 Definitions for UNIX domain stream and datagram 4 client / server programs 5 / 6 7 #include < stdio.h> 8 #include <unistd. h> 9 #include < strings. h> 10 #include <sys/types.h> 11 #include <sys/ socket.h> 12 #include <sys/un.h> #define UNIXSTR_PATH "./s.unixstr" 15 #define UNIXDG_PATH "./s.unixdg" #define MAXLINE Headerfiles Die wesentlichen Include-Anweisungen für die Inet-Domäne sind im Headerfile inet.h, die für die Unix-Domäne in unix.h zusammengefasst: Programm 7.3: Include-Anweisungen für Inet Domain (tcp0/inet.h) 1 / inet.h 2 Definitions for TCP and UDP client / server programs 3 / 4 5 #include < stdio.h> 6 #include < unistd.h> 7 #include < signal.h> 8 #include < string.h> 9 #include <errno.h> 10 #include <sys/wait. h> 11 #include <sys/types. h> 12 #include <sys/ socket.h> 13 #include < netinet /in.h> 14 #include <arpa/ inet. h>

102 7.3. ERSTE IMPLEMENTIERUNGEN TCP-Verbindung - Concurrent Server Der echo-server wird als concurrent server realisiert. Um Zombie-Prozesse zu vermeiden, wird der bereits früher behandelte signal handler verwendet: Programm 7.5: Headerfile für Signalbehandler (tcp0/sign.h) 1 / sign.h / 2 / Signal Handler / 3 4 #ifdef SIGN_H 5 #include < signal.h> 6 #include <sys/wait. h> 7 #include < unistd.h> 8 9 typedef void ( Sigfunc )( int ); #else #define SIGN_H 14 / avoid multiple includes / 15 #include < signal.h> typedef void ( Sigfunc )( int ); Sigfunc ignoresig ( int ); 20 / ignore interrupt and avoid zombies 21 just for midishell ( parent ) 22 / 23 Sigfunc ignoresig_bg ( int ); 24 / ignore interrupt 25 just for execution of background commands 26 / 27 Sigfunc entrysig ( int ); 28 / restore reaction on interrupt / 29 #endif Programm 7.6: Signalbehandler (tcp0/sign.c) 1 / sign.c / 2 3 #define SIGN_H 4 5 # include < stdio.h> 6 # include "sign.h" 7 8 void shell_handler ( int sig ){ 9 if ( ( sig == SIGCHLD) (sig == SIGCLD)) { 10 int status ; 11 waitpid (0, & status, WNOHANG); 12 } 13 return ; 14 } struct sigaction newact, oldact ; 198 KAPITEL 7. NETZWERK-PROGRAMMIERUNG Sigfunc ignoresig ( int sig ) { 19 static int first = 1; 20 newact. sa_handler = shell_handler ; 21 if ( first ) { 22 first = 0; 23 if ( sigemptyset (&newact.sa_mask ) < 0) 24 return SIG_ERR; 25 newact. sa_flags = 0; 26 newact. sa_flags = SA_RESTART; 27 if ( sigaction ( sig, &newact, & oldact ) < 0) 28 return SIG_ERR; 29 else 30 return oldact. sa_handler ; 31 } else { 32 if ( sigaction ( sig, &newact, NULL) < 0) 33 return SIG_ERR; 34 else 35 return NULL; 36 } 37 } struct sigaction newact_bg, oldact_bg ; Sigfunc ignoresig_bg ( int sig ) { 43 newact_bg. sa_handler = SIG_IGN; 44 if ( sigemptyset (&newact_bg.sa_mask ) < 0) 45 return SIG_ERR; 46 newact_bg. sa_flags = 0; 47 newact_bg. sa_flags = SA_RESTART; 48 if ( sigaction ( sig, &newact_bg, & oldact_bg ) < 0) 49 return SIG_ERR; 50 else 51 return oldact_bg. sa_handler ; 52 } Sigfunc entrysig ( int sig ) { 57 if ( sigaction ( sig, & oldact, NULL) < 0 ) 58 return SIG_ERR; 59 else 60 return NULL; 61 } 1 / server using TCP protocol 2 simple error handling 3 / 4 5 #include < stdlib.h> 6 #include " inet.h" Programm 7.7: TCP Echo Server / INET (tcp0/main-srv.c)

103 7.3. ERSTE IMPLEMENTIERUNGEN #include "sign.h" 8 9 int main () { 10 int sockfd, newsockfd, clilen, childpid, n; 11 struct sockaddr_in cli_addr, serv_addr ; 12 char recvline [MAXLINE]; if ( ( ignoresig (SIGCHLD) == SIG_ERR) ) { 15 perror ("Signal" ); 16 exit (1); 17 } 18 / 19 open a TCP socket Internet stream socket 20 / 21 if ( ( sockfd = socket (AF_INET, SOCK_STREAM,0)) < 0) { 22 perror (" server: can t open stream socket"); 23 exit (1); 24 } / 27 bind our local address so that the client can send us 28 / 29 / bzero (( char ) & serv_addr, sizeof ( serv_addr )); / 30 memset((char ) & serv_addr, 0, sizeof ( serv_addr )); 31 serv_addr. sin_family = AF_INET; serv_addr. sin_addr. s_addr = htonl (INADDR_ANY); 34 / INADR_ANY: tells the system that we ll accept a connection 35 on any Internet interface on the system, if it is multihomed 36 Address to accept any incoming messages ( > in. h ). 37 INADDR_ANY is defined as (( unsigned long int ) 0 x ) 38 / serv_addr. sin_port = htons (SERV_TCP_PORT); if ( bind ( sockfd, ( struct sockaddr )& serv_addr, 43 sizeof ( serv_addr )) < 0) { 44 perror (" server: can t bind local address" ); 45 exit (1); 46 } listen ( sockfd,5); while (1) { 51 / 52 wait for a connection from a client process 53 concurrent server 54 / 55 clilen =sizeof( cli_addr ); 56 newsockfd = accept ( sockfd, ( struct sockaddr ) & cli_addr, 57 & clilen ); if ( newsockfd < 0 ) { 60 if ( errno == EINTR) 200 KAPITEL 7. NETZWERK-PROGRAMMIERUNG 61 continue ; / try again / 62 perror ( "server: accept error" ); 63 exit (1); 64 } if ( ( childpid = fork ()) < 0) { 67 perror ( "server: fork error" ); 68 close ( sockfd ); 69 close ( newsockfd ); 70 exit (1); 71 } 72 else if ( childpid == 0) { 73 close ( sockfd ); 74 if ( ( n=recv( newsockfd, recvline,maxline,0)) < 0) 75 exit (2); 76 if ( send ( newsockfd, recvline,n,0) < n) 77 exit (3); 78 close ( newsockfd ); 79 exit (0); 80 } close ( newsockfd ); / parent / 83 } 84 } Programm 7.8: TCP Echo Client / INET (tcp0/main-cli.c) 1 / client using TCP protocol / 2 3 #include < stdlib.h> 4 #include " inet.h" 5 6 int main (){ 7 int sockfd ; 8 struct sockaddr_in serv_addr ; 9 char sendline [MAXLINE], recvline[maxline]; 10 int n; / 13 fill in the structure " serv_addr " with address 14 of server we want to connect with 15 / / bzero ( ( char ) & serv_addr, sizeof ( serv_addr )); / 18 memset ( ( char ) & serv_addr, 0, sizeof ( serv_addr )); 19 serv_addr. sin_family = AF_INET; 20 serv_addr. sin_addr. s_addr = inet_addr (SERV_HOST_ADDR); 21 serv_addr. sin_port = htons (SERV_TCP_PORT); / 24 open a TCP socket internet stream socket 25 / if ( ( sockfd = socket (AF_INET, SOCK_STREAM,0)) < 0 ) {

104 7.3. ERSTE IMPLEMENTIERUNGEN perror (" client : can t open stream socket"); 29 exit (1); 30 } / 33 connect to the server 34 / if ( connect ( sockfd, ( struct sockaddr ) & serv_addr, 37 sizeof ( serv_addr ) ) < 0) { 38 perror (" client : can t connect to server" ); 39 exit (2); 40 } printf ( "%25s", " Client give input: "); 43 if ( fgets ( sendline,maxline,stdin)!= NULL) { 44 n = strlen ( sendline ); 45 if ( send ( sockfd, sendline, n,0) < n) 46 exit (3); 47 shutdown( sockfd,1); 48 if ( recv ( sockfd, recvline,n,0) < 0) 49 exit (4); 50 recvline [n ] = \0 ; 51 printf ("%25s%s\n","Client got: ", recvline ); 52 } close ( sockfd ); 55 exit (0); 56 } Ausführung: hypatia$ make -f LinMakefile.srv gcc -Wall -c main_srv.c gcc -Wall -c sign.c gcc -Wall -o server main_srv.o sign.o hypatia$ make -f LinMakefile.cli gcc -Wall -c main_cli.c gcc -Wall -o client main_cli.o hypatia$ server & [1] 658 hypatia$ client Client - give input: Eine Eingabezeile Client - got: Eine Eingabezeile hypatia$ ps grep server 658 pts/0 00:00:00 server hypatia$ kill 658 [1]+ Terminated server hypatia$ 202 KAPITEL 7. NETZWERK-PROGRAMMIERUNG UDP-Verbindung - Iterative Server Der Echo-Server: Programm 7.9: UDB Echo Server / INET (udp0/main-srv.c) 1 / server using UDP protocol 2 simple error handling 3 / 4 5 #include < stdlib.h> 6 #include " inet.h" 7 8 int main () { 9 int sockfd, clilen, n; 10 struct sockaddr_in cli_addr, serv_addr ; 11 char recvline [MAXLINE]; / 14 open a UDP socket Internet datagramm socket 15 / 16 if ( ( sockfd = socket (AF_INET, SOCK_DGRAM,0)) < 0) { 17 perror (" server: can t open datagramm socket"); 18 exit (1); 19 } / 22 bind our local address so that the client can send us 23 / 24 bzero (( char ) & serv_addr, sizeof ( serv_addr )); 25 serv_addr. sin_family = AF_INET; serv_addr. sin_addr. s_addr = htonl (INADDR_ANY); 28 / INADR_ANY: tells the system that we ll accept a connection 29 on any Internet interface on the system, if it is multihomed 30 Address to accept any incoming messages ( > in. h ). 31 INADDR_ANY is defined as (( unsigned long int ) 0 x ) 32 / serv_addr. sin_port = htons (SERV_UDP_PORT); if ( bind ( sockfd, ( struct sockaddr )& serv_addr, 37 sizeof ( serv_addr )) < 0) { 38 perror (" server: can t bind local address" ); 39 exit (1); 40 } 41 clilen =sizeof( cli_addr ); while (1) { 44 / 45 wait for a connection from a client process 46 iterative server 47 / 48

105 7.3. ERSTE IMPLEMENTIERUNGEN if ( ( n=recvfrom( sockfd, recvline, MAXLINE,0, 50 ( struct sockaddr )& cli_addr,& clilen )) < 0) { 51 perror (" server recvfrom"); 52 exit (2); 53 } 54 if ( sendto ( sockfd, recvline,n,0, ( struct sockaddr )& cli_addr, 55 sizeof ( cli_addr )) < n ) { 56 perror (" server sendto"); 57 exit (3); 58 } 59 } 60 } Programm 7.10: UDP Echo Client / INET (udp0/main-cli.c) 1 / client using UDP protocol / 2 3 #include < stdlib.h> 4 #include " inet.h" 5 6 int main(int argc, char argv ){ 7 int sockfd ; 8 struct sockaddr_in cli_addr, serv_addr ; 9 char sendline [MAXLINE], recvline[maxline]; 10 int n; / 13 fill in the structure " serv_addr " with address 14 of server we want to connect with 15 / bzero ( ( char ) & serv_addr, sizeof ( serv_addr )); 18 serv_addr. sin_family = AF_INET; 19 serv_addr. sin_addr. s_addr = inet_addr (SERV_HOST_ADDR); 20 serv_addr. sin_port = htons (SERV_UDP_PORT); / 23 open a UDP socket internet datagramm socket 24 / if ( ( sockfd = socket (AF_INET, SOCK_DGRAM,0)) < 0 ) { 27 perror (" client : can t open datagramm socket"); 28 exit (1); 29 } / 32 bind any local address for us 33 / 34 bzero ( ( char ) & cli_addr, sizeof ( cli_addr )); 35 cli_addr. sin_family = AF_INET; 36 cli_addr. sin_addr. s_addr = htonl (INADDR_ANY); 37 cli_addr. sin_port = htons (0); if ( bind ( sockfd, ( struct sockaddr ) & cli_addr, 204 KAPITEL 7. NETZWERK-PROGRAMMIERUNG 40 sizeof ( cli_addr )) < 0 ) { 41 perror (" client : can t bind local address" ); 42 exit (2); 43 } printf ( "%25s", " Client give input: "); 46 if ( fgets ( sendline,maxline,stdin)!= NULL) { 47 n = strlen ( sendline ); 48 if ( sendto ( sockfd, sendline,n,0,( struct sockaddr )& serv_addr, 49 sizeof ( serv_addr )) < n ) { 50 perror (" client sendto"); 51 exit (3); 52 } 53 shutdown( sockfd,1); 54 if (( n=recvfrom( sockfd, recvline, n,0,( struct sockaddr )0, 55 ( int )0)) < 0) { 56 perror (" client recvfrom"); 57 exit (4); 58 } 59 recvline [n ] = \0 ; 60 printf ("%25s%s\n","Client got: ", recvline ); 61 } close ( sockfd ); 64 exit (0); 65 } Ausführung: thales$ make -f makefile.cli gcc -Wall -c main-cli.c gcc -Wall -o client main-cli.o -lsocket -lnsl thales$ make -f makefile.srv gcc -Wall -c main-srv.c gcc -Wall -o server main-srv.o -lsocket -lnsl thales$ server & [1] thales$ client Client - give input: Hallo, Server Client - got: Hallo, Server thales$ ps PID TTY TIME CMD pts/131 0:00 server pts/131 0:00 ps pts/131 0:00 bash thales$ kill thales$ ps PID TTY TIME CMD pts/131 0:00 ps pts/131 0:00 bash [1]+ Terminated server thales$ exit

106 7.3. ERSTE IMPLEMENTIERUNGEN TCP-Verbindung in der UNIX Domain Der Echo-Server: Programm 7.11: TCP Echo Server / Unix (ux-stream/main-srv.c) 1 / server using UNIX domain stream protocol / 2 3 #include < stdlib.h> 4 #include "unix.h" 5 #include "sign.h" int main () { 9 int sockfd, newsockfd, clilen, childpid, servlen,n; 10 struct sockaddr_un cli_addr, serv_addr ; 11 char recvline [MAXLINE]; if ( ( ignoresig (SIGCHLD) == SIG_ERR) ) { 14 perror ("Signal" ); 15 exit (1); 16 } / 19 open a UNIX domain stream socket 20 / 21 if (( sockfd = socket (AF_UNIX,SOCK_STREAM,0)) < 0 ) { 22 perror (" server: can t open a stream socket"); 23 exit (2); 24 } / 27 bind our local address so that the client can send to us 28 / / set all with zeros / 31 bzero ( ( char ) & serv_addr, sizeof ( serv_addr )); serv_addr. sun_family = AF_UNIX; 34 strcpy ( serv_addr. sun_path, UNIXSTR_PATH); / determine length of address : / 37 servlen = strlen ( serv_addr. sun_path ) + sizeof ( serv_addr. sun_family ); if ( bind ( sockfd, ( struct sockaddr ) & serv_addr, servlen ) < 0 ) { 40 perror (" server: can t bind local address" ); 41 exit (3); 42 } listen ( sockfd,5); while (1) { 47 / 48 wait for a connection from a client process 206 KAPITEL 7. NETZWERK-PROGRAMMIERUNG 49 concurrent server 50 / clilen = sizeof ( cli_addr ); 53 newsockfd = accept ( sockfd, ( struct sockaddr ) & cli_addr, 54 & clilen ); 55 if ( newsockfd < 0 ) { 56 perror (" server: accept error" ); 57 exit (4); 58 } if ( ( childpid = fork () ) < 0 ) { 61 perror (" server: can t fork" ); 62 exit (5); 63 } 64 else if ( childpid == 0) { / child / 65 close ( sockfd ); 66 if ( ( n=recv( newsockfd, recvline, MAXLINE,0)) < 0) 67 exit (6); 68 if ( send ( newsockfd, recvline,n,0) < n) 69 exit (7); 70 close ( newsockfd ); 71 exit (0); 72 } 73 / parent : / 74 close ( newsockfd ); 75 } 76 } Der Echo-Client: Programm 7.12: TCP Echo Client / Unix (ux-stream/main-cli.c) 1 / client using Unix domain stream protocol / 2 3 # include < stdlib.h> 4 #include "unix.h" 5 6 int main () { 7 int sockfd, servlen ; 8 struct sockaddr_un serv_addr ; 9 char sendline [MAXLINE], recvline[maxline]; 10 int n; / 13 Fill in the structure " serv_addr " with the 14 address of the server that we want to sent do 15 / 16 bzero ( ( char ) & serv_addr, sizeof ( serv_addr ) ); 17 serv_addr. sun_family = AF_UNIX; 18 strcpy ( serv_addr. sun_path, UNIXSTR_PATH); 19 servlen = strlen ( serv_addr. sun_path ) + sizeof ( serv_addr. sun_family ); / 22 open an Unix domain stream socket

107 7.3. ERSTE IMPLEMENTIERUNGEN / 24 if ( ( sockfd = socket (AF_UNIX, SOCK_STREAM, 0) ) < 0) { 25 perror (" client : can t open stream socket"); 26 exit (1); 27 } / 30 connect to the server 31 / 32 if ( connect ( sockfd, ( struct sockaddr ) & serv_addr, servlen ) < 0) { 33 perror (" client : can t connect to server" ); 34 exit (1); 35 } printf ( "%25s", " Client give input: "); 38 if ( fgets ( sendline,maxline,stdin)!= NULL) { 39 n = strlen ( sendline ); 40 if ( send ( sockfd, sendline, n,0) < n) 41 exit (3); 42 shutdown( sockfd,1); 43 if ( recv ( sockfd, recvline,n,0) < 0) 44 exit (4); 45 recvline [n ] = \0 ; 46 printf ("%25s%s\n","Client got: ", recvline ); 47 } close ( sockfd ); 51 exit (0); 52 } 208 KAPITEL 7. NETZWERK-PROGRAMMIERUNG Ausführung: hypatia$ make -f LinMakefile.sr v gcc -Wall -c main_srv.c gcc -Wall -c sign.c gcc -Wall -o server main_srv.o sign.o hypatia$ make -f LinMakefile.cl i gcc -Wall -c main_cli.c gcc -Wall -o client main_cli.o hypatia$ server & [1] 959 hypatia$ ls -l s.* srwxr-xr-x 1 swg users 0 Apr 30 12:12 s.unixstr hypatia$ client Client - give input: one line Client - got: one line hypatia$ ps grep server 959 pts/4 00:00:00 server hypatia$ kill 959 [1]+ Terminated server hypatia$ server & [1] 966 hypatia$ server: can t bind local address: Address already in use [1]+ Exit 3 server hypatia$ rm s.unixstr hypatia$

108 7.3. ERSTE IMPLEMENTIERUNGEN Modifikation der ersten Implementierung Der Client liest Zeile für Zeile von der Standardeingabe, schickt diese sukzessive an den Server, liest sie wieder und gibt sie markiert wieder an die Standardausgabe. Im folgenden wird nur der geänderte Teil dargestellt: Der Client-Teil: Programm 7.13: Modifikation des TCP INET Client (tcp1/cli.src) 1 while ( fgets ( sendline, MAXLINE,stdin)!= NULL) { 2 n = strlen ( sendline ); 3 if ( send ( sockfd, sendline, n,0) < n) 4 exit (3); 5 if ( recv ( sockfd, recvline,n,0) < 0) 6 exit (4); 7 recvline [n ] = \0 ; 8 printf ("»> %s", recvline ); 9 } Der Server-Teil: Programm 7.14: Modifikation des TCP INET Server (tcp1/srv.src) 1 else if ( childpid == 0) { 2 close ( sockfd ); 3 while ( ( n=recv( newsockfd, recvline,maxline,0)) > 0) { 4 if ( send ( newsockfd, recvline,n,0) < n) 5 exit (3); 6 } 7 close ( newsockfd ); 8 exit (0); 9 } Ausführung: hypatia$ server & [1] 1282 hypatia$ client < cli.src >>> while ( fgets(sendline,maxline,stdin)!= NULL) { >>> n = strlen(sendline); >>> if (send(sockfd, sendline,n,0) < n) >>> exit(3); >>> if(recv(sockfd,recvline,n,0) < 0) >>> exit(4); >>> recvline[n] = \0 ; >>> printf(">>> %s", recvline); >>> } hypatia$ 210 KAPITEL 7. NETZWERK-PROGRAMMIERUNG Anmerkungen Diese Implementierungen sind wenig robust (triviales Fehlerhandling); sie sollten die prinzipelle Anwendung der Socket Funktionen demonstrieren. Lese- und Schreiboperationen auf Stream Sockets können auch weniger als die spezifizierte Anzahl von Bytes als Resultatwert liefern. Dies ist generell bei allen über einen Deskriptor referenzierten Objekten möglich, wenn beispielsweise der zugrundeliegende Systemaufruf durch ein Signal unterbrochen wurde oder der Deskriptor sich im nicht-blockierenden Modus befindet. Dies kann auch dadurch passieren, dass beim Lesen oder Schreiben Ressourcenbeschränkungen verletzt werden oder momentan keine weitere Eingabe zur Verfügung steht. Die exakte Semantik ist von dem konkret referenzierten Objekt abhängig. Bei Stream Sockets ist das Ein- / Ausgabeverhalten vom Erreichen der Socket-Puffergrenzen im Betriebssystemkern abhängig. Die Kommunikationsverbindung ist hier bidirektional und vollduplex; jeder Socket besitzt einen Sendepuffer und einen Empfangspuffer, die beide voneinander unabhängig sind. Die Verläßlichkeit des Datentransfers wie auch die Datenflusskontrolle werden über das TCP-Protokoll und die internen Algorithmen der TCP-Implementierungen mit Hilfe der Sende- und Empfangspuffer der miteinander verbundenen Sockets geregelt. application buffer any size recv() SO_RCVBUF socket receive buffer TCP input TCP output application buffer any size send() SO_SNDBUF socket send buffer Abbildung 7.2: Socket-Pufferung (vereinfacht) application user process kernel Die Dimensionen des Sende- und Empfangpuffers sind systemabhängig voreingestellt und lassen sich über die beiden Socket-Optionen SO_SNDBUF und SO_RCVBUF modifizieren. Die Dimension des Puffers in der Anwendung ist frei wählbar. Die Ausführung der I/O-Funktionen auf Sockets bewirkt letztlich nur, dass die Daten zwischen dem Puffer der Anwendung (im Benutzeradressraum) und dem enstprechenden Socket-Puffer (im Adressraum des Kernels) kopiert werden. Im Fall einer Schreiboperation zeigt die als Resultat gelieferte Anzahl von Bytes nur an, dass diese Anzahl in den Sendepuffer des Socket kopiert wurden und nicht, dass diese Zahl von Bytes den Empfänger erreicht hat. Entsprechend werden beim Lesen die momentan im Empfangspuffer des Socket gehaltenen Daten in den Anwendungspuffer kopiert. Der tatsächliche Datentransfer über den Kommunikationskanal wird von den Ein- / Ausgabefunktionen der TCP-Implementierung asynchron vorgenommen und kann von der Anwendung nicht beeinflusst werden. Das Verhalten der Lese- und Schreibfunktionen ist also von den momentan im Empfangspuffer enthaltenen Daten bzw. dem freien Bereich im Sendepuffer abhängig (ähnlich der Funktionsweise von Pipes). Lesefunktionen liefern (im Erfolgsfall) immer das Minimum aus der angeforderten Datenmenge und den im Empfangspuffer gehaltenen Bytes zurück sofern der Systemaufruf nicht durch ein

109 7.3. ERSTE IMPLEMENTIERUNGEN 211 Signal unterbrochen wurde maximal aber SO_RCVBUF Bytes! Schreibfunktionen versuchen, die spezifizierte Anzahl von Bytes zu senden und blockieren solange, bis die gesamte Datenmenge in den Puffer kopiert wurde. Im Erfolgsfall gilt also, dass die als Resultat gelieferte Anzahl der spezifizierten Anzahl entspricht sofern kein Signal den Systemaufruf unterbricht! Diese Funktionalität kann auch in den Lesefunktionen der Socket-Schnittstelle durch Spezifikation der Flagge MSG_WAITALL erreicht werden. Die Existenz diese Flagge wie auch die Eigenschaft, dass Schreibfunktionen versuchen, die spezifizierte Datenmenge insgesamt zu versenden, sind allerdings von der jeweiligen Implementierung der Socket-Schnittstelle abhängig! In vielen Netzwerkanwendungen ist es oft notwendig, logisch unvollständige Lese- und Schreiboperationen die im Fall eines unterbrochenen Systemaufrufs ja auch keinen Fehler darstellen entsprechend zu behandeln. Dazu können z.b. die folgenden beiden Funktionen verwendet werden: sendn(): Programm 7.15: Schreiben mit sendn() (tcp2/ipc-send.c) 1 # define IPC_SEND_H 2 # include "ipc send.h" 3 4 ssize_t sendn( int fd, char buf, size_t len ) { 5 6 char ptr = buf ; 7 size_t nc; 8 ssize_t n; 9 10 for (nc = len ; nc > 0; ptr += n, nc =n) { 11 send_again : 12 if ( ( n=send(fd, ptr,nc,0)) <= 0) { 13 if ( errno == EINTR) { 14 errno = 0; 15 goto send_again ; 16 } else 17 return ( len = nc )? len : 1; 18 } 19 } 20 return len ; 21 } 212 KAPITEL 7. NETZWERK-PROGRAMMIERUNG recvn(): Programm 7.16: Lesen mit recvn() (tcp2/ipc-recv.c) 1 # define IPC_RECV_H 2 # include "ipc recv.h" 3 4 ssize_t recvn ( int fd, char buf, size_t len ) { 5 6 char ptr = buf ; 7 size_t nc; 8 ssize_t n; 9 int flags = 0; for (nc = len ; nc > 0; ptr += n, nc = n ) { 13 recv_again : 14 if ( ( n = recv ( fd, ptr, nc, flags )) < 0 ) 15 if ( errno == EINTR) { 16 errno = 0; 17 goto recv_again ; 18 } else 19 return ( len = nc )? len : 1; 20 else if ( n == 0 ) { 21 errno = ENOTCONN; 22 return ( len = nc )? len : 0; 23 } 24 if ( buf [n 1] == \n ) return n; 25 } 26 return len ; 27 } Diese beiden Funktionen implementieren inkrementelles Schreiben und Lesen, die die Anwendung jeweils solange blockieren, bis die spezifizierte Datenmenge insgesamt gesendet resp. empfangen wurde; sie berücksichtigen zudem die Möglichkeit, dass Systemaufrufe durch Signale unterbrochen werden können. Die Funktion sendn() ist unabhängig von der Semantik und Implementierung der Socket-Funktion send() realisiert: in einer Schleife werden die noch ausstehenden Daten durch weitere Aufrufe von send() gesendet, falls die als Resultat gelieferte Anzahl von Bytes kleiner als die spezifizierte Datenmenge ist. Die Funktion recvn() ist ähnlich realisiert; unterstützt die Socket-Funktion recv() die Flagge MSG_WAITALL, so wird diese auch zur Performance-Steigerung beim Einlesen der Daten genutzt. Ist inkrementelles Lesen erforderlich, so übernimmt in diesem Fall die Funktion recv() diese Aufgabe selbst und erspart der Anwendung weitere Systemaufrufe. Dennoch ist auch in diesem Fall die Schleifenkonstruktion erforderlich, falls recv() durch ein Signal unterbrochen wird und einen weiteren Aufruf notwendig macht. Der Resultatwert beider Funktionen ist im Erfolgsfall die in der Komponente len angegebene Anzahl Bytes. Dies gilt auch bei der Spezifikation von 0 Bytes. Im Fehlerfall liefert sendn() als Resultat -1, sofern noch keine Daten gesendet wurden, andernfalls die Anzahl der bis zum Auftreten des Fehlers erfolgreich gesendeten Bytes. Entsprechendes Fehlerverhalten gilt auch für recvn() mit der Ausnahme, dass eine ordnungsmäßige Termination der Verbindung durch den Kommunikationspartner als logischer Fehler behandelt wird und den Resultatwert 0 liefert, sofern noch keine Daten empfangen wurden.

110 7.4. VERBESSERTE IMPLEMENTIERUNGEN Verbesserte Implementierungen Zeilenorientierter Echo-Server Wie in den vorigen Beispielen wird ein zeilenorientierter Echo-Server betrachtet; der Client liest also eine Zeile (Folge von Bytes bis newline), schickt diese an den Server und dieser schickt sie wieder zurück. Dazu sollen die beiden Funktionen sendn() und recvn() verwendet werden. Dabei ist aber zu beachten, dass Zeilen deutlich kürzer sein können als die enstprechenden Puffer die Flagge MSG_WAITALL würde hier zu einer Blockade der Kommunikation führen. Die Dateien insgesamt: sign.h und sign.c wie bisher Programm 7.17: Verbesserter Echo Server Hauptprogramm (tcp2/main-srv.c) 1 / server using TCP protocol 2 simple error handling 3 / 4 5 #include < stdlib.h> 6 #include < strings.h> 7 #include " inet.h" 8 #include "sign.h" 9 #include " str echo.h" int main(int argc, char argv ) { int sockfd, newsockfd, clilen, childpid ; 14 struct sockaddr_in cli_addr, serv_addr ; if ( ( ignoresig (SIGCHLD) == SIG_ERR) ) { 17 perror ("Signal" ); 18 exit (1); 19 } 20 / 21 open a TCP socket Internet stream socket 22 / 23 if ( ( sockfd = socket (AF_INET, SOCK_STREAM,0)) < 0) { 24 perror (" server: can t open stream socket"); 25 exit (1); 26 } / 29 bind our local address so that the client can send us 30 / 31 bzero (( char ) & serv_addr, sizeof ( serv_addr )); 32 serv_addr. sin_family = AF_INET; serv_addr. sin_addr. s_addr = htonl (INADDR_ANY); 214 KAPITEL 7. NETZWERK-PROGRAMMIERUNG 35 / INADR_ANY: tells the system that we ll accept a connection 36 on any Internet interface on the system, if it is multihomed 37 Address to accept any incoming messages ( > in. h ). 38 INADDR_ANY is defined as (( unsigned long int ) 0 x ) 39 / serv_addr. sin_port = htons (SERV_TCP_PORT); if ( bind ( sockfd, ( struct sockaddr )& serv_addr, 44 sizeof ( serv_addr )) < 0) { 45 perror (" server: can t bind local address" ); 46 exit (1); 47 } listen ( sockfd,5); while (1) { 52 / 53 wait for a connection from a client process 54 concurrent server 55 / 56 clilen =sizeof( cli_addr ); 57 newsockfd = accept ( sockfd, ( struct sockaddr ) & cli_addr, 58 & clilen ); if ( newsockfd < 0 ) { 61 if ( errno == EINTR) 62 continue ; / try again / 63 perror ( "server: accept error" ); 64 exit (1); 65 } if ( ( childpid = fork ()) < 0) { 68 perror ( "server: fork error" ); 69 close ( sockfd ); 70 close ( newsockfd ); 71 exit (1); 72 } 73 else if ( childpid == 0) { 74 close ( sockfd ); 75 / / 76 str_echo ( newsockfd ); 77 / / exit (0); 80 } close ( newsockfd ); / parent / 83 } 84 }

111 7.4. VERBESSERTE IMPLEMENTIERUNGEN 215 Programm 7.18: Funktionalität des Servers (tcp2/str-echo.c) 1 / function used in connection oriented servers 2 read a stream socket one line at a time, 3 and write each line back to the sender 4 return when the connection is terminated 5 / 6 # define STR_ECHO_H 7 # include < stdlib.h> 8 # include " str echo.h" 9 10 void str_echo ( int sockfd ) { 11 int n; 12 char line [MAXLINE]; while (1) { 15 n = recvn ( sockfd, line,maxline); 16 if ( n == 0) 17 return ; / connection terminated / 18 else if ( n < 0 ) { 19 perror ( "str_echo: readline error" ); 20 exit (3); 21 } 22 / Ausgabe auf stderr des Servers : / if ( sendn( sockfd, line, n )!= n ) { 25 perror ( "str_echo: write error" ); 26 exit (3); 27 } 28 } 29 } Programm 7.19: Hauptprogramm des Client (tcp2/main-cli.c) 1 / client using TCP protocol / 2 3 #include < stdlib.h> 4 #include < strings.h> 5 #include " inet.h" 6 #include " str cli.h" 7 #include " str echo.h" 8 9 int main (){ 10 int sockfd ; 11 struct sockaddr_in serv_addr ; / 14 fill in the structure " serv_addr " with address 15 of server we want to connect with 16 / bzero ( ( char ) & serv_addr, sizeof ( serv_addr )); 19 serv_addr. sin_family = AF_INET; 20 serv_addr. sin_addr. s_addr = inet_addr (SERV_HOST_ADDR); 21 serv_addr. sin_port = htons (SERV_TCP_PORT); 216 KAPITEL 7. NETZWERK-PROGRAMMIERUNG / 24 open a TCP socket internet stream socket 25 / if ( ( sockfd = socket (AF_INET, SOCK_STREAM,0)) < 0 ) { 28 perror (" client : can t open stream socket"); 29 exit (2); 30 } / 33 connect to the server 34 / if ( connect ( sockfd, ( struct sockaddr ) & serv_addr, 37 sizeof ( serv_addr ) ) < 0) { 38 perror (" client : can t connect to server" ); 39 exit (3); 40 } / / 43 str_cli ( stdin, sockfd ); 44 / / close ( sockfd ); 47 printf ( "\n"); 48 exit (0); 49 } Programm 7.20: Funktionalität des Client (tcp2/str-cli.c) 1 / function used by connection oriented clients 2 3 read the contents of the FILE fp, write each line to the 4 stream socket ( to the server process ), then read a line back 5 from the socket and write it to stdout 6 7 return to caller when an EOF is encountered on the input file 8 / 9 10 # define STR_CLI_H 11 # include < stdlib.h> 12 # include < strings. h> 13 # include " str cli.h" void str_cli (FILE fp, int sockfd ) { 16 int n; 17 char sendline [MAXLINE], recvline[maxline+1]; while ( fgets ( sendline, MAXLINE,fp)!= NULL) { 20 n = strlen ( sendline ); 21 if ( sendn( sockfd, sendline,n )!= n ) { 22 perror ( " str_cli : write error on socket"); 23 exit (1);

112 7.4. VERBESSERTE IMPLEMENTIERUNGEN } / 27 now read a line from the socket and 28 write it to stdout 29 / 30 n = recvn ( sockfd, recvline, n ); 31 if ( n < 0) { 32 perror ( " str_cli : readline error" ); 33 exit (1); 34 } 35 recvline [n ] = \0 ; 36 printf ("»> " ); 37 fputs ( recvline, stdout ); } if ( ferror ( fp )) { 42 perror (" str_cli : error reading file " ); 43 exit (1); 44 } 45 } 218 KAPITEL 7. NETZWERK-PROGRAMMIERUNG hypatia$ make -f LinMakefile.srv gcc -Wall -c main_srv.c gcc -Wall -c ipc_send.c gcc -Wall -c ipc_recv.c gcc -Wall -c str_echo.c gcc -Wall -c sign.c gcc -Wall -o server main_srv.o str_echo.o ipc_send.o ipc_recv.o sign.o hypatia$ make -f LinMakefile.cli gcc -Wall -c main_cli.c gcc -Wall -c str_cli.c gcc -Wall -o client main_cli.o str_cli.o ipc_recv.o ipc_send.o hypatia$ server & [1] 1883 hypatia$ client eine Zeile >>> eine Zeile und noch eine Zeile >>> und noch eine Zeile ^d hypatia$ Programm 7.21: Funktion recvn() ohne die Flagge MSG_WAITALL (tcp2/ipc-recv.c) 1 # define IPC_RECV_H 2 # include "ipc recv.h" 3 4 ssize_t recvn ( int fd, char buf, size_t len ) { 5 6 char ptr = buf ; 7 size_t nc; 8 ssize_t n; 9 int flags = 0; for (nc = len ; nc > 0; ptr += n, nc = n ) { 13 recv_again : 14 if ( ( n = recv ( fd, ptr, nc, flags )) < 0 ) 15 if ( errno == EINTR) { 16 errno = 0; 17 goto recv_again ; 18 } else 19 return ( len = nc )? len : 1; 20 else if ( n == 0 ) { 21 errno = ENOTCONN; 22 return ( len = nc )? len : 0; 23 } 24 if ( buf [n 1] == \n ) return n; 25 } 26 return len ; 27 }

113 Anhang 219

114 222 LITERATUR Literatur [Bach86] M. J. Bach: The Design of the UNIX Operating System. Prentice Hall, [Comer84] D. Comer: Operating System Design: The XNIU Approach. Prentice Hall, [Darnell01] P. A. Darnell und P. E. Margolis: C: A Software Engineering Approach. Springer, Dritte Auflage, [Handschuch93] T. Handschuch: SOLARIS 2 für den Systemadministrator. Solaris Galerie, IWT- Verlag, [Herold99] H. Herold: Linux-Unix-Shells. Addison-Wesley, [Kernighan86] B. W. Kernighan und R. Pike: Der UNIX-Werkzeugkasten. Hanser, [Kernighan90] B. W. Kernighan und D. Ritchie: Programmieren in C. Hanser, Zweite Auflage, [Rochkind88] M. Rochkind: UNIX-Programmierung für Fortgeschrittene. Hanser Verlag, [Stevens92] [Tanenbaum87] W. R. Stevens: Advanced Programming in the UNIX Environment. Addison- Wesley, A. S. Tanenbaum: Operating Systems - Design and Implementation. Prentice Hall, jeweils auf aktuellste Ausgabe achten! 221

115 224 ABBILDUNGSVERZEICHNIS Abbildungsverzeichnis 1.1 Prozess-Hierarchie Prozess Zustandsmodell Virtuelle und physische Adressen Prozess-Kontext Ein neuer Prozess und sein Erzeuger Die exec()-familie Information über Kind-Prozess n-damen-problem: das Schachbrett n-damen-problem: der Lösungsbaum n-damen-problem: Zeilen-/Spaltenbedrohung Start einer Anwendung von der Shell Ablaufprinzip der tinysh Datenstruktur zur Wortzerlegung Der Init-Prozess TCP/IP-Schichtenmodell IP Datagramm - grober Aufbau IP Datagram - im Detail TCP/IP Layering Model UDP - Format UDP-Demultiplexing Vereinf. Modell der Implementierung von Sockets unter BSD Sockets Überblick API-Aufrufe Kommunikation in der Unix-Domain Verbindungsorientierte Client-Server-Kommunikation Aufbau einer verbind.-orient. Client/Server-Kommunikation Ports und Prozesse in Unix netscape als Client Server httpd antwortet Aufbau einer verb.-losen Client/Server-Kommunikation Organisation der Adress-Struktur sockaddr_in dotted-decimal notation Methoden der Adress-Resolution echo: Client/Server Socket-Pufferung (vereinfacht) Token-Erkennung Syntax-Analyse der Kommandozeile Ablauf der MidiShell Struktur der MidiShell IPC nur über den Kernel IPC auch über Rechnergrenzen Client-Server-Beispiel Unnamed Pipe - erster Schritt Unnamed Pipe zweiter Schritt Unnamed Pipe dritter Schritt Client-Server mit bidirektionaler Kommunikation ISO-OSI-Referenzmodell TCP/IP Unshielded-Twisted-Pair-Kabel Shielded-Twisted-Pair-Kabel Screened Shielded-Twisted-Pair-Kabel Koaxial-Kabel Ethernet Frame Format Layered System Architecture Internet Architektur Abstraktion der Identifikation Bit IP-Adresse in Version IP-Adresse: dotted decimal form IP-Adressklassen in Version

116 226 BEISPIEL-PROGRAMME Beispiel-Programme 1.1 Prozess-ID abfragen Von stdin lesen Priorität verändern Ein neuer Prozess mit fork() fork() zum zweiten Beenden mit exit() Beenden mit _exit() Beenden mit eigenem Aufräumen Neues Programm ausführen Programm 2.6 ohne fflush() Argumentvektor übergeben Environment ausgeben Umgebung ändern Umgebung ändern bei execl() Auf Kind-Prozess warten Auf Kind-Prozess warten Warten mit waitpid() Ein Prozess wird zum Waisenkind Erzeugen eines Zombie-Prozesses Damen klassisch Rekursion mit Unterprozessen Über 0 aus einer Datei lesen Über 1 in eine Datei schreiben tinysh: readline() Schnittstelle tinysh: readline() Implementierung tinysh: readline() Test tinysh: Datenstruktur für den tokenizer() Schnittstelle tinysh: Datenstruktur für den tokenizer() Implementierung tinysh: tokenizer() Schnittstelle tinysh: tokenizer() Implementierung tinysh: tokenizer() Test tinysh: Hauptprogramm Behandlung eines SIGINT-Signals read-operation mit Zeitlimit Versenden eines Signals an den übergeordneten Prozess Versenden eines Signals an den erzeugten Prozess Verwendung von kill() zur Überprüfung der Existenz eines Prozesses Virtuelles Ballspiel zweier Prozesse Header-File zu Erstes Beispiel zu sigaction() Testprogramm zu Modifikation von Verlust von Signalen Prozesse, auf die der Erzeuger nicht wartet Prozesse, auf die der Erzeuger ohne zu blockieren wartet Prozesse, auf die der Erzeuger ohne zu blockieren wartet Midi-Shell: Grundlegende Vereinbarungen Midi-Shell: Schnittstelle zur Tokenbestimmung Midi-Shell: Schnittstelle zum Signalbehandler Midi-Shell: Schnittstelle zur Kommandoausführung Midi-Shell: Schnittstelle zur Ausgabe des Exitstatus Midi-Shell: main-funktion Start Midi-Shell: Tokenbestimmung Midi-Shell: Kommandoausführung Midi-Shell: Signalbehandler Midi-Shell: Termination Midi-Shell: Testprogramm Midi-Shell: Testprogramm Einfache Kommunikation via Pipe Hauptprogramm zum Client/Server-Beispiel Client Client Server Server Beispiel mit popen(), pclose() /1 auf Pipe Termination? Termination! Pipelining der Shell Schreiben in eine Pipe ohne Leseende Timeserver an Port Timeclient für Port Hostnamen ermitteln Hostnamen bestimmen Funktion uname() Port und Protokoll eines Dienstes Concurrent Server Iterative Server Include-Anweisungen für Inet Domain Include-Anweisungen für UnixDomain Headerfile für Signalbehandler Signalbehandler TCP Echo Server / INET TCP Echo Client / INET UDB Echo Server / INET UDP Echo Client / INET TCP Echo Server / Unix TCP Echo Client / Unix Modifikation des TCP INET Client Modifikation des TCP INET Server Schreiben mit sendn() Lesen mit recvn() Verbesserter Echo Server Hauptprogramm Funktionalität des Servers Hauptprogramm des Client Funktionalität des Client

117 BEISPIEL-PROGRAMME Funktion recvn() ohne die Flagge MSG_WAITALL Index /etc/hosts, 187 /etc/services, 190 _Exit(), 64 _exit(), 20 abort(), 64 accept(), 164, 177 Adress-Resolution, 185, 187 AF_INET, 159 AF_UNIX, 159, 180 alarm(), 65 AN, 142 Anwendungsschicht, 129 API, 151 argv, 23 AS, 142 asynchron, 210 atexit(), 20 Autonome Systeme, 142 backlog, 164 Beendigungsstatus, 3, 4, 33 best-effort delivery, 135, 143 BGP, 141 bidirektional, 210 Big-Endian, 182 bind(), 160, 174, 175, 177 Bitübertragungsschicht, 128 Bootstrapping, 54 BREAK, 60 Bridge, 130 broadcast, 135 Broadcast-Adresse, 183 BSD, 151 child process, 15 Client, 163 Client-Server, 115 close(), 113, 174, 178 concurrent server, 193, 197, 205 connect(), 163, 176, 177 Connectionless, 143 control process, 3 CSMA/CD-Verfahren, 134 ctrl-\, 60 CTRL-c, 61 ctrl-c, 60 ctrl-z, 60 current working directory, 19, 24 Dämon, 3 Datagram, 174 Datagram Socket, 156, Datendarstellungsschicht, 129 Datentyp sig_atomic_, 64 Deadlock, 112 delete, 60 DIN/ISO-OSI, 128 DNS, 187 Domain Name System, 187 dotted decimal notation, 139, 183 dup(), 111 dup2(), 111 dynamic and/or private ports, 190 echo-client, 206 echo-server, 197, 202, 205 effective group ID, 19 effective user ID, 19 effektive group ID, 24 effektive user ID, 24 EINTR, 78 Empfangspuffer, 210 end-of-record, 173 ephemeral ports, 190 errno, 78 Ethernet, 135 exec, 22 execl(), 22 execle(), 22 execlp(), 22 execv(), 22 execve, 22 execvp(), 22 exit(), 3, 4, 20, 32, 33 Exit-Status, 3 exit-status, 33 fcntl(),

118 INDEX INDEX FDDI, 133 fflush(), 25 file locks, 24 file mode creation mask, 19, 24 fork(), 1, 15, 112, 113, 193 FQDN, 185 fread(), 46 Fully Qualified Domain Name, 185 GAN, 127 Gateway, 130 gethostbyaddr(), 185, 188 gethostbyname(), 185 gethostname(), 188 getpeername(), 176, 178 getpgrp(), 2 getpid(), 2 getppid(), 2 getservbyname(), 191 getservbyport(), 191 getsockname(), 176, 178 getsockopt(), 178 global area network, 127 HANGUP, 60 hangup-signal, 3 Host, 183 host interface, 135 hostent, 185 Hostname, 188 Hostnamen, 185, 187 htonl(), 182 htons(), 182 Hub, 130 IANA, 190 ICMP, 156 IGMP, 156 IGRP, 141 INADDR_ANY, 181 INADDR_NONE, 183 inet_addr(), 183 inet_aton(), 184 inet_ntoa(), 171, 184 inetd, 176 init-prozess, 33, 55, 56 Inter-Process Communication, 109 Internet Assigned Numbers Authority, 190 Internet Datagram, 144 Internet Domain, 155 Internet Protocol, 143, 156 Internet Superserver, 176 Internet-Adressen, 138, 183, 187 ioctl(), 178 IP, 143, 156 IP-Adresse, 138, 188 IP-Adresse 0, 181 IPC, 1, 4, 109, 110 iterative server, 193, 194, 202 Job, 7 Kabel Twisted-Pair, 131 Kernel Mode, 9 kill, 2, 5 kill(), 59, 60, 67 Kommando, 60 kill, 5 nice, 7 nohup, 7 top, 5 Kommunikationsdomäne, 151 Kommunikationsendpunkt, 151 Kommunikationssemantik, 155 Kommunikationssteuerungsschicht, 129 Kontext, 1 kurzlebige Portnummern, 181, 190 LAN, 127 listen(), 164, 177 Little-Endian, 182 loader, 55 local area network, 127 lsof, 168 MAC-Adresse, 133, 135 MAX_PATH, 180 MAXHOSTNAMELEN, 188 memory buffers, 179 MMU, 10 MSG_WAITALL, multi-homed host, 183 Nameserver, 187 NAT, 141 netdb.h, 190 Netwerktopologie, 127 network byte order, 182 Network Information Service, 187 Netzmaske, 140 nice, 7 nice(), 7 NIS, 187 ntohl(), 183 ntohs(), 171, 183 nuhup, 7 OSI, 128 OSPF, 141 out-of-band-data, 172 parent process, 2, 15 parent process ID, 24 PATH, 23, 119 pause(), 70, 72 PC, 1 pclose(), 119 Peer-Adresse, 164 PF_INET, 159 PF_UNIX, 159 PID, 2 pipe(), 111, 113 pipefd[0], 111 pipefd[1], 111 poll(), 66 popen(), 119 Port, 147 Port-Nummer, 181 Port-Nummern, 149 Portnummer, 178, 190 Portnummer 0, 181 process group ID, 19, 24 process group leader, 2 process ID, 24 program counter, 1 Protokolle, 127 Prozess, 1, 54 Prozessgruppe, 2 Prozessstatus, 5 Prozesstabelle, 12 ps-kommando, 5 Pseudo-Terminal, 61 punktiertes Dezimalformat, 139, 183 raise(), 67 Raw Socket, 156 read(), 112, 172, 177 readv(), 172, 177 real group ID, 19, 24 real user ID, 19, 24 Rechneradresse, 178 recv(), 172, 176, 177, 212 recvfrom(), 173, 175, 177 recvmsg(), 173, 175, 177 recvn(), 212, 213 regions, 11 registered ports, 190 Repeater, 130 reservierte Portnummern (UNIX), 190 Resolver, 185 RIP, 141 rlogin, 190 root directory, 19, 24 Router, 130 Routing, 138, 141 rsh, 190 sa_data, 179 sa_family, 179 sa_len, 179 select(), 66, 177 send(), 172, 176, 177, 212 Sendepuffer, 210 sendmsg(), 173, sendn(), sendto(), 173, Server, 163 set group ID, 24 set user Id, 24 setpgrp(), 2 setsockopt(), 178 Shell-Variable?, 4 $, 2 shutdown(), 174, 178 Sicherungsschicht, 128 sig_atomic_t, 64 SIG_ERR, 62 sigaction(), 73, 78, 79 sigaddset(), 76 SIGALRM, 65 SIGCHLD, 33, 82 SIGCLD, 33 SIGCONT, 60 sigemptyset(), 74 SIGFPE, 60 sighold(), 79, 80 SIGHUP, 7, 33, 69 SIGINT, 60 SIGKILL, 5, 61 Signal 9, 5 anhangiges, 78 SIGHUP, 7 SIGKILL, 5 SIGSTOP, 5 SIGTTIN, 6 signal handler, 61, 197 signal handling settings, 19 signal(), 33, 61, 63, 64 Signale, 24 SIGPIPE, 60 SIGQUIT, 60 sigrelse(), 79, 80 SIGSEGV, 60 SIGSTOP, 5, 60, 61

119 INDEX 231 SIGTERM, 69 SIGTTIN, 6 SIGUSR1, 69 SIGUSR2, 69 SO_RCVBUF, 210 SO_SNDBUF, 210 SOCK_DGRAM, 159, 174 SOCK_RAW, 159 SOCK_STREAM, 159 sockaddr, 171 Socket Adresse, 160 socket(), 159, 177 Socket-Typen, 155 socketpair(), 177 Sockets, 151 SOMAXCON, 170 Speicherklasse volatile, 63 stralloc, 44 Stream Socket, 156, 210 struct in_addr, 181 struct sockaddr, 160, 179 struct sockaddr_in, 162, 181 struct sockaddr_un, 161, 180 sun_family, 180 sun_len, 180 sun_path, 180 swapper, 55 Switch, 130 sys/param.h, 188 sys/utsname.h, 189 Unreliable Delivery, 143 User Mode, 9 verbindungslos, 155, 174 verbindungsorientiert, 155 Vererbung, 4, 19, 24 Vermittlungsschicht, 128 volatile, 63 voll-duplex, 174, 210 wait(), 4, 29, 30, 32 waitpid(), 29, 32, 86 WAN, 127 well-known ports, 190 well-known ports (UNIX), 190 WEXITSTATUS(status), 30 wide area network, 127 WIFEXITED(status), 30 WIFSIGNALED(status), 30 wireless LAN, 127 WLAN, 127, 133 WNOHANG, 31, 86 write(), 112, 172, 177 writev(), 172, 177 WTERMSIG(status), 30 WUNTRACED, 32 X Window Server, 190 xterm, 61 Zombie, 5, 20, 33, 197 TCP, 159, 190, 205 TCP/IP, 129, 143 Telnet-Server, 190 telnetd, 190 terminal group, 2 terminal group ID, 19, 24 TFTP-Server, 190 tftpd, 190 Thread, 1 Token-Verfahren, 134 top, 5 Transceiver, 135 Transportschicht, 129, 190 Trivial File Transfer Protocol, 190 Twisted-Pair, 131 u area, 11, 12 UDP, 190, 202 uname(), 189 uname-kommando, 188 UNIX domain, 155, 205 unnamed pipes, 111

Systemnahe Software (Systemnahe Software II)

Systemnahe Software (Systemnahe Software II) Systemnahe Software (Systemnahe Software II) F. Schweiggert, A. Borchert, M. Grabert und J. Mayer 22. Mai 2006 Fakultät Mathematik u. Wirtschaftswissenschaften Abteilung Angewandte Informationsverarbeitung

Mehr

Systemnahe Programmierung in C Übungen Jürgen Kleinöder, Michael Stilkerich Universität Erlangen-Nürnberg Informatik 4, 2011 U7.fm

Systemnahe Programmierung in C Übungen Jürgen Kleinöder, Michael Stilkerich Universität Erlangen-Nürnberg Informatik 4, 2011 U7.fm U7 POSIX-Prozesse U7 POSIX-Prozesse Prozesse POSIX-Prozess-Systemfunktionen Aufgabe 7 U7.1 U7-1 Prozesse: Überblick U7-1 Prozesse: Überblick Prozesse sind eine Ausführumgebung für Programme haben eine

Mehr

Was ist ein Prozess?

Was ist ein Prozess? Prozesse unter UNIX Definition Was ist ein Prozess? Zeitliche Abfolge von Aktionen Ein Programm, das ausgeführt wird Prozesshierachie Baumstruktur INIT-Prozess ist die Wurzel (pid=1) und wird beim Booten

Mehr

U7 POSIX-Prozesse U7 POSIX-Prozesse

U7 POSIX-Prozesse U7 POSIX-Prozesse U7 POSIX-Prozesse U7 POSIX-Prozesse Prozesse POSIX-Prozess-Systemfunktionen Aufgabe 6 U7.1 U7-1 Prozesse: Überblick U7-1 Prozesse: Überblick Prozesse sind eine Ausführumgebung für Programme haben eine

Mehr

Linux Prinzipien und Programmierung

Linux Prinzipien und Programmierung Linux Prinzipien und Programmierung Dr. Klaus Höppner Hochschule Darmstadt Wintersemester 2010/2011 1 / 18 2 / 18 fork und Daten Nach dem fork teilen sich Eltern- und Kindprozess zwar den Programmbereich

Mehr

G 5. Übung. G-1 Überblick. Besprechung 3. Aufgabe. Infos zur Aufgabe 5: fork, exec. Rechenzeiterfassung. Ü SoS I G.1

G 5. Übung. G-1 Überblick. Besprechung 3. Aufgabe. Infos zur Aufgabe 5: fork, exec. Rechenzeiterfassung. Ü SoS I G.1 G 5. Übung G 5. Übung G-1 Überblick Besprechung 3. Aufgabe Infos zur Aufgabe 5: fork, exec Rechenzeiterfassung G.1 G-2 Hinweise zur 5. Aufgabe G-2 Hinweise zur 5. Aufgabe Prozesse fork, exec exit wait

Mehr

Eine Mini-Shell als Literate Program

Eine Mini-Shell als Literate Program Eine Mini-Shell als Literate Program Hans-Georg Eßer 16.10.2013 Inhaltsverzeichnis 1 Eine Mini-Shell 1 1.1 Einen Befehl parsen......................... 2 1.2 Was tun mit dem Kommando?...................

Mehr

PROGRAMMIEREN MIT UNIX/LINUX-SYSTEMAUFRUFEN

PROGRAMMIEREN MIT UNIX/LINUX-SYSTEMAUFRUFEN PROGRAMMIEREN MIT UNIX/LINUX-SYSTEMAUFRUFEN 2. UNIX/Linux-Prozessverwaltung und zugehörige Systemaufrufe Wintersemester 2016/17 2. Die UNIX/LINUX-Prozessverwaltung Aufgaben: 1. Erzeugen neuer Prozesse

Mehr

Klausur Betriebssysteme I

Klausur Betriebssysteme I Prof. Dr. Michael Jäger FB MNI Klausur Betriebssysteme I 18.3.2011 Bitte bearbeiten Sie die Aufgaben auf den Aufgabenblättern. Die Benutzung von Unterlagen oder Hilfsmitteln ist nicht erlaubt. Die Bearbeitungszeit

Mehr

Dämon-Prozesse ( deamon )

Dämon-Prozesse ( deamon ) Prozesse unter UNIX - Prozessarten Interaktive Prozesse Shell-Prozesse arbeiten mit stdin ( Tastatur ) und stdout ( Bildschirm ) Dämon-Prozesse ( deamon ) arbeiten im Hintergrund ohne stdin und stdout

Mehr

Systemprogrammierung

Systemprogrammierung Systemprogrammierung 3Vom C-Programm zum laufenden Prozess 6. November 2008 Jürgen Kleinöder Universität Erlangen-Nürnberg Informatik 4, 2008 SS 2006 SOS 1 (03-Pro.fm 2008-11-06 08.52) 3 Vom C-Programm

Mehr

2Binden 3. und Bibliotheken

2Binden 3. und Bibliotheken 3 Vom C-Programm zum laufenden Prozess 3.1 Übersetzen - Objektmodule 1Übersetzen 3. - Objektmodule (2) Teil III 3Vom C-Programm zum laufenden Prozess 2. Schritt: Compilieren übersetzt C-Code in Assembler

Mehr

Netzwerk-Programmierung. Prozesse. Alexander Sczyrba Michael Beckstette.

Netzwerk-Programmierung. Prozesse. Alexander Sczyrba Michael Beckstette. Netzwerk-Programmierung Prozesse Alexander Sczyrba Michael Beckstette {asczyrba,[email protected] 1 Übersicht Prozesse fork() Parents und Childs system() und exec() 2 Prozesse moderne Betriebssysteme

Mehr

Prozesse, Logs und Systemverwaltung

Prozesse, Logs und Systemverwaltung Prozesse, Logs und Systemverwaltung Linux-Kurs der Unix-AG Zinching Dang 31. Januar 2018 Übersicht Wiederholung & Vertiefung: Benutzer & Gruppen Prozesse Log-Dateien Befehle & Optionen Zusammenfassung

Mehr

Tafelübung zu BS 1. Prozesse, Shell

Tafelübung zu BS 1. Prozesse, Shell Tafelübung zu BS 1. Prozesse, Shell Olaf Spinczyk Arbeitsgruppe Eingebettete Systemsoftware Lehrstuhl für Informatik 12 TU Dortmund [email protected] http://ess.cs.uni-dortmund.de/~os/ http://ess.cs.tu-dortmund.de/de/teaching/ss2012/bs/

Mehr

fork () Hans-Georg Eßer, Hochschule München Betriebssysteme I, SS 2008 2. Prozesse (2/2) Folie 4

fork () Hans-Georg Eßer, Hochschule München Betriebssysteme I, SS 2008 2. Prozesse (2/2) Folie 4 Sep 19 14:20:18 amd64 sshd[20494]: Accepted rsa for esser from ::ffff:87.234.201.207 port 61557 Sep 19 14:27:41 amd64 syslog-ng[7653]: SAS: dropped 0 Sep 20 01:00:01 amd64 /usr/sbin/cron[29278]: (root)

Mehr

3. Unix Prozesse. Betriebssysteme Harald Kosch Seite 57

3. Unix Prozesse. Betriebssysteme Harald Kosch Seite 57 3. Unix Prozesse Ein Prozeß ist die Umgebung eines laufenden Programms. Ein bißchen Analogie. Wer kocht gerne? Papa möchte mit Hilfe eines Rezeptes eine Torte für seine Tochter backen. Das Rezept ist das

Mehr

Threads. Foliensatz 8: Threads Folie 1. Hans-Georg Eßer, TH Nürnberg Systemprogrammierung, Sommersemester 2015

Threads. Foliensatz 8: Threads Folie 1. Hans-Georg Eßer, TH Nürnberg Systemprogrammierung, Sommersemester 2015 Sep 19 14:20:18 amd64 sshd[20494]: Accepted rsa for esser from ::ffff:87.234.201.207 port 61557 Sep 19 14:27:41 amd64 syslog-ng[7653]: STATS: dropped 0 Sep 20 01:00:01 amd64 /usr/sbin/cron[29278]: (root)

Mehr

Prozesse. Netzwerk - Programmierung. Alexander Sczyrba Madis Rumming

Prozesse. Netzwerk - Programmierung. Alexander Sczyrba Madis Rumming Netzwerk - Programmierung Prozesse Alexander Sczyrba [email protected] Madis Rumming [email protected] Übersicht Prozesse fork() Parents und Children system() und exec()

Mehr

Prozesse, Logs und Systemverwaltung

Prozesse, Logs und Systemverwaltung Prozesse, Logs und Systemverwaltung Linux-Kurs der Unix-AG Zinching Dang 31. Januar 2017 Übersicht Wiederholung & Vertiefung: Benutzer & Gruppen Prozesse Log-Dateien Befehle & Optionen Zusammenfassung

Mehr

Betriebssysteme: UNIX-Operationen zur Prozesskontrolle

Betriebssysteme: UNIX-Operationen zur Prozesskontrolle Betriebssysteme: UNIX-Operationen zur Prozesskontrolle Betriebssysteme: UNIX-Operationen zur Prozesskontrolle WS 2016/17 8. November 2016 1/1 Prozesse und Programme Programm Verschiedene Repräsentationen

Mehr

Threads. Netzwerk - Programmierung. Alexander Sczyrba Jan Krüger

Threads. Netzwerk - Programmierung. Alexander Sczyrba Jan Krüger Netzwerk - Programmierung Threads Alexander Sczyrba [email protected] Jan Krüger [email protected] Übersicht Probleme mit fork Threads Perl threads API Shared Data Mutexes

Mehr

Systeme I: Betriebssysteme Kapitel 4 Prozesse. Wolfram Burgard

Systeme I: Betriebssysteme Kapitel 4 Prozesse. Wolfram Burgard Systeme I: Betriebssysteme Kapitel 4 Prozesse Wolfram Burgard Version 18.11.2015 1 Inhalt Vorlesung Aufbau einfacher Rechner Überblick: Aufgabe, Historische Entwicklung, unterschiedliche Arten von Betriebssystemen

Mehr

Linker: Adreßräume verknüpfen. Informationen über einen Prozeß. Prozeß-Erzeugung: Verwandtschaft

Linker: Adreßräume verknüpfen. Informationen über einen Prozeß. Prozeß-Erzeugung: Verwandtschaft Prozeß: drei häufigste Zustände Prozeß: anatomische Betrachtung jeder Prozeß verfügt über seinen eigenen Adreßraum Sourcecode enthält Anweisungen und Variablen Compiler überträgt in Assembler bzw. Binärcode

Mehr

Klausurdeckblatt. Name: Studiengang: Matrikelnummer:

Klausurdeckblatt. Name: Studiengang: Matrikelnummer: Klausurdeckblatt Name der Prüfung: Systemnahe Software II Datum und Uhrzeit: 21. Juli 2016, 10-12 Uhr Prüfer: Dr. Andreas F. Borchert Bearbeitungszeit: 120 Min. Institut: Numerische Mathematik Vom Prüfungsteilnehmer

Mehr

Prozesszustände (1a)

Prozesszustände (1a) Prozesszustände (1a) NOT EXISTING DELETED CREATED Meta-Zustand (Theoretische Bedeutung) Prozesszustände Multiuser Umfeld (1c) Hintergrund-Prozess - der Prozess startet im Hintergrund - my-commandbin &

Mehr

Aufgabenblatt 5 Musterlösung

Aufgabenblatt 5 Musterlösung Prof. Dr. rer. nat. Roland Wismüller Aufgabenblatt 5 Musterlösung Vorlesung Betriebssysteme I Wintersemester 2017/18 Aufgabe 1: Implementierung von Threads (Bearbeitung zu Hause) Der größte Vorteil ist

Mehr

Programmiertechnik. Teil 4. C++ Funktionen: Prototypen Overloading Parameter. C++ Funktionen: Eigenschaften

Programmiertechnik. Teil 4. C++ Funktionen: Prototypen Overloading Parameter. C++ Funktionen: Eigenschaften Programmiertechnik Teil 4 C++ Funktionen: Prototypen Overloading Parameter C++ Funktionen: Eigenschaften Funktionen (Unterprogramme, Prozeduren) fassen Folgen von Anweisungen zusammen, die immer wieder

Mehr

Klausur Betriebssysteme

Klausur Betriebssysteme Prof. Dr. Michael Jäger FB MNI Klausur Betriebssysteme 5.2.2016 Die Dauer der Klausur beträgt 90 Minuten. Es sind keine Unterlagen und Hilfsmittel erlaubt. Bitte bearbeiten Sie die Aufgaben soweit wie

Mehr

Prozesse and Threads WS 09/10 IAIK 1

Prozesse and Threads WS 09/10 IAIK 1 Prozesse and Threads WS 09/10 IAIK 1 Prozesse Programm in Ausführung Mit einem Prozess verbunden: Adressraum Folge von Speicherstellen auf die der Prozess zugreifen kann Enthält ausführbares Programm,

Mehr

8.3 Sonstiges/Signalbehandlung

8.3 Sonstiges/Signalbehandlung 8. Sonstiges/Signalbehandlung 8..1 Was sind Signale? Signale bilden in der Sprache C die Funktionalität ab, die in Maschinensprache (bzw. Assembler) von Interrupts bekannt ist: Ein Prozess kann zu beliebiger

Mehr

Das Signalkonzept (T) Signale und Signalbehandlung (P)

Das Signalkonzept (T) Signale und Signalbehandlung (P) Systempraktikum im Wintersemester 2009/2010 (LMU): Vorlesung vom 10.12. Foliensatz 6 Das Signalkonzept (T) (P) Thomas Schaaf, Nils gentschen Felde Lehr- und Forschungseinheit für Kommunikationssysteme

Mehr

Zusammenfassung für CS-Prüfung 3 Seite 1. CS-Zusammenfassung für Prüfung 3 vom Im Beispiel gibt es 3 Deadlocks

Zusammenfassung für CS-Prüfung 3 Seite 1. CS-Zusammenfassung für Prüfung 3 vom Im Beispiel gibt es 3 Deadlocks Zusammenfassung für CS-Prüfung 3 Seite 1 CS-Zusammenfassung für Prüfung 3 vom 24. 6. 2002 Deadlock Im Beispiel gibt es 3 Deadlocks Unterschied zwischen Blockieren, Verklemmen und Verhungenrn= -> Band 1

Mehr

PROGRAMMIEREN MIT UNIX/LINUX-SYSTEMAUFRUFEN

PROGRAMMIEREN MIT UNIX/LINUX-SYSTEMAUFRUFEN PROGRAMMIEREN MIT UNIX/LINUX-SYSTEMAUFRUFEN 2. UNIX/Linux-Prozessverwaltung und zugehörige Systemaufrufe Wintersemester 2015/16 2. Die UNIX/LINUX-Prozessverwaltung Aufgaben: 1. Erzeugen neuer Prozesse

Mehr

Kommunikation von Prozessen: Signale und Pipes

Kommunikation von Prozessen: Signale und Pipes Netzwerk-Programmierung Kommunikation von Prozessen: Signale und Pipes Alexander Sczyrba Michael Beckstette {asczyrba,mbeckste}@techfak.uni-bielefeld.de Kommunikation von Prozessen Parent- und Child-Prozess

Mehr

Klausur Betriebssysteme I

Klausur Betriebssysteme I Prof. Dr. Michael Jäger FB MNI Klausur Betriebssysteme I 14.3.2008 Bitte bearbeiten Sie die Aufgaben auf den Aufgabenblättern. Die Benutzung von Unterlagen oder Hilfsmitteln ist nicht erlaubt. Die Bearbeitungszeit

Mehr

Single- und Multitasking

Single- und Multitasking Single- und Multitasking Peter B. Ladkin [email protected] Peter B. Ladkin Command Interpreter (ComInt) läuft wartet auf Tastatur-Eingabe "liest" (parst) die Eingabe (für Prog-Name) Macht "Lookup"

Mehr

Allgemeines. Shell Programmierung Unix. Kommentar. Vorgangsweise. Mag. Thomas Griesmayer

Allgemeines. Shell Programmierung Unix. Kommentar. Vorgangsweise. Mag. Thomas Griesmayer Allgemeines Shell Programmierung Unix Shell Scripts Unix Ein shell script ist eine Textdatei, welche eine Liste von Befehlen (Standard Unix Befehle) beinhaltet. Diese Datei kann durch Aufrufen ausgeführt

Mehr

Betriebssysteme Kapitel E : Prozesse

Betriebssysteme Kapitel E : Prozesse Betriebssysteme Kapitel E : Prozesse 1 Inhalt Prozesse Zustand eines Prozesses» Kontext» Kontextswitch Prozessbeschreibungsblock PCB Zustandsübergänge» Zustandsdiagramm 2 Hinweis Ein Programm(code) kann

Mehr

Lösung von Übungsblatt 7

Lösung von Übungsblatt 7 Lösung von Übungsblatt 7 Aufgabe 1 (Systemaufrufe) 1. x86-kompatible CPUs enthalten 4 Privilegienstufen ( Ringe ) für Prozesse. Markieren Sie in der Abbildung (deutlich erkennbar!) den Kernelmodus und

Mehr

Betriebssysteme Übung 2. Tutorium System Calls & Multiprogramming

Betriebssysteme Übung 2. Tutorium System Calls & Multiprogramming Betriebssysteme Übung 2. Tutorium System Calls & Multiprogramming Task Wiederholung 1 System SysCalls (1) Wozu? Sicherheit Stabilität Erfordert verschiedene modes of execution: user mode privileged mode

Mehr

Betriebssysteme (BS)

Betriebssysteme (BS) Betriebssysteme (BS) Prozesse Olaf Spinczyk Arbeitsgruppe Eingebettete Systemsoftware Lehrstuhl für Informatik 12 TU Dortmund [email protected] http://ess.cs.uni-dortmund.de/~os/ http://ess.cs.tu-dortmund.de/de/teaching/ss2011/bs/

Mehr

Arithmetik in der tcsh

Arithmetik in der tcsh Arithmetik in der tcsh Variablen speichern Zeichenketten (also Strings/Wörter) @ statt set Interpretation als arithmetische Ausdrücke (aus Ziffern, (, ), +, -, *, /, % bestehend) Beispiele: @ var = (3

Mehr

Verwendung Vereinbarung Wert einer Funktion Aufruf einer Funktion Parameter Rekursion. Programmieren in C

Verwendung Vereinbarung Wert einer Funktion Aufruf einer Funktion Parameter Rekursion. Programmieren in C Übersicht Funktionen Verwendung Vereinbarung Wert einer Funktion Aufruf einer Funktion Parameter Rekursion Sinn von Funktionen Wiederverwendung häufig verwendeter nicht banaler Programmteile Wiederverwendung

Mehr

Systeme I: Betriebssysteme Kapitel 4 Prozesse. Maren Bennewitz

Systeme I: Betriebssysteme Kapitel 4 Prozesse. Maren Bennewitz Systeme I: Betriebssysteme Kapitel 4 Prozesse Maren Bennewitz Version 21.11.2012 1 Begrüßung Heute ist Tag der offenen Tür Willkommen allen Schülerinnen und Schülern! 2 Testat nach Weihnachten Mittwoch

Mehr

Bash-Skripting Linux-Kurs der Unix-AG

Bash-Skripting Linux-Kurs der Unix-AG Bash-Skripting Linux-Kurs der Unix-AG Sebastian Weber 13.06.2012 Was ist ein Bash-Skript? Skript muss mit chmod +x ausführbar gemacht sein Aneinanderreihung von Befehlen normale Befehle nutzbar Sebastian

Mehr

Bash-Skripting Linux-Kurs der Unix-AG

Bash-Skripting Linux-Kurs der Unix-AG Bash-Skripting Linux-Kurs der Unix-AG Sebastian Weber 07.01.2013 Was ist ein Bash-Skript? Skript muss mit chmod +x ausführbar gemacht sein Aneinanderreihung von Befehlen normale Befehle nutzbar Sebastian

Mehr

Systeme I: Betriebssysteme Kapitel 4 Prozesse. Maren Bennewitz

Systeme I: Betriebssysteme Kapitel 4 Prozesse. Maren Bennewitz Systeme I: Betriebssysteme Kapitel 4 Prozesse Maren Bennewitz Version 13.11.2013 1 Inhalt Vorlesung Aufbau einfacher Rechner Überblick: Aufgabe, Historische Entwicklung, unterschiedliche Arten von Betriebssystemen

Mehr

UNIX/Linux Lösung. Mär 14, 17 20:40 Seite 1/6. Prozeßsynchronisation (was ist das?, wo kommt es vor?, Beispiel?):

UNIX/Linux Lösung. Mär 14, 17 20:40 Seite 1/6. Prozeßsynchronisation (was ist das?, wo kommt es vor?, Beispiel?): Mär 14, 17 20:40 Seite 1/6 Aufgabe 1: Erklären Sie folgende Begriffe (25 Punkte): Prozeßsynchronisation (was ist das?, wo kommt es vor?, Beispiel?): Abstimmen von mehreren Prozessen, warten auf Ergebnisse,

Mehr

Vorbemerkung. Allgemeines zu Shell Scripts. Aufruf. Einfaches Beispiel

Vorbemerkung. Allgemeines zu Shell Scripts. Aufruf. Einfaches Beispiel Inhalt: Vorbemerkung...1 Allgemeines zu Shell Scripts...1 Aufruf...1 Einfaches Beispiel...1 Testen eines Shell-Scripts...2 Kommandozeilen-Parameter...2 Prozeßsteuerung...3 Bedingte Ausführung: if...3 Mehrfachentscheidung:

Mehr

Prozesse erzeugen, überwachen, killen und Prozessprioritäten ändern

Prozesse erzeugen, überwachen, killen und Prozessprioritäten ändern LPI Zertifizierung 1.103.5 6 Prozesse erzeugen, überwachen, killen und Prozessprioritäten ändern Copyright ( ) 2006-2009 by Dr. Walter Kicherer. This work is licensed under the Creative Commons Attribution-Noncommercial-Share

Mehr

Sequentielle Programm- / Funktionsausführung innerhalb eines Prozesses ( thread = Ausführungsfaden )

Sequentielle Programm- / Funktionsausführung innerhalb eines Prozesses ( thread = Ausführungsfaden ) Threads Sequentielle Programm- / Funktionsausführung innerhalb eines Prozesses ( thread = Ausführungsfaden ) Ein thread bearbeitet eine sequentielle Teilaufgabe innerhalb eines Prozesses Mehrere nebenläufige

Mehr

Rechnerarchitektur und Betriebssysteme (CS201): Frühe Betriebssysteme, geschützte CPU-Befehle, CPU-Modus

Rechnerarchitektur und Betriebssysteme (CS201): Frühe Betriebssysteme, geschützte CPU-Befehle, CPU-Modus Rechnerarchitektur und Betriebssysteme (CS201): Frühe Betriebssysteme, geschützte CPU-Befehle, CPU-Modus 2. November 2012 Prof. Dr. Christian Tschudin Departement Mathematik und Informatik, Universität

Mehr

Beispiel 2. Verwandte Prozesse: fork, exec, wait Interprozesskommunikation mit Unnamed Pipes. Denise Ratasich. 11. April Verwandte Prozesse IPC

Beispiel 2. Verwandte Prozesse: fork, exec, wait Interprozesskommunikation mit Unnamed Pipes. Denise Ratasich. 11. April Verwandte Prozesse IPC e eigenschaften Beispiel 2 e: fork, exec, wait Interprozesskommunikation mit Unnamed Denise Ratasich basierend auf Slides von Daniel Prokesch Institut für Technische Informatik Technische Universität Wien

Mehr

UNIX - LINUX. Prozesse. Überblick. Prozesse: Start. Prozesszustände. Prozesskontrollblock (Prozesssteuerblock) Prozesszustände

UNIX - LINUX. Prozesse. Überblick. Prozesse: Start. Prozesszustände. Prozesskontrollblock (Prozesssteuerblock) Prozesszustände Überblick UNIX - LINUX Prozesse Prozesse sind neben Dateien das zweite wichtige Grundkonzept von UNIX Definition: Ein Prozess ist ein Programm in Ausführung Prozesse und Dateien Prozesse werden aus Programm-Dateien

Mehr

(a) Wie unterscheiden sich synchrone und asynchrone Unterbrechungen? (b) In welchen drei Schritten wird auf Unterbrechungen reagiert?

(a) Wie unterscheiden sich synchrone und asynchrone Unterbrechungen? (b) In welchen drei Schritten wird auf Unterbrechungen reagiert? SoSe 2014 Konzepte und Methoden der Systemsoftware Universität Paderborn Fachgebiet Rechnernetze Präsenzübung 2 2014-04-28 bis 2014-05-02 Aufgabe 1: Unterbrechungen (a) Wie unterscheiden sich synchrone

Mehr

PThreads. Pthreads. Jeder Hersteller hatte eine eigene Implementierung von Threads oder light weight processes

PThreads. Pthreads. Jeder Hersteller hatte eine eigene Implementierung von Threads oder light weight processes PThreads Prozesse und Threads Ein Unix-Prozess hat IDs (process,user,group) Umgebungsvariablen Verzeichnis Programmcode Register, Stack, Heap Dateideskriptoren, Signale message queues, pipes, shared memory

Mehr

Lösung Übungszettel 6

Lösung Übungszettel 6 Lösungen zur Vorlesung GRUNDLAGEN DER INFORMATIK I Studiengang Elektrotechnik SS 03 AG Betriebssysteme FB3 Kirsten Berkenkötter Lösung Übungszettel 6 1 Aufgabe 1: Parallel-Server 1.1 Client #include

Mehr

Betriebssysteme G: Parallele Prozesse (Teil A: Grundlagen)

Betriebssysteme G: Parallele Prozesse (Teil A: Grundlagen) Betriebssysteme G: Parallele Prozesse (Teil A: Grundlagen) 1 Prozesse Bei Betriebssystemen stoßen wir des öfteren auf den Begriff Prozess als wahrscheinlich am häufigsten verwendeter und am unklarsten

Mehr

Typ : void* aktuelle Parameter Pointer von beliebigem Typ

Typ : void* aktuelle Parameter Pointer von beliebigem Typ 2. Funktionen - Prototypvereinbarung typangabe funktionsname(parameterliste); - Funktionsdefinition typ funktionsname(parameterliste){ Anweisung - Funktionstyp -> Typ der Funktionswertes zulaessige Typangaben

Mehr

Einige Eigenschaften der Bourne-Shell und der bash

Einige Eigenschaften der Bourne-Shell und der bash Einige Eigenschaften der Bourne-Shell und der bash 1. Startup-Skripte/spezielle Dateien: ~/.[bashrc ]profile von Login-Shell abgearbeitet ~/.bashrc bei jedem Aufruf einer bash abgearbeitet ~/.bash logout

Mehr