小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

Linux Kernel 排程機(jī)制介紹 ? Loda's blog

 老匹夫 2014-08-12

Linux Kernel 排程機(jī)制介紹

hlchou@mail2000.com.tw

by loda.

2011/12/2

多核心架構(gòu)儼然是目前智慧型手機(jī)方案的新趨勢(shì),隨著省電與效能上的考量,多核心的架構(gòu)各家方案也都有所差異.為能讓同一個(gè)Linux Kernel在不同效能的處理器上能即時(shí)運(yùn)作,其中關(guān)鍵的部份便是核心的排程機(jī)制,也因此本文將以此為主題進(jìn)行介紹,希望能對(duì)目前投入這領(lǐng)域的開發(fā)者有所助益.

前一段時(shí)間,看到Qualcomm關(guān)於Snapdragon S4產(chǎn)品的新聞,這產(chǎn)品最讓人印象深刻的部份在於它支援a(chǎn)SMP(Asynchronous Symmetrical Multi-Processing)的處理器架構(gòu),基於ARMv7 Dual/Quad-Core 的Krait多核心處理器架構(gòu),可讓個(gè)別處理器根據(jù)執(zhí)行時(shí)期的負(fù)荷,動(dòng)態(tài)調(diào)整時(shí)脈(根據(jù)晶片型號(hào)差異,時(shí)脈最高可達(dá) 2.5Ghz)與電壓的DCVS (Dynamic Clock and Voltage Scaling)技術(shù). 每個(gè)aSMP的處理器都能獨(dú)立的調(diào)整電壓與時(shí)脈,並能夠關(guān)閉用不到的處理器,節(jié)省功耗. 由於可以根據(jù)需求個(gè)別調(diào)整主處理器的時(shí)脈,因此在aSMP架構(gòu)下,當(dāng)需要有類似Companion處理器的角色時(shí),就可以把其中一個(gè)主處理器降頻,同時(shí)兼顧到省電與執(zhí)行低算運(yùn)需求的目的 (Qualcomm採用的是28nm,省電製程.). 也因此,就無需在主處理器之外,額外提供一個(gè)運(yùn)算能力較低且低功耗的處理器,用來支援待機(jī)或是背景音樂播放的需求.

在同一則新聞中也提到NvidiaTegra3支援的Quad-Core與vSMP(Variable Symmetric Multiprocessing)架構(gòu).參考Nvidia的技術(shù)文件「Variable SMP – A Multi-Core CPU Architecture for Low Power and High Performance」,我們知道 Tegra3不同於前一代Tegra2是由兩個(gè)Cortex A9搭配一個(gè)低階ARM7的架構(gòu),以NVIDIA Kal -El的vSMP方案來說,會(huì)提供五個(gè)Cortex A9處理器(組合方式為4 個(gè)高效能Cortex A9主處理器 搭配 1個(gè)效能較低但強(qiáng)調(diào)省電的Cortex A9 Companion處理器),其中4個(gè)Cortex A9主處理器採用標(biāo)準(zhǔn)製程(時(shí)脈可到GHz.),以訴求較高的執(zhí)行效率,而剩下一個(gè)Companion Cortex A9處理器(時(shí)脈最高為500MHz),會(huì)採用低功耗(Low Power)的製程,可用於運(yùn)算量較低的待機(jī)或背景執(zhí)行需求.

基於vSMP的架構(gòu),這五個(gè)處理器,可以根據(jù)執(zhí)行的實(shí)際狀況,選擇主處理器或是Companion處理器運(yùn)作,並透過DVFS(Dynamic Voltage and Frequency Scaling) 動(dòng)態(tài)調(diào)整主處理器的頻率與電壓降低功耗,進(jìn)行Power Gating與利用目前Linux Kernel所支援的CPU Up/Down HotPlug機(jī)制,讓4個(gè)主處理器可以動(dòng)態(tài)的 CPU-Up/Down 掛起或移出Linux Kernel Tasks的排程範(fàn)圍(ex,Task Migration),以便讓閒置的處理器關(guān)閉節(jié)省功耗.

參考Tegra的WhitePaper,vSMP並不允許Companion處理器與主處理器被同時(shí)啟用,也因此Tegra3有提供軟硬體配套的CPU Governor 與 CPU Management Logic 去監(jiān)控處理器的執(zhí)行負(fù)荷,能在系統(tǒng)繁忙時(shí),關(guān)閉Companion CPU,把工作轉(zhuǎn)到4個(gè)主處理器上執(zhí)行,並監(jiān)控處理器執(zhí)行負(fù)載,動(dòng)態(tài)調(diào)整主時(shí)脈與哪個(gè)主處理器能被卸載. 也就是說,如果是處於待機(jī)或是背景音樂播放,系統(tǒng)就只開啟Companion 處理器,但若是需要上網(wǎng)或執(zhí)行更繁重的工作,就會(huì)卸載Companion 處理器,然後根據(jù)需求開啟1個(gè),2個(gè)或同時(shí)把4個(gè)主處理器致能. 基於這樣的設(shè)計(jì),也能對(duì)主處理器的時(shí)脈進(jìn)行調(diào)整,但不同於aSMP架構(gòu)的是,這4個(gè)主處理器的時(shí)脈會(huì)調(diào)整為一樣的時(shí)脈,要拉高就一起拉高,要降低就一起降低,因此對(duì)Linux Kernel的排程來說,並不會(huì)有參與排程的處理器運(yùn)算能力不一致的問題.

相對(duì)於上述提到的aSMP或vSMP架構(gòu),我們比較跟使用一個(gè)效率高的主處理器搭配一個(gè)省電且運(yùn)算能力較弱的Companion處理器的差異,例如 ARM9/ARM11 +DSP 或是一個(gè) Cortex A9 + ARM7,由於這兩個(gè)主處理器與Compaion處理器基礎(chǔ)架構(gòu)不同,兩個(gè)處理器的Memory Management與L2 Cache並沒有共用,甚至兩者在指令集的層次就完全不同,例如ARM環(huán)境中運(yùn)作的Linux Kernel配置好的記憶體分頁,或是多工環(huán)境下的Tasks,就不能直接透過排程分到DSP上運(yùn)作.而是必須在設(shè)計(jì)階段,就考慮到這兩個(gè)處理器協(xié)同運(yùn)作的配套機(jī)制,例如Linux Kernel,人機(jī)介面與檔案系統(tǒng)是運(yùn)作在ARM端,而Audio Codec是放在DSP上執(zhí)行,此時(shí)會(huì)由ARM去處理人機(jī)介面互動(dòng)並透過檔案系統(tǒng)把音樂檔案載入,讓DSP端可以播放. 兩個(gè)處理器之間,需要設(shè)計(jì)彼此溝通的介面機(jī)制,隨著應(yīng)用情境的多元化,在設(shè)計(jì)上的複雜度也會(huì)相對(duì)增加.

而aSMP與vSMP的優(yōu)點(diǎn)在於上面的主處理器與Companion處理器採用同樣的核心技術(shù),有一樣的指令集,共用同樣的Memory Management記憶體管理與L2 Cache共用. 以vSMP來說, Companion處理器跟主處理器有各自的L1 Cache,但共用同樣的L2 Cache與Memory Management,因此當(dāng)Linux Kernel在上面運(yùn)作時(shí),包括4GB的記憶體分頁,應(yīng)用程式的多工執(zhí)行環(huán)境,就能在有開啟Linux Kernel CPU Up/Down HotPlug的機(jī)制下無縫銜接,且上面運(yùn)作的Task能跨不同的處理器進(jìn)行Task Migration,無需解決跨到不同處理器與映射到不同記憶體空間的複雜問題.

談到這,不論是aSMP或是vSMP都有其擁護(hù)者,並能充分發(fā)揮Linux Kernel CPU Up/Down HotPlug的特性,也都各自具備相當(dāng)?shù)陌l(fā)展?jié)摿? 但要能讓這樣的設(shè)計(jì)充分發(fā)揮效益,還有一些在核心與排程機(jī)制上,需要優(yōu)化的空間. 以目前Linux Kernel的Tasks排程機(jī)制實(shí)作來說,並沒有考慮到每個(gè)處理器具備不同運(yùn)算能力的條件,就算是對(duì)處理器有Load Weight的計(jì)算,也是以該處理器所負(fù)荷的排程單元Load Weight來做統(tǒng)計(jì),只能反映出哪個(gè)處理器目前排程單元的負(fù)荷,但並不能根據(jù)個(gè)別處理器的效能,而給予合理的排程負(fù)荷計(jì)算參數(shù). 因此,如果有一個(gè)aSMP架構(gòu)的處理器動(dòng)態(tài)把時(shí)脈降低,而這處理器又跟其他運(yùn)算能力較高的處理器一起進(jìn)行Tasks排程,就有可能讓重要的Task被放到運(yùn)算能力較低,且當(dāng)下負(fù)荷又已經(jīng)過重的處理器上執(zhí)行,而導(dǎo)致在產(chǎn)品端的執(zhí)行效果不如預(yù)期.

本文,會(huì)把焦點(diǎn)放在排程機(jī)制的介紹上,有關(guān)產(chǎn)品上的實(shí)作與技術(shù)問題,開發(fā)者可以根據(jù)自己所面對(duì)的平臺(tái)與對(duì)於核心排程技術(shù)的了解,來進(jìn)行設(shè)計(jì). 本文主要參考的Linux核心有 2.6.10, 2.6.38.6 與 3.0.4,但由於Linux核心仍在持續(xù)的演進(jìn)中,若你使用的版本跟筆者不同,還請(qǐng)以你手中的Linux Kernel Source Code為依據(jù). 最後,所有的內(nèi)容筆者會(huì)盡力確保資訊的正確性,若有不足之處,還請(qǐng)不吝指教.

Linux Kernel 的排程機(jī)制.

如下圖所示,在linux Kernel 2.4 時(shí),多核心的架構(gòu)下是共用同一個(gè)Task Queue,且採用O(N)排程算法,在排程效率與對(duì)多核心的支援是相對(duì)較弱的.

在linux Kernel 2.6 時(shí),每個(gè)處理器都有兩個(gè)Task Queue,當(dāng)位於Active Task Queue的行程執(zhí)行完畢,就會(huì)依據(jù)行程的優(yōu)先級(jí),計(jì)算Priority與Time Slice,然後放到Expired Task Queue中,當(dāng)Active Task Queue執(zhí)行完畢後,就會(huì)Switch這兩個(gè)Task Queue,讓經(jīng)過計(jì)算後的新Active Task Queue可以立刻加入排程.


此時(shí)Linux Kernel的核心排程採用的是 O(1)排程算法,我們可以參考Linux Kernel 2.6.10的原始碼,首先,依據(jù)O(1)的概念,Task Struct(如下所示)中的run_list會(huì)以Link List方式把同一個(gè)Task Priority 的行程串起來.

struct task_struct {

volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */

struct thread_info *thread_info;

atomic_t usage;

unsigned long flags;    /* per process flags, defined below */

unsigned long ptrace;

int lock_depth;         /* Lock depth */

int prio, static_prio;

struct list_head run_list;

…..

}

而結(jié)構(gòu)list_head的宣告如下所示,

struct list_head {

struct list_head *next, *prev;

};

