SIGPIPEを防ぐ(または適切に処理する)方法


260

TCPまたはローカルUNIXソケットでの接続を受け入れ、単純なコマンドを読み取り、コマンドに応じて応答を送信する小さなサーバープログラムがあります。問題は、クライアントがときどき回答に関心を持たずに早く終了する可能性があるため、そのソケットに書き込むとSIGPIPEが発生し、サーバーがクラッシュすることです。ここでクラッシュを防ぐためのベストプラクティスは何ですか?行の反対側がまだ読んでいるかどうかを確認する方法はありますか?(select()は、ソケットが書き込み可能であると常に表示するため、ここでは機能しないようです)。または、ハンドラーでSIGPIPEをキャッチして無視する必要がありますか?

回答:


254

通常、を無視SIGPIPEして、コードでエラーを直接処理します。これは、Cのシグナルハンドラーが実行できる操作に多くの制限があるためです。

これを行う最もポータブルな方法は、SIGPIPEハンドラーをに設定することSIG_IGNです。これにより、ソケットまたはパイプの書き込みによってSIGPIPE信号が発生するのを防ぎます。

SIGPIPE信号を無視するには、次のコードを使用します。

signal(SIGPIPE, SIG_IGN);

send()通話を使用している場合、別のオプションは、通話ごとに動作をオフにするMSG_NOSIGNALオプションを使用するSIGPIPEことです。すべてのオペレーティングシステムがMSG_NOSIGNALフラグをサポートしているわけではないことに注意してください。

最後に、一部のオペレーティングシステムでSO_SIGNOPIPE設定できるソケットフラグを検討することもできsetsockopt()ます。これSIGPIPEにより、設定されているソケットへの書き込みだけが原因で発生するのを防ぎます。


75
これを明示的にするには:signal(SIGPIPE、SIG_IGN);
jhclark

5
これは、141(128 + 13:SIGPIPE)の終了戻りコードを受け取っている場合に必要になることがあります。SIG_IGNはシグナル無視ハンドラです。
velcrow

6
ソケットの場合、@ talashが言ったように、MSG_NOSIGNALでsend()を使用する方が本当に簡単です。
Pawel Veselov、2014年

3
プログラムが壊れたパイプ(私の場合はソケット)に書き込んだ場合、正確にはどうなりますか?SIG_IGNはプログラムにSIG_PIPEを無視させますが、その結果、send()は望ましくないことをしますか?
sudo 2014年

2
言及する価値があります。私は一度見つけたので、後でそれを忘れて混乱し、2回目に発見しました。デバッガー(gdbは知っています)がシグナル処理をオーバーライドするため、無視されたシグナルは無視されません!デバッガーの外でコードをテストして、SIGPIPEが発生しないことを確認します。stackoverflow.com/questions/6821469/...
ジェットスキーS型

155

もう1つの方法は、ソケットを変更して、write()でSIGPIPEを生成しないようにすることです。これは、SIGPIPEのグローバルシグナルハンドラーが不要なライブラリではより便利です。

ほとんどのBSDベースの(MacOS、FreeBSD ...)システム(C / C ++を使用している場合)では、次の方法でこれを行うことができます。

int set = 1;
setsockopt(sd, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int));

これにより、SIGPIPEシグナルが生成される代わりに、EPIPEが返されます。


45
これは良さそうに聞こえますが、SO_NOSIGPIPEはLinuxには存在しないようです。そのため、広く移植可能なソリューションではありません。
nobar

1
こんにちは、このコードをどこに配置する必要があるか教えていただけますか?私は感謝を理解していません
R. Dewi

9
Linuxでは、送信の
Aadishri

Mac OS Xに最適
キルガン2015年

116

私はパーティーに遅刻しましたSO_NOSIGPIPEが、移植性がなく、システムで動作しない可能性があります(BSDのようです)。

Linuxシステムを使用していない場合の良い代替策は、send(2)呼び出しにフラグSO_NOSIGPIPEを設定することMSG_NOSIGNALです。

