CFS グループのスケジューリング

注: この記事の略称の説明

db14c3e7066cc1e5be2017109782754f.png

1. CFS グループ スケジューリングの概要

1.1. 存在理由

要約すると、さまざまなグループのタスクが、高負荷の下で制御可能な割合の CPU リソースを割り当てることができることが望まれます。なぜこのような要件があるのでしょうか? たとえば、マルチユーザー コンピュータ システムでは、各ユーザーのすべてのタスクが 1 つのグループに分割されます. ユーザー A は 90 個の同一のタスクを持ち、ユーザー B は 10 個の同一のタスクしか持ちません. CPU が完全に満杯の場合、ユーザー A は CPU 時間の 90% を使用しますが、ユーザー B は CPU 時間の 10% しか使用しません。これは明らかにユーザー B にとって不公平です。または、同じユーザーが -j64 を使用して迅速にコンパイルしたいが、コンパイル タスクの影響を受けたくない場合は、コンパイル タスクを対応するグループに設定して、その CPU リソースを制限することもできます。

1.2. モバイル デバイスでのグループ ステータス

99e96464e0df770c3cee8fcc52cd2ab0.png

/dev/cpuctl ディレクトリは、struct task_group root_task_group によって表されます。その下の各サブディレクトリは、task_group 構造に抽象化されます。注意すべき点がいくつかあります。

  1. root グループ配下の cpu.shares ファイルのデフォルト値は 1024 であり、設定はサポートされていません。ルート グループの負荷も更新されません。

  2. ルート グループもタスク グループであり、ルート グループ配下のタスクもグループ化され、他のグループに属しなくなります。

  3. カーネル スレッドはデフォルトでルート グループ配下にあり、ルート cfs_rq からタイム スライスを直接割り当てるため、大きな利点があります。 kswapd カーネル スレッド。

  4. デフォルトの cpu.shares 構成で、すべてのタスクが nice=0 で、単一のコアのみが考慮される場合、完全にロードされたときにルート グループの下の各タスクが取得できるタイム スライスは、すべてのタスクに割り当てられるタイム スライスと等しくなります。他のグループの下 到着したタイム スライスの合計。

注: タスクとタスク グループの両方が重みによってタイム スライスを割り当てますが、タスクの重みはその優先度から得られ、タスク グループの重みはその cgroup ディレクトリの cpu.shares ファイルに設定された値から得られます。グループ スケジューリングを有効にすると、タスクに割り当てられたタイム スライスに応じて、優先順位に対応する重みだけでなく、そのタスク グループに割り当てられた重みと、グループ内の他のタスクの実行ステータスも確認できます。 .

二、タスクのタスクグループのグループ化

CFS グループスケジューリング機能は主にタスクのグループ化に反映され、グループは struct task_group で表されます。

2.1. グループ化の設定方法

タスク グループ グループ化構成インターフェイスは、cgroup ディレクトリ階層を介して cpu cgroup サブシステムによってユーザー空間にエクスポートされます。

9e94d656874a6c80d1576da49389eb88.png

タスクをタスク グループから削除するには? 直接削除する方法はありません.cgroup セマンティクスの下では, タスクは特定の時点でタスク グループに属している必要があります.他のグループにエコーすることによってのみ,現在のタスクから削除できます.グループを削除します。

2.2. Android でグループ化を設定する方法

Process.java は setProcessGroup(int pid, int group) を他のモジュールに提供し、pid プロセスを group パラメータで指定されたグループに設定して、他のモジュールが呼び出して設定できるようにします。たとえば、OomAdjuster.java では、タスクをフロント/バックグラウンドに切り替えて、パラメーター group=THREAD_GROUP_TOP_APP/THREAD_GROUP_BACKGROUND をそれぞれ呼び出します。

libprocessgroup は task_profiles.json という名前の構成ファイルを提供します。このファイルでは、AggregateProfiles 集約属性フィールドが、上位層が設定された後に対応する動作を構成します。たとえば、THREAD_GROUP_TOP_APP に対応する集約属性は SCHED_SP_TOP_APP であり、MaxPerformance 属性に対応する動作は cpu top-app グループに参加することです。

e2eea5144b4f9c1a8176cc01642f4a9d.png

「MaxPerformance」属性の構成は非常に読みやすく、cpu サブシステムの top-app グループに追加されていることがわかります。

2.3. Android を TOP-APP グルーピングに設定し、cgroup に何を設定するか

複数の cgroup サブシステムがあるため、ここで説明している CFS グループ スケジューリングに接続された cpu cgroup サブシステムに加えて、cpuset cgroup サブシステム (タスクが実行できる CPU と使用可能なメモリ ノードを制限する)、blkio cgroup サブシステム ( ブロックプロセスを制限するデバイス io)、freezer cgroup サブシステム (プロセスの凍結機能を提供する) など。The upper-level configuration grouping may not just cut a cgroup, but which subsystems are 具体的には、集計属性 AggregateProfiles の配列メンバーに反映されます. たとえば、上記の例の他の 2 つの属性に対応する動作は、結合することです. blkio サブシステムのルート グループとタスクの timer_slack_ns (hrtimer タイミング ウェイクアップの適時性と消費電力のバランスをとるパラメーター) は 50000ns に設定されます。