在每個(gè)Task被產(chǎn)生時(shí),會(huì)透過巨集INIT_LIST_HEAD初始化Task的run_list,把Link List的頭尾 next/prev都指向自己,而在Task被排入執(zhí)行時(shí)就會(huì)透過函式enqueue_task執(zhí)行巨集list_add_tail把Task的Link List放到對(duì)應(yīng)的Priority Link List 結(jié)尾,而在Task被移出執(zhí)行時(shí),就會(huì)透過函式dequeue_task執(zhí)行巨集list_del,把Task的Link List從對(duì)應(yīng)的Priority  Link List移出. 執(zhí)行的概念如下圖所示,

對(duì)同一個(gè)Task Priority來說,被排定執(zhí)行的Task就會(huì)透過 run_list Link List串連起來,依序執(zhí)行,並且在移除執(zhí)行時(shí),從Link List Node中移出.

參考kernel/sched.c中的原始碼, Priority Array會(huì)根據(jù)目前140個(gè)Priority層級(jí) (MAX_PRIO=140),幫每個(gè)Task Priority都配置一個(gè)Link List的 Queue Head,並且用unsigned long針對(duì)這140個(gè)Priority層級(jí),透過Bits為0或1,來表示該Task Priority層級(jí)是否有需要被執(zhí)行的Task在排程等待執(zhí)行中,相關(guān)代碼如下所示

#define BITMAP_SIZE ((((MAX_PRIO+1+7)/8)+sizeof(long)-1)/sizeof(long))

struct prio_array {

unsigned int nr_active;

unsigned long bitmap[BITMAP_SIZE];

struct list_head queue[MAX_PRIO];

};

如下圖所示,以目前140bits的Task Priroty來說,會(huì)透過五個(gè)unsigned long,來表示完整的140bits.

執(zhí)行時(shí)期,再透過巨集sched_find_first_bit (這部份會(huì)依據(jù)平臺(tái)以處理器優(yōu)化的指令集來處理),針對(duì)這140bits,找出第一個(gè)目前有Task在其中等待執(zhí)行的Task Priority.

整體運(yùn)作的概念,如下圖所示

雖然有了O(1)的實(shí)作,而且基於這實(shí)作,也確實(shí)可以很快讓排程機(jī)制很有效率的運(yùn)作,然而處理器雖然是具備多工的能力,但真實(shí)的情況下,一個(gè)處理器同一時(shí)間只能執(zhí)行一個(gè)Task,也就是說,在不考慮觸發(fā)中斷與重新觸發(fā)排程機(jī)制的情況下,在當(dāng)前Task的Time Slice尚未執(zhí)行完畢前,其他的Task是沒有機(jī)會(huì)被執(zhí)行的. 也因此,在Linux Kernel 2.6.23之後,Linux核心採用了新的 CFS(Completely Fair Scheduler)排程機(jī)制(作者為Ingo Molnar),希望可以基於排程的改善,盡可能的讓每個(gè)不同等級(jí)的Task可以更公平的分配到處理器的時(shí)間,基於RB Tree的概念,可以讓越少被執(zhí)行到的Task,有更高的優(yōu)先機(jī)率被處理器執(zhí)行到,避免優(yōu)先級(jí)較低的Task,被延遲較久的時(shí)間才得到被處理器執(zhí)行的機(jī)會(huì).

參考Linux Kernel 3.0.4中有關(guān)CFS的Design文件 (路徑在Documentation/scheduler/sched-design-CFS.txt),作者試著用一句 "CFS basically models an "ideal, precise multi-tasking CPU" on real hardware." 來簡要說明CFS 80%設(shè)計(jì)精神 .所謂理想的多工處理器,就是當(dāng)處理器有100%的能力,且系統(tǒng)中有兩個(gè)Tasks正在運(yùn)作,這兩個(gè)Tasks可以各自取得處理器50%的執(zhí)行能力,並被處理器平行執(zhí)行,也就是說任意時(shí)間間隔來看(例如,隨機(jī)取10ms間隔),這兩個(gè)Tasks都是分配到處理器50%的執(zhí)行能力.

以實(shí)際的處理器運(yùn)作行為來說,一個(gè)處理器同一時(shí)間只能執(zhí)行一個(gè)Task,當(dāng)這個(gè)Task Time-Slice結(jié)束後,會(huì)透過Context-Switch切換下一個(gè)Task進(jìn)來執(zhí)行,當(dāng)系統(tǒng)中的Tasks個(gè)數(shù)便多時(shí),在特定的時(shí)間間隔來看,處理器等於執(zhí)行特定Task的時(shí)間較長,而對(duì)於優(yōu)先級(jí)較低的Task,卻可能在該區(qū)間中都沒有得到執(zhí)行權(quán). 換個(gè)角度來說,Tasks有優(yōu)先級(jí)也有Time Slice長度的區(qū)別,假設(shè)Task #A Time Slice長度為10ms,以總Tasks數(shù)量與總Time Slice長度來說,會(huì)取得處理器5%的執(zhí)行時(shí)間,最理想的狀況是,我們?cè)谌我庖幻氲拈g隔來看,Task#A都可以分到處理器5%的執(zhí)行時(shí)間,但在現(xiàn)實(shí)的狀況來說,在任意一秒的間隔來看,只要有優(yōu)先級(jí)高的Task被排程到,且該Task分配到的Time Slice較長,就會(huì)讓Task#A呈現(xiàn)在一個(gè)不公平的時(shí)間分配中. 雖然,用較長的時(shí)間來看一定是公平的,但CFS希望解決的就是讓目前處理器執(zhí)行的機(jī)制上,可以盡可能的做到即時(shí)的公平.

因此,CFS導(dǎo)入了"Virtual Runtime"的概念(單位為nanosec),根據(jù)目前總共的Tasks數(shù)量,用以作為選擇下一個(gè)執(zhí)行Task Time Slice的依據(jù).例如,CFS會(huì)選擇目前p->se.vruntime值最低的Task來執(zhí)行(也就是距離上一次被執(zhí)行時(shí)間間隔最久的Task),會(huì)在每一次觸動(dòng)排程機(jī)制時(shí),盡可能的達(dá)到"理想的多工處理器"的目標(biāo).

然而,在實(shí)際Linux Kernel的Time Slice與Priority來看,CFS的概念還必須要考慮到依據(jù)每個(gè)Task的Nice Value所計(jì)算的Time Slice長度不一定每個(gè)Task都一切,而且每個(gè)Task的Real-Time Priority值也不盡相同,有關(guān)Task 計(jì)算Time Slice與Real-Time Priority的說明,會(huì)另外在下一段文章中介紹.

每次Scheduler Tick觸發(fā)時(shí),所進(jìn)行的動(dòng)作

不論是O(N),O(1)與CFS,系統(tǒng)的排程都是基於固定的Scheduling Tick來觸發(fā),也因此,每一次的Scheduling Tick觸發(fā),都攸關(guān)Task執(zhí)行的週期與是否要進(jìn)行Task的切換,也是排程機(jī)制核心的部分.

Linux Kernel的Scheduling機(jī)制,會(huì)根據(jù)編譯核心時(shí)配置的HZ值來決定,一般來說是每1/100秒或每1/1000秒觸發(fā)一次Scheduling Tick,讓系統(tǒng)的排程機(jī)制可以去計(jì)算每個(gè)Task執(zhí)行的時(shí)間,並藉此決定是不是要進(jìn)行Task Context-Switch.

以支援CFS的Linux Kernel 2.6.38.6來說,每次觸發(fā)Scheduling Tick都會(huì)執(zhí)行下述的動(dòng)作

1,取得目前的CPU ID,與目前CPU的 RunQueue指標(biāo),與目前RunQueue中正在執(zhí)行的Curr Task指標(biāo)

2,呼叫 sched_clock_tick (實(shí)作在kernel/sched_clock.c中),這函式需在有設(shè)定Unstable CPU Clock時(shí)才有作用,如果全域變數(shù)sched_clock_stable為1,這個(gè)函式就會(huì)不作用直接返回.(ARM環(huán)境中,這個(gè)變數(shù)並無作用.)

3,取得 RunQueue SpinLock

4,呼叫update_rq_clock (實(shí)作在kernel/sched.c中), 若runQueue沒有設(shè)定skip_clock_update,這函式會(huì)更新RunQueue的Clock值.並呼叫函式update_rq_clock_task, 更新RunQueue clock_task值,clock_task會(huì)把前後兩次RunQueue Clock的Delta值,減去處理器進(jìn)行Soft-IRQ與Hardware-IRQ的時(shí)間差值後,對(duì)clock_task進(jìn)行累加. 可用以反應(yīng)出RunQueue中Task實(shí)際被處理器執(zhí)行的時(shí)間累加值.

5,呼叫update_cpu_load_active,會(huì)透過函式update_cpu_load在每次Tick觸發(fā)時(shí),更新RunQueue cpu_load Array,用以反應(yīng)目前RunQueue所在的處理器負(fù)載.

6,執(zhí)行目前Task 所在Class的Tick函式(ex, curr->sched_class->task_tick),若Current Task是屬於Fair Class,就會(huì)呼叫task_tick_fair (實(shí)作在kernel/sched_fair.c中). 依序若Current Task為Stop/Real-Time/Idle Scheduling Class就會(huì)呼叫函式 task_tick_stop/ task_tick_rt/ task_tick_idle.

7,釋放RunQueue SpinLock

8,呼叫perf_event_task_tick (若編譯時(shí),沒設(shè)定CONFIG_PERF_EVENTS,這函式會(huì)是inline的空函式),主要用以支援核心處理器效能的監(jiān)聽動(dòng)作.

9,在SMP架構(gòu)下,會(huì)呼叫函式idle_cpu,用以更新RunQueue的idle_at_tick值,為1表示目前CPU RunQueue執(zhí)行的是Idle Task,反之為0,則非Idle Task. 可供判斷目前這RunQueue所在處理器是否處於閒置的狀態(tài).

10,在SMP架構(gòu)下,會(huì)呼叫trigger_load_balance, 若已達(dá)到RunQueue設(shè)定下一次觸發(fā)Load Balance的jiffies時(shí)間值(next_balance),就會(huì)觸發(fā)Scheduling Softare IRQ,並在Software IRQ中透過函式rebalance_domains,執(zhí)行Scheduling Domain Load Balance動(dòng)作.若未達(dá)RunQueue下一次觸發(fā)Load Balance的jiffies時(shí)間值,且在編譯核心時(shí)有設(shè)定CONFIG_NO_HZ (NO HZ可支援處理器執(zhí)行Idle Task時(shí),停止系統(tǒng)的Scheduling Tick,可在進(jìn)入pm_idle時(shí),減少Timer中斷觸發(fā)(例如HZ=100,每秒就有一百次Tick Timer中斷觸發(fā)),減少系統(tǒng)被觸發(fā)醒來的機(jī)會(huì).),就會(huì)呼叫函式nohz_balancer_kick,Kick一個(gè)處理器進(jìn)行No Hz Load Balance.被Kick選擇為進(jìn)行No Hz Load Balance的處理器所屬RunQueue的nohz_balance_kick值會(huì)設(shè)定為1.

