BS Übung 10 (Stefan Bosse) [03.02.2025] |
Punkte: | Total | /2 | 1. | /2 | 2. | /2 | 3. | /2 |
In dieser Übung sollen:
Was benötigt wird:
Vertiefende Informationen können hier gefunden werden:
Prozesse können über Pipes kommunizieren. Dabei werden wesentliche Aspekte adressiert:
Eine Pipe ist also eine Warteschlange (Queue) mit einer FIFO Reihenfolge. Eine Pipe verwendet zwei Dateideskriptoren mit dem ersten Deskriptor für das Lesen aus und den zweiten Deskriptor für das Schreiben in die Pipe.
#include <unistd.h>
int fd[2];
int pipe(int fd[2]);
// Returns: 0 if OK, –1 on error
Das Lesen und Schreiben kann mit den read
und write
Operationen durchgeführt werden. Beide Operationen können den Prozess blockieren. Bei der read
Operation ist das der Fall wenn keine Daten in der Pipe (Queue) sind, und bei der write
Operation wenn die Queue der Pipe voll ist (Größe des Puffers ist aber unbekannt).
#include <unistd.h>
int fd[2];
#define READ 0
#define WRITE 1
void foo() {
char recvbuf[10],sendbuf[10];
int n=pipe(fd);
if (n!=0) error();
...
n=write(fd[WRITE],sendbuf,numbytes);
// Returns number of written bytes or error
if (n<=0) error();
...
n=read(fd[READ],recvbuf,bufsize);
// Returns number of read bytes or error
if (n<=0) error();
}
Read und Write Operationen können den Prozess blockieren. Will man aus mehreren Kanälen "parallel" lesen wäre das mit den blockierenden Operationen nicht möglich.
Zum Beispiel soll ein Konsumten aus zwei Pipes Daten lesen. Im Programm wird zuerst die Read Operation auf der ersten Pipe durchgeführt und dann auf der zweiten Pipe. Jetzt hat ein anderer Produzentenprozess Daten in die Pipe 2 geschrieben, jedoch nicht in Pipe 1. Die Daten würden bis dahin nicht aus der Pipe 2 gelesen werden da der Konsument auf Pipe 1 wartet und blockiert.
Wir brauchen eine Möglichkeit mehrere Dateideskriptoren zu überwachen bevor das Programm aus einem Dateideskriptor liest. Das geschieht unter Unix mit der select
Operation.
Die Benutzung der select
Funktion erfordert etwas Aufwand wie nachfolgend skizziert ist. Die Select Operation blockiert den Prozess solange bis einer der zu überwachenden Dateideskriptoren "bereit" ist, d.h. z.B. Daten zum Lesen hat (wie die Pipe) oder ein Schreiben in einen Puffer wieder möglich (da nicht mehr voll). Weiterhin kann ein Timeout definiert werden nachdem die Select Operation garantiert zurück kehrt (d.h. die Prozessblockierung aufgehoben wird).
select
Operation. Das erste Argument gibt den höchsten Dateideskriptor + 1 an (und nicht die Anzahl!). Das zweite Argument ist ein Vektor von Dateideskriptoren die auf zu lesende Daten überwacht werden, das dritte Argument wäre ein Vektor von Dateideskriptoren die auf zu schreibende Daten überwacht werden. Soll es keinen Timeout geben dann ist als fünftes Argument NULL zu übergeben (und die Zeitstruktur timeval
wird nicht benötigt).
#define MAX(a,b) (a>b?a:b)
int fdA,fdB,...;
fd_set rfds;
struct timeval tv;
int nfds,retval;
...
FD_ZERO(&rfds);
FD_SET(fdA, &rfds);
FD_SET(fdB, &rfds);
nfds=MAX(fdA,fdB)+1;
/* Wait up to five seconds. */
tv.tv_sec = 5;
tv.tv_usec = 0;
// https://www.tutorialspoint.com/unix_system_calls/_newselect.htm
retval = select(nfds, &rfds, NULL, NULL, &tv);
if (retval<0) wegotatimeout();
if (FD_ISSET(fdA,&rfds)) {
int n=read(fdA,buf,bufsite);
...
}
if (FD_ISSET(fdB,&rfds)) {
int n=read(fdA,buf,bufsite);
...
}
Nun wollen wir mehrere Prozesse miteinander kommunizieren lassen. Dazu verwenden wir eine Master-Slave oder auch Produzenten-Konsumenten Architektur:
Das wäre unidirektionale Kommunikation. Bidirektionale Kommunikation bedeutet aber die Rücksendung einer Antwort an den anfragenden Prozess. D.h. Produzentenrolle und Komsumentenrolle kehren sich um.
Wir können ein Multiprozesssystem einfach mittels Forking aufbauen, d.h. ein Elternprozess erzeugt Kindprozesse. Die bidrektionale Kommunikation (Frage-Antwort) bezeichnet man auch als Remote Procedure Call (RPC) Kommunikation. Ein Prozess (Klient) fragt etwas bei einem Server an (der Call) und bekommt vom Server eine Antwort.
Wir können Pipes benutzen um RPC zu implementieren:
Aber das Diagramm ist etwas irreführend: Pipes sind nur Halbduplex (Einwegkommunikation), und obiges Beispiel würde im Vollduplex dazu führen dass ein schreibender Prozess wieder seine eigenen Daten lesen würde. Man kann nur einen der folgenden Schritte ausführen:
Wir brauchen für RPC immer (mindestens) zwei Pipes. Eine für die Anfrage, und eine für die Antwort. Daher kann (und sollte) jeder Prozess die Seite einer Pipe schließen (mit close
) die er nicht benötigt.
#define READ 0
#define WRITE 1
int chreq[2],chrep[2];
pipe(chreq);
pipe(chrep);
// Prozess 1: Klient
close(chreq[READ]);
close(chrep[WRITE]);
write(chreq[WRITE],sendbuf,msglen);
read(chrep[READ],recvbuf,nbuflen);
// Prozess 2: Server
close(chreq[WRITE]);
close(chrep[READ]);
read(chreq[READ],recvbuf,buflen);
write(chrep[WRITE],sendbuf,msglen);
Ist die Nachrichtenversendung über Pipes atomar? Was wäre wenn es mehrere Prozesse gäbe die in eine Pipe eine Nachricht schreiben? Können sich die Daten vermischen? Wir machen ein Experiment.
Aufgabe 1. Implementiere ein Produzenten-Konsumenten System: Ein einfacher Echo Server. Es gibt einen Elternprozess der als Server arbeitet, und zwei Kindprozesse die als Klienten arbeiten (durch Forking erzeugt). Es soll jeweils eine Pipe für die Anfrage (req) und die Antwort (rep) geben. Alle Klienten nutzen die gleichen Pipes. Ein Nachricht besteht nur aus einem Zeichen, der ID ('1' oder '2') des Klientenprozesses. Der Server sendet die Anfrage einfach zurück (also das ID Zeichen '1' oder '2'). Gebe die Anfragen und Antworten aus. Führe mehrfach Tests durch. Es wird die sleep
Funktion verwendet um sicher zu stellen dass die Klienten nicht vor dem Server laufen. Was ist zu beobachten, was geht schief?
Das Programm kann mit dem nachfolgenden Programmkode einfach erstellt werden:
gcc -o pipes0 pipes0.c
Ein einfaches drei-Prozess System mit Pipes.
▸
|
✗
≡
|
Aufgabe 2. Jetzt soll das obige Programm derart modifiziert werden dass jeder Klient seinen eigenen (privaten) Antwortkanal bekommt. Was ist jetzt zu beobachten, bekommt jeder Klient seine Antwort? Zur Vereinfachung wird jetzt die Klientennummer als Integer Zahl verarbeitet (hier 0/1).
Ein einfaches drei-Prozess System mit Pipes. Vervollständige und teste den Kode.