《Linux从练气到飞升》No.21 Linux简单实现一个shell

news2024/11/26 22:24:02

🕺作者: 主页

我的专栏
C语言从0到1
探秘C++
数据结构从0到1
探秘Linux
菜鸟刷题集

😘欢迎关注:👍点赞🙌收藏✍️留言

🏇码字不易,你的👍点赞🙌收藏❤️关注对我真的很重要,有问题可在评论区提出,感谢阅读!!!

文章目录

    • 前言
    • 01. 框架搭建
    • 02. 打印提示信息
    • 03. 获取用户键盘输入
      • 如何获取用户在命令行的输入呢?
    • 04. 命令行字符串解析
    • 05. 创建子进程执行命令
      • 怎么知道要调用的程序在哪里呢?
      • 为什么要替换?
      • 环境变量相关的数据,会被替换吗??
    • 06. 内置命令 —— cd
    • 07. 内置命令 —— export
      • shell 执行的命令通常有两种
      • shell的环境变量从哪里来的?(了解)
    • 08. 类似ll这种别名命令无法识别
    • 后记

前言

前面我们讲述了进程的相关知识,包括进程创建、进程等待、进程替换等,这些我们都在Linux上进行了测试,并且通常使用的shell来执行命令,那么我们能不能自己来实现一个简单的shell呢?

我们知道在shell上执行命令时,其原理不过也只是调用和执行文件罢了,也就是创建进程来执行程序,而shell一般是不退出的,那么我们现在开始玩一下

01. 框架搭建

命令行解释器一定是一个常驻内存的进程,不退出,所以我们使用while包起来

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

#define NUM 1024
#define SIZE 32
#define SEP " "

//保存完整的命令行字符串
char cmd_line[NUM];

//保存打散之后的命令行字符串
char *g_argv[SIZE];

// 写一个环境变量的buffer,用来测试
char g_myval[64];

// shell 运行原理 : 通过让子进程执行命令,父进程等待&&解析命令
int main()
{
    extern char**environ;
    //0. 命令行解释器,一定是一个常驻内存的进程,不退出
    while(1)
    {

    }
}

02. 打印提示信息

参考shell的提示信息:
在这里插入图片描述
它们都有各自的含义和获取方式,但是这里为了简化,不考虑这些细枝末节,由大家自己改进!
我这里就写死了哦~

我们可以直接使用printf函数打印,但是会有一个问题,如果设置了\n,它会换行,但是我们使用shell时并不会换行,那么我们就需要用到fflush函数来冲刷缓存区。

 //1. 打印出提示信息 [venus@localhost myshell]# 
        printf("[root@localhost myshell]# ");
        fflush(stdout);

03. 获取用户键盘输入

如何获取用户在命令行的输入呢?

我们用一个数组来存储命令,使用fgets函数来获取输入
步骤:

  • 先初始化数组
  • 获取存储命令
  • 将最后的回车符号设置为'\0'
//2. 获取用户的键盘输入[输入的是各种指令和选项: "ls -a -l -i"]
        if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
        {
            continue;
        }
        cmd_line[strlen(cmd_line)-1] = '\0';

04. 命令行字符串解析

到这一步,我们已经将命令行的字符串存储到数组中了,接下来就是解析它
步骤:

  • 这里要使用strtok函数来裁剪字符串
  • 将存储的命令和系统内部命令做比对,如果有就执行
g_argv[0] = strtok(cmd_line, SEP); //第一次调用,要传入原始字符串
int index = 1;
if(strcmp(g_argv[0], "ls") == 0)
{
    g_argv[index++] = "--color=auto";//加入配色
}
while(g_argv[index++] = strtok(NULL, SEP)); //第二次,如果还要解析原始字符串,传入NULL

05. 创建子进程执行命令

怎么知道要调用的程序在哪里呢?

直接使用进程替换,使用execvp函数,它可以直接使用环境变量不用自己写了,也就是直接掉用系统中的指令的程序来使用即可。

为什么要替换?

一切和应用场景有关,我们有时候必须要让子进程执行新的程序

环境变量相关的数据,会被替换吗??

没有!它不会被替换,它会把父进程的环境变量拷贝继承过来,它具有全局属性

pid_t id = fork();
if(id == 0) //child
{
    printf("下面功能让子进程进行的\n");
    execvp(g_argv[0], g_argv); // ls -a -l -i
    exit(1);
}
//father
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0) printf("exit code: %d\n", WEXITSTATUS(status));

