跳到主要内容C++ 与 Linux 基础:用 C++ 手写简易 Shell | 极客日志C++
C++ 与 Linux 基础:用 C++ 手写简易 Shell
使用 C++ 在 Linux 环境下从零实现一个简单的 Shell 程序。内容涵盖 Shell 的基本概念、内核与用户态的关系,重点讲解了 exec 系列系统调用函数(execl、execv、execvp、execvpe)的原理与用法。通过 fork 创建子进程、wait 等待子进程结束,实现了命令提示符打印、用户指令读取解析以及命令执行的核心功能。代码示例展示了如何构建参数数组、处理环境变量及格式化输出,帮助读者理解进程控制与系统编程的基础知识。
人间失格1 浏览 C++ 与 Linux 基础:用 C++ 手写简易 Shell
1. 知识准备:实战之前必须的储备
1-0 小前言
在 Linux 世界里,Shell 是我们最熟悉的陌生人。它既不是黑魔法,也不是操作系统本身,它只是一个运行在用户态的程序。它的工作就像一个不知疲倦的'传声筒':读取你的指令 -> 翻译给内核 -> 把结果拿回来给你。今天,我们就剥开这层'壳',用 C++ 从零实现一个简单的 Shell,看看 ls、cd 这些命令背后到底发生了什么。
1-1 Shell 是什么,是什么样的形式
Shell 的中文意思是'壳'。想象一下坚果或者鸡蛋:
- 核心(Kernel):坚果的果肉(或蛋黄)。这是操作系统最核心的部分,直接管理 CPU、内存、硬盘等硬件,它非常复杂、敏感,普通用户不能也不敢直接去操作它,否则很容易把系统搞崩。
- 外壳(Shell):坚果的硬壳(或蛋壳)。它包裹在核心外面,保护核心。
- 用户(User):我们在壳外面。
结论:Shell 就是包裹在操作系统内核(Kernel)外面的一层'软件壳'。它是用户和内核之间的翻译官。
- 用户名
- 主机名
- 当前工作目录
- 提示符符号(如
* 或 $)
随后在同一行去写我们的命令,比如 ls 或者 cd 之类的命令。
1-2 必备的函数 exec 系列函数
int execl(const char *path, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execve(const char *path, char *const argv[], char *const envp[]);
int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);
这些都是 exec 系列的函数,它是做什么的?在这之前我们已经讲过了 fork 这个函数,如果说 fork() 是'分身术',让一个进程复制出一个一模一样的自己;那么 exec 系列函数就是'夺舍'(或者叫'大脑移植')。
为什么这么说?当一个进程调用 exec 函数时,它的身体(PCB、进程 ID)还是原来的,但是灵魂(代码段、数据段、堆栈)被完全替换了。它不再执行原来的程序,而是开始执行新的程序。
一句话总结:exec 只有去,没有回。一旦调用成功,原来的后续代码就永远不会被执行了(因为被抹除了)。
简而言之,就是替换原来的程序的进程来执行新的命令,利用这个我们就可以来完成 shell 的功能:你写什么它就运行什么。
1-2-1 execl 函数:exec + l 例子精讲
先拿 int execl(const char *path, const char *arg, ...) 来简单做个例子。
#include <stdio.h>
#include <unistd.h>
int main() {
printf("这是我的进程\n");
execl("/bin/ls", "ls", "-a", "-l", NULL);
perror("execl failed");
printf("进程结束,如果没被替换,你就可以看到这个代码\n");
return 0;
}
可以看到的确出现了 ls -a -l 的命令,这就巧妙地利用这个接口,完成了进程的接环。
1-2-2 execv 函数:exec + v
这里的 v 就是指数组,这里我们不需要再像之前一样,只需传入数组指针就可以完成对指令的传递。我们需要把自己需要的指令放在一个数组里面,这样就可以了:
#include <unistd.h>
#include <stdio.h>
int main() {
printf("Before exec\n");
char *args[] = {"ls", "-l", "-a", NULL};
int ret = execv("/bin/ls", args);
perror("execv failed");
return 1;
}
在有图片中,我们可以清楚地看到我的程序原本是要打印后面的话的,但是并没有执行,这也是同样因为 execv() 这个接口,切换了进程。
1-2-3 execvp 函数 尾部再加一个 p
以数组举例,l 结尾的加一个 p 也是一样的效果:重点体验不一样的效果:有了这个 p(其实就是 path,这样就不用加 /bin/ls,直接使用 ls),这个也是比较方便的:
#include <unistd.h>
#include <stdio.h>
int main() {
char *args[] = {"ls", "-l", NULL};
printf("尝试运行 ls ...\n");
if (execvp("ls", args) == -1) {
perror("execvp 失败");
return 1;
}
return 0;
}
1-2-4 execvpe 函数(最全面)
还加上了一个关键词 e,这个就是 env,e (Environment):你可以传入一个新的环境变量数组 (envp[]),而不是继承当前进程的环境变量。
#define _GNU_SOURCE
#include <unistd.h>
int execvpe(const char *file, char *const argv[], char *const envp[]);
#define _GNU_SOURCE
#include <unistd.h>
#include <stdio.h>
int main() {
char *args[] = {"printenv", NULL};
char *new_env[] = {"MY_NAME=WWH", "HOME=/tmp", NULL};
printf("正在运行 printenv,并替换环境变量...\n");
execvpe("printenv", args, new_env);
perror("execvpe failed");
return 1;
}
一句话总结就是帮我在 PATH 里找这个程序,用我给你的参数数组 (v) 运行它,并且强行指定它的环境变量 (e)。
1-2-5 总结
| 后缀 | 含义 | 作用 |
|---|
| l | List | 参数一个个列出来 |
| v | Vector | 参数放数组里 |
| p | Path | 在 $PATH 里找程序 |
| e | Environment | 自定义环境变量 |
2. 主要功能 1:打出指定的格式
2-1 三个 Get 函数,获取信息
这里可以看到和之前在前言部分介绍的一样,我们只要利用我们的程序来完成打印这个一行命令就可以了:我们需要准备 3 个函数:
const char* GetUserName() {
char* name = getenv("USER");
return name == nullptr ? "None" : name;
}
第一个函数是查询进程的用户名字,由于我们这个进程是从父进程来的,所以是具有这些环境变量的,如果是空就返回 None。
剩下的两个也是这个意思:
const char* GetHostName() {
char* name = getenv("HOSTNAME");
return name == nullptr ? "None" : name;
}
const char* GetPwd() {
char* name = getenv("PWD");
return name == nullptr ? "None" : name;
}
这样我们三个变量就都搞到了。接下来就是打印指定的格式,为了区分我们的和 shell 的我们还做出一下的改变,比如加入 [] 和 *:
2-2 打印 shell,启动进程
void PrintCommand() {
printf("[%s@%s %s]* ", GetUserName(), GetHostName(), GetPwd());
fflush(stdout);
}
由于我们后面需要经常编译和清理:我们还可以尝试写一个 makefile 来帮助我们来完成本次的编译和删除:
MyShell: MyShell.cc
g++ -o $@ $^ -std=c++11
.PHONY: clean
clean:
rm -f MyShell
目前看到这个程序是的域名是空的,这是为什么呢?简单的说:HOSTNAME 通常只是 Shell 内部的一个私有变量,并没有被'导出' (export) 成环境变量,所以你的 C 程序看不到它。所以我们后面需要执行下面这个部分的命令:
随后在开始运行,我们可以看到一切正常,没问题。最后,我们在改进一下,让代码更好看点:
2-3 开始改进代码,复用性提高
void MakeCommand(char* cwd_prompt, int size) {
snprintf(cwd_prompt, size, "%s@%s %s* ", GetUserName(), GetHostName(), GetPwd());
}
void PrintCommand() {
char prompt[COMMAND_SIZE];
MakeCommand(prompt, sizeof(prompt));
printf("%s", prompt);
fflush(stdout);
}
这样就完成了对输出一个 shell 提示已经完成了。
3. 主要功能 2:获取用户指令
在我们之前讲的 exec 系列函数可以来帮助我们来实现这次的获取函数指令:我们可以看到用户从键盘中输出的时候,我们可以尝试使用空格来分割:
我们先利用 fgets 函数来完成对键盘命令的提取,将指定大小字节的缓冲区写入指定的字符串中:
bool GetCommandLine(char* out, int size) {
char* c = fgets(out, size, stdin);
if (c == nullptr) return false;
out[strlen(out) - 1] = 0;
if (strlen(out) == 0) return false;
return true;
}
这段代码就可以帮我们获取从用户中获得的指令,并且把 \n(换行)变成 \0,这样就的到了一个比较合适的字符串。接下来第二个步骤就是完成对空格的切割,完成命令表的构建:
#define SPE " "
bool CommandParse(char* command) {
g_argc = 0;
g_argv[g_argc++] = strtok(command, SPE);
while ((bool)(g_argv[g_argc++] = strtok(nullptr, SPE)));
g_argc--;
return true;
}
这里就是完成了对于字符串的切割,并且装入指定的命令表。记住最后要对 g_argc 进行减减,比如我们输入 ls -a 此时,真实的参数只有 2 个 (ls 和 -a),但是你的计数器 g_argc 却是 3。这是因为那个 ++ 操作是在赋值之后立刻执行的,它不知道刚才赋进去的是 NULL。它把最后的'结束符'也当成了一个参数算进去了。所以最后面还需要减减。
4. 主要功能 3:利用子进程执行
int Execute() {
pid_t id = fork();
if (id == 0) {
execvp(g_argv[0], g_argv);
exit(1);
}
pid_t rid = waitpid(id, nullptr, 0);
(void)rid;
return 0;
}
利用这个函数我们就可以完成执行,接下来我们来看一下这个主函数和执行结果:
int main() {
while (true) {
PrintCommand();
char commandline[COMMAND_SIZE];
GetCommandLine(commandline, sizeof(commandline));
CommandParse(commandline);
Execute();
}
return 0;
}
总结
其实这个 shell 还是不正确的,主要问题出在 cd 的命令上面,你可以试试。在这个这里面我们利用上面的知识点完成了对简易的 shell 工具进行了开发。
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- 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