環(huán)境
系統(tǒng)Ubuntu 20.04 64位系統(tǒng)
HW地址:HW2-Shell
正文
本次實(shí)驗(yàn)難度一般般,不需要寫很多的代碼,并且能夠幫我們熟悉常用的Unix system call((),比如說(shuō)open,close等等。在正式做這個(gè)作業(yè)之前,務(wù)必先閱讀xv6Book chapter 0。記下chapter 0中的對(duì)于各個(gè)system call的詳細(xì)描述。這樣能幫助我們對(duì)于本次作業(yè)的那些代碼的理解。
實(shí)驗(yàn)
下載實(shí)驗(yàn)用的shll,去這個(gè)超鏈接把里面的sh.c拷貝出來(lái)。然后把下面的語(yǔ)句寫入到一個(gè)叫t.sh文件:
ls > y
cat < y | sort | uniq | wc > y1
cat y1
rm y1
ls | sort | uniq | wc
rm y
接下來(lái)呢,把sh.c編譯下,就會(huì)得到一個(gè)a.out
gcc sh.c
然后運(yùn)行下面命令:
./a.out < t.sh
不出意外的話應(yīng)該會(huì)看到一大堆的錯(cuò)誤信息。這是因?yàn)槟氵€沒實(shí)現(xiàn)sh.c里面的代碼。你可能不知道<什么意思,問題不大,到這里為止下面的先別看了,先去看xv6Book的chapter0吧
實(shí)現(xiàn)最簡(jiǎn)單的命令
如果編譯了剛才那個(gè)sh.c,得到了a.out,如果我們使用
./a.out
然后在里面輸入ls,會(huì)發(fā)現(xiàn)是沒有結(jié)果的,報(bào)錯(cuò)。課程里面讓我們?nèi)タ聪耬xec的man page??催^(guò)xv6book我們應(yīng)該知道如果要執(zhí)行一個(gè)命令,應(yīng)該使用exec這個(gè)系統(tǒng)調(diào)用。這個(gè)系統(tǒng)調(diào)用有兩個(gè)參數(shù):
int execv(const char *path, char *const argv[]);
第一個(gè)表示需要運(yùn)行的程序的路徑,比如說(shuō),ls, wc等等。第二個(gè)參數(shù)表示程序所需呀的參數(shù),比如說(shuō)ls /,打印根目錄下的所有的目錄。
Most programs ignore the first argument, which is conventionally the name of the program
xv6book提到了說(shuō)大多數(shù)的程序忽略了第一個(gè)參數(shù),因?yàn)樗ǔJ浅绦虻拿帧?strong>也就是說(shuō),agrv[0]是程序名字,agrv是參數(shù)。
struct execcmd {
int type; // ' '
char *argv[MAXARGS]; // arguments to the command to be exec-ed
};
懂了吧,那么答案就很簡(jiǎn)單了,我們通過(guò)系統(tǒng)調(diào)用execv來(lái)執(zhí)行,第一個(gè)argv[0]是程序名字,argv是參數(shù)。答案呼之欲出:
case ' ':
ecmd = (struct execcmd*)cmd;
if(ecmd->argv[0] == 0)
_exit(0);
// fprintf(stderr, "exec not implemented\n");
// Your code here ...
execv(ecmd->argv[0],ecmd->argv);
break;
你再試試,是不是此時(shí)可以使用/bin/ls打印目錄了呢?