此时程序基本功能就已经实现了

但是,我们发现一个问题,使用cd命令时,他的路径不会改变,这是个bug

原因是:

  • 在cd的时候,自己写的shell都会执行execvp,它只会影响子进程的路径

  • 但是我们需要改变父进程的路径,所以像cd这种命令,我们不想让子进程去执行它而让父进程去执行它

  • 这种让父进程自己执行的命令叫做内置命令、内建命令,它的本质是shell中的一个函数调用

我们来修改下功能

06. 内置命令 —— cd

这里可以使用chdir函数来实现

chdir函数可以改变文件路径

我们可以使用下面代码,使得cd命令的使用合理,但是可能其他类似的命令也会出现相似的bug,需要一一比对实现,这里仅针对cd命令

if(strcmp(g_argv[0], "cd") == 0) //not child execute, father execute
{
    if(g_argv[1] != NULL) chdir(g_argv[1]); //cd path, cd ..

    continue;
}

07. 内置命令 —— export

上面我们讲了cd的bug,而export也和cd一样,也需要进行处理,export的作用是导入环境变量,我们既不想覆盖父进程的环境变量,又想导入自己的环境变量,该怎么做呢?

代码如下:

//导入环境变量
//比较第一个是不是export
if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)
{
    strcpy(g_myval, g_argv[1]);//是的就取出后面的值
    int ret = putenv(g_myval);//将它导入环境变量中
    if(ret == 0) printf("%s export success\n", g_argv[1]);//如果导入成功就打印出来
    continue;
}

shell 执行的命令通常有两种

  1. 第三方提供的对应的在磁盘中具有二进制文件的可执行程序(由子进程执行)

  2. shell内部自己实现的方法,由自己(父进程)来执行,有些命令就是要影响shell本身,如改变路径的(cd、export),shell代表的是用户。

shell的环境变量从哪里来的?(了解)

环境变量是写在配置文件中的,shell启动的时候,通过读取配置文件获得的起始环境变量

08. 类似ll这种别名命令无法识别

ll是ls -l的别名

想要支持就要当识别到ll时执行ls命令即可

if(strcmp(g_argv[0], "ll") == 0)
{
    g_argv[0] = "ls";
    g_argv[index++] = "-l";
    g_argv[index++] = "--color=auto";
}

系统中肯定不是这样实现的,但是大致原理相同

后记

最后我们就实现了一个简易的shell解释器

全部代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

#define NUM 1024
#define SIZE 32
#define SEP " "

//保存完整的命令行字符串
char cmd_line[NUM];

//保存打散之后的命令行字符串
char *g_argv[SIZE];

// 写一个环境变量的buffer,用来测试
char g_myval[64];

// shell 运行原理 : 通过让子进程执行命令,父进程等待&&解析命令
int main()
{
    extern char**environ;
    //0. 命令行解释器,一定是一个常驻内存的进程,不退出
    while(1)
    {
        //1. 打印出提示信息 [whb@localhost myshell]# 
        printf("[root@localhost myshell]# ");
        fflush(stdout);
        memset(cmd_line, '\0', sizeof cmd_line);
        //2. 获取用户的键盘输入[输入的是各种指令和选项: "ls -a -l -i"]
        if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
        {
            continue;
        }
        cmd_line[strlen(cmd_line)-1] = '\0';
        //3. 命令行字符串解析:"ls -a -l -i" -> "ls" "-a" "-i"
        // export myval=105
        g_argv[0] = strtok(cmd_line, SEP); //第一次调用,要传入原始字符串
        int index = 1;
        if(strcmp(g_argv[0], "ls") == 0)
        {
            g_argv[index++] = "--color=auto";
        }
        if(strcmp(g_argv[0], "ll") == 0)
        {
            g_argv[0] = "ls";
            g_argv[index++] = "-l";
            g_argv[index++] = "--color=auto";
        }
        while(g_argv[index++] = strtok(NULL, SEP)); //第二次,如果还要解析原始字符串,传入NULL
        if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)
        {
            strcpy(g_myval, g_argv[1]);
            int ret = putenv(g_myval);
            if(ret == 0) printf("%s export success\n", g_argv[1]);
            continue;
        }
        //4.内置命令, 让父进程(shell)自己执行的命令,我们叫做内置命令,内建命令
        //内建命令本质其实就是shell中的一个函数调用
        if(strcmp(g_argv[0], "cd") == 0) //not child execute, father execute
        {
            if(g_argv[1] != NULL) chdir(g_argv[1]); //cd path, cd ..

            continue;
        }
        //5. fork()
        pid_t id = fork();
        if(id == 0) //child
        {
            printf("下面功能让子进程进行的\n");
            printf("child, MYVAL: %s\n", getenv("MYVAL"));//测试环境变量
            printf("child, PATH: %s\n", getenv("PATH"));//测试环境变量
            //环境变量相关的数据,会被替换吗??没有!
            execvp(g_argv[0], g_argv); // ls -a -l -i
            exit(1);
        }
        //father
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if(ret > 0) printf("exit code: %d\n", WEXITSTATUS(status));
    }
}

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

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

