目录
fork创建子进程,对于创建的子进程有两种场景,第一种子进程和父进程使用if-else分流,让子进程执行不同的代码块,实现代码的分流,第二种调用exec系列函数,让子进程执行不同的程序,实现"创建新进程运行新任务"的效果,本文就来介绍如何让单进程以及子进程进行程序替换从而执行不同的程序
一、什么是进程程序替换
进程程序替换,是一个已存在的进程(通常是 fork 出的子进程),通过 exec 函数族,彻底丢弃自身原有的代码段、数据段、从磁盘加载一个全新的可执行程序,并从头开始执行这个新程序的过程。
注意点:
- 该子进程的"身份标识"(PID、PPID、打开的文件描述符等)完全不变,变的只是进程"肚子里的执行内容";
- 替换是"整体覆盖",不是"叠加"------新程序加载后,原进程中 exec 函数之后的代码,永远不会执行(因为原代码段已被删除)。
二、单进程版------最简单的看程序替换
execl



- 上面是七种进程替换函数,要使用这些函数要包头文件 #include<unistd.h> ,其中函数名中带 e 的函数需要声明一个全局变量 environ,其中我们首先看第一个execl函数,这个execl,要进行调用首先要传入路径 ,即要替换的可执行文件的路径 ,接下来是可执行的文件名 ,接下来是跟选项,选项要以NULL结尾,由于一个命令的选项可能有多个,所以execl的第三个形参是可变参数,即选项可以有任意个,但是一定要以NULL结尾,execl中的 l 是list的意思,代表列举,即将选项列举在execl上
- 例如,我们在bash命令行中调用 ls -a -l,我们写了可执行文件名ls,以及它对应的选项-a -l,其实也就是命令行如何写,我们就如何调用execl这个函数,只不过要添加一个文件路径,使用引号" "将命令以及选项引起来,最后使用NULL结尾即可
- 我们要执行一个程序,首先就是找到这个程序,然后是执行这个程序,如何执行这个程序,要不要涵盖选项,涵盖的选项有哪些,那么对于exec系列接口中,路径是如何找到程序,命令加选项加NULL就是如何执行这个程序
- 下面我们就在单进程中,实际调用一下execl执行程序替换,看看会发生什么现象,同时我们再使用shell脚本检测一下程序替换的过程中有没有创建新进程
运行结果:
- 在这个运行过程中,观察右边脚本监视,显示我们的进程运行起来,创建了进程,休眠2秒后们进行execl程序替换,程序替换的过程中并没有新进程,那么我们可以得出进程替换过程中,并不会创建新进程
- 观察左边的打印以及执行情况,对于打印仅仅打印了execl程序替换前的内容 ,程序替换后的打印任务并没有做,所以我们可以得出程序替换成功后,后续的代码并不会执行
- 当我们输入一个不存在的指令的路径的时候,此时exec系列函数就会找不到可执行程序的路径,就会失败,即程序替换失败的时候,exec系列函数会返回-1 ,后续的代码才会执行。所以exec系列函数只有失败的返回值,没有成功的返回值
三、谈进程替换的原理
其实上面的程序替换是不准确的,更准确的叫法是进程替换

当进程在命令行启动时,bash会fork一个子进程。操作系统会为该进程创建PCB、地址空间和页表,并将磁盘上的代码数据加载到物理内存,建立虚拟地址到物理地址的页表映射。随后,进程被加入CPU运行队列等待调度。在这个过程中,exec系列函数实际上扮演了加载器的角色 :bash fork子进程后,通过exec函数将可执行程序加载到内存,形成可运行的进程。
**当调用exec函数时,当前进程的代码和数据会被新程序完全替换,并重新建立页表映射。**需要注意的是:
- 虚拟地址空间保持不变
- 仅调整地址空间的部分字段
- 保留原PCB 整个过程不会创建新进程,而是直接跳转到新程序的起始代码继续执行。
这里有一个疑问,操作系统如何得知要从哪个位置开始执行代码,换句话来说,CPU如何得知我们程序的入口地址?
关于程序入口地址的问题:在Linux系统中,可执行程序采用ELF格式。编译时,程序的入口地址会被写入ELF文件头部。当程序启动时,操作系统会读取这个头部信息,CPU就能获取到正确的执行起始位置,从而顺利运行程序。
四、多进程版------验证各种程序替换接口
execlp

下面我们学习execlp函数。与execl相比,execlp函数名中的"p"代表path路径。在 execlp 函数中, file 参数是"要执行的程序名"(比如 "ls" )。因为 execlp 带了 p (对应PATH环境变量),所以它会自动在系统 PATH 指定的路径(比如 /bin 、 /usr/bin )里搜索这个 file 对应的可执行文件。execlp只能搜索系统命令,因为这些命令的路径都包含在PATH环境变量中。如果要执行我们自己编写的程序(不在PATH中),execlp会搜索失败导致程序替换失败,此时函数返回-1。

那么下来我们使用fork创建子进程的方式,让子进程进行程序替换,让子进程执行ls命令,而父进程则一直wait阻塞式等待子进程即可

