跳到主要内容
C
Linux 进程程序替换和 exec 函数族 Linux 进程通过 exec 系列函数实现程序替换,即当前进程加载新可执行文件覆盖原有代码段和数据段,而不创建新进程。通常配合 fork 使用,父进程 fork 子进程后,子进程调用 exec 运行新程序。exec 函数族包括 execl、execv 等变体,支持不同参数传递方式(列表或向量)及环境变量指定。成功调用 exec 不会返回,失败则返回 -1。环境变量在 fork 时继承,可通过 putenv 添加或使用 execle/execvpe 完全替换。
微码行者 发布于 2026/3/16 更新于 2026/4/26 10 浏览进程程序替换和 exec 函数族
0. 前言
在 Linux 中,进程除了能通过 fork 创建子进程外,还可以通过 exec 系列函数 进行进程替换 。所谓替换,就是让一个正在运行的进程丢掉原来的程序映像,转而执行另一个可执行文件。
这正是命令行运行程序、Shell 调用脚本的底层机制。本文将通过实例,从单进程替换到 fork+exec 的组合,再到不同的 exec 接口,逐步剖析这一机制的原理与用法。
1. 单进程的进程替换
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 ...") 没有执行
我们自己的进程,可以在运行时,运行其他路径的程序,这种现象就叫进程替换
2. 进程替换的原理 在 Linux 中,当我们在命令行运行一个程序时,本质上经历了以下几个步骤 :
1. 进程的创建
用户在 bash 中输入一条命令。
bash 进程调用 fork() 创建一个子进程,这个子进程就是将要执行用户命令的进程。
操作系统为该子进程分配必要的资源,包括:
PCB(进程控制块) :保存进程的标识、状态、寄存器等信息。
进程地址空间 :包含代码段、数据段、堆、栈等区域。
页表 :建立虚拟地址与物理地址的映射关系。
2. 可执行程序的加载 当子进程被创建后,bash 会调用 exec 系列函数 ,加载用户指定的可执行程序。
在这一过程中:
原进程在物理内存中的 代码段和数据段会被新程序替换 ,发生了写时拷贝 。
操作系统会 重新建立页表 ,将新程序的虚拟地址映射到新的物理内存页。
虚拟地址空间本身的结构保持不变(依然分为代码段、数据段、堆、栈),但内容会根据新程序进行调整。
PCB 不会被替换 ,它仍然是原来的进程控制块,只是内部记录的信息被更新。
因此,调用 exec 系列函数不会创建新进程 ,而是让当前进程'脱胎换骨',从此开始运行新的程序。
3. 程序入口地址的确定 一个关键问题是:CPU 如何知道新程序应该从哪里开始执行?
答案在于 ELF(Executable and Linkable Format,可执行与可链接格式) :
Linux 中形成的可执行程序,是有特定格式的,Linux 中的可执行程序的格式为 ELF
在程序源代码编译生成 ELF 文件时,编译器会将程序的 入口地址(entry point) 写入到 ELF 文件的头部(表头 ),可执行程序的入口地址存在表头中 。
当 exec 将 ELF 加载到内存后,操作系统会读取 ELF 头部信息,从而得到程序的入口地址。
CPU 在调度该进程运行时,会将指令寄存器(IP/EIP/RIP)设置为入口地址,从这个位置开始执行程序代码。
4. 总结
fork() 用于创建子进程。
exec() 用于替换子进程的内存映像,加载并运行新程序。
exec 系列函数的做法十分简单粗暴
调用 exec 系列函数时,直接用新程序的代码替换原来进程的代码,用新程序的数据,替换原来进程的数据 ,并让 CPU 执行新程序的代码开始
该过程中没有创建新进程,原进程的 PCB、进程地址空间都保持不变,页表中的相应字段被更新
进程替换后:
PCB 保留 (进程还是原来的进程)。
虚拟地址空间结构不变 ,但内容被新程序覆盖。
页表更新 ,建立新的虚拟地址到物理地址的映射。
入口地址由 ELF 文件头部提供 ,CPU 从此位置开始执行新程序。
最终,用户在命令行中输入的程序就能在 CPU 上开始运行。
3. 多进程的程序替换
int main () {
pid_t id = fork();
if (id == 0 ) {
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 );
}
pid_t ret = waitpid(id, NULL , 0 );
if (ret > 0 ) {
printf ("wait success, father: %d, ret %d\n" , getpid(), ret);
sleep(5 );
}
return 0 ;
}
1. 子进程被替换会不会影响父进程?
父子进程是独立的两个进程 ,它们各自拥有独立的虚拟地址空间。
当子进程调用 exec 系列函数时,替换的只是该子进程的用户空间代码和数据 ,不会对父进程的执行造成影响 。
父进程依然可以通过 wait/waitpid 等方式监控子进程的退出状态。
2. 进程替换是否创建新进程?
exec 并不会创建新进程,它只会在当前进程的上下文中 加载并运行一个新的程序。
因此:
进程 ID(PID)保持不变 。
PCB 仍然是原来的 ,只是其中的代码段、数据段、堆、栈等信息被新程序替换。
所以,exec 本质上是'用另一个程序替换自己',而不是启动一个新的进程。
3. fork 与 exec 的关系
调用 fork() 后,子进程和父进程最初运行的是相同的程序(代码空间一致,但地址空间独立)。
在很多场景下,子进程会立刻调用 exec ,以执行一个全新的程序。
父进程: 继续原有逻辑
子进程: fork → exec,替换为用户指定的程序
4. exec 调用后的代码执行情况
替换成功时 :
进程的代码和数据完全被新程序替换。
CPU 的指令寄存器被设置为新程序的入口地址。
exec 调用点之后的代码不会被执行 。
替换失败时 :
exec 会返回 -1,并设置 errno 表示错误原因。
此时,调用 exec 之后的代码才有机会被执行。
5. exec 系列函数的返回值
exec没有成功返回值 。
如果成功执行了替换,原来的程序逻辑已经不存在,因此不可能返回到原调用点。
只有在加载失败时,exec 才会返回 -1。
总结
子进程的替换不会影响父进程 ,它们的执行空间相互独立。
进程替换不创建新进程 ,只是在原有进程中加载新程序。
fork 与 exec 常常配合使用:fork 创建子进程,exec 让子进程运行新程序。
exec 成功执行后,调用点之后的代码不会被执行;只有失败时才会返回 -1 并继续向下执行,exec 系列函数无成功时的返回值 。
4. 验证各种程序替换接口 exec
1. exec 系列函数概述 在 Linux 中,exec 系列函数用于 用一个新的程序替换当前进程的映像 。
调用成功后,当前进程的代码段、数据段、堆、栈都会被新程序替换,不会创建新进程 (这点和 fork 不同)。
调用成功时 不会返回 ;若调用失败,才会返回 -1。
2. exec 系列函数族
int execl (const char *path, const char *arg, ... ) ;
int execlp (const char *file, const char *arg, ... ) ;
int execle (const char *path, const char *arg, ... ) ;
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[]) ;
以上函数,如果调用成功 则加载新的程序从启动代码开始执行 ,不再返回。
如果调用出错则返回 -1
所以 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
3. 函数参数说明和使用
exec 系列所有函数 :
第一个参数 :帮助函数找到该程序,因此传入程序的绝对路径或相对路径或程序名
第二个参数 :告诉程序如何执行,因此传入程序执行所需的参数
(1) path 和 file
path:需要给出 可执行文件的绝对路径或相对路径 ,例如 /usr/bin/ls。
file:只需要给出文件名,例如 ls,函数会在 PATH 环境变量 指定的路径中搜索程序。
execl("/usr/bin/ls" , "ls" , "-a" , "-l" , NULL );
execlp("ls" , "ls" , "-a" , "-l" , NULL );
(2) argv / arg
argv 和 arg 都表示 传递给新程序的参数列表 。
以 NULL 结尾 表示参数结束。
char *const myargv[] = {"ls" , "-a" , "-l" , NULL };
execv("/usr/bin/ls" , myargv);
(3) envp
execle 和 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 *myargv[] = {"ls" , "-a" , "-l" , NULL };
execvpe("ls" , myargv, envp);
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main () {
printf ("Before exec...\n" );
perror("exec failed" );
return 1 ;
}
execl("/usr/bin/ls" , "ls" , "-a" , "-l" , NULL );
execlp("ls" , "ls" , "-a" , "-l" , NULL );
char *args[] = {"ls" , "-a" , "-l" , NULL };
execv("/usr/bin/ls" , args);
char *envp[] = {"PATH=/bin" , NULL };
execle("/usr/bin/ls" , "ls" , "-a" , "-l" , NULL , envp);
第一个参数 pathname/file :传入程序的绝对路径或相对路径或程序名
后续如果是可变参数列表 :命令行中怎么写,参数列表就怎么写,只不过空格分隔变成了逗号分隔,选项放在了双引号中
根据需要可传入指定的环境变量
ls 以及其他程序是 C/C++ 写的程序,也有命令行参数,exec 系列函数调用时,会把后面参数列表 或argv[] 中的选项 和 envp[],形成命令行参数表和环境变量表 ,传递给 ls 或其他程序的 main 函数
在 Linux 中,所有进程都是由已有进程 fork 出来的子进程。命令行运行程序时,Bash 先 fork 出子进程,再用 exec 系列函数把目标程序加载进内存运行。
exec 的作用是清空当前进程占用的物理内存空间 ,把磁盘上的可执行文件读入内存,并让 CPU 从新程序的入口开始执行,相当于内核提供的'程序加载器'。
4. Makefile 一次编译多个可执行程序 .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 接口调用我们自己的写的可执行程序 以及调用其他语言形成的可执行程序 :
execl("./otherExe" , "otherExe" , NULL );
execl("/usr/bin/bash" , "bash" , "test.sh" , NULL );
execl("/usr/bin/python3" , "python3" , "test.py" , NULL );
程序运行后在操作系统看来都是 进程 ,无论是 C、Go 编译的二进制,还是 Python、Shell 脚本。
跨语言调用的本质就是 一个进程调用或替换成另一个进程 。
操作系统不关心语言,只负责加载和运行进程。
5. 一个程序调另一个程序验证命令行参数的传递
mycommand 程序向 otherExe 传递命令行参数
int main () {
pid_t id = fork();
if (id == 0 ) {
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 );
}
pid_t ret = waitpid(id, NULL , 0 );
if (ret > 0 ) {
printf ("wait success, father: %d, ret %d\n" , getpid(), ret);
sleep(3 );
}
return 0 ;
}
#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 ;
}
6. 一个程序调另一个程序验证环境变量的传递
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 ;
}
7. 给子进程传递新的环境变量
putenv 添加新的环境变量putenv("MYPRIVATE_ENV=123456"); putenv 是为当前进程添加环境变量 ,不影响父进程中的环境变量
int main () {
pid_t id = fork();
putenv("MYPRIVATE_ENV=123456" );
if (id == 0 ) {
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 );
}
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 ) {
printf ("before: I am a process pid: %d, ppid: %d\n" , getpid(), getppid());
sleep(3 );
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 );
}
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
5. 环境变量与进程的关系
1. 环境变量的本质
环境变量也是数据 ,存放在进程的用户空间中。
每个进程在运行时,都有一份属于自己的环境变量表。
2. 环境变量的继承
当父进程通过 fork 创建子进程时,环境变量表会被一同复制 到子进程的地址空间。
因此,子进程天生就继承了父进程的环境变量。
这意味着:环境变量是在 子进程创建阶段 就已经传递下去的,而不是运行后再赋值的。
3. 环境变量的访问方式
在 main 函数中,可以通过第三个参数 char* envp[] 访问环境变量;
即使不依赖 main 的参数,也可以通过 全局变量 :
✅ 总结一句话 :
环境变量是进程运行时的一部分数据,创建子进程时会自动继承父进程的环境变量表;无论通过 main 参数还是 environ 变量,都可以访问和修改它。
6. 结语 进程替换并不会创建新进程,而是在原进程中装载新程序 。配合 fork 使用,就形成了'父进程继续执行,子进程运行新程序'的经典模式。
理解 exec 系列函数,有助于把握 Linux 程序执行的本质:所有程序运行到最后都是进程,而进程既可以继承,也可以替换 。这正是 Linux 灵活而高效的关键所在。
相关免费在线工具 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