GNU/Linux >> Linux の 問題 >  >> Linux

Linuxでのプロセス間通信:ソケットとシグナル

これは、Linuxのプロセス間通信(IPC)に関するシリーズの3番目で最後の記事です。最初の記事は共有ストレージ(ファイルとメモリセグメント)を介したIPCに焦点を当て、2番目の記事は基本的なチャネル(パイプ(名前付きと名前なし)とメッセージキュー)について同じことを行います。この記事は、ハイエンドのIPC(ソケット)からローエンドのIPC(シグナル)に移行します。コード例は詳細を具体化します。

ソケット

パイプには2つのフレーバー(名前付きと名前なし)があるのと同じように、ソケットも同様です。 IPCソケット(別名Unixドメインソケット)は、同じ物理デバイス(ホスト)上のプロセスのチャネルベースの通信を可能にします )、一方、ネットワークソケットは、さまざまなホストで実行できるプロセスに対してこの種のIPCを有効にし、それによってネットワークを機能させます。ネットワークソケットは、TCP(伝送制御プロトコル)や下位レベルのUDP(ユーザーデータグラムプロトコル)などの基盤となるプロトコルからのサポートが必要です。

対照的に、IPCソケットは、通信をサポートするためにローカルシステムカーネルに依存しています。特に、IPCソケットは、ローカルファイルをソケットアドレスとして使用して通信します。これらの実装の違いにもかかわらず、IPCソケットとネットワークソケットAPIは本質的に同じです。次の例ではネットワークソケットについて説明しますが、サーバーはネットワークアドレス localhost を使用するため、サンプルサーバーとクライアントプログラムは同じマシンで実行できます。 (127.0.0.1)、ローカルマシン上のローカルマシンのアドレス。

ストリームとして構成されたソケット(以下で説明)は双方向であり、制御はクライアント/サーバーパターンに従います。クライアントはサーバーへの接続を試みて会話を開始し、サーバーは接続を受け入れようとします。すべてが機能する場合、クライアントからの要求とサーバーからの応答は、どちらかの端で閉じられるまでチャネルを通過し、それによって接続が切断されます。

[Linuxでのプロセス間通信の完全ガイドをダウンロード]

反復 サーバーは開発のみに適しており、接続されたクライアントを一度に1つずつ処理して完了します。最初のクライアントは最初から最後まで処理され、次に2番目のクライアントが処理されます。欠点は、特定のクライアントの処理がハングする可能性があり、その結果、待機しているすべてのクライアントが不足する可能性があることです。本番環境グレードのサーバーは並行になります 、通常、マルチプロセッシングとマルチスレッドを組み合わせて使用​​します。たとえば、デスクトップマシンのNginx Webサーバーには、クライアント要求を同時に処理できる4つのワーカープロセスのプールがあります。次のコード例は、反復サーバーを使用することにより、混乱を最小限に抑えます。したがって、並行性ではなく、基本的なAPIに焦点が当てられます。

最後に、ソケットAPIは、さまざまなPOSIXの改良が登場するにつれて、時間の経過とともに大幅に進化しました。サーバーとクライアントの現在のサンプルコードは意図的に単純ですが、ストリームベースのソケット接続の双方向性を強調しています。サーバーが端末で起動し、クライアントが別の端末で起動した場合の制御フローの概要は次のとおりです。

  • サーバーはクライアント接続を待機し、接続が成功すると、クライアントからバイトを読み取ります。
  • 双方向の会話を強調するために、サーバーはクライアントから受信したバイトをクライアントにエコーバックします。これらのバイトはASCII文字コードであり、本のタイトルを構成します。
  • クライアントは本のタイトルをサーバープロセスに書き込み、サーバーからエコーされた同じタイトルを読み取ります。サーバーとクライアントの両方がタイトルを画面に出力します。サーバーの出力は次のとおりです。基本的にクライアントの出力と同じです。
    Listening on port 9876 for clients...
    War and Peace
    Pride and Prejudice
    The Sound and the Fury
