Axel Rogat
Betriebssysteme und betriebssystemnahes Programmieren
 
9.6: Dateisperren Kapitel 9 10: Deadlocks 
 
  9.7 Sockets  
 

Unter UNIX kommunizieren Prozesse auf verschiedenen Rechnern standardmäßig über sogenannte Sockets ("Steckdosen"). Ein Socket ist dabei einfach das Ende eines Kommunikationswegs -- die Verbindungsdetails werden vom Kernel und Treibern übernommen. Sockets wurden zuerst in BSD eingeführt, aber in die meisten anderen UNIX-Geschmacksrichtungen (auch Linux) übernommen.

In diesem Kapitel interessieren uns natürlich nicht die technischen Grundlagen dieser Verbindung, sondern die Handhabung der Sockets in Benutzerprogrammen. Die unterliegenden Mechanismen wollen wir hier nur kurz betrachten.

Die Kommunikation findet auf drei unterschiedlichen Ebenen statt (man kann noch feinere Unterteilungen treffen):

Geräte-Ebene:
Das ist die Hardware-nächste Ebene, auf der die Verständigung z.B. der beteiligten Netzwerkkarten abläuft. Daran beteiligt sind die Hardware und deren Treiber im Kernel. In vielen Netzwerken wird "Ethernet" verwendet, eine Sammlung von Hardware- und Übertragungs-Spezifikationen. Es wird so festgelegt, durch welche physischen Signale Bits dargestellt werden, in welchen Gruppierungen, mit welchen Checksummen sie übertragen werden sollen, etc.

Protokoll-Ebene:
Auf dieser Ebene werden Datenstrukturen festgelegt, um Übertragungen in einem Netzwerk mit vielen Rechnern richtig zu interpretieren. Die Daten der unterliegenden Ebene werden "eingepackt" und mit zusätzlichen Informationen (wie Rechner-IDs, Kontrollstrukturen, etc.) versehen.

Sehr häufig wird als ein solches Protokoll TCP/IP (zwei Ebenen in sich) eingesetzt. TCP (Transfer Control Protocol) auf der "Transport-Ebene" beschäftigt sich mit der Verwaltung eingehender Datenpakete (Aufteilung in kleine Pakete, Sortieren von Paketen, Checksummen-Kontrolle und ggf. mehrfaches Senden). IP ( Internet Protocol) auf der "Netzwerk-Ebene" beschäftigt sich mit dem Routing (Wegfindung) und dem tatsächlichen Verschicken.

Wenn der Benutzer die Verwaltung der Pakete übernimmt, erhält man die Kombination UDP/IP (User Datagram Protocol). TCP ist die sicherere Wahl, da es selbst die Verantwortung für verlorengegangene Pakete etc. übernimmt. UDP ist simpler und kann im Ernstfall schneller sein. Außerdem können mit UDP größere Pakete (bis 8K) verschickt werden, während TCP Daten vergleichsweise klein zerteilt.

Programm-Ebene:
Die Benutzerprogramme sollen von den unterliegenden Details natürlich nichts mitbekommen müssen. Dazu dienen die Sockets, die wie Files per Deskriptor gelesen, beschrieben und kontrolliert werden können, und deren Daten vom Kernel transparent an die verwendeten Protokolle und Treiber übergeben werden.

Auf dieser Ebene sind weitere High-Level-Protokolle definiert, wie etwa HTTP ( HyperText Transfer Protocol) zum Austausch von Dokumenten im WWW, FTP ( File Transfer Protocol), Mail-Protokolle, etc.

Die Kommunikation über Sockets spiegelt den Austausch von kleinen Paketen (Datagrammen) auf den unteren Ebenen wieder. Sie ähnelt daher einem Message Passing mit Send- und Receive-Aufrufen. Wenn eine FIFO-ähnliche Verbindung gewünscht ist, müssen die beteiligten Prozesse das selbst verwalten.

9.7.1 Anlegen von Sockets

Die Include-Dateien, die bei Benutzung von Sockets wichtig sind, sind sys/socket.h (Socket-Definitionen), netdb.h (allgemeine Netzkommunikation), netinet/in.h (Internet-Definitionen).

