システムプログラム(第5回)

情報学群情報科学類
大山 恵弘

理解を深めるために

目次

  1. シグナルの基礎知識
  2. シグナルの利用
  3. シグナルの応用
  4. インターバルタイマ

シグナルの基礎知識

概要

プロセスは,オペレーティングシステムがコンピュータ(プロセッサ)を抽象化し,使いやすくしたものである. コンピュータは入出力機器からのイベント通知を割り込みという仕組みで受け取る. プロセスには,割り込みに相当するイベント通知のメカニズム(ソフトウェア割り込み)として,シグナルが提供されている.

入出力機器

割り込みは入出力機器からのイベント通知のために考え出されたものである. 入出力機器はコンピュータに I/O コントローラを通して接続されている. 以下の図は単純化したコンピュータの構成を示している.

コンピュータの構成

コンピュータは,プロセッサとメモリに加え,キーボード,マウス,ディスプレイ,ディスク,ネットワークインタフェースなどの入出力機器(I/Oデバイス)からなる. 入出力機器は I/O コントローラを通して制御される. プロセッサ,メインメモリ,I/O コントローラはシステムバスに接続されており,システムバスを通してお互いにデータをやり取りしている.

I/O コントローラは,システムバスと入出力機器の間でデータの橋渡しをする. 入出力機器は様々な種類のコンピュータに対応するために,標準的なインタフェースを提供している. PC のキーボードやマウスには PS/2 というインタフェースがよく使われていたが,最近では USB がよく使われている. ディスクには SATA や SCSI というインタフェースがよく使われている. 一方,システムバスはプロセッサ固有のものである. 入出力機器の標準的なインタフェースに対応するように,I/O コントローラはプロセッサとのセットで使われるように開発される.

システムバスは以下のバスから構成される.

データをメインメモリまたは I/O コントローラから取り出す場合,プロセッサは取り出したいデータのあるアドレスをアドレスバスに出す. 該当するアドレスにあるメインメモリまたは I/O コントローラのデータがデータバスに出され,それをプロセッサは取得する. 制御バスは,アドレスバスやデータバスへのアクセス制御を行ったり,I/O コントローラの割り込み要求をプロセッサに通知するために使用される.

ポーリングと割り込み

プロセッサは I/O コントローラに入出力要求を出し,I/O コントローラはその要求を解釈し,I/O デバイスに伝える. I/O デバイスは入出力要求を処理し,結果を I/O コントローラに転送し,プロセッサは I/O コントローラからその結果を受け取る. 下図は,この処理の流れを図示したものである.

I/O処理の流れ

入出力処理が終了したかどうかをプロセッサが知るための単純な方法は,プロセッサが I/O コントローラの状態をチェックし続けるというものである. ただしこの方法では,I/O コントローラ,I/O デバイスが入出力を処理している間は,プロセッサでの処理を続けられないため待ち時間ができてしまう. このような I/O デバイスへの入出力要求の処理方法をポーリング (polling) と呼ぶ.

通常,I/O デバイスの処理速度はプロセッサの処理速度と比べて非常に遅いため,ポーリングで入出力処理の終了を待っているとプロセッサの使用率を著しく下げてしまう. また,端末からの入力待ちなどの,ユーザからの入力を待つような処理では,複数の端末からの入力に対する応答性を確保することが難しい.

割り込み (interrupt) は,ポーリングの持つ非効率性を解決するため,入出力処理が終了したことをプロセッサに対し通知するために考え出された方法である. I/O コントローラは,I/O デバイスとの入出力処理が終了したら,プロセッサに対し割り込み要求を出す. プロセッサは割り込み要求を受け付けると,現在実行中の処理を中断し,割り込みを処理するために予め設定されたプログラムを呼び出し,実行する. この割り込み処理のためのプログラムを,割り込みハンドラとか割り込みサービスルーチン (ISR: Interrupt Service Routine) などと呼ぶ.

割り込みを用いたI/O処理の流れ

■ 割り込みは時間依存の処理を可能にする

割り込みが考え出されてから,時間に依存した処理もできるようになった. 時間を刻むタイマデバイスから一定周期で(例えば1秒ごとに)割り込みがかかるようにすれば,そのタイミングでプロセスの切り替えをしたり,ポーリングを行ったりすることができるようになる. 割り込みにより,特定のプロセスや特定のデバイスにかかりきりになることなく,公平に有効にプロセッサを分配活用できるようになった.

■ 関数呼び出しは同期処理,割り込みは非同期処理

割り込み処理は本来のプログラムの処理の流れとは無関係に,非同期的 (asynchronous) に発生する. 一方,関数呼び出しは同期的 (synchronous) であり,明示的に関数を呼び出し,呼び出された関数で処理が行われ,関数呼び出しから戻ってくる. 下図は,関数呼び出しと割り込み処理の違いを図示したものである.

