跳到主要内容 Linux 进程控制:深入理解进程程序替换与 exec 系列函数 | 极客日志
C
Linux 进程控制:深入理解进程程序替换与 exec 系列函数 Linux 进程程序替换通过 exec 系列系统调用实现,将磁盘上的新程序加载到当前进程地址空间覆盖原有代码和数据。替换原理、fork 后子进程替换流程、加载器概念以及 execl/execv/execle/execve/execvp/execvpe 六个库函数和一个系统调用的区别与用法。重点讲解了如何传递命令行参数与环境变量,包括使用自定义环境变量表或继承父进程环境,并展示了 C 语言调用 C++ 或其他语言程序的示例。
292440837 发布于 2026/2/4 更新于 2026/4/18 6.8K 浏览一、进程程序替换
之前讲过 fork() 之后,父子进程各自执行父进程代码的一部分,也就是代码共享,数据默认也'共享',但是发生写入后就会以写时拷贝各自私有。那如果子进程想执行一个全新的程序成为一个真正独立的进程呢?这就需要通过进程的程序替换来完成这个功能!
程序替换是通过特定的系统调用接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中!
替换原理
进程替换原理很简单,就是进程调用某种系统调用,从磁盘中加载一份全新的代码和数据到该进程物理内存中,覆盖掉原进程在内存中的代码和数据。程序替换并没有创建新进程,只是改变了进程的物理内存。
进程替换需要调用 exec(注意不是 excel 表格)系列接口,一共有六个,还有一个接口我们后面补充:
我们先看最简单的 execl:
int execl (const char * path, const char * arg, ...) ;
我们要执行一个程序首先要找到它,第一个参数就是用来帮助我们找到它,第二个参数是我们要执行程序的程序名,三个点表示可变参数,可填可不填,如果要填这部分参数指的是给程序传递的命令行选项,并且该部分参数传递完毕后必须以 NULL 结尾。
传递参数注意事项:除了 path 外,后面的参数你在命令行中怎么写,就在这里怎么传递。
下面直接上示例:
#include <stdio.h>
#include <unistd.h>
int main () {
printf ("我是一个进程:%d\n" , getpid());
sleep(1 );
execl("/usr/bin/ls" , "ls" , "-a" , "-l" , "-n" , NULL );
printf ("运行结束\n" );
return 10 ;
}
我们看到 execl 替换后的执行结果和 ls 命令一样,说明这样确实就可以让这个程序不执行自己的代码和数据,转而去执行 ls 的代码和数据。
但是这里还有个现象,替换后 printf("运行结束\n"); 这条代码为什么没有运行了呢?很容易理解,因为你的程序替换后开始执行另一个程序的代码了,你自己的代码已经被覆盖了。所以程序替换一旦成功,后续代码不再执行,因为没有了!
那既然程序替换有成功,那也一定有失败,我们下面直接让程序替换失败来看现象:(执行一个不存在的指令就会失败)
#include <stdio.h>
#
{
( , getpid());
sleep( );
n = execl( , , , , , );
( , n);
;
}
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 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
include
<unistd.h>
int
main
()
printf
"我是一个进程:%d\n"
1
int
"/usr/bin/lllls"
"ls"
"-a"
"-l"
"-n"
NULL
printf
"运行结束,n = %d\n"
return
10
我们可以看到程序替换失败后后续代码还会正常执行,所以一旦程序替换后续的代码被执行了,就表示程序替换失败。
我们还可以看到替换失败后 execl 返回 -1,那如果替换成功还需要返回值吗?我们仔细想想,程序替换成功后 execl 的返回值就没有意义了,就算有后续代码也不会执行。所以程序替换如果成功,不需要、也不会有返回值!——>所以 execl 系列函数,一旦返回,必然失败!
#include <stdio.h>
#include <unistd.h>
int main () {
printf ("我是一个进程:%d\n" , getpid());
sleep(1 );
execl("/usr/bin/ls" , "ls" , "-a" , "-l" , "-n" , NULL );
printf ("程序替换失败!\n" );
return 10 ;
}
子进程程序替换示例 有了上面的认识,我们再回过头看程序替换理论,前面都是替换当前进程的代码和数据,但我们一开始介绍程序替换概念的时候说程序替换是用来让子进程执行全新的代码的,所以接下来将介绍子进程是如何程序替换。
在开始编写代码之前,我们要先理解一些概念,子进程确实可以被替换,那么子进程替换后会影响父进程吗?我们知道进程之间具有独立性一定不会影响,但是先前不是讲的父子共用同一段代码吗?子进程替换数据我们知道会发生写时拷贝,其实子进程进行代码替换时操作系统也会进行类似写时拷贝的工作。
所以当子进程进行程序替换时,会把子进程的代码和数据加载进内存,而此时父子进程共享代码和数据,所以就会发生写时拷贝,系统会为子进程开辟新的物理内存,子进程的代码和数据就会加载进物理内存中,这样就保证了进程的独立性。
下面我们直接上代码,注意子进程替换后它本质还是那个子进程,当子进程执行完替换后的程序退出时也需要父进程来等待回收它。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main () {
pid_t id = fork();
if (id == 0 ) {
sleep(2 );
printf ("我是一个进程:%d\n" , getpid());
sleep(1 );
execl("/usr/bin/ls" , "ls" , "-a" , "-l" , NULL );
exit (1 );
}
pid_t rid = waitpid(id, NULL , 0 );
if (rid > 0 ) {
printf ("wait: %d success\n" , rid);
}
return 0 ;
}
加载器 要运行一个二进制文件要先把它加载到内存,这是冯诺依曼体系结构规定的,因为 CPU 不能直接访问外设,只有加载进内存的代码和数据才能被 CPU 执行。加载是把数据从一个硬件加载到另一个硬件,所以一定需要操作系统来执行加载任务,只有操作系统有这个权力,所以加载底层一定会调用系统调用,那么对于 linux 而言,加载的本质其实就是调用程序替换的系统调用接口。
(很多人会疑惑:为什么 Linux 不能像 Windows 一样'直接创建进程加载代码'?这是 Linux 的历史设计逻辑:fork()(创建子进程,复制父进程)和 execve()(替换子进程)是分离的两个系统调用,这样可以在 fork() 和 execve() 之间插入额外逻辑(如修改环境变量、重定向输入输出),灵活性更高。而 Windows 的 CreateProcess 是'一站式'接口,将'创建进程'和'加载程序'合并,无需单独的程序替换步骤。)
#include <stdio.h>
#include <unistd.h>
int main (int argc, char * argv[]) {
printf ("我是一个进程:%d\n" , getpid());
sleep(1 );
char ** myargv = &argv[1 ];
execv(myargv[0 ], myargv);
printf ("程序替换失败!\n" );
return 10 ;
}
以上就是写的一个简易加载器程序,myexec 就是加载器本体,可以通过它加载其他程序。
六个 exec 系列函数串讲 下面我们把 exec 系列系统调用串在一起讲,我们先梳理一下这批参数的共性,第一个参数是指你要执行谁,第二个以及后续参数是指你要如何执行,快速记忆就是在命令行上怎么写就在这里怎么填。
这些函数原型看起来很容易混,但只要掌握了规律就很好记:
execl int execl (const char * path, const char * arg, ...) ;
execl 我们前面已经介绍过了,execl 中的 l 表示 list,因为传递参数是以一个一个单独的字符串传递的。第一个参数是要执行程序的路径,第二个参数我们以 ls 命令为例,可以写成 ls,也可以写成 /usr/bin/ls,后续可变参数是要 ls 命令的选项,最后一个参数以 NULL 结尾。
exec 系列函数的所有代码小编都只贴上面子进程程序替换中的子进程内部逻辑,因为其他基本都一样。
if (id == 0 ) {
printf ("我是一个进程:%d\n" , getpid());
sleep(1 );
execl("/usr/bin/ls" , "ls" , "-a" , "-l" , "-n" , NULL );
printf ("程序替换失败!\n" );
exit (1 );
}
execv int execv (const char * path, char * const argv[]) ;
我们可以看到 execv 第二个参数是以字符串数组的方式传递的,所以 execv 中的 v 表示 vector,用法和 execl 类似。数组的最后一个元素也要为 NULL。
if (id == 0 ) {
printf ("我是一个进程:%d\n" , getpid());
sleep(1 );
char * const myargv[] = {(char *)"pwd" , NULL };
execv("/usr/bin/pwd" , myargv);
printf ("程序替换失败!\n" );
exit (1 );
}
execlp int execlp (const char * file, const char * arg, ...) ;
execlp 除了第一个参数其他参数和 execl 一样。execl 第一个参数传要执行程序的路径,而 execlp 第一个参数只用传要执行程序的程序名就行(比如 ls 和 ./mycmd),代表的依旧是你要执行谁。原理就是使用 execlp 我们只用传要执行命令的名字,execlp 自己会去环境变量 path 中寻找指定的程序并执行。
#include <stdio.h>
#include <unistd.h>
int main () {
printf ("我是一个进程:%d\n" , getpid());
sleep(1 );
execlp("ls" , "ls" , "-a" , "-l" , "-n" , NULL );
printf ("程序替换失败!\n" );
return 10 ;
}
execvp int execvp (const char * file, char * const argv[]) ;
有了前面的三个的介绍想必 execvp 各位都能理解了把,直接上代码。
if (id == 0 ) {
printf ("我是一个进程:%d\n" , getpid());
sleep(1 );
execlp("ls" , "ls" , "-a" , "-l" , "-n" , NULL );
printf ("程序替换失败!\n" );
exit (1 );
}
execvpe int execvpe (const char * file, char * const argv[], char * const envp[]) ;
带 e 的程序替换接口可以让程序员灵活地控制传递给替换后程序的环境变量,无论是自定义的环境变量还是系统的环境变量。
下面示例代码逻辑是 myexec 程序里创建一个子进程,然后子进程程序替换为 mycmd 程序,mycmd 程序会打印它的命令行参数和环境变量。当我们直接运行 mycmd 时它会打印从父进程 bash 拿到的命令行参数和环境变量:
#include <stdio.h>
int main (int argc, char * argv[], char * env[]) {
for (int i = 0 ; argv[i]; i++) {
printf ("argv[%d], %s\n" , i, argv[i]);
}
for (int i = 0 ; env[i]; i++) {
printf ("env[%d], %s\n" , i, env[i]);
}
return 0 ;
}
当我们把自定义的命令行参数和环境变量通过 execvpe 传递给程序替换后的 mycmd 程序后 mycmd 就会打印出它拿到的我们自定义的命令行参数和环境变量:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main () {
pid_t id = fork();
if (id == 0 ) {
char * const myargv[] = {"wusaqi" , "cmd" , NULL };
char * const myenv[] = {"strggle=888" , "luck=666" , NULL };
execvpe("./mycmd" , myargv, myenv);
exit (0 );
}
pid_t rid = waitpid(id, NULL , 0 );
if (rid > 0 ) {
printf ("wait: %d success\n" , rid);
}
return 0 ;
}
如果我们想把系统的环境变量传给替换后的程序,execvpe 第三个参数就可以传 environ。所以通过程序替换接口传递环境变量表默认意义是摒弃掉老的环境变量表,使用你自己设置的全新的环境变量表。如果程序替换不传环境变量表,替换后的新程序会默认使用调用 exec 函数的当前进程(即被替换的原进程)的环境变量表。
除了只传自定义的环境变量和传系统的环境变量,我们还可以既传系统环境变量,又传自定义的环境变量。这需要我们事先调用 putenv 传递自己的环境变量到系统的环境变量 environ 中,然后程序替换时传递 environ。示例代码如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
extern char ** environ;
char * my = "wusaqi=12345" ;
int main () {
pid_t id = fork();
if (id == 0 ) {
char * const myargv[] = {"wusaqi" , "cmd" , NULL };
putenv(my);
execvpe("./mycmd" , myargv, environ);
exit (0 );
}
pid_t rid = waitpid(id, NULL , 0 );
if (rid > 0 ) {
printf ("wait: %d success\n" , rid);
}
return 0 ;
}
execle int execle (const char * path, const char * arg, ..., char * const envp[]) ;
第七个程序替换接口 我们之前介绍的六个 exec 系列函数都是 man 3 号手册的库函数,而接下来的这个 execve 是 man 2 号手册的系统调用,所有程序替换操作最后都会调用这个系统调用,把当前进程的命令行参数和环境变量传递给被替换的程序。也就是说上面介绍的 6 个程序替换库函数底层都会调用 execve。所以这六个库函数只是传参形式不同,设计这六个库函数的目的是为了满足未来不同场景的需求。
子进程执行用户写的程序 子进程不仅可以替换系统命令,也可以通过程序替换执行我们自己写的程序,只要能找到就行了,示例如下,让子程序执行我们自己用 C++ 写的 mycmd 程序:
#include <iostream>
int main () {
std::cout << "hello C++" << std::endl;
return 0 ;
}
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main () {
pid_t id = fork();
if (id == 0 ) {
sleep(2 );
printf ("我是一个进程:%d\n" , getpid());
sleep(1 );
execl("./mycmd" , "./mycmd" , NULL );
exit (1 );
}
pid_t rid = waitpid(id, NULL , 0 );
if (rid > 0 ) {
printf ("wait: %d success\n" , rid);
}
return 0 ;
}
程序替换可以调用任意语言的程序 有了上面用 C 语言替换 C++ 程序的铺垫后,小编想说不论是什么语言编写的代码运行后都会变成进程,而程序替换其实就是替换进程,所以我们可以用 C 语言写的程序替换代码调用其他语言运行起来的程序。
程序替换是操作系统的功能,所以不止 C 语言,任何语言编写的程序都能完成程序替换。
传递命令行参数和环境变量的 2 种方式
有了上面关于程序替换的认识,我们学习到了命令行参数和环境变量有两种传递方式,第一种方式是通过程序替换接口(exec**e)的方式将当前程序的环境变量或者其他任意环境变量灵活传递给替换该当前程序的程序,所有 exec 函数都会把命令行参数传递给替换后的程序。
第二种方式是父子进程之间通过虚拟地址空间传递,因为在父进程的进程地址空间中会存在它的命令行参数和环境变量,就如同全局变量一样,子进程会继承到父进程的进程虚拟空间和页表,自然子进程也能拿到命令行参数和环境变量。