192454aea67f52589432d17db0fab2f8.png

2.4. サービス開始後、指定したグループに配置されます

たとえば、サービスの開始時に task_profiles を使用して構成します。

588f84b44b1d6d7594ad17d86b61d175.png

3. カーネル実装の概要

上記では、タスク グループのグループ化の構成について説明しました。このセクションでは、カーネルの実装について説明します。

3.1. 関連する機能依存関係

カーネル関連の CONFIG_* 依存関係は次のとおりです。

図1:

81a8b2a01d3759969c887bb0176b9a3e.png

CGROUP は cgroup ディレクトリ階層の機能を提供します; CGROUP_SCHED は cpu cgroup ディレクトリ階層 (/dev/cpuctl/top-app など) を提供し、各 cpu cgroup ディレクトリのタスク グループの概念を提供します; FAIR_GROUP_SCHED は、によって提供されるタスク グループに基づいて提供します。 cpu cgroup CFS タスク グループのスケジューリング機能。図 1 のグレーの点線フレームは、Android フォン カーネルではデフォルトで有効になっていません。

3.2. タスクグループのデータ構造図

以下の図 2 に示すように、カーネル内のタスク グループによって維持される主要なデータ構造のブロック図を示します。これは、図の次の概念を比較することで理解しやすくなります。

  1. グループ内のタスクが実行されている CPU を特定することは不可能であるため、タスク グループは各 CPU でグループ cfs_rq を維持します。グループ cfs_rq タスク)、したがって、グループ se も各 CPU で維持されます。

  2. タスク se の my_q メンバーは NULL であり、グループ se の my_q メンバーは対応するグループ cfs_rq を指し、この CPU でグループ化された準備完了タスクはこのグループ cfs_rq でハングします。

  3. タスク グループは入れ子にすることができ、親/兄弟/子メンバーは逆ツリー階層を形成し、ルート タスク グループの親は NULL を指します。

  4. すべての CPU のルート cfs_rq はルート タスク グループに属します。

図 2:

2146a94a2b535cf7190127b177855470.png

注:絵を描くのが面倒ですが、この絵は蝸牛窩を基準にしています。

4. 関連構造

4.1. 構造体 task_group

struct task_group は、cpu cgroup のグループ化を表します。FAIR_GROUP_SCHED が有効な場合、struct task_group は CFS グループ スケジューリングのタスク グループを表します。

1231664649ad6579d922f92b95f681e0.png

css: タスク グループに対応する cgroup ステータス情報。これを介して cgroup ディレクトリ階層にアタッチされます。

se: グループ se は配列ポインタで、配列のサイズは CPU の数です。タスク グループには複数のタスクがあり、すべての CPU で実行できるため、各 CPU にはタスク グループの se が必要です。

cfs_rq: グループ se の cfs_rq は、各 CPU 上のタスク グループの cfs_rq であり、配列ポインターでもあり、配列のサイズは CPU の数です。このタスク グループのタスクの準備ができたら、この cfs_rq でハングアップします。各 CPU の対応するポインターは、各 CPU のタスク グループの se->my_q と同じポインターを持ちます。init_tg_cfs_entry() を参照してください。

共有: タスク グループの重み。デフォルトは scale_up(1024) です。タスク se の重みと同様に、値が大きいほど、タスク グループが取得できる CPU タイム スライスが多くなります。ただしタスクとは異なり、タスクグループは CPU ごとにグループ se を持つため、一定のルールに従って各 CPU のグループ se に割り当てる必要があります。

load_avg: このタスク グループの負荷。これは load_avg 変数のみです (se および cfs_rq が構造体であるのとは異なります)。これについては以下で説明します。CPU単位ではないことに注意 このタスクグループのタスクは、CPUごとに更新されると更新されるので、パフォーマンスへの影響には注意が必要です。

親/兄弟/子: タスク グループを構成する階層。

カーネルには、ルート グループを表すグローバル変数 struct task_group root_task_group があります。その cfs_rq[] は各 cpu の cfs_rq であり、その se[] は NULL です。その重みは設定できません。sched_group_set_shares() を参照してください。負荷も更新されません。 update_tg_load_avg() を参照してください。

システム内のすべてのタスク グループ構造は、CFS 帯域幅制御で使用される task_groups リンク リストに追加されます。

初心者は、グループ スケジューリングとスケジューリング グループの 2 つの概念、および struct task_group と struct sched_group の違いを混同する可能性があります。グループスケジューリングは、タスクのグループを記述するために使用される struct task_group に対応し、主に util uclamp (タスクのグループの計算能力要件をクランプする) と CPU リソースの使用制限に使用されます。この記事で説明します。スケジューリング グループは、CPU トポロジ sched_domain の概念である struct sched_group に対応し、CPU (MC レベル)/クラスター (DIE レベル) の属性を記述するために使用され、主にコアの選択と負荷分散に使用されます。