同期処理と非同期処理

ある関数における処理から別の関数(割り込み処理ハンドラもプログラムの中では関数として定義される)が呼び出されるという処理の流れだけを見ると,関数呼び出しも割り込み処理も違いはない. しかし,関数呼び出しの方には関数を呼び出す命令が入っているのに対し,割り込み処理の方では割り込まれたプログラムが割り込みハンドラを呼び出す命令は一切ない. いつどこで割り込まれるかわからないのが割り込みの特徴である.

OS カーネルでの処理にいつでも割り込まれてしまうのでは,データの一貫性を保つことができないので,プロセッサには割り込みを禁止する命令が用意されている.

例外

割り込みとは別に,アクセスが許可されていない番地にアクセスしようとしたとか,0による除算をしようとしたとか,不正な命令(例えば特権が必要な命令)を実行しようとしたとかの理由で,プログラムの実行中にそのプログラムに原因が起因するエラーが起こることがある. このようなエラーのことを例外 (exception) と呼ぶ. 例外が発生した場合,OS カーネルの例外ハンドラ(例外処理のためのプログラム)が起動され対処する.

割り込みと例外が似ている点は,OS カーネルの割り込みハンドラまたは例外ハンドラが起動されるという点,そしてこれらのハンドラを呼び出す命令がプログラム中には含まれていないという点である. 異なっているのは,割り込みハンドラが起動されるのはデバイスなどプログラム外部が要因であるのに対し,例外ハンドラが起動されるのはプログラムの実行が要因であるという点である.

シグナル

ハードウェアからの割り込みや例外は OS カーネルにより処理される. ハードウェアを直接操作できるのは OS カーネルだけであるので,通常のユーザプロセスがハードウェアからの割り込みを直接受けることはない. 例外は,その要因となったプログラムが処理することが可能な場合もあるが,プログラムに共通する処理も多いため,通常 OS カーネルにより処理される.

しかし,割り込みや例外の概念はユーザプロセスとして動作するプログラムにも便利である. ある一定時間ごとに割り込みを発生させることができれば,現在の処理を継続しながら別の処理(例えばプロンプトを点滅させたり,アニメーションを実行したり,割り込みをサポートしていないデバイスに対しポーリングを行ったりというようなこと)ができる. また,非同期的に命令を送りそれに応じた処理を行わせるためにも,割り込みの概念は使用できる. 例外が起こった場合に,そのイベントに対するプログラム独自の対処をしたい場合もある.

UNIXではプロセッサとメモリというコンピュータの基本的な部分はプロセスとして抽象化されているが,同じように割り込みおよび例外を抽象化したのがシグナル (signal) である. 割り込みハンドラに相当するのがシグナルハンドラである. 一般的に使用される割り込みや例外の種類に応じたシグナルが OS カーネルにより定義されている. プロセスはそれぞれのシグナルの種類に対しシグナルハンドラを OS カーネルに登録することができる. プロセスは,実行中のプログラムに起因する例外や,プロセス外部からのイベントをシグナルとして受け取る. シグナルが受け取られると,OS カーネルに登録したシグナルハンドラが起動される.

プロセス外部からのイベントとしてよく使用されているのが,キーボードから Ctrl-C や Ctrl-Z によるプロセス実行の中止または中断である. これらのキーボードからの入力は,まず OS カーネルに含まれるデバイスを制御するデバイスドライバにより処理され,後にプロセスにシグナルとして通知される.

シグナルの利用

シグナルを用いたプログラミング

シグナルの種類とデフォルトの動作

SIGNAL(3)                BSD Library Functions Manual                SIGNAL(3)

NAME
     signal -- simplified software signal facilities

LIBRARY
     Standard C Library (libc, -lc)

SYNOPSIS
     #include <signal.h>

     void (*signal(int sig, void (*func)(int)))(int);

     or in the equivalent but easier to read typedef'd version:

     typedef void (*sig_t) (int);

     sig_t
     signal(int sig, sig_t func);

DESCRIPTION
     This signal() facility is a simplified interface to the more general sigaction(2) facility.

