文章目录

序章:蝶变的奥秘
在Linux操作系统的广袤天地中,进程如同生命体般流转不息。而在这流转的过程中,最为神奇的莫过于
进程程序替换
——一种灵魂的脱胎换骨,是进程保留其身份却完全改变内涵的奇妙现象。如同蝴蝶破茧而出,进程在替换中褪去旧壳,焕发新生。
本文将带领读者探索这种数字世界中的"灵魂转生",揭示Linux进程程序替换的深邃哲理与优雅实现。
🏙️正文
一、为何要进行程序替换?
在学习相关函数前,先要弄清楚为何要进行程序替换?
- 将运行中的程序看作一个
任务处理平台
- 由我们发出指令,交给
任务处理平台
去完成 - 因为每次发出的指令都可能不相同,所以
任务处理平台
中的代码不能固化 - 为了解决这个问题,
任务处理平台
可以通过创建子进程,让子进程完成对应指令 - 子进程实现对应指令依赖于程序替换
总结: 程序替换的目的是让子进程帮我们执行特定任务
就像汽车拥有各种各样的轮胎,如越野时需要换上路面兼容性更好、更耐造的越野胎;日常家用时,舒适性更好、胎噪更小的轮胎显然就更合适了,针对不同的使用场景替换不同的轮胎,程序替换时也是这么个意思,执行特定任务
shell 外壳中
的 bash
就是一个任务处理平台,当我们发出指令,如ls、pwd、touch
等指令时后,bash
会创建子进程,将其替换为对应的指令程序并执行任务,就能实现各种指令
进程程序替换图解
- Linux 中的指令都是用
C语言
写的可执行程序,所以可以进行替换 bash
运行后,输入指令
本质上就是在进行程序替换
二、七大替换函数
进程程序替换函数共有七个,其中六个都是在调用函数6,因此函数6 execve
才是真正的系统级接口
这些函数都属于 exec
替换家族,所以它们的返回值都一样
注意: 这七个函数只有在程序替换失败后才会有返回值,返回 -1,程序替换成功后不返回
程序都已经替换成功,后续代码也都将被替换,所以成功后的返回值也就没意义了
2.1、函数1 execl
首先是最简单的替换函数 execl
函数解读
- 返回值:替换失败返回 -1
- 参数1:待替换程序的路径,如 /usr/bin/ls
- 参数2:待替换程序的名称,如 ls
- 参数3~N:待替换程序的选项,如 -a -l等,最后一个参数为 NULL,表示选项传递结束
- … 表示可变参数列表,可以传递多个参数
注意: 参数选项传递结束或不传递参数,都要在最后加上 NULL
,类似于字符串的 ‘\0’
#include <stdio.h>
#include <unistd.h>
int main()
{
//execl 函数
printf("程序替换前,you can see me\n");
int ret = execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
//程序替换多发生于子进程,也可以通过子进程的退出码来判断是否替换成功
if(ret == -1)
printf("程序替换失败!\n");
printf("程序替换后,you can see me again?\n");
return 0;
}
可以看出,函数 execl 中
的 命令+选项+NULL
是以链式
的方式进行传递的
2.2、函数2 execv
替换函数 execv
是以顺序表 vector
的方式传递 参数2~N
的
函数解读
- 返回值:替换失败返回 -1
- 参数1:待替换程序的路径,如 /usr/bin/ls
- 参数2:待替换程序名及其命名构成的
指针数组
,相当于一张表
注意: 虽然 execv 只需传递两个参数,但在创建 argv 表时,最后一个元素仍然要为 NULL
#include <stdio.h>
#include <stdlib.h> //exit 函数头文件
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
//execv 函数
pid_t id = fork();
if(id == 0)
{
printf("子进程创建成功 PID:%d PPID:%d\n", getpid(), getppid());
char* const argv[] =
{
"ls",
"-a",
"-l",
NULL
}; //argv 表,实际为指针数组
execv("/usr/bin/ls", argv);
printf("程序替换失败\n");
exit(-1); //如果子进程有此退出码,说明替换失败
}
int status = 0;
waitpid(id, &status, 0); //父进程阻塞等待
if(WEXITSTATUS(status) != 255)
{
printf("子进程替换成功,程序正常运行 exit_code:%d\n", WEXITSTATUS(status));
}
else
{
printf("子进程替换失败,异常终止 exit_code:%d\n", WEXITSTATUS(status));
}
return 0;
}
正常运行的情况:
错误运行的情况,改变 path:
execv("/usr/bin", argv); //故意提供错误路径

