【Linux】进程控制(4)自主shell命令行解释器
这一部分的目标是:自己动手写一个极简的 shell,通过这个过程把之前学过的进程控制知识(fork/exec/wait/signal/管道/重定向等)串联起来。
目标 shell 的功能范围(极简版)
- 显示提示符(如
myshell$) - 读取一行用户输入
- 支持最基本的命令执行(不带参数也可以先支持)
- 支持带参数的命令(空格分隔)
- 支持后台运行(命令末尾有
&) - 支持前台命令的 wait
- 支持 exit / quit 退出
- (可选扩展)支持管道
|、重定向>>><、&&||等
实现思路总览
while (true) {
打印提示符
读取整行输入 → line
如果 line 为空 或 是 exit/quit → break
解析 line → 切分出命令 + 参数数组 argv[]
→ 判断是否有 & (后台)
if (是内置命令,如 cd exit) {
直接在当前进程执行
} else {
pid = fork()
if (pid == 0) { // 子进程
execvp(命令, argv)
perror("exec失败"); exit(1);
} else { // 父进程(shell)
if (不是后台) {
waitpid(pid, &status, 0);
} else {
printf("[后台任务] pid = %d\n", pid);
// 可以选择不 wait,等它自己结束或用 waitpid(-1,...) 回收
}
}
}
}
极简版代码(推荐先实现这个)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>
#define MAX_LINE 1024
#define MAX_ARGS 64
int main(void) {
char line[MAX_LINE];
char *argv[MAX_ARGS];
char prompt[] = "myshell$ ";
while (1) {
printf("%s", prompt);
fflush(stdout);
// 读取整行
if (!fgets(line, MAX_LINE, stdin)) {
break; // ctrl+D
}
// 去掉末尾换行
line[strcspn(line, "\n")] = 0;
// 空行直接继续
if (strlen(line) == 0) continue;
// 退出命令
if (strcmp(line, "exit") == 0 || strcmp(line, "quit") == 0) {
break;
}
// 解析参数
int argc = 0;
char *token = strtok(line, " \t");
int background = 0;
while (token) {
if (strcmp(token, "&") == 0) {
background = 1;
break; // & 后面不再解析
}
argv[argc++] = token;
token = strtok(NULL, " \t");
}
argv[argc] = NULL;
if (argc == 0) continue;
// 内置命令(先简单处理 exit/cd)
if (strcmp(argv[0], "cd") == 0) {
if (argc < 2) {
fprintf(stderr, "cd: 缺少参数\n");
} else if (chdir(argv[1]) != 0) {
perror("chdir");
}
continue;
}
// fork + exec
pid_t pid = fork();
if (pid < 0) {
perror("fork");
continue;
}
if (pid == 0) { // 子进程
execvp(argv[0], argv);
// 走到这里说明 exec 失败
fprintf(stderr, "命令 '%s' 执行失败: %s\n", argv[0], strerror(errno));
exit(127);
}
// 父进程
if (!background) {
int status;
waitpid(pid, &status, 0);
} else {
printf("[后台作业] pid = %d\n", pid);
// 可以不 wait,让它成为孤儿进程,由 init 回收
// 或者后续用 waitpid(-1, ...) 非阻塞回收
}
}
printf("\nbye~\n");
return 0;
}
编译 & 运行
gcc -o myshell myshell.c
./myshell
然后就可以输入:
myshell$ ls -l
myshell$ sleep 10 &
myshell$ pwd
myshell$ cd /tmp
myshell$ exit
进阶功能(按难度递增)
| 优先级 | 功能 | 关键实现点 | 难度 |
|---|---|---|---|
| ★☆☆ | 支持管道 cmd1 | cmd2 | fork 两次 + pipe() + dup2 | ★★★★ |
| ★★☆ | 支持重定向 > >> < | 解析 > >> <,用 open() + dup2 | ★★★ |
| ★★☆ | 回收所有后台进程(waitpid) | 非阻塞 waitpid(-1, &status, WNOHANG) 循环 | ★★ |
| ★★★ | 支持 && || | 根据前一个命令的退出码决定是否执行后一个 | ★★★★ |
| ★★★ | 实现 ! 历史命令 | 保存历史链表,按 !n 执行 | ★★★ |
| ★★★★ | 支持环境变量展开 $HOME | 解析 $ 开头的词,用 getenv() 替换 | ★★★★ |
| ★★★★ | 支持通配符 * ? | 用 glob() 或自己实现匹配 | ★★★★★ |
推荐进阶顺序(比较实用)
- 先把上面的极简版跑通
- 加上后台进程的非阻塞回收(每轮循环末尾试一次 waitpid(-1, …, WNOHANG))
- 实现最简单的重定向(只支持
>和<) - 再实现单级管道(最有成就感)
小提示
- 解析命令行时建议用
strtok_r()(可重入版本)更安全 - 处理输入时考虑连续空格、tab、行首尾空格
- execvp() 失败时退出码常用 127(command not found)
- 后台进程结束时如果不回收,会出现僵尸进程(zombie)
如果你现在想继续做这个项目,可以告诉我你想先实现哪个功能(管道、重定向、后台回收、历史命令……),我可以给你对应的核心代码片段和注意事项。
祝你写出一个属于自己的 shell~