web-dev-qa-db-ja.com

close()がソケットを適切に閉じていません

20個のスレッドを使用して、多数のリクエスト(1つのノードで最大500 /秒)を処理するマルチスレッドサーバー(スレッドプール)があります。着信接続を受け入れ、処理するハンドラスレッドのキューに入れるリスナースレッドがあります。応答の準備が整うと、スレッドはクライアントに書き込み、ソケットを閉じます。最近まで、テストクライアントプログラムは応答を読み取った後にランダムにハングし始めました。多くの掘り下げの後、サーバーからのclose()は実際にソケットを切断していないようです。ファイル記述子番号を使用してコードにデバッグ出力を追加し、このタイプの出力を取得しました。

Processing request for 21
Writing to 21
Closing 21

Close()の戻り値は0であるか、別のデバッグステートメントが出力されます。ハングしたクライアントでのこの出力の後、lsofは確立された接続を示しています。

サーバー8160ルート21u IPv4 32754237 TCP localhost:9980-> localhost:47530(ESTABLISHED)

CLIENT 17747 root 12u IPv4 32754228 TCP localhost:47530-> localhost:9980(ESTABLISHED)

サーバーがシャットダウンシーケンスをクライアントに送信しないように、クライアントが強制終了されるまでこの状態はハングし、サーバー側はクローズ待機状態のままになります。

サーバー8160ルート21u IPv4 32754237 TCP localhost:9980-> localhost:47530(CLOSE_WAIT)

また、クライアントにタイムアウトが指定されている場合、ハングする代わりにタイムアウトします。手動で実行することもできます

call close(21)

gdbからサーバーに接続すると、クライアントは切断されます。これは、おそらく50,000件のリクエストに1回発生しますが、長期間は発生しない可能性があります。

Linuxバージョン:2.6.21.7-2.fc8xen Centosバージョン:5.4(最終)

ソケットアクションは次のとおりです。

サーバ:

int client_socket; struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr);

while(true) {
  client_socket = accept(incoming_socket, (struct sockaddr *)&client_addr, &client_len);
  if (client_socket == -1)
    continue;
  /*  insert into queue here for threads to process  */
}

次に、スレッドはソケットを取得し、応答を作成します。

/*  get client_socket from queue  */

/*  processing request here  */

/*  now set to blocking for write; was previously set to non-blocking for reading  */
int flags = fcntl(client_socket, F_GETFL);
if (flags < 0)
  abort();
if (fcntl(client_socket, F_SETFL, flags|O_NONBLOCK) < 0)
  abort();

server_write(client_socket, response_buf, response_length);
server_close(client_socket);

server_writeおよびserver_close。

void server_write( int fd, char const *buf, ssize_t len ) {
    printf("Writing to %d\n", fd);
    while(len > 0) {
      ssize_t n = write(fd, buf, len);
      if(n <= 0)
        return;// I don't really care what error happened, we'll just drop the connection
      len -= n;
      buf += n;
    }
  }

void server_close( int fd ) {
    for(uint32_t i=0; i<10; i++) {
      int n = close(fd);
      if(!n) {//closed successfully                                                                                                                                   
        return;
      }
      usleep(100);
    }
    printf("Close failed for %d\n", fd);
  }

クライアント:

クライアント側はlibcurl v 7.27.0を使用しています

CURL *curl = curl_easy_init();
CURLcode res;
curl_easy_setopt( curl, CURLOPT_URL, url);
curl_easy_setopt( curl, CURLOPT_WRITEFUNCTION, write_callback );
curl_easy_setopt( curl, CURLOPT_WRITEDATA, write_tag );

res = curl_easy_perform(curl);

派手なものはなく、基本的なカール接続だけです。ソケットが閉じられていると認識されないため、クライアントはtranfer.c(libcurl内)でハングします。サーバーからの追加データを待っています。

私が今までに試したこと:

閉じる前にシャットダウン

shutdown(fd, SHUT_WR);                                                                                                                                            
char buf[64];                                                                                                                                                     
while(read(fd, buf, 64) > 0);                                                                                                                                         
/*  then close  */ 

1秒以内に強制的に閉じるようにSO_LINGERを設定する

struct linger l;
l.l_onoff = 1;
l.l_linger = 1;
if (setsockopt(client_socket, SOL_SOCKET, SO_LINGER, &l, sizeof(l)) == -1)
  abort();

これらは違いはありません。どんなアイデアでも大歓迎です。

編集-これは、最終的にキューライブラリ内のスレッド安全性の問題であり、複数のスレッドがソケットを不適切に処理する原因となりました。

23
DavidMFrey

ジョセフ・クインシーからのすばらしい答え。 haveInput関数に関するコメントがあります。 selectが、セットに含めなかったfdを返す可能性がどれほど高いか疑問に思います。これは、主要なOSのバグです。これは、通常のアプリではなく、select関数の単体テストを作成したかどうかを確認するものです。

if (!(status = select(fd + 1, &fds, 0, 0, &tv)))
   return FALSE;
else if (status > 0 && FD_ISSET(fd, &fds))
   return TRUE;
else if (status > 0)
   FatalError("I am confused"); // <--- fd unknown to function

私の他のコメントはEINTRの取り扱いに関するものです。理論的には、selectがEINTRを返し続けると、このエラーによりループが最初からやり直されるため、無限ループに陥ることがあります。非常に短いタイムアウト(0.01)を考えると、発生する可能性は非常に低いようです。ただし、これに対処する適切な方法は、呼び出し元(flushSocketBeforeClose)にエラーを返すことだと思います。呼び出し元は、タイムアウトが期限切れになっていない限りhaveInputを呼び出し続け、他のエラーの失敗を宣言できます。

追加#1

flushSocketBeforeCloseは、readがエラーを返す場合、すぐには終了しません。タイムアウトが期限切れになるまでループし続けます。すべてのエラーを予測するためにselect内のhaveInputに依存することはできません。 readには独自のエラーがあります(例:EIO)。

     while (haveInput(fd, 0.01)) 
        if (!read(fd, discard, sizeof discard)) <-- -1 does not end loop
           return TRUE; 
2
Philippe A.

これは、Linuxディストリビューションのバグのように思えます。

GNU Cライブラリのドキュメント のコメント:

ソケットの使用が終了したら、closeでファイル記述子を閉じることができます。

エラーフラグをクリアしたり、データがフラッシュされるのを待ったりするようなことは何もありません。

コードは問題ありません。 O/Sにバグがあります。

0
Nemo