spin lock による排他制御 †
spin lock は単純な排他制御機能です。排他制御をしたい一連の処理の前後に spin_lock*(), spin_unlock*() 関数を置いて使います。排他制御をしている時間が短い場合、効率的に使用できます。排他制御を獲得するまで状態変数を操作し続け(特殊な読み書き手順を続け)ます。排他制御時間が長いか、頻度が高い場合、状態変数を操作する時間が長くなり、本来行うべき演算のための時間を失います。使い分けと詳細なドキュメントは Documentation/locking/spinlocks.txt, Documentation/DocBook/kernel-locking/index.html にあります。
spin lock の種類 †
spin lock には次のような種類があります。排他制御を必要とするコンテキストに応じて使い分けてください。/Documentation/DocBook/kernel-locking/cheatsheet.html に早見表があります。
[*1] ハードウエア割り込み禁止に連動しています。
[*2] カウンタを up/down しています。カウンタが初期値(多くの場合 0)より大きければ禁止、初期値に一致すれば許可となります。
[*3] {割り込み|softirq|プリエンプション} の禁止/許可はローカル CPU (spin lock 関数を実行した CPU)が対象です。
どのコンテキストでも使える spin_lock_irqsave() と spin_unlock_irqrestore() †
数多く有る spin lock のうちどのコンテキストでも使える spin_lock_irqsave(), spin_unlock_irqrestore() を主に使っています。spin_lock_irqsave() spin_unlock_irqrestore() は状況によってはしなくても良い割り込み許可フラグの保存・復帰を無条件にします。無駄な処理かもしれません。それでも、実行時間の無駄よりは、spin_lock_irqsave(), spin_unlock_irqrestore() を呼び出した元のコンテキストが何か留意しながらコードを書くより安全な方法を選んでいます。
spin_lock_irqsave() †
spin_lock_irqsave() を呼び出し、関数から戻ってくると次の様な状態になっています。
- ローカル CPU 割り込フラグ保存
flags 変数(慣例的に名前として flags を使います)に格納されます。多くの場合、他の CPU 状態フラグも含んでいます。
- プリエンプション (preemption) カウント アップ
preempt_disable() を呼び禁止カウンタを増やす処理です。x86/amd64 アーキテクチャの場合は CPU 毎に存在する __preempt_count を 1 増やします。他のアーキテクチャの場合は thread_info->preempt_count を 1 増やします。preempt_count > 0 ならばプリエンプションは起きません。
- ローカル CPU 割り込み禁止
spin_lock_irqsave() を 呼び出したコードを実行している CPU の割り込みが禁止になります。他の CPU の割り込み許可/禁止状態は変えません。
- ロック変数を操作して自分のコンテキストがロックを得た状態になる
spin_unlock_restore() †
spin_unlock_irqrestore() を呼び出し、関数から戻ってくると次の様な状態になっています。
- ロック変数を操作して自分のコンテキストがロックを得ていない状態になる
- プリエンプション preemption カウント・ダウン
preempt_enable() を呼び禁止カウンタを減らす処理です。x86/amd64 アーキテクチャの場合は CPU 毎に存在する __preempt_count を 1 減らします。他のアーキテクチャの場合は thread_info->preempt_count を 1 減らします。preempt_count == 0 ならばプリエンプションが起きるようになります。
- ローカル CPU 割り込フラグ復帰
flags に保存した値を CPU の状態フラグへ格納します。割り込み許可・禁止状態は spin_lock_irqsave() を呼び出す前の状態になります。
spin_lock_irqsave() と spin_unlock_irqrestore() の注意点 †
spin_lock_irqsave(), spin_unlock_irqrestore() の注意点を列挙します。
- 割り込み許可フラグを保存する変数 flags の渡し方に注意
spin_lock_irqsave(), spin_unlock_irqrestore() ともロック変数は変数を指すアドレスを渡し、割り込み状態を保存する変数はアドレス演算子 & を付けずに渡します。
- 長時間 lock 状態にしない
ロック期間中はプリエンプションも、割り込みも禁止されます。ロック期間が長いと他のデバイスの処理を遅延させたり、ユーザー応答が悪くなります。繰り返し数が多いループを作り込んだり、時間が長い mdelay()、udelay() を使うのは避けて下さい。ロック期間内でする処理は最小になる様に考えて下さい。
マルチプロセッサ環境の場合、ロックを待つ処理はループで実装されていることにも注意が必要です。長期間あるいは高頻度で待ちになる場合、CPU は必要な処理をできずに実行時間を消費しています。小型セットの場合は電力消費を増やし、電池や電源に負担を掛けることになります。
- spin lock の中からコンテキスト・スイッチが強制的に発生し、自分のコンテキストが sleep 状態になる関数は呼べない
msleep() の様に明らかに待つ関数は呼べません。遅い bus をアクセスするような関数も呼べません。このような関数の中で IO 完了待ちが発生して sleep 状態に遷移しようとすると失敗します。stack dump が表示されます。