GNU/Linux >> Znalost Linux >  >> Linux

Meziprocesová komunikace v Linuxu:Sokety a signály

Toto je třetí a poslední článek ze série o meziprocesové komunikaci (IPC) v Linuxu. První článek se zaměřil na IPC prostřednictvím sdíleného úložiště (soubory a paměťové segmenty) a druhý článek dělá totéž pro základní kanály:roury (pojmenované i nepojmenované) a fronty zpráv. Tento článek se přesouvá z IPC na vyšší úrovni (zásuvky) na IPC na nižší úrovni (signály). Příklady kódu dokreslují podrobnosti.

Zásuvky

Stejně jako dýmky jsou ve dvou variantách (pojmenované a nepojmenované), tak i zásuvky. IPC sockety (neboli unixové doménové sockety) umožňují kanálovou komunikaci pro procesy na stejném fyzickém zařízení (hostitel ), zatímco síťové zásuvky umožňují tento druh IPC pro procesy, které mohou běžet na různých hostitelích, čímž se do hry vkládají sítě. Síťové sokety potřebují podporu základního protokolu, jako je TCP (Transmission Control Protocol) nebo UDP (User Datagram Protocol) nižší úrovně.

Naproti tomu sokety IPC se při podpoře komunikace spoléhají na místní jádro systému; zejména sokety IPC komunikují pomocí místního souboru jako adresy soketu. Navzdory těmto rozdílům v implementaci jsou rozhraní API IPC soketu a síťového soketu v podstatě stejná. Připravovaný příklad se týká síťových soketů, ale ukázkové serverové a klientské programy mohou běžet na stejném počítači, protože server používá síťovou adresu localhost (127.0.0.1), adresa místního počítače na místním počítači.

Sokety nakonfigurované jako streamy (diskutované níže) jsou obousměrné a ovládání se řídí vzorem klient/server:klient zahájí konverzaci pokusem o připojení k serveru, který se pokusí připojení přijmout. Pokud vše funguje, požadavky od klienta a odpovědi ze serveru pak mohou proudit kanálem, dokud není na obou koncích uzavřen, čímž se přeruší spojení.

[Stáhněte si kompletního průvodce meziprocesovou komunikací v Linuxu]

iterativní server, který je vhodný pouze pro vývoj, zpracovává připojené klienty jednoho po druhém až do dokončení:první klient je zpracováván od začátku do konce, pak druhý atd. Nevýhodou je, že manipulace s konkrétním klientem může viset, což pak vyhladoví všechny klienty čekající vzadu. Server produkční úrovně by byl souběžný , obvykle používající nějakou kombinaci multi-processingu a multi-threadingu. Například webový server Nginx na mém stolním počítači má fond čtyř pracovních procesů, které mohou zpracovávat požadavky klientů souběžně. Následující příklad kódu udržuje nepořádek na minimu pomocí iterativního serveru; zaměření tak zůstává na základní API, nikoli na souběžnost.

A konečně, soketové API se v průběhu času významně vyvíjelo, jak se objevila různá vylepšení POSIX. Aktuální ukázkový kód pro server a klienta je záměrně jednoduchý, ale podtrhuje obousměrný aspekt soketového připojení založeného na proudu. Zde je shrnutí toku řízení, přičemž server se spustil v terminálu a klient se spustil v samostatném terminálu:

  • Server čeká na připojení klientů a po úspěšném připojení načte bajty od klienta.
  • Za účelem podtržení obousměrné konverzace server odešle klientovi zpět bajty přijaté od klienta. Tyto bajty jsou kódy znaků ASCII, které tvoří názvy knih.
  • Klient zapisuje názvy knih do procesu serveru a poté čte stejné názvy, které se odrážejí od serveru. Server i klient vytisknou titulky na obrazovku. Zde je výstup serveru, v podstatě stejný jako výstup klienta:
    Listening on port 9876 for clients...
    War and Peace
    Pride and Prejudice
    The Sound and the Fury

Příklad 1. Server soketu