相关文章

​​​​​​​光伏智慧设施休息区

随着人们环保意识的日益增强&#xff0c;光伏智慧景观渐渐出现在大众的视野&#xff0c;成为低碳城镇建设的新景观、新亮点。打造光伏智慧设施休息区&#xff0c;既能满足游客休息纳凉&#xff0c;还能设置手机相机等充电区域。此设备组还可提供夜间照明灯、音乐广播、多媒体广…

flask中的操作数据库的插件Flask-SQLAlchemy

1、ORM 框架 Web 开发中&#xff0c;一个重要的组成部分便是数据库了。Web 程序中最常用的莫过于关系型数据库了&#xff0c;也称 SQL 数据库。另外&#xff0c;文档数据库&#xff08;如 mongodb&#xff09;、键值对数据库&#xff08;如 redis&#xff09;近几年也逐渐在 w…

普通平衡树 Splay

Splay 简介 Splay&#xff08;伸展树&#xff09;&#xff0c;又叫做分裂树&#xff0c;是一种自调整形式的二叉查找树&#xff0c;满足二叉查找树的性质&#xff1a;一个节点左子树的所有节点的权值&#xff0c;均小于这个节点的权值。且其右子树所有节点的权值&#xff0c;均…

IEC 62368-1:2023(第4版)《音频视频、信息和通信设备 第1部分安全要求》标准发布,IEC 62368-1第四版标准更新与变化

2023年05月26日&#xff0c;IEC 62368-1:2023 《音频视频、信息和通信设备 第1部分安全要求》第4版标准正式发布&#xff0c;2023年08月18日&#xff0c;IECEE又发布了TRF模板&#xff0c;为新版标准的实施和应用做好了准备. 下载地址 &#xff1a; https://download.csdn.net…

【个人博客系统网站】项目的发布 · 通过公网IP访问我们的网站 · 思考总结

【JavaEE】进阶 个人博客系统&#xff08;6&#xff09; 文章目录 【JavaEE】进阶 个人博客系统&#xff08;6&#xff09;1. 项目发布1.1 后端代码修改1.1.1 数据库密码1.1.2 端口号修改1.1.3 文件保存地址修改1.1.4 静态资源映射修改 1.2 云服务器1.2.1 建库建表1.2.2 必要…

算法通关村第十三关——溢出问题处理模板

前言 溢出问题是面试当中输出涉及到数字的一个需要特别注意的地方&#xff0c;典型的题目有三个&#xff1a;数字反转&#xff0c;将字符串转成数字和回文数。 1.整数反转 力扣7题&#xff0c;给你一个 32 位的有符号整数 x &#xff0c;返回将 x 中的数字部分反转后的结果。…

Vue + Element UI 前端篇(十五):嵌套外部网页

Vue Element UI 实现权限管理系统 前端篇&#xff08;十五&#xff09;&#xff1a;嵌套外部网页 嵌套外部网页 在有些时候&#xff0c;我们需要在我们的内容栏主区域显示外部网页。如查看服务端提供的SQL监控页面&#xff0c;接口文档页面等。 这个时候就要求我们的导航菜…

Intel OneAPI黑客松记录

题目&#xff1a; 生成2048*2048个随机单精度实数; 实现两维Real to complex FFT参考代码; 使用OneMKL计算两维Real to complex FFT; 对两维FFT输出数据进行全数据比对&#xff1b; 平均性能数据比对,输出FFT参考代码平均运行时间和oneMKL FFT平均运行时间。 代码&#x…

12 - 多线程之锁优化(上):深入了解Synchronized同步锁的优化方法

