要求:
- 能識別
>
,<
的輸入輸出重定向。 - 能識別出管道操作。
- 支持多重管道: 比如
cat | cat | cat | cat
。 - 支持管道和重定向的混合。
- 解決管道輸入輸出重定向和管道輸出重定向和文件重定向共存的問題。
分析:
- 簡單指令
此類指令無重定向, 無管道, 則其執行方式應該是主進程創建一個子進程, 將指令字符串組裝成字符串數組後再添加調用exec
函數即可。
- 帶有重定向的指令
方案一
:
> 和 <
直接當做命令行參數傳遞給exec
函數。經過測試, exec
並不能解析重定向符號。
方案二
:
手動打開文件然後後dup2
了。
- 帶有管道的指令
將指令從管道符號拆分成多條指令, 每條指令分給一個子線程, 並讓一個管道分隔的兩個子進程一個獲得一個管道的讀端, 一個獲得進程寫端, 且將寫端的進程的標準輸出重定向到寫端的文件描述符。如下圖所示:
實現
/**
* 完成一個模擬shell的程序。
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
char commands[1024]; /* 主進程讀入的一個整的指令*/
char parts[100][100][1024]; /* 第一維度表示子進程, 第二維度表示該進程指令參數, 第三個維度爲參數爲具體的值*/
char *vector[100][100]; /* 第一維度表示子進程, 每個進程的vector*/
char input_file[100][1024]; /* 第一維度表示子進程, 輸入重定向的文件*/
char output_file[100][1024]; /* 第一維度表示子進程, 輸出重定向的文件*/
int input_flag[100] = {0}; /* 第一維度表示子進程, 輸入重定向標記*/
char output_flag[100] = {0}; /* 第一維度表示子進程, 輸出重定向標記*/
int pipe_no[100][2]; /* 兩個進程共享一對管道的文件描述符*/
char child_command[1024]; /* 臨時存放一個子進程的指令*/
int child_process_cnt = 0; /* 子進程的數量*/
/**
* 讀入指令, 並對指令進行特殊處理去掉結尾的\n
*/
char * readCommand()
{
char *ret = fgets(commands, 1024, stdin);
if(ret)
{
char * t = strchr(commands, '\n');
*t = '\0';
}
return ret;
}
/**
* 將參數列表進行解析, 使用strsep函數即可。
* 將解析結果形成一個vector, 提供給execv函數使用。
* 解析正確返回0, 解析失敗返回-1。
*/
int parse_command()
{
// 先將大的指令差分成子進程指令
char *bbuf = commands;
char *pp = strsep(&bbuf, "|");
int pro_no = 0;
while(pp)
{
strcpy(child_command, pp);
#ifdef DEBUG
printf("-%s-\n", child_command);
#endif
char *buf = child_command;
char *p = NULL;
p = strsep(&buf, " ");
int index = 0, i = 0;
// 將參數解析的到part當中
while(p)
{
// 可能產生空串的情況
if(strcmp(p, "") == 0)
{
p = strsep(&buf, " ");
continue;
}
#ifdef DEBUG
printf("#%s#\n", p);
#endif
// 遇見輸入重定向標記, 則獲取輸出文件名
if(strcmp(p, "<") == 0)
{
input_flag[pro_no] = 1;
p = strsep(&buf, " ");
if(!p || strcmp(p, "") == 0) return -1;
if(strcmp(p, "<") == 0)
{
// 標準輸出當中執行, 不能連續出現兩次 <
printf("- myshell: synax error near unexpected token '<'");
return -1;
}
strcpy(input_file[pro_no], p);
}
else if(strcmp(p, ">") == 0)
{
output_flag[pro_no] = 1;
p = strsep(&buf, " ");
if(!p || strcmp(p, "") == 0) return -1;
if(strcmp(p, ">") == 0)
{
printf("- myshell: synax error near unexpected token '>'");
return -1;
}
strcpy(output_file[pro_no], p);
}
else strcpy(parts[pro_no][index++], p);
p = strsep(&buf, " ");
}
// 使用part形成vector
for(i = 0; i < index; ++i)
{
vector[pro_no][i] = parts[pro_no][i];
}
vector[pro_no][i] = NULL;
pp = strsep(&bbuf, "|");
pro_no += 1;
// 統計子進程的數量
child_process_cnt += 1;
}
return 0;
}
/**
* 關閉管道的文件描述符
*/
void closePipe()
{
int i = 0;
for(i = 0; i < child_process_cnt -1; ++i)
{
int ret1 = close(pipe_no[i][0]);
int ret2 = close(pipe_no[i][1]);
/* 此處允許error
if(ret1 < 0 || ret2 < 0)
{
printf("pipe close error!\n");
exit(1);
}
*/
}
}
int main()
{
int son_flag = 0; /* 子進程標記*/
system("stty erase ^H");
while(1)
{
printf("simulate Shell # ");
readCommand();
son_flag = 0;
if(strcmp(commands, "exit") == 0)
break;
parse_command();
#ifdef DEBUG
printf("子進程的數量 %d\n", child_process_cnt);
#endif
int i = 0;
for(i = 0; i < child_process_cnt; ++i)
{
// 創建新的管道
if(child_process_cnt > 1 && i != child_process_cnt - 1)
{
int ret = pipe(pipe_no[i]);
if(ret < 0)
{
printf("create pipe error\n");
exit(1);
}
}
// 釋放之前的管道
if(i >= 2)
{
close(pipe_no[i - 2][0]);
close(pipe_no[i - 2][1]);
}
int child_pid = fork();
if(child_pid == 0)
break;
}
if(i < child_process_cnt)
{
#ifdef DEBUG
printf("我是 %d 號進程, 我執行的指令是 #%s#\n", i, parts[i][0]);
#endif
// 先進行文件的重定向操作
if(input_flag[i] == 1)
{
int fd = open(input_file[i], O_RDONLY);
if(fd < 0)
{
printf("- myshell: %s: No such file or directory\n", input_file);
exit(1);
}
dup2(fd, STDIN_FILENO);
}
if(output_flag[i] == 1 && i == child_process_cnt - 1)
{
int fd = open(output_file[i], O_WRONLY | O_CREAT | O_TRUNC, 0644);
if(fd < 0)
{
printf("- myshell: %s :cant't create the file \n", output_file);
exit(1);
}
dup2(fd, STDOUT_FILENO);
}
// 管道處理
if(child_process_cnt > 1)
{
if(i == 0)
{
close(pipe_no[i][0]); /* 關閉讀端*/
dup2(pipe_no[i][1], STDOUT_FILENO); /* 標準輸出重定向到寫端*/
}
else if(i == child_process_cnt - 1)
{
close(pipe_no[i - 1][1]); /* 關閉寫端*/
dup2(pipe_no[i - 1][0], STDIN_FILENO); /* 標準輸入重定向到讀端*/
}
else
{
close(pipe_no[i - 1][1]); /* 關閉上一個管道寫端*/
dup2(pipe_no[i - 1][0], STDIN_FILENO); /* 重定向上一個管道爲輸入*/
close(pipe_no[i][0]); /* 關閉讀端*/
dup2(pipe_no[i][1], STDOUT_FILENO); /* 標準輸出重定向到寫端*/
}
}
int ret = execvp(parts[i][0], vector[i]);
if(ret < 0)
{
printf("%s : command not found or params error\n", parts[0]);
exit(1);
}
}
else
{
// 父進程回收子進程完畢後, 清理子進程數量標記, 子進程重定向標記, 關閉pipe
closePipe();
int ret = 0;
do
{
ret = wait(NULL);
}while(ret > 0);
child_process_cnt = 0;
memset(input_flag, 0, sizeof(input_flag));
memset(output_flag, 0, sizeof(output_flag));
}
}
return 0;
}
##### 難點, 發現, 總結:
- fgets讀取字符串結尾會會添加\n;
fgets
在結尾添加\n
會導致strsep
分離出來的最後一個字符串的結尾有\n
, 會導致execvp
函數解析參數發生錯誤。
- 終端輸入會出現
^H
,^[[D
,^[[C
的控制字符
通過命令stty erase ^H
即可解決。 但是每次重新啓動終端該問題還是存在, 因此每次啓動模擬shell的程序的時候, 就執行一次這個指令即可。
- 多次重定向:
如下指令多次指定了輸入輸出重定向, 則其只會對最後一次那個輸入輸出重定向有效。
cat < a.c < b.c
ps > a.dat > b.dat
strsep
的坑
經過測試 strsep
在開始和結尾處遇見 分隔符的時候會額外產生空串, 在對管道進行分割時, 會產生  指令
和 指令 >
的情況, 因此在對使用  
作爲分割符的時候, 需要跳過空串的情況, 以及重定向的文件名不能是空串的判斷。
- 如何管理管道的文件描述符
經過分析, 只有在子進程數量 > 1的情況下才要創建管道, 以及對管道描述符進行管理, 那麼我們子進程分爲三種情況, 第一個子進程, 中間的子進程, 和最後一個子進程。
第一個子進程關閉輸出端, 並將其標準輸出重定向到輸入端。
中間子進程關閉與上一個子進程之間的管道的輸入端, 並將標準輸入重定向管道的輸出端。 關閉與下一個子進程之間管道輸出端, 並將其標準輸出重定向到管道的輸入端。
對於最後一個子進程關閉輸入端, 並將其標準輸入重定向到管道的輸出端。
難點問題: 確保每個管道我只i要保留一個輸入和輸出端, 因爲像 cat grep
等指令是會循環阻塞等待輸入的, 如果其對應管道有多個寫端, 則上一個指令結束 此指令也不會結束的。
- 管道輸出重定向和文件輸出重定向衝突
比如指令:
ps -aux > 1.dat | cat
ps
的標準輸出是重定向到文件也可以重定向到管道的寫端, 重定向到管道會覆蓋到文件。但是對於上方指令, 我們不能創建出來一個文件, 需要注意。
- 管道輸入重定向和文件輸入重定向的衝突
比如指令:
cat | cat < 1.dat
分析系統shell
的表現個人,猜測文件輸入會覆蓋管道輸入, 造成輸出結果爲第二個指令的進程從1.dat當中讀入數據輸出後結束, 第一個子進程會等待從標準輸入讀入, 輸出到管道寫端, 但是此管道沒有讀端了, 因此內核向此進程發送一個SIGPIPE
信號, 導致此進程結束, 指令解析完畢。