模拟实现一个简单的命令行解释器(shell)

news2024/9/30 11:29:37

目录

前言

环境变量与本地变量

和环境变量相关的命令

获取环境变量的三种方法

 第一种

 第二种

第三种

 进程地址空间

页表

 为什么存在进程地址空间

第一

第二

 第三

进程控制

进程的产生

进程终止

进程等待

进程替换

模拟实现一个shell


前言

我们通过各种指令来实现对操作系统进行各种操作,这些指令本质上和我们写的可执行程序并没有区别,当然我们也可以实现一个类似于shell的命令行解释器。

环境变量与本地变量

上一篇博客中已经简单的讲解了环境变量怎么修改,怎么添加。

这里要引进另一个概念,本地变量。

 

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
    }
    else if(id == 0)
    {
        sleep(1);
        printf("这是子进程:pid : %d ppid : %d  | %s\n",getpid(),getppid(),getenv("MY_ENV"));
    }
    else
    {

        sleep(1);
        printf("这是父进程:pid : %d ppid : %d  | %s\n",getpid(),getppid(),getenv("MY_ENV"));                                                        
    }
    return 0;
}

从这里可以看出,本地变量完全是独立的,只在本进程内有效(bash),不能被子进程使用。

但是当本地变量被添加到环境变量中时,由于环境变量具有全局属性,可以被子进程所使用继承。

和环境变量相关的命令

1. echo: 显示某个环境变量值

2. export: 设置一个新的环境变量

3. env: 显示所有环境变量

4. unset: 清除环境变量

5. set: 显示本地定义的shell变量和环境变量

获取环境变量的三种方法

mian函数是可以带参数的——命令行参数。

int  amin(int argc,char* argv[])

argc是表示命令行元素的数量,argv则是将一个长字符串改成一个个短字符串写入argv,数组最后一个元素为NULL表示结束。

 

 除了命令行参数,main函数还有一个参数:

int  amin(int argc,char* argv[], char* env[])

env[]内部的内容与argv[]比较相似:

 第一种

就是通过命令行第三个参数来获得环境变量

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
int main(int argc,char* argv[], char* env[])
{

    for(int i = 0; env[i]; ++i)
    {
        printf("i : %d ->  %s\n", i, env[i]);                                                                                                      
    }
                                                                                                                  
    return 0;
}

 第二种

通过第三方变量environ获取

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
int main(int argc,char* argv[], char* env[])
{
    exturn char** environ;

    for(int i = 0; environ[i]; ++i)                                                           
    { 
        printf("i : %d ->  %s\n", i, environ[i]);                                                                                         
    }                                                                                                            
    return 0;
}

运行结果与第一种相同。

第三种

通过getenv()函数获取环境变量:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
int main()
{

    printf("%s\n", getenv("PATH"));                                                                                                                  
    return 0;
}

 进程地址空间

我们先引入一个现象来了解进程地址空间:

这时候问题产生了,子进程和父进程中n的地址是一样的,但是值却不一样,所以这个地址指的不是物理地址,也就是说子进程和父进程的内存空间是虚拟的,读取数值相同的地址时,读的并不是同一块内存。

这时候我们必须接受一个概念,那就是虚拟地址空间,也就是说,进程中的地址和物理地址并不是一回事。

我们要控制进程是依托于PCB的,在PCB中有一个mm_struct就是用来分配空间的,其中code_start和code_end等是用来表示进程地址空间相应区域的起始地址和结束地址,以32位系统为例:

页表

代码和数据由磁盘加载到内存中,内存和磁盘的数据交互的过程叫做IO,并且基本单位是4KB。

进程运行之后,在mm_struct中记录着进程地址空间的分配,页表的作用就是将进程地址空间,也就是虚拟地址和物理地址作一个映射。通过页表,进程可以使用内存。

每一个进程都有一个单独的页表:

 为什么存在进程地址空间

我们从三个方面来阐述:

第一

当然是因为安全问题,如果进程可以直接访问物理地址,那么完全有可能发生越界访问。页表的另一个作用就是防止进程访问不属于它的空间。

第二

地址空间的存在可以使进程和进程的数据代码解耦,保证了进程独立性这样的特质。

