ホーム 主筆 その他ソフト その他情報 Syuhitu.org English

Windows関連

スクリーンセーバー作成法

半透明ウインドウの性能

bootfont.bin

キャビネット形式

ウインドウスタイルをいじる

Java製ソフトをServiceに登録する

イベントログにメッセージを出力する

コントロールパネルにアイコンを追加する

スクリプトによる拡張1

スクリプトによる拡張2

ガジェットの作成

大容量メモリ

メモリ搭載量の下限に挑む

スパースファイルにする

表示されるアイコンの種類を調べてみた

メモリマップIOとエラー処理

ファイルを作る順番と速度の関係

Cryptography API: Next Generationを使う

Windows 10のアクセントカラー

iSCSIディスクにバックアップを取る

サーバプロセスを分離して実装する

サーバプロセスを分離して実装する - F#

レジストリに大量に書き込む

Solaris関連

OpenGL

Solaris設定

ディレクトリの読み込み

主筆プラグイン開発

マルチスレッドでの開発

door

音を出す

Blade100の正しい虐め方

パッケージの作成

画像入出力

BMPファイル

ICOファイル

ANIファイル

JPEGファイル

減色アルゴリズム

減色アルゴリズムの並列化

その他アルゴリズムなど

自由軸回転

Base64

文字列操作

CPU利用率の取得

正規表現ライブラリ

メタボールを作る

メタボールを作る2

正規表現とNFA・DFA

C言語の構文解析

液晶ディスプレイを解体してみた

iSCSIの理論と実装

単一フォルダにファイルを沢山作る

USB-HUBのカスケード接続

SafeIntの性能

VHDファイルのフォーマット

USBメモリに書き込み続けてみた

サーバプロセスを分離して実装する

2016年3月23日公開

タイトルだけでは何を言いたいのか分からないので補足する。

昔々、TCPのサーバはクライアントからの着信を受け入れる都度forkしていたのだという。しかしそれだとプロセス生成のコストが大きくて性能が劣化するので、プロセスではなくスレッドを作って処理を行うのが一般的なったのだと聞く。

プロセスで作る場合

スレッドで作る場合

しかしそうしていない例もある。例えばPostgreSQLvsftpdなどがそうだ。

これは、スレッドではなくプロセスで実装することによるメリットがあるためだと思われる。

信頼性

プロセスを分離していれば、万一何らかバグ等でプロセスが異常終了したとしても、全体が共倒れになることを避けることができる。

だが、スレッドで実装していた場合はそうはいかない。プロセスの異常終了は即ちシステムダウンを意味することになる。

セキュリティ

起動した子プロセスの権限を極限まで落とすことで、万一プロセスが乗っ取られたような場合においても、影響範囲を最低限に保つことができるようになるのだという。

スレッドの権限という概念もあるようだが、結局のところ同一プロセス内ではリソースを共有している以上、スレッド1つが悪意のユーザの支配下に入ればそのプロセス全体が乗っ取られることは避けられない。

このページで言いたいこと

話が長くなったが、このページでは、Windows用のTCPサーバでプロセスを分離するモデルを実現する方法について述べる。

性能の問題はあるにせよ設計上のメリットを優先したい場合もあるので、そのやり方について調べたことをまとめておく。

スレッドでの実装

実装例

ごく普通にサーバプロセスをマルチスレッドで実装してみるとこうなる。

#include "stdafx.h"
#include <process.h>
#include <WinSock2.h>
 
// クライアントに対してプロセスIDを応答する
void SendToClient( void* ptr )
{
  char buf[64];

  // クライアントと通信するためのSOCKETの値を取得する
  SOCKET SockCli = (SOCKET)ptr;

  // クライアントに応答する文字列(ここでは自分のプロセスID)を生成する
  _snprintf_s( buf, _countof( buf ), "%d\n", ::GetCurrentProcessId() );

  // 応答電文をクライアントに送信する
  // (親スレッドから指定されたSockCliの値は、
  //   当然のことながら子スレッドでも有効である)
  if ( send( SockCli, buf, strlen( buf ) + 1, 0 ) <= 0 )
    printf( "Failed to send data\n" );
  else
    printf( "Succeed to send data. Server PID = %s", buf );

  // 電文を送信し終われば、SOCKETは不要となる
  closesocket( SockCli );
}
 
