【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为空字符串或者字符串中包含=,就会报错;


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

相关推荐
上去我就QWER8 小时前
Qt快捷键“魔法师”:QKeySequence
开发语言·c++·qt
大聪明-PLUS10 小时前
如何使用 Docker 打包一个简单的应用程序:简易指南
linux·嵌入式·arm·smarc
Pluto_CSND10 小时前
Java中的静态代理与动态代理(Proxy.newProxyInstance)
java·开发语言
深耕AI11 小时前
【完整教程】宝塔面板FTP配置与FileZilla连接服务器
运维·服务器
serve the people11 小时前
Prompts for Chat Models in LangChain
java·linux·langchain
李昊哲小课12 小时前
Ubuntu 24.04 MariaDB 完整安装与配置文档
linux·ubuntu·mariadb
惊讶的猫12 小时前
LSTM论文解读
开发语言·python
獨枭12 小时前
C# 本地项目引用失效与恢复全攻略
开发语言·c#·visual studio
AI智域边界 - Alvin Cho13 小时前
Bloomberg、LSEG 与 MCP 缺口:为什么尚未发布完整的 MCP 服务器,以及多智能体系统如何解決这问题
运维·服务器
人间打气筒(Ada)13 小时前
zerotier内网穿透部署(rockylinux部署本地服务器)超详细~~~
linux·内网穿透·内网·公网·zerotier·穿透