【C言語】ソケット通信プログラムが突然終了する現象の原因と対処法(SIGPIPE)

ソケット通信プログラムの動作確認をしていて、突然いきなり音も立てずにプログラムが終了してしまったことありませんか?

私はこの現象に陥って原因突き止めるのに苦労しました…。おそらく同じ現象で困っている人もいると思いますので、このページで原因と対処法について解説しておこうと思います!

あ、僕もある!

無限ループでクライアントとデータのやりとりするプログラムが突然終了した…

無限ループだから、強制終了しないとプログラム終了するはずないのに…

おそらくこのページで解説するのと同じ原因だと思うよ!

突然何も言わずにプログラムが終了するから原因突き止めるの大変なんだよね…

特にこの現象が発生するのは Linux や MacOSX でソケット通信プログラムを作成しているときだと思います。

ですので、このページでは特に Linux や MacOSX でソケット通信プログラミングを行っている方向けに、突然プログラムが終了する原因とその対処法について解説していきたいと思います。

突然プログラムが終了する原因

では、ソケット通信プログラムが突然終了する原因について説明していきたいと思います。

原因はシグナル SIGPIPE の発生

ソケット通信プログラムが突然終了する原因としてシグナル SIGPIPE が発生していることが考えられます。

シグナル…?

難しい言葉が出てきたね!

じゃあまずはシグナルがどのようなものかについて確認していこうか!

シグナルとは

シグナルとは Unix 系の OS における「プロセスに何かが発生したことを伝える信号」です。プロセス間通信を行う場合などにも利用されることがあります。

MEMO

マルチプロセスプログラミングをしていないのであれば(シングルプロセスプログラミングを行っているのであれば)、プロセス = プログラムくらいに考えてしまって良いです

このページではプロセス = プログラムとして説明していきます

シグナルを受け取ったプログラムは、その時に実行していた処理を中断し、優先して「あらかじめ定義された処理」を行います。

シグナルを受け取った時のプログラムの動作

例えば、わかりやすいのが Ctrl+C キー入力です。

プログラム実行中にこのキー入力を行えば、実行中のプログラムを強制終了させることができますよね?

では、なぜプログラムを強制終了することができるのでしょうか?

なぜって…

うーん、考えたことなかったなぁ…

これも実はシグナルが関係しているよ!

実は、Ctrl+C キー入力は、シグナルを実行中のプログラムに送信する操作になります。

この時に送信されるシグナルは SIGINT というものになります。つまり、Ctrl+C を押すと実行中のプログラムにシグナル SIGINT が送信されます。

Ctrl+CでSIGINTを送信する様子

で、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 は通信相手がソケットを閉じているのに、そのソケットに対して sendwrite を実行した時に発生するシグナルとなります。

ですので、通信相手がソケットを閉じているのに、そのソケットに対して sendwrite を実行してしまうと、SIGPIPE をプログラムが受け取り、そのプログラムが突然終了するという現象が発生することになります。

突然プログラムが発生する例

次は、どのようなプログラムだと SIGPIPE が発生して突然プログラムが終了するのかについて、具体例を用いて説明していきたいと思います。

使用するプログラムのベースは下記ページのソケット通信の簡単なプログラム例で紹介している server.cclient.c としたいと思います。server.c はサーバープログラム、client.c はクライアントプログラムになります。

ソケット通信解説ページのアイキャッチ【C言語】ソケット通信について解説

これらがどのようなプログラムであるかは上記ページを参考にしていただければと思います。

server.ctransfer 関数を下記のように変更すれば、簡単に SIGPIPE を発生させられるようになります。

server.cのtransfer
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. を繰り返してみてください)。

  1. サーバープログラムを実行
    • サーバーが接続待ちになる
  2. クライアントプログラムを実行
    • クライアントとサーバー間の接続が確立される
    • 接続後、クライアントはユーザーからの「文字列入力待ち」になる
    • 接続後、サーバーはクライアントからの「データ受信待ち」になる
  3. クライアントプログラムを実行しているターミナルで Ctrl+C を入力する
    • クライアントプログラムが終了する
  4. SIGPIPE が発生!!
    • サーバープログラムが終了する

なぜ、上記により SIGPIPE が発生するのかを、変更後の server.ctransfer 関数と照らし合わせて確認していきたいと思います。

3. を行う時点でのクライアントプログラムとサーバープログラムの状態は下記のようになります。

  • クライアント:ユーザーからの文字列入力待ち(scanf 実行中)
  • サーバー:クライアントからのデータ受信待ち(recv 実行中)

