Signal programming [No.2]

Date : 2006-02-20
Author : Defolos

CONTENTS

(1.) 前回のおさらい
(2.) サンプルプログラムの仕様
- 前提条件
(3.) シグナル詳細
- signal関数
- リエントラント問題
(4.) SysVシグナル
- SysVのリエントラント処理
- プログラムの書き方
(5.) BSDシグナル
- BSDのリエントラント処理
- プログラムの書き方
(6.) POSIXシグナル
- POSIXのリエントラント処理
- sigaction関数
- sigaction構造体
- sa_handler
- sa_sigaction
- sa_mask
- sa_flags
- sa_restorer
- プログラムの書き方
(7.) volatile修飾子

前回のおさらい

 前回に引き続き割り込みプログラムについてまとめてみたいと思います。Signal programming [No.2]では実際のプログラムの書き方を重点的に解説したいと思いますので、さきにSignal programming [No.1]に一通り目を通していただきたいと思います。

 前編では、割り込みの概要と割り込みの動作をキーボード割り込みの実例を挙げて解説しました。また、UNIXでは割り込みを実現するためにシグナルという仕組みを利用しており、シグナルの動作にもSysV、BSD、POSIXの三つの処理体系があることを説明しました。今回はもう少し込み入ったシグナルの解説と、それぞれのシグナル処理体系における、割り込みプログラムの実際の書き方を解説していきたいと思います。


サンプルプログラムの仕様

 次にサンプルプログラムとして作成するプログラムの仕様を記述します。以後は、ここで記述した要求を満たすようなプログラムをシグナルを使って作って行くことになります。

● 前提条件

  1. プログラムのメイン処理はgets関数でキーボードからの入力を待ち受ける
  2. キーボードからの入力を変数に格納する
  3. Ctrl+C入力によるSIGINTシグナルを感知した場合、制御をシグナルハンドラに移す
  4. シグナルハンドラでは2.の変数の内容を文字列として出力する
  5. リエントラントテストを行うため割り込みハンドラ内で1秒間待機する
  6. 割り込みハンドラの処理を終えると、メイン処理へ制御を移す
  7. 以上の処理を繰り返す
=============
 main start
=============
   ↓
----------
 while(0) ←------.
----------        |
   ↓             |
---------------   |
printf(Message)   |
---------------   |
   ↓             |
----------        |
gets(buff)        |
----------        |
   ↓             |
--------          |
pause() --------→^
--------
   ↓
=============
 main stop
=============

::::::::::::::::::::::::::

===============
 warikomi start
===============
    ↓
---------------
printf(Message)
---------------
    ↓
------------
printf(buff)
------------
    ↓
---------
sleep(1)
---------
    ↓
===============
 warikomi stop
===============

シグナル詳細

● signal関数

 シグナルの動作には、あらかじめ決められているデフォルト動作というものがあります。このデフォルトの動作に不満が無ければ、特になにもせずにそのまま使えばよいのですが、もし動作に不満がある場合はユーザプログラムで新しい動作(シグナルハンドラ)を設定することができます。この動作を変更する関数がsignal関数です。signal関数のプロトタイプは次のようになっています。


#include <signal.h>

typedef void (*sighandler_t)(int);
void signal(int signum, void (*handler)(int) );

 signal関数は引数を二つとります。第一引数には動作を変更したいシグナルの番号を指定しますが、「SIGINT」や「SIGARLM」などのような文字列定数もsignal.h(Linuxではasm/signal.h)でそれぞれのシグナル番号と関連付けされているため指定できます。
 第二引数には、第一引数で動作を変更したシグナルが届いたときに制御を移すシグナルハンドラへの関数ポインタを指定します。あるいは第一引数で指定されたシグナルを受け取っても無視するようにするSIG_IGNか、第一引数で指定されたシグナルの動作をデフォルト動作に戻すSIG_DFLを指定することもできます。また、第二引数でシグナルハンドラへの関数ポインタを指定した場合、signumを引数としてとった状態でhandlerが呼び出されます。
 注意点としましては、SIGKILLやSIGSTOPにシグナルハンドラを設定することはできません。また、シグナル関数は成功するとハンドラルーチンを指すポインタを戻り値に返し、エラーの場合はSIG_ERRを返し、errnoにエラーの種類を示す値をセットします。

 ここまでの解説はSysVシグナル処理体系でもBSDシグナル処理体系でも同じです。SysVシグナルとBSDシグナルとの大きな違いは、後述しますがシグナルを受け取ったときの動作に現れます。

