Linux--进程程序替换

本文详细介绍了程序替换的概念,以及在C/C++中如何使用exec系列函数进行进程替换,包括execl、execlp、execv、execvp、execle和execve的用法。通过实例展示了如何替换系统指令和自定义可执行程序,并探讨了替换成功与否的影响。进程替换不创建新进程,而是直接替换现有进程的代码,使得进程执行新程序,同时讨论了父子进程间的独立性和写时拷贝机制。
摘要由CSDN通过智能技术生成

1. 替换概念

  • 为什么需要程序替换

父进程一般需要子进程能够做其他的事,那么也有可能做的是全新的、不同的事,这就需要进程替换。

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序

当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换, 从新程序的启动例程开始执行。

调用exec并不创建新进程,所以调用exec前后该进程的id并未改变

C/C++程序要运行,就要先把程序加载到内存中,那么这个过程需要的加载器其实就是exec*程序替换函数。

  • 本质

程序替换的本质就是把程序的进程代码+数据,加载到特定进程的上下文中。

也就是说每个可执行程序需要运行,都需要加载到内存中,这是因为冯诺依曼体系结构中规定外设不能直接和CPU打交道,而是要通过存储器;那么这个“加载”过程的实现就是通过exec*函数,所以它也被称为加载器

程序替换的前提是建立了一个完整的进程,这包括:PCB,虚拟地址空间,页表物理内存等。

2. 替换解释

2.1 替换函数

当子进程替换另一个代码时,父进程不受影响,因为每个进程是独立的;但是它们(父子进程)共享着一份代码,为什么可以不受影响?

这是因为程序替换的过程,对这个“共享”的代码进行了写入,会引发写时拷贝,此时它们执行的已经不是同一份代码了,也就互不影响。

头文件:

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
  • 替换函数execl
int execl(const char *path, const char *arg, ...);

path:代表要执行的目标程序的全路径,即路径和文件名;
arg:要执行的目标程序,在命令行上怎么执行,这里的参数就一个个传递;
并且参数必须以NULL结尾。

如果执行ls -a -l -n -i命令:

printf("hello one\n");
execl("/usr/bin/ls","ls","-a","-l","-n","-i",NULL);
printf("hello two\n");
return 0;

执行后,将不会打印hello two,因为进行了程序替换,程序都被代替了。

那么返回值也会被替换掉:

这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。

所以,可以实现一个程序调用另一个程序的功能,这其实也是一种程序替换,只需要把参数按照对应格式修改。

上面的例子演示了运行该可执行文件的时候不再执行原代码的程序,而是执行替换后的ls -a -l -n -i命令,实现了程序替换。

2.1.1 替换对于父进程的影响

对于父子进程来说,子进程的替换不会影响到父进程,因为进程间具有独立性。

但是之前说的代码共享呢?是否矛盾?

这里其实并不矛盾,因为这里的“共享”是建立在代码不发生改变的前提下,而此时的进程替换很明已经更改了代码区的代码,那么此时自然就会发生写时拷贝。所以最终的结果应该是:子进程执行新的代码,父进程继续执行旧的代码。

观察下列代码:

int main(){
    
    pid_t id = fork();
    if(id == 0){
        //子进程
        printf("i am a child!!pid:%d\n",getpid());
        sleep(3);
        execl("/usr/bin/ls","ls","-a","-l",NULL);
        printf("aaaaaaaaaaaaaaaaa\n");
        exit(0);//子进程执行完以后返回正常退出码
    }
    
    while(1){
        printf("i am a father\n");
        sleep(1);
    }
    waitpid(id,NULL,0);
    printf("wait success!!");
}

该程序建立了子进程,并且通过execl函数把子进程本该执行的打印aaaaa的代码替换成了打印"ls -a -l",那么程序的运行结果就是:
在这里插入图片描述

此时可以理解进程程序替换的本质:程序员在命令行当中启动一个程序, 本质上是bash程序启动了一个子进程, 子进程程序替换成为程序员启动的程序

2.2 替换成功与否

  1. 程序替换不会创建新进程,本质是由于task_struct等数据未改变;
  2. 进程程序替换一经替换,决不返回,后续代码不会执行,也就是说该进程的代码全部被替换了。

但是也存在替换失败的情况,函数会返回,但后续程序不受影响。也就是说。exec*系列函数只要返回了,就说明替换失败。

2.3 其他替换函数

2.3.1 替换系统指令
  • 其他函数

