帶你玩轉(zhuǎn)Linux內(nèi)核進(jìn)程創(chuàng)建-fork背后隱藏的技術(shù)細(xì)節(jié)(二)
上一篇帶你玩轉(zhuǎn)Linux內(nèi)核進(jìn)程創(chuàng)建-fork背后隱藏的技術(shù)細(xì)節(jié)(一)文章我們講到fork的時(shí)候內(nèi)存管理相關(guān)的內(nèi)容,時(shí)間大概隔了快一周了,發(fā)布下篇文章,寫文章確實(shí)費(fèi)時(shí)費(fèi)力,需要仔細(xì)推敲,原創(chuàng)不易,希望大家多多支持吧。本文講解fork的時(shí)候進(jìn)程管理相關(guān)的內(nèi)容,主要講解fork的時(shí)候進(jìn)程如何組裝調(diào)用相關(guān)的基礎(chǔ)設(shè)施組件,以及如何加入運(yùn)行隊(duì)列的,調(diào)度執(zhí)行的時(shí)候究竟會(huì)發(fā)生什么。
注:這里只講解cfs調(diào)度類,主要關(guān)注用戶任務(wù)
二、fork的進(jìn)程管理
2.1進(jìn)程相關(guān)基礎(chǔ)設(shè)施構(gòu)建
我們移步到如下調(diào)用路徑(當(dāng)前處于sched_fork函數(shù)中):
正如源代碼中的注釋一樣,在這里進(jìn)程調(diào)度相關(guān)的設(shè)置,以及分配cpu給進(jìn)程,但是請(qǐng)記?。悍峙渫阠pu后進(jìn)程并沒有參與調(diào)度執(zhí)行。
首先需要說明的一點(diǎn)是,進(jìn)程的task_struct是資源封裝和管理的結(jié)構(gòu),如管理進(jìn)程的虛擬內(nèi)存mm_struct,進(jìn)程的打開文件files_struct等,而進(jìn)程參與調(diào)度使用的是調(diào)度實(shí)體去管理調(diào)度(對(duì)于普通的進(jìn)程是sched_entity)。
所以在sched_fork函數(shù)中調(diào)用__sched_fork先來初始化,基本上都是一些清零操作:
然后設(shè)置了一些比較重要的一些屬性:
可以看出這里主要設(shè)置了一些調(diào)度相關(guān)的屬性:如調(diào)度優(yōu)先級(jí)(一般設(shè)置為nice為0),調(diào)度策略為SCHED_NORMAL,調(diào)度類為公平調(diào)度類,進(jìn)程權(quán)重信息等。
然后設(shè)置新的進(jìn)程在當(dāng)前cpu上。
接下來就調(diào)用了調(diào)度類的task_fork進(jìn)行設(shè)置虛擬運(yùn)行時(shí)間等(注意在task_fork_fair中會(huì)將設(shè)置的vruntime減去當(dāng)前cpu運(yùn)行cfs隊(duì)列的最小min_vruntime,喚醒的時(shí)候會(huì)加上所在cpu運(yùn)行隊(duì)列的min_vruntime)。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個(gè)人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。?!前100名進(jìn)群領(lǐng)取,額外贈(zèng)送一份價(jià)值699的內(nèi)核資料包(含視頻教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)? ??


