引言
当我们使用fork()创建子进程时,发现了一个有趣的场景------子进程仿佛是父进程的"克隆体",执行着完全相同的代码。就像影分身术,所有分身都在做同样的动作。
- 但想象一下这样的需求:一个厨师分身去做编程,另一个分身去画画。如果每个分身只能做和本体一样的事情,那多进程的价值就大打折扣了!
- 这就是我们面临的核心问题:fork()给了我们"复制"的能力,却没有赋予"改变"的自由。子进程被束缚在父进程的代码逻辑中,如何让它摆脱这种束缚,去执行一个全新的程序呢?
这时,我们需要一个"变身术"------进程程序替换!这个神奇的能力让一个进程可以"脱胎换骨",在保持外壳(进程ID、环境等)不变的情况下,完全替换内部执行的程序。


目录
一、替换原理
用fork创建子进程后,子进程最初执行的是和父进程相同的程序(但可能执行不同的代码分支)。子进程通常要调用一种exec函数来执行另一个完全不同的程序。
当进程调用exec函数时:
- 用户空间完全替换:原进程的用户空间代码和数据被完全清除
- 内存管理重构:操作系统释放原进程占用的所有物理内存页
- 新程序加载:从磁盘读取新程序的代码段、数据段等
- 映射关系重建 :在页表中建立虚拟地址→新物理页的映射
(注意:虚拟地址范围不变,但指向的物理页内容全新)
- 进程身份不变:PCB、PID、文件描述符表等保持不变
- 全新开始执行:从新程序的启动例程(如main函数)开始执行
整个过程不创建新进程 ,只是在现有进程框架内完成"程序内容"的彻底替换。

思考:子进程程序替换后,会影响父进程的代码和数据吗?
- 子进程刚被创建时,与父进程共享代码和数据,但当子进程需要进行进程程序替换时,也就意味着子进程需要对其数据和代码进行写入操作,这时便需要将父子进程共享的代码和数据进行写时拷贝,此后父子进程的代码和数据也就分离了,因此子进程进行程序替换后不会影响父进程的代码和数据。
二、替换函数
其实替换函数有六种exec开头的,我们统称为exec函数,所以让我们先来通过表格整体了解一下这6个函数。

| 函数名 | 核心特点 | 路径/文件名要求 | 参数传递方式 |
|---|---|---|---|
| execl | 基础 exec,列表传参 | 需传入程序绝对/相对路径(如 /bin/ls) |
可变参数列表(以 NULL 结尾) |
| execlp | 带 PATH 搜索的 execl | 可直接传程序名(如 ls) |
可变参数列表(以 NULL 结尾) |
| execle | 可自定义环境变量的 execl | 需传入程序绝对/相对路径 | 可变参数列表(NULL 后接 envp) |
| execv | 数组传参版 execl | 需传入程序绝对/相对路径 | argv[] 数组(以 NULL 结尾) |
| execvp | 带 PATH 搜索的 execv | 可直接传程序名 | argv[] 数组(以 NULL 结尾) |
| execvpe | PATH 搜索 + 自定义环境(GNU 扩展) | 可直接传程序名,自动从 PATH 查找 | argv[] + envp[](均以 NULL 结尾) |

接下来让我们一个一个来了解这些替换函数!
2.1 execl

- 第一个参数是执行程序的路径
- 第二个参数是可变参数列表
- 以NULL结尾
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("交换前执行, pid: %d, ppid: %d\n", getpid(), getppid());
execl("/usr/bin/ls", "ls", "-a", "-i","-l", NULL);
printf("交换后执行, pid: %d, ppid: %d\n", getpid(), getppid());
return 0;
}
2.2 execlp

- 第一个参数是要执行程序的名字
- 第二个参数是可变参数列表,表示你要如何执行这个程序
- 以NULL结尾。
cpp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
printf("父进程 pid: %d\n", getpid());
pid_t id = fork();
if (id == 0) // 子进程
{
printf("子进程(pid:%d)执行ls命令前\n", getpid());
// execlp执行ls -a -l,成功则后续代码不执行
execlp("ls", "ls", "-a", "-l", NULL);
// 仅execlp失败时执行以下代码
perror("execlp执行失败");
exit(1);
}
// 父进程:等待子进程并解析状态
int status;
wait(&status); // 阻塞等待子进程退出
if (WIFEXITED(status))
{
int code = WEXITSTATUS(status);
printf("\n子进程(pid:%d)正常退出,退出码:%d\n", id, code);
}
else
{
printf("子进程异常退出\n");
}
return 0;
}
2.3 execle

- 第一个参数是要执行程序的路径
- 第二个参数是可变参数列表,表示你要如何执行这个程序
- 并以NULL结尾
- 第三个参数是你自己设置的环境变量。
cpp
char* myenvp[] = { "MYVAL=2021", NULL };
execle("./mycmd", "mycmd", NULL, myenvp);
2.4 execv

- 第一个参数是要执行程序的路径
- 第二个参数是一个指针数组
- 数组当中的内容表示你要如何执行这个程序,数组以NULL结尾
cpp
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execv("/usr/bin/ls", myargv);
2.5 execvp

- 第一个参数是要执行程序的名字
- 第二个参数是一个指针数组
- 数组当中的内容表示你要如何执行这个程序,数组以NULL结尾。
cpp
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execvp("ls", myargv);
2.6 execvpe

- 第一个参数是要执行程序的名字
- 第二个参数是一个指针数组
- 数组当中的内容表示你要如何执行这个程序,数组以NULL结尾。
- 第四个参数是自己维护的环境变量
cpp
char* myargv[] = { "mycmd", NULL };
char* myenvp[] = { "MYVAL=2025", NULL };
execve("./mycmd", myargv, myenvp);
函数解释
这些函数如果调用成功,则加载指定的程序并从启动代码开始执行,不再返回。
如果调用出错,则返回-1。
也就是说,exec系列函数只要返回了,就意味着调用失败。
三、execve
事实上,只有execve才是真正的系统调用,其它五个函数最终都是调用的execve,所以execve在man手册的第2节,而其它五个函数在man手册的第3节,也就是说其他五个函数实际上是对系统调用execve进行了封装,以满足不同用户的不同调用场景的。
下图为exec系列函数族之间的关系:

总结
✨ 坚持用 清晰易懂的图解 + 代码语言, 让每个知识点都 简单直观 !
🚀 个人主页 :不呆头 · CSDN
🌱 代码仓库 :不呆头 · Gitee
📌 专栏系列 :
💬 座右铭 : "不患无位,患所以立。"
