Systemnahe Software (Systemnahe Software II)

Größe: px
Ab Seite anzeigen:

Download "Systemnahe Software (Systemnahe Software II)"

Transkript

1 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 (SAI) Vorlesungsbegleiter (gültig ab Sommersemester 2005) UNIVERSITÄT ULM DOCENDO CURANDO S CIENDO Hinweise: Das Urheberrecht ( c ) liegt bei den Autoren 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 ii

3 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 iii

4 iv INHALTSVERZEICHNIS 4 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 Gepufferte Ein- / Ausgabe für Netzwerkverbindungen Ein kleiner TCP-Server (concurrent) mit Eingabe-Pufferung Ein- / Ausgabe von Paketen für Netzwerkverbindungen Parallele Sitzungen

5 INHALTSVERZEICHNIS v 7 Threads Einführung Grundlegende Funktionen: Erzeugen und Beenden pthread_create() pthread_join() pthread_self() pthread_detach() pthread_exit() Beispiel: Matrix-Multiplikation TCP Echo Server mit Threads Thread-sichere Funktionen Echo Client mit Threads Synchronisation Mutual Exclusion Anhang 243 Literatur 245 Abbildungsverzeichnis 247 Beispiel-Programme 249 Index 249

6 vi INHALTSVERZEICHNIS

7 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 dem 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(). 1

8 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. (Fast) jeder Prozess hat einen Erzeuger (siehe oben: fork()), der sog. parent process Dessen PID kann ein Prozess mit der Funktion int getppid() erhalten. 1 # include < stdio. h> 2 # include < stdlib. h> 3 # include <unistd. h> 4 5 int main() { 6 Programm 1.1: Prozess-ID abfragen (pids.c) 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.

9 1.1. THEORIE DER PROZESSE 3 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 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). 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) 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 main-funktion erreicht wird, so entspricht dies einem exit(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).

10 4 KAPITEL 1. DAS UNIX-PROZESS-SUBSYSTEM 1.2 Prozess-Baum Hierarchie und Vererbung (siehe Abbildung 1.1 parent process pid1 = fork() (pid1 > 0) child process (pid1 == 0) pid2 = fork() child process (pid2 == 0) (pid2 > 0) 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$

11 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 23 (SIGSTOP Anhalten. Letzteres kann mit ctrl-z über die Tastatur erzeugt werden. 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!

12 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$

13 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! 1 # include <unistd. h> 2 # include < stdio. h> 3 # include < stdlib. h> 4 Programm 1.3: Priorität verändern (priority.c) 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 } 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

14 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$

15 1.6. PROZESSZUSTÄNDE UND ZUSTANDSÜBERGÄNGE Prozesszustände und Zustandsübergänge system call 1 return 9 exit() 2 preempt sleep reschedule 7 4 wakeup 3 enough memory swap out swap in swap out 8 fork() 6 5 wakeup not enough memory Abbildung 1.2: Prozess Zustandsmodell 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

16 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. 1.7 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. 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. virtuelle Adresse Abbildungstabelle der MMU 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.

17 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. 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. 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. 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. 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.

18 12 KAPITEL 1. DAS UNIX-PROZESS-SUBSYSTEM Prozess Tabelle für alle Prozess zusammen je Prozess resident auslagerbar (swappable) System Daten Kernel Adressraum (Programm ) Text Daten Benutzer Adressraum Abbildung 1.4: Prozess-Kontext 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 - 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.

19 1.8. DAS PROC-FILESYSTEM 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

20 14 KAPITEL 1. DAS UNIX-PROZESS-SUBSYSTEM

21 Kapitel 2 Prozesse unter Unix 2.1 System Call fork 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 */ 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 Kindprozess erhält 0; im Fehlerfall (z.b. bereits zuviele Prozesse erzeugt) kehrt er einmal mit -1 zurück. 15

22 16 KAPITEL 2. PROZESSE UNTER UNIX UFDT user stack u area open Files current dir data Kernel Stack KIT shared text user stack u area open Files current dir changed root data Kernel Stack Abbildung 2.1: Ein neuer Prozess und sein Erzeuger

23 2.1. SYSTEM CALL FORK 17 Beispiel 1 # include < stdio.h> 2 # include <unistd. h> 3 # include < stdlib.h> 4 5 int main() 6 { 7 int pid = fork (); 8 Programm 2.1: Ein neuer Prozess mit fork() (fork1.c) 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 = %ld)\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) 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!

24 18 KAPITEL 2. PROZESSE UNTER UNIX Oder so: Erzeuger schläft ein bisschen, bevor er terminiert 1 # include < stdio.h> 2 # include <unistd. h> 3 # include < stdlib.h> 4 Programm 2.2: fork() zum zweiten (fork2.c) 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

25 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)

26 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: 1 # include < stdio.h> 2 # include <unistd. h> 3 # include < stdlib.h> 4 Programm 2.3: Beenden mit exit() (exit1.c) 5 int main() { 6 printf ( "This is some text to write on stdout"); 7 exit (0); 8 } 1 # include < stdio.h> 2 # include <unistd. h> 3 # include < stdlib.h> 4 Programm 2.4: Beenden mit _exit() (exit2.c) 5 int main() { 6 printf ( "This is some text to write on stdout"); 7 _exit (0); 8 }

27 2.2. SYSTEM CALL EXIT 21 Manchmal ist es praktisch, selbst vor dem (selbst gewollten) Beenden noch einiges zu erledigen: Funktion atexit() 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 } 11 Programm 2.5: Beenden mit eigenem Aufräumen (exit3.c) 12 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$

28 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.

29 2.3. SYSTEM CALL EXEC 23 Zusammenhang execlp(file, arg,...,0) execl(path,arg,..., 0) execle(path, arg,...,0,envp) create argv create argv create argv execvp(file,argv) convert file to path execv(path,argv) add envp execve(path,argv,envp) system call Abbildung 2.2: Die exec()-familie 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 (muss ebenfalls mit einem NULL- Zeiger abgeschlossen sein).

30 24 KAPITEL 2. PROZESSE UNTER UNIX Vererbung (erhalten bleibt) bei exec(): 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 Änderungen 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

31 2.3. SYSTEM CALL EXEC 25 Beispiel: 1 # include < stdio.h> 2 # include <unistd. h> 3 # include < stdlib.h> 4 Programm 2.6: Neues Programm ausführen (exec1.c) 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ässt man in Programm 2.6 den Funktionsaufruf fflush() weg (siehe Programm 2.7, S. 26), so passiert folgendes: the lazy dogs. PP: Parent-PID: 4914 / Child-PID: 4915

32 26 KAPITEL 2. PROZESSE UNTER UNIX 1 # include < stdio.h> 2 # include <unistd. h> 3 # include < stdlib.h> 4 5 Programm 2.7: Programm 2.6 ohne fflush() (exec2.c) 6 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.

33 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) 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$

34 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)

35 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) Termination durch Argument von exit() 0x00 exit() 0x00 Signalnummer core Flag Signal Signalnummer 0x7F Prozess angehalten (nur in Spezialfällen) NB: Signal-Nummern sind größer 0! Abbildung 2.3: Information über Kind-Prozess

36 30 KAPITEL 2. PROZESSE UNTER UNIX Ein erstes Beispiel mit wait(): 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); 12 Programm 2.12: Auf Kind-Prozess warten (wait1.c) 13 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 & 0x3f ; 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) 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) 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

37 2.4. SYSTEM CALL WAIT 31 1 # include < stdio.h> 2 # include < stdlib.h> 3 # include <unistd. h> 4 # include <sys/wait. h> 5 Programm 2.13: Auf Kind-Prozess warten (forkandwait.c) 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 Parameter options: bitweises ODER folgender Konstanten WNOHANG: nicht blockieren, falls kein Kindprozess terminierte

38 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() 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 Programm 2.14: Warten mit waitpid() (waitpid1.c) 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 (20); 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 sleep (50); 36 printf ("Parent going to terminate!\n"); 37 return 0; 38 } 39 } Aufgaben des Kernel, wenn ein Prozess exit() aufruft: 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.

39 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. 1 # include < stdio.h> 2 # include < stdlib.h> 3 # include <unistd. h> 4 Programm 2.15: Ein Prozess wird zum Waisenkind (orphan.c) 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 } 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)

40 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$ 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 } Programm 2.16: Erzeugen eines Zombie-Prozesses (genzombie.c)

41 2.5. N-DAMEN-PROBLEM MIT PROZESSEN n-damen-problem mit Prozessen 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) Abbildung 2.4: n-damen-problem: das Schachbrett 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:

42 36 KAPITEL 2. PROZESSE UNTER UNIX Start (0,0) (0,1) (0,2) (0,3) (1,0) (1,1) (1,0) (1,1) (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 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) Dies ergibt dann das Programm 2.17, S. 36. Anzumerken bleibt, dass von den 92 Lösungen bei n = 8 sehr viele symmetrisch sind. 1 # include < stdio.h> 2 Programm 2.17: 8-Damen klassisch (classicqueens.c) 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 );

43 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: 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.

44 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 Unterprozess 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); } Weitere Minuspunkte ergeben sich aus der Verwendung einer goto-anweisung und gemeinsamer Programmpfade nach dem fork().

45 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 }

46 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$ 1 # include < stdio. h> 2 # include < fcntl. h> 3 # include <unistd. h> 4 # include < stdlib. h> 5 # include <sys/wait. h> 6 Programm 2.19: Über 0 aus einer Datei lesen (umlenkung1.c) 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 }

47 2.6. DATEI-UMLENKUNG 41 Das geht natürlich auch ausgabeseitig, wie Programm 2.20 (S. 41) zeigt: spatz$ gcc -Wall umlenkung2.c spatz$ a.out Ein kleiner Text auf 2 Zeilen spatz$ cat XXX Ein kleiner Text auf 2 Zeilen spatz$ 1 # include < stdio. h> 2 # include < fcntl. h> 3 # include <unistd. h> 4 # include <sys/types. h> 5 # include <sys/wait. h> 6 Programm 2.20: Über 1 in eine Datei schreiben (umlenkung2.c) 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 }

48 42 KAPITEL 2. PROZESSE UNTER UNIX 2.7 Eine kleine Shell (tinysh) 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() Umgebung einrichten exec() wait() 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;

49 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() I/O Umlenkung vorbereiten fassign() Argumentvektor aufbauen Kommando ausführen Warten Abbildung 2.8: Ablaufprinzip der tinysh

50 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 ; 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}; 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.

51 2.7. EINE KLEINE SHELL (TINYSH) 45 int stralloc_cats(stralloc* sa,const char* buf) Hängt den nullterminierten String buf an sa an. int stralloc_cat(stralloc* sa,stralloc* sa2) 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. 1 # ifndef SA_READLINE_H 2 # define SA_READLINE_H 3 Programm 2.21: tinysh: readline() Schnittstelle (tinysh/sareadline.h) 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 }

52 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 # 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 } Programm 2.23: tinysh: readline() Test (tinysh/test-readline.c) Ü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$

53 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. input s c p 0 x 0 y 0 len a 7 30 tokens list len 4 allocated 8 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.

54 48 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

55 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 }

56 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) 1 # ifndef TOKENIZER_H 2 Programm 2.26: tinysh: tokenizer() Schnittstelle (tinysh/tokenizer.h) 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 }

57 2.7. EINE KLEINE SHELL (TINYSH) 51 Ein kleines Test-Programm zeigt 2.28, S # 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 ); 11 Programm 2.28: tinysh: tokenizer() Test (tinysh/test-tokenizer.c) 12 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$

58 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;

59 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 }

60 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-Kindprozess-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.

61 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, muss 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. Phase 3: Standalone 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). 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.

62 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). fork() init fork() process ID = 1 fork() init init init exec() exec() exec() getty getty getty wartet auf login Name login exec Arg.: Login Name wartet auf Passwort exec() login exec() /bin/sh Abbildung 2.10: Der Init-Prozess 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.

63 2.9. DER INIT-PROZESS 57 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)

64 58 KAPITEL 2. PROZESSE UNTER UNIX

65 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. 59

66 60 KAPITEL 3. SIGNALE 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.

67 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 */

68 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. 1 # include < signal.h> 2 # include < stdio.h> 3 # include < stdlib.h> 4 Programm 3.1: Behandlung eines SIGINT-Signals (sigint.c) 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.

69 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$ 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,

70 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. 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.

71 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 }

72 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.

73 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.

74 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$

75 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 }

76 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.

77 3.5. DAS VERSENDEN VON SIGNALEN 71 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 10 8 Programm 3.6: Virtuelles Ballspiel zweier Prozesse (pingpong/pingpong.c) 9 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 }

78 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.

79 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

80 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) 1 # include "sign. h" 2 # include < stdio. h> 3 # include <unistd. h> 4 Programm 3.8: Erstes Beispiel zu sigaction() (sigaction1/sign.c) 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;

81 3.6. REAKTION AUF SIGNALE: SIGACTION() else 36 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 } 1 # include < stdio. h> 2 # include <unistd. h> 3 # include < stdlib. h> 4 # include "sign. h" 5 Programm 3.9: Testprogramm zu 3.8 (sigaction1/show.c) 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 }

82 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). 1 # include "sign. h" 2 # include < stdio. h> 3 # include <unistd. h> 4 Programm 3.10: Modifikation von 3.8 (sigaction2/sign.c) 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 }

83 3.6. REAKTION AUF SIGNALE: SIGACTION() 77 Ausführung: spatz$ show Break? go asleep for 5 sec ^Csignal handler: SIGNAL = >2< ^\ Quit spatz$ 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!

84 78 KAPITEL 3. SIGNALE 3.7 Die Zustellung von Signalen 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. 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

85 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. 1 # include < signal.h> 2 # include < stdio.h> 3 # include < stdlib.h> 4 # include <unistd. h> 5 Programm 3.11: Verlust von Signalen (sigfire/sigfire.c) 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 }

86 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:

87 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.

88 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. 1 # include < stdio. h> 2 # include < stdlib. h> 3 # include <unistd. h> 4 # include < signal. h> 5 Programm 3.12: Prozesse, auf die der Erzeuger nicht wartet (sigchld/show.c) 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 ) {

89 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 }

90 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;

91 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 }

92 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);

93 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!

94 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!

95 3.9. SIGNALBEHANDLUNG IN EINER (MINI-)SHELL 89 space > > > out(t_gtgt) else > ungetc; out(t_gt); NEUTRAL 2 TWO_GT \n > out(t_nl) & > out(t_amp) < > out(t_lt) <,>,&,\n,space > ungetc; out(t_word) else > word[0] = c; i++ > > out(t_two_gt) else > ungetc; word[0]= 2 ; else > word[i]=c; i++ WORD i++ GTGT Abbildung 3.1: Token-Erkennung T_WORD T_LT start T_WORD T_WORD infile expected T_GT T_GTGT T_TWO_GT T_NL T_AMP outfile expected command background command Abbildung 3.2: Syntax-Analyse der Kommandozeile Ablauf-Schema:

96 90 KAPITEL 3. SIGNALE Signal Handler einrichten LOOP FOREVER Prompt ausgeben Kommandozeile lesen > Token erkennen und Syntax prüfen Neuen Prozess erzeugen ggf. I/O Umlenkung vornehmen falls im Hintergrund: Signal Handler einrichten ggf. Warten via exec Kommando starten Abbildung 3.3: Ablauf der MidiShell Modularisierung: main.c: termination.h termination.c exit-status ausgeben cmd.h cmd.c Syntax-Analyse I/O-Umlenkung ggf. Signalhandler Kommando starten sign.c sign.h Signal-Handler defs.h grundlegende Vereinbarungen gettoken.h gettoken.c Token holen Abbildung 3.4: Struktur der MidiShell

97 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 /

98 92 KAPITEL 3. SIGNALE 26 # endif 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

99 3.9. SIGNALBEHANDLUNG IN EINER (MINI-)SHELL 93 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; 16 Programm 3.20: Midi-Shell: main-funktion Start (midishell/main.c) 17 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 }

100 94 KAPITEL 3. SIGNALE 1 # define GET_H 2 # include "gettoken. h" 3 Programm 3.21: Midi-Shell: Tokenbestimmung (midishell/gettoken.c) 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 ;

101 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 }

102 96 KAPITEL 3. SIGNALE 1 # include < string.h> 2 # define CMD_H 3 # include "cmd.h" 4 Programm 3.22: Midi-Shell: Kommandoausführung (midishell/cmd.c) 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 /

103 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 () /

104 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

105 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 ();

106 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 }

107 3.9. SIGNALBEHANDLUNG IN EINER (MINI-)SHELL # define SIGN_H 2 3 # include < stdio. h> 4 # include "sign. h" 5 Programm 3.23: Midi-Shell: Signalbehandler (midishell/sign.c) 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)&0xff ); 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;

108 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 } 1 # define TERM_H 2 # include " termination. h" 3 Programm 3.24: Midi-Shell: Termination (midishell/termination.c) 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 }

109 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 } 1 # include < stdio.h> 2 # include < stdlib.h> 3 Programm 3.25: Midi-Shell: Testprogramm 1 (midishell/sleepwell.c) Programm 3.26: Midi-Shell: Testprogramm 2 (midishell/read-something.c) 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.phony: clean realclean clean: rm -f *.o core realclean: rm -f *.o core midish sleepwell read-something

110 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

111 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$

112 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.

113 3.10. ÜBERBLICK DER SIGNALE AUS DEM POSIX-STANDARD 107 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.

114 108 KAPITEL 3. SIGNALE

115 Kapitel 4 Inter-Prozess-Kommunikation (IPC) 4.1 Einführung 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. user process user process KERNEL Abbildung 4.1: IPC nur über den Kernel 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. 109

116 110 KAPITEL 4. INTER-PROZESS-KOMMUNIKATION (IPC) Netzwerk-Kommunikation 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. User Process UserProcess KERNEL KERNEL Abbildung 4.2: IPC auch über Rechnergrenzen Für die Prozesse kann es völlig transparent sein, wo der jeweilige Partnerprozess abläuft. 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. 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 file contenst or error message stdin stdout filename 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.

117 4.3. SYSTEM CALLS DUP(), DUP2() 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 */ 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. 4.4 Unnamed Pipes 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 */ 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) In dieser Konstellation lässt sich die Pipe nur als Zwischenspeicher für Daten außerhalb des User Adressraums benutzen.

118 112 KAPITEL 4. INTER-PROZESS-KOMMUNIKATION (IPC) user process read fd write fd Kernel pipe flow of data Abbildung 4.4: Unnamed Pipe - erster Schritt 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). write fd parent process read fd fork write fd child process read fd Kernel 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).

119 4.4. UNNAMED PIPES 113 parent process child process read fd 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.

120 114 KAPITEL 4. INTER-PROZESS-KOMMUNIKATION (IPC) Realisierung: 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 ] ); 23 Programm 4.1: Einfache Kommunikation via Pipe (pipe.c) 24 / 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 }

121 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 stdin file content filename parent process child process file content (client) filename pipe_1 filename (server) file content pipe_2 Abbildung 4.7: Client-Server mit bidirektionaler Kommunikation Vorgehen: 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),

122 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 }

123 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 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; 14 Programm 4.4: Client (cli-srv/client.c) 15 / 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 }

124 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 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 ; 12 Programm 4.6: Server (cli-srv/server.c) 13 / 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 }

125 4.5. CLIENT-SERVER MIT UNNAMED PIPES 119

126 120 KAPITEL 4. INTER-PROZESS-KOMMUNIKATION (IPC) 4.6 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. 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.

127 4.6. STANDARD I/O BIBLIOTHEKSFUNKTION 121 Beispiel: 1 # include < stdio.h> 2 # include < stdlib.h> 3 4 # define BUFFER_SIZE int main() { 7 8 char buf [ BUFFER_SIZE ]; 9 FILE fp ; 10 Programm 4.7: Beispiel mit popen(), pclose() (popen/popen.c) 11 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 }

128 122 KAPITEL 4. INTER-PROZESS-KOMMUNIKATION (IPC) 4.7 Über Standarddeskriptoren in Pipe schreiben / lesen 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]); 14 Programm 4.8: 0/1 auf Pipe (pipe-2.c) 15 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 }

129 4.8. TERMINATION VON PIPELINE-VERBINDUNGEN Termination von Pipeline-Verbindungen 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]); 17 Programm 4.9: Termination? (pipe-3.c) 18 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 }

130 124 KAPITEL 4. INTER-PROZESS-KOMMUNIKATION (IPC) Übersetzung und Ausführung: 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: 1 // Termination!!! 2 # include < stdio.h> 3 # include <unistd. h> 4 # include < stdlib.h> 5 Programm 4.10: Termination! (pipe-4.c) 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 }

131 4.9. WIE DIE SHELL EINE PIPELINE MACHT Wie die Shell eine Pipeline macht 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 Programm 4.11: Pipelining der Shell (pipe-5.c) 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 }

132 126 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$

133 Kapitel 5 Netzwerk-Kommunikation 5.1 Übersicht Netzwerk räumlich verteiltes System von Rechnern, Steuereinheiten und Peripheriegeräten, verbunden mit Datenübertragungseinrichtungen aktive / passive Komponenten 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 127

