【Linux系统编程】程序替换:execve(execl、execlp、execle、execv、execvp、execvpe)

文章目录

前言

在现代操作系统中,进程是执行程序的基本单位,每个进程拥有独立的地址空间、代码和数据。当一个程序正在运行时,有时我们希望它能够"脱胎换骨",从自身变成另一个程序------这就是程序替换(Process Replacement)的概念。在 Unix/Linux 系统中,这种能力被 exec 系列函数实现,它允许一个进程在原地加载并执行新的可执行文件,而无需创建新进程,从而节省资源并保持进程的身份标识(PID)不变。


exec系列函数关系图

lua 复制代码
                  +--> execl()  ------  参数用列表
                  |
 execve()  <------+--> execv()  ------  参数用数组
                  |
                  +--> execlp() ------ 查 PATH
                  |
                  +--> execvp() ------ 查 PATH,参数用数组
                  |
                  +--> execle() ------ 自定义环境
                  |
                  +--> execvpe() ------ 查 PATH + 自定义环境
  • 返回值:成功则不返回(当前程序被覆盖);失败返回-1
  • exec*系列函数都是对系统调用接口execve()的封装 ,封装函数的形参中未实现的则使用系统默认的值;
    • 例如: execve 接口的参数有pathnameargvenvpexecl 的形参只有两个,pathargv;那么一个进程的程序数据使用execl 替换后,在替换的程序中访问argv,那就是我显示传入的形参argv的值,
      但是如果访问envp,那么就是系统默认传入的envp值(environ指向的环境变量);
  • 函数的命名规律:exec[l|v][p][e]
    • l:list(可变参数列表)
    • v:vector(数组)
    • p:path(PATH查找)
    • e:environ(自定义环境)

程序替换的原理

  1. ++可执行文件=代码+数据++
  2. 在一个进程在执行exec*替换函数时,会将当前进程的代码段、数据段、堆、栈都被清空,并初始化新的堆、栈(argv,envp)、数据和代码内容;
  3. 程序替换会将当前进程的代码和数据替换为其他文件的代码数据,所以我们一般会创建一个子进程,将其他文件的代码和数据替换到子进程中,进而不影响父进程的正常运行;

函数的介绍及使用

execl( )

函数原型: int execl(const char *path, const char *arg, ... /* (char *) NULL */);
参数说明:

  • path:文件的绝对路径
  • arg:可变参数列表(最后以NULL结尾,表示参数传入完毕)

代码示例

c 复制代码
#include <unistd.h>
#include <stdio.h>

int main() {
	printf("当前程序开始运行......\n");
	if(fork()==0)
	{
		execl("/usr/bin/ls","ls","-l","-a",NULL);
		exit(1);//如果返回并继续向下执行,说明替换失败,直接退出;
	}
	waitpid(-1,NULL,0);//进程等待
	printf("当前程序停止运行......\n");
}

执行结果:


execlp()

再execl基础上不仅可以通过文件名方式(环境变量PATH)查找还可以通过绝对路径的方式查找

原型int execlp(const char *file, const char *arg, .../* (char *) NULL */);
参数说明:

  • file:文件名或文件绝对路径;
  • arg:可变参数列表(最后以NULL结尾,表示参数传入完毕)

代码示例:(与execl基本相同)

c 复制代码
execlp("ls","ls","-l","-a",NULL);

execle()

指定文件绝对路径,自定义环境变量

原型: int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);