以上總結(jié)每次觸發(fā)Scheduling Tick時(shí),主要進(jìn)行的工作內(nèi)容.接下來我們?cè)偻高^pick_next_task,看在目前系統(tǒng)支援四種Scheduling Class的排程下,一個(gè)Task是如何被撿選出來執(zhí)行的.

pick_next_taskScheduling Class對(duì)排程的影響

檢視CFS對(duì)不同Scheduling Class行為最好的入口就是函式pick_next_task (實(shí)作在 kernel/sched.c中),在函式pick_next_task中會(huì)執(zhí)行如下的流程找到下一個(gè)要被載入執(zhí)行的Task

1,檢查目前總Runnable Task數(shù)量是否跟放在Fair Class中的數(shù)量一致 (rq->nr_running == rq->cfs.nr_running),若成立,就執(zhí)行執(zhí)行Fair Scheduling Class的pick_next_task抓取下一個(gè)執(zhí)行的Task

2,若1不成立,就會(huì)執(zhí)行 for_each_class(class) {….}迴圈,可以參考如下define宣告

#define sched_class_highest (&stop_sched_class)

#define for_each_class(class) \

for (class = sched_class_highest; class; class = class->next)

目前優(yōu)先級(jí)最高的Scheduling Class為 Stop-Task Scheduling Class,而在函式pick_next_task中,就會(huì)依序執(zhí)行 Stop-Task, Real-Time,Fair 與 Idle-Task Scheduling Class所實(shí)作的pick_next_task函式,先找到的Task就會(huì)由函式pick_next_task回傳. 也因此,當(dāng)有Task存在 Stop-Task或是Real-Time Scheduling Class中時(shí),這時(shí)屬於Fair Scheduling Class中的Task執(zhí)行的優(yōu)先級(jí)就會(huì)降低.

總結(jié)運(yùn)作的概念,可以參考下圖所示

CSF RBTREE(Red-Black Tree) – O(Log2(N))

CFS挑選Task的機(jī)制為RB-Tree,概念如下圖所示,包括Task或是Task Group,都會(huì)是RBTree中的一個(gè)Node支點(diǎn),每次選擇Task來執(zhí)行時(shí),都會(huì)選擇左邊目前Virtual RunTime最低的Task(也就是最久沒有被執(zhí)行到的Task)來執(zhí)行,雖然Virtual RunTime會(huì)根據(jù)Task的Priority而有增長的差異,例如:Nice Priority低的Task Virutal RunTime會(huì)增長的比較快,相對(duì)佔(zhàn)據(jù)處理器執(zhí)行的時(shí)間就會(huì)比較短,反之,Nice Priority 高的Task會(huì)因?yàn)閂irutal RunTime增長的慢,而得到相對(duì)比較多實(shí)際的處理器執(zhí)行時(shí)間. 但在理想的情況下,如果大家都是Nice 0的情況,基於CFS RBTree的機(jī)制,可以讓多數(shù)的Task能相對(duì)均勻的得到處理器的執(zhí)行時(shí)間,而避免有特定的Task等待比較久的時(shí)間沒有得到處理器的執(zhí)行機(jī)會(huì).

對(duì)CFS與RBTree建立基本的概念後,接下來讓我們進(jìn)一步的探討處理器排程RunQueue的資料結(jié)構(gòu),更清楚的掌握排程運(yùn)作的內(nèi)涵.

處理器的RunQueue

在目前的Linux Kernel中,每個(gè)處理器都會(huì)配置一個(gè)RunQueue,而從了解RunQueue的資料結(jié)構(gòu)著手,我們能對(duì)排程的運(yùn)作有更清楚的藍(lán)圖,進(jìn)而在程式碼的閱讀上,可以更加清晰. 在本節(jié),筆者會(huì)把RunQueue的資料結(jié)構(gòu)struct rq (宣告在 kernel/sched.c中)進(jìn)行介紹,讓各位可以知道核心排程是基於哪些參數(shù)進(jìn)行運(yùn)作,這參數(shù)的內(nèi)容比較繁瑣,建議可以先有個(gè)概念,若有興趣進(jìn)一步探究的,再回到本節(jié)即可.

如下所示,處理器RunQueue主要包含以下的參數(shù)與對(duì)應(yīng)的說明

參數(shù)名稱 說明
raw_spinlock_t lock 用以作為RunQueue的SpinLock
unsigned long nr_running 用以記錄目前處理器RunQueue中執(zhí)行的Task數(shù)量
unsigned long cpu_load[] cpu_load主要用以表示處理器的負(fù)載,目前這個(gè)Array大小為5 (定義在CPU_LOAD_IDX_MAX),在每個(gè)處理器的RunQueue中都會(huì)有對(duì)應(yīng)到該處理器的cpu_load參數(shù)配置,在每次處理器觸發(fā)Scheduler Tick時(shí), 都會(huì)呼叫函式update_cpu_load_active,進(jìn)行cpu_load的更新.

在系統(tǒng)初始化時(shí),會(huì)呼叫函式sched_init把RunQueue的cpu_load Array初始化為0.

了解cpu_load Array更新最好的方式應(yīng)該是透過函式update_cpu_load (實(shí)作在kernel/sched.c),基於這函式的實(shí)作,我們可以把cpu_load Array每個(gè)數(shù)值公式說明如下.

cpu_load[0]會(huì)直接等於RunQueue中l(wèi)oad.weight的值.

cpu_load[1]=(cpu_load[1]*(2-1)+cpu_load[0])/2

cpu_load[2]=(cpu_load[2]*(4-1)+cpu_load[0])/4

cpu_load[3]=(cpu_load[3]*(8-1)+cpu_load[0])/8

cpu_load[4]=(cpu_load[4]*(16-1)+cpu_load[0])/16

當(dāng)呼叫函式this_cpu_load時(shí),所傳回的 CPU Load值是cpu_load[0] (也就是會(huì)等於RunQueue中的load.weight).而在進(jìn)行CPU Blance或Migration時(shí),就會(huì)呼叫函式source_load與target_load取得對(duì)應(yīng)處理器與對(duì)應(yīng)cpu_load index值,來進(jìn)行計(jì)算.

unsigned long last_load_update_tick 用以記錄最後一次更新CPU Load的時(shí)間值. (會(huì)把當(dāng)時(shí)的jiffies值儲(chǔ)存到這變數(shù)).
ifdef CONFIG_NO_HZ
u64 nohz_stamp 在linux Kerel 2.6.38.6中,這個(gè)參數(shù)沒有作用.
unsigned char nohz_balance_kick 初始值為0,用在當(dāng)NO Hz機(jī)制有開啟時(shí)的CFS Fair Scheduling的Load Balance機(jī)制.可參考kernel/sched_fair.c 中的函式trigger_load_balance實(shí)作.

在SMP架構(gòu)下,且有設(shè)置NO HZ時(shí),會(huì)在每次Scheduling Tick觸發(fā)時(shí),由函式scheduler_tick(in kernel/sched.c)呼叫trigger_load_balance,再呼叫函式nohz_balancer_kick,Kick一個(gè)處理器進(jìn)行No Hz Load Balance.而被選擇進(jìn)行No Hz Load Balance工作處理器的RunQueue中的nohz_balance_kick數(shù)值會(huì)設(shè)定為1.

