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

Kommunikation zwischen Prozessen in Linux:Verwendung von Pipes und Nachrichtenwarteschlangen

Dies ist der zweite Artikel in einer Reihe über Interprozesskommunikation (IPC) in Linux. Der erste Artikel konzentrierte sich auf IPC durch gemeinsam genutzten Speicher:gemeinsam genutzte Dateien und gemeinsam genutzte Speichersegmente. Dieser Artikel wendet sich Pipes zu, bei denen es sich um Kanäle handelt, die Prozesse für die Kommunikation verbinden. Ein Kanal hat ein Schreibende zum Schreiben von Bytes und ein Leseende zum Lesen dieser Bytes in FIFO-Reihenfolge (first in, first out). Bei typischer Verwendung schreibt ein Prozess in den Kanal und ein anderer Prozess liest von demselben Kanal. Die Bytes selbst können alles darstellen:Zahlen, Mitarbeiterdaten, digitale Filme und so weiter.

Pipes gibt es in zwei Varianten, benannt und unbenannt, und können entweder interaktiv von der Befehlszeile oder innerhalb von Programmen verwendet werden; Beispiele sind in Vorbereitung. Dieser Artikel befasst sich auch mit Speicherwarteschlangen, die aus der Mode gekommen sind – aber zu Unrecht.

Die Codebeispiele im ersten Artikel bestätigten die Bedrohung durch Racebedingungen (entweder dateibasiert oder speicherbasiert) in IPC, die gemeinsam genutzten Speicher verwenden. Es stellt sich natürlich die Frage nach sicherer Parallelität für die kanalbasierte IPC, die in diesem Artikel behandelt wird. Die Codebeispiele für Pipes und Speicherwarteschlangen verwenden APIs mit dem POSIX-Gütesiegel, und ein Kernziel der POSIX-Standards ist Thread-Sicherheit.

Beachten Sie die Manpages für mq_open Funktion, die zur Memory Queue API gehört. Diese Seiten enthalten einen Abschnitt über Attribute mit dieser kleinen Tabelle:

Schnittstelle Attribut Wert
mq_open() Gewindesicherheit MT-sicher

Der Wert MT-Safe (mit MT für Multithreading) bedeutet, dass die mq_open Die Funktion ist threadsicher, was wiederum prozesssicher impliziert:Ein Prozess wird genau in dem Sinne ausgeführt, in dem einer seiner Threads ausgeführt wird, und wenn keine Race-Bedingung zwischen Threads in demselben auftreten kann Prozess kann eine solche Bedingung nicht zwischen Threads in verschiedenen Prozessen auftreten. Der MT-Safe -Attribut stellt sicher, dass bei Aufrufen von mq_open keine Race-Bedingung auftritt . Im Allgemeinen ist kanalbasiertes IPC gleichzeitig sicher, obwohl in den folgenden Beispielen ein Warnhinweis gegeben wird.

Unbenannte Pipes

Beginnen wir mit einem erfundenen Befehlszeilenbeispiel, das zeigt, wie unbenannte Pipes funktionieren. Auf allen modernen Systemen der vertikale Balken | stellt eine unbenannte Pipe in der Befehlszeile dar. Nehmen Sie % an ist die Eingabeaufforderung der Befehlszeile, und betrachten Sie diesen Befehl:

% sleep 5 | echo "Hello, world!" ## writer to the left of |, reader to the right

Der Schlaf und Echo Dienstprogramme werden als separate Prozesse ausgeführt, und die unbenannte Pipe ermöglicht ihnen die Kommunikation. Das Beispiel ist jedoch dahingehend erfunden, dass keine Kommunikation stattfindet. Die Begrüßung Hello, world! erscheint auf dem Bildschirm; dann, nach etwa fünf Sekunden, kehrt die Eingabeaufforderung der Befehlszeile zurück und zeigt an, dass sowohl der Ruhemodus und Echo Prozesse sind beendet. Was ist los?

