[Linux] Linux 模拟实现 Shell

news2024/10/25 5:26:41

标题:[Linux] Linux 模拟实现 Shell

个人主页@水墨不写bug(图片来源于网络)

目录

一、什么是shell

二、shell的理解

三、模拟实现shell

1)打印命令行提示

2)获取用户的输入字符串

3)分割命令字符串

4)检查是否是内建命令

1. 什么是内建命令

常见的内建命令

5)进程程序替换


 正文开始:

一、什么是shell

         Shell是用C语言编写的一个独立的可执行程序,它是用户与Linux或其他类Unix系统(如Mac OS)交流的桥梁,既是命令语言又是程序设计语言。

        定义:Shell是Linux内核的一个外层保护工具,并负责完成用户与内核之间的交互。

        功能:Shell是一个命令行解释器,它将用户输入的命令解析为操作系统所能理解的指令,实现用户与操作系统的交互。同时,Shell也提供了一种脚本编写功能,允许用户将一系列命令按照特定顺序排列,形成脚本文件,由Shell解释器逐行执行,以完成特定任务或实现一系列操作。

         Shell提供了丰富的内置命令和外部命令,如cd用于切换目录,ls用于列出目录内容,grep用于搜索文本等。这些命令可以组合使用,形成复杂的脚本,以实现各种功能。

 

二、shell的理解

         站在shell的角度,一个可执行程序,运行起来成为进程,它读取的一段指令其实一串字符串,操作系统无法理解这串字符串的具体意义,无法与用户交流,操作系统也就无法为用户服务。shell是外壳程序,就像一个包在操作系统外面的一层壳子,shell可以把用户输入的一串字符“翻译”

 给操作系统,把字符串转化为操作系统可以“听懂的”语言,这样操作系统就能听懂用户的需求,可以进一步为用户服务了。

        所以,根据这一理解,我们可以先设计一个框架,表明我们实现的shell的大致思路:

        1)打印命令行提示

        2)获取用户的输入字符串

        3)分割命令字符串,并存储到全局的字符串数组:myg_argv

        4)检查是否是内建命令,内建命令需要父进程自己执行

        5)进程程序替换

       

如果不知道什么是进程程序替换,可以浏览这一篇:《[Linux]进程程序替换》

        这也就表明了shell的运行机制,:

        假如shell读取到“ls”,shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。

三、模拟实现shell

         由于我们当前实现的shell比较简单,目前没有分离编译最后链接,简单点说就是没有把声明和实现分离为源文件和头文件。而是简单的把一个一个的步骤封装为一个一个的函数。

1)打印命令行提示

         我们在与Linux的bash(shell的一种)命令行进行交互的时候,会发现每一次输入命令的前面,都会有一个命令行提示:

这个命令行提示包含的信息有:

用户名+主机名+当前目录 

        幸运的是,这些信息我们可以直接从环境变量中获取。我们可以通过<stdlib.h>中的getenv()接口获得环境变量,获取了这些环境变量之后就好办了,只需要用printf(标准格式化输出)即可实现打印命令行提示。具体实现如下:


const char *get_username()
{
    const char *name = getenv("USER");
    if (name == NULL)
    {
        return "NONE";
    }
    return name;
}

const char *get_hostname()
{
    const char *hostname = getenv("HOSTNAME");
    if(hostname == NULL)
    {
        return "NONE";
    }
    return hostname;
}

const char *get_cwd()
{
    const char *cwd = getenv("PWD");
    if (cwd == NULL)
    {
        return "NONE";
    }
    return cwd;
}
void output_sng_line()
{
    // 直接打印输出到stdout
    const char *cwd = get_cwd();

   
    if (strlen(cwd) != 1)//特殊处理根目录“/”的情况
    {
        printf("%s@%s:~%s$ ", get_username(), get_hostname(), cwd);
    }
    else
    {
        printf("%s@%s:%s$ ", get_username(), get_hostname(), cwd);
    }

    fflush(stdout); // 刷新缓冲区
}

总结:

                通过库函数getenv获得环境变量,printf格式化输出即可;

                输出的格式可以对照你的本地的命令行提示符。

        上面的实例是按照我的阿里云服务器的命令行提示符来设计的,这是我的命令行提示符:

(包括普通目录和 特殊处理的根目录)

       

        注意:获取的环境变量PATH是一串完整的路径,与上图的路径不同,这就需要我们对路径进行截取,由于通过函数进行修改需要传递二级指针,比较麻烦,所以我们可以考虑使用宏替换函数实现:


