C言語でシグナルをハンドルする

# はじめに
C言語初心者(新人研修レベル)の私が、abortシグナルをハンドルするまでの履歴を記載します。

記載内容やコードにツッコミどころがあれば、ご指摘いただけると嬉しいです。

# 環境
プロジェクトでは、Redhat Enterprise Linux 7 を利用していますが、今回はCent OS 7で記載します。

- CentOS Linux release 7.4.1708 (Core)
- gcc 4.8.5

# シグナルについて
シグナルは、UNIXやLinuxで使用されるプロセス間通信の一つです。ぱっと聞いた感じだとイメージしにくいですが、ターミナル上でプログラムやコマンドを停止するときや特定のイベントを発生させたい時に利用します。
**Ctrl + C**で実行中のコマンドやプログラムを停止させようとすることもこれに該当します。

※今回はシグナルに関する詳しい情報については、ここでは説明しません。

Linuxで取り扱うシグナルは、**標準シグナル**と**リアルタイムシグナル**の２種類があります。また、それぞれのシグナルは３２個ずつあり、合計６４個シグナルが存在することになります。
この時、標準シグナルは１番〜３２番が割り当てられており、リアルタイムシグナルは３３番〜６４番までが割り当てられています。
Linuxに関してよく聞くシグナルは標準シグナルの**SIGINT**や**SIGKILL**などが該当するかと思います。