unsigned int skip_clock_update 如果skip_clock_update的值被設(shè)定為1,此時(shí)透過函式update_rq_clock要進(jìn)行RunQueue Clock值的更新時(shí),就會(huì)直接返回.(每次呼叫update_rq_clock函式,都會(huì)讓RunQueue中的Clock值等於sched_clock_cpu的返回值.
struct load_weight load 參考include/linux/sched.h中struct load_weight的宣告如下

struct load_weight {

unsigned long weight, inv_weight;

};

RunQueue中的load->weight值,會(huì)是目前所執(zhí)行的schedule entity的load->weight的總和,也就是說,RunQueue的load->weight越高,也表示所負(fù)責(zé)的排程單元load->weight總合越高,表示處理器所負(fù)荷的執(zhí)行單元也越重.

unsigned long nr_load_updates 在每次Scheduler Tick中呼叫update_cpu_load時(shí),這個(gè)值都會(huì)加一,可以用來反應(yīng)目前CPU Load更新的次數(shù).
u64 nr_switches 用來累加處理器進(jìn)行Context Switch的次數(shù),會(huì)在函式schedule呼叫時(shí)進(jìn)行累加,並可以透過函式nr_context_switches統(tǒng)計(jì)目前所有處理器總共的Context Switch次數(shù). 或是可以透過查看檔案/proc/stat中的ctxt欄位得知目前整個(gè)系統(tǒng)觸發(fā)Context Switch的總數(shù).
struct cfs_rq cfs 為CFS Fair Scheduling Class的 RunQueue
struct rt_rq rt 為Real-Time Scheduling Class的RunQueue
ifdef CONFIG_FAIR_GROUP_SCHED (用以支援可以Group CFS Tasks的機(jī)制.)
struct list_head leaf_cfs_rq_list 在有設(shè)置Fair Group Scheduling的環(huán)境下,會(huì)基於原本CFS RunQueue與包含有若干Task的Group所成的排程集合,也就是說當(dāng)有一個(gè)Group A,其下有20個(gè)Tasks,而這Group A就會(huì)有自己的CFS RunQueue用來排程自己所屬的Tasks,而屬於這Group A的Tasks所使用到的處理器時(shí)間,就會(huì)以這Group A總體所分得的時(shí)間為上限. 基於 Cgroup的 Fair Group Scheduling架構(gòu),可以創(chuàng)造出有階層性的Tasks組織,根據(jù)不同Tasks的功能群組化,在配置給該群組對(duì)應(yīng)的處理器資源,讓屬於該群組下的Tasks可以透過RunQueue機(jī)制排程,使用屬於該群組所配置到的資源.

這個(gè)變數(shù)主要是用來管理CFS RunQueue List,操作上可以透過函式list_add_leaf_cfs_rq把一個(gè)Group CFS RunQueue加入到List中,或透過函式list_del_leaf_cfs_rq把一個(gè)Group CFS RunQueue移除. 並可透過for_each_leaf_cfs_rq把一個(gè) RunQueue上的所有l(wèi)eaf cfs_rq走過一遍.

ifdef CONFIG_RT_GROUP_SCHED  (用以支援可以Group Real-Time Tasks的機(jī)制.
struct list_head leaf_rt_rq_list 類似於leaf_cfs_rq_list所扮演的角色,只是這是針對(duì)屬於Real-Time的Task,在實(shí)際操作上可以透過函式list_add_leaf_rt_rq,list_del_leaf_rt_rq或巨集for_each_leaf_rt_rq.
unsigned long nr_uninterruptible 一般來說,Linux Kernel的Task狀態(tài)可以為TASK_RUNNING, TASK_INTERRUPTIBLE (Sleep) , TASK_UNINTERRUPTIBLE (Deactivate Task,此時(shí)Task會(huì)從RunQueue移除)或TASK_STOPPED. (在此先忽略Trace,Zombie,Dead..etc). 透過這個(gè)變數(shù)會(huì)統(tǒng)計(jì)目前RunQueue中有多少Task屬於TASK_UNINTERRUPTIBLE的狀態(tài).當(dāng)呼叫函式activate_task時(shí),會(huì)把nr_uninterruptible值減一,並透過函式enqueue_task把對(duì)應(yīng)的Task依據(jù)所在的Scheduling Class放到對(duì)應(yīng)的RunQueue中,並把目前RunQueue的nr_running值加一.
struct task_struct *curr, *idle, *stop 為指向三個(gè)Task的指標(biāo). (Current Task,Idle Task與最高執(zhí)行級(jí)的Stop Task).

curr : 指向目前處理器正在執(zhí)行的Task

idle: 指向?qū)凫禝dle-Task Scheduling Class的Idle Task.

stop: 指向目前最高等級(jí)屬於 Stop-Task Scheduling Class的Task

unsigned long next_balance 基於處理器的jiffies值,用以記錄下次進(jìn)行處理器Balancing的時(shí)間點(diǎn).
struct mm_struct *prev_mm 用以儲(chǔ)存Context-Switch發(fā)生時(shí),前一個(gè)Task的Memory Management結(jié)構(gòu),並可用在函式finish_task_switch中,透過函式mmdrop釋放前一個(gè)Task的記憶體資源.
u64 clock 用以記錄目前RunQueue的Clock值,基本上該值會(huì)等於透過sched_clock_cpu(cpu_of(rq))的回傳值,並會(huì)在每次呼叫scheduler_tick時(shí)透過函式update_rq_clock更新目前RunQueue clock值.

在實(shí)作部分,函式sched_clock_cpu會(huì)透過sched_clock_local或ched_clock_remote取得對(duì)應(yīng)的sched_clock_data,而處理器的sched_clock_data值,會(huì)透過函式sched_clock_tick在每次呼叫scheduler_tick時(shí)進(jìn)行更新.

補(bǔ)充….

從函式update_rq_clock的實(shí)作來說,其實(shí)

delta = sched_clock_cpu(cpu_of(rq)) – rq->clock;

rq->clock += delta;

可以直接寫成

rq->clock= sched_clock_cpu(cpu_of(rq));

u64 clock_task 這個(gè)值會(huì)等於從上一次更新RunQueue Clock時(shí),到這次要更新RunQueue Clock時(shí)的時(shí)間間隔 Delta 減去在這段時(shí)間中,處理器用來進(jìn)行Soft-IRQ與Hardware-IRQ的時(shí)間差值的累加.

簡要來說,這個(gè)值會(huì)透過函式update_rq_clock_task進(jìn)行更新,用來反應(yīng)出RunQueue中Task實(shí)際執(zhí)行時(shí)所佔(zhàn)用到的處理器時(shí)間Clock累加值.

atomic_t nr_iowait 用以記錄目前RunQueue中有多少Task處於等待I/O的Sleep狀態(tài).

在實(shí)際的使用上,例如當(dāng)Driver接收來自Task的調(diào)用,但處於等待I/O回覆的階段時(shí),為了充分利用處理器的執(zhí)行資源,這時(shí)就可以在Driver中呼叫函式io_schedule,此時(shí)就會(huì)把目前RunQueue中的nr_iowait加一,並設(shè)定目前Task的in_iowait為1,然後觸發(fā)Scheduling讓其他Task有機(jī)會(huì)可以得到處理器執(zhí)行時(shí)間.

ifdef CONFIG_SMP
struct root_domain *rd Root Domain是基於多核心架構(gòu)下的機(jī)制,會(huì)由RunQueue結(jié)構(gòu)記住目前採用的Root Domain,其中包括了目前的CPU Mask(包括 Span, Online 與 RT Overload), Reference Count 跟cpupri.

當(dāng)Root Domain有被RunQueue參考到時(shí), refcount就會(huì)加一,反之就是減一. 而CPU Mask Span表示RunQueue可掛上的CPU Mask, Online為RunQueue目前已經(jīng)掛上排程的CPU Mask,最後的RT Overload(rto_mask)代表的是這個(gè)RunQueue可在對(duì)應(yīng)的rto_mask CPU上執(zhí)行Real-Time Task. 可以參考檔案kernel/sched_rt.c中函式pull_rt_task的實(shí)作,當(dāng)一個(gè)RunQueue中屬於Real-Time的Task已經(jīng)執(zhí)行完畢(也就是rq->rt.rt_nr_running==0),就會(huì)透過函式pull_rt_task從該RunQueue中屬於rto_mask CPU Mask可以執(zhí)行的處理器上,找出是否有一個(gè)處理器有大於一個(gè)以上的Real-Time Task,若有就會(huì)轉(zhuǎn)到目前這個(gè)執(zhí)行完畢Real-Time Task的處理器上.

而cpupri不同於Task本身有區(qū)分140個(gè)(0-139)Task Priority (0-99為RT Priority 而 100-139為Nice值 -20-19). CPU Priority本身有102個(gè)Priority (包括,-1 為Invalid,0為Idle,1為Normal,2-101對(duì)應(yīng)到Real-Time Priority 0-99).參考函式convert_prio, Task Priority如果是 140就會(huì)對(duì)應(yīng)到CPU Idle,如果是大於等於100就會(huì)對(duì)應(yīng)到CPU Normal,若是Task Priority介於0-99之間,就會(huì)對(duì)應(yīng)到CPU Real-Time Priority 101-2之間.) 在實(shí)際的操作上,例如可以透過函式cpupri_find帶入一個(gè)要插入的Real-Time Task,此時(shí)就會(huì)依據(jù)cpupri中pri_to_cpu選擇一個(gè)目前執(zhí)行Real-Time Task且該Task的優(yōu)先級(jí)比目前要插入的Task更低的處理器,並透過CPU Mask(lowest_mask)返回目前可以選擇的處理器Mask.實(shí)作的部份可以參考檔案kernel/sched_cpupri.c.

在初始化的過程中,會(huì)透過函式sched_init呼叫函式init_defrootdomain,對(duì)Root Domain與 CPU Priority機(jī)制進(jìn)行初始化.

struct sched_domain *sd Schedule Domain是基於多核心架構(gòu)下的機(jī)制.每個(gè)處理器都會(huì)有一個(gè)基礎(chǔ)的Scheduling Domain, Scheduling Domain可以有階層性的架構(gòu),透過parent可以找到上一層的Domain,或是透過child找到下一層的 Domain (NULL表示結(jié)尾.).並可透過span欄位,表示這個(gè)Domain所能涵蓋的處理器範(fàn)圍. 通常Base Domain會(huì)涵蓋系統(tǒng)中所有處理器的個(gè)數(shù),而Child Domain所能涵蓋的處理器個(gè)數(shù)不超過它的Parent Domain. 而當(dāng)在進(jìn)行Scheduling Domain 中的Task Balance時(shí),就會(huì)以該Domain所能涵蓋的處理器為最大範(fàn)圍.

同時(shí),每個(gè)Schedule Domain都會(huì)包括一個(gè)或一個(gè)以上的CPU Groups (結(jié)構(gòu)為struct sched_group),並透過next變數(shù)把CPU Groups串連在一起(成為一個(gè)單向的Circular linked list),每個(gè)CPU Group都會(huì)有變數(shù)cpumask來定義這個(gè)CPU Group所涵蓋的處理器範(fàn)圍.並且CPU Group所包括的處理器範(fàn)圍,必需涵蓋在所屬的Schedule Domain處理器範(fàn)圍中.

當(dāng)進(jìn)行Scheduling Domain的Balancing時(shí),會(huì)以其下的CPU Groups為單位,根據(jù)cpu_power (會(huì)是該Group所涵蓋的處理器Tasks Loading的總和)來比較不同的CPU Groups的負(fù)荷,以進(jìn)行Tasks的移動(dòng),達(dá)到Balancing的目的.

在有支援SMP的架構(gòu)下,會(huì)在函式sched_init中,呼叫open_softirq,註冊(cè) SCHED_SOFTIRQ Software IRQ與其對(duì)應(yīng)的 Callback函式 run_rebalance_domains. 並會(huì)在每次呼叫函式scheduler_tick時(shí),透過函式trigger_load_balance確認(rèn)是否目前的jiffies值已經(jīng)大於RunQueue下一次要觸發(fā)Load Balance的next_balance時(shí)間值,並透過函式raise_softirq觸發(fā)SCHED_SOFTIRQ Software IRQ. 在Software IRQ觸發(fā)後,就會(huì)呼叫函式run_rebalance_domains,並在函式rebalance_domains中,進(jìn)行後續(xù)處理器上的Scheduling Domain Load Balance動(dòng)作.

有關(guān)Scheduling Domain進(jìn)一步的內(nèi)容,也可以參考Linux Kernel文件 Documentation/scheduler/sched-domains.txt.

unsigned long cpu_power 在SMP架構(gòu)下,這值可用以表示目前RunQueue所在處理器的計(jì)算能力,在函式sched_init初始化時(shí),會(huì)把這值設(shè)定為SCHED_LOAD_SCALE (=Nice 0的Load Weight=1024).並可透過函式update_cpu_power (in kernel/sched_fair.c)更新這個(gè)值.

處理器的CPU Power值越大,表示這個(gè)處理器有更多的計(jì)算能力,可以分擔(dān)CPU Power值越小的處理器上的Tasks任務(wù).

