linux內核分析之調度演算法(一)
linux內核分析之調度演算法(一)
linux調度演算法在2.6.32中採用調度類實現模塊式的調度方式。這樣,能夠很好的加入新的調度演算法。
linux調度器是以模塊方式提供的,這樣做的目的是允許不同類型的進程可以有針對性地選擇調度演算法。這種模塊化結構被稱為調度器類,他允許多種不同哦可動態添加的調度演算法並存,調度屬於自己範疇的進程。每個調度器都有一個優先順序,調度代碼會按照優先順序遍歷調度類,擁有一個可執行進程的最高優先順序的調度器類勝出,去選擇下面要執行的那個程序。
linux上主要有兩大類調度演算法,CFS(完全公平調度演算法)和實時調度演算法。宏SCHED_NOMAL主要用於CFS調度,而SCHED_FIFO和SCHED_RR主要用於實時調度。如下面的宏定義:
view plaincopy to clipboardprint?/*
* Scheduling policies
*/
/*支援Real-Time Task的排程,包括有SCHED_FIFO與SCHED_RR.
*/
/*(也稱為SCHED_OTHER): 主要用以排程
一般目的的Task.*/
#define SCHED_NORMAL 0
#define SCHED_FIFO 1
/*task預設的 Time Slice長度為100 msecs*/
#define SCHED_RR 2
/*主要用以讓Task可以延長執行的時間
(Time Slice),減少被中斷發生Task Context-Switch
的次數.藉此可以提高 Cache的利用率
(每次Context-Switch都會導致Cache-Flush). 比
較適合用在固定週期執行的Batch Jobs任
務主機上,而不適合用在需要使用者互
動的產品 (會由於Task切換的延遲,而
感覺到系統效能不佳或是反應太慢).*/
#define SCHED_BATCH 3
/* SCHED_ISO: reserved but not implemented yet */
/*為系統中的Idle Task排程.*/
#define SCHED_IDLE 5
/*
* Scheduling policies
*/
/*支援Real-Time Task的排程,包括有SCHED_FIFO與SCHED_RR.
*/
/*(也稱為SCHED_OTHER): 主要用以排程
一般目的的Task.*/
#define SCHED_NORMAL 0
#define SCHED_FIFO 1
/*task預設的 Time Slice長度為100 msecs*/
#define SCHED_RR 2
/*主要用以讓Task可以延長執行的時間
(Time Slice),減少被中斷發生Task Context-Switch
的次數.藉此可以提高 Cache的利用率
(每次Context-Switch都會導致Cache-Flush). 比
較適合用在固定週期執行的Batch Jobs任
務主機上,而不適合用在需要使用者互
動的產品 (會由於Task切換的延遲,而
感覺到系統效能不佳或是反應太慢).*/
#define SCHED_BATCH 3
/* SCHED_ISO: reserved but not implemented yet */
/*為系統中的Idle Task排程.*/
#define SCHED_IDLE 5
linux調度演算法實現的高層數據結構主要有運行實體、調度類、運行隊列,下面我們主要看看這幾個數據結構的欄位和意義。
運行實體,rq結構體每個cpu有一個,主要存儲一些基本的用於調度的信息,包括實時調度的和CFS調度的
view plaincopy to clipboardprint? /*每個處理器都會配置一個rq*/
struct rq {
/* runqueue lock: */
spinlock_t lock;
/*
* nr_running and cpu_load should be in the same cacheline because
* remote CPUs use both these fields when doing load calculation.
*/
/*用以記錄目前處理器rq中執行task的數量*/
unsigned long nr_running;
#define CPU_LOAD_IDX_MAX 5
/*用以表示處理器的負載,在每個處理器的rq中
都會有對應到該處理器的cpu_load參數配置,在每次
處理器觸發scheduler tick時,都會呼叫函數
update_cpu_load_active,進行cpu_load的更新。在系統初始化的時候
會呼叫函數sched_init把rq的cpu_load array初始化為0.
了解他的更新方式最好的方式是通過函數update_cpu_load,公式如下澹?
cpu_load會直接等待rq中load.weight的值。
cpu_load=(cpu_load*(2-1)+cpu_load)/2
cpu_load=(cpu_load*(4-1)+cpu_load)/4
cpu_load=(cpu_load*(8-1)+cpu_load)/8
cpu_load=(cpu_load*(16-1)+cpu_load/16
呼叫函數this_cpu_load時,所返回的cpu load值是cpu_load
而在進行cpu blance或migration時,就會呼叫函數
source_load target_load取得對該處理器cpu_load index值,
來進行計算*/
unsigned long cpu_load;
#ifdef CONFIG_NO_HZ
unsigned long last_tick_seen;
unsigned char in_nohz_recently;
#endif
/* capture load from *all* tasks on this cpu: */
/*load->weight值,會是目前所執行的schedule entity的
load->weight的總和,也就是說rq的load->weight越高,
也表示所負責的排程單元load->weight總和越高
表示處理器所負荷的執行單元也越重*/
struct load_weight load;
/*在每次scheduler tick中呼叫update_cpu_load時,
這個值就增加一,可以用來反饋目前cpu
load更新的次數*/
unsigned long nr_load_updates;
/*用來累加處理器進行context switch的次數,會在
函數schedule呼叫時進行累加,並可以通過函數
nr_context_switches統計目前所有處理器總共的context switch
次數,或是可以透過查看檔案/proc/stat中的ctxt位得知目前
整個系統觸發context switch的次數*/
u64 nr_switches;
u64 nr_migrations_in;
/*為cfs fair scheduling class 的rq*/
struct cfs_rq cfs;
/*為real-time scheduling class 的rq*/
struct rt_rq rt;
/*用以支援可以group cfs tasks的機制*/
#ifdef CONFIG_FAIR_GROUP_SCHED
/* list of leaf cfs_rq on this cpu: */
/*在有設置fair group scheduling 的環境下,
會基於原本cfs rq中包含有若干task的group
所成的排程集合,也就是說當有一個group a
就會有自己的cfs rq用來排程自己所屬的tasks,
而屬於這group a的tasks所使用到的處理器時間
就會以這group a總共所分的的時間為上限。
基於cgroup的fair group scheduling 架構,可以創造出
有階層性的task組織,根據不同task的功能群組化
在配置給該群主對應的處理器資源,讓屬於
該群主下的task可以透過rq機制排程。使用屬於
該群主下的資源。
這個變數主要是管理CFS RQ list,操作上可以透過函數
list_add_leaf_cfs_rq把一個group cfs rq加入到list中,或透過
函數list_del_leaf_cfs_rq把一個group cfs rq移除,並可以
透過for_each_leaf_cfs_rq把一個rq上得所有leaf cfs_rq走一遍
*/
struct list_head leaf_cfs_rq_list;
#endif
/*用以支援可以group real-time tasks的機制*/
#ifdef CONFIG_RT_GROUP_SCHED
/*類似leaf_cfs_rq_list所扮演的角色,只是這裡
是針對屬於real-time的task,在實際操作上可以
透過函數list_add_leaf_rt_rq,list_del_leaf_rt_rq或
巨集for_each_leaf_rt_rq*/
struct list_head leaf_rt_rq_list;
#endif
/*
* This is part of a global counter where only the total sum
* over all CPUs matters. A task can increase this counter on
* one CPU and if it got migrated afterwards it may decrease
* it on another CPU. Always updated under the runqueue lock:
*/
/*一般來說,linux kernel 的task狀態可以為TASK_RUNNING
TASK_INTERRUPTIBLE(sleep),
TASK_UNINTERRUPTIBLE(Deactivate Task,此時Task會從rq中
移除)或TASK_STOPPED.
透過這個變數會統計目前rq中有多少task屬於
TASK_UNINTERRUPTIBLE的狀態。當呼叫函數
active_task時,會把nr_uninterruptible值減一,並透過 該函數
enqueue_task把對應的task依據所在的scheduling class
放在 對應的rq中,並把目前rq中nr_running值加一*/
unsigned long nr_uninterruptible;
/*curr:指向目前處理器正在執行的task;
idle:指向屬於idle-task scheduling class 的idle task;
stop:指向目前最高等級屬於stop-task scheduling class
的task;*/
struct task_struct *curr, *idle;
/*基於處理器的jiffies值,用以記錄下次進行處理器
balancing 的時間點*/
unsigned long next_balance;
/*用以存儲context-switch發生時,前一個task的memory management
結構並可用在函數finish_task_switch中,透過函數mmdrop釋放前一個
task的記憶體資源*/
struct mm_struct *prev_mm;
/*用以記錄目前rq的clock值,基本上該值會等於透過sched_clock_cpu
(cpu_of(rq))的回傳值,並會在每次呼叫scheduler_tick時透過
函數update_rq_clock更新目前rq clock值。
在實作部分,函數sched_clock_cpu會透過sched_clock_local或
ched_clock_remote取得對應的sched_clock_data,而處理的sched_clock_data
值,會透過函數sched_clock_tick在每次呼叫scheduler_tick時進行更新;
*/
u64 clock;
/*用以記錄目前rq中有多少task處於等待i/o的sleep狀態
在實際的使用上,例如當driver接受來自task的調用,但處於等待i/o
回復的階段時,為了充分利用處理器的執行資源,這時
就可以在driver中呼叫函數io_schedule,此時
就會把目前rq中的nr_iowait加一,並設定目前task的io_wait為1
然後觸發scheduling 讓其他task有機會可以得到處理器執行時間*/
atomic_t nr_iowait;
#ifdef CONFIG_SMP
/*root domain是基於多核心架構下的機制,
會由rq結構記住目前採用的root domain,其中包括了
目前的cpu mask(包括span,online rt overload), reference count 跟cpupri
當root domain有被rq參考到時,refcount 就加一,反之就減一。而cpu
mask span表示rq可掛上的cpu mask,noline為rq目前已經排程的
cpu mask cpu上執行real-time task.可以參考函數pull_rt_task,當一個rq中屬於
real-time的task已經執行完畢,就會透過函數pull_rt_task從該
rq中屬於rto_mask cpu mask 可以執行的處理器上,找出是否有一個處理器
有大於一個以上的real-time task,若有就會轉到目前這個執行完成
real-time task 的處理器上
而cpupri不同於Task本身有區分140個(0-139)
Task Priority (0-99為RT Priority 而 100-139為Nice值 -20-19).
CPU Priority本身有102個Priority (包括,-1 為Invalid,
0為Idle,1為Normal,2-101對應到Real-Time Priority 0-99).
參考函式convert_prio, Task Priority如果是 140就會對應到
CPU Idle,如果是大於等於100就會對應到CPU Normal,
若是Task Priority介於0-99之間,就會對應到CPU Real-Time Priority 101-2之間.)
在實際的操作上,例如可以透過函式cpupri_find
帶入一個要插入的Real-Time Task,此時就會依據cpupri中
pri_to_cpu選擇一個目前執行Real-Time Task且該Task
的優先級比目前要插入的Task更低的處理器,
並透過CPU Mask(lowest_mask)返回目前可以選擇的處理器Mask.
實作的部份可以參考檔案kernel/sched_cpupri.c.
在初始化的過程中,會透過函式sched_init呼叫函式init_defrootdomain,
對Root Domain與 CPU Priority機制進行初始化.
*/
struct root_domain *rd;
/*Schedule Domain是基於多核心架構下的機制.
每個處理器都會有一個基礎的Scheduling Domain,
Scheduling Domain可以有階層性的架構,透過parent
可以找到上一層的Domain,或是透過child找到
下一層的 Domain (NULL表示結尾.).並可透過span
欄位,表示這個Domain所能涵蓋的處理器範圍.
通常Base Domain會涵蓋系統中所有處理器的個數,
而Child Domain所能涵蓋的處理器個數不超過它的
Parent Domain. 而當在進行Scheduling Domain 中的Task Balance
時,就會以該Domain所能涵蓋的處理器為最大範圍.
同時,每個Schedule Domain都會包括一個或一個以上的
CPU Groups (結構為struct sched_group),並透過next變數把
CPU Groups串連在一起(成為一個單向的Circular linked list),
每個CPU Group都會有變數cpumask來定義這個CPU Group
所涵蓋的處理器範圍.並且CPU Group所包括的處理器
範圍,必需涵蓋在所屬的Schedule Domain處理器範圍中.
當進行Scheduling Domain的Balancing時,會以其下的CPU Groups
為單位,根據cpu_power (會是該Group所涵蓋的處理器
Tasks Loading的總和)來比較不同的CPU Groups的負荷,
以進行Tasks的移動,達到Balancing的目的.
在有支援SMP的架構下,會在函式sched_init中,呼叫open_softirq,
註冊 SCHED_SOFTIRQ Software IRQ與其對應的 Callback函式
run_rebalance_domains. 並會在每次呼叫函式scheduler_tick時,
透過函式trigger_load_balance確認是否目前的jiffies值已經
大於RunQueue下一次要觸發Load Balance的next_balance時間值,
並透過函式raise_softirq觸發SCHED_SOFTIRQ Software IRQ.
在Software IRQ觸發後,就會呼叫函式run_rebalance_domains,
並在函式rebalance_domains中,進行後續處理器上的
Scheduling Domain Load Balance動作.
有關Scheduling Domain進一步的內容,也可以參考
Linux Kernel文件 Documentation/scheduler/sched-domains.txt.
*/
struct sched_domain *sd;
/*這值會等於函式idle_cpu的返回值,如果為1表示
目前CPU RunQueue中執行的為Idle Task. 反之為0,
則表示處理器執行的不是Idle Task (也就是說
處理器正在忙碌中.).*/
unsigned char idle_at_tick;
/* For active balancing */
/*若這值不為0,表示會有在Schedule排程動作
結束前,要呼叫的收尾函式. (實作為inline函式
post_schedule in kernel/sched.c),目前只有Real-Time Scheduling
Class有支援這個機制(會呼叫函式has_pushable_tasks
in kernel/sched_rt.c).*/
int post_schedule;
/*當RunQueue中此值為1,表示這個RunQueue正在進行
Fair Scheduling的Load Balance,此時會呼叫stop_one_cpu_nowait
暫停該RunQueue所屬處理器的排程,並透過函式
active_load_balance_cpu_stop,把Tasks從最忙碌的處理器,
移到Idle的處理器上執行.*/
int active_balance;
/*用以儲存目前進入Idle且負責進行 Load Balance
流程的處理器ID. 呼叫的流程為,在呼叫函式schedule時,
若該處理器RunQueue的nr_running為0 (也就是目前沒有
正在執行的Task),就會呼叫idle_balance,並觸發後續Load
Balance流程.*/
int push_cpu;
/* cpu of this runqueue: */
/*用以儲存目前運作這個RunQueue的處理器ID*/
int cpu;
/*為1表示目前此RunQueue有在對應的處理器掛上
並執行.*/
int online;
/*如果RunQueue中目前有Task正在執行,這個值會
等於目前該RunQueue的Load Weight除以目前RunQueue
中Task數目的均值.
(rq->avg_load_per_task = rq->load.weight / nr_running;).*/
unsigned long avg_load_per_task;
struct task_struct *migration_thread;
struct list_head migration_queue;
/*這個值會由Real-Time Scheduling Class呼叫函式
update_curr_rt,用以統計目前Real-Time Task執行時間的
均值,在這函式中會以目前RunQueue的clock_task
減去目前Task執行的起始時間,取得執行時間的
Delta值. (delta_exec = rq->clock_task – curr->se.exec_start; ).
在透過函式sched_rt_avg_update把這Delta值跟原本
RunQueue中的rt_avg值取平均值. 以運作的週期來看,
這個值可反應目前系統中Real-Time Task平均被
分配到的執行時間值.*/
u64 rt_avg;
/*這個值主要在函式sched_avg_update更新,以筆者手中
的Linux Kernel 2.6.38.6的實作來說,當RunQueue Clock
減去age_stamp大於 0.5秒 (=sched_avg_period),就會把這值
累加0.5秒 (單位都是nanoseconds). 從函式scale_rt_power
的實作來說,age_stamp值離RunQueue Clock越遠,表示total
值越大,available值也越大,而函式scale_rt_power返回的
div_u64計算結果也越大,最終 RunQueue的cpu_power
與Scheduling Domain中的Scheduling Group的cpu_power
值也就越大. (可參考函式update_cpu_power的實作).*/
u64 age_stamp;
/*這值會在觸發Scheduling時,若判斷目前處理器
RunQueue沒有正在運作的Task,就會透過函式
idle_balance更新這值為為目前RunQueue的clock值.
可用以表示這個處理器是何時進入到Idle的
狀態*/
u64 idle_stamp;
/*會在有Task運作且idle_stamp不為0 (表示前一個
狀態是在Idle)時以目前RunQueue的clock減去
idle_stmp所計算出的Delta值為依據,更新這個值
. 可反應目前處理器進入Idle狀態的時間長短*/
u64 avg_idle;
#endif
/* calc_load related fields */
/*用以記錄下一次計算CPU Load的時間,初始值
為目前的jiffies加上五秒與1次的Scheduling Tick的
間隔 (=jiffies + LOAD_FREQ,且LOAD_FREQ=(5*HZ+1))*/
unsigned long calc_load_update;
/*會等於RunQueue中nr_running與nr_uninterruptible的
總和.(可參考函式calc_load_fold_active).*/
long calc_load_active;
#ifdef CONFIG_SCHED_HRTICK
#ifdef CONFIG_SMP
/*在函式init_rq_hrtick初始化RunQueue High-Resolution
Tick時,此值預設為0.
在函式hrtick_start中,會判斷目前觸發的RunQueue
跟目前處理器所使用的RunQueue是否一致,
若是,就直接呼叫函式hrtimer_restart,反之就會
依據RunQueue中hrtick_csd_pending的值,如果
hrtick_csd_pending為0,就會透過函式
__smp_call_function_single讓RunQueue所在的另一個
處理器執行rq->hrtick_csd.func 所指到的函式
__hrtick_start. 並等待該處理器執行完畢後,
才重新把hrtick_csd_pending設定為1.
也就是說, RunQueue的hrtick_csd_pending是用來作為
SMP架構下,由處理器A觸發處理器B執行
_hrtick_start函式的一個保護機制.而有關在
SMP下如何由一個處理器觸發另一個處理器
執行函式的機制,可以參考kernel/smp.c中
相關smp_call_function_xxxxxxx的實作.s*/
int hrtick_csd_pending;
/*用以儲存hrtick機制中,要跨處理器執行的
函式結構.*/
struct call_single_data hrtick_csd;
#endif
/*為High-Resolution Tick的結構,會透過函式
hrtimer_init初始化.*/
struct hrtimer hrtick_timer;
#endif
#ifdef CONFIG_SCHEDSTATS
/* latency stats */
/*為Scheduling Info.的統計結構,可以參考
include/linux/sched.h中的宣告. 例如在每次觸發
Schedule時,呼叫函式schedule_debug對上一個Task
的lock_depth進行確認(Fork一個新的Process 時,
會把此值預設為-1就是No-Lock,當呼叫
Kernel Lock時, 就會把Current Task的lock_depth加一.),
若lock_depth>=0,就會累加Scheduling Info.的bkl_count值,
用以代表Task Blocking的次數.*/
struct sched_info rq_sched_info;
/*可用以表示RunQueue中的Task所得到CPU執行
時間的累加值.
在發生Task Switch時,會透過sched_info_switch呼叫
sched_info_arrive並以目前RunQueue Clock值更新
Task 的sched_info.last_arrival時間,而在Task所分配時間
結束後,會在函式sched_info_depart中以現在的
RunQueue Clock值減去Task的sched_info.last_arrival
時間值,得到的 Delta作為變數rq_cpu_time的累
加值.*/
unsigned long long rq_cpu_time;
/* could above be rq->cfs_rq.exec_clock + rq->rt_rq.rt_runtime ? */
/* sys_sched_yield() stats */
/*用以統計呼叫System Call sys_sched_yield的次數.*/
unsigned int yld_count;
/* schedule() stats */
unsigned int sched_switch;
/*可用以統計觸發Scheduling的次數. 在每次觸發
Scheduling時,會透過函式schedule呼叫schedule_debug,
呼叫schedstat_inc對這變數進行累加.*/
unsigned int sched_count;
/*可用以統計進入到Idle Task的次數. 會在函式
pick_next_task_idle中,呼叫schedstat_inc對這變數進行
累加.*/
unsigned int sched_goidle;
/* try_to_wake_up() stats */
/*用以統計Wake Up Task的次數.*/
unsigned int ttwu_count;
/*用以統計Wake Up 同一個處理器Task的次數.*/
unsigned int ttwu_local;
/* BKL stats */
unsigned int bkl_count;
#endif
};
/*每個處理器都會配置一個rq*/
struct rq {
/* runqueue lock: */
spinlock_t lock;
/*
* nr_running and cpu_load should be in the same cacheline because
* remote CPUs use both these fields when doing load calculation.
*/
/*用以記錄目前處理器rq中執行task的數量*/
unsigned long nr_running;
#define CPU_LOAD_IDX_MAX 5
/*用以表示處理器的負載,在每個處理器的rq中
都會有對應到該處理器的cpu_load參數配置,在每次
處理器觸發scheduler tick時,都會呼叫函數
update_cpu_load_active,進行cpu_load的更新。在系統初始化的時候
會呼叫函數sched_init把rq的cpu_load array初始化為0.
了解他的更新方式最好的方式是通過函數update_cpu_load,公式如下澹?
cpu_load會直接等待rq中load.weight的值。
cpu_load=(cpu_load*(2-1)+cpu_load)/2
cpu_load=(cpu_load*(4-1)+cpu_load)/4
cpu_load=(cpu_load*(8-1)+cpu_load)/8
cpu_load=(cpu_load*(16-1)+cpu_load/16
呼叫函數this_cpu_load時,所返回的cpu load值是cpu_load
而在進行cpu blance或migration時,就會呼叫函數
source_load target_load取得對該處理器cpu_load index值,
來進行計算*/
unsigned long cpu_load;
#ifdef CONFIG_NO_HZ
unsigned long last_tick_seen;
unsigned char in_nohz_recently;
#endif
/* capture load from *all* tasks on this cpu: */
/*load->weight值,會是目前所執行的schedule entity的
load->weight的總和,也就是說rq的load->weight越高,
也表示所負責的排程單元load->weight總和越高
表示處理器所負荷的執行單元也越重*/
struct load_weight load;
/*在每次scheduler tick中呼叫update_cpu_load時,
這個值就增加一,可以用來反饋目前cpu
load更新的次數*/
unsigned long nr_load_updates;
/*用來累加處理器進行context switch的次數,會在
函數schedule呼叫時進行累加,並可以通過函數
nr_context_switches統計目前所有處理器總共的context switch
次數,或是可以透過查看檔案/proc/stat中的ctxt位得知目前
整個系統觸發context switch的次數*/
u64 nr_switches;
u64 nr_migrations_in;
/*為cfs fair scheduling class 的rq*/
struct cfs_rq cfs;
/*為real-time scheduling class 的rq*/
struct rt_rq rt;
《解決方案》
謝謝分享