实现一个简单的shell
使用已学习的各种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。我想问人,没人回答我,我想查资料,没找到。这也许就是小说中散修和宗门的区别吧。