参数说明:

  • path:文件绝对路径
  • arg:可变参数列表
  • envp:自定义的环境变量(注意:必须写在NULL的后面

代码示例:

c 复制代码
//test.c
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>

int main() {
	printf("当前程序开始运行......\n");

	if(fork()==0)
	{
		printf("hello test,child pid:%d\n",getpid());
		char* env[]={	//自定义环境变量
			"PATH=/learn/linux/leason15",
			NULL
		};
		execle("proc","./proc",NULL,env);
	}
	waitpid(-1,NULL,0);
	printf("当前程序停止运行......\n");
	return 0;
}
c 复制代码
//proc.c
#include <stdio.h>
#include <unistd.h>

int main(int argc,char* argv[],char* env[])
{
	printf("hello proc,child pid:%d\n",getpid());
	for(int i=0;env[i];i++)
		printf("env:%s\n",env[i]);
    
	return 0;
}

运行结果:

  • 这里可以看到test.c的子进程将proc文件进行替换;并在替换前和替换后输出PID,可以证明程序替换后确实没有创建新的进程;

execv()

指定绝对路径,自定义命令行参数

原型: int execv(const char *path, char *const argv[]);
参数说明:

  • path:文件绝对路径
  • argv: 自定义命令行参数
    • (指针数组,数组与系统命令行参数格式相同,最后一个参数为NULL;可看作可变参数列表)

代码示例:

c 复制代码
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>

int main() {

	printf("当前程序开始运行......\n");

	if(fork()==0)
	{
		char* argv[]={"ls","-l",NULL};
		execv("/usr/bin/ls",argv);	
	}
	waitpid(-1,NULL,0);
	printf("当前程序停止运行......\n");
	return 0;
}

运行结果 :


execvp()

与execv基本相同,唯一不同的是 可以通过文件名(PATH环境变量)的方式查找文件;

原型: int execvp(const char *file, char *const argv[]);

参数说明:

  • file:要执行文件名/绝对路径
  • argv: 自定义命令行参数

代码示例:

c 复制代码
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>

int main() {

	printf("当前程序开始运行......\n");

	if(fork()==0)
	{
		char* argv[]={"ls","-l",NULL};
		execvp("ls",argv);	//通过PATH查询
	}
	waitpid(-1,NULL,0);
	printf("当前程序停止运行......\n");
	return 0;
}

execvpe()

相当于execvpexecle的结合版;

原型: int execvpe(const char *file, char *const argv[],char *const envp[]);
参数说明:

  • file:要执行文件名/绝对路径
  • argv: 自定义命令行参数
  • envp:自定义环境变量

代码示例:

c 复制代码
//test.c
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>

int main() {

	printf("当前程序开始运行......\n");

	if(fork()==0)
	{
		char* argv[]={"-a","-b","-c",NULL};
		char* envp[]={
			"PATH=/1/2/3",
			"NAME=YANG",
			NULL
		};
		execvpe("./proc",argv,envp);	
	}
	waitpid(-1,NULL,0);
	printf("当前程序停止运行......\n");
}
c 复制代码
//proc.c
#include <stdio.h>
#include <unistd.h>

int main(int argc,char* argv[],char* env[])
{
	printf("hello proc,child pid:%d\n",getpid());
	for(int i=0;env[i];i++)
		printf("env:%s\n",env[i]);

    printf("argv:");
	for(int i=0;argv[i];i++)
		printf("%s ",argv[i]);
	printf("\n");
	return 0;
}

运行结果:


execve(系统调用)

前面的六个程序替换函数底层都是调用的execve接口;这里execve函数的参数和前面替换函数的用法相同,这里就简单说明每个参数的含义即可,了解完前面的函数自然就会使用这个函数接口

函数原型: int execve(const char *pathname, char *const argv[], char *const envp[]);

参数说明:

  • pathname:文件的绝对路径
  • argv[]:命令行参数数组(最后以 NULL 结束)
  • envp[]:环境变量数组(同样以 NULL 结束)

拓展

putenv

作用: 把指定的字符串指针直接放入进程的环境变量表(char** environ)中
返回值:

  • 成功返回 0
  • 失败返回 非 0(通常是内存不足)

参数:
string:字符串 (是一个形如 "NAME=VALUE" 的字符串)

特点: 直接将你传入的指针地址放到当前进程的环境变量表中;(也就意味着后续你如果修改这段字符串的内容,环境变量中的这个值也会随之改变)


在使用程序替换函数中envp参数时,都是我们自定义的;但是这里会有一个小问题:

  • 在我们还没对一个进程中的代码数据进行替换时,它的环境变量等参数都是从父进程继承下来的,也就是系统原本的环境变量,当我们传入自定义的envp参数之后,系统的环境变量表中的值也会被全部替换,在实际情况中,系统的环境变量中的参数我们也需要用到,自定义的环境变量参数也要用到;所以这里就需要用putenv函数来将我们自定义的环境变量参数插入到系统的环境变量表中,这样就不会覆盖掉原本的环境变量了;
  • char** environ参数: 全局变量,指向当前进程的环境变量表;
  • 自定义的环境变量传给envp时,我们就可以通过putenv将自定义环境变量中的值插入到environ的后面,就不会改变当前进程原有的环境变量了;

代码示例:

c 复制代码
//test.c
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>

int main() {

	printf("当前程序开始运行......\n");

	if(fork()==0)
	{
		char* argv[]={"-a","-b","-c",NULL};
		char* envp[]={
			"TEST_PATH=/1/2/3",//避免与进程环境变量中的PATH重名,导致PATH的value值
			"NAME=YANG",
			NULL
		};
		extern char** environ; 
		for(int i=0;envp[i];i++)
			putenv(envp[i]);	//将自定义环境变量中的值插入到当前进程的环境变量中
		execvpe("./proc",argv,environ);	//最后envp参数直接传入当前进程的环境变量environ
	}
	waitpid(-1,NULL,0);
	printf("当前程序停止运行......\n");
	return 0;
}
c 复制代码
//proc.c
#include <stdio.h>
#include <unistd.h>

int main(int argc,char* argv[],char* env[])
{
	for(int i=0;env[i];i++)
		printf("env:%s\n",env[i]);

    printf("argv:");
	for(int i=0;argv[i];i++)
		printf("%s ",argv[i]);
	printf("\n");
	return 0;
}

运行结果:


setenv / unsetenv

setenvputenv的作用基本相同;

在底层setenv会在堆上重新分配environ的空间并拷贝字符串,将参数设置(插入)到拷贝后的空间中;之后我们如果修改原来的字符串并不会影响到环境变量中的参数,相对putenv更加的安全,只是不断的拷贝性能会有所下降;(在实际编码时,更推荐使用setenv

setenv

原型: int setenv(const char *name, const char *value, int overwrite);
参数说明:

  • name:环境变量名,不能包含 =
  • value:环境变量值
  • overwrite
    • 若为 0,且环境变量已存在,则不修改;
    • 若为 非 0,则强制更新;

返回值

  • 成功返回 0
  • 失败返回 -1

unsetenv

作用: 删除指定名称的环境变量;
原型: int unsetenv(const char* name)
返回值:

  • 成功返回 0
  • 失败返回 -1

注意: 如果name在环境变量中不存在,unsetenv什么也不做;如果name为空字符串或者字符串中包含=,就会报错;


本篇文章就到此结束了,如果本文对你有帮助,麻烦你 👍点赞 ⭐收藏 ❤️关注 吧~

相关推荐
草莓熊Lotso3 小时前
C++11 核心特性实战:列表初始化 + 右值引用与移动语义(附完整代码)
java·服务器·开发语言·汇编·c++·人工智能·经验分享
Bigan(安)4 小时前
【奶茶Beta专项】【LVGL9.4源码分析】09-core-global全局核心管理
linux·c语言·mcu·arm·unix
初夏睡觉4 小时前
从0开始c++,但是重置版,第1篇(c++基本框架)
开发语言·c++
老王熬夜敲代码4 小时前
进程PCB
linux·笔记
草莓熊Lotso4 小时前
GCC/G++ 编译器完全指南:从编译流程到进阶用法(附实操案例)
linux·运维·服务器·网络·c++·人工智能·自动化
workflower9 小时前
时序数据获取事件
开发语言·人工智能·python·深度学习·机器学习·结对编程
鸠摩智首席音效师10 小时前
linux 系统中 Shutting Down, Restarting, Halting 有什么区别 ?
linux·运维·服务器
CIb0la10 小时前
Linux 将继续不支持 HDMI 2.1 实现
linux·运维·服务器
CoderYanger10 小时前
C.滑动窗口-求子数组个数-越长越合法——2799. 统计完全子数组的数目
java·c语言·开发语言·数据结构·算法·leetcode·职场和发展
C++业余爱好者10 小时前
Java 提供了8种基本数据类型及封装类型介绍
java·开发语言·python