一個新進(jìn)程的誕生(二)從內(nèi)核態(tài)到用戶態(tài)

本系列作為?你管這破玩意叫操作系統(tǒng)源碼?的第三大部分,講述了操作系統(tǒng)第一個進(jìn)程從無到有的誕生過程,這一部分你將看到內(nèi)核態(tài)與用戶態(tài)的轉(zhuǎn)換、進(jìn)程調(diào)度的上帝視角、系統(tǒng)調(diào)用的全鏈路、fork 函數(shù)的深度剖析。
不要聽到這些陌生的名詞就害怕,跟著我一點一點了解他們的全貌,你會發(fā)現(xiàn),這些概念竟然如此活靈活現(xiàn),如此順其自然且合理地出現(xiàn)在操作系統(tǒng)的啟動過程中。
本篇章作為一個全新的篇章,需要前置篇章的知識體系支撐。
當(dāng)然,沒讀過的也問題不大,我都會在文章里做說明,如果你覺得有困惑,就去我告訴你的相應(yīng)章節(jié)回顧就好了,放寬心。
------- 第三部分目錄?-------
------- 正文開始?-------
書接上回,上回書咱們從整體上鳥瞰了一下第三部分要講的內(nèi)容,代碼上就是還差四句話就走到了 main 函數(shù)的盡頭。
void?main(void)?{
????...????
????move_to_user_mode();
????if?(!fork())?{
????????init();
????}
????for(;;)?pause();
}
今天我們就重點講這第一句代碼,move_to_user_mode。
讓進(jìn)程無法逃出用戶態(tài)
這行代碼的意思直接說非常簡單,就是從內(nèi)核態(tài)轉(zhuǎn)變?yōu)榱擞脩魬B(tài),但要解釋清楚這個意思,還需要聽我慢慢道來。
我相信你肯定聽說過操作系統(tǒng)的內(nèi)核態(tài)與用戶態(tài),用戶進(jìn)程都在用戶態(tài)這個特權(quán)級下運行,而有時程序想要做一些內(nèi)核態(tài)才允許做的事情,比如讀取硬盤的數(shù)據(jù),就需要通過系統(tǒng)調(diào)用,來請求操作系統(tǒng)在內(nèi)核態(tài)特權(quán)級下執(zhí)行一些指令。
我們現(xiàn)在的代碼,還是在內(nèi)核態(tài)下運行,之后操作系統(tǒng)達(dá)到怠速狀態(tài)時,是以用戶態(tài)的 shell 進(jìn)程運行,隨時等待著來自用戶輸入的命令。
所以,就在這一步,也就是 move_to_user_mode 這行代碼,作用就是將當(dāng)前代碼的特權(quán)級,從內(nèi)核態(tài)變?yōu)橛脩魬B(tài)。
一旦轉(zhuǎn)變?yōu)榱擞脩魬B(tài),那么之后的代碼將一直處于用戶態(tài)的模式,除非發(fā)生了中斷,比如用戶發(fā)出了系統(tǒng)調(diào)用的中斷指令,那么此時將會從用戶態(tài)陷入內(nèi)核態(tài),不過當(dāng)中斷處理程序執(zhí)行完之后,又會通過中斷返回指令從內(nèi)核態(tài)回到用戶態(tài)。

整個過程被操作系統(tǒng)的機(jī)制拿捏的死死的,始終讓用戶進(jìn)程處于用戶態(tài)運行,必要的時候陷入一下內(nèi)核態(tài),但很快就會被返回而再次回到用戶態(tài),是不是非常無奈?這樣操作系統(tǒng)就掌控了控制權(quán),而用戶進(jìn)程再怎么折騰也無法逃出這個模式。
?
內(nèi)核態(tài)與用戶態(tài)的本質(zhì)-特權(quán)級
?
首先從一個最大的視角來看,這一切都源于 CPU 的保護(hù)機(jī)制。CPU 為了配合操作系統(tǒng)完成保護(hù)機(jī)制這一特性,分別設(shè)計了分段保護(hù)機(jī)制與分頁保護(hù)機(jī)制。
?
當(dāng)我們在?第七回 | 六行代碼就進(jìn)入了保護(hù)模式?將 cr0 寄存器的 PE 位開啟時,就開啟了保護(hù)模式,也即開啟了分段保護(hù)機(jī)制。
?

?
當(dāng)我們在?第九回 | Intel 內(nèi)存管理兩板斧:分段與分頁 將 cr0 寄存器的 PG 位開啟時,就開啟了分頁模式,也即開啟了分頁保護(hù)機(jī)制。
?

