Embedded UNIX Vol.1 重点記事 Linux 2.5で標準化されたプリエンプティブルカーネルの実装(木内志朗)
追加コラム
■プリエンプティブルカーネルでの適切なロック■
――プリエンプトセーフなカーネルコードを維持するために
カーネルソースに付属するプリエンプティブルカーネルのロック機構に関するドキュメントが,非常に有用なので翻訳しました(Documentation/preempt-locking.txt).
はじめに
プリエンプティブルカーネルは,新しいロックの問題を起こします.この問題は,同期およびリエントラントにおいて,SMPの場合と同じです.Linuxのプリエンプティブルカーネルモデルは,すでに存在しているSMPのロックメカニズムを使用しています.したがって,カーネルは,非常に少ない付加的な状態のために,明示的なロック機構を追加する必要があります.
このドキュメントは,カーネルハッカーのためのものです.開発されるカーネルのコードは,ここで示すルールを守る必要があります.
● ルール1:
CPUごとのデータは明示的に保護する必要がある
以下のサンプルコードでは,二つの似たような問題点があります.
struct this_needs_locking tux[NR_CPUS];
tux[smp_processor_id()]= some_value;
/* タスクはここでプリエンプトされる・・・ */
something = tux[smp_processor_id()];
この例では,CPUごとにデータがあるため,それらを明示的にSMPのロック機構で保護していませんが,ロック機構で保護する必要があります.次に,プリエンプトされたタスクが最終的にリスケジュールされたとき,smp_processor_id()の値は,前の値と等しくないかもしれません.したがって,これらの状況ではプリエンプションを不許可にして,これらの状態を保護しなくてなりません.
put_cpu()とget_cpu()を使用して,プリエンプションを不許可にすることができます.
● ルール2:
CPUステートは保護しなくてはならない
プリエンプション可能な環境では,CPUの状態は保護されなければなりません.これは,アーキテクチャに依存していますが,コンテキストスイッチで保存されなかったCPU構造とステートを含みます.
たとえば,x86の場合,FPUモードの入口と出口は,プリエンプションを不許可にして保護しなくてならないクリティカルな領域です.もし,カーネルがFPUの命令を実行中にプリエンプトされた場合,何が起きるか考えてみてください.カーネルは,ユーザータスク以外のFPUの状態は保存しないということを覚えておいてください.したがって,プリエンプションは,このような領域では不許可にしなくてはなりません.
注:いくつかのFPU機能は,すでに明白にプリエンプトに対して安全です.たとえば,kernel_fpu_beginおよびkernel_fpu_endは,それぞれプリエンプションを許可/不許可にします.しかし,math_state_restoreはプリエンプションを不許可にして呼ばなくてはなりません.
● ルール3:
ロックの取得とリリースは同じタスクで実行する
一つのタスクによって取得されたロックは,同じタスクでリリースされなくてはなりません.これは,あるタスクがロックを取得して,他のタスクがロックをリリースする間に別のことをするといった変わった使い方はできません.もし,同様のことを行いたいのであれば,同じコードパスのタスクで取得とリリースを行い,呼び出し側が一方のタスクに対してイベントで通知してください.
● 解決方法
プリエンプション環境におけるデータの保護は,クリティカルな領域をプリエンプション不許可にすることにより達成することができます.
○preempt_enable()
プリエンプトカウンタをデクリメントする
○preempt_disable()
プリエンプトカウンタをインクリメントする
○preempt_enable_no_resched()
デクリメントするが,ただちにプリエンプトはしない
○preempt_check_resched()
必要があれば,リスケジュールする
○preempt_count()
プリエンプト カウンタ値を返す
これらの関数は,ネスト可能です.すなわち,実行されているコードでpreempt_disable()をN回呼ぶことができますが,preempt_emable()がN回呼ばれるまでは,プリエンプトは行われません.もし,プリエンプトが許可されていなければ,プリエンプトの処理は何も行いません.
もし,何かロックを保持しているか,割り込みを不許可にしているなどといった明確にプリエンプションが不許可である状態であれば,明示的にプリエンプトを不許可にする必要がないことに注意してください.
しかし,割り込みを不許可にすることによりプリエンプションを不許可にする方法は,根本的に危険な方法であることを心にとめておいてください.
spin_unlock()によってプリエンプトカウントが0までデクリメントされると,リスケジュールを引き起こします.printk()により,簡単にリスケジュールを起こすことさえできます.関連があるコードは,このようなことが発生しないとわかっているときだけ,暗黙にプリエンプションが不許可になるという特性を使用すべきです.もっとも良い方法は,自分自身で書いた簡単な処理のコードをアトミック操作だけのための,非常に狭い領域で使用することです.
● 例
cpucache_t *cc; /* このデータはCPUごとに存在します */
preempt_disable();
cc = cc_data(searchp);
if (cc && cc->avail) {
__free_block(searchp, cc_entry(cc), cc->avail);
cc->avail = 0;
}
preempt_enable();
return 0;
どのようなプリエンプションのステートメントが,クリティカルな変数のすべての参照を含んでいるかに注意してください.また,以下は別の例です.
int buf[NR_CPUS];
set_cpu_val(buf);
if (buf[smp_processor_id()] == -1) printf(KERN_INFO "wee!エn");
spin_lock(&buf_lock);
/* ... */
このコードはプリエンプトセーフではありません.しかし,spin_lock()を2行上に移すことで容易に修正できます.
○割り込みを不許可にすることでプリエンプションを防止する
local_irq_disableとlocal_irq_saveを使用してプリエンプションのイベントを防止することが可能です.このようにする場合,need_reschedをセットし,プリエンプションのチェックにより生じるイベントを起こさないように非常に注意しなくてはなりません.疑わしいときには,明示的にプリエンプトを不許可にするか,ロックにより保護してください.
現在の2.5カーネルは,CPUごとに割り込みを不許可にします(ローカルCPU).
懸念事項は,local_irq_disableとlocal_irq_saveの適切な用法です.これらはプリエンプションから保護するために使用されるかもしれませんが,出口でプリエンプションが許可されているかもしれないので,プリエンプションが要求されているかどうかを確認するテストは行うべきです.これらが,spin_lock()および読み込み/書き込みのロックマクロから呼ばれれば,テストは行われます.
これらは,スピンロックで保護された領域内で呼ばれるかもしれません.しかし,保護された領域外で呼ばれれば,プリエンプションのテストは行うべきです.プリエンプションロックによって,割り込みステートやボトムハーフ/タスクレットから呼ばれるこれらのコールは,プリエンプションのロックによって保護されているおり,プリエンプションのテストを行わないバージョンを使用するかもしれないことに注目してください.
|