支持后台运行的Minishell实现
一、具体代码
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
char cmd[100];
char *args[10];
// 回收已经退出的子进程(非阻塞)
while (waitpid(-1, NULL, WNOHANG) > 0) {
// 自动回收僵尸进程
}
while (1) {
printf("> ");
fflush(stdout);
if (fgets(cmd, sizeof(cmd), stdin) == NULL) break;
// 解析命令
int i = 0;
args[i] = strtok(cmd, " \t\n");
while (args[i]) args[++i] = strtok(NULL, " \t\n");
if (!args[0]) continue;
if (strcmp(args[0], "quit") == 0) break;
else if (strcmp(args[0], "cd") == 0) {
if (args[1]) {
if (chdir(args[1]) != 0) perror("cd");
}
}
else {
pid_t pid = fork();
if (pid == 0) {
// 子进程
execvp(args[0], args);
perror(args[0]);
exit(1);
}
else if (pid > 0) {
// 父进程
// 这里可以立即进行非阻塞回收
int status;
pid_t ret = waitpid(pid, &status, WNOHANG);
if (ret == 0) {
printf("命令 %s 正在后台运行 (PID: %d)\n", args[0], pid);
}
else if (ret == pid) {
if (WIFEXITED(status)) {
printf("命令执行完成,退出码: %d\n", WEXITSTATUS(status));
}
}
}
}
}
// 等待所有子进程结束
while (waitpid(-1, NULL, 0) > 0);
return 0;
}
主要特点:
-
支持前台命令(阻塞等待)
-
支持后台命令(非阻塞执行)
-
自动回收僵尸进程
-
实现了基本的shell功能
二、逐行代码解释
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
char cmd[100]; // 存储用户输入的命令
char *args[10]; // 存储解析后的参数
// 回收已经退出的子进程(非阻塞)
while (waitpid(-1, NULL, WNOHANG) > 0) {
// 自动回收僵尸进程
}
1. 启动时清理僵尸进程
-
waitpid(-1, NULL, WNOHANG):非阻塞回收所有子进程 -
> 0:有子进程被回收 -
循环直到没有僵尸进程
while (1) {
printf("> ");
fflush(stdout); // 立即刷新输出缓冲区
// 读取用户输入
if (fgets(cmd, sizeof(cmd), stdin) == NULL) break;
2. 显示提示符和读取命令
-
fgets():安全读取一行输入 -
如果返回
NULL(如按Ctrl+D),则退出循环
// 解析命令
int i = 0;
args[i] = strtok(cmd, " \t\n"); // 第一次调用
while (args[i]) args[++i] = strtok(NULL, " \t\n"); // 后续调用
if (!args[0]) continue; // 空行,继续循环
3. 命令解析
-
strtok():按空格、制表符、换行符分割字符串 -
结果存储在
args数组中 -
args[0]是命令名,后续是参数
if (strcmp(args[0], "quit") == 0) break;
else if (strcmp(args[0], "cd") == 0) {
if (args[1]) {
if (chdir(args[1]) != 0) perror("cd");
}
}
4. 内置命令处理
-
quit:直接退出循环
-
cd :使用
chdir()改变当前工作目录 -
perror("cd"):如果chdir()失败,显示错误信息
else {
pid_t pid = fork(); // 创建子进程
if (pid == 0) {
// 子进程
execvp(args[0], args); // 执行命令
perror(args[0]); // 如果execvp失败
exit(1); // 子进程异常退出
}
else if (pid > 0) {
// 父进程 - 关键:非阻塞回收
int status;
pid_t ret = waitpid(pid, &status, WNOHANG);
5. 执行外部命令
-
fork():创建子进程 -
子进程:
execvp()执行命令 -
父进程:
waitpid(pid, &status, WNOHANG)非阻塞检查子进程状态
if (ret == 0) {
printf("命令 %s 正在后台运行 (PID: %d)\n", args[0], pid);
}
else if (ret == pid) {
if (WIFEXITED(status)) {
printf("命令执行完成,退出码: %d\n", WEXITSTATUS(status));
}
}
}
}
}
6. 非阻塞回收的状态判断
-
ret == 0:子进程还在运行,转为后台运行 -
ret == pid:子进程已结束,立即回收 -
WIFEXITED(status):检查是否正常退出 -
WEXITSTATUS(status):获取退出码
// 等待所有子进程结束
while (waitpid(-1, NULL, 0) > 0);
return 0;
}
7. 程序退出前清理
-
阻塞等待所有子进程结束
-
防止留下僵尸进程
三、关键知识点整理
1. 非阻塞回收的三种情况
pid_t ret = waitpid(pid, &status, WNOHANG);
// 情况1: ret > 0 (子进程已结束)
if (ret == pid) {
// 子进程已结束,可以获取状态
}
// 情况2: ret == 0 (子进程仍在运行)
if (ret == 0) {
// 子进程还在运行,可以做其他事情
}
// 情况3: ret == -1 (错误或没有子进程)
if (ret == -1) {
// 处理错误
}
2. 后台运行与前台运行的区别
| 特性 | 前台命令 | 后台命令 |
|---|---|---|
| wait方式 | 阻塞等待 | 非阻塞检查 |
| 用户交互 | 需要等待完成 | 立即返回提示符 |
| 示例 | ls |
sleep 10 &(这里没有&符号但效果类似) |
| 输出 | 直接显示 | 可能和用户输入交错 |
3. 僵尸进程处理策略
// 策略1: 启动时清理(代码开头)
while (waitpid(-1, NULL, WNOHANG) > 0);
// 策略2: 命令执行后立即非阻塞回收
waitpid(pid, &status, WNOHANG);
// 策略3: 退出前阻塞清理(代码结尾)
while (waitpid(-1, NULL, 0) > 0);
4. 这个实现的限制
-
没有真正的后台符号
&:-
所有命令都尝试非阻塞回收
-
如果命令执行很快(如
ls),可能立即完成 -
如果命令执行慢(如
sleep 5),才显示"后台运行"
-
-
输出可能混乱:
-
后台命令的输出可能与用户输入交错
-
没有进行输出重定向
-
-
不支持管道和重定向
5. 改进建议
// 添加后台运行符号&支持
if (args[i-1] && strcmp(args[i-1], "&") == 0) {
args[i-1] = NULL; // 移除&符号
// 使用非阻塞回收
waitpid(pid, &status, WNOHANG);
} else {
// 前台命令,阻塞等待
waitpid(pid, &status, 0);
}
// 添加信号处理避免僵尸进程
signal(SIGCHLD, SIG_IGN); // 忽略SIGCHLD,系统自动回收
四、测试示例
# 编译运行
$ gcc -o minishell minishell.c
$ ./minishell
# 测试场景1: 快速命令(可能立即完成)
> ls
file1.txt file2.txt
命令执行完成,退出码: 0
# 测试场景2: 慢速命令(转为后台)
> sleep 5
命令 sleep 正在后台运行 (PID: 12345)
> # 立即显示新提示符,可以继续输入命令
# 测试场景3: 内置命令
> cd /tmp
> pwd
/tmp
五、学习要点
-
非阻塞回收的应用场景:实现后台任务、避免程序阻塞
-
waitpid的灵活使用:通过选项控制等待行为
-
僵尸进程的预防:多种回收策略结合
-
Shell的基本架构:读-解析-执行循环