#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;
}

Výše uvedený serverový program provede klasické čtyři kroky, aby se připravil na požadavky klientů a poté přijal jednotlivé požadavky. Každý krok je pojmenován po systémové funkci, kterou server volá:

  1. zásuvka(…) :získat deskriptor souboru pro připojení soketu
  2. svázat(…) :svázat soket s adresou na hostiteli serveru
  3. poslouchejte(…) :poslouchat požadavky klientů
  4. přijmout(…) :přijmout konkrétní požadavek klienta

zásuvka celý hovor je:

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

První argument určuje síťový soket na rozdíl od soketu IPC. Pro druhý argument existuje několik možností, ale SOCK_STREAM a SOCK_DGRAM (datagram) jsou pravděpodobně nejpoužívanější. Na základě streamu socket podporuje spolehlivý kanál, ve kterém jsou hlášeny ztracené nebo změněné zprávy; kanál je obousměrný a užitečné zatížení z jedné strany na druhou může mít libovolnou velikost. Naproti tomu soket založený na datagramu je nespolehlivý (nejlepší pokus ), jednosměrný a vyžaduje užitečné zatížení pevné velikosti. Třetí argument pro zásuvku specifikuje protokol. Pro soket založený na streamu, který je zde ve hře, existuje jediná volba, kterou nula představuje:TCP. Protože úspěšné volání do zásuvky vrátí známý deskriptor souboru, soket se zapíše a přečte se stejnou syntaxí jako například místní soubor.

Vazba volání je nejsložitější, protože odráží různá vylepšení v rozhraní API soketu. Zajímavostí je, že toto volání spojuje soket s adresou paměti na serveru. Nicméně, poslouchejte hovor je přímočarý:

if (listen(fd, MaxConnects) < 0)

První argument je deskriptor souboru soketu a druhý určuje, kolik klientských připojení může být přizpůsobeno, než server vydá připojení odmítnuto chyba při pokusu o připojení. (MaxConnects je nastaveno na 8 v záhlaví souboru sock.h .)

Přijímám výchozí nastavení hovoru je blokovací čekání :server nedělá nic, dokud se klient nepokusí připojit a poté pokračuje. Přijímám funkce vrací -1 k označení chyby. Pokud je volání úspěšné, vrátí jiný deskriptor souboru – pro čtení/zápis na rozdíl od přijímání soket, na který odkazuje první argument v accept volání. Server používá soket pro čtení/zápis ke čtení požadavků od klienta a k zapisování odpovědí zpět. Přijímající soket se používá pouze k přijímání klientských připojení.

Podle návrhu běží server neomezeně dlouho. V souladu s tím lze server ukončit pomocí Ctrl+C z příkazového řádku.

Příklad 2. Klient soketu

#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;
}

Instalační kód klientského programu je podobný kódu serveru. Hlavní rozdíl mezi těmito dvěma je v tom, že klient neposlouchá ani nepřijímá, ale místo toho se připojuje:

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

Připojení hovor může selhat z několika důvodů; klient má například špatnou adresu serveru nebo je k serveru již připojeno příliš mnoho klientů. Pokud připojit operace úspěšná, klient zapíše požadavky a poté přečte odezvy v for smyčka. Po konverzaci se server i klient zavře soketu pro čtení/zápis, i když k uzavření spojení stačí operace zavření na obou stranách. Klient se poté ukončí, ale jak bylo uvedeno dříve, server zůstává otevřený pro podnikání.

Příklad soketu se zprávami požadavků odeslanými zpět klientovi naznačuje možnosti libovolně bohatých konverzací mezi serverem a klientem. Možná je to hlavní přitažlivost zásuvek. V moderních systémech je běžné, že klientské aplikace (např. databázový klient) komunikují se serverem prostřednictvím soketu. Jak již bylo zmíněno dříve, místní sokety IPC a síťové sokety se liší pouze v několika detailech implementace; obecně mají IPC zásuvky nižší režii a lepší výkon. Komunikační API je v podstatě pro oba stejné.

