3 ポイント 投稿者 GN⁺ 2024-10-20 | 1件のコメント | WhatsAppで共有
  • Linuxカーネルは、スループットと応答時間のトレードオフのために複数のプリエンプションモードを維持してきたが、Peter Zijlstraの新しいパッチセットにより、**遅延プリエンプション(PREEMPT_LAZY)**の議論が再び本格化している
  • 既存のPREEMPT_NONEPREEMPT_VOLUNTARYPREEMPT_FULLPREEMPT_RTはプリエンプションを許可する範囲が異なり、プリエンプションが頻繁なほど応答性は向上し得る一方で、スループットやロック競合への負担は大きくなる
  • PREEMPT_LAZYは**TIF_NEED_RESCHED_LAZY**フラグで「再スケジューリングは必要だが、即時ではない」ことを示し、大半のプリエンプションをタイマーティックまで遅らせる
  • 長期的には、非リアルタイムのプリエンプションモードをPREEMPT_LAZYPREEMPT_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_NONEPREEMPT_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_LAZYPREEMPT_FULLと同様に、ほぼ常にカーネルプリエンプションが有効な状態で実行できる
    • プリエンプションカウンタが許す場合は、いつでもプリエンプト可能である
    • 他の条件が妨げなければ、長時間実行されるカーネルコードもプリエンプトされ得る
  • 即時プリエンプションが本当に必要な場合は遅延しない
    • たとえば割り込み処理の結果としてリアルタイムタスクが実行可能になると、TIF_NEED_RESCHEDが設定される
    • この場合はタイマーティックを待たず、ほぼ即座にプリエンプションにつながる
  • TIF_NEED_RESCHED_LAZYだけが設定されている場合、プリエンプションは発生しない
    • したがってPREEMPT_LAZYカーネルは、PREEMPT_FULLカーネルよりも実行中のタスクをプリエンプトする可能性がはるかに低い

cond_resched()削除までに残された作業

  • 長期目標は、非リアルタイムのプリエンプションモードを2つに減らすことだ
    • PREEMPT_LAZY
    • PREEMPT_FULL
  • PREEMPT_LAZYPREEMPT_NONEPREEMPT_VOLUNTARYの間に位置し、両者を置き換える予定である
  • プリエンプションがほぼどこでも可能になれば、特定地点に自発的プリエンプション地点を別途追加する必要は減る
  • 現時点ではcond_resched()呼び出しが残っている
    • PREEMPT_NONEPREEMPT_VOLUNTARYが存在する間は必要である
    • 遅延プリエンプションの安定化中に問題が起きないよう支える役割もある
  • 現在のパッチセットでは、cond_resched()TIF_NEED_RESCHEDだけを確認する
    • そのため、PREEMPT_VOLUNTARYPREEMPT_NONEでは即時にプリエンプトされていた状況のかなりの部分が遅延される可能性がある
  • Steve Rostedtは特に、PREEMPT_VOLUNTARYcond_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件のコメント

 
