
观众老爷们大家好 这里是邪修KING的独家频道
本文属于系列Linux系统篇 ——操作指令
一起学Linux的小伙伴可订阅专栏: Linux系统篇
上一篇我们讲透了 fork() 分身术:调用一次,分裂出两个独立进程。但 fork 有个局限:子进程只能复制父进程的代码和数据,跑和父进程一样的逻辑。 如果想让子进程跑一个全新的程序呢?比如执行 ls 命令、跑 python 脚本、启动另一个程序? 这就需要 Linux 系统编程第二大利器:exec 系列程序替换函数。 本篇我们从替换原理讲起,拆解 6 个 exec 函数的命名规律、参数含义、使用场景,最后结合 fork 写出完整的程序替换实战代码,讲透所有坑点。
一、什么是进程程序替换?
1.1 本质:换代码,不换进程
很多人以为 exec 是创建新进程,完全不是。
程序替换的核心:进程 PID 不变,PCB 保留,只把用户态的代码段、数据段,全部替换成新程序(ELF 文件)的内容,重新初始化栈、堆,然后从新程序的入口开始执行。
一句话总结:还是原来那个进程,只是里面跑的代码换成别的程序了。
1.2 核心结构变化
- ✅ 不变:PID、PPID、PCB、打开的文件描述符、信号处理方式、进程优先级
- ❌ 全部替换:代码段、数据段、堆、栈、指令指针 PC
替换之后,进程从新程序的 _start 开始执行,原来的代码彻底消失,不会再继续往下走。
1.3 为什么 fork + exec 是黄金搭档?
- fork:创建子进程,拥有独立地址空间
- exec:子进程里执行替换,只改子进程自己的代码数据,父进程完全不受影响
这就是 shell 的工作原理:
💡 为什么不直接 exec 替换自己? 替换了 shell 自己,命令跑完 shell 就没了。所以必须 fork 出子进程来替换,父进程 shell 保留,继续接收命令。
二、exec 系列函数命名规律(核心记忆法)
exec 一共 6 个常用函数,不用死记硬背,名字里的每个字母都有明确含义,掌握规律一眼就能看懂。
表格
| l | list | 参数用列表形式传递,逐个写参数,最后以 NULL 结尾 |
| v | vector | 参数用数组形式传递,把所有参数放进数组,传数组首地址 |
| p | path | 自动在 PATH 环境变量里搜索可执行文件,不用写全路径 |
| e | environment | 传入自定义环境变量,不继承父进程的环境变量表 |
6 个函数就是这几个特征的组合:
- l / v 二选一:参数怎么传
- p 可选:要不要自动搜 PATH
- e 可选:要不要自定义环境变量
底层真相: 只有 execve 是真正的系统调用,剩下 5 个都是 C 标准库封装的库函数,最终都会调用 execve。
三、逐个接口详解:原型 + 参数 + 场景
3.1 execl:列表传参,需写全路径
函数原型
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
参数说明
适用场景
知道程序的完整路径,参数不多,逐个写比较清晰。
示例
// 执行 /usr/bin/ls -l -a
execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
3.2 execlp:列表传参,自动搜索 PATH
函数原型
int execlp(const char *file, const char *arg, ...);
参数说明
适用场景
执行系统自带命令(ls、cat、gcc),不用找路径,直接写命令名。
示例
// 执行 ls -l -a,自动在 PATH 里找 ls
execlp("ls", "ls", "-l", "-a", NULL);
💡 注意:第二个参数 ls 是传给新程序的 argv [0],和第一个参数文件名可以不一样,但约定写成一样的。
3.3 execle:列表传参,自定义环境变量
函数原型
int execle(const char *path, const char *arg, ..., char *const envp[]);
参数说明
适用场景
需要给新程序单独设置环境变量,隔离父进程环境。
示例
char *my_env[] = {
"PATH=/usr/bin",
"USER=test",
NULL
};
// 执行 ls,只传入自定义环境变量
execle("/usr/bin/ls", "ls", "-l", NULL, my_env);
3.4 execv:数组传参,需写全路径
函数原型
int execv(const char *path, char *const argv[]);
参数说明
适用场景
参数多、或者参数是动态生成的,放数组里更方便。
示例
char *const argv[] = {
"ls",
"-l",
"-a",
NULL
};
execv("/usr/bin/ls", argv);
3.5 execvp:数组传参,自动搜索 PATH
函数原型
int execvp(const char *file, char *const argv[]);
参数说明
适用场景
动态拼接参数,又不想写全路径,是最常用的组合之一。
示例
char *const argv[] = {
"ls",
"-l",
"-a",
NULL
};
execvp("ls", argv);
3.6 execve:系统调用本尊
函数原型
int execve(const char *path, char *const argv[], char *const envp[]);
参数说明
这是最完整的一个,也是真正的系统调用。前面 5 个库函数,内部最终都是调用它。
适用场景
需要完全控制参数和环境变量,或者想直接调用系统调用。
四、完整实战:fork + exec 程序替换
4.1 完整代码 proc.c
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("我的程序要运行了!\\n");
pid_t pid = fork();
if (pid == 0)
{
// ========== 子进程:执行程序替换 ==========
printf("[子进程] pid=%d,准备替换成 ls 程序\\n", getpid());
sleep(1);
// 方式1:execl 列表传参 + 全路径
// execl("/usr/bin/ls", "ls", "-ln", "-a", NULL);
// 方式2:execv 数组传参 + 全路径(推荐写法)
char *const argv[] = {
"ls",
"-l",
"-a",
NULL
};
execv("/usr/bin/ls", argv);
// ===== 注意:下面的代码只有 exec 失败才会执行 =====
// 成功的话,代码已经被替换,永远走不到这里
perror("exec 失败");
exit(1);
}
else
{
// ========== 父进程:等待子进程 ==========
waitpid(–1, NULL, 0);
printf("我的程序运行完毕了\\n");
}
return 0;
}
4.2 编译运行
gcc proc.c –o proc
./proc
4.3 运行结果
我的程序要运行了!
[子进程] pid=xxxx,准备替换成 ls 程序
总用量 xx
drwxrwxr-x ...
...(ls 命令的输出)
我的程序运行完毕了
4.4 逐行理解
💡 关键观察: exec 之后,子进程还是那个 PID,但跑的已经是 ls 的代码了。原来子进程里 exec 后面的代码,只要替换成功就永远不会执行。
五、核心特性与常见坑点
5.1 返回值:成功不返回,失败才返回
这是最反直觉的一点:
exec 系列函数,成功了就不会返回了,直接跑新程序去了。 只有出错的时候,才会返回 -1,并且设置 errno 错误码。
所以写法一定是:
execxxx(…);
// 走到这里一定是失败了
perror("exec fail");
exit(1);
不要写 if(execxxx() > 0) 这种判断,成功根本不会回来。
5.2 参数列表必须以 NULL 结尾
不管是 l 系列的列表,还是 v 系列的数组,最后必须加 NULL,告诉系统参数传完了。 不加 NULL 会导致越界访问,结果不可预期。
5.3 替换后原代码全部失效
exec 成功后,原进程后面的所有代码都被覆盖了,不会执行。 所以错误处理、清理工作一定要写在 exec 之前,或者失败分支里。
5.4 环境变量继承规则
- 不带 e 的函数(execl、execlp、execv、execvp):继承父进程的全部环境变量
- 带 e 的函数(execle、execve):只用你传入的环境变量,完全不继承父进程
六、6 个函数对比总结表
表格
| execl | 列表 | ❌ 要写全路径 | 继承父进程 | 库函数 | 参数少、知道完整路径 |
| execlp | 列表 | ✅ 自动搜索 | 继承父进程 | 库函数 | 执行系统命令、参数少 |
| execle | 列表 | ❌ 要写全路径 | ✅ 自定义 | 库函数 | 需要自定义环境变量 |
| execv | 数组 | ❌ 要写全路径 | 继承父进程 | 库函数 | 参数多、动态参数 |
| execvp | 数组 | ✅ 自动搜索 | 继承父进程 | 库函数 | 最常用,动态参数 + 系统命令 |
| execve | 数组 | ❌ 要写全路径 | ✅ 自定义 | 系统调用 | 底层完全控制 |
全文总结





