Linux 进程控制核心:exec 族函数、waitpid 与 system 全解析
exec 族:程序替换核心,fork+exec 是 Linux 进程编程的经典组合,exec 成功则进程被替换,失败才返回;waitpid:子进程资源回收的唯一方式,阻塞 / 非阻塞模式适配不同场景,避免僵尸进程;system:便捷但受限,无法修改父进程状态,底层是 fork+exec+waitpid;路径控制:getcwd 获取当前路径,chdir 修改路径(仅影响当前进程);核心原则。
一、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 在子进程执行。
更多推荐


所有评论(0)