4.2. 構造体 sched_entity

sched_entity は、タスク se だけでなく、グループ se も表すことができます。以下は、主にグループ スケジューリングを有効にした後の新しいメンバーをいくつか紹介します。

024369a93f988ebdead9c96c06bb4bca.png

load: se の重みを示します. gse の場合, 新規の場合は NICE_0_LOAD に初期化されます. init_tg_cfs_entry() を参照してください.

depth: タスク グループのネストの深さを示します。ルート グループの下の se の深さは 0 であり、ネスト レベルが深くなるごとに 1 ずつ増加します。例えば、/dev/cpuctl ディレクトリの下に tg1 ディレクトリがあり、tg1 ディレクトリの下に tg2 ディレクトリがあり、tg1 に対応するグループ se の深さは 0、tg1 の下のタスク se の深さは 1、 tg2 の下のタスク se の深さは 2 です。更新場所については、init_tg_cfs_entry()/attach_entity_cfs_rq() を参照してください。

親: 親 se ノードを指し、親と子の両方の se ノードが同じ cpu に対応します。ルート グループ配下のタスクが NULL を指しています。

cfs_rq: この se がマウントされる cfs_rq。root グループの下のタスクが rq の cfs_rq を指し、非 root グループのタスクがその親->my_rq を指す場合は、init_tg_cfs_entry() を参照してください。

my_q: この se の cfs_rq、グループ se のみが cfs_rq を持ち、タスク se は NULL. entity_is_task() マクロは、このメンバを通じてタスク se かグループ se かを判断します。

runnable_weight: gse の実行可能な負荷を計算するときに使用される gse->my_q->h_nr_running の値をキャッシュします。

avg: se の負荷。tse の場合は重みとして初期化され (作成時に負荷が高いと想定されます)、gse の場合は 0 に初期化されます。init_entity_runnable_average() を参照してください。タスク se とグループ se には特定の違いがあります。これについては、以下の第 5 章で説明します。

4.3. 構造体 cfs_rq

struct cfs_rq は、CPU ごとの CFS レディ キューと gse の my_q キューの両方を表すことができます。グループ スケジューリングに重要な一部のメンバーを以下にリストして説明します。

0d853eb1e91c1d0481d528309cbe891f.jpeg

load: ルート cfs_rq であるか grq であるかに関係なく、cfs_rq の重みを示します。ここでの重みは、そのキューにハングアップしているすべてのタスクの重みの合計に等しくなります。

nr_running: 現在のレベルでのタスク se とグループ se の数の合計。

h_nr_running: グループ se を除く、現在のレベルとすべてのサブレベルの下のタスク se の数の合計。

avg: cfs_rq の負荷。以下、タスクse、グループse、cfs_rqを比較して負荷を説明します。

削除: ウェイクアップ後にタスクが終了するか、他の CPU に移行する場合、タスクによってもたらされる負荷を元の CPU の cfs rq から削除する必要があります。この削除アクションは、最初に削除されたメンバーに削除された負荷を記録し、次に update_cfs_rq_load_avg() が呼び出されて cfs_rq 負荷を更新するときに削除します。nr は削除する se の数を示し、*_avg は削除するさまざまな負荷の合計を示します。

tg_load_avg_contrib: grq->avg.load_avg のキャッシュで、現在の grq 負荷の tg への寄与値を示します。tg->load_avg の更新時に tg->load_avg へのアクセス回数を減らすために使用します。また、gse が tg から取得する重みクォータを計算する際の近似アルゴリズムでも使用されます。calc_group_shares()/update_tg_load_avg() を参照してください。

伝播: 上位層に伝播する必要がある負荷があるかどうかをマークします。以下のセクション 7.3 で説明します。

prop_runnable_sum: 負荷がタスク グループ階層に沿って上位層に伝播するときに、アップロードされる tse/gse の load_sum 値を示します。

h_load: 階層負荷。主にロード バランシング パスで使用される、CPU の load_avg に対するこの層の cfs_rq の load_avg の寄与値を示します。以下に説明します。

last_h_load_update: h_load が最後に更新された時刻を示します (単位 jiffies)。

h_load_next: サブ gse を指し、タスクの階層負荷を取得するために (task_h_load 関数)、各レベルの cfs rq の h_load を最上位の cfs から下に更新する必要があります。したがって、ここでの h_load_next は、最上位の cfs rq から最下位の cfs rq への cfs rq--se--cfs rq--se 関係チェーンを形成することです。

rq: このメンバーは、グループ スケジューリングが有効化された後に追加されます. グループ スケジューリングが有効化されていない場合、cfs_rq は rq のメンバーであり、container_of はルーティングに使用されます. グループ スケジューリングが有効化された後、cfs_rq から rq へのルーティングに rq メンバーが追加されます.

on_list/leaf_cfs_rq_list: リーフ cfs_rq を直列に接続して、CFS ロード バランシングおよび帯域幅制御関連のロジックで使用してみてください。

