シグナルの操作(1)

このエントリーをはてなブックマークに追加

ここではシグナルの操作を行うシステムコールについて解説していきます。
シグナルにはSVR(SysV)とBSDで解説した通り、Unixでは異なる大きな2系統歴史があります。現在あるUnixもちろんLinuxもですが、これらを統合した形になっています。
ここではこれらの歴史の詳しい説明までは省きますが、ここを頭においていただければと思います。
さらにシグナルには注意事項が多くありますがこれらも紹介していきます。

ポインタ

関数ポインタ

まず最初に関数ポインタの説明から。関数ポインタはポインタの中でも高度な部類になりますが、皆さん使えますか?
シグナルではいわゆるコールバックの指定に関数ポインタが必要になってきますので簡単に説明をします。

void hoge(void) {
    printf("hoge\n");
}

int main(void)
{
    void (*func)(void);
    func = hoge;
    func();
    
    return 0;
}
カッコが多く少し見辛いですが簡単な話で、"関数の戻り値の型(*関数ポインタ名)(対象関数の引数の型)"の形です。
理解さえすれば特になんてことはないですよね?


ポインタを文章に変換

それでもポインタそのものは苦手? それであればポインタ記述を文章に変換してくれるcdeclというツールがUnixにはあります。ただし当然英語ですが...

$ cdecl
Type `help' or `?' for help
cdecl> explain char *p;
declare p as pointer to char #宣言pはcharへのポインタ
cdecl> explain int *p;
declare p as pointer to int  #宣言pはintへのポインタ
cdecl> explain void (*handler)(void);
declare handler as pointer to function (void) returning void #宣言handlerはvoidを返し、voidの引数を持つ関数へのポインタ
cdecl> explain void (*handler)(int);
declare handler as pointer to function (int) returning void #宣言handlerはvoidを返し、intの引数を持つ関数へのポインタ
"#"以降は筆者の方で訳した部分です。
便利ですね。英語が苦手でもなんとか読めますね。C言語のポインタは仕様上人間には読みづらいのでポインタに慣れるまではこういうツールを使っていきましょう。


受信側シグナル操作

signal(2)

最初はシグナルといえばこれが有名と思われるsignal(2)です。これはSysV系のシステムコールで今となっては古くなっていますが、いまでも良く使われているのではないでしょうか?
これはsignal(2)はシンプルで使いやすいですし、古いので長く使われているので最初の説明にはピッタリでしょう。

まずはプロトタイプから。

#include <signal.h>
//GNU版
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t sighandler);
//オリジナル版
void ( *signal(int signum, void (*handler)(int)) ) (int);

早速、sighandler_tとして関数ポインタが出てきました。
もう理解出来るは思いますが、cdeclを使ってみましょう。
cdecl> explain void (*sighandler_t)(int);
declare sighandler_t as pointer to function (int) returning void #宣言sighandler_tはvoidを返し、intの引数を持つ関数へのポインタ 

簡単ですね?
signal(2)は対象となるシグナルをsignumで指定し、sighandler_tでシグナルが発生した場合のコールバック関数を指定します。
コールバック関数が呼ばれるとそれまでの処理は中断され、コールバック関数が終了すると中断した箇所から処理が再開されます。
#include <signal.h>
#include <unistd.h>

//シグナルが発生すると呼ばれる
void handler(int signo) {
    //signoにはシグナル番号が入る
    char buf[] = "Signal:Catch";
    write(1, buf, sizeof(buf));
}

int main(void)
{
    //ここ以降、SIGINTが発生するとhandlerが呼ばれる
    signal(SIGINT, handler);

    //シグナルが発生するまでスリープで動き続ける
    while(1) {
        sleep(1);
    }

    return 0;
}

プロトタイプを見ると使い方が分かり難いですが、実際は簡単なものです。
動作も非常に簡単ですが、気になるのは表示にprintf(3)ではなくwrite(2)を使っている点です。
これは非常に重要な点で、シグナルのコールバック関数内ではすべての関数は使えないという点です。
安心して使える関数は、非同期シグナルで安全な関数(async-signal-safe functions)と呼び、signal(7)のmanに書かれています。

では、コンパイルして実行して見ましょう
$ gcc -o signal_test signal_test.c
$ ./signal_test
^CSignal:Catch

SIGINTは"Ctrl+c"で発生しますので、プログラムが終了せずにメッセージが表示されます。通常の方法では終了できないので、kill(1)コマンドを使用して終了させてください。
これは前回紹介したsl(1)の動作の再現です。つまりsl(1)はSIGINTにSIG_IGNを設定しているのです。


ユーザ定義以外の設定

コールバック関数にはユーザ定義の関数以外も設定できます。

#include <signal.h>
#include <unistd.h>

int main(void)
{
    //SIGINTは無視される
    signal(SIGINT, SIG_DFL);

    //何らかの処理

    //SIGINTはデフォルトの動作に戻る
    signal(SIGINT, SIG_IGN);

    return 0;
}

実に簡単ですね。まとめるとこのようになります。
名前説明
SIG_IGNシグナルは無視される
SIG_DFLデフォルトの動作が行われる
ユーザ定義の関数シグナルハンドラーが呼ばれる


pause(2)

これは特に難しい物ではなく、先ほどのシグナルを待つ間の処理を無限ループで待っている部分をpause(2)に出来るという物です。
せっかくシステムコールが用意されているのでこちらを使いましょう。

//pause(2)で書き換えた板
#include <signal.h>
#include <unistd.h>

//シグナルが発生すると呼ばれる
void handler(int signo) {
    //signoにはシグナル番号が入る
    char buf[] = "Signal:Catch";
    write(1, buf, sizeof(buf));
}


