su.Zero
back to main sitelogo and back to blog home
 
 

基本上,如果你是個對資訊非常熱情的人,應該都會對自己做一個系統層級的程式感到熱血 XD 做 Shell 真的感覺很酷阿! 雖然我做出來的 Shell 功能簡單,不過還是覺得很有趣。(對啦,我承認我不會拿來日常使用啦XD)同時,這也是常見的 OS 課程作業。我在弄完自己的 ZeroShell 之後,決定把一些小技巧分享出來。因為有些地方實在很難搞。

基本架構

1
2
3
4
while(true)
{
  ...
}

這是 Shell 的基本架構,因為 Shell 在執行程式跑完之後,應該要繼續顯示提示字元等待輸入。接著執行命令,如此週而復始。一個基本的 Shell 需要有兩個部分,一個部分分析輸入的指令,另外一個部分則負責執行指令。在 ZeroShell 之中,分析的部份稱之 ZRCommandParser 而執行的部份則是 ZRSh。在輸入的地方我是用 libreadline 來處理,因為使用這個函式庫,可以直接讓你的程式變得超專業 XD 支援上下切換命令,自動完成等等。詳細的部份就請直接參考 github 上的原始碼了。接下來提一些實際在寫程式的時候遇到的小問題及解決方法。

執行外部程式

1
2
3
4
5
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);

如果你要執行別的程式,當然是利用 exec 系列函式來做。但是 exec家族實在太多了,到底要用哪個?我建議是自己處理搜尋目錄的問題,然後用 execv 函式執行,這樣彈性會比較高。另外,請不要使用外部程式執行 cd,因為你如果 fork 出新 process 再跑 cd,切換的目錄不會影響到原有的 process。回到主 process 後會發現目錄根本沒有切換。解決這個問題,請使用 chdir 函式來切換。也請特別注意,exec 家族會將呼叫的 process 整個 image 換成被呼叫的程式。因此程式結束後 並不會 回到原程式,因此你需要 fork

Fork 叉子

對不起我嘗試耍冷了…XD fork 是一個很重要卻又很容易搞混的基本概念。簡單的說你可以這樣想,當一直執行 fork(),你的程式就會 撲~茲~~~(為什麼會發出這種聲音? XD) 的變成兩半。你可以想像一下這是細胞分裂,因為這兩邊是一模一樣的。不過這樣我要怎麼知道到底兩隻程式差別在哪?因此為了讓你能夠知道,fork() 函式耍了點招式! 在主程式會回傳子程式的 PID,因此是 > 0 的某數字。在有絲分裂出來的子程式回傳值是 0。如果失敗,主程式則會收到 -1。以下是範例程式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int pid = fork(); // 在這邊進行了有絲分裂
 
if (pid == 0)
{
    // 這邊是子程式
}
else if (pid > 0)
{
    // 主程式
}
else
{
    // 噢...失敗了 :(
}

在 Shell 中,常用的用法則是主程式依據狀況看是否等 child 執行結束(使用 waitpid )。在子程式中用上一節的 execve 來執行外部程式。

Pipe 水管!

這東西就難了… 為了水管我搞了好幾個晚上… 可是我決定把這件事情留給正在看文章的您自己想通。我只打算在這邊簡單的做一點介紹。

首先,pipe 的概念就像是一條水管。這條水管你在 寫入端(write end) 塞東西進去,他就會在 讀取端(read end) 跑出來。我們通常使用這個功能來把多個程式的 STDIN, STDOUT 串接起來,達到讓上一個程式輸出餵給下一個程式這樣的目的。為了取代原本的 STDIN, STDOUT,你必須使用 dup 或是 dup2 函式來把原有的管道換掉,有點像是改接水管這樣。

呼叫 pipe 必須先準備好一個兩個元素的一維整數陣列,傳進去後就會得到兩個 fd(File Descriptor)。pipe() 回傳 -1 的話就… well 你知道,失敗了。接著我們 fork, 然後在兩邊開始亂搞囉~ 我們在子程式用 dup2(fds[1], STDOUT_FILENO) 把輸入端接上 STDOUT。(沒錯,第二個元素是寫入端的fd,第一個元素是讀取端的fd。)主程式則用 dup2 把 fds[0] 接上 STDIN_FILENO。最後請記得在兩個程式裡面把兩個fd都關掉,這樣才會正確的讓管子可以讀到 EOF。一樣的,提供一個範例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int fds[2];
if (pipe(fds) == -1)
    cout << msg_head << "Unable to create pipe (code: " << errno << ")" << endl;
 
int pid = fork();
if (pid == 0)
{
    if (dup2(fds[1], STDOUT_FILENO) == -1)
        cerr << msg_head << "dup2 failed. (STDOUT)" << endl;
    if (close(fds[0]) == -1)
        cerr << msg_head << "close failed. (PIPE_READ)" << endl;
    if (close(fds[1]) == -1)
        cerr << msg_head << "close failed. (PIPE_WRITE)" << endl;
 
    // 執行第一個程式
}
else if (pid > 0)
{
    if (dup2(fds[0], STDIN_FILENO) == -1)
        cerr << msg_head << "dup2 failed. (STDIN)" << endl;
    if (close(fds[0]) == -1)
        cerr << msg_head << "close failed. (PIPE_READ)" << endl;
    if (close(fds[1]) == -1)
        cerr << msg_head << "close failed. (PIPE_WRITE)" << endl;
 
    // 執行第二個程式
}

不過要怎麼讓這個程式遞迴,可以 pipe 很多程式呢?這就靠你自己思考看看囉,在 zrsh.cpp 可以找到答案。

Ctrl+C

最後一個小技巧我們來談一下怎麼讓 Ctrl+C 不會砍掉自己。按下 Ctrl+C 的時候你的程式會收到 SIGINT。如果你不處理,你的程式就會自動被砍掉。所以你必須自行處理,透過 signal 函式可以變更處理函式。因為這邊很簡單,我就不提供範例程式了。請參考 zrsh.cpp 囉!

呼,總算寫完這篇文章了。希望這篇文章對想要寫 Shell 的人能夠有一點幫助。 :)

 

留言 Comments

 
© 2009 All Rights Reserved. | Powered by WordPress