tg: cfs_rq が属するタスク グループ。

5、タスクグループの重み

タスク グループの重みは、struct task_group の share メンバーによって表され、デフォルト値は scale_load(1024) です。cgroup ディレクトリの cpu.shares ファイルを介して読み書きできます。echo weight > cpu.shares は、タスク グループの重みを weight として設定するためのもので、shares メンバー変数に保存される値は scale_load(weight) です。root_task_group は重みの設定をサポートしていません。

さまざまなタスク グループの重みは、システム CPU がいっぱいになった後、どのタスク グループがより多く実行でき、どのタスク グループがより少なく実行できるかを示します。

5.1. gseの重量

タスクグループは各 CPU にグループ se を持っているため、タスクグループの重み tg->shares を一定の規則に従って各 gse に割り当てる必要があります。ルールは式 (1) です。

* tg-> 重量 * grq-> 負荷.重量

* ge->load.weight = ----------------------------------------- ( 1)

* \Sum grq->load.weight

このうち、tg->weight は tg->shares であり、grq->load.weight は各 CPU での tg の grq の重みを表します。つまり、各 gse は、その cfs_rq の重み比に従って tg の重みを割り当てます。cfs_rq の重みは、それにマウントされているタスクの重みの合計に等しくなります。tg の重みが 1024 で、システムに CPU が 2 つしかないため、gse が 2 つあるとします。grq のタスク ステータスが図 3 に示すようになっている場合、gse[0] の重みは 1024 * (1024 +2048+3072) /(1024+2048+3072+1024+1024) = 768; gse[1] の重みは 1024 * (1024+1024)/(1024+2048+3072+1024+1024) = 256 です。

画像 3:

8ae44155b35b5a67a3d159a5753227c6.png

gse の重み更新関数は update_cfs_group() です。以下の具体的な実装を参照してください。

d6491d8c2c6a0d5850c09d71353def50.png

tg の gse[X] への重みの配分は、calc_group_shares() で行われます。

\Sum grq->load.weight は式 (1) で使用されます。これは、gse の重みの更新が各 CPU の grq にアクセスする必要があることを意味し、ロック競合のコストが比較的高いため、一連の近似計算実行されます。

最初に交換を行います:

* grq->load.weight --> grq->avg.load_avg (2)

そして取得します:

* tg-> 重量 * grq->avg.load_avg

* ge->load.weight = ---------------------------------------- (3 )

* tg->load_avg

*

* ここで: tg->load_avg ~= \Sum grq->avg.load_avg

cfs_rq->avg.load_avg = cfs_rq->avg.load_sum/divider であるためです。また、cfs_rq->avg.load_sum は、cfs_rq->load.weight に非アイドル状態の等比数列を掛けた値に等しくなります。この近似は、tg の各 CPU での grq の非アイドル状態の時系列が同じであるという前提の下で、厳密に等しくなります。つまり、各 CPU での tg タスクの実行状態が一貫しているほど、この概算値に近づきます。

タスク グループがアイドル状態のときに、タスクを開始します。grq->avg.load_avg はビルドに時間がかかります。ビルド時間の特殊なケースでは、式 1 は次のように簡略化されます。

* tg-> 重量 * grq-> 負荷.重量

* ge->load.weight = --------------------------------------- = tg- >体重 (4)

* grp->load.weight

シングルコアシステムの状態に相当します。この特殊なケースで式 (3) を式 (4) に近づけるために、別の近似を行います。

* tg-> 重量 * grq-> 負荷.重量

* ge->load.weight = ------------------------------------------ -------------------------- (5)

* tg->load_avg - grq->avg.load_avg + grq->load.weight

しかし、grq にはタスクがないため、grq->load.weight が 0 に落ちる可能性があり、その結果、ゼロ除算が発生します。その下限として grq->avg.load_avg を使用し、次のように指定する必要があります。

* tg-> 重量 * grq-> 負荷.重量

* ge->load.weight = ------------------------------------------ (6)

* tg_load_avg'

*

* の:

* tg_load_avg' = tg->load_avg - grq->avg.load_avg + max(grq->load.weight, grq->avg.load_avg)

max(grq->load.weight, grq->avg.load_avg) は通常、grq->load.weight を使用します。これは、grq で実行中の + 実行可能なタスクが常に存在する場合にのみ、grq->load.weight に近づくためです。

calc_group_shares() 関数は、式 (6) によって各 gse の重みを概算します。

f4b7dbdb680cb440a4ff4c405b496141.png

tg の各タスクは gse の重みに寄与するため、grq のタスク数が変化すると gse の重みを更新する必要があります. se の負荷は近似プロセスで使用され、entity_tick() でも更新が実行されます。呼び出しパス:

b8065ddf7cf243bd4db441c8a77257b3.png

5.2. gse 上の各 tse に割り当てられる重み

タスク グループ内のタスクにも、重みの比率に従って gse の重みが割り当てられます。上記の図 2 に示すように、gse[0] の grq に掛けられた 3 つのタスクの場合、tse1 の重みは 768*1024/(1024+2048+3072)=128 であり、tse2 の重みは 768*2048/ です。 (1024+ 2048+3072)=256、tse3 の重みは 768*3072/(1024+2048+3072)=384 です。

