GNU/Linux >> LINUX-Kenntnisse >  >> Linux

Kommunikation zwischen Prozessen in Linux:Shared Storage

Dies ist der erste Artikel in einer Reihe über Interprozesskommunikation (IPC) in Linux. Die Serie verwendet Codebeispiele in C, um die folgenden IPC-Mechanismen zu verdeutlichen:

  • Freigegebene Dateien
  • Gemeinsamer Speicher (mit Semaphoren)
  • Pipes (benannt und unbenannt)
  • Nachrichtenwarteschlangen
  • Sockets
  • Signale

Dieser Artikel gibt einen Überblick über einige Kernkonzepte, bevor er zu den ersten beiden dieser Mechanismen übergeht:gemeinsam genutzte Dateien und gemeinsam genutzter Speicher.

Kernkonzepte

Ein Prozess ist ein Programm in Ausführung, und jeder Prozess hat seinen eigenen Adressraum, der die Speicherorte umfasst, auf die der Prozess zugreifen darf. Ein Prozess hat einen oder mehrere Threads der Ausführung, bei denen es sich um Sequenzen ausführbarer Anweisungen handelt:ein single-threaded Prozess hat nur einen Thread, während ein Multi-Threaded Prozess hat mehr als einen Thread. Threads innerhalb eines Prozesses teilen sich verschiedene Ressourcen, insbesondere den Adressraum. Dementsprechend können Threads innerhalb eines Prozesses direkt über Shared Memory kommunizieren, obwohl einige moderne Sprachen (z. B. Go) einen disziplinierteren Ansatz wie die Verwendung von Thread-sicheren Kanälen fördern. Interessant ist hier, dass andere Prozesse dies standardmäßig nicht tun Speicher teilen.

Es gibt verschiedene Möglichkeiten, Prozesse zu starten, die dann kommunizieren, und in den folgenden Beispielen dominieren zwei Möglichkeiten:

  • Ein Terminal wird verwendet, um einen Prozess zu starten, und vielleicht wird ein anderes Terminal verwendet, um einen anderen zu starten.
  • Die Systemfunktion fork wird innerhalb eines Prozesses (dem Elternprozess) aufgerufen, um einen anderen Prozess (den Kindprozess) hervorzubringen.

Die ersten Beispiele gehen vom Terminal-Ansatz aus. Die Codebeispiele sind in einer ZIP-Datei auf meiner Website verfügbar.

Freigegebene Dateien

Programmierer sind nur allzu vertraut mit dem Dateizugriff, einschließlich der vielen Fallstricke (nicht vorhandene Dateien, fehlerhafte Dateiberechtigungen usw.), die die Verwendung von Dateien in Programmen belasten. Nichtsdestotrotz können gemeinsam genutzte Dateien der grundlegendste IPC-Mechanismus sein. Betrachten Sie den relativ einfachen Fall, in dem ein Prozess (Produzent ) erstellt und schreibt in eine Datei, und ein anderer Prozess (consumer ) liest aus derselben Datei:

         writes  +-----------+  reads
producer-------->| disk file |<-------consumer
                 +-----------+

Die offensichtliche Herausforderung bei der Verwendung dieses IPC-Mechanismus ist, dass eine Wettlaufbedingung auftreten könnten:Der Produzent und der Verbraucher könnten genau zur gleichen Zeit auf die Datei zugreifen, wodurch das Ergebnis unbestimmt wird. Um eine Racebedingung zu vermeiden, muss die Datei so gesperrt werden, dass ein Konflikt zwischen einem Schreibvorgang verhindert wird Operation und jede andere Operation, sei es ein read oder ein schreiben . Die Sperr-API in der Standardsystembibliothek kann wie folgt zusammengefasst werden:

  • Ein Produzent sollte eine exklusive Sperre für die Datei erlangen, bevor er in die Datei schreibt. Ein exklusives Die Sperre kann höchstens von einem Prozess gehalten werden, was eine Race-Bedingung ausschließt, da kein anderer Prozess auf die Datei zugreifen kann, bis die Sperre aufgehoben wird.
  • Ein Konsument sollte zumindest eine gemeinsame Sperre für die Datei erhalten, bevor er aus der Datei liest. Mehrere Leser kann ein shared halten lock gleichzeitig, aber kein writer auf eine Datei zugreifen kann, wenn auch nur ein einziger Leser hält eine gemeinsame Sperre.

