Linux 进程程序替换和 exec 函数族
0. 前言
在 Linux 中,进程除了能通过 fork 创建子进程外,还可以通过 进行。所谓替换,就是让一个正在运行的进程丢掉原来的程序映像,转而执行另一个可执行文件。
Linux 进程替换通过 exec 系列函数实现,将当前进程映像替换为新程序而不创建新进程。文章详解 fork 与 exec 配合使用模式,分析 execl/execv 等函数参数差异及环境变量传递机制,并通过 C/C++ 代码示例验证命令行参数与环境变量的继承与覆盖规则。

在 Linux 中,进程除了能通过 fork 创建子进程外,还可以通过 进行。所谓替换,就是让一个正在运行的进程丢掉原来的程序映像,转而执行另一个可执行文件。
这正是命令行运行程序、Shell 调用脚本的底层机制。本文将通过实例,从单进程替换到 fork+exec 的组合,再到不同的 exec 接口,逐步剖析这一机制的原理与用法。
先简单看一下单进程进程替换的现象,代码如下:
Linux 为我们提供了一系列系统调用,用于进行进程替换!
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
printf("before: I am a process pid: %d, ppid: %d\n", getpid(), getppid());
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
printf("after: I am a process pid: %d, ppid: %d\n", getpid(), getppid());
return 0;
}
注意:这里 execl("/usr/bin/ls", "ls", "-a", "-l", NULL); 是 exec 系列函数的标准写法,方便记忆。
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);:第一个参数是可执行文件的路径,中间是可变参数列表,用于指明程序执行的参数,最后一个参数必须是 NULL。总结现象:
execl 参数中的命令和选项被执行了。execl 调用前的 printf("before ...") 函数,调用 execl 之后的 printf("after ...") 没有执行。在 Linux 中,当我们在命令行运行一个程序时,本质上经历了以下几个步骤:
fork() 创建一个子进程,这个子进程就是将要执行用户命令的进程。当子进程被创建后,bash 会调用 exec 系列函数,加载用户指定的可执行程序。
在这一过程中:
因此,调用 exec 系列函数不会创建新进程,而是让当前进程'脱胎换骨',从此开始运行新的程序。
一个关键问题是:CPU 如何知道新程序应该从哪里开始执行?
答案在于 ELF(Executable and Linkable Format,可执行与可链接格式):
Linux中形成的可执行程序,是有特定格式的,Linux 中的可执行程序的格式为ELF
exec 将 ELF 加载到内存后,操作系统会读取 ELF 头部信息,从而得到程序的入口地址。fork() 用于创建子进程。exec() 用于替换子进程的内存映像,加载并运行新程序。
exec 系列函数的做法十分简单粗暴:
exec 系列函数时,直接用新程序的代码替换原来进程的代码,用新程序的数据,替换原来进程的数据,并让 CPU 执行新程序的代码开始。最终,用户在命令行中输入的程序就能在 CPU 上开始运行。
代码演示:
// 多进程 程序替换
int main() {
pid_t id = fork();
if (id == 0) {
// child
printf("before: I am a process pid: %d, ppid: %d\n", getpid(), getppid());
sleep(5);
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
printf("after: I am a process pid: %d, ppid: %d\n", getpid(), getppid());
exit(0);
}
// father
pid_t ret = waitpid(id, NULL, 0); // 等待子进程,暂不关心进程退出状态,阻塞等待
if (ret > 0) {
printf("wait success, father: %d, ret %d\n", getpid(), ret);
sleep(5);
}
return 0;
}
问题与总结:
不会。
exec 系列函数时,替换的只是该子进程的用户空间代码和数据,不会对父进程的执行造成影响。wait/waitpid 等方式监控子进程的退出状态。进程替换不创建新进程,只进行进程代码和数据的替换。
exec 并不会创建新进程,它只会在当前进程的上下文中加载并运行一个新的程序。exec 本质上是'用另一个程序替换自己',而不是启动一个新的进程。fork 与 exec 的关系fork() 后,子进程和父进程最初运行的是相同的程序(代码空间一致,但地址空间独立)。exec,以执行一个全新的程序。这样形成了经典的模式:
父进程: 继续原有逻辑 子进程:
fork → exec,替换为用户指定的程序
exec 调用后的代码执行情况exec 调用点之后的代码不会被执行。exec 会返回 -1,并设置 errno 表示错误原因。exec 之后的代码才有机会被执行。exec 系列函数的返回值exec没有成功返回值。exec 才会返回 -1。fork 与 exec 常常配合使用:fork 创建子进程,exec 让子进程运行新程序。exec 成功执行后,调用点之后的代码不会被执行;只有失败时才会返回 -1 并继续向下执行,exec 系列函数无成功时的返回值。在 Linux 中,exec 系列函数用于 用一个新的程序替换当前进程的映像。
fork 不同)。它们主要定义在头文件:
#include <unistd.h>
常见的 exec 系列库函数有:
// 以下为库函数
int execl(const char* path, const char* arg, ... /* (char *) NULL */);
int execlp(const char* file, const char* arg, ... /* (char *) NULL */);
int execle(const char* path, const char* arg, ... /* (char *) NULL, 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[]);
// 以下为系统调用
int execvpe(const char* file, char* const argv[], char* const envp[]);
exec 函数只有出错的返回值,没有成功的返回值。命名规律总结:
exec 系列函数的后缀有规律:
| 后缀 | 含义 |
|---|---|
| l | 参数以列表(list)的形式传递(execl, execlp, execle),传参时直接写 "arg1", "arg2", ..., NULL,必须以NULL结尾。 |
| v | 参数以向量(vector,数组)的形式传递(execv, execvp, execvpe),传入 char *argv[]。 |
| p | 函数名中的 p:即 PATH。代表默认在 PATH 环境变量搜索可执行文件(execlp, execvp, execvpe),传参时无需传入路径,只需传入要执行的程序名。 |
| e | 允许指定新的环境变量 envp[](execle, execvpe)。 |
记忆口诀:
l= list,v= vector,p= path,e= environment
path 和 filepath:需要给出 可执行文件的绝对路径或相对路径,例如 /usr/bin/ls。file:只需要给出文件名,例如 ls,函数会在 PATH 环境变量 指定的路径中搜索程序。例如:
execl("/usr/bin/ls", "ls", "-a", "-l", NULL); // 直接指定路径
execlp("ls", "ls", "-a", "-l", NULL); // 默认在环境变量 PATH 搜索,只需传入程序名
argv / argargv 和 arg 都表示 传递给新程序的参数列表。示例:
char* const myargv[] = {"ls", "-a", "-l", NULL};
execv("/usr/bin/ls", myargv);
envpexecle 和 execvpe 可以显式指定环境变量;execl, execv, execlp, execvp)会默认继承调用进程的环境变量。envp 表示 环境变量列表,即一个以 NULL 结尾的字符串数组:
// 自定义的环境变量
char* envp[] = {"PATH=/usr/bin", "USER=guest", NULL};
execle("/usr/bin/ls", "ls", "-a", "-l", NULL, envp);
char* myagrv[] = {"ls", "-a", "-l", NULL};
execvpe("ls", myagrv, envp);
基本用法:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("Before exec...\n");
// ... 进程替换的接口
perror("exec failed");
return 1;
}
execl:
// 方法 1: execl (指定路径 + 列表传参)
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
execlp:
// 方法 2: execlp (用 PATH 搜索)
execlp("ls", "ls", "-a", "-l", NULL);
execv:
// 方法 3: execv (指定路径 + 向量传参)
char* args[] = {"ls", "-a", "-l", NULL};
execv("/usr/bin/ls", args);
execle:
// 方法 4: execle (指定路径 + 参数 + 环境变量)
char* envp[] = {"PATH=/bin", NULL};
execle("/usr/bin/ls", "ls", "-a", "-l", NULL, envp);
总结:
C/C++ 写的程序,也有命令行参数,exec 系列函数调用时,会把后面参数列表或argv[] 中的选项和 envp[],形成命令行参数表和环境变量表,传递给 ls 或其他程序的 main 函数。在 Linux 中,所有进程都是由已有进程
fork出来的子进程。命令行运行程序时,Bash 先fork出子进程,再用exec系列函数把目标程序加载进内存运行。
exec的作用是清空当前进程占用的物理内存空间,把磁盘上的可执行文件读入内存,并让 CPU 从新程序的入口开始执行,相当于内核提供的'程序加载器'。
.PHONY: all
all: otherExe mycommand
mycommand: mycommand.c
gcc -o $@ $^ -std=c99
otherExe: otherExe.cpp
g++ -o $@ $^ -std=c++11
.PHONY: clean
clean:
rm -f mycommand otherExe
.PHONY: all
all,表示 all 不是一个文件名,而是一个逻辑目标。.PHONY,当目录下存在一个叫 all 的文件时,make all 会误认为已经生成了目标而不执行命令。all: otherExe mycommand
all 目标依赖于 otherExe 和 mycommand。make all 时,会先尝试生成 otherExe,再生成 mycommand(顺序由 make 自行决定,但通常按依赖书写顺序来执行)。总结依赖关系图:
all
├── otherExe (依赖于 otherExe.cpp)
└── mycommand (依赖于 mycommand.c)
make 或 make all:同时编译 mycommand 和 otherExe。make mycommand:只编译 mycommand。make otherExe:只编译 otherExe。make clean:清理编译产物。exec 接口调用我们自己的写的可执行程序以及调用其他语言形成的可执行程序:// 执行我们自己写的程序 用 C 语言程序 调用 C++ 程序
execl("./otherExe", "otherExe", NULL);
// C 语言程序调用 shell 脚本
execl("/usr/bin/bash", "bash", "test.sh", NULL);
// C 语言程序调用 python 脚本
execl("/usr/bin/python3", "python3", "test.py", NULL);
'所有语言编写的程序,运行起来本质都是进程'。
为什么可执行程序或脚本能跨语言调用?
👉 所以所有语言写的程序都能互相调用。
mycommand 程序向 otherExe 传递命令行参数// mycommand.c
int main() {
pid_t id = fork();
if (id == 0) {
// child
printf("before: I am a process pid: %d, ppid: %d\n", getpid(), getppid());
sleep(3);
char* const myargv[] = {"otherExe", "-a", "-b", NULL};
execv("./otherExe", myargv);
printf("after: I am a process pid: %d, ppid: %d\n", getpid(), getppid());
exit(0);
}
// father
pid_t ret = waitpid(id, NULL, 0); // 等待子进程,暂不关心进程退出状态,阻塞等待
if (ret > 0) {
printf("wait success, father: %d, ret %d\n", getpid(), ret);
sleep(3);
}
return 0;
}
// otherExe.cpp
#include <iostream>
using namespace std;
int main(int argc, char* argv[]) {
cout << argv[0] << " begin running" << endl;
for (int i = 0; argv[i]; ++i) {
cout << i << " : " << argv[i] << endl;
}
cout << argv[0] << "otherExe stop running" << endl;
return 0;
}
mycommand 程序向 otherExe 传递环境变量extern char** environ;
execle("./otherExe", "otherExe", "-a", "-b", NULL, environ); // 传递系统的环境变量
#include <iostream>
using namespace std;
int main(int argc, char* argv[], char* env[]) {
cout << "这是命令行参数" << endl;
cout << argv[0] << " begin running" << endl;
for (int i = 0; argv[i]; ++i) {
cout << i << " : " << argv[i] << endl;
}
cout << "这是环境变量" << endl;
for (int i = 0; env[i]; ++i) {
cout << i << " : " << env[i] << endl;
}
cout << argv[0] << "otherExe stop running" << endl;
return 0;
}
传递新的环境变量有两种方式
putenv 添加新的环境变量putenv("MYPRIVATE_ENV=123456"); putenv 是为当前进程添加环境变量,不影响父进程中的环境变量
int main() {
pid_t id = fork();
putenv("MYPRIVATE_ENV=123456");
if (id == 0) {
// child
printf("before: I am a process pid: %d, ppid: %d\n", getpid(), getppid());
sleep(3);
char* const myargv[] = {"otherExe", "-a", "-b", NULL};
execv("./otherExe", myargv);
printf("after: I am a process pid: %d, ppid: %d\n", getpid(), getppid());
exit(0);
}
// father
pid_t ret = waitpid(id, NULL, 0); // 等待子进程,暂不关心进程退出状态,阻塞等待
if (ret > 0) {
printf("wait success, father: %d, ret %d\n", getpid(), ret);
sleep(3);
}
return 0;
}
int main() {
pid_t id = fork();
extern char** environ;
if (id == 0) {
// child
printf("before: I am a process pid: %d, ppid: %d\n", getpid(), getppid());
sleep(3);
// execle("./otherExe", "otherExe", "-a", "-b", NULL, environ); // 传系统的环境变量
// 传自定义的环境变量 会完全覆盖从系统继承下来的环境变量
char* const myenv[] = {"MYVAL=123456", "MYPATH=/usr/bin/xxx", NULL};
execle("./otherExe", "otherExe", "-a", "-b", NULL, myenv);
printf("after: I am a process pid: %d, ppid: %d\n", getpid(), getppid());
exit(0);
}
// father
pid_t ret = waitpid(id, NULL, 0); // 等待子进程,暂不关心进程退出状态,阻塞等待
if (ret > 0) {
printf("wait success, father: %d, ret %d\n", getpid(), ret);
sleep(3);
}
return 0;
}
exec 系列函数中带 e 的接口时(execle, execvpe),手动传入新的环境变量,会覆盖掉从父进程继承下来的环境变量。exec 系列函数所有接口的调用关系:
execve 是系统调用,头文件为 <unistd.h>。exec 函数是库函数,头文件为 <stdlib.h>,底层调用 execve。fork 创建子进程时,环境变量表会被一同复制到子进程的地址空间。main 函数中,可以通过第三个参数 char* envp[] 访问环境变量;main 的参数,也可以通过 全局变量:extern char** environ;
直接获取和操作当前进程的环境变量表。
✅ 总结一句话:
环境变量是进程运行时的一部分数据,创建子进程时会自动继承父进程的环境变量表;无论通过 main 参数还是 environ 变量,都可以访问和修改它。
进程替换并不会创建新进程,而是在原进程中装载新程序。配合 fork 使用,就形成了'父进程继续执行,子进程运行新程序'的经典模式。
理解 exec 系列函数,有助于把握 Linux 程序执行的本质:所有程序运行到最后都是进程,而进程既可以继承,也可以替换。这正是 Linux 灵活而高效的关键所在。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online