// クライアントからの着信を待ち受ける
int _tmain(int argc, _TCHAR* argv[])
{
  WSADATA wsaData;
  struct sockaddr_in addr;
  SOCKET SockSvr;
  SOCKET SockCli;
 
  if ( WSAStartup( MAKEWORD( 2,0 ), &wsaData ) != 0 )
    return -1;
 
  SockSvr = socket( AF_INET, SOCK_STREAM, 0 );
  if ( SockSvr == INVALID_SOCKET )
    return -1;
 
  addr.sin_family = AF_INET;
  addr.sin_port = htons( 20000 );
  addr.sin_addr.S_un.S_addr = INADDR_ANY;
 
  if ( bind( SockSvr, (struct sockaddr *)&addr, sizeof(addr) ) != 0 )
    return -1;
 
  if ( listen( SockSvr, 10000 ) != 0 )
    return -1;
 
  while ( -1 ) {

    // クライアントからの着信を待ち受ける
    SockCli = accept( SockSvr, NULL, NULL );
    if ( INVALID_SOCKET == SockCli )
      continue;

    printf( "accept client connection.\n" );
 
    // クライアントの相手をするスレッドを生成する
    _beginthread( SendToClient, 0, (void*)SockCli );

    // SockCliに保持するSOCKETは、子スレッドでクローズされるので
    // 親スレッドでは何もしない。
  }
  WSACleanup();
 
  return 0;
}

概説

あまり述べるべきことはない。WinSock2を用いたごくありきたりなTCPのサーバ側プロセスの実装例だ。

簡単に流れだけ述べる。

  1. _tmainで開始されたスレッドは、winsock2関連のAPIを呼び出し初期化を行う。最後にaccept関数でクライアントからの接続を待ち受ける。
  2. クライアントからの接続を受け入れると、SendToClient関数で始まるスレッドを生成する。SendToClient関数は、クライアントに対して自身のプロセスIDを応答する。

実行例

ここではクライアントとしてtelnetを使用している。

まず、サーバを起動しても何も表示されない。accept関数で、クライアントからの接続を待ち合わせている状態のまま処理が停止されている。

クライアントとしてtelnetを用いて、127.0.0.1の20000ポートに接続してやる。

そうすると、サーバ側の処理が先に進み、別スレッドが生成されてプロセスIDを応答する。

telnetは結果を画面に表示する。サーバ側からコネクションが切断されるため、結果を表示した時点でtelnetのコマンドが終了される。

プロセスでの実装

実装例

クライアントが着信する都度新規にプロセスを生成するようにした場合はこうなる。

#include "stdafx.h"
#include <process.h>
#include <WinSock2.h>

// クライアントに対してプロセスIDを応答する
void SendToClient( void* ptr )
{
  WSADATA wsaData;
  char buf[64];

  // クライアントと通信するためのSOCKETの値を取得する
  SOCKET SockCli = (SOCKET)ptr;

  // 分かりにくいから、一応ここでメッセージを表示しおこうか 
  printf( "CHILD : Child process was started. PID = %d\n", ::GetCurrentProcessId() );

  // プロセスが別である以上、子プロセスは子プロセスとして
  // WinSockの初期化が必要になる。
  if ( WSAStartup( MAKEWORD( 2,0 ), &wsaData ) != 0 )
    return ;

  // クライアントに応答する文字列を生成する
  _snprintf_s( buf, _countof( buf ), "%d\n", ::GetCurrentProcessId() );

  // クライアントに応答電文を返す
  if ( send( SockCli, buf, strlen( buf ) + 1, 0 ) <= 0 )
    printf( "CHILD : Failed to send data\n" );
  else
    printf( "CHILD : Succeed to send data. Server PID = %s", buf );

  // 応答電文を返してしまえばSOCKETは不要になる
  closesocket( SockCli );
 
  WSACleanup();
}
 