// 这样写do{}while(0):形成代码块;while(0)后面可随便带分号
#define Re_back(p)          \
    do                      \
    {                       \
        p += strlen(p) - 1; \
        while (*p != '/')   \
            --p;            \
    } while (0)

修改后的函数具体实现:


void output_sng_line()
{
    // 直接打印输出到stdout
    const char *cwd = get_cwd();

    // 这个宏后面可以随便加分号,看起来更像一个函数
    Re_back(cwd);

    if (strlen(cwd) != 1)
    {
        printf("%s@%s:~%s$ ", get_username(), get_hostname(), cwd);
    }
    else
    {
        printf("%s@%s:%s$ ", get_username(), get_hostname(), cwd);
    }

    fflush(stdout); // 刷新缓冲区
}


 

2)获取用户的输入字符串

         我们需要再设计一个缓冲区,用来存储读取的命令行信息——usercommand,SIZE的大小可以自己设计,可以大一些,这样允许一次输入比较长的指令。

        返回值表示获取命令是否成功,如果失败返回-1——如果失败,就没有运行的意义了,所以退出即可。

main函数接口调用:

获取用户的输入字符串 函数具体实现:

     一个小细节,在读取的时候,由于我们最后输入指令的时候,会附带一个换行符,表示输入命令结束,这个换行符也会被读入,于是需要将缓冲区的 '\0'的位置提前一个位置。


int get_user_command(char line[], int n)
{
    char *s = fgets(line, n, stdin);
    if (s == NULL)
    {
        printf("err\n");
        return -1;
    }
    // 处理字符串结尾的\n
    line[strlen(line) - 1] = ZERO;
    return 0;
    // printf("%s\n",line);
}


 

3)分割命令字符串

main函数接口调用:

         分割命令字符串具体实现:(复用库函数strtok(),字符串分割函数)


void partation_command(char *command, int n)
{
    (void)n; // 暂时禁用n,防止编译老是提醒

    myg_argv[0] = strtok(command, SKP);

    int index = 1;
    // 有意的设计 “=” :先赋值再判断;当srtok无法分割时,返回NULL,正好让g_argc最后一个元素为NULL
    while ((myg_argv[index++] = strtok(NULL, SKP)))
    {
    }
}

 


4)检查是否是内建命令

1. 什么是内建命令

        在Linux操作系统中,内建命令(builtin commands)是指那些直接由shell(如Bash、Zsh等)自身实现和提供的命令,而不是作为独立的可执行文件存在于文件系统中的命令。内建命令通常比外部命令(external commands)执行得更快,因为它们不需要启动一个新的进程来执行。 

常见的内建命令

以下是一些常见的Bash内建命令:

  • cd:更改当前工作目录。
  • echo:在标准输出上显示一行文本。
  • exec:用指定的命令替换当前的shell进程。

         等等....

        其实,内建命令之所以必须是内建命令,一定有一定的原因:

        以cd为例子,执行cd,如果父进程创建一个子进程让他帮自己执行,那么子进程的目录确实是切换了,但是父进程的目录还是没有变化啊!所以cd之所以必须是内建命令,是因为必须让父进程自身执行cd命令。让父进程自身切换工作路径,需要系统调用接口:

chdir() 

main函数接口调用:

        返回值表示:

        1——是内建命令,父进程需要自己完成命令,则不需要执行第五步操作;

        其他——不是内建命令,父进程可以通过创建子进程来让子进程替自己完成命令。 

 检查进程是否是内建命令其实是很简单朴素的,就是一个一个的条件判断,具体函数实现如下:

(这里也给出了CD内建命令的实现方式)


void _CD()
{
    char *newpath = myg_argv[1];
    // 说明只有一个cd,默认切换到家目录
    if (newpath == NULL)
    {
        newpath = (char *)get_home();
    }

    chdir(newpath);

    // 这个时候,环境变量没有被改变,使用cd的时候,虽然可以切换路径,但是getpath打印出来的不变
    // path环境变量需要shell自己维护
    char tem[SIZE];
    getcwd(tem, sizeof(tem));
    snprintf(cwd, sizeof(cwd), "PWD=%s", tem);
    putenv(cwd);
}

