Linux下实现简易Shell

news/2025/3/12 23:28:52/文章来源:https://www.cnblogs.com/mingyuer/p/18236418

转载:https://blog.csdn.net/dxyt2002/article/details/129800496

在这里插入图片描述


文章目录

  • 简易Shell实现
    • 简易shell功能
    • 实现shell
      • 1. 循环接收用户命令
      • 2. 创建子进程执行命令
        • 分割字符串
      • 3. 回收子进程
      • 4. 优化不足
      • 5. 自建命令添加
    • 简易shell代码

简易Shell实现

我们在Linux中使用的shell, 一般有两个 bash 和 zsh.

我们可以通过shell, 执行各种命令. 而本篇文章的主要内容, 就是实现一个简易的shell

简易shell功能

在实现shell之前, 肯定要明白简易的shell需要实现什么功能

  1. 首先要知道, shell应该是一个死循环的程序.

    为什么?因为shell是可以循环从命令行接收用户输入的内容

  2. 其次, shell 需要一个设置一个提示符. 类似这样的东西:image-20230311192332112

  3. 第三, 我们使用shell是需要执行命令的, 且这些命令需要在环境变量PATH下

    这些命令大多都是需要由我们的shell创建子进程来执行的

  4. 第四, shell需要可以 等待回收 创建的子进程

  5. 第五, 需要实现一些内建命令:比如 export

    这些命令 是不需要创建子进程来执行的

上面就是一个shell的最基本的功能

实现shell

1. 循环接收用户命令

我们实现的简易的shell的本质, 是一个死循环

且在接收用户输入的指令之前, 需要先输出一个用户提示符:

image-20230311195500369

执行:GIF 2023-3-11 19-56-28

用户提示符是打印出来了, 但是 是无限循环地打印.

解决这个无限循环的打印, 只需要在printf之后设置一个接收输入内容地函数即可, 这里我们使用 fgets():