unsigned char idle_at_tick 這值會(huì)等於函式idle_cpu的返回值,如果為1表示目前CPU RunQueue中執(zhí)行的為Idle Task. 反之為0,則表示處理器執(zhí)行的不是Idle Task (也就是說處理器正在忙碌中.).
int post_schedule 若這值不為0,表示會(huì)有在Schedule排程動(dòng)作結(jié)束前,要呼叫的收尾函式. (實(shí)作為inline函式post_schedule in kernel/sched.c),目前只有Real-Time Scheduling Class有支援這個(gè)機(jī)制(會(huì)呼叫函式has_pushable_tasks in kernel/sched_rt.c).
int active_balance 當(dāng)RunQueue中此值為1,表示這個(gè)RunQueue正在進(jìn)行Fair Scheduling的Load Balance,此時(shí)會(huì)呼叫stop_one_cpu_nowait暫停該RunQueue所屬處理器的排程,並透過函式active_load_balance_cpu_stop,把Tasks從最忙碌的處理器,移到Idle的處理器上執(zhí)行.
int push_cpu 用以儲(chǔ)存目前進(jìn)入Idle且負(fù)責(zé)進(jìn)行 Load Balance流程的處理器ID. 呼叫的流程為,在呼叫函式schedule時(shí),若該處理器RunQueue的nr_running為0 (也就是目前沒有正在執(zhí)行的Task),就會(huì)呼叫idle_balance,並觸發(fā)後續(xù)Load Balance流程.
struct cpu_stop_work active_balance_work 用以儲(chǔ)存給Stopper Scheduling Class所要執(zhí)行的函式與其對(duì)應(yīng)參數(shù). 會(huì)作為在函式stop_one_cpu_nowait中呼叫函式cpu_stop_queue_work的參數(shù).
int cpu 用以儲(chǔ)存目前運(yùn)作這個(gè)RunQueue的處理器ID.
int online 為1表示目前此RunQueue有在對(duì)應(yīng)的處理器掛上並執(zhí)行.
unsigned long avg_load_per_task 如果RunQueue中目前有Task正在執(zhí)行,這個(gè)值會(huì)等於目前該RunQueue的Load Weight除以目前RunQueue中Task數(shù)目的均值. (rq->avg_load_per_task = rq->load.weight / nr_running;).
u64 rt_avg 這個(gè)值會(huì)由Real-Time Scheduling Class呼叫函式update_curr_rt,用以統(tǒng)計(jì)目前Real-Time Task執(zhí)行時(shí)間的均值,在這函式中會(huì)以目前RunQueue的clock_task減去目前Task執(zhí)行的起始時(shí)間,取得執(zhí)行時(shí)間的Delta值. (delta_exec = rq->clock_task – curr->se.exec_start; ). 在透過函式sched_rt_avg_update把這Delta值跟原本RunQueue中的rt_avg值取平均值. 以運(yùn)作的週期來看,這個(gè)值可反應(yīng)目前系統(tǒng)中Real-Time Task平均被分配到的執(zhí)行時(shí)間值.
u64 age_stamp 這個(gè)值主要在函式sched_avg_update更新,以筆者手中的Linux Kernel 2.6.38.6的實(shí)作來說,當(dāng)RunQueue Clock減去age_stamp大於 0.5秒 (=sched_avg_period),就會(huì)把這值累加0.5秒 (單位都是nanoseconds). 從函式scale_rt_power的實(shí)作來說,age_stamp值離RunQueue Clock越遠(yuǎn),表示total值越大,available值也越大,而函式scale_rt_power返回的div_u64計(jì)算結(jié)果也越大,最終 RunQueue的cpu_power與Scheduling Domain中的Scheduling Group的cpu_power值也就越大. (可參考函式update_cpu_power的實(shí)作).
u64 idle_stamp 這值會(huì)在觸發(fā)Scheduling時(shí),若判斷目前處理器RunQueue沒有正在運(yùn)作的Task,就會(huì)透過函式idle_balance更新這值為為目前RunQueue的clock值.可用以表示這個(gè)處理器是何時(shí)進(jìn)入到Idle的狀態(tài).
u64 avg_idle 會(huì)在有Task運(yùn)作且idle_stamp不為0 (表示前一個(gè)狀態(tài)是在Idle)時(shí)以目前RunQueue的clock減去idle_stamp所計(jì)算出的Delta值為依據(jù),更新這個(gè)值. 可反應(yīng)目前處理器進(jìn)入Idle狀態(tài)的時(shí)間長短.
ifdef CONFIG_IRQ_TIME_ACCOUNTING
u64 prev_irq_time 這值會(huì)透過函式update_rq_clock_task更新,可用以代表處理器執(zhí)行Soft-IRQ與Hardware-IRQ所花的處理器Clock值的累加.s
unsigned long calc_load_update 用以記錄下一次計(jì)算CPU Load的時(shí)間,初始值為目前的jiffies加上五秒與1次的Scheduling Tick的間隔 (=jiffies + LOAD_FREQ,且LOAD_FREQ=(5*HZ+1))
long calc_load_active 會(huì)等於RunQueue中nr_running與nr_uninterruptible的總和.(可參考函式calc_load_fold_active).
ifdef CONFIG_SCHED_HRTICK
ifdef CONFIG_SMP
int hrtick_csd_pending 在函式init_rq_hrtick初始化RunQueue High-Resolution Tick時(shí),此值預(yù)設(shè)為0.

在函式hrtick_start中,會(huì)判斷目前觸發(fā)的RunQueue跟目前處理器所使用的RunQueue是否一致,若是,就直接呼叫函式hrtimer_restart,反之就會(huì)依據(jù)RunQueue中hrtick_csd_pending的值,如果hrtick_csd_pending為0,就會(huì)透過函式__smp_call_function_single讓RunQueue所在的另一個(gè)處理器執(zhí)行rq->hrtick_csd.func 所指到的函式 __hrtick_start. 並等待該處理器執(zhí)行完畢後,才重新把hrtick_csd_pending設(shè)定為1.

也就是說, RunQueue的hrtick_csd_pending是用來作為SMP架構(gòu)下,由處理器A觸發(fā)處理器B執(zhí)行_hrtick_start函式的一個(gè)保護(hù)機(jī)制.而有關(guān)在SMP下如何由一個(gè)處理器觸發(fā)另一個(gè)處理器執(zhí)行函式的機(jī)制,可以參考kernel/smp.c中相關(guān)smp_call_function_xxxxxxx的實(shí)作.s

struct call_single_data hrtick_csd 用以儲(chǔ)存hrtick機(jī)制中,要跨處理器執(zhí)行的函式結(jié)構(gòu).
struct hrtimer hrtick_timer 為High-Resolution Tick的結(jié)構(gòu),會(huì)透過函式hrtimer_init初始化.
ifdef CONFIG_SCHEDSTATS
struct sched_info rq_sched_info 為Scheduling Info.的統(tǒng)計(jì)結(jié)構(gòu),可以參考include/linux/sched.h中的宣告. 例如在每次觸發(fā)Schedule時(shí),呼叫函式schedule_debug對(duì)上一個(gè)Task的lock_depth進(jìn)行確認(rèn)(Fork一個(gè)新的Process 時(shí),會(huì)把此值預(yù)設(shè)為-1就是No-Lock,當(dāng)呼叫Kernel Lock時(shí), 就會(huì)把Current Task的lock_depth加一.),若lock_depth>=0,就會(huì)累加Scheduling Info.的bkl_count值,用以代表Task Blocking的次數(shù).
unsigned long long rq_cpu_time 可用以表示RunQueue中的Task所得到CPU執(zhí)行時(shí)間的累加值.

在發(fā)生Task Switch時(shí),會(huì)透過sched_info_switch呼叫sched_info_arrive並以目前RunQueue Clock值更新Task 的sched_info.last_arrival時(shí)間,而在Task所分配時(shí)間結(jié)束後,會(huì)在函式sched_info_depart中以現(xiàn)在的RunQueue Clock值減去Task的sched_info.last_arrival時(shí)間值,得到的 Delta作為變數(shù)rq_cpu_time的累加值.

unsigned int yld_count 用以統(tǒng)計(jì)呼叫System Call sys_sched_yield的次數(shù).
unsigned int sched_count 可用以統(tǒng)計(jì)觸發(fā)Scheduling的次數(shù). 在每次觸發(fā)Scheduling時(shí),會(huì)透過函式schedule呼叫schedule_debug,呼叫schedstat_inc對(duì)這變數(shù)進(jìn)行累加.
unsigned int sched_goidle 可用以統(tǒng)計(jì)進(jìn)入到Idle Task的次數(shù). 會(huì)在函式pick_next_task_idle中,呼叫schedstat_inc對(duì)這變數(shù)進(jìn)行累加.
unsigned int ttwu_count 用以統(tǒng)計(jì)Wake Up Task的次數(shù).
unsigned int ttwu_local 用以統(tǒng)計(jì)Wake Up 同一個(gè)處理器Task的次數(shù).

至此,有關(guān)RunQueue的結(jié)構(gòu)說明告一段落.

CFSScheduling Classes Scheduling Policies

CFS排程機(jī)制在設(shè)計(jì)時(shí),考慮到排程機(jī)制的彈性,定義了Scheduler Class的機(jī)制,讓排程機(jī)制可以根據(jù)設(shè)計(jì)的需求,延伸不同的排程模組進(jìn)來,每個(gè)新加入的排程機(jī)制都必須要提供Scheduler Class的實(shí)作,結(jié)構(gòu)為 struct sched_class (定義在檔案include/linux/sched.h中). 舉sched_fair.c的實(shí)作來說, Scheduler  Class實(shí)作的宣告如下

static const struct sched_class fair_sched_class = {

.next                   = &idle_sched_class,

.enqueue_task           = enqueue_task_fair,

.dequeue_task           = dequeue_task_fair,

.yield_task             = yield_task_fair,

.check_preempt_curr     = check_preempt_wakeup,

.pick_next_task         = pick_next_task_fair,

.put_prev_task          = put_prev_task_fair,

#ifdef CONFIG_SMP

.select_task_rq         = select_task_rq_fair,

.rq_online              = rq_online_fair,

.rq_offline             = rq_offline_fair,

.task_waking            = task_waking_fair,

#endif

.set_curr_task          = set_curr_task_fair,

.task_tick              = task_tick_fair,

.task_fork              = task_fork_fair,

.prio_changed           = prio_changed_fair,

.switched_to            = switched_to_fair,

.get_rr_interval        = get_rr_interval_fair,

#ifdef CONFIG_FAIR_GROUP_SCHED

.task_move_group        = task_move_group_fair,

#endif

};

其中相關(guān)函式的行為簡述如下

函式名稱 說明
next 會(huì)指向下一個(gè)Scheduling Class,以筆者所採用的Linux Kernel 2.6.38.6而言,Scheduling Class的順序?yàn)?p>

stop_sched_class->rt_sched_class->fair_sched_class->idle_sched_class

enqueue_task 當(dāng)Task屬於Runnable狀態(tài)時(shí),就會(huì)呼叫這個(gè)函式把Task配置到RunQueue RBTree中,進(jìn)行排程動(dòng)作,並呼叫inc_nr_running將RunQueue中nr_running的值加一.(nr_running用以代表目前RunQueue有多少Runnable Task進(jìn)行排程)
dequeue_task 當(dāng)Task不需要執(zhí)行時(shí),就會(huì)呼叫這個(gè)函式把Task從RunQueue RBTree中移除,並呼叫dec_nr_running將RunQueue中nr_running的值減一.
yield_task 用以暫停目前正在執(zhí)行中的Task,如果sysctl_sched_compat_yield有設(shè)定,就會(huì)找出目前RBTree中最右邊的Task(也就是vrruntime最多的Task),讓目前Task的vrruntime值等於最右邊Task值的vrruntime加一(可參考:se->vruntime = rightmost->vruntime + 1),如此在下次排程觸發(fā)時(shí)就會(huì)透過函式put_prev_task把目前的Task放到RBTree的最右邊,也就等同於暫停Task,讓該Task下次被執(zhí)行到的機(jī)會(huì)最低.
check_preempt_curr 用以決定一個(gè)Task是否可以中斷目前正在運(yùn)作的Task,取得執(zhí)行權(quán).以CFS本身的實(shí)作來說 (in sched_fair.c).如果想要取代目前Task的Task本身的Scheduling Policy為 Batch或是Idle時(shí),會(huì)直接返回,不會(huì)用來取代目前正在執(zhí)行中的Task.反之,如果目前正在執(zhí)行中的Task的Scheduling Policy為Idle,就會(huì)直接由所傳入的Task取代目前正在執(zhí)行的Task.
pick_next_task 用以在排程觸發(fā)時(shí),從RunQueue RBTree中,取出符合目前Scheduling邏輯的下一個(gè)要被執(zhí)行的Task.
put_prev_task 用以在排程觸發(fā)時(shí),把上一個(gè)執(zhí)行完畢的Task放到目前RunQueue RBTree中對(duì)應(yīng)的位置.
set_curr_task 這個(gè)函式用以改變Task目前所屬的Scheduling Class與改變Task Group.
task_tick 這是Scheduler的 Timer Tick來源,系統(tǒng)中觸發(fā)的Scheduling Tick會(huì)呼叫這個(gè)函式 (看HZ設(shè)定多少,100就是每秒呼叫這函式100次,1000就是每秒呼叫這函式1000次),