运行结果:
- 这段代码的核心逻辑是利用 fork+exec+wait 实现"父进程启动子进程执行外部命令":父进程先打印自身信息,通过 fork 创建子进程;子进程打印替换前信息后,用 execlp 替换为 ls -a -l 并执行;父进程则阻塞等待子进程,待其执行完 ls 退出后,验证子进程正常结束并打印结果。
- 最终效果是父进程安全启动独立子进程完成 ls 命令的执行,和Linux终端执行命令的底层逻辑一致(终端作为父进程,启动子进程执行命令)。
- 其实bash也是如此,当我们需要在命令行执行自己的可执行程序或系统的命令的时候,bash首先会创建子进程,然后进程程序替换,此时子进程就被替换成为了我们的可执行程序或系统的命令。
execv

接下来我们学习execv函数,并且我们观察一下,对于execv它的函数名并没有 l 而是v,这个v我们可以理解为vector,即是我们的命令加选项的字符串,同时最后仍然需要以NULL结尾,去传入一个字符指针数组 (对应"命令+选项",数组最后一个元素必须是 NULL ,比如 {"ls", "-a", "-l", NULL} ),进行传参,由于函数名中没有p,所以需要我们手动传入指令的路径
那么我们使用fork创建子进程的方式,让子进程进行程序替换,让子进程执行命令,而父进程则一直wait阻塞式等待子进程即可

运行结果:
- 整体代码逻辑和 execlp 一致,核心都是"父进程fork子进程→子进程程序替换→父进程wait子进程",只是把"程序替换的函数从 execlp 换成了 execv ",区别仅在于传参方式和路径的写法:之前 execlp 是"逐个传参+自动搜PATH ";现在 execv 是"数组传参+手动写完整路径"。
execvp

对于execvp函数中,函数名中有v,代表指令加选项加NULL需要以字符指针数组形式传参,并且函数名中有p代表我们不需要传入路径,直接传入命令(可执行程序名)即可
那么我们使用fork创建子进程的方式,让子进程进行程序替换,让子进程执行命令,而父进程则一直wait阻塞式等待子进程即可

运行结果:

execle

- execle函数的函数名中有 l 代表指令加选项加NULL需要以参数的形式传入,并且execle函数名中没有p,那么表示需要我们传入指令的路径,观察execle函数名中还有e,这个e其实是环境变量的意思,代表我们自定义环境变量替换原来的环境变量
- 既然 exec 系列函数能替换执行 ls 这类系统自带的命令,那能不能用它来替换执行我们自己编写的程序呢?
- 答案是可以的------就像之前代码里做的那样:我们先写一个C++程序,然后用 execl 函数让子进程替换为这个自己写的程序并执行。
对于C++程序,在linux中c++文件的后缀可以是cxx,可以是cc,同样也可以是cpp,但是由于cpp更为熟悉,所以我们这里使用cpp为后缀进行讲解,这里编写的c++程序可以打印命令行参数和环境变量

在我们自己的c语言程序中,那么我们使用fork创建子进程的方式,让子进程进行程序替换,让子进程执行我们自己的编译好的c++程序,而父进程则一直wait阻塞式等待子进程即可

下面就是自动化构建工具的编写,前面是构建一个可执行文件,思考一下我们该如何同时构建两个可执行文件?

运行结果:
- 如上,我们就可以在我们的c语言程序中,fork创建子进程,对于这个子进程可以替换为c++程序执行
- 但是对于这个c++程序,其实我们是使用的execl进行替换调用的,即我们并没有显示传入环境变量,但是为什么这个c++程序却可以打印出环境变量呢?
- 其实是由于环境变量具有全局属性,环境变量也是数据,在地址空间中环境变量的位置在栈的上方,由于父进程fork创建子进程的时候,由于子进程并没有自己的代码和数据以及内核数据结构,所以子进程会将父进程的内核数据结构大部分内容全部cv一份,所以对应的地址空间上的环境变量对应的虚拟地址,以及页表中环境变量对应的虚拟地址和物理地址的映射关系全部被子进程cv了一份,所以环境变量即使我们不传入,子进程中也会继承父进程的环境变量,即进程替换的时候,环境变量并不会被替换
如何给子进程传递环境变量
- 有两种方式,第一种是新增环境变量,第二种是完全替换
putenv传递环境变量

第一种方式,使用库函数putenv的方式(putenv可以新增环境变量到父进程中,这样fork子进程后,子进程自然而然继承父进程后也继承到了新增环境变量,同样可以在if-else代码分流的子进程中在子进程的代码区域putenv新增环境变量,putenv父进程和子进程上的区别就是是否修改父进程环境变量),给子进程新增环境变量

运行结果:
- 此时子进程的环境变量就在原来继承的父进程的环境变量的基础上新增了MYVALUE
execle自定义环境变量传参
第二种方式,创建自定义的字符串指针数组作为自定义环境变量,自定义环境变量通过程序替换中的execle或者execvpe接口进行自定义环境变量的传参

