跳到主要内容Linux 进程替换详解:从 fork 到 exec 的完整链路 | 极客日志C
Linux 进程替换详解:从 fork 到 exec 的完整链路
Linux 进程替换是指用新可执行程序完全覆盖当前进程的代码和数据空间,同时保持 PID 不变。核心流程涉及 fork 创建子进程后调用 exec 系列函数。exec 不会触发写时拷贝,而是直接销毁原有地址空间。常见接口包括 execl、execv 及其带 p 和 e 后缀的版本,分别对应路径查找和自定义环境变量。环境变量默认通过 fork 继承,若需彻底替换则需显式传递 envp 数组。CPU 通过 ELF 文件头的 e_entry 字段获取入口地址。该机制是 Shell 执行外部命令的基础。
Ne01 浏览 进程替换是什么?
进程替换的核心概念很简单:一个正在运行的进程,用一个全新的可执行程序,完全替换掉自己当前的代码、数据和堆栈,但进程的 PID 保持不变。
通俗点说,就像你正在用记事本编辑文档,突然窗口没变(PID 没变),但里面的程序直接变成了浏览器。我们先看个简单的例子,直观感受一下这个过程。
#include <iostream>
#include <unistd.h>
using namespace std;
int main() {
cout << "before: I am process, Mypid: " << getpid() << " Myppid: " << getppid() << endl;
execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
cout << "after: I am process, Mypid: " << getpid() << " Myppid: " << getppid() << endl;
return 0;
}
运行结果会显示 execl 之后程序变成了 ls,原进程后续的 cout 代码永远不会执行,这就是进程替换的本质。
进程替换原理
通常我们在 fork 创建子进程后,子进程默认执行的是和父进程相同的程序。为了让子进程去跑另一个程序,我们就会在子进程中调用 exec 系列函数。
当进程调用 exec 时,内核会将其用户空间的代码段、数据段等所有内存内容丢弃,重新加载新程序的镜像。注意,exec 不会创建新进程,所以调用前后 PID 不变。
1. 进程替换会发生写时拷贝吗?
很多人会担心 exec 会不会触发 COW(Copy-On-Write)。答案是:不会。
COW 的触发前提是父子进程共享内存页且一方写入。而 exec 的本质是直接销毁当前进程的地址空间映射,不再与任何进程共享内存页。无论是子进程还是父进程,在执行 exec 时都不存在共享内存的前提,因此自然不触发 COW。
2. 普通只读 vs COW 只读

