【进程控制】————自定义shell
目录
1. 自主Shell命令行解释器
1.1 目标
- 要能处理普通命令
- 要能处理内建命令
- 要能帮助我们理解内建命令/本地变量/环境变量这些概念
- 要能帮助我们理解shell的允许原理
1.2 实现原理
考虑下⾯这个与shell典型的互动:
[root@localhost epoll]# ls
client.cpp readme.md server.cpp utility.h
[root@localhost epoll]# ps
PID TTY TIME CMD
3451 pts/0 00:00:00 bash
3514 pts/0 00:00:00 ps
⽤下图的时间轴来表⽰事件的发⽣次序。其中时间从左向右。shell由标识为sh的⽅块代表,它随着时 间的流逝从左向右移动。shell从⽤⼾读⼊字符串"ls"。shell建⽴⼀个新的进程,然后在那个进程中运 ⾏ls程序并等待那个进程结束。
然后shell读取新的⼀⾏输⼊,建⽴⼀个新的进程,在这个进程中运⾏程序并等待这个进程结束。 所以要写⼀个shell,需要循环以下过程:
- 获取命令⾏
- 解析命令⾏
- 建⽴⼀个⼦进程(fork)
- 替换⼦进程(execvp)
- ⽗进程等待⼦进程退出(wait)
根据这些思路,和我们前⾯的学的技术,就可以⾃⼰来实现⼀个shell了。
1.3 完整源码
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <unordered_map>
#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# " // 命令行提示符格式
// 1.命令行参数表
#define MAXARGC 128 // 最大参数个数
int g_argc = 0;
char* g_argv[MAXARGC];// 参数数组,需要的是char*,因为每个元素都是一个字符串
// 2.环境变量表
#define MAX_ENVS 100
int g_envs = 0;
char* g_env[MAX_ENVS];
char cwd[1024];// 存储当前工作路径
char cwdenv[1024];// 用于更新PWD
int lastcode = 0;// 最近一个进程的退出码
// 别名功能
std::unordered_map<std::string, std::string> alias_list;
// 获取用户名
const char* GetUserName()
{
const char* name = getenv("USER");
return name == NULL ? "None":name;
}
// 获取主机名i
const char* GetHostName()
{
const char* hostname = getenv("HOSTNAME");
return hostname == NULL ? "None":hostname;
}
// 获取当前工作路径
//const char* GetPwd()
//{
// const char* pwd = getenv("PWD");
// return pwd == NULL ? "None":pwd;
//}
// 获取当前工作路径(执行cd后可随之改变)
const char* GetPwd()
{
const char* pwd = getcwd(cwd,sizeof(cwd));
if(pwd != NULL)
{
snprintf(cwdenv,sizeof(cwdenv),"PWD=%s",cwd);
putenv(cwdenv);// 更新PWD
}
return pwd == NULL?"None":pwd;
}
// 获取当前工作目录
std::string GetDirName(const char* pwd)
{
// shift+~:光标所在位置,大小写切换
#define SLASH "/"
std::string dir = pwd;
// '/','/a/b/c'
// 只有根返回根
if(dir == SLASH) return SLASH;
// 反向查找最后一个'/'
auto pos = dir.rfind(SLASH);
// npos,没找到,处理无'/'的异常情况
if(pos == std::string::npos) return "BUG?";
return dir.substr(pos+1);
}
// 获取当前家目录
const char* GetHome()
{
const char* home = getenv("HOME");
return home == NULL? "":home;
}
// 制作命令行提示符
void MakeCommandPrompt(char cmd_prompt[],int size)
{
// snprintf 是 C/C++ 中用于格式化字符串并安全写入指定大小缓冲区的函数
// str是字符串首地址,format是格式,...是填充信息
// int snprintf(char *str, size_t size, const char *format, ...);
//snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),GetPwd());
// .c_str将string转换成const char*
snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),GetDirName(GetPwd()).c_str());
}
// 环境变量表
void InitEnv()
{
// shell启动的时候,从系统中获取环境变量
// 我们的环境变量信息应该从shell统一来
extern char** environ;
// 1.获取环境变量
// void *memset(void *s, int c, size_t n);
memset(g_env,0,sizeof(g_env));
g_envs = 0;
for(int i = 0;environ[i];i++)
{
g_env[i] = (char*)malloc(strlen(environ[i])+1);// 申请空间
strcpy(g_env[i],environ[i]);
g_envs++;
}
g_env[g_envs++] = (char*)"HAHA=test";
g_env[g_envs] = NULL;// 环境变量表最后一个元素置空
// 2.导成环境变量
for(int i=0;g_env[i];i++)
{
putenv(g_env[i]);
}
environ=g_env;
}
// 内建命令
void CD()
{
// cd(默认会家目录)、cd 指定路径、cd -、cd~
// 只有cd,返回家目录
if(g_argc == 1)
{
std::string home = GetHome();
// 无法获取家目录
if(home.empty())
return;
// int chdir(const char *path);// 作用:改变当前进程的工作目录
chdir(home.c_str());
}
else
{
std::string where = g_argv[1];
if(where == "-")// 上一级目录
{
//TODU
}
else if(where == "~")
{
std::string home = GetHome();
// 无法获取家目录
if(home.empty())
return;
// int chdir(const char *path);// 作用:改变当前进程的工作目录
chdir(home.c_str());
}
else
{
chdir(where.c_str());
}
}
}
void ECHO()
{
if(g_argc == 2)
{
// echo "xxxx"
// echo $?
// echo $PATH
std::string opt = g_argv[1];
if(opt == "$?")
{
// 打印最近一个进程的退出码
std::cout<<lastcode<<std::endl;
lastcode = 0;// 更新退出码
}
else if(opt[0] == '$')
{
// 打印环境变量
std::string env_name = opt.substr(1);//截取string,去除$
const char* env_value = getenv(env_name.c_str());
// 获取成功,打印环境变量的值
if(env_value)
std::cout<<env_value<<std::endl;
}
else
{
// 打印字符串
std::cout<<opt<<std::endl;
}
}
}
// 1.打印命令行提示符
void PrintCommandPrompt()
{
char prompt[COMMAND_SIZE];
MakeCommandPrompt(prompt,sizeof(prompt));
printf("%s",prompt);
fflush(stdout);// 没有'\n',刷新一下
}
// 2.获取用户输入的命令
bool GetCommandLine(char* out, int size)
{
// "ls -a -l"->"ls -a -l\n"字符串
// char *fgets(char *s, int size, FILE *stream);// 获取行输入
char* cmd = fgets(out,size,stdin);
if(cmd == NULL) return false;
//'\n'也被读取, 清理'\n'
out[strlen(out)-1] = 0;
// 只按回车的情况
if(strlen(out) == 0) return false;
return true;
}
// 3.命令行分析 "ls -a -l" -> "ls" "-a" "-l"
bool CommandParse(char* commandline)
{
#define SEP " " // 定义分隔符
// char *strtok(char *str, const char *delim);
// 按分隔符拆解字符串,第一次传字符串,后续传NULL
g_argc = 0;
g_argv[g_argc++] = strtok(commandline,SEP);
// 直到切除完毕,返回null
while((bool)(g_argv[g_argc++] = strtok(nullptr,SEP)));
g_argc--;
return g_argc > 0?true:false;
}
// 打印分析完的命令
void PrintArgv()
{
for(int i = 0;g_argv[i];i++)
printf("g_argv[%d]->%s\n",i,g_argv[i]);
printf("g_argc:%d\n",g_argc);
}
// 4. 检测并处理内键命令
bool CheckAndExecBuiltin()
{
std::string cmd = g_argv[0];
if(cmd == "cd")
{
CD();
//lastcode = 0;// 更新退出码;
return true;
}
else if(cmd == "echo")
{
ECHO();
//lastcode = 0;// 更新退出码;
return true;
}
return false;
}
// 5.执行命令
void Execute()
{
pid_t id = fork();
if(id == 0)
{
// 子进程进行进程替换
execvp(g_argv[0],g_argv);
exit(1);
}
// 父进程阻塞等待当前子进程
int status = 0;
pid_t rid = waitpid(id,&status,0);
//(void)rid;// 使用一下,避免警告
lastcode = WEXITSTATUS(status);
}
int main()
{
// shell启动的时候,从系统中获取环境变量
// 我们的环境变量信息应该从shell统一来
InitEnv();
while(true)
{
// 1.输出命令行提示符
PrintCommandPrompt();
// 2.获取用户输入的命令
char commandline[COMMAND_SIZE];
// 获取失败则继续获取
if(!GetCommandLine(commandline,sizeof(commandline)))
continue;
// 获取成功输出信息
//if(GetCommandLine(commandline,sizeof(commandline)))
// printf("echo:%s\n",commandline);
// 3.命令行分析 "ls -a -l" -> "ls" "-a" "-l"
if(!CommandParse(commandline))
continue;
//PrintArgv();
// 4. 检测并处理内键命令
if(CheckAndExecBuiltin())
continue;
// 5.执行命令
Execute();
}
return 0;
}
2. 源码解析
2.1 输出命令行提示符
命令行提示符格式:

用户名、主机名、当前工作路径我们可以使用getenv()获取
然后通过snprintf函数将这些信息填充至我们的数组中
snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd());
snprintf函数解析:int snprintf(char *str, size_t size, const char *format, ...);
snprintf 是安全的字符串格式化函数,核心功能是:
- 按指定格式(如 FORMAT)将数据拼接成字符串;
- 写入指定大小的缓冲区,且严格限制写入字节数(最多写 size-1 个字符,最后自动加 \0);
- 彻底避免 sprintf 因内容超出缓冲区大小导致的内存越界、程序崩溃问题。
#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "// 命令行提示符格式
// 获取用户名字
const char *GetUserName()
{
const char *name = getenv("USER");
return name == NULL ? "None" : name;
}
// 获取主机名
const char *GetHostName()
{
const char *hostname = getenv("HOSTNAME");
return hostname == NULL ? "None" : hostname;
}
// 获取当前工作路径
const char *GetPwd()
{
//const char *pwd = getenv("PWD");
const char *pwd = getcwd(cwd, sizeof(cwd));
if(pwd != NULL)
{
snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);
putenv(cwdenv);
}
return pwd == NULL ? "None" : pwd;
}
// 制作命令行提示符
void MakeCommandLine(char cmd_prompt[], int size)
{
snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
//snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd());
}
// 打印命令行提示符
void PrintCommandPrompt()
{
char prompt[COMMAND_SIZE];
MakeCommandLine(prompt, sizeof(prompt));
printf("%s", prompt);
fflush(stdout);
}
2.2 获取用户输入的命令
命令+选项由空格隔开,所以不能由scanf读取,所以我们使用fgets函数进行行读取
char *fgets(char *s, int size, FILE *stream);
fgets 是 C/C++ 中安全的行读取函数,核心功能是:
- 从指定输入流(如 stdin 标准输入)读取一行字符;
- 最多读取 size-1 个字符(或遇到换行符 / 文件结束),末尾自动加 \0 形成合法字符串;
- 避免 gets 函数无长度限制导致的内存越界问题。
注意:行读取时也会读取到'\n',所以我们要清理一下:out[strlen(out)-1] = 0; // 清理\n
main函数逻辑中,读取失败 / 无有效命令时,跳过本次循环的后续逻辑(解析、执行),回到循环开头重新打印命令行提示符,等待用户输入。
bool GetCommandLine(char *out, int size)
{
// ls -a -l => "ls -a -l\n" 字符串
char *c = fgets(out, size, stdin);
if(c == NULL)
return false;
out[strlen(out)-1] = 0; // 清理\n
if(strlen(out) == 0)
return false;
return true;
}
// 2. 获取用户输入的命令
char commandline[COMMAND_SIZE];
if(!GetCommandLine(commandline, sizeof(commandline)))
continue;
2.3 命令行分析
命令行分析就是将获取到的用户输入的命令: "ls -a -l" 解析成-> "ls" "-a" "-l"
我们可以使用strtok函数对字符串进行分割
char *strtok(char *str, const char *delim);
strtok 是 C/C++ 中字符串分割函数,核心功能是:
- 按指定分隔符(如空格、逗号)将一个字符串拆分成多个子字符串
- 首次调用传入待分割字符串,后续调用传入 NULL,持续分割剩余内容,直到返回 NULL 表示分割完毕;
- 你的代码场景:将用户输入的命令行(如 ls -a -l)按空格拆分成 ls、-a、-l,为后续解析命令参数(g_argv)提供基础。
/ 1. 命令行参数表
#define MAXARGC 128
char *g_argv[MAXARGC];
int g_argc = 0;
// 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
bool CommandParse(char *commandline)
{
#define SEP " "
g_argc = 0;
// 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
g_argv[g_argc++] = strtok(commandline, SEP);
while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
g_argc--;
return g_argc > 0 ? true:false;
}
// 检查分析后的命令
void PrintArgv()
{
for(int i = 0; g_argv[i]; i++)
{
printf("argv[%d]->%s\n", i, g_argv[i]);
}
printf("argc: %d\n", g_argc);
}
// 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
if(!CommandParse(commandline))
continue;
//PrintArgv();
2.4 检测并处理内建命令
我们主要实现以下两个内建命令:
cd- 目录切换命令echo- 输出命令
2.4.1 cd命令实现:
cd命令有很多用法:
cd(无参数):进入用户家目录cd 目录路径:进入指定目录cd -:返回上一次所在的目录cd ~:进入用户家目录cd ..:返回上一级目录
我们这边实现一下cd(默认进入家目录)、cd 指定目录
-
默认进入家目录:
- 当
CheckAndExecBuiltin()检测到cd命令且参数个数为1时 - 通过环境变量获取家目录路径
- 使用
chdir()修改当前进程工作目录
- 当
-
进入指定目录:
- 当参数个数为2时
- 直接使用
chdir()切换到第二个参数指定的目录
2.4.2 echo命令实现:
echo命令也有很多用法:
echo "字符串":输出字符串echo $?:输出最近进程的退出码echo $环境变量:输出环境变量值
当参数个数为2时:
-
输出进程退出码:
- 若第二个参数为
$? - 从全局变量
lastcode获取最近进程退出码并输出 - 输出后将
lastcode重置为0
- 若第二个参数为
-
输出环境变量:
- 若第二个参数以
$开头 - 使用
getenv()获取对应环境变量值并输出
- 若第二个参数以
-
输出字符串:
- 不符合上述情况时
- 直接输出第二个参数内容
当检测到内建命令时,系统会返回true。执行完if(CheckAndExecBuiltin()) continue;语句后,程序会重新进入while循环从头开始执行,而不会继续执行外部命令处理函数的逻辑。
// 最近一个进程的退出码
int lastcode = 0;
const char *GetHome()
{
const char *home = getenv("HOME");
return home == NULL ? "" : home;
}
//command
bool Cd()
{
// cd argc = 1
if(g_argc == 1)
{
std::string home = GetHome();
if(home.empty()) return true;
chdir(home.c_str());
}
else
{
std::string where = g_argv[1];
// cd - / cd ~
if(where == "-")
{
// Todu
}
else if(where == "~")
{
// Todu
}
else
{
chdir(where.c_str());
}
}
return true;
}
void Echo()
{
if(g_argc == 2)
{
// echo "hello world"
// echo $?
// echo $PATH
std::string opt = g_argv[1];
if(opt == "$?")
{
std::cout << lastcode << std::endl;
lastcode = 0;
}
else if(opt[0] == '$')
{
std::string env_name = opt.substr(1);
const char *env_value = getenv(env_name.c_str());
if(env_value)
std::cout << env_value << std::endl;
}
else
{
std::cout << opt << std::endl;
}
}
}
bool CheckAndExecBuiltin()
{
//如果内键命令做重定向,更改shell的标准输入,输出,错误
std::string cmd = g_argv[0];
if(cmd == "cd")
{
Cd();
return true;
}
else if(cmd == "echo")
{
Echo();
return true;
}
else if(cmd == "export")
{
}
else if(cmd == "alias")
{
// std::string nickname = g_argv[1];
// alias_list.insert(k, v);
}
return false;
}
// 检测并处理内键命令
if(CheckAndExecBuiltin())
continue;
2.5 执行命令
命令执行的逻辑是通过进程替换实现的:首先创建子进程,子进程调用execvp(const char *file, char *const argv[])完成程序替换,并将命令行参数传递过去;随后父进程通过waitpid函数等待并回收子进程;最后使用WEXITSTATUS()函数更新最近进程的退出码(lastcode)。
int Execute()
{
pid_t id = fork();
if(id == 0)
{
//child
execvp(g_argv[0], g_argv);
exit(1);
}
int status = 0;
// father
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
}
return 0;
}
// 执行命令
Execute();
2.6 扩展知识
2.6.1 环境变量表
- 清空自定义环境变量数组
g_env,重置计数器g_envs,声明系统环境变量全局指针environ; - 遍历系统
environ中的所有环境变量,为每个变量分配独立内存并拷贝内容到g_env,同步更新计数器; - 在
g_env中添加自定义环境变量HAHA=for_test 作为测试,并按规范以NULL结尾; - 通过
putenv()将g_env中的变量注册为系统环境变量,最后将系统environ指向自定义的g_env,完成环境变量表的接管。
// 2. 环境变量表
#define MAX_ENVS 100
char *g_env[MAX_ENVS];
int g_envs = 0;
void InitEnv()
{
extern char **environ;
memset(g_env, 0, sizeof(g_env));
g_envs = 0;
//本来要从配置文件来
//1. 获取环境变量
for(int i = 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_test
g_env[g_envs] = NULL;
//2. 导成环境变量
for(int i = 0; g_env[i]; i++)
{
putenv(g_env[i]);
}
environ = g_env;
}
2.6.2 别名映射表
- 利用
unordered_map<std::string, std::string>的键值对特性,键存别名(如ll),值存真实命令(如ls -l),实现别名到原命令的快速映射(哈希查找效率高); - 解析
alias 别名=原命令格式的输入,提取别名和原命令字符串,插入到alias_list中(需处理重复别名的覆盖逻辑); - 在命令行解析(
CommandParse)后、执行命令前,检查当前命令(g_argv[0])是否存在于alias_list的键中,若存在则替换为对应的值,并重新拆分替换后的命令为g_argv数组(如ll替换为ls -l后,g_argv需更新为["ls", "-l", ...]); - 解析
unalias 别名输入,从alias_list中删除对应键值对,支持清空所有别名(unalias -a); - 避免别名递归解析(如别名
ls指向ls -a),解析时仅替换一次或限制递归深度;执行内置命令时跳过别名解析(如alias自身不触发映射)。
// 3. 别名映射表
std::unordered_map<std::string, std::string> alias_list;
2.6.3 重定向
请前往下一篇博客查看
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)