● リエントラント問題

 シグナルの到着は基本的に非同期であり、いつどのようなタイミングでシグナルが送信されてくるかはわかりません。ブロッキング関数が処理をブロック(※1)している状態や、システムコール(※2)処理中の状態でシグナルを受け取るといった状況も考えられます。この場合ブロッキング関数を中断してシグナルハンドラに処理を移すべきなのか、ブロッキング関数を優先してシグナルを無視するべきかといった問題が発生します。
 また、シグナルハンドラの処理を実行している間に、同一のシグナル番号の新しいシグナルが到着した場合に、どのように動作するべきなのかといった問題もあります。例えばサンプルプログラムの仕様にしたがって、Ctrl+Cでシグナルハンドラへ制御を切り替え、メッセージを出力している間にもう一度Ctrl+Cを入力した場合にどのような動作を行うかということです。

 このリエントラント問題の処理においては、SysVシグナル処理体系、BSDシグナル処理体系、POSIXシグナル処理体系のそれぞれで異なった動作を行います。リエントラント処理の違いがそれぞれ三つの処理体系の大きな違いといってもよいでしょう。

(※1)ブロック

readシステムコール関数の場合なら、読み込むためのデータの到着を待っている状態です。read関数などのように、その関数での仕事が終わるまで処理を占領してしまうような関数のことをブロッキング関数といい、処理を占領している状態のことを「ブロックしている」と表現します。ブロッキング関数には他にsend関数やaccpet関数などがあります。

(※2)システムコール

OSのカーネルが提供する機能のうち、プロセスから呼び出せるようになっている機能、もしくはその呼び出し規約のこと。ファイルアクセスやメモリの割り当て、子プロセスの生成などの機能が用意されていることが多い。
 最近のOSでは、システムコールではなく、APIという用語を使うことが多い。これは、OSのバージョンアップなどによって、従来はカーネルが提供していた機能がライブラリや別プロセスによって提供されるようになるなど、実装方法が多様化していることに対応したものである。