// クライアントからの着信を待ち受ける
int _tmain(int argc, _TCHAR* argv[])
{
  WSADATA wsaData;
  struct sockaddr_in addr;
  SOCKET SockSvr;
  SOCKET SockCli;
 
  if ( argc > 1 ) {
    // 着信してきたクライアントの相手をするプロセスであると判断する
    SockCli = (SOCKET)_ttoi( argv[1] );
    SendToClient( (void*)SockCli );
    return 0;
  }
 
  ///////////////////////////////////////////////////////////////
  // 引数がない場合は着信を待ち受けるプロセスと判断する

  // 分かりにくいから、一応ここでメッセージを表示しおこうか
  printf( "PARENT : Parent process was started. PID = %d\n", ::GetCurrentProcessId() );
 
  if ( WSAStartup( MAKEWORD( 2,0 ), &wsaData ) != 0 )
    return -1;
 
  SockSvr = socket( AF_INET, SOCK_STREAM, 0 );
  if ( SockSvr == INVALID_SOCKET )
    return -1;
 
  addr.sin_family = AF_INET;
  addr.sin_port = htons( 20000 );
  addr.sin_addr.S_un.S_addr = INADDR_ANY;
 
  if ( bind( SockSvr, (struct sockaddr *)&addr, sizeof(addr) ) != 0 )
    return -1;
 
  if ( listen( SockSvr, 10000 ) != 0 )
    return -1;
 
  while ( -1 ) {
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    TCHAR buf[64];
 
    // 受信待ち受け
    SockCli = accept( SockSvr, NULL, NULL );
    if ( INVALID_SOCKET == SockCli )
      continue;
    printf( "PARENT : accept client connection.\n" );
 
    // クライアントの相手をするプロセスを生成する
    memset( &si, 0, sizeof( si ) );
    si.cb = sizeof( si );
    si.hStdOutput = stdout;
    memset( &pi, 0, sizeof( pi ) );

    // クライアントと通信するためのSOCKETの値を
    // 無理やり文字列にして、子プロセス起動時の引数として指定する
    _stprintf_s( buf, _T( "PreforkTest.exe %d\n" ), SockCli );
 
    BOOL r = CreateProcess(
      _T( "D:\\MO\\VC++\\PreforkTest\\Debug\\PreforkTest.exe" ),
      buf,
      NULL,
      NULL,
      TRUE,  // 引数の継承を指定する
      NORMAL_PRIORITY_CLASS,
      NULL,
      NULL,
      &si,
      &pi
    );
    if ( r ) {
      printf(
        "PARENT : Succeed to create child process. PID=%d\n",
        pi.dwProcessId
      );
      CloseHandle( pi.hProcess );
      CloseHandle( pi.hThread );
    }
    else
      printf( "PARENT : Failed to create child process.\n" );

    // 親プロセス側では、これ以上クライアントと通信を行うことがないため、
    // 持っているハンドルをクローズする
    closesocket( SockCli );
  }
  WSACleanup();
 
  return 0;
}

概説

先と大きく異なる箇所を黄色く塗っている。

プロセスの処理は_tmain関数で始まる。

その直後に引数の有無で親プロセスとして起動されたのか、子プロセスとして起動されたのかの判断を行っている。これは親用のプログラムと子用のプログラムを分けずに1本で作っているからである。

主要な点は、着信待ち受け後にプロセスを生成していることである。その際、下記2つの事項に注意する必要がある。

  1. CreateProcess関数の第5引数にTRUEを指定して、ハンドルが継承されるよう指定する。
  2. 子プロセス起動時の引数として、ハンドルの値そのものを文字列化して指定する。

CreateProcess関数の5番目の引数bInheritHandlesにTRUEが指定された場合、「呼び出し側プロセス内で開いている、継承可能なハンドルは、新しいプロセスへ継承されます」と書かれている。

また、こうも書かれている「継承されたハンドルは、元のハンドルと同じ値とアクセス権を持ちます。」

要は、bInheritHandlesにTRUEを指定して、何らかの方法でaccept関数の戻り値を子プロセス側で指定してやれば、クライアントと通信ができるのである。

だったらということで、SOCKET型の値(とどのつまりは整数値)を文字列に変換して、プロセス起動時の引数として渡しているのである。

子プロセス側の処理は、スレッドで実装する場合と大して違いはない。起動時の第1引数(argv[1])を無理やりSOCKET型に変換して、それを使ってクライアントに応答を返している点だけが相違する。

実行例

とりあえず、上記を実行するとどうなるのかを示す。

まずサーバを起動すると、とりあえず自分のプロセスIDを表示してから、accept関数でクライアントからの接続を待ち合わせる。

先ほどと同様に、telnetで127.0.0.1の20000ポートに接続する。

接続されると以下の事象が起きる。

  1. 親プロセスで子プロセスを生成する。(上図ではPID=3192の親プロセスが、PID=5672の子プロセスを生成している)
  2. PID=5672の子プロセスの実行が開始される。
  3. 子プロセスが自分のプロセスID(つまり5672)を応答する。

ここで1つ注意すべきことは、クライアントからの接続直後に表示される「PARENT : accept client connection.」という表示の後で「PARENT : Succeed to create child process. PID=5672」のメッセージが表示されていることである。

これは、クライアントから接続された後で子プロセスが生成されることを意味している。

クライアント側のtelnetは応答された値「5672」を表示して処理を終了する。

子プロセスを事前に作っておく方法

プロセスの生成はコストが高い処理である。

