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

Kommunikation zwischen Prozessen in Linux:Sockets und Signale

Dies ist der dritte und letzte Artikel in einer Reihe über Interprozesskommunikation (IPC) in Linux. Der erste Artikel konzentrierte sich auf IPC über gemeinsam genutzten Speicher (Dateien und Speichersegmente), und der zweite Artikel behandelt dasselbe für grundlegende Kanäle:Pipes (benannt und unbenannt) und Nachrichtenwarteschlangen. Dieser Artikel bewegt sich von IPC am oberen Ende (Buchsen) zu IPC am unteren Ende (Signale). Codebeispiele konkretisieren die Details.

Sockets

So wie es Pipes in zwei Varianten gibt (benannt und unbenannt), gibt es auch Sockets. IPC-Sockets (auch bekannt als Unix-Domain-Sockets) ermöglichen eine kanalbasierte Kommunikation für Prozesse auf demselben physischen Gerät (Host ), während Netzwerk-Sockets diese Art von IPC für Prozesse ermöglichen, die auf verschiedenen Hosts laufen können, wodurch die Vernetzung ins Spiel kommt. Netzwerk-Sockets benötigen Unterstützung von einem zugrunde liegenden Protokoll wie TCP (Transmission Control Protocol) oder dem untergeordneten UDP (User Datagram Protocol).

Im Gegensatz dazu verlassen sich IPC-Sockets auf den lokalen Systemkern, um die Kommunikation zu unterstützen; insbesondere kommunizieren IPC-Sockets unter Verwendung einer lokalen Datei als Socket-Adresse. Trotz dieser Implementierungsunterschiede sind die IPC-Socket- und Netzwerk-Socket-APIs im Wesentlichen gleich. Das kommende Beispiel behandelt Netzwerk-Sockets, aber der Beispielserver und die Client-Programme können auf derselben Maschine laufen, da der Server die Netzwerkadresse localhost verwendet (127.0.0.1), die Adresse für den lokalen Rechner auf dem lokalen Rechner.

Als Streams konfigurierte Sockets (siehe unten) sind bidirektional, und die Steuerung folgt einem Client/Server-Muster:Der Client initiiert die Konversation, indem er versucht, eine Verbindung zu einem Server herzustellen, der versucht, die Verbindung anzunehmen. Wenn alles funktioniert, können Anfragen vom Client und Antworten vom Server durch den Kanal fließen, bis dieser an beiden Enden geschlossen wird, wodurch die Verbindung unterbrochen wird.

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

Eine Iteration Server, der nur für die Entwicklung geeignet ist, behandelt verbundene Clients nacheinander bis zur Fertigstellung:Der erste Client wird von Anfang bis Ende behandelt, dann der zweite und so weiter. Der Nachteil ist, dass die Handhabung eines bestimmten Clients hängen bleiben kann, wodurch alle wartenden Clients ausgehungert werden. Ein Produktionsserver wäre gleichzeitig , typischerweise mit einer Mischung aus Multi-Processing und Multi-Threading. Beispielsweise verfügt der Nginx-Webserver auf meinem Desktop-Computer über einen Pool von vier Worker-Prozessen, die Client-Anfragen gleichzeitig verarbeiten können. Das folgende Codebeispiel hält die Unordnung auf einem Minimum, indem es einen iterativen Server verwendet; der Fokus bleibt somit auf der Basis-API, nicht auf Nebenläufigkeit.

Schließlich hat sich die Socket-API im Laufe der Zeit erheblich weiterentwickelt, da verschiedene POSIX-Verfeinerungen entstanden sind. Der aktuelle Beispielcode für Server und Client ist bewusst einfach gehalten, unterstreicht aber den bidirektionalen Aspekt einer Stream-basierten Socket-Verbindung. Hier ist eine Zusammenfassung des Kontrollflusses, wobei der Server in einem Terminal und der Client in einem separaten Terminal gestartet wird:

  • Der Server erwartet Client-Verbindungen und liest bei erfolgreicher Verbindung die Bytes vom Client.
  • Um die wechselseitige Konversation zu unterstreichen, sendet der Server die vom Client empfangenen Bytes an den Client zurück. Diese Bytes sind ASCII-Zeichencodes, aus denen Buchtitel bestehen.
  • Der Client schreibt Buchtitel in den Serverprozess und liest dann dieselben Titel, die vom Server zurückgesendet werden. Sowohl der Server als auch der Client geben die Titel auf dem Bildschirm aus. Hier ist die Ausgabe des Servers, im Wesentlichen die gleiche wie die des Clients:
    Listening on port 9876 for clients...
    War and Peace
    Pride and Prejudice
    The Sound and the Fury

