【Linux】进程程序替换

文章目录

  • 替换原理
    • 站在进程的角度
    • 站在程序的角度
    • 初体验及理解原理
  • 替换函数
    • 函数解释
    • 命名理解
    • exec系列函数与main函数之间的关系
    • 在一个程序中调用我们自己写的程序

替换原理

创建子进程的目的是什么?
->想让子进程执行父进程代码的一部分

执行父进程对应的磁盘代码中的一部分

->想让子进程执行一个全新的程序

让子进程想办法加载磁盘上指定的程序,执行新程序的代码和数据->进程的程序替换

【Linux】进程程序替换_第1张图片

站在进程的角度

没有创建新的进程(PCB尤其是pid是没有变化的),只是程序的替换

站在程序的角度

  1. 这个程序被加载到内存中了->称exec函数为"加载器"
  2. 既然我自己的代码能加载到新的程序,操作系统也是
    1. 先创造进程数据结构(PCB),在再需要的时候加载代码和数据

初体验及理解原理

int execl(const char *path, const char *arg, ...);
将指定的程序加载到内存中,让制定进程进行执行

需要解决两个问题:1.找到程序 2.如何执行(cmd 选项一 选项二)
后面的三个点:可变参数列表:可以给函数不同数量的参数,例如:printf\scanf

示范程序:

#include
#include

int main()
{
	//.c->exe->load->process->运行->执行我们现在所写的程序
	printf("process is running...\n");

	//load->exe
	execl(
		"/usr/bin/ls"/*要执行哪一个程序*/, 
		"ls", 
		"-a",
		"-l",
		NULL/*你想怎么执行*/
		);//所有的exel程序都必须以NULL结尾,证明你把参数传完了

//但是此时就不能打印出下面的句子
//因为他也是代码,是在execl的,execl执行完毕的时候,代码已经被完全覆盖,开始执行新的程序的代码了,所以printf就无法执行了!
//但是如果调用失败了,就是没有替换成功,就会打印出来下面的句子
	printf("process running done...\n");
}

由上述的例子我们可以看出这个函数的特点:
只有失败才会返回-1(只需要做错误处理,即:只要返回值,就一定是调用错误了)
perror("execl");可以打印错误原因.
调用成功时则不返回,因为此时已经替换了,后续的代码已经没有意义了,没有必要返回值了.(成功了就和接下来的代码无关了)

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变
【Linux】进程程序替换_第2张图片

由虚拟地址空间+页表来保证进程的独立性,一旦有执行流想替换代码或者数据,就发生写时拷贝.->说明写时拷贝在代码区也可以发生的!

但是我们一般不直接在父进程进行程序替换,而是创建新的子进程进行程序替换.
于是我们优化上面的代码:

#include
#include
#include
#include
##include<sys/types.h>
#include
int main()
{
	printf("process is running...\n");
	pid_t id = fork();
	assert(id != -1);

	if (id == 0)
	{
		//这里的替换会影响父进程吗?
		//不会。进程具有独立性,在执行exec时会发生写时拷贝
		//类比:命令行怎么写,这里就怎么传参
		sleep(1);
		execl("/usr/bin/ls", "ls", "-a", "-l", "--color=auto", NULL);
		exit(-1);//must failed
	}

	int status = 0;
	pid_t ret = waitpid(id,&status,0);
	if (ret > 0)
	{
		printf("wait success: \nexit code: %d,sig: %d\n",(status>>8)&0xFF,status&0x7F);
	}

}

替换函数

其实有六种以exec开头的函数,统称exec函数:

#include `  
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[]);

其中,execve是系统调用,其余的都是封装,是为了让我们有更多的选择性

函数解释

这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
如果调用出错则返回-1
所以exec函数只有出错的返回值而没有成功的返回值
不用对该函数进行返回值判断,只要继续向后运行一定是失败的

命名理解

这些函数原型看起来很容易混,但只要掌握了规律就很好记。
l(list) : 表示参数采用列表
v(vector) : 参数用数组

可以将所有的执行参数放入数组中,统一传递,而不用进行使用可变参数方案.