134 128 KAPITEL 5. NETZWERK-KOMMUNIKATION Grundlegendes herstellerunabhängiges Konzept: DIN/ISO-OSI-Referenzmodell OSI - Open Systems Interconnection Sender D a t e n s t r o m Empfänger 7 Application H Paket i Anwendungs schicht 6 Presentation H Darstellungs schicht 5 Session H Sitzungs schicht 4 Transport H Transport schicht 3 Network H Vermittlungs schicht 2 Data Link H T Sicherungs schicht 1 Physical Bitfolge Bitübertragungs schicht Medium Medium Abbildung 5.1: ISO-OSI-Referenzmodell 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)

135 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. terminal emulation network manag. simple network manag. telnet (SMTP) protocol prot Host to host TCP User Datagram Prot.UDP address internet prot. internet control Internet layer resolution message prot Ethernet, IEEE 802, Arcnet, X network access or local network lay twisted pair, Koaxial,Glasfaser OSI TCP/IP Protokoll Implementierungen Abbildung 5.2: TCP/IP

136 130 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!

137 5.1. ÜBERSICHT 131 Übertragungsmedien (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ättzlicher Gesamtschirmung um die Seele (Abb. 5.5, S. 132) UTP 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

138 132 KAPITEL 5. NETZWERK-KOMMUNIKATION STP Kupferleiter Adernisolierung Paar Paarschirm Kabelmantel Abbildung 5.4: Shielded-Twisted-Pair-Kabel S/STP Kupferkabel Adernisolierung Paar Paarschirm Gesamtschirm Kabelmantel Abbildung 5.5: Screened Shielded-Twisted-Pair-Kabel Koaxialkabel, kurz: Koax-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

139 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). 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.

140 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 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

141 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) 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 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?

142 136 KAPITEL 5. NETZWERK-KOMMUNIKATION Frame Format: (siehe auch Abb. 5.1, S. 128) Preamble Source Destination Type Data CRC (Cyclic Redundancy Check) Abbildung 5.7: Ethernet Frame Format 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: 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)

143 5.3. INTERNETWORKING - KONZEPT & GRUNDLEGENDE ARCHITEKTUR 137 application level OS Abstraktion physical level Abbildung 5.8: Layered System Architecture Grundlegende Internet Architektur Netz 1 (Token-Ring: 8-Bit-Adressen) GATEWAY Internet-Protocol (IP) Adressen von Rechnern in Netz-2 aus Sicht der Rechner aus Netz-1??? Netz 2 <net_id,host_id> (Ethernet - 48-Bit-Adressen) Abbildung 5.9: Internet Architektur In a TCP/IP internet, computers called gateways provide all interconnections among physical networks

144 138 KAPITEL 5. NETZWERK-KOMMUNIKATION Frage: Muss ein Gateway alle in allen Netzen erreichbare Rechner kennen (ein Super-Computer)? Antwort: Nein! gateways route packets based on destination network, not on destination host Benutzer-Sicht: ein Internet als ein großes, virtuelles oder logisches Netzwerk Routing: 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? abhängig vom Routing-Protokoll und somit Einstellungssache des Routers Internet-Adressen globale Identifikation von hosts im (in ihrem) Netz NAME (was) ADRESSE (wo) abnehmende Abstraktion der Identifikation ROUTE, PFAD (wie dorthin) Abbildung 5.10: Abstraktion der Identifikation IP-Adresse 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

145 5.3. INTERNETWORKING - KONZEPT & GRUNDLEGENDE ARCHITEKTUR netid hostid Class A 1 0 netid hostid Class B netid hostid Class C muliticast id Class D reserved Class E Abbildung 5.11: 32-Bit IP-Adresse in Version 4 lesbare Adressen ( punktiertes Dezimalformat): (dotted decimal notation) (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: netid hostid 1.x.x.x bis 126.x.x.x 126 Subnetze x bis x Hosts Class B netid hostid x.x bis x.x Subnetze x.x.0.1 bis x.x Hosts Class C netid x bis x.x Subnetze hostid x.x.x.1 bis x.x.x Hosts Abbildung 5.13: IP-Adressklassen in Version 4

146 140 KAPITEL 5. NETZWERK-KOMMUNIKATION 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 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 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).

147 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 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

148 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 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

149 5.3. INTERNETWORKING - KONZEPT & GRUNDLEGENDE ARCHITEKTUR 143 TCP/IP Application Services Reliable Transport Service TCP Connectionless packet delivery Service IP 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 IP Internet Protocol: 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) 3. Regeln, wie Host s und Gateway s Pakete verarbeiten sollen, wie und wann Fehlermeldungen produziert werden sollen, Bedingungen für das Wegwerfen von Paketen

150 144 KAPITEL 5. NETZWERK-KOMMUNIKATION Internet Datagram - basic transfer unit grober Aufbau (Abb. 5.15, S. 144): Datagram Header Datagram Data Area Abbildung 5.15: IP Datagramm - grober Aufbau genauer ist dies in Abb. 5.16, S. 144 dargestellt Version Header Length Time to live (TTL) Identification Service Type Protocol Flags Source IP Address Destination IP Address Total Length Header Checksum Fragmentation offset IP Options (if any) Padding DATA DATA Abbildung 5.16: IP Datagram - im Detail

151 5.3. INTERNETWORKING - KONZEPT & GRUNDLEGENDE ARCHITEKTUR 145 TCP/IP Internet Layering Model Conceptual Layer Application Transport Internet Objects Passed Between Layers Messages or Streams Transport Protocol Packets IP Datagrams Network Interface (data link) Network-Specific Frames Hardware Abbildung 5.17: TCP/IP Layering Model 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)

152 146 KAPITEL 5. NETZWERK-KOMMUNIKATION jedem Paket wird zusätzliche Information beigefügt (welche Appl.?) Internet Layer Prüfsumme 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

153 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) UDP UDP Source Port UDP Destination Port UDP Message Length UDP Checksum 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

154 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 Based on Port IP Layer UDP Datagram arrives Abbildung 5.19: UDP-Demultiplexing

155 5.4. TRANSPORT-PROTOKOLLE 149 Port-Nummern: 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...

156 150 KAPITEL 5. NETZWERK-KOMMUNIKATION Beispiel einer TCP/IP Sitzung an Port 80 (Webserver): 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$

157 Kapitel 6 Berkeley Sockets 6.1 Grundlagen API (application program interface) zu Kommunikations-Protokollen 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. 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

158 152 KAPITEL 6. BERKELEY SOCKETS application socket system calls socket system call implementation user kernel socket layer functions protocol layer TCP/IP, UNIX, XNS device driver Abbildung 6.1: Vereinf. Modell der Implementierung von Sockets unter BSD 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.

159 6.2. EIN ERSTES BEISPIEL: TIMESERVER AN PORT Ein erstes Beispiel: Timeserver an Port 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 < string.h> 6 # include <sys/ socket. h> 7 # include <sys/time. h> 8 # include <time. h> 9 # include <unistd. h> 10 # include < stdlib.h> 11 # define TPORT int main() { 13 struct sockaddr_in addr, client_addr ; 14 size_t client_add_len = sizeof ( client_addr ); 15 int sfd, fd ; int optval = 1; // bzero (&addr, sizeof ( addr )); // mit 0en fuellen 18 // besser : 19 memset(&addr,0, sizeof ( addr )); addr. sin_family = AF_INET; // TCP/IP Verbindung 22 addr. sin_port = htons (TPORT);// Port eintragen, Network Byte Order if (( sfd = socket (AF_INET, SOCK_STREAM, 0)) <0) // socket erzeugen 25 perror ("socket" ), exit (1); if ( setsockopt ( sfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval))<0) 28 perror (" setsockopt " ), // rasche Wiederzuweisung ermoeglichen 29 exit (1); if ( bind ( sfd, ( struct sockaddr ) &addr, sizeof ( addr )) <0) 32 perror ("bind" ), exit (1); if ( listen ( sfd, SOMAXCONN) <0) 35 perror ( " listen " ), exit (1); while (( fd = accept ( sfd, ( struct sockaddr ) & client_addr, 38 & client_add_len )) >=0) { 39 time_t clock ; char tbuf ; 40 time(& clock ); // aktuelle Uhrzeit holen 41 tbuf = ctime(& clock ); // in String schreiben 42 write ( fd, tbuf, strlen ( tbuf )); // in Clientverbindung schreiben 43 close ( fd );

160 154 KAPITEL 6. BERKELEY SOCKETS 44 } 45 exit (0); // seldomly reached 46 }

161 6.2. EIN ERSTES BEISPIEL: TIMESERVER AN PORT Man beachte die Funktionen bzero(), memset(). An der Stelle, an der die IP- Adresse eingetragen werden könnten, stehen nun definiert Nullen dies entspricht dem Makro INADDR_ANY: /* in /usr/include/netinet/in.h: */ /* Address to accept any incoming messages. */ #define INADDR_ANY ((in_addr_t) 0x ) Übersetzung und Ausführung: 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

162 156 KAPITEL 6. BERKELEY SOCKETS 6.3 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. 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. Socket-Typen: In der Socket-Abstraktion entsprechen die Socket-Typen den für die Anwendung jeweils sichtbaren Kommunikationsverfahren (verbindungslos - verbindungsorientiert). 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!

163 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE 157 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. 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. 6.4 Die Socket-Programmierschnittstelle Vorbemerkungen 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-

164 158 KAPITEL 6. BERKELEY SOCKETS 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.

165 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE Überblick/Einordnung Zuverlässige vs. nicht zuverlässige Verbindung: Berkeley Sockets API SOCK_STREAM SOCK_DGRAM ISO/OSI TCP UDP = IP + Port + 4 reliable unreliable Checksum 3 IP (unreliable) / ARP 2 1 Ethernet Verkabelung / Protokoll Twisted Pair, Glasfaser, Koaxial Abbildung 6.2: Sockets Überblick Server Client Server Client socket() socket() socket() socket() bind() bind() bind() listen() accept() connect() read() write() recvfrom() sendto() write() read() sendto() recvfrom() Abbildung 6.3: API-Aufrufe

166 160 KAPITEL 6. BERKELEY SOCKETS Kommunikations-Domäne: Internet Domain: (bidirektionale) Kommunikation zwischen Rechnern TCP/IP oder UDP/IP Adressierung: das 5er-Tupel (Protocol, SendAddr, SendPort, RecvAddr, RecvPort) Unix Domain: (bidirektionale) Kommunikation auf einem (Unix-) Rechner 3er-Tupel: (Protocol, local-pathname, foreign-pathname) Server Client socket() socket() bind() listen() accept() connect() recv() send() send() recv() Abbildung 6.4: Kommunikation in der Unix-Domain

167 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE Erzeugung 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 ) 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.

168 162 KAPITEL 6. BERKELEY SOCKETS Benennung eines Socket 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: 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.

169 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE 163 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): # 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

170 164 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));

171 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 socket() Client Process socket() bind() listen() accept() connect() recv() send() send() recv() 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

172 166 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> int sd; struct sockaddr_in sin_addr; /* for address of the server*/ 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(): # 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 );

173 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE 167 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. server process client process passive socket connect() connection socket accept() connection socket send(), recv() Abbildung 6.6: Aufbau einer verbind.-orient. Client/Server-Kommunikation

174 168 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); 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,...); */ client_len = sizeof(client_addr); conn_sd = accept(sd, (struct sockaddr *) &client_addr, &client_len);

175 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE Client-Beispiel: Timeclient für Port # include <netdb. h> 2 # include < netinet /in.h> 3 # include < stdio.h> 4 # include < string.h> 5 # include <sys/ socket. h> 6 # include <unistd. h> 7 # include < stdlib.h> 8 9 # define TPORT Programm 6.2: Timeclient für Port (timcli.c) 11 int main( int argc, char argv) 12 { struct sockaddr_in addr ; struct hostent hp; 13 int fd ; int nbytes ; char buf [BUFSIZ]; if ( argc!=2) 16 printf ( "usage: %s hostname\n", argv[0]), exit (1); if (!( hp = gethostbyname ( argv [1]))) // IP Adresse des Servers holen 19 fprintf ( stderr, "unknown host: %s\n", argv[1]), 20 exit (1); // bzero (&addr, sizeof ( addr )); // mit 0en fuellen 23 // besser : 24 memset(&addr,0, sizeof ( addr )); 25 addr. sin_family = AF_INET; // TCP/IP Verbindung 26 addr. sin_port = htons (TPORT);// Port eintragen, Network Byte Order // Serveraddresse eintragen : 29 // bcopy(hp >h_addr, &addr. sin_addr, hp >h_length); 30 // besser ( aber Achtung auf Reihenfolge der Parameter ) 31 memcpy(&addr.sin_addr, hp >h_addr, hp >h_length); if (( fd = socket (AF_INET, SOCK_STREAM, 0)) <0) 34 perror ( "socket " ), // Socket erzeugen 35 exit (1); if ( connect ( fd, ( struct sockaddr ) &addr, sizeof ( addr )) <0) 38 perror ( "connect" ), // an Socketport verbinden 39 exit (1); 40 // vom Socket lesen 41 while (( nbytes = read ( fd, buf, sizeof ( buf ))) >0) 42 printf ( "%. s", nbytes, buf ); close ( fd ); 45 exit (0); 46 } Man beachte hier die Funktionen bcopy() und memcpy() und achte insbesondere auf die Reihenfolge der Parameter Ziel und Quelle!

176 170 KAPITEL 6. BERKELEY SOCKETS Überblick: Gebrauch von TCP-Ports server server process table tcp ports well known CMD UID PID timserv httpd liest / schreibt liest / schreibt ports, only superuser registered ports, some are still free client netscape free / dynamic liest / schreibt ports Unix Operating System 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

177 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE 171 Beispiel: 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) ulm.de (server) processes ports ports processes netscape temp. conn. temp. network connection const 80 httpd Abbildung 6.8: netscape als Client httpd (HTTP Dämon, Webserver-Prog.) auf erhält Anfrage von netscape auf thales.mathematik.uni-ulm.de

178 172 KAPITEL 6. BERKELEY SOCKETS 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) ulm.de (server) processes ports ports processes netscape temp temp. network connection bind 80 const httpd Abbildung 6.9: Server httpd antwortet

179 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE 173 struct sockaddr 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)

180 174 KAPITEL 6. BERKELEY SOCKETS 1 # include <netdb. h> 2 # include < netinet /in.h> 3 # include <arpa/ inet. h> 4 # include < stdio.h> 5 # include < strings.h> 6 # include < string.h> 7 # include <sys/ socket. h> 8 # include <sys/time. h> 9 # include <unistd. h> 10 # include < stdlib.h> 11 # include <sys/time. h> 12 # include <time. h> 13 Programm 6.3: Time-Server, der nach dem Woher fragt (timserv-1.c) 14 # define TPORT int main() 16 { struct sockaddr_in addr, client_addr ; 17 size_t client_add_len = sizeof ( client_addr ); 18 int sfd, fd ; 19 / 20 int optval = 1; 21 / 22 memset(&addr,0, sizeof ( addr )); // mit 0en fuellen 23 addr. sin_family = AF_INET; // TCP/IP Verbindung 24 addr. sin_port = htons (TPORT);// Port eintragen, Network Byte Order 25 if (( sfd = socket (AF_INET, SOCK_STREAM, 0)) <0) // socket erzeugen 26 perror ( "socket" ), exit (1); 27 / 28 if ( setsockopt ( sfd,sol_socket, SO_REUSEADDR, &optval, sizeof(optval))<0) 29 perror (" setsockopt "), // rasche Wiederzuweisung ermoeglichen 30 exit (1); 31 / 32 if ( bind ( sfd, ( struct sockaddr ) &addr, sizeof ( addr )) <0) 33 perror ( "bind" ), exit (1); if ( listen ( sfd, SOMAXCONN) <0) 36 perror ( " listen " ), exit (1); 37 while (( fd = accept ( sfd, ( struct sockaddr ) & client_addr, 38 & client_add_len )) >=0) { 39 time_t clock ; char tbuf ; 40 time(& clock ); // aktuelle Uhrzeit holen 41 tbuf = ctime(& clock ); // in String schreiben 42 write ( fd, tbuf, strlen ( tbuf )); // in Clientverbindung schreiben 43 printf ( "Server: client port is %d\n", ntohs( client_addr. sin_port )); 44 printf ( "Server: client IP is %s\n", inet_ntoa ( client_addr. sin_addr )); 45 close ( fd ); 46 } 47 exit (0); // seldomly reached 48 }

181 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE Der Datentransfer 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.

182 176 KAPITEL 6. BERKELEY SOCKETS 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(). 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!

183 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE 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 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.

184 178 KAPITEL 6. BERKELEY SOCKETS 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: server process socket() client process socket() bind() bind() recvfrom() sendto() sendto() recfrom() Abbildung 6.10: Aufbau einer verb.-losen Client/Server-Kommunikation 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!

185 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE 179 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. Die Programme 6.4 (S. 180) und 6.5 (S. 181) setzen den früheren Time-Server und zugehörigen Client auf Basis UDP um dies macht wenig Sinn in dieser Art der Anwendung, soll hier nur zur Demonstration dienen!

186 180 KAPITEL 6. BERKELEY SOCKETS Programm 6.4: Time-Server auf Basis UDP (nur zur Demo) (timserv-udp.c) 1 / Modifikation unseres Timeservers, 2 nur zur DEMO, so nicht sinnvoll 3 / 4 5 # include <netdb. h> 6 # include < netinet /in.h> 7 # include < stdio.h> 8 # include < strings.h> 9 # include < string.h> 10 # include <sys/ socket. h> 11 # include <sys/time. h> 12 # include <time. h> 13 # include <unistd. h> 14 # include < stdlib.h> 15 # define SERVPORT # define MAXLINE int main() { 19 struct sockaddr_in addr, client_addr ; 20 size_t client_addr_len = sizeof ( client_addr ); 21 char recvline [MAXLINE]; 22 int sfd ; memset(&addr, 0, sizeof ( addr )); // mit 0en fuellen addr. sin_family = AF_INET; // UDP Verbindung 27 addr. sin_port = htons (SERVPORT);// Port eintragen, Network Byte Order 28 addr. sin_addr. s_addr = htonl (INADDR_ANY); if (( sfd = socket (AF_INET, SOCK_DGRAM, 0)) <0) // socket erzeugen 31 perror ( "socket" ), exit (1); if ( bind ( sfd, ( struct sockaddr ) &addr, sizeof ( addr )) <0) 34 perror ( "bind" ), exit (2); while (1) { 37 if ( ( recvfrom ( sfd, recvline, MAXLINE,0, 38 ( struct sockaddr ) & client_addr, & client_addr_len )) < 0) { 39 perror ( "Server recfrom"); exit (3); 40 } 41 time_t clock ; char tbuf ; 42 time(& clock ); // aktuelle Uhrzeit holen 43 tbuf = ctime(& clock ); // in String schreiben 44 if ( sendto ( sfd, tbuf, strlen ( tbuf ),0, ( struct sockaddr ) & client_addr, 45 sizeof ( client_addr )) < strlen ( tbuf ) ) { 46 perror ("Server sendto"); exit (4); 47 } 48 } 49 exit (0); // seldomly reached 50 }

187 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE 181 Programm 6.5: Time-Client auf Basis UDP (nur zur Demo) (timcli-udp.c) 1 / Modifikation unseres Time Clients nur zur 2 Demo, so nicht sinnvoll 3 / 4 5 # include <netdb. h> 6 # include < netinet /in.h> 7 # include < stdio.h> 8 # include < strings.h> 9 # include < string.h> 10 # include <sys/ socket. h> 11 # include <unistd. h> 12 # include < stdlib.h> 13 # include <arpa/ inet. h> # define SERVPORT int main( int argc, char argv) 18 { struct sockaddr_in serv_addr, cli_addr ; struct hostent hp; 19 int fd ; int n; char buf [BUFSIZ]; if ( argc!=2) 22 printf ( "usage: %s hostname\n", argv[0]), exit (1); if (!( hp = gethostbyname ( argv [1]))) // IP Adresse des Servers holen 25 fprintf ( stderr, "unknown host: %s\n", argv[1]), 26 exit (1); memset(&serv_addr,0, sizeof ( serv_addr )); // mit 0en fuellen 29 serv_addr. sin_family = AF_INET; // TCP/IP Verbindung 30 serv_addr. sin_port = htons (SERVPORT);// Port eintragen, Network Byte Order 31 // Serveraddresse eintragen : 32 memcpy(&serv_addr. sin_addr. s_addr, hp >h_addr, hp >h_length); memset(& cli_addr,0, sizeof ( cli_addr )); // mit 0en fuellen 35 cli_addr. sin_family = AF_INET; // TCP/IP Verbindung 36 cli_addr. sin_port = htons (0); // Port eintragen, Network Byte Order 37 cli_addr. sin_addr. s_addr = htonl (INADDR_ANY); if (( fd = socket (AF_INET, SOCK_DGRAM, 0)) <0) 40 perror ( "socket " ), exit (1); if ( bind ( fd, ( struct sockaddr ) & cli_addr, sizeof ( cli_addr )) <0) 44 perror ( "bind" ), exit (1); if ( sendto ( fd, "TIME", 5, 0, ( struct sockaddr ) & serv_addr, 47 sizeof ( serv_addr )) < 0) { 48 perror ( " Client : sendto" ); exit (2); 49 } shutdown( fd,1); 52