.... 一部省略 ....

     No    Name         Default Action       Description
     1     SIGHUP       terminate process    terminal line hangup
     2     SIGINT       terminate process    interrupt program
     3     SIGQUIT      create core image    quit program
     4     SIGILL       create core image    illegal instruction
     5     SIGTRAP      create core image    trace trap
     6     SIGABRT      create core image    abort program (formerly SIGIOT)
     7     SIGEMT       create core image    emulate instruction executed
     8     SIGFPE       create core image    floating-point exception
     9     SIGKILL      terminate process    kill program
     10    SIGBUS       create core image    bus error
     11    SIGSEGV      create core image    segmentation violation
     12    SIGSYS       create core image    non-existent system call invoked
     13    SIGPIPE      terminate process    write on a pipe with no reader
     14    SIGALRM      terminate process    real-time timer expired
     15    SIGTERM      terminate process    software termination signal
     16    SIGURG       discard signal       urgent condition present on socket
     17    SIGSTOP      stop process         stop (cannot be caught or ignored)
     18    SIGTSTP      stop process         stop signal generated from keyboard
     19    SIGCONT      discard signal       continue after stop
     20    SIGCHLD      discard signal       child status has changed
     21    SIGTTIN      stop process         background read attempted from control terminal
     22    SIGTTOU      stop process         background write attempted to control terminal
     23    SIGIO        discard signal       I/O is possible on a descriptor (see fcntl(2))
     24    SIGXCPU      terminate process    cpu time limit exceeded (see setrlimit(2))
     25    SIGXFSZ      terminate process    file size limit exceeded (see setrlimit(2))
     26    SIGVTALRM    terminate process    virtual time alarm (see setitimer(2))
     27    SIGPROF      terminate process    profiling timer alarm (see setitimer(2))
     28    SIGWINCH     discard signal       Window size change
     29    SIGINFO      discard signal       status request from keyboard
     30    SIGUSR1      terminate process    User defined signal 1
     31    SIGUSR2      terminate process    User defined signal 2

     The sig argument specifies which signal was received.  The func procedure allows a

.... 以下省略 ....

signal システムコール

signal システムコールは,シグナルをサポートするために最初に作られたシステムコールである.


typedef void (*sig_t)(int);
sig_t signal(int sig, sig_t func);

シグナルハンドラを変更するシグナルと新しいシグナルハンドラを引数に指定して呼び出すと,古いシグナルハンドラが返される. signal システムコールの第2引数と戻り値の型は sig_t であり,これは int 型を引数に受け取って値を返さない(つまり void型の)関数へのポインタ(関数のアドレス)の型である. すなわち,シグナルハンドラは以下のように宣言される関数であり,このような型の関数へのポインタが,signal システムコールの引数や戻り値になる. シグナルハンドラの引数は,送られてきたシグナルの番号である.


void signal_handler(int);

古いシグナルハンドラの値として以下の値が返されることもある:

新しいシグナルハンドラの値として上記の値を与えることもできる.

シグナルと共に使われるシステムコールに pause がある. pause を呼び出したプロセスはシグナルを受け取るまで実行を停止する. シグナルを受け取ると,シグナルハンドラの実行後に pause からリターンする.


int pause(void);

以下は signal と pause を使ったプログラム例である.

つまり,Ctrl-C を入力するたびに sigint_handler が起動し,3回入力するとプログラムが終了する.

signal.c
     1  #include <stdio.h>
     2  #include <stdlib.h>
     3  #include <unistd.h>
     4  #include <signal.h>
     5
     6  volatile sig_atomic_t sigint_count = 3;
     7
     8  void sigint_handler(int signum)
     9  {
    10          printf("sigint_handler: signum(%d), sigint_count(%d)\n",
    11                 signum, sigint_count);
    12
    13          if (--sigint_count <= 0) {
    14                  printf("sigint_handler: exiting ...\n");
    15                  exit(1);
    16          }
    17
    18  #if 0   /* For the original System V signal */
    19          signal(SIGINT, &sigint_handler);
    20  #endif
    21  }
    22
    23  int main(void)
    24  {
    25          signal(SIGINT, &sigint_handler);
    26
    27          for (;;) {
    28                  printf("main: sigint_count(%d), calling pause ...\n",
    29                         sigint_count);
    30
    31                  pause();
    32
    33                  printf("main: returned from pause. sigint_count(%d)\n",
    34                         sigint_count);
    35          }
    36
    37          return 0;
    38  }

6行目の volatile sig_atomic_t の volatile という単語は見慣れないかもしれないが,その説明は後述する.

コンパイル,実行すると以下のようになる. Ctrl-C は ^C として表示される. 送られてきたシグナルの番号がシグナルハンドラ sigint_handler の引数になるため,SIGINT の番号である2が sigint_handler の引数として渡されているのがわかる.

$ ./a.out[←]
main: sigint_count(3), calling pause ...
[C-C]^Csigint_handler: signum(2), sigint_count(3)
main: returned from pause. sigint_count(2)
main: sigint_count(2), calling pause ...
[C-C]^Csigint_handler: signum(2), sigint_count(2)
main: returned from pause. sigint_count(1)
main: sigint_count(1), calling pause ...
[C-C]^Csigint_handler: signum(2), sigint_count(1)
sigint_handler: exiting ...
$