可以看到,輸入/bin/ls,當(dāng)前目錄下的內(nèi)容都輸出了。
IO redirection
上面的實(shí)驗(yàn)已經(jīng)可以運(yùn)行一些簡(jiǎn)單的命令了,但是對(duì)于IO redirection還是無(wú)法實(shí)現(xiàn)的。你在./a.out里面輸入下面的命令:
echo "6.828 is cool" > x.txt
cat < x.txt
在當(dāng)前目錄下,并沒有出現(xiàn)x.txt。所以接下來(lái)就要去實(shí)現(xiàn)IO redirection了??赐陎v6 book 關(guān)于 IO redirection 這一塊內(nèi)容后。應(yīng)該已經(jīng)知道了任何一個(gè)程序都有三個(gè)標(biāo)準(zhǔn)file descriptor:
standard input: 0
standard output: 1
standard error:2
我沒有去認(rèn)真研究過(guò)這些標(biāo)準(zhǔn)輸入所代表的意義是什么。助于理解,我直接將標(biāo)準(zhǔn)輸出理解為往shell輸出結(jié)果,標(biāo)準(zhǔn)輸入就是命令的輸入內(nèi)容。下面以xv6 book中 cat< input.txt作為一個(gè)例子:
char *argv[2];
argv[0] = "cat";
argv[1] = 0;
if(fork() == 0) {
close(0);
open("input.txt", O_RDONLY);
exec("cat", argv);
}
大概就是這樣的代碼,回想一下close()和open()這兩個(gè)系統(tǒng)調(diào)用的解釋:
close():The close system call releases a file descriptor, making it free for reuse by a future open, pipe, or dup system call (see below)
open():
A process may obtain a file descriptor by opening a file, directory, or device, or by creating a pipe, or by duplicating an existing descriptor. A newly allocated file descriptor is always the lowest-numbered unused descriptor of the current process
理解了吧,close()會(huì)釋放一個(gè)file descriptor,留給其他open(),pipe()使用。新分配的一個(gè)file descriptor總是可用的最小的file descriptor。
還有一個(gè)fork(),關(guān)于fork()的解釋,回想一下最開始在shell當(dāng)中執(zhí)行一個(gè)程序的過(guò)程。
- main (8701) 這里一個(gè)while(getcmd(buf, sizeof(buf)) >= 0)等待來(lái)自用戶的輸入
- 然后父進(jìn)程(shell)進(jìn)入到wait(),父進(jìn)程(shell)調(diào)用了fork(復(fù)制了一份shell,子進(jìn)程)執(zhí)行runcmd()函數(shù)
- 然后runcmd中判斷此時(shí)的是IO redirection 還是pipe還是說(shuō)就去執(zhí)行一個(gè)程序
所以說(shuō)要去執(zhí)行一個(gè)新的程序的時(shí)候,必然是要fork一個(gè)程序,然后讓父shell進(jìn)入wait()。上面cat< input.txt的例子自然就很好理解,先f(wàn)ork一個(gè)程序,這個(gè)程序是真正去執(zhí)行命令的。我相信你如果仔細(xì)的閱讀過(guò)xv6book,已經(jīng)能感受到file descriptor 到底妙在哪里。下圖是一個(gè)進(jìn)程:

不需要關(guān)注0 1 和這兩個(gè)file descriptor所對(duì)應(yīng)的到底是文件,還是device,還是一個(gè)dup得到的新的file descriptor。我只要往里面操作就行了,不必要關(guān)注它到底是什么。
所以上面代碼真的太好理解了,close(0),釋放0這個(gè)file descriptor,此時(shí)0就是lowest-number的file descriptor,然后我們重新open("input.txt"),就把0給了指向input.txt這個(gè)文件。接下來(lái)的任務(wù)就是交給cat去做了。所以如何實(shí)現(xiàn)一個(gè)IO答案呼之欲出了吧,先關(guān)閉對(duì)應(yīng)的file descriptor,然后open一個(gè)新的好了,然后再去執(zhí)行命令。
實(shí)現(xiàn)IO redirection:
case '<':
rcmd = (struct redircmd*)cmd;
// fprintf(stderr, "redir not implemented\n");
// Your code here ...
close(rcmd->fd);
if(open(rcmd->file, rcmd->flags) < 0){
fprintf(2, "open %s failed\n", rcmd->file);
_exit(0);
}
runcmd(rcmd->cmd);
break;
再去實(shí)現(xiàn)在IO redirection這一節(jié)開始,那兩句代碼吧,看看是否已經(jīng)得到了x.txt。這里會(huì)因?yàn)槲覀冊(cè)趏pen()沒有指定permission,所以打開這個(gè)文件的時(shí)候記得+sudo。
我的實(shí)驗(yàn)結(jié)果:

