読者です 読者をやめる 読者になる 読者になる

百日半狂乱

Shut the fuck up and write some code!!

世にも恐ろしいSIGPIPE、ソケットプログラミングの落とし穴

Linux C signal network programming socket programming system call SIGPIPE

前回、「次回もシグナルのことを書く」と書いたのでシグナルのことを書く*1.

ソケットプログラミングの落とし穴は色々あるけど、ここでは個人的に嵌ったシグナル関連の落とし穴に関して書き殴る.

結論から書くと、コネクションが切れたソケットに書き込み(send(2)とかwrite(2)とか、同じものだけど)を行うと、SIGPIPEシグナルが発生してプロセスが強制終了するので、きちんとSIGPIPEシグナルをハンドリングしておこうという話.

以下では、サンプルコードを使って、実際にパイプの書き込み先をkillして、SIGPIPEの発生を疑似体験してみる.

SISGPIPEを受けたプロセスの挙動とソケットプログラミングでの対応策

「sigpipe」で検索すると、同様の話はいくらでも記事になっていて、例えば、

「私の書いたサーバが突然死するんです。どうしてでしょうか」という質問を受けることがあります。これは多くの場合,SIGPIPEの処理を忘れていることが原因です。SIGPIPEとは,切断されたネットワークソケットなどにデータを書き込もうとした際に送出されるUNIXシグナルです。特に設定しない限り,プロセスはSIGPIPEを受け取ると強制終了されます。そのため,通信が突然切断される可能性のあるTCPサーバにおいては,SIGPIPEを無視するよう設定する必要があります。

第6回 UNIXプログラミングの勘所(3)

と言った記述があったりして、割と良くある話っぽい.

SIGPIPEシグナルを受け取ったプロセスのデフォルト動作は、coreファイルすら吐き出すことなくプロセス終了である.

この挙動は上記引用にある通り、ソケットプログラミングをやっている側からしたら何のエラー出力もなく突然ダウンするように見える*2

対応としては(これも上記引用にある通り)、例えば前回のSignal関数を使うなら、Signal(SIGPIPE, SIG_IGN);というのをプログラム開始直後に埋めておくだけで良い.

これでsend実行時に接続先がダウンしていても、プロセスが終了することなく単にsend(2)が-1を返す.

プロセスの終了ステータスとSIGPIPEの発生を確認する

ここでは以下のサンプルで、実際にパイプの書き込み先をkillして、SIGPIPEの発生を疑似体験してみる.

ソケットプログラミング的には、以下のsender.shが送信側(write(2),send(2))、receiver.shが受信側(read(2),recv(2))と思ってもらえば良い.

すなわち、受信側がダウンしているところに、送信側がwirte(2)を実行した時にどうなるかを見る.

straceコマンドでSIGPIPEの発生を確認する

SIGPIPEの発生確認は、straceコマンドの結果とプロセスの終了ステータスとで行おうと思う.

参考:strace コマンドの使い方をまとめてみた

以下は上記サンプルのうち、sigpipe.bashを実行して、その際に出力されるtrace.logの末尾(この場の議論に必要な部分だけ)を出力した結果.

$ ./sigpipe.bash 2> stderr.log
$ exit status:141 143

$ tail -n 3 trace.log
write(1, "Message from ./sender.sh\n", 25) = -1 EPIPE (Broken pipe)
--- SIGPIPE (Broken pipe) @ 0 (0) ---
+++ killed by SIGPIPE +++

まず、straceコマンドの結果を見ると、write(2)が失敗している(EPIPE (Broken pipe))ことが確認できる.プロセスはkilled by SIGPIPE

終了ステータスの結果を出力している${PIPESTATUS[@]}bash固有の機能である(便利!).

exit status:141 143sender.shがSIGPIPEによって終了(141)したことを、receiver.shがSIGTERMで終了(143)したことを意味している.

少なくともLinuxでは128にシグナル番号を加えたものを終了ステータスとしているらしい.

Posixでは戻り値は128以上でなければならないと定めらているだけだが、Linuxは128+シグナル番号を返す.

Linuxの標準シグナルの値を確認すると、SIGPIPE:13、SIGTERM:15とある.

reciever.shはkillコマンドからSIGTERM送信され終了している.すなわち、128+15=143

sender.shは送信先のプロセスが死んでいるので、SIGPIPEシグナルが発生して終了.すなわち、128+13=141

終了ステータスの意味的にも確かにSIGPIPEでダウンしていることが確認できる.

まぁ一番の教訓は、異なるアプローチのデバッグ方法を常に複数個所持しておけ、ということであるような気がする.

*1:前回から115日が経っている.挫折したアドベントカレンダーもそうだけど、やはり連載宣言はするものじゃない.自分のような興味関心がブレまくる人間が連載宣言をしても、宣言した時をピークにモチベーションは下がり続ける.インプットのためのアウトプット、所詮メモの大原則を忘れている.

*2:完全に嵌っている時はgdbでbacktraceしたくても何故かcoreファイルが吐き出されないのでデバッグできないなどと嘆きつつ途方に暮れていた.