signal システムコールの最初の(しかしながら System V 系の UNIX まで継承された)仕様は,多くの問題を持っていた.

  1. システムコールを呼び出して停止している時の動作:

    例えば read システムコールを呼び出し,入力待ちの状態で停止している間にシグナルが通知された場合,そのシグナルに対応するシグナルハンドラが起動される. この場合,その read システムコールはキャンセルされ EINTR というエラーが返される.

  2. シグナルハンドラのマスクとリセット:

    あるシグナルに対応したシグナルハンドラを実行中に,また同じシグナルが通知された場合,同じシグナルハンドラを起動すると,データの一貫性などに問題が生じることがある. そのため,シグナルハンドラは一度呼び出されるとデフォルトの動作にリセットされるようになっていた. そのため,シグナルハンドラが起動されるたびに signal システムコールでシグナルハンドラを設定し直す必要があった(上記のプログラムの18~20行目).

    しかし,この方法ではデフォルトの動作がプロセスの終了であった場合,シグナルハンドラを実行中に同じシグナルを受けたらプロセスが終了してしまうことになる.

    そこで,シグナルハンドラの実行中にそのシグナルを通知されたくない場合には,シグナルハンドラの先頭で,そのシグナルを無視するようにする(SIG_IGN をシグナルハンドラとして設定する)ようにすればよいように思われるが,この場合は

    という問題がある.

System V とは別の系列の UNIX に BSD (Berkeley Software Distribution) というものがある. BSD UNIX では上記のような問題に対し signal の動作を変えることで対応しようとした.

  1. システムコールを呼び出して停止している時の動作:

    システムコールはキャンセルされない. シグナルハンドラ実行後に,システムコールの実行は継続される(停止状態に戻る).

  2. シグナルハンドラのマスクとリセット:

    シグナルハンドラの実行中に同じシグナルが通知されたら,そのシグナルは保留される. 現在実行中のシグナルハンドラ処理の終了を待ち,終了後にもう一度シグナルハンドラを起動する. シグナルハンドラはリセットされない.

BSD UNIX の変更は機能するものであったが,System V UNIX の古い signal と同じ API を用いたため,混乱の原因となった. macOS は,マニュアルに書かれているように BSD UNIX ベースであるため,この signal の API を提供している. そのため上記のプログラムの18~20行目は不要になっている.

Linux カーネルが提供する signal システムコールは System V の API を持っている. しかし,glibc 2 以降では signal ライブラリ関数は signal システムコールを呼び出さず, 代わりに BSD UNIX と同じ動作をするようなフラグを与えて sigaction システムコールを呼び出している. これについては signal(2) マニュアルで説明されている. 結局,Linux でも macOS と同じく,上記のプログラムの18~20行目は不要である.

sigaction システムコール

signal システムコールの混乱状態を解決するために,POSIX では sigaction という新しいシステムコールを導入した. sigaction で導入されたシグナルは,旧来のメカニズムのシグナルと区別するために POSIX シグナルと呼ばれることがある. POSIX 準拠のシステムであれば sigaction を使用すべきであり,signal はもはや使用すべきではない.


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

sigaction システムコールは,第1引数にシグナル番号,第2引数に設定するシグナルの動作を指定して呼び出す.sigaction システムコールは戻り値としてエラーコードを返し,第3引数にシグナルの古い設定を返す.

上記の問題点における POSIX シグナルのデフォルトの動作は次のようになっている.

  1. システムコールを呼び出して停止している時の動作:

    この点については古い signal と同じである.システムコールはキャンセルされ EINTR というエラーが返される.

  2. シグナルハンドラのマスクとリセット:

    この点については BSD UNIX と同じである. シグナルハンドラの実行中に同じシグナルが通知されたら,そのシグナルは保留される.現在実行中のシグナルハンドラ処理の終了を待ち,終了後にもう一度シグナルハンドラを起動する. また,シグナルハンドラはリセットされない.

どちらの点についても,設定により古い signal または BSD UNIX の動作をさせることができるようになっている.

シグナルの動作は以下の struct sigaction を用いて設定する.

struct  sigaction {
        union __sigaction_u __sigaction_u;  /* signal handler */
        sigset_t sa_mask;               /* signal mask to apply */
        int     sa_flags;               /* see signal options below */
};

union __sigaction_u {
        void    (*__sa_handler)(int);
        void    (*__sa_sigaction)(int, struct __siginfo *,
                       void *);
};

#define sa_handler      __sigaction_u.__sa_handler
#define sa_sigaction    __sigaction_u.__sa_sigaction

古い signal のプログラムを,sigaction で sa_handler を使うように変更すると以下のようなプログラムになる. 基本的には全く同じであるが,若干異なっている.