Ein Socket wird meist angelegt durch folgenden Systemaufruf:

UNIX
int socket(int domain, int type, int protocol);
  legt einen Socket an; gibt einen passenden File-Deskriptor zurück

Die drei zu übergebenden Attribute bedeuten dabei folgendes:

domain:
Ein Socket ist immer fest an einen "Lebensbereich" gebunden, der globale Einstellungen (wie Protokolle, Adreßformate, etc.) vorgibt. Innerhalb eines UNIX-Systems kann ein effektiveres Format verwendet werden als in einem heterogenen Netz. Die wichtigsten hier erlaubten Konstanten sind AF_UNIX (UNIX-intern) und AF_INET (Internet).

type:
Der Typ legt fest, welcher Art die Verbindung sein soll. Ein Stream-Socket ("virtueller Kreis", SOCK_STREAM) ist eine sehr zuverlässige bidirektionale Verbindung, durch die dazu notwendigen Sicherheitsmechanismen aber auch ein wenig langsam. Weitere Typen sind "Datagramm" (SOCK_DGRAM, Verschicken kleiner benutzerdefinierter Packete) und "Roh" (SOCK_RAW, unter Umgehung von Netzwerk-Protokollen).

protocol:
Wenn in einer Domäne mehr als das Standard-Protokoll erlaubt ist, kann eines ausgewählt werden. Ansonsten wird hier 0 angegeben.

Geschlossen wird ein Socket mit close, ein vorzeitiges einseitiges Einstellen der Kommunikation kann mit shutdown erfolgen.

9.7.2 Socket-Adressen

Ein Socket muß noch an eine bestimmte Adresse gebunden werden (s.u.). Eine solche Adresse besteht aus der Definition des Rechners (z.B. Internet-Adresse) und eines "Ports". Ein Port ist einfach eine ganze Zahl, die den Adressaten innerhalb des Rechners identifiziert. Die Zahlen 0 bis 1023 sind für Systemdienste reserviert (z.B. Port 80 für den HTTP-Dämon, 25 für Mail-Dienste, etc.).

Ein Server-Prozeß (z.B. Webserver-Programm) definiert seinen eigenen Port und "hört" ihn dann ab, d.h. er wartet auf Daten, die unter Angabe dieser Nummer an seinen Rechner geschickt werden. Ein Client-Prozeß (z.B. Webbrowser) muß die Port-Nummer des Servers kennen, den er ansprechen will.

Zur Darstellung von Adressen für Sockets dient die Struktur sockaddr, bzw. speziellere Strukturen wie sockaddr_in für Internet-Sockets, etc.:

