跳到主要内容从零开始编写 Linux 自定义 Shell:原理与 C++ 实现 | 极客日志C++
从零开始编写 Linux 自定义 Shell:原理与 C++ 实现
基于 Linux 环境,使用 C++ 从零实现了一个简易的自定义 Shell 解释器。内容涵盖 Shell 的运行原理,包括命令行读取、参数解析、进程创建与执行以及内建命令处理。通过 fork、exec 和 wait 系统调用的实战应用,深入剖析了父子进程间的协作机制及环境变量管理。代码实现了基本的命令执行、cd 目录切换、echo 输出及 $? 退出码查询功能,适合希望深入理解操作系统底层交互的开发者参考。
从零开始编写 Linux 自定义 Shell
Shell 是用户与操作系统内核交互的命令行界面。它充当中介,解析用户输入并传递给内核执行。虽然 Bash 是最常用的 Shell,但理解其底层原理对于掌握操作系统至关重要。本章我们将基于 Linux 环境,用 C++ 手写一个简易的 Shell 解释器。
运行原理
要实现一个 Shell,首先要理解它的生命周期。典型的 Shell 交互流程如下:
- 读取输入:从标准输入(键盘)读取一行命令。
- 创建子进程:调用
fork() 创建一个新进程。
- 执行命令:在子进程中通过
exec 系列函数替换当前程序映像。
- 等待退出:父进程调用
waitpid() 等待子进程结束。
- 循环往复:处理完一条命令后,回到第一步继续等待下一条。
这个过程构成了 Shell 的核心骨架。接下来我们一步步填充细节。
实现步骤
为了简化操作,我们采用 C/C++ 混编的方式,主体逻辑使用 C 语言接口,辅以 C++ 的字符串处理能力。所有代码将放在一个 .cc 文件中。
1. 打印提示符
Shell 启动后通常会显示类似 [user@host ~]$ 的提示符。我们需要获取环境变量中的用户名、主机名和当前工作目录来构建这个字符串。
#include <cstdlib>
#include <cstdio>
#include <string>
#define SLASH "/"
const char* GetPwd() {
const char* pwd = getenv("PWD");
return pwd == NULL ? "None" : pwd;
}
const char* GetHostName() {
const char* hostname = getenv();
hostname == ? : hostname;
}
{
* user = ();
user == ? : user;
}
{
std::string dir = pwd;
(dir == SLASH) SLASH;
pos = dir.(SLASH);
(pos == std::string::npos) ;
dir.(pos + );
}
{
(out, size, FORMAT, (), (), (()).());
}
{
prompt[CMDLINE_MAX];
(prompt, CMDLINE_MAX);
(, prompt);
(stdout);
}
"HOSTNAME"
return
NULL
"None"
const char* GetUser()
const
char
getenv
"USER"
return
NULL
"None"
std::string GetPwdDir(const char* pwd)
if
return
auto
rfind
if
return
"BUG"
return
substr
1
#define FORMAT "[%s@%s %s]# "
#define CMDLINE_MAX 1024
void MakeCommandPrompt(char* out, int size)
snprintf
GetUser
GetHostName
GetPwdDir
GetPwd
c_str
void PrintCommandPrompt()
char
MakeCommandPrompt
printf
"%s"
fflush
这里有个小细节:我们在提示符末尾使用了 # 而不是 $,以便区分这是我们的自定义 Shell。同时,利用 getenv 获取环境变量时做了空值检查,防止程序崩溃。
2. 获取命令行参数
Shell 需要持续监听用户的输入。我们使用 fgets 安全地读取一行,并手动去除换行符。
bool GetCommandLine(char* out, int size) {
char* c = fgets(out, size, stdin);
if (c == NULL) return false;
out[strlen(out) - 1] = 0;
if (strlen(out) == 0) return false;
return true;
}
3. 命令行解析
拿到原始字符串后,我们需要将其分割成参数数组(argv)。这里使用 C 语言的 strtok 函数,以空格为分隔符。
#define DELIM " "
#define ARGV_MAX 1024
char* g_argv[ARGV_MAX];
int g_argc = 0;
bool CommandParse(char* commandline) {
g_argc = 0;
g_argv[g_argc++] = strtok(commandline, DELIM);
if (g_argv[0] == NULL) return false;
while ((bool)(g_argv[g_argc++] = strtok(NULL, DELIM)));
g_argc--;
return true;
}
注意 strtok 会修改原字符串,且第一次调用需传入待分割字符串,后续调用传入 NULL。循环结束后记得回退一次 argc,因为最后一次 strtok 返回 NULL 也会触发计数。
4. 执行命令
这是核心部分。我们需要 fork 一个子进程,并在其中执行命令。
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>
int lastcode = 0;
void Execute() {
pid_t id = fork();
if (id < 0) {
perror("fork failed:");
exit(1);
} else if (id == 0) {
execvp(g_argv[0], g_argv);
exit(1);
}
int status;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0) {
lastcode = WEXITSTATUS(status);
}
}
这里选择 execvp 是因为它支持搜索 PATH 环境变量,方便执行系统命令。同时,我们在父进程中捕获了子进程的退出状态,保存给 lastcode 变量,供后续内建命令(如 echo $?)使用。
5. 内建命令
像 cd 或 echo 这样的命令不需要创建子进程,直接在当前 Shell 进程中执行效率更高。我们需要先判断命令类型。
bool IsBuiltInCommand() {
if (!strcmp(g_argv[0], "cd")) {
CommandCd();
return true;
} else if (!strcmp(g_argv[0], "echo")) {
CommandEcho();
return true;
}
return false;
}
cd 命令
cd 涉及目录切换和环境变量更新。我们需要记录旧目录到 OLDPWD,新目录到 PWD。
void CommandCd() {
int ret;
std::string old_dir = get_current_dir_name();
if (g_argc == 1) {
std::string home = GetHome();
if (home.empty()) exit(1);
ret = chdir(home.c_str());
} else {
std::string where = g_argv[1];
if (where == "-") {
ret = chdir(GetOldpwd());
} else if (where == "~") {
ret = chdir(GetHome());
} else {
ret = chdir(where.c_str());
}
}
if (ret == -1) {
perror("cd");
lastcode = 1;
} else {
lastcode = 0;
setenv("PWD", get_current_dir_name(), 1);
setenv("OLDPWD", old_dir.c_str(), 1);
}
}
echo 命令
除了打印文本,还需要支持 $? 查看上一条命令的退出码。
void CommandEcho() {
if (g_argc == 1) {
printf("\n");
} else {
std::string opt = g_argv[1];
if (opt == "$") {
std::cout << lastcode << std::endl;
} 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;
}
}
lastcode = 0;
}
小结
至此,一个具备基本功能的自定义 Shell 就完成了。对比真实的 Shell,它还非常简陋,但核心机制已经跑通。
我们可以把进程和函数的关系类比为程序的调用:exec/exit 就像 call/return。被调用的程序执行操作后通过 exit 返回值,调用者通过 wait 获取结果。这种模式是结构化程序设计的基础,Linux 鼓励将其扩展到进程间通信中。
完整源码
#include <iostream>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>
#define CMDLINE_MAX 1024
#define FORMAT "[%s@%s %s]# "
#define DELIM " "
#define SLASH "/"
#define ARGV_MAX 1024
char* g_argv[ARGV_MAX];
int g_argc = 0;
int lastcode = 0;
void InitEnv() {
extern char** environ;
for (int i = 0; environ[i]; i++) {
putenv(environ[i]);
}
}
const char* GetPwd() {
const char* pwd = getenv("PWD");
return pwd == NULL ? "None" : pwd;
}
const char* GetOldpwd() {
const char* oldpwd = getenv("OLDPWD");
return oldpwd == NULL ? "None" : oldpwd;
}
const char* GetHome() {
const char* home = getenv("HOME");
return home == NULL ? "None" : home;
}
const char* GetHostName() {
const char* hostname = getenv("HOSTNAME");
return hostname == NULL ? "None" : hostname;
}
const char* GetUser() {
const char* user = getenv("USER");
return user == NULL ? "None" : user;
}
std::string GetPwdDir(const char* pwd) {
std::string dir = pwd;
if (dir == SLASH) return SLASH;
auto pos = dir.rfind(SLASH);
if (pos == std::string::npos) return "BUG";
return dir.substr(pos + 1);
}
void MakeCommandPrompt(char* out, int size) {
snprintf(out, size, FORMAT, GetUser(), GetHostName(), GetPwdDir(GetPwd()).c_str());
}
void PrintCommandPrompt() {
char prompt[CMDLINE_MAX];
MakeCommandPrompt(prompt, CMDLINE_MAX);
printf("%s", prompt);
fflush(stdout);
}
bool GetCommandLine(char* out, int size) {
char* c = fgets(out, size, stdin);
if (c == NULL) return false;
out[strlen(out) - 1] = 0;
if (strlen(out) == 0) return false;
return true;
}
bool CommandParse(char* commandline) {
g_argc = 0;
g_argv[g_argc++] = strtok(commandline, DELIM);
if (g_argv[0] == NULL) return false;
while ((bool)(g_argv[g_argc++] = strtok(nullptr, DELIM)));
g_argc--;
return true;
}
void Execute() {
pid_t id = fork();
if (id < 0) {
perror("fork failed:");
exit(1);
} else if (id == 0) {
execvp(g_argv[0], g_argv);
exit(1);
}
int status;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0) {
lastcode = WEXITSTATUS(status);
}
}
void CommandCd() {
int ret;
std::string old_dir = get_current_dir_name();
if (g_argc == 1) {
std::string home = GetHome();
if (home.empty()) exit(1);
ret = chdir(home.c_str());
} else {
std::string where = g_argv[1];
if (where == "-") {
ret = chdir(GetOldpwd());
} else if (where == "~") {
ret = chdir(GetHome());
} else {
ret = chdir(where.c_str());
}
}
if (ret == -1) {
perror("cd");
lastcode = 1;
} else {
lastcode = 0;
setenv("PWD", get_current_dir_name(), 1);
setenv("OLDPWD", old_dir.c_str(), 1);
}
}
void CommandEcho() {
if (g_argc == 1) {
printf("\n");
} else {
std::string opt = g_argv[1];
if (opt == "$") {
std::cout << lastcode << std::endl;
} 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;
}
}
lastcode = 0;
}
bool IsBuiltInCommand() {
if (!strcmp(g_argv[0], "cd")) {
CommandCd();
return true;
} else if (!strcmp(g_argv[0], "echo")) {
CommandEcho();
return true;
}
return false;
}
int main() {
InitEnv();
while (true) {
PrintCommandPrompt();
char commandline[CMDLINE_MAX];
if (!GetCommandLine(commandline, CMDLINE_MAX)) continue;
if (!CommandParse(commandline)) continue;
if (IsBuiltInCommand()) continue;
Execute();
}
return 0;
}
通过这个项目,我们不仅复习了进程控制(fork/exec/wait),还深入理解了环境变量和 Shell 的工作机制。实际开发中,Shell 的处理要复杂得多(信号处理、作业控制等),但这个基础框架足以让我们窥见操作系统交互的本质。
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online