本文的寫(xiě)作過(guò)程主要參考《Linux內(nèi)核源代碼情景分析》的內(nèi)容,結(jié)合自己的理解,重點(diǎn)討論Linux內(nèi)核如何進(jìn)行過(guò)程轉(zhuǎn)換。參考的代碼是linux-2.4.16,與書(shū)中使用的代碼版本略有不同,但不影響對(duì)問(wèn)題的理解。
我們?cè)诳紤]流程調(diào)度問(wèn)題時(shí)會(huì)涉及以下三個(gè)問(wèn)題。
調(diào)度的時(shí)機(jī):什么情況,什么時(shí)候調(diào)度;調(diào)度的戰(zhàn)略:選擇要運(yùn)行的下一個(gè)進(jìn)程的標(biāo)準(zhǔn):調(diào)度方法:對(duì)正在運(yùn)行的進(jìn)程是否采取“強(qiáng)制剝奪”或“自愿返回”執(zhí)行權(quán)。第一,調(diào)度時(shí)間。
自愿的日程可以隨時(shí)進(jìn)行。在用戶空間中,可以通過(guò)系統(tǒng)調(diào)用屬于應(yīng)用層庫(kù)函數(shù)的pause()或sleep函數(shù)來(lái)實(shí)現(xiàn)此目的。這個(gè)函數(shù)的基礎(chǔ)最終是通過(guò)系統(tǒng)調(diào)用nanosleep()來(lái)實(shí)現(xiàn)的。在內(nèi)核空間中,可以通過(guò)schedule()啟動(dòng)一次調(diào)度。當(dāng)然,在開(kāi)始之前,您可以將程序的狀態(tài)設(shè)定為T(mén)ASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE,暫時(shí)放棄執(zhí)行并睡覺(jué)。
另外,日程可能是非自愿的。也就是說(shuō),強(qiáng)制進(jìn)程(系統(tǒng)調(diào)用、中斷或異常處理)即將返回到用戶空間。也就是說(shuō),當(dāng)CPU在內(nèi)核上運(yùn)行時(shí),不考慮強(qiáng)制調(diào)度的可能性,系統(tǒng)空間中發(fā)生的中斷或異常也是可能的,但這不會(huì)導(dǎo)致調(diào)度。主要是早期內(nèi)核依賴這個(gè)原則來(lái)簡(jiǎn)化設(shè)計(jì)和實(shí)現(xiàn)。
注:用戶進(jìn)程從內(nèi)核空間返回到用戶空間只是調(diào)度發(fā)生的必要條件,不是充分條件。可以參考Arch/i386/kernel中的以下代碼:
系統(tǒng)調(diào)用返回處理宏
僅當(dāng)當(dāng)前進(jìn)程的task_struct結(jié)構(gòu)中的need_resched字段不為零時(shí),才轉(zhuǎn)到reschedule并調(diào)用schedule()。只有內(nèi)核需要喚醒進(jìn)程時(shí),才會(huì)設(shè)置此字段。
二、調(diào)度方法
Linux內(nèi)核的調(diào)度方式可以說(shuō)是“有條件的可剝奪”。進(jìn)程在用戶空間中運(yùn)行時(shí),無(wú)論是否自愿(例如,運(yùn)行時(shí)間足夠長(zhǎng)),內(nèi)核都可以暫時(shí)剝奪進(jìn)程的運(yùn)行。但是,進(jìn)入內(nèi)核空間后,將進(jìn)入超級(jí)管理程序模式。我知道內(nèi)核需要調(diào)度,但在進(jìn)程返回用戶空間之前,這種情況實(shí)際上不會(huì)發(fā)生。
三、調(diào)度策略
調(diào)度策略是基于優(yōu)先級(jí)的調(diào)度,內(nèi)核計(jì)算反映每個(gè)進(jìn)程的執(zhí)行“權(quán)利”的權(quán)重,然后選擇權(quán)重最高的進(jìn)程來(lái)執(zhí)行。在運(yùn)行過(guò)程中,流程的資格會(huì)隨著時(shí)間的推移而減少,下一個(gè)時(shí)間表中原始資格較低的可能會(huì)優(yōu)先運(yùn)行。
但是,為了滿足多種應(yīng)用程序的需求,實(shí)施了三種戰(zhàn)略:SCHED_FIFO、SCHED_RR和SCHED_OTHER。每個(gè)進(jìn)程都有自己的策略,可以通過(guò)系統(tǒng)調(diào)用sched_setscheduler()來(lái)設(shè)置自己的調(diào)度策略。
其中SCHED_FIFO適用于時(shí)間要求相對(duì)較強(qiáng),但每次運(yùn)行所需時(shí)間相對(duì)較短的進(jìn)程。SCHED_RR是輪詢策略,非常適合于相對(duì)較大的進(jìn)程,即每次運(yùn)行需要很長(zhǎng)時(shí)間的進(jìn)程。此外,SCHED_OTHER是適用于交互式分時(shí)應(yīng)用程序的現(xiàn)有策略。
四、深入明細(xì)表函數(shù)
1、分析此函數(shù)之前,必須了解task_struct的宏定義,該定義從下一個(gè)內(nèi)核獲取當(dāng)前進(jìn)程。
獲取當(dāng)前的task_struct
在內(nèi)核空間中,進(jìn)程描述符存儲(chǔ)在8k大小的進(jìn)程堆棧頂部(低地址,英特爾系統(tǒng)的堆棧從高地址擴(kuò)展到低地址),而esp寄存器保留當(dāng)前進(jìn)程的堆棧基礎(chǔ)地址,因此,將esp上指針的低13位(8k)清空為0,就可以獲得當(dāng)前進(jìn)程描述符的地址。
2.進(jìn)程的虛擬地址空間分為兩部分:用戶狀態(tài)程序可以訪問(wèn)的虛擬地址空間和內(nèi)核可以訪問(wèn)的內(nèi)核空間。每當(dāng)內(nèi)核執(zhí)行上下文切換時(shí),虛擬地址空間的用戶層部分都會(huì)切換,使當(dāng)前運(yùn)行的進(jìn)程相匹配,內(nèi)核空間不會(huì)切換。Task_struct流程描述符包含與流程地址空間相關(guān)的兩個(gè)字段mm:active _ mm。對(duì)于最終用戶進(jìn)程,mm是指虛擬地址空間的用戶空間部分;對(duì)于內(nèi)核線程,mm是NULL。
這為遵循所謂的惰性TLB處理(lazy TLB handing)進(jìn)行優(yōu)化提供了余地。Active_mm主要用于優(yōu)化。內(nèi)核線程與特定的用戶層進(jìn)程無(wú)關(guān),因此內(nèi)核不需要切換虛擬地址空間的用戶層部分,因此不需要保留以前的設(shè)置。內(nèi)核線程以前可能正在運(yùn)行所有用戶層進(jìn)程,因此用戶空間部分的內(nèi)容本質(zhì)上是隨機(jī)的,內(nèi)核線程不應(yīng)修改內(nèi)容,因此如果將mm設(shè)置為NULL并切換到用戶進(jìn)程,內(nèi)核會(huì)將原始進(jìn)程的mm存儲(chǔ)在新的內(nèi)核線程中。
的active_mm中,因?yàn)槟承r(shí)候內(nèi)核必須知道用戶空間當(dāng)前包含了什么。3、這就是剛開(kāi)始環(huán)境檢查中,active_mm不能為空的原因,否則,報(bào)出異常。
#define BUG() __asm__ __volatile__(".byte 0x0f,0x0b")
異常的產(chǎn)生是讓CPU去執(zhí)行兩個(gè)字節(jié)的非法指令,這會(huì)產(chǎn)生一次“invalid_op”異常,使得CPU捕獲并執(zhí)行do_invalid_op()。
4、繼續(xù)往下看,這里的counter表示當(dāng)前進(jìn)程的運(yùn)行時(shí)間配額,其數(shù)值在每次時(shí)鐘中斷的時(shí)候都要遞減,這一操作是在update_process_times()中進(jìn)行的。當(dāng)該值遞減到0,就要從runqueue隊(duì)列中移動(dòng)到末尾,同時(shí)恢復(fù)最初的時(shí)間配額。
到期移動(dòng)到隊(duì)尾
5、當(dāng)前進(jìn)程就是正在運(yùn)行的進(jìn)程,可是當(dāng)進(jìn)入到schedule()時(shí)其狀態(tài)不一定是TASK_RUNNING,比如當(dāng)前進(jìn)程在do_exit()中將狀態(tài)改成TASK_ZOMBLE,又比如當(dāng)前進(jìn)程在sys_wait4()中設(shè)置為T(mén)ASK_INTERRUPTIBLE。可以理解當(dāng)前進(jìn)程的意愿不是繼續(xù)運(yùn)行,則需要從隊(duì)列中撤下來(lái)。
進(jìn)程撤出運(yùn)行等待隊(duì)列
這里也可以看出,可中斷進(jìn)程和不可中斷進(jìn)程在睡眠狀態(tài)的區(qū)別,前者有信號(hào)等待處理的時(shí)候,還是運(yùn)行狀態(tài),要讓其處理完這些信號(hào)后再說(shuō),而后者則不受信號(hào)的影響。
6、這時(shí),就要挑選最佳的候選進(jìn)程了,c是這個(gè)進(jìn)程的綜合權(quán)值。挑選從idle進(jìn)程即0號(hào)進(jìn)程開(kāi)始,然后遍歷runqueue隊(duì)列,通過(guò)goodness計(jì)算每個(gè)進(jìn)程的權(quán)值,和最高的c進(jìn)行比較,至于選擇的計(jì)算規(guī)則這里就不深入去看了。
選擇下個(gè)進(jìn)程
從這里我們可以理解為什么當(dāng)系統(tǒng)任務(wù)較少,負(fù)載較低的時(shí)候,基本上都是運(yùn)行的idle進(jìn)程,也就是top命令中idle指數(shù)偏高的原因。
7、到這里,略過(guò)其他的一些細(xì)節(jié),實(shí)際上只剩下兩件事情,其一是對(duì)用戶虛擬空間的處理,其二就是進(jìn)程的切換switch_to()。
a、先來(lái)看對(duì)用戶空間的處理,這里補(bǔ)充了兩個(gè)變量mm和oldmm,內(nèi)核的設(shè)計(jì)和實(shí)現(xiàn)不允許active_mm指針為0,因?yàn)閳?zhí)行頁(yè)面映射的目錄指針就在這個(gè)數(shù)據(jù)結(jié)構(gòu)中。所以, 如果為0,就需要在進(jìn)入運(yùn)行時(shí)想被切換出去的進(jìn)程借用一個(gè)mm_struct,可行的原因是所需的只是系統(tǒng)空間的映射,而所有進(jìn)程的系統(tǒng)空間的映射都是相同的。在下次調(diào)度其他進(jìn)程運(yùn)行的時(shí)候,這個(gè)內(nèi)核線程被切換出去的時(shí)候歸還,這里的mmdrop()只是將共享計(jì)數(shù)減一,而不是真正的釋放。
進(jìn)行切換的最后處理
由于新的進(jìn)程有自己的用戶空間,所以就要通過(guò)switch_mm()進(jìn)行用戶空間的切換。這個(gè)函數(shù)的關(guān)鍵語(yǔ)句就一行,它將進(jìn)程的頁(yè)面起始物理地址裝入到CR3寄存器中。
asm volatile("movl %0,%%cr3": :"r" (__pa(next->pgd)));
b、現(xiàn)在就是最后的切換進(jìn)程的處理了,由宏操作switch_to()完成, 代碼在include/asm-i386中:
進(jìn)程切換操作
這個(gè)匯編還是有點(diǎn)難理解的,我們梳理一下:
方向 | 標(biāo)號(hào) | 寄存器 | 結(jié)合值 |
Out | %0 | m | prev-> |
Out | %1 | m | prev-> |
Out | %2 | ebx | last |
In | %3 | m | next-> |
In | %4 | m | next-> |
In | %5 | eax | prev |
In | %6 | edx | next |
In | %7 | ebx | prev |
先來(lái)看開(kāi)頭三條push指令和結(jié)尾的三條pop指令,看起來(lái)很一般,卻暗藏玄機(jī)。且看第20行,先將prev的esp保存,第21行將next的esp置入ESP寄存器,這兩條完成了棧的切換。也就是說(shuō),從21行開(kāi)始當(dāng)前進(jìn)程已經(jīng)是next了,為什么呢,我們獲取task_struct在文章剛開(kāi)始講過(guò)了,是宏定義從當(dāng)前的esp中計(jì)算得出的, 此時(shí)引用current宏已經(jīng)是next的
task_struct數(shù)據(jù)了。
但是要執(zhí)行起來(lái),還需要指令寄存器的支持,第22行就是將當(dāng)前進(jìn)程下次的執(zhí)行點(diǎn)給保存,這個(gè)點(diǎn)就是講事先當(dāng)前入棧的數(shù)據(jù)恢復(fù)出來(lái);然后第23行將next的eip給置位,就可以通過(guò)jmp調(diào)用__switch_to(), 這里為什么不使用call指令,因?yàn)閏all調(diào)用會(huì)產(chǎn)生壓棧破壞了當(dāng)前設(shè)置的寄存器,直接跳轉(zhuǎn)過(guò)去,最后返回的時(shí)候就是剛剛壓棧的next的eip,也就是標(biāo)號(hào)“1f”所在的地址。
如果讀者不是十分清楚這個(gè)過(guò)程,最好自己畫(huà)一下堆棧的變化,注意,這里有兩個(gè)堆棧,在這個(gè)過(guò)程中,有一個(gè)時(shí)期esp和ebp并不在同一個(gè)堆棧上,要格外注意這個(gè)時(shí)期里所有涉及堆棧的操作分別是在哪個(gè)堆棧上進(jìn)行的。記住一個(gè)簡(jiǎn)單的原則即可,pop/push這樣的操作,都是對(duì)esp所指向的堆棧進(jìn)行的,這些操作同時(shí)也會(huì)改變esp本身,除此之外,其它關(guān)于變量的引用,都是對(duì)ebp所指向的堆棧進(jìn)行的。
8、__switch_to的邏輯
這里的主要是TSS, 核心是將TSS中的內(nèi)核空間(0級(jí))堆棧指針換成next->esp()。
其次是將段寄存器的fs和gs的內(nèi)容也做了相應(yīng)的切換。至于CPU為debug而設(shè)置的一些寄存器,以及說(shuō)明I/O操作權(quán)限的位圖,暫時(shí)不是這篇文章關(guān)心的了。
總之,新舊進(jìn)程的交接點(diǎn)就在switch_to()這段代碼中。switch_to只是個(gè)普通的宏,但是卻能實(shí)現(xiàn)進(jìn)程的切換,很多人對(duì)此比較費(fèi)解。為了正確的理解,大家需要注意:這些代碼是所有進(jìn)程共用的,代碼本身不屬于某一個(gè)特定的進(jìn)程,所以判定當(dāng)前在哪一個(gè)進(jìn)程不是通過(guò)看執(zhí)行的代碼是哪個(gè)進(jìn)程的,而是通過(guò)esp指向哪個(gè)進(jìn)程的堆棧來(lái)判定的。所以,對(duì)于上面圖中的切換點(diǎn)也可以這樣理解,在這一點(diǎn)處,esp指向了其它進(jìn)程的堆棧,當(dāng)前進(jìn)程即被掛起,等待若干時(shí)間,當(dāng)esp指針再次指回這個(gè)進(jìn)程的堆棧時(shí),這個(gè)進(jìn)程又重新開(kāi)始運(yùn)行。
1.《【重新隨機(jī)進(jìn)程】源代碼分析Linux內(nèi)核進(jìn)程切換》援引自互聯(lián)網(wǎng),旨在傳遞更多網(wǎng)絡(luò)信息知識(shí),僅代表作者本人觀點(diǎn),與本網(wǎng)站無(wú)關(guān),侵刪請(qǐng)聯(lián)系頁(yè)腳下方聯(lián)系方式。
2.《【重新隨機(jī)進(jìn)程】源代碼分析Linux內(nèi)核進(jìn)程切換》僅供讀者參考,本網(wǎng)站未對(duì)該內(nèi)容進(jìn)行證實(shí),對(duì)其原創(chuàng)性、真實(shí)性、完整性、及時(shí)性不作任何保證。
3.文章轉(zhuǎn)載時(shí)請(qǐng)保留本站內(nèi)容來(lái)源地址,http://f99ss.com/gl/2557931.html