例1.ソケットサーバー
#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;
}

上記のサーバープログラムは、従来の4つの手順を実行して、クライアントリクエストの準備をし、個々のリクエストを受け入れます。各ステップは、サーバーが呼び出すシステム関数にちなんで名付けられています。

  1. ソケット(…) :ソケット接続のファイル記述子を取得します
  2. bind(…) :ソケットをサーバーのホスト上のアドレスにバインドします
  3. 聞く(…) :クライアントのリクエストをリッスンする
  4. accept(…) :特定のクライアントリクエストを受け入れる

ソケット 完全な呼び出しは次のとおりです:

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

最初の引数は、IPCソケットではなくネットワークソケットを指定します。 2番目の引数にはいくつかのオプションがありますが、 SOCK_STREAM およびSOCK_DGRAM (データグラム)が最も使用されている可能性があります。 ストリームベース ソケットは、失われたメッセージまたは変更されたメッセージが報告される信頼性の高いチャネルをサポートします。チャネルは双方向であり、一方から他方へのペイロードのサイズは任意です。対照的に、データグラムベースのソケットは信頼性がありません(ベストトライ )、単方向であり、固定サイズのペイロードが必要です。 ソケットの3番目の引数 プロトコルを指定します。ここで使用されているストリームベースのソケットの場合、ゼロが表す単一の選択肢があります。TCPです。 ソケットへの呼び出しが成功したため おなじみのファイル記述子を返します。ソケットは、たとえばローカルファイルと同じ構文で読み書きされます。

バインド 呼び出しは、ソケットAPIのさまざまな改良を反映しているため、最も複雑です。興味深いのは、この呼び出しがソケットをサーバーマシンのメモリアドレスにバインドすることです。ただし、聞く 呼び出しは簡単です:

if (listen(fd, MaxConnects) < 0)

最初の引数はソケットのファイル記述子であり、2番目の引数はサーバーが接続拒否を発行する前に収容できるクライアント接続の数を指定します。 接続しようとしたときにエラーが発生しました。 ( MaxConnects ヘッダーファイルsock.hで8に設定されています 。)

受け入れる 呼び出しのデフォルトはブロッキング待機です :サーバーは、クライアントが接続を試みて続行するまで何もしません。 受け入れる 関数は-1を返します エラーを示します。呼び出しが成功すると、読み取り/書き込み用の別のファイル記述子が返されます。 受け入れとは対照的なソケット acceptの最初の引数によって参照されるソケット 電話。サーバーは、読み取り/書き込みソケットを使用して、クライアントからの要求を読み取り、応答を書き戻します。受け入れソケットは、クライアント接続を受け入れるためにのみ使用されます。

設計上、サーバーは無期限に実行されます。したがって、サーバーは Ctrl + Cで終了できます。 コマンドラインから。

例2.ソケットクライアント
#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;
}

クライアントプログラムのセットアップコードは、サーバーのセットアップコードと似ています。 2つの主な違いは、クライアントがリッスンも受け入れもせず、代わりに接続することです。

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

接続 呼び出しはいくつかの理由で失敗する可能性があります。たとえば、クライアントのサーバーアドレスが間違っているか、サーバーに接続されているクライアントが多すぎます。 接続の場合 操作が成功すると、クライアントはリクエストを書き込み、エコーされた応答を forで読み取ります。 ループ。会話の後、サーバーとクライアントの両方が閉じる 読み取り/書き込みソケット。ただし、接続を閉じるには、どちらかの側で閉じる操作で十分です。その後、クライアントは終了しますが、前述のように、サーバーは引き続き営業しています。