sigaction1.c
     1  #include <stdio.h>
     2  #include <stdlib.h>
     3  #include <unistd.h>
     4  #include <signal.h>
     5  #include <string.h>
     6
     7  volatile sig_atomic_t sigint_count = 3;
     8
     9  void sigint_handler(int signum)
    10  {
    11          printf("sigint_handler: signum(%d), sigint_count(%d)\n",
    12                 signum, sigint_count);
    13
    14          if (--sigint_count <= 0) {
    15                  printf("sigint_handler: exiting ...\n");
    16                  exit(1);
    17          }
    18  }
    19
    20  int main(void)
    21  {
    22          struct sigaction sa_sigint;
    23
    24          memset(&sa_sigint, 0, sizeof(sa_sigint));
    25          sa_sigint.sa_handler = sigint_handler;
    26          sa_sigint.sa_flags = SA_RESTART;
    27
    28          if (sigaction(SIGINT, &sa_sigint, NULL) < 0) {
    29                  perror("sigaction");
    30                  exit(1);
    31          }
    32
    33          for (;;) {
    34                  printf("main: sigint_count(%d), calling pause ...\n",
    35                         sigint_count);
    36
    37                  pause();
    38
    39                  printf("main: returned from pause. sigint_count(%d)\n",
    40                         sigint_count);
    41          }
    42
    43          return 0;
    44  }

上記のプログラムの実行結果は,古い signal を用いた場合と同一であるので,省略.

同じプログラムを,sigaction で sa_sigaction を使うように変更したプログラムは以下のようになる.

sigaction2.c
     1  #include <stdio.h>
     2  #include <stdlib.h>
     3  #include <unistd.h>
     4  #include <signal.h>
     5  #include <string.h>
     6
     7  volatile sig_atomic_t sigint_count = 3;
     8
     9  void sigint_action(int signum, siginfo_t *info, void *ctx)
    10  {
    11          printf("sigint_handler(%d): sigint_count(%d) signum(%d) code(0x%x)\n",
    12                 signum, sigint_count, info->si_signo, info->si_code);
    13
    14          if (--sigint_count <= 0) {
    15                  printf("sigint_handler: exiting ...\n");
    16                  exit(1);
    17          }
    18  }
    19
    20  int main(void)
    21  {
    22          struct sigaction sa_sigint;
    23
    24          memset(&sa_sigint, 0, sizeof(sa_sigint));
    25          sa_sigint.sa_sigaction = sigint_action;
    26          sa_sigint.sa_flags = SA_RESTART | SA_SIGINFO;
    27
    28          if (sigaction(SIGINT, &sa_sigint, NULL) < 0) {
    29                  perror("sigaction");
    30                  exit(1);
    31          }
    32
    33          while (1) {
    34                  printf("main: sigint_count(%d), calling pause ...\n",
    35                         sigint_count);
    36
    37                  pause();
    38
    39                  printf("main: returned from pause. sigint_count(%d)\n",
    40                         sigint_count);
    41          }
    42
    43          return 0;
    44  }

コンパイル,実行すると以下のようになる. Ctrl-C は ^C として表示される. 通知されたシグナルについて,より多くの情報を引数として受け取れる.その詳細については sigaction のマニュアルを参照.

$ ./a.out[←]
main: sigint_count(3), calling pause ...
[C-C]^Csigint_handler(2): sigint_count(3) signum(2) code(0x0)
main: returned from pause. sigint_count(2)
main: sigint_count(2), calling pause ...
[C-C]^Csigint_handler(2): sigint_count(2) signum(2) code(0x0)
main: returned from pause. sigint_count(1)
main: sigint_count(1), calling pause ...
[C-C]^Csigint_handler(2): sigint_count(1) signum(2) code(0x0)
sigint_handler: exiting ...
$

システムコールやライブラリ関数の実行がシグナルハンドラに割り込まれた場合,シグナルハンドラからのリターン後にそのシステムコールの実行が再開されるかリターンするかは,OSごとに厳密に仕様で定められている.その際に sigaction の引数の SA_RESTART フラグが及ぼす作用も,その仕様で定められている.また,再開かリターンかの挙動は,実行しているシステムコールやライブラリ関数の種類に依存している.仕様はかなり複雑になっている.このあたりの動きが重要になるプログラムの開発では,ぜひ signal(7) のマニュアルの該当部分("Interruption of system calls and library functions by signal handlers"の部分)を熟読してほしい.
なお通常,getchar() ライブラリ関数は内部で最終的には標準入力(端末)に対して read システムコールを実行する.よって,割り込まれた後の挙動も,端末に対する read システムコールのそれとなる.
また,その部分では,pause() システムコールは,割り込まれたら SA_RESTART フラグがあってもなくても必ずリターンする(再開されない)と記述されている.

シグナルの応用

受け取りたくないシグナルは無視することができる. その場合シグナルハンドラに SIG_IGN を設定する.

