はじめに
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で取り扱うシグナルは、標準シグナルとリアルタイムシグナルの2種類があります。また、それぞれのシグナルは32個ずつあり、合計64個シグナルが存在することになります。
この時、標準シグナルは1番〜32番が割り当てられており、リアルタイムシグナルは33番〜64番までが割り当てられています。
Linuxに関してよく聞くシグナルは標準シグナルのSIGINTやSIGKILLなどが該当するかと思います。
シグナルの詳細は、Man page of SIGNALを参照ください。
今回は、SIGABRT SIGINTをベースに記載していきたいと思います。
SIGABRTに関する詳細を知りたい方は、Man page of ABORTを参照ください。
実際にabortをハンドルしてみる
C言語でシグナルをキャッチするには、signal関数とsigaction関数の2つがあります。まずは、signalで確認したいと思います。
実際に書いたコードが以下になります。
# 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;
}
[root@ca035c198d1f work]# ./signal_test.o
start main
^Chandle signal : 2
end main
以下は、ご指摘を受ける前のNGコードです。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);
}
実行結果は以下となります。
[root@ca035c198d1f work]# ./signal_test.o
start main
publish abort
handle signal : 6
end main
コードを見てわかると思いますが、そんなに難しくはありません。
signal関数にハンドルしたいシグナルを定義し、第2引数に関数を指定します。そうすることで、SIGABRTが発生した時に、abrt_handlerが実行されるようになります。また、abrt_handlerが呼ばれるときの第1引数には発生したシグナル番号が入ります。今回は、 "6"が入ります。
今回はabortを発行しているため、シグナルをハンドルした後、同じ場所に戻ってしまうとそのままプロセスが終了してしまいます。そのため、longjmpを利用し、同じ場所を通らないようにして、プロセスが終了することを防いでいます。
追記
シグナルハンドラ内で実行しているlongjmpについてご指摘をいただきましたので、修正いたしました。
シグナルハンドラ内のlongjmpについて、JPCERT CCで以下のような記載があります。
シグナルハンドラ内から longjmp() 関数を呼び出した結果、非同期安全でない関数が呼び出されると、未定義の動作につながり、プログラムの完全性が損なわれる可能性がある。このため、longjmp() も POSIX の siglongjmp() も、シグナルハンドラ内から呼び出してはならない。
上記コードでシグナルをハンドルできることがわかりましたが、singal関数を利用したシグナルハンドルはあまりオススメされていません。
Man page of SIGNALにも記載はありますが、移植性の低いsignalは避け、sigactionを使えとあります。詳細は、Man page of SIGNALにあります。
では、次にsigactionでの実装をやってみます。
実際に書いたコードは以下となります。
# 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;
}
実行結果は以下となります。
[root@ca035c198d1f work]# ./sigaction_test.o
start main
^Csi_signo:2
si_code:128
si_pid:0
si_uid:0
end main
以下は、ご指摘を受ける前のNGコードです。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);
}
実行結果は以下となります。
[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に対するお作法的なものは正直自信がありませんので、自分はこう書いているよ〜などの指摘やアドバイスがありましたら、教えてください。