Beispiel 1. Der Socket-Server

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include "sock.h"

void report(const char* msg, int terminate) {
  perror(msg);
  if (terminate) exit(-1); /* failure */
}

int main() {
  int fd = socket(AF_INET,     /* network versus AF_LOCAL */
                  SOCK_STREAM, /* reliable, bidirectional, arbitrary payload size */
                  0);          /* system picks underlying protocol (TCP) */
  if (fd < 0) report("socket", 1); /* terminate */

  /* bind the server's local address in memory */
  struct sockaddr_in saddr;
  memset(&saddr, 0, sizeof(saddr));          /* clear the bytes */
  saddr.sin_family = AF_INET;                /* versus AF_LOCAL */
  saddr.sin_addr.s_addr = htonl(INADDR_ANY); /* host-to-network endian */
  saddr.sin_port = htons(PortNumber);        /* for listening */

  if (bind(fd, (struct sockaddr *) &saddr, sizeof(saddr)) < 0)
    report("bind", 1); /* terminate */

  /* listen to the socket */
  if (listen(fd, MaxConnects) < 0) /* listen for clients, up to MaxConnects */
    report("listen", 1); /* terminate */

  fprintf(stderr, "Listening on port %i for clients...\n", PortNumber);
  /* a server traditionally listens indefinitely */
  while (1) {
    struct sockaddr_in caddr; /* client address */
    int len = sizeof(caddr);  /* address length could change */

    int client_fd = accept(fd, (struct sockaddr*) &caddr, &len);  /* accept blocks */
    if (client_fd < 0) {
      report("accept", 0); /* don't terminate, though there's a problem */
      continue;
    }

    /* read from client */
    int i;
    for (i = 0; i < ConversationLen; i++) {
      char buffer[BuffSize + 1];
      memset(buffer, '\0', sizeof(buffer));
      int count = read(client_fd, buffer, sizeof(buffer));
      if (count > 0) {
        puts(buffer);
        write(client_fd, buffer, sizeof(buffer)); /* echo as confirmation */
      }
    }
    close(client_fd); /* break connection */
  }  /* while(1) */
  return 0;
}

Das obige Serverprogramm führt die klassischen vier Schritte aus, um sich auf Clientanfragen vorzubereiten und dann einzelne Anfragen zu akzeptieren. Jeder Schritt ist nach einer Systemfunktion benannt, die der Server aufruft:

  1. Buchse(…) :einen Dateideskriptor für die Socket-Verbindung erhalten
  2. binden(…) :Den Socket an eine Adresse auf dem Host des Servers binden
  3. hör zu (…) :auf Client-Anfragen warten
  4. Akzeptieren (…) :Akzeptiere eine bestimmte Client-Anfrage

Die Buchse Anruf vollständig ist:

int sockfd = socket(AF_INET,      /* versus AF_LOCAL */
                    SOCK_STREAM,  /* reliable, bidirectional */
                    0);           /* system picks protocol (TCP) */

Das erste Argument gibt einen Netzwerk-Socket im Gegensatz zu einem IPC-Socket an. Für das zweite Argument gibt es mehrere Optionen, aber SOCK_STREAM und SOCK_DGRAM (Datagramm) werden wahrscheinlich am häufigsten verwendet. Eine streambasierte socket unterstützt einen zuverlässigen Kanal, in dem verlorene oder geänderte Nachrichten gemeldet werden; der Kanal ist bidirektional, und die Nutzdaten von einer Seite zur anderen können beliebig groß sein. Im Gegensatz dazu ist ein Datagramm-basierter Socket unzuverlässig (am besten versuchen ), unidirektional und erfordert Nutzlasten mit fester Größe. Das dritte Argument für socket spezifiziert das Protokoll. Für den Stream-basierten Socket, um den es hier geht, gibt es eine einzige Wahl, die die Null darstellt:TCP. Denn ein erfolgreicher Aufruf von socket gibt den bekannten Dateideskriptor zurück, ein Socket wird mit der gleichen Syntax geschrieben und gelesen wie beispielsweise eine lokale Datei.

