TCPまたはローカルUNIXソケットでの接続を受け入れ、簡単なコマンドを読み取り、コマンドに応じて応答を送信する小さなサーバープログラムがあります。問題は、クライアントが時々答えに興味を持たず、早期に終了する可能性があるため、そのソケットに書き込むとSIGPIPEが発生し、サーバーがクラッシュすることです。ここでクラッシュを防ぐためのベストプラクティスは何ですか?行の反対側がまだ読み取り中かどうかを確認する方法はありますか? (select()は常にソケットが書き込み可能であると言うので、ここでは動作しないようです)。または、ハンドラでSIGPIPEをキャッチして無視する必要がありますか?
通常、SIGPIPE
を無視して、コード内でエラーを直接処理します。これは、Cのシグナルハンドラには、実行できることに関して多くの制限があるためです。
これを行う最も移植性の高い方法は、SIGPIPE
ハンドラーをSIG_IGN
に設定することです。これにより、ソケットまたはパイプの書き込みがSIGPIPE
シグナルを発生させなくなります。
SIGPIPE
信号を無視するには、次のコードを使用します。
signal(SIGPIPE, SIG_IGN);
send()
呼び出しを使用している場合、別のオプションはMSG_NOSIGNAL
オプションを使用することです。これにより、呼び出しごとにSIGPIPE
の動作がオフになります。すべてのオペレーティングシステムがMSG_NOSIGNAL
フラグをサポートしているわけではないことに注意してください。
最後に、一部のオペレーティングシステムでsetsockopt()
を使用して設定できるSO_SIGNOPIPE
ソケットフラグを検討することもできます。これにより、設定されているソケットへの書き込みによってSIGPIPE
が発生することを防ぎます。
別の方法は、ソケットを変更して、write()でSIGPIPEを生成しないようにすることです。これは、SIGPIPEのグローバルシグナルハンドラが不要なライブラリでより便利です。
ほとんどのBSDベース(MacOS、FreeBSD ...)システムでは(C/C++を使用していると仮定して)、次の方法でこれを行うことができます。
int set = 1;
setsockopt(sd, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int));
これにより、SIGPIPEシグナルが生成される代わりに、EPIPEが返されます。
私はパーティーにとても遅れていますが、SO_NOSIGPIPE
は移植性がなく、あなたのシステムでは動作しないかもしれません(BSDのようです)。
たとえば、SO_NOSIGPIPE
のないLinuxシステムを使用している場合、send(2)呼び出しでMSG_NOSIGNAL
フラグを設定するのが良い方法です。
write(...)
をsend(...,MSG_NOSIGNAL)
に置き換える例( nobar のコメントを参照)
char buf[888];
//write( sockfd, buf, sizeof(buf) );
send( sockfd, buf, sizeof(buf), MSG_NOSIGNAL );
これで post SO_NOSIGPIPEもMSG_NOSIGNALも利用できない場合のSolarisの場合の可能な解決策を説明しました。
代わりに、ライブラリコードを実行する現在のスレッドでSIGPIPEを一時的に抑制する必要があります。これを行う方法は次のとおりです。SIGPIPEを抑制するには、まず保留中かどうかを確認します。存在する場合、これはこのスレッドでブロックされていることを意味し、何もする必要はありません。ライブラリが追加のSIGPIPEを生成する場合、保留中のSIGPIPEとマージされますが、これは何もしません。 SIGPIPEが保留されていない場合、このスレッドでブロックし、すでにブロックされているかどうかも確認します。その後、書き込みを自由に実行できます。 SIGPIPEを元の状態に復元する場合、次のことを行います。SIGPIPEが元々保留中だった場合、何もしません。それ以外の場合は、現在保留中であるかどうかを確認します。発生する場合(つまり、outアクションが1つまたは複数のSIGPIPEを生成したことを意味します)、このスレッドで待機し、保留状態をクリアします(これを行うには、タイムアウトなしでsigtimedwait()を使用します。これは、ブロックを回避するためです。悪意のあるユーザーがプロセス全体に手動でSIGPIPEを送信したシナリオ:この場合、保留中になりますが、待機する変更が行われる前に他のスレッドが処理する場合があります)。保留ステータスをクリアした後、このスレッドでSIGPIPEのブロックを解除しますが、元々ブロックされていなかった場合のみです。
https://github.com/kroki/XProbes/blob/1447f3d93b6dbf273919af15e59f35cca58fcc23/src/libxprobes.c#L156 のコード例
通常は、グローバルなシグナルイベントハンドラーではなく、ローカルでエラーを処理するのが最善です。ローカルで何が起こっているのか、どのような対策を講じるべきかについて、より多くのコンテキストが得られるからです。
私のアプリの1つに外部のアクセサリと通信できるようにする通信レイヤーがあります。書き込みエラーが発生した場合、通信レイヤーで例外をスローし、try catchブロックまでバブルさせてそこで処理します。
ローカルで処理できるようにSIGPIPEシグナルを無視するコードは次のとおりです。
// We expect write failures to occur but we want to handle them where
// the error occurs rather than in a SIGPIPE handler.
signal(SIGPIPE, SIG_IGN);
このコードはSIGPIPEシグナルの発生を防ぎますが、ソケットを使用しようとすると読み取り/書き込みエラーが発生するため、そのことを確認する必要があります。
パイプの遠端のプロセスが終了するのを防ぐことはできません。また、書き込みが完了する前に終了すると、SIGPIPEシグナルを受け取ります。シグナルをSIG_IGNすると、書き込みがエラーで返されます-そのエラーに注意して対応する必要があります。ハンドラーでシグナルをキャッチして無視するのは良い考えではありません-パイプが無効になったことに注意し、プログラムの動作を変更して、パイプに再び書き込まないようにする必要があります(シグナルが再び生成され、無視されるため)もう一度、もう一度試してみてください。プロセス全体がlong時間の間続き、多くのCPUパワーを浪費する可能性があります。
または、ハンドラでSIGPIPEをキャッチして無視する必要がありますか?
私はそれが正しいと信じています。相手がいつ記述子を閉じたかを知りたいのですが、それがSIGPIPEがあなたに伝えていることです。
サム
ここでクラッシュを防ぐためのベストプラクティスは何ですか?
シグパイプをすべて無効にするか、エラーをキャッチして無視してください。
行の反対側がまだ読み取り中かどうかを確認する方法はありますか?
はい、select()を使用します。
select()は、ソケットが書き込み可能であると常に言っているため、ここでは機能しないようです。
readビットで選択する必要があります。 writeビットはおそらく無視できます。
遠端がファイルハンドルを閉じると、selectは読み取り可能なデータがあることを通知します。これを読んで読むと、0バイトが返されます。これは、ファイルハンドルが閉じられたことをOSが通知する方法です。
書き込みビットを無視できないのは、大量のボリュームを送信している場合だけです。また、もう一方の端がバックログになり、バッファがいっぱいになるリスクがあります。その場合、ファイルハンドルに書き込もうとすると、プログラム/スレッドがブロックまたは失敗する可能性があります。書き込み前にselectをテストすると、それから保護されますが、相手が正常であることやデータが到着することを保証するものではありません。
書くときだけでなく、close()からsigpipeを取得できることに注意してください。
閉じるは、バッファリングされたデータをフラッシュします。もう一方の端がすでに閉じられている場合は、閉じることに失敗し、sigpipeを受け取ります。
バッファ付きTCPIPを使用している場合、書き込みが成功したということは、データが送信のためにキューに入れられたことを意味するだけで、送信されたことを意味しません。 closeを正常に呼び出すまで、データが送信されたことはわかりません。
Sigpipeは、何か問題が発生したことを伝えますが、何をすべきか、何をすべきかを伝えません。
Linuxマニュアルによると:
EPIPE接続指向のソケットでローカルエンドがシャットダウンされました。この場合、MSG_NOSIGNALが設定されていない限り、プロセスはSIGPIPEも受信します。
しかし、Ubuntu 12.04の場合は正しくありません。その場合のテストを作成し、常にSIGPIPEなしでEPIPEを受け取ります。同じ壊れたソケットに2回目に書き込もうとすると、SIGPIPEが生成されます。したがって、このシグナルが発生した場合、SIGPIPEを無視する必要はありません。これはプログラムの論理エラーを意味します。
最新のPOSIXシステム(Linuxなど)では、sigprocmask()
関数を使用できます。
#include <signal.h>
void block_signal(int signal_to_block /* i.e. SIGPIPE */ )
{
sigset_t set;
sigset_t old_state;
// get the current state
//
sigprocmask(SIG_BLOCK, NULL, &old_state);
// add signal_to_block to that existing state
//
set = old_state;
sigaddset(&set, signal_to_block);
// block that signal also
//
sigprocmask(SIG_BLOCK, &set, NULL);
// ... deal with old_state if required ...
}
以前の状態を後で復元する場合は、old_state
を安全な場所に保存してください。その関数を複数回呼び出す場合は、スタックを使用するか、最初または最後のold_state
...のみを保存するか、特定のブロックされた信号を削除する関数を用意する必要があります。
詳細については、 manページ を参照してください。