Day 8 实现简易Shell
一、什么是 Shell
在 Linux 中,Shell 本质上是一个用户态程序,它的核心职责只有一件事:
读取用户输入的命令,将其解析为程序执行请求,并通过内核机制创建和管理进程。
从内核视角看,Shell 并不特殊:
- 它不是系统调用
- 它不运行在内核态
- 它和
ls、cat一样,都是普通进程
Shell 的“特殊性”仅在于:
- 它长期运行
- 它频繁调用 fork / exec / wait
- 它充当用户与内核之间的交互中介
因此可以说:
Shell 是一个以“进程管理”为核心逻辑的普通程序。
二、一个「简易 Shell」需要实现哪些功能
为了避免目标失控,我们先定义一个教学级、可控范围内的简易 Shell。
这个 Shell 不追求功能全面,而追求概念完整、语义正确。
1️⃣ 命令读取(Command Reading)
Shell 首先要做的,是从标准输入中读取一行用户命令,例如:
ls -l /tmp
这一步的技术本质是:
- 从
stdin读取一行文本 - 正确处理换行符
- 正确处理 EOF(Ctrl+D)
常见手段包括:
getline()(推荐,自动扩容)read()(更底层,需自行管理缓冲区)
这是 Shell 与用户交互的起点。
2️⃣ 参数解析(Argument Parsing)
Shell 不能把整行字符串直接交给内核,它必须将命令拆解为:
argv[0] = "ls"
argv[1] = "-l"
argv[2] = "/tmp"
argv[3] = NULL
这一步的本质是:
把“文本命令”转换为 exec 系列函数所需的参数数组。
在简易 Shell 中,解析规则可以非常简单:
- 以空格分割
- 不考虑引号、不考虑转义、不考虑变量替换
但即便如此,这一步也清晰地区分了:
- Shell 层面的语法处理
- 内核层面的进程执行
3️⃣ 创建进程并执行命令(fork + exec)
这是 Shell 的核心执行路径:
- Shell 调用
fork() - 子进程调用
execvp() - 父进程调用
wait()或waitpid()
这一步体现了 Unix 设计中最重要的原则之一:
进程创建(fork)与程序执行(exec)是分离的。
4️⃣ 内建命令(Built-in Commands)
并非所有命令都能用 exec 实现。
例如:
cd /home
exit
这些命令的特点是:
- 必须影响 Shell 自身的状态
- 如果放到子进程中执行,将毫无意义
因此,一个简易 Shell 至少需要实现:
cdexit
这一步让你真正理解:
为什么 Shell 不能只是 fork + exec 的包装器。
5️⃣ 基础重定向与管道(核心进阶)
为了体现 Shell 的“操作系统味道”,简易 Shell 通常还会支持:
- 单个管道:
ls | wc -l - 基本重定向:
>、<
这背后的技术核心是:
pipedup2- 文件描述符的继承与关闭
这一步把你前面学过的 fd / pipe / dup2 串成一个完整系统。
三、循序渐进的技术路线图(强烈推荐)
下面是一条完全可执行、不会跳跃、每一步都有“完成感”的路线图。
🚀 Stage 1:最小 Shell 骨架(基础必做)
目标:
让 Shell 能执行最简单的外部命令。
你需要完成:
- 循环读取一行命令(getline)
- 将命令拆分为 argv(空格分割)
- fork
- execvp
- wait
完成标志:
myshell$ ls
myshell$ ps
代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int split_by_space(char *line, char *argv[], int max_argv);
int main(int, char **)
{
char *line = NULL;
size_t size = 0;
ssize_t nread;
while (1)
{
printf("mysh>");
fflush(stdout);
nread = getline(&line, &size, stdin);
if (nread == -1)
{
printf("\n");
break;
}
if (line[nread - 1] == '\n')
{
line[nread - 1] = '\0';
}
if (line[0] == '\0')
{
continue;
}
char *argv[64];
split_by_space(line, argv, 64);
pid_t pid = fork();
if (pid == 0)
{
execvp(argv[0], argv);
perror("exec");
exit(1);
}
wait(NULL);
}
free(line);
return 0;
}
int split_by_space(char *line, char *argv[], int max_argv)
{
int argc = 0;
char *token;
char *saveprt = NULL;
token = strtok_r(line, " ", &saveprt);
while (token != NULL && argc < max_argv)
{
// printf("token is:%s\n", token);
argv[argc++] = token;
token = strtok_r(NULL, " ", &saveprt);
}
argv[argc] = NULL;
return argc;
}
🚀 Stage 2:内建命令与健壮性
目标:
让 Shell 行为“像一个真正的 Shell”。
你需要完成:
cd(父进程中执行)exit- exec 失败处理
- Ctrl+D(EOF)退出
完成标志:
myshell$ cd /tmp
myshell$ exit
🚀 Stage 3:管道(pipe + dup2)
目标:
实现最经典的 Unix 管道模型。
你需要完成:
- 单管道解析
- pipe 创建
- dup2 重定向 stdin/stdout
- 正确关闭 fd
完成标志:
myshell$ ls | wc -l
🚀 Stage 4:重定向(输入 / 输出)
目标:
理解 fd 与文件的统一抽象。
你需要完成:
>输出重定向<输入重定向- 与 exec 配合
完成标志:
myshell$ ls > out.txt
myshell$ wc -l < out.txt
🚀 Stage 5(可选):信号与后台执行
进阶方向,可自由选择:
- Ctrl+C 不杀 shell
&后台执行- SIGCHLD 回收子进程

浙公网安备 33010602011771号