19、进程控制(二):exec族函数的使用
上一章我们聊了 fork(),它像孙悟空拔毫毛,变出一个一模一样的自己。但很多时候,我们想让新进程去干点别的事——比如在 shell 里执行 ls、grep 这些命令。这时候,就需要 exec 族函数登场了。
exec 族函数,说白了就是「换芯」。它不创建新进程,而是把当前进程的代码段、数据段、堆栈全部替换掉,然后从头执行一个新程序。嗯,进程 ID 不变,但灵魂已经换了。
核心理解:fork() 是分身,exec() 是变身。两者经常配合使用。
19.1 exec 族函数全家福
Linux 提供了 6 个 exec 函数,名字长得像兄弟连。我刚开始学的时候也晕,后来发现规律其实很简单——看后缀字母就行。
| 函数名 | 后缀含义 | 参数形式 | 搜索路径 | 环境变量 |
|---|---|---|---|---|
| execl() | l = list | 参数列表(可变参数) | 不搜索,需绝对/相对路径 | 继承当前环境 |
| execv() | v = vector | 参数数组 | 不搜索,需绝对/相对路径 | 继承当前环境 |
| execle() | l + e = list + env | 参数列表 + 环境数组 | 不搜索,需绝对/相对路径 | 自定义环境 |
| execve() | v + e = vector + env | 参数数组 + 环境数组 | 不搜索,需绝对/相对路径 | 自定义环境 |
| execlp() | l + p = list + path | 参数列表 | 搜索 PATH 环境变量 | 继承当前环境 |
| execvp() | v + p = vector + path | 参数数组 | 搜索 PATH 环境变量 | 继承当前环境 |
你看,规律其实很简单:l 表示参数用列表传,v 表示用数组传;p 表示搜索 PATH 路径;e 表示可以自定义环境变量。
19.2 核心函数详解
19.2.1 execl() — 参数列表形式
原型:
int execl(const char *path, const char *arg, ... /* (char *) NULL */);
第一个参数是程序路径,后面跟可变参数列表,最后必须以 NULL 结尾。我在项目中经常用它来执行一些固定路径的工具。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("我是父进程,PID=%d\n", getpid());
// 执行 /bin/ls,列出当前目录文件
execl("/bin/ls", "ls", "-l", "-a", NULL);
// 如果 exec 成功,下面这行不会执行
perror("execl 失败");
exit(1);
}
注意:第二个参数 "ls" 是 argv[0],也就是程序名。虽然它不影响实际执行哪个程序,但很多程序会用它来显示帮助信息或错误提示。我习惯写成和文件名一致。
19.2.2 execv() — 参数数组形式
原型:
int execv(const char *path, char *const argv[]);
当参数数量不确定时,用数组更灵活。我个人更喜欢 execv,因为可以动态构建参数列表。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
char *args[] = {"ls", "-l", "-a", NULL};
printf("准备执行 ls...\n");
execv("/bin/ls", args);
perror("execv 失败");
exit(1);
}
19.2.3 execlp() 和 execvp() — 带路径搜索
这两个函数带 p 后缀,会自动在 PATH 环境变量中搜索可执行文件。你想想看,平时在 shell 里直接敲 ls 就能运行,靠的就是这个机制。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("用 execlp 执行 ls...\n");
// 不需要写完整路径,系统会自动在 PATH 中找
execlp("ls", "ls", "-l", NULL);
perror("execlp 失败");
exit(1);
}
我曾经踩过的坑:有一次在嵌入式系统上,我用了 execlp() 但没设置 PATH 环境变量,结果程序一直返回 -1。查了半天才发现,那个系统的默认 PATH 是空的。所以用 p 系列函数前,最好确认一下 PATH 是否设置正确。
19.2.4 execle() 和 execve() — 自定义环境变量
带 e 后缀的函数可以传入自定义的环境变量数组。这在需要隔离环境或传递特殊配置时非常有用。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
char *env[] = {
"MY_VAR=hello",
"PATH=/usr/bin:/bin",
NULL
};
printf("用自定义环境执行程序...\n");
execle("/bin/printenv", "printenv", NULL, env);
perror("execle 失败");
exit(1);
}
19.3 知识体系图
下面这张图帮你理清 exec 族函数的选择逻辑:
19.4 实战:fork + exec 组合拳
在实际项目中,exec 很少单独使用。最常见的模式是:fork 一个子进程,然后在子进程里 exec 执行新程序。父进程继续做自己的事,子进程去跑别的任务。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork 失败");
exit(1);
}
if (pid == 0) {
// 子进程
printf("子进程 PID=%d,准备执行 ls...\n", getpid());
execlp("ls", "ls", "-l", NULL);
// 如果 exec 失败,子进程需要退出
perror("execlp 失败");
exit(1);
} else {
// 父进程
printf("父进程 PID=%d,等待子进程结束...\n", getpid());
int status;
wait(&status);
if (WIFEXITED(status)) {
printf("子进程正常退出,返回码=%d\n", WEXITSTATUS(status));
}
}
return 0;
}
我个人的习惯:在子进程里 exec 之后,一定要加 exit() 或 _exit()。因为如果 exec 失败,子进程会继续执行后面的代码,这往往不是我们想要的。我曾经见过一个 bug,就是忘了加 exit,结果子进程和父进程一起跑同样的逻辑,数据全乱了。
19.5 常见错误与调试
exec 函数执行失败时返回 -1,并设置 errno。常见的错误码有:
| errno | 含义 | 常见原因 |
|---|---|---|
| ENOENT | 文件或路径不存在 | 路径写错了,或者 PATH 没设置 |
| EACCES | 权限不足 | 文件没有可执行权限 |
| ENOMEM | 内存不足 | 系统资源耗尽 |
| E2BIG | 参数列表太长 | 参数或环境变量太多 |
我曾经遇到的坑:有一次在嵌入式 Linux 上,我用 execv 执行一个脚本,结果一直返回 ENOENT。后来发现脚本的第一行是 #!/bin/bash,但系统里装的是 busybox,没有 bash。换成 #!/bin/sh 就好了。所以执行脚本时,要注意解释器是否存在。
19.6 总结
exec 族函数是进程控制的精髓之一。记住几个要点:
- l 和 v 决定参数怎么传——列表还是数组
- p 决定是否搜索 PATH 路径
- e 决定是否自定义环境变量
- exec 成功后不会返回,失败后返回 -1
- fork + exec 是最经典的进程创建模式
嗯,这些函数看着多,但用熟了之后,你会发现它们其实就是一套工具的不同变体。选哪个,取决于你的场景——路径已知还是未知?参数固定还是动态?环境变量要不要隔离?想清楚了,选起来就顺手了。
公众号:蓝海资料掘金营,微信deep3321