用以讓排程機(jī)制可以決定哪些Task應(yīng)該要配執(zhí)行與哪些Task應(yīng)該要被移出RunQueue.

select_task_rq
(需在SMP架構(gòu)下)
通常用在執(zhí)行一個(gè)新的程序,或是WakeUp一個(gè)Task時(shí),會(huì)根據(jù)目前SMP下每個(gè)處理器的負(fù)荷,決定Task是否要切換到另一個(gè)處理器的RunQueue去執(zhí)行,執(zhí)行時(shí)會(huì)返回最後目標(biāo)處理器的值.

此外,在Linux Kernel SMP的架構(gòu)下,還會(huì)支援如下的函式

函式名稱 說明
pull_task 可用來把Task從一個(gè)RunQueue移到目前的RunQueue來,並設(shè)定Task會(huì)在目前處理器的RunQueue中執(zhí)行.
can_migrate_task 可用來判斷一個(gè)Task可否從另一個(gè)RunQueue移到目前的處理器來執(zhí)行,並返回判斷值.(返回值0就是不適合移動(dòng),1則是表明適合移動(dòng)).

目前有以下三種情況,會(huì)返回失敗.

1,該Task的cpus_allowed bitmap沒有設(shè)定目前的處理器 (也就是說在 CPU BitMap中該Task沒有被設(shè)定可以在目前處理器上能被同意執(zhí)行)

2,目前的Task正在該RunQueue被執(zhí)行中

3,透過函式task_hot,判斷目前的Task是否為該RunQueue與Scheduling Domain中被執(zhí)行時(shí)間較長的Task,基於對(duì)目前執(zhí)行中處理器Cache-Hot的考量,該Task也不會(huì)被同意可以移動(dòng)到目前的處理器.(可以避免Cache Flush,與在新的處理器上要重新進(jìn)行Cache的動(dòng)作.)

有關(guān)的巨集

#define cpu_rq(cpu)             (&per_cpu(runqueues, (cpu)))

#define this_rq()               (&__get_cpu_var(runqueues))

#define task_rq(p)              cpu_rq(task_cpu(p))

#define cpu_curr(cpu)           (cpu_rq(cpu)->curr)

#define raw_rq()                (&__raw_get_cpu_var(runqueues))

sched_class Class中宣告的Method,就是要在Linux Kernel排程中新增一個(gè)Scheduler Class所要實(shí)作的內(nèi)容,讓Linux Kernel可以根據(jù)不同行程的需求,調(diào)度這些在系統(tǒng)中有註冊(cè)的排程模組,讓系統(tǒng)可以達(dá)到預(yù)定的執(zhí)行目標(biāo)與行為.

以Linux Kernel 2.6.38來說,目前共支援4種Scheduler Class,包括

1,Fair Scheduler Class (實(shí)作在檔案 sched_fair.c中):

目前包括有

1.a, SCHED_NORMAL (也稱為SCHED_OTHER): 主要用以排程一般目的的Task.

1.b, SCHED_BATCH: 主要用以讓Task可以延長執(zhí)行的時(shí)間(Time Slice),減少被中斷發(fā)生Task Context-Switch的次數(shù).藉此可以提高 Cache的利用率 (每次Context-Switch都會(huì)導(dǎo)致Cache-Flush). 比較適合用在固定週期執(zhí)行的Batch Jobs任務(wù)主機(jī)上,而不適合用在需要使用者互動(dòng)的產(chǎn)品 (會(huì)由於Task切換的延遲,而感覺到系統(tǒng)效能不佳或是反應(yīng)太慢).

2,Real-Time Scheduler Class (實(shí)作在檔案 sched_rt.c中):

支援Real-Time Task的排程,包括有SCHED_FIFO與SCHED_RR. (SCHED_RR task預(yù)設(shè)的 Time Slice長度為100 msecs.).

3,Idle Task Scheduler Class (實(shí)作在檔案 sched_idletask.c中):

支援SCHED_IDLE,為系統(tǒng)中的Idle Task排程.

4,Stop Scheduler Class (實(shí)作在檔案 sched_stoptask.c中):屬於 Stop-Task Scheduling Class的Task是屬於系統(tǒng)中最高權(quán)限的Task,會(huì)中斷所有任務(wù)的執(zhí)行,並且不會(huì)被其他任務(wù)所中斷.

目前系統(tǒng)中,Scheduling Class的優(yōu)先級(jí)順序?yàn)?Stop-Task > Real-Time > Fair > Idle-Task,開發(fā)者可以根據(jù)自己的設(shè)計(jì)需求,來把所屬的Task配置到不同的Scheduling Class中.

在支援Fair Group Scheduling環(huán)境下,CGroup (Control Group) Scheduler分配TaskGroup的處理器執(zhí)行時(shí)間.

Control Group 提供了Linux核心可以把Tasks進(jìn)行整合/區(qū)分或是依據(jù)子Group進(jìn)行分派的動(dòng)作. 根據(jù)Control Group文件中的定義,一個(gè)CGroup可以關(guān)聯(lián)到屬於1或多組SubSystem中的多個(gè)Tasks.  SubSystem主要是基於CGroup提供的Task Group機(jī)制,扮演類似資源管控的角色,用以設(shè)定調(diào)配每個(gè)Group使用資源的限制. 在支援Control Group的Linux Kernel中,每一個(gè)Task在系統(tǒng)中都會(huì)屬於一個(gè)CGroup與一個(gè)SubSystem,每一個(gè)SubSystem都會(huì)有一個(gè)系統(tǒng)設(shè)定的組態(tài),並配置到對(duì)應(yīng)的CGroup中. 每一個(gè)Hierarchy架構(gòu),都有一個(gè)屬於CGroup虛擬檔案系統(tǒng)的Instance參考.

在系統(tǒng)運(yùn)作時(shí),會(huì)有多個(gè)有從屬關(guān)係的Task Groups同時(shí)運(yùn)作,每一個(gè)Hierarchy都會(huì)屬於這系統(tǒng)中所有Tasks的一部分. 使用者可以透過CGroup虛擬檔案系統(tǒng)中的目錄名稱產(chǎn)生或刪除對(duì)應(yīng)的CGroup,並可以查詢哪個(gè)CGroup有Task被指派,或是列出對(duì)應(yīng)CGroup中所有被指派的Task PIDs. 藉由CGroup的設(shè)計(jì),還可支援基於使用者帳號(hào)(Account)的處理器資源調(diào)配目的.(包括可以限定能使用的處理器或是Memory Node.)

參考 include/linux/cgroup_subsys.h的實(shí)做,目前CGroup包括以下的SubSystem,

1,cpuset (CONFIG_CPUSETS):用以限定指定的Tasks使用的CPU與Memory資源,有關(guān)cpuset的使用可以透過函式sched_setaffinity,sched_getaffinity,set_mempolicy,get_mempolicy,mbind. 例如下述的例子,可以由應(yīng)用程式透過函式sched_setaffinity指定當(dāng)應(yīng)用程式執(zhí)行時(shí),所要使用的處理器編號(hào).

http://fred-zone./2007/10/cpu.html

#include <stdio.h>

#include <stdlib.h>

#include <sched.h>

int main(void) {

cpu_set_t cmask;

unsigned long len = sizeof(cmask);

__CPU_ZERO(&cmask);   /* 初始化 cmask */

__CPU_SET(0, &cmask); /* 指定第一個(gè)處理器 */

/* 設(shè)定自己由指定的處理器執(zhí)行 */

if (!sched_setaffinity(0, len, &cmask)) {

printf("Could not set cpu affinity for current process.\n");

exit(1);

}

return 0;

}

2,cpu_cgroup (CONFIG_CGROUP_SCHED):這選項(xiàng)會(huì)支援Tasks能被群組(Group)化與依據(jù)設(shè)定切割群組佔(zhàn)用的處理器執(zhí)行時(shí)間. 更進(jìn)一步,還可以設(shè)定CONFIG_RT_GROUP_SCHED用以支援群組的 Real-Time排程 (支援包括 SCHED_FIFO SCHED_RR). 與可以設(shè)定CONFIG_FAIR_GROUP_SCHED 用以支援群組的CFS排程機(jī)制(支援包括SCHED_NORMALSCHED_BATCH).

3,cpuacct (CONFIG_CGROUP_CPUACCT):這選項(xiàng)用以支援CPU Accounting Controller機(jī)制,透過CGroup機(jī)制群組Task,並依據(jù)使用者的帳號(hào)(Account)所產(chǎn)生的Task群組來劃分處理器的執(zhí)行時(shí)間.

4,debug (CONFIG_CGROUP_DEBUG):用以支援CGroup的Debug機(jī)制,像是Task Reference Count等統(tǒng)計(jì)資訊.

其它SubSystem還包括 ns (CONFIG_CGROUP_NS),mem_cgroup (CONFIG_CGROUP_MEM_RES_CTLR),devices (CONFIG_CGROUP_DEVICE),freezer (CONFIG_CGROUP_FREEZER),net_cls (CONFIG_NET_CLS_CGROUP),blkio (CONFIG_BLK_CGROUP).

在一個(gè)基於CGroup FileSystem的 Task-Group機(jī)制下,會(huì)統(tǒng)籌100%的處理器資源. 所有的處理器支援會(huì)根據(jù)root_task_group與所屬的子Task-Group的 Weight (=se->load.weight)基於Fair-Scheduling機(jī)制來進(jìn)行處理器資源的分配工作.

舉實(shí)際的例子來說,當(dāng)root_task_group中有十個(gè)Tasks且每個(gè)Task的Weight值為1024,在root_task_group之下又有兩個(gè)子Task Groups且這兩個(gè)子Task Group的Weight也都為1024,此時(shí)這兩個(gè)子Task Group所分配到的處理資源為 1024 / (10*1024 + 1024 + 1024) = 8.33% .

我們可以參考如下操作流程,產(chǎn)生一個(gè)驗(yàn)證Control Group的目錄,並在其中產(chǎn)生cpu目錄,並掛起CPU與CGroup相關(guān)的Device Node.

[root@localhost ~]# mkdir /cgroup

[root@localhost ~]# mount -t tmpfs cgroup_root /cgroup

[root@localhost ~]# mkdir /cgroup/cpu

[root@localhost ~]# mount -t cgroup -ocpu none /cgroup/cpu

[root@localhost ~]# ls /cgroup/cpu

cgroup.clone_children

cgroup.event_control

cgroup.procs

cpu.rt_period_us

cpu.rt_runtime_us

cpu.shares

notify_on_release

release_agent

tasks

[root@localhost ~]#

我們可以從tasks檔案看到目前處理器上所有執(zhí)行的Task Process ID,並從cpu.shares知道目前處理器所分擔(dān)的CPU Load基數(shù).

接下來,我們透過Control Group產(chǎn)生 GroupA與GroupB兩個(gè)Task Group群組,如下所示

[root@localhost cpu]# mkdir GroupA

[root@localhost cpu]# mkdir GroupB

然後透過設(shè)定 cpu.shares,把GroupA的cpu.shares設(shè)定為4096,GroupB的cpu.shares設(shè)定為1024,也就是說在實(shí)際執(zhí)行上,屬於GroupA的Tasks會(huì)比 GroupB的Tasks多得到約4倍的實(shí)際處理器執(zhí)行時(shí)間.

