news 2026/4/16 21:25:18

【C++与Linux基础】进程篇 - 改进Shell,完成内建命令

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【C++与Linux基础】进程篇 - 改进Shell,完成内建命令

本系列主要旨在帮助初学者学习和巩固Linux系统。也是笔者自己学习Linux的心得体会。

个人主页:爱装代码的小瓶子
文章系列:Linux
2.C++

文章目录

  • 1. 回忆上一篇文章我们所写的Shell:
  • 2.前置知识点准备:
    • 2-1.外部命令和内嵌命令 :
    • 2-2`getenv("PWD")` 和 `getcwd()` 的区别
    • 2-3 环境变量表和命令行表的维护
      • 理由一:“父传子”的继承机制 (为了子进程)
        • 理由二:保持“逻辑状态”与“物理状态”的一致 (为了 cd)
        • 理由三:配置的持久化 (为了 export)
  • 3. 开始行动 :改进shell:
    • 3-1 开始构建内嵌命令:
    • 3-2 重写`GetPwd()`
    • 4. 维护系统变量表:
  • 总结:

1. 回忆上一篇文章我们所写的Shell:

【C++与Linux 基础】进程篇 -还在怕进程控制?带你用C++手写一个简易Shell里面已经写过了简单的shell程序,但是可惜的事情,我们并没有完成对内嵌命令的支持。你可以尝试使用cd和echo,你会发现他是不是支持的,还要export命令他也是不支持的。这是因为我们没有在shell内部去实现。

为了后面的Shell更容易理解,这里我们还是先讲一些知识点,当你看完这个就能知道:

  1. 外部命令 (External Command)和内嵌命令 (Built-in Command)。
  2. PWD vs getcwd,两者有何区别?
  3. 环境变量表和命令行表的维护。

2.前置知识点准备:

2-1.外部命令和内嵌命令 :

引入:我们可以看一下这个场景,当我们运行我们自己的程序的时候。输入cd + 指定的地址,结果查询pwd,发现所处的路径没有发生改变。

这并不是你的错误,而是我们之前的程序没有实现这个错误。

我们来想一想上面这个场景为什么会发生这个问题?