Die Bindung call ist am kompliziertesten, da er verschiedene Verfeinerungen in der Socket-API widerspiegelt. Der interessante Punkt ist, dass dieser Aufruf den Socket an eine Speicheradresse auf der Servermaschine bindet. Aber die hören Aufruf ist einfach:

if (listen(fd, MaxConnects) < 0)

Das erste Argument ist der Dateideskriptor des Sockets und das zweite gibt an, wie viele Client-Verbindungen aufgenommen werden können, bevor der Server eine Verbindung abgelehnt ausgibt Fehler bei einem Verbindungsversuch. (MaxConnects wird in der Header-Datei sock.h auf 8 gesetzt .)

Die akzeptieren Anruf wird standardmäßig auf ein blockierendes Warten gesetzt :Der Server tut nichts, bis ein Client versucht, eine Verbindung herzustellen, und fährt dann fort. Die akzeptieren Funktion gibt -1 zurück um auf einen Fehler hinzuweisen. Wenn der Aufruf erfolgreich ist, gibt er einen anderen Dateideskriptor zurück – für ein Lesen/Schreiben Socket im Gegensatz zum Annehmen Socket, auf das durch das erste Argument in accept verwiesen wird Forderung. Der Server verwendet den Lese-/Schreib-Socket, um Anforderungen vom Client zu lesen und Antworten zurückzuschreiben. Der akzeptierende Socket wird nur zum Akzeptieren von Client-Verbindungen verwendet.

Ein Server läuft standardmäßig unbegrenzt. Entsprechend kann der Server mit Strg+C beendet werden über die Befehlszeile.

Beispiel 2. Der Socket-Client

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <netdb.h>
#include "sock.h"

const char* books[] = {"War and Peace",
                       "Pride and Prejudice",
                       "The Sound and the Fury"};

void report(const char* msg, int terminate) {
  perror(msg);
  if (terminate) exit(-1); /* failure */
}

int main() {
  /* fd for the socket */
  int sockfd = socket(AF_INET,      /* versus AF_LOCAL */
                      SOCK_STREAM,  /* reliable, bidirectional */
                      0);           /* system picks protocol (TCP) */
  if (sockfd < 0) report("socket", 1); /* terminate */

  /* get the address of the host */
  struct hostent* hptr = gethostbyname(Host); /* localhost: 127.0.0.1 */
  if (!hptr) report("gethostbyname", 1); /* is hptr NULL? */
  if (hptr->h_addrtype != AF_INET)       /* versus AF_LOCAL */
    report("bad address family", 1);

  /* connect to the server: configure server's address 1st */
  struct sockaddr_in saddr;
  memset(&saddr, 0, sizeof(saddr));
  saddr.sin_family = AF_INET;
  saddr.sin_addr.s_addr =
     ((struct in_addr*) hptr->h_addr_list[0])->s_addr;
  saddr.sin_port = htons(PortNumber); /* port number in big-endian */

  if (connect(sockfd, (struct sockaddr*) &saddr, sizeof(saddr)) < 0)
    report("connect", 1);

  /* Write some stuff and read the echoes. */
  puts("Connect to server, about to write some stuff...");
  int i;
  for (i = 0; i < ConversationLen; i++) {
    if (write(sockfd, books[i], strlen(books[i])) > 0) {
      /* get confirmation echoed from server and print */
      char buffer[BuffSize + 1];
      memset(buffer, '\0', sizeof(buffer));
      if (read(sockfd, buffer, sizeof(buffer)) > 0)
        puts(buffer);
    }
  }
  puts("Client done, about to exit...");
  close(sockfd); /* close the connection */
  return 0;
}

Der Setup-Code des Client-Programms ähnelt dem des Servers. Der Hauptunterschied zwischen den beiden besteht darin, dass der Client weder zuhört noch akzeptiert, sondern stattdessen eine Verbindung herstellt:

if (connect(sockfd, (struct sockaddr*) &saddr, sizeof(saddr)) < 0)

