signal-piping - SIGNALをpipe経由でハンドラからメインスレッドに渡す方法

目的

Linux上(UNIX全般)で「安全な」シグナル処理を実装したい。
本来シグナルハンドラでしてもよい処理は非常に限られており、実質は特定の型のグローバル変数操作と、非同期シグナルセーフ関数の呼び出ししか安全ではない(参考:UNIX上でのC++ソフトウェア設計の定石 (2) - memologue)。
この条件を守ったうえで、多用な処理をするシグナルハンドラを実装したい。

結果

別途、pipeとスレッド(メインスレッドでも良い)を用意して、

  1. シグナルハンドラ: 受け取ったシグナル情報をpipeにwriteするだけ
  2. 別スレッド: 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しか呼んでいないので非同期シグナルセーフであり、メイン側は普通のスレッドなのでどんな処理でも可能。