要求メッセージがクライアントにエコーバックされるソケットの例は、サーバーとクライアント間の任意のリッチな会話の可能性を示唆しています。おそらくこれがソケットの最大の魅力です。最近のシステムでは、クライアントアプリケーション(データベースクライアントなど)がソケットを介してサーバーと通信するのが一般的です。前述のように、ローカルIPCソケットとネットワークソケットは、いくつかの実装の詳細のみが異なります。一般に、IPCソケットは、オーバーヘッドが低く、パフォーマンスが優れています。通信APIは基本的に両方で同じです。

信号

シグナル 実行中のプログラムに割り込み、この意味でプログラムと通信します。ほとんどのシグナルは、 SIGSTOP を使用して、無視(ブロック)または処理(指定されたコードを介して)できます。 (一時停止)および SIGKILL (すぐに終了)2つの注目すべき例外として。 SIGKILLなどの記号定数 整数値、この場合は9を持ちます。

信号は、ユーザーの操作で発生する可能性があります。たとえば、ユーザーが Ctrl + Cを押すと コマンドラインから開始されたプログラムを終了するには、コマンドラインから。 Ctrl + C SIGTERMを生成します 信号。 SIGTERM 終了の場合 、 SIGKILLとは異なり 、ブロックまたは処理できます。あるプロセスが別のプロセスに信号を送ることもできるため、信号をIPCメカニズムにします。

NginxWebサーバーなどのマルチプロセッシングアプリケーションが別のプロセスから正常にシャットダウンされる方法を検討してください。 キル 機能:

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

あるプロセスが別のプロセスまたはプロセスのグループを終了するために使用できます。関数の最初の引数がkillの場合 がゼロより大きい場合、この引数は pidとして扱われます。 (プロセスID)対象プロセスの;引数がゼロの場合、引数はシグナル送信者が属するプロセスのグループを識別します。

殺すへの2番目の議論 は標準の信号番号です(例: SIGTERM またはSIGKILL )または0。これはシグナルを呼び出します。 pidかどうかに関するクエリ 最初の引数は確かに有効です。したがって、マルチプロセッシングアプリケーションの正常なシャットダウンは、終了を送信することで実現できます。 シグナル-キルへの呼び出し SIGTERMで機能する 2番目の引数として-アプリケーションを構成するプロセスのグループへ。 (Nginxマスタープロセスは、 killを呼び出してワーカープロセスを終了する可能性があります その後、終了します。)キル 多くのライブラリ関数と同様に、関数は単純な呼び出し構文にパワーと柔軟性を備えています。

例3.マルチプロセッシングシステムの正常なシャットダウン
#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 */
}

シャットダウン 上記のプログラムは、マルチプロセッシングシステム(この場合は、親プロセスと単一の子プロセスで構成される単純なシステム)の正常なシャットダウンをシミュレートします。シミュレーションは次のように機能します。

  • 親プロセスは子をフォークしようとします。フォークが成功すると、各プロセスは独自のコードを実行します。子は関数 child_codeを実行します。 、そして親は関数 parent_codeを実行します 。
  • 子プロセスは潜在的に無限ループに入り、子は1秒間スリープし、メッセージを出力し、スリープに戻ります。まさにSIGTERM 子にシグナル処理コールバック関数を実行させる親からのシグナルgraceful 。したがって、シグナルは子プロセスをループから切り離し、子と親の両方の正常な終了を設定します。子は終了する前にメッセージを印刷します。
  • 親プロセスは、子をフォークした後、子がしばらく実行できるように5秒間スリープします。もちろん、このシミュレーションでは子供はほとんど眠ります。次に、親はキルを呼び出します SIGTERMで機能する 2番目の引数として、子が終了するのを待ってから終了します。

サンプル実行からの出力は次のとおりです。

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