そのため、クライアントから着信する都度その時点でプロセスを生成していると、クライアントへの応答がそれだけ遅延することになる。

ならば、あらかじめ暇なときにプロセスを生成しておくことはできないか?

Apacheのpreforkのようなことを、Windowsで実現できないか考えてみる。

ポイントは、クライアントからの着信を受け入れた親プロセスから、すでに起動している子プロセスに対してソケットのハンドルを引き渡してやることができるか否かである。

答えを先に書けば、それは可能である。WSADuplicateSocket関数と、WSASocket関数を使えば実現できる。

実装例

#include "stdafx.h"
#include <process.h>
#include <WinSock2.h>
#include <io.h>
#include <fcntl.h>

////////////////////////////////////////////////////////////////////
// 以下は子プロセス側の処理

// クライアントに対してプロセスIDを応答する
void SendToClient( int ReadFD )
{
  WSADATA wsaData;
  WSAPROTOCOL_INFO protoinfo;
  char buf[64] = { 0 };

  // 分かりにくいから、一応ここでメッセージを表示しておこうか
  printf( "CHILD : Child process was started. PID = %d\n", ::GetCurrentProcessId() );

  // プロセスが別である以上、子プロセスは子プロセスとして
  // WinSockの初期化が必要になる。
  if ( WSAStartup( MAKEWORD( 2,0 ), &wsaData ) != 0 )
    return ;
 
  // 親プロセスからソケットを取得する
  _read( ReadFD, &protoinfo, sizeof( protoinfo ) );
  SOCKET SockCli = WSASocket( AF_INET, SOCK_STREAM, IPPROTO_TCP, &protoinfo, 0, 0 );
  if ( INVALID_SOCKET == SockCli ) {
    printf( "CHILD : Failed to get socket\n" );
    return ;
  }

  // クライアントに、自分自身のプロセスIDを文字列化して応答する
  _snprintf_s( buf, _countof( buf ), "%d\n", ::GetCurrentProcessId() );

  if ( send( SockCli, buf, strlen( buf ) + 1, 0 ) <= 0 )
    printf( "CHILD : Failed to send data\n" );
  else
    printf( "CHILD : Succeed to send data. Server PID = %s", buf );

  closesocket( SockCli );
  WSACleanup();

  // そういえばここで、パイプの読み込み側の記述子をクローズしていなかったが、
  // まぁ、どうせ直後にプロセスが終了されるから、まぁいいか
}

////////////////////////////////////////////////////////////////////
// 以下は親プロセス側の処理

// プロセスを生成する
int GenChildProc( int *pWritePipe )
{
  STARTUPINFO si;
  PROCESS_INFORMATION pi;
  int vPipe[2];
  TCHAR buf[64];
 
  // 子プロセスを通信を行うためのパイプを生成する
  _pipe( vPipe, 256, _O_BINARY );

  // 書き込み側の記述子を呼び元に返す
  (*pWritePipe) = vPipe[1];
 
  memset( &si, 0, sizeof( si ) );
  si.cb = sizeof( si );
  si.hStdInput = stdin;
  si.hStdError = stderr;
  si.hStdOutput = stdout;
  memset( &pi, 0, sizeof( pi ) );
 
  // 読み込み側の記述子の値を引数として指定する
  _stprintf_s( buf, _T( "%d" ), vPipe[0] );

  // 子プロセスを起動する
  int ChildPHD = _tspawnl( 
    P_NOWAIT,
    _T( "D:\\MO\\VC++\\PreforkTest\\Debug\\PreforkTest.exe" ),
    _T( "PreforkTest.exe" ),
    buf,
    NULL
  );

  // 起動した子プロセスのプロセスIDを取得する。
  // (_tspawnlの戻り値はプロセスのハンドルであることに注意)
  int ChildPID = GetProcessId( (HANDLE)ChildPHD );
  printf( "PARENT : Succeed to create child process. PID=%d\n", ChildPID );

  // 親プロセスでは読み込み側の記述子は不要となる
  _close( vPipe[0] );
 
  return ChildPID;
}

////////////////////////////////////////////////////////////////////
// main関数
// 子プロセスもここから実行されるが、主に親プロセス側の処理を記述している

