2017-06-27
上篇文章簡要介紹了Linux進程調度,以及結合源代碼窺探了下CFS的調度實例。但是沒有深入內部區分析調度下面的操作,比如就緒隊列的維護以及進程時間的更新等。本節就這些問題做深入討論。
回想進程調度,在thread_info中有一個重調度位,標識當前進程是否需要被調度,如果該位被設置表明當前進程需要被調度,在那么就調用調度器,執行下一個進程。但是該位是如何被設置的呢?換句話說,什么時候會設置該值,主要有以下幾個地方
- 1、時鍾中斷
- 2、主動禮讓
- 3、喚醒進程,檢查優先級
- 4、更改進程的調度策略
- 5、fork進程
今天我們重點說說時鍾中斷的情況,即進程的周期調度器。周期調度器正式通過時鍾中斷觸發的。時鍾中斷的內容請參考前面關於時間管理部分,這里僅僅是從時鍾中斷引入周期調度器。為了簡便,我們以普通的周期時鍾為例,這種情況下中斷處理程序為timer_interrupt,每次時鍾中斷都是調用update_process_times來更新進程的運行時間,之后會調用一個函數scheduler_tick,這個就是扮演的周期調度器的角色。之前講進程調度的時候也有提到,進程調度依賴於具體的調度類,目前普通進程一般采用CFS做為調度算法,所以這里我們以CFS調度類為例做介紹。
CFS調度類的周期調度函數為task_tick_fair,看下代碼
1 static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued) 2 { 3 struct cfs_rq *cfs_rq; 4 struct sched_entity *se = &curr->se; 5 6 for_each_sched_entity(se) { 7 cfs_rq = cfs_rq_of(se); 8 entity_tick(cfs_rq, se, queued); 9 } 10 11 if (sched_feat_numa(NUMA)) 12 task_tick_numa(rq, curr); 13 14 update_rq_runnable_avg(rq, 1); 15 }
這里操作的是調度實體sched_entity,這樣就把具體的調度對象抽象化,借助於調度實體可以調度進程、也可以調度進程組。所以這里是for_each……我們按照調度單個進程來講,里面調用了entity_tick,該函數主要實現兩個功能1、通過update_curr更新當前進程的運行時間,注意這里是主要更新vruntime和調度實體的時間字段,然后調用check_preempt_tick檢查搶占。
1、vruntime的更新
update_curr()->__update_curr,在此之前我們還是回顧下調度實體中的幾個關於時間的字段
struct sched_entity {
……
/*每次周期調度器執行,當前時間和exec_start之差被計算,然后更新exec_start到當前時間,差值被加到sum_exec_runtime*/
u64 exec_start; /*記錄進程在CPU 上運行的總時間,在update_curr更新*/ u64 sum_exec_runtime; /*進程的虛擬時間*/ u64 vruntime; /*當進程從CPU 挪下,當前的sum_exec_runtime移動到prev_sum_exec_runtime 在進程搶占的時候會用到,但是並不會清除sum_exec_runtime,該值仍然是持續增長的 */ u64 prev_sum_exec_runtime;
…… }
現在看下函數代碼:
static void update_curr(struct cfs_rq *cfs_rq) { struct sched_entity *curr = cfs_rq->curr; u64 now = rq_of(cfs_rq)->clock_task; unsigned long delta_exec; if (unlikely(!curr)) return; /* * Get the amount of time the current task was running * since the last time we changed load (this cannot * overflow on 32 bits): */ /*上次更新到現在的時間差*/ delta_exec = (unsigned long)(now - curr->exec_start); if (!delta_exec) return; __update_curr(cfs_rq, curr, delta_exec); /*更新exec_start*/ curr->exec_start = now; if (entity_is_task(curr)) { struct task_struct *curtask = task_of(curr); trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime); cpuacct_charge(curtask, delta_exec); account_group_exec_runtime(curtask, delta_exec); } account_cfs_rq_runtime(cfs_rq, delta_exec); }
可以看到這里操作的實際上是當前隊列的正在運行的調度實體,如果該實體為空,則返回。然后獲取上次更新到現在的時間差,如果沒有更新那么就沒必要接下來的更新。否則調用__update_curr,該函數更新了vruntime;接下來更新exec_start到當前時間。如果當前實體是進程的話,還有接下來的統計操作,最后更新隊列總的運行時間。
static inline void __update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr, unsigned long delta_exec) { unsigned long delta_exec_weighted; schedstat_set(curr->statistics.exec_max, max((u64)delta_exec, curr->statistics.exec_max)); /*增加進程的總的運行時間*/ curr->sum_exec_runtime += delta_exec; schedstat_add(cfs_rq, exec_clock, delta_exec); delta_exec_weighted = calc_delta_fair(delta_exec, curr); /*隨着進程的運行,vruntime會增加,在紅黑樹中的位置越靠右*/ curr->vruntime += delta_exec_weighted; update_min_vruntime(cfs_rq); }
這里增加了進程實際運行的總時間,然后調用calc_delta_fair計算vruntime,vruntime大致是delta*NICE_0_LOAD/curr->load.weight,可以看到vruntime是跟實際運行時間成正比,跟自身權重成反比的,權重越大在運行相同時間的情況下其vruntime增長的越慢,得到調度的機會就越多。得到的結果會加到當前進程的vruntime上,故隨着進程的運行,其vruntime會一直增加。潛在的優先級也就越來越低。最后調用update_min_vruntime對隊列的min_vrntime進行更新,更新條件是當前進程的vruntime 和下一個要被調度的進程的vruntime做對比,取小的 值。如果該值大於cfs_rq的vruntime,則更新,否則不更新。
2、check_preempt_tick檢查搶占
static void check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr) { unsigned long ideal_runtime, delta_exec; struct sched_entity *se; s64 delta; ideal_runtime = sched_slice(cfs_rq, curr); /*進程的執行時間*/ delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime; /*如果執行時間超過分配時間,則設置沖調度位*/ if (delta_exec > ideal_runtime) { resched_task(rq_of(cfs_rq)->curr); /* * The current task ran long enough, ensure it doesn't get * re-elected due to buddy favours. */ clear_buddies(cfs_rq, curr); return; } /* * Ensure that a task that missed wakeup preemption by a * narrow margin doesn't have to wait for a full slice. * This also mitigates buddy induced latencies under load. */ if (delta_exec < sysctl_sched_min_granularity) return; se = __pick_first_entity(cfs_rq); delta = curr->vruntime - se->vruntime; if (delta < 0) return; if (delta > ideal_runtime) resched_task(rq_of(cfs_rq)->curr); }
該函數主要判斷當前進程是不是運行的時間已經足夠長了,是的話就設置重調度位(僅僅是設置重調度位,在調度器檢查調度的時候才會真正執行調度)。分為一下三種情況
- 當前進程運行時間大於ideal_runtime,就設置重調度位。
- 當前進程運行時間小於sysctl_sched_min_granularity,就返回繼續運行。
- 當前進程的vruntime大於下一個即將調度進程的vruntime,且差值大於ideal_runtime,設置重調度位。
說到這里需要介紹下幾個變量,內核中有個ideal_runtime的概念,保證每個進程運行都至少運行一個固定的時間,默認情況該值為sysctl_sched_latency,同時又一個就緒進程的數目sched_nr_latency,當當前可運行進程數目大於該值時,運行時間sysctl_sched_latency也要相應擴展。同時內核中還有規定一個進程運行的最短時間sysctl_sched_min_granularity,當進程數目超過sched_nr_latency,就是根據sysctl_sched_min_granularity得到的ideal_runtime,ideal_runtime=sysctl_sched_min_granularity*nr_running
總結:根據以上的介紹可以發現,vruntime所影響的僅僅是得到調度的機會,一個優先級高的進程比優先級低的進程更可能得到調度,但是隨着優先級高的進程運行時間的增加,其vruntime會逐漸增大,而隨着優先級低的進程總有機會得到調度。就進程每次運行的時間而言,高優先級和低優先級的進程每次分到的時間是一樣的。CFS的公平體現在減少了高優先級和低優先級進程在調度時帶來的待遇的不平等,讓低優先級的進程始終有機會得到調度。
參考資料:
linux3.10.1內核源碼
深入linux內核架構