马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
Linux 进程程序替换:exec 系列函数到底做了什么
fork 出来的子进程,跑的还是和父进程一样的代码,只是走了不同的分支。想让子进程去执行一个完全另一个程序,就必须靠 exec 家族函数。理解了 exec,才算是摸到多进程编程真正有用的那一步。
一、程序替换的原理
用 fork 创建子进程后,子进程执行的是和父进程相同的程序(只是可能走不同的代码分支)。要让子进程去执行另一个程序,就需要调用 exec 函数。
当进程调用 exec 函数时,该进程的用户空间代码和数据会被完全替换成新程序的内容,然后从新程序的启动例程开始执行。这个替换在机制上类似于写时拷贝——代码被换了,但不会反向影响父进程后续的代码。
一个进程 = 内核数据结构 + 代码 + 数据。替换时代码和数据会被换掉,内核数据结构基本不变:不释放结构,也不创建新的进程。所以判断"有没有创建新进程"看的是 fork,不是 exec。站在被替换进程的角度看,本质上就是"新程序被加载进内存"这件事;Linux 里 exec* 就是起这个加载作用的。
为什么非要先把程序加载到内存?因为冯·诺依曼体系结构要求程序先放入内存,CPU 只去内存里取数据和指令。内核里的 execve 才是真正的系统调用,exec* 系列只是它的封装函数。
二、为什么要配合多进程使用
exec 系列函数一旦调用成功,后续代码就不会继续执行了——代码已经被换掉。如果不想让调用方的后续代码被覆盖,正确做法是创建子进程,让子进程去做替换,父进程专心等待。
父进程创建子进程后,让子进程执行父进程的一份代码,或者让子进程执行一份全新的代码,两者都成立。创建子进程的目的是让它去完成工作,而程序替换在子进程中发生时会触发写时拷贝,父进程安然无恙。
三、exec 家族函数一览
返回值不需要关心:替换成功就绝不返回,返回了就说明一定失败。常用成员:
| 函数 | 特点 |
execl | 路径 + 可变参数列表,参数以 NULL 结尾 |
execv | 路径 + 参数指针数组 |
execle | 路径 + 参数数组 + 自定义环境变量 |
execvp / execlp | 按 PATH 查找程序,不用写完整路径 |
execvpe | PATH 查找 + 自定义环境变量 |
名字后缀的规律:l 表示 list(列表形式传参),v 表示 vector(数组形式传参),p 表示按 PATH 查找,e 表示自己提供环境变量。
四、基本示例
4.1 让子进程执行 ls
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
printf("my process begin.....\n");
pid_t id = fork();
if (id == 0)
{
sleep(2);
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
printf("father wait success, child exit code:%d\n", WEXITSTATUS(status));
}
printf("my process end.....\n");
return 0;
}
父进程打印 begin,fork 出子进程;子进程 sleeping 两秒后调用 execl 执行 ls -a -l,代码被换掉;父进程调用 waitpid 等待回收,最后打印 end。这就是多进程 + 程序替换的标准结构。
4.2 单个进程直接替换
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
printf("my process end.....\n");
这里 printf("my process end") 在替换成功后永远不会执行——因为代码已经不再是原来的代码了。这正是 exec 返回值不必关心的原因:成功即不返回,失败才继续往下走,此时可以 perror 或 exit。
4.3 替换自己写的程序
execl("./test", "test", NULL);
execl 不仅能替换系统命令,也能替换自己编译出来的程序。
4.4 带环境变量的替换
execvpe("./pragma", argv, environ);
第三个参数直接传 environ,把当前进程的环境原样交给新程序。
五、execl 参数怎么理解
int execl(const char *path, const char *arg, ...);
l 就是 list,用列表的方式记录命令行里要执行的命令。参数上,第一个 path 是待执行程序的路径(怎么找到程序由调用者告诉函数),后面几个是可变参数——命令行里怎么执行就怎么传,最后一个必须是 NULL。
一句话总结:第一个参数解决"去哪儿找这个程序",后面的参数解决"这个程序怎么被运行"。
六、容易踩的坑
- 忘记传 NULL 结尾:可变参数列表必须以 NULL 收尾,否则程序会读到垃圾参数甚至崩溃。
- 路径写错:用
execl 必须给绝对路径或用 execvp 走 PATH;路径错会返回 -1 并设置 errno,此时原来代码才会继续跑。
- 在父进程里 exec:父进程代码被覆盖,后续
wait 逻辑不复存在,父进程应保留。
- 先 fork 再 exec 失败:子进程 exec 失败后一定要
exit,否则会继续执行父进程后续代码,形成僵尸或逻辑混乱。
- 文件描述符继承:exec 后已打开的描述符默认仍然继承,不想要的要提前
close 或设 O_CLOEXEC。
小结
程序替换的本质是一次"覆盖加载":进程的用户空间代码和数据被新程序替换,内核数据结构保持不变。exec 家族的核心用法是配合 fork——子进程负责替换成目标程序,父进程负责 wait 回收。记住"成功不返回、失败才继续"这条,绝大多数 exec 相关的疑问都能自己推出来。
qidao123.com企服评测-应用市场 编辑提示 |