在并发编程中&#xff0c;多个线程访问同一个共享资源时&#xff0c;我们必须考虑如何维护数据的原子性。在 JDK1.5 之前&#xff0c;Java 是依靠 Synchronized 关键字实现锁功能来做到这点的。Synchronized 是 JVM 实现的一种内置锁&#xff0c;锁的获取和释放是由 JVM 隐式实…

序列化二叉树

解题思路一&#xff1a; import java.util.*; /* public class TreeNode {int val 0;TreeNode left null;TreeNode right null;public TreeNode(int val) {this.val val;}} */ public class Solution {int index -1;String Serialize(TreeNode root) {StringBuffer resul…

Jenkins介绍

Jenkins介绍 持续集成、持续部署的工具很多&#xff0c;其中Jenkins是一个开源的持续集成平台。 Jenkins涉及到将编写完毕的代码发布到测试环境和生产环境的任务&#xff0c;并且还涉及到了构建项目等任务。 Jenkins需要大量的插件保证工作&#xff0c;安装成本较高&#xff0…

FreeRTOS源码分析-14 操作系统多任务核心

1 列表及列表项概念及其应用 1.1 freeRTOS列表介绍 任务调度列表介绍 其他组件列表介绍 1.2 列表及列表项的定义 双向链表结构 1&#xff1a;插入与删除效率高&#xff0c;只要操作一次就能完成 2&#xff1a;对数据的管理更加灵活与便捷&#xff0c;有利于编写逻辑清晰的…

MyBatisPlus入门篇2 - 条件查询、查询投影、查询条件、id生成策略、多记录操作、逻辑删除

目录 1.条件查询、多条件查询 MyBatisPlus将书写复杂的SQL查询条件进行了封装&#xff0c;使用编程的形式完成查询条件的组合。 Test void testGetByCondition() {// 方式一&#xff1a;按条件查询QueryWrapper<User> qw new QueryWrapper<User>();qw.lt("…

解决虚拟机克隆后IP和命名冲突问题

目录 解决IP冲突问题 解决命名冲突 解决IP冲突问题 克隆后的虚拟机和硬件地址和ip和我们原虚拟机的相同&#xff0c;我们需要重新生成硬件地址和定义ip&#xff0c;步骤如下&#xff1a; &#xff08;1&#xff09;进入 /etc/sysconfig/network-scripts/ifcfg-ens33 配置文件…

【MySQL】7、MHA高可用配置及故障切换

MHA概述 MHA&#xff08;Master High Availability&#xff09;是一套优秀的MySQL高可用环境下故障切换和主从复制的软件。 MHA用来解决MySQL单点故障问题&#xff1b; MySQL故障切换过程中&#xff0c;能30秒内自动完成故障切换&#xff0c;并保证数据的一致性&#xff0c;实…

远程工作面试:特殊情况下的面试技巧

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

Python批处理(一)提取txt中数据存入excel

Python批处理&#xff08;一&#xff09;提取txt中数据存入excel 问题描述 现从冠层分析软件中保存了叶面积指数分析的结果&#xff0c;然而软件保存格式为txt&#xff0c;且在不同的文件夹中&#xff0c;每个文件夹的txt文件数量不固定&#xff0c;但是txt文件格式固定。现需…

Java多线程父线程向子线程传值解决方案

目录 1 背景2 ThreadLocalTaskDecorator3 RequestContextHolderTaskDecorator4 MDCTaskDecorator5 InheritableThreadLocal5.1 源码分析5.2 InheritableThreadLocal存在的问题 6 TransmittableThreadLocal 1 背景 在实际开发过程中我们需要父子之间传递一些数据&#xff0c;比…

zemax非序列文件转为序列文件

打开系统内部的一个参考案例&#xff1a; 添加一个新面在最前面&#xff0c;设置为孔径光阑&#xff1a; 转换&#xff1a; 此时的3D外形图&#xff1a; 删去不必要的&#xff0c;保留透镜&#xff1a; 加入椭圆光源&#xff1a; 插入探测器&#xff1a; 执行光线追迹&#xff…

day 47 | ● 392.判断子序列 ● 115.不同的子序列

392.判断子序列 如果用dp判断true or false无法满足&#xff0c;所以dp用来表示以下标i-1为结尾的字符串s&#xff0c;和以下标j-1为结尾的字符串t&#xff0c;相同子序列的长度 func isSubsequence(s string, t string) bool {dp : make([][]int, len(s) 1)for i : 0; i &…