🌟 各位看官好,我是!****
🌍 Linux == Linux is not Unix !
🚀 通过对进程方面系统的学习,接下来可以动手实现一个迷你Xshell解释器!
👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享给更多人哦!
目录
回顾进程
前面我们系统梳理了 Linux 进程管理及相关底层机制的核心知识:从进程的本质(由 PCB 与代码数据构成,内核通过链表管理),到进程的创建(fork 函数的双返回值特性与写时拷贝技术优化内存使用);从进程的生命周期管理(终止的三种场景、return/exit/_exit 的差异,以及退出码的意义),到进程等待的必要性(wait/waitpid 函数避免僵尸进程,非阻塞等待的实现逻辑);同时也清晰了程序替换的原理(exec 函数族在不创建新进程的情况下替换代码数据,底层统一依赖 execve 系统调用),还掌握了命令行参数的传递(argc/argv 的应用)与环境变量的机制(继承特性、PATH 等关键变量的作用)------ 这些知识点看似分散,实则围绕 "进程的创建、控制与资源交互" 形成了完整的技术链条,而这恰好是实现命令解释器的核心基础。
要手写一个迷你 Xshell 解释器,本质上就是实现 "接收用户命令→解析命令→创建进程执行命令→等待命令执行完成" 的闭环,这正好能把前面的知识串联起来:用户输入的 "ls -l""cd ../" 等命令,需要用命令行参数解析的逻辑拆分成指令与选项(对应 argc/argv 的处理);执行外部命令时,需通过 fork 创建子进程(利用写时拷贝减少内存开销),再调用 exec 函数族(比如用 execvp 自动从 PATH 中查找命令路径)替换子进程代码;子进程执行期间,父进程(解释器本身)需通过 waitpid 等待其结束,避免僵尸进程;而环境变量(如 PATH、PWD)的继承特性,又能保证命令执行时的环境一致性 ------ 可以说,前面掌握的进程管理、程序替换、参数解析等技术,正是搭建迷你 Xshell 的 "积木",接下来就可以基于这些基础动手实现了。
目标及实现思路
- 要能处理普通命令
- 要能处理内建命令
- 要能帮助我们理解内建命令/本地变量/环境变量这些概念
- 要能帮助我们理解shell的允许原理
用下图的时间轴来表⽰事件的发⽣次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读⼊字符串"ls"。shell建⽴⼀个新的进程,然后在那个进程中运 ⾏ls程序并等待那个进程结束。然后shell读取新的⼀⾏输⼊,建⽴⼀个新的进程,在这个进程中运⾏程序 并等待这个进程结束。 要写⼀个shell,要循环以下过程:
- 初始化化数据
- 打印命令行提示符
- 获取用户输入指令
- 解析用户指令
- 检测命令,内建命令,要让shell自己来执行!!!
- 执行命令,让子进程来执行!!!
实现原理及代码实现
打印命令行提示符
egoist@hcss-ecs-3ec8:~$
在xshell中,它的命令行提示符如上所示.基于前面的知识铺垫,我们可以通过环境变量获取命令提示符所需的信息。
cpp
static std::string GetUserName()
{
std::string username = getenv("USER");
return username.empty()?"None":username;
}
static std::string GetLangName()
{
std::string langname = getenv("LANG");
return langname.empty()?"None":langname;
}
static std::string GetPwd()
{
std::string pwd = getenv("PWD");
return pwd.empty()?"None":pwd;
}
void PrintCommandPrompt()
{
std::string user = GetUserName();
std::string lang = GetLangName();
std::string pwd = GetPwd();
printf("[%s@%s:%s]# ",user.c_str(),lang.c_str(),pwd.c_str());
}
获取用户输入指令
GetCommanString 函数的核心作用是从标准输入(键盘)读取用户输入的命令字符串,存储到指定的缓冲区中,并处理输入末尾的换行符,为后续的命令解析做准备。
- 当用户输入完指令后,该函数获取用户输入指令,存在 cmd_str_buff 数组中。
- 每次输完指令后回车,即敲\n,导致 cmd_str_buff 读取到了\n,因此需要将 \n 修改为 \0 。
bash
#define SIZE 1024
char commanstr[SIZE];
//2.获取用户输入指令
bool GetCommanString(char cmd_str_buff[], int len)
{
if(cmd_str_buff==NULL||len<=0)
return false;
//输入指令
char *res = fgets(cmd_str_buff,len,stdin);
if(res==NULL)
return false;
//"ls -a -l\n" --> "ls -a -l\0"
// cmd_str_buff[strlen(cmd_str_buff)] = 0; //err
cmd_str_buff[strlen(cmd_str_buff) - 1] = 0;
return strlen(cmd_str_buff)==0?false:true;
}
解析用户输入指令
在父进程创建子进程的过程中,子进程会以父进程为模板完成拷贝操作,这其中就包括对命令行参数的复制。基于这一特性,我们特意将命令行参数表设为全局变量 ------ 如此一来,当子进程完成创建时,便能自然地继承这份参数表,无需额外的传递操作,从而为后续子进程执行相关命令提供便捷的参数支持。
bash
char *gargv[ARGS] = {NULL};
int gargc = 0;
当在 Xshell 中输入类似 "ls -a -l" 这样的指令并回车后,解释器会对该字符串进行解析处理:首先按空格分割出命令与各选项,将其拆分为 "ls"、"-a"、"-l" 三个独立的部分,然后依次存入命令行参数表中,并且按照规范在参数表的末尾添加 NULL 作为结束标志。
bash
//3.解析用户指令
bool PraseCommanString(char cmd[])
{
if(cmd==NULL)
return false;
#define SEP " "
//3."ls -a -l" --> "ls" "-a" "-l"
gargv[gargc++]=strtok(cmd,SEP);
//最后以NULL结尾
while((bool)(gargv[gargc++]=strtok(NULL,SEP)));
//回退一次gargc
gargc--;
return true;
}
初始化数据
然而,当进行下一次指令输入时,由于命令行参数表被设为全局变量,且未对其原有数据进行清空操作,这就引发了如下所示的问题:每次新输入指令本应生成独立的参数表,却因全局参数表留存旧数据,使得后续解析填充时,旧数据未被覆盖干净,从而出现参数表内容异常叠加、数据混乱的情况,像第二次执行 ls -a -l 时,参数数量和内容都出现了不符合预期的错误扩展,影响了命令解析与执行的正确性 。