int main(void)
{
    //ここ以降、SIGINTが発生するとhandlerが呼ばれる
    signal(SIGINT, handler);

    //シグナルが発生するまでスリープしてくれる
    pause();

    return 0;
}


送信側シグナル操作

kill(2)

signal(2)はシグナルを受けた側の処理方法でしたが、これはシグナルを送信するシステムコールです。
kill(2)は特定のプロセスにシグナルを送信できます。kill(1)コマンドは内部でこれを利用しています。

//"kill -kill pid"の再現
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
#include <errno.h>

int main(int argc, char *argv[])
{
        int i;
        int ret;
        pid_t pid;

        if (argc < 2) {
                return 1;
        }

        for(i=1; i<argc; i++) {
                pid = (pid_t)atoi(argv[i]);
                if (pid <= 1) {
                        //念の為、無視
                        continue;
                }
                ret = kill(pid, SIGKILL);
                if (ret < 0) {
                        perror("kill error");
                }
        }

        return 0;
}

送信側は特に難しい物ではありませんね。kill(2)は第1引数でプロセスIDを、第2引数でシグナルを指定します。
関連するシステムコールとしてkillpg(2)があります。これはシグナルをプロセスグループに送ることができます。
引数の指定には通常の指定の他にもバリエーションがあります。
プロセスID(pid)/シグナル番号(sig)指定説明
pid正の値プロセスIDとみなす通常の動作となります
pid0呼び出し元(自分)のプロセスグループ全体にシグナルを送信する
pid-1呼び出し元のプロセスがシグナルを送る許可を持つ全てのプロセスに送信される。ただしinit(pid=1)以外に。
pid-1より小さい値-pidのプロセスグループへシグナルが送信されます。
sig0シグナルは送信されませんが、エラーチェックはされます。つまりプロセスの生存確認ができます。


raise(2)

kill(2)で自分自身にシグナルを送信するにはどうしたら良いでしょうか?
自分自身のプロセスIDを取得してからという手もありますが面倒なので専用のシステムコールが用意されており、それがraise(2)です。

//raise(2)で自分にシグナルを送信する
#include <signal.h>
#include <unistd.h>

void handler(int signo) {
    char buf[] = "Signal:Catch";
    write(1, buf, sizeof(buf));
}

int main(void)
{
    signal(SIGINT, handler);
    raise(SIGINT);

    return 0;
}

これは実行すると直ぐに自分自身にシグナルを送信し、シグナルハンドラが呼ばれて終了します。


alarm(2)

これはraise(2)の異なるバージョンで、シグナルを送信するタイミングをn秒後に指定できるというものです。

#include <signal.h>
#include <unistd.h>

void handler(int signo) {
    char buf[] = "Signal:Catch";
    write(1, buf, sizeof(buf));
}

int main(void)
{
    signal(SIGALRM, handler); //alarm(2)はSIGALRMを送信する
    alarm(1); //1秒後

    return 0;
}

注意点は2つ。alarm(2)はSIGALRMを利用していますが、sleep(3)もSIGALRMを利用している可能性があるので同時利用はできません。
残りは、setitimer(2)の使用禁止です。これは内部的に同じタイマーを共有しているからです。


注意点

リエントラント問題と移植性

シグナルの難しい部分はその動作のややこしさにあります。
すでに説明したシグナルセーフ問題がありますが、それ以外でもいくつかの注意事項があります。リエントラント問題もその1つです。

まず理解する必要があるのはシグナルはそもそも非同期的という点にあります。分かりやすく言えばシグナルはいつ発生するかは不明なのです。1秒後なのが1ms後なのかが分からないのです。
感の良い方は気づかれたとは思いますが、シグナルハンドラが動作している間にもシグナルが発生する可能性があります。これをリエントラントといいます。
また当然ながらシグナルハンドラ内での処理は非常に短い処理を入れる必要があります。これはシグナルハンドラの処理が非常に重ければその分リエントラントが発生しやすくなるからです。
さらに冒頭で説明した通りUnixがSysV系かBSD系かによってもsignal(2)の動作が異なります。
当然移植性の問題も出てきます。

signal(2)のmanにはこのように書かれています。

移植性のある signal()  の使い方は、シグナルの処理方法を SIG_DFL か SIG_IGN に設定する方法だけである。
シグナル・ハンドラを設定するのに signal()  を使ったときの動作はシステムにより異なる
(POSIX.1 は明示的にこの違いを認めている)。
移植性が必要なときはこのシステムコールを使用しないこと。

ではリエントラントが発生した場合は各システムはどのようになるのでしょうか?
リエントラントタイミングSysV系の動作BSD系の動作
システムコールの呼び出し中システムコールはエラーとなり処理は中止する
つまり、シグナルハンドラは呼ばれない
連続でシグナルハンドラが呼ばれる
システムコール処理中デフォルトの動作になる(SIG_DFLに設定される)
オリジナルのUNIXもこの処理方法
シグナルを保留する。
manによれば"同じシグナルのさらなる生成は配送がブロックされる"となる。

ちなみに、Linuxは基本的にSysV系の処理方法を採用していますが、glibcのバージョンによっても異なる動作になります。詳しくはsignal(2)のmanを参照してください。
結論としてsingal(2)は移植性が低く、Linuxでもバージョンによって動作も一様に決めることができません。つまり、signal(2)はもはや使うべきではありません


まとめ

シグナルには紹介したように、非常に厄介な問題があります。またここでは紹介していませんがスレッドと同時に使うとさらに問題があります。(スレッド問題は別途解説します)
次回は今回紹介したシステムによって異なる問題を解決したPOSIXシグナルについて解説していきます。