タスクスケジューラ用のsysctlパラメタ
はじめに
本記事はLinux Advent Calendar 2015 19日目用に書きました。タスクスケジューラに関するsysctlパラメタについての解説をしています。取り扱ったのは全パラメタではなく、使用頻度が比較的高そうなものだけです。調査に使用したカーネルのバージョンは4.4-rc5です。
Completely Fair Scheduler(CFS)に関するsysctlパラメタ
linux上の各タスクにはスケジューリングポリシー(以下ポリシーと記載)というものを設定できます。通常のタスクはデフォルトSCHED_OTHERというポリシーを使います。このポリシーを使うタスクのスケジューリングにはCFSというスケジューラを使います。ここではCFSに関するsysctlパラメタを紹介します。 スケジューリングポリシーの詳細についてはman 2 sched_{get,set}schedulerを参照してください。
以下CFSの説明をするにあたって、nice値を考慮すると説明が複雑化するので、ここではすべてのタスクのnice値をすべてデフォルトの0と考えてください。
kernel.sched_latency_ns
時分割によるスケジューリングの一単位の期間を示すlatencyという値。単位はナノ秒。デフォルト値は"6,000,000 * (1+log2(CPU数))"。
実行可能タスクが複数存在する場合、CFSは時分割形式でCPU実行権を各タスクに公平に分け合います。latencyと呼ばれる期間毎に各タスクはlatency/実行可能タスク数だけCPU時間を使えます。この割り当て時間をタイムスライスと呼びます。例えばlatencyが100ミリ秒で実行可能タスクが10個なら、各タスクは100ミリ秒ごとに"100/10=10"ミリ秒動けます。同様に実行可能タスクの数が5個なら、100ミリ秒ごとに"100/5=20"ミリ秒動けます。
上記の動作を実現するために、CFSは次のような内部論理を持っています。CFSで管理されるタスクは各自vruntimeという値を持っています。CFSはvruntimeが小さいタスクから順番にCPUを割り当てます。vruntimeは、次のような特徴を持つ、各タスクの仮想的な累計実行時間を指しています。
- 実行可能なタスクについて意味を持つ
- 現在時刻がlatencyの倍数になったときに、全タスクがのvruntimeがその値になる
- タイムスライスで示されるの期間、CPU上で動くと、latencyで示される期間だけ動いたように値が修正される
- スリープしたタスクが起床すると、当該タスクのvruntimeは、ランキュー内のタスクのvruntime最小値に切り上げられる
言葉で説明してもわかりづらいので、例えば以下のような状況を考えます。
latencyは20ミリ秒 T0,T1という2つのタスクが存在する(つまりタイムスライスは"20/2=10"ミリ秒) T0,T1は常に実行可能状態(スリープしない) T0,T1の実際の実行時間をt0,t1とする。このときvruntimeはv0,v1。初期値は全て0ミリ秒
この場合、実時間の経過と共に、2つのタスクのvruntimeは以下の表のように変化します。表の中の数値の単位はすべてミリ秒です。v0,v1が同じ値の場合はどちらが先に動いてもいいのですが、ここでは借りにT0が先に動くものとします。
実時間 | t0 | t1 | v0 | v1 |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
10 | 10 | 0 | 20 | 0 |
20 | 10 | 10 | 20 | 20 |
30 | 20 | 10 | 40 | 20 |
40 | 20 | 20 | 40 | 40 |
latencyの倍数の時間において(0,20,40ミリ秒時点)、次のことがわかります。
- t0, t1を足すと実時間の経過時間に等しい
- 実時間がlatencyの倍数の時間には、それぞれのタスクのvruntimeの時間は実時間に等しい
T1が実時間でいう0ミリ秒から20ミリ秒までスリープしていた場合はどうなるでしょうか。上記の通り、スリープしていたタスクが起床したときには、当該タスクのvruntimeを、実行可能なタスクのvruntimeのうちの最小値にするという仕組みが存在します。これを踏まえると、実時間20ミリ秒時点でT1が起床する場合、次のような挙動になります。
実時間 | t0 | t1 | v0 | v1 |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
10 | 10 | 0 | 10 | 0 |
20 | 20 | 0 | 20 | 20| <- ここでT1が起床。v1はv0まで繰り上げられる |
30 | 30 | 0 | 40 | 20 |
40 | 30 | 10 | 40 | 40 |
この場合も、latencyの倍数の時間において(0,20,40ミリ秒時点)、1つ前の例と同じ条件を満たしていることがわかります。
kernel.sched_min_granularity_ns
各タスクがCPU実行権を得た際の最低実行時間を示すmin_granularityという値。単位はナノ秒。デフォルト値は"750_000 * (1+log2(CPU数))"。
上述したCFSのスケジューリングの仕組みをそのまま使うと、実行可能なタスクが大量に存在する場合に、コンテキストスイッチによるオーバーヘッドが無制限に大きくなってしまいます。例えばlatencyが100ミリ秒でタスクが1,000個あった場合、各タスクは一度に"100/1,000=0.1"ミリ秒、つまり100マイクロ秒しか動けません。仮にコンテキストスイッチのコストを10マイクロ秒とすると(もちろん実際の値はシステムによって異なります)、コンテキストスイッチだけでCPUリソースの10%を食い潰してしまいます。これを防ぐためにmin_granularityという値が存在します。"latency/実行可能タスク数 < min_granularity"の場合、実際のlatencyも"min_granularity*タスク数"になる(sysctlパラメタの値は変わらない)と共に、各タスクのタイムスライスはmin_granularityになります。
kernel.sched_wakeup_granularity_ns
スリープ状態から起床したタスクが現在実行中のタスクに割り込んで実行できるかどうか(preemptできるかどうか)を決めるwakeup_granularityという値。単位はナノ秒。デフォルト値は"1,000,000 * (1+log2(# of CPUS))"。
スリープ状態から起床したタスクは、動作直後に短時間だけCPUを使ってまたスリープするという傾向にあります。それが顕著なのがshellなどの対話型のタスクです。システムの応答性を上げるためには、これらタスクの応答性を上げる必要があります。そのためにCFSは、スリープ状態から起床したタスクのvruntimeが現在実行中のタスクのそれより少なければ、前者は後者をpreemptできるという仕組みになっています。
ただし、この仕組みをそのまま使ってしまうと、スリープと起床を繰り返すようなタスクが大量に存在する場合に、preemptによるコンテキストスイッチが大量に発生すると共に、そのオーバーヘッドも大きくなってしまいます。本パラメタはそれを防ぐために存在します。latencyで定められた期間の中で、起床したタスクの実行時間が現在実行中のタスクのそれよりもwakeup_granularityだけ少ない場合のみpreemptできるようになっています。
リアルタイムスケジューラに関するsysctlパラメタ
SCHED_OTHERの他には、SCHED_FIFO, SCHED_RRというポリシーがあります。これらのポリシーはリアルタイムポリシーと呼ばれます。リアルタイムポリシーを使うタスクは、SCHED_OTHERポリシーを使うすべてのタスクより優先的に実行することができます。これらのタスクのスケジューリングにはリアルタイムスケジューラと呼ばれるスケジューラを使います。ここではリアルタイムスケジューラに関するsysctlパラメタについて説明します。
kernel.sched_rr_timeslice_ms
SCHED_RRポリシーを使うタスクのタイムスライスを示す値。単位はミリ秒。デフォルト値はHZ(カーネルコンフィグの中のCONFIG_HZ_
SCHED_RRポリシーを使う実行可能なタスクが複数存在する場合、所定のタイムスライスごとにそれらタスクの間でCPU実行権を順番に割り当てるラウンドロビン形式のスケジューリングをします。本パラメタはそのタイムスライスに該当します。
kernel.sched_rt_period_us
リアルタイムポリシーを使うタスクが暴走した場合にシステムがハングしないようにする仕組みに使うrt_periodという値。単位はマイクロ秒。デフォルトは1,000,000。
リアルタイムタスクが全CPU上でCPU実行権を明け渡さない状態になると、shellを含めて誰もこれらタスクをkillすることができなくなってしまい、システムがハング状態に陥ります。これを避けるために、リアルタイムタスクの実行時間をrt_periodと呼ばれる所定の期間中に、rt_runtimeと呼ばれる時間しか実行できないようにしています。
kernel.sched_rt_runtime_us
上記rt_runtime。単位はマイクロ秒。デフォルト値は950,000。