因为进程具有独立性,当一个进程对被共享的数据进行修改,不能影响到其他进程。通过写时拷贝可以实现这一特性。

 当父进程分出子进程时,两个进程分享数据和代码,这时两个进程所使用的数据在物理内存上是同一块,当有一个进程要对数据进行写入或者修改时,系统会进行数据拷贝,更改页表映射,再修改数据,这个过程被称为写时拷贝。

 第三

让进程以统一的视角来看待对应的代码和数据及各个区域,方便编译器也以统一的视角编译代码。

要了解这句话,我们需要接受这么几个概念:

1.在磁盘上的可执行文件(没有被加载到内存上)是有逻辑地址空间的,在我们对代码进行反汇编时可以看出来。

2.虚拟地址空间是系统和编译器都要遵守的规则。

3.当程序加载到内存中之后就有了一个天然的物理地址。

 可执行程序中有虚拟地址空间,和进程地址空间是同一套,也就是说在编译阶段就把代码和常量数据在进程地址空间中的位置确定了。当可执行程序从磁盘中被加载到内存中,有一个物理地址被填到页表的右侧,这时由于程序内部已经有代码的虚拟地址了,直接填入页表左侧。cpu通过PCB访问内存,也就是说CPU从头到尾不接触物理内存。

进程控制

进程的产生

聊到进程控制我们会遇到一个怎么也绕不过去的函数:

fork()

   那为什么在代码中会有两个返回值呢?我们要了解到,一个函数在运行到return之前,主要的功能都已经实现了。也就是说在运行返回值时子进程已经产生了,并且由于写时拷贝此时id值不一样就可以解释了。

进程终止

进程退出场景:

代码运行完毕,结果正确

代码运行完毕,结果不正确

代码异常终止

正常终止(运行完毕)有三种方式:

1. 从main返回

2. 调用exit

3. _exit

在命令行可以通过 echo $? 查看进程退出码

 exit是库函数,_exit是系统调用接口,那么两者之间有什么区别:

我们先来看一下代码:

 再看运行结果:

再将exit修改为_exit,再看结果:

很明显地看到_exit并没有打印hello  word!!!,数据丢失,这是由于exit比_exit多做了一件事,就是将缓冲区中的数据进行IO.这也说明了缓冲区是用户级的,如果缓冲区是在操作系统内预留了,那么_exit也应该可以对缓冲区进行刷新才对。 

进程等待

进程等待必要性 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。

另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法 杀死一个已经死去的进程。

最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。

父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

进程等待的方法:

 我们重点讲一下waitpid():

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

返回值:

        当正常返回的时候waitpid返回收集到的子进程的进程ID;

        如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;

        如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数:

        pid:

                Pid=-1,等待任一个子进程。与wait等效。 Pid>0.等待其进程ID与pid相等的子进程。

        status:

                WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)

                WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

        options:

                 WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进 程的ID。

wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。

如果传递NULL,表示不关心子进程的退出状态信息。

否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特 位):

 options参数传递0,表示如果子进程还没结束则阻塞等待。

进程返回先看终止信号,如果都为零说明无异常,程序运行结束,否则程序异常退出。如果无异常看退出状态推断任务完成情况。

 

第一个是正常结束(终止信号为0),第二个是进程异常 (终止信号不为0)。

进程替换

在此之前我们写的代码,子进程是通过if条件判断执行父进程代码的一部分,但是如果我们想要让子进程执行全新的代码呢?

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数 以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动 例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

替换函数

这些都是库函数,但其实更底层是系统调用接口:

函数解释: 

这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。

如果调用出错则返回-1

所以exec函数只有出错的返回值而没有成功的返回值。

 这些函数原型看起来很容易混,但只要掌握了规律就很好记。

l(list) : 表示参数采用列表

v(vector) : 参数用数组

p(path) : 有p自动搜索环境变量

PATH e(env) : 表示自己维护环境变量

我们通过第一种和最后一种来练习:

我们上面是使用系统路径中的指令,接下来让程序替换成我们自己的可执行文件:

 

 值得注意的是putenv()用来添加环境变量。

模拟实现一个shell

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

#define  LINESIZE 1024
#define  ARGNUM 64