intExecute(){pid_t id=fork();if(id==0){//childexecvp(g_argv[0],g_argv);exit(1);}//父亲开始等待pid_t rid=waitpid(id,nullptr,0);(void)rid;return0;}

当我们我们获取cd这个命令的时候,我们利用子程序来完成这个命令,的确子进程改变了自己的位置,但是父进程并没有改变。当子程序执行被execvp里面的替换执行后,他就自己退出了,并不能影响或者改变shell这个进程。

由此,我们可以引入什么是内嵌命令,什么是外部命令,什么是内嵌命令(也叫内建命令)是指直接包含在 Shell 程序本身内部的指令,而不是磁盘上的某个独立的可执行文件。

  • 外部命令 (External Command): 当你输入 ls 或 vim 时,Shell 会去磁盘的 /bin 或 /usr/bin 目录下寻找对应的程序,然后创建一个新的子进程 (fork) 来运行它。
  • 内嵌命令 (Built-in Command): 当你输入 cd 或 echo 时,Shell 不会创建子进程,而是直接调用 Shell 程序内部的一个函数来执行。

我们再来说说为什么echo他也是内嵌命令:其实很多像echo命令一样,它既是内嵌命令,也是外部命令,他有两个实现,但是其实主要还是内嵌命令,虽然 /bin/echo 也是存在的,但 Shell 通常默认使用内建的 echo。

  • 性能原因: 创建进程 (fork + exec) 的开销很大。echo 通常只打印简短的字符串,如果为了打印几个字就启动一个新进程,属于“杀鸡用牛刀”,效率极低。
  • 方便访问 Shell 变量: 内建 echo 可以更方便地处理 Shell 内部的特殊变量和转义字符。

还要export,这个必须导入我们自己实现的shell中,如果导入子程序中,后面他还是会结束的,并不能作用在父进程上面,同样也需要内嵌实现。

2-2getenv("PWD")getcwd()的区别

在 C 程序中,这两者都可以用来获取“当前工作目录”,但来源、可靠性、实现方式和适用场景完全不同。

我们先来说说getenv("PWD")这个就是我们在上一版在用的,代码如下:

constchar*GetPwd(){char*name=getenv("PWD");returnname==nullptr?"None":name;}
  • 来源:它是去查环境变量表,读取名为PWD的那个变量的值。
  • 本质:这是一个字符串读取操作。
  • 缺点:它是不可靠的。
    • 环境变量是可以被用户或脚本随意修改的(例如export PWD=/tmp,即使你人不在/tmp)。
    • 如果你通过硬链接跳转或者程序崩溃导致环境变量没更新,这个值可能就是错的。
    • 它只是 Shell 维护的一个“逻辑状态”。

比如如果程序用chdir()改变了目录但没有更新环境变量 PWD,它会返回旧值。提前剧透:我们需要用chdir()这个函数。

再来看看getcwd(): (Get Current Working Directory)

  • 来源:这是一个系统调用 (System Call)。它直接询问操作系统内核:“我现在到底在哪里?”
  • 本质:内核会去查该进程的文件系统描述符,根据 inode 信息反向推导出绝对路径。
  • 优点:它是绝对真实的物理状态。
  • 缺点:相比读取内存中的变量,系统调用的开销稍大(微乎其微)

这个就是准确的,在后序的实现中,我们需要实现,并且使用。

2-3 环境变量表和命令行表的维护

我们来看看我们上一个程序,其中有一行注释是错误的,在上一行里面,我们只维护了一个命令行表,为了让我们的shell支持自己环境变量表,这个也是需要我们维护的。
为什么需要维护,其实结合上面的你就可以理解了为什么需要维护环境变量表了:

理由一:“父传子”的继承机制 (为了子进程)

这是最重要的原因。Shell 本身不仅仅是用来运行的,它是用来启动别人的。

  • 场景: 你在你的 Shell 里启动了vim
  • 过程:fork()-> 子进程继承父进程的环境变量 ->exec
  • 如果你不维护:
    • 假设用户改了PATH(export PATH=/my/custom/bin:$PATH),但你没有更新环境变量表。
    • 然后用户输入mytool(这个工具在/my/custom/bin下)。
    • 结果: 你的 Shell 找不到它,或者即便你找到了,启动的子进程如果内部想调用同目录下的其他脚本,也会失败。
    • 再比如: 你没维护TERM变量。用户启动vimvim发现环境变量里没有TERM,它不知道屏幕多大,不知道支持什么颜色,直接报错退出或者变成黑白模式。
理由二:保持“逻辑状态”与“物理状态”的一致 (为了 cd)

这是我们在上一个问题里讨论的PWD问题。

  • 物理状态: 内核里的cwd(Current Working Directory)。
  • 逻辑状态: 环境变量里的PWD
  • 为什么不同步很危险:
    • 有些程序(比如ls显示超链接,或者一些构建工具make)并不直接调系统调用getcwd,而是为了快直接读$PWD
    • 如果你用chdir走了,却没改PWD变量,这些工具就会以为你还在原来的地方,导致逻辑错误(比如生成的文件路径不对)。
理由三:配置的持久化 (为了 export)

用户在 Shell 里最常用的操作之一就是配置环境。

  • 用户操作:export JAVA_HOME=/usr/local/java
  • 你的任务: 用户希望从此以后,在这个 Shell 里启动的所有 Java 程序都能读到这个路径。
  • 如果你不维护:
    • 用户敲了命令,你没把它写进environ表。
    • 用户紧接着输入java -version
    • Java 程序启动了,去读JAVA_HOME,发现是空的。
    • 结果: 用户会觉得你的 Shell 是坏的,“我明明设置了环境变量,为什么不生效?”

3. 开始行动 :改进shell:

3-1 开始构建内嵌命令:

我们之前的主函数的执行顺序如下:

  1. 打印前置的格式
  2. 获取用户输入的命令。
  3. 分析用户输入的指令
  4. 利用子进程来完成执行。

我们只需在第三部和第四步骤之间加上检查是不是内嵌命令就可以了。我们先来完善内嵌函数命令的检查:

boolCheckBuildIn(){std::string cmd=g_argv[0];if(cmd=="cd"){cd();returntrue;}elseif(cmd=="echo"){echo();returntrue;}//目前只完成这两个,暂时做个示例:returnfalse;}

我这里只完成了者两个,其他的类似于export可以自己尝试完善。我们这里只要是echo和cd都是由shell本身这个父进程来完成。

我们先来看看什么cd命令是怎么实现的:

boolcd(){if(g_argc==1){//说明就只有一个cdstd::string home=GetHome();if(home.empty())returntrue;chdir(home.c_str());}else{std::string where=g_argv[1];if(where=="-"){//todo}elseif(where=="~"){//todo}else{chdir(where.c_str());}}returnfalse;}

其中chdir是改变目录的系统调用函数,可以帮助进程改变位置。这样我们就可以完成执行cd命令。

我们可以看到,这个pwd是发生了改变。但是打印的这个还是没有变化。这个需要后面才能解决。

接下来,我们解决echo命令,这个主要是打印和查询环境变量和退出码:

voidecho(){if(g_argc==2){std::string opt=g_argv[1];if(opt=="$?"){//查询最近一次的退出码std::cout<<lastcode<<std::endl;lastcode=0;//打印完成就设置为0}elseif(opt[1]=='$'){std::string env_name=opt.substr(1);constchar*env_value=getenv("env_name.c_str()");std::cout<<env_value<<std::endl;}else{std::cout<<opt<<std::endl;}}}

除了这个上面的代码,我们还需要设置一个全局变量:最近任务的退出码。注意的是我这里的退出码就只争对了子进程(不是内嵌命令)。那么对于执行函数也是需要发生改变的。

intExecute(){pid_t id=fork();if(id==0){//childexecvp(g_argv[0],g_argv);exit(1);}//父亲开始等待intstatus=0;pid_t rid=waitpid(id,&status,0);if(rid>0){lastcode=WEXITSTATUS(status);//运行程序的退出码。}return0;}

这样就完成了查询最近的退出码,看看结果是怎么样的?

可以看到还打印了一个$?,这是怎么回事?这是主函数出了问题:致命错误:内建命令执行完后,没有“拦住”外部命令。当你知道echo是内嵌命令,但是后面还是利用子进程打印了这个$?.所以后面还是需要改变的:


重新改变之后,我们可以看到是没有发生变化的。这是没有问题的。改变之后的主函数如下:

intmain(){while(true){//1.先打印出一个类似于[user @域名 当前pwd]://需要什么变量:user 和 localhost 还有pwd,这些都在环境变量表里面PrintCommand();//2. 获取用户命令:charcommandline[COMMAND_SIZE];GetCommandLine(commandline,sizeof(commandline));//3.分割命令:CommandParse(commandline);//4 检查是不是内嵌命令if(CheckBuildIn())continue;//5.利用子进程来执行:Execute();}

这样就问题不大了。

3-2 重写GetPwd()

我们在上面的图片,其实也可以看到pwd一直不变化,这里我们来尝试改写,让他随着cd命令变化而变化。随后解释为什么之前不变化。

新增两个暂时的表:

charcwd[1024];charcwd_env[1048];

利用两个表,来完成获取pwd,当cd变化的时候,get的值也会发生变化:

constchar*GetPwd(){char*c=getcwd(cwd,sizeof(cwd));if(c!=nullptr){snprintf(cwd_env,sizeof(cwd_env),"PWD=%s",cwd);//把cwd里面的地址给环境表putenv(cwd_env);//环境表加入系统环境变量}returnc==nullptr?"None":c;}

这个“顺手”发生在你的 main 循环里。请看整个流程:

  1. 用户输入 cd /tmp
    你的 CheckBuildIn -> Cd -> chdir(“/tmp”) 被执行。
    此时此刻:物理目录变成了 /tmp,但环境变量表里的 PWD 还是 /home/wwh。状态是不一致的!
    但是没关系,程序继续往下跑。

  2. while(true) 循环进入下一轮
    代码执行到第一行:PrintCommand()。

  3. 打印提示符
    PrintCommand 想要打印 [wwh@host /tmp]#,它必须知道当前在哪。
    于是它调用了 GetPwd()。

  4. 触发同步(Magic Happens Here)
    GetPwd 一运行,马上执行了 getcwd(拿到 /tmp)。
    紧接着,它执行了 putenv。
    就在这一瞬间,环境变量表里的 PWD 被修正成了 /tmp。


只要你调用 GetPwd()(比如在打印提示符的时候),它就会顺手把环境变量 PWD 给修正了。保证了显示出来的路径和实际所在的路径永远是一致的。

4. 维护系统变量表:

voidInitEnv(){externchar**environ;memset(g_env,0,sizeof(g_env));g_envs=0;//本来要从配置文件来//1. 获取环境变量for(inti=0;environ[i];i++){// 1.1 申请空间g_env[i]=(char*)malloc(strlen(environ[i])+1);strcpy(g_env[i],environ[i]);g_envs++;}g_env[g_envs++]=(char*)"HAHA=for_test";//for_testg_env[g_envs]=NULL;//2. 导成环境变量for(inti=0;g_env[i];i++){putenv(g_env[i]);}environ=g_env;}

这个还是利用C语言的malloc来完成的,我就不在我的shell里面实现了。

总结:

我们今天认识了三个前置知识点,完成对shell的小改进,也许还是不太像真正的shell,但是也是很辛苦各位了
代码已近资源绑定。本文写的也不算很好,但是我一进尽力了,还希望大家能多点点赞。
这段代码不仅仅是一个简易的 Shell 解释器,更是一次向 Linux 内核深处进发的孤独探险。
从最初面对空白光标的迷茫,到亲手用fork刻画出父子进程的离合,用exec完成灵魂的替换;我们曾在字符串解析的碎片中迷失,在野指针的边缘试探,更在环境变量与物理路径的“精神分裂”中反复挣扎。这一路,是为了让cd不再只是一个命令,而是进程状态的真实跳动;是为了让$?不再只是一个符号,而是父进程对子进程最负责的守望。这几百行代码,凝结的不仅是逻辑的闭环,更是你终于读懂了操作系统那沉默而精密脉搏后的豁然开朗——原来每一个闪烁的提示符背后,都站着一个不断修正自我、维护秩序的灵魂。

感谢各位对本篇文章的支持。谢谢各位点个三连吧!



版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 10:57:56

Kubernetes 集群运维:故障排查、资源调度与高可用配置

第一部分&#xff1a;Kubernetes 故障排查方法论系统化故障诊断框架有效的Kubernetes故障排查需要建立系统化的诊断框架&#xff0c;这一框架应当遵循从外到内、自上而下的逻辑顺序。根据Google SRE&#xff08;Site Reliability Engineering&#xff09;方法论&#xff0c;故障…

作者头像 李华
网站建设 2026/4/16 10:42:37

聚焦前沿科技:博士后高级研究人才在多传感器融合定位与机器人智能控制领域的机遇与挑战

天津滨海高新技术产业开发区人力资源和社会保障局 博士后-高级研究人才(天津市天安博瑞科技有限公司) 职位信息 (一)公司名称: 天津市天安博瑞科技有限公司: 博士后招聘需求:1名 学科、研究方向:人工智能、算法计算 硬件工程师(计算机/电子/通信/自动化等相关专业 (…

作者头像 李华
网站建设 2026/4/16 10:43:25

YOLO26改进策略【Backbone/主干网络】| 替换骨干网络为2023-CVPR LSKNet (附网络详解和完整配置步骤)

一、本文介绍 本文记录的是基于LSKNet的YOLO26骨干网络改进方法研究。 LSKNet利用大核卷积获取上下文信息进行辅助,使模型能够产生具有各种大感受野的多个特征的同时,动态地根据输入调整模型的行为,使网络更好地适应图像中不同物体的检测需求。 本文在YOLO26的基础上配置…

作者头像 李华
网站建设 2026/4/16 14:28:40

YOLO26改进策略【Backbone/主干网络】| CVPR 2024替换骨干网络为 UniRepLKNet,解决大核 ConvNets 难题

一、本文介绍 本文记录的是基于UniRepLKNet的YOLO26骨干网络改进方法研究。UniRepLKNet提出了独特的大核设计能有效捕捉图像特征,在多模态任务中展现出强大的通用感知能力。将UniRepLKNet应用到YOLO26的骨干网络中,提升YOLO26在目标检测任务中的精度和效率 。 本文在YOLO26…

作者头像 李华
网站建设 2026/4/16 12:46:27

基于STM32单片机智能快递柜 智能加热 温湿度采集照明控制系统

目录 STM32单片机智能快递柜系统概述智能加热功能温湿度采集模块照明控制系统硬件设计软件设计应用场景 源码文档获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01; STM32单片机智能快递柜系统概述 该系统基于STM32单片机设计&#xff0c;集成智能…

作者头像 李华
网站建设 2026/4/16 11:13:48

浅谈 OpenAI Agents SDK

一、OpenAI Agents SDK是什么&#xff1f; OpenAI Agents SDK是一个轻量级且易于使用的工具包&#xff0c;用于构建基于代理的AI应用程序。 提供了一些基本构建块&#xff0c;包括具备指令和工具的代理&#xff08;Agents&#xff09;、用于代理间任务委托的交接&#xff08;…

作者头像 李华