Ein gemeinsames Schloss fördert die Effizienz. Wenn ein Prozess nur eine Datei liest und ihren Inhalt nicht ändert, gibt es keinen Grund, andere Prozesse daran zu hindern, dasselbe zu tun. Das Schreiben erfordert jedoch eindeutig den exklusiven Zugriff auf eine Datei.

Die Standard-E/A-Bibliothek enthält eine Hilfsfunktion namens fcntl die verwendet werden kann, um sowohl exklusive als auch gemeinsam genutzte Sperren für eine Datei zu untersuchen und zu manipulieren. Die Funktion funktioniert über einen Dateideskriptor , ein nicht negativer ganzzahliger Wert, der innerhalb eines Prozesses eine Datei identifiziert. (Unterschiedliche Dateideskriptoren in unterschiedlichen Prozessen können dieselbe physische Datei identifizieren.) Zum Sperren von Dateien bietet Linux die Bibliotheksfunktion flock , was ein dünner Wrapper um fcntl ist . Das erste Beispiel verwendet das fcntl Funktion, um API-Details anzuzeigen.

Beispiel 1. Der Produzent Programm

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#define FileName "data.dat"
#define DataString "Now is the winter of our discontent\nMade glorious summer by this sun of York\n"

void report_and_exit(const char* msg) {
  perror(msg);
  exit(-1); /* EXIT_FAILURE */
}

int main() {
  struct flock lock;
  lock.l_type = F_WRLCK;    /* read/write (exclusive versus shared) lock */
  lock.l_whence = SEEK_SET; /* base for seek offsets */
  lock.l_start = 0;         /* 1st byte in file */
  lock.l_len = 0;           /* 0 here means 'until EOF' */
  lock.l_pid = getpid();    /* process id */

  int fd; /* file descriptor to identify a file within a process */
  if ((fd = open(FileName, O_RDWR | O_CREAT, 0666)) < 0)  /* -1 signals an error */
    report_and_exit("open failed...");

  if (fcntl(fd, F_SETLK, &lock) < 0) /** F_SETLK doesn't block, F_SETLKW does **/
    report_and_exit("fcntl failed to get lock...");
  else {
    write(fd, DataString, strlen(DataString)); /* populate data file */
    fprintf(stderr, "Process %d has written to data file...\n", lock.l_pid);
  }

  /* Now release the lock explicitly. */
  lock.l_type = F_UNLCK;
  if (fcntl(fd, F_SETLK, &lock) < 0)
    report_and_exit("explicit unlocking failed...");

  close(fd); /* close the file: would unlock if needed */
  return 0;  /* terminating the process would unlock as well */
}