188 182 KAPITEL 6. BERKELEY SOCKETS 53 if (( n = recvfrom ( fd, buf, sizeof ( buf ),0, ( struct sockaddr ) 0, 54 ( int ) 0)) < 0 ) { 55 perror ( " Client : recvfrom"); exit (3); 56 } 57 buf [n] = \0 ; 58 printf ( "%. s", n, buf ); close ( fd ); 61 exit (0); 62 }

189 6.4. DIE SOCKET-PROGRAMMIERSCHNITTSTELLE 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: # include <sys/socket.h> int getsockname ( int sd, struct sockaddr * address, int * addresslen ); int getpeername ( int sd, struct sockaddr * address, int * addresslen ); Die Parameter sd, address, addresslen sind dabei in gleicher Weise wie in der Funktion accept() zu spezifizieren! 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.

190 184 KAPITEL 6. BERKELEY SOCKETS 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

191 6.5. KONSTRUKTION VON ADRESSEN 185 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.

192 186 KAPITEL 6. BERKELEY SOCKETS 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. 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).

193 6.5. KONSTRUKTION VON ADRESSEN 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

194 188 KAPITEL 6. BERKELEY SOCKETS Socket-Adressen in der Internet-Domäne Hier ist die Socket-Adressstruktur in netinet/in.h wie folgt definiert: 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. 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. 189). 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.

195 6.5. KONSTRUKTION VON ADRESSEN 189 sockaddr{} len family d a t a Bytes sockaddr_in{} len (16) AF_INET port addr zero \0 \0 \0 \0 \0 \0 \0 \ in_addr{} addr.s_addr 4 Bytes Abbildung 6.11: Organisation der Adress-Struktur sockaddr_in Byte-Ordnung 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: 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 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*/

196 190 KAPITEL 6. BERKELEY SOCKETS 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) 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:

197 6.5. KONSTRUKTION VON ADRESSEN 191 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));

198 192 KAPITEL 6. BERKELEY SOCKETS 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.

199 6.5. KONSTRUKTION VON ADRESSEN / 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 ; 16 Programm 6.6: Hostnamen ermitteln (hostent.c) 17 (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

200 194 KAPITEL 6. BERKELEY SOCKETS alias: loghost 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: application local hosts database gethostbyxyz() resolver local name server internet other name servers NIS resolver configuration Abbildung 6.13: Methoden der Adress-Resolution 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: 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

201 6.5. KONSTRUKTION VON ADRESSEN 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. Die Funktion gethostname(): # include <unistd.h> int gethostname(char * name, size_t namelen); 1 # include < stdio. h> 2 # include <unistd. h> 3 4 int main() { 5 Programm 6.7: Hostnamen bestimmen (gethost.c) 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

202 196 KAPITEL 6. BERKELEY SOCKETS 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: 1 # include < stdio. h> 2 # include <sys/utsname.h> 3 4 int main (){ 5 Programm 6.8: Funktion uname() (uname.c) 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 }

203 6.5. KONSTRUKTION VON ADRESSEN 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 */ }

204 198 KAPITEL 6. BERKELEY SOCKETS Die Zugriffsfunktionen: getservbyname() # include <netdb.h> struct servent * getservbyname( char * name, char * protocol ); getservbyport() # include <netdb.h> struct servent * getservbyport( int port; char * protocol ); 1 # include < stdio. h> 2 # include < stdlib. h> 3 # include <netdb. h> 4 # include < netinet /in. h> 5 Programm 6.9: Port und Protokoll eines Dienstes (getserv.c) 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 }

205 6.6. GEPUFFERTE EIN- / AUSGABE FÜR NETZWERKVERBINDUNGEN 199 Übersetzung aud Ausführung: thales$ gcc -Wall -o getserv -lxnet getserv.c thales$ getserv telnet tcp Offizieller Name des Dienstes: telnet Port-#: 23 Protokoll: tcp thales$ 6.6 Gepufferte Ein- / Ausgabe für Netzwerkverbindungen Die Ein- und Ausgabe über Netzwerkverbindungen bringt in Vergleich zur Behandlungen von Dateien und interaktiven Benutzern einige Veränderungen mit sich. Wenn SOCK_STREAM zum Einsatz gelangt, so kommen die Daten zwar in der korrekten Reihenfolge an, jedoch nicht in der ursprünglichen Paketisierung. Als ursprüngliche Pakete werden hier die Daten betrachtet, die mit Hilfe eines einzigen Aufrufs von write() geschrieben werden: const char greeting[] = "Hi, how are you?\r\n"; ssize_t nbytes = write(sfd, greeting, sizeof greeting); Wenn beispielsweise bei einer Netzwerkverbindung immer vollständige Zeilen mit write() geschrieben werden, ist es möglich, dass die korrespondierende read() -Operation nur einen Teil einer Zeile zurückliefert oder auch ein Fragment, das sich über mehr als eine Zeile erstreckt. Diese Problematik legt es nahe, nur zeichenweise einzulesen, wenn genau eine einzelne Zeile eingelesen werden soll: char ch; stralloc line = {0}; while (read(fd, &ch, sizeof ch) == 1 && ch!= \n ) { stralloc_append(&line, &ch); } Diese Vorgehensweise ist außerordentlich ineffizient, weil Systemaufrufe wie read() zu einem Kontextwechsel zwischen dem aufrufenden Prozess und dem Betriebssystem führen. Wenn ein Kontextwechsel für jedes einzulesende Byte initiiert wird, dann ist der betroffene Rechner mehr mit Kontextwechseln als mit sinnvollen Tätigkeiten beschäftigt. Wenn jedoch mit char buf[512]; ssize_t nbytes = read(fd, buf, sizeof buf) eingelesen wird, ist möglicherweise mehr als nur die gewünschte Zeile in buf zu finden, möglicherweise auch nur ein Teil der Zeile. Entsprechend ist eine gepufferte Eingabe notwendig, bei der die Eingabe-Operationen aus einem Puffer versorgt werden, der, wenn er leer wird, mit Hilfe einer read()-operation aufzufüllen ist. Die Datenstruktur für einen Eingabe-Puffer benötigt entsprechend einen Dateideskriptor, einen Puffer und einen Positionszeiger innerhalb des Puffers (siehe Abbildung 6.14, S. 200):

206 200 KAPITEL 6. BERKELEY SOCKETS 0 pos buf.len buf.a fd buf s len a pos Abbildung 6.14: Struktur eines Eingabepuffers typedef struct inbuf { int fd; stralloc buf; unsigned int pos; } inbuf; Entsprechend dem letzten erfolgreichen Aufruf von read() ergibt sich ein Füllgrad des Puffers, der von buf.len repräsentiert wird. Der Positionszeiger pos begann unmittelbar nach der read()-operation auf Position 0 und wandert bei jeder Einlese-Operation aus dem Puffer der Grenze von buf.len entgegen. Wird die Grenze erreicht, so ist die nächste read()-operation fällig. Programmtext 6.10 (S. 201) zeigt eine Schnittstelle für diesen Eingabepuffer. Die Funktion inbuf_alloc() dient dazu, die Größe des Puffers einzurichten, wobei eine sinnvolle Voreinstellung automatisch gewählt wird, wenn der Aufruf dieser Funktion unterbleibt. Als Einlese-Operationen vom Puffer dienen inbuf_read() und inbuf_getchar(), die sich in ihrer Aufrufsemantik an read() bzw. fgetc() orientieren. Ein zuviel gelesenes Zeichen kann mit inbuf_back() wieder zum erneuten Einlesen zur Verfügung gestellt werden. Mit inbuf_free() wird der Puffer freigegeben (de-alloziert).

207 6.6. GEPUFFERTE EIN- / AUSGABE FÜR NETZWERKVERBINDUNGEN # ifndef INBUF_H 2 # define INBUF_H 3 4 # include < stralloc. h> 5 # include <unistd. h> 6 7 typedef struct inbuf { 8 int fd ; 9 stralloc buf ; 10 unsigned int pos ; 11 } inbuf ; 12 Programm 6.10: Schnittstelle für einen Eingabe-Puffer (buffered/inbuf.h) 13 / set size of input buffer / 14 int inbuf_alloc ( inbuf ibuf, unsigned int size ); / works like read (2) but from ibuf / 17 ssize_t inbuf_read ( inbuf ibuf, void buf, size_t size ); / give buffer parameters it s just for demonstration / 20 int inbuf_pars ( inbuf ibuf, int len, int a, int pos ); / works like fgetc but from ibuf / 23 int inbuf_getchar ( inbuf ibuf ); / move backward one position / 26 int inbuf_back ( inbuf ibuf ); / release storage associated with ibuf / 29 void inbuf_free ( inbuf ibuf ); # endif

208 202 KAPITEL 6. BERKELEY SOCKETS 1 # include <errno. h> 2 # include < string.h> 3 # include <unistd. h> 4 # include "inbuf. h" 5 6 # include < stdio.h> 7 Programm 6.11: Implementierung des Eingabe-Puffers (buffered/inbuf.c) 8 / set size of input buffer / 9 int inbuf_alloc ( inbuf ibuf, unsigned int size ) { 10 ibuf >pos = 0; 11 ibuf >buf = ( stralloc ) {0}; 12 return stralloc_ready (& ibuf >buf, size ); 13 } / works like read (2) but from ibuf / 16 ssize_t inbuf_read ( inbuf ibuf, void buf, size_t size ) { 17 if ( size == 0) return 0; 18 if ( ibuf >pos >= ibuf >buf.len ) { 19 if ( ibuf >buf.a == 0 &&! inbuf_alloc ( ibuf, 512)) return 1; 20 / fill input buffer / 21 ssize_t nbytes ; 22 do { 23 errno = 0; 24 nbytes = read ( ibuf >fd, ibuf >buf.s, ibuf >buf.a ); 25 } while ( nbytes < 0 && errno == EINTR); 26 if ( nbytes <= 0) return nbytes ; 27 ibuf >buf.len = nbytes ; 28 ibuf >pos = 0; 29 } 30 ssize_t nbytes = ibuf >buf.len ibuf >pos; 31 if ( size < nbytes ) nbytes = size ; 32 memcpy(buf, ibuf >buf.s + ibuf >pos, nbytes ); 33 ibuf >pos += nbytes ; 34 return nbytes ; 35 } / give buffer parameters / 38 int inbuf_pars ( inbuf ibuf, int len, int a, int pos ) { 39 len = ibuf. buf. len ; a = ibuf. buf.a ; 40 pos = ibuf. pos ; 41 return 1; 42 } / works like fgetc but from ibuf / 46 int inbuf_getchar ( inbuf ibuf ) { 47 char ch ; 48 ssize_t nbytes = inbuf_read ( ibuf, &ch, sizeof ch ); 49 if ( nbytes <= 0) return 1; 50 return ch ; 51 } 52

209 6.6. GEPUFFERTE EIN- / AUSGABE FÜR NETZWERKVERBINDUNGEN / move backward one position / 54 int inbuf_back ( inbuf ibuf ) { 55 if ( ibuf >pos == 0) return 0; 56 ibuf >pos ; 57 return 1; 58 } / release storage associated with ibuf / 61 void inbuf_free ( inbuf ibuf ) { 62 stralloc_free (& ibuf >buf); 63 } Programmtext 6.11 (S. 202) zeigt die Implementierung der Schnittstelle für diesen Eingabepuffer. Hier ist insbesondere inbuf_read() interessant. In Zeile 14 wird untersucht, ob der Puffer bereits geleert ist, d.h. ob pos bereits buf.len erreicht hat; falls ja, wird in den Zeilen 15 bis 24 der Puffer neu gefüllt. In Zeile 15 wird zunächst untersucht, ob der Puffer bereits alloziert worden ist; falls nicht, wird dies mit der Standardgröße von 512 Bytes versucht. In Zeile 20 erfolgt die read()-operation, bei der grundsätzlich versucht wird, den gesamten Puffer zu füllen. Allerdings ist damit zu rechnen, dass die Zahl der tatsächlich gelesenen Bytes nbytes niedriger ist als buf.a. Dies liegt daran, dass das Betriebssystem bereits vorhandene Daten sofort zur Verfügung stellt, selbst wenn es sich um eine geringere Quantität als angefordert handelt. Dies stellt sicher, dass effizientes Einlesen mit größen Puffergrößen ohne unnötiges Blockieren möglich ist. Die read()-operation selbst ist in eine Schleife in den Zeilen 18 bis 21 eingebettet, die sicherstellt, dass es zu einem erneuten Versuch kommt, falls read() wegen einer Signalunterbrechung nicht erfolgreich sein konnte. Sobald sichergestellt ist, dass mindestens ein Byte in dem Puffer verfügbar ist, wird nbytes in Zeile 26 auf die maximal mögliche Rückgabequantität gesetzt. Wurden weniger verlangt, so wird nbytes in Zeile 27 entsprechend zurückgesetzt. In Zeile 28 wird die zurückzuliefernde Quantität an Bytes aus dem Puffer in buf mit Hilfe von memcpy() kopiert. Der erste Parameter (buf) zeigt dabei auf das Ziel, der zweite Parameter (buf.s + pos) auf die Quelle und der dritte Parameter gibt die Zahl der zu kopierenden Bytes an (nbytes). Nach der Kopieraktion wird pos entsprechend aktualisiert.

210 204 KAPITEL 6. BERKELEY SOCKETS Ausgabepufferung: Die Ausgabe sollte ebenfalls gepuffert erfolgen, um die Zahl der Systemaufrufe zu minimieren. Ein Positionszeiger ist nicht erforderlich, wenn Puffer grundsätzlich vollständig an write() übergeben werden. Das einzige Problem liegt hier darin, dass die write()-operation unter Umständen nicht den gesamten gewünschten Umfang akzeptiert und nur einen Teil der zu schreibenden Bytes akzeptiert und entsprechend eine geringere Quantität als Wert zurückgibt. 1 # ifndef OUTBUF_H 2 # define OUTBUF_H 3 4 # include < stralloc. h> 5 # include <unistd. h> 6 7 typedef struct outbuf { 8 int fd ; 9 stralloc buf ; 10 } outbuf ; 11 Programm 6.12: Schnittstelle für einen Ausgabe-Puffer (buffered/outbuf.h) 12 / works like write (2) but to obuf / 13 ssize_t outbuf_write ( outbuf obuf, void buf, size_t size ); / works like fputc but to obuf / 16 int outbuf_putchar ( outbuf obuf, char ch ); / write contents of obuf to the associated fd / 19 int outbuf_flush ( outbuf obuf ); / release storage associated with obuf / 22 void outbuf_free ( outbuf obuf ); # endif Programmtext 6.12 zeigt die Schnittstelle für einen Ausgabe-Puffer. Die Funktion outbuf_write() schreibt in den gegebenen Puffer und entspricht ansonsten dem Systemaufruf write(). Mit Hilfe von outbuf_putchar() können bequem einzelne Zeichen in den Puffer ausgegeben werden. Beide Schreiboperationen führen nur zur Verlängerung des Pufferinhalts, ohne dass dieser mit Hilfe einer write()-operation geleert wird. Letzteres ist nur durch den Aufruf von outbuf_flush() möglich. Wenn der Puffer nicht mehr benötigt wird, kann er durch outbuf_free() freigegeben werden.

211 6.6. GEPUFFERTE EIN- / AUSGABE FÜR NETZWERKVERBINDUNGEN # include <errno. h> 2 # include < stralloc. h> 3 # include < string.h> 4 # include "outbuf.h" 5 Programm 6.13: Implementierung des Ausgabe-Puffers (buffered/outbuf.c) 6 / works like write (2) but to obuf / 7 ssize_t outbuf_write ( outbuf obuf, void buf, size_t size ) { 8 if ( size == 0) return 0; 9 if (! stralloc_readyplus (&obuf >buf, size )) return 1; 10 memcpy(obuf >buf.s + obuf >buf.len, buf, size ); 11 obuf >buf.len += size ; 12 return size ; 13 } / works like fputc but to obuf / 16 int outbuf_putchar ( outbuf obuf, char ch) { 17 if ( outbuf_write ( obuf, &ch, sizeof ch) <= 0) return 1; 18 return ch ; 19 } / write contents of obuf to the associated fd / 22 int outbuf_flush ( outbuf obuf ) { 23 ssize_t left = obuf >buf.len ; ssize_t written = 0; 24 while ( left > 0) { 25 ssize_t nbytes ; 26 do { 27 errno = 0; 28 nbytes = write ( obuf >fd, obuf >buf.s + written, left ); 29 } while ( nbytes < 0 && errno == EINTR); 30 if ( nbytes <= 0) return 0; 31 left = nbytes ; written += nbytes ; 32 } 33 obuf >buf.len = 0; 34 return 1; 35 } / release storage associated with obuf / 38 void outbuf_free ( outbuf obuf ) { 39 stralloc_free (&obuf >buf); 40 } Erläuterungen zu Programm 6.13 (S. 205): In outbuf_write() wird in Zeile 9 darauf geachtet, dass der Puffer genügend Platz für den aufzunehmenden Inhalt aufweist, wonach in Zeile 10 der Kopiervorgang mit Hilfe von memcpy() durchgeführt werden kann. Danach muss nur noch buf.len in Zeile 11 angepasst werden. In der Funktion outbuf_flush() gibt es zwei Schleifen. Die äußere Schleife in den Zeilen 24 bis 32 sorgt dafür, dass der gesamte Puffer-Inhalt geschrieben wird, da einzelne

212 206 KAPITEL 6. BERKELEY SOCKETS write()-operationen die Freiheit haben, nur einen Teil umzusetzen. Mit Hilfe der Variablen left und written wird vermerkt, wieviel noch zu schreiben ist bzw. wieviel bereits geschrieben wurde. Die innere Schleife in den Zeilen 26 bis 29 wiederholt die write()- Operation im Falle von Unterbrechungen. 6.7 Ein kleiner TCP-Server (concurrent) mit Eingabe-Pufferung Programm 6.14: Headerfile für Netzwerkverbindung (buffered/inet.h) 1 // Definitions for TCP and UDP client / server programs 2 3 # define SERV_UDP_PORT # define SERV_TCP_PORT // # define SERV_HOST_ADDR " " 6 / thales / 7 # define SERV_HOST_ADDR " " 8 / local host / Programm 6.15: Hauptprogramm des TCP-Servers mit Eingabepufferung (buffered/main-srv.c) 1 # include " inet. h" 2 # include < stdio. h> 3 # include <unistd. h> 4 # include < stdlib. h> 5 # include < signal. h> 6 # include < string. h> 7 # include < strings. h> 8 # include <errno. h> 9 # include <sys/types. h> 10 # include <sys/ socket. h> 11 # include < netinet /in. h> 12 # include <arpa/ inet. h> extern void str_echo ( int ); int main( int argc, char argv) { int sockfd, newsockfd, clilen, childpid ; 21 struct sigaction action ; 22 struct sockaddr_in cli_addr, serv_addr ; / our childs shall not become zombies / 25 action. sa_handler = SIG_IGN; 26 action. sa_flags = SA_NOCLDWAIT; 27 if ( sigaction (SIGCHLD, &action, 0) < 0) exit (1); if ( ( sockfd = socket (AF_INET, SOCK_STREAM,0)) < 0) { 31 perror ( " server : can t open stream socket"); 32 exit (1);

213 6.7. EIN KLEINER TCP-SERVER (CONCURRENT) MIT EINGABE-PUFFERUNG } bzero (( char ) &serv_addr, sizeof ( serv_addr )); serv_addr. sin_family = AF_INET; 38 serv_addr. sin_addr. s_addr = htonl (INADDR_ANY); 39 / INADR_ANY: tells the system that we ll accept a connection 40 on any Internet interface on the system, if it is multihomed 41 Address to accept any incoming messages ( > in. h ). 42 INADDR_ANY is defined as (( unsigned long int ) 0x ) 43 / serv_addr. sin_port = htons (SERV_TCP_PORT); if ( bind ( sockfd, ( struct sockaddr )&serv_addr, 48 sizeof ( serv_addr )) < 0) { 49 perror ( " server : can t bind local address" ); 50 exit (2); 51 } listen ( sockfd,5); while(1) { 56 / 57 wait for a connection from a client process 58 concurrent server 59 / 60 clilen = sizeof ( cli_addr ); 61 newsockfd = accept ( sockfd, ( struct sockaddr ) & cli_addr, 62 & clilen ); if ( newsockfd < 0 ) { 65 if ( errno == EINTR) 66 continue; / try again / 67 perror ( " server : accept error" ); 68 exit (3); 69 } if ( ( childpid = fork ()) < 0) { 72 perror ( " server : fork error" ); 73 close ( sockfd ); 74 close ( newsockfd ); 75 exit (4); 76 } 77 else if ( childpid == 0) { 78 close ( sockfd ); 79 str_echo ( newsockfd ); 80 exit (0); 81 } close ( newsockfd ); / parent / 84 } 85 exit (0); 86 }

214 208 KAPITEL 6. BERKELEY SOCKETS