例置き換えるwrite(...)ことにより、send(...,MSG_NOSIGNAL)(参照nobarのコメント)

char buf[888];
//write( sockfd, buf, sizeof(buf) );
send(    sockfd, buf, sizeof(buf), MSG_NOSIGNAL );

5
つまり、write()の代わりにsend(...、MSG_NOSIGNAL)を使用すると、SIGPIPEを取得できません。これはソケット(サポートされているプラ​​ットフォーム上)でうまく機能しますが、send()は(パイプではなく)ソケットでの使用に制限されているようです。そのため、これはSIGPIPEの問題に対する一般的な解決策ではありません。
nobar

@ Ray2k 2.2未満のLinuxアプリケーションを開発しているのは一体誰ですか?それは実際には中途半端な質問です。ほとんどのディストリビューションには、少なくとも2.6が付属しています。
パルティアンショット

1
@パルティアンショット:あなたの答えを考え直す必要があります。古い組み込みシステムのメンテナンスは、古いLinuxバージョンを気にする正当な理由です。
kirsche40 14

1
@ kirsche40私はOPではありません。
パルティアンショット

@sklndこれは私にとって完璧に機能しました。Qt QTcpSocketオブジェクトを使用してソケットの使用法をラップしています。writeメソッド呼び出しをOSで置き換える必要がありましたsendsocketDescriptorメソッドを使用)。誰かがQTcpSocketクラスでこのオプションを設定するためのよりクリーンなオプションを知っていますか?
EmilioGonzálezMontaña18年

29

この投稿では、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のコード例


22

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シグナルが発生するのを防ぎますが、ソケットを使用しようとすると読み取り/書き込みエラーが発生するので、それを確認する必要があります。


この呼び出しは、ソケットの接続後またはその前に行う必要がありますか?どこに配置するのが最適ですか。おかげで
アーメド

@Ahmed個人的にapplicationDidFinishLaunchingに入れました。主なことは、それがソケット接続と対話する前でなければならないということです。
サム

ただし、ソケットを使用しようとすると、読み取り/書き込みエラーが発生するため、それを確認する必要があります。-これはどのようにして可能ですか?signal(SIGPIPE、SIG_IGN)は機能しますが、デバッグモードではエラーが返されますが、そのエラーも無視できますか?
jongbanaag 2013年

@ Dreyfus15ローカルで処理するために、try / catchブロックのソケットを使用して呼び出しをラップしただけだと思います。これを見たのは久しぶりです。
2013年

15

パイプの遠端のプロセスが終了するのを防ぐことはできません。書き込みが完了する前に終了すると、SIGPIPEシグナルを受け取ります。シグナルをSIG_IGNした場合、書き込みはエラーで返されます。そのエラーに注意して対処する必要があります。ハンドラーでシグナルをキャッチして無視するのは良い考えではありません-パイプが機能しなくなったことに注意し、プログラムの動作を変更して再度パイプに書き込まないようにする必要があります(シグナルが再度生成されて無視されるため)繰り返しますが、もう一度やり直すと、プロセス全体が長時間続き、CPUパワーが大量に消費される可能性があります)。


6

または、ハンドラーでSIGPIPEをキャッチして無視する必要がありますか?

私はそれが正しいと信じています。相手側が記述子を閉じたときを知りたい場合、それがSIGPIPEによって通知されます。

サム


3

Linuxマニュアルによると:

EPIPE接続指向のソケットでローカルエンドがシャットダウンされました。この場合、MSG_NOSIGNALが設定されていない限り、プロセスはSIGPIPEも受け取ります。

しかし、Ubuntu 12.04の場合は正しくありません。私はそのケースのテストを作成し、私は常にSIGPIPEなしでEPIPEを受け取ります。2回目に同じ壊れたソケットに書き込もうとすると、SIGPIPEが生成されます。したがって、この信号が発生した場合にSIGPIPEを無視する必要はありません。これは、プログラムの論理エラーを意味します。


