home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

C言語系/「デーモン君のソース探検」読書メモ/A08, pause(3), sigsuspend(2)

C言語系/「デーモン君のソース探検」読書メモ/A08, pause(3), sigsuspend(2)

C言語系 / 「デーモン君のソース探検」読書メモ / A08, pause(3), sigsuspend(2)
id: 848 所有者: msakamoto-sf    作成日: 2010-11-23 19:15:41
カテゴリ: BSD C言語 

お題:pause(3)関数を使う上での問題点と、sigsuspend(2)による対処法を検討せよ

※この章は「デーモン君のソース探検」に載っていませんが、msakamoto-sf自身が個人的に興味を持って調べ、"Appendix"として読書メモシリーズに入れてありますのでご注意下さい。

(今回の内容は、"Advanced UNIX Programming 2nd Ed"(AUP), "9.2 Waiting for Signal" のパクリです。)


pause(3)の使い方

何かしらのシグナルを受信し、シグナルハンドラが実行されるまで待機する。

pause1.c:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
 
void sig_usr(int signo)
{
        printf("sig_usr : signo=%d\n", signo);
}
 
int main(int argc, char *argv[])
{
        struct sigaction sa;
        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = sig_usr;
        if (-1 == sigaction(SIGUSR1, &sa, NULL)) {
                perror("sigaction");
                exit(1);
        }
        printf("pid = %d, pause start...\n", getpid());
        pause();
        printf("pause end.\n");
        return 0;
}
$ make pause1  # 手抜きしてmakeのデフォルトルールでコンパイル
pid = 797, pause start...
(別端末から kill -USR1 797)
sig_usr : signo=30
pause end.
$

pause(3)のソースコード

$ locate pause
...
/usr/src/lib/libc/gen/pause.3
/usr/src/lib/libc/gen/pause.c
...

/usr/src/lib/libc/gen/pause.c:

/*
 * Backwards compatible pause.
 */
int
pause()
{
        sigset_t omask;
 
        /* 現在のシグナルマスクを取得 */
        if (sigprocmask(SIG_BLOCK, NULL, &omask) == -1)
                return -1;
 
        /* omaskにセットされているもの「以外の」シグナルを
           受信するまで待機 */
        return sigsuspend(&omask);
}

以上より、pause(3)の仕組みは次の2ステップになる。

  1. 現在block中のシグナルマスクを取得し、
  2. blockしていないシグナルを受信してシグナルハンドラから戻るまで待機

今回のお題については、ここまで・・・としたいところだが、お題の内容が「pause(3)の問題点とsigsuspend(2)による対処法」である以上、このままでは不十分である。
pause(3), sigsuspend(2) ともにシグナルが到着するまで待機する点は共通だが、pause(3)の使用が問題となる場面、およびsigsuspend(2)による対処法を "Advanced UNIX Programming 2nd Ed" "9.2 Waiting for Signal" をお手本に読み解いてみる。

いくつかのシグナル系関数の復習

ウォーミングアップとして単純なサンプル(codepiece)をいくつか作成し、シグナル系の関数を復習してみる。
なお各システムコールや関数のプロトタイプ・引数・戻り値の仕様などは省略する。各自manpageを参照のこと。

(以下のソースコードではシグナルハンドラ中でasync-signal-safeでないprintf()を使っている。本来、シグナルハンドラ中ではwrite()などasync-signal-safeな関数のみを使う。今回はサンプルコードということで簡略のためprintf()を使っている。実務でのプログラミングにおいては注意が必要。)

sigaction()の復習

sigaction()を復習してみる。

SIGUSR1で実験 : getchar()中にSIGUSR1を受けて終了

sig01.c:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
 
void sig_usr(int signo)
{
        printf("sig_usr : signo=%d\n", signo);
}
 
