ソケット通信プログラムの動作確認をしていて、突然いきなり音も立てずにプログラムが終了してしまったことありませんか?
私はこの現象に陥って原因突き止めるのに苦労しました…。おそらく同じ現象で困っている人もいると思いますので、このページで原因と対処法について解説しておこうと思います!
あ、僕もある!
無限ループでクライアントとデータのやりとりするプログラムが突然終了した…
無限ループだから、強制終了しないとプログラム終了するはずないのに…
おそらくこのページで解説するのと同じ原因だと思うよ!
突然何も言わずにプログラムが終了するから原因突き止めるの大変なんだよね…
特にこの現象が発生するのは Linux や MacOSX でソケット通信プログラムを作成しているときだと思います。
ですので、このページでは特に Linux や MacOSX でソケット通信プログラミングを行っている方向けに、突然プログラムが終了する原因とその対処法について解説していきたいと思います。
Contents
突然プログラムが終了する原因
では、ソケット通信プログラムが突然終了する原因について説明していきたいと思います。
原因はシグナル SIGPIPE
の発生
ソケット通信プログラムが突然終了する原因としてシグナル SIGPIPE
が発生していることが考えられます。
難しい言葉が出てきたね!
じゃあまずはシグナルがどのようなものかについて確認していこうか!
シグナルとは
シグナルとは Unix 系の OS における「プロセスに何かが発生したことを伝える信号」です。プロセス間通信を行う場合などにも利用されることがあります。
マルチプロセスプログラミングをしていないのであれば(シングルプロセスプログラミングを行っているのであれば)、プロセス = プログラムくらいに考えてしまって良いです
このページではプロセス = プログラムとして説明していきます
シグナルを受け取ったプログラムは、その時に実行していた処理を中断し、優先して「あらかじめ定義された処理」を行います。
例えば、わかりやすいのが Ctrl+C
キー入力です。
プログラム実行中にこのキー入力を行えば、実行中のプログラムを強制終了させることができますよね?
では、なぜプログラムを強制終了することができるのでしょうか?
なぜって…
うーん、考えたことなかったなぁ…
実は、Ctrl+C
キー入力は、シグナルを実行中のプログラムに送信する操作になります。
この時に送信されるシグナルは SIGINT
というものになります。つまり、Ctrl+C
を押すと実行中のプログラムにシグナル SIGINT
が送信されます。
で、SIGINT
を受け取った場合、プログラムは終了することになっています。これは SIGINT
に対する「あらかじめ定義された処理」が「プログラムを終了する」と設定されているからです。
このあたりが、Ctrl+C
を押すとプログラムを強制終了させることができる理由になります。
上記の例ではシグナルがユーザー操作起因で発生していましたが、プログラムの動作が原因でシグナルが発生する場合もあります。
例えばメモリアクセス違反が発生したときは、プログラムの動作が原因でシグナル SIGSEGV
が発生し、この SIGSEGV
をプログラムが受け取ることになります(OS がシグナルをプログラムに送信する)。
他にシグナルにどのようなものがあるかは、man
コマンドで確認することができます。
man signal
ポイントは、私たちが作成するプログラムはさまざまな要因でシグナルを受け取る可能性があり、このシグナルを受け取った場合に、そのシグナルに応じた「あらかじめ定義された処理」が実行されるというところです。
で、ソケット通信プログラムを行う際にはシグナル SIGPIPE
をプログラムが受け取る可能性があります。この SIGPIPE
を受け取った際の「あらかじめ定義された処理」は「プログラムの終了」です。
なので、ソケット通信プログラムが突然終了する場合は、この SIGPIPE
が原因である可能性が高いです。
スポンサーリンク
SIGPIPE
が発生する原因
では、この SIGPIPE
が発生する原因は何のか?ここについて説明していきたいと思います。
まず SIGPIPE
がどのようなシグナルであるかを man
コマンドで確認してみましょう!私の環境だと SIGPIPE
の説明文は下記のように記載されていました。
13 SIGPIPE terminate process write on a pipe with no reader
各項目はそれぞれ下記を説明するものになります。
- シグナル番号:
13
- シグナル名:
SIGPIPE
- シグナルを受け取った際の「あらかじめ定義された処理」:
terminate process
- シグナルの説明:
write on a pipe with no reader
要は SIGPIPE
とは「読み手のいないパイプに書き込んだ際に発生するシグナル」で、SIGPIPE
を受け取ったプロセス(プログラム)は「終了する」処理が行われます。
パイプは、ソケット通信の場合は「ソケット」と考えて良いです。
つまり、SIGPIPE
は通信相手がソケットを閉じているのに、そのソケットに対して send
や write
を実行した時に発生するシグナルとなります。
ですので、通信相手がソケットを閉じているのに、そのソケットに対して send
や write
を実行してしまうと、SIGPIPE
をプログラムが受け取り、そのプログラムが突然終了するという現象が発生することになります。
突然プログラムが発生する例
次は、どのようなプログラムだと SIGPIPE
が発生して突然プログラムが終了するのかについて、具体例を用いて説明していきたいと思います。
使用するプログラムのベースは下記ページのソケット通信の簡単なプログラム例で紹介している server.c
と client.c
としたいと思います。server.c
はサーバープログラム、client.c
はクライアントプログラムになります。
これらがどのようなプログラムであるかは上記ページを参考にしていただければと思います。
server.c
の transfer
関数を下記のように変更すれば、簡単に SIGPIPE
を発生させられるようになります。
int transfer(int sock) {
int recv_size, send_size;
char recv_buf[BUF_SIZE], send_buf;
while (1) {
/* クライアントから文字列を受信 */
recv_size = recv(sock, recv_buf, BUF_SIZE, 0);
if (recv_size == -1) {
printf("recv error\n");
break;
}
if (recv_size == 0) {
/* 受信サイズが0の場合は相手が接続閉じていると判断 */
printf("connection ended\n");
// break;
}
/* 受信した文字列を表示 */
printf("%s\n", recv_buf);
/* 文字列が"finish"ならクライアントとの接続終了 */
if (strcmp(recv_buf, "finish") == 0) {
/* 接続終了を表す0を送信 */
send_buf = 0;
send_size = send(sock, &send_buf, 1, 0);
if (send_size == -1) {
printf("send error\n");
break;
}
break;
} else {
/* "finish"以外の場合はクライアントとの接続を継続 */
send_buf = 1;
send_size = send(sock, &send_buf, 1, 0);
if (send_size == -1) {
printf("send error\n");
break;
}
}
}
return 0;
}
変更点は下記の recv
の戻り値が 0
の時の break
をコメントアウトしただけになります。
if (recv_size == 0) {
/* 受信サイズが0の場合は相手が接続閉じていると判断 */
printf("connection ended\n");
// break;
}
発生させる手順は下記の通りになります(毎回発生するわけではないので、発生しないようであれば、サーバプログラムを起動したまま 2. と 3. を繰り返してみてください)。
- サーバープログラムを実行
- サーバーが接続待ちになる
- クライアントプログラムを実行
- クライアントとサーバー間の接続が確立される
- 接続後、クライアントはユーザーからの「文字列入力待ち」になる
- 接続後、サーバーはクライアントからの「データ受信待ち」になる
- クライアントプログラムを実行しているターミナルで
Ctrl+C
を入力する- クライアントプログラムが終了する
SIGPIPE
が発生!!- サーバープログラムが終了する
なぜ、上記により SIGPIPE
が発生するのかを、変更後の server.c
の transfer
関数と照らし合わせて確認していきたいと思います。
3. を行う時点でのクライアントプログラムとサーバープログラムの状態は下記のようになります。
- クライアント:ユーザーからの文字列入力待ち(
scanf
実行中) - サーバー:クライアントからのデータ受信待ち(
recv
実行中)
この状態で、クライアントプログラムを終了させると、クライアント側はプログラム終了に伴いサーバーと接続していたソケットも閉じられることになります。
さらにサーバー側は、クライアントがソケットを閉じたことにより下記の recv
関数が終了します。この時の戻り値は 0
になります。
/* クライアントから文字列を受信 */
recv_size = recv(sock, recv_buf, BUF_SIZE, 0);
サーバープログラムでは recv
関数が終了することで次の処理に移ります。前述の通り break
文をコメントアウトしたので、次は受信文字列が表示され、さらに send
関数によりクライアントへのデータの送信が行われます。
send_size = send(sock, &send_buf, 1, 0);
ただし、この時点ですでにクライアントのソケットは閉じられていることになります。
ここで既に閉じられているソケットに対して書き込み(send
)が実行されるので、SIGPIPE
が発生します。そしてこの SIGPIPE
をサーバープログラムが受け取り、プログラムが終了します。
このような現象は、特に通信相手が強制終了するような際に発生しやすいです。例えばクライアントが強制終了するとサーバーで SIGPIPE
が発生し、サーバーも道連れになる感じで強制終了するようなことが多いです。
突然プログラムが終了する現象の対処法
では、ソケット通信プログラムが突然終了する現象はどのようにして対処すれば良いでしょうか?
ここまで解説してきたように、原因は「SIGPIPE
が発生」した時に「プログラムが終了する」ことですので、大きく分けると対処の方針としては下記の2つが考えられます。
SIGPIPE
が発生しないようにするSIGPIPE
発生時にプログラムが終了しないようにする
スポンサーリンク
SIGPIPE
が発生しないようにする
SIGPIPE
が発生しないようにするためには、下記の2つの対処法が考えられます。
- 相手のソケットクローズ後は
send
しないようにする send
実行時にシグナルが発生しないようにする
相手のソケットクローズ後は send
しないようにする
SIGPIPE
が発生する原因は「既に閉じられているソケットに対して send
(write
)を実行する」ことですので、相手がソケットクローズした時は send
しないようにしてやれば SIGPIPE
の発生を防ぐことができます。
例えば突然プログラムが発生する例で紹介したプログラムであれば、コメントアウトした部分を元に戻せば良いです。つまり、recv
関数の戻り値が 0
の時に break
で while
ループを抜けて send
を実行しないようにします。
recv
関数では、相手のソケットがクローズされている場合に戻り値が 0
になります。なので、この戻り値が 0
であるかどうかで相手のソケットがクローズされているかを判断することができます(ただし、サイズ 0
のデータを受信している場合もあります)。
こんな感じで、相手のソケットがクローズされていると分かったときに、send
しないようにすることで、SIGPIPE
の発生をほぼ防ぐことができます。
ただし、防ぐことができるのは “ほぼ” で、完全に防ぐことは難しいと思います。
例えば、recv
関数の戻り値が 1
以上の場合でも、recv
関数がリターンした直後にクライアントのソケットがクローズされた場合、結局そのソケットに対して send
を行うことになるので SIGPIPE
が発生してしまう可能性があります。
もちろん recv
と send
の間の処理を少なくすれば、その間の時間も短くなって SIGPIPE
が発生する可能性は減りますが、あくまでも可能性が減るだけで SIGPIPE
発生する可能性は残ります。
また、たとえ他の方法で send
関数を実行する前に相手のソケットがクローズされているかどうかを判断したとしても、その判断の直後に相手のソケットがクローズされる可能性もあるので、完璧に SIGPIPE
発生を防ぐのは難しいです。
要は通信相手が他のプログラム、さらには他の端末(PC やスマホ)で実行されるので、いつ相手のソケットがクローズされるか分からない&自身のプログラムから相手のソケットのクローズのタイミングは制御しようがないのです…。
ダメじゃん…
結局 SIGPIPE
の発生は防げないってこと?
この方法だけだと完璧に防ぐことは難しいね…
「相手のソケットがクローズした状態で send
する可能性はある」
と考えて対策したほうが良いよ
そのため、もちろん相手のソケットがクローズした後に send
しないようにすることも重要ですが、完璧に防ぐことは難しいことを踏まえて他の方法と合わせて対処することが必要になります。
ここ紹介する対処法は、相手のソケットがクローズした後に send
したとしても、「SIGPIPE
が発生しない」or「SIGPIPE
発生時にプログラムが終了しない」ようにするための対策になります。
send
実行時にシグナルが発生しないようにする
まず説明するのは send
関数実行時に SIGPIPE
を発生しないようにする方法です。
send
関数の定義は下記のようになり、第4引数の flags
の設定によって、相手のソケットがクローズした後に send
したとしても SIGPIPE
が発生しないようにすることができます。
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
flags
に設定すべき値は、Linux と MacOSX で異なり、具体的には下記になります。
- Linux :
MSG_NOSIGNAL
- MacOSX:
SO_NOSIGPIPE
例えば突然プログラムが発生する例で紹介したプログラムであれば、send
関数実行部分を下記のように変更します(下記は MacOSX の例になります)。
send_size = send(sock, &send_buf, 1, SO_NOSIGPIPE);
これにより、相手のソケットがクローズした後に send
を実行したとしても、SIGPIPE
が発生しないようになります。ですので、SIGPIPE
が原因でプログラムが突然終了するようなことはなくなります。
また、SIGPIPE
が発生しなくなる代わりに send
関数がエラーを返却するようになりますので、通常のエラー時同様にエラー処理を行えば良いことになります(エラー時には errno
に EPIPE
が設定されます)。
SIGPIPE
発生時にプログラムが終了しないようにする
上記では SIGPIPE
の「発生を防ぐ」方針での対処法を紹介しましたが、ここでは SIGPIPE
が「発生した時にプログラムが終了しないようにする」方針での対処法を説明していきます。
SIGPIPE
発生時に実行される処理を置き換えるSIGPIPE
が発生しても無視するようにする
SIGPIPE
発生時に実行される処理を置き換える
1つ目の対処法は「SIGPIPE
発生時に実行される処理を置き換える」方法になります。
シグナルとはで解説したように、プログラムはシグナルを受け取ると、そのシグナルに応じて「あらかじめ定義された処理」が実行されます。
SIGPIPE
の場合は、「プログラムを終了する処理」が「あらかじめ定義された処理」になります。
なので、SIGPIPE
を受け取った場合に、この「あらかじめ定義された処理」を行わないようにできれば、プログラムが突然終了する現象を防ぐことができることになります。
この方法の1つ目が、その「あらかじめ定義された処理」を別の処理に置き換える方法になります。
Linux や MacOSX 環境では signal
関数や sigaction
関数が用意されており、これらの関数により、シグナルを受け取った時に実行する関数を設定することができるようになっています(ただし移植性が低いため signal
の利用は非推奨らしい)。
ですので、signal
関数や sigaction
関数でその設定を行うことにより、SIGPIPE
を受け取った際に「あらかじめ定義された処理」とは異なる処理が実行されるようにすることができます。
例えば sigaction
関数の定義は下記になります。
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
sigaction
関数の引数は下記のようになります。
signum
:動作を設定したいシグナル番号act
:動作を設定した構造体(のアドレス)oldact
:設定前の動作が設定された構造体(のアドレス)
要は sigaction
関数は、第1引数で指定したシグナルを受け取った時の動作を、第2引数で指定する構造体の設定に応じた動作に置き換えてしまう関数になります(第3引数で sigaction
で置き換える前の動作が格納された構造体を取得することも可能)。
ですので、第1引数に SIGPIPE
を設定して sigaction
関数を実行すれば、SIGPIPE
を受け取った時の動作を第2引数に指定した構造体に設定した動作に置き換えることができます。
struct sigaction
構造体や sigaction
関数の詳細は下記の Man page が詳しいと思います。
https://linuxjm.osdn.jp/html/LDP_man-pages/man2/sigaction.2.html
この struct sigaction
構造体の設定で細かな動作を設定することができるのですが、今回やりたいことはシグナルを受け取った時に実行する処理の変更です。
このシグナルを受け取った時の処理の変更は、struct sigaction
構造体の sa_handler
メンバに「シグナルを受け取った時に実行したい関数のアドレス(関数名)」を設定して sigaction
を実行することで実現することができます。
例えば SIGPIPE
を受け取った時に「プログラムを終了する」のではなく、代わりに sigpipe_handler
関数を実行するようにしたいのであれば、プログラムの最初で下記を実行すれば良いと思います
struct sigaction act;
memset(&act, 0, sizeof(struct sigaction));
act.sa_handler = sigpipe_handler;
if (sigaction(SIGPIPE, &act, NULL) == -1) {
printf("sigaction error\n");
return -1;
}
sa_handler
メンバに設定する関数の型は void 関数名(int)
になります。引数にシグナル番号が格納された状態で実行されることになります。
とりあえず処理を置き換えたいだけであれば、下記のような何もしない関数を用意して sa_handler
メンバに設定してやれば目的は達成することができます。
void sigpipe_handler(int sig) {
return;
}
上記のように sigaction
関数を実行すると、SIGPIPE
が発生した場合は sigpipe_handler
関数が実行されるようになります。
ですので、プログラムが突然終了するようなことはなく、代わりに send
関数実行時にエラーが発生するようになります(エラー時には errno
に EPIPE
が設定される)。
SIGPIPE
が発生しても無視するようにする
2つ目の対処法は「SIGPIPE
が発生しても無視するようにする」方法になります。
要は SIGPIPE
を受け取ってもそれを無視して「あらかじめ定義された処理」を行わないようにします。
このシグナルを無視するための設定も、signal
関数や sigaction
関数で実行することができます。
例えば sigaction
関数の場合は、sa_handler
メンバに設定 に SIG_IGN
を設定することで、シグナル発生時に第1引数で指定したシグナルを無視することができるようになります。
struct sigaction act;
memset(&act, 0, sizeof(struct sigaction));
act.sa_handler = SIG_IGN;
if (sigaction(SIGPIPE, &act, NULL) == -1) {
printf("sigaction error\n");
return -1;
}
上記のように sigaction
関数を実行すると、SIGPIPE
が無視され send
関数実行時にエラーが発生するようになります(エラー時には errno
に EPIPE
が設定される)。
まとめ
このページでは、特にソケット通信プログラムで突然プログラムが終了する現象の原因と対処法について解説しました!
この現象は SIGPIPE
の発生が原因であり、この対処法として下記の4つを紹介しました。
- 相手のソケットクローズ後は
send
しないようにする send
実行時にシグナルが発生しないようにするSIGPIPE
発生時に実行される処理を置き換えるSIGPIPE
が発生しても無視するようにする
ただ、1. のみで完璧に SIGPIPE
の発生を防ぐことは難しいため、2. 3. 4. の方法と組み合わせて SIGPIPE
が発生するような場面でもプログラムが突然終了しないようにすることが重要です。
シグナルを完全に理解することは難しいですが、大事なのはシグナルの存在を知っておくことだと思います。そして、このシグナルによってプログラムが突然終了することを知っておけば、同じ現象に出くわした時に原因や対処法の調べ方を推測しやすくなります。
ぜひシグナルの存在を、特にソケット通信を行う方は SIGPIPE
の存在を頭の片隅にでも置いておいてください!