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

你會(huì)跟著我一起,看著一個(gè)操作系統(tǒng)從啥都沒(méi)有開(kāi)始,一步一步最終實(shí)現(xiàn)它復(fù)雜又精巧的設(shè)計(jì),讀完這個(gè)系列后希望你能發(fā)出感嘆,原來(lái)操作系統(tǒng)源碼就是這破玩意。
以下是已發(fā)布文章的列表,詳細(xì)了解本系列可以先從開(kāi)篇詞看起。
第一部分 進(jìn)入內(nèi)核前的苦力活
第4回 | 把自己在硬盤(pán)里的其他部分也放到內(nèi)存來(lái)
第5回 | 進(jìn)入保護(hù)模式前的最后一次折騰內(nèi)存
第8回 | 煩死了又要重新設(shè)置一遍 idt 和 gdt
第9回 | Intel 內(nèi)存管理兩板斧:分段與分頁(yè)
第10回 | 進(jìn)入 main 函數(shù)前的最后一躍!
第二部分 大戰(zhàn)前期的初始化工作
第11回 | 整個(gè)操作系統(tǒng)就 20 幾行代碼
第12回 | 管理內(nèi)存前先劃分出三個(gè)邊界值
第15回 | 塊設(shè)備請(qǐng)求項(xiàng)初始化 blk_dev_init
第18回 | 進(jìn)程調(diào)度初始化 sched_init
第三部分:一個(gè)新進(jìn)程的誕生
第22回 | 從內(nèi)核態(tài)切換到用戶態(tài)
第23回 | 如果讓你來(lái)設(shè)計(jì)進(jìn)程調(diào)度
第24回 | 從一次定時(shí)器滴答來(lái)看進(jìn)程調(diào)度
第25回 | 通過(guò) fork 看一次系統(tǒng)調(diào)用
第26回 | fork 中進(jìn)程基本信息的復(fù)制
第27回 | 透過(guò) fork 來(lái)看進(jìn)程的內(nèi)存規(guī)劃
第28回 | 番外篇 - 我居然會(huì)認(rèn)為權(quán)威書(shū)籍寫(xiě)錯(cuò)了...
第29回 | 番外篇 - 讓我們一起來(lái)寫(xiě)本書(shū)?
第30回 | 番外篇 - 寫(xiě)時(shí)復(fù)制就這么幾行代碼
------- 正文開(kāi)始?-------
書(shū)接上回,上回書(shū)咱們說(shuō)到,Linux 通過(guò)缺頁(yè)中斷處理過(guò)程,將 /bin/sh 的代碼從硬盤(pán)加載到了內(nèi)存,此時(shí)便可以正式執(zhí)行 shell 程序了。
?
這個(gè) shell 程序,也就是 Linux 0.11 中要執(zhí)行的這個(gè) /bin/sh 程序,它的源碼并沒(méi)有體現(xiàn)在 Linux 0.11 源碼中。
?
也可以說(shuō),不論這個(gè) /bin/sh 是個(gè)啥文件,哪怕只是個(gè) hello world 程序,Linux 0.11 的啟動(dòng)過(guò)程中也會(huì)傻傻地去執(zhí)行它。
?
但同時(shí),shell 又是一個(gè)我們?cè)偈煜げ贿^(guò)的東西了。
?
在我的騰訊云服務(wù)器上(用 Termius 連接),它是這個(gè)樣子的。
?

?
在我的 Ubuntu 16.04 虛擬機(jī)上,它是這個(gè)樣子的。
?

?
在我的 mac 電腦上,它是這個(gè)樣子的。
?

