Axel Rogat
Betriebssysteme und betriebssystemnahes Programmieren
 
9.4: Semaphore Kapitel 9 9.6: Dateisperren 
 
  9.5 Shared Memory  
 

Um größere Datenmengen innerhalb eines Ein-Prozessor-Systems auszutauschen, sind FIFOs und Message Queues unverhältnismäßig langsam. Für diesen Fall gibt es die Möglichkeit, einzelne Speicher-Segmente ausnahmsweise für mehrere Prozesse zugänglich zu machen ("shared memory segments").

Das System verwaltet die Segmente wiederum mit IDs. Für den eigentlichen Zugriff verbindet man den eigenen Prozeß mit dem Segment ("attach") und erhält dann einen echten Pointer, über den man ganz normal auf den Speicher zugreifen kann. Benötigt man das Segment nicht mehr, koppelt man es wieder vom Prozeß ab (" detach").

Zugriffe mehrere Prozesse müssen natürlich (z.B. mit Semaphoren) synchronisiert werden. Es handelt sich hier um das typische Readers/Writers-Problem aus 8.3.4. Writer brauchen exklusiven Zugriff, während gleichzeitig mehrere Reader erlaubt sind.

Die notwendigen Strukturen und Funktionen sind in sys/shm.h und sys/ipc.h definiert. Es gibt folgende Systemaufrufe:

UNIX
int shmget(key_t key, int size, int flg);
  get shared memory segment, erfragt die ID eines bestehenden Segments oder legt ein neues an (siehe 9.2). size gibt die Größe des Segments in Bytes an.
int shmctl(int id, int cmd, struct shmid_ds *buf);
  shared memory control, zur Manipulation eines Segments (cmd= IPC_RMID zum Löschen).
char *shmat(int id, char *addr, int flg);
  shared memory attach, Ankoppeln des Segments id an den aktuellen Prozeß. Man erhält einen Pointer zurück, über den man in das Segment schauen und schreiben kann. addr ist ein Adreßvorschlag, der vom System meist ignoriert wird (am besten addr=0). Durch Setzen von SHM_RDONLY in flg kann das Segment schreibgeschützt werden.
int shmdt(char *addr);
  shared memory detach, Abkoppeln des Segments ab der Adresse addr vom aktuellen Prozeß.

Die Segmente können größer sein als angefordert - size wird immer auf ein ganzzahliges Vielfaches von PAGE_SIZE (z.B. 4 KByte) aufgerundet. Die maximale Größe eines Segments ist SHMMAX (z.B. 32 MByte).

Jedes Segment zählt mit, an wieviel Prozesse es aktuell angekoppelt ist. Es wird nicht automatisch gelöscht, wenn es keinen Besitzer mehr hat, erst wenn der Hauptspeicher wiederverwendet werden muß.

Die Segmente werden bei einem fork an das Kind vererbt. Bei exec und _exit dagegen werden sie abgekoppelt.

Das Anlegen eines Segments für 256 double-Zahlen und Beschreiben mit Zufallszahlen könnte also beispielsweise wie folgt geschehen:

double *data,*P; int i,shmid;

shmid=shmget(IPC_PRIVATE,sizeof(double[256]),0644|IPC_CREAT); data=(double *)shmat(shmid,0,0); if (data==(double*)-1) { perror("sorry"); exit(0); } for (i=256,P=data;i>0;--i) *P++=drand48();

"ipcs -m" liefert als Ausgabe etwas wie:

------ Shared Memory Segments -------- shmid owner perms bytes nattch status 1 axel 644 2048 1
Bei Beendigung des Prozesses wird das Segment automatisch abgekoppelt. Wenn es bereits vorher nicht mehr benötigt wird, sollte man es aber natürlich explizit abkoppeln und freigeben:
if (data!=0) shmdt((void *)data); if (shmid>=0) shmctl(shmid,IPC_RMID,0);
Mit shmctl können außerdem Status und Besitzer abgefragt werden, und der Superuser kann das Segment sperren und wieder freigeben.

9.5.1 Eine Klasse für exklusive Segmente

Ganz analog zur Klasse für Semaphoren stellen wir nun noch eine einfache Klasse für gemeinsame Speichersegmente vor. Sie nehmen einem das Ankoppeln und Entkoppeln, sowie zusätzliche Verwaltungsarbeit mit Semaphoren ab. Sie sind der Einfachheit halber für den exklusiven Zugriff (beim Lesen und beim Schreiben) gedacht.

Die Objekte lassen sich dazu mit den Funktionen lock und unlock sperren bzw. freigeben. Dazu verwendet die Klasse intern Locks in Form binärer Semaphore aus unserer Klasse semarray.

Es gibt wiederum einen Konstruktor, der das Segment neu anlegt, und einen, der über eine bekannte ID auf ein bestehendes Segment zugreift. In das Segment werden vorne die ID des verwendeten Semaphors und bei Bedarf die ID des Prozesses eingeschrieben, der zuletzt Zugriff hatte (auf diese Weise können Client und Server zusammenfinden).

Es folgt zunächst die Header-Datei comseg.h:

class comseg { private: bool creator; // neu angelegt? semarray *sem; // Semaphor-Menge struct da_tag { pid_t pid; // letzte Prozeß-ID char data[0]; // eigentliche Daten } *data_area; // Segment-Inhalt public: // neu anlegen, mit Größe comseg(const char *lockfile, unsigned long datasize, int mode); comseg(const char *lockfile, int mode); // mitverwenden ~comseg(); // Destruktor int id() const { return shm_id; } // ID auslesen void setpid(pid_t pid) { data_area->pid=pid; } // PID einschreiben pid_t pid() const { return data_area->pid; } // PID auslesen char *data() const { return data_area->data; } // Zeiger auf Daten lesen void lock() { sem->down(); } // Zugriff sperren void unlock() { sem->up(); } // Zugriff freigeben };
Die Implementationsdatei comseg.cpp birgt keine Überraschungen:
// include sys/types sys/ipc sys/shm sys/stat stdio unistd fcntl // include semarray comseg

comseg::comseg(const char *lockname, unsigned long datasize, int mode) : creator(true) { struct stat statbuf; if (stat(lockname,&statbuf)>=0) { fprintf(stderr,"%s exists\n",lockname); exit(1); }

shm_id=shmget(IPC_PRIVATE,sizeof(*data_area)+datasize,0666|IPC_CREAT); if (shm_id<0) { perror("shmget"); exit(0); } data_area=(da_tag *)shmat(shm_id,0,mode); // Ankoppeln if (data_area==(da_tag *)-1) { shmctl(shm_id,IPC_RMID,0); perror("shmat"); exit(0); }

sem=new semarray(lockname,1,1); // Semaphor neu anlegen int fd=open(lockname,O_RDWR|O_APPEND); if (fd<0) { perror("open"); exit(1); } write(fd,&shm_id,sizeof(shm_id)); close(fd); }

comseg::comseg(const char *lockname, int mode) : creator(false) { sem=new semarray(lockname); // Semaphor mitverwenden

int fd=open(lockname,O_RDONLY); if (fd<0) { perror("open"); exit(1); } lseek(fd,sizeof(int),SEEK_SET); read(fd,&shm_id,sizeof(shm_id)); close(fd);

data_area=(da_tag *)shmat(shm_id,0,mode); // Ankoppeln if (data_area==(da_tag *)-1) { perror("shmat"); exit(0); } } comseg::~comseg() { shmdt((char *)data_area); // Abkoppeln if (creator) shmctl(shm_id,IPC_RMID,0); // nur der Erzeuger gibt frei delete sem; // Semaphor freigeben }

Im nächsten Abschnitt folgt direkt ein ausführliches Beispiel für die Verwendung der Klasse.

9.5.2 Client/Server-Beispiel mit Shared Memory

Wir verwenden unsere Klasse comseg in einem Client-Server-Einsatz. Der Server dient diesmal dazu, "lange" natürliche Zahlen miteinander zu multiplizieren (unsere Puffer sind hier auf ca. 2048 Stellen beschränkt). Schon hier - und noch weniger bei noch größeren Datenmengen - macht es keinen Sinn, die Daten durch FIFOs oder Message Queues zu schleusen.

In unserem FIFO-Beispiel gab es eine Ergebnis-FIFO pro Client, im Beispiel mit den Message Queues nur eine Queue insgesamt. Hier richten wir zwei Shared-Memory-Segmente ein:
  • Das Segment cmd mit der Anfrage (zwei Zahlen mit max. ca. 2000 Stellen) wird vom Client gelockt, bevor er die beiden Faktoren hineinschreibt, und vom Server entlockt, nachdem er diese Daten entnommen hat.

  • Das Segment res mit dem Ergebnis (das Produkt mit max. ca. 4000 Stellen) wird vom Server gelockt, bevor er das Produkt hineinschreibt, und vom Client entlockt, wenn er es entnommen hat.
Clients und Server verwenden außerdem Signale:

Wenn man zusätzliche Eingaben in das Programm einbaut (wie " mit dem Schreiben beginnen <RET>?"), die vor den Lese- und Schreiboperationen anhalten, kann man verfolgen, wie ein Client mit einem neuen Kommando an einem Semaphor angehalten wird, weil der Server die Seite noch nicht freigegeben hat, etc.

Client und Server verwenden folgende Header-Datei lmcs.h mit den Namen der Lockfiles und der Datengröße des Segments:

static const char LM_NAME1[]="/tmp/lm_lock1"; static const char LM_NAME2[]="/tmp/lm_lock2"; #define DATA_SIZE 4088
Die eigentliche Rechnung lagern wir in ein Modul longmul.o aus, das über folgende Header-Datei vom Server verwendet wird:
// longmul.h extern char *long_mul(const char *, const char *);
long_mul verändert also seine Argumente nicht und legt selbständig den Speicher für das Resultat an, und zwar mit calloc, da new den Speicher nicht mit Nullen vorbelegt. Der Server gibt den Speicher nach Verwendung mit free frei. Die Implementation wird der Vollständigkeit halber am Ende dieses Abschnitts nachgeliefert.

Es folgt zunächst der Quelltext des Servers:

// include sys/ipc sys/shm iostream stdlib string signal fcntl unistd ctype // include lmcs longmul semarray comseg comseg *cmd, *res; bool answer=false; void cleanup() { delete cmd; delete res; } void error(char *str) { cerr << "server error: " << str << endl; exit(1); } void sigint_handler(int s) { cerr << "server interrupted" << endl; exit(1); } void sigusr1_handler(int s) { answer=true; } int isnumber(char *P) { char c; while ((c=*P++)!='\0') if (!isdigit(c)) return 0; return 1; } int main() { atexit(cleanup); signal(SIGINT,sigint_handler); static struct sigaction sa; sa.sa_handler=sigusr1_handler; sa.sa_flags=SA_RESTART; sigaction(SIGUSR1,&sa,&sa); cmd=new comseg(LM_NAME1,DATA_SIZE,0); res=new comseg(LM_NAME2,DATA_SIZE,0); res->setpid(getpid()); for (;;) { pid_t client_pid; char *num1, *num2; if (!answer) pause(); client_pid=cmd->pid(); num1=cmd->data(); num2=num1+strlen(num1)+1; res->lock(); if (!isnumber(num1)||!isnumber(num2)) strcpy(res->data(),"illegal operands"); else { char *num3=long_mul(num1,num2); strcpy(res->data(),num3); free(num3); } cmd->unlock(); answer=false; kill(client_pid,SIGUSR1); } }
Das Signal SIGUSR1 wird mit einem Handler abgefangen, der hinter den pause-Aufruf zurückkehrt. Der Server läuft ewig und muß mit CTRL-C abgebrochen werden. Daher wird auch SIGINT abgefangen und führt zum Abbruch. Mit atexit wird noch die Funktion cleanup eingehängt, die vor dem Programmende noch aufräumt (Speichersegmente und damit die Semaphore freigibt und die Lockfiles löscht).

Der Client ist der Einfachheit halber so aufgebaut, daß er am Anfang des Programms die Speichersegmente ankoppelt und bis zum Ende behält. Wenn der Server zwischendurch abgebrochen wird, bekommt der Client es nicht mit und erzeugt erst beim Zugriff auf die nicht mehr vorhandenen Segmente einen Fehler.

Wenn man es ganz sauber realisieren wollte, müßte der Server alle Clients zunächst registrieren und sie bei seinem Abbruch mit einem Signal benachrichtigen.

// include sys/ipc sys/shm iostream stdlib string signal fcntl unistd // include lmcs comseg semarray comseg *cmd, *res; bool answer=false; void cleanup() { delete cmd; delete res; } void error(char *str) { cerr << "client error: " << str << endl; exit(0); } void sigint_handler(int s) { cerr << "client interrupted" << endl; exit(0); } void sigusr1_handler(int s) { answer=true; } int main() { atexit(cleanup); signal(SIGINT,sigint_handler); static struct sigaction sa; sa.sa_handler=sigusr1_handler; sa.sa_flags=SA_RESTART; sigaction(SIGUSR1,&sa,&sa); cmd=new comseg(LM_NAME1,0); res=new comseg(LM_NAME2,0); res->lock(); pid_t server_pid=res->pid(); res->unlock(); for (;;) { static char num1[2048], num2[2048]; int l1,l2; cout << "1. Zahl: "; cin.getline(num1,2048); cout << "2. Zahl: "; cin.getline(num2,2048); num1[l1=strlen(num1)]=num2[l2=strlen(num2)]=0; if (l1+l2>DATA_SIZE-2) cerr << "combined operands too long" << endl; else { cmd->lock(); cmd->setpid(getpid()); strcpy(cmd->data(),num1); strcpy(cmd->data()+(l1+1),num2);

answer=false; kill(server_pid,SIGUSR1); if (!answer) pause(); cout << "\n " << num1 << "\n* " << num2 << "\n= " << res->data() << endl << endl; res->unlock(); } } }

Schließlich folgt noch die versprochene Implementation der eigentlichen Multiplikation (longmul.cpp):
// include stdlib string char *long_mul(const char *f1, const char *f2) { int n1,n2,nr,i,j; char *result,*Pr,*P3; const char *P1, *P2; nr=(n1=strlen(f1))+(n2=strlen(f2)); if ((result=(char *)calloc(nr+1,sizeof(char)))==0) return 0; for ( i=n2-1 , P2=f2 ; i>=0 ; --i , ++P2 ) { int cyph, carry=0, fac=*P2-'0'; for ( j=n1 , Pr=result+nr-i , P1=f1+n1 ; j>0 ; --j ) { cyph=(*--P1-'0')*fac+*--Pr+carry; for ( carry=0 ; cyph>=10 ; ++carry ) cyph-=10; *Pr=cyph; } while (carry) { cyph=*--Pr+carry; for ( carry=0 ; cyph>=10 ; ++carry ) cyph-=10; *Pr=cyph; } } P2=P3=result; while ( *P2==0 && nr>1 ) { ++P2; --nr; } for ( i=nr ; i>0 ; --i ) *P3++='0'+*P2++; *P3=0; return result; }

 
9.4: Semaphore Startseite 9.6: Dateisperren