Die wichtigsten Schritte im Produzenten Programm oben kann wie folgt zusammengefasst werden:

  • Das Programm deklariert eine Variable vom Typ struct flock , das eine Sperre darstellt, und initialisiert die fünf Felder der Struktur. Die erste Initialisierung:
    lock.l_type = F_WRLCK; /* exclusive lock */

    macht die Sperre exklusiv (Lesen-Schreiben ) statt einer gemeinsam genutzten (schreibgeschützten ) sperren. Wenn der Produzent erhält die Sperre, dann kann kein anderer Prozess die Datei schreiben oder lesen, bis der Produzent hebt die Sperre auf, entweder explizit mit dem entsprechenden Aufruf von fcntl oder implizit durch Schließen der Datei. (Wenn der Prozess beendet wird, werden alle geöffneten Dateien automatisch geschlossen, wodurch die Sperre aufgehoben wird.)

  • Das Programm initialisiert dann die restlichen Felder. Der Haupteffekt ist, dass das gesamte Datei soll gesperrt werden. Die Sperr-API lässt jedoch zu, dass nur bestimmte Bytes gesperrt werden. Wenn die Datei beispielsweise mehrere Textdatensätze enthält, kann ein einzelner Datensatz (oder sogar ein Teil eines Datensatzes) gesperrt und der Rest nicht gesperrt bleiben.
  • Der erste Aufruf von fcntl :
    if (fcntl(fd, F_SETLK, &lock) < 0)

    versucht, die Datei exklusiv zu sperren und prüft, ob der Aufruf erfolgreich war. Im Allgemeinen ist die fcntl Funktion gibt -1 zurück (daher kleiner als Null), um einen Fehler anzuzeigen. Das zweite Argument F_SETLK bedeutet, dass der Aufruf von fcntl tut nicht block:Die Funktion kehrt sofort zurück und gewährt entweder die Sperre oder zeigt einen Fehler an. Wenn das Flag F_SETLKW (das W am Ende steht für warten ) wurden stattdessen verwendet, der Aufruf von fcntl würde blockieren, bis das Erlangen der Sperre möglich war. In den Aufrufen von fcntl , das erste Argument fd der Dateideskriptor ist, gibt das zweite Argument die auszuführende Aktion an (in diesem Fall F_SETLK zum Setzen der Sperre), und das dritte Argument ist die Adresse der Sperrstruktur (in diesem Fall &lock ).

  • Wenn der Produzent erhält die Sperre, schreibt das Programm zwei Textsätze in die Datei.
  • Nach dem Schreiben in die Datei wird der Produzent ändert den l_type der Sperrstruktur Feld zum Entsperren Wert:
    lock.l_type = F_UNLCK;

    und ruft fcntl auf um den Entriegelungsvorgang durchzuführen. Das Programm wird beendet, indem die Datei geschlossen und beendet wird.

Beispiel 2. Der Verbraucher Programm

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

#define FileName "data.dat"

void report_and_exit(const char* msg) {
  perror(msg);
  exit(-1); /* EXIT_FAILURE */
}

int main() {
  struct flock lock;
  lock.l_type = F_WRLCK;    /* read/write (exclusive) lock */
  lock.l_whence = SEEK_SET; /* base for seek offsets */
  lock.l_start = 0;         /* 1st byte in file */
  lock.l_len = 0;           /* 0 here means 'until EOF' */
  lock.l_pid = getpid();    /* process id */

  int fd; /* file descriptor to identify a file within a process */
  if ((fd = open(FileName, O_RDONLY)) < 0)  /* -1 signals an error */
    report_and_exit("open to read failed...");

  /* If the file is write-locked, we can't continue. */
  fcntl(fd, F_GETLK, &lock); /* sets lock.l_type to F_UNLCK if no write lock */
  if (lock.l_type != F_UNLCK)
    report_and_exit("file is still write locked...");

  lock.l_type = F_RDLCK; /* prevents any writing during the reading */
  if (fcntl(fd, F_SETLK, &lock) < 0)
    report_and_exit("can't get a read-only lock...");

  /* Read the bytes (they happen to be ASCII codes) one at a time. */
  int c; /* buffer for read bytes */
  while (read(fd, &c, 1) > 0)    /* 0 signals EOF */
    write(STDOUT_FILENO, &c, 1); /* write one byte to the standard output */

  /* Release the lock explicitly. */
  lock.l_type = F_UNLCK;
  if (fcntl(fd, F_SETLK, &lock) < 0)
    report_and_exit("explicit unlocking failed...");

  close(fd);
  return 0;
}