[root@localhost cpu]# echo 4096 > GroupA/cpu.shares

[root@localhost cpu]# echo 1024 > GroupB/cpu.shares

接下來我們用如下的程式碼,分別編譯為GroupA與GroupB的執(zhí)行檔名稱,兩者唯一的差異只在於讓printf顯示的字串各別表示出自己是由GroupA或 GroupB所吐出的內(nèi)容而已.

#include <stdio.h>

#define PrintThreshold 0×10000000

int main()

{

int i,vCount;

i=0;

vCount=0;

while(1)

{

i++;

if(i>PrintThreshold)

{

i=0;

vCount++;

printf("GroupA:%ld\n",vCount);

}

}

return 0;

}

執(zhí)行編譯好的GroupA與GroupB執(zhí)行檔,兩者Process ID分別為 1738與 1739,然後把這兩個(gè)Process ID分別設(shè)定給我們已經(jīng)設(shè)定好差距四倍處理器執(zhí)行時(shí)間的兩個(gè)Control Group目錄下的檔案tasks.

[root@localhost cpu]# echo 1738 > GroupA/tasks

[root@localhost cpu]# echo 1739 > GroupB/tasks

我們可以透過top 指令查看這兩個(gè)程式執(zhí)行時(shí)各自所佔(zhàn)的CPU比率與兩者的差距,如下所示,GroupA佔(zhàn)據(jù)處理器約80.6%的執(zhí)行時(shí)間而GroupB佔(zhàn)據(jù)處理器約19.9%的執(zhí)行時(shí)間. (兩者相加為100.5%,超過100%,但這是筆者環(huán)境中top的真實(shí)結(jié)果(或該說誤差),在此就不作討論),以比率來說兩者所佔(zhàn)CPU實(shí)際執(zhí)行時(shí)間差距大約也是四倍,符合我們透過Control Group對(duì)Task執(zhí)行時(shí)間比率所做的設(shè)定.

PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND

1738 root      20   0  3880  264  212 R 80.6 0.0   2:02.50 GroupA

1739 root      20   0  3880  264  212 R 19.9 0.0   0:41.98 GroupB

再以實(shí)際的執(zhí)行結(jié)果來看,由於GroupA比GroupB多分到四倍的處理器執(zhí)行時(shí)間,也因此在Console所顯示的結(jié)果上,GroupA大約每顯示四個(gè)字串結(jié)果後,GroupB才有機(jī)會(huì)顯示一次,如此也符合我們透過Control Group所設(shè)定分配的處理器執(zhí)行資源比率.

GroupA:27

GroupA:28

GroupA:29

GroupA:30

GroupB:21

GroupA:31

GroupA:32

GroupA:33

GroupA:34

GroupB:22

GroupA:35

GroupA:36

GroupA:37

GroupA:38

GroupB:23

GroupA:39

GroupA:40

GroupA:41

GroupA:42

GroupB:24

GroupA:43

GroupA:44

GroupA:45

在Control Group之後,我們?cè)籴槍?duì)原本Linux Kernel 2.6與在支援CFS排程機(jī)制後的核心排程,進(jìn)一步的加以說明.

CFS(Kernel 2.6.23)之前Linux Kernel 2.6 Time SliceReal Time優(yōu)先級(jí)的簡要介紹

如下圖所示為CFS之前,Linux Kernel 2.6排程的優(yōu)先級(jí)機(jī)制,右邊是屬於Task Real-Time優(yōu)先權(quán),左邊是Task的Nice Value,系統(tǒng)初始化後,一個(gè)最原始的行程產(chǎn)生時(shí),預(yù)設(shè)的Real-Time優(yōu)先權(quán)為0,而Nice優(yōu)先權(quán)亦為0. Linux Kernel會(huì)根據(jù)Nice Value來計(jì)算該Task執(zhí)行時(shí)間Time-Slice值的大小. 當(dāng)使用者把行程的Nice Value遞減時(shí)(最低為-20),就表示該行程的優(yōu)先權(quán)提高,相對(duì)的在排程時(shí)就會(huì)取得比較多的Time-Slice. 相對(duì)的,若不斷的增加Nice值(最大為19),就表示該行程的優(yōu)先權(quán)降低,所取得的Time-Slice亦較低.

但若使用者有Real-Time的需求時(shí),希望特定的行程可以以較高的優(yōu)先權(quán)執(zhí)行,但又不希望如同增加Nice值一般,也對(duì)應(yīng)增加了Time-Slice時(shí),就必須要設(shè)定屬於Real-Time的優(yōu)先權(quán). 而real-Time的優(yōu)先權(quán)最小為1,最大為99,該值越大在排程時(shí)會(huì)越先被執(zhí)行到.

有關(guān)Linux Kernel 2.6.10 核心Nice Value與對(duì)應(yīng)之Time-Slice關(guān)係,可以參考下表

Nice Value Time Slice
-20
-19
-18
-17
-16
-15
-14
-13
-12
-11
-10
-9
-8
-7
-6
-5
-4
-3
-2
-1
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
800ms (DEF_TIMESLICE * 4 * 40 /20)
780ms (DEF_TIMESLICE * 4 * 39 /20)
760ms (DEF_TIMESLICE * 4 * 38 /20)
740ms (DEF_TIMESLICE * 4 * 37 /20)
720ms (DEF_TIMESLICE * 4 * 36 /20)
700ms (DEF_TIMESLICE * 4 * 35 /20)
680ms (DEF_TIMESLICE * 4 * 34 /20)
660ms (DEF_TIMESLICE * 4 * 33 /20)
640ms (DEF_TIMESLICE * 4 * 32 /20)
620ms (DEF_TIMESLICE * 4 * 31 /20)
600ms (DEF_TIMESLICE * 4 * 30 /20)
580ms (DEF_TIMESLICE * 4 * 29 /20)
560ms (DEF_TIMESLICE * 4 * 28 /20)
540ms (DEF_TIMESLICE * 4 * 27 /20)
520ms (DEF_TIMESLICE * 4 * 26 /20)
500ms (DEF_TIMESLICE * 4 * 25 /20)
480ms (DEF_TIMESLICE * 4 * 24 /20)
460ms (DEF_TIMESLICE * 4 * 23 /20)
440ms (DEF_TIMESLICE * 4 * 22 /20)
420ms (DEF_TIMESLICE * 4 * 21 /20)
100ms (DEF_TIMESLICE * 1 * 20 /20)
95ms (DEF_TIMESLICE * 1 * 19 /20)
90ms (DEF_TIMESLICE * 1 * 18 /20)
85ms (DEF_TIMESLICE * 1 * 17 /20)
80ms (DEF_TIMESLICE * 1 * 16 /20)
75ms (DEF_TIMESLICE * 1 * 15 /20)
70ms (DEF_TIMESLICE * 1 * 14 /20)
65ms (DEF_TIMESLICE * 1 * 13 /20)
60ms (DEF_TIMESLICE * 1 * 12 /20)
55ms (DEF_TIMESLICE * 1 * 11 /20)
50ms (DEF_TIMESLICE * 1 * 10 /20)
45ms (DEF_TIMESLICE * 1 * 9 /20)
40ms (DEF_TIMESLICE * 1 * 8 /20)
35ms (DEF_TIMESLICE * 1 * 7 /20)
30ms (DEF_TIMESLICE * 1 * 6 /20)
25ms (DEF_TIMESLICE * 1 * 5 /20)
20ms (DEF_TIMESLICE * 1 * 4 /20)
15ms (DEF_TIMESLICE * 1 * 3 /20)
10ms (DEF_TIMESLICE * 1 * 2 /20)
5m (DEF_TIMESLICE * 1 * 1 /20)

如下圖所示,筆者把Nice值對(duì)應(yīng)到的Task Priority與對(duì)應(yīng)的排程機(jī)制進(jìn)行對(duì)比,我們可以看到在Task Priority 100-139 (對(duì)應(yīng)為Nice -20 – 19 或 User Priority 0-39)的區(qū)間內(nèi),為一般應(yīng)用Task的排程機(jī)制,使用的Scheduling Class為Normal與Batch,而屬於Real-Time排程機(jī)制對(duì)應(yīng)到的Task Priority為0-99,使用的Scheduling Class為Round-Robin與FIFO.

CFSLinux Kernel 2.6.23之後,參考Tasks優(yōu)先級(jí)所施行的Time SliceReal-Time措施

基於CFS RBTree的概念,由於會(huì)一直確保Virtual RunTime值的平衡,透過抓取佔(zhàn)據(jù)Virtual RunTime執(zhí)行時(shí)間最短的Task來執(zhí)行,因此像是原本排程中會(huì)透過Nice值計(jì)算對(duì)應(yīng)Task可運(yùn)作Time Slice的設(shè)計(jì)方式,就變的需要有所調(diào)整,也因此在CFS中會(huì)透過函式calc_delta_mine (實(shí)作在 kernel/sched.c),根據(jù)目前Task的優(yōu)先級(jí)計(jì)算在排程時(shí)每次一個(gè)Scheduling Tick要增加的Delta值,也就是說如果該Task Nice值越高(優(yōu)先級(jí)越低)該Delta值就越大,對(duì)該執(zhí)行的Task來說就是Virtual RunTime在運(yùn)作時(shí),增加的速度越快(等於縮短實(shí)際執(zhí)行的時(shí)間),反之,如果該Task Nice值越低(優(yōu)先級(jí)越高)則Delta值就越小,在每次Scheduling Tick觸發(fā)時(shí),Virtual RunTime增加的速度越慢,能獲得處理器執(zhí)行的實(shí)際時(shí)間也就越長.

參考kernel/sched_fair.c的實(shí)作,函式calc_delta_mine的呼叫來源有兩處,

來源 說明
1,從函式calc_delta_fair中呼叫

static inline unsigned long

calc_delta_fair(unsigned long delta, struct sched_entity *se)

{

if (unlikely(se->load.weight != NICE_0_LOAD))

delta = calc_delta_mine(delta, NICE_0_LOAD, &se->load);

return delta;

}

當(dāng)目前的Scheduling Entity Load Weight不為NICE 0 的Load Weight (=1024)時(shí),就會(huì)根據(jù)所帶入的Delta值與對(duì)應(yīng)的Load Weight,計(jì)算出新的Delta值,通常用在

1,更新目前運(yùn)作的Current Task執(zhí)行的Virtual RunTime時(shí),就會(huì)呼叫calc_delta_fair用以取得Delta時(shí)間,並累加至變數(shù)curr->sum_exec_runtime中. (有關(guān)的實(shí)作可以參考函式__update_curr).

2,用在判斷是否要中斷目前的Task讓下一個(gè)Task (Scheduling Entity)可以進(jìn)來執(zhí)行,實(shí)作部分可以參考函式 wakeup_preempt_entity,首先會(huì)判斷是否要取代的Task Virtual RunTime比目前的Task大,若是就直接返回-1,再來就以 1ms (=sysctl_sched_wakeup_granularity)計(jì)算要取代Task的WakeUp Granularity,若兩個(gè)Task的Virtual RunTime差值大於WakeUp Granularity就返回1,表示可以用下一個(gè)Task取代目前的Task,反之則返回0.