sigaction3.c
     1  #include <stdio.h>
     2  #include <stdlib.h>
     3  #include <unistd.h>
     4  #include <signal.h>
     5  #include <string.h>
     6
     7  volatile sig_atomic_t sigint_count = 3;
     8
     9  int main(void)
    10  {
    11          struct sigaction sa_ignore;
    12
    13          memset(&sa_ignore, 0, sizeof(sa_ignore));
    14          sa_ignore.sa_handler = SIG_IGN;
    15
    16          if (sigaction(SIGINT, &sa_ignore, NULL) < 0) {
    17                  perror("sigaction");
    18                  exit(1);
    19          }
    20
    21          while (1) {
    22                  printf("main: sigint_count(%d), calling pause ...\n",
    23                         sigint_count);
    24
    25                  pause();
    26
    27                  printf("main: returned from pause. sigint_count(%d)\n",
    28                         sigint_count);
    29          }
    30
    31          return 0;
    32  }

コンパイル,実行すると以下のようになる. Ctrl-C は ^C,Ctrl-Z は ^Z として表示される.

$ ./a.out[←]
main: sigint_count(3), calling pause ...
[C-C]^C[C-C]^C[C-C]^C[C-z]^Z
[1]+  Stopped                 ./a.out
$ jobs[←]
[1]+  Stopped                 ./a.out
$ kill -KILL %1[←]

[1]+  Stopped                 ./a.out
$ [←]
[1]+  Killed: 9               ./a.out
$

以下のように,SIG_IGN の値は1である.また,デフォルトの動作を表す SIG_DFL の値は0である. sigaction は第3引数で指定された構造体にシグナルの古い設定を書き込むが,古い設定が無視やデフォルトであった場合は,これらの値がそこに書き込まれることになる.


#define SIG_DFL         (void (*)(int))0
#define SIG_IGN         (void (*)(int))1

kill

プロセスへシグナルを送るためには kill システムコールを使用する. 引数に送り先のプロセス ID と送るシグナルを指定する.


int kill(pid_t pid, int sig);

以下は,コマンドライン引数にプロセス ID を受け取り,そのプロセスに対し SIGINT を送るプログラムである.

kill.c
     1  #include <stdio.h>
     2  #include <stdlib.h>
     3  #include <signal.h>
     4
     5  int main(int argc, char *argv[])
     6  {
     7          pid_t pid;
     8
     9          if (argc != 2) {
    10                  fprintf(stderr, "Usage: %s pid\n", argv[0]);
    11                  exit(1);
    12          }
    13
    14          pid = atoi(argv[1]);
    15
    16          if (pid <= 0) {
    17                  fprintf(stderr, "Invalid pid: %d\n", pid);
    18                  exit(1);
    19          }
    20
    21          if (kill(pid, SIGINT) < 0) {
    22                  perror("kill");
    23                  exit(1);
    24          }
    25
    26          return 0;
    27  }

シグナルハンドラ内での処理についての注意

Ctrl-C を3回入力すると終了する上記の sigaction2.c で,シグナルハンドラ内で値が変更される変数は,以下のように宣言されている.


volatile sig_atomic_t sigint_count = 3;

sig_atomic_t は,割り込まれずに書き込みが可能な変数の型である. volatile は型修飾子と呼ばれるもので,型の前に付けて使用する. volatile を付けることによって,コンパイラによる最適化が抑止され,sigint_count に対する読み書きは,必ずメモリに対して行なわれるようになる(sigint_count がレジスタに割り当てられることがなくなる).

このような宣言が必要な理由は,シグナルハンドラの呼び出しが,関数呼び出しのようにプログラムの制御の流れに沿ったものではなく,プログラム外部のイベントに起因し非同期的に起こるものだからである. プログラムの制御の流れに沿っている場合,変数への読み書きはコンパイラの制御下にあるため,最適化により一時的にレジスタに置かれている変数の値が正しく参照される. しかし,シグナルハンドラは,本来のプログラムの制御の流れとは無関係に,プログラムの実行に割り込んで呼び出される. 最適化により変数の値が一時的にレジスタに置かれているところにシグナルハンドラの呼び出しが起こった場合,シグナルハンドラはその最新の値にアクセスすることはできない. また,シグナルハンドラが変数の値を書き換えたとしても,割り込まれたプログラムはその変更に気がつかないこともあり得る.

volatile と宣言された変数は必ずメモリに割り当てられるため,どういうタイミングでシグナルによる割り込みが起きても,必ず最新の値を参照することができる. また,sig_atomic_t は割り込まれずに書き込みが可能な変数の型を提供するため,変数の読み書きの途中で割り込まれることがない. 例えば,値を変数の下位バイトには書き込んだが上位バイトにまだ書き込んでいない瞬間に割り込まれることがない. そのため,一貫性のある値を読み書きすることができる.

