Linux シグナル入門

2013-11-6, 2013-12-3 (鈴)

6. SIGALRM と sigaction システム・コール

   SIGALRM (= 14, シグアラーム, alarm)

Linux を含む Unix 類はプロセスごとに一つずつセルフタイマーのようなものを持つことができる。 このタイマーはシステム・コール alarm(2) でセットできる。

unsigned int alarm(unsigned int seconds);

引数 seconds の値はタイマーが切れるまでの秒数または 0 である。 0 のときはタイマーをセットしない。 戻り値は,呼び出し前にタイマーがセットされていたならばその残り秒数,セットされていなかったならば 0 である。 呼び出し前にセットされていたタイマーはキャンセルされる。

つまり,引数 seconds として 0 を与えたときは単にタイマーを取り消し,そのタイマーの残り秒数だった値 (または元からタイマーがなければ 0) を返す。

ですから,はじめの予定に追加してタイマーをセットしたいつもりならば,alarm を都合3回呼び出します。 例えば 10 秒後にもタイマーが切れるようにしたいときは,まず i = alarm(0) として今のタイマーの残り秒数を i に得ます。 もしも i < 10 ならば alarm(i) としてはじめの予定どおりの時刻にタイマーが切れるようにします。 i 秒たってタイマーが切れたら alarm(10 - i) として i の設定以来 10 秒後にタイマーがまた切れるようにします。 もし逆ならば i と 10 を入れ替えて同じことをします (各処理は 1 秒よりも十分に短い時間で終わるものとします)。

ではタイマーが切れたとき何が起こるのだろうか?

タイマーが切れたときシグナル SIGALRM が発生する。 SIGALRM に対するデフォルトの動作は (SIGINT などと同じく) プロセスの終了である。

したがって,プロセスの自爆タイマー以外の用途で alarm(2) を使うときは SIGALRM にシグナル・ハンドラを設定する必要があります。

この機構は read(2) や recv(2) のような待ち合わせシステム・コールに時間制限を設けたいとき役に立つ。

read(2) や recv(2) のような処理に長時間かかることがあるシステム・コールはどれもシグナルによってその処理を打ち切らせることができる。 実際,これらのシステム・コールはエラー終了の理由の一つとして

   EINTR (error, interrupted) 

を定義している。 システム・コールがエラーで終了した時,errno がこの値ならば処理がシグナルで打ち切られたことを意味する。 したがって,シグナルとして alarm(2) による SIGALRM を使えば時間制限付きの呼び出しを実現できる。

プログラム例を示す。

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

static void handler(int sig) {}

int main(void)
{
    struct sigaction act = {
        .sa_handler = handler,
        .sa_flags = 0,
    };
    sigemptyset(&act.sa_mask);

    if (sigaction(SIGALRM, &act, NULL) < 0)
        return 1;

    char buf[100];
    alarm(10);
    int n = read(0, buf, 99);
    if (n < 0) {
        perror("read");
        return 2;
    }
    alarm(0);
    buf[n] = '\0';
    printf("%d byte(s): %s\n", n, buf);
   return 0;
}

上記のプログラムでは

    alarm(10);

で 10 秒のタイマーを開始している。 もしも実行開始後 10 秒以内に行入力をすれば,入力したバイト数と行の内容が表示される。

コンパイル&実行例を示す。

01:~/tmp$ gcc -Wall do-timer.c
01:~/tmp$ ./a.out
hello, world.
14 byte(s): hello, world.

01:~/tmp$  

このとき "14 byte(s): hello, world." を printf(3) で表示する手前で

    alarm(0);

でタイマーをキャンセルしていることに注意されたい。

たまたまこのプログラム例ではキャンセルしなくても特に実害はありません。 しかし,一般には想定外の箇所でのタイマーの発動は望ましくありません。 この例ではタイマーが不要になったら取り消す「お手本」としてこの処理を入れています。

もしも実行開始後 10 秒たっても入力がなければ,SIGALRM が発生し,read(2) が EINTR のエラーで打ち切られる。 この例では perror(3) を使って EINTR に対応するエラーメッセージ "Interrupted system call" を表示している。

01:~/tmp$ ./a.out
read: Interrupted system call
21:~/tmp$  

システム・コールをシグナルで打ち切るときハンドラでは特に何もする必要はないから handler(int sig) の本体は空 (つまり {}) とする。