Der Verbraucher Programm ist komplizierter als nötig, um Funktionen der Sperr-API hervorzuheben. Insbesondere der Verbraucher Das Programm prüft zunächst, ob die Datei exklusiv gesperrt ist und versucht erst dann, eine gemeinsame Sperre zu erlangen. Der relevante Code lautet:

lock.l_type = F_WRLCK;
...
fcntl(fd, F_GETLK, &lock); /* sets lock.l_type to F_UNLCK if no write lock */
if (lock.l_type != F_UNLCK)
  report_and_exit("file is still write locked...");

Der F_GETLK Operation, die in fcntl angegeben ist Der Aufruf sucht nach einer Sperre, in diesem Fall einer exklusiven Sperre, die als F_WRLCK angegeben ist in der ersten Aussage oben. Wenn die angegebene Sperre nicht existiert, dann wird die fcntl Der Aufruf ändert das Sperrtypfeld automatisch in F_UNLCK um auf diese Tatsache hinzuweisen. Wenn die Datei exklusiv gesperrt ist, wird der Verbraucher endet. (Eine robustere Version des Programms könnte die consumer schlafen etwas und versuchen Sie es mehrmals.)

Wenn die Datei derzeit nicht gesperrt ist, dann der Verbraucher versucht, eine gemeinsam genutzte (schreibgeschützte ) sperren (F_RDLCK ). Um das Programm zu verkürzen, das F_GETLK Aufruf von fcntl könnte entfallen, weil F_RDLCK -Aufruf würde fehlschlagen, wenn ein Lesen-Schreiben lock bereits von einem anderen Prozess gehalten wurden. Denken Sie daran, dass eine schreibgeschützte lock hindert andere Prozesse daran, in die Datei zu schreiben, erlaubt aber anderen Prozessen, aus der Datei zu lesen. Kurz gesagt, eine geteilte Die Sperre kann von mehreren Prozessen gehalten werden. Nachdem er eine gemeinsame Sperre erhalten hat, wird der Verbraucher Das Programm liest die Bytes einzeln aus der Datei, gibt die Bytes auf der Standardausgabe aus, hebt die Sperre auf, schließt die Datei und beendet sich.

Hier ist die Ausgabe der beiden Programme, die von demselben Terminal mit % gestartet wurden als Kommandozeilen-Prompt:

% ./producer
Process 29255 has written to data file...

% ./consumer
Now is the winter of our discontent
Made glorious summer by this sun of York

In diesem ersten Codebeispiel handelt es sich bei den über IPC geteilten Daten um Text:zwei Zeilen aus Shakespeares Stück Richard III . Der Inhalt der gemeinsam genutzten Datei könnte jedoch umfangreiche, willkürliche Bytes sein (z. B. ein digitalisierter Film), was die gemeinsame Nutzung von Dateien zu einem beeindruckend flexiblen IPC-Mechanismus macht. Der Nachteil ist, dass der Dateizugriff relativ langsam ist, egal ob es sich um einen Lese- oder einen Schreibzugriff handelt. Wie immer ist die Programmierung mit Kompromissen verbunden. Das nächste Beispiel hat die Vorteile von IPC durch gemeinsam genutzten Speicher anstelle von gemeinsam genutzten Dateien mit einer entsprechenden Leistungssteigerung.

Gemeinsamer Speicher

Linux-Systeme bieten zwei separate APIs für gemeinsam genutzten Speicher:die Legacy-System-V-API und die neuere POSIX-API. Diese APIs sollten jedoch niemals in einer einzigen Anwendung gemischt werden. Ein Nachteil des POSIX-Ansatzes besteht darin, dass sich die Funktionen noch in der Entwicklung befinden und von der installierten Kernelversion abhängen, was sich auf die Codeportabilität auswirkt. Beispielsweise implementiert die POSIX-API standardmäßig Shared Memory als memory-mapped file :Für ein gemeinsam genutztes Speichersegment verwaltet das System eine Backing-Datei mit entsprechenden Inhalten. Shared Memory unter POSIX kann ohne Sicherungsdatei konfiguriert werden, aber dies kann die Portabilität beeinträchtigen. Mein Beispiel verwendet die POSIX-API mit einer Sicherungsdatei, die die Vorteile von Speicherzugriff (Geschwindigkeit) und Dateispeicherung (Persistenz) kombiniert.

