一、exec族函数:让进程“变身”的核心机制
进程执行这个话题,说白了就是“怎么让一个程序去跑另一个程序”。
我刚开始学Linux编程时,总觉得进程创建就是fork(),后来才发现——fork只是生了个孩子,exec才是让孩子换身衣服去干别的事。
exec族函数的核心作用:用新程序替换当前进程的代码段、数据段、堆和栈。进程PID不变,但干的事完全变了。
关键理解:exec不创建新进程,它只是让当前进程“改头换面”。
1.1 exec族函数全家福
这六个函数长得像,但各有脾气。我按命名规律给你拆开讲:
| 函数名 | 路径查找 | 参数传递 | 环境变量 |
|---|---|---|---|
| execl | 需绝对/相对路径 | 列表形式(可变参数) | 继承当前环境 |
| execlp | 通过PATH环境变量查找 | 列表形式 | 继承当前环境 |
| execle | 需绝对/相对路径 | 列表形式 | 自定义环境变量 |
| execv | 需绝对/相对路径 | 数组形式(argv) | 继承当前环境 |
| execvp | 通过PATH查找 | 数组形式 | 继承当前环境 |
| execve | 需绝对/相对路径 | 数组形式 | 自定义环境变量 |
命名规律其实很好记:带p的通过PATH找,带e的可以传环境变量,带v的用数组传参,带l的用列表传参。
1.2 execl:最基础的exec
先看个最简单的例子:
#include <unistd.h>
#include <stdio.h>
int main() {
printf("执行前:PID = %d\n", getpid());
// 执行/bin/ls,参数列表以NULL结尾
execl("/bin/ls", "ls", "-l", "/home", NULL);
// 如果exec成功,下面这行不会执行
printf("如果看到这行,说明exec失败了\n");
return 1;
}
注意那个NULL——它告诉函数“参数结束了”。我见过新手漏掉这个,结果程序直接崩溃。
重要:exec函数如果成功,不会返回。如果返回了,一定是出错了(返回-1)。
1.3 execlp:偷懒神器
execlp的好处是——你不用写完整路径。它会自动去PATH环境变量里找:
// 不用写/bin/ls,直接写ls就行
execlp("ls", "ls", "-l", NULL);
我个人习惯在写脚本工具时用execlp,省得纠结路径问题。但生产环境里,我建议还是用完整路径,避免PATH被篡改导致安全问题。
1.4 execle:环境变量自己说了算
这个函数带个e,意思是你可以给新程序定制环境变量:
char *env[] = {
"PATH=/usr/bin",
"HOME=/tmp",
"MY_VAR=hello",
NULL
};
execle("/bin/bash", "bash", "-c", "echo $MY_VAR", NULL, env);
我在项目中遇到过一个问题:子进程继承了父进程的环境变量,里面有个敏感信息泄露了。后来改用execle,只传必要的环境变量,问题就解决了。
1.5 execv和execvp:数组传参更灵活
当参数数量不确定时,用数组比列表方便:
char *args[] = {"ls", "-l", "/home", NULL};
execv("/bin/ls", args);
// 或者
execvp("ls", args);
execvp和execlp一样,会去PATH里找。区别只是传参方式不同。
二、fork+exec组合:真正的进程创建
单独用exec,只是替换当前进程。但实际开发中,我们通常需要父进程继续运行,子进程去干别的事。
这就是fork+exec组合的由来。
pid_t pid = fork();
if (pid == 0) {
// 子进程
execl("/bin/ls", "ls", "-l", NULL);
// 如果exec失败,必须退出
exit(1);
} else if (pid > 0) {
// 父进程
wait(NULL); // 等待子进程结束
printf("子进程执行完毕\n");
} else {
perror("fork失败");
}
这个模式太常见了——shell执行命令、服务器处理请求、任务调度……本质上都是fork+exec。
我的经验:fork之后,子进程一定要检查exec的返回值。如果exec失败,子进程必须exit(),否则它会继续执行父进程的代码,造成混乱。
三、system函数:偷懒但危险
system函数是fork+exec+wait的封装。你传一个字符串,它帮你执行:
system("ls -l /home");
看起来很方便对吧?但我要泼盆冷水——system函数有安全隐患。
我曾经在代码里写过:
char cmd[256];
sprintf(cmd, "rm -rf %s", user_input);
system(cmd);
结果用户输入了; rm -rf /……嗯,你懂的。从那以后,我再也不敢在生产环境用system了。
避坑指南:
- system会阻塞父进程,直到命令执行完
- system会继承父进程的信号处理方式
- 如果用户输入可控,绝对不要用system
- 优先用fork+exec,自己控制参数
四、知识体系总览
下面这张图,把exec族函数、fork+exec、system的关系理清楚了:
五、实战中的选择建议
说了这么多,到底该用哪个?我总结几条经验:
- 写简单脚本工具:用execlp或execvp,省事
- 生产环境服务:用execl或execv,指定完整路径
- 需要隔离环境变量:用execle或execve
- 执行用户输入的命令:绝对不要用system,自己写fork+exec
- 临时调试:system("command") 凑合用,但别提交到代码库
我的习惯:我一般用execvp,因为它既支持PATH查找,又用数组传参,灵活性最好。只有在需要自定义环境变量时,才换成execve。
嗯,exec族函数就这些内容。记住一句话:exec是让进程“变身”,fork+exec是“生个孩子让他去变身”,system是“偷懒但可能出事”。
下一节我们会讲进程间通信,到时候这些exec的知识还会用上。