int main(int argc, char *argv[])
{
        struct sigaction sa;
        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = sig_usr;
        if (-1 == sigaction(SIGUSR1, &sa, NULL)) {
                perror("sigaction");
                exit(1);
        }
        printf("my pid = %d\n", getpid());
        if (EOF == getchar()) {
                perror("getchar()");
        }
        return 0;
}
$ make sig01 # makeのデフォルトルールでコンパイル:手抜き。
$ ./sig01
my pid = 760
(別端末:$ kill -USR1 760)
sig_usr : signo=30
getchar(): Interrupted system call
$ # getchar()の入力を待たずに終了
SIGUSR1で実験その2 : SA_RESTART付けてgetchar()続行

sig02.c:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
 
void sig_usr(int signo)
{
        printf("sig_usr : signo=%d\n", signo);
}
 
int main(int argc, char *argv[])
{
        struct sigaction sa;
        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = sig_usr;
        sa.sa_flags = SA_RESTART;
        if (-1 == sigaction(SIGUSR1, &sa, NULL)) {
                perror("sigaction");
                exit(1);
        }
        printf("my pid = %d\n", getpid());
        if (EOF == getchar()) {
                perror("getchar()");
        }
        return 0;
}

シグナルを受けてシグナルハンドラが実行された後も、getchar()は中断されずに入力を待ち続ける。

$ make sig02
$ ./sig02
my pid = 769
(別端末:$ kill -USR1 769)
sig_usr : signo=30
(別端末:$ kill -USR1 769, 二回目)
sig_usr : signo=30
a
$

sigprocmask()の復習

sigprocmask()を復習してみる。

sig03.c:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
 
void sig_usr(int signo)
{
    printf("sig_usr : signo=%d\n", signo);
}
 
int main(int argc, char *argv[])
{
    int c;
    sigset_t new_ss, old_ss;
    struct sigaction sa;
 
 
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = sig_usr;
    sa.sa_flags = SA_RESTART;
    if (-1 == sigaction(SIGUSR1, &sa, NULL)) {
        perror("sigaction"); exit(1);
    }
    printf("my pid = %d\n", getpid());
 
    /* SIGUSR1をブロックし、受信しても
       保留状態(pending)にして、シグナルハンドラ
       を実行しない */
    sigemptyset(&new_ss);
    sigaddset(&new_ss, SIGUSR1);
    if (-1 == sigprocmask(SIG_BLOCK, &new_ss, &old_ss)) {
        perror("sigprocmask"); exit(1);
    }
 
    printf("input char:");
    c = getchar();
    printf("your input : %c\n", c);
 
    /* SIGUSR1ブロック前の状態に戻す
       = SIGUSR1のブロックを解除する。すでにSIGUSR1を受信し
       pendingになっていた場合は、この後直ちにSIGUSR1の
       シグナルハンドラが実行される */
    if (-1 == sigprocmask(SIG_SETMASK, &old_ss, NULL)) {
        perror("sigprocmask"); exit(1);
    }
 
    return 0;
}

実行し、getchar()中にSIGUSR1を送信してみる:

$ make sig03
cc -O2   -o sig03 sig03.c
$ ./sig03
my pid = 1019
(別端末から kill -USR1 1019)
input char:a
your input : a
sig_usr : signo=30
$

block解除後にSIGUSR1のシグナルハンドラが実行されている。

sigprocmask()とpause()を組み合わせてみる
  1. SIGUSR1とSIGUSR2にシグナルハンドラをセットする。
  2. SIGUSR1をブロックする。
  3. ブロック中にpause()を呼び、SIGUSR2の受信を期待して待機する。
  4. pause()復帰後、SIGUSR1のブロックを解除する。

sig04.c:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
 
void sig_usr(int signo)
{
        printf("sig_usr : signo=%d\n", signo);
}
 
int main(int argc, char *argv[])
{
        int c;
        sigset_t new_ss, old_ss;
        struct sigaction sa;
 
        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = sig_usr;
        sa.sa_flags = SA_RESTART;
        if (-1 == sigaction(SIGUSR1, &sa, NULL)) {
                perror("sigaction"); exit(1);
        }
        if (-1 == sigaction(SIGUSR2, &sa, NULL)) {
                perror("sigaction"); exit(1);
        }
        printf("my pid = %d\n", getpid());
 
        sigemptyset(&new_ss);
        sigaddset(&new_ss, SIGUSR1);
        if (-1 == sigprocmask(SIG_BLOCK, &new_ss, &old_ss)) {
                perror("sigprocmask"); exit(1);
        }
 
        printf("pause start...\n");
        pause();
        printf("pause end.\n");
 
        if (-1 == sigprocmask(SIG_SETMASK, &old_ss, NULL)) {
                perror("sigprocmask"); exit(1);
        }
 
        return 0;
}