struct sockaddr_in { short sin_family; // Domain unsigned short sin_port; // Port-Nummer struct in_addr sin_addr; // Internet-Adresse unsigned char __pad[...]; // Auffüllen auf sockaddr-Größe };

9.7.3 Datenaustausch über Sockets

Viele Datei-Operationen mit File-Deskriptoren sind auch mit den Deskriptoren für Sockets möglich; beispielsweise schließt man ja mit close eine Verbindung und gibt den Socket frei.

Für das Abschicken von Daten ist die Funktion sendto, für das Empfangen recvfrom zuständig.

Wenn man mit Stream-Sockets arbeitet, besteht eine permanente Verbindung. In diesem Fall verwendet man send und recv, es funktionieren aber genausogut write und read (wodurch z.B. Standard-Ein-/Ausgabe auf Sockets umgelenkt werden können). send und recv bieten lediglich zusätzliche Steuermöglichkeiten (Protokolle umgehen, etc.):

UNIX
int send(int s, const void *msg, int len, unsigned int flags);
  schickt Daten der Länge len Bytes ab der Adresse msg über den Socket s
int recv(int s, void *buf, int len, unsigned int flags);
  empfängt max. len Bytes Daten über den Socket s und schreibt sie nach buf

Die Funktionen liefern die Anzahl der tatsächlich geschriebenen/empfangenen Bytes zurück (-1 bei Fehler).

Der recv-Aufruf blockiert normalerweise, wenn keine Daten vorliegen, es sei denn, man hat das mit einem fcntl-Aufruf anders festgelegt:

fcntl(s,F_SETFL,O_NONBLOCK);
Es ist aber dennoch nicht gewährleistet, daß ein recv-Aufruf soviele Bytes liefert wie gewünscht. Folgende Funktion ruft so lange recv auf, bis die geforderte Menge angekommen ist (funktioniert natürlich auch für normale Files):
int x_recv(int fd, void *data, int size) { int left=size,bytes; char *P=(char *)data; do { if ((bytes=read(fd,P,left))<0) return 0; P+=bytes; left-=bytes; } while (left>0); return size; }
Wenn man bei Kommunikation mit mehreren anderen Rechnern auf mehrere Sockets gleichzeitig warten möchte, sollte man den Systemaufruf select verwenden. Er ist für File-Deskriptoren ausgelegt und funktioniert auch mit normalen Dateien (einzubinden ist sys/time.h):

UNIX
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
  wartet auf Status-Änderungen bei beliebig vielen File-Deskriptoren

Es werden drei Deskriptor-Mengen readfds, writefds und exceptfds angegeben. Die Funktion blockiert so lange, bis über einen der Deskriptoren Daten gelesen bzw. geschrieben werden können, bzw. bis eine Datei ihren Status ändert. n ist der numerisch größte File-Deskriptor +1.

Zurückgegeben wird die Anzahl der Deskriptoren, die das Ende von select ausgelöst haben. In timeout kann man eine maximale Wartezeit angeben. Bei timeout=0 blockiert select unbegrenzt lange.

Um die Deskriptor-Mengen zu manipulieren, sollte man nur die Makros FD_ZERO (leeren), FD_SET (einen Deskriptor eintragen) und FD_CLR (austragen) verwenden. Nach einem select kann man mit dem Makro FD_ISSET fragen, ob ein bestimmter Selektor Auslöser war:

FD_CLR(int fd, fd_set *set); FD_ISSET(int fd, fd_set *set); FD_SET(int fd, fd_set *set); FD_ZERO(fd_set *set);
Das ausführliche Beispiel am Ende des Kapitels verwendet diesen Mechanismus.

9.7.4 Server- und Client-Sockets

Es gibt zwei (bzw. drei) Arten, einen Socket einzusetzen:

Client-Sockets:

Hier dient ein Socket hauptsächlich dazu, einen speziellen Host anzusprechen (einen Server), ihm Kommandos zu schicken und Daten als Antwort von ihm entgegenzunehmen. Auf dem entfernten Rechner muß bereits ein Socket existieren, mit dem der neue verbunden wird.

Folgende Schritte sind erforderlich, um so eine Verbindung herzustellen:

Server-Sockets:

Ein "Listen-Socket" ist eine Anlaufstelle für Client-Anfragen. Er wird wie folgt erzeugt:

Über den Accept-Socket kann nun die Kommunikation mit dem Client erfolgen, während über den Listen-Socket weiterhin Anfragen anderer Clients entgegengenommen werden können. Wenn die Bearbeitung des Client-Auftrags länger dauert, kann man sie am besten mit fork abspalten und direkt wieder mit accept auf weitere Clients horchen.

Das folgende Bild faßt die einzelnen Abläufe (für Stream-Sockets mit ständiger Verbindung) noch einmal zusammen:

Im folgenden sind die benötigten Funktionen aufgeführt:

UNIX
struct hostent *gethostbyname(const char *name);
  liefert eine (statisch angelegte) Struktur hostent mit Informationen über den Host mit dem Namen name (alphanumerisch oder numerisch mit 4 oder 6 Feldern);
der Eintrag h_addr der Struktur liefert die numerische Adresse im internen Format
int connect(int fd, struct sockaddr *serv_addr, int addrlen);
  öffnet eine Verbindung vom Socket fd zu einem passenden Socket auf dem Server mit der Adresse serv_addr;
es können unterschiedliche Strukturen bei serv_addr angegeben werden (beispielsweise sockaddr_in für Internet, daher muß die Größe der Struktur in addrlen übergeben werden
int bind(int fd, struct sockaddr *my_addr, int addrlen);
  bindet den Socket fd an die lokale Adresse my_addr (Länge addrlen)
int listen(int s, int backlog);
  definiert für den Server-Socket s die Länge der Warteschlange für eingehende Verbindungen
int accept(int s, struct sockaddr *addr, int *addrlen);
  akzeptiert eine Verbindung über den Server-Socket s, erzeugt einen passenden Accept-Socket und gibt dessen Deskriptor zurück; *addrlen muß die Länge der Adreßstruktur enthalten;
füllt außerdem addr mit Informationen über den anfragenden Client (und addrlen mit der echten Adreßlänge)

Wenn nicht anders angegeben, liefern die Funktionen 0 bei okay zurück, -1 bei Fehlern.

Beispiel: Als Client-/Server-Beispiel implementieren wir wieder die lange Multiplikation. Wir verwenden der Einfachheit halber Stream-Sockets.

Der Client sclient erwartet als Aufrufparameter die Adresse des Server-Rechners, die Port-Nummer und die beiden Faktoren (um den Quelltext abzukürzen, werden einige Sicherheitsabfragen ausgelassen). Ein Aufruf wäre also: "sclient vulcan 4711 1000000 12345678".

// include netinet/in netdb stdio stdlib string iostream unistd fcntl void sorry(const char *s) { perror(s); exit(1); } int main(int argc, char *argv[]) { char buffer[1024]; int len;

if (argc!=5) { cerr << "Usage: sclient host port num1 num2"; exit(1); }

struct hostent *host=gethostbyname(argv[1]); if (host==0) sorry("host"); int host_sock=socket(AF_INET,SOCK_STREAM,0); if (host_sock<0) sorry("socket");

static struct sockaddr_in host_in; host_in.sin_family=AF_INET; host_in.sin_addr.s_addr=((struct in_addr*)(host->h_addr))->s_addr; host_in.sin_port=htons(atoi(argv[2]));

if (connect(host_sock,(struct sockaddr*)&host_in,sizeof(host_in))<0) sorry("connect");

sprintf(buffer,"%s %s",argv[3],argv[4]); len=strlen(buffer)+1; if (send(host_sock,buffer,len,0)!=len) sorry("send");

if (recv(host_sock,buffer,1024,0)>0) cout << argv[3] << '*' << argv[4] << '=' << buffer << endl; else sorry("recv"); close(host_sock); }

Die Bibliotheksfunktion htons (Host-To-Network/Short) dient hier dazu, die Byte-Reihenfolge der Port-Nummer vom Rechner-Format ins Netzwerk-Format umzuwandeln (Intel-Prozessoren speichern längere Integer-Zahlen in umgekehrter Reihenfolge ab als der Rest der Welt). Auf Rechner, auf denen die Reihenfolgen übereinstimmen, tut die Funktion also nichts.

Das Server-Gegenstück sserver erhält als Parameter nur die Port-Nummer, die er verwenden soll. Er empfängt von beliebigen Clients einmal zwei Zahlen als Nachricht und schickt das Produkt zurück. Damit er sofort für andere Clients empfangsbereit ist, spaltet er die Berechnung und Beantwortung in einen Kind-Prozeß ab:

// include sys/socket unistd stdio stdlib netinet/in ctype iostream string

void sorry(const char *s) { perror(s); exit(1); }

ostream & operator << (ostream &o, const struct in_addr &adr) { unsigned char c[4]; memcpy(c,&adr,4); return o << (int)c[0]<<'.'<<(int)c[1]<<'.'<<(int)c[2]<<'.'<<(int)c[3]; }

int main(int argc, char *argv[]) { if (argc!=2) { cerr << "Usage: sserver port\n"; exit(1); }

int lsocket=socket(AF_INET,SOCK_STREAM,0); if (lsocket<0) { perror("socket"); exit(1); }

static struct sockaddr_in s_in; s_in.sin_family=AF_INET; s_in.sin_addr.s_addr=INADDR_ANY; s_in.sin_port=htons(atoi(argv[1]));

if (bind(lsocket,(struct sockaddr*)&s_in,sizeof(s_in))<0) sorry("bind"); if (listen(lsocket,10)<0) sorry("listen");

for (;;) { static struct sockaddr_in incoming; int addrlen=sizeof(incoming); int asocket=accept(lsocket,(struct sockaddr*)&incoming,&addrlen); if (asocket<0) sorry("accept");

if (fork()==0) { cout << "request from " << incoming.sin_addr << endl;

char buffer[1024]; int bytes; bytes=recv(asocket,buffer,1024,0); if (bytes>=3) { char *s1=strtok(buffer," "), *s2=strtok(0," "); char *s3=long_mul(s1,s2); int len=strlen(s3)+1; if (send(asocket,s3,len,0)!=len) sorry("send"); free(s3); } close(asocket); _exit(0); } else close(asocket); } }

9.7.5 Ausführliches Beispiel

Als komplexere Anwendung schreiben wir eine Client-/Server-Kombination zur verteilten Berechnung von Ausschnitten der Mandelbrotmenge.

Die Mandelbrotmenge ist die Menge Zahlen c in C, für die die Folge mc in CN mit mc,0:=c und mc,i+1=mc,i2+c (für alle i>=0) nicht divergiert. Als Divergenzkriterium verwenden wir, daß |mc,i|>=2 wird.

Wir stellen einen rechteckigen Ausschnitt der komplexen Ebene dar. Für jeden Pixel berechnen wir die ersten Glieder der zugehörigen Folge. Wenn wir Divergenz feststellen, färben wir den Pixel entsprechend der Nummer des erreichten Folgenglieds unterschiedlich ein. Wenn wir nach einer Maximalzahl von Folgengliedern noch keine Divergenz festgestellt haben, färben wir den Pixel schwarz.

Für die Darstellung in einem X-Fenster verwenden wir eine einfache C++-Klasse SimpleWindow, deren Interna hier nicht relevant sind. Wir benötigen hauptsächlich Konstruktor/Destruktor und eine Funktion zum Zeichnen eines Pixels.

Die Port-Nummern und die Strukturen für die beiden Nachrichtentypen sind in der gemeinsamen Header-Datei multimandel.h definiert:

#define MASTERPORT 6666 #define SERVANTPORT 6667

struct cmd_msg // vom Master zum Servant { unsigned short num; // Nummer der Servants char command,dummy; // Kommando: q(uit), s(tart), c(ompute) unsigned short line; // Nummer der Zeile double y,x1,x2; // Imaginärteil der Zeile, Realteil-Begrenzungen unsigned short width; // Bildbreite unsigned short depth; // maximale Iterationstiefe };

struct data_msg // vom Servant zum Master { unsigned short num; // Nummer des Servants unsigned short line; // Nummer der Zeile unsigned char it[0]; // Farbdaten, eigentlich variabel it[width] };

Die zweite Struktur ist "variabel groß" (abhängig von der Bildbreite). Ihre tatsächliche Größe in Bytes wird per sizeof im Programm berechnet.

Es folgt der Code des Servants (servant.cpp. Die Funktionen sorry, x_recv und der Ausgabe-Operator << für Adressen sind aus den vorhergehenden Programmen übernommen.

// include sys/socket unistd stdio stdlib netinet/in ctype iostream iomanip // include string signal multimandel int in_sock,recv_sock,send_sock; struct cmd_msg msgbuf; struct data_msg *data; size_t data_size; double x_1,x_2,dx,*xval,*xsq; int width,numcolors,maxit,*modtab; void sigpipe_handler(int) { raise(SIGINT); } // "Broken Pipe" ==> Abbruch int one_point(double x0, double y0, double x2, double y2) { // einen Punkt berechnen double x=x0,y=y0; int it=0,cmp=msgbuf.depth; if (x2+y2>=4.0) return 0; for (;;) { if (++it==cmp) return it; y+=(y*=x)+y0; x=x2-y2+x0; if ((x2=x*x)+(y2=y*y)>=4.0) return it; } } void one_line() // eine Zeile berechnen { unsigned char *P=data->it; double y2=msgbuf.y*msgbuf.y,*xP=xval,*x2P=xsq;

for (int i=0;i<width;++i) *P++=modtab[one_point(*xP++,msgbuf.y,*x2P++,y2)]; } void init_mandel() // Initialisierungen { width=msgbuf.width; numcolors=msgbuf.line; maxit=msgbuf.depth; x_1=msgbuf.x1; x_2=msgbuf.x2; dx=(x_2-x_1)/width; xval=new double[width]; xsq=new double[width]; for (int i=0;i<width;++i) { xval[i]=x_1+i*dx; xsq[i]=xval[i]*xval[i]; } modtab=new int[maxit+1]; for (int i=0,j=1;i<maxit;++i) { modtab[i]=j; if (++j>=numcolors) j=1; } } int main() { signal(SIGPIPE,sigpipe_handler); in_sock=socket(AF_INET,SOCK_STREAM,0); if (in_sock<0) sorry("socket"); static struct sockaddr_in s_in; s_in.sin_family=AF_INET; s_in.sin_addr.s_addr=INADDR_ANY; s_in.sin_port=htons(SERVANTPORT); if (bind(in_sock,(struct sockaddr*)&s_in,sizeof(s_in))<0) sorry("bind"); if (listen(in_sock,10)<0) sorry("listen"); struct sockaddr_in incoming; int addrlen=sizeof(incoming); recv_sock=accept(in_sock,(struct sockaddr*)&incoming,&addrlen); if (recv_sock<0) sorry("accept"); cout << "accepted from " << incoming.sin_addr << endl; send_sock=socket(AF_INET,SOCK_STREAM,0); if (send_sock<0) sorry("socket"); incoming.sin_port=htons(MASTERPORT); if (connect(send_sock,(struct sockaddr *)&incoming,sizeof(incoming))<0) sorry("connect"); for (;;) { if (x_recv(recv_sock,&msgbuf,sizeof(msgbuf))==0) sorry("recv"); switch (msgbuf.command) { case 'q': // quit: Stop-Signal vom Master erhalten, STOP exit(0); case 's': // start: Initialisierung, "hello" zurückschicken init_mandel(); data_size=sizeof(data_msg)+sizeof(unsigned char[width]); data=(data_msg*)new char[data_size]; data->num=msgbuf.num; if (send(send_sock,data,data_size,0)<0) sorry("send"); break; case 'c': // compute: eine Zeile berechnen & zurückschicken one_line(); data->line=msgbuf.line; if (send(send_sock,data,data_size,0)<0) sorry("send"); break; default: cerr << "unknown command " << msgbuf.command << endl; } } }

Es folgt der Code des Masters, zu dem zunächst einige Bemerkungen vorangestellt werden.

// include netinet/in netdb stdio stdlib string iostream sys/types sys/time // include sys/timeb unistd fcntl math signal multimandel swindow // --------------------------------------------------------------------------- /** die beteiligten Rechner (Servants): ***/ char *servant[]={ "vulcan.solar.system", "qonos.solar.system" }; /** Bildbreite und Hohe: ***/ static int width=800,height=600; /** Berechnungs- und Darstellungs-Parameter ***/ static double x_1=-2.0,x_2=0.5,y_1=-0.9375,y_2=0.9375; static const int maxit=1500, numcolors=32; // --------------------------------------------------------------------------- const int num_servants=(sizeof(servant)/sizeof(servant[0])); int send_sock[num_servants],master_sock,recv_sock[num_servants]; bool alive[num_servants]; int num_alive; int compline[num_servants],countlines[num_servants]; pid_t rsh[num_servants]; struct sockaddr_in s_in; struct cmd_msg msgbuf; struct data_msg *data; size_t data_size; double dx; SimpleWindow *pwin; static int (*colors)[3]; void sorry(const char *s) { extern void fini(int); perror(s); fini(0); } void send_msgbuf(int i) { if (send(send_sock[i],&msgbuf,sizeof(msgbuf),0)<0) sorry("send"); } void fini(int) { delete pwin; msgbuf.command='q'; for (int i=0;i<num_servants;++i) { if (send_sock[i]>0) send_msgbuf(i); kill(rsh[i],SIGTERM); } signal(SIGINT,SIG_DFL); raise(SIGINT); } // Rückruf-Funktionen der Fensterklasse static void expose_callback(const int &w, const int &h) { pwin->copy_pixmap(); } static void keypress_callback(const int &key) { if (key=='q') fini(0); } // Darstellung void make_colors() { ... } // Farb-Berechnung, hier ausgelassen! void show_one_line(int ypix, unsigned char *line) { for (int i=0;i<width;++i) { pwin->set_color(*line++); pwin->draw_point_pm(i,ypix); } pwin->copy_pixmap_line(ypix); } // Socket-Definitionen int make_server_socket(int port) { int sock=socket(AF_INET,SOCK_STREAM,0); if (sock<0) sorry("socket1"); memset(&s_in,0,sizeof(s_in)); s_in.sin_family=AF_INET; s_in.sin_addr.s_addr=INADDR_ANY; s_in.sin_port=htons(MASTERPORT); if (bind(sock,(struct sockaddr*)&s_in,sizeof(s_in))<0) sorry("bind"); if (listen(sock,10)<0) sorry("listen"); return sock; } int make_client_socket(const char *name, int port) { struct hostent *host=gethostbyname(name); if (host==0) sorry("host"); for (int i=5;;) { int sock=socket(AF_INET,SOCK_STREAM,0); if (sock<0) sorry("socket"); memset(&s_in,0,sizeof(s_in)); s_in.sin_family=AF_INET; s_in.sin_addr.s_addr=((struct in_addr*)(host->h_addr))->s_addr; s_in.sin_port=htons(port); if (connect(sock,(struct sockaddr*)&s_in,sizeof(s_in))>=0) return sock; if (--i==0) sorry(name); close(sock); sleep(1); } } // Verschicken und Empfangen void send_task(int to, int line) { if (to<0||to>=num_servants) { cerr << "illegal send" << endl; exit(1); } msgbuf.num=to; msgbuf.command='c'; msgbuf.line=line; msgbuf.y=y_1+line*dx; send_msgbuf(to); compline[to]=line; } void init_mandel() // Initialisierung { if (x_2<x_1) swap(x_1,x_2); if (y_2<y_1) swap(y_1,y_2); dx=(x_2-x_1)/(double)width; double f=0.5*dx*height,g=0.5*(y_1+y_2); y_1=g-f; y_2=g+f; } int main(int argc, char *argv[]) // Master-Hauptprogramm { int i; struct timeb t1,t2,t3; ftime(&t1); signal(SIGINT,fini); init_mandel(); if (argc>=2 && argv[1][0]=='+') for (i=0;i<num_servants;++i) { if ((rsh[i]=fork())==0) { char buffer[80]; execlp("rsh","rsh","-n",servant[i],"/home/axel/servant",0); } } msgbuf.x1=x_1; msgbuf.x2=x_2; msgbuf.width=width; msgbuf.depth=maxit; msgbuf.line=numcolors; data_size=sizeof(data_msg)+sizeof(unsigned char[width]); data=(data_msg*)new char[data_size]; master_sock=make_server_socket(MASTERPORT); for (i=0;i<num_servants;++i) { send_sock[i]=make_client_socket(servant[i],SERVANTPORT); msgbuf.num=i; msgbuf.command='s'; send_msgbuf(i); } num_alive=0; for (i=0;i<num_servants;++i) { int addrlen; int sock=accept(master_sock,(struct sockaddr*)&s_in,&addrlen); if (sock<0) sorry("accept"); cout << "connected to " << s_in.sin_addr << endl; if (x_recv(sock,data,data_size)==0) sorry("recv"); recv_sock[data->num]=sock; if (alive[data->num]) exit(99); alive[data->num]=true; ++num_alive; send_task(data->num,i); } if (num_alive==0) { cerr << "nobody wants to talk to me" << endl; exit(1); } make_colors(); pwin=new SimpleWindow(width,height,"MandelMaster",numcolors,colors); pwin->set_expose_func(expose_callback); pwin->set_keypress_func(keypress_callback); int lines_to_receive=height, next_line=num_servants-1; ftime(&t2); do { fd_set wait_set; FD_ZERO(&wait_set); int max=0; for (i=0;i<num_servants;++i) if (alive[i]) { FD_SET(recv_sock[i],&wait_set); if (recv_sock[i]>max) max=recv_sock[i]; } struct timeval tv_wait; tv_wait.tv_sec=20; tv_wait.tv_usec=0; if (select(max+1,&wait_set,0,0,&tv_wait)==0) cerr << "warning: no message in 20 seconds" << endl; for (i=0;i<num_servants;++i) if (FD_ISSET(recv_sock[i],&wait_set)) { int bytes=x_recv(recv_sock[i],data,data_size); if (bytes<=0) { cerr << i << " died" << endl; alive[i]=false; if (--num_alive==0) { cerr << "all dead" << endl; exit(1); } } else { if (data->num>=num_servants) cerr << "something's obviously wrong..." << endl; else { if ( bytes!=data_size || data->line!=compline[data->num] ) { cerr << "received some garbage..." << endl; send_task(data->num,compline[data->num]); } else { ++countlines[data->num]; if (--lines_to_receive>0 && ++next_line<height) send_task(data->num,next_line); show_one_line(data->line,data->it); if (lines_to_receive==0) break; } } } } pwin->handle_events(false); } while (lines_to_receive>0); ftime(&t3); cout << "\nsuccessfully completed!\n"; for (i=0;i<num_servants;++i) cout << "received " << countlines[i] << " lines from " << servant[i] << endl; cout << "time: " << (t3.time-t1.time)+(t3.millitm-t1.millitm)/1000.0 << " s (" << (t3.time-t2.time)+(t3.millitm-t2.millitm)/1000.0 << " s computing time)" << endl;

pwin->handle_events(true); fini(0); }

