引言php
爲何須要調度node
調度有關的進程描述符linux
struct task_struct { int prio, static_prio, normal_prio; unsigned int rt_priority; const struct sched_class *sched_class; struct sched_entity se; struct sched_rt_entity rt; … unsigned int policy; cpumask_t cpus_allowed; …};
-
prio
表示進程的優先級。進程運行時間,搶佔頻率都依賴於這些值。rt_priority
則用於實時(real-time)任務; -
sched_class
表示進程位於哪一個調度類; -
sched_entity
的意義比較特殊。一般把一個線程(Linux 中的進程、任務同義詞)叫做最小調度單元。可是 Linux 調度器不只僅只可以調度單個任務,並且還能夠將一組進程,甚至屬於某個用戶的全部進程做爲總體進行調度。這就容許咱們實現組調度,從而將 CPU 時間先分配到進程組,再在組內分配到單個線程。當引入這項功能後,能夠大幅度提高桌面系統的交互性。好比,能夠將編譯任務彙集成一個組,而後進行調度,從而不會對交互性產生明顯的影響。這裏再次強調下,**Linux 調度器不只僅能直接調度進程,也能對調度單元(schedulable entities)進行調度。這樣的調度單元正是用struct sched_entity
來表示的。須要說明的是,它並不是一個指針,而是直接嵌套在進程描述符中的。固然,後面的談論將聚焦在單進程調度這種簡單場景。因爲調度器是面向調度單元設計的,因此它會將單個進程也視爲調度單元,所以會使用sched_entity
結構體操做它們。sched_rt_entity
則是實時調度時使用的。 -
policy
代表任務的調度策略:一般意味着針對某些特定的進程組(如須要更長時間片,更高優先級等)應用特殊的調度決策。Linux 內核目前支持的調度策略以下: -
SCHED_NORMAL
:普通任務使用的調度策略; -
SCHED_BATCH
:不像普通任務那樣被頻繁搶佔,可容許任務運行儘量長的時間,從而更好地利用緩存,可是代價天然是損失交互性能。這種很是適合批量任務調度(批量的 CPU 密集型任務); -
SCHED_IDLE
:它要比 nice 19 的任務優先級還要低,但它並不是真的空閒任務; -
SCHED_FIFO
和SCHED_RR
是軟實時進程調度策略。它們是由 POSIX 標準定義的,由<kernel/sched/rt.c>
裏面定義的實時調度器負責調度。RR 實現的是帶有固定時間片的輪轉調度方式;SCHED_FIFO 則使用的是先進先出的隊列機制。 -
cpus_allowed
:用來表示任務的 CPU 親和性。用戶空間能夠經過sched_setaffinity
系統調用來設置。
優先級 Prioritynginx
普通任務優先級:算法
nice(int increment)
系統調用來修改當前進程的優先級。該系統調用的實現位於
<kernel/shced/core.c>
中。默認狀況下,用戶只能爲該用戶啓動的進程增長 nice 值(即下降優先級)。若是須要增長優先級(減小 nice 值),或者修改其它用戶進程優先級,則必須以 root 身份操做。
-
硬實時任務:會有嚴格的時間限制,任務必須在時限內完成。好比直升機的飛控系統,就須要及時響應駕駛員的操控,並作出預期的動做。然而,Linux 自己並不支持硬實時任務,可是有一些基於它修改的版本,如 RTLinux(它們一般被稱爲 RTOS)則是支持硬實時調度的。 -
軟實時任務:軟實時任務其實也會有時間限制,但不是那麼嚴格。也就是說,任務晚一點運行任務,並不會形成不可挽回的災難性事故。實踐中,軟實時任務會提供必定的時間限制保障,可是不要過分依賴這種特性。例如,VOIP 軟件會使用軟實時保障的協議傳來送音視頻信號,可是即使由於操做系統負載太高,而產生一點延遲,也不會形成很大影響。 不管如何,軟實時任務總會比普通任務的優先級更高。
相似其它的 Unix 系統,Linux 也是基於 POSIX 1b 標準定義的 「Real-time Extensions」實現實時優先級。能夠經過以下的命令查看系統中的實時任務:
shell
$ ps -eo pid, rtprio, cmd
chrt -p pid
查看單個進程的詳情。Linux 中能夠經過
chrt -p prio pid
更改實時任務優先級。這裏須要注意的是,若是操做的是一個系統進程(一般並不會將普通用戶的進程設置爲實時的),則必須有 root 權限才能夠修改實時優先級。
內核視角下的進程優先級:
數組
#define MAX_NICE 19#define MIN_NICE -20#define NICE_WIDTH (MAX_NICE - MIN_NICE + 1)…#define MAX_USER_RT_PRIO 100#define MAX_RT_PRIO MAX_USER_RT_PRIO#define MAX_PRIO (MAX_RT_PRIO + NICE_WIDTH)#define DEFAULT_PRIO (MAX_RT_PRIO + NICE_WIDTH / 2)/** Convert user-nice values [ -20 ... 0 ... 19 ]* to static priority [ MAX_RT_PRIO..MAX_PRIO-1 ],* and back.*/#define NICE_TO_PRIO(nice) ((nice) + DEFAULT_PRIO)#define PRIO_TO_NICE(prio) ((prio) - DEFAULT_PRIO)/** 'User priority' is the nice value converted to something we* can work with better when scaling various scheduler parameters,* it's a [ 0 ... 39 ] range.*/#define USER_PRIO(p) ((p)-MAX_RT_PRIO)#define TASK_USER_PRIO(p) USER_PRIO((p)->static_prio)#define MAX_USER_PRIO (USER_PRIO(MAX_PRIO))
優先級計算:緩存
int prio, static_prio, normal_prio;unsigned int rt_priority;
static_prio 是由用戶或系統設定的「靜態」優先級映射成內核表示的優先級:安全
p->static_prio = NICE_TO_PRIO(nice_value);
normal_prio 存放的是基於 static_prio 和進程調度策略(實時或普通)決定的優先級,相同的靜態優先級,在不一樣的調度策略下,獲得的正常優先級是不一樣的。子進程在 fork 時,會繼承父進程的 normal_prio。 微信
prio 則是「動態優先級」,在某些場景下優先級會發生變更。一種場景就是,系統能夠經過給某個任務優先級提高一段時間,從而搶佔其它高優先級任務,一旦 static_prio 肯定,prio 字段就能夠經過下面的方式計算:
p->prio = effective_prio(p);// kernel/sched/core.c 中定義了計算方法static int effective_prio(struct task_struct *p){ p->normal_prio = normal_prio(p); /* * If we are RT tasks or we were boosted to RT priority, * keep the priority unchanged. Otherwise, update priority * to the normal priority: */ if (!rt_prio(p->prio)) return p->normal_prio; return p->prio;}
static inline int normal_prio(struct task_struct *p){ int prio; if (task_has_dl_policy(p)) prio = MAX_DL_PRIO-1; else if (task_has_rt_policy(p)) prio = MAX_RT_PRIO-1 - p->rt_priority; else prio = __normal_prio(p); return prio;}
static inline int __normal_prio(struct task_struct *p){ return p->static_prio;}
task_struct->se.load
中看到進程的權重,定義以下:
struct sched_entity { struct load_weight load; /* for load-balancing */ …}struct load_weight { unsigned long weight; u32 inv_weight;};
爲了讓 nice 值的變化反映到 CPU 時間變化片上更加合理,Linux 內核中定義了一個數組,用於映射 nice 值到權重:
static const int prio_to_weight[40] = { /* -20 */ 88761, 71755, 56483, 46273, 36291, /* -15 */ 29154, 23254, 18705, 14949, 11916, /* -10 */ 9548, 7620, 6100, 4904, 3906, /* -5 */ 3121, 2501, 1991, 1586, 1277, /* 0 */ 1024, 820, 655, 526, 423, /* 5 */ 335, 272, 215, 172, 137, /* 10 */ 110, 87, 70, 56, 45, /* 15 */ 36, 29, 23, 18, 15,};
來看看如何使用上面的映射表,假設有兩個優先級都是 0 的任務,每一個都能得到 50% 的 CPU 時間(1024 / (1024 + 1024) = 0.5)。若是忽然給其中的一個任務優先級提高了 1 (nice 值 -1)。此時,一個任務應該會得到額外 10% 左右的 CPU 時間,而另外一個則會減小 10% CPU 時間。來看看計算結果:1277 / (1024 + 1277) ≈ 0.55,1024 / (1024 + 1277) ≈ 0.45,兩者差距恰好在 10% 左右,符合預期。完整的計算函數定義在 <kernel/sched/core.c> 中:
static void set_load_weight(struct task_struct *p){ int prio = p->static_prio - MAX_RT_PRIO; struct load_weight *load = &p->se.load; /* * SCHED_IDLE tasks get minimal weight: */ if (p->policy == SCHED_IDLE) { load->weight = scale_load(WEIGHT_IDLEPRIO); load->inv_weight = WMULT_IDLEPRIO; return; } load->weight = scale_load(prio_to_weight[prio]); load->inv_weight = prio_to_wmult[prio];}
調度類 Scheduling Classes
// 爲了簡單起見,隱藏了部分代碼(如 SMP 相關的)struct sched_class { // 多個 sched_class 是連接在一塊兒的 const struct sched_class *next; // 該 hook 會在任務進入可運行狀態時調用。它會將調度單元(如一個任務)放到 // 隊列中,同時遞增 `nr_running` 變量(該變量表示運行隊列中可運行的任務數) void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags); // 該 hook 會在任務不可運行時調用。它會將任務移出隊列,同時遞減 `nr_running` void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags); // 該 hook 能夠在任務須要主動放棄 CPU 時調用,可是須要注意的是,它不會改變 // 任務的可運行狀態,也就是說依然會在隊列中等待下次調度。相似於先 dequeue_task, // 再 enqueue_task void (*yield_task) (struct rq *rq); // 該 hook 會在任務進入可運行狀態時調用並檢查是否須要搶佔當前任務 void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags); // 該 hook 用來選擇最適合運行的下一個任務 struct task_struct * (*pick_next_task) (struct rq *rq, struct task_struct *prev); // 該 hook 會在任務修改自身的調度類或者任務組時調用 void (*set_curr_task) (struct rq *rq); // 一般是在時鐘中斷時調用,可能會致使任務切換 void (*task_tick) (struct rq *rq, struct task_struct *p, int queued); // 當任務被 fork 時通知調度器 void (*task_fork) (struct task_struct *p); // 當任務掛掉時通知調度器 void (*task_dead) (struct task_struct *p);};
-
core.c
包含調度器的核心部分; -
fair.c
實現了 CFS(Comple Faire Scheduler,徹底公平任務調度器) 調度器,應用於普通任務; -
rt.c
實現了實時調度,應用於實時任務; -
idle_task.c
當沒有其它可運行的任務時,會運行空閒任務。
內核是基於任務的調度策略(SCHED_*)來決定使用何種調度類實現,並會調用相應的方法。SCHED_NORMAL
,SCHED_BATCH
和SCHED_IDLE
進程會映射到fair_sched_class
(由 CFS 實現);SCHED_RR
和SCHED_FIFO
則映射的rt_sched_class
(實時調度器)。
運行隊列 runqueue
運行隊列數據結構定義以下(位於 <kernel/sched/sched.h>):
// 爲了簡單起見,隱藏了部分代碼(SMP 相關)// 這個是每一個 CPU 都會有的一個任務運行隊列struct rq{ // 表示當前隊列中總共有多少個可運行的任務(包含全部的 sched class) unsigned int nr_running;#define CPU_LOAD_IDX_MAX 5 unsigned long cpu_load[CPU_LOAD_IDX_MAX]; // 運行隊列負載記錄 struct load_weight load; // 嵌套的 CFS 調度器運行隊列 struct cfs_rq cfs; // 嵌套的實時任務調度器運行隊列 struct rt_rq rt; // curr 指向當前正在運行的進程描述符 // idle 則指向空閒進程描述符(當沒有其它可運行任務時,該任務纔會啓動) struct task_struct *curr, *idle; u64 clock; int cpu;}
什麼時候運行調度器?
schedule()
會在不少場景下被調用。有的是直接調用,有的則是隱式調用(經過設置
TIF_NEED_RESCHED
來提示操做系統儘快運行調度函數)。如下三個調度時機值得關注下:
時鐘中斷髮生時,會調用 scheduler_tick() 函數,該函數會更新一些和調度有關的數據統計,並觸發調度類的週期調度方法,從而間接地進行調度。以 2.6.39 源碼爲例,可能的調用鏈路以下:
scheduler_tick└── task_tick └── entity_tick └── check_preempt_tick └── resched_task └── set_tsk_need_resched
當前正在運行的任務進入睡眠狀態。在這種狀況下,任務會主動釋放 CPU。一般狀況下,該任務會由於等待指定的事件而睡眠,它能夠將本身添加到等待隊列,並啓動循環檢查指望的條件是否知足。在進入睡眠前,任務能夠將本身的狀態設置爲 TASK_INTERRUPTABLE(除了任務要等待的事件可喚醒外,也能夠被信號喚醒)或者 TASK_UNINTERRUPTABLE(天然是不會理會信號咯),而後調用 schedule() 選擇下一個任務運行。
Linux 調度器
Linux 0.0.1 版本就已經有了一個簡單的調度器,固然並不是適合擁有特別多處理器的系統。該調度器只維護了一個全局的進程隊列,每次都須要遍歷該隊列來尋找新的進程執行,並且對任務數量還有嚴格限制(NR_TASKS 在最初的版本中只有 32)。下面來看看這個調度器是如何實現的吧:
// 'schedule()' is the scheduler function. // This is GOOD CODE! There probably won't be any reason to change // this, as it should work well in all circumstances (ie gives // IO-bound processes good response etc)...void schedule(void){ int i, next, c; struct task_struct **p; // 遍歷全部任務,若是有信號,則須要喚醒 `TASK_INTERRUPTABLE` 的任務 for (p = &LAST_TASK; p > &FIRST_TASK; --p) if (*p) { if ((*p)->alarm && (*p)->alarm < jiffies) { (*p)->signal |= (1 << (SIGALRM - 1)); (*p)->alarm = 0; } if ((*p)->signal && (*p)->state == TASK_INTERRUPTIBLE) (*p)->state = TASK_RUNNING; } while (1) { c = -1; next = 0; i = NR_TASKS; p = &task[NR_TASKS]; // 遍歷全部任務,找到時間片最長的那個 while (--i) { if (!*--p) continue; if ((*p)->state == TASK_RUNNING && (*p)->counter > c) c = (*p)->counter, next = i; } if (c) break; // 遍歷任務,從新設值時間片 for (p = &LAST_TASK; p > &FIRST_TASK; --p) if (*p) (*p)->counter = ((*p)->counter >> 1) + (*p)->priority; } // 切換到下一個須要執行的任務 switch_to(next);}
O(n):
// schedule() 算法會遍歷全部的任務(O(N)),而且計算出每一個任務的// goodness 值,且挑選出「最好」的任務來運行。// 如下是部分核心源碼,主要是瞭解下它的思路。asmlinkage void schedule(void){ // 任務(進程)描述符: // 1. prev: 當前正在運行的任務 // 2. next: 下一個將運行的任務 // 3. p: 當前正在遍歷的任務 struct task_struct *prev, *next, *p; int this_cpu, c; // c 表示權重值repeat_schedule: // 默認選中的任務 next = idle_task(this_cpu); c = -1000; list_for_each(tmp, &runqueue_head) { p = list_entry(tmp, struct task_struct, run_list); if (can_schedule(p, this_cpu)) { int weight = goodness(p, this_cpu, prev->active_mm); if (weight > c) c = weight, next = p; } }}
源碼中的 goodness() 函數會計算出一個權重值,它的算法基本思想就是基於進程所剩餘的時鐘節拍數(時間片),再加上基於進程優先級的權重值。返回值以下:
-
-1000 表示不要選擇該進程運行 -
0 表示時間片用完了,須要從新計算 counters(可能會被選中運行) -
正整數:表示 goodness 值(越大越好) -
+1000 表示實時進程,接下來就要選擇它運行
-
算法實現很是簡單,可是不高效(任務越多,遍歷耗費時間越久) -
沒有很好的擴展性,多核處理器怎麼辦? -
對於實時任務調度支持較弱(不管如何做爲優先級高的實時任務都須要在遍歷完列表後才能夠知道)
O(1):
-
全局優先級單位,範圍是 0~139,數值越低,優先級越高 -
將任務拆分紅實時(099)和正常(100139)兩部分。更高優先級任務得到更多時間片 -
即刻搶佔(early preemption)。當任務狀態變成 TASK_RUNNING
時,內核會檢查其優先級是否比當前運行的任務優先級更高,若是是的話,則搶佔當前正在運行的任務,切換到該任務 -
實時任務使用靜態優先級 -
普通任務使用使用動態優先級。任務優先級會在其使用完本身的時間片後從新計算,內核會考慮它過去的行爲,決定它的交互性等級。交互型任務更容易獲得調度
O(n) 的調度器會在每一個紀元結束後(全部任務的時間片都使用過),纔會從新計算任務優先級。而 O(1) 則是在每一個任務時間片配額用完後就從新計算優先級。O(1) 調度器爲每一個 CPU 維護了兩個隊列,即 active 和 expired。active 隊列存放的是時間片還沒有用完的任務,而 expired 則是時間片已經耗盡的任務。當一個任務的時間片用完後,就會被轉到 expired 隊列,並且會從新計算它的優先級。當 active 隊列任務所有轉移到 expired 隊列後,會交換兩者(讓 active 指向 expired 隊列,expired 指向 active 隊列)。能夠看到,優先級的計算,隊列切換都和任務數量多寡無關,可以在 O(1) 時間複雜度下完成。
struct runqueue { unsigned long nr_running; /* 可運行的任務總數(某個 CPU) */ struct prio_array *active; /* 指向 active 的隊列的指針 */ struct prio_array *expired; /* 指向 expired 的隊列的指針 */ struct prio_array arrays[2]; /* 實際存放不一樣優先級對應的任務鏈表 */}
經過下面的圖能夠直觀感覺下任務隊列:
接下來看看 prio_array 是怎麼定義的:
struct prio_array { int nr_active; /* 列表中的任務總數 */ unsigned long bitmap[BITMAP_SIZE]; /* 位圖表示對應優先級鏈表是否有任務存在 */ struct list_head queue[MAX_PRIO]; /* 任務隊列(每種優先級對應一個雙向鏈表) */};
能夠看到,在 prio_array 中存在一個位圖,它是用來標記每一個 priority 對應的任務鏈表是否存在任務的。接下來看看爲什麼 O(1) 調度器能夠在常數時間找到須要運行的任務:
-
常數時間肯定優先級:首先會在位圖中查找到第一個設置爲 1 的位(總共有 140 bits,從第一個 bit 開始搜索,這樣能夠保證高優先級的任務先獲得機會運行),若是找到了就能夠肯定哪一個優先級有任務,假設找到後的值爲 priority
; -
常數時間得到下一個任務:在 queue[priority]
對應的任務鏈表中提取第一個任務來執行(多個任務會輪轉執行)。
-
設計上要比 O(n) 調度器更加複雜精妙; -
相對來講擴展性更好,性能更優,在任務切換上的開銷更小; -
用來標記任務是否爲交互類型的算法仍是過於複雜,且容易出錯。
CFS 經過在必定時間內運行調度全部的線程來避免飢餓問題。當運行的 線程數在 8 個及如下時,默認的時間週期是 48ms;而當多於 8 個線程時,時間週期就會隨着線程數量而增長(6ms * 線程數,之因此選擇 6ms,是爲了不頻繁搶佔,致使上下文切換頻繁切換的開銷)。因爲 CFS 老是會挑選 vruntime 最小的線程執行,它就須要避免某個線程的 vruntime 過小,以致於其它線程須要等待好久才能獲得調度(會有飢餓問題)。因此在實踐中,CFS 會保證全部線程之間的 vruntime 之差低於搶佔時間(6ms),它是經過以下兩點來保證的:
當線程建立時,它的 vruntime 值等於運行隊列中等待執行線程的最大 vruntime;
當線程從睡眠中喚醒時,它的 vruntime 值會被更新爲大於或等於全部待調度線程中最小的 vruntime。使用最小 vruntime 還能夠保證頻繁睡眠的線程優先被調度,這對於桌面系統很是適合,它會減小交互應用的響應延遲。
CFS 還引入了啓發式調度思想來改善高速緩存利用率。例如,當線程被喚醒時,它會檢查該線程的 vruntime 和正在運行的線程 vruntime 之差是否很是顯著(臨界值是 1ms),若是不是的話,則不會搶佔當前正在運行的任務。可是這種作法仍是以犧牲調度延遲爲代價的,算是一種權衡吧。
多核負載均衡:
爲了多個處理器上的工做量均衡,CFS 使用了 load 指標來衡量線程和處理器的負載狀況。線程的負載和線程的 CPU 平均使用率相關:常常睡眠的線程負載要低於不睡眠的線程負載。相似 vruntime,線程的負載也是線程的優先級加權獲得的。而處理器的負載是在該處理器上可運行線程的負載之和。CFS 會嘗試均衡處理器的負載。
CFS 會在線程建立和喚醒時關注處理器的負載狀況,調度器首先要決定將任務放在哪一個處理器的運行隊列中。這裏也會涉及到啓發式思想,好比,若是 CFS 檢查到生產者-消費者模型,那麼它會將消費者線程儘量地分散到機器的多個處理器上,由於多數核心都適合處理喚醒的線程。
負載均衡還會週期性發生,每隔 4ms,每一個處理器都會嘗試從其它處理器偷取一些工做。固然,這種 work-stealing 均衡方法還會考慮機器的拓撲結構:處理器會嘗試從距離它們「更近」的其它處理器上嘗試竊取工做,而非距離「更遠」的處理器(如遠程 NUMA 節點)。當處理器決定要從其它處理器竊取任務時,它會嘗試在兩者之間均衡負載,而且會竊取多達 32 個線程。此外,當處理器進入空閒狀態時,它也會馬上調用負載均衡器。
在大型的 NUMA 機器上,CFS 並不會粗暴地比較全部 CPU 的負載,而是以分層的方式進行負載均衡。以一臺有兩個 NUMA 節點的機器爲例,CFS 會先在 NUMA 節點內部的處理器之間進行負載均衡,而後比較 NUMA 節點之間的負載(經過節點內部處理器負載計算獲得),再決定要不要在兩個節點之間進行負載均衡。若是 NUMA 節點之間的負載差距在 25% 之內,則不會進行負載均衡。總結來講,若是兩個處理器(或處理器組)之間的距離越遠,那麼只有在不平衡性差距越大的狀況下才會考慮負載均衡。
運行隊列:
CFS 引入了紅黑樹(本質上是一棵半平衡二叉樹,對於插入和查找都有 O(log(N)) 的時間複雜度)來維護運行隊列,樹的節點值是調度單元的 vruntime,擁有最小 vruntime 的節點位於樹的最左下邊。
接下來看看 cfs_rq 數據結構的定義(位於 <kernel/sched/sched.h>):
struct cfs_rq{ // 全部任務的累計權重值 struct load_weight load; // 表示該隊列中有多少個可運行的任務 unsigned int nr_running; // 運行隊列中最小的 vruntime u64 min_vruntime; // 紅黑樹的根節點,指向運行任務隊列 struct rb_root tasks_timeline; // 下一個即將被調度的任務 struct rb_node *rb_leftmost; // 指向當前正在運行的調度單元 struct sched_entity *curr;}
CFS 算法實際應用於調度單元(這是一個更通用的抽象,能夠是線程、cgroups 等),調度單元數據結構定義以下(位於 <include/linux/sched.h>):
struct sched_entity{ // 表示調度單元的負載權重(好比該調度單元是一個組,則該值就是該組下全部線程的負載權重的組合) struct load_weight load; /* for load-balancing */ // 表示紅黑樹的節點 struct rb_node run_node; // 表示當前調度單元是否位於運行隊列 unsigned int on_rq; // 開始執行時間 u64 exec_start; // 總共運行的時間,該值是經過 `update_curr()` 更新的。 u64 sum_exec_runtime; // 基於虛擬時鐘計算出該調度單元已運行的時間 u64 vruntime;
// 用於記錄以前運行的時間之和 u64 prev_sum_exec_runtime;};
虛擬時鐘:
static void update_curr(struct cfs_rq *cfs_rq){ struct sched_entity *curr = cfs_rq->curr; u64 now = rq_clock_task(rq_of(cfs_rq)); u64 delta_exec; if (unlikely(!curr)) return; // 計算出調度單元開始執行時間和當前之間的差值,即真實運行時間 delta_exec = now - curr->exec_start; curr->vruntime += calc_delta_fair(delta_exec, curr); update_min_vruntime(cfs_rq);}
static inline u64 calc_delta_fair(u64 delta, struct sched_entity *se){ // 若是任務的優先級是默認的優先級(內部 nice 值是 120),那麼虛擬運行時間 // 就是真實運行時間。不然,會基於 `__calc_delta` 計算出虛擬運行時間。 if (unlikely(se->load.weight != NICE_0_LOAD)) // 該計算過程基本等同於: // delta = delta_exec * NICE_0_LOAD / cur->load.weight; delta = __calc_delta(delta, NICE_0_LOAD, &se->load); return delta;}
static void update_min_vruntime(struct cfs_rq *cfs_rq){ u64 vruntime = cfs_rq->min_vruntime; if (cfs_rq->curr) // 若是此時有任務在運行,就更新最小運行時間爲當前任務的 vruntime vruntime = cfs_rq->curr->vruntime; if (cfs_rq->rb_leftmost) { // 得到下一個要運行的調度單元 struct sched_entity *se = rb_entry(cfs_rq->rb_leftmost, struct sched_entity, run_node); if (!cfs_rq->curr) vruntime = se->vruntime; else // 保證 min_vruntime 是兩者之間較小的那個值 vruntime = min_vruntime(vruntime, se->vruntime); }
// 這裏之因此去兩者之間的最大值,是爲了保證 min_vruntime 可以單調增加 // 能夠想一想爲何須要這樣作? cfs_rq->min_vruntime = max_vruntime(cfs_rq->min_vruntime, vruntime);}
當任務運行時,它的虛擬時間老是會增長,從而保證它會被移動到紅黑樹的右側;
對於高優先級的任務,虛擬時鐘的節拍更慢,從而讓它移動到紅黑樹右側的速度就越慢,所以它們被再次調度的機會就更大些。
選擇下一個任務:
struct sched_entity *__pick_first_entity(struct cfs_rq *cfs_rq){ // 其實這裏取的是緩存的 leftmost 節點 // 因此執行就會更快了 struct rb_node *left = cfs_rq->rb_leftmost; if (!left) return NULL; return rb_entry(left, struct sched_entity, run_node);}
實時調度器:
SCHED_FIFO: 這個其實就是一個先到先服務的調度算法。這類任務沒有時間片限制,它們會一直運行直到阻塞或者主動放棄 CPU,亦或者被更高優先級的實時任務搶佔。該類任務總會搶佔 SCHED_NORMAL 任務。若是多個任務具備相同的優先級,那它們會以輪詢的方式調度(也就是當一個任務完成後,會被放到隊列尾部等待下次執行);
SCHED_RR: 這種策略相似於 SCHED_FIFO,只是多了時間片限制。相同優先級的任務會以輪詢的方式被調度,每一個運行的任務都會一直運行,直到其用光本身的時間片,或者被更高優先級的任務搶佔。當任務的時間片用光後,它會從新補充能量,並被加入到隊列尾部。默認的時間片是 100ms,能夠在 <include/linux/sched/rt.h> 找到其定義。
實時任務的優先級是靜態的,不會像以前提到的算法,會從新計算任務優先級。用戶能夠經過 chrt 命令更改任務優先級。
實現細節:
struct sched_rt_entity{ struct list_head run_list; unsigned long timeout; unsigned long watchdog_stamp; unsigned int time_slice; struct sched_rt_entity *back; struct sched_rt_entity *parent; /* rq on which this entity is (to be) queued: */ struct rt_rq *rt_rq;};
SCHED_FIFO 的時間片是 0,能夠在 <kernel/sched/rt.c> 中看到具體定義:
int sched_rr_timeslice = RR_TIMESLICE;static unsigned int get_rr_interval_rt(struct rq *rq, struct task_struct *task){ if (task->policy == SCHED_RR) return sched_rr_timeslice; else return 0;}
/* Real-Time classes' related field in a runqueue: */struct rt_rq{ // 全部相同優先級的實時任務都保存在 `active.queue[prio]` 鏈表中 struct rt_prio_array active; unsigned int rt_nr_running; struct rq *rq; /* main runqueue */};
/** This is the priority-queue data structure of the RT scheduling class:*/struct rt_prio_array{ /* include 1 bit for delimiter */ // 相似 O(1) 調度器,使用位圖來標記對應優先級的鏈表是否爲空 DECLARE_BITMAP(bitmap, MAX_RT_PRIO + 1); struct list_head queue[MAX_RT_PRIO];};
每次須要在對應優先級鏈表中遍歷查找須要執行任務,這個時間複雜度爲 O(n)。因此新的調度器引入了跳錶來解決該問題,從而將時間複雜度下降到 O(1)。
全局鎖爭奪的開銷優化,採用 try_lock 替代 lock。
本文分享自微信公衆號 - 人人都是極客(rrgeek)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。