法大奥山研究室

 previous  contents

17.2. volatile


 volatile修飾型は,次の文法を持ちます。

プログラムの実行とは無関係に値が変化するデータという意味なのですが,仕様書(C99)だけでは良く分からないので,冗長にならない程度に説明を付します。

 volatile は該当の変数に処理の最適化をしないようにコンパイラに知らせるためのものです。コンパイラは処理の最適化を試み,一部の変数を CPU のレジスタに割り当てたり,文を削除することがあります。次の例では,register を付していないにもかかわらず,変数 i が CPU のレジスタ(r30)に割り当てられています。

* 逆アセンブル disassemble の結果を読むにはアセンブリ言語の知識が必要です。マニュアルは各CPUの生産元からダウンロード可能です。また,同じような最適化をすべてのコンパイラが行うとは限りません。

/* test1.c */

#include <stdio.h>

int main(void)
{
       int i;
       for(i = 0; i < 5; i++)
              printf("%d\n", i);

       return 0;
}
% cc -O -Wall test1.c
% gdb a.out
GNU gdb 5.1-20020125 (Apple version gdb-213) (Wed Apr  3 04:16:48 GMT 2002)
Copyright 2002 Free Software Foundation, Inc.
...
(gdb) disassemble main
...
0x1e94 <main+20>:       mflr    r31
0x1e98 <main+24>:       li      r30,0
0x1e9c <main+28>:       addis   r3,r31,0
0x1ea0 <main+32>:       addi    r3,r3,216
0x1ea4 <main+36>:       mr      r4,r30
0x1ea8 <main+40>:       bl      0x1fdc <dyld_stub_printf>
0x1eac <main+44>:       addi    r30,r30,1
0x1eb0 <main+48>:       cmpwi   r30,4
0x1eb4 <main+52>:       ble+    0x1e9c <main+28>
0x1eb8 <main+56>:       li      r3,0
0x1ebc <main+60>:       lwz     r0,88(r1)
0x1ec0 <main+64>:       addi    r1,r1,80
0x1ec4 <main+68>:       mtlr    r0
0x1ec8 <main+72>:       lmw     r30,-8(r1)
0x1ecc <main+76>:       blr

次の例では,最適化によって while文全体が削除されています。(i0 なため,繰り返しの条件はつねに偽。コンパイラは while文の処理が無駄であると考えたため削除。)

/* test2.c */

#include <stdio.h>

int main(void)
{
       int i = 0;
       while(i) {
              printf("%d\n", i);
              i = 0;
       }
       printf("%d\n", i);
       return 0;
}
% cc -O -Wall test2.c
% gdb a.out
GNU gdb 5.1-20020125 (Apple version gdb-213) (Wed Apr  3 04:16:48 GMT 2002)
Copyright 2002 Free Software Foundation, Inc.
...
(gdb) disassemble main
...
0x1ea4 <main+20>:       mflr    r31
0x1ea8 <main+24>:       addis   r3,r31,0
0x1eac <main+28>:       addi    r3,r3,200
0x1eb0 <main+32>:       li      r4,0
0x1eb4 <main+36>:       bl      0x1fdc <dyld_stub_printf>
0x1eb8 <main+40>:       li      r3,0
0x1ebc <main+44>:       lwz     r0,88(r1)
0x1ec0 <main+48>:       addi    r1,r1,80
0x1ec4 <main+52>:       mtlr    r0
0x1ec8 <main+56>:       lwz     r31,-4(r1)
0x1ecc <main+60>:       blr

しかしながら,このような最適化は,上のようなプログラムでは,特段,問題はありません。むしろ歓迎でしょう。それではいつ volatile を使うのでしょうか。

 仕様(C99)で volatile を付さない場合に未定義の動作となるのは次の2つです。

* これらが仕様に含まれた経緯は ISO/IEC JTC1/SC22/WG14, A draft rationale for the C99 standard (N897) が参考になります。

シグナルによる割り込み(interrupt)は,プログラムの進行を遮断するため,変数の値が記憶されているか否か不明になります。volatile を付すと,読み込み時点で値が変化することを許します。(通常は,代入演算子などでプログラムの中で値が変更され保存される。volatile を付すと,参照するだけで値が変化することをコンパイラは考慮する。)volatile は,プログラムの外で値が変化する変数のための型修飾子とも言えます。例えば,次のようなケースです。

マウス,プリンタ,モデムなど,デバイスのプログラムをする方には必需品です。

■非局所ジャンプ(Nonlocal Jump):setjmplongjmp [C99, 7.13]

#include <setjmp.h>
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

setjmplongjmp で使われる環境 env を保存します。型 jmp_buf はヘッダ setjmp.h で定義されており,保存する内容を確認できます。setjmpifswitch の選択文(selection statement)と whilefor などの繰返し文(iteration statement)のみに使えます。longjmpsetjmp で保存された環境 env を,setjmp が呼ばれたところで復元します。setjmp は,longjmp から戻った場合には val を,そうでない場合には 0 を返します。

/* Example 17.3 */

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

void f(void);

static jmp_buf env;

int main(void)
{
       int val = -1;
       int x;     // volatile が必要

       x = 3;
       if((val = setjmp(env)) != 0) {
              printf("val = %d (x = %d)\n", val, x);
              exit(0);
       }

       x = 5;
       printf("val = %d (x = %d)\n", val, x);
       f();

       exit(0);
}

void f(void)
{
       longjmp(env, 1);
}

実行結果です。

val = 0 (x = 5)
val = 1 (x = 3)

valsetjmp が最初に呼ばれた時点で 0 となり,longjmp が呼ばれたところで 1 となります。envsetjmp が実行された時点の環境,例えば,x = 3 が保存されており,longjmp が実行された時点での x = 5 という環境は保存されていません。longjmp が実行された時点での x の値を setjmp にジャンプした後に参照するには,

volatile int x;

としなければなりません。volatile を付さない場合,longjmp から setjmp にジャンプ後, x が3なのか5なのか,動作は未定義です。

signal関数と割り込み(Interrupt)[C99, 7.14]

#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);

シグナル sig を受け取ると,シグナルハンドラ func を実行します。シグナルハンドラには,次の2つが最初から用意されています。

SIG_DFL   規定の処理
SIG_IGN   シグナルを無視

また,シグナル sig は仕様(C99)に掲載されている

SIGFPE    floating-point exception
SIGILL    illegal instruction
SIGSEGV   segmentation violation

以外にも多数あります。各シグナルに対する SIG_DFL での処理も含め,man 3 signal で確認してください。

/* Example 17.4 */

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>   // sleep

void sigHandler(int);

int main(void)
{
       int i;

       signal(SIGINT, sigHandler);

       for(i = 1; i < 60; i++) {
              printf("sleeping...(%d)\n", i);
              sleep(1);
       }
       exit(0);
}

void sigHandler(int sig)
{
       if(sig == SIGINT)
              fprintf(stderr, "\nSIGINT recieved.\n");
       else
              fprintf(stderr, "\nUnpredictable signal recieved.\n");

       exit(0);
}

実行例です。「コントロール+C」でシグナル SIGINT がおきます。

% ./a.out
sleeping...(1)
sleeping...(2)
sleeping...(3)
sleeping...(4)
sleeping...(5)
sleeping...(6)
^C
SIGINT recieved.

シグナルハンドラ sigHandler が参照する変数は volatile修飾型である必要があります。


 previous  contents