Hier liegen die Quelltexte:
multimandel.h master.cpp servant.cpp

Bei acht Test-Rechnern ergab sich beispielsweise folgende Ausgabe:

received 87 lines from wmpi01.math.uni-wuppertal.de received 58 lines from wmpi02.math.uni-wuppertal.de received 82 lines from wmpi03.math.uni-wuppertal.de received 74 lines from wmpi04.math.uni-wuppertal.de received 69 lines from wmpi05.math.uni-wuppertal.de received 77 lines from wmpi06.math.uni-wuppertal.de received 75 lines from wmpi07.math.uni-wuppertal.de received 78 lines from wmpi08.math.uni-wuppertal.de time: 5.964 s (4.343 s computing time)
Die Rechner sind in etwa gleich schnell, waren aber unterschiedlich stark anderweitig beschäftigt. Auf wmpi01 lief auch der Master, so daß es nicht verwunderlich ist, daß von dort die meisten Zeilen stammen.

Ein gleichwertiges Programm auf einem Rechner alleine ohne jegliches Verschicken benötigt 13.9 Sekunden, mit Verschicken an sich selbst dagegen 30.4 Sekunden! Bei den vergleichsweise kurzen Berechnungszeiten ist der Verwaltungsaufwand des Datentransports dominant. Erst bei drei Rechnern sind wir wieder schneller als die 13.9 Sekunden.

Servant 1 2 3 4 5 6 7 8 ... 19
Zeit [s] 30.4 17.3 10.1 7.6 6.1 5.1 4.6 4.3 ... 4.0
Bei mehr als 8 Rechnern ist kein wesentlicher Geschwindigkeitsvorteil mehr festzustellen. Bis zu den getesteten 19 Rechnern stieg die Berechnungszeit aber auch nicht wieder an.

 
9.6: Dateisperren Startseite 10: Deadlocks