Signály

signál přeruší vykonávaný program a v tomto smyslu s ním komunikuje. Většinu signálů lze ignorovat (zablokovat) nebo zpracovat (pomocí určeného kódu) pomocí SIGSTOP (pauza) a SIGKILL (ukončit okamžitě) jako dvě významné výjimky. Symbolické konstanty jako SIGKILL mají celočíselné hodnoty, v tomto případě 9.

Signály mohou vznikat v interakci s uživatelem. Uživatel například stiskne Ctrl+C z příkazového řádku pro ukončení programu spuštěného z příkazového řádku; Ctrl+C vygeneruje SIGTERM signál. SIGTERM pro ukončit , na rozdíl od SIGKILL , lze buď zablokovat, nebo zpracovat. Jeden proces také může signalizovat druhému, čímž ze signálů vytvoří mechanismus IPC.

Zvažte, jak může být aplikace pro více zpracování, jako je webový server Nginx, elegantně vypnuta z jiného procesu. zabití funkce:

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

mohou být použity jedním procesem k ukončení jiného procesu nebo skupiny procesů. Pokud první argument funkce zabije je větší než nula, je tento argument považován za pid (ID procesu) cíleného procesu; pokud je argument nula, argument identifikuje skupinu procesů, do které patří odesílatel signálu.

Druhý argument k zabití je buď standardní číslo signálu (např. SIGTERM nebo SIGKILL ) nebo 0, čímž se volání signalizuje dotaz, zda pid v prvním argumentu skutečně platí. Půvabné vypnutí víceprocesorové aplikace by tedy mohlo být provedeno odesláním ukončení signál — volání k zabití funkce s SIGTERM jako druhý argument – ​​ke skupině procesů, které tvoří aplikaci. (Hlavní proces Nginx by mohl ukončit pracovní procesy voláním kill a poté se sám ukončí.) zabití Funkce, stejně jako mnoho funkcí knihovny, nabízí výkon a flexibilitu v jednoduché syntaxi vyvolání.

Příklad 3. Elegantní vypnutí systému s více procesy

#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 */
}

Vypnutí výše uvedený program simuluje elegantní vypnutí systému s více procesy, v tomto případě jednoduchého sestávajícího z nadřazeného procesu a jednoho podřízeného procesu. Simulace funguje následovně:

  • Rodičovský proces se pokouší rozdělit dítě. Pokud je rozvětvení úspěšné, každý proces spustí svůj vlastní kód:dítě spustí funkci child_code a rodič provede funkci parent_code .
  • Podřízený proces přejde do potenciálně nekonečné smyčky, ve které dítě na sekundu spí, vytiskne zprávu, vrátí se do spánku atd. Je to přesně SIGTERM signál od rodiče, který způsobí, že dítě provede funkci zpětného volání zpracování signálu ladné . Signál tak vytrhne podřízený proces ze smyčky a nastaví ladné ukončení dítěte i rodiče. Dítě před ukončením vytiskne zprávu.
  • Rodičovský proces se po rozvětvení dítěte na pět sekund uspí, takže dítě může chvíli provádět; samozřejmě dítě v této simulaci většinou spí. Rodič pak zavolá zabití funkce s SIGTERM jako druhý argument čeká, až dítě skončí, a poté skončí.

Zde je výstup z ukázkového běhu:

% ./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...