In der vertikalen Balkensyntax von der Befehlszeile aus wird der Prozess links (sleep ) ist der Writer und der Prozess rechts (echo ) ist der Leser. Standardmäßig blockiert der Reader, bis es Bytes zum Lesen aus dem Kanal gibt, und der Writer – nachdem er seine Bytes geschrieben hat – beendet den Vorgang, indem er eine Stream-Ende-Markierung sendet. (Auch wenn der Writer vorzeitig beendet wird, wird eine Stream-Ende-Markierung an den Reader gesendet.) Die unbenannte Pipe bleibt bestehen, bis sowohl der Writer als auch der Reader beendet werden.

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

Im erfundenen Beispiel der sleep Der Prozess schreibt keine Bytes in den Kanal, wird jedoch nach etwa fünf Sekunden beendet, wodurch eine Stream-Ende-Markierung an den Kanal gesendet wird. In der Zwischenzeit das Echo Der Prozess schreibt die Begrüßung sofort auf die Standardausgabe (den Bildschirm), da dieser Prozess keine Bytes aus dem Kanal liest und daher nicht wartet. Einmal den Schlaf und Echo Prozesse werden beendet, die unbenannte Pipe – überhaupt nicht für die Kommunikation verwendet – verschwindet und die Befehlszeilen-Eingabeaufforderung kehrt zurück.

Hier ist ein nützlicheres Beispiel mit zwei unbenannten Pipes. Angenommen, die Datei test.dat sieht so aus:

this
is
the
way
the
world
ends

Der Befehl:

% cat test.dat | sort | uniq

leitet die Ausgabe von cat weiter (verketten) Prozess in die Sortierung verarbeitet, um eine sortierte Ausgabe zu erzeugen, und leitet dann die sortierte Ausgabe an uniq weiter Vorgang zum Eliminieren doppelter Datensätze (in diesem Fall die beiden Vorkommen von the auf eins reduzieren):

ends
is
the
this
way
world

Die Szene ist jetzt bereit für ein Programm mit zwei Prozessen, die über eine unbenannte Pipe kommunizieren.

Beispiel 1. Zwei Prozesse kommunizieren über eine unbenannte Pipe.

#include <sys/wait.h> /* wait */
#include <stdio.h>
#include <stdlib.h>   /* exit functions */
#include <unistd.h>   /* read, write, pipe, _exit */
#include <string.h>

#define ReadEnd  0
#define WriteEnd 1

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

int main() {
  int pipeFDs[2]; /* two file descriptors */
  char buf;       /* 1-byte buffer */
  const char* msg = "Nature's first green is gold\n"; /* bytes to write */

  if (pipe(pipeFDs) < 0) report_and_exit("pipeFD");
  pid_t cpid = fork();                                /* fork a child process */
  if (cpid < 0) report_and_exit("fork");              /* check for failure */

  if (0 == cpid) {    /*** child ***/                 /* child process */
    close(pipeFDs[WriteEnd]);                         /* child reads, doesn't write */

    while (read(pipeFDs[ReadEnd], &buf, 1) > 0)       /* read until end of byte stream */
      write(STDOUT_FILENO, &buf, sizeof(buf));        /* echo to the standard output */

    close(pipeFDs[ReadEnd]);                          /* close the ReadEnd: all done */
    _exit(0);                                         /* exit and notify parent at once  */
  }
  else {              /*** parent ***/
    close(pipeFDs[ReadEnd]);                          /* parent writes, doesn't read */

    write(pipeFDs[WriteEnd], msg, strlen(msg));       /* write the bytes to the pipe */
    close(pipeFDs[WriteEnd]);                         /* done writing: generate eof */

    wait(NULL);                                       /* wait for child to exit */
    exit(0);                                          /* exit normally */
  }
  return 0;
}

