Linux 进程程序替换
若父进程希望创建的子进程执行和父进程完全不一样的全新的程序,就可以利用进程的程序替换来实现这个功能
程序替换是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中
替换原理
- fork() 系统调用
- 创建子进程时,会复制父进程的进程地址空间(代码段、数据段、堆栈等)
- 子进程获得父进程文件描述符表的副本
- 父子进程执行相同的程序,通过返回值区分:
pid_t pid = fork();
if (pid == 0) {
// 子进程代码
} else {
// 父进程代码
}
- exec 函数族
- 包含多个变体:execl, execv, execle, execve 等
- 核心功能是替换当前进程的地址空间:
char *argv[] = {"ls", "-l", NULL};
execvp("ls", argv);
- 特点:
- 不创建新进程(PID不变)
- 新程序从 main() 开始执行
- 文件描述符默认保持打开(除非设置 FD_CLOEXEC)
- 环境变量处理
- exec 调用默认继承原环境变量
- 使用 execle/execve 可以指定新环境:
char *envp[] = {"PATH=/usr/bin", NULL};
execle("/bin/ls", "ls", NULL, envp);
- 程序计数器(PC)处理
- 进程控制块(PCB)中保存了执行位置信息
- exec 调用后,PC 会被设置为新程序的入口点(通常是 _start 或 main)
下面这个图示完美的展示了程序替换所作出的改动:

替换函数
有六种以 exec 开头的函数,统称为 exec 函数:
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,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[]);
execl 函数
首先,我们先来了解一下 execl 函数的用法:
execl() 是 exec 函数族中的一个成员,用于 替换当前进程的镜像(加载并执行新程序)。它的特点是 参数以可变参数列表(...)的形式传递,适合参数数量已知且固定的场景。
函数原型
#include <unistd.h>
int execl(const char *path, const char *arg, ... /* (char *) NULL */);
参数说明
path- 新程序的 完整路径(如
"/bin/ls")。 - 必须是可执行文件的绝对路径或相对路径(依赖
PATH环境变量的execlp是另一个函数)。
- 新程序的 完整路径(如
arg- 第一个参数是 新程序的名称(通常与
path的最后一部分一致,但可以任意命名)。 - 例如:
"ls"是程序名,"-l"是它的参数。
- 第一个参数是 新程序的名称(通常与
...- 可变参数列表,表示传递给新程序的 命令行参数。
- 必须以
(char *) NULL结尾(表示参数结束)。
返回值
- 成功:不返回(原程序已被替换,后续代码不会执行)。
- 失败:返回
-1,并设置errno(例如文件不存在、权限不足等)。
使用示例
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
int main()
{
// 先创建一个子进程
pid_t id = fork();
if(id == 0)
{
cout << "子进程pid:" << getpid() << endl;
// 让子进程执行 ls -a -l 命令
execl("/usr/bin/ls", "ls", "-l", "-s", nullptr);
// 若执行到了这里,说明程序替换失败
exit(1);
}
int ret = waitpid(-1, nullptr, 0);
if(ret < 0){
perror("waitpid");
exit(2);
}
cout << "父进程已回收子进程 " << id << endl;
return 0;
}
执行结果如下:
╭─ljx@VM-16-15-debian ~/linux_review/lesson2
╰─➤ ./pro_rep.o
子进程pid:2880883
total 64
4 -rw-r--r-- 1 ljx ljx 594 Jul 17 13:40 pro_rep.cc
20 -rwxr-xr-x 1 ljx ljx 16880 Jul 17 13:40 pro_rep.o
4 -rw-r--r-- 1 ljx ljx 2009 Jul 16 23:14 pro_wait.cc
36 -rwxr-xr-x 1 ljx ljx 33808 Jul 16 23:14 pro_wait.o
父进程已回收子进程 2880883
同样,我们可以利用这个函数执行自己的可执行程序:
// pro_rep.cc
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
int main()
{
// 先创建一个子进程
pid_t id = fork();
if(id == 0)
{
cout << "子进程pid:" << getpid() << endl;
// 让子进程执行 ls -a -l 命令
execl("./test.o", "test.o", "aaa", "bbb", nullptr);
// 若执行到了这里,说明程序替换失败
exit(1);
}
int ret = waitpid(-1, nullptr, 0);
if(ret < 0){
perror("waitpid");
exit(2);
}
cout << "父进程已回收子进程 " << id << endl;
return 0;
}
// test.cc
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
for(int i = 1; argv[i]; ++i)
{
cout << i << ":" << argv[i] << endl;
}
return 0;
}
我们可以发现,替换的进程会将 execl 传递的命令行参数接受,我们可以将其打印出来
execv 函数
该函数和 execl 函数唯一的区别就在于该函数不是通过可变参数模板传递命令行参数的,而是通过数组的方式传递参数:
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <vector>
#include <cstring>
using namespace std;
int main()
{
// 先创建一个子进程
pid_t id = fork();
if(id == 0)
{
cout << "子进程pid:" << getpid() << endl;
// 让子进程执行 ls -a -l 命令(通过数组传递参数)
vector<char*> argv = {const_cast<char*>("test.o"), const_cast<char*>("aaa"), const_cast<char*>("bbb"), nullptr};
execv("./test.o", argv.data());
// 若执行到了这里,说明程序替换失败
exit(1);
}
int status;
int ret = waitpid(-1, &status, 0);
if(ret < 0){
perror("waitpid");
exit(2);
}
if(WIFEXITED(status)){
cout << "子进程 " << id << "运行完毕,状态码:" << WEXITSTATUS(status) << endl;
}
else cout << "子进程 " << id << "异常退出,信号:" << WTERMSIG(status) << "(" << strsignal(WTERMSIG(status)) << ")" << endl;
return 0;
}
执行结果如下:
╭─ljx@VM-16-15-debian ~/linux_review/lesson2
╰─➤ ./pro_rep.o
子进程pid:2891886
1:aaa
2:bbb
子进程 2891886运行完毕,状态码:0
可以看到,我们通过父进程获取了子进程的退出状态,这里我们让子进程触发除零错误,此时即便子进程被进程程序替换了,父进程依然可以知晓子进程的状态,
因为子进程的程序替换并没有替换进程 pid,因此父进程依然和子进程保持着父子关系:
// 修改替换程序,故意引发除零错误
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
int a = 10 / 0;
for(int i = 1; argv[i]; ++i)
{
cout << i << ":" << argv[i] << endl;
}
return 0;
}
╭─ljx@VM-16-15-debian ~/linux_review/lesson2
╰─➤ ./pro_rep.o
子进程pid:2893724
子进程 2893724异常退出,信号:8(Floating point exception)
可以看到,子进程的异常状况被父进程捕捉到了
execlp 和 execvp
execlp 和 execvp 命令在执行环境变量路径下的命令时不需要带路径,可以自动根据环境变量搜索命令:
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <vector>
#include <cstring>
using namespace std;
int main()
{
// 先创建一个子进程
pid_t id = fork();
if(id == 0)
{
cout << "子进程pid:" << getpid() << endl;
// 让子进程执行 ls -a -l 命令(通过数组传递参数)
// vector<char*> argv = {const_cast<char*>("test.o"), const_cast<char*>("aaa"), const_cast<char*>("bbb"), nullptr};
execlp("ls", "-a", "-l", nullptr);
// 若执行到了这里,说明程序替换失败
exit(1);
}
int status;
int ret = waitpid(-1, &status, 0);
if(ret < 0){
perror("waitpid");
exit(2);
}
if(WIFEXITED(status)){
cout << "子进程 " << id << "运行完毕,状态码:" << WEXITSTATUS(status) << endl;
}
else cout << "子进程 " << id << "异常退出,信号:" << WTERMSIG(status) << "(" << strsignal(WTERMSIG(status)) << ")" << endl;
return 0;
}
可以看到,即便不指定路径,依然可以获取到可执行程序:
╭─ljx@VM-16-15-debian ~/linux_review/lesson2
╰─➤ ./pro_rep.o
子进程pid:2895026
total 88
-rw-r--r-- 1 ljx ljx 1002 Jul 17 14:26 pro_rep.cc
-rwxr-xr-x 1 ljx ljx 16936 Jul 17 14:26 pro_rep.o
-rw-r--r-- 1 ljx ljx 2009 Jul 16 23:14 pro_wait.cc
-rwxr-xr-x 1 ljx ljx 33808 Jul 16 23:14 pro_wait.o
-rw-r--r-- 1 ljx ljx 202 Jul 17 14:23 test.cc
-rwxr-xr-x 1 ljx ljx 16592 Jul 17 14:26 test.o
子进程 2895026运行完毕,状态码:0
但是,若不是系统命令,自己的可执行程序的路径不在环境变量当中,当然就需要指定路径了,若不指定路径则会导致程序替换失败,下面用 execvp 举例:
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <vector>
#include <cstring>
using namespace std;
int main()
{
// 先创建一个子进程
pid_t id = fork();
if(id == 0)
{
cout << "子进程pid:" << getpid() << endl;
// 让子进程执行 ls -a -l 命令(通过数组传递参数)
vector<char*> argv = {const_cast<char*>("test.o"), const_cast<char*>("aaa"), const_cast<char*>("bbb"), nullptr};
execvp("test.o", argv.data());
// 若执行到了这里,说明程序替换失败
exit(1);
}
int status;
int ret = waitpid(-1, &status, 0);
if(ret < 0){
perror("waitpid");
exit(2);
}
if(WIFEXITED(status)){
cout << "子进程 " << id << "运行完毕,状态码:" << WEXITSTATUS(status) << endl;
}
else cout << "子进程 " << id << "异常退出,信号:" << WTERMSIG(status) << "(" << strsignal(WTERMSIG(status)) << ")" << endl;
return 0;
}
执行结果如下:
╭─ljx@VM-16-15-debian ~/linux_review/lesson2
╰─➤ ./pro_rep.o
子进程pid:2895822
子进程 2895822运行完毕,状态码:1
execle 和 execve
这两个命令可以传递新的环境变量到替换的程序当中去,这里的传递是覆盖式传递,而不是追加环境变量,因此若想追加环境变量一定要在原始的环境变量参数上追加新的环境变量再传递参数:
假若父进程只希望给子进程追加一个环境变量而父进程不需要追加,实现方式如下:
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <vector>
#include <cstring>
using namespace std;
int main()
{
// 先创建一个子进程
pid_t id = fork();
if(id == 0)
{
cout << "子进程pid:" << getpid() << endl;
// 让子进程执行 ls -a -l 命令(通过数组传递参数)
vector<char*> argv = {const_cast<char*>("test.o"), const_cast<char*>("aaa"), const_cast<char*>("bbb"), nullptr};
size_t len = 0;
while (environ[len] != nullptr) len++;
// 使用指针范围构造 vector
std::vector<char*> new_env(environ, environ + len);
new_env.push_back(const_cast<char*>("new_env_op=111"));
new_env.push_back(nullptr);
execve("./test.o", argv.data(), new_env.data());
// 若执行到了这里,说明程序替换失败
exit(1);
}
int status;
int ret = waitpid(-1, &status, 0);
if(ret < 0){
perror("waitpid");
exit(2);
}
if(WIFEXITED(status)){
cout << "子进程 " << id << "运行完毕,状态码:" << WEXITSTATUS(status) << endl;
}
else cout << "子进程 " << id << "异常退出,信号:" << WTERMSIG(status) << "(" << strsignal(WTERMSIG(status)) << ")" << endl;
return 0;
}
我们让子进程的替换程序打印环境变量,可以看到子进程最终成功打印了新追加的环境变量:
26:new_env_op=111
子进程 2899087运行完毕,状态码:0
下面是对这6个函数的总结:
| 函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
|---|---|---|---|
| execl | 列表 | 否 | 是 |
| execlp | 列表 | 是 | 是 |
| execle | 列表 | 否 | 否,须自己组装环境变量 |
| execv | 数组 | 否 | 是 |
| execvp | 数组 | 是 | 是 |
| execve | 数组 | 否 | 否,须自己组装环境变量 |