?
有關(guān)特權(quán)級的保護(hù),實際上屬于分段保護(hù)機(jī)制的一種。具體怎么保護(hù)的呢?由于這里的細(xì)節(jié)比較繁瑣,所以我舉個例子簡單理解下即可,實際上的特權(quán)級檢查規(guī)則要比我說的多好多內(nèi)容。
?
我們目前正在執(zhí)行的代碼地址,是通過 CPU 中的兩個寄存器 cs : eip 指向的對吧?cs 寄存器是代碼段寄存器,里面存著的是段選擇子,還記得它的結(jié)構(gòu)么?
?

?
這里面的低端兩位,此時表示 CPL,也就是當(dāng)前所處的特權(quán)級,假如我們現(xiàn)在這個時刻,CS 寄存器的后兩位為 3,二進(jìn)制就是 11,就表示是當(dāng)前處理器處于用戶態(tài)這個特權(quán)級。
?
假如我們此時要跳轉(zhuǎn)到另一處內(nèi)存地址執(zhí)行,在最終的匯編指令層面無非就是 jmp、call 和中斷。我們拿 jmp 跳轉(zhuǎn)來舉例。
?
如果是短跳轉(zhuǎn),也就是直接 jmp xxx,那不涉及到段的變換,也就沒有特權(quán)級檢查這回事。
?
如果是長跳轉(zhuǎn),也就是 jmp yyy : xxx,這里的 yyy 就是另一個要跳轉(zhuǎn)到的段的段選擇子結(jié)構(gòu)。
?

?
這個結(jié)構(gòu)仍然是一樣的段選擇子結(jié)構(gòu),只不過這里的低端兩位,表示 RPL,也就是請求特權(quán)級,表示我想請求的特權(quán)級是什么。同時,CPU 會拿這個段選擇子去全局描述符表中尋找段描述符,從中找到段基址。
?

?
那還記得段描述符的樣子么?
?

?
你看,這里面又有個 DPL,這表示目標(biāo)代碼段特權(quán)級,也就是即將要跳轉(zhuǎn)過去的那個段的特權(quán)級。
?
好了,我們總結(jié)一下簡圖,就是這三個玩意的比較。
?

?
這里的檢查規(guī)則比較多,簡單說,絕大多數(shù)情況下,要求 CPL 必須等于 DPL,才會跳轉(zhuǎn)成功,否則就會報錯。
?
也就是說,當(dāng)前代碼所處段的特權(quán)級,必須要等于要跳轉(zhuǎn)過去的代碼所處的段的特權(quán)級,那就只能用戶態(tài)往用戶態(tài)跳,內(nèi)核態(tài)往內(nèi)核態(tài)跳,這樣就防止了處于用戶態(tài)的程序,跳轉(zhuǎn)到內(nèi)核態(tài)的代碼段中做壞事。
?
這只是代碼段跳轉(zhuǎn)時所做的特權(quán)級檢查,還有訪問內(nèi)存數(shù)據(jù)時也會有數(shù)據(jù)段的特權(quán)級檢查,這里就不展開了。最終的效果是,處于內(nèi)核態(tài)的代碼可以訪問任何特權(quán)級的數(shù)據(jù)段,處于用戶態(tài)的代碼則只可以訪問用戶態(tài)的數(shù)據(jù)段,這也就實現(xiàn)了內(nèi)存數(shù)據(jù)讀寫的保護(hù)。
?
說了這么多,其實就是,代碼跳轉(zhuǎn)只能同特權(quán)級,數(shù)據(jù)訪問只能高特權(quán)級訪問低特權(quán)級。
特權(quán)級轉(zhuǎn)換的方式
?
誒不對呀,那我們今天要講的是,從內(nèi)核態(tài)轉(zhuǎn)變?yōu)橛脩魬B(tài),那如果代碼跳轉(zhuǎn)只能同特權(quán)級跳,我們現(xiàn)在處于內(nèi)核態(tài),要怎么樣才能跳轉(zhuǎn)到用戶態(tài)呢?
?
Intel 設(shè)計了好多種特權(quán)級轉(zhuǎn)換的方式,中斷和中斷返回就是其中的一種。
處于用戶態(tài)的程序,通過觸發(fā)中斷,可以進(jìn)入內(nèi)核態(tài),之后再通過中斷返回,又可以恢復(fù)為用戶態(tài)。
?
就是剛剛的圖所表示的。