image-20230311201651077
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#define SIZE 128
int main() {while(1) {char command_V[SIZE];memset(command_V, '\0', SIZE);// 首先是用户提示符: printf("[七月July@MyBlog 当前目录]# ");fgets(command_V, SIZE, stdin);printf("%s", argV);					// 测试进程是否接收了输入内容}return 0;
}

执行上述代码的结果是:

myShell_fgets

可以实现命令行输入, 并且接收输入内容.

2. 创建子进程执行命令

shell 中的大多数命令都是通过创建子进来执行的.

可以通过fork()创建子进程, 然后进程替换实现命令的执行

实现fork()子进程替换为命令子进程, 最佳的进程替换的接口是:exevp()

  1. 首先是因为, 我们接收了命令行输入的程序及选项字符串, 将字符串根据空格分割开 就是一个命令和选项的数组
  2. p字的接口, 会默认从环境变量PATH的路径下搜索, 不需要在添加程序的路径

那么, 首先就是对接收的命令字符串的分割:

C语言中, 关于字符串的函数中, 有一个strtok()函数是用来分割字符串的, 使用strtok()可以将指定字符串按照传入的分割符分开

strtok:

image-20230311215224476

第一个参数str, 传入需要分割的字符串

第二个参数delimiters, 传入分割符

此函数的返回规则为, 如果分割出了字符串, 则返回此字符串的指针; 否则 返回空指针

且, 第一次调用此函数之后, 若原字符串中还存在可以分割的字符串, 可以直接在strtok()的第一个参数传入空指针以再次调用此函数从上次分割出的字符串之后继续分割

但是在分割字符串之前, 需要将接收到的字符串的最后一个有效字符设置为'\0', 因为接收到最后一个字符是'\n'

command_V[strlen(command_V)-1] = '\0', 既可以修改'\n''\0'

分割字符串

strtok()的使用方法, 如下列代码示意:

// 分割命令行
command_argV[0] = strtok(command_S, " ");
int index = 1;
while(command_argV[index++] = strtok(NULL, " "));
// strtok分割不到字符串时, 会返回空指针, 刚好可以作为数组的最后一个元素 及 循环结束的条件

command_argV是一个指针数组, 每个元素存储一个字符串. 此指针数组的作用是, 进程替换时传入execvp()接口

所以应该按照要求存储数组, 即 第0元素存储命令名, 之后每个元素存储一个选项

而我们从命令行接收到正确的命令的字符串的格式应该是:命令名 选项1 选项2 选项3 ...

所以 strtok() 传入的分割符应该是 " ", 第一次执行 strtok(command_S, " ")分割出命令名, 会返回命令名字符串

此后, 可以再次使用strtok(NULL, " ") strtok会自动从命令名之后再次分割

command_argV[0] 设置为 命令名之后, 从 command_arg[1] 开始 将每一个选项存入其中

分割存储之后的 command_argV 内容可以展示一下:

image-20230311225342322 image-20230311225845184

将接收到的字符串分割存储到字符指针数组中之后, 就可以创建子进程并进程替换了

创建子进程就非常简单了:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#define SIZE 128
char *command_argV[SIZE];
int main() {while(1) {char command_S[SIZE];memset(command_S, '\0', SIZE);// 首先是用户提示符: printf("[七月July@MyBlog 当前目录]# ");fgets(command_S, SIZE, stdin);command_S[strlen(command_S) - 1] = '\0';        // 修改'\n' 为 '\0'// 分割命令行command_argV[0] = strtok(command_S, " ");int index = 1;while(command_argV[index++] = strtok(NULL, " "));// strtok分割不到字符串时, 会返回空指针, 刚好可以作为循环结束的条件// 创建子进程, 并进程替换pid_t id = fork();if(id == 0) {//进程替换execvp(command_argV[0], command_argV);exit(-1);       // 替换失败则 退出码-1}}return 0;
}

此时的代码, 就可以完成一些命令操作了:

image-20230311231019639

但是 执行结果好像有些奇怪

3. 回收子进程

在介绍过进程等待之后, 回收子进程的操作就显得格外简单了

博主的进程等待相关文章:
【Linux】[万字] 详析进程控制:fork子进程运行规则?怎么回收子进程?

我们使用waitpid()来等待子进程. fork()获取的子进程pid, 刚好可以指定回收子进程

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#define SIZE 128
char *command_argV[SIZE];
int main() {while(1) {char command_S[SIZE];memset(command_S, '\0', SIZE);// 首先是用户提示符: printf("[七月July@MyBlog 当前目录]# ");fgets(command_S, SIZE, stdin);command_S[strlen(command_S) - 1] = '\0';        // 修改'\n' 为 '\0'// 分割命令行command_argV[0] = strtok(command_S, " ");int index = 1;while(command_argV[index++] = strtok(NULL, " "));// strtok分割不到字符串时, 会返回空指针, 刚好可以作为循环结束的条件// 创建子进程, 并进程替换pid_t id = fork();if(id == 0) {//进程替换execvp(command_argV[0], command_argV);exit(-1);       // 替换失败则 退出码-1}// 父进程回收子进程int status = 0;pid_t ret = waitpid(id, &status, 0);if(ret > 0) {printf("父进程成功回收子进程, exit_code: %d, exit_sig: %d\n", WEXITSTATUS(status), WTERMSIG(status));}}return 0;
}

此时, 再执行代码:

image-20230311231631386

可以看到, 命令就可以正常执行了.

4. 优化不足

我们的myShell已经可以正常执行大部分的命令了, 但是还存在一些不足:

image-20230311231958249

导致这些不足的原因是什么?怎么优化这些不足呢?

当我们使用 bash, 查看这些命令时:

image-20230311232612930

可以发现, 这两个命令真正执行的并不是简单的原命令, 那么我们也可以在myShell中做出优化

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#define SIZE 128
char *command_argV[SIZE];
int main() {while(1) {char command_S[SIZE];memset(command_S, '\0', SIZE);// 首先是用户提示符: printf("[七月July@MyBlog 当前目录]# ");fgets(command_S, SIZE, stdin);command_S[strlen(command_S) - 1] = '\0';        // 修改'\n' 为 '\0'// 分割命令行command_argV[0] = strtok(command_S, " ");int index = 1;// ls 色彩优化if(strcmp(command_argV[0], "ls") == 0) {command_argV[index++] = "--color=auto";         // 若执行ls命令, 则在ls命令后携带一个--color=auto选项} // ll 命令优化if(strcmp(command_argV[0], "ll") == 0) {command_argV[0] = "ls";command_argV[index++] = "-l";command_argV[index++] = "--color=auto";}while(command_argV[index++] = strtok(NULL, " "));// strtok分割不到字符串时, 会返回空指针, 刚好可以作为循环结束的条件// 创建子进程, 并进程替换pid_t id = fork();if(id == 0) {//进程替换execvp(command_argV[0], command_argV);exit(-1);       // 替换失败则 退出码-1}// 父进程回收子进程int status = 0;pid_t ret = waitpid(id, &status, 0);if(ret > 0) {printf("父进程成功回收子进程, exit_code: %d, exit_sig: %d\n", WEXITSTATUS(status), WTERMSIG(status));}}return 0;
}

此时, ll 和 ls 就可以更加完善的执行:

image-20230311233716429

5. 自建命令添加

shell最基本的功能已经实现了

我们可以通过自己的实现的简易的EasyShell, 来执行大多数的命令

但是 有一些命令是无法执行的:

image-20230313154548153

image-20230313154906491

为什么 cd 和 export 明明都可以执行, 但是却没有作用呢?

因为, cd 和 export 命令实际上都是shell的内建命令, PATH环境变量路径下存在的程序其实也没有实际功能的:

image-20230313162046302

可以看到, 执行 /usr/bin 路径下的cd程序, 也是没有作用的

其实并不是没有作用, 而是cd进程当前运行的路径没有改变

在介绍Linux进程时提到过, 进程存在一个当前路径, 表示进程当前运行的路径, 在/proc目录下的进程目录下可以看到

举个例子:

当我在/home/July 路径下执行 /home/July/procTest/a.out 程序时, 创建出来的进程运行的当前路径是什么呢?

image-20230315180607190

可以看到, 在 /home/July 路径下执行 /home/July/procTest/a.out 程序时, 创建出的进程的当前运行的路径其实时 /home/July

同样的道理, 我们执行cd总是在用户当前所处的路径下, 那么cd执行之后的当前路径也就是用户执行cd时所在的路径, 并不会发生改变

所以 要实现cd的功能, 就需要内建命令实现修改进程当前运行的路径

这些命令并不是shell通过创建子进程的方式执行的, 而是shell自己在内部执行的, 此类的命令被称为内建命令:

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#define SIZE 128
int putEnvInmyShell(char *put_Env) {putenv(put_Env);return 0;
}
int changeDir(const char* new_path) {chdir(new_path);					// 系统调用return 0;
}
char *command_argV[SIZE];
char copy_env[SIZE];
int main() {while(1) {char command_S[SIZE];memset(command_S, '\0', SIZE);// 首先是用户提示符: printf("[七月July@MyBlog 当前目录]# ");fgets(command_S, SIZE, stdin);command_S[strlen(command_S) - 1] = '\0';        // 修改'\n' 为 '\0'// 分割命令行command_argV[0] = strtok(command_S, " ");int index = 1;if(strcmp(command_argV[0], "ls") == 0) {command_argV[index++] = "--color=auto";         // 若执行ls命令, 则在ls命令后携带一个--color=auto选项} if(strcmp(command_argV[0], "ll") == 0) {command_argV[0] = "ls";command_argV[index++] = "-l";command_argV[index++] = "--color=auto";}while(command_argV[index++] = strtok(NULL, " "));// strtok分割不到字符串时, 会返回空指针, 刚好可以作为循环结束的条件// 内建命令if(strcmp(command_argV[0], "cd") == 0 && command_argV[1] != NULL) {// 使用cd命令时, command_argV[1]位置应该是需要进入的路径changeDir(command_argV[1]);continue;								// 非子进程命令, 不用执行下面的代码, 所以直接进入下个循环}if(strcmp(command_argV[0], "export") == 0 && command_argV[1] != NULL) {// 我们接收的命令, 都在command_S 字符串中, 此字符串每次循环都会被清除// 所以不能直接将 command_argV[1] putenv到环境变量中, 因为指向的同一块地址// 所以 需要先拷贝一份strcpy(copy_env, command_argV[1]);putEnvInmyShell(copy_env);continue;}// 创建子进程, 并进程替换pid_t id = fork();if(id == 0) {//进程替换execvp(command_argV[0], command_argV);exit(-1);       // 替换失败则 退出码-1}// 父进程回收子进程int status = 0;pid_t ret = waitpid(id, &status, 0);if(ret < 0) {exit(-1);}}return 0;
}
image-20230313164408816

image-20230313165631214

简易shell代码

完成了两个内建命令的代码之后, 简易shell可以说是基本实现了

实现这个简易的shell, 只是为了加深对进程、环境变量、进程等待、进程替换等进程相关知识的理解

比起真正的一个完善的shell, 差的还有十万八千里.

我们一般使用的bash, 除了执行命令的功能, 至少还有:backspace删除历史命令Tab补全等非常方便的功能, 这些功能都没有在本篇文章中实现, 有兴趣的话可以查找资料实现一下

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#define SIZE 128
int putEnvInmyShell(char *put_Env) {putenv(put_Env);return 0;
}
int changeDir(const char* new_path) {chdir(new_path);					// 系统调用return 0;
}
char *command_argV[SIZE];
char copy_env[SIZE];
int main() {while(1) {char command_S[SIZE];memset(command_S, '\0', SIZE);// 首先是用户提示符: printf("[七月July@MyBlog 当前目录]# ");fgets(command_S, SIZE, stdin);command_S[strlen(command_S) - 1] = '\0';        // 修改'\n' 为 '\0'// 分割命令行command_argV[0] = strtok(command_S, " ");int index = 1;if(strcmp(command_argV[0], "ls") == 0) {command_argV[index++] = "--color=auto";         // 若执行ls命令, 则在ls命令后携带一个--color=auto选项} if(strcmp(command_argV[0], "ll") == 0) {command_argV[0] = "ls";command_argV[index++] = "-l";command_argV[index++] = "--color=auto";}while(command_argV[index++] = strtok(NULL, " "));// strtok分割不到字符串时, 会返回空指针, 刚好可以作为循环结束的条件// 内建命令if(strcmp(command_argV[0], "cd") == 0 && command_argV[1] != NULL) {// 使用cd命令时, command_argV[1]位置应该是需要进入的路径changeDir(command_argV[1]);continue;								// 非子进程命令, 不用执行下面的代码, 所以直接进入下个循环}if(strcmp(command_argV[0], "export") == 0 && command_argV[1] != NULL) {// 我们接收的命令, 都在command_S 字符串中, 此字符串每次循环都会被清除// 所以不能直接将 command_argV[1] putenv到环境变量中, 因为指向的同一块地址// 所以 需要先拷贝一份strcpy(copy_env, command_argV[1]);putEnvInmyShell(copy_env);continue;}// 创建子进程, 并进程替换pid_t id = fork();if(id == 0) {//进程替换execvp(command_argV[0], command_argV);exit(-1);       // 替换失败则 退出码-1}// 父进程回收子进程int status = 0;pid_t ret = waitpid(id, &status, 0);if(ret < 0) {exit(-1);}}return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/721223.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

详析进程控制进程替换

转载:https://blog.csdn.net/dxyt2002/article/details/129745548文章目录 再识fork()为什么子进程只运行fork()之后代码写时拷贝为什么要用写时拷贝? fork()也可能创建子进程失败 再识进程终止正确认识进程终止查看进程的退出码 exit() 和 _exit() 退出进程 进程等待等待方法…

Android studio增删改查尚未全部完成时如何查看数据库

Android studio相当体贴的内嵌了,点击App Inspection(一般在Android studio左下角附近) 然后不需要操作了,稍等一下数据库表格就会刷新出来

[GAMES101]图形学入门笔记

线性代数基础知识 此处只补充部份线代内容。 向量点乘公式:\(\vec{a} \cdot \vec{b} = ||\vec{a}|| ||\vec{b}|| \cos \theta\)​​ 同时也可以获得两个向量的余弦角:\(\cos\theta = \dfrac{\vec{a}\cdot \vec{b}}{||\vec{a}|| ||\vec{b}||}\)​ 如果是单位向量的话,模长为1…

一文读懂web组态

随着工业4.0的到来,物联网、大数据、人工智能等技术的融合应用,使得工业领域正在经历一场深刻的变革。在这个过程中,web组态技术以其独特的优势,正在逐渐受到越来越多企业的关注和认可。那么,什么是web组态?web组态软件哪个好用?本文将围绕这两个问题展开探讨。 随着工…

什么是web组态

WEB组态是充分利用了HTML5、CSS3、JavaScript等现代Web技术,为用户提供了丰富、交互式的体验。一、web组态的定义和背景 在深入探讨之前,我们先回顾一下“组态”的定义。在工业自动化领域,组态软件是用于创建监控和数据采集(SCADA)系统的工具,它允许工程师构建图形界…

电脑PotPlayer 如何像手机 B站一样自由地放大缩小视频画面

摘自:https://zhuanlan.zhihu.com/p/666067891 一、放大缩小视频画面 点击数字键盘区域的「+」号和「-」号可以放大缩小视频画面,效果如下图:二、拖动画面 在PotPlayer中,按F5键,进入设置界面。 选中「基本」 -->「鼠标」 --> 勾选「使用Alt,Ctrl,Shift键配合鼠标选…

猿人学内部练习平台第54~60题

第54题 无限debugger练习/入门js 本题打开控制台就会自动无限 debugger,解决无限 debugger 的最简单方式就是使用 Firefox 121 版本以上的版本,Firefox 121 以上的版本会对代码内部的 debugger 自动过滤,只有手动打的断点才会生效。 本题是无限 debugger 练习,尝试手动解决…

总结与思考 :OOP课程PTA作业4 - 6

一. 前言 本次Blog旨在总结面向对象的程序设计的作业,前一题为答题判题程序,后两题为家居强电电路模拟程序。具体的设计过程和心得如下。 二. 设计与分析 (一) 答题判题程序相对上一题迭代的内容点击查看题目 本次作业新增内容:1、输入选择题题目信息题目信息为独行输入…

深度学习--爱因斯坦求和einsum--85

目录1. 爱因斯坦求和的来源2. 求和表达式的规范3. 规则1:外部重复做乘积4. 规则2 内部重复把数取5. 规则3 从有到无要求和6. 规则4 重复默认要丢弃7. 把这个实现一下 加深理解 1. 爱因斯坦求和的来源 https://zhuanlan.zhihu.com/p/672346603 爱因斯坦在研究相对论时,曾经对冗…

python 使用 pyinstaller打包可执行文件

首先要pip install pyinstaller 命令 安装库,下面这图显示我已经安装成功了。主要是看在哪里输入命令。 然后打开设置,点击加号点完加号之后,就是这样的。name 自己设置,主要是Tool Settings 里面的三个选项。 第一个是刚才下载好的pyinstaller.exe文件地址,点击浏览文件…

WebLogic T3反序列化漏洞

在Weblogic中RMI通信的实现是使用T3协议,并且在T3的传输过程中同样会进行序列化和反序列化的操作。目录前言T3协议概述T3反序列漏洞分析漏洞复现修复措施 前言 WebLogic的反序列化漏洞是一个经典的漏洞系列,原因就在于WebLogic在通信过程中使用T3协议传输数据,涉及到了序列化…

自定义监控项

采集TCP连接状态(实战项目) 精确分析tcp连接状态,可以精准得知服务器的链接情况,确保web服务器的健康1. 命令获取tcp的状态[root@web-7 ~]# # -a 显示所有socket、-t显示tcp协议连接 -n 只显示ip [root@web-7 ~]#netstat -ant Active Internet connections (servers and e…