シグナルハンドラは,本来のプログラムの制御の流れに割り込んで呼び出されるため,プログラムの処理に異常をもたらす場合もある. シグナルハンドラ内で呼び出すと異常をもたらすライブラリ関数やシステムコールは数多くあるため,シグナルハンドラ内で使用可能なライブラリ関数やシステムコールは制限されている. 例えば,printf はシグナルハンドラ内で使用可能ではない(外部サイトの参考資料). write は使用可能なので,メッセージを出力する必要がある場合には write を用いる. シグナルハンドラ内で使用可能な関数のリストについては sigaction のマニュアルを参照のこと.

インターバルタイマ

ある一定時間ごとにシグナルを発生させ,現在の処理を継続しながら別の処理(例えばプロンプトを点滅させたり,アニメーションを実行したり,割り込みをサポートしていないデバイスに対しポーリングを行ったりというようなこと)がしたい場合にはインターバルタイマという機能が使える. インターバルタイマは setitimer システムコールを用いて設定できる.


int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);

インターバルタイマは第2引数で指定した時間が経つ(残り時間が0になる)とシグナルで通知する. 時間経過の計測方法について,以下の中から1つを第1引数で設定する.

第2引数で時間を指定するために使用する型 struct itimerval とその中で使われている型 struct timeval の定義は以下のようになっている.

struct itimerval {
        struct  timeval it_interval;    /* timer interval */
        struct  timeval it_value;       /* current value */
};

struct timeval {
        time_t       tv_sec;   /* seconds since Jan. 1, 1970 */
        suseconds_t  tv_usec;  /* and microseconds */
};

it_value に与えられた時間が経過すると,第1引数で指定されたシグナルが通知される. そして,it_interval の値が次の残り時間(it_value)として設定される. it_value が0に設定されたタイマは無効になる(時間計測およびシグナルの通知を停止する).

以下は,ITIMER_REAL を指定した setitimer によって作られるインターバルタイマにより,実際の時間で1秒ごとにシグナルを受け,10回シグナルを受け取ると終了するプログラムである. 10回シグナルを受け取ったら it_value と it_interval に0を設定し,タイマを無効にしている(その後すぐにプログラムが終了するので,無効化する意義はないが).

itimer.c
     1  #include <stdio.h>
     2  #include <stdlib.h>
     3  #include <unistd.h>
     4  #include <signal.h>
     5  #include <string.h>
     6  #include <sys/time.h>
     7
     8  volatile sig_atomic_t alrm_count = 10;
     9
    10  void alrm(int signum)
    11  {
    12          alrm_count--;
    13  }
    14
    15  int main(void)
    16  {
    17          struct sigaction sa_alarm;
    18          struct itimerval itimer;
    19
    20          memset(&sa_alarm, 0, sizeof(sa_alarm));
    21          sa_alarm.sa_handler = alrm;
    22          sa_alarm.sa_flags = SA_RESTART;
    23
    24          if (sigaction(SIGALRM, &sa_alarm, NULL) < 0) {
    25                  perror("sigaction");
    26                  exit(1);
    27          }
    28
    29          itimer.it_value.tv_sec  = itimer.it_interval.tv_sec  = 1;
    30          itimer.it_value.tv_usec = itimer.it_interval.tv_usec = 0;
    31
    32          if (setitimer(ITIMER_REAL, &itimer, NULL) < 0) {
    33                  perror("setitimer");
    34                  exit(1);
    35          }
    36
    37          while (alrm_count) {
    38                  pause();
    39                  printf("%d: %d\n", alrm_count, time(NULL));
    40          }
    41
    42          itimer.it_value.tv_sec  = itimer.it_interval.tv_sec  = 0;
    43          itimer.it_value.tv_usec = itimer.it_interval.tv_usec = 0;
    44
    45          if (setitimer(ITIMER_REAL, &itimer, NULL) < 0) {
    46                  perror("setitimer");
    47                  exit(1);
    48          }
    49
    50          return 0;
    51  }

コンパイル,実行すると以下のようになる.time(NULL) によって取得される時刻が1秒ずつ増えていくことがわかる.

$ ./a.out[←]
9: 1487757391
8: 1487757392
7: 1487757393
6: 1487757394
5: 1487757395
4: 1487757396
3: 1487757397
2: 1487757398
1: 1487757399
0: 1487757400
$

2008年版の POSIX の仕様書に書かれた getitimer, setitimer のマニュアルによると,getitimer と setitimer は廃止予定であり,timer_gettime, timer_settime を使うことが推奨されているようである. timer_gettime と timer_settime は,この演習で使用している Linux サーバの環境では使えるが,macOS の環境では使えない.