@nifty:デジタル用語辞典:システムコール(http://www.nifty.com/webapp/digitalword/word/037/03749.htm)より引用


SysVシグナル

 SysVシグナルは古くからUNIXに実装されてきたシグナルであるため、もはや古いシグナル処理体系であるといえます。しかし、現在でもまだ動作する処理体系であることと、シグナルの動作の実装がどのように変化してきたかという歴史を知ることは、それ以後の技術を理解する手助けになると思いますので、ここでは古いといわれるSysVシグナルの解説も行います。

● SysVのリエントラント処理

 前述のリエントラント問題でもふれたように、リエントラント処理はそれぞれのシグナル処理体系で動作が異なっています。SysVシグナル処理体系では、次のようにリエントラント処理を行っています。

» ブロック中のシグナル受信

 システムコールやブロッキング関数などの処理中にシグナルが到着した場合は、システムコール・ブロッキング関数を停止させます。システムコール関数の場合、エラー時には-1を返すので、シグナルによる割り込みが発生した場合エラー(-1)を吐いて停止します。

» 同一シグナルの到着

 SysVシグナルでは、このような状況が起こらないようにすることで対処しています。プログラム内でsignal関数を用いて指定されたシグナルを受け取ると、signal関数で指定したシグナルハンドラへ処理を移しますが、そのときシグナルの動作をデフォルトへ戻してしまいます。この動きはLinuxカーネルとlibc4,5でも同様なようです。
 例えばサンプルプログラムの場合、Ctrl+Cが入力された場合にシグナルハンドラとして割り込みの発生を通知するようなメッセージを出力し、1秒間待機しますが、この1秒間の間にもう一度Ctrl+Cが押された場合の処理についてです。SysVシグナルでは一度シグナルハンドラが呼び出されたときにシグナルの動作をデフォルトに戻してしまいますので、サンプルプログラムのシグナルハンドラ中にもう一度Ctrl+Cを押した場合はSIGINTのデフォルト動作である「終了」が実行されます。

● プログラムの書き方

 上記の点を踏まえてサンプルの仕様を満たすプログラムを書くと次のようになります。グローバル変数を文字列格納に使うなどという無茶をやっていますが、テスト用プログラムということで許してやってください。


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

char buff[256];

void warikomi(int signo){

        int i = 0;

        signal(SIGINT, SIG_IGN);
        printf("there was interrupt.\n");
        puts(buff);
        sleep(1);

        /*バッファクリア*/
        while (buff[i] != '\0'){
        buff[i] = '\0';
        i++;
        }
        i = 0;

        signal(SIGINT, warikomi);
}

int main(void){

        signal(SIGINT, warikomi);

           while(1){
              gets(buff);
              printf("I'm waiting!\n");
              pause();
           }

return 0;
}


 まず、pause関数について説明します。pause関数はシグナルを受け取るまでプロセスの実行をブロックし、シグナルを受け取るとシグナルハンドラ実行後にpauseから戻ります。プロトタイプは次のようになっています。


#include <unistd.h>

int pause(void);

 pause関数が戻り値を返すのは、シグナルを受け取ってシグナル捕獲関数から返った場合だけです。この場合は-1を返し、errnoにEINTRが設定されます。

 このプログラムではsignal(SIGINT, warikomi);の部分でSIGINTシグナルの動作をwarikomi関数を呼び出すように設定しています。設定された状態でCtrl+C(SIGINT)を入力すればwarikomi関数を呼び出すことができます。warikomi関数内ではsignal(SIGINT, SIG_IGN);の部分でCtrl+C(SIGINT)を無視するように設定しています。Ctrl+C(SIGINT)を無視することでシグナルハンドラの処理中に同じシグナルが到着することを防いでいます。さらに、SysVではシグナルに設定された動作は一度でも呼び出されるとデフォルトの動作に戻るので、たとえsignal(SIGINT, SIG_IGN);の設定がエラーになっても同一シグナルが到着することはありません。  しかし、一度呼び出されるとデフォルトの動作に戻ってしまうが故にmain関数内で設定しているsignal(SIGINT, warikomi);をシグナルハンドラ内で再設定する必要があります。

 また、システムコール関数を使用したプログラムを作製した場合、システムコールのブロック中にSIGINTシグナルを受け取る可能性があります。このときシステムコールはSysVシグナル処理体系にしたがって、エラー(-1)を返して停止してしまいます。こういった動作を避けるためにSysVシグナル処理体系では次のようにコーディングすることになります。


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

char buff[256];

void warikomi(int signo){

        int i = 0;

        signal(SIGINT, SIG_IGN);
        printf("there was interrupt.\n");
        puts(buff);
        sleep(1);

        /*バッファクリア*/
        while (buff[i] != '\0'){
        buff[i] = '\0';
        i++;
        }
        i = 0;

        signal(SIGINT, warikomi);
}

int main(void){

        int rc;

        signal(SIGINT, warikomi);

           while(1){
              rc = gets(buff);
                if(rc == -1){
                   if(errno != EINTR){
                      printf("error!\n" );
                      exit(1);
                   }
                }
              printf("I'm waiting!\n");
              pause();
           }

return 0;
}


 warikomi関数内の処理は同じですが、main関数内のgets関数の戻り値を取得して、-1が戻っていた場合(シグナルが発生した場合)さらにerrnoの値がEINTR(システムコールがシグナルに割り込まれた場合)かどうかを判断し、EINTRではないエラーの場合には終了します。つまり、システムコールがシグナルの割り込みによってエラーとなった場合は無視して処理を続行するようにコーディングしています。


BSDシグナル

 前述のSysVシグナル処理体系のリエントラント問題の解決方法は少々強引な解決方法でした。そこでBSD開発者達は別な方法でリエントラント問題を解決しようとしました。

● BSDのリエントラント処理

 BSD処理体系ではそれぞれ次のようにリエントラント処理を行っています。

» ブロック中のシグナル受信

 BSDシグナル処理体系では、システムコールやブロッキング関数などの処理中にシグナルが到着した場合は、エラー値で戻るようなことはしません。一時的にシグナルハンドラに処理が移りますが、シグナルハンドラの処理が終わればシステムコールやブロッキング関数などの処理を続行することになります。

» 同一シグナルの到着

 同一シグナルがシグナルハンドラの実行中に到着した場合、後に到着したシグナルはOSによって保留(ペンディング)されます。シグナルハンドラの実行完了後に保留されていたシグナルが処理され、後に到着したシグナルによって新たにシグナルハンドラが呼び出されます。OSによって保留されるため、一度シグナルハンドラが呼び出されてもデフォルトの動作に戻ることはありません。ちなみに、glibc2ライブラリではBSDの動作に従っているようです。

● プログラムの書き方

 BSDシグナル処理体系でサンプルプログラムを書くと、次のようになります。


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

char buff[256];

void warikomi(int signo){

        int i = 0;

        printf("there was interrupt.\n");
        puts(buff);
        sleep(1);

        /*バッファクリア*/
        while (buff[i] != '\0'){
        buff[i] = '\0';
        i++;
        }
        i = 0;
}

int main(void){

        signal(SIGINT, warikomi);

           while(1){
              gets(buff);
              printf("I'm waiting!\n");
              pause();
           }

return 0;
}


 BSDシグナルはシステムコール中のシグナル割り込みをエラーで戻らず、同一シグナルが到着しても新しいシグナルを保留するので、このように非常にストレートな書き方ができます。
 また、libc5システムにおいて<signal.h>のかわりに<bsd/signal.h>をインクルードすると、signal()は__bsd_signalに再定義されてBSDシグナル処理体系となります。どちらの種類のsignal関数もsigaction関数を用いて作られたライブラリルーチンであり、推奨されません。


POSIXシグナル

 リエントラント問題に関して、一見優れた解決法と思われたBSDシグナルは、前編でもふれたようにあまり普及しませんでした。そのうえ、二つの処理体系ができたことで互換性の問題が表れました。
 現在ではこのような、二つの処理体系が入り交じり、混沌とした状況を打開するためPOSIXが策定したPOSIXシグナル処割り込みを実装するべきとされています。

● POSIXのリエントラント処理

 POSIXシグナル処理体系のデフォルトでは、次のようになっています。しかし、これらの動作は設定によって変えることができるため、BSDの動作もSysVの動作も模倣することができます。

» ブロック中のシグナル受信

 POSIXのデフォルトでは、SysVのようにシステムコールなどのブロック中にシグナルが到着した場合、エラー(-1)を吐いて停止します。

» 同一シグナルの到着

 デフォルトではBSDのように新しく到着したシグナルを保留します。それ故にシグナルハンドラが呼び出されるたびにシグナルの動作をデフォルトに戻すこともしません。

● sigaction関数

 POSIXシグナル処理体系ではBSDのシグナルの保留という考えを取り入れ、シグナルマスクでシグナルの動作を指定します。POSIXではsigactionシステムコール関数を使ってシグナルの動作を設定します。sigactionのプロトタイプは次のようになっています。


#include <signal.h>

int sigaction(int signum, const struct sigaction *newaction, struct sigaction *oldaction);

 第一引数のsignumにはSIGKILLとSIGSTOP以外のシグナルをなんでも指定できます。第二引数のactがNULL以外であれば、signumの新しい動作としてactが設定されます。第三引数のoldactがNULLでないならば、今までの動作がoldactに格納されます。つまり、シグナルの古い動作が格納されているsigaction構造体がコピーされます。
 sigaction関数の戻り値は、成功時には0が返り、失敗時には-1が返ります。

● sigaction構造体

 第二引数のactを格納するsigaction構造体は次のようになっています。このsigaction構造体を用いてシグナルの動作を設定します。


struct sigaction {
   void (*sa_handler)(int);
   void (*sa_sigaction)(int, siginfo_t *, void *);
   sigset_t sa_mask;
   int sa_flags;
   void (*sa_restorer)(void);
}

» sa_handler
sigaction関数の第一引数で与えられたシグナルへの動作を設定
» sa_sigaction
より詳細な情報を引数に受け取ることのできるシグナルハンドラを設定
» sa_mask
シグナルハンドラ実行中にブロックするシグナルを設定
» sa_flags
動作の詳細を設定するフラグ
» sa_restorer
廃止予定

● sa_handler

 int型の引数をひとつとってvoidを返す関数へのポインタです。ここでsigaction関数の第一引数で与えられたシグナルへの動作を設定します。例えばSIG_IGNと指定すればシグナルを無視し、SIG_DELと指定すればシグナルの動作をデフォルトに戻し、関数のアドレスを指定すると送信されたシグナルを識別するためのパラメータ(シグナル番号)が付けられてその関数が呼び出されます。

● sa_sigaction

 より詳細な情報を引数に受け取ることのできるシグナルハンドラを設定します。a_flagsにSA_SIGINFOを加えることでsa_handlerの代わりに使用できるようになります。
 sa_sigactionのパラメータであるsiginfo_tは次の要素を持つ構造体です。


siginfo_t {
   int      si_signo;  /* Signal number */
   int      si_errno;  /* An errno value */
   int      si_code;   /* Signal code */
   pid_t    si_pid;    /* Sending process ID */
   uid_t    si_uid;    /* Real user ID of sending process */
   int      si_status; /* Exit value or signal */
   clock_t  si_utime;  /* User time consumed */
   clock_t  si_stime;  /* System time consumed */
   sigval_t si_value;  /* Signal value */
   int      si_int;    /* POSIX.1b signal */
   void *   si_ptr;    /* POSIX.1b signal */
   void *   si_addr;   /* Memory location which caused fault */
   int      si_band;   /* Band event */
   int      si_fd;     /* File descriptor */
}

 si_signoはシステムによって生成されたシグナル番号が格納されており、si_errnoが0以外であればerrno.hで関連付けされたエラー番号が格納され、si_codeにはシグナルが発生した理由を表すコードが格納されます。その他の変数についてはシグナルによって変わってきます。si_pidはシグナルを送信したプロセスのIDが格納され、si_uidはシグナル送信元プロセスの実ユーザIDが格納されます。si_statusは終了値や終了シグナルが格納され、si_valueにはシグナル値が、si_addrにはフォルトしている命令のアドレスが格納されます。

 si_codeはシグナルが発生した理由を表したコードが格納されます。次の値が si_codeに格納されることになります。

・SI_USER
kill()、raise()などから送られたシグナル
・SI_QUEUE
sigqueue()から送信されたシグナル
・SI_TIMER
timer_settime()で設定されたタイマーが終了した時に生成されたシグナル
・SI_ASYNCIO
非同期I/Oの要求が完了した時に生成されたシグナル
・SI_MESGQ
空のメッセージキューにメッセージが到着した時に生成されたシグナル

 上記で挙げたイベントや関数以外でシグナルが生成された場合、si_codeには上記の値とは異なる値(処理系定義の値)が格納されることになります。Signalの列はどのシグナルにおいての理由なのかを表しています。シグナルの種類によって発生理由にばらつきがあります。

  Signal    Code            Reason
  ________________________________________________________________
  SIGILL
            ILL_ILLOPC      不当なオペコード
            ILL_ILLOPN      不当なオペランド
            ILL_ILLADR      不当なアドレスモード
            ILL_ILLTRP      不当なトラップ
            ILL_PRVOPC      特権オペコード
            ILL_PRVREG      特権レジスタ
            ILL_COPROC      コプロセッサエラー
            ILL_BADSTK      内部スタックエラー
  ________________________________________________________________
  SIGFPE
            FPE_INTDIV      0による整数除算
            FPE_INTOVF      整数のオーバフロー
            FPE_FLTDIV      0による浮動小数点除算
            FPE_FLTOVF      浮動小数点のオーバフロー
            FPE_FLTUND      浮動小数点のアンダフロー
            FPE_FLTRES      浮動小数点の不正確な結果
            FPE_FLTINV      無効な浮動小数点演算
            FPE_FLTSUB      添え字の範囲超過
  ________________________________________________________________
  SIGSEGV   SEGV_MAPERR     アドレスがオブジェクトにマップしてない
            SEGV_ACCERR     マップしたオブジェクトの無効な許可
  ________________________________________________________________
  SIGBUS
            BUS_ADRALN      無効なアドレス整列
            BUS_ADRERR      存在しない物理アドレス
            BUS_OBJERR      オブジェクト特有のハードウェアエラー
            BUS_XMEM        I/Oアドレスへの予約済み命令
  ________________________________________________________________
  SIGTRAP
            TRAP_BRKPT      プロセスのブレークポイント
            TRAP_TRACE      プロセスのトレーストラップ
  ________________________________________________________________
  SIGCHLD
            CLD_EXITED      子プロセスが存在してる
            CLD_KILLED      子が終了させられた
            CLD_DUMPED      子が終了しコアファイルを作成した
            CLD_TRAPPED     トレースした子がトラップしている
            CLD_STOPPED     子プロセスの停止
            CLD_CONTINUED   停止した子プロセスの再開
  ________________________________________________________________
  SIGPOLL
            POLL_IN         データが入力可能
            POLL_OUT        出力バッファが使用可能
            POLL_MSG        入力メッセージが使用可能
            POLL_ERR        I/Oエラー
            POLL_PRI        優先的入力が使用可能
            POLL_HUP        デバイスが非接続

● sa_mask

 シグナルはシグナルハンドラの処理中に他のシグナルのシグナルハンドラを呼び出すことができ、これが問題となることがあります。sa_mask要素は、sigaction関数の第一引数で与えられたシグナルのシグナルハンドラ実行中に無視(block)するシグナルのマスクを表します。これが設定できるのはSIG_IGN、SIG_DEL以外のシグナルハンドラです。
 さらに、SA_NODEFERフラグが指定されていない場合は、シグナルハンドラを呼び出したシグナルにもsa_maskが適用されます。例えばSIGINTで呼び出されたシグナルハンドラ処理中に、新たにSIGINTが到着した場合、新しく到着したSIGINTを無視します。これはデフォルトとなっています。

 sa_maskはひとつが一種類のシグナルを扱うboolen型のフラグの集合として実装されており、このフラグセットの操作は次の4つの関数を用います。


int sigemptyset(sigset_t *set)
    /* setの全フラグをセット */

int sigfillset(sigset_t *set)
     /* setの全フラグをリセット */

int sigaddset(sigset_t *set, int signum)
     /* signumで指定したフラグを個別にセット */

int sigdelset(sigset_t *set, int signum)
     /* signumで指定したフラグを個別にリセット */

● sa_flags

 sa_flagsはシグナルハンドラの動作を変更するためのフラグの集合を指定します。sa_flagsには、次のフラグの論理和をとったものを指定します。

・SA_NOCLDSTOP

 signumがSIGCHLDの場合、子プロセスが停止したり再開したりしたときにSIGCHLDの通知を受けなくなります。

・SA_NOCLDWAIT

 signumがSIGCHLDの場合、子プロセスが終了したときに子プロセスをゾンビプロセスに変化させません。 (Linux2.6以降)

・SA_RESETHAND

 シグナルハンドラが呼ばれるごとにシグナルの動作をデフォルトに戻します。SvsVシグナル処理体系のような動作をします。

・SA_ONSTACK

 sigaltstack関数で提供される、別のシグナルスタックでシグナルハンドラを呼び出します。別のシグナルスタックが利用可能でなければ、デフォルトのスタックが使用されます。

・SA_RESTART

 いくつかのシステムコールをシグナルの到着の前後で再開できるようにして、BSDシグナル処理体系のセマンティックスと互換性のある動作を提供します。

・SA_NODEFER

 それ自身のシグナルハンドラ内部にいる時でもそのシグナルをブロックしないようにします。つまり、BSDシグナル処理体系から保留という概念を取り除いたような動作をします。

・SA_SIGINFO

 シグナルハンドラはひとつではなく、三つの引き数をとります。この場合はsa_handlerのかわりにsa_sigactionを設定しなければなりません 。

● sa_restorer

 廃止予定ですので、使用するべきではありません。POSIXではsa_restorer要素に関する規定はなくなっています。

● プログラムの書き方

 sigaction関数を用いてサンプルのプログラムを書くには、次のようにします。sigactionは、sa_flagsのメンバの設定によってBSD風の動作もSysV風の動作も模倣できますので二パターンの記述方法を説明します。

» BSD風の動作をさせる場合

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

char buff[256];

void warikomi(int signo){

        int i = 0;

        printf("there was interrupt.\n");
        puts(buff);
        sleep(1);

        /*バッファクリア*/
        while (buff[i] != '\0'){
        buff[i] = '\0';
        i++;
        }
        i = 0;
}

int main(void){

struct sigaction sa;

        memset(&sa, 0, sizeof(struct sigaction));
        sa.sa_handler = warikomi;
        sa.sa_flags   = SA_RESTART;

        if(sigaction(SIGINT, &sa, NULL) != 0 ){
            printf("sigaction error\n");
            exit(1);
        }

        while(1){
            gets(buff);
            printf("I'm waiting!\n");
            pause();
        }

return 0;
}


 main関数内のmemsetでsa構造体を0でクリアしていますが、あくまで一応であり絶対に必要な処理ではないでしょう。sa.sa_handlerでwarikomi関数をハンドラとして設定しています。sa.sa_flagsでシステムコールを中止しないように設定します。sigaction関数でSIG_INTのシグナルにsa構造体の設定を適用しています。これで、BSD風の動作を模倣することができます。

» SysV風の動作をさせる場合

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

char buff[256];

struct sigaction sa;
struct sigaction ignore;

void warikomi(int signo){

        int i = 0;

        sigaction(SIGINT, &ignore, NULL);

        printf("there was interrupt.\n");
        puts(buff);
        sleep(1);

        /*バッファクリア*/
        while (buff[i] != '\0'){
        buff[i] = '\0';
        i++;
        }
        i = 0;

        sigaction(SIGINT, &sa, NULL);
}

int main(void){

        int rc;

        memset(&sa, 0, sizeof(struct sigaction));
        sa.sa_handler = warikomi;
        sa.sa_flags   = SA_NODEFER;
        sa.sa_flags  |= SA_RESETHAND;

        memset(&ignore, 0, sizeof(struct sigaction));
        ignore.sa_handler = SIG_IGN;
        ignore.sa_flags   = SA_NODEFER;
        ignore.sa_flags  |= SA_RESETHAND;

        if(sigaction(SIGINT, &sa, NULL) != 0){
            printf("sigaction error\n");
            exit(1);
        }

        while(1){
            rc = gets(buff);
            if(rc == -1){
                 if(errno != EINTR){
                     printf("error!\n" );
                     exit(1);
                 }
            }
            printf("I'm waiting!\n");
            pause();
        }
return 0;
}


 SysVシグナルを模倣するにはsa構造体とignore構造体をハンドラ内とmain関数内で切り替えることで実現しています。
 sa構造体はハンドラをwarikomi関数に設定しており、sa_flagsにSA_NODEFERとSA_RESETHANDを設定することで、二重に起動するシグナルをブロックしないようにし、シグナルを呼び出されるたびにデフォルトの動作にするようにしています。
 main関数内ではsa構造体を適用し、ハンドラが呼び出されるとignore構造体を適用することでハンドラ内で新たに到着するシグナルを無視しています。そのままmain関数へ戻ってしまっては次にSIGINTが到着してもデフォルト動作へ戻ってしまうため、ハンドラの最後でSvsVと同じようにsa構造体を適用してシグナルの到着を待ち受けます。

 さて、POSIXシグナル処理体系のデフォルトではシグナルハンドラ処理中の同一シグナルの到着を保留します。しかし、シグナルはキューに入らないという性質がありますのでシグナルの状態は保留中かそうでないかのどちらかしかありません。シグナルハンドラの処理中に、シグナルハンドラを呼び出したシグナルと同一のシグナルが複数回到着しても、最初に送られてきた同一シグナルを保留して、残りを全て破棄します。例えばSIGINTでハンドラを呼び出し、その後5回SIGINTを発生させたとしても保留されるのははじめの1回で、後の4回は破棄されてしまいます。シグナルハンドラの処理終了時に保留されていたシグナルが実行されます。


volatile修飾子

 volatile修飾子は変数に処理の最適化をしないようにコンパイラに知らせるためのものです。コンパイラは最適化という工程で変数の値をCPUのレジスタに割り当てたり、不要な命令を削除するといった、場合によっては勝手なことをやってくれます。シグナルを用いてプログラムを作る場合、この最適化が問題になることがあります。例えば、次のプログラムをご覧ください。


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

int warikomi_flag = 0; /*割り込みフラグを初期値0に設定*/

void warikomi(signo){

        warikomi_flag = 1; /*割り込みが発生したため1にする*/

        /* ....なんらかの処理..... */

        warikomi_flag = 0; /*割り込み処理が終わったため0に戻す*/
}

int main(void){

        printf("main start.\n");

     while(1){
        while(warikomi_flag != 0;){ /*割り込み発生してない間*/
           /* ....なんらかの処理..... */
        }
        sleep(1); /*割り込みが発生した時1秒寝ます*/
     }
return 0;
}


 このプログラムはグローバル変数warikomi_flagを用いて割り込みの発生時はmain関数内での処理を1秒づつ延期するプログラムです。見ての通り変数warikomi_flagの値が0か1かによって割り込みが発生しているのかどうかを判断しています。
 しかし、もしコンパイラが最適化の途中でwarikomi_flagの値をCPU内部のレジスタのひとつに格納し、これ以降のwarikomi_flagが参照される部分で主記憶装置ではなくレジスタに保存した値を参照するようになった場合、常に割り込みが発生していないことになってしまいます。こういった事態は、コンパイラが遅い主記憶装置へのアクセスよりも高速なレジスタへのアクセスへ最適化するために起こり得ます。

 このような場合、コンパイラに対してwarikomi_flagの値は変更される可能性があることを知らせる必要があります。この通知がvolatile修飾子です。先ほどのプログラムの場合ですと「int warikomi_flag = 0;」の部分を「volatile int warikomi_flag = 0;」と書き直すことで通知することができます。
 こういった理由から、割り込みを利用するプログラムのフラグになるような変数にはvolatile修飾子をつけておくのが無難です。

 これらのことを頭の片隅においてシグナル処理プログラミングを満喫していただきたく思います。


■参考文献


go back to the TOP page of Glazheim Lykeion.