SIGPIPE を防ぐ方法 (または適切に処理する方法)
-
01-07-2019 - |
質問
TCP またはローカル UNIX ソケットで接続を受け入れ、単純なコマンドを読み取り、コマンドに応じて応答を送信する小さなサーバー プログラムがあります。問題は、クライアントが時々応答に興味がなく、早期に終了する可能性があるため、そのソケットに書き込むと SIGPIPE が発生し、サーバーがクラッシュすることです。ここでクラッシュを防ぐためのベストプラクティスは何ですか?回線の相手がまだ読み取り中かどうかを確認する方法はありますか?(select() は常にソケットが書き込み可能であると表示するため、ここでは機能しないようです)。それとも、ハンドラーで SIGPIPE をキャッチし、無視する必要がありますか?
解決
通常、無視したいのは、 SIGPIPE
コード内でエラーを直接処理します。これは、C のシグナル ハンドラーには実行できる内容に多くの制限があるためです。
これを行う最も移植性の高い方法は、 SIGPIPE
ハンドラーから SIG_IGN
. 。これにより、ソケットまたはパイプへの書き込みによるエラーの発生を防ぐことができます。 SIGPIPE
信号。
無視するには、 SIGPIPE
信号の場合は、次のコードを使用します。
signal(SIGPIPE, SIG_IGN);
を使用している場合は、 send()
を呼び出す場合、別のオプションは MSG_NOSIGNAL
オプションを選択すると、 SIGPIPE
通話ごとに動作をオフにします。すべてのオペレーティング システムがサポートしているわけではないことに注意してください。 MSG_NOSIGNAL
フラグ。
最後に、次のことも考慮するとよいでしょう。 SO_SIGNOPIPE
で設定できるソケットフラグ setsockopt()
一部のオペレーティング システムでは。これにより防止されます SIGPIPE
設定されているソケットへの書き込みだけが原因で発生することがなくなります。
他のヒント
もう 1 つの方法は、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 の問題のようです)。
たとえば、Linux システムを使用している場合に最適な代替手段です。 SO_NOSIGPIPE
を設定することになります MSG_NOSIGNAL
send(2) 呼び出しのフラグ。
置き換え例 write(...)
による send(...,MSG_NOSIGNAL)
(見る ノーバーさんのコメント)
char buf[888];
//write( sockfd, buf, sizeof(buf) );
send( sockfd, buf, sizeof(buf), MSG_NOSIGNAL );
この中で 役職 SO_NOSIGPIPE も MSG_NOSIGNAL も使用できない場合の Solaris の場合に考えられる解決策について説明しました。
代わりに、ライブラリ コードを実行する現在のスレッドで SIGPIPE を一時的に抑制する必要があります。これを行う方法は次のとおりです。SIGPIPE を抑制するには、まず保留中かどうかを確認します。そうなった場合は、このスレッドでブロックされていることを意味するため、何もする必要はありません。ライブラリが追加の SIGPIPE を生成した場合、それは保留中のものとマージされ、何も行われません。SIGPIPE が保留中でない場合は、このスレッドでブロックし、すでにブロックされているかどうかも確認します。その後、自由に書き込みを実行できるようになります。SIGPIPE を元の状態に復元する場合は、次の手順を実行します。SIGPIPE が元々保留中の場合は、何もしません。それ以外の場合は、現在保留中かどうかを確認します。存在する場合 (これは、out アクションが 1 つ以上の SIGPIPE を生成したことを意味します)、このスレッドでそれを待ち、保留ステータスをクリアします (これを行うには、タイムアウトをゼロにして sigtimedwait() を使用します。これは、悪意のあるユーザーが手動でプロセス全体に SIGPIPE を送信したシナリオでのブロックを回避するためです。この場合、保留中であることがわかりますが、変更を待つ前に他のスレッドが処理する可能性があります)。保留ステータスをクリアした後、このスレッドで SIGPIPE のブロックを解除しますが、これは元々ブロックされていない場合に限ります。
コード例は次のとおりです。 https://github.com/kroki/XProbes/blob/1447f3d93b6dbf273919af15e59f35cca58fcc23/src/libxprobes.c#L156
SIGPIPEをローカルで処理する
通常、グローバル信号イベント ハンドラーではなくローカルでエラーを処理することが最善です。ローカルでは、何が起こっているのか、どのような手段をとるべきかについてより多くのコンテキストが得られるからです。
アプリの 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 すると、書き込みはエラーで返されます。そのエラーに注意して対応する必要があります。ハンドラーでシグナルをキャッチして無視するだけでは得策ではありません。パイプが機能しなくなったことに注意し、パイプに再度書き込まないようにプログラムの動作を変更する必要があります (シグナルが再度生成され、無視されるため)もう一度やり直すと、プロセス全体がしばらく続く可能性があります 長さ 時間がかかり、CPU パワーを大量に浪費します)。
それとも、ハンドラーで SIGPIPE をキャッチし、無視する必要がありますか?
それは正しいと思います。もう一方の端がいつ記述子を閉じたかを知りたいとすると、それが SIGPIPE によって通知されます。
サム
Linux マニュアルには次のように書かれていました。
EPIPEローカルエンドは、接続指向のソケットでシャットダウンされています。この場合、MSG_NOSIGNALが設定されていない限り、プロセスもSIGPIPEを受信します。
しかし、Ubuntu 12.04 ではそれは正しくありません。その場合に備えてテストを作成しましたが、常に SIGPIPE なしで EPIPE を受け取ります。同じ壊れたソケットに 2 回目に書き込もうとすると、SIGPIPE が生成されます。したがって、この信号が発生した場合、プログラム内のロジック エラーを意味するため、SIGPIPE を無視する必要はありません。
ここでクラッシュを防ぐためのベストプラクティスは何ですか?
全員に従って sigpipe を無効にするか、エラーをキャッチして無視してください。
回線の相手がまだ読み取り中かどうかを確認する方法はありますか?
はい、select() を使用してください。
select() はソケットが書き込み可能であると常に言うため、ここでは機能しないようです。
で選択する必要があります 読む ビット。おそらく無視してもよいでしょう 書く ビット。
遠端がそのファイル ハンドルを閉じると、select は読み取る準備ができたデータがあることを通知します。これを読み込むと 0 バイトが返されます。これは、ファイル ハンドルが閉じられたことを OS が通知する方法です。
書き込みビットを無視できないのは、大量のデータを送信する場合だけで、相手側でバックログが発生し、バッファがいっぱいになる危険性があります。その場合、ファイル ハンドルに書き込もうとすると、プログラム/スレッドがブロックされたり失敗したりする可能性があります。書き込む前に選択をテストすると、そのような事態から保護できますが、相手が正常であることや、データが到着することは保証されません。
sigpipe は、書き込み時だけでなく close() からも取得できることに注意してください。
閉じると、バッファされたデータがすべてフラッシュされます。もう一方の端がすでに閉じられている場合、閉じることは失敗し、sigpipe を受け取ります。
バッファー付き TCPIP を使用している場合、書き込みが成功したということは、データが送信キューに入れられたことを意味するだけで、データが送信されたことを意味するわけではありません。close の呼び出しが成功するまで、データが送信されたかどうかはわかりません。
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
...あるいは、特定のブロックされた信号を除去する機能があるかもしれません。
詳細については、以下をお読みください。 マニュアルページ.