前回に引き続き割り込みプログラムについてまとめてみたいと思います。Signal programming [No.2]では実際のプログラムの書き方を重点的に解説したいと思いますので、さきにSignal programming [No.1]に一通り目を通していただきたいと思います。
前編では、割り込みの概要と割り込みの動作をキーボード割り込みの実例を挙げて解説しました。また、UNIXでは割り込みを実現するためにシグナルという仕組みを利用しており、シグナルの動作にもSysV、BSD、POSIXの三つの処理体系があることを説明しました。今回はもう少し込み入ったシグナルの解説と、それぞれのシグナル処理体系における、割り込みプログラムの実際の書き方を解説していきたいと思います。
次にサンプルプログラムとして作成するプログラムの仕様を記述します。以後は、ここで記述した要求を満たすようなプログラムをシグナルを使って作って行くことになります。
============= main start ============= ↓ ---------- while(0) ←------. ---------- | ↓ | --------------- | printf(Message) | --------------- | ↓ | ---------- | gets(buff) | ---------- | ↓ | -------- | pause() --------→^ -------- ↓ ============= main stop ============= :::::::::::::::::::::::::: =============== warikomi start =============== ↓ --------------- printf(Message) --------------- ↓ ------------ printf(buff) ------------ ↓ --------- sleep(1) --------- ↓ =============== warikomi stop ===============
シグナルの動作には、あらかじめ決められているデフォルト動作というものがあります。このデフォルトの動作に不満が無ければ、特になにもせずにそのまま使えばよいのですが、もし動作に不満がある場合はユーザプログラムで新しい動作(シグナルハンドラ)を設定することができます。この動作を変更する関数が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シグナル処理体系のそれぞれで異なった動作を行います。リエントラント処理の違いがそれぞれ三つの処理体系の大きな違いといってもよいでしょう。
readシステムコール関数の場合なら、読み込むためのデータの到着を待っている状態です。read関数などのように、その関数での仕事が終わるまで処理を占領してしまうような関数のことをブロッキング関数といい、処理を占領している状態のことを「ブロックしている」と表現します。ブロッキング関数には他にsend関数やaccpet関数などがあります。
OSのカーネルが提供する機能のうち、プロセスから呼び出せるようになっている機能、もしくはその呼び出し規約のこと。ファイルアクセスやメモリの割り当て、子プロセスの生成などの機能が用意されていることが多い。
最近のOSでは、システムコールではなく、APIという用語を使うことが多い。これは、OSのバージョンアップなどによって、従来はカーネルが提供していた機能がライブラリや別プロセスによって提供されるようになるなど、実装方法が多様化していることに対応したものである。@nifty:デジタル用語辞典:システムコール(http://www.nifty.com/webapp/digitalword/word/037/03749.htm)より引用
SysVシグナルは古くからUNIXに実装されてきたシグナルであるため、もはや古いシグナル処理体系であるといえます。しかし、現在でもまだ動作する処理体系であることと、シグナルの動作の実装がどのように変化してきたかという歴史を知ることは、それ以後の技術を理解する手助けになると思いますので、ここでは古いといわれる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ではないエラーの場合には終了します。つまり、システムコールがシグナルの割り込みによってエラーとなった場合は無視して処理を続行するようにコーディングしています。
前述のSysVシグナル処理体系のリエントラント問題の解決方法は少々強引な解決方法でした。そこで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関数を用いて作られたライブラリルーチンであり、推奨されません。
リエントラント問題に関して、一見優れた解決法と思われたBSDシグナルは、前編でもふれたようにあまり普及しませんでした。そのうえ、二つの処理体系ができたことで互換性の問題が表れました。
現在ではこのような、二つの処理体系が入り交じり、混沌とした状況を打開するためPOSIXが策定したPOSIXシグナル処割り込みを実装するべきとされています。
POSIXシグナル処理体系のデフォルトでは、次のようになっています。しかし、これらの動作は設定によって変えることができるため、BSDの動作もSysVの動作も模倣することができます。
POSIXのデフォルトでは、SysVのようにシステムコールなどのブロック中にシグナルが到着した場合、エラー(-1)を吐いて停止します。
デフォルトではBSDのように新しく到着したシグナルを保留します。それ故にシグナルハンドラが呼び出されるたびにシグナルの動作をデフォルトに戻すこともしません。
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が返ります。
第二引数の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);
}
int型の引数をひとつとってvoidを返す関数へのポインタです。ここでsigaction関数の第一引数で与えられたシグナルへの動作を設定します。例えばSIG_IGNと指定すればシグナルを無視し、SIG_DELと指定すればシグナルの動作をデフォルトに戻し、関数のアドレスを指定すると送信されたシグナルを識別するためのパラメータ(シグナル番号)が付けられてその関数が呼び出されます。
より詳細な情報を引数に受け取ることのできるシグナルハンドラを設定します。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_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要素は、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には、次のフラグの論理和をとったものを指定します。
signumがSIGCHLDの場合、子プロセスが停止したり再開したりしたときにSIGCHLDの通知を受けなくなります。
signumがSIGCHLDの場合、子プロセスが終了したときに子プロセスをゾンビプロセスに変化させません。 (Linux2.6以降)
シグナルハンドラが呼ばれるごとにシグナルの動作をデフォルトに戻します。SvsVシグナル処理体系のような動作をします。
sigaltstack関数で提供される、別のシグナルスタックでシグナルハンドラを呼び出します。別のシグナルスタックが利用可能でなければ、デフォルトのスタックが使用されます。
いくつかのシステムコールをシグナルの到着の前後で再開できるようにして、BSDシグナル処理体系のセマンティックスと互換性のある動作を提供します。
それ自身のシグナルハンドラ内部にいる時でもそのシグナルをブロックしないようにします。つまり、BSDシグナル処理体系から保留という概念を取り除いたような動作をします。
シグナルハンドラはひとつではなく、三つの引き数をとります。この場合はsa_handlerのかわりにsa_sigactionを設定しなければなりません 。
廃止予定ですので、使用するべきではありません。POSIXではsa_restorer要素に関する規定はなくなっています。
sigaction関数を用いてサンプルのプログラムを書くには、次のようにします。sigactionは、sa_flagsのメンバの設定によってBSD風の動作もSysV風の動作も模倣できますので二パターンの記述方法を説明します。
#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風の動作を模倣することができます。
#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修飾子は変数に処理の最適化をしないようにコンパイラに知らせるためのものです。コンパイラは最適化という工程で変数の値を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修飾子をつけておくのが無難です。
これらのことを頭の片隅においてシグナル処理プログラミングを満喫していただきたく思います。