215 6.7. EIN KLEINER TCP-SERVER (CONCURRENT) MIT EINGABE-PUFFERUNG 209 Programm 6.16: Funktionalität des TCP-Servers mit Eingabepufferung (buffered/str-echo.c) 1 # include <unistd. h> 2 # include < stdio. h> 3 # include < stdlib. h> 4 # include "inbuf. h" 5 6 # define MAXLINE void str_echo ( int sockfd ) { 9 int n; 10 / for testing : 11 int a, len, pos ; 12 / 13 char line [MAXLINE]; 14 inbuf in ; 15 inbuf_alloc (&in, 1024); 16 in. fd = sockfd ; 17 / for testing : 18 inbuf_pars ( in, & len, & a, & pos ); 19 fprintf ( stderr, "\nserver : len = %d, a = %d, pos = %d\n", 20 len,a, pos ); 21 / while(1) { 24 n = inbuf_read (&in, line, MAXLINE); 25 / for testing : 26 inbuf_pars (in, & len, & a, & pos ); 27 fprintf ( stderr, "\nserver : len = %d, a = %d, pos = %d, n = %d\n", 28 len,a, pos,n ); 29 / 30 if (n == 0) 31 return ; / connection terminated / 32 else if ( n < 0 ) { 33 perror ( " str_echo : read error" ); 34 exit (5); 35 } 36 / write to server s stderr : / 37 write (2, line,n ); if ( write ( sockfd, line, n)!= n) { 40 perror ( " str_echo : write error" ); 41 exit (6); 42 } 43 } 44 }

216 210 KAPITEL 6. BERKELEY SOCKETS Programm 6.17: Hauptprogramm des TCP-Clients mit Eingabepufferung (buffered/main-cli.c) 1 # include " inet. h" 2 # include < stdio.h> 3 # include <unistd. h> 4 # include < stdlib.h> 5 # include < signal.h> 6 # include < string.h> 7 # include < strings.h> 8 # include <errno. h> 9 # include <sys/types. h> 10 # include <sys/ socket. h> 11 # include < netinet /in.h> 12 # include <arpa/ inet. h> extern void str_cli (FILE, int ); int main( int argc, char argv ){ 17 int sockfd ; FILE fp ; 18 struct sockaddr_in serv_addr ; if ( argc!= 2 ) { 21 fprintf ( stderr, "usage: %s file\n", argv [0]); 22 exit (1); 23 } 24 if ( ( fp = fopen (argv [1], "r") ) == NULL) 25 perror ( "fopen" ), exit (2); bzero ( (char ) &serv_addr, sizeof ( serv_addr )); 28 serv_addr. sin_family = AF_INET; 29 serv_addr. sin_addr. s_addr = inet_addr (SERV_HOST_ADDR); 30 serv_addr. sin_port = htons (SERV_TCP_PORT); if ( ( sockfd = socket (AF_INET, SOCK_STREAM,0)) < 0 ) { 33 perror ( " client : can t open stream socket"); 34 exit (3); 35 } if ( connect ( sockfd, ( struct sockaddr ) &serv_addr, 38 sizeof ( serv_addr ) ) < 0) { 39 perror ( " client : can t connect to server" ); 40 exit (4); 41 } str_cli ( fp, sockfd ); close ( sockfd ); 46 exit (0); 47 }

217 6.7. EIN KLEINER TCP-SERVER (CONCURRENT) MIT EINGABE-PUFFERUNG 211 Programm 6.18: Funktionalität des TCP-Clients mit Eingabepufferung (buffered/str-cli.c) 1 / str_cli. c 2 function used by connection oriented clients 3 4 read the contents of the FILE fp, write each line to the 5 stream socket ( to the server process ), then read a line back 6 from the socket and write it to stdout 7 8 return to caller when an EOF is encountered on the input file 9 / # include < stdio.h> 12 # include <unistd. h> 13 # include < stdlib.h> 14 # include < string.h> 15 # include <sys/ socket. h> 16 # include "inbuf. h" # define MAXLINE void str_cli (FILE fp, int sockfd ) { 21 int n; 22 char sendline [MAXLINE], recvline[maxline+1]; inbuf in ; 25 inbuf_alloc (&in, 512); 26 in. fd = sockfd ; 27 while ( fgets ( sendline, MAXLINE,fp)!= NULL) { 28 n = strlen ( sendline ); 29 if ( write ( sockfd, sendline,n)!= n) { 30 perror ( " str_cli : write error on socket"); 31 exit (4); 32 } / 35 now read a line from the socket and 36 write it to stdout 37 / 38 n = inbuf_read (&in, recvline, MAXLINE); 39 if (n < 0) { 40 perror ( " str_cli : read error" ); 41 exit (5); 42 } 43 write (1, recvline, n ); 44 } 45 shutdown( sockfd,1); 46 return ; 47 }

218 212 KAPITEL 6. BERKELEY SOCKETS 6.8 Ein- / Ausgabe von Paketen für Netzwerkverbindungen Zwischen Dienste-Anbietern und ihren Klienten auf dem Netzwerk besteht häufig ein ähnliches Verhältnis wie zwischen einer Shell und dem zugehörigen Benutzer: Der Klient gibt ein Kommando, das typischerweise mit dem Zeilentrenner CR LF 1 beendet wird, und der Dienste- Anbieter sendet darauf eine Antwort zurück, die zum Ausdruck bringt, ob das Kommando erfolgreich verlief oder fehlschlug, und einen Antworttext über eine oder mehrere Zeilen. Es gibt keine zwingende Notwendigkeit, bei einem Protokoll Zeilentrenner zu verwenden. Es gibt auch Alternativen wie die von Dan Bernstein vorgeschlagenen Net-Strings 2, jedoch erlaubt die Konvention mit dem Zeilentrenner CR LF die interaktive Benutzung eines Dienstes mit dem telnet-kommando. Bei jedem Protokoll lohnt es sich, eine gewisse Grundstruktur über die generelle Syntax von Kommandos und deren Antworten festzulegen. Hier ist ein Beispiel für eine (nicht sehr konstruktive) Sitzung mit einem Dienst, über den s zugestellt werden können. Zum Einsatz kommt hier das SMTP-Protokoll 3 : doolin$ telnet mail.rz.uni-ulm.de smtp Trying Connected to mail.rz.uni-ulm.de. Escape character is ^]. 220 mail.rz.uni-ulm.de ESMTP Sendmail / help This is sendmail version Topics: HELO EHLO MAIL RCPT DATA RSET NOOP QUIT HELP VRFY EXPN VERB ETRN DSN AUTH STARTTLS For more info use "HELP <topic>" To report bugs in the implementation send to [email protected] For local information send to Postmaster at your site End of HELP info huhu Command unrecognized: "huhu" helo doolin.andreas-borchert.de 250 mail.rz.uni-ulm.de Hello doolin.andreas-borchert.de quit mail.rz.uni-ulm.de closing connection Connection to mail.rz.uni-ulm.de closed by foreign host. doolin$ 1 carriage return, gefolgt von line feed. 2 Siehe 3 SMTP steht für simple mail transfer protocol, siehe RFC 2821

219 6.8. EIN- / AUSGABE VON PAKETEN FÜR NETZWERKVERBINDUNGEN 213 Beim SMTP-Protokoll erfolgt zunächst eine Begrüßung des Dienste-Anbieters. Die Begrüßung oder auch eine andere Antwort des Anbieters besteht aus einer dreistelligen Nummer, einem Leerzeichen oder einem Minus und beliebigem Text, der durch CR LF abgeschlossen wird. Die erste Ziffer der dreistelligen Nummer legt hier fest, ob ein Erfolg oder ein Problem vorliegt. Die beiden weiteren Ziffern werden zur feineren Unterscheidung der Rückmeldung verwendet. Eine führende 2 bedeutet Erfolg, eine 4 signalisiert ein temporäres Problem und eine 5 signalisiert einen permanenten Fehler. In der Beispielsitzung ist das erste Kommando ein help, gefolgt von CR LF. Da die Antwort sich über mehrere Zeilen erstreckt, werden alle Zeilen, hinter der noch mindestens eine folgt, mit einem Minuszeichen hinter der dreistelligen Zahl gekennzeichnet. Danach wurde mit dem unbekannten Kommando huhu eine Fehlermeldung provoziert, die durch den Code 500 signalisiert wurde. Das SMTP-Protokoll erlaubt auch eine Fortsetzung des Dialogs nach Fehlern, so dass dann noch ein helo -Kommando akzeptiert wurde. Die Verbindung wurde mit dem quit -Befehl beendet. Programm 6.19: Schnittstelle für MXP-Anfrage-Pakete (MXP/mxprequest.h) 1 # ifndef MXP_REQUEST_H 2 # define MXP_REQUEST_H 3 4 # include < stralloc. h> 5 # include "inbuf. h" 6 # include "outbuf.h" 7 8 typedef struct mxp_request { 9 stralloc keyword; 10 stralloc parameter ; 11 } mxp_request ; / read one request from the given input buffer / 14 int read_mxp_request ( inbuf ibuf, mxp_request request ); / write one request to the given outbuf buffer / 17 int write_mxp_request ( outbuf obuf, mxp_request request ); / release resources associated with request / 20 void free_mxp_request ( mxp_request request ); # endif

220 214 KAPITEL 6. BERKELEY SOCKETS Programm 6.20: Schnittstelle für MXP-Antwort-Pakete (MXP/mxpresponse.h) 1 # ifndef MXP_RESPONSE_H 2 # define MXP_RESPONSE_H 3 4 # include "inbuf. h" 5 # include "outbuf.h" 6 7 typedef enum mxp_status { 8 MXP_SUCCESS = S, 9 MXP_FAILURE = F, 10 MXP_CONTINUATION = C, 11 } mxp_status ; typedef struct mxp_response { 14 mxp_status status ; 15 stralloc message ; 16 } mxp_response ; / write one ( possibly partial ) response to the given output buffer / 19 int write_mxp_response ( outbuf obuf, mxp_response response ); / read one ( possibly partial ) response from the given input buffer / 22 int read_mxp_response ( inbuf ibuf, mxp_response response ); void free_mxp_response ( mxp_response response ); # endif Ungeachtet seines Namens ist das SMTP-Protokoll nicht mehr sehr einfach. Um ein überschaubareres Beispiel zu erhalten, wird im weiteren Text von folgenden Konventionen ausgegangen: Kommandos bestehen aus einem Kommandonamen (nur aus Kleinbuchstaben), einem Leerzeichen und genau einem Parameter. Abgeschlossen wird ein Kommando durch die Sequenz CR LF. Antworten beginnen mit S, F oder C, gefolgt von einem beliebigen Text, der durch CR LF abgeschlossen wird. S deutet einen Erfolg an (success), F einen Fehlschlag (failure) und C gibt an, dass noch weitere Zeilen folgen. Sobald dieser syntaktische Rahmen festliegt, ist es möglich, entsprechende Datenstrukturen für Anfragen und deren Antworten zu deklarieren und zugehörige Ein- und Ausgabefunktionen anzubieten wie Programmtext 6.19 (S. 213) und 6.20 (S. 214) und zeigen. 4 Es ist dabei sinnvoll, jeweils die Ein- und Ausgabefunktionen zusammen zu belassen, da beide Funktionen jeweils genau zusammenpassen müssen. 4 MXP steht hierbei für das mutual exclusion protocol, das im übernächsten Abschnitt vorgestellt wird.

221 6.8. EIN- / AUSGABE VON PAKETEN FÜR NETZWERKVERBINDUNGEN 215 Programm 6.21: Das Einlesen eines MXP-Anfrage-Pakets (MXP/mxprequest.c) 1 # include "inbuf. h" 2 # include "outbuf.h" 3 # include "mxprequest.h" 4 5 static int read_keyword ( inbuf ibuf, stralloc keyword) { 6 int ch ; 7 if (! stralloc_copys (keyword, "" )) return 0; 8 while (( ch = inbuf_getchar ( ibuf )) >= 0 && 9 ch >= a && ch <= z ) { 10 if (! stralloc_readyplus (keyword, 1)) return 0; 11 keyword >s[keyword >len++] = ch; 12 } 13 if ( ch >= 0) inbuf_back ( ibuf ); 14 return keyword >len > 0; 15 } static int read_parameter ( inbuf ibuf, stralloc parameter ) { 18 int ch ; 19 if (! stralloc_copys ( parameter, "" )) return 0; 20 while (( ch = inbuf_getchar ( ibuf )) >= 0 && 21 ch!= \r && ch!= \n && ch!= / && ch!= \0 ) { 22 if (! stralloc_readyplus ( parameter, 1)) return 0; 23 parameter >s[parameter >len++] = ch ; 24 } 25 if ( ch >= 0) inbuf_back ( ibuf ); 26 return parameter >len > 0; 27 } static int expect_delimiter ( inbuf ibuf, char delimiter ) { 30 int ch = inbuf_getchar ( ibuf ); 31 if ( ch < 0) return 0; 32 return ch == delimiter ; 33 } / read one request from the given input buffer / 36 int read_mxp_request ( inbuf ibuf, mxp_request request ) { 37 return 38 read_keyword ( ibuf, & request >keyword) && 39 expect_delimiter ( ibuf, ) && 40 read_parameter ( ibuf, & request >parameter) && 41 expect_delimiter ( ibuf, \r ) && 42 expect_delimiter ( ibuf, \n ); 43 } / write one request to the given outbuf buffer / 46 int write_mxp_request ( outbuf obuf, mxp_request request ) { 47 return 48 outbuf_write ( obuf, request >keyword.s, 49 request >keyword.len) == request >keyword.len && 50 outbuf_putchar ( obuf, ) == && 51 outbuf_write ( obuf, request >parameter.s, 52 request >parameter. len ) == request >parameter. len &&

222 216 KAPITEL 6. BERKELEY SOCKETS 53 outbuf_putchar ( obuf, \r ) == \r && 54 outbuf_putchar ( obuf, \n ) == \n ; 55 } / release resources associated with request / 58 void free_mxp_request ( mxp_request request ) { 59 stralloc_free (& request >keyword); 60 stralloc_free (& request >parameter); 61 } Programm 6.21 (S. 215) demonstriert das Einlesen eines Anfrage-Pakets: Da eine Anfrage aus einem Kommandonamen, einem Leerzeichen, dem Parameter und den Zeilenterminator besteht, wurde das Einlesen entsprechend aufgeteilt. Die Funktionen read_keyword() und read_parameter() lesen jeweils ihren Teil in ein stralloc-objekt ein und achten mit Hilfe von inbuf_back() darauf, dass das jeweilige terminierende Zeichen anschließend zum Einlesen wieder zur Verfügung steht. Die Funktion expect_delimiter() liest ein Zeichen ein und vergleicht es mit dem erwarteten Trennzeichen. Auf diese Weise lassen sich Leerzeichen und der Zeilentrenner überlesen. Übertriebene Toleranz bezüglich mehreren Leerzeichen oder verschiedenen Varianten bei Zeilentrennern ist hier fehl am Platze, da dies nur den Programmtext verkompliziert ohne einen wahren Vorteil zu bringen, da primär nur Programme über dieses Protokoll kommunizieren. Zu beachten ist, dass outbuf_flush() nicht von write_mxp_request() aufgerufen wird. Die Kontrolle darüber, wann wirklich der Ausgabepuffer geleert wird, verbleibt so beim Kern des Programmtexts, der mit dem Protokoll umgeht. Programm 6.22: Das Einlesen eines MXP-Antwort-Pakets (MXP/mxpresponse.c) 1 # include "inbuf. h" 2 # include "mxpresponse.h" 3 # include "outbuf.h" 4 5 static int read_message ( inbuf ibuf, stralloc message) { 6 int ch ; 7 if (! stralloc_copys ( message, "" )) return 0; 8 while (( ch = inbuf_getchar ( ibuf )) >= 0 && 9 ch!= \r && ch!= \n ) { 10 if (! stralloc_readyplus ( message, 1)) return 0; 11 message >s[message >len++] = ch; 12 } 13 if ( ch >= 0) inbuf_back ( ibuf ); 14 return 1; 15 } static int expect_delimiter ( inbuf ibuf, char delimiter ) { 18 int ch = inbuf_getchar ( ibuf ); 19 if ( ch < 0) return 0; 20 return ch == delimiter ; 21 }

223 6.9. PARALLELE SITZUNGEN / read one ( possibly partial ) response from the given input buffer / 24 int read_mxp_response ( inbuf ibuf, mxp_response response ) { 25 int ch = inbuf_getchar ( ibuf ); 26 switch (ch) { 27 case MXP_SUCCESS: 28 case MXP_FAILURE: 29 case MXP_CONTINUATION: 30 response >status = ch ; 31 break; 32 default : 33 return 0; 34 } 35 if (! read_message ( ibuf, &response >message)) return 0; 36 if (! expect_delimiter ( ibuf, \r )) return 0; 37 if (! expect_delimiter ( ibuf, \n )) return 0; 38 return 1; 39 } / write one ( possibly partial ) response to the given output buffer / 42 int write_mxp_response ( outbuf obuf, mxp_response response ) { 43 if ( outbuf_putchar ( obuf, response >status) < 0) return 0; 44 if ( response >message.len > 0) { 45 ssize_t nbytes = outbuf_write ( obuf, response >message.s, 46 response >message.len ); 47 if ( nbytes!= response >message.len) return 0; 48 } 49 return 50 outbuf_putchar ( obuf, \r ) >= 0 && 51 outbuf_putchar ( obuf, \n ) >= 0; 52 } void free_mxp_response ( mxp_response response ) { 55 stralloc_free (& response >message); 56 } Programm 6.22 (S. 216) zeigt analog das Einlesen eines Antwort-Pakets. 6.9 Parallele Sitzungen Der zuvor mit dem Programmtext 6.1 (auf Seite 153) vorgestellte Zeitdienst wartete mit accept() auf den nächsten Aufruf und kümmerte sich dann ausschließlich um diesen. Da eine Sitzung des Zeitdienstes ohne Interaktionen auskommt und nur aus der Zeitansage besteht, führte dies nicht zu Problemen bei mehreren parallelen Anfragen. Entsprechend der bei listen() angegebenen Zahl (mit SOMAXCONN wird normalerweise das Maximum gewählt) ist es möglich, weitere Anrufe in eine Warteschlange einzureihen, bis die derzeitig laufende Sitzung beendet ist und accept() erneut aufgerufen wird. Man spricht hier von einem iterative server. Dies ist jedoch bei längeren Sitzungen nicht mehr akzeptabel. Es gibt drei Ansätze, um parallele Sitzungen zu ermöglichen:

224 218 KAPITEL 6. BERKELEY SOCKETS Für jede neue Sitzung wird mit Hilfe von fork() ein neuer Prozess erzeugt, der sich um die Verbindung zu genau einem Klienten kümmert (concurrent server). siehe Programm 6.15, S. 206 Für jede neue Sitzung wird ein neuer Thread gestartet. Sämtliche Ein- und Ausgabe-Operationen werden asynchron abgewickelt mit Hilfe der O_NONBLOCK-Option und der Verwendung von poll() oder select(). Hierbei blockieren Ein- und Ausgabe-Operationen nicht mehr, sondern geben eine Fehlerindikation zurück, falls noch keine Eingabe vorliegt bzw. eine Ausgabe momentan noch nicht akzeptiert werden kann. Mit Hilfe von poll() oder select() ist es dann möglich, bei einer Vielzahl von Dateideskriptoren darauf zu waren, dass eine Lese- oder Schreib-Operation möglich ist. Die erste Methode ist am einfachsten umzusetzen. Normalerweise wird nur dann eine der beiden anderen Varianten gewählt, wenn die parallel laufenden Sitzungen auf gemeinsame Datenstrukturen zugreifen möchten. Die erste Variante ist so gängig, dass es sich lohnt, sie in verallgemeinerter Form zur Verfügung zu stellen. Von Dan J. Bernstein gibt es hierfür ein Werkzeug namens tcpserver 5, das die gesamte Sequenz von socket() bis accept() so verpackt, dass der eigentliche Dienst die gesamte Kommunikation über die Standard-Ein- und Ausgabe abwickeln kann. Auf diese Weise lassen sich sogar relativ einfach kleine Dienste auf Basis bestehender Kommandos einrichten. Mit einem ersten Beispiel echo-server od -bc soll dies demonstriert werden. Wir starten in einem xterm den Server: spatz$ tcpserver /bin/sh -c \ > while read x; do echo $x od -bc; done ^C spatz$ Erläuterungen: Allgemeine Aufrufsyntax: tcpserver opts host port prog Zu opts (Optionen) siehe: Das erste Argument host von tcpserver (hier: 0): IP-Adresse des Servers (hier: INADDR_ANY) Das zweite Argument: TCP-Port-Nummer (hier: 11011) Das dritte Argument prog: Für jede eingehende Verbindung wird ein neuer Prozess erzeugt, bei dem Standard-Ein- (0) und Ausgabe (1) mit der Netzwerkverbindung verknüpft werden. Anschließend erfolgt ein execvp() zum angegebenen Kommando hier ist das Programm die Shell /bin/sh/: Mit der Option -c erhält die Shell das auszuführende Kommando auf der Argumentzeile Die Quotierung in einfache Apostrophen hat die Bedeutung, dass das eingechlossene zum einen nicht sofort von der aktuellen Shell interpretiert wird und zum anderen das dazwischenstehende so wie es ist als ein Argument an /bin/sh übergeben wird. 5 Siehe

