Systemprogrammierung - Teil 7

Let's talk together II

von Wolfgang Hetzler


Systemprogrammierung gilt als schwierig, da ihre Inhalte dem Beutzer sehr oft abstrakt gegenüber. Das diese Abstraktheit nicht sein muß und die Systemcalls eine wohldefinierte Schnittstelle zum Kernel darstellen, will diese Serie zeigen. Diesmal geht es um Interprozeß-Kommunikation mit Messages.

Nachdem in der letzten Folge die Signale behandelt worden sind, wollen wir uns in dieser und weiteren Folgen dem IPC-Modul von Unix zuwenden, das den rellen Kern der Prozeßkommunikation representiert. Drei Varianten möglicher Kommunikationsstrukturen kennzeichnen die inhaltliche Gestaltung dieses Moduls

Messages implizieren das Versenden von Nachrichten, bei Shared Memory nutzen verschiedene Prozesse Hauptspeicher gemeinsam und Semaphore sind Ampeln zur Vermeidung kritischer Situationen. Die Messages, die als Nachrichtenschlangen realisiert sind, ermöglichen eine verbindungslose Kommunikation - ein Prozeß schickt eine Nachricht, ein anderer empfängt sie oder auch nicht. Nachrichten können auch zu einem späteren Zeitpunkt empfangen werden, der Empfänger muß zum Zeitpunkt des Versendens der Nachricht nicht als Prozeß laufen. Bestimmte Bereiche des Hauptspeichers gemeinsam zu benutzen, impliziert verbindungsorientierte Kommunikation und bedarf zur korrekten Synchronisation der Semaphore.

Nachrichtenschlagen können von allen Prozessen benutzt werden, jeder ist berechtigt, eine Nachricht in eine der Warteschlagen zu stellen oder Nachrichten nach Selektion zu empfangen. Prinzipiell sind eine Vielzahl von Nachrichtenschlangen machbar, von denen jede eine Vielzahl von Nachrichten enthält. Diese Größen sind jedoch von System zu System verschieden, bei Linux liegt die maximale Anzahl von Schlangen bei 128 , die maximale Größe einer Nachricht bei 4056 Bytes und die Größe der Schlange insgesamt bei 16384 Bytes. Zunächst einmal die Prototypen der Systemcalls zur Behandlung von Messages:

Prototypen

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget ((key_t key, int msgflg));
int msgsnd ((int msqid, struct msgbuf *msgp, 
             int msgsz, int msgflg));
int msgrcv ((int msqid, struct msgbuf *msgp, 
             int msgsz, long msgtyp, int msgflg));
int msgctl ((int msqid, int cmd, struct msqid_ds *buf));

Msgget erwirbt den Zugriff auf eine Warteschlange, msgsnd sendet und msgget empfängt eine Nachricht, mit msgctl sind Manipulationen von Nachrichtenschlangen möglich. Zur Demonstration dieser Systemcalls schreibe ich eine kleine Client-Server Applikation: der Client versendet Nachrichten, die vom Server zurückgeliefert werden. Schauen wir uns erst den Server an:

Listing 1: msgserver.cpp

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include "msgbuf.h"
#include <stdio.h>

main(int argc, char **argv) { 
  MSGBUF buf; 
  int pid; 
  int qid; 
  qid = msgget(KEY,0600|IPC_CREAT); 

  while(1) { 
     msgrcv(qid,&buf,516,1,0); 
     puts(buf.msg); 
     buf.mtype = buf.pid; 
     msgsnd(qid,&buf,516,0); 
  } 
}

Msgget erwartet als ersten Parameter den Namen der Nachrichtenschlange, der numerisch angegeben werden muß (siehe Listing 5 - msgbuf.h). Der zweite Parameter spezifiziert die Neuanlage und/oder den Zugriff, der Rückgabewert ist dabei die id der Schlage, die analog zu einem Filedeskriptor von allen anderen Systemaufrufen erwartet wird. Msgrcv wartet auf eine Nachricht, wobei folgende Varianten möglich sind, die durch den vierten Paramter, den Typ der Nachricht, spezifiert werden:

Damit läßt sich eine gezielte Entnahme von Nachrichten aus der Schlage realisieren:

