【SIGALRM】シグナルハンドラで非同期処理

Apache HTTP Server や Nginx など多くの HTTPサーバでは UNIXシグナル を送信して設定ファイルや各種ログファイルを開き直したりできます。例えば Nginx のマスタープロセスは以下のシグナルをサポートしています。

シグナル 動作
SIGINT, SIGTERM シャットダウン
SIGHUP 設定ファイルの再読み込み
SIGQUIT Gracefully shutdown
SIGUSR1 ログファイルの再読み込み
SIGUSR2 実行ファイルの更新
SIGWINCH 全ての worker processes を Gracefully shutdown

本題ですが, あるHTTPサーバのプラグイン開発でRTSPサーバとの連携実装しようと思っていたのですが、 fd の遅延解放がどうしても必要なタイミングで適切な hook-entry が見つからなかったので、この非同期処理をシグナルを使って実装しました。

sigactionの実装は以下の通り。注意点として singal(2) ではなく sigaction(2) です。

struct sigaction {
	union {
	  __sighandler_t _sa_handler;
	  void (*_sa_sigaction)(int, struct siginfo *, void *);
	} _u;
	sigset_t sa_mask;
	unsigned long sa_flags;
	void (*sa_restorer)(void);
};

timer.it_interval.tv_secで設定した値の間隔でSIGALRMを発生させます。
中ではalarm(2)システムコールを使っていて, timerが切れた時にシグナルがカーネルからプロセスに対して通知されSignalHandlerで操作を行えます。

int main(void)
{
  struct sigaction action;
  struct itimerval timer;
  memset(&action, 0, sizeof(action));

  /* set signal handler */
  action.sa_handler = SignalHandler;
  action.sa_flags = SA_RESTART;
  sigemptyset(&action.sa_mask);
  if(sigaction(SIGALRM, &action, NULL) < 0){
    perror("sigaction error");
    exit(1);
  }

  /* set timer */
  timer.it_value.tv_sec = 1;
  timer.it_value.tv_usec = 0;
  timer.it_interval.tv_sec = 1;
  timer.it_interval.tv_usec =0;
  if(setitimer(ITIMER_REAL, &timer, NULL) < 0){
    perror("setitimer error");
    exit(1);
  }

  printf("waiting timer for fire\n");

  for(;;){};
  return 0;
}

SignalHandler では任意の処理を行う事ができますが制約は多いです。安全なシグナルハンドラを実装するには ――C/C++セキュアコーディング入門(4)から抜粋すると以下があるそうです。

  • 非同期シグナル安全 (async-signal-safe) な関数の呼び出し
  • volatile sig_atomic_t型変数の読み書き
  • シグナルハンドラ内の自動変数の読み書き

malloc(), free() などヒープ領域を使う関数は使用できません。アトミックでない操作はできないということです。
分かり易い例では、 main() で free() 解放中に、偶然シグナルハンドラが発生すると二重解放となる可能性があります。

また, sig_atomic_t型変数の読み書きとありますが, 実際は typedef int なのでなんとも。

typedef int sig_atomic_t; 

SignalHandlerでは signum は 14(SIGALRM) が入ってきます。close(), kill(), recvfrom() などは非同期安全関数なので使用できます。

void SignalHandler(int signum)
{
    close(fd);
    return;
}

Shell Script からシグナルを投げる方法としては trap(1) や kill(1) があります。

kill -l でUNIXシグナルの一覧が確認できます。強制終了の SIGKILL や通常の終了シグナルの SIGTERM は馴染み深いですね。SIGUSR1, SIGUSR2 はアプリケーションで独自に定義できるシグナルです。他にも Signal がこんなにあるとは知らなかったです。

# centos 6.5 
$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX

ちなみに SIGCHLD は fork() 等で生成した子プロセスが終了したタイミングで親プロセスに通知されるシグナルです。


[1] Linuxカーネルでは/arch/arm/include/uapi/asm/signal.hや/arch/arm/include/asm/signal.hにARM実装があります。(github)
[2] ちなみに signal の仕組みは Linux Kernel 0.01 からあります。
[3] shellのtrapについて覚え書き