6.1 sigaction

ところで,このプログラムはハンドラを設定するために singal(2) ではなく sigaction(2) を使っている。 signal(2) ではいけないのだろうか?

この話題に踏み込むとき,Unix の一つの暗黒面をのぞきこむことになります。

6.1.1 sigaction 関数の紹介

sigaction(2) は Linux を含む近代的な Unix 類にとって sinal(2) よりも基本的な (つまり signal(2) と等価な処理はもちろん,より広範な設定も可能な) システム・コールである。

int sigaction(int signum, const struct sigaction *act,
              struct sigaction *oldact);

第1引数は設定対象とするシグナルである。 第2,第3引数は sigaction 構造体へのポインタである。 第2引数の構造体では下記に挙げた sa_handler, sa_mask, sa_flags の各メンバを与える。 それらの値がシグナルに対する現在のプロセスでの新しい設定値になる。 第3引数が NULL でなければ,それが指す構造体に今までの設定値が格納される。 戻り値は成功ならば 0, エラーならば -1 である。

struct sigaction {
    void (*sa_handler)(int);
    sigset_t sa_mask;
    int sa_flags;
    ……
};

sa_mask メンバの意義と利用例については §7.2 を参照されたい。 本章のプログラム例では sigemptyset(3) を使ってシグナルの空集合を与えており,sa_mask の機能は使っていない。 構造体の各メンバ値を設定するコードを下記に再掲する。 初期化子があるとき,そこで指示されないメンバはもしあっても自動的に 0 で初期化されることに注意されたい。

    struct sigaction act = {
        .sa_handler = handler,
        .sa_flags = 0,
    };
    sigemptyset(&act.sa_mask);

ではなぜ sa_flags をわざわざ 0 にしているのだろうか?

それは,これこそがプログラムで signal(2) ではなく sigaction(2) を使っている理由だからであり,そのことを強調するために (論理的には不要だがあえて) ここでは .sa_flags = 0 を書いている。

sigaction(2) の API 設計者の意図としては sa_flags メンバのすべてのビットが寝ている 0 のときがオプションなしのデフォルト状態であってしかるべきだっただろう。 もし signal(2) がそうだったらここで単純に signal(2) を使うことができた。 しかし,過去も現在も少なくとも Unix 類の主要な実装で signal(2) の動作がそのデフォルト状態だったことはない。

それに該当するのは後述する System V UNIX の sigset(2) です。 Linux にもありますから単純にこれを使ってすませることもできました。 しかし,そうすることは Linux でははっきり非推奨とされています。

6.1.2 signal 関数の真実

現行の Linux (すなわち GNU/Linux) の glibc 標準ライブラリは signal(2) を BSD UNIX 互換のライブラリ実装にすげ替えている。 すなわち,signal 関数を sigaction(2) で sa_flags メンバに

   SA_RESTART

を与えた場合と等価であるように定義している。

つまり,Linux では signal 関数はシステム・コール signal(2) ではなく本当はライブラリ関数 signal(3) というわけです。 しかも Linux は大半の仕様を System V UNIX に準拠しつつも,BSD UNIX との最大の相違点の一つである signal 関数では BSD 陣営の一派になっています。

SA_RESTART オプションを与えた場合,シグナルを受けた各システム・コールはシグナルで打ち切られた処理を内部で自動的に再開 (restart) する。 本章のプログラム例でいえば,制限時間の 10 秒がたっても read(2) はそのまま入力待ちを続けることになる。 しかし,これは意図したことではない。

自動的な再開にはいくつかの例外があります。 (システム・コールではなくライブラリ関数ですが) 第2章 で sleep(3) がシグナルで打ち切られたことを思い出してください。 詳細は Linux の man ページ signal(7) の Interruption of System Calls and Library Functions by Signal Handlers の節を参照してください。

シグナルを受けたシステム・コールがそのまま打ち切られるようにするために .sa_flags = 0 と指定できることが,本章のプログラム例で sigaction(2) を使っている理由である。 暗黙のうちに .sa_flags = SA_RESTART とする signal 関数を使うことはできない。

ちなみに signal(2) に関して初期の Unix の仕様を引き継ぐ System V UNIX では signal 関数を sigaction 関数で sa_flags メンバに
   SA_RESETHAND | SA_NODEFER