Die Verbindung Aufruf kann aus mehreren Gründen fehlschlagen; Beispielsweise hat der Client die falsche Serveradresse oder es sind bereits zu viele Clients mit dem Server verbunden. Wenn die Verbindung Operation erfolgreich ist, schreibt der Client Anforderungen und liest dann die zurückgesendeten Antworten in einem for Schleife. Nach der Konversation werden sowohl der Server als auch der Client geschlossen dem Lese-/Schreib-Socket, obwohl eine Schließoperation auf beiden Seiten ausreicht, um die Verbindung zu schließen. Der Client wird danach beendet, aber wie bereits erwähnt, bleibt der Server für den Geschäftsbetrieb geöffnet.

Das Socket-Beispiel, bei dem Anforderungsnachrichten an den Client zurückgesendet werden, deutet auf die Möglichkeiten beliebig umfangreicher Konversationen zwischen dem Server und dem Client hin. Vielleicht ist dies der Hauptvorteil von Steckdosen. Auf modernen Systemen ist es üblich, dass Client-Anwendungen (z. B. ein Datenbank-Client) über einen Socket mit einem Server kommunizieren. Wie bereits erwähnt, unterscheiden sich lokale IPC-Sockets und Netzwerk-Sockets nur in einigen Implementierungsdetails; Im Allgemeinen haben IPC-Sockets einen geringeren Overhead und eine bessere Leistung. Die Kommunikations-API ist im Wesentlichen für beide gleich.

Signale

Ein Signal unterbricht ein laufendes Programm und kommuniziert in diesem Sinne mit ihm. Die meisten Signale können mit SIGSTOP entweder ignoriert (blockiert) oder behandelt werden (über einen bestimmten Code). (Pause) und SIGKILL (sofort beenden) als die zwei bemerkenswerten Ausnahmen. Symbolische Konstanten wie SIGKILL haben ganzzahlige Werte, in diesem Fall 9.

Signale können in der Benutzerinteraktion entstehen. Beispiel:Ein Benutzer drückt Strg+C von der Befehlszeile, um ein Programm zu beenden, das von der Befehlszeile gestartet wurde; Strg+C generiert ein SIGTERM Signal. SIGTERM für beenden , im Gegensatz zu SIGKILL , können entweder blockiert oder behandelt werden. Ein Prozess kann auch einem anderen signalisieren, wodurch Signale zu einem IPC-Mechanismus werden.

Überlegen Sie, wie eine Multi-Processing-Anwendung wie der Nginx-Webserver ordnungsgemäß von einem anderen Prozess heruntergefahren werden könnte. Das töten Funktion:

int kill(pid_t pid, int signum); /* declaration */

kann von einem Prozess verwendet werden, um einen anderen Prozess oder eine Gruppe von Prozessen zu beenden. Wenn das erste Argument für die Funktion kill ist größer als Null ist, wird dieses Argument als PID behandelt (Prozess-ID) des Zielprozesses; wenn das Argument Null ist, identifiziert das Argument die Gruppe von Prozessen, zu der der Signalsender gehört.

Das zweite Argument zum töten ist entweder eine Standardsignalnummer (z. B. SIGTERM oder SIGKILL ) oder 0, wodurch der Anruf signalisiert wird eine Abfrage, ob die pid im ersten Argument ist in der Tat gültig. Das ordnungsgemäße Herunterfahren einer Multi-Processing-Anwendung könnte somit durch Senden eines terminate erreicht werden Signal – ein Ruf zum Töten Funktion mit SIGTERM als zweites Argument – ​​an die Gruppe von Prozessen, aus denen die Anwendung besteht. (Der Nginx-Masterprozess könnte die Worker-Prozesse mit einem Aufruf von kill beenden und sich dann selbst beenden.) Der Kill -Funktion beherbergt, wie so viele Bibliotheksfunktionen, Leistung und Flexibilität in einer einfachen Aufrufsyntax.

Beispiel 3. Das ordnungsgemäße Herunterfahren eines Multiprozessorsystems

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

void graceful(int signum) {
  printf("\tChild confirming received signal: %i\n", signum);
  puts("\tChild about to terminate gracefully...");
  sleep(1);
  puts("\tChild terminating now...");
  _exit(0); /* fast-track notification of parent */
}