実行し、pause()中にSIGUSR1, SIGUSR2を順に送信してみる:

$ make sig04
cc -O2   -o sig04 sig04.c
$ ./sig04
my pid = 842
pause start...
(別端末から kill -USR1 842)
(別端末から kill -USR2 842)
sig_usr : signo=31
pause end.
sig_usr : signo=30
$

SIGUSR1送信時点ではpause()が解除されず、SIGUSR2を送信してシグナルハンドラ実行後pause()から復帰、ブロックされていたSIGUSR1がシグナルハンドラに送られたことがわかる。

sigprocmask()とpause()を組み合わせてみる、ただしSIGUSR2のハンドラは未設定

sig04.cと同じだが、SIGUSR2には特にシグナルハンドラを設定せずデフォルトのままとしている。
sig05.c:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
 
void sig_usr(int signo)
{
        printf("sig_usr : signo=%d\n", signo);
}
 
int main(int argc, char *argv[])
{
        int c;
        sigset_t new_ss, old_ss;
        struct sigaction sa;
 
        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = sig_usr;
        sa.sa_flags = SA_RESTART;
        if (-1 == sigaction(SIGUSR1, &sa, NULL)) {
                perror("sigaction"); exit(1);
        }
        printf("my pid = %d\n", getpid());
 
        sigemptyset(&new_ss);
        sigaddset(&new_ss, SIGUSR1);
        if (-1 == sigprocmask(SIG_BLOCK, &new_ss, &old_ss)) {
                perror("sigprocmask"); exit(1);
        }
 
        printf("pause start...\n");
        pause();
        printf("pause end.\n");
 
        if (-1 == sigprocmask(SIG_SETMASK, &old_ss, NULL)) {
                perror("sigprocmask"); exit(1);
        }
 
        return 0;
}

実行し、pause()中にSIGUSR1, SIGUSR2を順に送信してみる:

$ make sig05
cc -O2   -o sig05 sig05.c
$ ./sig05
my pid = 863
pause start...
(別端末から kill -USR1 863)
(別端末から kill -USR2 863)
User defined signal 2

SIGUSR2受信時点でデフォルトのシグナルハンドラが処理され、プロセスが終了したことが分かる。

sigsuspend()の復習

ブロックしたいsigset_tを渡すと一時的にシグナルマスクを切り替え、ブロック対象外のシグナルを受信するまで待機する。
シグナルマスクの切り替えと待機までをatomicに行ってくれる。
sig06.c:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
 
void sig_usr(int signo)
{
        printf("sig_usr : signo=%d\n", signo);
}
 
int main(int argc, char *argv[])
{
        int c;
        sigset_t new_ss, old_ss;
        struct sigaction sa;
 
        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = sig_usr;
        sa.sa_flags = SA_RESTART;
        if (-1 == sigaction(SIGUSR1, &sa, NULL)) {
                perror("sigaction"); exit(1);
        }
        if (-1 == sigaction(SIGUSR2, &sa, NULL)) {
                perror("sigaction"); exit(1);
        }
        printf("my pid = %d\n", getpid());
 
        sigemptyset(&new_ss);
        sigaddset(&new_ss, SIGUSR1);
 
        printf("sigsuspend start...\n");
        sigsuspend(&new_ss);
        printf("sigsuspend end.\n");
 
        return 0;
}

実行し、SIGUSR1, SIGUSR2と順に送信してみる:

$ make sig06
cc -O2   -o sig06 sig06.c
$ ./sig06
my pid = 882
sigsuspend start...
(別端末から kill -USR1 882)
(別端末から kill -USR2 882)
sig_usr : signo=31
sig_usr : signo=30
sigsuspend end.
$