?
沒(méi)錯(cuò),它就是我們通常說(shuō)的那個(gè)命令行黑窗口。
?
當(dāng)然 shell 只是一個(gè)標(biāo)準(zhǔn),具體的實(shí)現(xiàn)可以有很多,比如在我的 Ubuntu 16.04 上,具體的 shell 實(shí)現(xiàn)是 bash。
flash:~$?echo?$SHELL
/bin/bash
而在我的 mac 上,具體的實(shí)現(xiàn)是 zsh。
~?echo?$SHELL
/bin/zsh
當(dāng)然,默認(rèn)的 shell 實(shí)現(xiàn)也可以手動(dòng)進(jìn)行設(shè)置并更改。
?
還有個(gè)有意思的事,shell 前面的提示符,是否可以修改呢?
?
我的騰訊云服務(wù)器上,提示符是
[root@VM-24-11-centos?~]#
我的 Ubuntu 虛擬機(jī)上,提示符是
flash:~$
我的 mac 電腦上更簡(jiǎn)單,提示符是
~
我現(xiàn)在覺(jué)得我那個(gè)騰訊云服務(wù)器上的提示符太長(zhǎng)了怎么辦?我們先查看一個(gè)變量 PS1 的值
[root@VM-24-11-centos?~]#?echo?$PS1
[\u@\h?\W]\$
然后,我們直接把這個(gè)值給改了。
[root@VM-24-11-centos?~]#?echo?$PS1
[\u@\h?\W]\$
[root@VM-24-11-centos?~]#?PS1=[呵呵呵]
[呵呵呵]
可以看到神奇的事情發(fā)生了,前面的提示符變成了我們自己定義的樣子。
?
其實(shí)我就想說(shuō),shell 程序也僅僅是個(gè)程序而已,它的輸出,它的輸入,它的執(zhí)行邏輯,是完全可以通過(guò)閱讀程序源碼來(lái)知道的,和一個(gè)普通的程序并沒(méi)有任何區(qū)別。
?
好了,接下來(lái)我們就閱讀一下 shell 程序的源碼,只需要找到它的一個(gè)具體實(shí)現(xiàn)即可。但是 bash,zsh 等實(shí)現(xiàn)都過(guò)于復(fù)雜,很多東西對(duì)于我們學(xué)習(xí)完全沒(méi)必要。
?
所以這里我通過(guò)一個(gè)非常非常精簡(jiǎn)的 shell 實(shí)現(xiàn),即 xv6 里的 shell 實(shí)現(xiàn)為例,來(lái)進(jìn)行講解。
?
xv6 是一個(gè)非常非常經(jīng)典且簡(jiǎn)單的操作系統(tǒng),是由麻省理工學(xué)院為操作系統(tǒng)工程的課程開(kāi)發(fā)的一個(gè)教學(xué)目的的操作系統(tǒng),所以非常適合操作系統(tǒng)的學(xué)習(xí)。
?

?
而在它的源代碼中,又恰好實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的 shell 程序,所以閱讀它的代碼,對(duì)我們這個(gè)系列課程來(lái)說(shuō),簡(jiǎn)直再合適不過(guò)了。
?