Die pipeUN obiges Programm verwendet die Systemfunktion fork einen Prozess zu erstellen. Obwohl das Programm nur eine einzige Quelldatei hat, tritt während der (erfolgreichen) Ausführung eine Mehrfachverarbeitung auf. Hier sind die Einzelheiten in einem kurzen Überblick darüber, wie die Bibliothek fork funktioniert funktioniert:

  • Die Gabelung Funktion, die im Eltern aufgerufen wird Prozess, gibt -1 zurück bei Misserfolg an die Eltern. In der pipeUN Der Aufruf lautet beispielsweise:
    pid_t cpid = fork(); /* called in parent */

    Der zurückgegebene Wert wird in diesem Beispiel in der Variable cpid gespeichert vom ganzzahligen Typ pid_t . (Jeder Prozess hat seine eigene Prozess-ID , eine nicht negative Ganzzahl, die den Prozess identifiziert.) Das Verzweigen eines neuen Prozesses kann aus mehreren Gründen fehlschlagen, einschließlich einer vollständigen Prozesstabelle , eine Struktur, die das System verwaltet, um Prozesse zu verfolgen. Zombie-Prozesse, die in Kürze erläutert werden, können dazu führen, dass sich eine Prozesstabelle füllt, wenn diese nicht geerntet werden.

  • Wenn die Gabelung Aufruf erfolgreich ist, erzeugt (erzeugt) er dadurch einen neuen untergeordneten Prozess, der einen Wert an den übergeordneten Prozess zurückgibt, aber einen anderen Wert an den untergeordneten Prozess. Sowohl der übergeordnete als auch der untergeordnete Prozess führen dasselbe aus Code, der dem Aufruf von fork folgt . (Das untergeordnete Element erbt Kopien aller bisher im übergeordneten Element deklarierten Variablen.) Insbesondere ein erfolgreicher Aufruf von fork gibt zurück:
    • Null zum untergeordneten Prozess
    • Die Prozess-ID des untergeordneten Prozesses für den übergeordneten Prozess
  • Ein wenn/sonst oder ein gleichwertiges Konstrukt wird typischerweise nach einem erfolgreichen fork verwendet Aufruf zum Trennen von Code, der für den Elternteil bestimmt ist, von Code, der für das Kind bestimmt ist. In diesem Beispiel lautet das Konstrukt:
    if (0 == cpid) {    /*** child ***/
    ...
    }
    else {              /*** parent ***/
    ...
    }

Wenn das Verzweigen eines untergeordneten Elements erfolgreich ist, wird die pipeUN Programm geht wie folgt vor. Es gibt ein Integer-Array:

int pipeFDs[2]; /* two file descriptors */

um zwei Dateideskriptoren zu halten, einen zum Schreiben in die Pipe und einen zum Lesen aus der Pipe. (Das Array-Element pipeFDs[0] ist der Dateideskriptor für das gelesene Ende und das Array-Element pipeFDs[1] ist der Dateideskriptor für das Schreibende.) Ein erfolgreicher Aufruf der Pipe des Systems Funktion, unmittelbar vor dem Aufruf von fork , füllt das Array mit den beiden Dateideskriptoren:

if (pipe(pipeFDs) < 0) report_and_exit("pipeFD");

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

Das Elternteil und das Kind haben jetzt Kopien beider Dateideskriptoren, aber die Trennung der Bedenken Muster bedeutet, dass jeder Prozess genau einen der Deskriptoren benötigt. In diesem Beispiel schreiben die Eltern und das Kind liest, obwohl die Rollen vertauscht werden könnten. Die erste Anweisung im untergeordneten if -clause-Code schließt daher das Schreibende der Pipe:

close(pipeFDs[WriteEnd]); /* called in child code */

und die erste Anweisung im übergeordneten else -clause code schließt das Leseende der Pipe:

close(pipeFDs[ReadEnd]);  /* called in parent code */

Der Elternteil schreibt dann einige Bytes (ASCII-Codes) in die unbenannte Pipe, und der Kindteil liest diese und gibt sie an die Standardausgabe zurück.

Ein weiterer Aspekt des Programms muss geklärt werden:der Aufruf zum Warten Funktion im übergeordneten Code. Einmal erzeugt, ist ein Kindprozess weitgehend unabhängig von seinem Elternprozess, wie sogar die kurze pipeUN Programm veranschaulicht. Das Kind kann beliebigen Code ausführen, der möglicherweise nichts mit dem Elternteil zu tun hat. Das System benachrichtigt den Elternteil jedoch durch ein Signal – falls und wann das Kind aufhört.