信号処理の例では、 sigactionを使用しています 従来のシグナルではなくライブラリ関数(POSIXを推奨) 移植性の問題がある関数。主な関心のあるコードセグメントは次のとおりです。

  • フォークへの呼び出しの場合 成功すると、親は parent_codeを実行します 関数と子がchild_codeを実行します 働き。親は5秒間待ってから、子に通知します。
    puts("Parent sleeping for a time...");
    sleep(5);
    if (-1 == kill(cpid, SIGTERM)) {
    ...

    殺すの場合 呼び出しが成功すると、親は待機を実行します 子供が恒久的なゾンビになるのを防ぐための子供の終了時。待機後、親は終了します。

  • child_code 関数は最初にset_handlerを呼び出します そして、その潜在的に無限の睡眠ループに入ります。これがset_handlerです レビュー用の関数:
    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 */
    }

    最初の3行は準備です。 4番目のステートメントは、ハンドラーを関数 graceful に設定します 、 _exitを呼び出す前にいくつかのメッセージを出力します 終了します。次に、5番目で最後のステートメントは、 sigaction の呼び出しを通じて、ハンドラーをシステムに登録します。 。 sigactionへの最初の議論 SIGTERM 終了の場合 、2番目は現在の sigaction セットアップ、および最後の引数( NULL この場合)は、以前のシグニクションを保存するために使用できます セットアップ、おそらく後で使用するため。

IPCに信号を使用することは確かに最小限のアプローチですが、それは実証済みのアプローチです。信号を介したIPCは、明らかにIPCツールボックスに属しています。

このシリーズのまとめ

IPCに関するこれらの3つの記事では、コード例を通じて次のメカニズムについて説明しています。

  • 共有ファイル
  • 共有メモリ(セマフォ付き)
  • パイプ(名前付きおよび名前なし)
  • メッセージキュー
  • ソケット
  • 信号

Java、C#、Goなどのスレッド中心の言語が非常に普及した今日でも、マルチプロセッシングによる同時実行性にはマルチスレッドよりも明らかな利点があるため、IPCは魅力的です。デフォルトでは、すべてのプロセスに独自のアドレス空間があります。 、共有メモリのIPCメカニズムが機能しない限り、マルチプロセッシングでのメモリベースの競合状態を除外します。 (安全な同時実行のために、共有メモリはマルチプロセッシングとマルチスレッドの両方でロックする必要があります。)共有変数を介した通信を使用して基本的なマルチスレッドプログラムを作成したことがある人なら誰でも、スレッドセーフでありながら明確な書き込みがいかに難しいかを知っています。効率的なコード。シングルスレッドプロセスを使用したマルチプロセッシングは、メモリベースの競合状態の固有のリスクなしに、今日のマルチプロセッサマシンを活用するための実行可能な、実際には非常に魅力的な方法です。

もちろん、IPCメカニズムの中でどれが最適かという質問に対する簡単な答えはありません。それぞれに、プログラミングで一般的なトレードオフが含まれます。単純さと機能です。たとえば、シグナルは比較的単純なIPCメカニズムですが、プロセス間の豊富な会話をサポートしていません。そのような変換が必要な場合は、他の選択肢の1つがより適切です。ロック付きの共有ファイルはかなり簡単ですが、プロセスが大量のデータストリームを共有する必要がある場合、共有ファイルは十分に機能しない可能性があります。より複雑なAPIを備えたパイプまたはソケットでさえ、より良い選択かもしれません。手元の問題が選択の指針となります。

サンプルコード(私のWebサイトで入手可能)はすべてCですが、他のプログラミング言語では、これらのIPCメカニズムの薄いラッパーが提供されることがよくあります。コード例は短くて単純なので、実験することをお勧めします。


Linux
  1. PrometheusとGrafanaを使用してLinuxサーバーを監視する

  2. PrometheusとGrafanaでLinuxサーバーを監視する

  3. Linux ユーザーとパスワードを新しいサーバーにコピーする

  1. Linuxでのプロセス間通信のガイドを紹介します

  2. Linuxでのプロセス間通信:パイプとメッセージキューの使用

  3. Linuxでのプロセス間通信:共有ストレージ

  1. Linux に RabbitMQ サーバーと Erlang をインストールする方法

  2. Linux NTP サーバーとクライアントをインストールして構成する方法

  3. 管理 Linux サーバー