SIGUSR1を送信しても、一時的にblockされているので反応しない。SIGUSR2を送信するとシグナルハンドラが実行されsigsuspend()から復帰する。このとき自動的にSIGUSR1をblockしていたマスクも解除されるので、即座にSIGUSR1の分のシグナルハンドラが実行される。"sigsuspend end."の表示は、その後に出力される。

sigsuspend()の復習、ただしSIGUSR2のシグナルハンドラ未設定

sig06.cと同様だが、SIGUSR2のシグナルハンドラを未設定にしておく。
sig07.c:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
 
void sig_usr(int signo)
{
        printf("sig_usr : signo=%d\n", signo);
}
 
int main(int argc, char *argv[])
{
        int c;
        sigset_t new_ss, old_ss;
        struct sigaction sa;
 
        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = sig_usr;
        sa.sa_flags = SA_RESTART;
        if (-1 == sigaction(SIGUSR1, &sa, NULL)) {
                perror("sigaction"); exit(1);
        }
        printf("my pid = %d\n", getpid());
 
        sigemptyset(&new_ss);
        sigaddset(&new_ss, SIGUSR1);
 
        printf("sigsuspend start...\n");
        sigsuspend(&new_ss);
        printf("sigsuspend end.\n");
 
        return 0;
}

実行し、SIGUSR1, SIGUSR2と順に送信してみる:

$ make sig07
cc -O2   -o sig07 sig07.c
$ ./sig07
my pid = 895
sigsuspend start...
(別端末から kill -USR1 895)
(別端末から kill -USR2 895)
User defined signal 2
$

pause()のときと同じく、SIGUSR2のデフォルトハンドラに処理されたため、SIGUSR2受信によりプロセスが終了している。

sigfilset() -> sigsuspend() : 無限wait

sigsuspend()に渡すsigset_tだが、全てのシグナルをセットするとどうなるだろうか?
全てのシグナルをblockするため、SIGTERMとSIGKILL以外は反応しない無限waitが予想される。

sig08.c:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
 
void sig_usr(int signo)
{
        printf("sig_usr : signo=%d\n", signo);
}
 
int main(int argc, char *argv[])
{
        int c;
        sigset_t new_ss, old_ss;
        struct sigaction sa;
 
 
        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = sig_usr;
        sa.sa_flags = SA_RESTART;
        if (-1 == sigaction(SIGUSR1, &sa, NULL)) {
                perror("sigaction"); exit(1);
        }
        printf("my pid = %d\n", getpid());
 
        sigfillset(&new_ss);
 
        printf("sigsuspend start...\n");
        sigsuspend(&new_ss);
        printf("sigsuspend end.\n");
 
        return 0;
}

実行し、いくつか送信してみる:

$ make sig08
cc -O2   -o sig08 sig08.c
$ ./sig08
my pid = 908
sigsuspend start...
(別端末から kill -USR1 -> 無反応)
(別端末から kill -USR2 -> 無反応)
(別端末から kill -INT -> 無反応)
(別端末から kill -HUP -> 無反応)
(別端末から kill -KILL)
Killed
$

SIGINT, SIGHUP, SIGQUIT, SIGTERM, SIGCONT, SIGALRM, SIGABRT, SIGBUS, SIGSEGV などが無反応。
SIGSTOPを送るとバックグランドジョブに切り替わる。
SIGTERMすら無反応なので、終了にはSIGKILLを送信する。

sigemptyset() -> sigsuspend() : とにかく何でも良いのでシグナル受信で待機解除

sigsuspend()に渡すsigset_tだが、空のsigset_tを渡すと、全シグナルでblock解除となる。
何でもよいのでシグナルを受信したら待機解除したい場合に有効だろう。
sig09.c:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
 
void sig_usr(int signo)
{
        printf("sig_usr : signo=%d\n", signo);
}
 