Der erste Parameter von msgrcv ist die id der Queue, der zweite Parameter die Adresse des Puffers, der den Typ und die Nachricht aufnimmt, der dritte Paramter ist die Größe der Nachricht ohne den Typ, der fünfte Parameter spezifiziert das Verhalten von msgrcv, falls keine Nachricht vorliegt: bei 0 wird blockiert, das Makro IPC_NOWAIT veranlaßt eine sofortige Rückkehr mit Fehler, falls keine geeignete Nachricht vorliegt. Anschließend wird die pid des Senders nach mtype kopiert (der Empfänger soll seine Nachricht erkennen) und die Message zurückgeschickt. Msgsnd erwartet den Deskriptor, die Startadresse der Message, die ja Typ und Nachricht umfaßt (siehe msgbuf.h), die Größe der Nachricht und ein Flag, das das Verhalten von msgsnd kontrolliert: 0 blockiert bei voller Queue, IPC_NOWAIT kehrt mit Fehler zrück. Nachfolgend nun der Client:

Listing 2: msgclient.cpp

#include <sys/types.h>  
#include <sys/ipc.h>  
#include <sys/msg.h>  
#include "msgbuf.h"  
#include <stdio.h>  

main(int argc, char **argv)  {  
  MSGBUF buf;  
  int pid;  
  int qid;  
  qid = msgget(KEY,0600);  

  while(1) {  
    gets(buf.msg);  
    if(feof(stdin))  
      break;                 
    buf.mtype = 1;  
    buf.pid = getpid();  
    msgsnd(qid,&buf,516,0);  
    msgrcv(qid,&buf,516,getpid(),0);  
    puts(buf.msg);  
  }  
}  

Der Client eröffnet den Zugriff auf die Queue (wobei natürlich zwischen Server und Client Einigkeit über den Namen existieren muß), liest in einer Endlosschleife von der Standardeingabe, bereitet die Nachricht auf, indem er den Typ auf 1 setzt und zum korrekten Empfangen zusätzlich seine pid zur Nachricht hinzufügt und anschließend via msgsnd die Nachricht an den Server schickt und der Antwort harrt. Bei der Eingabe von EOF terminiert der Client.

Unser nächstes Beipspiel ist etwas realistischer - ein kleiner Druckserver und das Clientprogramm - natürlich fehlen bei dem Server eine Vielzahl von Aktivitäten (abgesehen davon, daß lpd seine Aktivitäten über Unix-Sockets abwickelt), aber zur Demonstration einer funktionierenden sinnvollen Anwendung mag es genügen. Zunächst wieder der Server:

Listing 3: lps.cpp

#include <sys/types.h>  
#include <sys/ipc.h>  
#include <sys/msg.h>  
#include "msgbuf.h"  
#include <stdio.h>  
#include <fstream.h>  
#include <signal.h>  

void catcher(int);  
int print(char *);  
int qid;  

main(int argc, char **argv)  {  
  MSGBUF buf;  
  int pid;  
 
  qid = msgget(KEY,0600|IPC_CREAT);  
  signal(SIGINT,catcher);  
  while(1) {  
    msgrcv(qid,(struct msgbuf *)&buf,516,1,0);  
    print(buf.msg);  
  }  
}  

int print(char *name)  
{  
  fstream printer("/dev/lp1",ios::out);  
  fstream datei(name,ios::in);  
  char newline,buffer[512];  

  while(datei.get(buffer,512)) {  
    datei.get(newline);  
    printer << buffer;  
    printer << '\n' << '\r';  
  }  
}  

void catcher(int sig) {  
  msgctl(qid,IPC_RMID,NULL);  
}

Der Server liest Nachrichten aus der Queue und gibt die Dateinamen an die Funktion print weiter, die die Dateien auf dem Drucker ausgibt. Sollte das Signal SIGINT eintreffen, wird catcher aufgerufen und die Messagequeue gelöscht, die Bereitschaft zur Reaktion auf dieses Signal wurde mit dem Systemcall signal (siehe Folge 6 in der letzen Ausgabe) gesetzt. Der Systemcall msgctl ist wesentlich umfangreicher, wird jedoch meistens nur zum Löschen benutzt, ansonsten kann er auch jede Menge Informationen zur Queue liefern. Wir kommen weiter unten darauf zurück. Es folgt der Client:

Listing 4: lp.cpp

