2012-10-04 16 views
21

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

Processing request for 21 
Writing to 21 
Closing 21 

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

のIPv4 32754237 TCP localhostを21U

、SERVER 8160のルート:9980->はlocalhost:47530(ESTABLISHED)のIPv4 32754228 TCP localhostを12U

CLIENT 17747ルート:47530->はlocalhost:9980(ESTABLISHED)

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

SERVER 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(最終)

ソケットアクションがある

SERVER:

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

CLIENT:

クライアント側は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); 

何も、単に基本的なカール接続を使用していません。ソケットが閉じられていると認識されないため、(libcurlの)tranfer.cでクライアントがハングします。それはサーバーからのより多くのデータを待っています。

物事私がこれまで試した:

シャットダウン近い

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

前には差が行われていない1秒

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

これらの中で強制的に閉じるようにSO_LINGERを設定します。どんなアイデアでも大歓迎です。

EDIT - これは、キューライブラリ内のスレッドセーフの問題であり、ソケットが複数のスレッドによって不適切に処理されてしまいました。ここで

+0

あなたは100%肯定的ですが、他のスレッドは、そのソケットで 'close'を呼び出すときにソケットを使用している可能性がありますか?ノンブロッキングの読み込みはどのように行いますか? –

+0

私はちょうどここにログインし、この問題を覚えていますか?私は後で、接続を渡すために使用されるキューにスレッドセーフティの問題があることを知りました。ここにバグはありませんでした。誤った情報を申し訳ありません。 – DavidMFrey

答えて

54

は、私はソケットを閉じるために、多くのUnixライクなシステム(例えばのSunOS 4、SGI IRIX、HP-UX 10.20、CentOSの5、Cygwinの)上で使用してきたいくつかのコードです:

int getSO_ERROR(int fd) { 
    int err = 1; 
    socklen_t len = sizeof err; 
    if (-1 == getsockopt(fd, SOL_SOCKET, SO_ERROR, (char *)&err, &len)) 
     FatalError("getSO_ERROR"); 
    if (err) 
     errno = err;    // set errno to the socket SO_ERROR 
    return err; 
} 

void closeSocket(int fd) {  // *not* the Windows closesocket() 
    if (fd >= 0) { 
     getSO_ERROR(fd); // first clear any errors, which can cause close to fail 
     if (shutdown(fd, SHUT_RDWR) < 0) // secondly, terminate the 'reliable' delivery 
     if (errno != ENOTCONN && errno != EINVAL) // SGI causes EINVAL 
      Perror("shutdown"); 
     if (close(fd) < 0) // finally call close() 
     Perror("close"); 
    } 
} 

しかし、上記はありませんバッファリングされた書き込みが送信されることを保証するものではありません。

正常終了:ソケットを閉じる方法を理解するのに約10年かかりました。しかし、もう10年の間、私はちょっと遅れてusleep(20000)と少し遅れて、書き込みバッファが閉じる前にフラッシュされたことを「確実に」保証しました。これは明らかにあまり巧妙ではありません。

  • ほとんどの場合、遅延が長すぎました。
  • 遅延が一部の時間が短すぎました - おそらく!
  • このようなSIGCHLDシグナルは、usleep()(通常はusleep()と呼ばれ、このケースを処理するためにはハック)に終わることがあります。
  • これが機能するかどうかは示されませんでした。しかし、これはおそらく重要なことではない:a)ハードリセットが完全にうまくいけば、そして/またはb)あなたはリンクの両側を制御できる。

しかし、適切なフラッシュを行うことは驚くほど困難です。明らかにSO_LINGERを使用していますではない行く方法;例えば:

そしてSIOCOUTQはLinux固有であるように思われます。

shutdown(fd, SHUT_WR)は、その名前に反して、そしておそらくman 2 shutdownとは逆の書き込みを停止しません。

このコードflushSocketBeforeClose()は、0バイトの読み取りが完了するまで、またはタイマーが終了するまで待機します。関数haveInput()は、select(2)の単純なラッパーで、最大1/100秒間ブロックするように設定されています。

bool haveInput(int fd, double timeout) { 
    int status; 
    fd_set fds; 
    struct timeval tv; 
    FD_ZERO(&fds); 
    FD_SET(fd, &fds); 
    tv.tv_sec = (long)timeout; // cast needed for C++ 
    tv.tv_usec = (long)((timeout - tv.tv_sec) * 1000000); // 'suseconds_t' 

    while (1) { 
     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"); 
     else if (errno != EINTR) 
     FatalError("select"); // tbd EBADF: man page "an error has occurred" 
    } 
} 