225 6.9. PARALLELE SITZUNGEN 219 Die Shell als Kommando-Interpreter ist auch eine Programmiersprache ; die while- Schleife lautet: while <<statement>> do <<statements>> done oder kurz while <<statement>> ; do <statements>> ; done Ist der Exit-Status des Kommandos nach while 0, entspricht dies dem Bool schen Wert true, jeder andere Exit-Status ist false Das built-in-kommado read liest eine Zeile von der Standardeingabe in die angegebene Variable (hier: x) das Kommando echo gibt sein Argument (hier: das, was nach der Substitution der Variablen x durch ihren Wert ergibt) an die Standardausgabe (innerhalb des Shell- Kommandos!) Das Kommando od ist ein Filter und wandelt seine Ausgabe in Oktaldarstellung Nun starten wir in einem anderen xterm einen Client: spatz$ telnet spatz Trying Connected to spatz. Escape character is ^]. anton a n t o n \r \n \r \n Connection closed by foreign host. spatz$ Erläuterungen: Erste Eingabe hier ist anton, die zweite 12 Man beachte die letzten beiden Zeichen in der Ausgabe des Servers: \r\n Das Kommando od kann nun durch andere (passende) Kommandos ersetzt werden. Hier ist ein Beispiel auf Basis des factor-kommandos (siehe man factor), das ganze Zahlen in ihre Primteiler zerlegt. Bevor wir dies einfügen, sei es zunächst einmal demonstriert:

226 220 KAPITEL 6. BERKELEY SOCKETS spatz$ echo 12 factor 12: spatz$ echo 12x tr x \r factor factor: 12 is not a valid positive integer spatz$ echo 12x factor factor: 12x is not a valid positive integer spatz$ Obiges war auf einem Linux-Rechner dasselbe auf der thales: thales$ echo 12 factor thales$ echo 12x tr x \r factor thales$ echo 12x factor thales$ Starten wir uns factor-server auf obigem Linux-Rechner, so sollten wir auf das Zeichen \r achten: spatz$ tcpserver /bin/sh -c \ > while read x; do echo $x tr \\r \ factor ; done ^c spatz$ Anm.: Das Kommando tr (translate) transformiert Zeichen(-bereiche) Es soll das Zeichen \r (ein Zeichen) in ein Blank umsetzen; dazu wird die Backslah-Quotierung verwendet! Auf der thales können wir uns den Zwischenschritt mit tr sparen!

227 6.9. PARALLELE SITZUNGEN 221 Prinzipiell wäre es natürlich auch möglich, factor direkt von tcpserver aufrufen zu lassen. Wegen der gepufferten Ausgabe von factor würden wir die Faktorisierung jedoch zu spät oder überhaupt nicht zu sehen bekommen. Wenn hingegen für jede Eingabezeile ein Kommandoaufruf erfolgt, ist damit auch ein implizites Leeren des Ausgabe-Puffers verbunden, sobald das Kommando beendet ist. Das Problem tritt bei einer normalen interaktiven Verwendung nicht auf, da die stdio implizit bei der Verwendung eines Terminals auf eine zeilenweise Pufferung umschaltet. Dies geschieht jedoch nicht bei Netzwerkverbindungen! Die Kontrollstruktur, die tcpserver auf der Ebene der Kommandozeile liefert, lässt sich ebenso in genereller Form als Bibliotheksfunktion repräsentieren. Programmtext 6.23 (S. 221) zeigt eine entsprechende Schnittstelle und 6.24 (S. 221) die zugehörige Implementierung. Genauso wie tcpserver benötigt die Funktion run_service() die lokal zu verwendende Adresse. Der Einfachheit halber wird hier allerdings nur die Portnummer als Parameter übergeben und implizit INADDR_ANY verwendet. Für jeden eingehenden Anruf soll run_service() einen neuen Prozess mit fork() erzeugen und in diesem die als Parameter übergebene Funktion handler aufrufen. Diese Funktion erhält als Parameter den Dateideskriptor der offenen Netzwerkverbindung und analog zu tcpserver die verbliebenen Kommandozeilenparameter. Genau wie tcpserver endet run_service() nur im Fehlerfalle und läuft ansonsten endlos. Programm 6.23: Schnittstelle für die Kontrollstruktur von Netzwerkdiensten (service.h) 1 # ifndef SERVICE_H 2 # define SERVICE_H 3 4 # include < netinet /in.h> 5 6 typedef void ( session_handler )( int fd, int argc, char argv ); 7 8 / 9 listen on the given port and invoke the handler for each 10 incoming connection 11 / 12 void run_service ( in_port_t port, session_handler handler, 13 int argc, char argv ); # endif

228 222 KAPITEL 6. BERKELEY SOCKETS Programm 6.24: Implementierung für die Kontrollstruktur von Netzwerkdiensten (service.c) 1 # include < netinet /in.h> 2 # include < signal.h> 3 # include < stdio.h> 4 # include < stdlib.h> 5 # include < string.h> 6 # include <sys/ socket. h> 7 # include <sys/time. h> 8 # include <time. h> 9 # include <unistd. h> 10 # include " service. h" / 13 listen on the given port and invoke the handler for each 14 incoming connection 15 / 16 void run_service ( in_port_t port, session_handler handler, 17 int argc, char argv) { 18 struct sockaddr_in address = {0}; 19 address. sin_family = AF_INET; 20 address. sin_addr. s_addr = htonl (INADDR_ANY); 21 address. sin_port = htons ( port ); int sfd = socket (PF_INET, SOCK_STREAM, 0); 24 int optval = 1; 25 if ( sfd < 0 26 setsockopt ( sfd, SOL_SOCKET, SO_REUSEADDR, 27 &optval, sizeof optval ) < 0 28 bind ( sfd, ( struct sockaddr ) &address, 29 sizeof address ) < 0 30 listen ( sfd, SOMAXCONN) < 0) { 31 return ; 32 } / our childs shall not become zombies / 35 struct sigaction action = {0}; 36 action. sa_handler = SIG_IGN; 37 action. sa_flags = SA_NOCLDWAIT; 38 if ( sigaction (SIGCHLD, &action, 0) < 0) return ; int fd ; 41 while (( fd = accept ( sfd, 0, 0)) >= 0) { 42 pid_t child = fork (); 43 if ( child == 0) { 44 handler ( fd, argc, argv ); 45 exit (0); 46 } 47 close ( fd ); 48 } 49 }

229 Kapitel 7 Threads 7.1 Einführung thread: Faden, Faser, Gedankengang, Strang Probleme mit fork(): fork() ist teuer: es wird Speicher vom Erzeuger zum Kind kopiert, Desktiptoren werden dupliziert,... Aktuelle Implementierungen nutzen zwar copy-on-write, d.h. Speicher wird erst dann kopiert, wenn eigene Kopie benötigt wird fork() ist dennoch teuer! IPC wird benötigt, um zwischen Erzeuger und Kinde Daten auszutauschen (nach dem fork()). Vor dem fork Daten austauschen geht einfach, da das Kind ja mit einer (fast vollständigen) Kopie des Erzeugers startet. Rückgabe von Informationen vom Kind zum Erzeuger ist nicht so einfach! Lösung: Threads (leichtgewichtige Prozesse), deren Erzeugung 10 bis 100 mal schneller geht ([Stevens04]) Alle Threads innerhalb eines Prozesses teilen sich denselben globalen Speicher (sprich die globalen Variablen) damit ist der Informationsaustausch einfach. Aber: Synchronisation ist notwendig! Alle Threads innerhalb eines Prozesses teilen sich weiterhin Prozess-Anweisungen (Text) Fast alle Daten 223

230 224 KAPITEL 7. THREADS Offene Dateiverbindungen (UFDT) Signalbehandler und Signaleinstellungen Aktuellen Arbeitskatalog User und Group IDs Jeder Thread hat für sich allein eine Thread-ID einen Satz von Registern (Befehlszähler, Stack-Pointer) die Variable errno Signal-Maske (siehe sigaction()) eine Priorität Nach Stevens[04]: One analogy is to think of signal handlers as a tyep of thread as we discussed in.... That is, in the traditional Unix model, we have the main flow of execution (one thread) and a signal handler (another thread). If the main flow of execution is in the midlle of updating a linked list when a signal occurs, and the signal handler also tries to update the linked list, havoc 1 normally results. The main flow and signal handler share the same global variables, but each has its own stack. Im folgenden werden Threads nach dem POSIX Standard (1995 als Teil des PO- SIX.1c Standards standardisiert) behandelt. Alle Funktionen beginnen mit pthread_ POSIC Thread Library: libptread gcc -Wall example.c -lpthread 1 Chaos, Verwüstung

231 7.2. GRUNDLEGENDE FUNKTIONEN: ERZEUGEN UND BEENDEN 225 one process one thread one process multiple threads multiple processes one thread per process multiple processes multiple threads per process Abbildung 7.1: Multithreading vs. Single threading 7.2 Grundlegende Funktionen: Erzeugen und Beenden pthread_create() Wir ein Programm via exec gestartet, wird ein einzelner Thread gestartet: initial thread oder main thread # include <pthread.h> int pthread_create(pthread_t * tid, pthread_attr_t * attr, void * (*func)(void *), void * arg); /* returns 0 on success, positive Exxx value on error */ Jeder Thread innerhalb eines Prozess hat eine eindeutige ID (* tid) (Typ pthread_t, meist unsigned int) Jeder Thread hat zahlreiche Attribute: Priorität, anfängliche Stack-Größe, Dämen-Thread ja/nein,... Diese Attribute können über den Parameter attr spezifiziert werden i.a. reicht die default- Einstellung, daher wird als formaler Parameter hier der Null-Zeiger angegeben Der dritte Parameter spezifiziert die auszuführende Start-Routine: der Thread startet mit der Ausführung dieser Routine und terminiert entweder explizit durch Aufruf der Funktion pthread_exit

232 226 KAPITEL 7. THREADS oder implizit durch normalle Rückkehr aus der Start-Routine Die Adresse der Start-Routine wird über den Parameter func; diese wird mit einem einzigen Zeiger-Argument arg aufgerufen. Werden in der Start-Routine mehrere Argumente benötigt, so müssen diese in eine Struktur zusammengepackt werden und die Adresse dieser Struktur kann übergeben werden. Die Funktion func erhält ein Argument einen generischen Zeiger (void *) und liefert einen Wert ebenfalls ein generischer Zeiger (void *) zurück. Rückgabewert: im Erfolgsfall 0 Im Fehlerfall wird nicht -1 geliefert, sondern der positive Fehlercode (siehe <sys/errno.h>) die errno Variable wird nicht gesetzt! Beispiel: Es kann kein Thread mehr erzeugt werden, weil bereits zuviele erzeugt wurden der Rückgabewert ist dann EAGAIN

233 7.2. GRUNDLEGENDE FUNKTIONEN: ERZEUGEN UND BEENDEN pthread_join() # include <pthread.h> int pthread_join(pthread_t tid, void ** status); /* returns 0 on success, positive Exxx value on error */ Mit pthread_join() wird auf einen bestimmten Thread (tid) gewartet pthread_create() ist das Analogon zu fork(), pthread_join() das zu waitpid() zu sehen Ein Warten auf einen beliebigen Thread (analog zu waitpid(-1,...)) ist nicht möglich! Ist der Zeiger status nicht Null, so wird der Rückgabewert des Threads an einer Stelle abgelegt, auf die status zeigt pthread_self() # include <pthread.h> ptread_t pthread_self(void); /* returns thread ID of calling thread */ Das Analogon bei Prozessen ist getpit()! pthread_detach() # include <pthread.h> int pthread_detach(pthread_t tid); /* returns 0 on success, positive Exxx value on error */ Ein Thread ist entweder joinable (Default) oder detached. Terminiert ein joinable Thread, so werden seine ID und sein Exitstatus solange aufbewahrt, bis ein anderer Thread pthread_join aufruft! Ein detached Thread ist wie Dämen-Prozess: bei Termination werden alle seine Ressourcen freigegeben, man kann nicht auf ihn warten Typischer Aufruf: pthread_detach(pthread_self());

234 228 KAPITEL 7. THREADS pthread_exit() # include <pthread.h> int pthread_exit(void * status); /* Does not return to caller */ Ist der Thread nicht detached, so werden seine ID und sein Exitstatus für einen späteren pthread_join Aufruf aufbewahrt. der Zeiger status darf nicht auf ein bzgl. des Thread lokales Objekt zeigen (verschwindet, wenn der Thread terminiert). Andere Wege wie ein Thread termniniert: Die Startroutine des Thread (drittes Argument von pthread_create()) kehrt zurück und liefert den Exitstatus Wenn die main Funktion des Prozesses kehrt zurück oder irgendein erzeugter Thread ruft exit() auf, so terminiert der Prozess inklusive all seiner Threads. 7.3 Beispiel: Matrix-Multiplikation Das folgende Beispiel zeigt die Multiplikation einer quadratischen Matrix mit sich selbst die Parallelisierung bietet sich hier regelrecht, da jedes Ergebniselement unabhängig von den anderen berechnet werden kann. Programm 7.1: Matrix-Multiplikation mit Threads (thread-mult.c) 1 / 2 compute square of a matrix with one thread for each element in result matrix 3 / 4 5 # include < stdio. h> 6 # include < stdlib. h> 7 # include <pthread. h> 8 9 # define DIM 3 10 int matrix [DIM][DIM]; typedef struct { 13 int myid; / id of thread (0.. nthreads 1) / 14 int row, col ; / position (r, c) / 15 int localsum ; 16 } thread_arg_t ; void colrow_sum(void id ); void init_matrix () {

235 7.3. BEISPIEL: MATRIX-MULTIPLIKATION int i, j ; 22 for ( i = 0; i < DIM; i++) { 23 for ( j = 0; j < DIM; j++){ 24 matrix [ i ][ j ] = i + j ; 25 printf ( "%4d ", matrix[ i ][ j ]); 26 } 27 printf ("\n" ); 28 } 29 printf ( "\n" ); 30 } int main() { 33 int i, j,n; 34 int localsum ; / partial results / 35 int nthreads = DIM DIM; / number of threads / 36 pthread_t thread_id ; / ids of all threads / 37 thread_arg_t thread_arg ; / arguments for the threads / / allocate and initialize data / 40 thread_id = ( pthread_t ) malloc ( nthreads sizeof ( pthread_t )); 41 thread_arg = ( thread_arg_t ) malloc ( nthreads sizeof ( thread_arg_t )); 42 localsum = ( int ) malloc ( nthreads sizeof ( int )); init_matrix (); / compute arguments for all threads / 47 for ( i = 0; i < DIM; i++) { 48 for ( j = 0; j < DIM; j++){ 49 n = i DIM + j; 50 thread_arg [n ]. myid = n; 51 thread_arg [n ]. localsum = localsum ; 52 thread_arg [n ]. row = i ; 53 thread_arg [n ]. col = j ; 54 } 55 } / create threads, which all call colrow_sum() / 58 for ( i =1; i<nthreads ; i++) { 59 pthread_create (& thread_id [ i ], NULL, colrow_sum, &thread_arg [ i ]); 60 } / work myself, as thread 0 / 63 colrow_sum(& thread_arg [0]); / wait for other threads / 66 for ( i =1; i<nthreads ; i++) { 67 pthread_join ( thread_id [ i ], NULL); 68 } 69 / write result / 70 printf ( "\nresult:\n" ); 71 for ( i = 0; i < DIM; i++) { 72 for ( j = 0; j < DIM; j++) 73 printf ( "%4d ", localsum [ i DIM + j ]); 74 printf ("\n" );

236 230 KAPITEL 7. THREADS 75 } exit (0); 78 } / routine called by the threads 81 gets arguments myid, localsum returns result in localsum [myid] 82 / 83 void colrow_sum(void args ) 84 { 85 thread_arg_t myarg_p = ( thread_arg_t ) args ; 86 int myid; 87 int localsum ; 88 int i,row, col ; / unpack arguments / 91 myid = myarg_p >myid; 92 localsum = myarg_p >localsum; 93 row = myarg_p >row; 94 col = myarg_p >col; / threads s share of work / 97 localsum [myid] = 0; 98 for ( i = 0; i < DIM; i++) 99 localsum [myid] += matrix [row][ i ] matrix [ i ][ col ]; 100 printf ( "Thread %d has result %d\n", myid, localsum[myid]); 101 return(null); 102 } Übersetzung und Ausführung: spatz$ gcc -Wall thread-mult.c -lpthread spatz$ a.out Thread 1 has result 8 Thread 2 has result 11 Thread 3 has result 8 Thread 4 has result 14 Thread 5 has result 20 Thread 6 has result 11 Thread 7 has result 20 Thread 8 has result 29 Thread 0 has result 5 Result: spatz$

237 7.4. TCP ECHO SERVER MIT THREADS TCP Echo Server mit Threads Mit den Programmen 6.15, S.206 und 6.16, S.209 hatten wir einen kleinen TCP-basierten Echo- Server mit Eingabepufferung vorgestellt, der eingehende Verbindung in einem neuen Prozess abwickelt. Programm 7.2, S. 231, zeigt dasselbe mit Threads. Programm 7.2: Echo Server mit Threads (thread-echo/main-srv.c) 1 / instead of fork () we use threads / 2 3 # include " inet. h" 4 # include < stdio. h> 5 # include <unistd. h> 6 # include < stdlib. h> 7 # include < string. h> 8 # include < strings. h> 9 # include <errno. h> 10 # include <sys/types. h> 11 # include <sys/ socket. h> 12 # include < netinet /in. h> 13 # include <arpa/ inet. h> 14 # include <pthread. h> extern void str_echo ( int ); 18 static void doit (void ); int main( int argc, char argv) { int sockfd, newsockfd, clilen ; 23 pthread_t tid ; 24 struct sockaddr_in cli_addr, serv_addr ; if ( ( sockfd = socket (AF_INET, SOCK_STREAM,0)) < 0) { 27 perror ( " server : can t open stream socket"); 28 exit (1); 29 } bzero (( char ) &serv_addr, sizeof ( serv_addr )); serv_addr. sin_family = AF_INET; 34 serv_addr. sin_addr. s_addr = htonl (INADDR_ANY); 35 serv_addr. sin_port = htons (SERV_TCP_PORT); if ( bind ( sockfd, ( struct sockaddr )&serv_addr, 38 sizeof ( serv_addr )) < 0) { 39 perror ( " server : can t bind local address" ); 40 exit (2); 41 } listen ( sockfd,5); while(1) { 46 clilen = sizeof ( cli_addr );

238 232 KAPITEL 7. THREADS 47 newsockfd = accept ( sockfd, ( struct sockaddr ) & cli_addr, 48 & clilen ); if ( newsockfd < 0 ) { 51 if ( errno == EINTR) 52 continue; / try again / 53 perror ( " server : accept error" ); 54 exit (3); 55 } pthread_create (&tid, NULL, &doit, (void ) newsockfd ); 58 / this cast works if sizeof ( int ) is less or equal to 59 size of pointers 60 / } 63 exit (0); 64 } static void doit (void arg ) { 67 pthread_detach ( pthread_self ()); 68 str_echo (( int ) arg ); 69 close ( ( int ) arg ); 70 return NULL; 71 } Anmerkungen: Beim Aufruf von pthread_creat() wird der von accept gelieferte neue Socket-Deskriptor vom Typ int einfach auf einen Zeiger vom Typ void * ge-cast-et und in der Funktion doit() wird dieser wieder auf eine Integer zurückge-cast-et funktioniert nicht immer und ist eher eine unschöne Lösung! Eine auf den ersten Blick bessere Alternative könnte der Weg wie im Programm 7.3, S. 233 dargestellt sein.