GN⁺ 2024-10-20
Hacker News のコメント
  • 有望に見える。EEVDF のように現状を単純化しつつ改善する方向なので、これ以上はなかなか望みにくいだろう

  • なぜプリエンプションレベルがグローバルモードではなく、特定イベントの属性ではないのか気になる。あるイベントは他のイベントより低いレイテンシで処理されるべきだ

    • イベントの優先度を評価するには、まず CPU 時間が必要になる。現在 CPU で実行中のプロセスを割り込ませて初めて、その評価が可能になる
      したがって、イベントが持ち得る最高優先度も、プログラムがコンテキストスイッチを経るまでに受け取れるタイムスライスがどれだけ短いかによって制限される。どんな種類のイベントであれ低レイテンシで安定して対応するには、CPU 集約的なすべてのプログラムが、そのイベントがどれほどまれでも常に性能コストを払わなければならない
    • ここには混同しやすい二つの概念がある。一つはプロセスがプリエンプトされ得る時点で、もう一つは実際にプリエンプトされるかどうかだ
      潜在的なプリエンプションポイントはスケジューラの属性であり、ここでグローバルモードとして議論されているのもそれだ。プリエンプションポイントが増えれば、当然ながら都合の悪いタイミングでプロセスがプリエンプトされる可能性は高まるが、同時に優先度を正しく反映する機会も増える。質問で言っているプリエンプションレベル、つまりスケジューラが与える優先度はプロセスの属性であり、設定も可能だ。Linux のデフォルトスケジューラも、優先度のあるプロセスにはより多くのタイムスライスを与え、他のプロセスをあまりプリエンプトしないようにする
    • 記事で説明されている PREEMPT_VOLUNTARY は、ある程度そうした方向の試みで、今は廃止されつつあると見てよい
    • このパッチがある程度その役割を果たしている。https://lwn.net/ml/all/20241008144829.GG14587@noisy.programm... によれば:
      SCHED_IDLE、SCHED_BATCH、SCHED_NORMAL/OTHER は遅延プリエンプションを使い、FIFO、RR、DEADLINE は従来の Full 動作を使う
    • そういうシステムは、プログラム同士が自分こそ重要だと優先度を要求し合う争いを生みやすい。現実的には、大企業が「より良い」ユーザー体験のために活用する可能性が高い
      だから、実行中のアプリケーション数を最小限にするか、多くのユーザーが経験する短い瞬間について手動で制御することが重要だ。CPU 集約的な処理も、本当に効率的なリソース利用というより、悪いコードである可能性の方が高い場合がある。ゲームでは性能を優先すべきだが、マルチタスクのためにシステムを止めてしまってはいけないという繊細なバランスが必要になる。いずれにせよ、これは主にアイドル作業向けのものなので、ユーザーがスクリプトから複数の動作を切り替えられる簡単なコマンドを用意する以上に自動化する必要はあまりなさそうだ
  • 「現在のカーネルには、あるタスクが別のタスクのためにプリエンプトされ得る時点を調整する 4 つのモードがある」とあるが、これはカーネルタスクについてなのか、ユーザータスクまで含むのか気になる

    • カーネルコードの話だ。ユーザー空間コードは常にプリエンプト可能だ
  • パッチが投稿されたリンク先のスレッドで数値を見つけられなかった。この変更の現実的な可能性を示す初期ベンチマークくらいは、すでにあったのではないかと思うのだが

    • 記事の最後から 2 番目の段落に出ている
      広範な性能テストが必要で、Mike Galbraith が初期作業を始めており、遅延プリエンプションのスループットは PREEMPT_VOLUNTARY よりわずかに低いという結果を示したという
    • こういうものはどうベンチマークすればよいのだろう。複数プロセスを同時に実行して総実行時間で並べる方式だろうか、それとも個々のプロセスの待ち時間を測るべきだろうか
  • スケジューラがカーネルの他のコードとどれほど強く結合しているのか気になる
    例えばプリエンプションをまったく気にしない科学計算用アプリケーションのために、スケジューラを大幅に単純化したい場合、きれいでモジュール化された形で可能なのだろうか。実際の利点もあるのだろうか

    • できるだけプリエンプションを減らしてプロセス群を実行したいなら、例えば HPC 環境では、一部のコアを分離 CPU に設定して再起動し、taskset でジョブをそこに直接載せる方法が最も強力だ
      ただしそうすると、ジョブを本当に手作業で CPU に割り当てる必要があり、すべてのジョブが見当違いの CPU に載ってしまう状況も簡単に起こる。標準的な方法は、割り込みマスクを設定して「作業」CPU に割り込みが行かないようにし、cpuset を使って特定の cgroup だけが指定された cpuset で実行されるようにすることだ
    • デーモンがほとんどないクリーンなシステムで、アプリケーションを CPU スレッドあたり OS スレッド 1 つに合わせ、CPU 固定をかけて移動しないようにすれば、95% くらいは到達できる
      実行キューが非常に短くなるため、スケジューラが何をしても影響はかなり小さいはずだ。アプリケーションが入出力を多用しないなら、割り込みも多くない。tickless カーネルを使えるなら、今でも別オプションなのかデフォルトなのかは分からないが、長時間にわたって割り込みがほとんどないこともあり得る
    • 最後に見たときは、意外にうまく分離されていた
      ただし大幅に単純化したい理由はバグを避けるためであって、適切に設定されたデフォルトスケジューラと比べて得られる性能は多くない。設定項目は多いが、その方面のバグも多くはなかった。素朴に単純化すると、たいていは性能を得るより失うことになる。非対話型システムを動かすなら、最も簡単な変更はプロセスのタイムクォンタムを増やすことだ
    • 自分なら単に RT Linux を使う。独自の基本スケジューラがあり、カーネルスケジューラはアイドルタスクとして動作し、リアルタイムタスクがすべてに優先する