int main()
{
    char* myarg[ARGNUM];
    char  command[LINESIZE];

    while(1)
    {
        printf("[用户名@ 主机名 当前地址#] ");
        fflush(stdout);
        //接受指令  分割指令
        char* c = fgets(command,LINESIZE - 1,stdin);
        assert(c != NULL);                                                                                                                         
        (void)c;
        command[strlen(command) - 1] = 0;
        myarg[0] = strtok(command, " ");
        int i = 1;
        while(myarg[i++] = strtok(NULL," "));
         //执行指令
        pid_t id = fork();
        assert(id != -1);
        if(id == 0)
        {
            int exeret = execvp(myarg[0],myarg);
            if(exeret == -1)
            {
                exit(10);
            }
            exit(1);

        }
        waitpid(id,NULL,0);
    }

    return 0;
}

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

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

相关文章

脚手架2 以nacos为注册中心,基于Gateway构架网关

在前一步上已经说明&#xff0c;组件脚手架的第一步就是构建注册中心&#xff0c;由于采用nacos&#xff0c;这些将直接放在配置文件中实现&#xff0c;不再单独搭建eureka。 spring nacos jdk各组件依赖版本推荐 Spring Boot&#xff0c;Spring Cloud&#xff0c;Spring Clo…

HTC Cosmos手柄的坑

HTC Cosmos手柄的坑Unreal蓝图通过手柄射线操作UI用浏览器插件进行游戏界面设计Cosmos手柄遇到的问题Unreal蓝图通过手柄射线操作UI Unreal蓝图通过手柄射线操作UI很简单&#xff0c;虚幻提供一个WidgetInteraction的组件&#xff0c;可以模拟键盘和鼠标操作。 Enable Hit Te…

