Linuxネットワークプログラミング(マルチプロセスで多重化)

投稿者: | 2011年11月21日
Pocket

サーバ環境

製品名 OpenBlockS 600/R
CPU 600MHz(AMCC PowerPC 405EX)
メモリ 1GB(DDR2 SDRAM)
ストレージ 8GB(Compact Flash)

マルチプロセスによる多重化の特徴

シングルプロセス・シングルスレッド方式は、select()を使用して複数のディスクリプタを同時に見張り、クライアントからの新規接続や接続中クライアントからの受信など、記述子の変化があった場合に処理を進めることで、多重化を実現していました。

一方、今回紹介するマルチプロセス方式では、accept()にてクライアントからの接続を待ち、接続が来るたびに子プロセスを生成して送受信処理を子プロセスに任せ、自身(親プロセス)は再度accept()を実行して接続処理に専念するという方式になります。

マルチプロセスというと、シングルプロセスの応用に聞こえるため難しそうですが、fork()とシグナルハンドラを理解していれば、シングルプロセスのように込み入ったテクニックが不要な分シンプルで分かりやすいコードになります。その一方で、プロセス立ち上げにはそれなりの負荷がかかるというデメリットもあります。

※あらかじめいくつかの子プロセスを立ち上げておくプリフォークという方法もありますが、本ページではクライアントからの接続が来るたびに子プロセスを生成する基本的な方法についてのみ説明します。

前回取り上げた各方式のメリットとデメリットのうち、マルチプロセスについて再度以下に掲載します。

マルチプロセスのメリットとデメリット

メリット 時間のかかる処理がある場合に並列性を確保しやすい

プログラム異常時に他の処理に影響が出にくい

プログラムがシンプルで分かりやすい

各処理の終了時にリソースが確実に解放される

1プロセスのリソース制限を受けない

デメリット リソースを多く必要とする

子プロセス生成時の処理が重たい

送受信の連携が面倒

UNIX系OS以外では利用が難しい

終了処理の考慮が面倒

マルチプロセスのコード例

以下の例では、accept()で接続受付を行った後、fork()で子プロセスを生成します。

子プロセスは外部クライアントの接続処理を行わないので、ソケット(soc)をクローズして送受信処理(send_recv)のみに専念します(fork()==0)。一方、平行して親プロセス(pid>0)はアクセプトソケット(acc)を閉じ、ループの先頭に戻って再度acceptで接続を待ちます。その間、子プロセスが送受信処理を行っていたとしても接続が終了していたとしても、親プロセスの処理にはなんら影響を与えません。

子プロセスは送受信処理を終えると_exit()で終了します。その際に親プロセスにSIGCHLDが通知されるので、シグナルハンドラにてwait()で終了状態を得ます。(子プロセスはwait()しないで放置するとゾンビプロセスになってしまいます。また、実際はwaitpid()も実施すべきですがここでは割愛しています。)

それではコード例を載せます。実際はincludeも必要ですが割愛しています。

/* 子プロセス終了のシグナルハンドラ */
void sig_chld_handler(int sig) {
  int status;
  pid_t pid;
  pid = wait(&status);
  (void) fprintf(stderr, "sig_chld_handler:wait:pid=%d,status=%d\n",pid,status);
}

/* socket→bind→listenを実行する関数。詳細は割愛。 */
int server_sock(const char *portnm) {
    ...
}

/* 送受信処理を実施する関数。詳細は割愛。 */
int send_recv(int acc) {
    ...
}

void accept_loop (int soc) {
  char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
  struct sockaddr_storage from;
  int acc, status;
  pid_t pid;
  socklen_t len;

  for (;;) {
    /* サーバソケットレディ */
    len = (socklen_t) sizeof(from);

    /* 接続受付 */
    if ((acc = accept(soc, (struct sockaddr *) &from, &len)) == -1) {
      if(errno!=EINTR){
        perror("accept");
      }
    } else {
      (void) getnameinfo((struct sockaddr *) &from, len, hbuf, sizeof(hbuf), sbuf, sizeof(sbuf), NI_NUMERICHOST | NI_NUMERICSERV);
      (void) fprintf(stderr, "accept:%s:%s\n", hbuf, sbuf);

      /* 子プロセス */
      if ((pid = fork()) == 0) {
        /* サーバソケットクローズ */
        (void) close(soc);

        /* 送受信処理を実施 */
        send_recv(acc);

        /* アクセプトソケットクローズ */
        (void) close(acc);

        _exit(1);

      /* fork()成功:親プロセス */
      } else if (pid > 0) {
        /* アクセプトソケットクローズ */
        (void) close(acc);
        acc = -1;
        printf("oya\n");
        (void) close(acc);
        acc = -1;
        printf("oya\n");

      /* fork()失敗 */
      } else {
        /* アクセプトソケットクローズ */
        (void) close(acc);
        acc = -1;
      }
    }
  }
}

int main(int argc, char *argv[]) {
  int soc;

  /* 子プロセス終了時のシグナルハンドラを登録 */
  (void) signal(SIGCHLD, sig_chld_handler);

  /* サーバソケット準備(socket→bind→listenを実行する関数)。本題では無いため本関数は割愛 */
  if ((soc = server_sock(argv[1])) == -1) {
    (void) fprintf(stderr, "server_socket(%s):error\n", argv[1]);
    return (EX_UNAVAILABLE);
  }
  (void) fprintf(stderr, "ready for accept\n");

  accept_loop(soc);

  (void) close(soc);
  return (EX_OK);
}
Pocket

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です