Was ist, wenn der Elternteil vor dem Kind kündigt? In diesem Fall wird und bleibt das Kind ein Zombie, wenn keine Vorsichtsmaßnahmen getroffen werden Prozess mit einem Eintrag in der Prozesstabelle. Es gibt zwei Arten von Vorsichtsmaßnahmen. Eine Vorsichtsmaßnahme besteht darin, dass der Elternteil dem System mitteilt, dass der Elternteil kein Interesse an der Kündigung des Kindes hat:

signal(SIGCHLD, SIG_IGN); /* in parent: ignore notification */

Ein zweiter Ansatz besteht darin, den Elternteil eine Wartezeit ausführen zu lassen bei der Beendigung des Kindes, wodurch sichergestellt wird, dass der Elternteil das Kind überlebt. Dieser zweite Ansatz wird in pipeUN verwendet Programm, wobei der übergeordnete Code diesen Aufruf hat:

wait(NULL); /* called in parent */

Dieser Aufruf zum Warten bedeutet warten, bis die Beendigung eines Kindes eintritt , und in der pipeUN Programm gibt es nur einen untergeordneten Prozess. (Die NULL Das Argument könnte durch die Adresse einer Integer-Variablen ersetzt werden, die den Exit-Status des Kindes enthält.) Es gibt eine flexiblere waitpid Funktion zur feinkörnigen Steuerung, z. B. zum Spezifizieren eines bestimmten untergeordneten Prozesses unter mehreren.

Die pipeUN Programm trifft eine weitere Vorsichtsmaßnahme. Wenn der Elternteil mit dem Warten fertig ist, beendet der Elternteil mit dem Aufruf zum regulären Ausgang Funktion. Im Gegensatz dazu beendet das Kind mit einem Aufruf von _exit Variante, die die Kündigung beschleunigt. Tatsächlich weist das Kind das System an, das Elternteil so schnell wie möglich zu benachrichtigen, dass das Kind gekündigt hat.

Wenn zwei Prozesse in dieselbe unbenannte Pipe schreiben, können die Bytes verschachtelt werden? Zum Beispiel, wenn Prozess P1 schreibt:

foo bar

in eine Pipe und Prozess P2 schreibt gleichzeitig:

baz baz

zur gleichen Pipe, scheint es, dass der Inhalt der Pipe willkürlich sein könnte, wie zum Beispiel:

baz foo baz bar

Der POSIX-Standard stellt sicher, dass Schreibvorgänge nicht verschachtelt werden, solange kein Schreibvorgang PIPE_BUF überschreitet Byte. Auf Linux-Systemen PIPE_BUF ist 4.096 Byte groß. Ich bevorzuge bei Pipes einen einzelnen Autor und einen einzelnen Leser, wodurch das Problem umgangen wird.

Benannte Pipes

Eine unbenannte Pipe hat keine Sicherungsdatei:Das System verwaltet einen In-Memory-Puffer, um Bytes vom Writer zum Reader zu übertragen. Sobald der Writer und der Reader beendet sind, wird der Puffer zurückgefordert, sodass die unbenannte Pipe verschwindet. Im Gegensatz dazu hat eine benannte Pipe eine Hintergrunddatei und eine eigene API.

