第34回 | 進程2的創(chuàng)建
新讀者看這里,老讀者直接跳過。
本系列會以一個讀小說的心態(tài),從開機啟動后的代碼執(zhí)行順序,帶著大家閱讀和賞析 Linux 0.11 全部核心代碼,了解操作系統(tǒng)的技術(shù)細節(jié)和設(shè)計思想。
本系列的 GitHub 地址如下,希望給個 star 以示鼓勵(文末閱讀原文可直接跳轉(zhuǎn),也可以將下面的鏈接復制到瀏覽器里打開)
https://github.com/sunym1993/flash-linux0.11-talk
本回的內(nèi)容屬于第四部分。

你會跟著我一起,看著一個操作系統(tǒng)從啥都沒有開始,一步一步最終實現(xiàn)它復雜又精巧的設(shè)計,讀完這個系列后希望你能發(fā)出感嘆,原來操作系統(tǒng)源碼就是這破玩意。
以下是已發(fā)布文章的列表,詳細了解本系列可以先從開篇詞看起。
第一部分 進入內(nèi)核前的苦力活
第8回 | 煩死了又要重新設(shè)置一遍 idt 和 gdt
第9回 | Intel 內(nèi)存管理兩板斧:分段與分頁
第二部分 大戰(zhàn)前期的初始化工作
第15回 | 塊設(shè)備請求項初始化 blk_dev_init
第18回 | 進程調(diào)度初始化 sched_init
第三部分:一個新進程的誕生
第22回 | 從內(nèi)核態(tài)切換到用戶態(tài)
第25回 | 通過 fork 看一次系統(tǒng)調(diào)用
第27回 | 透過 fork 來看進程的內(nèi)存規(guī)劃
第28回 | 番外篇 - 我居然會認為權(quán)威書籍寫錯了...
------- 正文開始?-------
書接上回,上回書咱們說到,進程 1 通過 open 函數(shù)建立了與外設(shè)交互的能力,具體其實就是打開了 tty0 這個設(shè)備文件,并綁定了標準輸入 0,標準輸出 1 和 標準錯誤輸出 2 這三個文件描述符。
?

?
同時我們看到源碼中用 printf 函數(shù),調(diào)用 write 函數(shù),向 1 號文件描述符輸出了字符串的效果。
?

?
到此為止,標志著進程 1 的工作基本結(jié)束了,準確說是能力建設(shè)的工作結(jié)束了,接下來就是控制流程和創(chuàng)建新的進程了,我們繼續(xù)往下看。
void?init(void)?{
????...
????if?(!(pid=fork()))?{
????????close(0);
????????open("/etc/rc",O_RDONLY,0);
????????execve("/bin/sh",argv_rc,envp_rc);
????????_exit(2);
????}
????if?(pid>0)
????????while?(pid?!=?wait(&i))
????????????/*?nothing?*/;
????while?(1)?{
????????if?(!(pid=fork()))?{
????????????close(0);close(1);close(2);
????????????setsid();
????????????(void)?open("/dev/tty0",O_RDWR,0);
????????????(void)?dup(0);
????????????(void)?dup(0);
????????????_exit(execve("/bin/sh",argv,envp));
????????}
????????while?(1)
????????????if?(pid?==?wait(&i))
????????????????break;
????????printf("\n\rchild?%d?died?with?code?%04x\n\r",pid,i);
????????sync();
????}
????_exit(0);???/*?NOTE!?_exit,?not?exit()?*/
}
別急,我們一點點看,我仍然是去掉了一些錯誤校驗的旁路分支。
void?init(void)?{
????...
????if?(!(pid=fork()))?{
????????close(0);
????????open("/etc/rc",O_RDONLY,0);
????????execve("/bin/sh",argv_rc,envp_rc);
????????_exit(2);
????}
????...
}
先看這個第一段,我們先嘗試口述翻譯一遍。
?
?
聽起來還蠻合邏輯的,創(chuàng)建進程(fork)、關(guān)閉(close)、打開(open)、執(zhí)行(execve)四步走,接下來我們一點點拆解。
fork
?
fork 前面講過了,就是將進程的 task_struct 結(jié)構(gòu)進行一下復制,比如進程 0 fork 出進程 1 的時候。
?

?
之后,新進程再重寫一些基本信息,包括元信息和 tss 里的寄存器信息。再之后,用 copy_page_tables 復制了一下頁表(這里涉及到寫時復制的伏筆)。
比如進程 0 復制出進程 1 的時候,頁表是這樣復制的。
?

