1. 程式人生 > 實用技巧 >【原創】(四)Linux程序排程-組排程及頻寬控制

【原創】(四)Linux程序排程-組排程及頻寬控制

背景

  • Read the fucking source code! --By 魯迅
  • A picture is worth a thousand words. --By 高爾基

說明:

  1. Kernel版本:4.14
  2. ARM64處理器,Contex-A53,雙核
  3. 使用工具:Source Insight 3.5, Visio

1. 概述

組排程(task_group)是使用Linux cgroup(control group)的cpu子系統來實現的,可以將程序進行分組,按組來分配CPU資源等。
比如,看一個實際的例子:
A和B兩個使用者使用同一臺機器,A使用者16個程序,B使用者2個程序,如果按照程序的個數來分配CPU資源,顯然A使用者會佔據大量的CPU時間,這對於B使用者是不公平的。組排程就可以解決這個問題,分別將A、B使用者程序劃分成組,並將兩組的權重設定成佔比50%即可。

頻寬(bandwidth)控制,是用於控制使用者組(task_group)的CPU頻寬,通過設定每個使用者組的限額值,可以調整CPU的排程分配。在給定週期內,當用戶組消耗CPU的時間超過了限額值,該使用者組內的任務將會受到限制。

由於組排程和頻寬控制緊密聯絡,因此本文將探討這兩個主題,本文的討論都基於CFS排程器,開始吧。

2. task_group

  • 組排程,在核心中是通過struct task_group來組織的,task_group本身支援cfs組排程rt組排程,本文主要分析cfs組排程
  • CFS排程器管理的是sched_entity排程實體,task_struct(代表程序)task_group(代表程序組)
    中分別包含sched_entity,進而來參與排程;

關於組排程的相關資料結構,組織如下:

  • 核心維護了一個全域性連結串列task_groups,建立的task_group會新增到這個連結串列中;
  • 核心定義了root_task_group全域性結構,充當task_group的根節點,以它為根構建樹狀結構;
  • struct task_group的子節點,會加入到父節點的siblings連結串列中;
  • 每個struct task_group會分配執行佇列陣列和排程實體陣列(以CFS為例,RT排程類似),其中陣列的個數為系統CPU的個數,也就是為每個CPU都分配了執行佇列和排程實體;

對應到實際的執行中,如下:

  • struct cfs_rq包含了紅黑樹結構,sched_entity排程實體參與排程時,都會掛入到紅黑樹中,task_structtask_group都屬於被排程物件;
  • task_group會為每個CPU再維護一個cfs_rq,這個cfs_rq用於組織掛在這個任務組上的任務以及子任務組,參考圖中的Group A
  • 排程器在排程的時候,比如呼叫pick_next_task_fair時,會從遍歷佇列,選擇sched_entity,如果發現sched_entity對應的是task_group,則會繼續往下選擇;
  • 由於sched_entity結構中存在parent指標,指向它的父結構,因此,系統的執行也能從下而上的進行遍歷操作,通常使用函式walk_tg_tree_from進行遍歷;

2.2 task_group權重

  • 程序或程序組都有權重的概念,排程器會根據權重來分配CPU的時間。
  • 程序組的權重設定,可以通過/sys檔案系統進行設定,比如操作/sys/fs/cgoup/cpu/A/shares

呼叫流程如下圖:

  • sched_group_set_shares來完成最終的設定;
  • task_group為每個CPU都分配了一個sched_entity,針對當前sched_entity設定更新完後,往上對sched_entity->parent設定更新,直到根節點;
  • shares的值計算與load相關,因此也需要呼叫update_load_avg進行更新計算;

看一下實際的效果圖吧:

  • 寫節點操作可以通過echo XXX > /sys/fs/cgroup/cpu/A/B/cpu.shares
  • 橙色的線代表傳入引數指向的物件;
  • 紫色的線代表每次更新涉及到的物件,包括三個部分;
  • 處理完sched_entity後,繼續按同樣的流程處理sched_entity->parent

3. cfs_bandwidth

先看一下/sys/fs/cgroup/cpu下的內容吧:

  • 有兩個關鍵的欄位:cfs_period_uscfs_quota_us,這兩個與cfs_bandwidth息息相關;
  • period表示週期,quota表示限額,也就是在period期間內,使用者組的CPU限額為quota值,當超過這個值的時候,使用者組將會被限制執行(throttle),等到下一個週期開始被解除限制(unthrottle);

來一張圖直觀理解一下:

  • 在每個週期內限制在quota的配額下,超過了就throttle,下一個週期重新開始;

3.1 資料結構

核心中使用struct cfs_bandwidth來描述頻寬,該結構包含在struct task_group中。
此外,struct cfs_rq中也有與頻寬控制相關的欄位。
還是來看一下程式碼吧:

struct cfs_bandwidth {
#ifdef CONFIG_CFS_BANDWIDTH
	raw_spinlock_t lock;
	ktime_t period;
	u64 quota, runtime;
	s64 hierarchical_quota;
	u64 runtime_expires;