シグナルの詳細は、[Man page of SIGNAL](https://linuxjm.osdn.jp/html/LDP_man-pages/man7/signal.7.html)を参照ください。

今回は、~~SIGABRT~~ SIGINTをベースに記載していきたいと思います。

~~SIGABRTに関する詳細を知りたい方は、[Man page of ABORT](https://linuxjm.osdn.jp/html/LDP_man-pages/man3/abort.3.html)を参照ください。~~

# 実際にabortをハンドルしてみる
C言語でシグナルをキャッチするには、`signal`関数と`sigaction`関数の２つがあります。まずは、`signal`で確認したいと思います。

実際に書いたコードが以下になります。

```c:test_signal.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

volatile sig_atomic_t e_flag = 0;

void abrt_handler(int sig);

int main() {
  printf("start %s\n", __func__);

  if ( signal(SIGINT, abrt_handler) == SIG_ERR ) {
    exit(1);
  }

  while (!e_flag) {}

  printf("end %s\n", __func__);

  return 0;
}

void abrt_handler(int sig) {
  e_flag = 1;
}
```

```shell:実行結果
[root@ca035c198d1f work]# ./signal_test.o
start main
^Chandle signal : 2
end main
```

以下は、ご指摘を受ける前のNGコードです。NGサンプルとして記載を残しておきます。

```C:test_signal.c(NG)
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <setjmp.h>

jmp_buf buf;

void abrt_handler(int sig);

int main() {
  printf("start %s\n", __func__);

  // SIG_ABRTのハンドルを設定する。
  // ハンドルに失敗した場合は、処理を終了する。
  if ( signal(SIGABRT, abrt_handler) == SIG_ERR ) {
    exit(1);
  }

  // シグナルハンドル後の復帰ポイントを設定する。
  // 復帰後に再度abortが呼ばれないようにする。
  if ( setjmp(buf) == 0 ) {
    printf("publish abort\n");
    abort();
  }

  printf("end %s\n", __func__);

  return 0;
}

// シグナルハンドル関数
void abrt_handler(int sig) {
  printf("handle signal : %d\n", sig);
  // メッセージ表示後は、setjmpに復帰する。
  longjmp(buf, 1);
}
```

実行結果は以下となります。

```shell:実行結果
[root@ca035c198d1f work]# ./signal_test.o
start main
publish abort
handle signal : 6
end main
```

コードを見てわかると思いますが、そんなに難しくはありません。
`signal`関数にハンドルしたいシグナルを定義し、第２引数に関数を指定します。そうすることで、SIGABRTが発生した時に、`abrt_handler`が実行されるようになります。また、`abrt_handler`が呼ばれるときの第１引数には発生したシグナル番号が入ります。今回は、 "６"が入ります。

~~今回はabortを発行しているため、シグナルをハンドルした後、同じ場所に戻ってしまうとそのままプロセスが終了してしまいます。そのため、`longjmp`を利用し、同じ場所を通らないようにして、プロセスが終了することを防いでいます。~~

### 追記
シグナルハンドラ内で実行している`longjmp`についてご指摘をいただきましたので、修正いたしました。
シグナルハンドラ内の`longjmp`について、[JPCERT CC](https://www.jpcert.or.jp/sc-rules/c-sig32-c.html)で以下のような記載があります。

> シグナルハンドラ内から longjmp() 関数を呼び出した結果、非同期安全でない関数が呼び出されると、未定義の動作につながり、プログラムの完全性が損なわれる可能性がある。このため、longjmp() も POSIX の siglongjmp() も、シグナルハンドラ内から呼び出してはならない。

上記コードでシグナルをハンドルできることがわかりましたが、`singal`関数を利用したシグナルハンドルはあまりオススメされていません。
Man page of SIGNALにも記載はありますが、移植性の低い`signal`は避け、`sigaction`を使えとあります。詳細は、[Man page of SIGNAL](https://linuxjm.osdn.jp/html/LDP_man-pages/man7/signal.7.html)にあります。


では、次に`sigaction`での実装をやってみます。

実際に書いたコードは以下となります。

```c:sigaction_test.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>

void abrt_handler(int sig, siginfo_t *info, void *ctx);
volatile sig_atomic_t eflag = 0;

int main() {
  printf("start %s\n", __func__);

  struct sigaction sa_sigabrt;
  memset(&sa_sigabrt, 0, sizeof(sa_sigabrt));
  sa_sigabrt.sa_sigaction = abrt_handler;
  sa_sigabrt.sa_flags = SA_SIGINFO;

  if ( sigaction(SIGINT, &sa_sigabrt, NULL) < 0 ) {
    exit(1);
  }

  while ( !eflag ) {}

  printf("end %s\n", __func__);
  return 0;
}

void abrt_handler(int sig, siginfo_t *info, void *ctx) {
  // siginfo_tの値が取得できているか確認するために、printfで表示している。
  // 本来、printfは非同期安全でないためここで使用するべきではない。
  printf("si_signo:%d\nsi_code:%d\n", info->si_signo, info->si_code);
  printf("si_pid:%d\nsi_uid:%d\n", (int)info->si_pid, (int)info->si_uid);
  eflag = 1;
}
```

実行結果は以下となります。

```shell:実行結果
[root@ca035c198d1f work]# ./sigaction_test.o
start main
^Csi_signo:2
si_code:128
si_pid:0
si_uid:0
end main
```

以下は、ご指摘を受ける前のNGコードです。NGサンプルとして記載を残しておきます。

```C:sigaction_test.c(NG)
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <setjmp.h>
#include <unistd.h>
#include <string.h>

jmp_buf buf;

void abrt_handler(int sig, siginfo_t *info, void *ctx);

int main() {
  printf("start %s\n", __func__);

  // sigactionに対する設定
  struct sigaction sa_sigabrt;
  memset(&sa_sigabrt, 0, sizeof(sa_sigabrt));
  sa_sigabrt.sa_sigaction = abrt_handler;
  sa_sigabrt.sa_flags = SA_SIGINFO;

  // SIG_ABRTのハンドルを設定する。
  // ハンドルに失敗した場合は、処理を終了する。
  if ( sigaction(SIGABRT, &sa_sigabrt, NULL) < 0 ) {
    exit(1);
  }

  // シグナルハンドル後の復帰ポイントを設定する。
  // 復帰後に再度abortが呼ばれないようにする。
  if ( setjmp(buf) == 0 ) {
    printf("publish abort\n");
    abort();
  }

  printf("end %s\n", __func__);
  return 0;
}

void abrt_handler(int sig, siginfo_t *info, void *ctx) {
  printf("si_signo:%d\nsi_code:%d\n", info->si_signo, info->si_code);
  printf("si_pid:%d\nsi_uid:%d\n", (int)info->si_pid, (int)info->si_uid);
  // メッセージ表示後は、setjmpに復帰する。
  longjmp(buf, 1);
}
```

実行結果は以下となります。

```Shell:実行結果
[root@ca035c198d1f work]# start main
publish abort
si_signo:6
si_code:-6
si_pid:79
si_uid:0
end main
^C
[1]+  Done                    ./sigaction_test.o
```

コードを見てわかると思いますが、`signal`関数よりも設定することが多くなっています。`sigaction`に対して、ハンドル時に実行する関数と`sa_flags`を設定しています。関数の設定については、おおよそ`signal`と同じですが、`sa_flags`には、様々な値を設定することができます。今回は、`SA_SIGINFO`を設定し、シグナルが送信された時の情報を取得できるようにしてみました。

シグナルハンドル時に実行する関数の引数が、`signal`の時とは異なっていると思います。今回は、`sa_flags`に`SA_SIGINFO`を設定したため、情報が格納された`siginfo_t`が引数となります。ハンドル時に、`siginfo_t`の`*info`から実行時のプロセスIDなどが取得できていることがわかると思います。

他にも`ctx`とありますが、今回はここまでは調査していません。

# まとめ
ざっくりとでしたが、シグナルをハンドルすることができるようになりました。今回はabortに対してのみ行なっていますが、他のシグナルにも同様の対応が行えます。

今回記載した内容に関して、signalに対するお作法的なものは正直自信がありませんので、自分はこう書いているよ〜などの指摘やアドバイスがありましたら、教えてください。