操作系统实验二:shell的实现
- 实验内容
实现具有管道、重定向功能的shell,能够执行一些简单的基本命令,如进程执行、列目录等。
- 实验目的
通过实验,让学生了解Shell实现机制。
- 设计思路和流程图
实验内容主要是管道和重定向,这两个功能涉及shell“|”和“<”以及“>”等不同符号,所以要对输入的命令进行解析。初步按照空格来分,之后再按照<、>、|这些涉及管道和重定向的符号来分。
Shell 输入输出重定向
command > file 将输出重定向到 file
command < file 将输入重定向到 file
command >>file 将输出以追加的方式重定向到 file
n > file 将文件描述符为 n 的文件重定向到 file
n >>file 将文件描述符为 n 的文件以追加的方式重定向到 file
n >& m 将输出文件 m 和 n 合并
n <& m 将输入文件 m 和 n 合并
<< tag 将开始标记 tag 和结束标记 tag 之间的内容作为输入
Shell 管道
Shell 可以将两个或者多个命令(程序或者进程)连接到一起
把一个命令的输出作为下一个命令的输入
以这种方式连接的两个或者多个命令就形成了管道(pipe)
Linux 管道使用竖线|连接多个命令,这被称为管道符。Linux 管道的具体语法格式如下:
command1 | command2
当在两个命令之间设置管道时,管道符|左边命令的输出就变成了右边命令的输入
只要第一个命令向标准输出写入,而第二个命令是从标准输入读取,那么这两个命令就可以形成一个管道
大部分的 Linux 命令都可以用来形成管道。
- 主要数据结构及其说明
主要使用了数组和指针,存放相关的命令,通过字符串操作实现一些基本的逻辑。
- 源程序并附上注释(关键部分)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <sys/stat.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/wait.h>
//该函数将用户输入(即参数 w)以空格为间隔存入数组 ar。其中函数strtok_r 函数的作用是分割字符串,第一个参数w是原字符串,split为分隔符;在w中检索 split,并将匹配之后的字符串写进&lefts 中。
void analyze_user_input(char* w, char** ar)
{
int count = 0;
memset(ar, 0, sizeof(char*) * (64));
char* lefts = NULL;const char* split = " ";
while (1)
{
char* p = strtok_r(w, split, &lefts);
if (p == NULL)
{
break;
}
ar[count] = p;
w= lefts;
count++;
}
if (strcmp(ar[0], "exit") == 0)
exit(0);
}
//该函数的作用是将参数str格式化,删除str中所有空格。
char* get_first_word(char* str)
{
int i = 0;
int j = 0;
char* ptr = malloc(sizeof(char*) * strlen(str));
for (i = 0; str[i] != '\0'; i++)
if (str[i] != ' ')
{
ptr[j] = str[i];
j++;
}
ptr[j] = '\0';
str= ptr;
return str;
}
//该函数的作用是执行基本命令。如果 fork 失败则直接退出;父进程等待子
进程的执行结束,子进程执行参数 argv 所指向的内容。其中 execvp 函数的作用是从 PATH 环境变量所指的目录中查找符合参数,argv[0]的文件名。找到后便执行该文件,并将第二个参数 argv传给欲执行的文件。因此可以从环境变量中找到命令对应的工作目录,并不用用户自行给出具体的路径。由于cd命令无法在exec函数中执行,所以对cd指令单独判断:如果传入的命令是cd命令,则退出。
void execute(char** argv)
{
pid_t pid;
if ((pid = fork()) < 0)
{
printf("error:fork failed.\n");
exit(1);
}
else if (pid == 0)
{
if (execvp(argv[0], argv) < 0 && strcmp(argv[0], "cd"))
printf("error:invalid command.(failed to execvp(**argv)in execute())\n");
exit(0);
}
else
{
while (wait(NULL) != pid);
}
}
//该函数实现输出的重定向,即 command > file,即输出重定向到 file。通过创建子进程实现重定向的功能。参数argv是原始的参数向量,out是分割“>”之后剩下的字符串,即重定向输出的位置。
void execute_output(char** argv, char* out)
{
pid_t pid;
int flag;
char* file = NULL;
if ((pid = fork()) < 0)
{
printf("error:fork failed.\n");
exit(1);
}
else if (pid==0)
{
if (strstr(out, "<") > 0 || strstr(out, ">") > 0 ||
strstr(out, "|") > 0)
{
printf("NO MORE!");
exit(0);
}
int old_stdout = dup(1);
//利用freopen函数。该函数作用是以指定模式重新指到另一个文件。拷贝stdout的文件描述符,将stdout重定向到out这个文件中。
FILE* fp1 = freopen(out, "w+", stdout);
if (execvp(argv[0], argv) < 0)
printf("error:in exec");
fclose(stdout);
//利用fdopen函数。该函数作用是将old_stdout的文件描述符转化为对应文件指针后返回。再执行argv向量的代表的内容,如果执行失败则将stdout 恢复并退出进程。
FILE* fp2 = fdopen(old_stdout, "w");
*stdout = *fp2;
exit(0);
}
else
{
while (wait(NULL) != pid);
}
}
//该函数实现输入重定向,即 command < file,即将输入重定向到 file。
利用 fork 函数创建子进程。利用 strstr 函数判断 out中是否存在“>”;如果out中仍有“>”符号,则说明需要进行输出重定向。
void execute_input(char** argv, char* out)
{
pid_t pid;
int fd;
char* file;
if ((pid = fork()) < 0)
{
printf("error:fork failed\n");
exit(1);
}
else if (pid == 0)
{
if (strstr(out, ">") > 0)
{
char* p = strtok_r(out, ">", &file);
file = get_first_word(file);
int old_stdout = dup(1);
FILE* fp1 = freopen(file, "w+", stdout);
fd = open(out, O_RDONLY);
close(0);
dup(fd);
if (execvp(argv[0], argv) < 0)
{
printf("error:in exec");
}
close(fd);
fclose(stdout);
FILE* fp2 = fdopen(old_stdout, "w");
*stdout = *fp2;
exit(0);
}
fd = open(out, O_RDONLY);
close(0);
dup(fd);
if (execvp(argv[0], argv) < 0)
{
printf("error:in exec");
}
close(fd);
exit(0);
}
else
{
while (wait(NULL) != pid);
}
}
//该函数的作用是实现管道。即实现 command1 | command2,该函数实现思路是将管道前后的命令分别由不同的进程执行,即 command1和 command2 分别通过子进程执行。再通过管道把两个进程的标准输入输出连接起来,把管道前的子进程的标准输出作为后面的输入,即重定向到管道数据入口,把管道后的标准输入重定向到管道数据出口就实现了管道。
void execute_pipe(char** argv, char* out)
{
int pfds[2];
char* file;
pid_t pid, pid2;
int old_stdout;
pipe(pfds);
int blah = 0;
char* args[64];
if ((pid = fork()) < 0)
{
printf("error:fork failed\n");
exit(1);
}
if ((pid2 = fork()) < 0)
{
printf("error:fork failed\n");
exit(1);}
if (pid == 0 && pid2 != 0)
{
close(1);
dup(pfds[1]);
close(pfds[0]);
close(pfds[1]);
if (execvp(argv[0], argv) < 0)
{
close(pfds[0]);
close(pfds[1]);
printf("error:in exec");
kill(pid2, SIGUSR1);
exit(0);
}
}
else if (pid2 == 0 && pid != 0)
{
if (strstr(out, ">") > 0)
{
char* p = strtok_r(out, ">", &file);
file = get_first_word(file);
analyze_user_input(out, args);
blah = 1;
}
else
{
analyze_user_input(out, args);
}
close(0);
dup(pfds[0]);
close(pfds[1]);
close(pfds[0]);
if (blah == 1)
{
old_stdout = dup(1);
FILE* fp1 = freopen(file, "w+", stdout);
}
if (execvp(args[0], args) < 0)
{
fflush(stdout);printf("error:in exec %d", pid);
kill(pid, SIGUSR1);
close(pfds[0]);
close(pfds[1]);
}
fflush(stdout);
printf("HERE");
if (blah == 1)
{
fclose(stdout);
FILE* fp2 = fdopen(old_stdout, "w");
*stdout = *fp2;
}
}
else
{
close(pfds[0]);
close(pfds[1]);
while (wait(NULL) != pid);
while (wait(NULL) != pid2);
}
}
int main()
{
char* argv[64];
char* args[64];
char* left;
size_t size = 0;
char* file;while (1)
{
int flag = 0;
char* word = NULL;
printf("SHELL");
char buf[80];
getcwd(buf, sizeof(buf));
printf("%s", buf);
printf("$");
int len = getline(&word, &size, stdin);
if (*word == '\n')
continue;
word[len - 1] = '\0';
char* file = NULL;
int i = 0;
char* temp = (char*)malloc(150);
strcpy(temp, word);
analyze_user_input(temp, argv);
if (strcmp(word, "exit") == 0)
{
exit(0);
}
if (strcmp(argv[0], "cd") == 0)
{
int ch = chdir(argv[1]);
if (ch < 0)
{
printf("No such file or directory \n");
}
continue;
}
for (i = 0; word[i] != '\0'; i++)
{
if (word[i] == '>')
{
char* p = strtok_r(word, ">", &file);
file = get_first_word(file);
flag = 1;
break;
}
else if (word[i] == '<')
{
char* p = strtok_r(word, "<", &file);
file = get_first_word(file);
flag = 2;
break;
}
else if (word[i] == '|')
{
char* p = strtok_r(word, "|", &left);
flag = 3;
break;
}
}
if (flag == 1)
{
analyze_user_input(word, argv);
execute_output(argv, file);
}
else if (flag == 2)
{
analyze_user_input(word, argv);
execute_input(argv, file);
}
else if (flag == 3){
char* output, * file;
analyze_user_input(word, argv);
execute_pipe(argv, left);
}
else
{
analyze_user_input(word, argv);
execute(argv);
}
}
}
- 程序运行结果及分析
- 运行Shell_test:
2.测试输入重定向,即 command < file
再打开test1.txt,可以看到ls的内容被重定向到test1.txt中:
测试 ps aux > test_1.txt
再打开test_1.txt,可以看到ps aux的内容被重定向到test_1.txt中:
3.测试输出重定向,即 command > file:
显示了test1.txt的字符数是13
测试 date < test.txt
显示出了test1.txt的date信息:Sun Aug 14 06:07:11 PDT 2022
- 测试输入输出重定向,即 command < file1 > file2:
打开test_1.txt查看,可以看到test1.txt的date信息被写入test_1.txt
中:
5. 测试管道功能
打开test1.txt查看内容,可以看到输出内容被重定向到test1.txt中:
- 实验体会
在计算机科学中,Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(command interpreter,命令解析器)。它类似于DOS下的COMMAND.COM和后来的cmd.exe。它接收用户命令,然后调用相应的应用程序。Shell 的实现相对来说还是比较难的,许多函数之前根本就没有接触过:freopen 函数、execvp 函数、pipe 函数等等。找了很多书籍,也在网上查找了许多的资料。很多书籍和博客都讲得很是朦胧,只涉及到一些概念和原理。通过本次 Shell 实验的学习和实践,我对OS有了更加深刻的理解。现在再回看上学期的操作系统PPT,感觉也有了更清晰的认识,“纸上得来终觉浅,绝知此事要躬行”,古人诚不我欺!