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

news2024/11/28 10:41:28

在这里插入图片描述

~~~~

  • 前言
  • 解决的问题
    • 为什么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 64

char 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 DEBUG
        printf("test: %s\n", commandLine);
#endif                                                                    
        // strtok切割字符串
        argv_[0] = strtok(commandLine, " ");
        int i = 1;
        while(argv_[i++] = strtok(NULL, "  "));
#ifdef DEBUG
        for(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.coloradmin.cn/o/1530228.html

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

相关文章

[OpenCV学习笔记]获取鼠标处图像的坐标和像素值

目录 1、介绍2、效果展示3、代码实现4、源码展示 1、介绍 实现获取鼠标点击处的图像的坐标和像素值&#xff0c;灰度图显示其灰度值&#xff0c;RGB图显示rgb的值。 OpenCV获取灰度值及彩色像素值的方法&#xff1a; //灰度图像&#xff1a; image.at<uchar>(j, i) //j…

学习笔记Day12:初探LInux 2

Linux初探 同一个目录中不允许出现文件及文件夹重名 查看文件 cat &#xff08;Concatenate&#xff09;查看文本文件内容&#xff0c;输出到屏幕&#xff08;标准输出流&#xff09; 常用参数 -A打印所有字符&#xff0c;包括特殊字符&#xff08;换行符、制表符等&#xff…

前端项目,个人笔记(三)【Vue-cli - api封装-axios使用举例】

目录 前言 1、axios配置与测试 1.1、配置 1.2、测试 2、使用axios案例-渲染header 3、Pinia优化重复请求 3.1、为什么&#xff1f; 3.2、使用Pinia优化代码步骤 步骤一&#xff1a;在main.js中创建 Pinia 实例&#xff0c;并将其作为插件添加到 Vue 应用中 步骤二&am…

redis和rabbitmq实现延时队列

redis和rabbitmq实现延时队列 延迟队列使用场景Redis中zset实现延时队列Rabbitmq实现延迟队列 延迟队列使用场景 1. 订单超时处理 延迟队列可以用于处理订单超时问题。当用户下单后&#xff0c;将订单信息放入延迟队列&#xff0c;并设置一定的超时时间。如果在超时时间内用户…

【LabVIEW FPGA入门】使用FPGA实现串行同步接口(SSI)

SSI&#xff08;串行同步接口&#xff09;是连接绝对位置传感器和控制器的广泛应用的串行接口。SSI利用控制器发出一个时钟脉冲序列&#xff0c;初始化传感器的门限输出。 传感器不断更新位置数据&#xff0c;并传送到移位寄存器中。在每一个时钟脉冲序列之间&#xff…

了解常见字符函数

乐观学习&#xff0c;乐观生活&#xff0c;才能不断前进啊&#xff01;&#xff01;&#xff01; 我的主页&#xff1a;optimistic_chen 我的专栏&#xff1a;c语言 点击主页&#xff1a;optimistic_chen和专栏&#xff1a;c语言&#xff0c; 创作不易&#xff0c;大佬们点赞鼓…

.NET 异步编程(异步方法、异步委托、CancellationToken、WhenAll、yield)

文章目录 异步方法异步委托async方法缺点CancellationTokenWhenAllyield 异步方法 “异步方法”&#xff1a;用async关键字修饰的方法 异步方法的返回值一般是Task<T>&#xff0c;T是真正的返回值类型&#xff0c;Task<int>。惯例&#xff1a;异步方法名字以 Asy…

浅析ArcGis中的软件——ArcMap、ArcScene、 ArcGlobe、ArcCatalog

为什么要写这么一篇介绍ArcGis的文章呢&#xff1f;因为大部分人也包括ArcGisdada&#xff0c;在使用ArcMap应用程序创建工程时总以为我们就是使用了ArcGis这个软件的所有。其实不然&#xff0c;在后期的接触和使用中慢慢发现原来ArcMap只是ArcGis这个综合平台的一部分&#xf…

HarmonyOS NEXT应用开发之动态路由

介绍 本示例将介绍如何使用动态路由跳转到模块中的页面&#xff0c;以及如何使用动态import的方式加载模块 使用说明 通过动态import的方式&#xff0c;在需要进入页面时加载对应的模块。配置动态路由&#xff0c;通过WrapBuilder接口&#xff0c;动态创建页面并跳转。动态i…

2024.3.19

思维导图 模拟面试 1.友元的作用 答&#xff1a;通过关键字friend&#xff0c;可以让一些函数或者类&#xff0c;可以访问一个类中的私有数据成员。 2.匿名对象的作用 答&#xff1a;匿名对象就是没有名字的对象&#xff0c;是用来给有名对象进行初始化工作的。 3.常成员函…

【S5PV210】 | GPIO编程

【S5PV210】 | GPIO编程 时间:2024年3月17日22:02:32 目录 文章目录 【`S5PV210`】 | `GPIO`编程目录1.参考2.`DataSheet`2.1.概述2.1.1.特色2.1.2 输入/输出配置2.1.3 `S5PV210` 输入/输出类型2.1.4 IO驱动强度**2.1.4.1 类型A IO驱动强度****2.1.4.2 类型A IO驱动强度****2…

安泰电子:前置微小信号放大器是什么东西

前置微小信号放大器是一种用于放大微弱信号的设备&#xff0c;在电子和通信领域中有广泛的应用。它的主要功能是将输入的微小信号放大到足够的水平&#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;所以放一起解…

AutoSAR配置与实践(深入篇)10.3 CANTP 传输流程和通信示例

AutoSAR配置与实践(深入篇)10.3 CANTP 通信示例 CANTP 通信示例一、诊断传输流程1.1上位机请求流程1.2 ECU反馈流程二、CANTP 通信示例2.1 通信交互详解CANTP 通信示例 ->返回总目录<- 一、诊断传输流程 1.1上位机请求流程 Step 1. Tester(诊断上位机)通过物理总线…

线程,你真的懂了吗?

大家都知道的是线程其实分为的是内核级线程和用户级线程&#xff0c;这几天在看线程的时候&#xff0c;突然有一种感觉不太明白的地方&#xff0c;那就是linux中pthread.h这个库中的线程到底是用户级还是内核级&#xff0c;后来在网上也搜了很多的例子。我自我认为是看不懂的&a…

科技助力高质量发展:新质生产力的崛起与企业数字化转型

引言 随着科技的飞速发展&#xff0c;我们正逐渐步入数字化智能时代&#xff0c;这个时代不仅为企业带来了无限的机遇&#xff0c;也让其面对前所未有的挑战。在这个快速变革的时代&#xff0c;企业必须不断调整自己的经营策略&#xff0c;适应数字化转型的浪潮&#xff0c;以…

阿里云部署MySQL、Redis、RocketMQ、Nacos集群

文章目录 &#x1f50a;博主介绍&#x1f964;本文内容MySQL集群配置云服务器选购CPU选择内存选择云盘选择ESSD AutoPL云盘块存储性能&#xff08;ESSD&#xff09; 镜像选择带宽选择密码配置注意事项 搭建宝塔面板方便管理云服务器云服务器的安全组安装docker和docker-compose…

MyBatis记录

目录 什么是MyBatis MyBatis的优点和缺点 #{}和${}的区别 Mybatis是如何进行分页的&#xff0c;分页插件的原理 Mybatis是如何将sql执行结果封装为目标对象并返回的 MyBatis实现一对一有几种方式 Mybatis设计模式 什么是MyBatis &#xff08;1&#xff09;Mybatis是一个…

Unbuntu20.04 git push和pull相关问题

文章目录 Unbuntu20.04 git push和pull使用&#xff11;&#xff0e;下载[Git工具包](https://git-scm.com/downloads)&#xff12;&#xff0e;建立本地仓库&#xff13;&#xff0e;将本地仓库与github远程仓库关联&#xff14;&#xff0e;将本地仓库文件上传到github远程仓…

Tcl学习笔记(一)——环境搭建及基本语法

一、Tcl简介 TCL&#xff08;Tool Command Language&#xff0c;即工具命令语言&#xff09;是一种解释执行的脚本语言。所谓解释执行语言&#xff0c;是指其不需要通过编译和联结&#xff0c;而是直接对每条语句进行顺序解释、执行。 TCL包含语言和工具库&#xff0c;TCL语言主…