这里有个细节:父进程依赖物理内存页引用计数触发 COW,而子进程依赖页表中的 COW 标志。只有子进程的页表会有 COW 标志,修改共享页最终都会触发 COW 实现隔离,但这与 exec 替换本身无关。
3. exec 执行后,后续代码还会执行吗?
- 成功:进程被完全替换,
exec 后面的代码永远不会执行。因为成功时没有返回值,控制流不会回到原程序。
- 失败:
exec 返回 -1,程序会继续执行后续代码(通常这里我们会写错误处理,比如 perror 然后退出)。
4. CPU 如何知道入口地址?
Linux 下的可执行文件是 ELF 格式。文件头部的 e_entry 字段存储了程序的入口地址。当 exec 加载新程序时,内核读取这个地址,把 CPU 指令指针(PC)设置过去,程序就从这里开始跑了。
5. 子进程替换会影响父进程吗?
不会。子进程替换时会构建全新的地址空间,与父进程完全独立,互不影响。
exec 系列接口详解
execve() 是内核提供的系统调用真身,其他 exec 函数都是 C 库封装的'马甲',最终都调用 execve()。
1. execl
参数以可变参数列表形式传递,最后必须用 NULL 结束。
int execl(const char *path, const char *arg, ...);
path:完整路径(如 /bin/ls),不去 PATH 查找。
arg:第一个通常是程序名,后面跟参数。
#include <iostream>
#include <unistd.h>
using namespace std;
int main() {
cout << "before: I am process, Mypid: " << getpid() << endl;
execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
cout << "after: I am process" << endl;
return 0;
}
2. execlp
区别在于 p (path),它会自动在 PATH 环境变量中查找程序,无需写完整路径。
int execlp(const char *file, const char *arg, ...);
#include <iostream>
#include <unistd.h>
#include <cstdio>
using namespace std;
int main() {
cout << "Before execlp, PID: " << getpid() << endl;
int ret = execlp("ls", "ls", "-l", "-a", NULL);
if (ret == -1) {
perror("execlp failed");
}
return 0;
}
3. execv
int execv(const char *path, char *const argv[]);
#include <iostream>
#include <unistd.h>
using namespace std;
int main() {
cout << "Before execv, PID: " << getpid() << endl;
char *const argv[] = {"ls", "-l", "-a", NULL};
int ret = execv("/bin/ls", argv);
if (ret == -1) {
perror("execv failed");
}
return 0;
}
4. execvp
结合了 execlp 的路径查找和 execv 的数组参数。
int execvp(const char *file, char *const argv[]);
5. execle
多了 e (environment),允许自定义环境变量表,完全替换子进程的环境变量。
int execl(const char *path, const char *arg, ..., char *const envp[]);
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
extern char** environ;
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
return 1;
} else if (pid == 0) {
int ret = execle("./testc", "./testc", "-a", "-b", "-c", NULL, environ);
if (ret == -1) {
perror("execle failed");
return 1;
}
} else {
wait(NULL);
}
return 0;
}
6. execvpe
与 execle 类似,但使用数组传参,功能一致。
常见问题解答
exec 只能执行系统命令吗?
当然不是。只要编译成可执行文件,传给 exec 即可运行自己的程序。
#include <iostream>
#include <unistd.h>
using namespace std;
int main() {
int ret = execl("./testc", "testc", NULL);
if (ret == -1) {
cout << "execl failed" << endl;
}
return 0;
}
有人可能会问:为什么命令行执行要加 ./,这里不用?
因为在终端里,系统需要明确路径找文件;而在 exec 的第一个参数里,我们已经传了 ./testc 作为路径,后面的 testc 只是 argv[0],不是用来定位文件的。
为什么脚本能跨语言调用?
本质都是进程。不管是 C++ 编译的二进制、Shell 脚本还是 Python,最终都由操作系统创建为独立进程执行。调用的本质就是让 OS 加载并执行目标程序,与语言无关。
exec 有加载器作用
exec 系列函数是操作系统层面的程序加载接口。终端输入的非内建指令,本质上都是 Shell 先 fork 再 exec。它是非内建指令执行的核心环节。
如何传递参数和环境变量?
命令行参数通过 argv 数组传递。环境变量比较特殊,fork 时会自动继承父进程的环境变量,除非你用 execle 或 execvpe 显式指定新的环境表。
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
return 1;
} else if (pid == 0) {
char* const argv[] = { "./testc", "-a", "-b", "-c", NULL };
int ret = execv("./testc", argv);
if (ret == -1) {
perror("execv failed");
return 1;
}
}
wait(NULL);
return 0;
}
配合子程序打印 main(int argc, char *argv[], char *env[]),你会发现即使没手动传环境变量,子进程依然能访问到,因为这是 fork 时自动复制的。
putenv 新增环境变量
putenv 用于在当前进程添加或修改变量,仅对当前进程及后续 fork 的子进程有效。
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
int main() {
putenv("MY_ENV=666666");
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
return 1;
} else if (pid == 0) {
char* const argv[] = { "./testc", "-a", "-b", "-c", NULL };
int ret = execv("./testc", argv);
if (ret == -1) {
perror("execv failed");
return 1;
}
} else {
wait(NULL);
cout << "Parent env: " << getenv("MY_ENV") << endl;
}
return 0;
}
如何彻底替换环境变量?
如果想完全抛弃父进程的环境变量,必须使用 execle 或 execvpe 显式传入自定义数组。
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork failed");
return 1;
} else if (pid == 0) {
char* const argv[] = { "./testc", "-a", "-b", "-c", NULL };
char* const myenv[] = { "MYAVL=111111", "HELLO=222222", NULL };
int ret = execle("./testc", "./testc", "-a", "-b", "-c", NULL, myenv);
if (ret == -1) {
perror("execle failed");
return 1;
}
} else {
wait(NULL);
}
return 0;
}
这样子进程就只会看到 MYAVL 和 HELLO,原有的 PATH 等变量都被切断了。
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online