239 7.4. TCP ECHO SERVER MIT THREADS 233 Programm 7.3: Echo Server mit Threads Alternative zu Prog. 7.2, S. 231? (thread-echo/main-srv-a) 1 /... / 2 3 int main( int argc, char argv) { 4 5 /... / 6 7 while(1) { 8 clilen = sizeof ( cli_addr ); 9 newsockfd = accept ( sockfd, ( struct sockaddr ) & cli_addr, 10 & clilen ); 11 /... / 12 // 13 // V 14 pthread_create (&tid, NULL, &doit, & newsockfd ); } 17 exit (0); 18 } static void doit (void arg ) { 21 pthread_detach ( pthread_self ()); 22 // 23 // V 24 str_echo ( arg ); 25 close ( ( int ) arg ); 26 return NULL; 27 } Die Lösung in Programm 7.3, S. 233 hat gravierende Nachteile: Die eine Variable newsockfd (shared variable) wird bei jedem Aufruf von accept überschrieben folgendes Szenario ist möglich: beim ersten Aufruf von accept() erhält newsockfd den Wert 5 der Zeiger auf newsockfd wird als letztes Argument an pthread_create() übergeben. ein Thread wird erzeugt und die Funktion doit() wird eingeplant zuvor aber kehrt accept() erneut zurück, der Rückgabewert (z.b. 8) wird in newsockfd abgelegt. der erste Thread startet statt mit dem Wert 6 mit dem Wert 8! Problem also: Zugriff zweier Threads auf eine gemeinsame Variable (newsockfd) ohne Synchronisation! Eine bessere Lösung zeigt Programm 7.4, S. 234.

240 234 KAPITEL 7. THREADS Programm 7.4: Echo Server mit Threads Alternative zu Prog. 7.2, S. 231 (thread-echo/main-srv-b.c) 1 / instead of fork () we use threads / 2 3 # include " inet. h" 4 # include < stdio. h> 5 # include <unistd. h> 6 # include < stdlib. h> 7 # include < string. h> 8 # include < strings. h> 9 # include <errno. h> 10 # include <sys/types. h> 11 # include <sys/ socket. h> 12 # include < netinet /in. h> 13 # include <arpa/ inet. h> 14 # include <pthread. h> extern void str_echo ( int ); 18 static void doit (void ); int main( int argc, char argv) { int sockfd, clilen ; 23 int p_newsockfd ; 24 pthread_t tid ; 25 struct sockaddr_in cli_addr, serv_addr ; if ( ( sockfd = socket (AF_INET, SOCK_STREAM,0)) < 0) { 28 perror ( " server : can t open stream socket"); 29 exit (1); 30 } bzero (( char ) &serv_addr, sizeof ( serv_addr )); serv_addr. sin_family = AF_INET; 35 serv_addr. sin_addr. s_addr = htonl (INADDR_ANY); 36 serv_addr. sin_port = htons (SERV_TCP_PORT); if ( bind ( sockfd, ( struct sockaddr )&serv_addr, 39 sizeof ( serv_addr )) < 0) { 40 perror ( " server : can t bind local address" ); 41 exit (2); 42 } listen ( sockfd,5); while(1) { 47 clilen = sizeof ( cli_addr ); 48 p_newsockfd = malloc ( sizeof ( int )); 49 p_newsockfd = accept ( sockfd, ( struct sockaddr ) & cli_addr, 50 & clilen ); if ( p_newsockfd < 0 ) {

241 7.4. TCP ECHO SERVER MIT THREADS if ( errno == EINTR) 54 continue; / try again / 55 perror ( " server : accept error" ); 56 exit (3); 57 } pthread_create (&tid, NULL, &doit, p_newsockfd ); } 62 exit (0); 63 } static void doit (void arg ) { 66 int connfd = ( ( int ) arg ); 67 free ( arg ); 68 pthread_detach ( pthread_self ()); 69 str_echo ( connfd ); 70 close ( connfd ); 71 return NULL; 72 } Anmerkungen zu Programm 7.4, S. 234: Vor jedem Aufruf von accept() wird dynamisch Speicherplatz für eine Integer-Größe den Deskriptor für die neue Socket-Verbindung alloziert. Damit hat jeder Thread seine eigene Kopie! Der Thread holt sich diesen Wert in eine eigene lokale Variable (connfd in der Funktion doit() und gibt danach den Speicherplatz für die von accept() gelieferte Größe wieder frei!

242 236 KAPITEL 7. THREADS 7.5 Thread-sichere Funktionen Die Funktionen malloc() wie auch free() waren ursprünglich nicht re-entrant-fähig : wird ein Thread (oder die Abarbeitung einer Funktion) durch Context-Wechsel (Aufruf eines Signalbehandlers / einer Signalbehandlerfunktion oder Fortsetzung eines anderen Threads) unterbrechen und unterbrochener wie neu ausgeführter Code teilen sich gemeinsame Objekte, so kann es zu chaotischen Zuständen kommen eine Funktion ist reentrantfähig, wenn mehrfache (überlappende) Aufrufe möglich sind, die sich gegenseitig nicht beeinflussen Der POSIX-Standard verlangt, dass u.a. die Funktione malloc() und free() reentrant-fähig sind! POSIX.1 verlangt dass alle in POSIX.1 wie im ANSI-C-Standard defineirten Funktionen mit einigen Ausnahmen (!) Thread-sicher sind! Bei eigenen Funktionen muss man selbst auf die Reentrant-Fähigkeit achten, z.b.: Die Funktion sollte sich nicht selbst (ihre Instruktionen) verändern! Zugriff auf alle shared Objekte nur atomar (siehe Datentyp sig_atomic_t) oder: alle Objekte, die von der Funktion modifiziert werden, sind einer speziellen Instanz (Aufruf, Inkarnation) zugeordnet (instanz-spezifische Objekte)! keine static-variable in Funktionen, keinen Rückgabezeiger auf eine static-variable! kein Aufruf nicht-reentrantfähiger Funktionen kein nicht-atomarer Zugriff auf die Hardware mehr dazu siehe z.b. W.R. Stevens e.a.: UNIX Network Programming Volume I: The Sockets Networking API. Addison Wesley 2004, 3rd Ed., S. 686ff

243 7.6. ECHO CLIENT MIT THREADS Echo Client mit Threads Wir können auch den Client mit Threads arbeiten lassen (siehe Abb. 7.2, S. 237). fp copyto thread pthread_create server stdout main thread Abbildung 7.2: Echo Client mit Threads Das Hauptprogramm des Client bleibt wie in Programm 6.17, S/210, dargestellt. Die Funktionalität ist in Programm 7.5, S. 237 dargestellt. Die beiden globalen Variablen für den copyto-thread (fp und sockfd) hätten auch in eine Struktur zusammengefasst werden können und beim Aufruf von pthread_create mit einem Zeiger darauf übergeben werden können! Programm 7.5: Echo Client mit Threads (thread-echo/strcli-thread.c) 1 / str_cli. c thread version 2 function used by connection oriented clients 3 4 one thread reads the contents of the FILE fp, write each line to the 5 stream socket ( to the server process ), the other thread reads a line back 6 from the socket and writes it to stdout 7 8 return to caller when an EOF is encountered on the input file 9 / # include < stdio.h> 12 # include <unistd. h> 13 # include < stdlib.h> 14 # include < string.h> 15 # include <sys/ socket. h> 16 # include <pthread. h> 17 # include "inbuf. h" 18 # define MAXLINE void copyto (void ); static int sockfd ; 23 static FILE fp ; void str_cli (FILE fp_arg, int sockfd_arg ) { 26 int n; 27 char recvline [MAXLINE+1]; 28 pthread_t tid ;

244 238 KAPITEL 7. THREADS 29 inbuf in ; 30 inbuf_alloc (&in, 512); 31 sockfd = sockfd_arg ; 32 fp = fp_arg ; 33 in. fd = sockfd ; pthread_create (& tid, NULL,copyto,NULL); while ( (n = inbuf_read (&in, recvline, MAXLINE) )!= 0) { 38 if (n < 0) { 39 perror ( " str_cli : read error" ); 40 exit (5); 41 } 42 write (1, recvline, n ); 43 } 44 return ; 45 } void copyto (void arg ) { 48 char sendline [MAXLINE]; int n; 49 while ( fgets ( sendline, MAXLINE,fp)!= NULL) { 50 n = strlen ( sendline ); 51 if ( write ( sockfd, sendline,n)!= n) { 52 perror ( " str_cli : write error on socket"); 53 exit (4); 54 } 55 } 56 shutdown( sockfd,1); 57 return NULL; 58 }

245 7.7. SYNCHRONISATION MUTUAL EXCLUSION Synchronisation Mutual Exclusion Wie im vorletzten Abschnitt bereits angesprochen, ist der wechselseitige Zugriff auf gemeinsame Datenbereich kritisch dies soll ein einfaches Beispiel in Programm 7.6, S. 239, zeigen. 1 # include <pthread. h> 2 # include < stdio. h> 3 # include < stdlib. h> 4 5 # define NLOOP int counter = 0; 8 void doit (void ); 9 10 int main() { 11 pthread_t tida, tidb ; 12 Programm 7.6: Zähler hochzählen ohne Synchronisation (mutex/mutex1.c) 13 // create two threads with same function 14 pthread_create (&tida, NULL, &doit, NULL); 15 pthread_create (&tidb, NULL, &doit, NULL); // wait for both threads to terminate 18 pthread_join ( tida, NULL); 19 pthread_join ( tidb, NULL); exit (0); 22 } void doit (void arg ) { 25 int i, val ; 26 for ( i= 0; i < NLOOP; i++) { 27 val = counter ; 28 printf ( "%d: %d\n", (int) pthread_self (), val + 1); 29 counter = val + 1; 30 } 31 return NULL; 32 } Das Ergebnis dieses Programms ist nicht-deterministisch, der Fehler offenkundig! Hier bieten sich im gegebenen Kontext spezielle Objekte, sog. MutexVariablle (Typ: pthread_mutex_t) eine Art Semaphore ( mit speziellen Zugriffsoperation zum Sperren (protect nach Dijkstra oder Funktionen lockf() / fcntl()) und Freigeben (vrej nach Dijkstra oder Funktion fcntl()) an. Ein solches Objekt kann die Zustände unlocked (nicht im Besitz eines Thread) und locked (im Besitz genau eines Thread). Versucht ein Thread, ein Mutex-Objekt im Zustand locked zu sperren, so wird er blockiert, bis der Thread, der das Mutex-Objekt besitzt dieses freigibt. Startwert: PTHREAD_MUTEX_INITIALIZER

246 240 KAPITEL 7. THREADS Funktion zum Sperren und Freigeben: # include <pthread.h> int int pthread_mutex_lock(pthread_mutex_t * mptr); pthread_mutex_unlock(pthread_mutex_t * mptr); /* both return 0 if OK, positive Exxx value on error */ Damit lässt sich das Programm 7.6 sicherer machen (siehe Programm 7.7, S. 240). 1 # include <pthread. h> 2 # include < stdio. h> 3 # include < stdlib. h> 4 5 # define NLOOP Programm 7.7: Zähler hochzählen mit Synchronisation (mutex/mutex2.c) 7 int counter = 0; 8 pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER; 9 10 void doit (void ); int main() { 13 pthread_t tida, tidb ; // create two threads with same function 16 pthread_create (&tida, NULL, &doit, NULL); 17 pthread_create (&tidb, NULL, &doit, NULL); // wait for both threads to terminate 20 pthread_join ( tida, NULL); 21 pthread_join ( tidb, NULL); exit (0); 24 } void doit (void arg ) { 27 int i, val ; 28 for ( i= 0; i < NLOOP; i++) { 29 pthread_mutex_lock (& counter_mutex ); 30 val = counter ; 31 printf ( "%d: %d\n", (int) pthread_self (), val + 1); 32 counter = val + 1; 33 pthread_mutex_unlock(& counter_mutex ); 34 } 35 return NULL; 36 }

247 7.7. SYNCHRONISATION MUTUAL EXCLUSION 241 Deadlock: Das Arbeiten mit Mutex-Variablen hat allerdings auch seine Tücken. Nehmen wir der Einfachheit halber an, zwei Threads wollen zwei Zähler z1 und z2 hochzählen nach folgender Maßgabe: Thread A: z1++, if ( z1 > z2 ) { z2 += 2 } else z2++; Thread B: z2++, if ( z2 > z1 ) { z1 += 2 } else z1++; Ein mögliche Implementierung zeigt Programm 7.8, S # include <pthread. h> 2 # include < stdio. h> 3 # include < stdlib. h> 4 5 # define NLOOP Programm 7.8: Synchronisation mit Deadlock (mutex/deadlock.c) 7 int counter_x = 0; // Zaehler z1 8 int counter_y = 0; // Zaehler z2 9 pthread_mutex_t counter_x_mutex = PTHREAD_MUTEX_INITIALIZER; 10 pthread_mutex_t counter_y_mutex = PTHREAD_MUTEX_INITIALIZER; void A_doit (void ); 13 void B_doit (void ); int main() { 16 pthread_t tida, tidb ; // create two threads with same function 19 pthread_create (&tida, NULL, &A_doit, NULL); 20 pthread_create (&tidb, NULL, &B_doit, NULL); // wait for both threads to terminate 23 pthread_join ( tida, NULL); 24 pthread_join ( tidb, NULL); exit (0); 27 } void A_doit (void arg ) { 30 int i, val_x, val_y, incr ; 31 for ( i= 0; i < NLOOP; i++) { 32 pthread_mutex_lock (& counter_x_mutex ); 33 val_x = counter_x ; 34 printf ( "%d: x = %d\n", (int) pthread_self (), val_x + 1); 35 counter_x = val_x + 1; pthread_mutex_lock (& counter_y_mutex ); 38 val_y = counter_y ; 39 incr = ( counter_x > val_y )? 2 : 1; 40 printf ( "%d: y = %d\n", (int) pthread_self (), val_y + incr ); 41 counter_y = val_y + incr ;

248 242 KAPITEL 7. THREADS pthread_mutex_unlock(& counter_y_mutex ); 44 pthread_mutex_unlock(& counter_x_mutex ); 45 } 46 return NULL; 47 } void B_doit (void arg ) { 50 int i, val_x, val_y, incr ; 51 for ( i= 0; i < NLOOP; i++) { 52 pthread_mutex_lock (& counter_y_mutex ); 53 val_y = counter_y ; 54 printf ( "%d: y = %d\n", (int) pthread_self (), val_y + 1); 55 counter_y = val_y + 1; pthread_mutex_lock (& counter_x_mutex ); 58 val_x = counter_x ; 59 incr = ( counter_y > val_x )? 2 : 1; 60 printf ( "%d: x = %d\n", (int) pthread_self (), val_x + incr ); 61 counter_x = val_x + incr ; pthread_mutex_unlock(& counter_x_mutex ); 64 pthread_mutex_unlock(& counter_y_mutex ); 65 } 66 return NULL; 67 } Klassischer (kausallogischer) Deadlock: Prozess P1 hat Ressource R1 exklusiv und benötigt Ressource R2 zum Weitermachen ebenfalls exklusiv Prozess P2 hat Ressource R2 exklusiv und benötigt Ressource R2 zum Weitermachen ebenfalls exklusiv RIEN NE VA PLUS Wann? Dann, wenn man am Wenigsten damit rechnet (Murphy)!

249 Anhang 243

250

251 Literatur [Bach86] [Comer84] [Darnell01] M. J. Bach: The Design of the UNIX Operating System. Prentice Hall, D. Comer: Operating System Design: The XNIU Approach. Prentice Hall, 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] [Kernighan90] [Rochkind04] [Stevens92] [Stevens04] [Stevens98] B. W. Kernighan und R. Pike: Der UNIX-Werkzeugkasten. Hanser, B. W. Kernighan und D. Ritchie: Programmieren in C. Hanser, Zweite Auflage, M. Rochkind: Advanced UNIX Programming. 2nd Ed., Prentice Hall 2004 W. R. Stevens: Advanced Programming in the UNIX Environment. Addison-Wesley, W.R. Stevens: UNIX Network Programming, Volume 1: The Sockets Networking API. 3rd ed., Addison Wesley 2004 W.R. Stevens: UNIX Network Programming, Volume 2: Interprocess Communications. 2nd. Ed., Addison Wesley 1998 [Tanenbaum02] A. S. Tanenbaum: Moderne Betriebssysteme Addison Wesley [Tanenbaum03] A. S. Tanenbaum: Computernetzwerke. 4th Ed., Prentice Hall 2003 jeweils auf aktuellste Ausgabe achten! 245

252 246 LITERATUR

253 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 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

254 248 ABBILDUNGSVERZEICHNIS 5.9 Internet Architektur Abstraktion der Identifikation Bit IP-Adresse in Version IP-Adresse: dotted decimal form IP-Adressklassen in Version 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 Struktur eines Eingabepuffers Multithreading vs. Single threading Echo Client mit Threads

255 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

256 250 BEISPIEL-PROGRAMME 3.5 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 Time-Server, der nach dem Woher fragt Time-Server auf Basis UDP (nur zur Demo) Time-Client auf Basis UDP (nur zur Demo) Hostnamen ermitteln Hostnamen bestimmen Funktion uname() Port und Protokoll eines Dienstes Schnittstelle für einen Eingabe-Puffer Implementierung des Eingabe-Puffers Schnittstelle für einen Ausgabe-Puffer

257 BEISPIEL-PROGRAMME Implementierung des Ausgabe-Puffers Headerfile für Netzwerkverbindung Hauptprogramm des TCP-Servers mit Eingabepufferung Funktionalität des TCP-Servers mit Eingabepufferung Hauptprogramm des TCP-Clients mit Eingabepufferung Funktionalität des TCP-Clients mit Eingabepufferung Schnittstelle für MXP-Anfrage-Pakete Schnittstelle für MXP-Antwort-Pakete Das Einlesen eines MXP-Anfrage-Pakets Das Einlesen eines MXP-Antwort-Pakets Schnittstelle für die Kontrollstruktur von Netzwerkdiensten Implementierung für die Kontrollstruktur von Netzwerkdiensten Matrix-Multiplikation mit Threads Echo Server mit Threads Echo Server mit Threads Alternative zu Prog. 7.2, S. 231? Echo Server mit Threads Alternative zu Prog. 7.2, S Echo Client mit Threads Zähler hochzählen ohne Synchronisation Zähler hochzählen mit Synchronisation Synchronisation mit Deadlock

258 Index /etc/hosts, 194 /etc/services, 197 _Exit(), 64 _exit(), 20 abort(), 64 accept(), 166, 184 Adress-Resolution, 192, 194 AF_INET, 161 AF_UNIX, 161, 187 alarm(), 65 AN, 142 Anwendungsschicht, 129 API, 151 argv, 23 AS, 142 atexit(), 21 Autonome Systeme, 142 backlog, 166 bcopy(), 169 Beendigungsstatus, 3, 4, 33 best-effort delivery, 135, 143 BGP, 141 Big-Endian, 189 bind(), 162, 177, 178, 184 Bitübertragungsschicht, 128 Bootstrapping, 54 BREAK, 60 Bridge, 130 broadcast, 135 Broadcast-Adresse, 190 BSD, 151 bzero(), 155 child process, 15 Client, 165 Client-Server, 115 close(), 113, 177, 185 concurrent server, 218 connect(), 165, 179, 184 Connectionless, 143 control process, 3 CSMA/CD-Verfahren, 134 ctrl-\, 60 CTRL-c, 61 ctrl-c, 60 ctrl-z, 60 SIGSTOP, 5 current working directory, 19, 24 Dämon, 3 Datagram, 177 Datagram Socket, 157, Datendarstellungsschicht, 129 Datentyp sig_atomic_, 64 Deadlock, 112 delete, 60 DIN/ISO-OSI, 128 DNS, 194 Domain Name System, 194 dotted decimal notation, 139, 190 dup(), 111 dup2(), 111 dynamic and/or private ports, 197 effective group ID, 19 effective user ID, 19 effektive group ID, 24 effektive user ID, 24 EINTR, 78 end-of-record, 176 ephemeral ports, 197 errno, 78, 224 Ethernet, 135 exec, 22 execl(),

259 INDEX 253 execle(), 22 execlp(), 22 execv(), 22 execve, 22 execvp(), 22 exit(), 3, 4, 20, 32, 33 Exit-Status, 3 exit-status, 33 fcntl(), 185 FDDI, 133 fflush(), 25 file locks, 24 file mode creation mask, 19, 24 fork(), 1, 15, 112, 113 FQDN, 192 fread(), 46 Fully Qualified Domain Name, 192 GAN, 127 Gateway, 130 gethostbyaddr(), 192, 195 gethostbyname(), 192 gethostname(), 195 getpeername(), 183, 185 getpgrp(), 2 getpid(), 2 getppid(), 2 getservbyname(), 198 getservbyport(), 198 getsockname(), 183, 185 getsockopt(), 185 global area network, 127 HANGUP, 60 hangup-signal, 3 Host, 190 host interface, 135 hostent, 192 Hostname, 195 Hostnamen, 192, 194 htonl(), 189 htons(), 189 Hub, 130 IANA, 197 ICMP, 157 IGMP, 157 IGRP, 141 INADDR_ANY, 155, 188 INADDR_NONE, 190 inet_addr(), 190 inet_aton(), 191 inet_ntoa(), 173, 191 inetd, 183 init-prozess, 33, 55, 56 Inter-Process Communication, 109 Internet Assigned Numbers Authority, 197 Internet Datagram, 144 Internet Domain, 156 Internet Protocol, 143, 157 Internet Superserver, 183 Internet-Adressen, 138, 190, 194 ioctl(), 185 IP, 143, 157 IP-Adresse, 138, 195 IP-Adresse 0, 188 IPC, 1, 4, 109, 110 iterative server, 217 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, 156 Kommunikationssteuerungsschicht, 129 Kontext, 1 kurzlebige Portnummern, 188, 197 LAN, 127 listen(), 166, 184 Little-Endian, 189 loader, 55

260 254 INDEX local area network, 127 lsof, 170 MAC-Adresse, 133, 135 MAX_PATH, 187 MAXHOSTNAMELEN, 195 memcpy(), 169 memory buffers, 186 memset(), 155 MMU, 10 multi-homed host, 190 Mutex, 239 Nameserver, 194 NAT, 141 netdb.h, 197 Netwerktopologie, 127 network byte order, 189 Network Information Service, 194 Netzmaske, 140 nice, 7 nice(), 7 NIS, 194 ntohl(), 190 ntohs(), 173, 190 nuhup, 7 OSI, 128 OSPF, 141 out-of-band-data, 175 parent process, 2, 15 parent process ID, 24 PATH, 23, 120 pause(), 70, 72 PC, 1 pclose(), 120 Peer-Adresse, 166 PF_INET, 161 PF_UNIX, 161 PID, 2 pipe(), 111, 113 pipefd[0], 111 pipefd[1], 111 poll(), 66, 218 popen(), 120 Port, 147 Port-Nummer, 188 Port-Nummern, 149 Portnummer, 185, 197 Portnummer 0, 188 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 pthread_, 224 pthread_create(), 225 pthread_detach(), 227 pthread_exit(), 228 pthread_join(), 227 pthread_mutex_lock, 240 pthread_mutex_t, 239 pthread_mutex_unlock, 240 pthread_self(), 227 punktiertes Dezimalformat, 139, 190 raise(), 67 Raw Socket, 157 re-entrant, 236 read(), 112, 175, 184 readv(), 175, 184 real group ID, 19, 24 real user ID, 19, 24 Rechneradresse, 185 recv(), 175, 179, 184 recvfrom(), 176, 178, 184 recvmsg(), 176, 178, 184 regions, 11 registered ports, 197 Repeater, 130 reservierte Portnummern (UNIX), 197 Resolver, 192 RIP, 141 rlogin, 197 root directory, 19, 24 Router, 130