void set_handler() {
  struct sigaction current;
  sigemptyset(&current.sa_mask);         /* clear the signal set */
  current.sa_flags = 0;                  /* enables setting sa_handler, not sa_action */
  current.sa_handler = graceful;         /* specify a handler */
  sigaction(SIGTERM, &current, NULL);    /* register the handler */
}

void child_code() {
  set_handler();

  while (1) {   /** loop until interrupted **/
    sleep(1);
    puts("\tChild just woke up, but going back to sleep.");
  }
}

void parent_code(pid_t cpid) {
  puts("Parent sleeping for a time...");
  sleep(5);

  /* Try to terminate child. */
  if (-1 == kill(cpid, SIGTERM)) {
    perror("kill");
    exit(-1);
  }
  wait(NULL); /** wait for child to terminate **/
  puts("My child terminated, about to exit myself...");
}

int main() {
  pid_t pid = fork();
  if (pid < 0) {
    perror("fork");
    return -1; /* error */
  }
  if (0 == pid)
    child_code();
  else
    parent_code(pid);
  return 0;  /* normal */
}

Die Abschaltung Das obige Programm simuliert das ordnungsgemäße Herunterfahren eines Multiprozessorsystems, in diesem Fall eines einfachen Systems, das aus einem übergeordneten Prozess und einem einzelnen untergeordneten Prozess besteht. Die Simulation funktioniert wie folgt:

  • Der Elternprozess versucht, einen Kindprozess zu forken. Wenn der Fork erfolgreich ist, führt jeder Prozess seinen eigenen Code aus:Das Kind führt die Funktion child_code aus , und der Elternteil führt die Funktion parent_code aus .
  • Der untergeordnete Prozess geht in eine potenzielle Endlosschleife über, in der das Kind für eine Sekunde schläft, eine Nachricht druckt, wieder schläft und so weiter. Es ist genau ein SIGTERM Signal vom Elternteil, das das Kind veranlasst, die Callback-Funktion zur Signalbehandlung graceful auszuführen . Das Signal unterbricht somit den untergeordneten Prozess aus seiner Schleife und richtet die ordnungsgemäße Beendigung sowohl des untergeordneten als auch des übergeordneten Prozesses ein. Das Kind gibt vor dem Beenden eine Nachricht aus.
  • Der Elternprozess ruht nach dem Verzweigen des Kindes fünf Sekunden lang, damit das Kind eine Weile ausgeführt werden kann; Natürlich schläft das Kind in dieser Simulation meistens. Der Elternteil ruft dann den Kill Funktion mit SIGTERM wartet als zweites Argument auf die Beendigung des untergeordneten Elements und beendet sich dann.

Hier ist die Ausgabe eines Beispiellaufs:

% ./shutdown
Parent sleeping for a time...
        Child just woke up, but going back to sleep.
        Child just woke up, but going back to sleep.
        Child just woke up, but going back to sleep.
        Child just woke up, but going back to sleep.
        Child confirming received signal: 15  ## SIGTERM is 15
        Child about to terminate gracefully...
        Child terminating now...
My child terminated, about to exit myself...

