TCPIPProgrammierung 3 Systemnahe Programmierung Hochschule Fulda FB AI
TCP/IP-Programmierung 3 Systemnahe Programmierung Hochschule Fulda – FB AI Wintersemester 2017/18 http: //tp. rz. hs-fulda. de Peter Klingebiel, HS Fulda, FB AI
Socket 1 • Wiederholung: Socketschnittstelle Anwendungsprogramm Socketschnittstelle Betriebssystem Netzschnittstelle LAN TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 2
Socket 2 • Wiederholung: Transparenz von Socket-IO TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 3
Systemcall 1 • Definition • ein Systemaufruf, auch Systemcall (von englisch system call) oder kurz Syscall, ist in der Computer-technik eine von Anwendungsprogrammen benutzte Methode, um vom Betriebssystem bereitgestellte Funktionalitäten auszuführen, wie etwa das Lesen einer Datei. Dabei wird die Kontrolle vom Programm an den Kernel übergeben. • nach: https: //de. wikipedia. org/wiki/Systemaufruf • die Übergabe der Kontrolle von der Anwendung an den Kernel (und vice versa) wird Kontextwechsel genannt • der context switch vom user mode in den kernel mode und wieder zurück ist relativ aufwendig TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 4
Systemcall 2 • Systemcall-Schnittstelle des Betriebssystems Anwendungsprogramm Systemaufrufschnittstelle Systemaufrufbibliotheken Systemaufrufe / -tabelle Betriebssystemmodule user mode context switch kernel mode Hardware TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 5
Systemcall 3 • Systemcalls werden der Anwendung in Form von C-Bibliotheksroutinen zur Verfügung gestellt • sie sind in den Manualpages i. d. R. in der Sektion 2 dokumentiert • Beispiel: man -s 2 open • die meisten sind in der Standard libc enthalten • einige müssen ggfs. extra dazugebunden werden • zum Beispiel erfordern die Socketsyscalls bei Solaris das Einbinden von libsocket und libnsl • Beispiel: gcc -o stest. c -lsocket –lnsl • bei Linuxsystemen Socketsyscalls libc • bei Windows abhängig vom Compiler, meist libc TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 6
Systemcall 4 • im Betriebssystem sind die Syscalls in einer Tabelle mit den Verzweigungsadressen der jeweiligen Syscall -Routinen verfügbar • bei Aufruf eines Syscalls werden die Parameter der Syscall-Funktion und die Nummer des Syscalls (= Index in die Syscalltable) an das Betriebssystem übergeben • der Syscall wird durch einen Interrupt ausgelöst • dabei Kontextwechsel user mode kernel mode • im Betriebssystem: Abarbeitung der Syscall-Routine • Rückgabe des Returnwerts • dann Kontextwechsel kernel mode user mode TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 7
Prozesse 1 • wichtige Systemaufrufe für Prozesse • Prozessinformationen Die Informationen oder Daten, die das Betriebssystem über Prozesse im PCB (Process Control Block) zum Management des Prozessystems speichert, umfassen z. B. die Process Id PID des Prozesses, die Process Id des Elternprozesses PPID sowie die User Id UID und die Group Id GID, mit denen der Prozess ausgeführt wird. Diese Informationen können nicht durch einen direkten Zugriff auf den PCB gelesen werden, sondern müssen über entsprechende Systemaufrufe vom Betriebssystemkernel ausgelesen werden. TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 8
Prozesse 2 • für die vier genannten Prozessattribute PID, PPID, UID und GID sind dies #include <unistd. h> #include <sys/types. h> pid_t getpid(void); // liefert PID pid_t getppid(void); // liefert PPID uid_t getuid(void); // liefert UID gid_t getgid(void); // liefert GID TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 9
Prozesse 3 • zwei weitere wichtige Attribute von Prozessen sind noch die Effective UID EUID und die Effective GID EGID, die IDs angeben, unter den ein Prozess tatsächlich läuft und aus denen sich die entsprechenden Zugriffsrechte des Prozesses ergeben • dies ist oft bei Serverprozessen notwendig, die von der root und damit Superuser-Privilegien gestartet werden und nach dem Start die IDs auf eine weniger priviligierte ID wechseln, um nur mit den von ihnen benötigten Rechten abzulaufen • Beispiel: ein Webserver wird von der root mit root-Rechten gestartet, die er z. B. für das Reservieren von sockets benötigt. Sobald die root-Rechte nicht mehr erforderlich sind, wechselt der Prozess auf eine eigene UID, z. B. httpd TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 10
Prozesse 4 • Prozesserzeugung und Programmausführung • die Erzeugung eines neuen Prozesses erfolgt mit dem Systemcall fork(). Dabei wird ein Kindprozess als Kopie des aufrufenden Prozesses (Elterprozess) angelegt, der i. w. alle Daten des Elternprozesses erbt. Nach dem Aufruf von fork() existieren also zwei Prozesse, der Elternprozess und der Kindprozess. An den Elternprozess gibt fork() die PID des Kindprozesses zurück, der Kindprozess bekommt als Rückgabe den Wert 0. Im Fehlerfall wird -1 geliefert. Am Rückgabewert kann also erkannt und unterschieden werden, welcher Prozess Elternprozess und welcher Prozess Kindprozess ist und was im Elternprozess und was im Kindprozess im weiteren Programmlauf bearbeitet werden muss. TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 11
Prozesse 5 • Beide Prozesse sind nach dem fork() weitgehend unabhängig voneinander und können jeweils ganz verschiedene eigene Aufgaben erledigen. Der Elterprozess sollte aber unbedingt auf den Exit des Kindprozesses warten, um einen Zombiezustand des Kindes zu verhindern, und den Exitwert des Kindes ermitteln und auswerten zu können. Dies erfolgt mit dem Systemcall wait(). In wait() wartet der Elternprozess auf des Ende des Kindes und kann dann aus dem zu übergebenden Statusparameter den Rückgabewert des Kindes ermitteln. Der Statusparameter von Typ int enthält im oberen Byte den Rückgabewert des Kindprozesses, im unteren Byte finden sich Statusinfos. TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 12
Prozesse 6 • Schnittstellen von fork() und wait() #include <unistd. h> #include <sys/types. h #include <sys/wait. h> // neuen Prozess erzeugen pid_t fork(); // auf Ende des Kindprozesses warten pid_t wait(int *status); TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 13
Prozesse 7 • typischer Programmrumpf pid_t pid; // PID des neuen Prozesses int status; // Kindstatus nach dem Ende pid = fork(); // Kindprozess erzeugen if(pid == -1) // Fehler . . . // ab hier: zwei Prozesse else if(pid == 0) // im Kindprozess . . . exit(0); // Kind terminiert else // pid > 0 // im Elternprozess . . . wait(&status); // Kindende warten TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 14
Prozesse 8 • Ausführung eines externen Programm • Häufig wird im Kindprozess ein anderes, externes Programm ausgeführt, d. h. das Kind wird mit dem neuen Programm überlagert und die Kontrolle im Kindprozess geht an das neue Programm über. Dies geschieht mit den exec()-Funktionen, die es in einigen Varianten gibt. #include <unistd. h> int execl(char *path, char *arg, . . . ); Hierbei enthält path den Pfadnamen des auszuführenden Programms, danach folgen die üblichen Parameter für die Funktion main(), wobei der letzte Parameter der NULLString sein muss, z. B. execl("/bin/ps", "-f", NULL); TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 15
Signale 1 • Synchronisation/Kommunikation mit Signalen • Eine sehr einfache Möglichkeit der Prozesskommunikation und/oder -synchronisation ist die Versendung von Signalen. UNIX definiert eine ganze Reihe dieser Signale, z. B. SIGTERM (Prozess beenden), SIGINT (Prozess abbrechen) oder SIGUSR 1 (Nutzerdefiniertes Signal 1) u. v. a. m. Von der Kommandozeile aus wird mit der Eingabe von <Strg-C> ein laufendes Programm abgebrochen, mit dem Kommando kill -USR 1 pid wird das Signal SIGUSR 1 an den Prozess mit der PID pid versendet. Bei den meisten Signalen wird der aktuelle Prozess abgebrochen bzw. terminiert, wenn diese nicht explizit abgefangen werden, um sie mit einem Signalhandler entsprechend zu behandeln. Mit dem Systemcall kill() können Signale an Prozesse, z. B. vom Eltern- zum Kindprozess oder vom Kind- zum Elternprozess versendet werden. TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 16
Signale 2 • Schnittstellen der Signalfunktionen #include <sys/types. h> #include <signal. h> typedef void (*sighandler_t)(int); sighandler_t signal(int signo, sighandler_t handler); int kill(pid_t pid, int sig); • Das Vorgehen ist daher so, dass zunächst ein Signalhandler für jedes Signal erstellt wird, das abgefangen und behandelt werden soll. Wichtig ist dabei, dass im Signalhandler das Signal erneut abgefangen werden muss. TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 17
Signale 3 • typischer Programmrumpf // Signalhandler fuer das Signal SIGUSR 1 void handleusr 1(int sig) { signal(SIGUSR 1, handleusr 1) // Signal abfangen . . . // Handlercode } // main int main(int arc, char **argv) { . . . // Maincode signal(SIGUSR 1, handleusr 1); // SIGUSR 1 abfangen . . . } TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 18
Signale 4 • Ein Kindprozess z. B. kann mit dem Systemcall kill() ein Signal an den Elternprozess senden: pid_t ppid = getppid(); . . . kill(ppid, SIGUSR 1); Damit ist eine, wenn auch sehr einfache Kommunikation zwischen Eltern- und Kindprozessen möglich. TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 19
Input / Output 1 • Eingaben und Ausgaben, Lesen und Schreiben passieren i. w. über die beiden Systemcalls read() und write(). Diese werden auch von den höherwertigen Funktionen wie printf() und scanf() u. a. verwendet. #include <unistd. h> ssize_t read(int fd, void *buf, size_t nbyte); ssize_t write(int fd, void *buf, size_t nbyte); Die Parameter für die beiden Systemcalls sind der Filedescriptor fd, ein Index in die dem Prozess zugehörige Filetabelle. Die Filedescriptoren 0 für stdin, 1 für stdout und 2 für stderr sind beim Programmstart für die Standardeingabe, die Standardausgabe und die Standardfehlerausgabe geöffnet. TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 20
Input / Output 2 • Mit dem CPP-Makro fileno(FILE *) , z. B. fileno(stdin), können die Filedescriptoren ermittelt werden. Beim Lesen wird der Puffer buf mit maximal nbyte Bytes gefüllt, beim Schreiben werden maximal nbyte Bytes aus dem Puffer buf geschrieben. Bedeutsam ist, dass die beiden Systemcalls read() und write() für (fast) beliebige Ein-/Ausgabedevices verwendet werden können, insbesondere Dateien, Geräte (Tastatur, Bildschirm), Pipes, TCP/IP-Sockets usw. Das eigentliche IO, also Lesen und Schreiben ist somit transparent. Beide Systemcalls liefern die Anzahl gelesener oder geschriebener Bytes, 0 bei EOF und -1 bei Fehler. TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 21
Dateien 1 • Dateihandling • Dateien werden mit dem Systemcall open() geöffnet und ggfs. erzeugt, mit creat() erzeugt und mit close() geschlossen. #include <sys/types. h> #include <sys/stat. h> #include <fcntl. h> int open(char *name, int flags); int creat(char *name, mode_t mode); int open(char *name, int flags, mode_t mode); int close(int fd); TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 22
Dateien 2 • Der Parameter name gibt dabei einen Pfadnamen für die Datei an, flags die Art des Öffnens (O_RDONLY - read only, O_WRONLY - write only, O_RDWR - read and write) an, mode die Zugriffsrechte beim Neuerzeugen einer Datei, z. B. 0644, d. i. owner: read+write, group: read, world: read, mit creat() oder open(). Beim Kreieren einer Datei mit open() muss flags auch O_CREAT kodieren, z. B. mit O_WRONLY|O_CREAT. open() und creat() liefern einen gültigen Filedescriptor oder -1 bei Fehler. Mit close() wird eine offene Datei wieder geschlossen, wobei der Parameter fd einen gültigen Filedescriptor darstellt. Nach dem Öffnen kann, referenziert über den Filedescriptor, die Datei (das Gerät, der Socket, die Pipe usw. ) gelesen und beschrieben werden. TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 23
Dateien 3 char buf[128]; // Schreib-/ Lesepuffer int fdr, fdw; // Filedescriptoren int nr, nw; // Anzahl Bytes // Datei from. txt zum Lesen oeffnen fdr = open( from. txt , O_RDONLY); if(fdr == -1). . . // Fehler // Datei to. txt Kreieren und zum Schreiben oeffnen fdw = open( to. txt , O_WRONLY |O_CREAT, 0644); if(fdw == -1). . . // Fehler // Kopieren: von fdr lesen und auf fdw schreiben nr = read(fdr, buf, sizeof(buf)); if(nr == 0). . . // EOF - end of file else if(nr == -1). . . // Fehler nw = write(fdw, buf, nr); if(nw != nr). . . // Fehler close(fdr); close(fdw); TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 24
Pipes 1 • Pipes, in diesem Fall anonyme Pipes, bilden einen unidirektionalen Kanal zur Kommunikation von Prozessen. Mit dem Systemcall pipe() wird eine Pipe erzeugt und passend geöffnet. #include <unistd. h> int pipe(int pipefd[2]); Der Parameter pipefd ist ein Feld von zwei Integern, dem im Index 0 die Leseseite, im Index 1 die Schreibseite der Pipe zugeordnet wird, d. h. pipefd[0] ist zum Lesen der Pipe, pipefd[1] zum Schreiben der Pipe geöffnet. Die eigentlichen IO-Operationen geschehen dann mit den Syscalls read() und write(). Pipes werden i. d. R. zwischen voneinander abhängigen Prozessen angelegt, also z. B. zwischen Eltern - und Kindprozess. TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 25
Pipes 2 • Da bei der Prozesserzeugung mit fork() eine Kopie des aufrufenden Prozesses erstellt wird und somit auch die Umgebung des aufrufenden Prozesses, insbesondere auch die offenen Files, an den Kindprozess vererbt wird, muss eine Pipe vor dem fork()-Aufruf erzeugt werden, um die Kommunikation zwischen Eltern- und Kindprozess möglich zu machen. int pfd[2]; pid_t pid; pipe(pfd); . . . pid = fork(); . . . // Pipe -FDs // Kind-PID // Pipe erzeugen // Neuen Prozess erzeugen TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 26
Pipes 3 • Nach dem fork() ist die Pipe für Eltern - und Kindprozess angelegt und geöffnet und kann zur Kommunikation zwischen beiden genutzt werden. Da Pipes unidirektional sind, ist es guter Brauch, dass der schreibende Prozess die Leseseite der Pipe schliesst (close(pfd[0])) und der lesende Prozess die Schreibseite der Pipe (close(pfd[1])). Interessant wird eine Pipe auch dadurch, dass man die Filedescriptoren der Pipe auf die Filedescriptoren von stdin, stdout usw. duplizieren bzw. kopieren kann, was mit dem Systemcall dup 2() vorgenommen wird. #include <unistd. h> int dup 2(int oldfd, int newfd); TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 27
Dateiattribute 1 • Dateien im Dateisystem von Unix/Linux haben Attribute wie Typ, Größe, Besitzer, Zugriffsrechte und einige andere mehr. Diese Informationen können mit dem Systemcall stat() ausgelesen und interpretiert werden. #include <sys/types. h> #include <sys/stat. h> #include <unistd. h> int stat(char *pathname, struct stat *buf); Dabei gibt pathname den Dateinamen an, buf ist ein Pointer auf ein struct stat, das die Informationen enthält. TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 28
Dateiattribute 2 • Einige wichtige Elemente von struct stat bzw. Attribute von Dateien sind: struct stat { . . . ino_t st_ino; mode_t st_mode; nlink_t st_nlink; uid_t st_uid; gid_t st_gid; . . . off_t st_size; . . . }; // Inode –Nummer // Modus / Zugriffsrechte // Anzahl Links // Benutzer-ID Besitzer // Gruppen-ID Besitzer // Dateigröße in Bytes TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 29
Dateiattribute 3 • Der Aufruf von stat() und die Ausgabe von Attributen kann z. B. in folgender Weise erfolgen: char *filename; struct statbuf; if(stat(filename, &statbuf) == 0) { // ok printf("Datei: %sn", filename); printf("Groesse: %d Bytesn", statbuf. st_size); printf("Modus: %on", statbuf. st_mode); } In der Komponente Modus (statbuf. st_mode) ist ausser den Zugriffsrechten der Datei (3 Bit Besitzer, 3 Bit Gruppe, 3 Bit Rest der Welt) auch der Dateityp kodiert, der am einfachsten über entsprechende Makros ermittelt werden kann: if(S_ISREG(statbuf. st_mode)) printf("Dateityp: regulaere Datein"); else if(S_ISDIR(statbuf. st_mode)) printf("Dateityp: Verzeichnisn"); TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 30
Verzeichnisse 1 • Directories sind bei Unix/Linux Dateien, die spezielle Informationen über die im Verzeichnis vorhandenen Dateien enthalten, insbesondere den Dateinamen und die Inodenummer jeder Datei. Verzeichnisse können mit den C-Funktionen der Standardbibliothek opendir(), closedir() und readdir() geöffnet und gelesen werden. #include <sys/types. h> #include <dirent. h> DIR *opendir(const char *name); int closedir(DIR *dirp); struct dirent *readdir(DIR *dirp); TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 31
Verzeichnisse 2 • Die Funktion opendir() öffnet ein Verzeichnis mit dem Namen name und liefert bei Erfolg einen Zeiger auf den Directorydatentyp DIR zurück, im Fehlerfall den NULLPointer. Mit closedir() wird das Verzeichnis wieder geschlossen. Die Funktion readdir() liefert für ein mit opendir() geöffnetes Verzeichnis DI einen Pointer auf den Datentyp struct dirent, der Informationen zu den Einträgen / Dateien im Verzeichnis enthält. Der Typ struct dirent enthält z. B. folgende Informationen: struct dirent { ino_t d_ino; . . . char d_name[256]; }; // Inodenumer der Datei // Dateinamen TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 32
Verzeichnisse 3 • Ein Verzeichnis kann so in folgender Weise gelesen werden: char *dirname; DIR *dptr; struct dirent *deptr; . . . if(dptr = opendir(dirname)) { while(deptr = readdir(dptr)) printf("%dt%s n", deptr->d_ino, deptr->d_name); closedir(dptr); }. . . TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 33
Synchrones Multiplexing 1 • Prozess-Zustandsdiagramm TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 34
Synchrones Multiplexing 2 • I/O ist i. d. R. blockierend, d. h. ein Prozess, der z. B. an einem I/O-Kanal (=Filedeskriptor) auf eine Eingabe wartet, wird bis zum Abschluss der Eingabe blockiert • damit werden auch weitere I/O-Kanäle des Prozesses, die für I/O bereit sind, blockiert, das I/O wird nicht bearbeitet • ein Umschalten der Filedeskriptoren auf nichtblockierendes I/O ist zwar möglich, führt aber zum sog. I/O-Polling • I/O-Polling bedeutet ständiges Abfragen auf I/O und damit unnötige Last für die CPU und Verbrauch von Rechenzeit • wartet ein Prozess also auf I/O von mehreren Kanälen, ist synchrones I/O-Multiplexing sinnvoll • synchrones Multiplexing kann mit der Systemfunktion select()durchgeführt werden TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 35
Synchrones Multiplexing 3 • Schnittstelle von select() #include <sys/select. h> int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); • dabei gibt n den höchstmöglichen Filedescriptor an, die folgenden drei Parameter sind Pointer auf Filedescriptor. Sets fd_set für read(), write() und Ausnahmesituationen, z. B. Out-of-band-Daten. Der Typ fd_set ist ein Bitfeld, in dem jedem Bit ein zu überwachender Filedescriptor zugeordnet wird. Mit timeout vom Typ struct timeval* kann ein Timeout eingestellt werden • nur erforderliche fd_set-Parameter werden gesetzt und übergeben, weitere Parameter sind NULL gesetzt TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 36
Synchrones Multiplexing 4 • die Einstellung der fd_set-Parameter erfolgt über Makros FD_ZERO(fd_set *set); FD_SET(int fd, fd_set *set); FD_CLR(int fd, fd_set *set); FD_ISSET(int fd, fd_set *set); – – FD_ZERO() setzt alle Bits im fd_set auf 0 FD_SET() setzt Filedescriptor fd im fd_set auf 1 FD_CLR() setzt Filedescriptor fd im fd_set auf 0 FD_ISSET() prüft, ob Bit für Filedescriptor fd gesetzt ist • häufig werden nur die Lese-Filedescriptoren mittels synchronem Multiplexing mit select() überwacht • ein Timeout ist oft nicht erforderlich • select() blockiert bis der erste Kanal für I/O bereit ist • meist Parameter n und readfds gesetzt, der Rest ist NULL • select() gibt die Anzahl bereiter Filedeskriptoren zurück TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 37
Synchrones Multiplexing 5 • einfaches Beispiel, bei dem die Filedescriptoren fd 1 und fd 2 überwacht werden (fd 2 sei der höchste Deskriptor) int fd 1 =. . . // Lese-Filedescriptor int fd 2 =. . . // Lese-Filedescriptor. . . fd_set rfds; // zu überwachendes fd_set. . . FD_ZERO(&rfds); // rfds-Bits auf 0 setzen FD_SET(fd 1, &rfds); // fd 1 in rfds eintragen FD_SET(fd 2, &rfds); // fd 2 in rfds eintragen. . . int r = select(fd 2+1, &rfds, NULL, NULL); if(r > 0) { if(FD_ISSET(fd 1, &rfds)) // von fd 1 lesen . . . if(FD_ISSET(fd 2, &rfds)) // von fd 2 lesen . . . } TCP/IP-Programmierung - Peter Klingebiel - HS Fulda - FB AI 38
- Slides: 38