- Linuxカーネルは、スループットと応答時間のトレードオフのために複数のプリエンプションモードを維持してきたが、Peter Zijlstraの新しいパッチセットにより、**遅延プリエンプション(PREEMPT_LAZY)**の議論が再び本格化している
- 既存の
PREEMPT_NONE、PREEMPT_VOLUNTARY、PREEMPT_FULL、PREEMPT_RTはプリエンプションを許可する範囲が異なり、プリエンプションが頻繁なほど応答性は向上し得る一方で、スループットやロック競合への負担は大きくなる
PREEMPT_LAZYは**TIF_NEED_RESCHED_LAZY**フラグで「再スケジューリングは必要だが、即時ではない」ことを示し、大半のプリエンプションをタイマーティックまで遅らせる
- 長期的には、非リアルタイムのプリエンプションモードを
PREEMPT_LAZYとPREEMPT_FULLに減らし、カーネル各所の**cond_resched()呼び出し**をほとんど削除する方向だ
- 現在のパッチセットは、安定化、呼び出し箇所の検討、性能テストがさらに必要で、初期テストでは
PREEMPT_LAZYのスループットはPREEMPT_VOLUNTARYをわずかに下回っている
Linuxカーネルの既存プリエンプションモード
- 現在のカーネルは、実行中のタスクが他のタスクによっていつプリエンプトされ得るかを調整する複数のプリエンプションモードを提供している
PREEMPT_NONE: 実行中のタスクがタイムスライスを使い切ったときだけプリエンプションを許可する、最も単純なモード
PREEMPT_VOLUNTARY: カーネル内部に、必要に応じてプリエンプトできる地点を多数追加したモード
PREEMPT_FULL: スピンロック保持中のようにカーネルが遮る区間を除き、ほぼすべての地点でプリエンプションを許可するモード
PREEMPT_RT: ほとんどのものよりプリエンプションを優先し、スピンロックを保持するコードの大半もプリエンプト可能にするモード
- プリエンプションレベルが高いと、マウスの移動や原子炉の差し迫った異常信号のようなイベントに、より素早く反応できる
- その代わり、プリエンプションが頻繁になると、長時間実行されるCPU集約型タスクの全体スループットが下がり、ロック競合も増える可能性がある
- 多くのディストリビューションは
PREEMPT_DYNAMIC疑似モードでカーネルをビルドしている
- 起動時に、上記3つの非リアルタイムモードのいずれかを選択できる
- デフォルトは
PREEMPT_VOLUNTARY
debugfsがマウントされたシステムでは、/sys/kernel/debug/sched/preemptで現在のモードを確認できる
cond_resched()が必要だった理由
PREEMPT_NONEとPREEMPT_VOLUNTARYは、カーネルコード実行中の任意のプリエンプションを許可しない
- カーネル内部で長い処理が続くと、最小レイテンシを最優先しないシステムでも過度な遅延が生じる可能性がある
- これを避けるため、長時間実行されるループの各所に
cond_resched()呼び出しが追加された
- 各呼び出しは追加の自発的プリエンプション地点である
PREEMPT_NONEモードでも動作する
- カーネルにはこうした呼び出しが数百個ある
- この方式は、開発者が入れておいた位置でのみ機能するヒューリスティックである
- 不要な呼び出しが存在する可能性がある
- 必要な場所で呼び出しが抜けている可能性もある
- スケジューリング判断がカーネル全体のコードに広がる構造になる
遅延プリエンプションの中核的な動作
- カーネルは現在のタスクをプリエンプトできるかどうかを判断する際、複数の変数を合わせて見る
- そのうち
TIF_NEED_RESCHEDは、より高い優先度のタスクがCPUへのアクセスを待っていることを示すフラグである
- 高優先度のタスクが起床すると、現在実行中のタスクにこのフラグが設定されることがある
- このフラグがなければ、カーネルは現在のタスクをプリエンプトする必要がない
- カーネルは複数の地点で
TIF_NEED_RESCHEDを確認し、現在のタスクをプリエンプトできる
- スケジューラのタイマーティック
- システムコール後にユーザー空間へ戻るとき
- 割り込みハンドラの完了時点
cond_resched()呼び出し
- 遅延プリエンプションのパッチは、新しいフラグ**
TIF_NEED_RESCHED_LAZY**を追加する
- 再スケジューリングは必要だが、必ずしも直ちに実行する必要はない、という意味である
PREEMPT_LAZYモードでは、大半のイベントがTIF_NEED_RESCHEDの代わりにこの新しいフラグを設定する
- カーネルからユーザー空間へ戻る地点では、2つのフラグのどちらか一方が設定されていればスケジューラ呼び出しにつながる
- 自発的プリエンプション地点と割り込みからの復帰経路では、
TIF_NEED_RESCHEDだけを確認する
PREEMPT_LAZYがもたらすトレードオフ
PREEMPT_LAZYでは、カーネル内部の大半のイベントが現在のタスクを即座にはプリエンプトしない
- 代わりに、タイマーティックハンドラが
TIF_NEED_RESCHED_LAZYが設定されているかどうかを確認する
- 設定されていれば
TIF_NEED_RESCHEDも設定する
- その結果、実行中のタスクがプリエンプトされ得る
- タスクは通常、自発的にCPUを手放さない限り、自身のタイムスライスに近い時間だけ実行される
- この動作は良好なスループットにつながると期待されている
- この変更により、
PREEMPT_LAZYもPREEMPT_FULLと同様に、ほぼ常にカーネルプリエンプションが有効な状態で実行できる
- プリエンプションカウンタが許す場合は、いつでもプリエンプト可能である
- 他の条件が妨げなければ、長時間実行されるカーネルコードもプリエンプトされ得る
- 即時プリエンプションが本当に必要な場合は遅延しない
- たとえば割り込み処理の結果としてリアルタイムタスクが実行可能になると、
TIF_NEED_RESCHEDが設定される
- この場合はタイマーティックを待たず、ほぼ即座にプリエンプションにつながる
TIF_NEED_RESCHED_LAZYだけが設定されている場合、プリエンプションは発生しない
- したがって
PREEMPT_LAZYカーネルは、PREEMPT_FULLカーネルよりも実行中のタスクをプリエンプトする可能性がはるかに低い
cond_resched()削除までに残された作業
- 長期目標は、非リアルタイムのプリエンプションモードを2つに減らすことだ
PREEMPT_LAZY
PREEMPT_FULL
PREEMPT_LAZYはPREEMPT_NONEとPREEMPT_VOLUNTARYの間に位置し、両者を置き換える予定である
- プリエンプションがほぼどこでも可能になれば、特定地点に自発的プリエンプション地点を別途追加する必要は減る
- 現時点では
cond_resched()呼び出しが残っている
PREEMPT_NONEとPREEMPT_VOLUNTARYが存在する間は必要である
- 遅延プリエンプションの安定化中に問題が起きないよう支える役割もある
- 現在のパッチセットでは、
cond_resched()はTIF_NEED_RESCHEDだけを確認する
- そのため、
PREEMPT_VOLUNTARYやPREEMPT_NONEでは即時にプリエンプトされていた状況のかなりの部分が遅延される可能性がある
- Steve Rostedtは特に、
PREEMPT_VOLUNTARYでcond_resched()が以前の意味を維持すれば移行が容易になるのかを質問した
- Thomas Gleixnerは、
TIF_NEED_RESCHEDだけを確認するという選択が正しいと見ている
- すべての
cond_resched()呼び出しを見直させるためである
- lazyビットの確認が不要な呼び出しは、
PREEMPT_LAZY適用時に削除できる
- lazyビットの確認が必要な呼び出しは残す必要がある
- Gleixnerは、
cond_resched()呼び出しのうち5%未満だけがTIF_NEED_RESCHED_LAZYの確認を必要とすると予想している
- 移行が完了するまでは、数百個の
cond_resched()呼び出しを見直し、その大半を削除しなければならない
- Ankur Aroraの別のパッチセットは、関連する詳細の一部を扱っている
- 広範な性能テストも必要である
- Mike Galbraithの初期テストでは、遅延プリエンプションのスループットは
PREEMPT_VOLUNTARYをわずかに下回った
最終目標
- 遅延プリエンプションの取り組みの結果として、カーネルは少し小さく、単純になる可能性がある
- スケジューラ関連の呼び出しをコード全体に散らばらせなくても、予測可能なレイテンシを提供するカーネルが目標である
- 現在のアプローチはより良い解に見えるが、その状態に到達するにはまだ時間が必要だ
1件のコメント
Hacker News のコメント
有望に見える。EEVDF のように現状を単純化しつつ改善する方向なので、これ以上はなかなか望みにくいだろう
なぜプリエンプションレベルがグローバルモードではなく、特定イベントの属性ではないのか気になる。あるイベントは他のイベントより低いレイテンシで処理されるべきだ
したがって、イベントが持ち得る最高優先度も、プログラムがコンテキストスイッチを経るまでに受け取れるタイムスライスがどれだけ短いかによって制限される。どんな種類のイベントであれ低レイテンシで安定して対応するには、CPU 集約的なすべてのプログラムが、そのイベントがどれほどまれでも常に性能コストを払わなければならない
潜在的なプリエンプションポイントはスケジューラの属性であり、ここでグローバルモードとして議論されているのもそれだ。プリエンプションポイントが増えれば、当然ながら都合の悪いタイミングでプロセスがプリエンプトされる可能性は高まるが、同時に優先度を正しく反映する機会も増える。質問で言っているプリエンプションレベル、つまりスケジューラが与える優先度はプロセスの属性であり、設定も可能だ。Linux のデフォルトスケジューラも、優先度のあるプロセスにはより多くのタイムスライスを与え、他のプロセスをあまりプリエンプトしないようにする
SCHED_IDLE、SCHED_BATCH、SCHED_NORMAL/OTHER は遅延プリエンプションを使い、FIFO、RR、DEADLINE は従来の Full 動作を使う
だから、実行中のアプリケーション数を最小限にするか、多くのユーザーが経験する短い瞬間について手動で制御することが重要だ。CPU 集約的な処理も、本当に効率的なリソース利用というより、悪いコードである可能性の方が高い場合がある。ゲームでは性能を優先すべきだが、マルチタスクのためにシステムを止めてしまってはいけないという繊細なバランスが必要になる。いずれにせよ、これは主にアイドル作業向けのものなので、ユーザーがスクリプトから複数の動作を切り替えられる簡単なコマンドを用意する以上に自動化する必要はあまりなさそうだ
「現在のカーネルには、あるタスクが別のタスクのためにプリエンプトされ得る時点を調整する 4 つのモードがある」とあるが、これはカーネルタスクについてなのか、ユーザータスクまで含むのか気になる
パッチが投稿されたリンク先のスレッドで数値を見つけられなかった。この変更の現実的な可能性を示す初期ベンチマークくらいは、すでにあったのではないかと思うのだが
広範な性能テストが必要で、Mike Galbraith が初期作業を始めており、遅延プリエンプションのスループットは PREEMPT_VOLUNTARY よりわずかに低いという結果を示したという
スケジューラがカーネルの他のコードとどれほど強く結合しているのか気になる
例えばプリエンプションをまったく気にしない科学計算用アプリケーションのために、スケジューラを大幅に単純化したい場合、きれいでモジュール化された形で可能なのだろうか。実際の利点もあるのだろうか
tasksetでジョブをそこに直接載せる方法が最も強力だただしそうすると、ジョブを本当に手作業で CPU に割り当てる必要があり、すべてのジョブが見当違いの CPU に載ってしまう状況も簡単に起こる。標準的な方法は、割り込みマスクを設定して「作業」CPU に割り込みが行かないようにし、cpuset を使って特定の cgroup だけが指定された cpuset で実行されるようにすることだ
実行キューが非常に短くなるため、スケジューラが何をしても影響はかなり小さいはずだ。アプリケーションが入出力を多用しないなら、割り込みも多くない。tickless カーネルを使えるなら、今でも別オプションなのかデフォルトなのかは分からないが、長時間にわたって割り込みがほとんどないこともあり得る
ただし大幅に単純化したい理由はバグを避けるためであって、適切に設定されたデフォルトスケジューラと比べて得られる性能は多くない。設定項目は多いが、その方面のバグも多くはなかった。素朴に単純化すると、たいていは性能を得るより失うことになる。非対話型システムを動かすなら、最も簡単な変更はプロセスのタイムクォンタムを増やすことだ