進(jìn)程和內(nèi)存
進(jìn)程有自己的內(nèi)存空間(指令 數(shù)據(jù) 和棧),翻譯這段話真別扭。操作系統(tǒng)可以在一組需要執(zhí)行的進(jìn)程之間進(jìn)行切換,當(dāng)一個進(jìn)程沒有執(zhí)行時,系統(tǒng)會保存進(jìn)程的寄存器狀態(tài),在下一次執(zhí)行的時候恢復(fù)狀態(tài)。是不是很簡單,簡單,你寫一個試試。內(nèi)核會給每個進(jìn)程分配一個進(jìn)程標(biāo)識,就是經(jīng)常看到的pid。
利用fork可以創(chuàng)建一個新的進(jìn)程,這個是什么意思?
看看fork
int pid = fork();
if(pid > 0){
printf("parent: child=%d\n", pid);
pid = wait();
printf("child %d is done\n", pid);}
else if(pid == 0){
printf("child: exiting\n");
exit();
} else {
printf("fork error\n");
}
首先父進(jìn)程調(diào)用fork,產(chǎn)生子進(jìn)程,子進(jìn)程擁有和父親相同內(nèi)容的內(nèi)存空間。最奇怪的就是fork調(diào)用會返回兩次,所以后面就是兩個程序了,就像影分身。在父進(jìn)程里面返回子進(jìn)程的進(jìn)程id,在子進(jìn)程里面返回0(為啥喜歡這樣干,迷惑吧)。
exit調(diào)用會終止進(jìn)程,且釋放內(nèi)存和文件資源。wait會等待子進(jìn)程退出。
程序的輸出
parent: child=1234// p
child: exiting //c
parent: child 1234 is done
p和c的順序不是一定的,看誰首先獲得調(diào)度,但是最后一句肯定在后面。現(xiàn)在兩個程序有不同的寄存器和內(nèi)存,改變一個變量不會影響另外一個。
exec這個系統(tǒng)調(diào)用,會從文件系統(tǒng)中加載一個可執(zhí)行文件,替換掉當(dāng)前的進(jìn)程內(nèi)存空間。這個文件需要符合一定的格式,包含指令,啟動地址,和數(shù)據(jù),調(diào)用以后直接執(zhí)行啟動命令。
char *argv[3];
argv[0] = "echo";
argv[1] = "hello";
argv[2] = 0;
exec("/bin/echo", argv);
printf("exec error\n");
這兩個的區(qū)別是一個復(fù)制父進(jìn)程,一個替換,是不是有點(diǎn)難以理解,為啥不新建一個(如果你也思考了這個問題,那后面就會有回答),再做處理,這可能是進(jìn)程資源創(chuàng)建的問題。exec會分配足夠的內(nèi)存,如果不夠用,再進(jìn)行系統(tǒng)調(diào)用分配內(nèi)存。
是不是感覺獲得了一甲子功力?孩子別傻,現(xiàn)在還早,到現(xiàn)在一切還流于表面,后續(xù)的路還多著。請不要著急,耐心一點(diǎn)。
io和文件描述符
文件描述符是一個小整數(shù),代表進(jìn)程要讀寫的對象,這沒有什么奇怪的,匯編里面也支持讀寫文件,在上一節(jié)中,可以在寄存器里填上值,代表不同的操作。
這里的文件描述符,代表一種抽象的文件,可以是文件,設(shè)備,管道,都當(dāng)做一種字節(jié)流來處理。
每個進(jìn)程都保存這一個文件資源表格,默認(rèn)從0開始,代表輸入,1代表輸出,2代表錯誤。shell為了實(shí)現(xiàn)重定向(> <)和管道(|),要確保這幾個總是打開的狀態(tài)。
read會讀取幾個字節(jié)到buf里面,返回字節(jié)數(shù),如果沒有了就返回0。這里初學(xué)者比較難以理解的就是為啥要buf,一下把文件讀入內(nèi)存不就得了,得到我要的字符串就好了。這肯定沒有考慮到大文件的情況,內(nèi)存放不下的時候,一般測試者這里只是一個有幾字節(jié)的小文件而已。剛開始嘛,想一下子解決問題,誰會想到用這些奇怪的技巧,但是起碼得漲下經(jīng)驗(yàn)吧。write類似就不說了。
基本的cat實(shí)現(xiàn)(單純用c語言的getchar putchar 也可以實(shí)現(xiàn))
char buf[512];
int n;
for(;;){
n = read(0, buf, sizeof buf);
if(n == 0)break;
if(n < 0){
fprintf(2, "read error\n");
exit();
}
if(write(1, buf, n) != n){
fprintf(2, "write error\n");
exit();
}
}
注意這里的cat基本不知道從哪里讀,往哪里寫,是文件還是管道。
還有一點(diǎn)沒有提的就是,fork和exec調(diào)用都會保存父進(jìn)程的文件表格,所以借用這些就可以實(shí)現(xiàn)重定向。
cat <input.txt
char *argv[2];
argv[0] = "cat";
argv[1] = 0;
if(fork() == 0) {
close(0);
open("input.txt", O_RDONLY);
exec("cat", argv);
}
因?yàn)樵趀xec->cat后,進(jìn)程的0文件還是input.txt里面的內(nèi)容,當(dāng)然這次1就成了控制臺了。
其他
(真是個好的分類,什么都可以扔這里,因?yàn)榈竭@里已經(jīng)寫的很累了,讀者肯定也累了,也不要期望,一下子理解,因?yàn)槲矣X得不可能。我會分出一篇接口二。)
管道利用了一些類似的技巧,可以得到期望的文件描述符。
文件系統(tǒng)很值得學(xué)習(xí)的就是路徑的表示方法,是樹這種數(shù)據(jù)結(jié)構(gòu)一種神來之筆,樹真的是一種強(qiáng)大的數(shù)據(jù)結(jié)構(gòu),無處不在。互聯(lián)網(wǎng)也有這種路徑的表示方法的意思,包括最近rest接口定義。
文件實(shí)現(xiàn)相關(guān)的肯定需要了解磁盤的結(jié)構(gòu)了,后面再說吧,cpu本身就有文件的讀寫指令。這種將各種設(shè)備資源抽象成文件的方法,受到人們的追捧,我也不知道好不好。
總之里面shell展示了系統(tǒng)調(diào)用(如wait close等)的靈活組裝方式,真可以感嘆一些這些精巧是如何想到的,起碼對于我來說很不自然。
總結(jié)
一些目前學(xué)到的東西,以便更好的前行。首先是一些超凡脫俗的名詞,看文章首的接口圖片,還有一些沒有講到的自己查閱。大師是命名高手,一個詞匯代表了太多的東西,絕逼是抽象派的。
良好的接口定義,可以讓系統(tǒng)容易實(shí)現(xiàn),且擁有靈活的組合方式,畢竟接口定義了這個操作系統(tǒng),你說牛不牛。Java有jnr實(shí)現(xiàn)了一系列底層的接口,js都可以加載操作系統(tǒng)了。如今docker和openstack如日中天,你說不學(xué)點(diǎn)操作系統(tǒng)對得起誰?接口說完了,該說一下實(shí)現(xiàn)了,對不起這篇文章已經(jīng)太長了,期待下一篇吧,下次要火力全開了。
這系列教程,是在閱讀一些書籍和代碼過程的記錄,基本是xv6的翻譯了(暈吧) 。如果你讀到更好的文章推薦給我,我不寫了。歡迎閱讀我的其它文章。