bool flushSocketBeforeClose(int fd, double timeout) { 
    const double start = getWallTimeEpoch(); 
    char discard[99]; 
    ASSERT(SHUT_WR == 1); 
    if (shutdown(fd, 1) != -1) 
     while (getWallTimeEpoch() < start + timeout) 
     while (haveInput(fd, 0.01)) // can block for 0.01 secs 
      if (!read(fd, discard, sizeof discard)) 
       return TRUE; // success! 
    return FALSE; 
} 

使用例:上記で

if (!flushSocketBeforeClose(fd, 2.0)) // can block for 2s 
     printf("Warning: Cannot gracefully close socket\n"); 
    closeSocket(fd); 

、私getWallTimeEpoch()time(),に似ており、Perror()perror().

編集のためのラッパーです:いくつかのコメント:

  • 私の最初の入場はちょっと恥ずかしいです。 OPとNemoは閉じる前に内部so_errorをクリアする必要があると挑戦しましたが、私は今これに関するリファレンスを見つけることができません。問題のシステムはHPUX 10.20でした。 connect()が失敗した後、close()を呼び出すだけで、システムが未解決のエラーを私に通知したいので、ファイル記述子が解放されませんでした。しかし、ほとんどの人のように、返り値close.をチェックすることに悩まされることはありませんでした。最終的には私は最終的にファイルディスクリプタ(ulimit -n),を使い果たしました。

  • (非常に小さな点)1人の解説者が、ハードコードされた数値の引数をshutdown()に、例えば、 SHUT_WR for 1。最も簡単な答えは、Windowsが異なる#define/enumを使用することです。 SD_SEND。多くの他の作家(例えば、Beej)は、多くのレガシーシステムと同様に定数を使用しています。

  • また、私のアプリケーションでは子どもに渡されることは決してありませんし、もっと重要なのは、抱擁された子どもが私に影響を与えたくないから、常に私のすべてのソケットにFD_CLOEXECを設定します。 CLOEXEC設定する

サンプルコード:

static void setFD_CLOEXEC(int fd) { 
     int status = fcntl(fd, F_GETFD, 0); 
     if (status >= 0) 
     status = fcntl(fd, F_SETFD, status | FD_CLOEXEC); 
     if (status < 0) 
     Perror("Error getting/setting socket FD_CLOEXEC flags"); 
    } 
+5

私はこれを2回投票できるといいですね。これは私が野生で見た正しく閉じたソケットの2番目のサンプルです。 – grieve

+1

'SO_ERROR'の' getsockopt() 'に対して+1します。 – alk

+0

@JosephQuinsey - 「エラー... close()が無視される原因」の参照がありますか?好ましくはPOSIX仕様からですか? – Nemo

0

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

GNU C library documentationは言う:

あなたはソケットを使用して終了したら、あなたは、単にclose

任意のエラーフラグをクリアするかにデータを待っについては何もしてそのファイル ディスクリプタを閉じることができます洗い流されるか、そのようなもの。

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

+0

この答えに傾いています。テストするために別のOSを取得するには、いくつかの作業が必要です。私は一度テストした後、これを再訪します。私はそれが質問に関連しているように@ニモからこのリンクを追加したいと思います。添付された応答が削除されました。 https://sites.google.com/site/michaelsafyan/software-engineering/checkforeintrwheninvokingclosethinkagain – DavidMFrey

+0

pthread条件ではなくセマフォを使用するようにスレッドセーフなキューを変更すると、わかりにくくなりました(とにかく自分自身)問題。 – DavidMFrey

+3

'エラーフラグをクリアしたり、データがフラッシュされるのを待つことなど何もありません。'おそらく、「データをフラッシュするのを待っています。 –

2

Joseph Quinseyからの偉大な答え。私はhaveInput機能に関するコメントがあります。あなたがあなたのセットに含まれていなかったfdをselectが返す可能性があることを疑問に思う。これは主要なOSバグIMHOになります。これは普通のアプリではなく、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の呼び出しが長く続くことを保つことができ、他のエラーに対しては失敗を宣言します。

ADDITION#1

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

関連する問題