int main(int argc, char *argv[])
{
        int c;
        sigset_t new_ss, old_ss;
        struct sigaction sa;
 
        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = sig_usr;
        sa.sa_flags = SA_RESTART;
        if (-1 == sigaction(SIGUSR1, &sa, NULL)) {
                perror("sigaction"); exit(1);
        }
        printf("my pid = %d\n", getpid());
 
        sigemptyset(&new_ss);
 
        printf("sigsuspend start...\n");
        sigsuspend(&new_ss);
        printf("sigsuspend end.\n");
 
        return 0;
}

実行してみる : SIGUSR1でシグナルハンドラ実行後、待機解除となっている。

$ make sig09
cc -O2   -o sig09 sig09.c
$ ./sig09
my pid = 929
sigsuspend start...
(別端末から kill -USR1 929)
sig_usr : signo=30
sigsuspend end.
$

pause(3)が問題となる場面とsigsuspend(2)による対処

異なる点
  • pause(3)はシグナルマスクを選べない。
  • sigsuspend(2)はシグナルマスクを選べる上に、シグナルマスクの変更・待機・復帰時にシグナルマスクを戻す、までをatomicに行ってくれる。
pause(3)が問題となる場面

上で述べた異なる点と関連して、一時的にblockしたいシグナルを変更してシグナル受信を待機するときはpause(3)だと問題が発生する。

/* (1) */ sigprocmask(SIG_SETMASK, &temp_block_sigset, &old);
/* (2) */ pause();
/* (3) */ sigprocmask(SIG_SETMASK, &old, NULL);

このようにシグナルマスクの変更(1)と待機(2)が分離してしまうため、(1)と(2)の間に、不測のシグナルを受信しシグナルハンドラが実行される可能性がある。もしそのシグナルが、pause()が期待しているものだとすれば、pause()で待機する前にシグナルハンドラを実行し終えてしまうため、本来望んでいたタイミングから外れてしまう。もしも一度しか受信されないものだとすれば、二度と来ないシグナルをpause()で無限に待機してしまう可能性がある。

sigsuspend(2)による対処

上記pause(3)の問題を解消するのがsigsuspend(2)となり、内部的にatomic処理される。

sigsuspend(&temp_block_sigset);

よって上記pause(3)のようなタイミングずれに伴う問題を回避できる。

実例 : 親子プロセスでfork()直後に同期をとる例

"Advanced UNIX Programming 2nd Ed", "9.2.3 sigsuspend System Call"を元ネタとして、やや噛み砕いてみていく。

ex01:

void foo(void)
{
  if (0 == fork()) {
    printf("child\n");
    exit(EXIT_SUCCESS);
  }
  printf("parent\n");
  return;
}

ここで確実に

parent
child

の順に表示されるようにしたい。つまり、子プロセスは親プロセスの準備が整うまで待機したい。

(以降、変数宣言や初期化、エラー処理などを大幅に省略した、擬似コードに近いソースコードで解説を続けます)

シグナルを使うパターンだと、一番単純なのが子プロセスがpause()で待機、親プロセスが無害なシグナルを送信して子プロセスの待機を解除する方式が考えられる。

ex02:

/* とりあえず中身は空っぽでよい */
void sighandler(int signo) {}

void foo(void)
{
  pid_t pid;
  if (0 == (pid = fork())) {        /* (1) */
    struct sigaction sa;
    sa.sa_handler = sighandler;
    sigaction(SIGUSR1, &sa, NULL);  /* (2) */
    pause();                        /* (3) */
    printf("child\n");
    exit(EXIT_SUCCESS);
  }
  printf("parent\n");
  kill(pid, SIGUSR1);
  return;
}

しかし、"(1) - (2)"の間 or "(2) - (3)"の間、つまり子プロセスがpause()を呼ぶ前に親プロセスがSIGUSR1を送信する可能性があり、その場合は期待した動作にならない。

まずは"(2) - (3)"間でSIGUSR1が送信された場合にそなえ、グローバルなフラグ変数を導入する。
ex03:

volatile sig_atomic_t got_sig;
void sighandler(int signo) { got_sig = 1; }