int check_inbuildcommand_execute()
// 内建命令需要父进程执行,如果子进程执行cd,则子进程目录切换,父进程没切换
{
    int yes = 0;
    // myg_argv[0]一定是命令
    char *inb_cmd = myg_argv[0];

    // 检查是否是cd命令,
    if (strcmp(inb_cmd, "cd") == 0)
    {
        yes = 1;
        _CD();
    }
    else if (strcmp(inb_cmd, "echo") == 0 && strcmp(myg_argv[1], "$?") == 0)
    {
        yes = 1;
        printf("%d\n", lastcode);
    }
    return yes;
}

 


 

5)进程程序替换

 main函数接口调用:

         父进程创建一个子进程帮自己干活,父进程只需要坐在一旁等着就行了:


void execute_command()
{
    pid_t id = fork();
    if (id == 0) // 子进程给父进程做事
    {
        int ret = execvp(myg_argv[0], myg_argv);
        if (ret == -1)
        {
            exit(errno);
        }
    }
    else // 父进程
    {
        int status = 0;
        pid_t rid = waitpid(id, &status, 0); // 等待子进程退出

        if (rid > 0) // 等待子进程成功
        {
            if (WIFEXITED(status))
            {
                lastcode = WEXITSTATUS(status);
            }
            else
            {
                printf("lastcode 不重要了,子进程被信号杀掉\n");
            }
        }
        else
        {
            printf("waitpid fail");
            exit(-1); // 父进程shell退出
        }
    }
}

        最后不要忘了回收子进程的退出信息。 


完~

未经作者同意禁止转载

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

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

相关文章

【优选算法篇】双指针的优雅舞步:C++ 算法世界的浪漫探索

文章目录 C 双指针详解&#xff1a;基础题解与思维分析前言第一章&#xff1a;对撞指针1.1 移动零解题思路图解分析C代码实现易错点提示代码解读 1.2 复写零解题思路算法步骤C代码实现易错点提示代码复杂度 1.3 盛最多水的容器1. 题目链接2. 题目描述解法一&#xff08;暴力求解…

链表(4)_合并K个升序链表_面试题

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 链表(4)_合并K个升序链表_面试题 收录于专栏【经典算法练习】 本专栏旨在分享学习算法的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录…

第十五届蓝桥杯C++B组省赛

文章目录 1.握手问题解题思路1&#xff08;组合数学&#xff09;解题思路2&#xff08;暴力枚举&#xff09; 2.小球反弹做题思路 3.好数算法思路&#xff08;暴力解法&#xff09;---不会超时 4.R格式算法思路 5.宝石组合算法思路---唯一分解定理 6.数字接龙算法思路----DFS 7…

【Oracle数据库进阶】001.SQL基础查询_查询语句

课 程 推 荐我 的 个 人 主 页&#xff1a;&#x1f449;&#x1f449; 失心疯的个人主页 &#x1f448;&#x1f448;入 门 教 程 推 荐 &#xff1a;&#x1f449;&#x1f449; Python零基础入门教程合集 &#x1f448;&#x1f448;虚 拟 环 境 搭 建 &#xff1a;&#x1…

Egg考古系列-EggCore的生命周期

关于EGG egg框架的第一个版本还是2017-03-21&#xff0c;距今已有7年了。虽然最近几年没有什么更新&#xff0c;但它在国内的使用还是挺多的&#xff0c;mvc的分层模式也很受大家喜欢。虽然声称是面向企业级、中大型项目场景的框架&#xff0c;但这种约定式在大型项目中其实也很…

高校学科竞赛管理:SpringBoot实现的高效策略

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常…

【M2-Mixer】核心方法解读

abstract&#xff1a; 在本文中&#xff0c;我们提出了M2-Mixer&#xff0c;这是一种基于MLPMixer的结构&#xff0c;具有多头损失&#xff0c;用于多模态分类。它比基于卷积、循环或神经结构搜索的基线模型具有更好的性能&#xff0c;其主要优势是概念和计算简单。所提出的多…

【电子电力】LCL滤波器设计,包括电流控制调谐

摘要 LCL 滤波器是电力电子领域中广泛应用于并网逆变器的滤波器之一&#xff0c;其主要功能是减少高频开关的谐波&#xff0c;确保输出电流的质量。本文设计并实现了基于 MATLAB 的 LCL 滤波器模型&#xff0c;结合电流控制器和调谐技术&#xff0c;验证了其在谐波抑制方面的效…

从RNN讲起(RNN、LSTM、GRU、BiGRU)——序列数据处理网络

