深入了解linux系统—— 自定义shell

shell的原理

我们知道,我们程序启动时创建的进程,它的父进程都是bash也就是shell命令行解释器;

bash都做了哪些工作呢?

根据已有的知识,我们可以简单理解为:

  1. 输出命令行提示符
  2. 获取并解析我们输入的指令
  3. 执行内建命令或者创建子进程执行命令

在这里插入图片描述

就如下图所示,bash读取我们输入的命令,并进行解析;然后创建子进程执行命令(bash等待子进程退出)。

在这里插入图片描述

自定义shell实现

根据上述bash的工作原理,我们现在实现一个简单的自定义shell

要想实现一个自定义shell,我们就要执行以下过程:

  • 获取命令行
  • 解析命令行
  • 创建子进程,让子进程执行命令(使用程序替换)
  • shell等待子进程退出

当然,还存在一部分内建命令,它是由bash自主实现的;我们要进行特殊处理;

1. 输出命令行提示符

在实现自定义shell之前,我们来看

在这里插入图片描述

我们的bash在每次都会输出命令行提示符,然后等待我们用户输入;

看这个命令行提示符,它包含以下信息:

  • 用户名USER
  • 主机名HOSTNAME
  • 当前工作路径PWD

这些在我们的环境变量表中都能够找到,所以我们就可以使用getenv来获取。

在这里插入图片描述

所以这个就非常容易实现了,直接按照格式输出即可;

这样我们需要获取环境变量USERHOSTNAMEPWD等;

但是我们会发现bash输出的命令行提示符中的当前工作路径只有当前文件,而我们通过环境变量PWD获取的是当前工作目录的绝对路径,所以我们这里要进行一下分割;

详细代码如下:

//命令行提示符格式
#define CLP "[%s@%s %s]#"
//命令行提示符的最大长度
#define MAX_CLP 100
//获取环境变量
const char* GetUser(){return getenv("USER");
}
const char* GetHostName(){return getenv("HOSTNAME");
}
const char* GetPwd()
{return getenv("PWD");
}
//分割路径
//"/home/lxb/linux/MYSHELL" --> "MYSHELL"
string DirPwd(char s[])
{
#define SLASH "/"string str = s;if(str == SLASH) return str;                                                                       auto pos = str.rfind(SLASH);if(pos == std::string::npos) return "err";return str.substr(pos+1);
}         
//生成命令行提示符
void CommandLinePrompt(char buffer[])
{sprintf(buffer,CLP,GetUser(),GetHostName(),DirPwd(GetPwd());
}
//输出命令行提示符
void PrintCommandPrompt()
{char buffer[100];CommandLinePrompt(buffer);printf("%s",buffer);fflush(stdout);
}

在这里插入图片描述

2. 获取用户输入的信息

输出了命令行提示符,接下来就要获取用户输入的信息了,也就是输入的命令;

在这里插入图片描述

在用户输入时,是会输入空格的,所以这里我们不能使用scanf/cin进行输入;我们要使用fgets进行输入。

而也可能存在只输入一个回车的情况,所以我们要进行特殊判断:当只输入一个回车时就再次输出命令行提示符,然后等待用户输入。

输入:

//命令行信息最大长度    
#define MAX_COMLINE 1024
char* GetCommandLine(char buff[]){char* c = fgets(buff,MAX_COMLINE,stdin);buff[strlen(buff)-1] = 0;//处理回车return c;
}

这里来测试一下输出命令行提示符和获取用户输入信息;

如果获取用户输入信息成功,那就输出获取的输入信息,如果失败或者只输入了一个回车就再次输出命令行提示符,然后等待用户输入。

int main()
{while(1){//1. 输出命令行提示符PrintCommandPrompt();//2. 获取用户输入信息char buff[MAX_COMLINE];char* c = GetCommandLine(buff);if(c == NULL)//读取用户输入信息失败continue;if(strlen(buff) == 0)//只输入了空格continue;printf("%s\n",buff);}return 0;
}

在这里插入图片描述

3. 命令行解析

获取了用户输入的信息,但是我们获得的是一个字符串,而我们要想执行用户输入的命令,要先对这个字符串进行解析;生成对应的命令行参数表,才能够去执行。

命令行参数个数g_argc,命令行参数表g_argv;我们可以设置成全局的,这样每次通过修改argcargv中最后一个指针为NULL即可。

这里,我们可以使用strtok函数进行分割命令行参数;

简单描述一下strtok,在str字符串中查找sep字符串的内容,找到并将其修改成\0并返回指向这个字符串的指针。

在这里插入图片描述

在分割完成之后,我们直接让g_argv命令行参数表指向对应位置即可。

在这里插入图片描述

#define MAX_ARGC 50
//命令行参数表
int g_argc;
char* g_argv[MAX_ARGC];
//解析命令行参数                                                               
//"ls -a -l"--> "ls" "-a" "-l"                                                      
void PrasCommandLine(char buff[]){                                  g_argc = 0;                                          const char* sep = " ";      for(g_argv[g_argc] = strtok(buff,sep);g_argv[g_argc] != NULL; g_argv[g_argc] = strtok(NULL,sep))    g_argc++;    
}   

这里还是测试,命令行解析是否成功。

在这里插入图片描述

4. 创建子进程执行命令

解析命令行,生成命令行参数表之后,现在就是去执行命令了;

我们的shell并不是自己去执行,而是创建子进程,然后让子进程去执行命令,shell等待子进程退出。

void CreateChildExecute(){    int id = fork();    if(id < 0)    {    perror("fork");    exit(1);    }    else if (id == 0){    //child    execvp(g_argv[0],g_argv);    exit(2);    }    //parent    wait(NULL);    
}

这里我们使用的程序替换函数是execvp,我们有命令行参数表(数组),而且我们输入的系统命令是不带路径的;

看一下运行效果:

在这里插入图片描述

扩展部分

在上述描述中,简单的shell运行就OK了;

但是上述我们没有考虑内建命令环境变量表等这些东西;

环境变量表

bash启动时,它的环境变量表从我们系统的配置文件中来,但是我们这里没办法从系统配置文件中读;所以我们这里就只能从父进程bash获取环境变量表;

这里即从bash中获取环境变量;

但是拿到了环境变量表,进程中还是保存的来自父进程bash的环境变量;environ还是执行bash的环境变量表。

我们需要导出环境变量,使用putenv来导出环境变量;然后让environ执行我们的环境遍历表。

//环境变量表最大数量
#define MAX_GENV 500
int g_argc;
char* g_argv[MAX_GARGC];
//环境变量表
int g_envs;    
char* g_env[MAX_GENV];    
//导入环境变量    
void EnvInit(){      extern char** environ;    memset(g_env,0,sizeof(g_env));    g_envs = 0;                              //环境变量表要从系统文件中来             //这从bash中获取    for(int i = 0;environ[i]!=NULL;i++){    g_env[i] = (char*) malloc(strlen(environ[i])+1);    if(g_env[i] == NULL){    perror("malloc");    exit(3);    }    strcpy(g_env[i], environ[i]);    g_envs++;    }    g_env[g_envs] = NULL;    //导出环境变量    for(int i = 0;i < g_envs;i++){    putenv(g_env[i]);    }    environ = g_env;                                                                                                                                                                              
}

在我们程序启动时,从父进程bash获取环境变量即可。

内建命令

内建命令,指bash不创建子进程去执行,而是bash自己去执行的命令;

我们现在知道内建命令有cdexportecho等。

cd

cd命令,仔细想一想,肯定不会是子进程执行的;因为子进程执行它修改的是子进程的工作路径。

我们要让shell去执行cd命令,肯定不能使用程序替换了,我们可以使用chdir系统调用来修改当前工作路径;

在这里插入图片描述

cd命令:

  1. cd:会进入用户的家目录
  2. cd ~:进入用户的家目录
  3. cd where:进入指定路径
  4. cd -:进入上次的工作路径
void CD(){std::string oldpwd = getenv("PWD");std::string where;if(g_argc == 1){where = GetHome();if(where.empty()) return;chdir(where.c_str());                                                                                                                                                                     }else{where = g_argv[1];if(strcmp("-", g_argv[1]) == 0){where = getenv("OLDPWD");}else if(strcmp("~", g_argv[1]) == 0){where = GetHome();if(where.empty()) return;}chdir(where.c_str());//修改环境变量}
}

当然呢,这里存在一个问题,当我们cd -进入上次各种目录时就会发现,它进入的一直都是同一个目录;

因为我们这里没有修改环境变量OLDPWD

echo

echo命令也是内建命令,我们知道,echo $?可以查看最近一次进程退出时的退出码;

但是在我们的shell中,如果让子进程去执行echo $?,它则是直接输出$?

在这里插入图片描述

echo $?,查看最近一次进程退出时的退出码;而这些退出码在哪里呢?

肯定不会在子进程中,那就在bash中了;

所以在我们的shell中,我们可以定义一个全局变量,每次执行一次命令就对其进行一次修改。

//最近一次进程退出时的退出码
int last_code;
void Echo(){                                                      if(g_argc == 2){    std::string str = g_argv[1];    if(str == "$?"){    std::cout<<last_code<<std::endl;    }    else if(str[1] == '$'){    std::string env_name = str.substr(1);    const char* s = getenv(env_name.c_str());    if(s)    std::cout<<s<<std::endl;    }    else{    std::cout<<str<<std::endl;    }    }    
}    

这里,设置了last_code,那在每次执行命令之后,都要进行更新last_code

除此之外呢,还有非常多的内建命令,比如exportunset等;这里就不实现了。

别名alias

如果测试我们可以发现,bash支持ll,而我们的shell是不支持的;

我们知道ll是别名,所以如果想要我们shell支持别名,我们就要在shell中新增一张别名表;

然后维护这张别名表,就可以支持ll等指令的别名了。

这里就不实现了,可以使用unordered_map或者map来存储这张别名表。

到这里本篇文章大致内容就结束了;

本篇文章自定义实现shell,帮助理解进程,以及bash是如何工作的

附源码:

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <cstdbool>
#include <sys/types.h>
#include <sys/wait.h>
#include <string>
//命令行提示符格式
#define CLP "[%s@%s %s]# "
#define MAX_CLP 100
//命令行信息最大长度
#define MAX_COMLINE 1024
//命令行参数最大个数
#define MAX_GARGC 50
//环境变量表最大数量
#define MAX_GENV 500
int g_argc;
char* g_argv[MAX_GARGC];
//环境变量表
int g_envs;
char* g_env[MAX_GENV];
//最近一次进程退出时的退出码
int last_code = 0;
//导入环境变量
void EnvInit(){extern char** environ;memset(g_env,0,sizeof(g_env));g_envs = 0;//环境变量表要从系统文件中来                                                                                                                                                                  //这从bash中获取for(int i = 0;environ[i]!=NULL;i++){g_env[i] = (char*) malloc(strlen(environ[i])+1);if(g_env[i] == NULL){perror("malloc");exit(3);}strcpy(g_env[i], environ[i]);g_envs++;                                                                                                                                                                                 }g_env[g_envs] = NULL;//导出环境变量for(int i = 0;i < g_envs;i++){putenv(g_env[i]);}environ = g_env;
}
//获取环境变量
char* GetUser(){return getenv("USER");
}
char* GetHostName(){return getenv("HOSTNAME");
}
//路径切割
std::string DirPwd(const char s[])
{
#define SLASH "/"std::string str = s;if(str == SLASH) return str;auto pos = str.rfind(SLASH);if(pos == std::string::npos) return "err";return str.substr(pos+1);
}
const char* GetPwd()
{//return getenv("PWD");return DirPwd(getenv("PWD")).c_str();
}
const char* GetHome(){return getenv("HOME");                                                  
}
//生成命令行提示符
void CommandLinePrompt(char buffer[])
{sprintf(buffer,CLP,GetUser(),GetHostName(),GetPwd());//sprintf(buffer,CLP,GetUser(),GetHostName(),DirPwd(GetPwd()).c_str());
}
void PrintCommandPrompt()
{char buffer[100];CommandLinePrompt(buffer);printf("%s",buffer);fflush(stdout);
}
char* GetCommandLine(char buff[]){char* c = fgets(buff,MAX_COMLINE,stdin);buff[strlen(buff)-1] = 0;return c;
}
void PrasCommandLine(char* buff){g_argc = 0;const char* sep = " ";for(g_argv[g_argc] = strtok(buff,sep); g_argv[g_argc] != NULL; g_argv[g_argc] = strtok(NULL,sep)){g_argc++;}
}
void CreateChildExecute(){int id = fork();if(id < 0){perror("fork");exit(1);                                                                                                                                                                                  }else if (id == 0){//childexecvp(g_argv[0],g_argv);exit(2);}//parentint status = 0;int rid = wait(&status);if(rid > 0)last_code = WEXITSTATUS(status);
}
void Cd(){std::string oldpwd = getenv("PWD");std::string where;if(g_argc == 1){where = GetHome();if(where.empty()) return;                                                                                                                                                                 chdir(where.c_str());}else{where = g_argv[1];if(strcmp("-", g_argv[1]) == 0){where = getenv("OLDPWD");}else if(strcmp("~", g_argv[1]) == 0){where = GetHome();if(where.empty()) return;}chdir(where.c_str());//修改环境变量}//std::string old = std::string("OLDPWD=") + oldpwd;//char* arr = (char*)malloc(old.size()+1);//for(size_t i = 0;i<old.size();i++){//    arr[i] = old[i];//}//arr[old.size()] = 0;//putenv(arr);
}
void Echo(){if(g_argc == 2){                                                                                                                                                                              std::string str = g_argv[1];if(str == "$?"){std::cout<<last_code<<std::endl;}else if(str[1] == '$'){std::string env_name = str.substr(1);const char* s = getenv(env_name.c_str());if(s)std::cout<<s<<std::endl;}else{std::cout<<str<<std::endl;}}
}
//判断内建命令
bool BinCommand(){std::string str = g_argv[0];if(str == "cd"){Cd();last_code = 0;return true;}else if(str == "echo"){Echo();last_code = 0;return true;}return false;            
}
void PrintArgv(){for(int i = 0;i < g_argc; i++){printf("g_argv[%d] : %s\n",i,g_argv[i]);}
}
void PrintEnv(){for(int i = 0; i < g_envs;i++){printf("g_env[%d] : %s\n",i,g_env[i]);}
}
int main()
{//获取环境变量表EnvInit();//PrintEnv();while(1){                                                                                                                                                                                     //1. 输出命令行提示符PrintCommandPrompt();//2. 获取用户输入信息char buff[MAX_COMLINE];char* c = GetCommandLine(buff);if(c == NULL)//读取用户输入信息失败continue;if(strlen(buff) == 0)//只输入了空格continue;//3. 命令行解析PrasCommandLine(buff);//4.内建命令if(BinCommand())continue;//5. 创建子进程执行命令CreateChildExecute();}return 0;
}

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

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

相关文章

Redux和Vuex

为什么React和Vue需要Redux和Vuex 状态管理需求的演变 #mermaid-svg-GaKl3pkZ82yc1m8E {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GaKl3pkZ82yc1m8E .error-icon{fill:#552222;}#mermaid-svg-GaKl3pkZ82yc1m8E…

Kubernetes排错(十三):Pod间偶发超时问题排查

在微服务架构中&#xff0c;Pod间偶发的通信超时是最令人头疼的问题之一。本文将通过生产环境中的真实案例&#xff0c;手把手教你定位这类"幽灵问题"。 一、快速定位问题方向&#xff08;5分钟缩小范围&#xff09; 1. 基础检查三板斧 # 检查Service与Endpoint映…

Nginx 源码安装成服务

一、环境准备 一台装有 CentOS 7.9 的虚拟机&#xff08;IP: 192.168.40.81&#xff09;nginx-1.21.6.tar.gz 安装包一个&#xff08;版本随意&#xff09; 二、安装 1&#xff09;解压 nginx-1.21.6.tar.gz tar -xzvf nginx-1.21.6.tar.gz -tar&#xff1a;这是一个在 Linu…

L51.【LeetCode题解】438. 找到字符串中所有字母异位词(四种方法)

目录 1.题目 2.分析 暴力解法 方法1:排序(超时) 方法2:哈希表(险过) ★判断两个哈希表是否相同算法(通用方法,必须掌握) 能相等的前提:两个哈希表的大小相等 哈希表有迭代器,可以使用范围for从头到尾遍历 提交结果 优化方法:定长滑动窗口 提交结果 使用哈希数组更快…

Qt模块化架构设计教程 -- 轻松上手插件开发

概述 在软件开发领域,随着项目的增长和需求的变化,保持代码的可维护性和扩展性变得尤为重要。一个有效的解决方案是采用模块化架构,尤其是利用插件系统来增强应用的功能性和灵活性。Qt框架提供了一套强大的插件机制,可以帮助开发者轻松实现这种架构。 模块化与插件系统 模…

深入理解 HashMap 的索引计算:右移与异或的作用

在 Java 中&#xff0c;HashMap 是一种高效的数据结构&#xff0c;它通过将键映射到数组中的索引位置来实现快速的插入和查找。但之前看源码总是理解到它要hash之后散列到数组中某一个位置&#xff0c;但却从未深究它究竟怎么散列的&#xff0c;如果不够散那就意味着hash冲突增…

overleaf较高级的细节指令

换行命令 原来代码是将三个矩阵表达式在同一行显示&#xff0c;使用aligned环境&#xff08;需引入amsmath宏包&#xff0c;一般文档导言区默认会引入&#xff09;&#xff0c;把三个矩阵的定义分别放在不同行&#xff0c;可通过\\换行。 对齐命令 &放在等号前&#xff0…

LiteLLM:统一API接口,让多种LLM模型调用如臂使指

在人工智能迅猛发展的今天,各种大语言模型(LLM)层出不穷。对开发者而言,如何高效集成和管理这些模型成为一个棘手问题。LiteLLM应运而生,它提供了一个统一的API接口,让开发者可以轻松调用包括OpenAI、Anthropic、Cohere等在内的多种LLM模型。本文将深入介绍LiteLLM的特性、…

Google语法整理

以下是从整理出的 Google 语法&#xff1a; site&#xff1a;指定域名&#xff0c;如 “apache site:bbs.xuegod.cn”&#xff0c;可查询网站的收录情况 。 inurl&#xff1a;限定在 url 中搜索&#xff0c;如 “inurl:qq.txt”&#xff0c;可搜索 url 中包含特定内容的页面&a…

python 写一个工作 简单 番茄钟

1、图 2、需求 番茄钟&#xff08;Pomodoro Technique&#xff09;是一种时间管理方法&#xff0c;由弗朗西斯科西里洛&#xff08;Francesco Cirillo&#xff09;在 20 世纪 80 年代创立。“Pomodoro”在意大利语中意为“番茄”&#xff0c;这个名字来源于西里洛最初使用的一个…

Compose Multiplatform iOS 稳定版发布:可用于生产环境,并支持 hotload

随着 Compose Multiplatform 1.8.0 的发布&#xff0c;iOS 版本也引来的第一个稳定版本&#xff0c;按照官方的原话&#xff1a;「iOS Is Stable and Production-Ready」 &#xff0c;而 1.8.0 版本&#xff0c;也让 Kotlin 和 Compose 在移动端有了完整的支持。 在 2023 年 4 …

Jenkins 服务器上安装 Git

安装 Git # 更新包列表 sudo apt update# 安装 Git sudo apt install git 验证安装 # 检查 Git 版本 git --version 查看所有全局配置 git config --global --list 查看特定配置项 # 查看用户名配置 git config --global user.name# 查看邮箱配置 git config --global u…

OpenHarmony SystemUI开发——实现全局导航栏和状态栏关闭

在实际生产中&#xff0c;进场遇到需要关闭导航栏和状态栏的需求&#xff0c;现分享解决办法&#xff1a; 开发环境 OpenHarmony 5.0.0r 代码分析 思路&#xff1a; launcher本身可以关闭 导航栏&#xff08;实际是 公共事件&#xff0c;发送消息给systemUI来实控制&#x…

大模型微调终极方案:LoRA、QLoRA原理详解与LLaMA-Factory、Xtuner实战对比

文章目录 一、微调概述1.1 微调步骤1.2 微调场景 二、微调方法2.1 三种方法2.2 方法对比2.3 关键结论 三、微调技术3.1 微调依据3.2 LoRA3.2.1 原理3.2.2 示例 3.3 QLoRA3.4 适用场景 四、微调框架4.1 LLaMA-Factory4.2 Xtuner4.3 对比 一、微调概述 微调&#xff08;Fine-tun…

单片机-STM32部分:10-2、逻辑分析仪

飞书文档https://x509p6c8to.feishu.cn/wiki/VrdkwVzOnifH8xktu3Bcuc4Enie 安装包如下&#xff1a;根据自己的系统选择&#xff0c;目前这个工具只有window版本哦 安装方法比较简单&#xff0c;都按默认下一步即可&#xff0c;注意不要安装到中文路径哦。 其余部分参考飞书文档…

uniapp-商城-48-后台 分类数据添加修改弹窗bug

在第47章的操作中&#xff0c;涉及到分类的添加、删除和更新功能&#xff0c;但发现uni-popup组件存在bug。该组件的函数接口错误导致在小程序中出现以下问题&#xff1a;1. 点击修改肉类名称时&#xff0c;回调显示为空&#xff0c;并报错“setVal is not defined”&#xff0…

STM32-ADC模数转换器(7)

目录 一、ADC简介 二、逐次逼近型ADC 三、ADC基本结构图 四、规则组的四种转换模式 五、转换时间 对GPIO来说&#xff0c;它只能读取引脚的高低电平&#xff0c;使用了ADC模数转化器之后&#xff0c;就可以对高电平和低电平之间的任意电压进行量化&#xff0c;最终用一个变…

智能商品推荐系统技术路线图

智能商品推荐系统技术路线图 系统架构图 --------------------------------------------------------------------------------------------------------------- | 用户交互层 (Presentation Layer) …

【Docker系列】docker inspect查看容器部署位置

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

标量/向量/矩阵/张量/范数详解及其在机器学习中的应用

标量&#xff08;Scalar&#xff09;、向量&#xff08;Vector&#xff09;、矩阵&#xff08;Matrix&#xff09;、张量&#xff08;Tensor&#xff09;与范数&#xff08;Norm&#xff09;详解及其在机器学习中的应用 1. 标量&#xff08;Scalar&#xff09; 定义&#xff1…