1.知识准备
1-0 小前言
在 Linux 世界里,Shell 是用户最熟悉的程序之一。它既不是黑魔法,也不是操作系统本身,只是一个运行在用户态的程序。它的工作像一个传声筒:读取指令 -> 翻译给内核 -> 把结果拿回来。今天,我们用 C++ 从零实现一个简单的 Shell。
1-1 Shell 是什么
Shell 的中文意思是'壳'。想象一下坚果或者鸡蛋:
- 核心(Kernel):坚果的果肉。这是操作系统最核心的部分,直接管理 CPU、内存等硬件。
- 外壳(Shell):坚果的硬壳。包裹在核心外面,保护核心。
- 用户(User):我们在壳外面。 结论:Shell 就是包裹在操作系统内核外面的一层软件壳。它是用户和内核之间的翻译官。
1-2 必备的 exec 系列函数
exec 系列函数用于替换当前进程的映像。如果说 fork() 是分身术,那么 exec 系列函数就是夺舍。当一个进程调用 exec 函数时,它的身体(PCB、进程 ID)还是原来的,但是灵魂(代码段、数据段、堆栈)被完全替换了。一句话总结:exec 只有去,没有回。
1-2-1 execl 函数
#include <stdio.h>
#include <unistd.h>
int main() {
printf("这是我的进程\n");
// 参数列表以 NULL 结尾
execl("/bin/ls", "ls", "-a", "-l", NULL);
// 如果 execl 成功,下面这行永远不会执行
perror("execl failed");
return 0;
}
1-2-2 execv 函数
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;
}
1-2-3 execvp 函数
尾部加一个 p,表示在 PATH 环境变量中查找程序,不需要写完整路径。
#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 代表 Environment,可以传入自定义环境变量数组。
#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;
}
1-2-5 总结
| 后缀 | 含义 | 作用 |
|---|---|---|
| l | List | 参数一个个列出来 |
| v | Vector | 参数放数组里 |
| p | Path | 在 $PATH 里找程序 |
| e | Environment | 自定义环境变量 |
2.主要功能 1:打出指定的格式
2-1 三个 Get 函数,获取信息
利用 getenv 获取环境变量。
const char* GetUserName() {
char* name = getenv("USER");
return name == nullptr ? "None" : name;
}
const char* GetHostName() {
char* name = getenv("HOSTNAME");
return name == nullptr ? "None" : name;
}
const char* GetPwd() {
char* name = getenv("PWD");
return name == nullptr ? "None" : name;
}
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
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);
}
3.主要功能 2:获取用户指令
使用 fgets 从键盘读取,并用 strtok 分割命令。
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;
}
#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;
}
4.主要功能 3:利用子进程执行
int Execute() {
pid_t id = fork();
if (id == 0) {
// child
execvp(g_argv[0], g_argv);
exit(1);
}
// father wait
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 等内置命令的处理。通过 fork 和 exec 系列函数,我们完成了对简易 Shell 工具的开发。