上面構(gòu)建好調(diào)度基礎(chǔ)設(shè)施之后,接下來需要設(shè)置異常返回時(shí)的現(xiàn)場以及調(diào)度現(xiàn)場信息,使得進(jìn)程能夠返回正確的位置執(zhí)行:
copy_thread這個(gè)函數(shù)對(duì)于進(jìn)程調(diào)度來說至關(guān)重要,決定進(jìn)程第一次被調(diào)度的時(shí)候執(zhí)行哪個(gè)代碼,決定fork調(diào)用的返回值。寫到這里不得不提到兩個(gè)相關(guān)重要的兩個(gè)結(jié)構(gòu)體:pt_regs和cpu_context,他倆都是處理器架構(gòu)相關(guān)的結(jié)構(gòu)。
pt_regs描述的發(fā)生異常的時(shí)候保存的現(xiàn)場信息,主要是一些通用寄存器,我們這里稱為異?,F(xiàn)場:
當(dāng)異常發(fā)生時(shí),異常的現(xiàn)場(通用寄存器的內(nèi)容,如發(fā)生異常時(shí)的x0-x30,sp, pc, pstate)會(huì)被壓到內(nèi)核棧,通過pt_regs結(jié)構(gòu)來描述,而當(dāng)異常處理結(jié)束的時(shí)候,會(huì)需要恢復(fù)現(xiàn)場,將這些保存的值恢復(fù)到通用寄存器中。
cpu_context描述的是進(jìn)程調(diào)度的時(shí)候需要保存的進(jìn)程上下文,我們這里成為調(diào)度現(xiàn)場:
當(dāng)進(jìn)程切換的時(shí)候,會(huì)將處理器的當(dāng)前需要保存的寄存器保存到前一個(gè)進(jìn)程的tsk的thread.cpu_context中,并將后一個(gè)即將要調(diào)度的進(jìn)程的上下文從tsk的thread.cpu_context中恢復(fù)到相應(yīng)的寄存器,就完成了處理器狀態(tài)的切換(如前一個(gè)進(jìn)程的pc和sp的位置被保存起來,后一個(gè)進(jìn)程的pc和sp的位置恢復(fù)到相關(guān)寄存器);
介紹完了這倆結(jié)構(gòu)體,就可以在這兩個(gè)結(jié)構(gòu)體上做手腳,但是我們需要明確的是:
pt_regs和cpu_context都是處理器架構(gòu)相關(guān)的結(jié)構(gòu)。
pt_regs是發(fā)生異常時(shí)(當(dāng)然包括中斷)保存的處理器現(xiàn)場,用于異常處理完后來恢復(fù)現(xiàn)場,就好像沒有發(fā)生異常一樣,它保存在進(jìn)程內(nèi)核棧中。
cpu_context是發(fā)生進(jìn)程切換時(shí),保存當(dāng)前進(jìn)程的上下文,保存在當(dāng)前進(jìn)程的進(jìn)程描述符中。
pt_regs表征發(fā)生異常時(shí)處理器現(xiàn)場,cpu_context發(fā)生調(diào)度時(shí)當(dāng)前進(jìn)程的處理器現(xiàn)場。
ok,下面就可以在fork中做一些手腳:首先先將p->thread.cpu_context清零,然后對(duì)于用戶進(jìn)程和內(nèi)核線程有不同的處理:
上面以及做了注釋,需要說明的是:
我們沒有看到當(dāng)創(chuàng)建用戶任務(wù)的時(shí)候,異常返回后處理器的狀態(tài),實(shí)際上不需要設(shè)置,因?yàn)槲覀兪峭ㄟ^fork系統(tǒng)調(diào)用的方式陷入內(nèi)核,發(fā)生svc異常的時(shí)候,處理器的狀態(tài)已經(jīng)保存好了,已經(jīng)是el0(PSR_MODE_EL0t)。
childregs->regs[0] = 0;的設(shè)置保證了,子進(jìn)程被調(diào)度返回用戶空間的時(shí)候,fork的返回值為0,這就是為何fork返回值為0表示是子進(jìn)程的原因。
如果創(chuàng)建的是子進(jìn)程,那么就直接和父進(jìn)程寫時(shí)復(fù)制方式共享用戶棧,而棧不需要在進(jìn)行設(shè)置,直接使用父進(jìn)程的。
最后兩句,來設(shè)置的是進(jìn)程切換時(shí),子進(jìn)程的pc和sp,當(dāng)子進(jìn)程第一次被調(diào)度的時(shí)候,從ret_from_fork開始執(zhí)行指令,棧指針指向childregs,即為設(shè)置后pt_regs。
2.3子進(jìn)程被喚醒
前面已經(jīng)為子進(jìn)程的調(diào)度做好了一些數(shù)據(jù)結(jié)構(gòu)的準(zhǔn)備,但是子進(jìn)程并沒有被調(diào)度執(zhí)行,那么何時(shí)開始被喚醒呢?我們回退到kernel_clone中,copy_process做了一些資源的復(fù)制之后,開始喚醒子進(jìn)程:
這里面做了幾步非常重要的操作:
設(shè)置進(jìn)程狀態(tài)為TASK_RUNNING。
通過__set_task_cpu為子進(jìn)程選擇空閑的cpu,有可能不是當(dāng)前的cpu(進(jìn)程創(chuàng)建的時(shí)候是做負(fù)載均衡最好的時(shí)機(jī),這個(gè)時(shí)候進(jìn)程在cpu的cache還沒有數(shù)據(jù))。
activate_task來將進(jìn)程加入到選擇的cpu的運(yùn)行隊(duì)列,這里加入到選擇cpu的紅黑樹。
check_preempt_curr就會(huì)檢查是否能夠搶占所在cpu的當(dāng)前進(jìn)程,這是創(chuàng)建進(jìn)程時(shí)發(fā)生搶占的一個(gè)時(shí)機(jī)。
wake_up_new_task執(zhí)行完之后,子進(jìn)程就已經(jīng)在所選擇的cpu的運(yùn)行隊(duì)列了,也已經(jīng)是TASK_RUNNING狀態(tài),等待調(diào)度器在合適的調(diào)度時(shí)機(jī)選擇他。
其實(shí),在這里我們也能看的,喚醒的實(shí)質(zhì)是:將進(jìn)程的狀態(tài)設(shè)置TASK_RUNNING(調(diào)度器只選擇TASK_RUNNING的進(jìn)程),加入到cpu的運(yùn)行隊(duì)列(根據(jù)調(diào)度類加入到cpu的不同的調(diào)度隊(duì)列,這里只是一種形象的說法,實(shí)際上不一定是隊(duì)列,如:cfs類進(jìn)程加入到紅黑樹),然后做喚醒搶占檢查。
2.4子進(jìn)程被選擇調(diào)度
走到這里,子進(jìn)程已經(jīng)被放置到了cpu的運(yùn)行隊(duì)列,已經(jīng)具備調(diào)度條件,萬事具備只欠東風(fēng),這個(gè)東風(fēng)就是在何時(shí)的時(shí)候調(diào)度器選擇這個(gè)子進(jìn)程,幾次上下文切換,子進(jìn)程處在了紅黑樹最左邊的那個(gè)節(jié)點(diǎn)上(這是有可能的,由于進(jìn)程運(yùn)行過程中,虛擬運(yùn)行時(shí)間單調(diào)遞增,向紅黑樹右側(cè)移動(dòng),子進(jìn)程就會(huì)逐漸移動(dòng)到紅黑樹最左邊),假如在某一時(shí)刻,子進(jìn)程所在的cpu的運(yùn)行隊(duì)列上一個(gè)進(jìn)程被tick中斷打斷,然后走到scheduler_tick中執(zhí)行如下路徑:
假如子進(jìn)程剛好滿足delta > ideal_runtime的條件,然后當(dāng)前進(jìn)程就被設(shè)置了重新調(diào)度標(biāo)志,當(dāng)tick中斷返回的時(shí)候,發(fā)生搶占時(shí)調(diào)度:
schedule的代碼就不在分析,大致說明一下:
schedule實(shí)現(xiàn)中會(huì)選擇一個(gè)合適的進(jìn)程來調(diào)度,對(duì)于cfs調(diào)度類,選擇紅黑樹最左邊的那個(gè)調(diào)度實(shí)體所對(duì)應(yīng)的進(jìn)程,當(dāng)前場景也就是渴望調(diào)度的子進(jìn)程,然后進(jìn)行進(jìn)程的上下文切換,包括地址空間切換到子進(jìn)程(見上篇),處理器狀態(tài)切換,這里就切換了cpu_context到相應(yīng)的寄存器。
這時(shí),子進(jìn)程就歡快的運(yùn)行了。
2.5子進(jìn)程開始執(zhí)行
進(jìn)程上下文切換之后,子進(jìn)程于是就獲得了cpu,開始執(zhí)行,那么最重要的兩步就是pc和sp,當(dāng)然上面我們知道fork的時(shí)候已經(jīng)做了設(shè)置:
于是cpu就開始從ret_from_fork下面開始取指令執(zhí)行,所處的上下文為子進(jìn)程:
ret_from_fork首先跳轉(zhuǎn)到schedule_tail(會(huì)raw_spin_unlock_irq打開中斷和自旋鎖以及一些對(duì)前一個(gè)進(jìn)程做回收等操作)中執(zhí)行,然后對(duì)于內(nèi)核線程直接調(diào)用之前設(shè)置的內(nèi)核執(zhí)行的函數(shù),對(duì)于用戶任務(wù)通過 ret_to_user 返回用戶空間。
2.6父子進(jìn)程返回用戶空間
上面我們知道,當(dāng)子進(jìn)程被調(diào)度執(zhí)行的時(shí)候從ret_from_fork開始執(zhí)行,sp指向子進(jìn)程內(nèi)核棧的pt_regs, 最終執(zhí)行 ret_to_user 來返回用戶空間:
可以看的,子進(jìn)程將自己內(nèi)核棧中的pt_regs恢復(fù)到相應(yīng)的寄存器中,完成了異常的恢復(fù),最終調(diào)用eret,從異常中返回,這個(gè)時(shí)候硬件自動(dòng)將 elr_el1設(shè)置到pc, spsr_el1設(shè)置到pstate, sp使用了sp_el0。
這里需要說明一下,以便更好的理解:
elr_el1的值是原來父進(jìn)程復(fù)制過來的,還記得copy_thread中的*childregs = *current_pt_regs()嗎?,由于我們?cè)瓉硎莊ork系統(tǒng)調(diào)用,所以這里是執(zhí)行svc系統(tǒng)調(diào)用的下一條指令。
spsr_el1 是之前fork系統(tǒng)調(diào)用時(shí)保存的處理器的狀態(tài),現(xiàn)在恢復(fù)這個(gè)狀態(tài),當(dāng)然原來在el0,現(xiàn)在也是el0。
sp 改變?yōu)榱藄p_el0,共享父進(jìn)程的用戶棧(對(duì)于創(chuàng)建子進(jìn)程來說)。
子進(jìn)程返回的時(shí)候,由于負(fù)載均衡,不一定和父進(jìn)程在一個(gè)cpu上,所以父子進(jìn)程可以并發(fā)執(zhí)行。
父進(jìn)程創(chuàng)建完子進(jìn)程,并喚醒子進(jìn)程后,也會(huì)沿著原來的svc調(diào)用路徑一路返回到 ret_to_user ,然后恢復(fù)上下文,和子進(jìn)程經(jīng)歷同樣的過程,也會(huì)svc系統(tǒng)調(diào)用的下一條指令,繼續(xù)使用原來的用戶棧指針,好像什么都沒發(fā)生一起,但是他卻孕育了新的進(jìn)程在當(dāng)前cpu或者其他cpu上活躍著。
父子進(jìn)程返回用戶空間后都會(huì)從fork返回,fork函數(shù)調(diào)用一次卻返回兩次,這是由于是兩個(gè)不同的進(jìn)程參與調(diào)度,而且他們寫實(shí)復(fù)制方式共享相同的地址空間,對(duì)于共享的私有數(shù)據(jù),如堆棧會(huì)通過寫實(shí)復(fù)制方式為寫者分配新的頁并作拷貝和映射操作(見上篇)。
寫到這里來總結(jié)一下,發(fā)生fork的時(shí)候進(jìn)程管理做的事情:
首先是調(diào)用sched_fork為新創(chuàng)建的進(jìn)程構(gòu)建調(diào)度相關(guān)的基礎(chǔ)組件,如設(shè)置優(yōu)先級(jí)、調(diào)度類計(jì)算虛擬運(yùn)行時(shí)間等屬性信息,為參與最終的調(diào)度做準(zhǔn)備,然后調(diào)用copy_thread來設(shè)置異常返回的上下文和調(diào)度上下文這是為調(diào)度子進(jìn)程后處理器狀態(tài)做準(zhǔn)備,最后通過wake_up_new_task來喚醒子進(jìn)程將它放置到合適cpu的運(yùn)行隊(duì)列,來等待合適的調(diào)度時(shí)機(jī)參與進(jìn)程調(diào)度,來獲得cpu資源。
下面給出精心繪制的創(chuàng)建子進(jìn)程后調(diào)度相關(guān)的圖示:

三、總結(jié)
寫到這里,Linux內(nèi)核進(jìn)程創(chuàng)建也就講完了,當(dāng)然fork的實(shí)現(xiàn)涉及到很多內(nèi)容,這里只是從內(nèi)存管理和進(jìn)程調(diào)度的兩個(gè)維度來看進(jìn)程的創(chuàng)建過程,閱讀完這兩篇文章希望能幫助大家理解fork的時(shí)候背后隱藏的一些技術(shù)細(xì)節(jié),真正理解到fork的時(shí)候創(chuàng)建的頁表如何被使用的,進(jìn)程又是如何參與到調(diào)度的,從fork系統(tǒng)調(diào)用到最后的返回用戶空間整個(gè)過程有所了解.