?
看到?jīng)],甚至在這么一個(gè)小小的截圖里,已經(jīng)可以完整展示 sh.c 里全部的 main 方法代碼了。
?
但我仍然十分貪婪,即便是這么短的代碼,我也幫你把一些多余的校驗(yàn)邏輯去掉,再去掉關(guān)于 cd 命令的特殊處理分支,來(lái)一個(gè)最干凈的版本。
//?xv6-public?sh.c
int?main(void)?{
????static?char?buf[100];
????//?讀取命令
????while(getcmd(buf,?sizeof(buf))?>=?0){
????????//?創(chuàng)建新進(jìn)程
????????if(fork()?==?0)
????????????//?執(zhí)行命令
????????????runcmd(parsecmd(buf));
????????//?等待進(jìn)程退出
????????wait();
????}
}
看,shell 程序變得異常簡(jiǎn)單了!
?
總得來(lái)說(shuō),shell 程序就是個(gè)死循環(huán),它永遠(yuǎn)不會(huì)自己退出,除非我們手動(dòng)終止了這個(gè) shell 進(jìn)程。
?
在死循環(huán)里面,shell 就是不斷讀?。?strong>getcmd)我們用戶輸入的命令,創(chuàng)建一個(gè)新的進(jìn)程(fork),在新進(jìn)程里執(zhí)行(runcmd)剛剛讀取到的命令,最后等待(wait)進(jìn)程退出,再次進(jìn)入讀取下一條命令的循環(huán)中。
?
由此你是不是也感受到了 xv6 源碼的簡(jiǎn)單之美,真的是見(jiàn)名知意,當(dāng)你跟我走完這個(gè) Linux 0.11 之旅后,再去閱讀 xv6 的源碼你會(huì)覺(jué)得非常舒服,因?yàn)?Linux 0.11 很多地方都用了非常騷的編碼技巧,使得理解起來(lái)很困難,誰(shuí)讓 Linus 這么特立獨(dú)行呢。
?
我們之前說(shuō)過(guò) shell 就是不斷 fork + execve 完成執(zhí)行一個(gè)新程序的功能的,那 execve 在哪呢?
?
那我們就要看執(zhí)行命令的 runcmd 代碼了。
void?runcmd(struct?cmd?*cmd)?{
????...
????struct?execcmd?ecmd?=?(struct?execcmd*)cmd;
????...
????exec(ecmd->argv[0],?ecmd->argv);
????...
}
這里我又省略了很多代碼,比如遇到管道命令 PIPE,遇到命令集合 LIST 時(shí)的處理邏輯,我們僅僅看單純執(zhí)行一條命令的邏輯。
?
可以看到,就是簡(jiǎn)簡(jiǎn)單單調(diào)用了個(gè) exec 函數(shù),這個(gè) exec 是 xv6 代碼里的名字,在 Linux 0.11 里就是我們?cè)?第35回 | execve 加載并執(zhí)行 shell 程序 里講的 execve 函數(shù)。
?
shell 執(zhí)行一個(gè)我們所指定的程序,就和我們?cè)?Linux 0.11 里通過(guò) fork + execve 函數(shù)執(zhí)行了 /bin/sh 程序是一個(gè)道理。
?
你看,fork 和 execve 函數(shù)你一旦懂了,shell 程序的原理你就直接秒懂了。而 fork 和 execve 函數(shù)的原理,其實(shí)如果你非常熟練地掌握中斷、虛擬內(nèi)存、文件系統(tǒng)、進(jìn)程調(diào)度等更為底層的基礎(chǔ)知識(shí),其實(shí)也不難理解。
?
所以,根基真的很重要,本回已經(jīng)到操作系統(tǒng)啟動(dòng)流程的最后一哆嗦了,如果你現(xiàn)在感覺(jué)十分混亂,最好的辦法就是,不斷去啃之前那些你認(rèn)為"無(wú)聊的"、"沒(méi)用的"章節(jié)。
?
好了,今天的 shell 就到這里了,畢竟我們是講 Linux 0.11 核心流程的系列,不必過(guò)多深入 shell 這個(gè)應(yīng)用程序。
?
接下來(lái)有個(gè)問(wèn)題,shell 程序執(zhí)行了,操作系統(tǒng)就結(jié)束了么?
?
欲知后事如何,且聽(tīng)下回分解。
------- 關(guān)于本系列?-------
本系列的開(kāi)篇詞看這,開(kāi)篇詞
本系列的番外故事看這,讓我們一起來(lái)寫(xiě)本書(shū)?也可以直接無(wú)腦加入星球,共同參與這場(chǎng)旅行。
最后,本系列完全免費(fèi),希望大家能多多傳播給同樣喜歡的人,同時(shí)給我的 GitHub 項(xiàng)目點(diǎn)個(gè) star,就在閱讀原文處,這些就足夠讓我堅(jiān)持寫(xiě)下去了!我們下回見(jiàn)。
