Linux 2.4调度系统分析--转
http://www.ibm.com/developerworks/cn/linux/kernel/l-k24sch/index.html
楊沙洲?(pubb@163.net)國(guó)防科技大學(xué)計(jì)算機(jī)學(xué)院?
簡(jiǎn)介:?本文詳盡地分析了Linux 2.4內(nèi)核中調(diào)度系統(tǒng)的工作原理,特別是i386體系結(jié)構(gòu)下SMP系統(tǒng)的調(diào)度表現(xiàn)。通過對(duì)2.4調(diào)度系統(tǒng)實(shí)現(xiàn)原理及其細(xì)節(jié)的分析,文章在文末指出了2.4調(diào)度系統(tǒng)在功能上、實(shí)時(shí)性上以及多處理機(jī)系統(tǒng)表現(xiàn)上存在的不足,為后繼的2.6系統(tǒng)的分析作鋪墊。
一. 前言
在開源操作系統(tǒng)中,Linux的發(fā)展最為顯著,到目前為止,它在低端服務(wù)器市場(chǎng)已經(jīng)占據(jù)了相當(dāng)大的份額。從最新的Linux 2.6系統(tǒng)來看,Linux的發(fā)展方向主要有兩個(gè):嵌入式系統(tǒng)和高端計(jì)算領(lǐng)域。
調(diào)度系統(tǒng)對(duì)于操作系統(tǒng)的整體性能有著非常重要的影響,嵌入式系統(tǒng)、桌面系統(tǒng)和高端服務(wù)器對(duì)于調(diào)度器的要求是很不一樣的。Linux調(diào)度器的特點(diǎn)主要有兩個(gè):
- 核心不可搶占;
- 調(diào)度算法簡(jiǎn)單有效。
由于Linux適用于多種平臺(tái),本文所指缺省為i386下的SMP系統(tǒng)。
回頁(yè)首
二. 相關(guān)數(shù)據(jù)結(jié)構(gòu)
在Linux中,進(jìn)程用task_struct表示,所有進(jìn)程被組織到以init_task為表頭的雙向鏈表中(見[include/linux/sched.h]SET_LINKS()宏),該鏈表是全系統(tǒng)唯一的。所有CPU被組織到以schedule_data(對(duì)界后)為元素的數(shù)組之中。進(jìn)程與所運(yùn)行的CPU之間可以相互訪問(詳見下)。
所有處于運(yùn)行態(tài)的進(jìn)程(TASK_RUNNING)被組織到以runqueue_head為表頭的雙向鏈表之中,調(diào)度器總是從中尋找最適合調(diào)度的進(jìn)程。runqueue_head也是全系統(tǒng)唯一的。
下面分別介紹這些與調(diào)度器工作相關(guān)的數(shù)據(jù)結(jié)構(gòu)。
1. init_tss
TSS,Task State Segment,80x86平臺(tái)特有的進(jìn)程運(yùn)行環(huán)境,盡管Linux并不使用TSS,但將TSS所需要描述的信息保存在以cpu號(hào)為索引的tss_struct數(shù)組init_tss中,進(jìn)程切換時(shí),其中的值將獲得更新。
2. task_struct
在Linux中,線程、進(jìn)程使用的是相同的核心數(shù)據(jù)結(jié)構(gòu),可以說,在2.4的內(nèi)核里只有進(jìn)程,其中包含輕量進(jìn)程。一個(gè)進(jìn)程在核心中使用一個(gè)task_struct結(jié)構(gòu)來表示,包含了大量描述該進(jìn)程的信息,其中與調(diào)度器相關(guān)的信息主要包括以下幾個(gè):
i. state
Linux的進(jìn)程狀態(tài)主要分為三類:可運(yùn)行的(TASK_RUNNING,相當(dāng)于運(yùn)行態(tài)和就緒態(tài));被掛起的(TASK_INTERRUPTIBLE、TASK_UNINTERRUPTIBLE和TASK_STOPPED);不可運(yùn)行的(TASK_ZOMBIE),調(diào)度器主要處理的是可運(yùn)行和被掛起兩種狀態(tài)下的進(jìn)程,其中TASK_STOPPED又專門用于SIGSTP等IPC信號(hào)的響應(yīng),而TASK_ZOMBIE指的是已退出而暫時(shí)沒有被父進(jìn)程收回資源的"僵尸"進(jìn)程。
ii. need_resched
布爾值,在調(diào)度器中用于表示該進(jìn)程需要申請(qǐng)調(diào)度(詳見"調(diào)度器工作流程")。
iii. policy
在Linux 2.4中,進(jìn)程的調(diào)度策略可以有三種選擇:SCHED_FIFO(先進(jìn)先出式調(diào)度,除非有更高優(yōu)先級(jí)進(jìn)程申請(qǐng)運(yùn)行,否則該進(jìn)程將保持運(yùn)行至退出才讓出CPU)、SCHED_RR(輪轉(zhuǎn)式調(diào)度,該進(jìn)程被調(diào)度下來后將被置于運(yùn)行隊(duì)列的末尾,以保證其他實(shí)時(shí)進(jìn)程有機(jī)會(huì)運(yùn)行)、SCHED_OTHER(常規(guī)的分時(shí)調(diào)度策略)。另外,policy中還包含了一個(gè)SCHED_YIELD位,置位時(shí)表示主動(dòng)放棄CPU。
iv. rt_priority
用于表征實(shí)時(shí)進(jìn)程的優(yōu)先級(jí),從1-99取值,非實(shí)時(shí)進(jìn)程該項(xiàng)應(yīng)該為0。這一屬性將用于調(diào)度時(shí)的權(quán)值計(jì)算(詳見"就緒進(jìn)程選擇算法")。
v. counter
該屬性記錄的是當(dāng)前時(shí)間片內(nèi)該進(jìn)程還允許運(yùn)行的時(shí)間(以CPU時(shí)鐘tick值為單位,每個(gè)進(jìn)程的counter初值與nice值有關(guān),nice越小則counter越大,即優(yōu)先級(jí)越高的進(jìn)程所允許獲得的CPU時(shí)間也相對(duì)越多),并參與"就緒進(jìn)程選擇算法"。在Linux 2.4中,每個(gè)(非SCHED_FIFO實(shí)時(shí))進(jìn)程都不允許運(yùn)行大于某一時(shí)間片的時(shí)間,一旦超時(shí),調(diào)度器將強(qiáng)制選擇另一進(jìn)程運(yùn)行(詳見"調(diào)度器工作流程")
vi. nice
用戶可支配的進(jìn)程優(yōu)先級(jí),將參與"就緒進(jìn)程選擇算法",同時(shí)該值也決定了該進(jìn)程的時(shí)間片長(zhǎng)度(詳見下)。
vii. cpus_allowed
以位向量的形式表示可用于該進(jìn)程運(yùn)行的CPU(見"調(diào)度器工作流程")。
viii. cpus_runnable
以位向量的形式表示當(dāng)前運(yùn)行該進(jìn)程的CPU(相應(yīng)位為1)。如果不在任何CPU上運(yùn)行,則為全1。這一屬性和cpus_allowed屬性結(jié)合,可以迅速判斷該進(jìn)程是否能調(diào)度到某一CPU上運(yùn)行(位"與")。
ix. processor
本進(jìn)程當(dāng)前(或最近)所在CPU編號(hào)。
x. thread
用于保存進(jìn)程執(zhí)行環(huán)境(各個(gè)寄存器的值以及IO操作許可權(quán)映射表),內(nèi)容與TSS相近。因?yàn)門SS以CPU id為索引,而Linux無(wú)法預(yù)測(cè)被替換下來的進(jìn)程下一次將在哪個(gè)CPU上運(yùn)行,所以這些信息不能保存在TSS中。
3. current
核心經(jīng)常需要獲知當(dāng)前在某CPU上運(yùn)行的進(jìn)程的task_struct,在Linux中用current指針指向這一描述符。current的實(shí)現(xiàn)采用了一個(gè)小技巧以獲得高效的訪問速度,這個(gè)小技巧與Linux進(jìn)程task_struct的存儲(chǔ)方式有關(guān)。
在Linux中,進(jìn)程在核心級(jí)運(yùn)行時(shí)所使用的棧不同于在用戶級(jí)所分配和使用的棧。因?yàn)檫@個(gè)棧使用率不高,因此僅在創(chuàng)建進(jìn)程時(shí)分配了兩個(gè)頁(yè)(8KB),并且將該進(jìn)程的task_struct安排在棧頂。(實(shí)際上這兩個(gè)頁(yè)是在分配task_struct時(shí)申請(qǐng)的,初始化完task_struct后即將esp預(yù)設(shè)為頁(yè)尾作為進(jìn)程的核心棧棧底,往task_struct方向延伸。)
因此,要訪問本進(jìn)程的task_struct,只需要執(zhí)行以下簡(jiǎn)單操作:
__asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL));此句將esp與0x0ffffe0作"與"運(yùn)算,獲得核心棧的首頁(yè)基址,此即為task_struct的地址。
4. schedule_data
task_struct是用于描述進(jìn)程的數(shù)據(jù)結(jié)構(gòu),其中包含了指向所運(yùn)行CPU的屬性。在Linux中,另有一個(gè)數(shù)據(jù)結(jié)構(gòu)對(duì)應(yīng)于CPU,可以利用它訪問到某CPU上運(yùn)行的進(jìn)程,這個(gè)數(shù)據(jù)結(jié)構(gòu)定義為schedule_data結(jié)構(gòu),包含兩個(gè)屬性:curr指針,指向當(dāng)前運(yùn)行于該CPU上的進(jìn)程的task_struct,通常用cpu_curr(cpu)宏來訪問;last_schedule時(shí)間戳,記錄了上一次該CPU上進(jìn)程切換的時(shí)間,通常用last_schedule(cpu)宏來訪問。
為了使該數(shù)據(jù)結(jié)構(gòu)的訪問能與CPU的Cache line大小相一致,schedule_data被組織到以SMP_CACHE_BYTES為單位的aligned_data聯(lián)合數(shù)組中,系統(tǒng)中每個(gè)CPU對(duì)應(yīng)數(shù)組上的一個(gè)元素。
5. init_tasks
調(diào)度器并不直接使用init_task為表頭的進(jìn)程鏈表,而僅使用其中的"idle_task"。該進(jìn)程在引導(dǎo)完系統(tǒng)后即處于cpu_idle()循環(huán)中(詳見"其他核心應(yīng)用的調(diào)度相關(guān)部分"之"IDLE進(jìn)程")。SMP系統(tǒng)中,每個(gè)CPU都分別對(duì)應(yīng)了一個(gè)idle_task,它們的task_struct指針被組織到init_tasks[NR_CPUS]數(shù)組中,調(diào)度器通過idle_task(cpu)宏來訪問這些"idle"進(jìn)程(詳見"調(diào)度器工作流程")。
?
6. runqueue_head
以runqueue_head為表頭的鏈表記錄了所有處于就緒態(tài)的進(jìn)程(當(dāng)前正在運(yùn)行的進(jìn)程也在其中,但idle_task除外),調(diào)度器總是從中選取最適合調(diào)度的進(jìn)程投入運(yùn)行。
回頁(yè)首
三. 進(jìn)程切換過程
從一個(gè)進(jìn)程的上下文切換到另一個(gè)進(jìn)程的上下文,因?yàn)槠浒l(fā)生頻率很高,所以通常都是調(diào)度器效率高低的關(guān)鍵。在Linux中,這一功能是以一段經(jīng)典的匯編代碼實(shí)現(xiàn)的,此處就著力描述這段代碼。
這段名為switch_to()的代碼段在schedule()過程中調(diào)用,以一個(gè)宏實(shí)現(xiàn):
| /* 節(jié)選自[include/asm-i386/system.h] */ #define switch_to(prev,next,last) do { \asm volatile("pushl %%esi\n\t" \"pushl %%edi\n\t" \"pushl %%ebp\n\t" \保存esi、edi、ebp寄存器"movl %%esp,%0\n\t" \esp保存到prev->thread.esp中"movl %3,%%esp\n\t" \從next->thread.esp恢復(fù)esp"movl $1f,%1\n\t" \在prev->thread.eip中保存"1:"的跳轉(zhuǎn)地址,\當(dāng)prev被再次切換到的時(shí)候?qū)哪抢镩_始執(zhí)行"pushl %4\n\t" \在棧上保存next->thread.eip,__switch_to()返回時(shí)將轉(zhuǎn)到那里執(zhí)行,\即進(jìn)入next進(jìn)程的上下文"jmp __switch_to\n" \跳轉(zhuǎn)到__switch_to(),進(jìn)一步處理(見下)"1:\t" \"popl %%ebp\n\t" \"popl %%edi\n\t" \"popl %%esi\n\t" \先恢復(fù)上次被切換走時(shí)保存的寄存器值,\再?gòu)膕witch_to()中返回。:"=m" (prev->thread.esp), \%0"=m" (prev->thread.eip),\%1"=b" (last) \ebx, \因?yàn)檫M(jìn)程切換后,恢復(fù)的棧上的prev信息不是剛被切換走的進(jìn)程描述符, \因此此處使用ebx寄存器傳遞該值給prev:"m" (next->thread.esp), \%3"m" (next->thread.eip), \%4"a" (prev), "d" (next), \eax,edx"b" (prev)); \ebx } while (0) |
?
進(jìn)程切換過程可以分成兩個(gè)階段,上面這段匯編代碼可以看作第一階段,它保存一些關(guān)鍵的寄存器,并在棧上設(shè)置好跳轉(zhuǎn)到新進(jìn)程的地址。第二階段在switch_to()中啟動(dòng),實(shí)現(xiàn)在__switch_to()函數(shù)中,主要用于保存和更新不是非常關(guān)鍵的一些寄存器(以及IO操作許可權(quán)映射表ioperm)的值:
- unlazy_fpu(),如果老進(jìn)程在task_struct的flags中設(shè)置了PF_USEDFPU位,表明它使用了FPU,unlazy_fpu()就會(huì)將FPU內(nèi)容保存在task_struct::thread中;
- 用新進(jìn)程的esp0(task_struct::thread中)更新init_tss中相應(yīng)位置的esp0;
- 在老進(jìn)程的task_struct::thread中保存當(dāng)前的fs和gs寄存器,然后從新進(jìn)程的task_struct::thread中恢復(fù)fs和gs寄存器;
- 從新進(jìn)程的task_struct::thread中恢復(fù)六個(gè)調(diào)試寄存器的值;
- 用next中的ioperm更新init_tss中的相應(yīng)內(nèi)容
switch_to()函數(shù)正常返回,棧上的返回地址是新進(jìn)程的task_struct::thread::eip,即新進(jìn)程上一次被掛起時(shí)設(shè)置的繼續(xù)運(yùn)行的位置(上一次執(zhí)行switch_to()時(shí)的標(biāo)號(hào)"1:"位置)。至此轉(zhuǎn)入新進(jìn)程的上下文中運(yùn)行。
在以前的Linux內(nèi)核中,進(jìn)程的切換使用的是far jmp指令,2.4采用如上所示的手控跳轉(zhuǎn),所做的動(dòng)作以及所用的時(shí)間均與far jmp差不多,但更利于優(yōu)化和控制。
回頁(yè)首
四. 就緒進(jìn)程選擇算法
Linux schedule()函數(shù)將遍歷就緒隊(duì)列中的所有進(jìn)程,調(diào)用goodness()函數(shù)計(jì)算每一個(gè)進(jìn)程的權(quán)值weight,從中選擇權(quán)值最大的進(jìn)程投入運(yùn)行。
進(jìn)程調(diào)度權(quán)值的計(jì)算分為實(shí)時(shí)進(jìn)程和非實(shí)時(shí)進(jìn)程兩類,對(duì)于非實(shí)時(shí)進(jìn)程(SCHED_OTHER),影響權(quán)值的因素主要有以下幾個(gè):
1. 進(jìn)程當(dāng)前時(shí)間片內(nèi)所剩的tick數(shù),即task_struct的counter值,相當(dāng)于counter越大的進(jìn)程獲得CPU的機(jī)會(huì)也越大,因?yàn)閏ounter的初值與(-nice)相關(guān),因此這一因素一方面代表了進(jìn)程的優(yōu)先級(jí),另一方面也代表了進(jìn)程的"欠運(yùn)行程度";(weight = p->counter;)
2. 進(jìn)程上次運(yùn)行的CPU是否就是當(dāng)前CPU,如果是,則權(quán)值增加一個(gè)常量,表示優(yōu)先考慮不遷移CPU的調(diào)度,因?yàn)榇藭r(shí)Cache信息還有效;(weight += PROC_CHANGE_PENALTY;)
3. 此次切換是否需要切換內(nèi)存,如果不需要(或者是同一進(jìn)程的兩個(gè)線程間的切換,或者是沒有mm屬性的核心線程),則權(quán)值加1,表示(稍微)優(yōu)先考慮不切換內(nèi)存的進(jìn)程;(weight += 1;)
4. 進(jìn)程的用戶可見的優(yōu)先級(jí)nice,nice越小則權(quán)值越大。(Linux中的nice值在-20到+19之間選擇,缺省值為0,nice()系統(tǒng)調(diào)用可以用來修改優(yōu)先級(jí)。)(weight += 20 - p->nice;) 對(duì)于實(shí)時(shí)進(jìn)程(SCHED_FIFO、SCHED_RR),權(quán)值大小僅由該進(jìn)程的rt_priority值決定(weight = 1000 + p->rt_priority;),1000的基準(zhǔn)量使得實(shí)時(shí)進(jìn)程的權(quán)值比所有非實(shí)時(shí)進(jìn)程都要大,因此只要就緒隊(duì)列中存在實(shí)時(shí)進(jìn)程,調(diào)度器都將優(yōu)先滿足它的運(yùn)行需要。
如果權(quán)值相同,則選擇就緒隊(duì)列中位于前列的進(jìn)程投入運(yùn)行。
除了以上標(biāo)準(zhǔn)值以外,goodness()還可能返回-1,表示該進(jìn)程設(shè)置了SCHED_YIELD位,此時(shí),僅當(dāng)不存在其他就緒進(jìn)程時(shí)才會(huì)選擇它。
如果遍歷所有就緒進(jìn)程后,weight值為0,表示當(dāng)前時(shí)間片已經(jīng)結(jié)束了,此時(shí)將重新計(jì)算所有進(jìn)程(不僅僅是就緒進(jìn)程)的counter值,再重新進(jìn)行就緒進(jìn)程選擇(詳見"調(diào)度器工作流程")。
回頁(yè)首
五. 調(diào)度器
Linux的調(diào)度器主要實(shí)現(xiàn)在schedule()函數(shù)中。
1.調(diào)度器工作流程
schedule()函數(shù)的基本流程可以概括為四步:
1). 清理當(dāng)前運(yùn)行中的進(jìn)程
2). 選擇下一個(gè)投入運(yùn)行的進(jìn)程
3). 設(shè)置新進(jìn)程的運(yùn)行環(huán)境
4). 執(zhí)行進(jìn)程上下文切換
5). 后期整理
其中包含了一些鎖操作:就緒隊(duì)列鎖runquque_lock,全局核心鎖kernel_flag,全局中斷鎖global_irq_lock,進(jìn)程列表鎖tasklist_lock。下面先從鎖操作開始描述調(diào)度器的工作過程。
A. 相關(guān)鎖
- runqueue_lock,定義為自旋鎖,對(duì)就緒隊(duì)列進(jìn)行操作之前,必須鎖定;
- kernel_flag,定義為自旋鎖,因?yàn)楹芏嗪诵牟僮?#xff08;例如驅(qū)動(dòng)中)需要保證當(dāng)前僅由一個(gè)進(jìn)程執(zhí)行,所以需要調(diào)用lock_kernel()/release_kernel()對(duì)核心鎖進(jìn)行操作,它在鎖定/解鎖kernel_flag的同時(shí)還在task_struct::lock_depth上設(shè)置了標(biāo)志,lock_depth小于0表示未加鎖。當(dāng)發(fā)生進(jìn)程切換的時(shí)候,不允許被切換走的進(jìn)程握有kernel_flag鎖,所以必須調(diào)用release_kernel_lock()強(qiáng)制釋放,同時(shí),新進(jìn)程投入運(yùn)行時(shí)如果lock_depth>0,即表明該進(jìn)程被切換走之前握有核心鎖,必須調(diào)用reacquire_kernel_lock()再次鎖定;
- global_irq_lock,定義為全局的內(nèi)存長(zhǎng)整型,使用clear_bit()/set_bit()系列進(jìn)行操作,它與global_irq_holder配合表示當(dāng)前哪個(gè)cpu握有全局中斷鎖,該鎖掛起全局范圍內(nèi)的中斷處理(見irq_enter());
- tasklist_lock,定義為讀寫鎖,保護(hù)以init_task為頭的進(jìn)程列表結(jié)構(gòu)。
B. prev
在schedule中,當(dāng)前進(jìn)程(也就是可能被調(diào)度走的進(jìn)程)用prev指針訪問。
對(duì)于SCHED_RR的實(shí)時(shí)進(jìn)程,僅當(dāng)該進(jìn)程時(shí)間片結(jié)束(counter==0)后才會(huì)切換到別的進(jìn)程,此時(shí)將根據(jù)nice值重置counter,并將該進(jìn)程置于就緒隊(duì)列的末尾。當(dāng)然,如果當(dāng)前就緒隊(duì)列中不存在其他實(shí)時(shí)進(jìn)程,則根據(jù)前面提到的goodness()算法,調(diào)度器仍將選擇到該進(jìn)程。
如果處于TASK_INTERRUPTIBLE狀態(tài)的進(jìn)程有信號(hào)需要處理(這可能發(fā)生在進(jìn)程因等待信號(hào)而準(zhǔn)備主動(dòng)放棄CPU,在放棄CPU之前,信號(hào)已經(jīng)發(fā)生了的情況),調(diào)度器并不立即執(zhí)行該進(jìn)程,而是將該進(jìn)程置為就緒態(tài)(該進(jìn)程還未來得及從就緒隊(duì)列中刪除),參與緊接著的goodness選擇。
如果prev不處于就緒態(tài),也不處于上面這種有信號(hào)等待處理的掛起態(tài)(prev為等待資源而主動(dòng)調(diào)用schedule()放棄CPU),那么它將從就緒隊(duì)列中刪除,此后,除非有喚醒操作將進(jìn)程重新放回到就緒隊(duì)列,否則它將不參與調(diào)度。
被動(dòng)方式啟動(dòng)調(diào)度器工作時(shí),當(dāng)前進(jìn)程的need_resched屬性會(huì)置位(見下"調(diào)度器工作時(shí)機(jī)")。在schedule()中,該位會(huì)被清掉,表示該進(jìn)程已經(jīng)在調(diào)度器中得到了處理(當(dāng)然,這一處理并不意味著該進(jìn)程就一定獲得了CPU)。
C. goodness
調(diào)度器遍歷就緒隊(duì)列中的所有進(jìn)程,只要它當(dāng)前可被調(diào)度(cpus_runnable & cpus_allowed & (1 << cpu),表示該進(jìn)程可在當(dāng)前運(yùn)行調(diào)度器的CPU上運(yùn)行,且不在其他CPU上運(yùn)行),就調(diào)用goodness()計(jì)算其權(quán)值。next指針用來指向權(quán)值最大的進(jìn)程,缺省指向idle_task,如果就緒隊(duì)列為空,就使用缺省的idle_task作為next。
正如前面所提到的,如果遍歷結(jié)束后的最大權(quán)值為0,則表示當(dāng)前所有可被調(diào)度的就緒進(jìn)程的時(shí)間片都用完了,這時(shí)調(diào)度器將需要重新設(shè)置所有進(jìn)程(包括就緒的和掛起的)的counter值,未完成時(shí)間片的進(jìn)程(例如當(dāng)前被掛起的進(jìn)程或者當(dāng)前正在其他CPU上運(yùn)行的進(jìn)程),其剩下的時(shí)間片的一半將疊加到新的時(shí)間片中。
將選中的進(jìn)程設(shè)置為在本CPU上運(yùn)行(task_set_cpu())之后,runqueue_lock就可以解開了,接下來就將對(duì)next進(jìn)行配置。
D. next
選取的新進(jìn)程可能剛好就是需要替換出去的老進(jìn)程,此時(shí)因?yàn)閷?shí)際上不需要進(jìn)行進(jìn)程切換,所以可以跳過配置next以及下面的"switch"和"schedule_tail"兩個(gè)階段。
新進(jìn)程的運(yùn)行環(huán)境實(shí)際上主要就是指內(nèi)存。在task_struct中有兩個(gè)與調(diào)度器相關(guān)的內(nèi)存屬性:mm和active_mm,前者指向進(jìn)程所擁有的內(nèi)存區(qū)域,后者則指向進(jìn)程所實(shí)際使用的內(nèi)存。對(duì)于大多數(shù)進(jìn)程,mm和active_mm是相同的,但核心線程沒有自主的內(nèi)存,它們的mm指針永遠(yuǎn)為NULL。我們知道,任何進(jìn)程的虛頁(yè)表中,核心空間永遠(yuǎn)映射到了虛存的高端固定位置,所以,核心線程所使用的內(nèi)存無(wú)論對(duì)于哪個(gè)進(jìn)程空間都是一樣的,所以也就沒有必要切換進(jìn)程的內(nèi)存。在調(diào)度器中,只要判斷一下next->mm是否為空就能知道該進(jìn)程是不是核心線程,如果是,則繼續(xù)使用prev的active_mm(next->active_mm = prev->active_mm),并通過設(shè)置cpu_tlbstate[cpu].state為TLBSTATE_LAZY,告訴內(nèi)存管理部件不要刷新TLB;否則就調(diào)用switch_mm()函數(shù)進(jìn)行內(nèi)存的切換(具體過程牽涉到內(nèi)存管理模塊的知識(shí),這里就從略了)。實(shí)際上,在switch_mm()中還會(huì)對(duì)prev->active_mm和next->mm判斷一次,如果兩值相等,說明兩個(gè)進(jìn)程是同屬于一個(gè)"進(jìn)程"的兩個(gè)"線程"(實(shí)際上是輕量進(jìn)程),此時(shí)也不需要執(zhí)行內(nèi)存的切換,但這種情況TLB還是需要刷新的。
設(shè)置好next的內(nèi)存環(huán)境以后,就可以調(diào)用mmdrop()釋放掉prev的內(nèi)存結(jié)構(gòu)了。所有不在運(yùn)行中的進(jìn)程,其active_mm屬性都應(yīng)該為空。
E. switch
進(jìn)程切換的過程在上文中已經(jīng)描述得比較詳細(xì)了。
F. schedule_tail
完成切換后,調(diào)度器將調(diào)用__schedule_tail()。這一函數(shù)對(duì)于UP系統(tǒng)基本沒什么影響,對(duì)于SMP系統(tǒng),如果被切換下來的進(jìn)程(用p表示)仍然處于就緒態(tài)且未被任何CPU調(diào)度到,__schedule_tail()將調(diào)用reschedule_idle(),為p挑選一個(gè)空閑的(或者是所運(yùn)行的進(jìn)程優(yōu)先級(jí)比p低的)CPU,并強(qiáng)迫該CPU重新調(diào)度,以便將p重新投入運(yùn)行。進(jìn)程從休眠狀態(tài)中醒來時(shí)也同樣需要挑選一個(gè)合適的CPU運(yùn)行,這一操作是通過在wake_up_process()函數(shù)中調(diào)用reschedule_idle()實(shí)現(xiàn)的。挑選CPU的原則如下:
- p上次運(yùn)行的CPU目前空閑。很顯然,這是最佳選擇,因?yàn)椴恍枰獡屨糃PU,CPU Cache也最有可能和p吻合。不過,既然p可運(yùn)行,調(diào)度器就不可能調(diào)度到idle_task,所以這種情況只會(huì)發(fā)生在wake_up_process()的時(shí)候。
- 所有空閑的CPU中最近最少活躍(last_schedule(cpu)最小)的一個(gè)。該CPU中的Cache信息最有可能是無(wú)用的,因此這種選擇方式可以盡最大可能減少搶占CPU的開銷,同時(shí)也盡可能避免頻繁搶占。值得注意的是,在使用支持超線程技術(shù)的CPU的SMP平臺(tái)上,一旦發(fā)現(xiàn)一個(gè)物理CPU的兩個(gè)邏輯CPU均空閑,則該CPU的其中一個(gè)邏輯CPU立即成為p候選的調(diào)度CPU,而不需要繼續(xù)尋找最近最少活躍的CPU。
- CPU不空閑,但所運(yùn)行的進(jìn)程優(yōu)先級(jí)比p的優(yōu)先級(jí)低,且差值最大。計(jì)算優(yōu)先級(jí)時(shí)使用的是goodness()函數(shù),因?yàn)樗男畔⒆疃唷?/li>
找到合適的CPU后,reschedule_idle()就會(huì)將目標(biāo)進(jìn)程(正在該CPU上運(yùn)行的進(jìn)程,可能是idle_task)的need_resched置為1,以便調(diào)度器能夠工作(見"調(diào)度器工作時(shí)機(jī)")。同時(shí),因?yàn)閕dle_task很多情況下都使cpu處于停機(jī)(halt)狀態(tài)以節(jié)電,所以有必要調(diào)用smp_send_reschedule(cpu)向cpu發(fā)RESCHEDULE_VECTOR中斷(通過IPI接口),以喚醒該cpu。
注:對(duì)于目標(biāo)進(jìn)程是idle_task的情況,還要判斷它的need_resched標(biāo)志位,僅當(dāng)它為0的時(shí)候才會(huì)啟動(dòng)調(diào)度,因?yàn)榉?狀態(tài)的idle_task本身一直都在檢查need_resched值,它自己會(huì)啟動(dòng)schedule()(見下"IDLE進(jìn)程")。
G. clear
調(diào)度器工作的結(jié)果有兩種:發(fā)生了切換、沒有發(fā)生切換,但調(diào)度器退出前的清理工作是一樣的,就是恢復(fù)新進(jìn)程的狀態(tài)。主要包含兩個(gè)動(dòng)作:
- 清被切換走的進(jìn)程的SCHED_YIELD位(不管它是否置位);
- 如果新進(jìn)程(p)的lock_depth大于等于0,則重新為核心鎖kernel_flag加鎖(見上"相關(guān)鎖")。
2. 調(diào)度器工作時(shí)機(jī)
調(diào)度器的啟動(dòng)通常有兩種方式:
A. 主動(dòng)式
在核心應(yīng)用中直接調(diào)用schedule()。這通常發(fā)生在因等待核心事件而需要將進(jìn)程置于掛起(休眠)狀態(tài)的時(shí)候--這時(shí)應(yīng)該主動(dòng)請(qǐng)求調(diào)度以方便其他進(jìn)程使用CPU。下面就是一個(gè)主動(dòng)調(diào)度的例子:
| /* 節(jié)選自[drivers/input/mousedev.c] mousedev_read() */add_wait_queue(&list->mousedev->wait, &wait);current->state = TASK_INTERRUPTIBLE;while (!list->ready) {if (file->f_flags & O_NONBLOCK) {retval = -EAGAIN;break;}if (signal_pending(current)) {retval = -ERESTARTSYS;break;}schedule();}current->state = TASK_RUNNING; /* 這一句實(shí)際上可以省略,因?yàn)檫M(jìn)程的狀態(tài)在喚醒過程中就已經(jīng)恢復(fù)到TASK_RUNNING了 */remove_wait_queue(&list->mousedev->wait, &wait); |
?
其過程通常可分為四步:
- 將進(jìn)程添加到事件等待隊(duì)列中;
- 置進(jìn)程狀態(tài)為TASK_INTERRUPTIBLE(或TASK_UNINTERRUPTIBLE);
- 在循環(huán)中檢查等待條件是否滿足,不滿足則調(diào)用schedule(),滿足了就退出循環(huán);
- 將進(jìn)程從事件等待隊(duì)列中刪除。
從"調(diào)度器工作流程"中我們知道,調(diào)度器會(huì)將處于休眠狀態(tài)的進(jìn)程從就緒隊(duì)列中刪除,而只有就緒隊(duì)列中的進(jìn)程才有可能被調(diào)度到。將該進(jìn)程重新放到就緒隊(duì)列中的動(dòng)作是在事件發(fā)生時(shí)的"喚醒"過程中完成的。在以上所示的鼠標(biāo)驅(qū)動(dòng)中,鼠標(biāo)中斷將調(diào)用mousedev_event()函數(shù),該函數(shù)的最后就會(huì)使用wake_up_interruptible()喚醒等待鼠標(biāo)事件的所有進(jìn)程。wake_up_interruptible()將最終調(diào)用try_to_wake_up()函數(shù):
| /* 節(jié)選自[kernel/sched.c] */ static inline int try_to_wake_up(struct task_struct * p, int synchronous) {unsigned long flags;int success = 0;spin_lock_irqsave(&runqueue_lock, flags);p->state = TASK_RUNNING;if (task_on_runqueue(p))goto out;add_to_runqueue(p); /* 添加到就緒隊(duì)列中 */if (!synchronous || !(p->cpus_allowed & (1 << smp_processor_id())))reschedule_idle(p); /* 這種情況下調(diào)用wake_up(),synchronous總為0,此時(shí),*//* 如果本CPU不適合運(yùn)行該進(jìn)程,則需要調(diào)用reschedule_idle()尋找合適的CPU */success = 1;out:spin_unlock_irqrestore(&runqueue_lock, flags);return success; } |
?
這時(shí)啟動(dòng)schedule()就是被動(dòng)的了。
B. 被動(dòng)式
在系統(tǒng)調(diào)用執(zhí)行結(jié)束后,控制由核心態(tài)返回到用戶態(tài)之前,Linux都將在ret_from_sys_call入口檢查當(dāng)前進(jìn)程的need_resched值,如果該值為1,則調(diào)用schedule():
| /* 節(jié)選自[arch/i386/kernel/entry.S] */ENTRY(ret_from_sys_call)clicmpl $0,need_resched(%ebx) #ebx中存放著current指針jne reschedule……reschedule:call SYMBOL_NAME(schedule) jmp ret_from_sys_call #反復(fù)查詢need_resched |
?
因此,只需要設(shè)置當(dāng)前進(jìn)程(current)的need_resched,就有機(jī)會(huì)啟動(dòng)調(diào)度器。通常有如下幾種場(chǎng)合會(huì)設(shè)置need_resched:
- update_process_times(),由時(shí)鐘中斷觸發(fā),負(fù)責(zé)管理除0號(hào)進(jìn)程(idle進(jìn)程)以外的其他各個(gè)進(jìn)程的時(shí)間片消耗。如果當(dāng)前進(jìn)程(SCHED_FIFO實(shí)時(shí)進(jìn)程除外)的時(shí)間片用完了(counter==0),則設(shè)置need_resched為1;(注意:此時(shí)并不計(jì)算或重置counter值,這個(gè)工作在所有進(jìn)程的時(shí)間片都耗完以后在schedule()中進(jìn)行)
- reschedule_idle(),此函數(shù)的功能在"調(diào)度器工作流程"一節(jié)中已經(jīng)詳細(xì)描述了,不過,最經(jīng)常的調(diào)用者是在某一事件等待隊(duì)列上休眠的進(jìn)程的喚醒過程--wake_up_process()及其他一系列wake_up函數(shù)(見上"主動(dòng)式調(diào)度");
- sched_setscheduler()、sched_yield()系統(tǒng)調(diào)用,以及系統(tǒng)初始化(rest_init()中)、創(chuàng)建新進(jìn)程(do_fork()中)等從語(yǔ)義上就希望啟動(dòng)調(diào)度器工作的場(chǎng)合。
由于啟動(dòng)schedule()的時(shí)機(jī)實(shí)際上由當(dāng)前進(jìn)程決定,因此設(shè)置了need_resched并不意味著就能及時(shí)調(diào)度,這也是"Linux內(nèi)核不可搶占"的原因(詳見下"Linux 2.4調(diào)度系統(tǒng)的一些問題"之"內(nèi)核不可搶占")。
回頁(yè)首
六. 其他核心應(yīng)用的調(diào)度相關(guān)部分
系統(tǒng)中很多技術(shù)都和調(diào)度器相關(guān),這里僅就其中幾個(gè)稍作展開,并且不涉及該技術(shù)的細(xì)節(jié),僅就其中與調(diào)度器相關(guān)的部分進(jìn)行討論,假定讀者對(duì)于該技術(shù)有初步的了解。
1. IDLE進(jìn)程
系統(tǒng)最初的引導(dǎo)進(jìn)程(init_task)在引導(dǎo)結(jié)束后即成為cpu 0上的idle進(jìn)程。在每個(gè)cpu上都有一個(gè)idle進(jìn)程,正如上文所言,這些進(jìn)程登記在init_tasks[]數(shù)組中,并可用idle_task()宏訪問(見上"相關(guān)數(shù)據(jù)結(jié)構(gòu)")。idle進(jìn)程不進(jìn)入就緒隊(duì)列,系統(tǒng)穩(wěn)定后,僅當(dāng)就緒隊(duì)列為空的時(shí)候idle進(jìn)程才會(huì)被調(diào)度到。
init_task的task_struct是靜態(tài)配置的,定義在[include/linux/sched.h]中的INIT_TASK()宏中,其中與調(diào)度相關(guān)的幾個(gè)屬性分別是:
- state:TASK_RUNNING;
- counter:10*HZ/100;i386上大約100ms
- nice:0;缺省的優(yōu)先級(jí)
- policy:SCHED_OTHER;非實(shí)時(shí)進(jìn)程
- cpus_runnable:-1;全1,未在任何cpu上運(yùn)行
- cpus_allowed:-1;全1,可在任何cpu上運(yùn)行
在smp_init()中(實(shí)際上是在[arch/i386/kernel/smpboot.c]中的smp_boot_cpus()中),init_task的processor屬性被設(shè)為0,對(duì)應(yīng)的schedule_data也設(shè)置好相應(yīng)的值。在創(chuàng)建了一個(gè)核心線程用于執(zhí)行init()函數(shù)之后([/init/main.c]rest_init()),init_task設(shè)置自己的need_resched等于1,然后調(diào)用cpu_idle()進(jìn)入IDLE循環(huán)。
在cpu_idle()中,init_task的nice值被設(shè)為20(最低優(yōu)先級(jí)),counter為-100(無(wú)意義的足夠小),然后cpu_idle()進(jìn)入無(wú)限循環(huán):
| /* 節(jié)選自[arch/i386/kernel/processs.c] cpu_idle() */ while (1) {void (*idle)(void) = pm_idle;if (!idle)idle = default_idle;while (!current->need_resched)idle();schedule();check_pgt_cache(); } |
?
初始化過程中第一次執(zhí)行cpu_idle(),因need_resched為1,所以直接啟動(dòng)schedule()進(jìn)行第一次調(diào)度。如上文所述,schedule()會(huì)清掉need_resched位,因此,之后本循環(huán)都將執(zhí)行idle()函數(shù),直至need_resched再被設(shè)置為非0(比如在reschedule_idle()中,見上"調(diào)度器工作時(shí)機(jī)")。
idle()函數(shù)有三種實(shí)現(xiàn)可能:
- default_idle(),執(zhí)行hlt指令;
- poll_idle(),如果核心參數(shù)上定義了"idle=poll",則pm_idle會(huì)指向poll_idle(),它將need_resched設(shè)置為特殊的-1,然后反復(fù)循環(huán)直到need_resched不等于-1。因?yàn)閜oll_idle()采用更高效的指令,所以運(yùn)行效率比default_idle()要高;
- 電源管理相關(guān)的idle過程,例如APM和ACPI模塊中定義的idle過程。
因?yàn)閮H當(dāng)就緒隊(duì)列為空的時(shí)候才會(huì)調(diào)度到idle進(jìn)程,所以,只有在系統(tǒng)完全空閑時(shí)才會(huì)執(zhí)行check_pgt_cache()操作,清理頁(yè)表緩存。
2. 進(jìn)程創(chuàng)建
系統(tǒng)中除了init_task是手工創(chuàng)建的以外,其他進(jìn)程,包括其他CPU上的idle進(jìn)程都是通過do_fork()創(chuàng)建的,所不同的是,創(chuàng)建idle進(jìn)程時(shí)使用了CLONE_PID標(biāo)志位。
在do_fork()中,新進(jìn)程的屬性設(shè)置為:
- state:TASK_UNINTERRUPTIBLE
- pid:如果設(shè)置了CLONE_PID則與父進(jìn)程相同(僅可能為0),否則為下一個(gè)合理的pid
- cpus_runnable:全1;未在任何cpu上運(yùn)行
- processor:與父進(jìn)程的processor相同;子進(jìn)程在哪里創(chuàng)建就優(yōu)先在哪里運(yùn)行
- counter:父進(jìn)程counter值加1的一半;同時(shí)父進(jìn)程自己的counter也減半,保證進(jìn)程不能通過多次fork來偷取更多的運(yùn)行時(shí)間(同樣,在子進(jìn)程結(jié)束運(yùn)行時(shí),它的剩余時(shí)間片也將歸還給父進(jìn)程,以免父進(jìn)程因創(chuàng)建子進(jìn)程而遭受時(shí)間片的損失)
- 其他值與父進(jìn)程相同
子進(jìn)程通過SET_LINKS()鏈入進(jìn)程列表,然后調(diào)用wake_up_process()喚醒(見上"調(diào)度器工作時(shí)機(jī)")。
3. smp系統(tǒng)初始化
init_task在完成關(guān)鍵數(shù)據(jù)結(jié)構(gòu)初始化之后,在進(jìn)行硬件的初始化之前,會(huì)調(diào)用smp_init()對(duì)SMP系統(tǒng)進(jìn)行初始化。smp_init()調(diào)用smp_boot_cpus(),smp_boot_cpus()對(duì)每一個(gè)CPU都調(diào)用一次do_boot_cpu(),完成SMP其他CPU的初始化工作。
| /* 節(jié)選自[arch/i386/kernel/smpboot.c] do_boot_cpu() */if (fork_by_hand() < 0) /* do_fork(CLONE_VM|CLONE_PID)創(chuàng)建一個(gè)新進(jìn)程,與init_task一樣具有0號(hào)pid */panic("failed fork for CPU %d", cpu);idle = init_task.prev_task; /*在進(jìn)程列表中,新進(jìn)程總是位于init_task的左鏈prev上 */if (!idle)panic("No idle process for CPU %d", cpu);idle->processor = cpu;idle->cpus_runnable = 1 << cpu; /* 在指定CPU上運(yùn)行 */map_cpu_to_boot_apicid(cpu, apicid);idle->thread.eip = (unsigned long) start_secondary; /* 被調(diào)度到后的啟動(dòng)地址 */del_from_runqueue(idle); /* idle進(jìn)程不通過就緒隊(duì)列調(diào)度 */unhash_process(idle);init_tasks[cpu] = idle; /* 所有idle進(jìn)程都可通過init_tasks[]數(shù)組訪問 */ |
?
該進(jìn)程被調(diào)度到時(shí)即執(zhí)行start_secondary(),最終將調(diào)用cpu_idle(),成為IDLE進(jìn)程。
回頁(yè)首
七. Linux 2.4調(diào)度系統(tǒng)的一些問題
1. 進(jìn)程時(shí)間片
2.4內(nèi)核中進(jìn)程缺省時(shí)間片是根據(jù)以下公式計(jì)算的:
| /* 節(jié)選自[kernel/sched.c] */ #if HZ < 200 #define TICK_SCALE(x) ((x) >> 2) #elif HZ < 400 #define TICK_SCALE(x) ((x) >> 1) #elif HZ < 800 #define TICK_SCALE(x) (x) #elif HZ < 1600 #define TICK_SCALE(x) ((x) << 1) #else #define TICK_SCALE(x) ((x) << 2) #endif #define NICE_TO_TICKS(nice) (TICK_SCALE(20-(nice))+1) …… schedule() { …… p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice); …… } |
?
如上所述,時(shí)鐘中斷將不斷對(duì)當(dāng)前運(yùn)行的非IDLE進(jìn)程進(jìn)行時(shí)間片剩余值減1的操作,直至所有就緒隊(duì)列中的counter都減為0了,就在schedule()中對(duì)每個(gè)進(jìn)程(包括休眠進(jìn)程)利用上述公式執(zhí)行時(shí)間片的更新。其中在[include/asm-i386/param.h]中定義了HZ為100,而counter通常初值為0,nice缺省為0(nice在-20到19之間選擇),所以,i386下counter的缺省值為6,也就是大約60ms(時(shí)鐘中斷大約每10ms一次)。
同時(shí),對(duì)于休眠的進(jìn)程而言,其參與計(jì)算的counter非0,因此實(shí)際上它的counter是在累加,構(gòu)成一個(gè)等比數(shù)列COUNTER=COUNTER/2+k,1<k<=11,其最大值趨近于2*k,也就是說,2.4系統(tǒng)中進(jìn)程的時(shí)間片不會(huì)超過230ms。
因?yàn)榫途w進(jìn)程選取算法中counter的值占很大比重(見"就緒進(jìn)程選擇算法"),因此,這種對(duì)于休眠進(jìn)程時(shí)間片疊加的做法體現(xiàn)了Linux傾向于優(yōu)先執(zhí)行休眠次數(shù)比較多,也就是IO密集(IO-bound)的進(jìn)程。
Linux設(shè)計(jì)者最初是希望因此而提高交互式進(jìn)程的響應(yīng)速度,從而方便終端用戶,但I(xiàn)O密集的進(jìn)程并不一定就是交互式進(jìn)程,例如數(shù)據(jù)庫(kù)操作需要頻繁地讀寫磁盤,從而經(jīng)常處于休眠狀態(tài),動(dòng)態(tài)優(yōu)先級(jí)通常較高,但這種應(yīng)用并不需要用戶交互,所以它反而影響了真正的交互動(dòng)作的響應(yīng)。
時(shí)間片的長(zhǎng)度對(duì)系統(tǒng)性能影響也很大。如果太短,進(jìn)程切換就會(huì)過于頻繁,開銷很大;如果太長(zhǎng),系統(tǒng)響應(yīng)就會(huì)太慢,Linux的策略是在系統(tǒng)響應(yīng)不至于太慢的前提下讓時(shí)間片盡可能地長(zhǎng)。
2. 內(nèi)核不可搶占
從上面的分析我們可以看到,schedule()是進(jìn)行進(jìn)程切換的唯一入口,而它的運(yùn)行時(shí)機(jī)很特殊。一旦控制進(jìn)入核心態(tài),就沒有任何辦法可以打斷它,除非自己放棄cpu。一個(gè)最典型的例子就是核心線程中如果出現(xiàn)死循環(huán)(只要循環(huán)中不調(diào)用schedule()),系統(tǒng)就會(huì)失去響應(yīng),此時(shí)各種中斷(包括時(shí)鐘中斷)仍然在響應(yīng),但卻不會(huì)發(fā)生調(diào)度,其他進(jìn)程(包括核心進(jìn)程)都沒有機(jī)會(huì)運(yùn)行。
下面給出的是中斷返回的代碼:
| /* 節(jié)選自[arch/i386/entry.S] */ ENTRY(ret_from_intr)GET_CURRENT(%ebx) #將current指針存到ebx寄存器中備用 ret_from_exception:movl EFLAGS(%esp),%eax #取EFLAGS中的VM_MASK位判斷是否處于VM86模式movb CS(%esp),%al #取CS低兩位判斷是否處于用戶態(tài)testl $(VM_MASK | 3),%eax jne ret_from_sys_call #如果處于VM86模式或者處于用戶態(tài),就從ret_from_sys_call入口返回, #否則直接返回jmp restore_all |
?
這是此時(shí)唯一可能調(diào)用schedule()的地方(通過ret_from_sys_call,見"調(diào)度器工作時(shí)機(jī)"),但普通的核心線程不屬于任何一種要求的狀態(tài),它能響應(yīng)中斷,但不能導(dǎo)致調(diào)度。
這個(gè)特點(diǎn)的表現(xiàn)之一就是,高優(yōu)先級(jí)的進(jìn)程無(wú)法打斷正在核內(nèi)執(zhí)行系統(tǒng)調(diào)用(或者中斷服務(wù))的低優(yōu)先級(jí)進(jìn)程,這對(duì)于實(shí)時(shí)系統(tǒng)來說是致命的,但卻簡(jiǎn)化了核心代碼。內(nèi)核中很多地方都利用了這一特點(diǎn),能夠不做過多保護(hù)地訪問共享數(shù)據(jù),而不用擔(dān)心其他進(jìn)程的打擾。
3. 實(shí)時(shí)性能
Linux 2.4通過就緒進(jìn)程選擇算法的設(shè)計(jì)區(qū)分實(shí)時(shí)進(jìn)程和非實(shí)時(shí)進(jìn)程,只要有實(shí)時(shí)進(jìn)程可運(yùn)行,非實(shí)時(shí)進(jìn)程就不會(huì)獲得運(yùn)行機(jī)會(huì)。Linux又將實(shí)時(shí)進(jìn)程分為SCHED_RR和SCHED_FIFO兩類。SCHED_RR時(shí)間片結(jié)束后會(huì)發(fā)生調(diào)度,并將自己置于就緒隊(duì)列的末尾,從而給其他rt_priority相同(或更高)的實(shí)時(shí)進(jìn)程運(yùn)行機(jī)會(huì)(見"調(diào)度器工作流程"),而SCHED_FIFO不會(huì)因時(shí)間片結(jié)束而放棄CPU(見"調(diào)度器工作時(shí)機(jī)"),或者出現(xiàn)更高優(yōu)先級(jí)的實(shí)時(shí)進(jìn)程,或者主動(dòng)放棄CPU,否則SCHED_FIFO將運(yùn)行到進(jìn)程結(jié)束。
盡管Linux 2.4中區(qū)分了實(shí)時(shí)進(jìn)程和非實(shí)時(shí)進(jìn)程的調(diào)度優(yōu)先權(quán),但也僅此而已。不支持核心搶占運(yùn)行的操作系統(tǒng)很難實(shí)現(xiàn)真正的實(shí)時(shí)性,因?yàn)閷?shí)時(shí)任務(wù)的響應(yīng)時(shí)間無(wú)法預(yù)測(cè)。有兩種辦法使系統(tǒng)的實(shí)時(shí)性更好,一種是采用設(shè)置類似搶占調(diào)度點(diǎn)的做法,一種就是使內(nèi)核真正具備可搶占性。
即使是內(nèi)核可搶占的系統(tǒng),也并不一定滿足實(shí)時(shí)性要求,它僅僅解決了CPU資源的訪問優(yōu)先權(quán)問題,其他資源也同樣需要"被搶占",例如實(shí)時(shí)進(jìn)程應(yīng)該能夠從握有某個(gè)共享資源的普通進(jìn)程手中奪得它所需要的資源,它使用完后再還給普通進(jìn)程。但實(shí)際上,很多系統(tǒng)都無(wú)法做到這一點(diǎn),Linux的調(diào)度器更是不具備這種能力。
4. 多處理機(jī)系統(tǒng)中的局限性
Linux的調(diào)度器原本是針對(duì)單處理機(jī)系統(tǒng)設(shè)計(jì)的,在內(nèi)核發(fā)展過程中,不斷通過補(bǔ)丁來提高多處理機(jī)系統(tǒng)(主要是SMP系統(tǒng))的執(zhí)行效率。這種開發(fā)方式一直持續(xù)到2.4版本,因此在2.4內(nèi)核中,SMP應(yīng)用仍然有很多無(wú)法突破的障礙,例如全局共享的就緒隊(duì)列。很多研究團(tuán)體都在針對(duì)Linux調(diào)度器的多處理機(jī)擴(kuò)展性作研究,參考文獻(xiàn)中列舉了其中兩個(gè)[5][6],但最權(quán)威的改進(jìn)還是在2.6內(nèi)核中。
對(duì)于超線程CPU,Linux調(diào)度器的支持有限,它可以區(qū)分同一物理CPU上的兩個(gè)邏輯CPU,在兩個(gè)邏輯CPU都空閑的時(shí)候,調(diào)度器可以優(yōu)先考慮將進(jìn)程調(diào)度到其中一個(gè)邏輯CPU上運(yùn)行(見"調(diào)度器工作流程")。從原理上說,超線程CPU是存在兩個(gè)(或多個(gè))執(zhí)行現(xiàn)場(chǎng)的單CPU,只有兩個(gè)使用CPU不同部件(比如定點(diǎn)部件和浮點(diǎn)部件)的線程在其上運(yùn)行的時(shí)候才有正的加速,否則,由于執(zhí)行部件沖突以及Cache miss,使用超線程技術(shù)甚至?xí)硪欢ǔ潭壬系男阅軗p失。Linux 2.4的調(diào)度器并不能區(qū)分哪些進(jìn)程是"類似"的,哪些進(jìn)程會(huì)使用不同的執(zhí)行部件,因此,實(shí)際上無(wú)法恰當(dāng)使用超線程CPU。 對(duì)于其他更復(fù)雜的多處理機(jī)系統(tǒng),例如目前高端系統(tǒng)中占統(tǒng)治地位的NUMA結(jié)構(gòu)機(jī)器,Linux在調(diào)度器上基本未作考慮。例如進(jìn)程(線程)總優(yōu)先在創(chuàng)建它的CPU上運(yùn)行(見"其他核心應(yīng)用的調(diào)度相關(guān)部分"之"進(jìn)程創(chuàng)建"),并傾向于保持在該CPU上(見"就緒進(jìn)程選擇算法"),整個(gè)CPU選擇過程沒有做任何局部性優(yōu)化。
回頁(yè)首
八. 后記
調(diào)度系統(tǒng)的表現(xiàn)關(guān)系到整個(gè)系統(tǒng)的性能,Linux的應(yīng)用目前主要集中在低端服務(wù)器系統(tǒng)和桌面系統(tǒng),將來很可能向高端服務(wù)器市場(chǎng)和嵌入式系統(tǒng)發(fā)展,這就要求調(diào)度系統(tǒng)有大的改動(dòng)。在新的Linux內(nèi)核2.6版本中,調(diào)度器的改動(dòng)是最引人注目的,它一方面提供了核心可搶占的支持,另一方面又對(duì)多處理機(jī)系統(tǒng)上的表現(xiàn)進(jìn)行了優(yōu)化。在熟悉了2.4的調(diào)度系統(tǒng)之后,我們將分析2.6中調(diào)度器的表現(xiàn)。
?
參考資料
[1][Linus Torvalds,2003]?
Linux內(nèi)核源碼v2.4.21
[2][Daniel P. Bovet, Marco Cesati,2002]?
Understanding the Linux Kernel, 2nd Edition,O'Reilly
[3][Moshe Bar,2000]?
Kernel Korner: The Linux Scheduler,Linux Journal
[4][Paul Bemowsky,2003]?
Hyper-Threading Linux,Linux World
[5][Mike Kravetz,2001]?
Enhancing Linux Scheduler Scalability,IBM Linux Tech. Center
[6][Chris King, Scott Lathrop, Steve Molloy, Paul Moore,2001]?
ELSC : Scalable Linux Scheduling on a Symmetric Multi-Processor Machine,University of Michigan
轉(zhuǎn)載于:https://www.cnblogs.com/davidwang456/p/3592502.html
總結(jié)
以上是生活随笔為你收集整理的Linux 2.4调度系统分析--转的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于Linux的集群系统(八)--转
- 下一篇: 对request.getSession(