使用已學習的各種C函數實現一個簡單的交互式Shell,要求:
1、給出提示符,讓用戶輸入一行命令,識別程序名和參數並調用適當的exec函數執行程序,待執行完成后再次給出提示符。
2、該程序可識別和處理以下符號:
1) 簡單的標准輸入輸出重定向:仿照例 "父子進程ls | wc -l",先dup2然后exec。
2) 管道(|):Shell進程先調用pipe創建管道,然后fork出兩個子進程。一個子進程關閉讀端,調用dup2將寫端賦給標准輸出,另一個子進程關閉寫端,調用dup2把讀端賦給標准輸入,兩個子進程分別調用exec執行程序,而Shell進程把管道的兩端都關閉,調用wait等待兩個子進程終止。
實現步驟:
1. 接收用戶輸入命令字符串,拆分命令及參數存儲。(自行設計數據存儲結構)
2. 實現普通命令加載功能
3. 實現輸入、輸出重定向的功能
4. 實現管道
5. 支持多重管道
以上。
出於簡單,我假設我們輸入的命令字符串是符合要求,沒有錯誤的。
我們要實現的有:普通命令;輸入輸出重定向;單個管道。有四種情況:①ls -ahl 單個命令;②ls -alh > a.txt 輸出重定向;③ls -ahl | grep root 管道;④cat < a.txt輸出重定向。其實更具體細分還有命令帶參數和不帶參數的情況。情況有這幾種,我們應該用標志將他們區分,所以,儲存命令的數據結構就很重要了。這是我設計的結構體:
typedef struct My_order
{
char *argv[32]; //命令以及參數、文件
int pipe;
int right;
int left;
} My_order;
我將其命名為My_order。現在我們要做的事是解析用戶輸入的命令字符串:ls -ahl | grep root 。理想情況下,我們應該將其拆分為 ls 、-ahl、|、grep、root這些字符串。該怎么拆分呢?觀察命令字符串:命令參數之間用空格隔開的,我們可以利用這個特性。但是我們要自己造輪子么?不用,C庫函數為我們提供了一個字符串分割函數strtok():
原型:char *strtok(char *restrict s1,const char * restrict s2);
描述:該函數把s1字符串分解為單獨的記號。s2字符串包含了作為記號分隔符的字符。按順序調用該函數。第一次調用時,s1應指向待分解的字符串。函數定位到非分隔符后的第一個記號分隔符,並用空字符替換它。函數返回一個指針,指向存儲第一個記號的字符串。若未找到,返回NULL。再次調用strtok查找字符串中的更多記號。每次調用都返回指向下一個記號的指針。未找到返回NULL。
於是,我們像下面這樣調用該函數就可以完美的解決問題了。
int resolve_order(My_order *my_order, char p[])
{
//先初始化
my_order->pipe = my_order->left = my_order->right = 0;
for (int i = 0; i != 32; i++)
{
my_order->argv[i] = NULL;
}
int i = 0;
int option = 0;
my_order->argv[i] = strtok(p, " ");
while (my_order->argv[++i] = strtok(NULL, " "))
{
if (strcmp(my_order->argv[i], " | ") == 0)
{
my_order->pipe++;
}
else if (strcmp(my_order->argv[i], ">") == 0)
{
my_order->right++;
}
else if (strcmp(my_order->argv[i], "<") == 0)
{
my_order->left++;
}
}
return 0;
}
當命令字符串中有管道,輸入輸出重定向符的時候,相應的值就要增加。但是最多也只能是1,再多的話,我這個簡單的shell就不能勝任了。即像這樣的命令:cat|cat|cat我是解決不了的。
我們上面的示例命令有管道,所以我們要用到pipe函數,建立管道,使進程之間能夠相互通訊。但是我們第一步是要創建進程,不多,一個就夠了,使用fork()函數。但是在此之前我們還有問題要解決:是子進程解決管道前面的命令呢還是父進程先解決?子進程和父進程誰先執行?這里廢話一點:以前有個牛人(抱歉不記得是誰了,若是知道請告知)做了個實驗:觀察父子進程誰先被執行,最后得出的結論是絕大部分情況下是父進程先搶到CPU資源。但是這並沒有理論支撐。計算機科學沒有理論來支持這個結論。(當故事聽就好哈,不要較真,本人還是萌新。)雖然有大牛得出這樣的結論來了,但是我還是沒有遵循這個結論。^_^。所以我讓子進程去執行管道前面的命令了, 哎。還好我寫了這個博客,不然我會鬧大笑話。必須要兩個子進程,一個不行,除非我就執行這一個管道命令。exec族函數的一大特點是什么?執行完成指定程序之后根本就不回來!意味着這個進程死掉了,無論是父進程還是子進程都會被回收掉。所以還是要兩個子進程,這里要注意的是,使用兄弟進程進行通訊的時候父進程應該使用waitpid函數進行非阻塞回收。但是在我的實現上依舊有那種阻塞情況發生,是在是不懂怎么回事。不過這不重要。(瑪德,廢話真多。)代碼:
void my_pipe(My_order *my_order)
{
int fd[2];
int p_ret = pipe(fd);//fd[0]->r;fd[1]->w
if (-1 == p_ret)
{
perror("pipe error ");
exit(1);
}
int i = 0;
int pid;
for (; i != 2; i++)
{
if (!(pid = fork()))
{
break;
}
}
if (0 == i)
{
if (strlen(my_order->argv[1]) > 1)
{
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp(my_order->argv[3], my_order->argv[3], my_order->argv[4], NULL);
}
else
{
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp(my_order->argv[2], my_order->argv[2], my_order->argv[3], NULL);
}
}
else if (1 == i)
{
if (strlen(my_order->argv[1]) > 1)//有參數
{
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp(my_order->argv[0], my_order->argv[0], my_order->argv[1], NULL);
}
else
{
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp(my_order->argv[0], my_order->argv[0], NULL);
}
}
else
{
waitpid(-1, NULL, WNOHANG);
waitpid(-1, NULL, WNOHANG);
}
return 0;
}
我寫的不夠嚴謹,都沒有什么錯誤檢查。別像我這樣寫,要檢查錯誤,檢查函數返回值。
比如就是萬一有用戶這樣寫 ls -alh | a.txt 雖然這樣在真正的shell也不能通過,但是別人有錯誤提示啊。
其實有管道這個是整個程序中最難的部分。接下來的重定向其實很簡單的。進過我的測試(用我那點可憐的知識)發現,重定向無非三種正確(的簡單的)情況:命令>命令;命令>文件;命令<文件。前面的部分全是命令,后面的就稍微有點不同。那么問題來了:如何判斷后面的是文件還是命令?以有無后綴區分?但是在Linux中后綴是方便我們識別的而不是系統的剛需啊。我也經常看到gcc main.c -o a這樣的命令啊。(別噴別噴)沒事,大部分的Linux命令都在/bin目錄下呢。簡單的實現也無需考慮那么多,現在就是我們需要去查看目錄中有無對應字符串內容的命令。讀目錄也很簡單啊,我的博客前幾篇(忘了哪一篇了)介紹了讀取指定目錄獲取文件數目內容。我們稍微變換一下就可以用來區分文件or命令了:
int get_dirfile(char *name) //命令存在返回0;不存在返回-1;
{
DIR *dir = opendir(" / bin");
struct dirent *di;
while ((di = readdir(dir)) != NULL)
{
if (strcmp(di->d_name, name) == 0)
{
return 0;
break;
}
}
return -1;
}
//是命令就執行。是文件就打開(創建)。打開文件也很簡單嘛:
int open_file(char p[])
{
int o_ret = open(p, O_RDWR | O_CREAT | O_TRUNC, 0644);
if (o_ret == -1)
{
perror("open file error ");
exit(1);
}
return o_ret;
}
相關的函數、宏若不知道意思,請參閱前幾篇(也忘了是哪一篇了)的介紹。
接下來,就要解決重定向了。dup2函數一定是需要的(我也在前幾篇介紹了的),這里就不介紹了。接下來就很簡單了,就是每個命令就要確定一下參數有無。
int exec_order(My_order *my_order)
{
if ((my_order->pipe == 0) && (my_order->left == 0) && (my_order->right == 0))
{
if (!fork())
{
if (my_order->argv[1] != NULL)
execlp(my_order->argv[0], my_order->argv[0], my_order->argv[1], NULL);
else
execlp(my_order->argv[0], my_order->argv[0], NULL);
}
else
{
wait(NULL);
}
}
else if ((my_order->pipe == 1) && (my_order->left == 0) && (my_order->right == 0))
{
my_pipe(my_order);
}
else if ((my_order->pipe == 0) && (my_order->left == 1) && (my_order->right == 0))
{
if (!fork())
{
execlp(my_order->argv[0], my_order->argv[0], my_order->argv[2], NULL);
}
else
{
wait(NULL);
}
}
else if ((my_order->pipe == 0) && (my_order->left == 0) && (my_order->right == 1))
{
if (!fork())
{
if (strlen(my_order->argv[1]) > 1)
{
int fd = open_file(my_order->argv[3]);
dup2(fd, STDOUT_FILENO);//執行之后,標准輸入就指向了fd
execlp(my_order->argv[0], my_order->argv[0], my_order->argv[1], NULL);
close(fd);
}
else
{
int fd = open_file(my_order->argv[2]);
dup2(fd, STDOUT_FILENO);
execlp(my_order->argv[0], my_order->argv[0], NULL);
close(fd);
}
}
else
{
wait(NULL);
}
}
}
其實這里有個小問題,就是像ps這樣的命令參數是沒有-的,直接就是ps a這樣。為了簡便,先這樣吧。
main函數就很簡單了。
int main(void)
{
while (1)
{
My_order my_order;
char p[32] = { '\0' };
puts("GYJ_LoveDanDan@desktop:—————————————— - ");
gets(p);
//char p[8] = { "ls -alh | grep lovedan " };
resolve_order(&my_order, p);
exec_order(&my_order);
}
return 0;
}
寫個這程序,真的是,感覺到了自己真是菜雞。最開始的任務其實有這個:
你的程序應該可以處理以下命令:
○ls△-l△-R○>○file1○
○cat○<○file1○|○wc△-c○>○file1○
注:○表示零個或多個空格,△表示一個或多個空格
5. 支持多重管道:類似於cat|cat|cat
我最開始為了解析字符串,操碎了心,眼看就要成功了,但是因為我用來儲存的數據結構不好用於執行execlp函數,就放棄了,幾經波折,我看透了。自己砍了要求,很勉強的實現了這個四不像shell。我想問人,沒人回答我,我想查資料,沒找到。這也許就是小說中散修和宗門的區別吧。