Schauen wir uns ein weiteres Befehlszeilenbeispiel an, um den Kern von Named Pipes zu verstehen. Hier sind die Schritte:

  • Öffne zwei Terminals. Das Arbeitsverzeichnis sollte für beide gleich sein.
  • Geben Sie in einem der Terminals diese beiden Befehle ein (die Eingabeaufforderung lautet wieder % , und meine Kommentare beginnen mit ## ):
    % mkfifo tester  ## creates a backing file named tester
    % cat tester     ## type the pipe's contents to stdout

    Am Anfang sollte nichts im Terminal erscheinen, da noch nichts in die Named Pipe geschrieben wurde.

  • Geben Sie im zweiten Terminal den Befehl ein:
    % cat > tester  ## redirect keyboard input to the pipe
    hello, world!   ## then hit Return key
    bye, bye        ## ditto
    <Control-C>     ## terminate session with a Control-C

    Was auch immer in dieses Terminal eingegeben wird, wird im anderen wiedergegeben. Einmal Strg+C eingegeben wird, kehrt die reguläre Befehlszeilen-Eingabeaufforderung in beiden Terminals zurück:Die Pipe wurde geschlossen.

  • Aufräumen durch Entfernen der Datei, die die benannte Pipe implementiert:
    % unlink tester

Als Name des Dienstprogramms mkfifo Impliziert, dass eine benannte Pipe auch als FIFO bezeichnet wird, da das erste eingehende Byte das erste ausgehende Byte ist und so weiter. Es gibt eine Bibliotheksfunktion namens mkfifo das eine Named Pipe in Programmen erstellt und im nächsten Beispiel verwendet wird, das aus zwei Prozessen besteht:Einer schreibt in die Named Pipe und der andere liest aus dieser Pipe.

Beispiel 2. Der fifoWriter Programm

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
#include <stdio.h>

#define MaxLoops         12000   /* outer loop */
#define ChunkSize           16   /* how many written at a time */
#define IntsPerChunk         4   /* four 4-byte ints per chunk */
#define MaxZs              250   /* max microseconds to sleep */

int main() {
  const char* pipeName = "./fifoChannel";
  mkfifo(pipeName, 0666);                      /* read/write for user/group/others */
  int fd = open(pipeName, O_CREAT | O_WRONLY); /* open as write-only */
  if (fd < 0) return -1;                       /* can't go on */

  int i;
  for (i = 0; i < MaxLoops; i++) {          /* write MaxWrites times */
    int j;
    for (j = 0; j < ChunkSize; j++) {       /* each time, write ChunkSize bytes */
      int k;
      int chunk[IntsPerChunk];
      for (k = 0; k < IntsPerChunk; k++)
        chunk[k] = rand();
      write(fd, chunk, sizeof(chunk));
    }
    usleep((rand() % MaxZs) + 1);           /* pause a bit for realism */
  }

  close(fd);           /* close pipe: generates an end-of-stream marker */
  unlink(pipeName);    /* unlink from the implementing file */
  printf("%i ints sent to the pipe.\n", MaxLoops * ChunkSize * IntsPerChunk);

  return 0;
}

Der fifoWriter Programm oben kann wie folgt zusammengefasst werden:

  • Das Programm erstellt eine benannte Pipe zum Schreiben:
    mkfifo(pipeName, 0666); /* read/write perms for user/group/others */
    int fd = open(pipeName, O_CREAT | O_WRONLY);

    wo pipeName ist der Name der Sicherungsdatei, die an mkfifo übergeben wird als erstes Argument. Die Named Pipe wird dann mit dem inzwischen bekannten Aufruf von open geöffnet Funktion, die einen Dateideskriptor zurückgibt.

  • Für einen Hauch von Realismus, der fifoWriter schreibt nicht alle Daten auf einmal, sondern einen Chunk, schläft eine zufällige Anzahl von Mikrosekunden und so weiter. Insgesamt werden 768.000 4-Byte-Ganzzahlwerte in die benannte Pipe geschrieben.
  • Nach dem Schließen der Named Pipe wird der fifoWriter hebt auch die Verknüpfung der Datei auf:
    close(fd);        /* close pipe: generates end-of-stream marker */
    unlink(pipeName); /* unlink from the implementing file */

    Das System fordert die Sicherungsdatei zurück, sobald jeder mit der Pipe verbundene Prozess die Unlink-Operation durchgeführt hat. In diesem Beispiel gibt es nur zwei solcher Prozesse:den fifoWriter und der fifoReader , die beide eine Verknüpfung aufheben Betrieb.

Die beiden Programme sollten auf verschiedenen Terminals mit demselben Arbeitsverzeichnis ausgeführt werden. Der fifoWriter sollte vor dem fifoReader gestartet werden , da ersteres die Pfeife erzeugt. Der fifoReader greift dann auf die bereits erstellte Named Pipe zu.

Beispiel 3. Der fifoReader Programm

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

unsigned is_prime(unsigned n) { /* not pretty, but efficient */
  if (n <= 3) return n > 1;
  if (0 == (n % 2) || 0 == (n % 3)) return 0;

  unsigned i;
  for (i = 5; (i * i) <= n; i += 6)
    if (0 == (n % i) || 0 == (n % (i + 2))) return 0;

  return 1; /* found a prime! */
}

int main() {
  const char* file = "./fifoChannel";
  int fd = open(file, O_RDONLY);
  if (fd < 0) return -1; /* no point in continuing */
  unsigned count = 0, total = 0, primes_count = 0;

  while (1) {
    int next;
    int i;

    ssize_t count = read(fd, &next, sizeof(int));
    if (0 == count) break;                  /* end of stream */
    else if (count == sizeof(int)) {        /* read a 4-byte int value */
      total++;
      if (is_prime(next)) primes_count++;
    }
  }

  close(fd);       /* close pipe from read end */
  unlink(file);    /* unlink from the underlying file */
  printf("Received ints: %u, primes: %u\n", total, primes_count);

  return 0;
}

Der fifoReader Programm oben kann wie folgt zusammengefasst werden:

  • Weil der fifoWriter erstellt die benannte Pipe, den fifoReader braucht nur den Standardaufruf open um auf die Pipe über die Sicherungsdatei zuzugreifen:
    const char* file = "./fifoChannel";
    int fd = open(file, O_RDONLY);

    Die Datei wird schreibgeschützt geöffnet.

  • Das Programm geht dann in eine potenzielle Endlosschleife und versucht, bei jeder Iteration einen 4-Byte-Block zu lesen. Das lesen call:
    ssize_t count = read(fd, &next, sizeof(int));

    gibt 0 zurück, um das Ende des Streams anzuzeigen, in diesem Fall der fifoReader bricht aus der Schleife aus, schließt die benannte Pipe und hebt die Verknüpfung der Sicherungsdatei auf, bevor sie beendet wird.

  • Nach dem Lesen einer 4-Byte-Ganzzahl wird der fifoReader prüft, ob die Zahl eine Primzahl ist. Dies stellt die Geschäftslogik dar, die ein produktionstaugliches Lesegerät für die empfangenen Bytes ausführen könnte. Bei einem Stichprobenlauf gab es 37.682 Primzahlen unter den 768.000 empfangenen Ganzzahlen.

Bei wiederholten Beispielläufen wird der fifoReader erfolgreich alle Bytes gelesen, die der fifoWriter schrieb. Dies ist nicht überraschend. Die beiden Prozesse werden auf demselben Host ausgeführt, wodurch Netzwerkprobleme aus der Gleichung herausgenommen werden. Named Pipes sind ein äußerst zuverlässiger und effizienter IPC-Mechanismus und daher weit verbreitet.

Hier ist die Ausgabe der beiden Programme, die jeweils von einem separaten Terminal aus gestartet werden, aber mit demselben Arbeitsverzeichnis:

% ./fifoWriter
768000 ints sent to the pipe.
###
% ./fifoReader
Received ints: 768000, primes: 37682

Nachrichtenwarteschlangen

Pipes haben striktes FIFO-Verhalten:Das erste geschriebene Byte ist das erste gelesene Byte, das zweite geschriebene Byte ist das zweite gelesene Byte und so weiter. Nachrichtenwarteschlangen können sich genauso verhalten, sind aber flexibel genug, dass Byte-Blöcke außerhalb der FIFO-Reihenfolge abgerufen werden können.

Wie der Name schon sagt, ist eine Nachrichtenwarteschlange eine Folge von Nachrichten, die jeweils aus zwei Teilen bestehen:

  • Die Nutzlast, die ein Array von Bytes ist (char in C)
  • Ein Typ, angegeben als positiver ganzzahliger Wert; Typen kategorisieren Nachrichten für flexiblen Abruf

Betrachten Sie die folgende Darstellung einer Nachrichtenwarteschlange, wobei jede Nachricht mit einem ganzzahligen Typ gekennzeichnet ist:

          +-+    +-+    +-+    +-+
sender--->|3|--->|2|--->|2|--->|1|--->receiver
          +-+    +-+    +-+    +-+

Von den vier angezeigten Nachrichten befindet sich die mit 1 gekennzeichnete vorne, d. h. am nächsten zum Empfänger. Als nächstes kommen zwei Nachrichten mit der Bezeichnung 2 und schließlich eine Nachricht mit der Bezeichnung 3 auf der Rückseite. Wenn strenges FIFO-Verhalten im Spiel wäre, würden die Nachrichten in der Reihenfolge 1-2-2-3 empfangen werden. Die Nachrichtenwarteschlange lässt jedoch andere Abrufaufträge zu. Beispielsweise könnten die Nachrichten vom Empfänger in der Reihenfolge 3-2-1-2 abgerufen werden.

Die mqueue Beispiel besteht aus zwei Programmen, dem sender die in die Nachrichtenwarteschlange und den Empfänger schreibt die aus dieser Warteschlange liest. Beide Programme enthalten die Header-Datei queue.h unten gezeigt:

Beispiel 4. Die Header-Datei queue.h

#define ProjectId 123
#define PathName  "queue.h" /* any existing, accessible file would do */
#define MsgLen    4
#define MsgCount  6

typedef struct {
  long type;                 /* must be of type long */
  char payload[MsgLen + 1];  /* bytes in the message */
} queuedMessage;

Die Header-Datei definiert einen Strukturtyp namens queuedMessage , mit Nutzlast (Byte-Array) und Typ (ganzzahlige) Felder. Diese Datei definiert auch symbolische Konstanten (die Datei #define -Anweisungen), von denen die ersten beiden zum Generieren eines Schlüssels verwendet werden, der wiederum zum Abrufen einer Nachrichtenwarteschlangen-ID verwendet wird. Die Projekt-ID kann ein beliebiger positiver ganzzahliger Wert sein, und der PathName muss eine vorhandene, zugängliche Datei sein – in diesem Fall die Datei queue.h . Die Setup-Anweisungen sowohl im sender und der Empfänger Programme sind:

key_t key = ftok(PathName, ProjectId);   /* generate key */
int qid = msgget(key, 0666 | IPC_CREAT); /* use key to get queue id */

Die ID qid ist praktisch das Gegenstück zu einem Dateideskriptor für Nachrichtenwarteschlangen.

Beispiel 5. Der Absender der Nachricht Programm

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
#include "queue.h"

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

int main() {
  key_t key = ftok(PathName, ProjectId);
  if (key < 0) report_and_exit("couldn't get key...");

  int qid = msgget(key, 0666 | IPC_CREAT);
  if (qid < 0) report_and_exit("couldn't get queue id...");

  char* payloads[] = {"msg1", "msg2", "msg3", "msg4", "msg5", "msg6"};
  int types[] = {1, 1, 2, 2, 3, 3}; /* each must be > 0 */
  int i;
  for (i = 0; i < MsgCount; i++) {
    /* build the message */
    queuedMessage msg;
    msg.type = types[i];
    strcpy(msg.payload, payloads[i]);

    /* send the message */
    msgsnd(qid, &msg, sizeof(msg), IPC_NOWAIT); /* don't block */
    printf("%s sent as type %i\n", msg.payload, (int) msg.type);
  }
  return 0;
}

Der Absender Das obige Programm sendet sechs Nachrichten aus, jeweils zwei von einem bestimmten Typ:Die ersten Nachrichten sind vom Typ 1, die nächsten beiden vom Typ 2 und die letzten beiden vom Typ 3. Die Sendeanweisung:

msgsnd(qid, &msg, sizeof(msg), IPC_NOWAIT);

nicht blockierend konfiguriert ist (das Flag IPC_NOWAIT ), weil die Nachrichten so klein sind. Die einzige Gefahr besteht darin, dass eine volle Warteschlange, was in diesem Beispiel unwahrscheinlich ist, zu einem Sendefehler führen würde. Der Empfänger Das folgende Programm empfängt auch Nachrichten mit dem IPC_NOWAIT Flagge.

Beispiel 6. Der Nachrichten-Empfänger Programm

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
#include "queue.h"

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

int main() {
  key_t key= ftok(PathName, ProjectId); /* key to identify the queue */
  if (key < 0) report_and_exit("key not gotten...");

  int qid = msgget(key, 0666 | IPC_CREAT); /* access if created already */
  if (qid < 0) report_and_exit("no access to queue...");

  int types[] = {3, 1, 2, 1, 3, 2}; /* different than in sender */
  int i;
  for (i = 0; i < MsgCount; i++) {
    queuedMessage msg; /* defined in queue.h */
    if (msgrcv(qid, &msg, sizeof(msg), types[i], MSG_NOERROR | IPC_NOWAIT) < 0)
      puts("msgrcv trouble...");
    printf("%s received as type %i\n", msg.payload, (int) msg.type);
  }

  /** remove the queue **/
  if (msgctl(qid, IPC_RMID, NULL) < 0)  /* NULL = 'no flags' */
    report_and_exit("trouble removing queue...");

  return 0;
}

Der Empfänger Das Programm erstellt keine Nachrichtenwarteschlange, obwohl die API dies nahelegt. Im Empfänger , der Aufruf:

int qid = msgget(key, 0666 | IPC_CREAT);

ist aufgrund des IPC_CREAT irreführend Flag, aber dieses Flag bedeutet eigentlich bei Bedarf erstellen, sonst zugreifen . Der Absender Programm ruft msgsnd auf um Nachrichten zu senden, während der Empfänger ruft msgrcv auf um sie abzurufen. In diesem Beispiel der Absender sendet die Nachrichten in der Reihenfolge 1-1-2-2-3-3, aber der Empfänger ruft sie dann in der Reihenfolge 3-1-2-1-3-2 ab, was zeigt, dass Nachrichtenwarteschlangen nicht an das strikte FIFO-Verhalten gebunden sind:

% ./sender
msg1 sent as type 1
msg2 sent as type 1
msg3 sent as type 2
msg4 sent as type 2
msg5 sent as type 3
msg6 sent as type 3

% ./receiver
msg5 received as type 3
msg1 received as type 1
msg3 received as type 2
msg2 received as type 1
msg6 received as type 3
msg4 received as type 2

Die obige Ausgabe zeigt, dass der sender und der Empfänger kann von demselben Terminal aus gestartet werden. Die Ausgabe zeigt auch, dass die Nachrichtenwarteschlange auch nach dem Sender bestehen bleibt Der Prozess erstellt die Warteschlange, schreibt in sie und wird beendet. Die Warteschlange verschwindet erst nach dem Empfänger Der Prozess entfernt es explizit mit dem Aufruf von msgctl :

if (msgctl(qid, IPC_RMID, NULL) < 0) /* remove queue */

Abschluss

Die APIs für Pipes und Nachrichtenwarteschlangen sind grundsätzlich unidirektional :Ein Prozess schreibt und ein anderer liest. Es gibt Implementierungen von bidirektionalen benannten Pipes, aber mein zwei Cent ist, dass dieser IPC-Mechanismus am besten ist, wenn er am einfachsten ist. Wie bereits erwähnt, haben Nachrichtenwarteschlangen an Popularität verloren – aber ohne guten Grund; diese Warteschlangen sind ein weiteres Werkzeug in der IPC-Toolbox. Teil 3 vervollständigt diese kurze Tour durch die IPC-Toolbox mit Codebeispielen von IPC durch Sockets und Signale.


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

  2. Kommunikation zwischen Prozessen in Linux:Sockets und Signale

  3. So starten Sie den Linux-Befehl im Hintergrund und trennen den Prozess im Terminal

  4. Wie beendet man einen Prozess unter Linux mit dem Befehl?

  5. Pipes und Redirection in Linux - erklärt!

Wall-Befehl unter Linux

Tipps und Tricks zur Verwendung des Linux-Befehls wget

So klonen und wiederherstellen Sie eine Linux-Partition mit dem dd-Befehl

mailx-Befehl unter Linux – Internet-Mail senden und empfangen

Verwenden des Watch-Befehls unter Linux

So beenden Sie Prozesse in Linux mit kill, killall und pkill