リエントラント

出典: フリー百科事典『ウィキペディア(Wikipedia)』
移動: 案内検索

リエントラントreentrant再入可能)とは、プログラムサブルーチンが、実行の途中で割り込まれ、その実行が完了する前に再び呼び出され実行されても安全だという性質を指す。割り込みは分岐や呼び出しなどの内部的な動きによって生じる場合もあるし、ハードウェア割り込みシグナルなどの外部の動きによって生じる場合もある。割り込んでの呼び出しが完了すれば、割り込まれた呼び出しが実行を継続できる。

この定義はシングルスレッドのプログラミング環境が起源であり、ハードウェア割り込みで割り込まれた制御の流れが割り込みサービスルーチン (ISR) に転送されることから生まれた。ISRが使用するサブルーチンは割り込みをきっかけとして実行される可能性があるため、リエントラントでなければならない。OSのカーネルが使用するサブルーチンの多くは、カーネルで確保済みのリソースを超えられない制限がありリエントラントではない。そのためISRでできることは限られている。例えば、一般にISRからファイルシステムにはアクセスできないし、場合によってはヒープ領域も確保できない。

直接または間接に再帰可能なサブルーチンはリエントラントである。しかし、グローバル変数が処理の流れの中でしか変化しないことを前提としているサブルーチンはリエントラントではない。グローバル変数を更新するサブルーチンが再帰的に呼び出されれば、1回のサブルーチン実行の中でグローバル変数は突然変化することになる。

リエントラント性の概念はシングルスレッドの環境に起源があり、マルチスレッド環境でのスレッドセーフという概念とは異なる。リエントラントなサブルーチンはスレッドセーフにすることもできるが[1]、リエントラントだというだけであらゆる状況でスレッドセーフと言えるわけではない。逆にスレッドセーフなコードはリエントラントである必要はない(後述の例を参照)。

[編集]

次の例の swap() 関数は、リエントラントではない(同時にスレッドセーフでもない)。したがってこれを割り込みサービスルーチン isr() で使用すべきでない。

int t;
 
void swap(int *x, int *y)
{
 t = *x;
 *x = *y;
 // ここでハード割り込みが起きて isr() が呼び出される可能性がある
 *y = t;
}
 
void isr()
{
 int x = 1, y = 2;
 swap(&x, &y);
}

swap()tスレッド局所記憶にすることでスレッドセーフにできる。しかしそのようにしてもリエントラントにはならず、swap() 実行中に同じスレッドのコンテキストで isr() が呼び出されれば問題を生じる可能性が残っている。

次の工夫を加えたswap関数では、実行完了時のグローバルなデータを注意深く一貫性を保つようにしており、完全にリエントラントである。ただし、実行途中のグローバルなデータの一貫性は保証されていないのでスレッドセーフではない。

int t;
 
void swap(int *x, int *y)
{
 int s;
 
 s = t; // グローバル変数をセーブ
 t = *x;
 *x = *y;
 // ここでハード割り込みが起きて isr() が呼び出される可能性がある
 *y = t;
 t = s; // グローバル変数をリストア
}
 
void isr()
{
 int x = 1, y = 2;
 swap(&x, &y);
}

背景[編集]

リエントラント性と冪等性は同義ではない。冪等な関数は、何度呼び出したとしても1度だけ呼び出したかのように全く同じ出力を生成する。一般化すれば、共有データを使わず、入力データに基づいて出力データを生成する関数である(ただし、入力や出力はオプション)。共有データはいつでも誰でもアクセスできる。データを誰かが更新し、誰も更新を把握していない場合、そのデータが以前と比べて変化したのかどうかさえ誰にもわからない。冪等性はリエントラント性を包含するが、逆は必ずしも真ではない。

データにはスコープという属性がある。グローバルなデータはあらゆる関数のスコープの範囲外にあり、寿命は不定である。一方局所的なデータは関数が呼び出されるたびに生成され、関数から抜けるときに破棄される。

局所的データはルーチン間で共有されず、再入時にも共有されない。したがってリエントラント性を阻害しない。グローバルなデータは任意の関数間で共有でき、あるいは再入時にも共有される。したがってリエントラント性を阻害する。