文章目录 RNN&#xff08;Recurrent Neural Network&#xff0c;循环神经网络&#xff09;1. 什么是RNN&#xff1f;2. 经典RNN的结构3. RNN的主要特点4. RNN存在问题——长期依赖&#xff08;Long-TermDependencies&#xff09;问题 LSTM&#xff08;Long Short-Term Memory&a…

PostgreSQL学习笔记七:常规SQL操作

PostgreSQL 支持标准的 SQL 语句&#xff0c;同时也扩展了一些特有的功能。以下是一些常规的 SQL 语句示例&#xff0c;这些示例涵盖了数据定义、数据操作和数据查询的基本操作&#xff1a; 数据定义语言 (DDL 创建数据库&#xff1a; CREATE DATABASE mydatabase;创建表&#…

stm32单片机个人学习笔记9(TIM输入捕获)

前言 本篇文章属于stm32单片机&#xff08;以下简称单片机&#xff09;的学习笔记&#xff0c;来源于B站教学视频。下面是这位up主的视频链接。本文为个人学习笔记&#xff0c;只能做参考&#xff0c;细节方面建议观看视频&#xff0c;肯定受益匪浅。 STM32入门教程-2023版 细…

AWD入门

一、简介 AWD(Attack With Defense&#xff0c;攻防兼备)模式。你需要在一场比赛里要扮演攻击方和防守方&#xff0c;攻者得分&#xff0c;失守者会被扣分。也就是说攻击别人的靶机可以获取 Flag 分数时&#xff0c;别人会被扣分&#xff0c;同时你也要保护自己的主机不被别人…

Docker 教程四 (Docker 镜像加速)

Docker 镜像加速 国内从 DockerHub 拉取镜像有时会遇到困难&#xff0c;此时可以配置镜像加速器。 目前国内 Docker 镜像源出现了一些问题&#xff0c;基本不能用了&#xff0c;后期能用我再更新下。* Docker 官方和国内很多云服务商都提供了国内加速器服务&#xff0c;例如…

Python网络爬虫入门指南

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

MPA-SVM多变量回归预测|海洋捕食者优化算法-支持向量机|Matalb

目录 一、程序及算法内容介绍&#xff1a; 基本内容&#xff1a; 亮点与优势&#xff1a; 二、实际运行效果&#xff1a; 三、算法介绍&#xff1a; 四、完整程序下载&#xff1a; 一、程序及算法内容介绍&#xff1a; 基本内容&#xff1a; 本代码基于Matlab平台编译&am…

2024年网络安全进阶学习路径-2024年进阶学习指南

&#x1f91f; 基于入门网络安全/黑客打造的&#xff1a;&#x1f449;黑客&网络安全入门&进阶学习资源包 前言 什么是网络安全 网络安全可以基于攻击和防御视角来分类&#xff0c;我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”、…

KDD 2024论文分享┆用于序列推荐的数据集再生

论文简介 本推文介绍了2024 KDD的最佳学生论文《Dataset Regeneration for Sequential Recommendation》。该论文提出了一种基于数据中心化范式的新框架&#xff0c;称为DR4SR&#xff0c;该框架通过模型无关的数据再生机制&#xff0c;能够生成具有出色跨架构泛化能力的理想训…

git(版本回退,分支管理,vscode集成git)

一、安装与简单命令 1.官网 https://git-scm.com/downloads 2.查看版本号git --version 3.设置用户签名&#xff08;用户名和邮箱&#xff09; 用来标识用户&#xff0c;以区分不同的开发人员 git config --global user.name "Your Name" git config --global u…

2024年最新算法:青蒿素优化算法(Artemisinin Optimization Algorithm, AOA)原理介绍

青蒿素优化算法&#xff08;Artemisinin Optimization Algorithm, AOA&#xff09;是2024年提出的一种受青蒿素抗疟疾特性启发的元启发式优化算法。青蒿素是一种从中草药青蒿中提取的化合物&#xff0c;因其在治疗疟疾方面的显著效果而闻名。AOA算法的设计者将青蒿素的这一特性…

【机器学习】深入浅出讲解贝叶斯分类算法

0. 前言 1.贝叶斯分类器介绍 贝叶斯分类是一类分类算法的总称&#xff0c;这类算法均以贝叶斯定理为基础&#xff0c;故统称为贝叶斯分类。而朴素贝叶斯&#xff08;Naive Bayes&#xff09;分类是贝叶斯分类中最简单&#xff0c;也是常见的一种分类方法。 一些很常见的分类…