?
而這里的進程 1 fork 出進程 2,也是同樣的流程,不同之處在于兩點細節(jié):
?
第一點,進程 1 打開了三個文件描述符并指向了 tty0,那這個也被復制到進程 2 了,具體說來就是進程結(jié)構(gòu) task_struct 里的 flip[] 數(shù)組被復制了一份。
struct?task_struct?{
????...
????struct?file?*filp[NR_OPEN];
????...
};
而進程 0 fork 出進程 1 時是沒有復制這部分信息的,因為進程 0 沒有打開任何文件。這也是剛剛說的與外設(shè)交互能力的體現(xiàn),即進程 0 沒有與外設(shè)交互的能力,進程 1 有,哎,其實就是這個 flip 數(shù)組里有沒有東西而已嘛~
?
第二點,進程 0 復制進程 1 時頁表的復制只有 160 項,也就是映射 640K,而之后進程的復制,統(tǒng)統(tǒng)都是復制 1024 項,也就是映射 4M 空間。
int?copy_page_tables(unsigned?long?from,unsigned?long?to,long?size)?{
????...
????nr?=?(from==0)?0xA0:1024;
????...
}
整體看就是如圖所示。
?

?
除此之外,就沒有別的區(qū)別了。
close
好了,我們繼續(xù)看。
void?init(void)?{
????...
????if?(!(pid=fork()))?{
????????close(0);
????????open("/etc/rc",O_RDONLY,0);
????????execve("/bin/sh",argv_rc,envp_rc);
????????_exit(2);
????}
????...
}
fork 完之后,后面 if 里面的代碼都是進程 2 在執(zhí)行了。
?
close(0) 就是關(guān)閉 0 號文件描述符,也就是進程 1 復制過來的打開了 tty0 并作為標準輸入的文件描述符,那么此時 0 號文件描述符就空出來了。
下面是 close 對應(yīng)的系統(tǒng)調(diào)用函數(shù),很簡單。
int?sys_close(unsigned?int?fd)?{???
????...
????current->filp[fd]?=?NULL;
????...
}
open
接下來 open 函數(shù)以只讀形式打開了一個叫 /etc/rc 的文件,剛好占據(jù)了 0 號文件描述符的位置。
void?init(void)?{
????...
????if?(!(pid=fork()))?{
????????...
????????open("/etc/rc",O_RDONLY,0);
????????...
????}
????...
}
這個 rc 文件表示配置文件,具體什么內(nèi)容,取決于你的硬盤里這個位置處放了什么內(nèi)容,與操作系統(tǒng)內(nèi)核無關(guān),所以我們暫且不用管。
?
此時,進程 2 與進程 1 幾乎完全一樣,只不過進程 2 通過 close 和 open 操作,將原來進程 1 的指向標準輸入的 0 號文件描述符,重新指向了 /etc/rc 文件。
到目前為止,進程?2?與進程?1?的區(qū)別,僅僅是將?0?號文件描述符重新指向了?/etc/rc?文件,其他的沒啥區(qū)別。
而這個 rc 文件是干嘛的,現(xiàn)在還不用管,肯定是后面 sh 程序要用到的,到時候在說。
execve
?
好,接下來進程 2 就將變得不一樣了,會通過一個經(jīng)典的,也是最難理解的 execve 函數(shù)調(diào)用,使自己搖身一變,成為 /bin/sh 程序繼續(xù)運行,這就是下一章的重點!
void?init(void)?{
????...
????if?(!(pid=fork()))?{
????????...
????????execve("/bin/sh",argv_rc,envp_rc);
????????...
????}
????...
}
這里就包含著操作系統(tǒng)究竟是如何加載并執(zhí)行一個程序的原理,包括如何從文件系統(tǒng)中找到這個文件,如何解析一個可執(zhí)行文件(在現(xiàn)代的 Linux 里稱作 ELF 可執(zhí)行文件),如何講可執(zhí)行文件中的代碼和數(shù)據(jù)加載到內(nèi)存并運行。
加載到內(nèi)存并運行又包含著虛擬內(nèi)存等相關(guān)的知識。所以這里面的水很深,了解了這個函數(shù),再加上 fork 函數(shù),基本就可以把操作系統(tǒng)全部核心邏輯都串起來了。
欲知后事如何,且聽下回分解。
------- 關(guān)于本系列?-------
本系列的開篇詞看這,開篇詞
本系列的番外故事看這,讓我們一起來寫本書?也可以直接無腦加入星球,共同參與這場旅行。
最后,本系列完全免費,希望大家能多多傳播給同樣喜歡的人,同時給我的 GitHub 項目點個 star,就在閱讀原文處,這些就足夠讓我堅持寫下去了!我們下回見。
