问题导入:
前面我们知道了,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。