261 INDEX 255 Routing, 138, 141 rsh, 197 sa_data, 186 sa_family, 186 sa_len, 186 select(), 66, 184, 218 Semaphore, 239 send(), 175, 179, 184 sendmsg(), 176, 178, 179, 184 sendto(), 176, 178, 179, 184 Server, 165 set group ID, 24 set user Id, 24 setpgrp(), 2 setsockopt(), 185 Shell-Variable?, 4 $, 2 shutdown(), 177, 185 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 ctrl-z, 5 SIGHUP, 7 SIGKILL, 5 SIGSTOP, 5 SIGTTIN, 6 signal handler, 61 signal handling settings, 19 signal(), 33, 61, 63, 64 Signale, 24 SIGPIPE, 60 SIGQUIT, 60 sigrelse(), 79, 80 SIGSEGV, 60 SIGSTOP, 5, 60, 61 ctrl-z, 5 SIGTERM, 69 SIGTTIN, 6 SIGUSR1, 69 SIGUSR2, 69 SOCK_DGRAM, 161, 177 SOCK_RAW, 161 SOCK_STREAM, 161 sockaddr, 173 Socket Adresse, 162 socket(), 161, 184 Socket-Typen, 156 socketpair(), 184 Sockets, 151 SOMAXCON, 172 SOMAXCONN, 217 Speicherklasse volatile, 63 stralloc, 44 Stream Socket, 157 struct in_addr, 188 struct sockaddr, 162, 186 struct sockaddr_in, 164, 188 struct sockaddr_un, 163, 187 sun_family, 187 sun_len, 187 sun_path, 187 swapper, 55 Switch, 130 sys/param.h, 195 sys/utsname.h, 196 TCP, 161, 197 TCP/IP, 129, 143 tcpserver, 218 Telnet-Server, 197 telnetd, 197 terminal group, 3 terminal group ID, 19, 24 TFTP-Server, 197

262 256 INDEX tftpd, 197 Thread, 1 initial, 225 main, 225 thread, 223 Token-Verfahren, 134 top, 5 Transceiver, 135 Transportschicht, 129, 197 Trivial File Transfer Protocol, 197 Twisted-Pair, 131 Zombie, 5, 20, 33 u area, 11, 12 UDP, 197 uname(), 196 uname-kommando, 195 UNIX domain, 156 unnamed pipes, 111 Unreliable Delivery, 143 User Mode, 9 verbindungslos, 156, 177 verbindungsorientiert, 156 Vererbung, 4, 19, 24 Vermittlungsschicht, 128 volatile, 63 voll-duplex, 177 wait(), 4, 29, 30, 32 waitpid(), 29, 32, 86 WAN, 127 well-known ports, 197 well-known ports (UNIX), 197 WEXITSTATUS(status), 30 wide area network, 127 WIFEXITED(status), 30 WIFSIGNALED(status), 30 wireless LAN, 127 WLAN, 127, 133 WNOHANG, 31, 86 write(), 112, 175, 184 writev(), 175, 184 WTERMSIG(status), 30 WUNTRACED, 32 X Window Server, 197 xterm, 61

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

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

Betriebssysteme. Dipl.-Ing.(FH) Volker Schepper

Betriebssysteme. Dipl.-Ing.(FH) Volker Schepper 1. Der Prozess beginnt im Zustand Erzeugt, nachdem sein Vaterprozess den Systemaufruf fork() (s.u.) abgesetzt hat. In diesem Zustand wird der Prozess-Kontext initialisiert. 2. Ist diese Aufbauphase abgeschlossen,

Mehr

Prozesse und Logs Linux-Kurs der Unix-AG

Prozesse und Logs Linux-Kurs der Unix-AG Prozesse und Logs Linux-Kurs der Unix-AG Benjamin Eberle 22. Januar 2015 Prozesse unter Linux gestartete Programme laufen unter Linux als Prozesse jeder Prozess hat eine eindeutige Prozess-ID (PID) jeder

Mehr

Systemprogrammierung I - Aufgaben zur Erlangung der Klausurzulassung für Informatiker und Wirtschaftsinformatiker

Systemprogrammierung I - Aufgaben zur Erlangung der Klausurzulassung für Informatiker und Wirtschaftsinformatiker Systemprogrammierung I - Aufgaben zur Erlangung der Klausurzulassung für Informatiker und Nachfolgend finden Sie die drei Aufgaben, die Sie als Zulassungsvoraussetzung für die Scheinklausur am 18.7.2001

Mehr

2A Basistechniken: Weitere Aufgaben

2A Basistechniken: Weitere Aufgaben 2A Basistechniken: Weitere Aufgaben 2A.3 Programmierung unter UNIX/Linux 1. Gegeben sind einige Ausschnitte von C-Programmen, die unter UNIX/Linux ausgeführt werden sollen. Beantworten Sie die zugehörigen

Mehr

Architektur Verteilter Systeme Teil 2: Prozesse und Threads

Architektur Verteilter Systeme Teil 2: Prozesse und Threads Architektur Verteilter Systeme Teil 2: Prozesse und Threads 21.10.15 1 Übersicht Prozess Thread Scheduler Time Sharing 2 Begriff Prozess und Thread I Prozess = Sequentiell ablaufendes Programm Thread =

Mehr

Prozesse. Stefan Janssen. [email protected]. Alexander Sczyrba [email protected]

Prozesse. Stefan Janssen. sjanssen@cebitec.uni-bielefeld.de. Alexander Sczyrba asczyrba@cebitec.uni-bielefeld.de Netzwerk - Programmierung Prozesse Stefan Janssen [email protected] Alexander Sczyrba [email protected] Madis Rumming [email protected] Übersicht Prozesse

Mehr

Systeme 1. Kapitel 6. Nebenläufigkeit und wechselseitiger Ausschluss

Systeme 1. Kapitel 6. Nebenläufigkeit und wechselseitiger Ausschluss Systeme 1 Kapitel 6 Nebenläufigkeit und wechselseitiger Ausschluss Threads Die Adressräume verschiedener Prozesse sind getrennt und geschützt gegen den Zugriff anderer Prozesse. Threads sind leichtgewichtige

Mehr

Linux Prinzipien und Programmierung

Linux Prinzipien und Programmierung Linux Prinzipien und Programmierung Dr. Klaus Höppner Hochschule Darmstadt Sommersemester 2014 1 / 25 2 / 25 Pipes Die Bash kennt drei Standard-Dateideskriptoren: Standard In (stdin) Standard-Eingabe,

Mehr

Prozesse und Logs Linux-Kurs der Unix-AG

Prozesse und Logs Linux-Kurs der Unix-AG Prozesse und Logs Linux-Kurs der Unix-AG Andreas Teuchert 27./28. Juni 2012 Prozesse unter Linux gestartete Programme laufen unter Linux als Prozesse jeder Prozess hat eine eindeutige Prozess-ID (PID)

Mehr

Monitore. Klicken bearbeiten

Monitore. Klicken bearbeiten Sascha Kretzschmann Institut für Informatik Monitore Formatvorlage und deren Umsetzung des Untertitelmasters durch Klicken bearbeiten Inhalt 1. Monitore und Concurrent Pascal 1.1 Warum Monitore? 1.2 Monitordefinition

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

Zählen von Objekten einer bestimmten Klasse

Zählen von Objekten einer bestimmten Klasse Zählen von Objekten einer bestimmten Klasse Ziel, Inhalt Zur Übung versuchen wir eine Klasse zu schreiben, mit der es möglich ist Objekte einer bestimmten Klasse zu zählen. Wir werden den ++ und den --

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

Handbuch Fischertechnik-Einzelteiltabelle V3.7.3

Handbuch Fischertechnik-Einzelteiltabelle V3.7.3 Handbuch Fischertechnik-Einzelteiltabelle V3.7.3 von Markus Mack Stand: Samstag, 17. April 2004 Inhaltsverzeichnis 1. Systemvorraussetzungen...3 2. Installation und Start...3 3. Anpassen der Tabelle...3

Mehr

Stundenerfassung Version 1.8 Anleitung Arbeiten mit Replikaten

Stundenerfassung Version 1.8 Anleitung Arbeiten mit Replikaten Stundenerfassung Version 1.8 Anleitung Arbeiten mit Replikaten 2008 netcadservice GmbH netcadservice GmbH Augustinerstraße 3 D-83395 Freilassing Dieses Programm ist urheberrechtlich geschützt. Eine Weitergabe

Mehr

CMS.R. Bedienungsanleitung. Modul Cron. Copyright 10.09.2009. www.sruttloff.de CMS.R. - 1 - Revision 1

CMS.R. Bedienungsanleitung. Modul Cron. Copyright 10.09.2009. www.sruttloff.de CMS.R. - 1 - Revision 1 CMS.R. Bedienungsanleitung Modul Cron Revision 1 Copyright 10.09.2009 www.sruttloff.de CMS.R. - 1 - WOZU CRON...3 VERWENDUNG...3 EINSTELLUNGEN...5 TASK ERSTELLEN / BEARBEITEN...6 RECHTE...7 EREIGNISSE...7

Mehr

Softwarelösungen: Versuch 4

Softwarelösungen: Versuch 4 Softwarelösungen: Versuch 4 Nichtstun in Schleife wird ersetzt durch zeitweilige Zurücknahme der Anforderung, um es anderen Prozessen zu erlauben, die Ressource zu belegen: /* Prozess 0 */ wiederhole flag[0]

Mehr

1 Vom Problem zum Programm

1 Vom Problem zum Programm Hintergrundinformationen zur Vorlesung GRUNDLAGEN DER INFORMATIK I Studiengang Elektrotechnik WS 02/03 AG Betriebssysteme FB3 Kirsten Berkenkötter 1 Vom Problem zum Programm Aufgabenstellung analysieren

Mehr

Primzahlen und RSA-Verschlüsselung

Primzahlen und RSA-Verschlüsselung Primzahlen und RSA-Verschlüsselung Michael Fütterer und Jonathan Zachhuber 1 Einiges zu Primzahlen Ein paar Definitionen: Wir bezeichnen mit Z die Menge der positiven und negativen ganzen Zahlen, also

Mehr

10.6 Programmier-Exits für Workitems

10.6 Programmier-Exits für Workitems 10.6 Programmier-Exits für Workitems 279 10.6 Programmier-Exits für Workitems 10.6.1 Das Interface IF_SWF_IFS_WORKITEM_EXIT Am Schritt einer Workflow-Definition im Reiter»Programmier-Exits«können verschiedene

Mehr

Client-Server mit Socket und API von Berkeley

Client-Server mit Socket und API von Berkeley Client-Server mit Socket und API von Berkeley L A TEX Projektbereich Deutsche Sprache Klasse 3F Schuljahr 2015/2016 Copyleft 3F Inhaltsverzeichnis 1 NETZWERKPROTOKOLLE 3 1.1 TCP/IP..................................................

Mehr

Dokumentation IBIS Monitor

Dokumentation IBIS Monitor Dokumentation IBIS Monitor Seite 1 von 16 11.01.06 Inhaltsverzeichnis 1. Allgemein 2. Installation und Programm starten 3. Programmkonfiguration 4. Aufzeichnung 4.1 Aufzeichnung mitschneiden 4.1.1 Inhalt

Mehr

I Serverkalender in Thunderbird einrichten

I Serverkalender in Thunderbird einrichten I Serverkalender in Thunderbird einrichten Damit Sie den Kalender auf dem SC-IT-Server nutzen können, schreiben Sie bitte zuerst eine Mail mit Ihrer Absicht an das SC-IT ([email protected]). Dann

Mehr

Speicher in der Cloud

Speicher in der Cloud Speicher in der Cloud Kostenbremse, Sicherheitsrisiko oder Basis für die unternehmensweite Kollaboration? von Cornelius Höchel-Winter 2013 ComConsult Research GmbH, Aachen 3 SYNCHRONISATION TEUFELSZEUG

Mehr

Tutorial - www.root13.de

Tutorial - www.root13.de Tutorial - www.root13.de Netzwerk unter Linux einrichten (SuSE 7.0 oder höher) Inhaltsverzeichnis: - Netzwerk einrichten - Apache einrichten - einfaches FTP einrichten - GRUB einrichten Seite 1 Netzwerk

Mehr

Datensicherung. Beschreibung der Datensicherung

Datensicherung. Beschreibung der Datensicherung Datensicherung Mit dem Datensicherungsprogramm können Sie Ihre persönlichen Daten problemlos Sichern. Es ist möglich eine komplette Datensicherung durchzuführen, aber auch nur die neuen und geänderten

Mehr

Auf der linken Seite wählen Sie nun den Punkt Personen bearbeiten.

Auf der linken Seite wählen Sie nun den Punkt Personen bearbeiten. Personenverzeichnis Ab dem Wintersemester 2009/2010 wird das Personenverzeichnis für jeden Mitarbeiter / jede Mitarbeiterin mit einer Kennung zur Nutzung zentraler Dienste über das LSF-Portal druckbar

Mehr

Binäre Bäume. 1. Allgemeines. 2. Funktionsweise. 2.1 Eintragen

Binäre Bäume. 1. Allgemeines. 2. Funktionsweise. 2.1 Eintragen Binäre Bäume 1. Allgemeines Binäre Bäume werden grundsätzlich verwendet, um Zahlen der Größe nach, oder Wörter dem Alphabet nach zu sortieren. Dem einfacheren Verständnis zu Liebe werde ich mich hier besonders

Mehr

Produktschulung WinDachJournal

Produktschulung WinDachJournal Produktschulung WinDachJournal Codex GmbH Stand 2009 Inhaltsverzeichnis Einleitung... 3 Starten des Programms... 4 Erfassen von Notizen in WinJournal... 6 Einfügen von vorgefertigten Objekten in WinJournal...

Mehr

4. BEZIEHUNGEN ZWISCHEN TABELLEN

4. BEZIEHUNGEN ZWISCHEN TABELLEN 4. BEZIEHUNGEN ZWISCHEN TABELLEN Zwischen Tabellen können in MS Access Beziehungen bestehen. Durch das Verwenden von Tabellen, die zueinander in Beziehung stehen, können Sie Folgendes erreichen: Die Größe

Mehr

Matrix42. Use Case - Sicherung und Rücksicherung persönlicher Einstellungen über Personal Backup. Version 1.0.0. 23. September 2015 - 1 -

Matrix42. Use Case - Sicherung und Rücksicherung persönlicher Einstellungen über Personal Backup. Version 1.0.0. 23. September 2015 - 1 - Matrix42 Use Case - Sicherung und Rücksicherung persönlicher Version 1.0.0 23. September 2015-1 - Inhaltsverzeichnis 1 Einleitung 3 1.1 Beschreibung 3 1.2 Vorbereitung 3 1.3 Ziel 3 2 Use Case 4-2 - 1 Einleitung

Mehr

Die Statistiken von SiMedia

Die Statistiken von SiMedia Die Statistiken von SiMedia Unsere Statistiken sind unter folgender Adresse erreichbar: http://stats.simedia.info Kategorie Titel Einfach Erweitert Übersicht Datum und Zeit Inhalt Besucher-Demographie

Mehr

Enigmail Konfiguration

Enigmail Konfiguration Enigmail Konfiguration 11.06.2006 [email protected] Enigmail ist in der Grundkonfiguration so eingestellt, dass alles funktioniert ohne weitere Einstellungen vornehmen zu müssen. Für alle, die es

Mehr

Nicht kopieren. Der neue Report von: Stefan Ploberger. 1. Ausgabe 2003

Nicht kopieren. Der neue Report von: Stefan Ploberger. 1. Ausgabe 2003 Nicht kopieren Der neue Report von: Stefan Ploberger 1. Ausgabe 2003 Herausgeber: Verlag Ploberger & Partner 2003 by: Stefan Ploberger Verlag Ploberger & Partner, Postfach 11 46, D-82065 Baierbrunn Tel.

Mehr

Thermoguard. Thermoguard CIM Custom Integration Module Version 2.70

Thermoguard. Thermoguard CIM Custom Integration Module Version 2.70 Thermoguard Thermoguard CIM Custom Integration Module Version 2.70 Inhalt - Einleitung... 3 - Voraussetzungen... 3 - Aktivierung und Funktion der Schnittstelle... 3 - Parameter... 4 - NLS-Einfluss... 4

Mehr

Advoware mit VPN Zugriff lokaler Server / PC auf externe Datenbank

Advoware mit VPN Zugriff lokaler Server / PC auf externe Datenbank Advoware mit VPN Zugriff lokaler Server / PC auf externe Datenbank Die Entscheidung Advoware über VPN direkt auf dem lokalen PC / Netzwerk mit Zugriff auf die Datenbank des zentralen Servers am anderen

Mehr

Aufklappelemente anlegen

Aufklappelemente anlegen Aufklappelemente anlegen Dieses Dokument beschreibt die grundsätzliche Erstellung der Aufklappelemente in der mittleren und rechten Spalte. Login Melden Sie sich an der jeweiligen Website an, in dem Sie

Mehr

DELFI. Benutzeranleitung Dateiversand für unsere Kunden. Grontmij GmbH. Postfach 34 70 17 28339 Bremen. Friedrich-Mißler-Straße 42 28211 Bremen

DELFI. Benutzeranleitung Dateiversand für unsere Kunden. Grontmij GmbH. Postfach 34 70 17 28339 Bremen. Friedrich-Mißler-Straße 42 28211 Bremen Grontmij GmbH Postfach 34 70 17 28339 Bremen Friedrich-Mißler-Straße 42 28211 Bremen T +49 421 2032-6 F +49 421 2032-747 E [email protected] W www.grontmij.de DELFI Benutzeranleitung Dateiversand für unsere

Mehr

Zwischenablage (Bilder, Texte,...)

Zwischenablage (Bilder, Texte,...) Zwischenablage was ist das? Informationen über. die Bedeutung der Windows-Zwischenablage Kopieren und Einfügen mit der Zwischenablage Vermeiden von Fehlern beim Arbeiten mit der Zwischenablage Bei diesen

Mehr

Hilfe Bearbeitung von Rahmenleistungsverzeichnissen

Hilfe Bearbeitung von Rahmenleistungsverzeichnissen Hilfe Bearbeitung von Rahmenleistungsverzeichnissen Allgemeine Hinweise Inhaltsverzeichnis 1 Allgemeine Hinweise... 3 1.1 Grundlagen...3 1.2 Erstellen und Bearbeiten eines Rahmen-Leistungsverzeichnisses...

Mehr

Kennen, können, beherrschen lernen was gebraucht wird www.doelle-web.de

Kennen, können, beherrschen lernen was gebraucht wird www.doelle-web.de Inhaltsverzeichnis Inhaltsverzeichnis... 1 Grundlagen... 2 Hyperlinks innerhalb einer Datei... 2 Verweisziel definieren... 2 Einen Querverweis setzen... 3 Verschiedene Arten von Hyperlinks... 3 Einfache

Mehr

Wordpress: Blogbeiträge richtig löschen, archivieren und weiterleiten

Wordpress: Blogbeiträge richtig löschen, archivieren und weiterleiten Wordpress: Blogbeiträge richtig löschen, archivieren und weiterleiten Version 1.0 Wordpress: Blogbeiträge richtig löschen, archivieren und weiterleiten In unserer Anleitung zeigen wir Dir, wie Du Blogbeiträge

Mehr

Systemnahe Software (Systemnahe Software II)

Systemnahe Software (Systemnahe Software II) 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

Mehr

Anleitung über den Umgang mit Schildern

Anleitung über den Umgang mit Schildern Anleitung über den Umgang mit Schildern -Vorwort -Wo bekommt man Schilder? -Wo und wie speichert man die Schilder? -Wie füge ich die Schilder in meinen Track ein? -Welche Bauteile kann man noch für Schilder

Mehr

Programme im Griff Was bringt Ihnen dieses Kapitel?

Programme im Griff Was bringt Ihnen dieses Kapitel? 3-8272-5838-3 Windows Me 2 Programme im Griff Was bringt Ihnen dieses Kapitel? Wenn Sie unter Windows arbeiten (z.b. einen Brief schreiben, etwas ausdrucken oder ein Fenster öffnen), steckt letztendlich

Mehr

Datenbanken Kapitel 2

Datenbanken Kapitel 2 Datenbanken Kapitel 2 1 Eine existierende Datenbank öffnen Eine Datenbank, die mit Microsoft Access erschaffen wurde, kann mit dem gleichen Programm auch wieder geladen werden: Die einfachste Methode ist,

Mehr

HANDBUCH PHOENIX II - DOKUMENTENVERWALTUNG

