【Gemini生物识别集成实战指南】:20年安全架构师亲授5大避坑法则与3步零故障上线秘籍
2026/5/31 19:19:01
#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) { // 自动回收僵尸进程 }waitpid(-1, NULL, WNOHANG):非阻塞回收所有子进程
> 0:有子进程被回收
循环直到没有僵尸进程
while (1) { printf("> "); fflush(stdout); // 立即刷新输出缓冲区 // 读取用户输入 if (fgets(cmd, sizeof(cmd), stdin) == NULL) break;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; // 空行,继续循环
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"); } }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);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)); } } } } }ret == 0:子进程还在运行,转为后台运行
ret == pid:子进程已结束,立即回收
WIFEXITED(status):检查是否正常退出
WEXITSTATUS(status):获取退出码
// 等待所有子进程结束 while (waitpid(-1, NULL, 0) > 0); return 0; }
阻塞等待所有子进程结束
防止留下僵尸进程
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) { // 处理错误 }| 特性 | 前台命令 | 后台命令 |
|---|---|---|
| wait方式 | 阻塞等待 | 非阻塞检查 |
| 用户交互 | 需要等待完成 | 立即返回提示符 |
| 示例 | ls | sleep 10 &(这里没有&符号但效果类似) |
| 输出 | 直接显示 | 可能和用户输入交错 |
// 策略1: 启动时清理(代码开头) while (waitpid(-1, NULL, WNOHANG) > 0); // 策略2: 命令执行后立即非阻塞回收 waitpid(pid, &status, WNOHANG); // 策略3: 退出前阻塞清理(代码结尾) while (waitpid(-1, NULL, 0) > 0);
没有真正的后台符号&:
所有命令都尝试非阻塞回收
如果命令执行很快(如ls),可能立即完成
如果命令执行慢(如sleep 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的基本架构:读-解析-执行循环