【最优潮流】二阶锥松弛在配电网最优潮流计算中的应用(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

组件化 – 额外知识补充

1、组件的生命周期 1.1、认识生命周期 什么是生命周期呢&#xff1f; 生物学上&#xff0c;生物生命周期指得是一个生物体在生命开始到结束周而复始所历经的一系列变化过程&#xff1b;每个组件都可能会经历从创建、挂载、更新、卸载等一系列的过程&#xff1b;在这个过程中…

升级 Node 版本教程

【node】升级 Node 版本教程 文章目录 Window 系统Mac 或 Linux系统 Window 系统 window系统升级node只能到node官网下载window安装包来覆盖之前的node。node 安装教程附下载地址&#xff1a;https://blog.csdn.net/qq_45677671/article/details/114535955因为 n 模块是不支…

transformers学习笔记3

创建model的方法automodel创建预训练模型使用automodel直接创建&#xff0c;使用的是hug face官网上的预训练好的模型参数&#xff0c;完成创建后再使用自己本地的数据集进行迁移训练automodel api 去官网上下载用户指定类型模型的config file 和model file&#xff0c;config说…

【Rvnc】基于noVnc集成的远程终端管理平台

新年好&#xff01;祝各位小伙伴们新年快乐&#xff01;工资翻倍&#xff01; 项目介绍 项目地址&#xff1a;https://github.com/LogestCai/Rvnc 基于noVNC、C#和easyAdmin快速开发的远程管理平台。 可快速部署企业内部远程管理平台&#xff0c;方便企业运维管理。 安装教程…

I.MX6ULL裸机开发笔记6:GPIO控制原理

目录 一、了解GPIO 二、时钟 三、引脚复用 四、引脚属性 五、控制GPIO总结 六、硬件原理图 一、了解GPIO 数量 5组GPIO&#xff08;GPIO1~GPIO5&#xff09;,每组最多32个&#xff0c;共124个 GPIO1_IO0——GPIO1_IO31GPIO2_IO0——GPIO2_IO21GPIO3_IO0——GPIO3_IO2…

#C. wll 的糖果分配

说明过年啦&#xff01;wll 带着好多好多的糖果回到家里&#xff0c;打算分给弟弟妹妹们她一共带回了 66 种不同的糖果&#xff0c;第 ii 种糖果的美味度为 ii&#xff0c;共有 a_iai 颗但是弟弟们和妹妹们不想在一起玩&#xff0c;他们想分别拿走糖果&#xff0c;各自玩耍那么…

物联网到底是什么,生活中能用得上吗?

物联网在近些年以来一直都是热点&#xff0c;人人都在提物联网。但物联网到底是什么&#xff1f;究竟能做什么&#xff1f;说起物联网&#xff0c;你是不是感到既熟悉又陌生&#xff1f;没错&#xff0c;从随处可见的射频技术&#xff0c;智能穿戴&#xff0c;智能电器&#xf…

Android 系统 Framework 中定制实现开关机动画实践

文章目录写在前面需求背景主要问题接口测试权限问题对比测试最后实现方案其他问题总结写在前面 本文主要记录了在Android 10 系统 定制开关机动画时遇到的权限&#xff08;读写&#xff09;问题以用开关机动画资源的流程、文件要求等问题。 涉及知识点&#xff1a; Linux中文件…

【iOS】—— 工厂设计模式

工厂设计模式 文章目录工厂设计模式设计模式概念设计模式七大准则开闭原则单⼀职责原则里氏替换原则依赖倒转原则接口隔离原则迪米特法则合成复用原则类族模式简单工厂模式优点缺点主要作用示例文件分类实现效果&#xff1a;工厂方法模式优点缺点主要作用&#xff1a;示例&…

Ceres 目标函数(pose_graph_3d使用之)构建学习笔记

问题说明 ceres-solver库是google的非线性优化库&#xff0c;可以对slam问题&#xff0c;机器人位姿进行优化&#xff0c;使其建图的效果得到改善。pose_graph_3d是官方给出的二维平面上机器人位姿优化问题&#xff0c;需要读取一个g2o文件&#xff0c;运行程序后返回一个pose…

Android 课设之个人音乐播放器

第一章 绪论1.1选题背景由于时代快速发展&#xff0c;各种各样的音乐播放器层出不穷&#xff0c;此时需要一个可以根据个人爱好来播放的音乐播放器就尤为重要&#xff0c;因此我特意制作了一个根据自己喜好的音乐播放器&#xff0c;只需要把音乐文件放进制定的目录下即可。1.2开…

C++语法小笔记:内联函数,auto关键字,nullptr

目录 一.内联函数 1.回顾c语言中的“宏函数” 2.内联函数 3.内联函数的特性 二.C auto 关键字 1.auto的基本概念 2.auto使用的注意事项 3.auto不能使用的地方 三. C11中的 nullptr 一.内联函数 1.回顾c语言中的“宏函数” 先给出一段简单的代码&#xff1a; int Add(in…

plt设置柱状图标注

1、plt.text方法 在matplotlib 3.4.0之前的版本中&#xff0c;一般使用plt.text方法绘制数据标签。顾名思义&#xff0c;plt.text可以在图像的任何地方绘制指定的文本。基于此&#xff0c;我们只需要在相应数据点的坐标位置绘制相应的值&#xff0c;即可显示数据标签。 2、plt.…

react初始高阶组件

首先 我们要了解什么是高阶组件 第一 高阶组件必须是一个函数 第二 高阶组件接收一个参数&#xff0c;这个参数也必须是一个组件 第三 他的返回值 也是一个组件 至于高阶组件的作用 我们后续会讲解 本文只是带大家认识一下高阶组件 并手把手带大家创建一个 下面我们来创建一个…

微服务调用组件Feign学习笔记

目录 JAVA 项目中如何实现接口调用&#xff1f; 1. 什么是Feign 2. Spring Cloud Alibaba快速整合OpenFeign 3. Spring Cloud Feign的自定义配置及使用 4.自定义拦截器 5.超时时间配置 JAVA 项目中如何实现接口调用&#xff1f; 1&#xff09;Httpclient HttpClient 是 …

数据结构(模式匹配及相关算法)

目录 模式匹配 BF算法 算法实现 算法分析 KMP算法 问题的引入&#xff08;一&#xff09; 问题的引入&#xff08;二&#xff09; 问题的引入&#xff08;三&#xff09; 相关概念 计算失配函数的算法 算法思路 算法优点 模式匹配 函数int find(const sstring &am…

机器学习(三):人工智能主要分支

文章目录 人工智能主要分支 一、计算机视觉 二、语音识别 三、文本挖掘/分类 四、机器翻译 五、机器人 人工智能主要分支 通讯、感知与行动是现代人工智能的三个关键能力&#xff0c;在这里我们将根据这些能力/应用对这三个技术领域进行介绍&#xff1a; 计算机视觉(CV…