而系統(tǒng)調(diào)用就是這么玩的,用戶通過 int 0x80 中斷指令觸發(fā)了中斷,CPU 切換至內(nèi)核態(tài),執(zhí)行中斷處理程序,之后中斷程序返回,又從內(nèi)核態(tài)切換回用戶態(tài)。
?
但有個問題是,我們當(dāng)前的代碼,此時就是處于內(nèi)核態(tài),并不是由一個用戶態(tài)程序通過中斷而切換到的內(nèi)核態(tài),那怎么回到原來的用戶態(tài)呢?答案還是,通過中斷返回。
?
沒有中斷也能中斷返回?可以的,Intel 設(shè)計的 CPU 就是這樣不符合人們的直覺,中斷和中斷返回的確是應(yīng)該配套使用的,但也可以單獨使用,我們看代碼。
void?main(void)?{
????...????
????move_to_user_mode();
????...
}
#define?move_to_user_mode()?\
_asm?{?\
????_asm?mov?eax,esp?\
????_asm?push?00000017h?\
????_asm?push?eax?\
????_asm?pushfd?\
????_asm?push?0000000fh?\
????_asm?push?offset?l1?\
????_asm?iretd?/*?執(zhí)行中斷返回指令*/?\
_asm?l1:?mov?eax,17h?\
????_asm?mov?ds,ax?\
????_asm?mov?es,ax?\
????_asm?mov?fs,ax?\
????_asm?mov?gs,ax?\
}
你看,這個方法里直接就執(zhí)行了中斷返回指令 iretd。
?
那么為什么之前進(jìn)行了一共五次的壓棧操作呢?因為中斷返回理論上就是應(yīng)該和中斷配合使用的,而此時并不是真的發(fā)生了中斷到這里,所以我們得假裝發(fā)生了中斷才行。
?
怎么假裝呢?其實就把棧做做工作就好了,中斷發(fā)生時,CPU 會自動幫我們做如下的壓棧操作。而中斷返回時,CPU 又會幫我們把壓棧的這些值返序賦值給響應(yīng)的寄存器。
?

?
去掉錯誤碼,剛好是五個參數(shù),所以我們在代碼中模仿 CPU 進(jìn)行了五次壓棧操作,這樣在執(zhí)行 iretd 指令時,硬件會按順序?qū)倓倝喝霔V械臄?shù)據(jù),分別賦值給 SS、ESP、EFLAGS、CS、EIP 這幾個寄存器,這就感覺像是正確返回了一樣,讓其誤以為這是通過中斷進(jìn)來的。
?
壓入棧的 CS 和 EIP 就表示中斷發(fā)生前代碼所處的位置,這樣中斷返回后好繼續(xù)去那里執(zhí)行。
壓入棧的 SS 和 ESP 表示中斷發(fā)生前的棧的位置,這樣中斷返回后才好恢復(fù)原來的棧。
其中,特權(quán)級的轉(zhuǎn)換,就體現(xiàn)在 CS 和 SS 寄存器的值里,都是細(xì)節(jié)!
CS 和 SS 寄存器是段寄存器的一種,段寄存器里的值是段選擇子,其結(jié)構(gòu)上面已經(jīng)提過兩遍了,在?第六回 | 先解決段寄存器的歷史包袱問題?中也專門講了這個結(jié)構(gòu)的作用。

對著這個結(jié)構(gòu),我們看代碼。
#define?move_to_user_mode()?\
_asm?{?\
????_asm?mov?eax,esp?\
????_asm?push?00000017h?\ ; 給 SS 賦值
????_asm?push?eax?\
????_asm?pushfd?\
????_asm?push?0000000fh?\ ; 給 CS 賦值
????_asm?push?offset?l1?\
????_asm?iretd?/*?執(zhí)行中斷返回指令*/?\
_asm?l1:?mov?eax,17h?\
????_asm?mov?ds,ax?\
????_asm?mov?es,ax?\
????_asm?mov?fs,ax?\
????_asm?mov?gs,ax?\
}
拿 CS 舉例,給它賦的值是,0000000fh,用二進(jìn)制表示為:
0000000000001111
最后兩位 11 表示特權(quán)級為 3,即用戶態(tài)。而我們剛剛說了,CS 寄存器里的特權(quán)級,表示 CPL,即當(dāng)前處理器特權(quán)級。
所以經(jīng)過 iretd 返回之后,CS 的值就變成了它,而當(dāng)前處理器特權(quán)級,也就變成了用戶態(tài)特權(quán)級。
除了改變特權(quán)級之外