运行结果:
此时子进程的环境变量就被我们的自定义环境变量完全替换了
execvpe

- 对于execvpe函数,其函数名中有v,代表我们需要将指令加选项加NULL以字符串指针数组的形式传参
- 其函数名有p,代表我们不需要写入路径,只需要写入文件名即可,但是这里不同,由于小编要执行的是我们自己的程序,而我们自己的程序的路径并不在系统的路径中,如果让execvpe函数去PATH环境变量中的系统路径中查找会找不到可执行文件的路径,会返回-1表示程序替换失败,所以这里仍然需要我们手动传入可执行程序的路径
- 其函数名中有e,代表我们需要传入环境变量,其实这个环境变量也可以是父进程原封不动的环境变量,即使用第三方环境变量extern char** environ即可,下面我们就来演示一下

运行结果:
父进程行为:成功打印自身 pid ,通过 fork 创建子进程,然后调用 wait 阻塞等待子进程结束;
子进程行为:替换前打印自身 pid (与父进程 pid 不同);调用 execve 替换为 ./otherExe 程序,并传递参数 -a / -b / -c 、传递环境变量 ;由于 execve 执行成功,子进程后续的 printf / exit 未执行(符合 exec 函数的特性);execve 的作用:子进程完全替换为 otherExe ,执行 otherExe 的逻辑(打印命令行参数、环境变量)。
- 命令行参数传递成功: otherExe 打印出了 [0]otherExe [1]-a [2]-b [3]-c ,说明 myargv 数组的参数被正确传递;
- 环境变量传递成功: otherExe 打印出了系统环境变量(如 SHELL / HOME 等),说明 environ (全局环境变量数组)被正确传递;
- 进程同步正常:父进程通过 wait 捕获到子进程的退出状态(退出码 0 ),说明父子进程的同步逻辑有效。
execve

事实上还存在一个进程替换函数execve,它位于2号手册中,说明这是一个系统调用。而execl、execlp、execv、execvp、execle和execvpe这六个函数则是库函数。本质上,这六个进程替换函数都是对execve的封装,它们的底层实现最终都会转化为execve系统调用。因此,这些库函数的主要区别仅在于参数传递方式的不同。
四、拓展
进程替换shell脚本
Shell脚本是一种解释性语言,在Linux系统中由Bash解释器执行。Shell脚本文件通常以.sh为后缀名,且文件首行必须包含#!/usr/bin/bash声明。这个#!组合是固定格式,而/usr/bin/bash则指向Bash解释器的安装路径。Shell脚本的主要内容由常规Bash命令构成,并辅以特定的语法特性。本质上,Shell脚本就是纯文本文件,由Bash解释器逐行解析执行。运行脚本时,只需执行bash 文件名命令即可

运行结果:

那么接下来我们使用exec系列函数,以execl为例将fork的子进程程序替换后执行,这里的路径就应该是可执行程序bash的路径,即/usr/bin/bash,选项中带上bash加shell脚本文件加NULL即可

运行结果:

进程替换python语言
- python也是一门解释性语言,解释器是python系列,这里我们使用python3,python程序的文件后缀是.py
- python程序的内容开头的格式同样是要使用#!后面跟上解释器的路径,即/usr/bin/python3
- python程序运行的时候,我们使用python系列运行即可,这里我们使用python3

运行结果:

那么接下来我们使用exec系列函数,以execl为例将fork的子进程程序替换后执行,这里的路径就应该是可执行程序python3的路径,即/usr/bin/python3,选项中带上python3加python程序文件加NULL即可

运行结果:

- 无论使用C++、Shell脚本、Python还是其他编程语言,所有由这些语言编写的文件在编译后生成的可执行程序,都必须以进程的形式运行。可执行程序需要先被加载到内存中,才能被CPU调度执行。具体加载过程是通过bash创建子进程,并借助exec系列函数(其本质就是加载器的工作原理)实现的。exec系列函数利用程序替换机制将可执行程序加载到内存。
- 由此可见,正是由于exec系列函数的存在,即便我们使用C语言,也能调用其他语言编写的可执行程序。事实上,所有主流编程语言都提供了类似exec系列的函数功能,这使得不同语言之间能够实现相互调用。
五、总结
本文介绍了Linux系统中进程程序替换的机制和实现方法。通过fork创建子进程后,可以使用exec系列函数实现两种场景:1)父子进程执行不同代码块;2)子进程执行全新程序。重点讲解了exec函数族(execl、execlp、execv、execvp、execle、execvpe)的使用方法和区别,包括参数传递方式(列表或数组)、路径处理(自动搜索PATH或手动指定)以及环境变量处理(继承或自定义)。通过实例演示了如何替换执行系统命令、自定义程序、Shell脚本和Python程序,并解释了进程替换的原理:不创建新进程,而是重用原有进程结构,仅替换代码和数据段。最后指出execve是系统调用,其他exec函数都是对其的封装,不同语言都提供了类似功能实现程序间调用。
感谢大家的观看!







