本节重点
- 进程创建:fork
- 进程终止,理解环境变量 $?
- 进程等待
- 进程程序替换
- 微型 Shell,深入理解 Shell 运行原理
回顾上文:
5. 自主Shell命令行解释器(综合实战)
本章基于前文进程创建、进程终止、进程等待、程序替换、环境变量配置所有知识点,从零实现一个简易版自定义Shell解释器。该实战是Linux进程控制的终极综合案例,完美串联所有核心API,同时帮助深度理解Shell运行原理、内建命令、环境变量、本地变量的本质区别。
5-1 项目实现目标
本自定义Shell实现完整终端交互能力,核心目标如下:
-
支持处理Linux普通外部命令(ls、ps、pwd、touch等);
-
支持处理Shell内建命令(cd、export、env、echo $?等);
-
实现自定义命令行提示符,模拟原生Shell交互效果;
-
自主管理环境变量,支持追加、查看自定义环境变量;
-
记录上一条命令退出码,支持
echo $?功能; -
深度理解Shell运行机制、内建命令与外部命令的核心差异。
5-2 核心实现原理
5-2-1 原生Shell运行逻辑复盘
我们日常使用的Linux终端(bash),本质是一个无限循环的进程:持续读取用户输入命令、解析命令、执行命令、等待命令结束,再等待下一次输入。
以执行 ls、ps 命令为例的事件时序:
- Shell主进程持续运行,等待用户输入; 2. 用户输入命令字符串(如ls),Shell读取命令; 3. Shell调用fork创建子进程,生成全新子进程; 4. 子进程通过exec系列函数程序替换,运行ls程序; 5. 父进程调用waitpid阻塞等待子进程退出,回收资源; 6. 子进程运行结束退出,父进程完成回收,再次进入循环,等待下一次用户输入。
5-2-2 自定义Shell核心循环流程
自定义Shell完全复刻原生bash逻辑,固定五步循环机制,也是本项目的核心骨架:
-
打印命令行提示符:展示用户名、主机名、当前工作目录,模拟原生终端样式;
-
获取用户命令行输入:读取用户输入的完整命令字符串;
-
解析命令行:分割命令、参数,统计参数个数,格式化参数数组;
-
区分内建/外部命令执行:内建命令由Shell自身直接调用函数执行,外部命令通过fork+execvp子进程执行;
-
父进程等待回收子进程:记录命令退出码,完成一次命令执行循环。
5-2-3 内建命令与外部命令核心区别
-
外部命令:磁盘上的可执行程序(ls、ps、mkdir),必须创建子进程,通过程序替换执行,子进程运行、父进程等待;
-
内建命令 :Shell内部实现的函数逻辑(cd、export、env、echo),无需创建子进程,由Shell主进程直接执行,直接修改Shell进程自身属性(工作目录、环境变量)。
5-3 完整可运行源码
代码整合所有功能,包含提示符生成、命令读取、命令解析、内建命令处理、外部命令执行、环境变量初始化与管理、退出码记录等全套逻辑:
cpp
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctype.h>
using namespace std;
// 基础配置常量
const int basesize = 1024;
const int argvnum = 64;
const int envnum = 64;
// 全局命令行参数表
char *gargv[argvnum];
int gargc = 0;
// 记录上一条命令的退出码
int lastcode = 0;
// 自定义shell的环境变量表
char *genv[envnum];
// 全局工作路径缓存
char pwd[basesize];
char pwdenv[basesize];
// 去除字符串首尾空格宏
#define TrimSpace(pos) do{\
while(isspace(*pos)){\
pos++;\
}\
}while(0)
// 获取当前登录用户名
string GetUserName()
{
string name = getenv("USER");
return name.empty() ? "None" : name;
}
// 获取主机名
string GetHostName()
{
string hostname = getenv("HOSTNAME");
return hostname.empty() ? "None" : hostname;
}
// 获取当前工作目录,并更新PWD环境变量
string GetPwd()
{
if(nullptr == getcwd(pwd, sizeof(pwd))) return "None";
snprintf(pwdenv, sizeof(pwdenv),"PWD=%s", pwd);
putenv(pwdenv);
return pwd;
}
// 获取当前目录名(用于命令行提示符精简展示)
string LastDir()
{
string curr = GetPwd();
if(curr == "/" || curr == "None") return curr;
size_t pos = curr.rfind("/");
if(pos == std::string::npos) return curr;
return curr.substr(pos+1);
}
// 拼接完整命令行提示符
string MakeCommandLine()
{
char command_line[basesize];
snprintf(command_line, basesize, "[%s@%s %s]# ",\
GetUserName().c_str(), GetHostName().c_str(), LastDir().c_str());
return command_line;
}
// 打印命令行提示符
void PrintCommandLine()
{
printf("%s", MakeCommandLine().c_str());
fflush(stdout);
}
// 获取用户输入的命令行
bool GetCommandLine(char command_buffer[], int size)
{
char *result = fgets(command_buffer, size, stdin);
if(!result)
{
return false;
}
// 去除fgets读取的换行符
command_buffer[strlen(command_buffer)-1] = 0;
// 空输入直接忽略
if(strlen(command_buffer) == 0) return false;
return true;
}
// 解析命令行:分割命令与参数
void ParseCommandLine(char command_buffer[], int len)
{
(void)len;
memset(gargv, 0, sizeof(gargv));
gargc = 0;
// 以空格为分隔符分割参数
const char *sep = " ";
gargv[gargc++] = strtok(command_buffer, sep);
while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
gargc--;
}
// 调试函数:打印解析后的命令参数
void debug()
{
printf("argc: %d\n", gargc);
for(int i = 0; gargv[i]; i++)
{
printf("argv[%d]: %s\n", i, gargv[i]);
}
}
// 执行外部命令:fork子进程 + execvpe程序替换 + waitpid等待回收
bool ExecuteCommand()
{
pid_t id = fork();
if(id < 0) return false;
if(id == 0)
{
// 子进程:执行外部命令,使用自定义环境变量
execvpe(gargv[0], gargv, genv);
// 执行失败才会走到此处
perror("execvpe failed");
exit(1);
}
// 父进程:阻塞等待子进程退出
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
// 解析子进程退出状态
if(WIFEXITED(status))
{
lastcode = WEXITSTATUS(status);
}
else
{
lastcode = 100;
}
return true;
}
return false;
}
// 新增自定义环境变量
void AddEnv(const char *item)
{
int index = 0;
// 遍历找到环境变量数组末尾
while(genv[index])
{
index++;
}
// 动态内存拷贝,避免指针覆盖
genv[index] = (char*)malloc(strlen(item)+1);
strncpy(genv[index], item, strlen(item)+1);
genv[++index] = nullptr;
}
// 检测并执行内建命令(Shell自身执行,无需子进程)
bool CheckAndExecBuiltCommand()
{
// 内建命令:cd 切换工作目录
if(strcmp(gargv[0], "cd") == 0)
{
if(gargc == 2)
{
chdir(gargv[1]);
lastcode = 0;
}
else
{
lastcode = 1;
}
return true;
}
// 内建命令:export 新增环境变量
else if(strcmp(gargv[0], "export") == 0)
{
if(gargc == 2)
{
AddEnv(gargv[1]);
lastcode = 0;
}
else
{
lastcode = 2;
}
return true;
}
// 内建命令:env 打印所有自定义环境变量
else if(strcmp(gargv[0], "env") == 0)
{
for(int i = 0; genv[i]; i++)
{
printf("%s\n", genv[i]);
}
lastcode = 0;
return true;
}
// 内建命令:echo 输出内容/查看退出码
else if(strcmp(gargv[0], "echo") == 0)
{
if(gargc == 2)
{
// 支持 echo $? 查看上一条命令退出码
if(gargv[1][0] == '$')
{
if(gargv[1][1] == '?')
{
printf("%d\n", lastcode);
lastcode = 0;
}
}
// 普通字符串输出
else
{
printf("%s\n", gargv[1]);
lastcode = 0;
}
}
else
{
lastcode = 3;
}
return true;
}
// 非内建命令,返回false交由外部命令逻辑处理
return false;
}
// 初始化环境变量:从系统继承所有原生环境变量
void InitEnv()
{
extern char **environ;
int index = 0;
// 拷贝系统环境变量到自定义环境变量表
while(environ[index])
{
genv[index] = (char*)malloc(strlen(environ[index])+1);
strncpy(genv[index], environ[index], strlen(environ[index])+1);
index++;
}
genv[index] = nullptr;
}
int main()
{
// 初始化环境变量
InitEnv();
char command_buffer[basesize];
// Shell死循环,持续交互
while(true)
{
PrintCommandLine();
// 获取用户命令输入
if( !GetCommandLine(command_buffer, basesize) )
{
continue;
}
// 解析命令参数
ParseCommandLine(command_buffer, strlen(command_buffer));
// 优先执行内建命令,否则执行外部命令
if ( CheckAndExecBuiltCommand() )
{
continue;
}
ExecuteCommand();
}
return 0;
}
5-4 核心功能逐点解析
5-4-1 环境变量管理机制
- 初始化逻辑 :程序启动时通过
environ全局变量拷贝系统所有原生环境变量,保证自定义Shell兼容系统所有配置; 2. 自定义追加机制 :通过export key=val命令可新增自定义环境变量,通过动态内存拷贝存入genv数组; 3. 全局生效 :所有外部命令执行时,通过execvpe传递自定义环境变量表,实现「系统环境+自定义环境」的继承效果,完美复用前文环境变量进阶知识点。
5-4-2 内建命令运行机制
所有内建命令均由Shell主进程直接执行,不创建子进程,核心原因:内建命令修改的是Shell进程自身属性(工作目录、环境变量、退出码),若创建子进程执行,修改仅在子进程生效,主进程无变化,失去命令意义。
支持的内建命令功能:
-
cd :调用
chdir()修改主进程工作目录,同步更新PWD环境变量; -
export:追加自定义环境变量到当前Shell环境;
-
env:遍历打印当前Shell所有环境变量;
-
echo :支持普通字符串输出、
echo $?查看上一条命令退出码。
5-4-3 外部命令运行机制
完全遵循标准多进程执行流程:fork创建子进程 → execvpe程序替换运行外部命令 → waitpid阻塞回收子进程 → 记录退出码,完整串联前文进程控制所有核心操作。
5-4-4 退出码记录机制
全局变量 lastcode 永久保存上一条命令的退出状态:外部命令通过 WEXITSTATUS 解析子进程退出码,内建命令手动赋值退出码,通过 echo $? 可随时查看,完全复刻原生Shell退出码逻辑。
5-5 项目核心总结
5-5-1 进程与函数的层级映射关系
程序级进程调用 与 代码级函数调用逻辑高度一致,是Linux系统核心设计思想:
-
函数调用:
call函数 → 函数执行 → return返回值 -
进程调用:
fork/exec创建进程 → 子进程执行程序 → exit退出值 → wait获取结果
Linux将程序内部的函数调用模型,拓展为多进程调用模型,实现了程序之间的解耦与独立运行。
5-5-2 核心知识点复盘
-
进程创建:fork函数生成子进程,实现任务分离;
-
程序替换:execvpe加载外部程序,不创建新进程、仅替换代码数据;
-
进程等待:waitpid阻塞回收子进程,避免僵尸进程,获取退出状态;
-
环境变量:继承系统ENV+自定义追加,进程环境变量隔离独立;
-
内建/外部命令:区分进程执行与自身执行的核心差异,理解Shell本质。
5-5-3 Shell本质理解
Shell本质就是一个死循环运行的多进程管理程序:不断接收用户指令,判断指令类型,通过子进程执行外部程序,自身执行内置功能,同时负责资源回收、环境管理、状态记录,所有功能均基于本章及前文进程控制知识点实现。