因此,每次解析用户指令前都需要将命令行参数表进行清空。
bash
//0.初始化化数据
void InitGlobal()
{
gargc = 0;
memset(gargv,0,sizeof(gargv));
}
执行指令
Bash 的核心执行逻辑是通过创建子进程来运行命令 ------ 这种设计既让 Bash 得以稳定承担用户与系统的交互中介角色,又能高效管控各命令的执行流程,也因此成为 Linux 系统中至关重要的核心组件。
具体到执行流程:Bash 先创建子进程,由子进程通过程序替换函数(如 exec 系列)执行解析后的命令;与此同时,Bash 会进入阻塞状态等待子进程,直至获取其执行结果。
此外,我们还可以借鉴echo $?获取进程退出码的机制,在这里实现类似功能:将子进程的退出码存入lastcode变量中,方便后续查看。
bash
//5.执行命令,让子进程来执行!!!
void ForkAndExec()
{
pid_t id = fork();
if(id<0)
{
perror("fork"); //将错误码转为错误信息
return;
}
else if(id == 0) //子进程
{
execvp(gargv[0],gargv);
exit(0);
}
else
{
//父进程
//等待子进程
int status = 0;
pid_t rid = waitpid(id,&status,0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
}
}
}
检测指令

在操作中我们发现一个有趣的现象:当使用 cd .. 命令试图切换目录,之后执行 pwd 查看路径,会发现路径并没有如预期回退。这是因为我们当前采用的是 Bash 创建子进程执行命令 的方式,而 cd 这类命令比较特殊,得从进程工作原理说起:
核心原因:子进程无法改变父进程的工作目录
- 子进程会独立拷贝父进程(Bash)的运行环境,包括当前工作目录(CWD)。
- 子进程执行
cd ..确实会在自己的环境里切换目录,但这一改动 仅作用于子进程自身 ,不会影响到父进程(Bash)的工作目录。 - 后续执行
pwd时,依旧创建子进程拷贝的工作目录,这就解释了为什么cd ..后pwd路径没回退。
因此像cd、echo 这种命令是内建命令,是要由父进程来完成的。
进行路径切换,本质是父进程bash在进行路径切换,路径就会被子进程继承下去,因此pwd时能查到新路径。所以在执行指令之前,需要先进行对指令的检测,如果是内建命令则让bash自己执行.
bash
static std::string GetHomePath()
{
std::string homepath = getenv("HOME");
return homepath.empty()?"/":homepath;
}
//4.检测命令,内建命令,要让shell自己来执行!!!
bool BuiltInCommanExec()
{
std::string cmd = gargv[0];
bool ret = false;
if(cmd == "cd")
{
if(gargc == 2)
{
std::string target = gargv[1];
if(target == "~")
{
ret = true;
chdir(GetHomePath().c_str());
}
else
{
//解决 cd .. 问题
ret = true;
chdir(gargv[1]);
}
}
else if(gargc == 1)
{
ret =true;
chdir(GetHomePath().c_str());
}
else
{
//BUG
}
}
else if(cmd == "echo")
{
if(gargc == 2)
{
std::string args = gargv[1];
if(args[0]=='$')
{
if(args[1]=='?')
{
//打印错误码
printf("lastcode:%d\n",lastcode);
lastcode = 0;
ret = true;
}
else
{
const char *name = &args[1];
printf("%s\n",getenv(name));
lastcode = 0;
ret = true;
}
}
else
{
printf("%s\n",args.c_str());
ret = true;
}
}
}
return ret;
}
更新命令行提示符