3,用在取得要置入Run Queue的Task Scheduling Entity的Virtual RunTime Slice值,實(shí)作的部分可以參考函式place_entity與函式 sched_vslice.

2,從函式sched_slice中呼叫

static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)

{

u64 slice = __sched_period(cfs_rq->nr_running + !se->on_rq);

for_each_sched_entity(se) {

struct load_weight *load;

struct load_weight lw;

cfs_rq = cfs_rq_of(se);

load = &cfs_rq->load;

if (unlikely(!se->on_rq)) {

lw = cfs_rq->load;

update_load_add(&lw, se->load.weight);

load = &lw;

}

slice = calc_delta_mine(slice, se->load.weight, load);

}

return slice;

}

用來取得Task Scheduling Entity的Wall-Time slice.

以筆者在 64bits處理器下,所使用的Linux Kernel 2.6.38.6來說,函式calc_delta_mine的幾個(gè)關(guān)鍵數(shù)字如下所示

1,NICE_0_LOAD 為1024 (參考kernel/sched.c原始碼 NICE_0_LOAD=SCHED_LOAD_SCALE,並參考include/linux/sched.h中 SCHED_LOAD_SCALE = (1L << SCHED_LOAD_SHIFT), 而SCHED_LOAD_SHIFT = 10)

2,LONG_MAX為9223372036854775807 (ㄟ…我是用64bits處理器驗(yàn)證,所以該值為0x7FFFFFFFFFFFFFFF) (參考原始碼include/linux/kernel.h,其中LONG_MAX = ((long)(~0UL>>1)) )

3,SRR(x, y) 為 (((x) + (1UL << ((y) – 1))) >> (y))   (參考原始碼kernel/sched.c)

4,BITS_PER_LONG:64

5,WMULT_CONST:4294967296  (=0×100000000)

6,WMULT_SHIFT:32

函式calc_delta_mine宣告原型如下

static unsigned long calc_delta_mine(unsigned long delta_exec, unsigned long weight, struct load_weight *lw)

根據(jù)目前的實(shí)作,Delta的返回值會(huì)透過如下流程來計(jì)算

1,如果lw->inv_weight 為0,就會(huì)執(zhí)行下列的計(jì)算

lw->inv_weight = 1 + (WMULT_CONST-lw->weight/2) / (lw->weight+1);

2,Temp-Delta值為delta_exec * weight

2-1,如果Temp-Delta >  0×100000000

2-1-1,Delta = SRR(SRR(Temp-Delta, WMULT_SHIFT/2) * lw->inv_weight,WMULT_SHIFT/2);

2-2,如果Temp-Delta <= 0×100000000

2-2-1,Delta = SRR(Temp-Delta * lw->inv_weight, WMULT_SHIFT);

舉實(shí)際的例子來說,當(dāng)傳入的參數(shù)

1,delta_exec=6000000

2,weight=1024

3,lw->weight=1024

4,lw->inv_weight=0

由於 lw->inv_weight為0,因此會(huì)進(jìn)行如下的計(jì)算

lw->inv_weight=1 + ( 0×100000000 – 1024/2) / (1024+1) = 4190212.

而Temp-Delta 的值 =  delta_exec *  weight = 6000000 * 1024 = 6144000000=0x16E360000,大於 WMULT_CONST的值 (0×100000000).

因此,最後的Delta執(zhí)會(huì)進(jìn)行如下計(jì)算

Delta = SRR(SRR(Temp-Delta, WMULT_SHIFT/2) * lw->inv_weight,WMULT_SHIFT/2)

=SRR(SRR(0x16E360000, 32/2) * 4190212,32/2)

=SRR(((0x16E360000 + 1<<15)>>16)* 4190212,16)

=SRR(  ( 0x16E368000 >>16)* 4190212,16)

=SRR( 0x16E36* 4190212,16)

=SRR( 392832375000,16)

= (392832375000 + 1<15) >> 16

=5994146.

對(duì)應(yīng)到不同函式參數(shù)所換算回來的Delta值,筆者整理如下表所示

delta_exec weight lw->weight 呼叫前的 lw->inv_weight 呼叫後的 lw->inv_weight 返回Delta值

units: nanoseconds

40 1024 1586 2708050 2708050 26
502 1024 2501 1717300 1717300 206
650 1024 1586 2708050 2708050 420
3850 1024 1586 2708050 2708050 2486
4100 1024 1586 2708050 2708050 2647
4351 1024 1586 2708050 2708050 2809
4599 1024 1586 2708050 2708050 2969
6585 1024 2501 1717300 1717300 2696
10315 1024 1586 2708050 2708050 6660
11893 1024 2501 1717300 1717300 4869
11972 1024 2501 1717300 1717300 4902
22495 1024 2501 1717300 1717300 9210
27733 1024 2501 1717300 1717300 11355
53413 1024 1586 2708050 2708050 34486
54762 1024 1586 2708050 2708050 35357
55454 1024 1586 2708050 2708050 35804
60593 1024 1586 2708050 2708050 39122
118071 1024 1586 2708050 2708050 76232
127372 1024 1586 2708050 2708050 82238
724313 1024 1586 2708050 2708050 467652
773872 1024 2501 1717300 1717300 316851
782207 1024 2501 1717300 1717300 320264
1000000 1024 1586 2708050 2708050 645649
1000000 1024 2501 1717300 1717300 409436
1000000 1024 2501 2708050 2708050 645649
1187365 1024 1586 2708050 2708050 766622
2984950 1024 1586 2708050 2708050 1927231
6000000 1024 1024 0 4190212 5994146
6000000 1024 2048 0 2096129 2998537
6000000 1024 2063 2080895 2080895 2976744
6000000 1024 3072 0 1397646 1999349
6000000 1024 4096 1048320 1048320 1499634
6000000 1024 6706 0 640371 916058
6000000 1024 9978 0 430401 615694
6000000 1586 5673 0 756956 1677128
6000000 1586 6244 687745 687745 1523783
20250000 2501 63658 0 67468 795567
21750000 1586 44870 0 95718 768770
21750000 1586 45432 0 94534 759261

透過上述的計(jì)算,我們可以知道影響Virtual RunTime Slice增加的Delta值最大的關(guān)鍵是在下面三個(gè)參數(shù),筆者在此也簡述這三個(gè)參數(shù)的意義.

1,delta_exec: 是目前Virtual RunTime Slice的Delta值,會(huì)用來當(dāng)作calc_delta_mine的第一個(gè)參數(shù),用以計(jì)算出新的Delta值. 根據(jù)目前的實(shí)作,這個(gè)值可以表示在呼叫函式calc_delta_mine前,該Task所執(zhí)行的時(shí)間間隔,也就是說如果該值越大,表示該Task在這次呼叫函式calc_delta_mine前,所得到的執(zhí)行時(shí)間越長,相對(duì)的再透過Weight的計(jì)算後,下一次得到的Delta值就越大,對(duì)應(yīng)到真實(shí)執(zhí)行時(shí)間就會(huì)越少 (因?yàn)镈elta值變大,執(zhí)行時(shí)Virtual RunTime增加的速度就越快.).

2,weight:為目前Task的Load Weight,若Task的Nice值為 0,則此值為1024. 這個(gè)值越大,透過calc_delta_mine所計(jì)算的inv_weight 值越小,對(duì)應(yīng)該 Task所計(jì)算出來的Delta值也越小,也就表示該Task的Virtual RunTime增加的越慢,因此實(shí)際得到處理器執(zhí)行的真實(shí)時(shí)間就越長.

3,lw->weight:為目前排程單位sched_entity的Load Weight . 如果se->load.weight等於Nice 0的 Load Weight (1024),函式calc_delta_fair就會(huì)直接返回所傳入的Delta值,反之才會(huì)進(jìn)入函式calc_delta_mine,重新計(jì)算新的Delta值. RunQueue的參數(shù)load->weight,會(huì)等於該RunQueue中所有Schedule Entity的load->weight總合,分別表示整個(gè)RunQueue執(zhí)行單元的負(fù)荷與個(gè)別執(zhí)行單元的負(fù)荷.

參考函式set_load_weight的實(shí)作(in kernel/sched.c),我們可以把不同的Task Priority ,Nice值與所對(duì)應(yīng)的Scheduling Entity Load Weight值對(duì)應(yīng)如下表所示

Task Priority Task Nice Scheduling Entity Load Weight Scheduling Entity Load Inverse-Weight
100 -20 88761 48388
101 -19 71755 59856
102 -18 56483 76040
103 -17 46273 92818
104 -16 36291 118348
105 -15 29154 147320
106 -14 23254 184698
107 -13 18705 229616
108 -12 14949 287308
109 -11 11916 360437
110 -10 9548 449829
111 -9 7620 563644
112 -8 6100 704093
113 -7 4904 875809
114 -6 3906 1099582
115 -5 3121 1376151
116 -4 2501 1717300
117 -3 1991 2157191
118 -2 1586 2708050
119 -1 1277 3363326
120 0 1024 4194304
121 1 820 5237765
122 2 655 6557202
123 3 526 8165337
124 4 423 10153587
125 5 335 12820798
126 6 272 15790321
127 7 215 19976592
128 8 172 24970740
129 9 137 31350126
130 10 110 39045157
131 11 87 49367440
132 12 70 61356676
133 13 56 76695844
134 14 45 95443717
135 15 36 119304647
136 16 29 148102320
137 17 23 186737708
138 18 18 238609294
139 19 15 286331153

一般來說,如果Nice值加一 (也就是優(yōu)先級(jí)降低一),大約會(huì)減少10%處理器的執(zhí)行時(shí)間,而如果是Nice值減一(也就是優(yōu)先級(jí)調(diào)升),大約就會(huì)增加10%處理器的執(zhí)行時(shí)間.而參考上表整理的結(jié)果,每個(gè)Nice值的區(qū)間所對(duì)應(yīng)的Weight大約會(huì)相差1.25倍. 例如15*1.25=18.75,18*1.25=22.5…

介紹到此,我們可以發(fā)現(xiàn)像是O(N),O(1)或是CFS (O(Log2(N))),都沒有把處理器不等速的問題考慮進(jìn)來.也因此,在支援像是aSMP架構(gòu)時(shí),這部份的排程機(jī)制改善,就是攸關(guān)能否發(fā)揮aSMP架構(gòu)優(yōu)勢(shì)的關(guān)鍵.

結(jié)語

多核心架構(gòu)肯定會(huì)是未來智慧型手機(jī)晶片的主流,要能在智慧型手機(jī)方案中發(fā)揮架構(gòu)的優(yōu)勢(shì),了解Linux Kernel的排程機(jī)制,會(huì)是一個(gè)必要的基礎(chǔ)功課.

本文主要著重在Linux Kernel排程的介紹,從前一代的Linux核心排程機(jī)制談起,到目前基於CFS的核心排程與相關(guān)資料結(jié)構(gòu)的說明. 但要發(fā)揮多核心的優(yōu)勢(shì),除了Linux Kernel的層次外,在Android Framework上也需有所因應(yīng),後續(xù)筆者會(huì)再進(jìn)一步的加以介紹.希望能讓閱讀本文的開發(fā)者有所助益.


People who looked at this item also looked at…

Related items

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多