#define?lldt(n)?__asm__("lldt?%%ax"::"a"?(_LDT(n)))
void?sched_init(void)?{
????...
????lldt(0);
????...
}
而整個 GDT 與 LDT 表的設(shè)計,經(jīng)過整個?第一部分 進(jìn)入內(nèi)核前的苦力活?和?第二部分 大戰(zhàn)前期的初始化工作?的設(shè)計后,成了這個樣子。

所以,一目了然。
再看這行代碼,把 EIP 寄存器賦值為了那行標(biāo)號的地址。
void?main(void)?{
????...????
????move_to_user_mode();
????...
}
#define?move_to_user_mode()?\
_asm?{?\
????_asm?mov?eax,esp?\
????_asm?push?00000017h?\
????_asm?push?eax?\
????_asm?pushfd?\
????_asm?push?0000000fh?\
????_asm?push?offset?l1?\
????_asm?iretd?/*?執(zhí)行中斷返回指令*/?\
_asm?l1:?mov?eax,17h?\
????_asm?mov?ds,ax?\
????_asm?mov?es,ax?\
????_asm?mov?fs,ax?\
????_asm?mov?gs,ax?\
}
這里剛好設(shè)置的是下面標(biāo)號 l1 的位置,所以 iretd 之后 CPU 就乖乖去那里執(zhí)行了。所以其實從效果上看,就是順序往下執(zhí)行,只不過利用了 iretd 做了些特權(quán)級轉(zhuǎn)換等工作。
同理,這里的棧段 ss 和數(shù)據(jù)段 ds,都被賦值為了 17h,大家可以展開二進(jìn)制算一下,他們又是什么特權(quán)級,對應(yīng)的描述符又是誰。
總結(jié)
?
所以其實,最終效果上看就是按順序執(zhí)行了我們所寫的指令,仿佛沒有經(jīng)過什么中斷和中斷返回的過程,但卻通過中斷返回實現(xiàn)了特權(quán)級的翻轉(zhuǎn),也就是從內(nèi)核態(tài)變?yōu)榱擞脩魬B(tài),順便設(shè)置了棧段、代碼段和數(shù)據(jù)段的基地址。
好了,我們兜兜轉(zhuǎn)轉(zhuǎn)終于把這個 mov_to_user_mode 講完了,特權(quán)級這塊的檢查細(xì)節(jié)非常繁瑣,為了理解操作系統(tǒng),我們只需要暫且記住如下一句話就好了:
?
數(shù)據(jù)訪問只能高特權(quán)級訪問低特權(quán)級,代碼跳轉(zhuǎn)只能同特權(quán)級跳轉(zhuǎn),要想實現(xiàn)特權(quán)級轉(zhuǎn)換,可以通過中斷和中斷返回來實現(xiàn)。
?
OK,我們現(xiàn)在已經(jīng)進(jìn)入了用戶態(tài),也即表明了需要內(nèi)核態(tài)來完成的工作已經(jīng)全部安排妥當(dāng)了,其實就是整個?第一部分 進(jìn)入內(nèi)核前的苦力活 和?第二部分 大戰(zhàn)前期的初始化工作 的內(nèi)容,對全局描述符表、中斷描述符表、頁表等關(guān)鍵內(nèi)存結(jié)構(gòu)進(jìn)行設(shè)置,以及對 CPU 特殊寄存器如 cr0 和 cr3 的設(shè)置,還有對外設(shè)如硬盤、鍵盤、定時器的設(shè)置等。
?
看來我們又完成了一大堆苦力活呀,內(nèi)核態(tài)做的工作也真是枯燥乏味呢。接下來只需要在用戶態(tài)進(jìn)行工作即可了!
?
欲知后事如何,且聽下回分解。
------- 關(guān)于本系列的完整內(nèi)容?-------
本系列的開篇詞看這
本系列的擴(kuò)展資料看這(也可點擊閱讀原文),這里有很多有趣的資料、答疑、互動參與項目,持續(xù)更新中,希望有你的參與。
https://github.com/sunym1993/flash-linux0.11-talk
本系列全局視角

最后,祝大家都能追更到系列結(jié)束,只要你敢持續(xù)追更,并且把每一回的內(nèi)容搞懂,我就敢讓你在系列結(jié)束后說一句,我對 Linux 0.11 很熟悉。
公眾號更新系列文章不易,閱讀量越來越低,希望大家多多傳播,不方便的話點個小小的在看我也會很開心,我相信星火燎原的力量,謝謝大家咯。
另外,本系列完全免費,希望大家能多多傳播給同樣喜歡的人,同時給我的 GitHub 項目點個 star,就在閱讀原文處,這些就足夠讓我堅持寫下去了!我們下回見。