この状態で、クライアントプログラムを終了させると、クライアント側はプログラム終了に伴いサーバーと接続していたソケットも閉じられることになります。

さらにサーバー側は、クライアントがソケットを閉じたことにより下記の recv 関数が終了します。この時の戻り値は 0 になります。

recvの実行
/* クライアントから文字列を受信 */
recv_size = recv(sock, recv_buf, BUF_SIZE, 0);

サーバープログラムでは recv 関数が終了することで次の処理に移ります。前述の通り break 文をコメントアウトしたので、次は受信文字列が表示され、さらに send 関数によりクライアントへのデータの送信が行われます。

sendの実行
send_size = send(sock, &send_buf, 1, 0);

ただし、この時点ですでにクライアントのソケットは閉じられていることになります。

ここで既に閉じられているソケットに対して書き込み(send)が実行されるので、SIGPIPE が発生します。そしてこの SIGPIPE をサーバープログラムが受け取り、プログラムが終了します。

このような現象は、特に通信相手が強制終了するような際に発生しやすいです。例えばクライアントが強制終了するとサーバーで SIGPIPE が発生し、サーバーも道連れになる感じで強制終了するようなことが多いです。

クライアントの道連れにサーバーも終了する様子

突然プログラムが終了する現象の対処法

では、ソケット通信プログラムが突然終了する現象はどのようにして対処すれば良いでしょうか?

ここまで解説してきたように、原因は「SIGPIPE が発生」した時に「プログラムが終了する」ことですので、大きく分けると対処の方針としては下記の2つが考えられます。

  • SIGPIPE が発生しないようにする
  • SIGPIPE 発生時にプログラムが終了しないようにする

スポンサーリンク

SIGPIPE が発生しないようにする

SIGPIPE が発生しないようにするためには、下記の2つの対処法が考えられます。

  • 相手のソケットクローズ後は send しないようにする
  • send 実行時にシグナルが発生しないようにする

相手のソケットクローズ後は send しないようにする

SIGPIPE が発生する原因は「既に閉じられているソケットに対して sendwrite)を実行する」ことですので、相手がソケットクローズした時は send しないようにしてやれば SIGPIPE の発生を防ぐことができます。

例えば突然プログラムが発生する例で紹介したプログラムであれば、コメントアウトした部分を元に戻せば良いです。つまり、recv 関数の戻り値が 0 の時に breakwhile ループを抜けて send を実行しないようにします。

recv 関数では、相手のソケットがクローズされている場合に戻り値が 0 になります。なので、この戻り値が 0 であるかどうかで相手のソケットがクローズされているかを判断することができます(ただし、サイズ 0 のデータを受信している場合もあります)。

こんな感じで、相手のソケットがクローズされていると分かったときに、send しないようにすることで、SIGPIPE の発生をほぼ防ぐことができます。

ただし、防ぐことができるのは “ほぼ” で、完全に防ぐことは難しいと思います。

例えば、recv 関数の戻り値が 1 以上の場合でも、recv 関数がリターンした直後にクライアントのソケットがクローズされた場合、結局そのソケットに対して send を行うことになるので SIGPIPE が発生してしまう可能性があります。

recv後のsendでSIGPIPEが発生する様子

もちろん recvsend の間の処理を少なくすれば、その間の時間も短くなって SIGPIPE が発生する可能性は減りますが、あくまでも可能性が減るだけで SIGPIPE 発生する可能性は残ります。

また、たとえ他の方法で send 関数を実行する前に相手のソケットがクローズされているかどうかを判断したとしても、その判断の直後に相手のソケットがクローズされる可能性もあるので、完璧に SIGPIPE 発生を防ぐのは難しいです。

要は通信相手が他のプログラム、さらには他の端末(PC やスマホ)で実行されるので、いつ相手のソケットがクローズされるか分からない&自身のプログラムから相手のソケットのクローズのタイミングは制御しようがないのです…。

ダメじゃん…

結局 SIGPIPE の発生は防げないってこと?

この方法だけだと完璧に防ぐことは難しいね…

「相手のソケットがクローズした状態で send する可能性はある」

と考えて対策したほうが良いよ

そのため、もちろん相手のソケットがクローズした後に send しないようにすることも重要ですが、完璧に防ぐことは難しいことを踏まえて他の方法と合わせて対処することが必要になります。

ここ紹介する対処法は、相手のソケットがクローズした後に send したとしても、「SIGPIPE が発生しない」or「SIGPIPE 発生時にプログラムが終了しない」ようにするための対策になります。

send 実行時にシグナルが発生しないようにする

まず説明するのは send 関数実行時に SIGPIPE を発生しないようにする方法です。