tg のタスクが tg によって割り当てられたタイム スライスを割り当てる場合、この比例重みが使用されます。グループの入れ子が深いほど、比例して割り当てることができる重みは小さくなり、タスク グループ内のタスクは、高負荷下でのタイム スライスの割り当てに役立たないことがわかります。

六、タスクグループタイムスライス

6.1. タイムスライスの割り当て

CFS グループ スケジューリングが有効な場合、上位層によって割り当てられたタイム スライスは、上から下への重み比によって層ごとに割り当てられます。割り当て関数は sched_slice() です。しかし、上から下にトラバースするのは都合が悪いので、下から上にトラバースするように変更すると、結局、A*B*C と C*B*A は等しくなります。

9530895e61c8f8621bcc84a9f24b7962.png

sched_slice の主なパスは次のとおりです。

5dab32d84992ae330f47495f1f1734da.png

ティック割り込みでは、se の実行時間が割り当てられたタイム スライスを超えていることが判明した場合、CPU を放棄できるようにプリエンプションがトリガーされます。

図 4 に示すように、tg が 2 層でネストされ、現在の CPU での tg からの gse の各層の重みが 1024 であると仮定し、期間がタスク数から直接計算されると仮定すると、5 tse、期間は 3 * 5 = 15ms です。

tse1 は 1024/(1024+1024) * 15 = 7.5ms を取得します。

tse2 は [1024/(1024+1024+1024)] * {[1024/(1024+1024)] * 15 }= 2.5ms を取得します。

tse4 は [1024/(1024+1024)] * {[1024/(1024+1024+1024)] * [1024/(1024+1024)] * 15} = 1.25ms を取得します。

図 4:

73625b62657fcda8b896b7382989418b.png

注: tg1 と tg2 の重みは、cpu.shares ファイルを介して構成されます。次に、各 cpu の gse は、cpu.shares によって構成された重みから、その上の grq の重み比率に従って重みを分配します。gse の重みは、nice 値に関連付けられなくなりました。

6.2. ランタイム伝導

pick_next_task_fair() は、仮想時間が最小の se を優先して選択します。gse の仮想時間はどのように更新されますか? 仮想時間は update_curr() で更新され、次に for_each_sched_entity を介してレイヤーごとに gse の仮想時間が更新されます。tse が 5 ミリ秒実行される場合、その親レベルの各 gse は 5 ミリ秒実行され、各レベルは独自の重みに従って仮想時間を更新します。

60b2685dbb2a0a4200fee852c411e2a8.png

メインの呼び出しパス:

4f59c378f24b994d8e71630f04294a9c.png

次に実行するタスクを選択するときは、レベルごとに最小の仮想時間を持つ se を選択します. gse が選択されている場合は、tse が選択されるまでその grq から選択を続けます.

7.タスクグループのPELTロード

7.1. ロードで使用されるタイムラインを計算する

負荷の計算に使用されるタイムラインは、仮想時間の計算に使用されるタイムラインとは異なります。仮想時間の計算に使用されるタイムラインは rq->clock_task で、実行にかかる時間です。計算負荷で使用されるタイムラインは rq->clock_pelt で、CPU の計算能力と現在の周波数ポイントに応じてスケーリングされます.CPU がアイドル状態のときは、rq->clock_task に同期されます. したがって、PELT によって計算された荷重は、WALT によって計算された荷重のようなスケールを必要とせずに、直接使用できます。rq->clock_pelt のタイムラインを更新する関数は update_rq_clock_pelt() です

778a45ee47680d5c876133eb46a484d6.png

最終的に計算された delta= delta * (capacity_cpu / capacity_max(1024)) * (cur_cpu_freq / max_cpu_freq) は、現在の CPU を現在の周波数ポイントで実行し、最大値に対応する最大周波数ポイントにスケーリングすることによって得られるデルタ時間値です。パフォーマンス CPU デルタ時間値。次に、clock_pelt に追加します。たとえば、小さなコアで 1 GHz で 5 ミリ秒実行しても、超大規模コアで 1 ミリ秒しか実行できない場合があるため、異なるクラスターの CPU コアで同じ時間実行すると、負荷の増加が異なります。

7.2. 負荷の定義と計算

load_avg は次のように定義されます: load_avg = runnable% * scale_load_down(load)。

runnable_avg定义は:runnable_avg = runnable% * SCHED_CAPACITY_SCALE。

util_avg は次のように定義されます: util_avg = running% * SCHED_CAPACITY_SCALE。

これらの負荷値は、se および cfs_rq 構造に組み込まれている struct sched_avg 構造に格納されます。さらに、struct sched_avg は、計算を支援するために load_sum、runnable_sum、util_sum メンバーも導入します。さまざまなエンティティ (tse/gse/grq/cfs_rq) の負荷は、runnable% が実行したい量であり、実行量は running% とは異なります。これらの 2 つの要素は、tse の値 [0,1] のみを取り、他のエンティティのこの範囲を超えています。