成功了!
實(shí)現(xiàn)Pipe
Pipe說(shuō)白就是內(nèi)核當(dāng)中一段緩存,是以一對(duì)descriptor供文件使用大,一個(gè)指向了pipe的寫端,一個(gè)指向了pipe的讀端口。pipe的作用就是讓兩個(gè)進(jìn)程之間通信。我個(gè)人曾經(jīng)在pipe這里花了不少時(shí)間,肯定上面兩個(gè)多。為了更好理解,先來(lái)說(shuō)一下xb6 book中示例代碼的:
int p[2];
char *argv[2];
argv[0] = "wc";
argv[1] = 0;
pipe(p);
if(fork() == 0) {
close(0);
dup(p[0]);
close(p[0]);
close(p[1]);
exec("/bin/wc", argv);
} else {
close(p[0]);
write(p[1], "hello world\n", 12);
close(p[1]);
}
現(xiàn)在我們應(yīng)該很熟悉了,我們需要執(zhí)行一個(gè)程序肯定先f(wàn)ork一個(gè)新的進(jìn)程。父進(jìn)程與子進(jìn)程都有一對(duì)file descriptor指向了這個(gè)pipe(注意,這個(gè)pipe是子父進(jìn)程共用的,試想一下如果子父進(jìn)程各自有管道,那還怎么通信呢?)。上述的示例代碼要完成的任務(wù)是: wc程序的standard input連到pipe的read一端。
上述代碼解釋
首先先使用system call,pipe()建立一個(gè)pipe,并且p[1]是write一端的file descriptor,p[0]是read一端的descriptor。接著fork一個(gè)子進(jìn)程,它負(fù)責(zé)去執(zhí)行wc實(shí)際的內(nèi)容。子進(jìn)程和進(jìn)程有相同的內(nèi)容,所以父進(jìn)程首先if(fork() == 0),條件不成立,但是創(chuàng)建了子進(jìn)程。于是父進(jìn)程跳轉(zhuǎn)到了:
else {
close(p[0]);
write(p[1], "hello world\n", 12);
close(p[1]);
}
此時(shí)父進(jìn)程,關(guān)閉了管道的wirte一端,如果我們不關(guān)閉,如果程序一直往里面塞數(shù)據(jù),那不是永遠(yuǎn)都看不到結(jié)束了?所以我們close(p[0]),接著父進(jìn)程wirte(p[1]),往pipe的寫端寫入數(shù)據(jù)就行。好,接下來(lái)去子程序:
if(fork() == 0) {
close(0);
dup(p[0]);
close(p[0]);
close(p[1]);
exec("/bin/wc", argv);
}
在子進(jìn)程中if(fork() == 0)條件成立,所以我們首先先關(guān)閉子進(jìn)程的standard input,然后dup(p[0]),接著關(guān)閉pipe的兩個(gè)fiel descriptor。這里有有疑問的地方就是可能是父進(jìn)程不是關(guān)閉了嗎,怎么子進(jìn)程還要關(guān)閉。這樣理解,pipe是一塊內(nèi)存,每一個(gè)進(jìn)程要通過(guò)一對(duì)file descriptor來(lái)寫入或者讀。簡(jiǎn)而言之,pipe是只有一個(gè),但是每一個(gè)進(jìn)程往pipe操作的時(shí)候是使用各自的file descriptor的。dup()的目的也很好理解,close(0)把standard input空出來(lái),然后dup,那么就把pipe的read指向了standard input。自然wc讀取的數(shù)據(jù)都是來(lái)自pipe。解釋完畢~
PS:
If no data is available, a read on a pipe waits for either data to be written or all
file descriptors referring to the write end to be closed; in the latter case, read will re-
turn 0, just as if the end of a data file had been reached.The fact that read blocks until it is impossible for new data to arrive is one reason that it’s important for the child to close the write end of the pipe before executing wc above: if one of wc’s file descriptors referred to the write end of the pipe, wc would never see end-of-file.
在執(zhí)行wc之間,一定要關(guān)閉write,否者wc永遠(yuǎn)看不到end of file。第一次看到這里也有點(diǎn)沒理解?;叵胍幌拢艿朗枪玫?,但是有各自的file desciptor。pipe會(huì)一直等著數(shù)據(jù)來(lái)或者所有指向write端的file descriptor關(guān)閉的時(shí)候(子父進(jìn)程都有file descriptor指向pipe),read()會(huì)返回0。這種形式就是阻塞式的(block),如果我們不把子父進(jìn)程的write端的close了,那么wc就永遠(yuǎn)看不到end of file。因?yàn)閞ead一直就阻塞著,只有在all file descriptors referring to the write end to be closed的時(shí)候才返回。懂了吧,這就解釋了為什么一定要先關(guān)閉所有的wirte端,才可以執(zhí)行程序。
上面代碼還不是一個(gè)真正的pipe,下面代碼就是一個(gè)真正的pipe:
echo "hello world!" | wc -l
pipe的實(shí)際意義就是嘛我們可以同時(shí)運(yùn)行多個(gè)進(jìn)程,前一個(gè)進(jìn)程的輸出是后一個(gè)進(jìn)程的輸入。中間通過(guò)的媒介就是pipe,示意圖如下:

設(shè)想一下,我們要在shell里面運(yùn)行兩個(gè)程序,一個(gè)echo "hello world",另外一個(gè) wc -l。所以很自然的需要fork兩個(gè)進(jìn)程。然后echo輸出到p[1],wc從p[0]讀取。
pipe實(shí)現(xiàn)的詳細(xì)思路:
如上圖所示的例子,我們首先 fork(),把close(1)。把標(biāo)準(zhǔn)輸出替換為p[1],這樣一來(lái)就把輸入內(nèi)容發(fā)送到pipe當(dāng)中去了。再把剩下的工作較給echo,echo程序來(lái)負(fù)責(zé)把內(nèi)容發(fā)送到pipe。別忘了發(fā)送完了記得關(guān)閉pipe,所以close(p[0]),close(p[1])。此時(shí)file descriptor 1可用,所有dup(p[1])。echo的standard output就指向p[1]。
與之相類似,wc先close(0),然后dup(p[0]),這樣一來(lái)就可以從pipe讀取數(shù)據(jù)。用完了記得關(guān)閉,剩下的任務(wù)較給wc去做。
不知道各位老鐵懂了沒,不得不說(shuō)真的設(shè)計(jì)的太妙了,如此的通用,不需要做任何的改變。將輸入輸出抽象出來(lái),太妙了?。?!
pipe的具體實(shí)現(xiàn)
case '|':
pcmd = (struct pipecmd*)cmd;
// fprintf(stderr, "pipe not implemented\n");
// Your code here ...
if(pipe(p) < 0) {
fprintf(stderr, "fail to create pipe\n");
_exit(0);
}
if(fork1() == 0){
close(1);
dup(p[1]);
close(p[0]);
close(p[1]);
runcmd(pcmd->left);
}
if(fork1() == 0){
close(0);
dup(p[0]);
close(p[0]);
close(p[1]);
runcmd(pcmd->right);
}
close(p[0]);
close(p[1]);
wait(&r);
wait(&r);
break;
對(duì)照一下上面的圖,應(yīng)該可以理解的,別忘了,和前面一樣,父進(jìn)程(shell)和兩個(gè)子進(jìn)程(這倆也是shell,只不過(guò)他們?nèi)?zhí)行wc和echo了)有相同的file descriptor, 我們還要關(guān)閉所有父進(jìn)程中的的指向read和write的file descriptor。這樣以來(lái)pipe就只有echo 和 wc在使用了。在xv6 book當(dāng)中有一句話提到a shell may create a tree of process。這句話很關(guān)鍵,比如說(shuō)一個(gè)pipe: a | b,b= c |d 。b進(jìn)程又可以分為兩個(gè)子進(jìn)程。所以我們最開始在執(zhí)行命令的時(shí)候是fork的是a | b, 然后執(zhí)行a | b的時(shí)候,fork a進(jìn)程,在fork()b進(jìn)程,這就解釋了為什么在pipe的時(shí)候又要fork().

這是我的實(shí)驗(yàn)結(jié)果,可以看到pipe已經(jīng)正常工作了?。?/p>
結(jié)尾
chapter 0 看了兩邊才理解這些內(nèi)容,一開始草草看了下,沒有記住多少的東西。后面結(jié)合這個(gè)作業(yè)和chapter 0好好看。受益匪淺,體會(huì)到了unix一些設(shè)計(jì)是多么的高雅。以前不知道pipe,這次學(xué)習(xí)也知道pipe和IO redirection還是挺好用的。Linux 上手不容易,用習(xí)慣了越來(lái)越發(fā)現(xiàn)它的的好處!本次實(shí)驗(yàn)有一些challenge,不會(huì)做,暫時(shí)放棄。