exec*系列一共有六种函数:

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[]);
//execve是独特的
int execve(const char *path, char *const argv[], char *const envp[]);

乍一看复杂的很,好像都一样又好像都不一样……但其实它们有分类依据:

根据带有的单词不同,有不同的分类依据:

l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量

就比如最开始的execl,它的参数就是采用列表,也就是一个个罗列出来。
在这里插入图片描述

  • 解释
  1. 对于execv,其实和execl没有太大差别,只不过execv是先把参数打包在数组argv中,再一起传参。
int execv(const char *path, char *const argv[]);
//程序替换
//execv--对比execl传参是用数组形式
int main()
{
  if(0 == fork()){
    //child 
    printf("command begin.......\n");
    char* argv[] = {
      "ls",
      "-a",
      "-l",
      NULL
    };
    execv("/usr/bin/ls",argv);//数组形式传参
    printf("command end.......\n");
  }  
  waitpid(-1,NULL,0);
  return 0;
}

运行结果:
在这里插入图片描述

  1. 对于execlp,它的file参数意思是你要执行谁,传参后PATH环境变量会自动帮你找到
int execlp(const char *file, const char *arg, ...);

例如:

execlp("ls","ls","-a","-l","-n","-i",NULL);

第一个ls代表你要执行谁,后面的参数都代表你想执行的程序,所以execlp对比execl就是可以利用环境变量PATH自动搜索程序。

int main()
{
  if(0 == fork()){
    //child
    printf("command begin.......\n");
    execlp("ls","ls","-a","-l",NULL);
    printf("command end.......\n");
  }  
  waitpid(-1,NULL,0);
  printf("waitpid success!\n");
  return 0;
}

在这里插入图片描述

  1. 以此类推,对于execvp就说明是用数组传参,并且采用环境变量查找指令:
int execvp(const char *file, char *const argv[]);
execvp("ls",argv);//自定义数组传参
2.3.2 替换自定义可执行程序

也可以用来替换自定义的可执行程序,如execl:

先在myexe.c实现:

int main(){
  printf("hahaahaahhaha!!i am your exec!\n");
  return 0;
}

在myload.c中实现(makefile中实现的可执行程序就是对应的程序名,例如myexe.c—>myexe):

//在myload.c函数中写入此代码
execl("./myexe","myexe",NULL);
//其他代码省略

运行结果如下,可以看到此时运行的是可执行程序myload,但是执行的却是myexe.c的代码:
在这里插入图片描述

  1. 对于execle,与execl对比区别就是需要多传一个env,也就是可以自定义环境变量:

所以只要我在myload.c中自定义环境变量,通过execle是可以导入到myexe.c中并且由它输出的,就像前面替换系统指令ls一样:

在myexe.c中实现:

int main(){ 
  //打印环境变量
  extern char** environ;
  for(int i = 0;environ[i];i++)
  {
    printf("%s\n",environ[i]);
  }
  printf("hahaahaahhaha!!i am your exec!\n");
  return 0;
}

在myload.c中实现:

int main()
{
  if(0 == fork())
  {
    //child
    char* myenv[] = {
        "MY_ENV1 = AAAAAAA",
        "MY_ENV2 = AAAAAAA",
        "MY_ENV3 = AAAAAAA",
        "MY_ENV4 = AAAAAAA",
        "MY_ENV5 = AAAAAAA",
    };
    execle("./myexe","myexe",NULL,myenv);
  }
  //其他代码省略
}

此时运行myload.c得出:
在这里插入图片描述

如果运行myexe.c,得出结果是系统默认的很多环境变量:

在这里插入图片描述

  • 结论

原本直接运行myexe中打印的是系统的环境变量,而运行myload以后,由于调用了execle函数并且把自己想要查找的环境变量给到了myexe,这时候myexe找的不再是系统的环境变量,而是被替换后,myload自定义的环境变量env。

但是如果直接运行myexe.c,得出的就是系统默认的环境变量。就是上面输出的一堆环境变量。

下面是makefile的实现:
在这里插入图片描述

  1. 对于独特的execve:这其实就是execle的另一种版本,单独罗列出来是因为这个接口在Linux中是单独一个手册的,而其它六个是封装在了一起。

3. 总结

有这么多的接口,是为了满足不同的需求。(带l的参数都需要列表,带p的都可以使用环境变量PATH,带e的需要自己组装环境变量)

事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。

那么程序替换运行程序,和直接运行程序有什么区别?

区别就是直接运行还需要创建新的进程,而程序替换不需要创建新进程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

久菜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值