7.2.1. ツェーロード

tse負荷の計算式を見てみましょう. 印象を深めるために、無限ループを実行する例を挙げます. 計算関数については update_load_avg --> __update_load_avg_se() を参照してください。

load_avg: weight * load_sum/divider に等しい。ここで、weight = sched_prio_to_weight[prio-100]。load_sum はタスク実行中 + 実行可能状態の等比数列なので、divider はほぼ等比数列の最大値であるため、無限ループ タスクの load_avg はその重みに近くなります。

runnable_avg: runnable_sum / 除算器に等しい。runnable_sum はタスク実行中 + 実行可能状態の等比数列を拡大したものであるため、分割数はほぼ等比数列の最大値であるため、無限ループ タスクの runnable_avg は SCHED_CAPACITY_SCALE に近くなります。

util_avg: util_sum / 分周器に等しい。util_sum はタスクの実行状態の等比数列を拡大したものであるため、分割数はほぼ等比数列の最大値になり、無限ループ タスクの util_avg は SCHED_CAPACITY_SCALE に近くなります。

load_sum: タスクの単純な実行中 + 実行可能状態の等比数列の累積値です。無限ループの場合、この値は LOAD_AVG_MAX に近づきます。

runnable_sum: タスク実行中+実行可能状態の等比数列の累積値であり、スケールアップ後の値です。無限ループの場合、この値は LOAD_AVG_MAX * SCHED_CAPACITY_SCALE になる傾向があります。

util_sum: タスクの実行状態の等比数列の累積値で、スケールアップ後の値です。特定のコアを独占する無限ループの場合、この値は LOAD_AVG_MAX * SCHED_CAPACITY_SCALE になる傾向があります. 独占できない場合は、この値よりも小さくなります.

7.2.2. cfs_rq のロード

cfs_rqの負荷計算式を見てみましょう. 印象を深めるために、無限ループを実行する例を挙げます. 計算関数については、update_load_avg --> update_cfs_rq_load_avg --> __update_load_avg_cfs_rq() を参照してください。

load_avg: load_sum/divider に直接等しくなります。cfs_rq は完全に実行され (無限ループまたは複数の無限ループを実行)、cfs_rq の重みに近づきます。これは、cfs_rq に関連付けられているすべてのスケジューリング エンティティの重みの合計、つまり Sum(sched_prio_to_weight[prio-100]) に近づきます。

runnable_avg: runnable_sum / 除算器に等しい。cfs_rq がフルに実行され (無限ループまたは複数の無限ループが実行され)、cfs_rq のタスク数に SCHED_CAPACITY_SCALE を掛けた数に近づきます。

util_avg: util_sum / 分周器に等しい。cfs_rq が完全に実行され (無限ループまたは複数の無限ループが実行され)、SCHED_CAPACITY_SCALE に近づきます。

load_sum: cfs_rq の重み、つまり、このレベルのすべての ses の重みの合計に非アイドル状態の等比数列を掛けたもの。このレベルであることに注意してください。これは、以下でレベル ロード h_load を説明するときに役立ちます。

runnable_sum: cfs_rq のすべてのレベルでの実行可能 + 実行状態のタスクの数に、非アイドル状態の等比数列を掛けてから、SCHED_CAPACITY_SCALE の値を掛けたもの。__update_load_avg_cfs_rq() を参照してください。

util_sum: cfs_rq で実行されているすべてのタスクの等比数列の合計に SCHED_CAPACITY_SCALE を掛けたもの。

load_avg、runnable_avg、および util_avg は、重み (優先度)、タスク数、および CPU タイム スライス占有率の 3 つの次元から CPU 負荷を表します。

7.2.3. gse ロード

gse を説明する tse とは対照的に:

(1) gse は、tse と同じロード更新プロセスに従います (レイヤーごとに更新し、gse に更新します)。

(2) gse の実行可能な負荷は、tse とは異なります。tse の runnable_sum は、タスク実行中 + 実行可能状態の幾何級数の累積値であり、スケールアップ後の値です。また、gse は、現在のレベルの下にあるすべてのレベルの tse の数の合計に、時間の等比数を掛けてスケールアップしたものです。__update_load_avg_se() 関数の実行可能なパラメーターの違いを参照してください。

(3) gse と tse の load_avg は se->weight * load_sum/divider と同じですが、___update_load_avg() のパラメーターの違いを参照してください。ただし、重みのソースが異なるため、相違点と見なすことができます. tse->重みはその優先度に由来し、gse は tg から割り当てられたクォータに由来します.

(4) gse は tse よりも負荷伝導更新プロセスが 1 つ多くなりますが、これについては以下で説明します (CFS グループ スケジューリングが有効でない場合、レイヤーは 1 つだけであり、tg の階層構造がないため、伝導は必要ありません。 cfs_rq に更新する必要があるだけです)。

7.2.4. grq ロード