1
Linuxのソケットで最初にEPIPEを確認せずにSIGPIPEを確実に受信します。これはカーネルのバグのように聞こえます。
davmac 2016年

3

ここでクラッシュを防ぐためのベストプラクティスは何ですか?

みんなのようにシグパイプを無効にするか、エラーをキャッチして無視してください。

行の反対側がまだ読んでいるかどうかを確認する方法はありますか?

はい、select()を使用してください。

select()は、ソケットが書き込み可能であると常に表示するため、ここでは機能しないようです。

読み取りビットを選択する必要があります。おそらく書き込みビットは無視できます。

遠端がファイルハンドルを閉じると、selectは読み取り可能なデータがあることを通知します。行ってそれを読むと、0バイトが返されます。これは、ファイルハンドルが閉じられたことをOSが通知する方法です。

書き込みビットを無視できないのは、大量のボリュームを送信している場合で、もう一方の端がバックログされ、バッファがいっぱいになる可能性があります。その場合、ファイルハンドルに書き込もうとすると、プログラム/スレッドがブロックまたは失敗する可能性があります。書き込み前にselectをテストすると、それから保護されますが、反対側が正常であること、またはデータが到着することを保証するものではありません。

書き込むときと同様に、close()からsigpipeを取得できることに注意してください。

Closeは、バッファリングされたデータをすべてフラッシュします。もう一方の端がすでに閉じられている場合は、閉じることができず、シグパイプが表示されます。

バッファー付きTCPIPを使用している場合、書き込みが成功したということは、データが送信するためにキューに入れられたことを意味するだけであり、送信されたことを意味するわけではありません。closeの呼び出しが成功するまで、データが送信されたことはわかりません。

Sigpipeは、問題が発生したことを通知しますが、何をすべきか、何をすべきかについては通知しません。


Sigpipeは、問題が発生したことを通知しますが、何をすべきか、何をすべきかについては通知しません。丁度。SIGPIPEの実質的に唯一の目的は、次のステージで入力が不要になったときに、パイプラインコマンドラインユーティリティを破棄することです。プログラムがネットワーキングを行っている場合は、通常、プログラム全体でSIGPIPEをブロックまたは無視する必要があります。
DepressedDaniel 2017年

正確に。SIGPIPEは「find XXX | head」のような状況を対象としています。頭が10行に一致したら、検索を続行しても意味がありません。したがって、headは終了し、次にfindがそれに話しかけようとすると、sigipeを取得し、それも終了できることがわかります。
Ben Aveling 2017年

本当に、SIGPIPEの唯一の目的は、プログラムがずさんになり、書き込みエラーをチェックしないようにすることです!
ウィリアムパーセル

2

最新の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ページを参照してください


このようにブロックされた信号のセットを読み取り、変更、書き込みする必要はありません。sigprocmaskブロックされた信号のセットに追加されるため、1回の呼び出しでこれをすべて実行できます。
Luchs、2018年

私は、読み取り、変更、書き込みを行わず、保持しold_stateている現在の状態を保存して、後で復元できるように、後で復元できるように読み取ります。状態を復元する必要がないことがわかっている場合は、実際にこの方法で状態を読み取って保存する必要はありません。
Alexis Wilke

1
しかし、そうしsigprocmask()ます。古い状態を読み取るための最初の呼び出し、sigaddsetそれを変更するための呼び出し、sigprocmask()書き込むための2番目の呼び出し。最初の呼び出しを削除し、で初期化しsigemptyset(&set)て、2番目の呼び出しをに変更できsigprocmask(SIG_BLOCK, &set, &old_state)ます。
Luchs、

ああ!ははは!あなたが正しい。私がやります。ええと...私のソフトウェアでは、どの信号が既にブロックされているか、ブロックされていないのかわかりません。だから私はこのようにします。ただし、「このように信号を設定」が1つしかない場合は、単純な明確な+設定で十分であることに同意します。
Alexis Wilke
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.