#include <sys/types.h> 
#include <sys/ipc.h> 
#include <sys/msg.h> 
#include "msgbuf.h" 
#include <stdio.h> 

main(int argc, char **argv) { 
  MSGBUF buf; 
  int pid; 
  int qid; 
  qid = msgget(KEY,0600); 
  while(*++argv) { 
    buf.mtype = 1; 
    buf.pid = getpid(); 
    strcpy(buf.msg,*argv); 
    msgsnd(qid,(struct msgbuf *)&buf,516,0); 
  } 
}

Die Namen der zu druckenden Dateien werden übergeben, die Schleife läuft dabei über alle Namen. Die Nachricht wird wie gewohnt aufbereitet und ab geht die Post. Listing 5 zeigt die Deklaration der struct MSGBUF, für die auch ein template in msg.h existiert. KEY definiert hierbei einen Namen für eine Queue.

Listing 5: msgbuf.h

struct MSGBUF 
{ 
        int mtype; 
        int pid; 
        char msg[512]; 
}; 

#define SIZE    512 
#define KEY     111 

Die Komponente mtype nimmt den Typ der Nachricht auf, pid und msg sind Bestandteil der Nachricht, die verschickt oder empfangen wird. Wichtig ist hier, daß die Größe der Nachricht in Bytes sich immer ergibt aus

sizeof MSGBUF - sizeof mtype

d.h., die Größe des Typs wird nicht zum Umfang der Nachricht gerechnet. Zum Abschluß schreibe ich noch eine light-Variante des Kommandos ipcs, zunächst der Quelltext, der die struct msqid_ds ebenfalls wiedergibt.

Listing 6: ipcs.cpp

struct msqid_ds {  
    struct ipc_perm msg_perm;  
    struct msg *msg_first;  /* first message on queue */  
    struct msg *msg_last;  /* last message in queue */  
    time_t msg_stime;      /* last msgsnd time */  
    time_t msg_rtime;      /* last msgrcv time */  
    time_t msg_ctime;      /* last change time */  
    struct wait_queue *wwait;  
    struct wait_queue *rwait;  
    ushort msg_cbytes;     /* current number of bytes on queue */  
    ushort msg_qnum;       /* number of messages in queue */  
    ushort msg_qbytes;     /* max number of bytes on queue */  
    ushort msg_lspid;      /* pid of last msgsnd */  
    ushort msg_lrpid;      /* last receive pid */  
};  

#include <sys/types.h>  
#include <sys/ipc.h>  
#include <sys/msg.h>  
#include <iostream.h>  

main(int argc,char **argv) {  
  struct msqid_ds buffer;  
  int qid;  
  qid = msgget(128,700);  
  msgctl(qid,IPC_STAT,&buffer);  
  cout << buffer.msg_qnum;  
}  

Diese kleine Anwendung benutzt msgctl zur Informationsgewinnung: die Anzahl der Nachrichten in der Nachrichtenschlage 128 wird ausgegeben. Messages sind eine einfach zu handhabende Methode, Kommunikation zwischen zwei oder mehreren Prozessen zu ermöglichen. Nachteilig ist, daß sie lediglich zum Transport kleiner Datenmengen geeignet sind und bedingt durch ihre Konstruktion relativ langsam sind. Die nächste Folge behandelt Shared Memory, eine rasche und effiziente Methode zum Austauschen von Informationen.

Infos

[1] Linux-Kernel-Programmierung, Addison-Wesley, 1997
[2] Rochkind, Unix-Programmierung für Fortgeschrittene, Hanser, 1985
[3] Bentson,R.,Inside Linux,Seattle:SSC 1996
[4] Brown,Chris,UNIX distributed programming,Prentice Hall
[5] Robbins, K.A:,Robbins,Steven, Practical Unix Programming,Prentice Hall

Der Autor

Wolfgang Hetzler arbeitet seit 1985 als Dozent an einem privaten Institut für Fort- und Ausbildung im EDV-Bereich und als Lehrbeauftragter an der FH-Frankfurt im Bereich Ingenieur-Informatik. Linux ist ihm seit der Version 0.9xx bekannt und eine Quelle ständiger Auseinandersetzung. Zu erreichen ist er unter het@het.gg.eunet.de.

Copyright © 1998 Linux-Magazin Verlag