grq ロードは、更新に関しては cfs_rq ロードと同じです。grq は cfs_rq よりも負荷伝導の更新プロセスが 1 つ多くなります。これについては以下で説明します。

7.2.5. tg の負荷

tg には tg->load_avg という 1 つの負荷しかなく、値は \Sum tg->cfs_rq[]->avg.load_avg であり、これは tg のすべての CPU での grq の load_avg の合計です。tg 負荷更新は update_tg_load_avg() で実装され、主に gse[] に重みを割り当てるために使用されます。

c4b15b815d91ab779b2fb94edd787395.png

呼び出しパス:

5adc48146396ed83c0e064455ed11602.png

7.3. 負荷伝導

負荷伝導は、CFS グループ スケジューリングが有効になった後にのみ概念になります。tg 階層で tse を挿入または削除すると、階層全体の負荷が変化するため、レイヤーごとに実行する必要があります。

7.3.1. 負荷導通のトリガー条件

負荷の伝導が必要かどうかは、struct cfs_rq の伝播メンバーによってマークされます。grq で tse を追加/削除すると、負荷伝導プロセスがトリガーされます。tse の load_sum 値は、struct cfs_rq の prop_runnable_sum メンバーに記録され、層ごとに上向きに実行されます。他のロード (runnable_*、util_*) は、tse-->grq-->gse-->grq... を介してレイヤーごとに送信されます。

add_tg_cfs_propagate() で負荷伝導の必要性をマークします。

6e86350720f0d77e658414866698bc88.png

この関数呼び出しパス:

655160c0dfc68804c6c74094ba52ctab.png

上記から、非 CSF スケジューリング クラスが CFS スケジューリング クラスに変更され、現在の tg に移動されると、新しく作成されたタスクが cfs_rq でハングし始め、現在の CPU に移行することがわかります。このとき、ヒエラルキー全体に転送されます 伝導は、このタスクによってもたらされる負荷を追加します。タスクが現在の CPU から移行するか、非 CFS スケジューリング クラスになるか、または tg から移行すると、このタスクを削除することによって削減された負荷が階層全体に転送されます。

休止状態のタスクの負荷は除去されませんが、その負荷は休止期間中に増加せず、時間の経過とともに減衰することに注意してください。

7.3.2. 負荷伝導処理

負荷の伝導過程は、層ごとに負荷を更新する過程に反映される。次のように、ロード更新関数 update_load_avg() がメイン パスの下の各レイヤーで呼び出されます。

7b619c7b683639b9d9cdefdb23544b4c.png

負荷転送関数と転送が必要な関数は同じ add_tg_cfs_propagate() であり、その呼び出しパスは次のとおりです。

c5fbec465e0d5cc8d4ac9d7ffa15cbf3.png

7.3.2.1. update_tg_cfs_util() gse と grq の util_* ペイロードを更新し、ペイロードを上位層に配信する役割を担います。

eed9c5be988788e08fc11e35a78417e0.png

gse の util 負荷が、伝導中に grq の util 負荷を直接受けることがわかります。次に、上位層 grq の util_avg を更新して上位層に送信します。

7.3.2.2. update_tg_cfs_runnable() gse と grq の runnable_* ロードを更新し、ロードを上位層に渡す役割を担います。

7fe9c357740b63291ac657ecad35506f.png

gse の実行可能な負荷も、伝導中にその grq の実行可能な負荷から直接取得されることがわかります。次に、上位層の grq の runnable_avg を更新して、上位層に実行します。

7.3.2.3. update_tg_cfs_load() gse と grq の load_* ロードを更新し、ロードを上位層に配信する役割を担います。

load load はかなり特殊で、負荷を送信する際に grq の load load から直接取得するのではなく、grq にタスクを追加/削除する際に tse の load_sum 値を記録し、レイヤーごとに送信します。 add_tg_cfs_propagate()、送信位置はパスと呼ばれます:

17f1327c45ea7043b5c58a9a8850303e.png

負荷負荷のマーキングと伝導はすべてこの機能です。

e15a5a5228beeb980103b811fa769fbb.png

ロードロード更新機能:

1aff3e0f193d891621cc118ab49d3b5e.png

削除タスクは、grq の se の平均 load_sum を gse に割り当てることです。追加タスクは、デルタ値を gse の load_sum に直接追加することです。

load_avg は通常の tse と同じ方法で計算され、load_sum*se_weight(gse)/divider になります。

runnable load と util load の伝導方向は、それぞれ runnable_avg/util_avg を介して grq-->gse からであり、gse は grq の値を直接取ることが比較からわかります。負荷 load の伝導方向は gse→grq で伝導し、load_sum で伝導します。

