UNIX管道應用及Shell實現(一)-主體框架
作業系統的第一個大作業是做一個簡單的Shell,實現重定向、管道等功能。奮戰了好幾天終於基本搞定了= =
基本要求
Shell能夠解析的命令列法如下:
帶引數的程式執行功能。
program arg1 arg2 … argN
重定向功能,將檔案作為程式的輸入/輸出。
a)“>”表示覆蓋寫program arg1 arg2 … argN > output-file
b)“>>”表示追加寫
program arg1 arg2 … argN >> output-file
c)“<”表示檔案輸入
program arg1 arg2 … argN < input-file
管道符號“|”,在程式間傳遞資料(最後也可用重定向符號)
programA arg1 … argN | programB arg1 … argN | programC …
後臺符號& ,表示此命令將以後臺執行的方式執行
program arg1 arg2 … argN &
工作路徑移動命令cd
Shell退出命令exit
history顯示開始任務後執行的命令;history n顯示最近執行的n條指令
基本思路
很明顯本次實驗主要是以考察Shell基本功能以及管道的實現為主。之前已經詳細講解了管道的實現,可以參考這篇文章。
熟悉命令
首先我們先在UNIX自帶的Shell下實現重定向和管道功能,示例命令可以參考如下:
# ps &
# cat numbers.txt | sort > temp.txt
# sort < numbers.txt | grep 1 > a.txt
# ps -ef | grep -sh
# cd ..
我們不難發現:
1. “>”,“>>”重定向命令只能在命令中出現一次,一旦出現後,之後還有什麼命令也是無效的。
2. “<”命令也只能出現一次,但是後面可以接管道命令。
2. “|”管道命令可以出現多次,且管道之後還可以使用重定向符號。
3. 實際上所有命令進入程式之後都是一串字串,因此對字串的解析是最重要的。
4. 對於ps
5. 如果注意,可以發現系統Shell在實現後臺程序時,可能會出現如下情況:
我們讓ls的結果在後臺執行,但為什麼會在結果前多一個“#”呢?
原因是因為後臺執行的子程序和前臺執行的父程序同時進行,誰先誰後不能確定,圖中就是父程序先執行,列印了“#”,子程序再列印ls的結果,因此出現了這種情況。
難點
- 管道的實現以及fork()的使用。
- 子父程序進行訊號互動,以及回收殭屍程序。
- 多檔案的協調和編譯。
大體框架
主函式入口
由於我們在Windows下寫這個Shell無法編譯,每次必須在UNIX下編譯,因此必須在寫之前就想好模組佈局,不然很難debug和進行單元測試。
一個Shell其實就是一個while(1)的死迴圈,每次輸出提示符到螢幕,然後執行輸入的字串命令。因此不難寫出大體框架:
int main() {
/*Command line*/
while (1) {
printf("cmd >");
/*set buf to empty*/
memset(buf, 0, sizeof(buf));
/*Read from keyboard*/
fgets(buf, MAXLINE, stdin);
/*The function feof() tests the end-of-file indicator
for the stream pointed to by stream,
returning non-zero if it is set. */
if (feof(stdin))
exit(0);
/*update the command history*/
UpdateHistory();
/*command exceve*/
command();
}
return 0;
}
主程式的確很簡單,就是每次用buf讀取輸入的字串,然後更新輸入列表(為了 history功能的實現),然後再解析命令(command)即可。
字串命令儲存方式
Shell主要就是對得到的命令進行操作,因此命令如何儲存是至關重要的。最簡單的想法就是用一個char*[]字串陣列儲存,但是我們後面對命令解析需要 命令的下標等其他資訊,因此這裡選擇用struct進行儲存更為方便。
定義結構體如下:
struct CommandInfomation {
char* argv[512]; /*store the command after Parsing*/
int argc; /*the number of argv,split with space*/
int index; /*store the index of special character*/
int background; /*whether it is a background command*/
enum specify type[50];
int override; /* in case after < command has muti pipes */
char* file;
};
初始化函式為:
void initStruct(struct CommandInfomation* a) {
a->argc = 0;
a->index = 0;
a->background = 0;
a->override = 0;
a->file = NULL;
memset(a->type, 0, sizeof(a->type));
}
特殊字元命令
對於重定向”>”,管道”|”等特殊命令,我們需要使用額外的標識來註明,方便後面的操作。這裡使用eunm實現。
/*the enum stand for different command*/
enum specify {NORMAL, OUT_REDIRECT, IN_REDIRECT, OUT_ADD, PIPE};
主要函式詳解
pipe(fd2)
此函式用於實現無名管道,將fd2陣列中的兩個檔案描述符分別標記為管道讀(fd[0])和管道寫(fd[1])。
dup(fd)
為複製檔案操作符的系統函式,可以定向目前未被使用的最小檔案操作符到fd所指的檔案。相類似的函式還有dup2[fd1,fd2],意思是 未被使用的檔案描述符fd2
作為fd1的副本,進過此函式後,fd1和fd2都可訪問同一個檔案。
execlp(const char *file, const char *arg, …)
屬於exec()函式族,會從PATH環境變數所指的目錄中查詢符合引數file的檔名,找到後便執行該檔案,然後將第二個以後的引數當做該檔案的argv[0]、argv1……,最後一個引數必須用空指標(NULL)作結束。
命令中的ls,ps等內建系統命令都可以由此函式進行解析。要注意,此函式一經呼叫就不會再返回。
chdir(const char * path)
改變當前的工作路徑以引數path所指的目錄,使用比較簡單,支援常用的改變路徑的方式,例如退回上一級:cd .. ,也支援絕對路徑。
執行命令
由主函式可知,我們得到了命令需要進行解析,由於我們知道exceve函式一旦呼叫就不會返回,因此要使用fork()函式對其子程序進行處理。
這裡需要注意的是,由於子程序一定要比父程序先結束,因此我們需要將執行的命令放到子程序中,父程序進行等待或者執行後面的命令,否則會出現父程序結束子程序還在執行的錯誤。
父子程序進行通訊
需要注意的是,Shell支援後臺程式執行,因此,父程序不一定要等待子程序執行結束才做後面的事情,但這就涉及到子程序結束後,父程序需要回收殭屍程序。那麼,如何做到這一點呢?
Linux上進行訊號遮蔽
在Linux系統上,我們可以使用signal(int signum, sighandler_t handler)函式來設定某一類的訊號處理或者遮蔽。我們知道,子程序要exit()之前,會發送SIGCHLD訊號給父程序,提醒父程序來回收子程序的退出狀態和其他資訊。
在這裡,我們可以使用一個特殊的技巧:
signal(SIGCHLD, SIG_IGN)
這裡是讓父程序遮蔽子程序的訊號,為什麼這樣就可以做到回收殭屍程序的作用呢?原來是因為在Linux中,當我們忽略SIGCHLD訊號時,核心將把殭屍程序交由init程序去處理,能夠省去大量殭屍程序佔用系統資源。因此,遮蔽了子訊號後,子程式在要結束時傳送訊號沒人應答,核心就會認為這是一個孤兒程序,因此被init程序去回收,可以很好的解決我們面臨的問題。
BSD系統上的訊號處理
而筆者使用的是Minix3.3的系統,經過實測,核心並不會在父程序遮蔽訊號後主動回收孤兒程序,因此不能使用這種方法。
那怎麼辦呢?因此只能自己寫一個handler,規定父程序在收到子程序結束的訊號後再wait,這樣也可以實現此功能。但缺點就是wait函式需要阻塞父程序直到子程序結束為止,對於併發要求較高的併發伺服器,可能就不是很適用。
我們使用這種方法完成後臺程式的執行:
void SIG_IGN_handler(int sig)
{
waitpid(-1, NULL, 0);
return;
}
在主程式中install這個handler:
signal(SIGCHLD, SIG_IGN_handler);
這樣就完成了後臺程序的功能。
history功能實現
查詢前n個命令是比較簡單的功能,我們可以使用佇列進行實現,在這裡筆者就稍微偷懶一點,直接使用定長的字串陣列進行。
/*update command history*/
void UpdateHistory()
{
char *temp;
if (strcmp(buf, "\n") == 0)
return;
if (HistoryIndex > MAXLINE)
HistoryIndex = 0;
temp = (char *)malloc(sizeof(buf));
strcpy(temp, buf);
CommandHistory[HistoryIndex++] = temp;
return;
}
/*print the command with n lines*/
void PrintCommand(int n)
{
int i,j=0;
if (n == -1) {
for (i = 0 ; i < HistoryIndex; i++)
printf("the %d command: %s\n", i, CommandHistory[i]);
}
else {
if (n > HistoryIndex) {
printf("Warning: the argument is too large.\n");
return;
}
for (i = HistoryIndex - n; i < HistoryIndex; i++)
printf("the %d command: %s\n", ++j, CommandHistory[i]);
}
}
參考資料
- Operating System:Design and Implementation,Third Edition
- Computer Systems: A Programmer’s Perspective, 3/E
歡迎關注我的個人部落格。