Das Shared-Memory-Beispiel hat zwei Programme namens memwriter und memreader , und verwendet ein Semaphor ihren Zugriff auf den gemeinsamen Speicher zu koordinieren. Immer wenn gemeinsames Gedächtnis mit einem Schreiber ins Spiel kommt , ob bei Multi-Processing oder Multi-Threading, ebenso das Risiko einer speicherbasierten Race-Condition; daher wird die Semaphore verwendet, um den Zugriff auf den gemeinsam genutzten Speicher zu koordinieren (synchronisieren).

Der Memwriter Programm sollte zuerst in einem eigenen Terminal gestartet werden. Der Memreader Programm kann dann (innerhalb eines Dutzend Sekunden) in einem eigenen Terminal gestartet werden. Die Ausgabe des memreader ist:

This is the way the world ends...

Weitere Linux-Ressourcen

  • Spickzettel für Linux-Befehle
  • Spickzettel für fortgeschrittene Linux-Befehle
  • Kostenloser Online-Kurs:RHEL Technical Overview
  • Spickzettel für Linux-Netzwerke
  • SELinux-Spickzettel
  • Spickzettel für allgemeine Linux-Befehle
  • Was sind Linux-Container?
  • Unsere neuesten Linux-Artikel

Jede Quelldatei hat oben eine Dokumentation, die die Link-Flags erklärt, die während der Kompilierung einzufügen sind.

Beginnen wir mit einem Überblick darüber, wie Semaphoren als Synchronisationsmechanismus funktionieren. Ein allgemeines Semaphor wird auch als Zählsemaphor bezeichnet , da es einen Wert hat (normalerweise auf Null initialisiert), der erhöht werden kann. Stellen Sie sich ein Geschäft vor, das Fahrräder vermietet, von denen Hunderte auf Lager sind, mit einem Programm, das die Verkäufer für die Vermietung verwenden. Jedes Mal, wenn ein Fahrrad ausgeliehen wird, wird die Ampel um eins erhöht; Bei Rückgabe eines Fahrrads wird die Semaphore um eins verringert. Die Ausleihe kann fortgesetzt werden, bis der Wert 100 erreicht, muss dann aber anhalten, bis mindestens ein Fahrrad zurückgegeben wird, wodurch der Semaphor auf 99 verringert wird.

Ein binäres Semaphor ist ein Sonderfall, der nur zwei Werte erfordert:0 und 1. In dieser Situation fungiert ein Semaphor als Mutex :ein Konstrukt des gegenseitigen Ausschlusses. Das Shared-Memory-Beispiel verwendet ein Semaphor als Mutex. Wenn der Wert des Semaphors 0 ist, wird der memwriter allein auf den gemeinsamen Speicher zugreifen kann. Nach dem Schreiben erhöht dieser Prozess den Wert des Semaphors, wodurch der memreader zugelassen wird um den gemeinsamen Speicher zu lesen.

Beispiel 3. Quellcode für den memwriter verarbeiten

/** Compilation: gcc -o memwriter memwriter.c -lrt -lpthread **/
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <semaphore.h>
#include <string.h>
#include "shmem.h"

void report_and_exit(const char* msg) {
  perror(msg);
  exit(-1);
}