負荷伝導割り当て方法がランナブル負荷およびユーティリティ負荷と異なる理由は、その統計アルゴリズムに関連している可能性があります。runnable_avg の場合、gse は、現在のレベルの下にあるすべてのレベルの tse の数に実行可能な状態の時系列を乗じた比率を計算します。上位レベルに 1 つの tse を追加することは、tse の数に 1 を追加することと同じです。util_avg の場合、 gse は、時間進行に対するすべての tse の実行状態の等比数列の比率を計算し、上位層に 1 つの tse を追加することは、tse の実行状態の等比数列を増加させることに相当し、load_avg は se の重みに関連し、 gse と tse の重み 重みのソースは異なります. 前者は tg->shares から割り当てられたクォータに由来し、後者は優先度に由来し、直接加算または減算することはできません. se の場合、load_sum は実行可能な状態の単純な時系列であり、重みを含まないため、tse と gse の両方で使用できます。

load_avg の伝導については、例えば以下の図 5 に示すように、ts2 がスリープ状態で、ts1 と ts3 が 2 つの無限ループである場合、gse1 の grq1 の load_avg は 4096 に近づき、ルート cfs_rq の負荷が近づきます。 2048, if At this time, ts3 needs to be migrated away. runnable と util の負荷を計算するように直接削減したい場合、得られるデルタ値は -4096 であり、ルート cfs_rq の load_avg は負の値になります。 (2048-4096<0)、これは明らかに間違っています。load_sum を介して実行される場合、それは単なる時系列であり、減算後は、ルート cfs_rq の負荷の 50% を失うことに相当します。

図 5:

f89e8c534ef4322614183a4f814eeb28.png

注: これは、タスクが tg の階層で追加/削除されたときの負荷の伝導更新パスにすぎません。時間の経過とともに、タスクが追加/削除されていなくても、通常の負荷が更新されるため、gse/grq の負荷が更新されます。関数 __update_load_avg_se() /update_cfs_rq_load_avg() は、tse または gse、cfs_rq または grq を区別しません。

7.4. 階層ローディング

ロード バランシング中は、CPU の負荷を移動してバランスを取る必要があります。この目標を達成するには、CPU 間でタスクを移動する必要があります。ただし、タスク se/cfs rq は常に特定のレベルで負荷の平均を計算するため、各タスク se の負荷の平均は、ルート cfs rq (つまり、CPU) への負荷の寄与を正確に反映することはできません。たとえば、grq の load_avg は、それに接続されているすべての tse の load_avg の合計と等しくありません。これは、runnable の時系列が Sum(tse) > grq でなければならない (runnable が実行を待機している状態がある) ためです。

CPU 上のタスクの負荷 (h_load) を計算するために、階層負荷の概念が各 cfs rq に導入されます. 最上位の cfs rq の場合、その階層負荷は cfs rq の負荷平均に等しくなります.階層が進むにつれて、cfs rq 階層の負荷は次のように定義されます。

次のレイヤーの cfs rq の h_load = 前のレイヤーの cfs rq の h_load x 前のレイヤーの cfs load における gse load の割合

ボトム ツェーの h_load を計算するときは、次の式を使用します。

tse の h_load = grq の h_load x tse の負荷平均 / grq の負荷平均

タスクの h_load を取得および更新する関数は次のとおりです。

3e31215e0b38d5d0a3db56e0dc15ae14.png

grq の h_load を更新する関数は次のとおりです。

7ead88e80f03e2694358006984e02fa9.png

呼び出しパス:

526c2f62de17408d5dee38a63aa1ac0e.png

主に wake_affine_weight メカニズムと負荷分散ロジックで使用されていることがわかります。たとえば、移行タイプが負荷である負荷分散では、負荷分散を行うために移行する必要がある load_avg の量には、task_h_load() が使用されます。detach_tasks() を参照してください。

8. まとめ

この記事では、CFS グループ スケジューリング機能を導入する理由、設定方法、実装の詳細について紹介します。この機能は、グループ間で CPU リソースを公平に使用するという目的を達成するために、高負荷時に各グループ タスクが使用する CPU リソースの割合を (CFS 帯域幅制御と比較して) 「ソフト リミット」することができます。ネイティブ Android コードの古いバージョンでは、バックグラウンド グループ化の制限が厳しく (background/cpu.shares を 52 に設定することさえ)、CPU リソースの焦点はフォアグラウンド グループ化に傾いていますが、この構成は一部のアプリでフォアグラウンドに表示される場合があります。シナリオ タスクがバックグラウンド タスクでスタックした場合、ユニバーサル構成の場合、最新の Android バージョンでは、グループ間の CPU リソースの公平性を追求するために、各グループの cpu.shares は 1024 に設定されます。

9. 参照

1. カーネル ソース コード (https://www.kernel.org/) および Android ソース コード (https://source.android.com/docs/setup/download/downloading)

2. カーネルのドキュメント Documentation/admin-guide/cgroup-v1

3. CFS スケジューラ (3) - グループスケジューリング: http://www.wowotech.net/process_management/449.html

4. PELT アルゴリズムの分析: http://www.wowotech.net/process_management/pelt.html

cd0efbe34c6a1c76cd569d3256ced31c.gif

長押しでカーネル職人のWeChatをフォロー

Linux Kernel Black テクノロジー | 技術記事 | 注目のチュートリアル

おすすめ

転載: blog.csdn.net/feelabclihu/article/details/128586905