// クライアントからの着信を待ち受ける
int _tmain(int argc, _TCHAR* argv[])
{
  WSADATA wsaData;
  struct sockaddr_in addr;
  SOCKET SockSvr;
  SOCKET SockCli;
  int WriteFD;
 
  if ( argc > 1 ) {
    // 着信してきたクライアントの相手をするプロセスであると判断する
    SendToClient( _ttoi( argv[1] ) );
    return 0;
  }
 
  ///////////////////////////////////////////////////////////////
  // 引数がない場合は着信を待ち受けるプロセスと判断する

  // 分かりにくいから、一応ここでメッセージを表示しておこうか
  printf( "PARENT : Parent process was started. PID = %d\n", ::GetCurrentProcessId() );
 
  if ( WSAStartup( MAKEWORD( 2,0 ), &wsaData ) != 0 )
    return -1;
 
  SockSvr = socket( AF_INET, SOCK_STREAM, 0 );
  if ( SockSvr == INVALID_SOCKET )
    return -1;
 
  addr.sin_family = AF_INET;
  addr.sin_port = htons( 20000 );
  addr.sin_addr.S_un.S_addr = INADDR_ANY;
 
  if ( bind( SockSvr, (struct sockaddr *)&addr, sizeof( addr ) ) != 0 )
    return -1;
 
  if ( listen( SockSvr, 10000 ) != 0 )
    return -1;
 
  while ( -1 ) {
    WSAPROTOCOL_INFO protoinfo;
 
    // 事前にプロセスを生成しておく
    int ChildPID = GenChildProc( &WriteFD );
 
    // 受信待ち受け
    SockCli = accept( SockSvr, NULL, NULL );
    if ( INVALID_SOCKET == SockCli )
      continue;
    printf( "PARENT : accept client connection.\n" );
 
    // ソケットの引き渡しを行う
    memset( &protoinfo, 0, sizeof( protoinfo ) );
    if ( WSADuplicateSocket( SockCli, ChildPID, &protoinfo ) )
      printf( "Failed to duplicate socket.\n" );
    else
      _write( WriteFD, &protoinfo, sizeof( protoinfo ) );

    // 子プロセスにソケットの情報を送信したら、
    // パイプの終端(書き込み側の記述子)は不要となる
    _close( WriteFD );

    // クライアントとの通信用のソケットは、親プロセスでは不要となる
    closesocket( SockCli );
  }
  WSACleanup();
 
  return 0;
}

概説

残念なことに、さらにロジックが煩雑になっている。

図にするとこんな感じである。

着信時にプロセスを作る方法では、CreateProcess関数でハンドルを子プロセスに継承するよう指定するのがポイントだった。

だが今度はそうもいかない。子プロセスを作る時点ではまだクライアントと通信するためのSOCKETが存在しないのである。

だから、今までとは異なり下記のことを行わなければならない。

  1. 親プロセスと子プロセス間で通信ができるよう、パイプを構築する。
  2. 親プロセス側で、WSADuplicateSocket関数によりクライアントと通信するためのSOCKETの情報を取得、子プロセスにそれを送信する。
  3. 子プロセスでSOCKETの情報を受信し、クライアントと通信するためのSOCKETを取得する。

親と子の通信用にパイプを使用している都合から、子プロセスの起動時にパイプの読み込み側終端の記述子を引数として指定している。

これは「クライアントと通信するためのSOCKETの値」ではなく、「親プロセスと子プロセスで通信を行うためのパイプの終端」であることに注意が必要である。

実行例

実行例を以下に示す。

サーバ側のプロセス(PID=1188)を起動すると、まずいきなり子プロセス(PID=3528)を生成する。当然、起動された子プロセスは実行が開始される。

子プロセスは実行が開始された後、SOCKETの引き渡しで待機する。具体的には_read関数の実行でブロックされる。

今までと同じく、クライアントにtelnetを用いて、127.0.0.1の20000ポートに接続する。

クライアントが接続すると、親プロセスはSOCKETの情報を取得して、パイプを通じて子プロセスに送信する。具体的には_write関数でパイプに書き込む。

すると_read関数でブロックしていた子プロセスが実行を再開、クライアントと通信するためのSOCKETを取得し、クライアントに自身のPIDを応答する。

そのあと、というか理屈の上では同時に、親プロセスは次の子プロセス(PID=6492)を生成し、起動する。無論、PID=6492の子プロセスは実行が開始される。

クライアントであるtelnetは、受信した応答電文(すなわち、1つ目の子プロセスのPIDである3528)を表示し、処理を終了する。

2つ目のプログラムとの最大の違いは、クライアントが接続するよりも先に子プロセスの生成が行われていることである。この例では、クライアントが接続するよりも先に子プロセスのIDが分かっていることから、そのことが示されている。

なお、この例では子プロセスを事前に1つだけ生成しているが、当然複数個準備しておくことも可能である。

 


連絡先 - サイトマップ - 更新履歴
Copyright (C) 2000 - 2016 nabiki_t All Rights Reserved.