int main() {
  int fd = shm_open(BackingFile,      /* name from smem.h */
                    O_RDWR | O_CREAT, /* read/write, create if needed */
                    AccessPerms);     /* access permissions (0644) */
  if (fd < 0) report_and_exit("Can't open shared mem segment...");

  ftruncate(fd, ByteSize); /* get the bytes */

  caddr_t memptr = mmap(NULL,       /* let system pick where to put segment */
                        ByteSize,   /* how many bytes */
                        PROT_READ | PROT_WRITE, /* access protections */
                        MAP_SHARED, /* mapping visible to other processes */
                        fd,         /* file descriptor */
                        0);         /* offset: start at 1st byte */
  if ((caddr_t) -1  == memptr) report_and_exit("Can't get segment...");

  fprintf(stderr, "shared mem address: %p [0..%d]\n", memptr, ByteSize - 1);
  fprintf(stderr, "backing file:       /dev/shm%s\n", BackingFile );

  /* semaphore code to lock the shared mem */
  sem_t* semptr = sem_open(SemaphoreName, /* name */
                           O_CREAT,       /* create the semaphore */
                           AccessPerms,   /* protection perms */
                           0);            /* initial value */
  if (semptr == (void*) -1) report_and_exit("sem_open");

  strcpy(memptr, MemContents); /* copy some ASCII bytes to the segment */

  /* increment the semaphore so that memreader can read */
  if (sem_post(semptr) < 0) report_and_exit("sem_post");

  sleep(12); /* give reader a chance */

  /* clean up */
  munmap(memptr, ByteSize); /* unmap the storage */
  close(fd);
  sem_close(semptr);
  shm_unlink(BackingFile); /* unlink from the backing file */
  return 0;
}

Hier ist eine Übersicht darüber, wie der memwriter und memreader Programme kommunizieren über Shared Memory:

  • Der Memwriter Das oben gezeigte Programm ruft shm_open auf Funktion, um einen Dateideskriptor für die Sicherungsdatei zu erhalten, die das System mit dem gemeinsam genutzten Speicher koordiniert. Zu diesem Zeitpunkt wurde noch kein Speicher zugewiesen. Der anschließende Aufruf der irreführend benannten Funktion ftruncate :
    ftruncate(fd, ByteSize); /* get the bytes */

    weist ByteSize zu Bytes, in diesem Fall bescheidene 512 Bytes. Der Memwriter und memreader Programme greifen nur auf den gemeinsam genutzten Speicher zu, nicht auf die Sicherungsdatei. Das System ist für die Synchronisierung des gemeinsam genutzten Speichers und der Sicherungsdatei verantwortlich.

  • Der Memwriter ruft dann die mmap auf Funktion:
    caddr_t memptr = mmap(NULL,       /* let system pick where to put segment */
                          ByteSize,   /* how many bytes */
                          PROT_READ | PROT_WRITE, /* access protections */
                          MAP_SHARED, /* mapping visible to other processes */
                          fd,         /* file descriptor */
                          0);         /* offset: start at 1st byte */

    um einen Zeiger auf den gemeinsam genutzten Speicher zu erhalten. (Der memreader macht einen ähnlichen Aufruf.) Der Zeigertyp caddr_t beginnt mit einem c für calloc , eine Systemfunktion, die dynamisch zugewiesenen Speicher auf Nullen initialisiert. Der Memwriter verwendet den memptr für das spätere schreiben Vorgang unter Verwendung der Bibliothek strcpy (String kopieren) Funktion.

  • An dieser Stelle der Memwriter ist schreibbereit, erzeugt aber zunächst eine Semaphore, um den exklusiven Zugriff auf den Shared Memory zu gewährleisten. Eine Racebedingung würde eintreten, wenn der memwriter schrieben, während der memreader las. Wenn der Aufruf von sem_open erfolgreich:
    sem_t* semptr = sem_open(SemaphoreName, /* name */
                             O_CREAT,       /* create the semaphore */
                             AccessPerms,   /* protection perms */
                             0);            /* initial value */

    dann kann das Schreiben fortgesetzt werden. Der SemaphoreName (jeder eindeutige nicht leere Name reicht aus) identifiziert die Semaphore sowohl im memwriter und der memreader . Der Anfangswert von null gibt den Ersteller des Semaphors an, in diesem Fall den Memwriter , das Recht, in diesem Fall mit dem Schreiben fortzufahren Betrieb.

  • Nach dem Schreiben der Memwriter erhöht den Semaphorwert auf 1:
    if (sem_post(semptr) < 0) ..

    mit einem Anruf bei der sem_post Funktion. Das Erhöhen des Semaphors gibt die Mutex-Sperre frei und aktiviert den memreader um das Lesen auszuführen Betrieb. Zu guter Letzt der Memwriter hebt auch die Zuordnung des gemeinsam genutzten Speichers vom memwriter auf Adressraum:

    munmap(memptr, ByteSize); /* unmap the storage *

    Dies sperrt den Memwriter vor weiteren Zugriffen auf den Shared Memory.

Beispiel 4. Quellcode für den memreader verarbeiten

/** Compilation: gcc -o memreader memreader.c -lrt -lpthread **/
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <semaphore.h>
#include <string.h>
#include "shmem.h"

