Linux实践 - 命令行解释器 简易版

在这里插入图片描述

~~~~

  • 前言
  • 解决的问题
    • 为什么shell要以子进程的方式执行我们的命令?
    • 为什么直接使用程序名ls,而不是路径/usr/bin/ls?
  • 头文件包含
  • 命令行提示符
  • 接受用户命令行输入
  • 解析用户的输入
  • 内建命令&&特殊处理
    • ls 时目录等文件不带高亮颜色
    • cd时目录不变的问题
    • echo
      • echo命令能显示本地变量而env命令获取不到的原因
      • echo $?显示上一次进程的退出码
  • 创建子进程
  • 子进程执行进程程序替换
  • 父进程等待
  • myshell.c 源码
  • 结语

前言

本文将根据进程创建fork()、进程替换exec系列函数、进程等待waitpid()实现一个简单的命令行解释器。


解决的问题

为什么shell要以子进程的方式执行我们的命令?

shell也是一个进程,shell会提取用户在命令行输入的内容以空格字符作为分隔符切割成一个个的子串,然后执行exec程序替换函数。如果没有子进程。shell进程本身会被替换,shell也就结束运行了,但是我们需要shell一直运行,持续解析命令行的,所以shell通过fork创建子进程,让子进程执行程序替换,父进程shell然后等待子进程退出,之后shell将再次等待命令行的输入。

为什么直接使用程序名ls,而不是路径/usr/bin/ls?

shell以fork子进程的方式,通过exec替换子进程执行其他程序。子进程继承了shell的环境变量,使用exec函数时不需要制定替换程序的路径,使用程序名即可,操作系统会在PATH包含的路径下自动寻找。
echo的问题 : 内建命令

头文件包含

#include<stdio.h>
#include<stdlib.h>// exec系列替换函数
#include<string.h>// 字符串函数
#include<assert.h>// 断言判断
#include<unistd.h>// fork创建子进程
#include<sys/types.h>// 进程等待
#include<sys/wait.h>// 进程等待

命令行提示符

首先我们登录shell时左侧会提示我们进行输入的提示符,包含了当前登录的用户名、主机名和当前所在目录。
外
我们仿照xshell的写法即可:

printf("[用户名@主机名 路径]# ");

外

接受用户命令行输入

定义接收用户输入的长度为NUM的字符数组commandLine;

#define NUM 1024    
char commandLine[NUM];

我们需要接受用户的一行输入,这里使用fgets函数。
fgets函数声明

char *fgets(char *s, int size, FILE *stream);

从标准输入stdin中读取最多数组长度-1个字符到commandLine数组中。
空出来的一个位置是为了放’\0’,防止出可能的越界问题。

char* s = fgets(commandLine, sizeof(commandLine) - 1, stdin);
assert(s);
(void)s;