HANDBUCH PHOENIX II - DOKUMENTENVERWALTUNG it4sport GmbH HANDBUCH PHOENIX II - DOKUMENTENVERWALTUNG Stand 10.07.2014 Version 2.0 1. INHALTSVERZEICHNIS 2. Abbildungsverzeichnis... 3 3. Dokumentenumfang... 4 4. Dokumente anzeigen... 5 4.1 Dokumente

Mehr

Lineargleichungssysteme: Additions-/ Subtraktionsverfahren

Lineargleichungssysteme: Additions-/ Subtraktionsverfahren Lineargleichungssysteme: Additions-/ Subtraktionsverfahren W. Kippels 22. Februar 2014 Inhaltsverzeichnis 1 Einleitung 2 2 Lineargleichungssysteme zweiten Grades 2 3 Lineargleichungssysteme höheren als

Mehr

Drucken aus der Anwendung

Drucken aus der Anwendung Drucken aus der Anwendung Drucken aus der Anwendung Nicht jeder Großformatdruck benötigt die volle Funktionsvielfalt von PosterJet - häufig sind es Standarddrucke wie Flussdiagramme und Organigramme die

Mehr

Unterprogramme. Funktionen. Bedeutung von Funktionen in C++ Definition einer Funktion. Definition einer Prozedur

Unterprogramme. Funktionen. Bedeutung von Funktionen in C++ Definition einer Funktion. Definition einer Prozedur Unterprogramme Unterprogramme sind abgekapselte Programmfragmente, welche es erlauben, bestimmte Aufgaben in wiederverwendbarer Art umzusetzen. Man unterscheidet zwischen Unterprogrammen mit Rückgabewert

Mehr

Daten-Synchronisation zwischen dem ZDV-Webmailer und Outlook (2002-2007) Zentrum für Datenverarbeitung der Universität Tübingen

Daten-Synchronisation zwischen dem ZDV-Webmailer und Outlook (2002-2007) Zentrum für Datenverarbeitung der Universität Tübingen Daten-Synchronisation zwischen dem ZDV-Webmailer und Outlook (2002-2007) Zentrum für Datenverarbeitung der Universität Tübingen Inhalt 1. Die Funambol Software... 3 2. Download und Installation... 3 3.

Mehr

Externe Abfrage von E-Mail für Benutzer der HSA über Mozilla-Thunderbird

Externe Abfrage von E-Mail für Benutzer der HSA über Mozilla-Thunderbird Externe Abfrage von E-Mail für Benutzer der HSA über Mozilla-Thunderbird Vorweg zunächst einige allgemeine Worte: Sie müssen über einen Account bei uns verfügen und ein E-Mail-Postfach bei uns haben. Dann

Mehr

S7-Hantierungsbausteine für R355, R6000 und R2700

S7-Hantierungsbausteine für R355, R6000 und R2700 S7-Hantierungsbausteine für R355, R6000 und R2700 1. FB90, Zyklus_R/W Dieser Baustein dient zur zentralen Kommunikation zwischen Anwenderprogramm und dem Modul R355 sowie den Geräten R6000 und R2700 über

Mehr

Stellen Sie bitte den Cursor in die Spalte B2 und rufen die Funktion Sverweis auf. Es öffnet sich folgendes Dialogfenster

Stellen Sie bitte den Cursor in die Spalte B2 und rufen die Funktion Sverweis auf. Es öffnet sich folgendes Dialogfenster Es gibt in Excel unter anderem die so genannten Suchfunktionen / Matrixfunktionen Damit können Sie Werte innerhalb eines bestimmten Bereichs suchen. Als Beispiel möchte ich die Funktion Sverweis zeigen.

Mehr

Mediator 9 - Lernprogramm

Mediator 9 - Lernprogramm Mediator 9 - Lernprogramm Ein Lernprogramm mit Mediator erstellen Mediator 9 bietet viele Möglichkeiten, CBT-Module (Computer Based Training = Computerunterstütztes Lernen) zu erstellen, z. B. Drag & Drop

Mehr

AGROPLUS Buchhaltung. Daten-Server und Sicherheitskopie. Version vom 21.10.2013b

AGROPLUS Buchhaltung. Daten-Server und Sicherheitskopie. Version vom 21.10.2013b AGROPLUS Buchhaltung Daten-Server und Sicherheitskopie Version vom 21.10.2013b 3a) Der Daten-Server Modus und der Tresor Der Daten-Server ist eine Betriebsart welche dem Nutzer eine grosse Flexibilität

Mehr

I.1 Die Parrot Assemblersprache

I.1 Die Parrot Assemblersprache I.1 Die Parrot Assemblersprache Die virtuelle CPU Parrot ermöglicht die Verarbeitung der Parrot Assemblersprache (PASM). Zum Einstieg soll ein erstes Beispiel die Ausführung einer PASM-Datei zeigen. Legen

Mehr

GEONET Anleitung für Web-Autoren

GEONET Anleitung für Web-Autoren GEONET Anleitung für Web-Autoren Alfred Wassermann Universität Bayreuth [email protected] 5. Mai 1999 Inhaltsverzeichnis 1 Technische Voraussetzungen 1 2 JAVA-Programme in HTML-Seiten verwenden

Mehr

Anton Ochsenkühn. amac BUCH VERLAG. Ecxel 2016. für Mac. amac-buch Verlag

Anton Ochsenkühn. amac BUCH VERLAG. Ecxel 2016. für Mac. amac-buch Verlag Anton Ochsenkühn amac BUCH VERLAG Ecxel 2016 für Mac amac-buch Verlag 2 Word-Dokumentenkatalog! Zudem können unterhalb von Neu noch Zuletzt verwendet eingeblendet werden. Damit hat der Anwender einen sehr

Mehr

Benutzung der LS-Miniscanner

Benutzung der LS-Miniscanner Benutzung der LS-Miniscanner Seit Januar 2010 ist es möglich für bestimmte Vorgänge (Umlagerungen, Retouren, Inventur) die von LS lieferbaren Miniscanner im Format Autoschlüsselgröße zu benutzen. Diese

Mehr

Erweiterung der Aufgabe. Die Notenberechnung soll nicht nur für einen Schüler, sondern für bis zu 35 Schüler gehen:

Erweiterung der Aufgabe. Die Notenberechnung soll nicht nur für einen Schüler, sondern für bis zu 35 Schüler gehen: VBA Programmierung mit Excel Schleifen 1/6 Erweiterung der Aufgabe Die Notenberechnung soll nicht nur für einen Schüler, sondern für bis zu 35 Schüler gehen: Es müssen also 11 (B L) x 35 = 385 Zellen berücksichtigt

Mehr

Einführung in die Programmierung

Einführung in die Programmierung : Inhalt Einführung in die Programmierung Wintersemester 2008/09 Prof. Dr. Günter Rudolph Lehrstuhl für Algorithm Engineering Fakultät für Informatik TU Dortmund - mit / ohne Parameter - mit / ohne Rückgabewerte

Mehr

SelfLinux-0.10.0. cron

SelfLinux-0.10.0. cron cron Autor: JC PollmanChristian Richter ([email protected]@users.sourceforge.net) Formatierung: Matthias Hagedorn ([email protected]) Lizenz: GPL Linux bietet zwei verwandte Programme

Mehr

Logging, Threaded Server

Logging, Threaded Server Netzwerk-Programmierung Logging, Threaded Server Alexander Sczyrba Michael Beckstette {asczyrba,[email protected] 1 Übersicht Logging Varianten für concurrent server 2 current working directory

Mehr

Luis Kornblueh. May 22, 2014

Luis Kornblueh. May 22, 2014 Einführung in die Bash Luis Kornblueh KlosterCluster Team 2013/2014, Klosterschule May 22, 2014 1 / 17 Inhaltsverzeichnis Einführung in das Scripting Einfache Beispiele Kommandos ersetzen Bedingungen Tests

Mehr

Automatisierung ( Fernsteuerung ) von Excel unter Microsoft Windows Tilman Küpper ([email protected])

Automatisierung ( Fernsteuerung ) von Excel unter Microsoft Windows Tilman Küpper (tilman.kuepper@hm.edu) HMExcel Automatisierung ( Fernsteuerung ) von Excel unter Microsoft Windows Tilman Küpper ([email protected]) Inhalt 1. Einleitung...1 2. Beispiele...2 2.1. Daten in ein Tabellenblatt schreiben...2

Mehr

Abschluss Version 1.0

Abschluss Version 1.0 Beschreibung Der Abschluss wird normalerweise nur einmal jährlich durchgeführt. Dieses Tech-Note soll helfen, diesen doch seltenen aber periodisch notwendigen Vorgang problemlos durchzuführen. Abschlussvarianten

Mehr

Arbeiten mit UMLed und Delphi

Arbeiten mit UMLed und Delphi Arbeiten mit UMLed und Delphi Diese Anleitung soll zeigen, wie man Klassen mit dem UML ( Unified Modeling Language ) Editor UMLed erstellt, in Delphi exportiert und dort so einbindet, dass diese (bis auf

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

Einführung in die technische Informatik

Einführung in die technische Informatik Einführung in die technische Informatik Christopher Kruegel [email protected] http://www.auto.tuwien.ac.at/~chris Betriebssysteme Aufgaben Management von Ressourcen Präsentation einer einheitlichen

Mehr

Outlook. sysplus.ch outlook - mail-grundlagen Seite 1/8. Mail-Grundlagen. Posteingang

Outlook. sysplus.ch outlook - mail-grundlagen Seite 1/8. Mail-Grundlagen. Posteingang sysplus.ch outlook - mail-grundlagen Seite 1/8 Outlook Mail-Grundlagen Posteingang Es gibt verschiedene Möglichkeiten, um zum Posteingang zu gelangen. Man kann links im Outlook-Fenster auf die Schaltfläche

Mehr

K.U.Müller www.muellerbahn.de November 2009

K.U.Müller www.muellerbahn.de November 2009 K.U.Müller www.muellerbahn.de November 2009 Anschluss der Acryl-Röhren an die MpC Ziel ist es, den jeweiligen Röhren eine Block-Nummer zuzuordnen, um in diesem Block außerhalb der Anlage einen Zug parken

Mehr

DIRECTINFO 5.7 SICHERHEITSKONZEPTE FÜR BENUTZER, INFORMATIONEN UND FUNKTIONEN

DIRECTINFO 5.7 SICHERHEITSKONZEPTE FÜR BENUTZER, INFORMATIONEN UND FUNKTIONEN DIRECTINFO 5.7 SICHERHEITSKONZEPTE FÜR BENUTZER, INFORMATIONEN UND FUNKTIONEN - Whitepaper 1 Autor: Peter Kopecki Version: 1.2 Stand: Mai 2006 DIRECTINFO 5.7... 1 SICHERHEITSKONZEPTE FÜR BENUTZER, INFORMATIONEN

Mehr

Leitfaden zur ersten Nutzung der R FOM Portable-Version für Windows (Version 1.0)

Leitfaden zur ersten Nutzung der R FOM Portable-Version für Windows (Version 1.0) Leitfaden zur ersten Nutzung der R FOM Portable-Version für Windows (Version 1.0) Peter Koos 03. Dezember 2015 0 Inhaltsverzeichnis 1 Voraussetzung... 3 2 Hintergrundinformationen... 3 2.1 Installationsarten...

Mehr

Dieses UPGRADE konvertiert Ihr HOBA-Finanzmanagement 6.2 in die neue Version 6.3. Ein UPGRADE einer DEMO-Version ist nicht möglich.

Dieses UPGRADE konvertiert Ihr HOBA-Finanzmanagement 6.2 in die neue Version 6.3. Ein UPGRADE einer DEMO-Version ist nicht möglich. UPGRADE Version 6.2 -> Version 6.3 Dieses UPGRADE konvertiert Ihr HOBA-Finanzmanagement 6.2 in die neue Version 6.3. Ein UPGRADE einer DEMO-Version ist nicht möglich. Bitte beachten Sie, dass das UPGRADE

Mehr

Guideline. Facebook Posting. mit advertzoom Version 2.3

Guideline. Facebook Posting. mit advertzoom Version 2.3 Guideline Facebook Posting mit advertzoom Version 2.3 advertzoom GmbH advertzoom GmbH Stand November 2012 Seite [1] Inhalt 1 Facebook Posting Schnittstelle... 3 1.1 Funktionsüberblick... 3 2 Externe Ressource

Mehr

Informatik Grundlagen, WS04, Seminar 13

Informatik Grundlagen, WS04, Seminar 13 Informatik Grundlagen, WS04, Seminar 13 Informatik Informatik Grundlagen, Seminar 13 WS04 1 Was wir heute besprechen Nachbesprechen von Übungsblatt 11 Rekursion Grundprinzipien Übung Besprechung Übungsblatt

Mehr

C.M.I. Control and Monitoring Interface. Zusatzanleitung: Datentransfer mit CAN over Ethernet (COE) Version 1.08

C.M.I. Control and Monitoring Interface. Zusatzanleitung: Datentransfer mit CAN over Ethernet (COE) Version 1.08 C.M.I. Version 1.08 Control and Monitoring Interface Zusatzanleitung: Datentransfer mit CAN over Ethernet (COE) de LAN LAN Beschreibung der Datentransfermethode Mit dieser Methode ist es möglich, analoge

Mehr

Hilfedatei der Oden$-Börse Stand Juni 2014

Hilfedatei der Oden$-Börse Stand Juni 2014 Hilfedatei der Oden$-Börse Stand Juni 2014 Inhalt 1. Einleitung... 2 2. Die Anmeldung... 2 2.1 Die Erstregistrierung... 3 2.2 Die Mitgliedsnummer anfordern... 4 3. Die Funktionen für Nutzer... 5 3.1 Arbeiten

Mehr

Leitfaden zum Sichern einer Festplatte als Image mit der System Rescue CD

Leitfaden zum Sichern einer Festplatte als Image mit der System Rescue CD Leitfaden zum Sichern einer Festplatte als Image mit der System Rescue CD Benötigte Dinge: Eine System Rescue CD (kann vom Internet heruntergeladen werden http://www.sysresccd.org) Eine USB Festplatte

Mehr

BEDIENUNG ABADISCOVER

BEDIENUNG ABADISCOVER BEDIENUNG ABADISCOVER Juni 2005 / EMO v.2005.1 Diese Unterlagen sind urheberrechtlich geschützt. Alle Rechte, auch die der Übersetzung, des Nachdrucks und der Vervielfältigung der Unterlagen, oder Teilen

Mehr

Mind Mapping am PC. für Präsentationen, Vorträge, Selbstmanagement. von Isolde Kommer, Helmut Reinke. 1. Auflage. Hanser München 1999

Mind Mapping am PC. für Präsentationen, Vorträge, Selbstmanagement. von Isolde Kommer, Helmut Reinke. 1. Auflage. Hanser München 1999 Mind Mapping am PC für Präsentationen, Vorträge, Selbstmanagement von Isolde Kommer, Helmut Reinke 1. Auflage Hanser München 1999 Verlag C.H. Beck im Internet: www.beck.de ISBN 978 3 446 21222 0 schnell

Mehr

Feiertage in Marvin hinterlegen

Feiertage in Marvin hinterlegen von 6 Goecom GmbH & Co KG Marvin How to's Feiertage in Marvin hinterlegen Feiertage spielen in Marvin an einer Reihe von Stellen eine nicht unerhebliche Rolle. Daher ist es wichtig, zum Einen zu hinterlegen,

Mehr

Inhaltverzeichnis 1 Einführung... 1 2 Zugang zu den Unifr Servern... 1. 3 Zugang zu den Druckern... 4 4 Nützliche Links... 6

Inhaltverzeichnis 1 Einführung... 1 2 Zugang zu den Unifr Servern... 1. 3 Zugang zu den Druckern... 4 4 Nützliche Links... 6 Inhaltverzeichnis 1 Einführung... 1 2 Zugang zu den Unifr Servern... 1 2.1 Version Mac OSX 10.1-10.4, 10.6-10.7... 1 2.2 Version Mac OSX 10.5 (Leopard)... 2 3 Zugang zu den Druckern... 4 4 Nützliche Links...

Mehr

Antolin-Titel jetzt automatisch in WinBIAP kennzeichnen

Antolin-Titel jetzt automatisch in WinBIAP kennzeichnen & Antolin-Titel jetzt automatisch in WinBIAP kennzeichnen Immer mehr Schulen setzen das erfolgreiche Leseförderungsprojekt "Antolin" ein - und die Bibliotheken verzeichnen große Nachfrage nach den Kinderbüchern,

Mehr

Delegatesund Ereignisse

Delegatesund Ereignisse Delegatesund Ereignisse «Delegierter» Methoden Schablone Funktionszeiger Dr. Beatrice Amrhein Überblick Definition eines Delegat Einfache Delegate Beispiele von Delegat-Anwendungen Definition eines Ereignisses

Mehr

TR75E002-A FA / 07.2014. Bedienungsanleitung Industrie-Handsender-Verwaltung IHV DEUTSCH

TR75E002-A FA / 07.2014. Bedienungsanleitung Industrie-Handsender-Verwaltung IHV DEUTSCH TR75E002-A FA / 07.2014 Bedienungsanleitung Industrie-Handsender-Verwaltung IHV DEUTSCH Inhaltsverzeichnis 1 Das Programm Industrie-Handsender-Verwaltung IHV... 3 1.1 Was ist das Programm Industrie-Handsender-Verwaltung

Mehr

R-ADSL2+ Einrichthinweise unter Windows 98/ME

R-ADSL2+ Einrichthinweise unter Windows 98/ME R-ADSL2+ Einrichthinweise unter Windows 98/ME Verwenden Sie einen externen Router? Dann folgen Sie bitte der Anleitung des Routers und NICHT unseren zur Einrichtung einer Internetverbindung unter Windows

Mehr

Er musste so eingerichtet werden, dass das D-Laufwerk auf das E-Laufwerk gespiegelt

Er musste so eingerichtet werden, dass das D-Laufwerk auf das E-Laufwerk gespiegelt Inhaltsverzeichnis Aufgabe... 1 Allgemein... 1 Active Directory... 1 Konfiguration... 2 Benutzer erstellen... 3 Eigenes Verzeichnis erstellen... 3 Benutzerkonto erstellen... 3 Profil einrichten... 5 Berechtigungen

Mehr

Text Formatierung in Excel

Text Formatierung in Excel Text Formatierung in Excel Das Aussehen des Textes einer oder mehrerer Zellen kann in Excel über verschiedene Knöpfe beeinflusst werden. Dazu zuerst die betroffenen Zelle(n) anwählen und danach den entsprechenden

Mehr

Wo möchten Sie die MIZ-Dokumente (aufbereitete Medikamentenlisten) einsehen?

Wo möchten Sie die MIZ-Dokumente (aufbereitete Medikamentenlisten) einsehen? Anleitung für Evident Seite 1 Anleitung für Evident-Anwender: Einbinden der MIZ-Dokumente in Evident. Wo möchten Sie die MIZ-Dokumente (aufbereitete Medikamentenlisten) einsehen? Zunächst müssen Sie entscheiden,

Mehr

affilinet_ Flash-Spezifikationen

affilinet_ Flash-Spezifikationen affilinet_ Flash-Spezifikationen Inhaltsverzeichnis Allgemeines...2 Klickzählung...2 Lead/Sale Programme... 2 PPC und Kombi Programme...3 Übergabe von Formulardaten...4 clicktag Variante Sale/Lead Programm...4

Mehr

Artikel Schnittstelle über CSV

Artikel Schnittstelle über CSV Artikel Schnittstelle über CSV Sie können Artikeldaten aus Ihrem EDV System in das NCFOX importieren, dies geschieht durch eine CSV Schnittstelle. Dies hat mehrere Vorteile: Zeitersparnis, die Karteikarte

Mehr

Stepperfocuser 2.0 mit Bootloader

Stepperfocuser 2.0 mit Bootloader Stepperfocuser 2.0 mit Bootloader Info Für den Stepperfocuser 2.0 gibt es einen Bootloader. Dieser ermöglicht es, die Firmware zu aktualisieren ohne dass man ein spezielles Programmiergerät benötigt. Die

Mehr

Übung zu Grundlagen der Betriebssysteme. 13. Übung 22.01.2012

Übung zu Grundlagen der Betriebssysteme. 13. Übung 22.01.2012 Übung zu Grundlagen der Betriebssysteme 13. Übung 22.01.2012 Aufgabe 1 Fragmentierung Erläutern Sie den Unterschied zwischen interner und externer Fragmentierung! Als interne Fragmentierung oder Verschnitt

Mehr

Massenversand Dorfstrasse 143 CH - 8802 Kilchberg Telefon 01 / 716 10 00 Telefax 01 / 716 10 05 [email protected] www.hp-engineering.

Massenversand Dorfstrasse 143 CH - 8802 Kilchberg Telefon 01 / 716 10 00 Telefax 01 / 716 10 05 info@hp-engineering.com www.hp-engineering. Massenversand Massenversand Seite 1 Massenversand Seite 2 Inhaltsverzeichnis 1. WICHTIGE INFORMATIONEN ZUR BEDIENUNG VON CUMULUS 4 2. STAMMDATEN FÜR DEN MASSENVERSAND 4 2.1 ALLGEMEINE STAMMDATEN 4 2.2

Mehr