void report_and_exit(const char* msg) {
  perror(msg);
  exit(-1);
}

int main() {
  int fd = shm_open(BackingFile, O_RDWR, AccessPerms);  /* empty to begin */
  if (fd < 0) report_and_exit("Can't get file descriptor...");

  /* get a pointer to memory */
  caddr_t memptr = mmap(NULL,       /* let system pick where to put segment */
                        ByteSize,   /* how many bytes */
                        PROT_READ | PROT_WRITE, /* access protections */
                        MAP_SHARED, /* mapping visible to other processes */
                        fd,         /* file descriptor */
                        0);         /* offset: start at 1st byte */
  if ((caddr_t) -1 == memptr) report_and_exit("Can't access segment...");

  /* create a semaphore for mutual exclusion */
  sem_t* semptr = sem_open(SemaphoreName, /* name */
                           O_CREAT,       /* create the semaphore */
                           AccessPerms,   /* protection perms */
                           0);            /* initial value */
  if (semptr == (void*) -1) report_and_exit("sem_open");

  /* use semaphore as a mutex (lock) by waiting for writer to increment it */
  if (!sem_wait(semptr)) { /* wait until semaphore != 0 */
    int i;
    for (i = 0; i < strlen(MemContents); i++)
      write(STDOUT_FILENO, memptr + i, 1); /* one byte at a time */
    sem_post(semptr);
  }

  /* cleanup */
  munmap(memptr, ByteSize);
  close(fd);
  sem_close(semptr);
  unlink(BackingFile);
  return 0;
}

Sowohl im memwriter und memreader Programmen sind die Shared-Memory-Funktionen von Hauptinteresse shm_open und mmap :Bei Erfolg gibt der erste Aufruf einen Dateideskriptor für die Sicherungsdatei zurück, den der zweite Aufruf dann verwendet, um einen Zeiger auf das gemeinsam genutzte Speichersegment zu erhalten. Die Aufrufe von shm_open sind in den beiden Programmen ähnlich, außer dass der memwriter Programm erstellt den gemeinsamen Speicher, während der memreader greift nur auf diesen bereits angelegten Speicher zu:

int fd = shm_open(BackingFile, O_RDWR | O_CREAT, AccessPerms); /* memwriter */
int fd = shm_open(BackingFile, O_RDWR, AccessPerms);           /* memreader */

Mit einem Dateideskriptor in der Hand werden die Aufrufe von mmap sind gleich:

caddr_t memptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

Das erste Argument für mmap ist NULL , was bedeutet, dass das System bestimmt, wo der Speicher im virtuellen Adressraum zugewiesen werden soll. Es ist möglich (aber schwierig), stattdessen eine Adresse anzugeben. Die MAP_SHARED Flag gibt an, dass der zugewiesene Speicher von Prozessen gemeinsam genutzt werden kann, und das letzte Argument (in diesem Fall null) bedeutet, dass der Offset für den gemeinsam genutzten Speicher das erste Byte sein sollte. Die Größe Das Argument gibt die Anzahl der zuzuweisenden Bytes an (in diesem Fall 512), und das Schutzargument gibt an, dass der gemeinsam genutzte Speicher geschrieben und gelesen werden kann.