リエントラント性はスレッドセーフの概念とは異なるが、密接に関連している。関数がスレッドセーフであっても、リエントラントでないことがある。例えば関数全体をミューテックスで囲むと、マルチスレッド環境では問題を防げるが、割り込みサービスルーチンで使用すると再入時にミューテックスの解放を待ち続けることになる。混乱を避ける鍵は、リエントラントが1つのスレッド実行でも問題になるという点である。リエントラント性はマルチタスクOSの存在する前からの概念である。

リエントラント性の原則[編集]

リエントラントなコードは、静的変数英語版やグローバル変数を保持しない。
リエントラントな関数はグローバルなデータを使えないわけではない。例えばリエントラントな割り込みサービスルーチンは、(例えば、シリアルポートのバッファを読み取るなど)ハードウェアのステータス情報を取得できるが、それはグローバルなデータであると同時に揮発性である。それでも静的変数やグローバルなデータを普通に使うことは勧められず、不可分なリード・モディファイ・ライト命令を使ってそのような変数にアクセスするべきである(そのような不可分命令を実行中は割り込みやシグナルが処理を中断できない)。
リエントラントなコードは自分のコードを書き換えない。
OSによってはプロセスが自身のコードを書きかえることを許している。その理由は様々だが(例えば、BitBltでグラフィックスを高速化するためなど)、呼び出す度にコードが変化している可能性があるなら、リエントラント性との両立は難しい。
しかし、呼び出す度にメモリ上の新たな場所にある機械語コードを実行するなら、コードを書き換えても他の呼び出しには影響せず、両立は不可能ではない。
リエントラントなコードは、リエントラントでないプログラムサブルーチンを呼び出さない。
ユーザー/オブジェクト/プロセスに複数レベルの優先度があることや、マルチプロセッシングがリエントラントなコードの制御を複雑化させている。リエントラントな設計においては、あらゆるアクセスに絶えず注意することが重要であり、ルーチン内の副作用に注意するすることが重要である。

リエントラントな割り込みハンドラ[編集]

リエントラントな割り込みハンドラは、割り込み処理中に早期に割り込み可能にする割り込みハンドラである。それによって割り込みレイテンシ英語版が低減される[2]。一般に割り込みサービスルーチンをプログラミングする際、割り込みハンドラ内でなるべく早期に割り込み可能な状態にすることが推奨される。それによって割り込みを拾い損なうのを防ぎやすくなる[3]

さらなる例[編集]

以下のコードにある関数 fg もリエントラントではない

int g_var = 1;
 
int f()
{
 g_var = g_var + 2;
 return g_var;
}
 
int g()
{
 return f() + 2;
}

上記のコードで、f はグローバル変数 g_var に依存している。したがって、2つのプロセス(スレッド)が f を実行すると、g_var に同時並行的にアクセスし、結果はタイミングに依存することになる。したがって、f はリエントラントではない。その f を呼び出している g もリエントラントではない。

これらを若干変更したリエントラントである版を以下に示す:

int f(int i)
{
 return i + 2;
}
 
int g(int i)
{
 return f(i) + 2;
}

新しい版では、グローバル変数 g_var は使われていない。引数を渡して、それに基づいて処理を行って結果を返す。共有される可能性のあるオブジェクトにはアクセスしないようになっている。その代わり、呼出し側が前回の戻り値を引数として渡してやらなければならない。このように、リエントラントなサブルーチンでは、必要な静的データは呼出し側が管理しなければならない。

次のC言語のコードでは、関数はスレッドセーフだが、リエントラントではない。

int function()
{
 mutex_lock();
 ...
 function body
 ...
 mutex_unlock();
}

この function は複数のスレッドから呼び出されても全く問題はない、しかし、リエントラントな割り込みハンドラがこの関数を呼び出す場合、2つ目の割り込みがこの関数実行中に発生すると、二度目の呼び出しはミューテックスを獲得できず、永久に停止する。割り込みサービスでは他の割り込みをディセーブルするので、システム全体がハングアップすることになる。

脚注[編集]

参考文献[編集]

  • Kerrisk, Michael (2010). The Linux Programming Interface. No Starch Press. 

関連項目[編集]

外部リンク[編集]