練習問題

練習問題(501)

kill システムコールでシグナルを送るプログラム kill.c と,シグナルを受け取るプログラム sigaction1.c を使用して,プロセスからプロセスへシグナルが送られるのを確かめなさい.

ヒント: 端末ウィンドウを2つ開き,片方のウィンドウでシグナルを受け取るプログラムを動かし,もう片方のウィンドウでシグナルを送るプログラムを動かす. シグナルを受け取るプロセスのIDは ps コマンドで調べるか,シグナルを受け取るプログラムを変更して自分のプロセスIDを表示させるようにする.

練習問題(502)

複数の異なるシグナルに対してシグナルハンドラを設定し,シグナルに応じたシグナルハンドラが起動されることを確かめなさい.

ヒント: シグナルは様々な方法で発生させることができる. kill コマンドや kill システムコールを使えば,好きなシグナルを好きなプロセスに送信することができる. キー入力や GUI 操作でシグナルを発生させることもできる. 例えば Ctrl-Z を入力すると SIGTSTP シグナルを発生させることができ,端末のウィンドウのサイズを変えると SIGWINCH シグナルを発生させることができる.

練習問題(503)

OS はプロセスによる様々な不正な処理の実行に対してシグナルを送信する. たとえば,この演習で使用している Linux は(そして macOS も),不正なメモリアクセスを行ったプロセスに SIGSEGV(segmentation violation, セグメンテーション違反)シグナルを送信し, ゼロによる整数除算を試みたプロセスに SIGFPE(floating point exception, 浮動小数点数例外)シグナルを送信する. 以下のプログラムを作成しなさい.

アクセスしたメモリアドレスの情報はシグナルが渡すデータ構造である siginfo_t 型の構造体から取得できる. この演習の Linux サーバの環境では siginfo_t 型の構造体についての情報は sigaction のマニュアルに書かれている.

本来はシグナルハンドラの中で printf を呼び出してはいけないが,この問題に対する解答のプログラムでは呼び出してもよいものとする. もちろん,printf ではなく write などの呼び出してもよい関数を使うプログラムにするほうが望ましい.

メモ: 少なくともこの演習の計算機環境の Linux サーバでは整数のゼロ除算に対しても SIGFPE(浮動小数点数例外)シグナルが送信される. さらになんと,浮動小数点数のゼロ除算に対しては,浮動小数点数に関するシグナルは送信されない.

練習問題(504)

sleep は実はライブラリ関数である. シグナルの機能を用いて,sleep 相当の機能を持つ関数 mysleep を作りなさい.

mysleep は sleep と同じ戻り値を返すようにし,さらに,それを確かめる実行例を含めるようにすること. 現在時刻は time 関数により取得できる. 使用が終わり不要になったシグナルの設定はキャンセルし,もとの設定に戻すこと. nanosleep システムコールは使用しないこと. 細かい仕様が気になる人のために言っておくと,mysleep はリエントラント(再入可能)である必要はない.

ヒント: 現在時刻は time 関数や gettimeofday 関数によって取得できる. Linux では clock_gettime 関数の利用が推奨されている.

練習問題(505)

キーボードから1文字入力を読み込む関数 getchar にタイムアウト機能を追加した関数 mygetchar を作りなさい.

実行例では,上記の仕様を満たしていることを,以下の2つを表示することにより示しなさい. mygetchar の戻り値として,場合に応じて,キー入力値,-1,-2,-3 の4種類が返されることを実行例で示しなさい. -3 を返す機能をあらゆる理由のリターンに対処できるように実装することは面倒なので,SIGINT シグナル(割り込みシグナル)を受け取った場合だけに対応できていればよく,実行例にもその場合のものだけを含めればよいとする. もちろん,あらゆる理由のリターンに対処できるように実装してもよい.

細かい仕様が気になる人のために言っておくと,mygetchar はリエントラント(再入可能)である必要はない.

ヒント: シグナルの設定に用いるフラグに注意しよう.
ヒント: Ctrl-D によって,端末からの入力の終わりを意味するデータすなわち EOF を入力することができる.
ヒント: Ctrl-C によって,フォアグラウンドのプロセスに SIGINT シグナルを送信することができる.
ヒント: getchar はファイルや端末からの入力の終わりの場合にも,シグナルなどにより文字の読み込みに失敗した場合にも,EOF を返す. どちらの場合による EOF かを判定するためには,feof 関数や ferror 関数を使う.

練習問題(506)

練習問題(407)と同じように,親プロセスと子プロセスが交互に実行することにより,交互に1文字出力するプログラムを,シグナルを用いて書きなさい. fork で作る子プロセスは1つだけとする.


Yoshihiro Oyama / <oyama@cs.tsukuba.ac.jp>