Wenn der Memwriter das Programm erfolgreich ausgeführt wird, erstellt und verwaltet das System die Sicherungsdatei; Auf meinem System ist die Datei /dev/shm/shMemEx , mit shMemEx als mein Name (angegeben in der Header-Datei shmem.h ) für den gemeinsam genutzten Speicher. In der aktuellen Version des memwriter und memreader Programmen die Anweisung:

shm_unlink(BackingFile); /* removes backing file */

entfernt die Sicherungsdatei. Wenn die Verknüpfung aufheben -Anweisung weggelassen wird, dann bleibt die Sicherungsdatei bestehen, nachdem das Programm beendet wurde.

Der Memreader , wie der memwriter , greift auf das Semaphor über seinen Namen in einem Aufruf von sem_open zu . Aber der memreader geht dann in einen Wartezustand bis der memwriter erhöht die Semaphore, deren Anfangswert 0 ist:

if (!sem_wait(semptr)) { /* wait until semaphore != 0 */

Sobald das Warten vorbei ist, wird der memreader liest die ASCII-Bytes aus dem gemeinsamen Speicher, räumt auf und beendet sich.

Die Shared-Memory-API enthält Operationen, um das Shared-Memory-Segment und die Sicherungsdatei explizit zu synchronisieren. Diese Vorgänge wurden aus dem Beispiel weggelassen, um Unordnung zu vermeiden und den Fokus auf die gemeinsame Speichernutzung und den Semaphor-Code zu legen.

Der Memwriter und memreader Programme werden wahrscheinlich ausgeführt, ohne eine Race-Condition auszulösen, selbst wenn der Semaphor-Code entfernt wird:der memwriter erstellt das gemeinsame Speichersegment und schreibt sofort darauf; der memreader kann nicht einmal auf den gemeinsamen Speicher zugreifen, bis dieser erstellt wurde. Best Practice erfordert jedoch, dass der Shared-Memory-Zugriff bei jedem Schreibvorgang synchronisiert wird Betrieb ist in der Mischung, und die Semaphor-API ist wichtig genug, um in einem Codebeispiel hervorgehoben zu werden.

Abschluss

Die Beispiele für gemeinsam genutzte Dateien und gemeinsam genutzten Speicher zeigen, wie Prozesse über gemeinsam genutzten Speicher kommunizieren können , Dateien in einem Fall und Speichersegmente in dem anderen. Die APIs für beide Ansätze sind relativ unkompliziert. Haben diese Ansätze einen gemeinsamen Nachteil? Moderne Anwendungen befassen sich oft mit Streaming-Daten, in der Tat mit massiv großen Datenströmen. Weder der Shared-File- noch der Shared-Memory-Ansatz sind für massive Datenströme gut geeignet. Kanäle der einen oder anderen Art sind besser geeignet. Teil 2 stellt daher Kanäle und Nachrichtenwarteschlangen vor, wiederum mit Codebeispielen in C.

[Laden Sie die vollständige Anleitung zur Kommunikation zwischen Prozessen unter Linux herunter]


Linux
  1. Einführung in den Leitfaden zur Kommunikation zwischen Prozessen in Linux

  2. Kommunikation zwischen Prozessen in Linux:Sockets und Signale

  3. Grundlagen der Linux-Dateiberechtigungen

  4. Linux – Alles ist eine Datei?

  5. Empfehlung für die Kommunikation zwischen Prozessen

Weniger Befehl unter Linux

Gzip-Befehl unter Linux

Gunzip-Befehl unter Linux

Stat-Befehl unter Linux

Was ist umask unter Linux?

So verknüpfen Sie eine Datei unter Linux per Symlink