【Linux】进程替换

一、程序替换与原理

fork() 之后,父子进程执行的是同一份代码 ,只是走不同的分支。如果子进程想运行一个全新的程序 ,就需要进程程序替换。

程序替换本质,就是把磁盘上另一个程序的代码和数据 覆盖到调用进程的物理内存中,然后从新程序的入口开始执行。

二、exec*函数

exec*函数用于函数替换。

当进程调用一种 exec* 函数时:

  1. 当前进程的用户空间代码和数据被整体替换为新程序的代码和数据

  2. 从新程序的启动代码开始执行(_start → main)

  3. 没有创建新进程,所以 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不拷贝!传入的内存必须一直有效:
cpp 复制代码
void 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 的核心
相关推荐
灯澜忆梦2 小时前
【Linux】#10 | Linux 终端实用快捷键 + 安装应用
linux·运维·服务器
码士集团小青2 小时前
面霸篇:高频 Java 基础问题(核心卷一)
linux·服务器·数据库·redis·缓存·1024程序员节
水饺编程3 小时前
第2章:Linux 基础,切换用户
linux·c语言·汇编
Lsetea4 小时前
curl指定--tlsv1.2仍连接TLS1.3:最低版本与tls-max排查
linux·https·ssl证书·curl·tls
Lsetea4 小时前
curl访问HTTPS报47 Too many redirects:重定向循环与max-redirs排查
linux·http·https·ssl证书·curl
9624565 小时前
一次 Dell PowerEdge R750xs 启动故障复盘:从 iDRAC 带外管理到 Linux 多内核回退与 initramfs 重建
linux·运维·服务器
Zevalin爱灰灰6 小时前
Linux入门第一部分(Ubuntu入门篇) 第二章 浅尝Ubuntu(二)
linux·嵌入式·1024程序员节
北京中科新远科技7 小时前
高速接口兼容排查实战:OSFP、QSFP112与QSFP56如何逐层验证
linux·运维·网络
Ruiery7 小时前
Linux 6.6内核 CPU 深度解析(零):x86 内核引导协议 — 从 bootloader 到 main()
linux·运维·服务器