一、exec 族函数:进程的 “程序替换” 神器
1.1 核心功能
exec 族函数的核心作用是替换当前进程的代码段、数据段、堆、栈—— 执行 exec 后,进程的 PID 不变,但运行的程序会被完全替换为新的可执行文件;若 exec 执行成功,原进程的后续代码不会执行;若执行失败,才会继续执行原进程代码。
exec 族函数通常与 fork 搭配使用:父进程 fork 创建子进程,子进程执行 exec 替换为目标程序,既保证父进程不被替换,又能通过子进程执行任意可执行文件。
1.2 内存视角的变化
| 阶段 | 进程内存状态 |
|---|---|
| exec 执行前 | 进程运行原程序的代码段、数据段、堆、栈 |
| exec 执行后 | 原内存区域被新程序覆盖,仅保留 PID、文件描述符等内核态信息 |
| 新程序执行结束 | 整个进程终止(无需返回原程序) |
1.3 exec 族 4 个核心函数
exec 族函数有多个变体,核心差异在于参数形式和程序路径查找方式,以下是最常用的 4 个函数:
| 函数原型 | 核心特点 | 参数说明 |
|---|---|---|
int execl(const char *path, const char *arg, ...); | l=list(参数列表),需指定程序绝对 / 相对路径 | path:程序路径 + 文件名(如/bin/ls);arg:参数列表,以 NULL 结尾(如execl("/bin/ls", "ls", "-l", NULL)) |
int execlp(const char *file, const char *arg, ...); | l=list,p=PATH(自动从环境变量 PATH 查找程序) | file:程序名(如ls),无需写路径;arg:参数列表,以 NULL 结尾(如execlp("ls", "ls", "-l", NULL)) |
int execv(const char *path, char *const argv[]); | v=vector(数组),需指定程序路径 | path:程序路径 + 文件名;argv:参数数组,最后一个元素为 NULL(如char *argv[] = {"ls", "-l", NULL}; execv("/bin/ls", argv)) |
int execvp(const char *file, char *const argv[]); | v=vector,p=PATH(自动查找程序) | file:程序名;argv:参数数组,以 NULL 结尾(如char *argv[] = {"ls", "-l", NULL}; execvp("ls", argv)) |
共性说明
- 返回值:仅执行失败时返回 - 1(成功则无返回);
- 参数规则:第一个参数(arg/argv [0])通常是程序名(与 file/path 一致),后续为程序的运行参数;
- 路径规则:若要执行自定义可执行程序(非系统命令),4 个函数的第一个参数都需填写路径 + 文件名(如
./myprog)。
示例:execlp 执行 ls 命令
c
运行
#include <unistd.h> #include <stdio.h> int main() { // 子进程执行ls -l,自动从PATH查找ls if (fork() == 0) { execlp("ls", "ls", "-l", NULL); perror("execlp failed"); // 仅exec失败时执行 return 1; } return 0; }二、waitpid:子进程资源的 “回收者”
当子进程终止(正常 / 异常),其用户空间内存会释放,但内核中的 PCB(进程控制块)需父进程主动回收,否则会变成僵尸进程。waitpid 是回收子进程资源的核心函数,也是 wait 的增强版。
2.1 函数原型与参数
c
运行
#include <sys/wait.h> pid_t waitpid(pid_t pid, int *status, int options);| 参数 | 取值与含义 |
|---|---|
| pid | -1:回收所有子进程;>0:回收指定 PID 的子进程;0:回收同组的子进程;<-1:回收指定进程组的子进程 |
| status | 存储子进程退出状态(不关心则传 NULL);可通过宏解析状态(见 2.3) |
| options | 0:阻塞模式(父进程等待子进程终止);WNOHANG:非阻塞模式(无子进程终止则立即返回) |
2.2 返回值说明
| 返回值 | 含义 |
|---|---|
| >0 | 成功回收的子进程 PID |
| 0 | WNOHANG 模式下,无子进程终止(需再次尝试回收) |
| -1 | 回收失败(如无待回收的子进程、系统错误) |
2.3 退出状态解析宏
通过以下宏可解析 status 参数,判断子进程终止方式:
| 宏 | 功能 | 配套使用 |
|---|---|---|
| WIFEXITED(status) | 判断是否正常终止(return/exit/_exit) | 是:WEXITSTATUS (status) 获取退出码 |
| WEXITSTATUS(status) | 获取正常终止的退出码(0-255) | 仅 WIFEXITED 为真时有效 |
| WIFSIGNALED(status) | 判断是否被信号异常终止(如 kill -9) | 是:WTERMSIG (status) 获取信号编号 |
| WTERMSIG(status) | 获取终止子进程的信号编号 | 仅 WIFSIGNALED 为真时有效 |
2.4 核心用法示例
示例 1:阻塞回收指定子进程
c
运行
#include <sys/wait.h> #include <unistd.h> #include <stdio.h> int main() { pid_t pid = fork(); if (pid == 0) { execlp("ls", "ls", NULL); exit(1); } // 阻塞等待pid对应的子进程终止 int status; waitpid(pid, &status, 0); if (WIFEXITED(status)) { printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status)); } return 0; }示例 2:非阻塞回收所有子进程
c
运行
#include <sys/wait.h> #include <unistd.h> #include <stdio.h> int main() { // 创建多个子进程... while (1) { pid_t ret = waitpid(-1, NULL, WNOHANG); if (ret == 0) { printf("暂无子进程退出,稍后重试\n"); usleep(100000); // 100ms后重试 } else if (ret == -1) { printf("所有子进程已回收\n"); break; } else { printf("回收子进程PID:%d\n", ret); } } return 0; }等价关系
waitpid(-1, status, 0)完全等价于wait(status)—— 阻塞回收任意子进程。
三、system 函数:fork+exec 的 “便捷封装”
system 函数是对 fork+exec+waitpid 的封装,可直接执行 shell 命令,无需手动处理进程创建和回收。
3.1 函数原型与功能
c
运行
#include <stdlib.h> int system(const char *command);- 功能:执行指定的 shell 命令(如
system("ls -l")); - 实现逻辑:
- fork 创建子进程;
- 子进程执行 exec 调用 shell(如 /bin/sh)执行 command;
- 父进程 waitpid 等待子进程终止。
3.2 返回值
- -1:fork/exec 失败;
- 其他值:子进程的退出状态(可通过 waitpid 的宏解析)。
3.3 使用限制
system 执行的命令无法修改父进程状态(如 cd 命令)—— 因为命令在子进程中执行,子进程的目录切换、环境变量修改等操作不会影响父进程。
适合场景:执行信息输出、文件操作等无状态修改的命令(如 ls、cp、cat);不适合场景:需要修改父进程状态的操作(如 cd、export)。
3.4 示例:system 执行 cp 命令
c
运行
#include <stdlib.h> int main() { // 执行cp 1.txt 2.txt int ret = system("cp 1.txt 2.txt"); if (ret == -1) { perror("system failed"); } return 0; }四、工作路径控制:getcwd 与 chdir
在 Shell、进程管理场景中,获取 / 修改当前工作路径是高频操作,核心依赖 getcwd 和 chdir 函数。
4.1 获取当前工作路径:getcwd
c
运行
#include <unistd.h> char *getcwd(char *buf, size_t size);- 功能:将当前工作目录的绝对路径存入 buf;
- 参数:
- buf:存储路径的字符数组;
- size:buf 的最大长度(避免越界);
- 返回值:成功返回 buf 指针,失败返回 NULL。
示例:打印当前工作路径
c
运行
#include <unistd.h> #include <stdio.h> int main() { char buf[512]; if (getcwd(buf, sizeof(buf)) != NULL) { printf("当前工作路径:%s\n", buf); } else { perror("getcwd failed"); } return 0; }4.2 切换工作路径:chdir
c
运行
#include <unistd.h> int chdir(const char *path);- 功能:修改当前进程的工作路径;
- 参数:path 为目标路径(绝对 / 相对路径);
- 返回值:成功返回 0,失败返回 - 1。
示例:切换到 /tmp 目录
c
运行
#include <unistd.h> #include <stdio.h> int main() { if (chdir("/tmp") == 0) { printf("切换到/tmp成功\n"); // 验证:打印新路径 char buf[512]; getcwd(buf, sizeof(buf)); printf("新工作路径:%s\n", buf); } else { perror("chdir failed"); } return 0; }关键注意点
chdir 仅修改当前进程的工作路径:
- 若在子进程中执行 chdir,父进程的路径不会变化;
- Shell 的 cd 命令必须在主进程执行(而非子进程),否则路径切换不生效。
五、实战:fork+exec+waitpid 实现 MiniShell 核心
结合以上知识点,实现支持 cd/ls/cp/cat 的 MiniShell 核心逻辑(无 system,纯 fork+exec):
c
运行
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/wait.h> #define MAX_LINE 1024 #define MAX_ARGS 10 int parse_cmd(char *line, char **argv) { int argc = 0; char *token = strtok(line, " \n"); while (token != NULL && argc < MAX_ARGS-1) { argv[argc++] = token; token = strtok(NULL, " \n"); } argv[argc] = NULL; return argc; } void exec_cmd(char **argv) { pid_t pid = fork(); if (pid < 0) { perror("fork failed"); return; } if (pid == 0) { execvp(argv[0], argv); perror("command not found"); exit(1); } else { waitpid(pid, NULL, 0); } } int main() { char line[MAX_LINE], *argv[MAX_ARGS], cwd[512]; while (1) { // 打印带当前路径的提示符 getcwd(cwd, sizeof(cwd)); printf("%s > ", cwd); fflush(stdout); if (fgets(line, MAX_LINE, stdin) == NULL) break; int argc = parse_cmd(line, argv); if (argc == 0) continue; // 内置命令:exit if (!strcmp(argv[0], "exit")) exit(0); // 内置命令:cd(主进程执行) if (!strcmp(argv[0], "cd")) { char *dir = argc>1 ? argv[1] : getenv("HOME"); if (chdir(dir) == -1) perror("cd failed"); continue; } // 外部命令:ls/cp/cat(fork+exec) exec_cmd(argv); } return 0; }六、核心总结
- exec 族:程序替换核心,fork+exec 是 Linux 进程编程的经典组合,exec 成功则进程被替换,失败才返回;
- waitpid:子进程资源回收的唯一方式,阻塞 / 非阻塞模式适配不同场景,避免僵尸进程;
- system:便捷但受限,无法修改父进程状态,底层是 fork+exec+waitpid;
- 路径控制:getcwd 获取当前路径,chdir 修改路径(仅影响当前进程);
- 核心原则:需修改父进程状态的操作(如 cd)必须在主进程执行,无需修改状态的命令(如 ls/cp)可通过 fork+exec 在子进程执行。