Für die Signalbehandlung verwendet das Beispiel die sigaction Bibliotheksfunktion (POSIX empfohlen) anstelle des alten Signals Funktion, die Portabilitätsprobleme hat. Hier sind die Codesegmente von Hauptinteresse:

  • Falls der Aufruf fork wird erfolgreich ist, führt der Elternteil den parent_code aus Funktion und das Kind führt den child_code aus Funktion. Der Elternteil wartet fünf Sekunden, bevor er dem Kind signalisiert:
    puts("Parent sleeping for a time...");
    sleep(5);
    if (-1 == kill(cpid, SIGTERM)) {
    ...

    Wenn die töten Anruf erfolgreich, der Elternteil wartet bei der Beendigung des Kindes, um zu verhindern, dass das Kind ein dauerhafter Zombie wird; nach dem Warten beendet der Elternteil.

  • Der child_code Funktion ruft zuerst set_handler auf und geht dann in seine potenziell unendliche Schlafschleife. Hier ist der set_handler Funktion zur Überprüfung:
    void set_handler() {
      struct sigaction current;            /* current setup */
      sigemptyset(&current.sa_mask);       /* clear the signal set */
      current.sa_flags = 0;                /* for setting sa_handler, not sa_action */
      current.sa_handler = graceful;       /* specify a handler */
      sigaction(SIGTERM, &current, NULL);  /* register the handler */
    }

    Die ersten drei Zeilen sind Vorbereitung. Die vierte Anweisung setzt den Handler auf die Funktion graceful , das vor dem Aufruf von _exit einige Meldungen ausgibt zu beenden. Die fünfte und letzte Anweisung registriert dann den Handler beim System durch den Aufruf von sigaction . Das erste Argument für sigaction ist SIGTERM für beenden , die zweite ist die aktuelle sigaction setup und das letzte Argument (NULL in diesem Fall) kann verwendet werden, um eine vorherige Sigaction zu speichern einrichten, vielleicht für spätere Verwendung.

Die Verwendung von Signalen für IPC ist zwar ein minimalistischer Ansatz, aber ein altbewährter noch dazu. IPC Through Signals gehört eindeutig in die IPC Toolbox.

Zum Abschluss dieser Serie

Diese drei Artikel auf IPC haben die folgenden Mechanismen anhand von Codebeispielen behandelt:

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

Selbst heute, wo Thread-zentrierte Sprachen wie Java, C# und Go so populär geworden sind, bleibt IPC attraktiv, weil Parallelität durch Multi-Processing einen offensichtlichen Vorteil gegenüber Multi-Threading hat:Jeder Prozess hat standardmäßig seinen eigenen Adressraum , die speicherbasierte Race-Bedingungen bei der Mehrfachverarbeitung ausschließt, es sei denn, der IPC-Mechanismus des gemeinsam genutzten Speichers wird ins Spiel gebracht. (Shared Memory muss sowohl bei Multi-Processing als auch Multi-Threading für sichere Parallelität gesperrt sein.) Jeder, der auch nur ein elementares Multi-Threading-Programm mit Kommunikation über Shared-Variablen geschrieben hat, weiß, wie herausfordernd es sein kann, Thread-sicher und dennoch klar zu schreiben. effizienter Code. Multi-Processing mit Single-Threaded-Prozessen bleibt eine praktikable – in der Tat ziemlich ansprechende – Methode, um die Vorteile heutiger Multiprozessor-Maschinen ohne das inhärente Risiko speicherbasierter Race-Conditions zu nutzen.

Auf die Frage, welcher der IPC-Mechanismen der beste ist, gibt es natürlich keine einfache Antwort. Jeder beinhaltet einen für die Programmierung typischen Kompromiss:Einfachheit versus Funktionalität. Signale sind zum Beispiel ein relativ einfacher IPC-Mechanismus, unterstützen jedoch keine reichhaltigen Konversationen zwischen Prozessen. Wenn eine solche Konvertierung erforderlich ist, ist eine der anderen Optionen besser geeignet. Freigegebene Dateien mit Sperren sind ziemlich einfach, aber freigegebene Dateien funktionieren möglicherweise nicht gut genug, wenn Prozesse massive Datenströme gemeinsam nutzen müssen. Pipes oder sogar Sockets mit komplizierteren APIs sind möglicherweise die bessere Wahl. Lassen Sie sich von dem vorliegenden Problem bei der Auswahl leiten.

Obwohl der Beispielcode (verfügbar auf meiner Website) vollständig in C ist, bieten andere Programmiersprachen oft dünne Hüllen um diese IPC-Mechanismen. Ich hoffe, die Codebeispiele sind kurz und einfach genug, um Sie zum Experimentieren anzuregen.


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

  2. Kommunikation zwischen Prozessen in Linux:Verwendung von Pipes und Nachrichtenwarteschlangen

  3. Kommunikation zwischen Prozessen in Linux:Shared Storage

  4. Überwachen Sie den Linux-Server mit Prometheus und Grafana

  5. Überwachen Sie Linux-Server mit Prometheus und Grafana

ReaR:Sichern und stellen Sie Ihren Linux-Server vertrauensvoll wieder her

So installieren und konfigurieren Sie einen NFS-Server auf einem Linux-System

Jenkins Server unter Linux:Ein kostenloser und Open-Source-Automatisierungsserver

4 einfache Schritte zum Installieren und Konfigurieren von VMware Server 2 unter Linux

So installieren und konfigurieren Sie den DNS-Server unter Linux

Die 20 besten Mailserver-Software und -Lösungen für Linux