を与えた場合と等価なものとして提供しています (もちろん歴史的に見れば順序が逆です。初期の Unix の仕様を再現するオプションとして SA_RESETHAND と SA_NODEFER が用意されました)。

SA_RESETHAND はハンドラの実行開始時にそのシグナルに対するハンドラ設定をデフォルトにリセットするようにします (reset handler)。 SA_NODEFER はハンドラの実行中にそのシグナルをシグナル・マスクに自動的には追加しないようにします (§7.1 参照),つまり,当該シグナルの処理は遅延されません (no defer)。 このような仕様ですから,初期の Unix ではハンドラは普通,最初に再び signal 関数を使ってハンドラを再設定しました:
handler(sig)
{
    signal(sig, handler);
    ……
}
しかし,もし同じシグナルが立て続けに発生した場合,ハンドラ本体の開始からそこにある signal 関数の呼び出しまでの,設定がデフォルトに戻るわずかな瞬間にシグナルが到来し,プログラムが強制終了したり,シグナルを取りこぼしたりする可能性がどうしても残ります。 これでは信頼性のあるハンドリングはできません。 いわば仕様として固定されてしまったバグです。

バグはバグなのですが歴史的に由緒正しい仕様として,Linux では gcc のオプションに -ansi または -std=xx (例えば -std=c99) を指定してコンパイルすると signal 関数が Sytem V 互換のライブラリ実装にすげ替えられます。 さらに,普段はライブラリ関数に隠蔽されている Linux カーネルの真の signal システム・コールも System V 互換に実装されています。 詳細は Linux の man ページ signal(2) の Portability の節を参照してください。
その一方で System V UNIX は .sa_flags = 0 でハンドラを設定するシステム・コール sigset(2) を定義しています。 Linux の標準ライブラリ glibc も互換性のためにライブラリ関数 sigset(3) としてこれを用意しています。

sigset 関数を設けた意図は,初期の Unix からの残念な互換性を signal 関数にそのまま背負わせつつ,sigaction 関数のデフォルトとして決めたような動作で働く簡易ハンドラ設定関数の決定版を作ることにあったのかもしれません。 初期の Unix と同じ関数名のまま,妥当とはいえ非互換な仕様に改変した BSD のアプローチよりは正攻法といえます。 しかし,現実にはこれが広く Unix 全体に普及することはありませんでした。 もし普及していたら,世の Unix や Linux の入門書はこぞって sigset 関数の紹介からシグナルの説明をはじめていたことでしょう。

Linux 以前の典型的な用法としては,当時,今日で言うオープンソース・ソフトウェアが多く作られた BSD UNIX 向けのプログラムを System V に移植するときに BSD 版 singal 関数のさしあたりの代用品として使われたことが挙げられます。 System V 版 signal 関数とでは SA_RESTART, SA_RESETHAND, SA_NODEFER の三点で動作が違いますが,sigset 関数とならば SA_RESTART かどうかだけの違いです。 マクロ定義で signal を sigset にすげかえてとりあえず動かす移植方法が当時の常套手段でした。 しかし,BSD UNIX の一族に sigset 関数が広まることはありませんでした。 その末裔である Mac OS X にも sigaction(2) や BSD 仕様の signal(3) はありますが sigset 関数はありません。

Linux の man ページでは This API is obsolete: new applications should use the POSIX signal API (この API は時代遅れである。新しいアプリケーションは POSIX のシグナル API を使うべきである) としてその使用を非推奨としています。

ちなみに,Linux プログラミングの有名な参考書の一つである
   R. Love 著 千住訳「Linux システムプログラミング」, 
   オライリー・ジャパン, 2008, ISBN 978-4-97311-362-3
では sigset 関数は §9.4.1「リエントラントなインタフェース」の関数一覧に説明なしに載っているだけです。 なお同書について言えば,シグナルに関して本稿は同書にない事項を多く扱っていますが,同書も本稿にない事項を多く扱っています。 できれば併せてお読みください。

移植性のためには,Linux を含む広範な近代的 Unix 類で標準化され,詳細に動作を指定できる sigaction(2) をシグナル・ハンドラの設定に使うことが望ましい。


7. siglongjmp による大域脱出 へ続く


Copyright (c) 2013 OKI Software Co., Ltd.