void foo(void)
{
  pid_t pid;
  got_sig = 0;
  if (0 == (pid = fork())) {        /* (1) */
    struct sigaction sa;
    sa.sa_handler = sighandler;
    sigaction(SIGUSR1, &sa, NULL);  /* (2) */
    while (!got_sig) {
      pause();                      /* (3) */
    }
    printf("child\n");
    exit(EXIT_SUCCESS);
  }
  printf("parent\n");
  kill(pid, SIGUSR1);
  return;
}

これにより、"(2) - (3)"間でSIGUSR1を受信した場合はpause()せずに"child"を出力する。なぜならSIGUSR1を受信したということはparent側が準備完了したことを意味するからだ。

しかしまだ "(1) - (2)" 間でSIGUSR1を受信してしまう可能性は残っている。そこで、子プロセスにfork()した直後にSIGUSR1をsigprocmask()でblockし、pause()の直前でSIGUSR1のblockを解除してみる。
ex04:

volatile sig_atomic_t got_sig;
void sighandler(int signo) { got_sig = 1; }

void foo(void)
{
  pid_t pid;
  got_sig = 0;
  if (0 == (pid = fork())) {        /* (1) */

    sigset_t newss, oldss;
    sigemptyset(&newss);
    sigaddset(&newss, SIGUSR1);
    sigprocmask(SIG_BLOCK, &newss, &oldss);  /* (2) */

    struct sigaction sa;
    sa.sa_handler = sighandler;
    sigaction(SIGUSR1, &sa, NULL);  /* (3) */

    sigprocmask(SIG_SETMASK, &oldss, NULL);  /* (4) */
    while (!got_sig) {
      pause();                      /* (5) */
    }
    printf("child\n");
    exit(EXIT_SUCCESS);
  }
  printf("parent\n");
  kill(pid, SIGUSR1);
  return;
}

しかしこれもまだ十分ではない。"(1)-(2)"の間に親プロセスがSIGUSR1を送信してしまう可能性がある。
つまり子プロセスがfork()した時点で、すでにSIGUSR1がblockされている必要がある。

ここで、fork()時にはシグナルマスクがコピーされる仕様を利用し、fork()する前にSIGUSR1をblockしてしまう。
ex05:

volatile sig_atomic_t got_sig;
void sighandler(int signo) { got_sig = 1; }

void foo(void)
{
  pid_t pid;
  got_sig = 0;
  sigset_t newss, oldss;
  sigemptyset(&newss);
  sigaddset(&newss, SIGUSR1);
  sigprocmask(SIG_BLOCK, &newss, &oldss);

  if (0 == (pid = fork())) {        /* (1) */
    struct sigaction sa;
    sa.sa_handler = sighandler;
    sigaction(SIGUSR1, &sa, NULL);  /* (2) */
    sigprocmask(SIG_SETMASK, &oldss, NULL);  /* (3) */
    while (!got_sig) {
      pause();                      /* (4) */
    }
    printf("child\n");
    exit(EXIT_SUCCESS);
  }
  printf("parent\n");
  kill(pid, SIGUSR1);
  return;
}

しかしまだ "(3) - (4)" の間でSIGUSR1が送信される可能性がある。"(3)"が無ければこの「隙間」も消えるが、SIGUSR1はblockされたままなのでSIGUSR1受信によるpause()解除も行えなくなってしまう。

「一時的にblockするシグナルマスクを変更→シグナル受信待機」をatomicに行いたい、つまりsigsuspend(2)の出番となる。

ex06:

volatile sig_atomic_t got_sig;
void sighandler(int signo) { got_sig = 1; }

void foo(void)
{
  pid_t pid;
  got_sig = 0;
  sigset_t newss, oldss;
  sigemptyset(&newss);
  sigaddset(&newss, SIGUSR1);
  sigprocmask(SIG_BLOCK, &newss, &oldss);

  if (0 == (pid = fork())) {        /* (1) */
    struct sigaction sa;
    sa.sa_handler = sighandler;
    sigaction(SIGUSR1, &sa, NULL);  /* (2) */
    sigsuspend(&oldss);
    printf("child\n");
    exit(EXIT_SUCCESS);
  }
  printf("parent\n");
  kill(pid, SIGUSR1);
  return;
}

