前面我们学习了 Linux 进程的创建、终止与等待,学会使用 fork 创建子进程,并用 wait/waitpid 回收僵尸进程。但 fork 出来的子进程默认和父进程运行同一份代码,很多场景下我们希望子进程去执行一个完全不同的外部程序,比如在代码里执行 ls、gcc 等命令。进程替换(exec 系列函数)就是用来解决这个需求的核心机制。
目录
[二、exec 系列函数](#二、exec 系列函数)
[execv /execvp(参数放数组)](#execv /execvp(参数放数组))
[execle /execve(自定义环境变量 e)](#execle /execve(自定义环境变量 e))
[三、exec 原理](#三、exec 原理)
一、进程替换概念
我们先用 fork() 创建子进程:
fork 之后,子进程复制父进程的地址空间(代码段、数据段、堆、栈等) ,父子进程跑一样的程序,只是 pid 不同。
很多场景我们不想要子进程继续执行父进程的代码,而是想让子进程去跑一个全新的可执行 程序 (比如在代码里调用 ls、gcc、python)。
进程替换(exec) : 在不创建新进程、不改变 PID 的前提下,把当前进程的代码段、数据段、堆、栈全部清空,加载一个新的可执行文件到这个进程中,从新程序的入口开始执行。
结论:
PID 不变!exec 不是创建新进程,只是 "换程序";fork 才是创建新进程。
exec 调用成功后,后面的代码不会执行,直接被新程序覆盖。
exec 失败才会返回 -1,继续执行原来代码。
文件描述符默认继承(默认不会关闭)。
fork + execfork 创建子进程 → 子进程调用 exec 替换成别的程序,父进程等待子进程退出(wait/waitpid)。
二、exec 系列函数
总览
cpp
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
命名规律
l:list,参数用可变参数列表 ,以 NULL 结尾
v:vector,参数放到字符串数组 argv
p:path,会去环境变量 PATH 里搜索程序(不用写绝对路径)
e:env,自己传入环境变量数组,不继承父进程环境变量
表格
| 函数 | 路径查找 | 参数形式 | 环境变量 |
|---|---|---|---|
| execl | 必须绝对路径 | 列表 | 继承 |
| execlp | PATH 搜索 | 列表 | 继承 |
| execle | 绝对路径 | 列表 | 自定义 |
| execv | 绝对路径 | 数组 | 继承 |
| execvp | PATH 搜索 | 数组 | 继承 |
| execve | 绝对路径 | 数组 | 自定义 |
execve是系统调用,剩下 5 个都是库函数,底层封装 execve。参数规则:
argv[0]是程序名(惯例,可随便写,一般和程序同名),真正参数从 argv 1 开始,数组最后必须以 NULL 结尾。
execl
execl:需要写程序完整路径,参数一个个传,NULL 收尾
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if(pid == -1)
{
perror("fork");
return 1;
}
if(pid == 0)
{
// 子进程:替换为 ls 程序
// /bin/ls 程序路径, argv0:ls, argv1:-l, argv2:-a, NULL结束
execl("/bin/ls", "ls", "-l", "-a", NULL);
// 如果execl成功,这行永远跑不到!只有失败才执行
perror("execl failed");
}
else
{
// 父进程等待子进程结束
wait(NULL);
printf("子进程执行完毕,父进程继续\n");
}
return 0;
}
子进程 PID 不变,地址空间被 ls 程序覆盖,执行 ls -l -a。 如果路径写错,execl 返回 - 1,触发 perror 打印错误。
结果如下

execlp
execlp 不需要写绝对路径,会去 PATH 里面找命令,写命令名即可
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if(pid == 0)
{
// 直接写ls,不用/bin/ls
execlp("ls", "ls", "-l", NULL);
perror("execlp");
}
else
{
wait(NULL);
}
return 0;
}
结果如下

execv /execvp(参数放数组)
适合参数数量动态变化的场景,把参数放到 char * 数组
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if(pid == 0)
{
char* argv[] = {"ls", "-l", "-h", NULL};
// execv 需要绝对路径
// execv("/bin/ls", argv);
// execvp 自动搜PATH
execvp("ls", argv);
perror("execvp");
}
else
{
wait(NULL);
}
return 0;
}
结果如下

execle /execve(自定义环境变量 e)
execle /execve 可以给新程序指定一套全新环境变量,不再继承父进程的
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if(pid == 0)
{
char* env[] = {"MYNAME=process_replace", "AGE=20", NULL};
// execle:列表参数 + 自定义环境变量
execle("/bin/env", "env", NULL, env);
perror("execle");
}
else
{
wait(NULL);
}
return 0;
}
结果如下

三、exec 原理
- 用户调用 exec 系列函数,陷入内核,调用
execve系统调用 - 内核校验可执行文件(ELF)
- 释放当前进程原有地址空间(代码段、数据、堆)
- 把新 ELF 文件加载到进程虚拟地址空间,建立页表映射
- 初始化栈、参数 argv、环境变量 env
- 修改 pc 寄存器,跳转到新程序入口点执行
四、什么时候用进程替换?
- shell 命令行:bash 就是父进程,输入命令 → fork 子进程 → exec 替换成对应程序
- 服务器程序:主进程负责监听,fork 子进程调用别的工具程序
- 程序内部调用外部脚本、编译器、第三方可执行文件
五、总结
进程替换不会创建新进程,进程 PID 保持不变,只是清空当前进程地址空间、加载新的 ELF 可执行程序。exec 一共 6 个函数,依靠后缀 l/v/p/e 区分传参方式、路径查找规则和环境变量继承方式,其中 execve 是真正的系统调用,其余都是封装库函数。
exec 成功之后后续代码不会执行,只有调用失败才会返回继续执行原有代码,一般配合 fork 使用:fork 创建子进程,子进程调用 exec 替换程序,父进程使用 wait/waitpid 等待回收。exec 在 Shell 解释器、服务程序调用外部工具等场景广泛使用。
结合上一部分内容,fork(创建进程) + exec(替换程序) + wait/waitpid(资源回收)共同构成 Linux 多进程开发最基础、最重要的一套组合。