本系列主要旨在帮助初学者学习和巩固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更容易理解,这里我们还是先讲一些知识点,当你看完这个就能知道:
- 外部命令 (External Command)和内嵌命令 (Built-in Command)。
- PWD vs getcwd,两者有何区别?
- 环境变量表和命令行表的维护。
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变量。用户启动vim,vim发现环境变量里没有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 开始构建内嵌命令:
我们之前的主函数的执行顺序如下:
- 打印前置的格式
- 获取用户输入的命令。
- 分析用户输入的指令
- 利用子进程来完成执行。
我们只需在第三部和第四步骤之间加上检查是不是内嵌命令就可以了。我们先来完善内嵌函数命令的检查:
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 循环里。请看整个流程:
用户输入 cd /tmp
你的 CheckBuildIn -> Cd -> chdir(“/tmp”) 被执行。
此时此刻:物理目录变成了 /tmp,但环境变量表里的 PWD 还是 /home/wwh。状态是不一致的!
但是没关系,程序继续往下跑。while(true) 循环进入下一轮
代码执行到第一行:PrintCommand()。打印提示符
PrintCommand 想要打印 [wwh@host /tmp]#,它必须知道当前在哪。
于是它调用了 GetPwd()。触发同步(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不再只是一个命令,而是进程状态的真实跳动;是为了让$?不再只是一个符号,而是父进程对子进程最负责的守望。这几百行代码,凝结的不仅是逻辑的闭环,更是你终于读懂了操作系统那沉默而精密脉搏后的豁然开朗——原来每一个闪烁的提示符背后,都站着一个不断修正自我、维护秩序的灵魂。
感谢各位对本篇文章的支持。谢谢各位点个三连吧!