signal-piping - SIGNALをpipe経由でハンドラからメインスレッドに渡す方法
目的
Linux上(UNIX全般)で「安全な」シグナル処理を実装したい。
本来シグナルハンドラでしてもよい処理は非常に限られており、実質は特定の型のグローバル変数操作と、非同期シグナルセーフ関数の呼び出ししか安全ではない(参考:UNIX上でのC++ソフトウェア設計の定石 (2) - memologue)。
この条件を守ったうえで、多用な処理をするシグナルハンドラを実装したい。
結果
別途、pipeとスレッド(メインスレッドでも良い)を用意して、
- シグナルハンドラ: 受け取ったシグナル情報をpipeにwriteするだけ
- 別スレッド: pipeをreadしてその後の処理を実行
という役割分担にすれば良い。
以下、詳細。
「非同期シグナルセーフ」な関数
System Interfaces Chapter 2にあるように、非同期シグナルセーフな関数は、確かに少ない。
シグナルハンドラ内では使わなそうな関数(bind, chdir, fork,sysconfなど)が多いくせに、printfやmallocなどの「つい使いたくなる関数」はリストに入っていない。
この関数だけを使って、有用な処理が行えるか?
いや、むり。
役割分担を検討
役割分担を変えて、シグナルハンドラでは通知のみを行って、他のスレッドで処理を肩代りすればよい。
では、通知に使えそうな関数は…
- connect/send/sendmsg/sendto → ×同じプロセス内の通信なのにソケット使うのも大げさだ
- open/creat → ×同じプロセス内の通信なのにファイル使うのも大げさだ
- exec*/fork → ×同じプロセス内(ry
- kill/raise/ → ×シグナルハンドラ内からまたシグナル送ってもなあ
- write → ○同じプロセス内なら、pipeが使えるじゃないか!
ということで、pipeにwriteしてハンドラとスレッド間の通知を行うことにする。
全体の構成
おおざっぱには、以下の構成になる。
カーネル → シグナルハンドラ → (pipe) → 別スレッド
通信に使うpipeは、シグナルハンドラの登録前に作成しておき、グローバル変数として持つ(シグナルハンドラからでも、不変なグローバル変数のreadは問題無い)。
実装例
上記構成にそった、ハンドラと別スレッド(この場合はメインスレッド)の例:
- signal-piping.c
- https://sssvn.jp/svn/spikelet/c/signal-piping/signal-piping.c
#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <signal.h> static int sighdr_pipe[2]; static int sighdr(int signo, siginfo_t *info, void *w) { write(sighdr_pipe[1], info, sizeof(siginfo_t)); return 0; } static void set_handler(void) { struct sigaction act; act.sa_handler = (void(*)(int))sighdr; act.sa_flags = SA_SIGINFO; sigaction(SIGUSR1, &act, NULL); } int main(void) { printf("signal-piping runs as pid = %d.\n", getpid()); pipe(sighdr_pipe); fcntl(sighdr_pipe[0], F_SETFL, fcntl(sighdr_pipe[0], F_GETFL, 0) | O_NONBLOCK); set_handler(); while (1) { siginfo_t info; if (read(sighdr_pipe[0], &info, sizeof(siginfo_t)) > 0) { printf("siginfo: si_fd=%d, si_signo=%d\n", info.si_fd, info.si_signo); } printf("...\n"); sleep(1); } }
実行例は下記:
% ./signal-piping & % signal-piping runs as pid = 20481. ... ... kill -USR1 20481... siginfo: si_fd=500, si_signo=10 % ...
メイン側では、pipeを作成して非同期モードにしたあと(これをしないとwrite時にブロックされてしまうので)で、pipeからのデータがあればを待ち、あれば内容を表示している。
シグナルハンドラ側はwriteしか呼んでいないので非同期シグナルセーフであり、メイン側は普通のスレッドなのでどんな処理でも可能。