上图当中:cd .. 进行回退路径时候,pwd确实验证了我们的路径进行了回退,但是我们也发现一个问题,路径更新的时候命令行提示符的路径并没有更新,为什么会这样呢?并且我们的命令行提示符路径太长了,能不能像xshell实现那样呢?
环境变量的变化,可能会依赖于进程,pwd需要shell自己更新环境变量的值。
bash
static std::string GetPwd()
{
char temp[1024];
getcwd(temp,sizeof(temp));
//顺便更新以下shell自己的环境变量pwd
snprintf(pwd,sizeof(pwd),"PWD=%s",temp);
putenv(pwd);
std::string pwd_lable = temp;
const std::string pathsep = "/";
auto pos = pwd_lable.rfind(pathsep);
if(pos == std::string::npos)
return "None";
pwd_lable = pwd_lable.substr(pos+pathsep.size());
return pwd_lable.empty()?"/":pwd_lable;
}
总结
本文的核心目标是通过亲手实现一个简易的 Shell(myshell),来深入理解 Shell 的工作原理,特别是以下几个关键概念:
-
内建命令 (Built-in Commands) vs. 普通命令 (外部命令)
-
环境变量 (Environment Variables) 和 本地变量 的作用与生命周期。
-
进程的独立性 和 进程创建 (
fork) / 程序替换 (exec) 机制。
通过这个简单的 myshell 实现,我们清晰地看到了 Shell 的底层工作模型:
Shell 本身是一个死循环程序,它通过解析命令、识别内建命令、并巧妙地利用 fork 和 exec 系统调用来管理所有外部命令的执行,从而扮演了用户与操作系统内核之间的翻译官和管理者的角色。
附源码
bash
#include"myshell.h"
#define SIZE 1024
int main()
{
char commanstr[SIZE];
while(true)
{
//初始化化数据
InitGlobal();
//1.打印命令行提示符
PrintCommandPrompt();
//2.获取用户输入指令
//用户输入指令错误的话重来
if(!GetCommanString(commanstr,SIZE))
continue;
//3.解析用户指令
PraseCommanString(commanstr);
//4.检测命令,内建命令,要让shell自己来执行!!!
if(BuiltInCommanExec())
{
continue;
}
//4.执行命令,让子进程来执行!!!
ForkAndExec();
}
return 0;
}
myshell.h
bash
#ifndef __MYSHELL_H__
#define __MYSHELL_H__
#include<stdio.h>
#include<string>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#define ARGS 64
void Debug();
void PrintCommandPrompt();
bool GetCommanString(char cmd_str_buff[], int len);
bool PraseCommanString(char cmd[]);
void InitGlobal();
void ForkAndExec();
bool BuiltInCommanExec();
#endif
bash
#include"myshell.h"
int lastcode = 0;
char pwd[1024]; // 全局变量空间,保存当前shell进程的工作路径
//命令行参数表故意设为全局,为的是能给子进程继承下来
char *gargv[ARGS] = {NULL};
int gargc = 0;
void Debug()
{
printf("hello myshell!\n");
}
static std::string GetUserName()
{
std::string username = getenv("USER");
return username.empty()?"None":username;
}
static std::string GetLangName()
{
std::string langname = getenv("LANG");
return langname.empty()?"None":langname;
}
//static std::string GetPwd()
//{
// std::string pwd = getenv("PWD");
// return pwd.empty()?"None":pwd;
//}
static std::string GetPwd()
{
// 环境变量的变化,可能会依赖于进程,pwd需要shell自己更新环境变量的值
char temp[1024];
getcwd(temp,sizeof(temp));
//顺便更新以下shell自己的环境变量pwd
snprintf(pwd,sizeof(pwd),"PWD=%s",temp);
putenv(pwd);
std::string pwd_lable = temp;
const std::string pathsep = "/";
auto pos = pwd_lable.rfind(pathsep);
if(pos == std::string::npos)
return "None";
pwd_lable = pwd_lable.substr(pos+pathsep.size());
return pwd_lable.empty()?"/":pwd_lable;
}
static std::string GetHomePath()
{
std::string homepath = getenv("HOME");
return homepath.empty()?"/":homepath;
}
//1.打印命令行提示符
void PrintCommandPrompt()
{
std::string user = GetUserName();
std::string lang = GetLangName();
std::string pwd = GetPwd();
printf("[%s@%s:%s]# ",user.c_str(),lang.c_str(),pwd.c_str());
}
//2.获取用户输入指令
bool GetCommanString(char cmd_str_buff[], int len)
{
if(cmd_str_buff==NULL||len<=0)
return false;
//输入指令
char *res = fgets(cmd_str_buff,len,stdin);
if(res==NULL)
return false;
//"ls -a -l\n" --> "ls -a -l\0"
cmd_str_buff[strlen(cmd_str_buff) - 1] = 0;
return strlen(cmd_str_buff)==0?false:true;
}
//3.解析用户指令
bool PraseCommanString(char cmd[])
{
if(cmd==NULL)
return false;
#define SEP " "
//3."ls -a -l" --> "ls" "-a" "-l"
gargv[gargc++]=strtok(cmd,SEP);
//最后以NULL结尾
while((bool)(gargv[gargc++]=strtok(NULL,SEP)));
//回退一次gargc
gargc--;
////打印验证
//#define DEBUG
#ifdef DEBUG
printf("gargc: %d\n", gargc);
printf("----------------------\n");
for(int i = 0; i < gargc; i++)
{
printf("gargv[%d]: %s\n",i, gargv[i]);
}
printf("----------------------\n");
for(int i = 0; gargv[i]; i++)
{
printf("gargv[%d]: %s\n",i, gargv[i]);
}
#endif
return true;
}
//初始化化数据
void InitGlobal()
{
gargc = 0;
memset(gargv,0,sizeof(gargv));
}
//5.执行命令,让子进程来执行!!!
void ForkAndExec()
{
pid_t id = fork();
if(id<0)
{
perror("fork"); //将错误码转为错误信息
return;
}
else if(id == 0) //子进程
{
execvp(gargv[0],gargv);
exit(0);
}
else
{
//父进程
//等待子进程
int status = 0;
pid_t rid = waitpid(id,&status,0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
}
}
}
//4.检测命令,内建命令,要让shell自己来执行!!!
bool BuiltInCommanExec()
{
std::string cmd = gargv[0];
bool ret = false;
if(cmd == "cd")
{
if(gargc == 2)
{
std::string target = gargv[1];
if(target == "~")
{
ret = true;
chdir(GetHomePath().c_str());
}
else
{
//解决 cd .. 问题
ret = true;
chdir(gargv[1]);
}
}
else if(gargc == 1)
{
ret =true;
chdir(GetHomePath().c_str());
}
else
{
//BUG
}
}
else if(cmd == "echo")
{
if(gargc == 2)
{
std::string args = gargv[1];
if(args[0]=='$')
{
if(args[1]=='?')
{
//打印错误码
printf("lastcode:%d\n",lastcode);
lastcode = 0;
ret = true;
}
else
{
const char *name = &args[1];
printf("%s\n",getenv(name));
lastcode = 0;
ret = true;
}
}
else
{
printf("%s\n",args.c_str());
ret = true;
}
}
}
return ret;
}