	int idle, period_active;
	struct hrtimer period_timer, slack_timer;
	struct list_head throttled_cfs_rq;

	/* statistics */
	int nr_periods, nr_throttled;
	u64 throttled_time;
#endif
};
  • period:週期值;
  • quota:限額值;
  • runtime:記錄限額剩餘時間,會使用quota值來週期性賦值;
  • hierarchical_quota:層級管理任務組的限額比率;
  • runtime_expires:每個週期的到期時間;
  • idle:空閒狀態,不需要執行時分配;
  • period_active:週期性計時已經啟動;
  • period_timer:高精度週期性定時器,用於重新填充執行時間消耗;
  • slack_timer:延遲定時器,在任務出列時,將剩餘的執行時間返回到全域性池裡;
  • throttled_cfs_rq:限流執行佇列列表;
  • nr_periods/nr_throttled/throttled_time:統計值;

struct cfs_rq結構中相關欄位如下:

struct cfs_rq {
...
#ifdef CONFIG_CFS_BANDWIDTH
	int runtime_enabled;
	u64 runtime_expires;
	s64 runtime_remaining;

	u64 throttled_clock, throttled_clock_task;
	u64 throttled_clock_task_time;
	int throttled, throttle_count;
	struct list_head throttled_list;
#endif /* CONFIG_CFS_BANDWIDTH */
...
}
  • runtime_enabled:週期計時器使能;
  • runtime_expires:週期計時器到期時間;
  • runtime_remaining:剩餘的執行時間;

3.2 流程分析

3.2.1 初始化流程

先看一下初始化的操作,初始化函式init_cfs_bandwidth本身比較簡單,完成的工作就是將struct cfs_bandwidth結構體程序初始化。

  • 註冊兩個高精度定時器:period_timerslack_timer
  • period_timer定時器,用於在時間到期時重新填充關聯的任務組的限額,並在適當的時候unthrottlecfs執行佇列;
  • slack_timer定時器,slack_period週期預設為5ms,在該定時器函式中也會呼叫distribute_cfs_runtime從全域性執行時間中分配runtime;
  • start_cfs_bandwidthstart_cfs_slack_bandwidth分別用於啟動定時器執行,其中可以看出在dequeue_entity的時候會去利用slack_timer,將執行佇列的剩餘時間返回給tg->cfs_b這個runtime pool
  • unthrottle_cfs_rq函式,會將throttled_list中的對應cfs_rq刪除,並且從下往上遍歷任務組,針對每個任務組呼叫tg_unthrottle_up處理,最後也會根據cfs_rq對應的sched_entity從下往上遍歷處理,如果sched_entity不在執行佇列上,那就重新enqueue_entity以便參與排程執行,這個也就完成了解除限制的操作;

do_sched_cfs_period_timer函式與do_sched_cfs_slack_timer()函式都呼叫了distrbute_cfs_runtime(),該函式用於分發tg->cfs_b的全域性執行時間runtime,用於在該task_group中平衡各個CPU上的cfs_rq的執行時間runtime,來一張示意圖:

  • 系統中兩個CPU,因此task_group針對每個cpu都維護了一個cfs_rq,這些cfs_rq來共享該task_group的限額執行時間;
  • CPU0上的執行時間,淺黃色模組表示超額了,那麼在下一個週期的定時器點上會進行彌補處理;

3.2.2 使用者設定流程

使用者可以通過操作/sys中的節點來進行設定:

  • 操作/sys/fs/cgroup/cpu/下的cfs_quota_us/cfs_period_us節點,最終會呼叫到tg_set_cfs_bandwidth函式;
  • tg_set_cfs_bandwidth會從root_task_group根節點開始,遍歷組排程樹,並逐個設定限額比率 ;
  • 更新cfs_bandwidthruntime資訊;
  • 如果使能了cfs_bandwidth功能,則啟動頻寬定時器;
  • 遍歷task_group中的每個cfs_rq佇列,設定runtime_remaining值,如果cfs_rq佇列限流了,則需要進行解除限流操作;

3.2.3 throttle限流操作

cfs_rq執行佇列被限制,是在throttle_cfs_rq函式中實現的,其中呼叫關係如下圖:

  • 排程實體sched_entity入列時,進行檢測是否執行時間已經達到限額,達到則進行限制處理;
  • pick_next_task_fair/put_prev_task_fair在選擇任務排程時,也需要進行檢測判斷;

3.2.4 總結

總體來說,頻寬控制的原理就是通過task_group中的cfs_bandwidth來管理一個全域性的時間池,分配給屬於這個任務組的執行佇列,當超過限額的時候則限制佇列的排程。同時,cfs_bandwidth維護兩個定時器,一個用於週期性的填充限額並進行時間分發處理,一個用於將未用完的時間再返回到時間池中,大抵如此。

組排程和頻寬控制就先分析到此,下篇文章將分析CFS排程器了,敬請期待。