使用字符指针s接收fgets的返回值,需要判断一下是否读取成功;
![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png
去除commandLine多读取到的换行符\n

commandLine[strlen(commandLine) - 1] = 0;

外

解析用户的输入

读取的用户输入都在字符数组commandLine中,且以空格分隔,所以需要先把commandLine按空格分隔成多个子串。
为了保存分隔的子串,定义一个字符指针数组argv_按顺序依次指向分割的子串,且以NULL空指针结尾。
假定最多分隔的子串不超过63个;

#define OPT_NUM 64
char* argv_[OPT_NUM];

分割字符串的方法很多,这里采用库函数strtok进行commandLine的分割;
strtok函数声明

char *strtok(char *str, const char *delim);

使用strtok时,第一次分割需要指明要分割的是哪个字符串,后续我们还需要继续切割,所以第一个参数填NULL,循环切割,直到strtok函数返回NULL结束。
正巧的是,strtok返回NULL时正好也是argv_所需要的结束,所以while循环简写了。

argv_[0] = strtok(commandLine, " ");
int i = 1;
while(argv_[i++] = strtok(NULL, "  "));

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

内建命令&&特殊处理

解析完commandLine长串为多个子串之后,可以知道argv_[0]是用户期望执行的程序名,而之后的所有子串都是执行该程序的选项。

ls 时目录等文件不带高亮颜色

我们使用ls命令时,一些文件没有高亮,对此,除了我们每次显式的输入"--color=auto"之外,直接在父进程内部进行特殊处理即可:
外链图片
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

if(argv_[0] && strcmp("ls", argv_[0]) == 0){// strcmp传入的参数确保是有效的,否则结果未定义argv_[i - 1] = (char*)"--color=auto";argv_[i] = 0;
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

cd时目录不变的问题

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
为什么我们的shell,cd的时候,路径没有变化呢?

shell以子进程的方式执行cd命令,子进程有自己的工作目录,cd更改的是子进程的目录,而子进程执行完毕就退出了,继续运行的是父进程shell,而父进程的工作目录从始至终都没有更改
所以解决方法是cd命令时特殊判断,父进程直接执行cd命令,本次循环的后续代码不再执行(称之为自建命令)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
解决方法是:特殊判断cd,直接在父进程中执行实现cd命令的效果–更改进程的当前工作目录。
我们使用chdir()函数实现:

#include <unistd.h>
int chdir(const char *path);
if(argv_[0] && strcmp("cd", argv_[0]) == 0){    if(argv_[1]){    chdir(argv_[1]);    }     continue;    
}

改变完父进程myshell的工作目录之后,已经完成了cd的功能,后续代码无须执行,直接continue开始下一次循环,继续等待用户下一次命令行输入。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

echo

echo命令能显示本地变量而env命令获取不到的原因

echo其实是bash的内建命令,不是fork创建子进程去执行的,而是bash亲自执行的,本地变量就在bash内,当然bash能够获取;
而env不是内建命令,是bash通过fork创建子进程然后进程替换(exec)为env进程,然后env进程再查找环境变量的。env是bash的子进程,继承了bash的环境变量,但是bash的本地变量(没有导入到bash环境变量中)没有被env继承,所以env当然就找不到bash的本地变量了。

echo $?显示上一次进程的退出码

既然是内建命令,那么就需要myshell父进程邵本身进行特殊判断和处理:

if(argv_[0] && strcmp("echo", argv_[0]) == 0){    if(argv_[1] && strcmp("$?", argv_[1]) == 0){    printf("sig: %d, exit code: %d\n", lastSig, lastExitCode);                  lastSig = 0;lastExitCode = 0;continue;            }                
} 

创建子进程

我们使用fork函数为myshell程序创建子进程,让子进程执行程序替换exec从而执行用户期望的程序。

pid_t id = fork();    
assert(id != -1);    

如果子进程创建失败,fork返回-1,后续程序不再执行。

子进程执行进程程序替换

fork函数创建子进程之后函数返回之前,就有了两个执行流:父进程myshell和子进程。
通过父子进程fork返回值的不同,让父子进程执行后续代码的不同部分。
对于子进程,fork函数返回0。
子进程需要进行程序替换,进程替换函数(或者说加载函数)exec有多个,我们选择哪一个呢?

我期望用户直接输入程序名执行而不是路径名,所以需要带p(path),系统自动在PATH中帮我找程序位置; 我期望传递字符指针数组,而不是可变参数列表,所以需要v(vector);
我期望子进程继承默认环境变量就行,即我不想显式传递环境变量,所以没有e(environ);

所以我选择的是execvp函数

int execvp(const char *file, char *const argv[]);
if(id == 0){    execvp(argv_[0], argv_);    exit(1);// 到这一步,程序替换失败,进程退出,且退出码设置为-1
}

父进程等待

父进程阻塞式等待子进程,知道子进程退出。

pid_t waitpid(pid_t pid, int *status, int options);

使用watpid函数,第一个参数表示等待的子进程id,第二个参数是输出型参数(为NULL时不接受),接收子进程退出状态,第三个参数为0表示父进程阻塞式等待子进程。
我们先不接首子进程状态,第二个参数设置为NULL

int ret = waitpid(id, NULL, 0);    
assert(ret != -1);    
(void)ret;    

waitpid函数返回如果是-1表示等待失败,需要判断一下,等待失败就不再继续执行。

现在我们想要实现xshell中echo $?显示上一次进程运行退出码,怎么实现呢?
其实很简单,定义全局变量lastSig记录子进程退出信号和lastExitCode记录子进程退出码。

int lastSig = 0;
int lastExitCode = 0;

每次父进程等待成功都根据status设置一次lastSiglastExitCode即可。

int status = 0;
int ret = waitpid(id, &status, 0);    
assert(ret != -1);    
(void)ret; 
lastSig = status & 0x7f;// 0~6位表示信号   
lastExitCode = (status >> 8) & 0xff;// 低8~15位表示退出码

myshell.c 源码

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>#define NUM 1024
#define OPT_NUM 64char commandLine[NUM];// 获取用户输入
char* argv_[OPT_NUM];// 存放按空格切割的字符串的多个子串int status = 0;
int lastSig = 0;
int lastExitCode = 0;int main(){while(1){// 输出命令行提示符printf("[用户名@主机名 路径]# ");                                 // 用户输入char* s = fgets(commandLine, sizeof(commandLine) - 1, stdin);assert(s);(void)s;commandLine[strlen(commandLine) - 1] = 0;// 处理用户输入的\n
#ifdef DEBUGprintf("test: %s\n", commandLine);
#endif                                                                    // strtok切割字符串argv_[0] = strtok(commandLine, " ");int i = 1;while(argv_[i++] = strtok(NULL, "  "));
#ifdef DEBUGfor(int i = 0; argv_[i]; i++)printf("argv_[%d]:%s\n", i, argv_[i]);
#endif// 命令行带颜色if(argv_[0] && strcmp("ls", argv_[0]) == 0){argv_[i - 1] = (char*)"--color=auto";argv_[i] = 0;}// cd命令父进程直接执行,改变的是父进程shell的当前工作目录,而不是  更改子进程的工作目录。如果子进程执行cd命令,更改完自己的工作目录就退出了,  父进程工作目录并没有改变。if(argv_[0] && strcmp("cd", argv_[0]) == 0){if(argv_[1]){chdir(argv_[1]);} continue;// echo $? 查看最近一次进程运行结果信息if(argv_[0] && strcmp("echo", argv_[0]) == 0){if(argv_[1] && strcmp("$?", argv_[1]) == 0){printf("sig: %d, exit code: %d\n", lastSig, lastExitCode);lastSig = 0;lastExitCode = 0;continue;}}// fork子进程执行新程序pid_t id = fork();assert(id != -1);if(id == 0){execvp(argv_[0], argv_);exit(1);}// 父进程waitpid子进程int ret = waitpid(id, &status, 0);assert(ret != -1);(void)ret;lastSig = status & 0x7f;lastExitCode = (status >> 8) & 0xff;}return 0;
}

结语


T h e E n d TheEnd TheEnd

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

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

相关文章

LabVIEW NV色心频率扫描

LabVIEW NV色心频率扫描 通过LabVIEW软件开发一个能够实现对金刚石氮空位&#xff08;Nitrogen-Vacancy&#xff0c;NV&#xff09;色心的频率扫描系统。系统通过USB协议与硬件设备通信&#xff0c;对NV色心进行高精度的频率扫描&#xff0c;满足了频率在2.6 GHz到3.2 GHz范围…

nginx搭建及部署

目录 一、nginx是什么&#xff1f; 二、安装部署 1.下载 2.配置 3.代理Swagger服务 4.nginx命令 一、nginx是什么&#xff1f; 是用于 Web 服务、反向代理、内容缓存、负载均衡、媒体流传输等场景的开源软件。它最初是一款专为实现最高性能和稳定性而设计的 Web 服务器。…

Laravel框架项目首页内容修改

#Laravel# 安装Laravel框架成功后运行项目&#xff0c;看到下面这个图就说明安装框架成功了 需要根据自己的需求修改页面时&#xff0c;先找到首页的文件 首页对应的页面文件为项目根目录下的resources/views/welcome.blade.php文件 <!DOCTYPE html> <html lang&quo…

C++特性三:多态---案例三(电脑组装)

案例描述&#xff1a; 电脑主要组成部件为 CPU&#xff08;用于计算&#xff09;&#xff0c;显卡&#xff08;用于显示&#xff09;&#xff0c;内存条&#xff08;用于存储&#xff09; 将每个零件封装出抽象基类&#xff0c;并且提供不同的厂商生产不同的零件&#xff0c;例…

Visual Studio 2013 - 调试模式下查看监视窗口

Visual Studio 2013 - 调试模式下查看监视窗口 1. 监视窗口References 1. 监视窗口 Ctrl Alt W&#xff0c;1-4&#xff1a;监视窗口 (数字键不能使用小键盘) or 调试 -> 窗口 -> 监视 -> 监视 1-4 调试状态下使用&#xff1a; 在窗口中点击空白行&#xff0c;…

目标检测——PP-PicoDet算法解读

PP-YOLO系列&#xff0c;均是基于百度自研PaddlePaddle深度学习框架发布的算法&#xff0c;2020年基于YOLOv3改进发布PP-YOLO&#xff0c;2021年发布PP-YOLOv2和移动端检测算法PP-PicoDet&#xff0c;2022年发布PP-YOLOE和PP-YOLOE-R。由于均是一个系列&#xff0c;所以放一起解…

Java八股文(RabbitMQ)

Java八股文のRabbitMQ RabbitMQ RabbitMQ RabbitMQ 是什么&#xff1f;它解决了哪些问题&#xff1f; RabbitMQ 是一个开源的消息代理中间件&#xff0c;用于在应用程序之间进行可靠的异步消息传递。 它解决了应用程序间解耦、消息传递、负载均衡、故障恢复等问题。 RabbitMQ …

长安链智能合约标准协议第二草案——BNS与DID协议邀请社区用户评审

长安链智能合约标准协议 在智能合约编写过程中&#xff0c;不同的产品及开发人员对业务理解和编程习惯不同&#xff0c;即使同一业务所编写的合约在具体实现上也可能有很大差异&#xff0c;在运维或业务对接中面临较大的学习和理解成本&#xff0c;现有公链合约协议规范又不能完…

软件测试 -- Selenium常用API(java)

写在前面 // 如果文章有问题的地方, 欢迎评论区或者私信指正 目录 什么是Selenium 一个简单的用例 元素定位 id定位 xpath定位 name定位 tag name 定位和class name 定位 操作元素 click send_keys submit text getAttribute 1. 获取元素的 class 属性 2. 获取元素…

Word为图表设置图注并在图表清单中自动生成

1如果需要自动插入题注&#xff0c;请不要自己为文件增加新的标题样式或删除自带的标题1样式 2章节大标题最好是标题1&#xff0c;2,3而不要设置标题一、二、三&#xff0c;否则图例在自动生成时会显示 图一 -1&#xff0c;调整起来会非常不方便 若实在要使用大写中文标题&…

系统资源耗尽对服务器的影响有什么?

在当今数字化时代&#xff0c;服务器作为核心计算设备&#xff0c;为企业和组织的业务连续性提供了重要保障。然而&#xff0c;随着业务的增长和复杂性的提升&#xff0c;服务器也面临着越来越多的挑战。其中&#xff0c;系统资源耗尽是服务器面临的一个重要问题。今天德迅云安…

1、初识JVM

一、JVM是什么&#xff1f; JVM的英文全称是 Java Virtual Machine&#xff0c;其中文译名为Java虚拟机。它在本质上就是是一个运行在计算机上的程序&#xff0c;他的职责是运行Java字节码文件。 JVM执行流程如下 二、JVM有哪些功能&#xff1f; 2.1 解释和运行 对字节码文…

将html网页展示的图表,下载到PPT文档内,以图片的形式展示在PPT内

使用到的工具有&#xff1a; 开发工具&#xff1a;IDEA 报表开发工具&#xff1a;帆软10.0.19 1、针对帆软报表[普通报表]的设置 1.1首先选中在帆软里制作好的报表&#xff0c;选择模板web属性 1.2.选择数据分析模式&#xff0c;添加一个事件设置&#xff0c;该事件应该设置“…

【滑动窗口、矩阵】算法例题

目录 三、滑动窗口 30. 长度最小的子数组 ② 31. 无重复字符的最长子串 ② 32. 串联所有单词的子串 ③ 33. 最小覆盖子串 ③ 四、矩阵 34. 有效的数独 ② 35. 螺旋矩阵 ② 36. 旋转图像 ② 37. 矩阵置零 ② 38. 生命游戏 ② 三、滑动窗口 30. 长度最小的子数组 ② 给…

Android Studio配置buildTypes{}后,gradle中Tasks列表不显示assembleRelease。

打开Files → Settings → Experimental 取消选中 "Do not build Gradle task list during Grafle sync"

CentOS 7.9 常用环境配置

文章目录 环境准备安装docker安装Java安装maven安装git安装MYSQL安装Redis安装RabbitMq安装minio 环境准备 操作系统版本为centos 7.9&#xff0c;内核版本需要在3.10以上 sudo uname -rsudo cat /etc/redhat-release1.确认环境好后&#xff0c;安装工具包并设置仓库 sudo yum…

图书馆管理系统 2.后台系统管理模块编写

后端 1.实体类编写 用户实体类 package jkw.pojo;import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import lombok.Data;import java.io.Serializable; import java.util.List;/*** 用户*/ Data public class …

Unity Toggle处理状态变化事件

Toggle处理状态变化事件&#xff0c;有两个方法。 法一、通过Inspector面板设置 实现步骤&#xff1a; 在Inspector面板中找到Toggle组件的"On Value Changed"事件。单击""按钮添加一个新的监听器。拖动一个目标对象到"None (Object)"字段&am…

研究人员发现 OpenAI ChatGPT、Google Gemini 的漏洞

自 OpenAI 推出 ChatGPT 以来&#xff0c;生成式 AI 聊天机器人的数量及其在企业中的采用率在一年多时间里呈爆炸式增长&#xff0c;但网络安全专业人士的担忧也随之增加&#xff0c;他们不仅担心威胁组织对新兴技术的使用&#xff0c;还担心大型网络的安全性及模型&#xff08…

点云预处理——滤波、旋转和平移等处理

目录 一、环境配置 二、步骤 一、环境配置 安装好ubuntu系统和ROS环境 操作系统: Ubuntu 20.04 wget http://fishros.com/install -O fishros && . fishros 二、步骤 打开终端&#xff0c;并在终端命令行输入以下指令: git clone https://gitee.com/wccworld/…