| 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:
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:
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:
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:
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
}
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ß.
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:
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 Implementationsdatei comseg.cpp birgt keine Überraschungen:
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:
|
|
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:
Die eigentliche Rechnung lagern wir in ein Modul longmul.o aus, das über folgende Header-Datei vom Server verwendet wird:static const char LM_NAME1[]="/tmp/lm_lock1"; static const char LM_NAME2[]="/tmp/lm_lock2"; #define DATA_SIZE 4088
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.// longmul.h extern char *long_mul(const char *, const char *);
Es folgt zunächst der Quelltext des Servers:
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).// 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); } }
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.
Schließlich folgt noch die versprochene Implementation der eigentlichen Multiplikation (longmul.cpp):// 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(); } } }
// 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 
|
|   |