char* const argv_[] = {
	"-ls",
	"-a",
	"-l",
	"--color=auto",
	NULL
};
execv("usr/bin/ls",argv_);

p(path) : 有p自动搜索环境变量PATH

带p字符的函数,不用告诉我程序的路径,你只要告诉我程序的名字,我会自动在环境变量PATH中进行可执行程序的查找.
execlp("ls", "ls", "-a", "-l", "--color=auto", NULL);
这里前面的ls是告诉系统我要执行谁,第二个是告诉系统我想要怎么执行

e(env) : 表示自己维护环境变量

#include
#include
#include
#include
##include<sys/types.h>
#include
int main()
{
	printf("process is running...\n");
	pid_t id = fork();
	assert(id != -1);

	if (id == 0)
	{
		sleep(1);
		char* const envp_[] = {
			(char*)"MYENV=1122233334444",
			NULL;
		}
		extern char** environ;//声明系统默认的环境变量列表
		execle("./mybin","mybin",NULL,environ);//这里是只传了系统默认的环境变量
		
		putenv((char*)"MYENV=444333222111");//将指定环境变量导入系统中environ指向对应的环境变量表中
		
		execle("./mybin","mybin",NULL,envp_);//这里是只传了你自定义的环境变量
		
		execle("./mybin","mybin",NULL,environ);//实际上,默认环境变量你不传,子进程也能获取. 这里是传了默认的环境变量加上你自己添加的环境变量

		exit(1);
	}

	int status = 0;
	pid_t ret = waitpid(id,&status,0);
	if (ret > 0)
	{
		printf("wait success: \nexit code: %d,sig: %d\n",(status>>8)&0xFF,status&0x7F);
	}

}

exec系列函数与main函数之间的关系

exec*系列函数的意义是:
将我们的程序加载到内存中

int main(int argc,char* argv[],char* env[])

所以应该是先加载到内存中,之后才是调用main函数,main函数中的argc argc[]分别是exec函数中的前面的两个参数,env[]是从exec函数的环境变量参数中加载出来的

【Linux】进程程序替换_第3张图片

exec调用举例如下:

#include 
int main()
{
	char* const argv[] = { "ps", "-ef", NULL };
	char* const envp[] = { "PATH=/bin:/usr/bin", "TERM=console", NULL };
	execl("/bin/ps", "ps", "-ef", NULL);
	// 带p的,可以使用环境变量PATH,无需写全路径
	execlp("ps", "ps", "-ef", NULL);
	// 带e的,需要自己组装环境变量
	execle("ps", "ps", "-ef", NULL, envp);
	execv("/bin/ps", argv);
	// 带p的,可以使用环境变量PATH,无需写全路径
	execvp("ps", argv);
	// 带e的,需要自己组装环境变量
	//没有带e的话会默认传入系统默认的环境变量
	execve("/bin/ps", argv, envp);
	exit(0);
}

在一个程序中调用我们自己写的程序

首先,我们需要在makefile文件中同时创建两个文件并且同时删除两个文件,那该怎么书写呢?

.PHONY:all
all: mybin myexec

mybin:mybin.c
	gcc -o $@ $^ -std=c99
myexec:myexec.c
	gcc -o $@ $^ -std=c99	

.PHONY:clean
clean:
	rm -f myexec mybin

myexec.c

#include
#include
#include
#include
##include<sys/types.h>
#include
int main()
{
	printf("process is running...\n");
	pid_t id = fork();
	assert(id != -1);

	if (id == 0)
	{
		execl("./mybin", "mybin", NULL);
		exit(-1);
	}

	int status = 0;
	pid_t ret = waitpid(id,&status,0);
	if (ret > 0)
	{
		printf("wait success: \nexit code: %d,sig: %d\n",(status>>8)&0xFF,status&0x7F);
	}

}

可以使用程序替换调用任何后端语言所对应的可执行!!!


事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。
下图exec函数族 一个完整的例子:
【Linux】进程程序替换_第4张图片

你可能感兴趣的:(linux,运维,服务器)