以上がpause(3)が不適切で、sigsuspend(2)により対処可能な実例である。

sigwait(3) (pthread_signal(3)) の利用

Real Time Signal と Thread がサポートされている場合、より簡単に使えるsigwait(3)が利用できるかもしれない。

int sigwait(const sigset_t *restrict set, int *restrict sig);

受信したいシグナルをsetに設定する。シグナルマスクの変更と受信待機をatomicに処理してくれる。
アプリケーション側でシグナルハンドラを設定していないシグナルについても対応しているのが嬉しい。

(なおsigwait(3)は「デーモン君のソース探検」で使っているNetBSD 1.6では未対応。以降のサンプルはCentOS 5.x上で確認している。)

もし対象シグナルがblockされていて、sigwait()呼び出し前にすでに受信されてpending状態になっていれば、sigwait()は待機せずに呼び出し元に戻り、シグナル番号をsig引数にセットする。
もし対象シグナルがblockされていて、sigwait()呼び出し前にまだ受信されていなければ、sigwait()は受信するまで待機する。受信後、sigwait()はシグナルハンドラに配送される前にシグナルをクリアしてしまう点に注意が必要。

sigwait()呼び出し前に対象シグナルがblockされていない、シグナルマスクに設定されていない場合、アプリケーションがシグナルハンドラを登録していなければデフォルトのシグナルハンドラが実行され、登録していればsigwait()から戻るが、sigwait()がシグナルをクリアしてしまうのでアプリケーションが登録したシグナルハンドラは実行されない。ただしこれはCentOS5.xで実験した結果に基づいており、AUPやAPUEでは事前のシグナルマスクによるblock無しでのsigwait()は推奨されていない。

事前block無しでsigwait(3)を使ってみる

sig10.c:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
 
void sig_usr(int signo) {
    printf("sig_usr: signo = %d\n", signo);
}
 
int main(int argc, char *argv[])
{
    int signum;
    sigset_t target_ss;
    struct sigaction sa;
 
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = sig_usr;
    sa.sa_flags = SA_RESTART;
    sigaction(SIGUSR1, &sa, NULL);
 
    printf("my pid = %d\n", getpid());
 
    sigemptyset(&target_ss);
    sigaddset(&target_ss, SIGUSR1);
    sigaddset(&target_ss, SIGUSR2);
 
    printf("sigwait start...\n");
    sigwait(&target_ss, &signum);
    printf("sigwait end, signum = %d\n", signum);
 
    return 0;
}

コンパイル・実行し、アプリケーション側でシグナルハンドラをインストールしているSIGUSR1を別端末から送信してみる:

$ make sig10
$ ./sig10
my pid = 3991
sigwait start...
(別端末から kill -USR1)
sigwait end, signum = 10
$

sigwait()から戻ってきているが、シグナルハンドラは実行されていない。

続いて、アプリケーション側でシグナルハンドラをインストールしていないSIGUSR2を別端末から送信してみる:

$ ./sig10
my pid = 3992
sigwait start...
(別端末から kill -USR2)
ユーザ定義シグナル 2
$

システムのデフォルト挙動となり、sigwait()から戻ったのか戻っていないのか不明なままプロセスが終了した。

事前block後にsigwait(3)を使ってみる

SIGUSR1, SIGUSR2 をblockした後、sigwait()を呼んでみる。SIGUSR1にはアプリケーション側でシグナルハンドラをインストールしておく。

sig11.c:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
 
void sig_usr(int signo) {
    printf("sig_usr: signo = %d\n", signo);
}
 