send 関数の定義は下記のようになり、第4引数の flags の設定によって、相手のソケットがクローズした後に send したとしても SIGPIPE が発生しないようにすることができます。

send
#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 の例になります)。

SIGPIPEを発生しなくする
send_size = send(sock, &send_buf, 1, SO_NOSIGPIPE);

これにより、相手のソケットがクローズした後に send を実行したとしても、SIGPIPE が発生しないようになります。ですので、SIGPIPE が原因でプログラムが突然終了するようなことはなくなります。

また、SIGPIPE が発生しなくなる代わりに send 関数がエラーを返却するようになりますので、通常のエラー時同様にエラー処理を行えば良いことになります(エラー時には errnoEPIPE が設定されます)。

SIGPIPE 発生時にプログラムが終了しないようにする

上記では SIGPIPE の「発生を防ぐ」方針での対処法を紹介しましたが、ここでは SIGPIPE が「発生した時にプログラムが終了しないようにする」方針での対処法を説明していきます。

  • SIGPIPE 発生時に実行される処理を置き換える
  • SIGPIPE が発生しても無視するようにする

SIGPIPE 発生時に実行される処理を置き換える

1つ目の対処法は「SIGPIPE 発生時に実行される処理を置き換える」方法になります。

シグナルとはで解説したように、プログラムはシグナルを受け取ると、そのシグナルに応じて「あらかじめ定義された処理」が実行されます。

SIGPIPE の場合は、「プログラムを終了する処理」が「あらかじめ定義された処理」になります。

なので、SIGPIPE を受け取った場合に、この「あらかじめ定義された処理」を行わないようにできれば、プログラムが突然終了する現象を防ぐことができることになります。

この方法の1つ目が、その「あらかじめ定義された処理」を別の処理に置き換える方法になります。

Linux や MacOSX 環境では signal 関数や sigaction 関数が用意されており、これらの関数により、シグナルを受け取った時に実行する関数を設定することができるようになっています(ただし移植性が低いため signal の利用は非推奨らしい)。

ですので、signal 関数や sigaction 関数でその設定を行うことにより、SIGPIPE を受け取った際に「あらかじめ定義された処理」とは異なる処理が実行されるようにすることができます。

例えば sigaction 関数の定義は下記になります。

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 関数を実行するようにしたいのであれば、プログラムの最初で下記を実行すれば良いと思います

SIGPIPE受け取った時の処理置き換え
    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 メンバに設定してやれば目的は達成することができます。

SIGPIPE受け取った時の処理の例
void sigpipe_handler(int sig) {
    return;
}

上記のように sigaction 関数を実行すると、SIGPIPE が発生した場合は sigpipe_handler 関数が実行されるようになります。

ですので、プログラムが突然終了するようなことはなく、代わりに send 関数実行時にエラーが発生するようになります(エラー時には errnoEPIPE が設定される)。

SIGPIPE が発生しても無視するようにする

2つ目の対処法は「SIGPIPE が発生しても無視するようにする」方法になります。

要は SIGPIPE を受け取ってもそれを無視して「あらかじめ定義された処理」を行わないようにします。

このシグナルを無視するための設定も、signal 関数や sigaction 関数で実行することができます。

例えば sigaction 関数の場合は、sa_handler メンバに設定 に SIG_IGN を設定することで、シグナル発生時に第1引数で指定したシグナルを無視することができるようになります。

SIGPIPEを無視する
    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 関数実行時にエラーが発生するようになります(エラー時には errnoEPIPE が設定される)。

まとめ

このページでは、特にソケット通信プログラムで突然プログラムが終了する現象の原因と対処法について解説しました!

この現象は SIGPIPE の発生が原因であり、この対処法として下記の4つを紹介しました。

  1. 相手のソケットクローズ後は send しないようにする
  2. send 実行時にシグナルが発生しないようにする
  3. SIGPIPE 発生時に実行される処理を置き換える
  4. SIGPIPE が発生しても無視するようにする

ただ、1. のみで完璧に SIGPIPE の発生を防ぐことは難しいため、2. 3. 4. の方法と組み合わせて SIGPIPE が発生するような場面でもプログラムが突然終了しないようにすることが重要です。

シグナルを完全に理解することは難しいですが、大事なのはシグナルの存在を知っておくことだと思います。そして、このシグナルによってプログラムが突然終了することを知っておけば、同じ現象に出くわした時に原因や対処法の調べ方を推測しやすくなります。

ぜひシグナルの存在を、特にソケット通信を行う方は SIGPIPE の存在を頭の片隅にでも置いておいてください!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です