与 execl
函数不同,execv
是以表
的形式进行参数传递的
2.3、函数3 execlp
可能有的人觉得写 path
路径很麻烦,还有可能会写错,那么能否换成 自动挡 替
换呢?
答案是可以的,execlp
函数在进行程序替换时,可以不用写path
路径
#include <unistd.h>
int execlp(const char* file, const char* arg, ...);
函数解读
- 返回值:替换失败返回 -1
- 参数1:待替换程序名,如 ls、pwd、clear
- 参数2~N:可变参数列表,为命令的选项
execlp
就像是 execl
的升级版,可以自动到 PATH 变量中查找程序
注意: 只能在环境变量表
中的 PATH 变量中搜索,如果待程序路径没有在 PATH 变量中,是无法进行替换的
#include <stdio.h>
#include <stdlib.h> //exit 函数头文件
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
//execlp 函数
pid_t id = fork();
if(id == 0)
{
printf("you can see me\n");
execlp("ls", "ls", "-a", "-l", NULL); //程序替换
printf("you can see me again?");
exit(-1);
}
int status = 0;
waitpid(id, &status, 0); //等待阻塞
if(WEXITSTATUS(status) != 255)
printf("子进程替换成功 exit_code:%d\n", WEXITSTATUS(status));
else
printf("子进程替换失败 exit_code:%d\n", WEXITSTATUS(status));
return 0;
}
使用execlp
替换程序更加方便,只要待替换程序路径位于 PATH
中,就不会替换失败
2.4、函数4 execvp
execv
加个 p
也能实现自动查询替换,即 execvp
#include <unistd.h>
int execvp(const char* file, char* const argv[]);
#include <stdio.h>
#include <stdlib.h> //exit 函数头文件
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
//execvp 函数
pid_t id = fork();
if(id == 0)
{
printf("子进程创建成功 PID:%d PPID:%d\n", getpid(), getppid());
char* const argv[] =
{
"ls",
"-a",
"-l",
NULL
};
execvp("ls", argv);
printf("程序替换失败\n");
exit(-1); //如果子进程有此退出码,说明替换失败
}
int status = 0;
waitpid(id, &status, 0); //父进程阻塞等待
if(WEXITSTATUS(status) != 255)
{
printf("子进程替换成功,程序正常运行 exit_code:%d\n", WEXITSTATUS(status));
}
else
{
printf("子进程替换失败,异常终止 exit_code:%d\n", WEXITSTATUS(status));
}
return 0;
}
函数解读
- 返回值:替换失败返回 -1
- 参数1:待替换程序名,需要位于 PATH 中
- 参数2:待替换程序名及其命名构成的
指针数组
假若参数1 file 的路径不在 PATH 中,程序会替换错误
如果想替换自己写的程序,那么只需要将路径添加至 PATH
中即可
2.5、函数5 execle
e
表示 env 环境变量表,可以将自定义或当前程序中的环境变量表传给待替换程序
#include <unistd.h>
int execl(const char* path, const char* arg, ..., char* const envp[]);
函数解读
- 最后一个参数:替换成功后,待替换程序的环境变量表,可以自定义
char* const myenv[] = {"myval=100", NULL}; //自定义环境变量表
execle("./other/CPP", NULL, myenv); //程序替换
替换为自己写的程序 CPP
//当前源文件为 test.cc 即 C++源文件
// .xx 后缀也可以表示 C++源文件
#include <iostream>
using namespace std;
extern char** environ; //声明环境变量表
int main()
{
int pos = 0;
//只打印5条
while(environ[pos] && pos < 5)
{
cout << environ[pos++] << endl;
}
return 0;
}
按照预期替换程序并传入自定义环境变量表后
可以看到,程序 CPP 中的环境变量表变成了自定义环境变量,即只有一个环境变量 myval=100
改变 execle
最后一个参数,传入默认环境变量表
extern char** environ;
execle("./other/CPP", NULL, environ); //继承环境变量表
结论: 如果主动传入环境变量后,待替换程序中的原环境变量表将被覆盖
现在可以理解为什么在 bash 中创建程序并运行,程序能继承 bash 中的环境变量表了
- 在
bash
下执行程序,等价于在bash
下替换子进程为指定程序,并将bash
中的环境变量表environ
传递给指定程序 - 其他没有带
e
的替换函数,默认传递当前程序中的环境变量
2.6、函数6 execve
execve
是系统真正提供的程序替换函数,其他替换函数都是在调用 execve
比如
execl
相当于将链式信息转化为 argv 表,供execve
参数2使用execlp
相当于在 PATH 中找到目标路径信息后,传给execve
参数1使用execle
的 envp 最终也是传给execve
中的参数3
#include <unistd.h>
int execve(const char* filename, char* const argv[], char* const envp[]);
函数解读
- 返回值:替换失败返回 -1
- 参数1:待替换程序的路径
- 参数2:待替换程序名及其参数组成的 argv 表
- 参数3:传递给待替换程序的环境变量表
替换 ls -a -l
程序
extern char** environ;
execve("/usr/bin/ls", argv, environ);
替换为自定义程序 CPP
extern char** environ;
execve("./other/CPP", argv, environ);
替换函数除了能替换为 C++ 编写的程序外,还能替换为其他语言编写的程序,如 Java、Python、PHP
等等,虽然它们在语法上各不相同,但在 OS
看来都属于 可执行程序,数据位于 代码段
和 数据段
,直接替换即可
系统级接口是不分语言的,因为不论什么语言最终都需要调用
系统级接口
,比如文件流操作中的open
、close
、write
等函数,无论什么语言的文件流操作函数都需要调用它们
2.7、函数7 execvpe
对execvp
的再一层封装,使用方法与 execvp
一致,不过最后一个参数可以传递环境变量表
#include <unistd.h>
int execvpe(const char* file, char* const argv[], char* const envp[]);
函数解读
- 返回值:替换失败返回 -1
- 参数1:待替换程序名,需要位于
PATH
中 - 参数2:待替换程序名及其命名构成的
指针数组
- 参数3:传递给待替换程序的环境变量表
extern char** environ;
execvpe("ls", argv, environ);
三、补充
最后再补充一些关于程序替换的知识
3.1、函数名记忆
七大替换函数按 程序名+选项
传递方式可以分为两组
- 列表:execl、execlp、execle
- 顺序:execv、execvp、execve、execvpe
可以看出,列表传递中必有 l,顺序传递则必有 v,函数名中字符的含义如下
exec
该函数隶属于程序替换家族
- l 即 list,列表传递
- v 为 vector,顺序传递
- p 表示 PATH,根据程序名自动在 PATH 中查找
- e 则是 environ,是否手动传递环境变量表
3.2、替换现象
子进程程序替换后,并不会创建新进程,而是对原有程序中的 数据
和 代码
进行修改,可以通过替换以下程序观察
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
while(1)
{
cout << "程序替换成功";
cout << " PID:" << getpid() << " PPID:" << getppid() << endl;
sleep(1);
}
return 0;
}
可以看到在进行程序替换后,子进程和待替换程序为同一个进程
这就表明程序替换并不是进程替换
- 因为是同一个进程,所以对父进程没有任何影响,体现了进程间的独立性
- 在子进程执行程序替换前,子进程和父进程共享一份只读区域的数据,但因为发生了程序替换,触发
写时拷贝
机制,令子进程读取另一块区域的数据
写时拷贝
在只读数据区也能触发,因为不能影响到父进程
尾声:数字世界中的生命轮回
当进程通过
exec
族函数完成自我蜕变时,我们仿佛看到了生命本质的另一种表达——身份可以保留,而灵魂可以更新;容器可以不变,而内容可以焕然一新。这种机制超越了简单的代码执行,成为了一种关于存在与转变的深刻思考。
“看,那是一个进程,它携带着程序员的意志,在数字的林间悄然前行,时而蜕变,时而重生,却始终保持着对存在本质的探索…”
本篇关于进程程序替换的介绍就暂告段落啦,希望能对大家的学习产生帮助,欢迎各位佬前来支持斧正!!!