一、程序替换与原理
fork() 之后,父子进程执行的是同一份代码 ,只是走不同的分支。如果子进程想运行一个全新的程序 ,就需要进程程序替换。
程序替换本质,就是把磁盘上另一个程序的代码和数据 覆盖到调用进程的物理内存中,然后从新程序的入口开始执行。
二、exec*函数
exec*函数用于函数替换。
当进程调用一种 exec* 函数时:
-
当前进程的用户空间代码和数据被整体替换为新程序的代码和数据
-
从新程序的启动代码开始执行(
_start→main) -
没有创建新进程,所以 PID 不变
fork() → 复制出一个新进程(程序还是原来的)
exec*() → 把这个进程里的程序整个换成另一个可执行文件
| 特点 | 说明 |
|---|---|
| 成功不返回 | 原来的代码已经被替换,没有地方可返回 |
| 失败返回 -1 | 所以 exec 只有"出错的返回值",没有"成功的返回值" |
| PID 不变 | 还是原来的进程,只是里面跑的程序换了 |
| 文件描述符默认保留 | 除非设置了 FD_CLOEXEC,shell 的重定向就依赖这一点 |
因此 exec 后面通常紧跟一句 exit(1),它只有在 exec 失败时才会执行:
cpp
execl("./other", "other", NULL);
exit(1); // 只有 exec 失败才会走到这里
exec 后不保留的东西:
- 不保留:自定义信号处理函数(重置为默认)、内存映射、堆栈、设了 FD_CLOEXEC 的 fd
- 保留:PID、PPID、当前工作目录、信号屏蔽字、被忽略的信号、未设 CLOEXEC 的 fd
三、六个替换函数
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[]);
execvpe 是 GNU 扩展(需要 #define _GNU_SOURCE)
命名规律
| 字母 | 含义 |
|---|---|
l(list) |
参数以列表 形式逐个传入,以 NULL 结尾 |
v(vector) |
参数放进 vector(char *argv) 数组传入 |
p(path) |
自动在 PATH 环境变量里搜索可执行文件 |
e(env) |
自己维护环境变量 ,传入 envp |
3.1 execl
cpp
execl("/usr/bin/ls", "/usr/bin/ls", "-ln", "-a", NULL);
等价:
execl("/usr/bin/ls", "ls", "-ln", "-a", NULL);
// 相当于命令行 ls -ln -a
| 位置 | 如何传 |
|---|---|
| 第 1 个 | 加载哪个文件(给内核看的路径),想要执行什么文件 |
| 第 2 个 | 新程序的 argv[0] |
| 第 3 个 | argv[1] |
| 第...个 | ... |
| 最后 | NULL,可变参数约定的结束字符串 |
第 1 个参数:告诉内核,去哪里加载文件
第 2 个参数以及之后:告诉新程序 ,自己的
argv[0]、argv[1]等是什么。把在终端敲的命令以空格划分,逗号隔开,也就是分别传argv0、argv1...其中:
argv[0](第 2 个参数) :新程序的"名字",习惯上写成命令名(如"ls"、"./other")。技术上内核不会用它找文件,多数程序也不依赖它,但按约定要写。argv[1]及之后(第 3 个参数起) :真正传给程序的命令行参数 (如"-l"、"-a"、"other.py"),程序靠它们决定怎么干活,是真正有实际作用的部分。- 最后的
NULL:参数列表的结束标志,必须有。
3.2 l(list)和 v(vector)
区别在于第 2 个及之后的参数怎么传:
l(list) :把参数一个个直接写在函数调用里 ,用逗号隔开,最后以NULL结尾。v(vector) :先把参数放进一个char *数组 ,再把数组名传进去。数组最后一项同样必须是NULL。
cpp
execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
└────────┬────────┘
这几项原样放进数组
↓
char *argv[] = {"ls", "-l", "-a", NULL};
execv("/usr/bin/ls", argv);
3.3 p(path)
区别在于第 1 个参数怎么找到可执行文件:
- 不带
p:第 1 个参数必须是路径(绝对路径或相对路径),内核直接按这个路径去加载。 - 带
p:第 1 个参数可以只写文件名,系统会自动到环境变量PATH列出的各个目录里依次查找,找到第一个可执行的就加载。
cpp
execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
└────┬─────┘
必须写完整路径
↓ 加上 p,只写文件名
execlp("ls", "ls", "-l", "-a", NULL);
└┬┘
系统去 PATH 里找,最终找到 /usr/bin/ls
3.4 e(env)
3.4.1 区别
区别在于新程序的环境变量从哪来:
- 不带
e:新程序自动继承当前进程的环境(environ)。 - 带
e:多出一个envp参数,新程序的环境完全由你传的这一份决定,原来的环境变量全部不看。
传了 envp,新程序的环境就只有你传的这些。不是合并,是整份替换。
cpp
execl("./other", "other", NULL); // 不带 e:自动继承当前环境
↓ 加上 e,多传一个环境数组
char *const envp[] = {"MYVAL=123456789", NULL};
execle("./other", "other", NULL, envp); // 新程序的环境只有 MYVAL
数组版同理:
cpp
execv("./other", argv); // 继承父进程传下来的环境变量
execve("./other", argv, envp); // 自己传
手写 envp:全新环境
cpp
char *const env[] = {
(char *const)"MYVAL=123456789",
NULL
};
execve("./other", argv, env);
运行结果里 env[0]: MYVAL=123456789 之后就没有了。新程序里没有 PATH、HOME、USER,shell 里有的变量都不存在,只有传的env。因为新程序的环境变量在这个进程里被覆盖了。
exec 会把用户空间整体替换,包括旧的
environ那块内存(旧程序的栈、堆、数据都没了)。新程序启动时,内核把传的envp放到新栈上,新程序的environ就指向这一份。所以environ被替换了。"覆盖"的范围到底有多大:覆盖新程序的环境变量
手写
env数组只是一个普通局部变量,不会修改当前进程自己的环境 (environ没动)。它只在调用execvpe的那一刻被交给新程序,作用范围仅限这一次 exec。
注意参数位置
execle的NULL在envp前面:
execle("/bin/ps", "ps", "-ef", NULL, envp); // ↑参数结束 ↑环境数组
3.4.2在原环境基础上追加变量
想要"原环境 + 一个新变量",有两种做法。
方法一:putenv / setenv 后用不带 e 的函数
cpp
putenv("MYVAL=123");
execvp("./other", argv); // 自动继承,里面含 MYVAL
方法二:putenv 后把 environ 传给带 e 的函数
cpp
putenv("MYVAL=123");
extern char **environ; // libc 提供的全局变量,当前进程的完整环境
execve("./other", argv, environ);
putenv不拷贝!传入的内存必须一直有效:
cppvoid f() { char buf[32] = "MYVAL=hello"; putenv(buf); // 危险!f 返回后 buf 被销毁,environ 里是悬空指针 }由于
putenv的设计特性:putenv不拷贝字符串,environ里存的就是传进去的那个指针,字符串本体由调用者负责保管。所以 f () 返回后 buf 被销毁,environ 里是悬空指针。应使用字符串字面量、全局/静态数组或堆内存 。新手更推荐
setenv:在内部分配新内存,拷贝名字和值,而不是像putenv只存一个指针。
四、只有 execve 是系统调用
其他的函数都是底层调用execve()函数
execl ─┐
execlp ─┤ 整理参数(列表拼成数组、查 PATH、补上 environ)
execle ─┤
execv ─┤ ──► execve(系统调用)──► 内核加载新程序
execvp ─┘
五、总结
| 要点 | 内容 |
|---|---|
| 本质 | 把新程序加载到当前进程的地址空间,PID 不变,成功不返回 |
| 命名 | l 列表、v 数组、p 查 PATH、e 自己传环境 |
| 第 2 个参数 | 是新程序的 argv[0],不决定加载谁 |
| 列表末尾 | 必须是 NULL,数组末尾也必须是 NULL |
| 带 e | 传什么环境,新程序就只有什么(整份替换,仅限这一次调用) |
| 追加环境 | putenv 后传 environ,或不带 e 直接继承 |
| 环境变量 | 属于进程,向下传、不向上传,进程结束即消失 |
| 系统调用 | 只有 execve,其余是 libc 封装 |
| 配套 | fork 造进程,exec 换程序,wait 回收,三者组成 shell 的核心 |