int main(int argc, char *argv[])
{
    int signum;
    sigset_t target_ss, old_ss;
    struct sigaction sa;
 
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = sig_usr;
    sa.sa_flags = SA_RESTART;
    sigaction(SIGUSR1, &sa, NULL);
 
    printf("my pid = %d\n", getpid());
 
    sigemptyset(&target_ss);
    sigaddset(&target_ss, SIGUSR1);
    sigaddset(&target_ss, SIGUSR2);
    sigprocmask(SIG_BLOCK, &target_ss, &old_ss);
    printf("SIGUSR1, SIGUSR2 blocking, hit return then sigwait start:");
    getchar();
 
    printf("sigwait start...\n");
    sigwait(&target_ss, &signum);
    printf("sigwait end, signum = %d\n", signum);
 
    printf("hit return then unblock SIGUSR1, SIGUSR2:");
    getchar();
 
    sigprocmask(SIG_SETMASK, &old_ss, NULL);
 
    printf("end.\n");
    return 0;
}

コンパイル・実行してみる。まずはsigwait()を呼ぶ前にSIGUSR1を送信しておく。

$ make sig11
$ ./sig11
my pid = 4615
SIGUSR1, SIGUSR2 blocking, hit return then sigwait start:
(別端末から kill -USR1) (RETURN)
sigwait start...
sigwait end, signum = 10
hit return then unblock SIGUSR1, SIGUSR2:
end.
$

既にpending状態になっていたため、sigwait()は待機せず戻る。sigwait()によりクリアされたのでSIGUSR1はシグナルハンドラに配送されていない。

sigwait()の後にSIGUSR1を送信してみる。

$ ./sig11
my pid = 4681
SIGUSR1, SIGUSR2 blocking, hit return then sigwait start:
sigwait start...
(待機に入る)
(別端末から kill -USR1)
sigwait end, signum = 10
hit return then unblock SIGUSR1, SIGUSR2:
end.
$

待機に入った後、別端末からSIGUSR1を送信するとsigwait()より戻り、シグナルハンドラは実行されず処理続行となっている。

アプリケーション側でシグナルハンドラを未設定の、SIGUSR2をsigwait()実行前に送信してみる。

$ ./sig11
my pid = 4708
SIGUSR1, SIGUSR2 blocking, hit return then sigwait start:
(別端末から kill -USR2) (RETURN)
sigwait start...
sigwait end, signum = 12
hit return then unblock SIGUSR1, SIGUSR2:
end.
$

SIGUSR1のときと同様である。

結果は省略するが、sigwait()呼出し後にSIGUSR2を送信した場合もSIGUSR1と同様である。

実例 : 親子プロセスでfork()直後に同期をとる例でsigwait()を使ってみる

sigwait()を使うことで、アプリケーション側でのシグナルハンドラのインストールが不要となる。

ex07:

void foo(void)
{
  pid_t pid;
  sigset_t newss, oldss;
  sigemptyset(&newss);
  sigaddset(&newss, SIGUSR1);
  sigprocmask(SIG_BLOCK, &newss, &oldss);

  if (0 == (pid = fork())) {
    int signo;
    sigwait(&newss, &signo);
    printf("child\n");
    exit(EXIT_SUCCESS);
  }
  printf("parent\n");
  kill(pid, SIGUSR1);
  return;
}

まとめ

以上でpause(3)関数を使う上での問題点と、sigsuspend(2)による対処法が判明した。

  1. どちらもシグナルを受信するまで待機する機能がある。
  2. pause(3)関数は待機解除となるシグナルを選択できない(現在のシグナルマスクに依存)が、sigsuspend(2)は選択できる。
  3. sigsuspend(2)はシグナルマスクの変更・待機・復帰時のシグナルマスクの復元をatomicに実行できる。
  4. 一時的にシグナルマスクを変更して待機したい場合、pause(3)関数ではsigprocmask(2)と組み合わせる必要があり、どうしてもタイミングの問題("window of time")が発生する。
    1. その場合は、atomicに処理してくれるsigsuspend(2)を使うとよい。
  5. RealTimeSingalとThreadがサポートされているならば、sigwait(3)を使ってもよい。

今回のお題については、ここまで。



プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2010-11-24 09:22:43
md5:4235f249c9ce3bb0286aeb6604b25ef6
sha1:32b221e346196b8736e8f61b65a38d478fd1258c
コメント
コメントを投稿するにはログインして下さい。