问题导入:
前面我们知道了,fork之后,子进程会继承父进程的代码和“数据”(写实拷贝)。
那么如果我们需要子进程完全去完成一个自己的程序怎么办呢?
1.替换原理
我们先用一个简单的示例来见见进程替换:
int execl(const char *path, const char *arg, ...);
利用最简单的exec函数,这里的path是可执行程序的地址,文件的地址可以用指针来表示,const char *arg,...这里表示可变参数,说明可以传入多个参数。
1.1可变参数
具体可见:

C 语言通过 <stdarg.h> 实现可变参数,图里重点用到这些:
va_list:参数列表类型,本质是指针(比如va_list args,用来 “指向” 可变参数在栈里的位置)。va_start(args, count):初始化,让args指向可变参数的 “起始位置”(count是固定参数,用来定位可变参数从哪开始)。va_arg(args, double):逐个取参数,按类型(这里是double)从栈里读数据,读完后args自动指向下一个参数。va_end(args):收尾清理,释放va_list相关资源(有些环境里是 “形式上” 的规范,实际也需调用)。
栈内存视角:参数怎么存?
右侧 “栈帧”(main 调用 sum 的栈结构)是关键:
- 固定参数:
count(图里是3)是固定参数,先入栈,用来告诉函数 “可变参数有几个”。 - 可变参数:
1.0、2.0、3.0这些可变参数,按从右到左顺序入栈(C 语言调用约定常见规则),存在栈里等待读取。
代码里 sum(3, 1.0, 2.0, 3.0) 调用时,栈里布局大致是:
高地址 → [count=3] [1.0] [2.0] [3.0] ← 低地址
(实际栈增长方向是 “高地址 → 低地址”,但参数入栈顺序是 3 先压,然后 1.0、2.0、3.0 依次压,所以低地址侧是可变参数)
代码流程:怎么读可变参数?
结合图里 sum 函数逻辑,流程是:
- 初始化:
va_start(args, count)→ 让args指向可变参数起始位置(跳过固定参数count,指向第一个可变参数1.0所在栈地址 )。 - 循环读取:
va_arg(args, double)→ 每次按double类型从栈里取数据,累加到total。取完后,args自动偏移(因为double占 8 字节,所以args会+= sizeof(double)指向下一个参数 )。 - 收尾:
va_end(args)→ 释放资源,结束可变参数处理。
类比 printf:可变参数的 “通用逻辑”
图里也提到 printf(const char *format, ...) ,它的逻辑和 sum 类似:
format是固定参数(类似count),用来 “描述可变参数的类型、个数”(比如%d对应int,%f对应double)。- 内部也是用
va_list读取可变参数,按format里的占位符,逐个解析栈里的数据。
1.2 简单使用
知道可变参数之后我们开始简单使用一下execl,


通过以上例子我们提出两个疑问。
(1)为什么没打印“进程结束”?
(2)如果是在子进程中执行exec可以替换子进程的代码的数据吗?
好的,我们一一解决
(1)为什么没打印“进程结束”?
替换了,你的进程,已经执行另一个程序的代码了你自己的代码,已经没有了!!
程序替换函数,一旦调用成功,后续代码,不在执行,因为没有了!
如果失败呢??
失败的话就会回到原代码,并且exe系列函数会有一个返回值,exe系列的函数,只要返回,必然失败! 程序替换,如果成功,不需要,也不会有返回值! 失败返回-1
(2)如果是在子进程中执行exec可以替换子进程的代码的数据吗?
子进程执行一个全新的程序,会影响父进程吗?不会!!进程必须具有独立性(父子代码共享,数据写时拷贝啊)
你可以理解成为,代码如果被替换,也要进行写时拷贝,会在内存中为子进程开辟数据的代码的空间。

由此可见,execl在这里就是起到一个加载器的作用。
2.exe家族的其他接口
这里我们在linux的命令行中man 一下exec。

可见,这里有六个接口,其实有七个,还有一个我们稍后再讲。
2.1命名理解
• l(list) : 表⽰参数采⽤列表• v(vector) : 参数⽤数组• p(path) : 有p⾃动搜索环境变量PATH• e(env) : 表⽰⾃⼰维护环境变量
| 函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
|---|---|---|---|
| execl | 列表 | 不是 | 是 |
| execlp | 列表 | 是 | 是 |
| execle | 列表 | 不是 | 不是,须自己组装环境变量 |
| execv | 数组 | 不是 | 是 |
| execvp | 数组 | 是 | 是 |
| execve | 数组 | 不是 | 不是,须自己组装环境变量 |
2.2exe家族的使用
#include <stdio.h>
#include <unistd.h>
int main()
{
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使⽤环境变量PATH,⽆需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,需要⾃⼰组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
execv("/bin/ps", argv);
// 带p的,可以使⽤环境变量PATH,⽆需写全路径
execvp("ps", argv);
// 带e的,需要⾃⼰组装环境变量
execve("/bin/ps", argv, envp);
exit(0);
}
函数的使用比较简单,可以拿着格式自己试试,同时需要注意的是,这里的argv和envp是我们之前学到的命令行参数和环境变量,可以拿自己也可以直接在main函数中接收,可以直接拿着父进程的用,如果都要可以使用putenv()函数。
还有个点值得注意,在你的进程的地址空间,就如同全局变量一样,如果你不以参数形式传递给子进程,子进程也照样能拿到!!!!地址空间和页表!!!
3.六个接口与第七个的关系
上面我们不是提到了第七个接口,其实他叫做execve。
















![[蓝桥杯]图形排版](https://i-blog.csdnimg.cn/direct/b88570d063f646e7bdc5011519d44a5f.png)



