当前位置: 首页 > news >正文

操作系统实验二: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);

}

}

}

  • 程序运行结果及分析
  1. 运行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

  1. 测试输入输出重定向,即 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,感觉也有了更清晰的认识,“纸上得来终觉浅,绝知此事要躬行”,古人诚不我欺!


http://www.mrgr.cn/news/51862.html

相关文章:

  • 制造企业数字化转型顶层规划案例(55页满分PPT)
  • 92、Python之异常:异常的概念及异常处理机制
  • MyBatis的占位符(day36)
  • 中科星图GVE案例——利用最短距离方法实现土地分类(合肥)
  • 【JavaEE】——三次握手()详细、易理解
  • Spring 声明式事务
  • 基于 MyBatis Plus 分页封装分页方法
  • 第九课:Python学习之函数基础
  • 2024年的5款AI写作工具,你用过几个?
  • 【含文档】基于Springboot+Vue的仓库管理系统设计与实现(含源码+数据库+lw)
  • 高级IO——五种IO模型
  • 5分钟精通Windows环境变量
  • Cesium的一些计算方法浅析(1)
  • 数据库->库的操作
  • The 48 bit pointer
  • 参加CSP-J/S 认证,要学多久C++才能达到获奖水平?
  • AI学习指南深度学习篇- 预训练模型的原理
  • 常用的网络配置命令
  • 揭秘提升3DMAX效率的6款必备神级插件!
  • 刷爆Leetcode Day2