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 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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

electron-builder 打包问题,下载慢解决方案

目录 问题说明设置下载源 &#xff1f;解决方案思路下载Electron下载winCodeSign下载nsis下载nsis-resources 总结 问题说明 项目使用了Electron&#xff0c;在第一次打包时会遇见下载慢&#xff0c;导致打包进度几乎停滞不前&#xff0c;甚至可能直接报错 其实这是因为Electr…

【SpringBoot3+Mybatis】框架快速搭建

文章目录 GitHub 项目一、依赖二、 配置文件三、启动类四、SpringBoot3兼容Druid报错五、工具类5.1 结果封装类5.2 解决枚举类5.3 MD5加密工具类 GitHub 项目 springboot-part——springboot-integrate-07 Mybatis-plus版完整CRUD项目文档记录&#xff1a; 【SpringBoot3Myba…

【Python循环6/6】循环的综合运用

目录 回顾 for循环遍历列表 for循环进行累加/累乘的计算 复杂的条件判断 嵌套 嵌套循环 练习 遍历整数列表 总结 回顾 在之前的博文中&#xff0c;我们学习了for计数循环&#xff1b;while条件循环&#xff1b;以及跳出循环的两种方法break&#xff0c;continu…

CMU 10-414/714: Deep Learning Systems --hw3

实现功能 在ndarray.py文件中完成一些python array操作 我们实现的NDArray底层存储就是一个一维向量&#xff0c;只不过会有一些额外的属性&#xff08;如shape、strides&#xff09;来表明这个flat array在维度上的分布。底层运算&#xff08;如加法、矩阵乘法&#xff09;都…

幻兽帕鲁游戏搭建(docker)

系列文章目录 第一章&#xff1a; 幻兽帕陆游戏搭建 文章目录 系列文章目录前言一、镜像安装1.创建游戏目录2.拉取镜像3.下载配置文件4.启动游戏 二、自定义配置总结 前言 这段时间一直在写论文还有找工作&#xff0c;也没学啥新技术&#xff0c;所以博客也很长时间没写了&am…

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

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

高效备考2025年AMC8竞赛:吃透2000-2024年600道真题(免费送题)

我们继续来随机看五道AMC8的真题和解析&#xff0c;根据实践经验&#xff0c;对于想了解或者加AMC8美国数学竞赛的考生来说&#xff0c;吃透AMC8历年真题是备考更加科学、有效的方法之一。 即使不参加AMC8竞赛&#xff0c;吃透了历年真题600道和背后的知识体系&#xff0c;那么…

Qt笔记 mainwindow

mainwindow是用来做应用界面的&#xff0c;有菜单栏&#xff0c;工具栏&#xff0c;浮动窗口,中心部件以及状态栏这几个部分组成。 举个例子&#xff1a; 1.菜单栏: #include <QMenuBar>QMenuBar *menubar new QMenuBar(this); setMenuBar(menubar);//设置到当前窗口 …

31-Java前端控制器模式(Front Controller Pattern)

Java前端控制器模式 实现范例 前端控制器模式&#xff08;Front Controller Pattern&#xff09;是用来提供一个集中的请求处理机制&#xff0c;所有的请求都将由一个单一的处理程序处理该处理程序可以做认证/授权/记录日志&#xff0c;或者跟踪请求&#xff0c;然后把请求传给…

AI系统性学习05—向量数据库

文章目录 1、Chroma向量数据库1.1 安装Chroma1.2 初始化Chroma客户端1.3 创建一个合集1.4 添加数据1.5 查询数据1.6 持久化数据1.7 集合操作1.7.1 创建集合1.7.2 获取集合1.7.3 删除集合1.7.4 其他操作1.8 向集合添加数据1.9 查询集合数据1.10 更新集合数据1.11 删除集合数据1.…

[CISCN2019 华北赛区 Day1 Web5]CyberPunk --不会编程的崽

继续sql,哈哈。我按照我的思路来讲。 四个功能&#xff0c;提交&#xff0c;查找&#xff0c;修改&#xff0c;删除。多半是sql注入&#xff0c;而且又有修改&#xff0c;查找功能。多半还是二次注入。昨天那个修改密码的&#xff0c;也是二次注入。这里需要先找到注入点 姓名 …

STM32CubeMX学习笔记24---FreeRTOS(消息队列)

一. 队列简介 队列是为了任务与任务、任务与中断之间的通信而准备的&#xff0c;可以在任务与任务、任务与中 断之间传递消息&#xff0c;队列中可以存储有限的、大小固定的数据项目。任务与任务、任务与中断之 间要交流的数据保存在队列中&#xff0c;叫做队列项目。队列…

SLAM 求解IPC算法

基础知识&#xff1a;方差&#xff0c;协方差&#xff0c;协方差矩阵 方差&#xff1a;描述了一组随机变量的离散程度 方差 每个样本值 与 全部样本的平均值 相差的平方和 再求平均数&#xff0c;记作&#xff1a; 例如&#xff1a;计算数字1-5的方差&#xff0c;如下 去中心化…

Vulnhub靶机渗透:DC-7打靶记录

前言 自信自强&#xff0c;来自于不怕苦、不怕难的积淀。宝剑锋从磨砺出&#xff0c;梅花香自苦寒来&#xff1b;任何美好理想&#xff0c;都离不开筚路蓝缕、手胼足胝的艰苦奋斗&#xff01; 靶场介绍 DC-7是一个初中级的靶场&#xff0c;需要具备以下前置知识&#xff1a;…

【开源】SpringBoot框架开发不良邮件过滤系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 系统用户模块2.2 收件箱模块2.3 发件箱模块2.4 垃圾箱模块2.5 回收站模块2.6 邮箱过滤设置模块 三、实体类设计3.1 系统用户3.2 邮件3.3 其他实体 四、系统展示五、核心代码5.1 查询收件箱档案5.2 查询回收站档案5.3 新…

[Qt项目实战]Qt实现美松标签打印机标签二维码打印(QR混排模式+页打印模式)

1、硬件信息、环境参数及配套资料 1.1 打印机信息及开发环境 打印机 美松标签打印机串口/USB通讯Qt5.9 64位程序 1.2 打印机配套开发资料 打印机主要配套测试工具、开发SDK及驱动等&#xff0c;均由厂家提供。 开发Demo及动态库&#xff1a;MsPrintSDK-DLL-V2.2.2.5 链接&…

3、java虚拟机-类的生命周期-初始化阶段(与程序员有关)

一 、静态代码块执行顺序和字节码文件中的执行顺序以及什么赋值。 类的生命周期-初始化阶段-被static所修饰的常量才会被赋予值 初始化阶段-代码中静态代码块和静态变量的顺序和字节码中的执行顺序是一致的。 二、4种情况下&#xff0c;类会被初始化。 1、怎样查看类是…

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

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

Go语言学习13-常见软件架构的实现

Go语言学习13-常见软件架构的实现 架构模式 An architectural pattern is a general, reusable solution to a commonly occurring problem in software architectural within a given context. ——wikipedia Pipe-Filter 架构 Pipe-Filter 模式 非常适合于数据处理及数据分…

taro之Picker,PickerView基础用法

1.Picker 直接上代码 import Taro,{Component} from "tarojs/taro"; import {View,Picker} from tarojs/components import { AtIcon } from taro-ui import { putKey } from /src/utils/storage-utilsclass AgriculturePolicy extends Component{constructor (prop…