news 2026/4/18 10:09:29

【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/18 3:43:04

跨端前端开发工程师:技术深度、实践与面试指南

中控技术股份有限公司 前端开发工程师(跨端方向) 职位信息 岗位职责 1、负责公司核心产品的跨端技术架构设计与开发,覆盖Android、iOS、微信/支付宝小程序、H5、Web 网页、鸿蒙(HarmonyOS)等多端场景; 2、主导跨端技术方案选型与落地(如 React Native、Flutter、UniApp、…

作者头像 李华
网站建设 2026/4/18 8:20:18

小额消费贷款产品特征抽取与推荐分析平台的设计与实现开题报告

一、研究背景与意义 &#xff08;一&#xff09;研究背景 随着数字金融的快速发展与居民消费观念的升级&#xff0c;小额消费贷款已成为满足大众短期资金周转、品质消费需求的重要金融工具。近年来&#xff0c;我国小额消费贷款市场规模持续扩大&#xff0c;参与机构不断增多…

作者头像 李华
网站建设 2026/4/18 5:37:59

YOLO26改进策略【Backbone/主干网络】| CVPR 2024 替换骨干网络为 RMT,增强空间信息的感知能力

一、本文介绍 本文记录的是将RMT应用于YOLO26骨干网络的改进方法研究。 RMT通过构建基于曼哈顿距离的空间衰减矩阵,引入显式空间先验,同时提出新的注意力分解形式,在不破坏空间衰减矩阵的前提下,以线性复杂度对全局信息进行稀疏建模。将RMT融入YOLO26的骨干网络,能够有效…

作者头像 李华
网站建设 2026/4/17 13:26:22

新零售行业低代码平台应用实践

在数字化浪潮推动下&#xff0c;新零售行业正经历从“渠道融合”到“全链路智能协同”的转型深水区。线上线下数据割裂、业务流程繁琐、需求迭代滞后、复合型人才短缺等痛点&#xff0c;成为制约企业突破增长瓶颈的核心障碍。低代码平台凭借“可视化搭建、快速迭代、全民参与”…

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

震惊!中国电信AI新神作TeleMem让大模型“开挂“,记忆暴涨38%!小白程序员也能轻松上手!

思维导图曾被证明可以帮助学习障碍者快速提升成绩&#xff0c;那么当前已经可堪一用的智能体系统如果引入类似工具是否可以帮助改善长期学习记忆能力呢&#xff1f;有研究团队做出了探索性尝试。 在长上下文和Agent应用不断落地的过程中&#xff0c;越来越多研究者开始意识到&…

作者头像 李华