Pro zpracování signálu příklad používá sigaction funkce knihovny (doporučeno POSIX) spíše než starší signál funkce, která má problémy s přenositelností. Zde jsou hlavní části kódu:

  • Pokud se hovor rozvětví uspěje, rodič spustí parent_code a dítě spustí child_code funkce. Rodič čeká pět sekund, než dítěti signalizuje:
    puts("Parent sleeping for a time...");
    sleep(5);
    if (-1 == kill(cpid, SIGTERM)) {
    ...

    Pokud zabije hovor je úspěšný, rodič čeká o ukončení dítěte, aby se zabránilo tomu, že se dítě stane trvalou zombie; po čekání se rodič ukončí.

  • child_code funkce nejprve volá set_handler a pak jde do své potenciálně nekonečné spánkové smyčky. Zde je set_handler funkce pro kontrolu:
    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 */
    }

    První tři řádky jsou příprava. Čtvrtý příkaz nastaví handler na funkci graceful , která před voláním _exit vytiskne některé zprávy ukončit. Pátý a poslední příkaz pak zaregistruje handler do systému prostřednictvím volání sigaction . První argument pro sigaction je SIGTERM pro ukončit , druhá je aktuální sigaction setup a poslední argument (NULL v tomto případě) lze použít k uložení předchozí sigaction nastavení, možná pro pozdější použití.

Použití signálů pro IPC je skutečně minimalistický přístup, ale osvědčený. IPC prostřednictvím signálů jednoznačně patří do sady nástrojů IPC.

Uzavření této série

Tyto tři články o IPC pokryly následující mechanismy prostřednictvím příkladů kódu:

  • Sdílené soubory
  • Sdílená paměť (se semafory)
  • Trubky (pojmenované i nepojmenované)
  • Fronty zpráv
  • Zásuvky
  • Signály

Dokonce i dnes, kdy se jazyky zaměřené na vlákna, jako je Java, C# a Go staly tak populární, zůstává IPC přitažlivým, protože souběžnost prostřednictvím multiprocesingu má zjevnou výhodu oproti multi-threadingu:každý proces má ve výchozím nastavení svůj vlastní adresní prostor. , který vylučuje rasové podmínky založené na paměti ve víceprocesorovém zpracování, pokud není zapojen mechanismus IPC sdílené paměti. (Sdílená paměť musí být uzamčena v multiprocesingu i multi-threadingu pro bezpečný souběžný provoz.) Každý, kdo napsal byť jen elementární multi-threadingový program s komunikací přes sdílené proměnné, ví, jak náročné může být psaní vláknově bezpečné a přitom jasné, efektivní kód. Víceprocesorové zpracování s jednovláknovými procesy zůstává životaschopným – skutečně velmi přitažlivým – způsobem, jak využít výhody dnešních víceprocesorových strojů bez přirozeného rizika rasových podmínek založených na paměti.

Na otázku, který z mechanismů IPC je nejlepší, samozřejmě neexistuje jednoduchá odpověď. Každý zahrnuje kompromis typický pro programování:jednoduchost versus funkčnost. Například signály jsou relativně jednoduchým mechanismem IPC, ale nepodporují bohaté konverzace mezi procesy. Pokud je takový převod potřeba, pak je vhodnější jedna z dalších možností. Sdílené soubory s uzamčením jsou poměrně jednoduché, ale sdílené soubory nemusí fungovat dostatečně dobře, pokud procesy potřebují sdílet masivní datové toky; trubky nebo dokonce zásuvky s komplikovanějšími API mohou být lepší volbou. Nechte se při výběru řídit aktuálním problémem.

Ačkoli ukázkový kód (dostupný na mém webu) je celý v C, jiné programovací jazyky často poskytují tenké obaly kolem těchto mechanismů IPC. Příklady kódu jsou dostatečně krátké a jednoduché, doufám, že vás povzbudí k experimentování.


Linux
  1. Monitorujte linuxový server pomocí Prometheus a Grafana

  2. Monitorujte Linuxový server pomocí Prometheus a Grafana

  3. Nova-agent (Linux) a agent Rackspace (Windows)

  1. Představení průvodce meziprocesovou komunikací v Linuxu

  2. Jak nainstalovat RabbitMQ Server a Erlang na Linux

  3. Kopírování uživatelů a hesel Linuxu na nový server

  1. Meziprocesová komunikace v Linuxu:Použití kanálů a front zpráv

  2. Meziprocesová komunikace v Linuxu:Sdílené úložiště

  3. Jak nainstalovat CVS a vytvořit úložiště CVS na serveru Linux