【Linux】进程控制(三)——进程程序替换

进程程序替换(exec系列)是Linux中"让一个进程跑新程序"的核心操作------它不用新建进程,而是直接把当前进程的代码/数据换成新程序,同时保留PID等核心属性。让我们来逐步学习~

目录

一、程序替换现象演示

二、子进程替换

三、替换原理介绍

调用execl函数后:

替换后的结果:

替换后虚拟地址空间与物理空间的核心变化:

四、对比各个替换函数

1.execl

2.execlp

3.execv

4.execvp

execvpe(了解)

五、替换自己的程序


一、程序替换现象演示

我们先写一个简单的程序来运行

正常运行没有问题,现在我们直接写一个程序替换函数execl到代码中,这个库函数的用法和参数大家先不用纠结,后面都会统一讲解。

我们发现,自己写的程序在打印PID后就帮我们执行ls指令了,而也惊奇发现,后面的六行打印全部都没有执行,这是就是因为进行程序替换了,将我们原进程的代码全部都覆盖替换成了ls的代码,因此后面仅仅执行了ls指令,这就是程序替换的现象

二、子进程替换

前面讲父子进程的时候,提到了关于创建子进程的两个常见使用,其中一个就是要创建子进程来执行不同的程序,这样父子进程的代码不相互影响,各自完成自己的任务,子进程调用execl也就不会覆盖掉子进程的代码了,话不多说,直接上实操代码给大家演示

这样就完成了父子进程的分工合作。

我把刚刚用到的代码给大家贴在下面了

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>


int main()
{
  printf("我变成了一个进程,PID:%d\n", getpid());
 
  pid_t id = fork();
  if(id == 0)
  {
    execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
    exit(1);
  }

  //执行系统指令ls的代码
  //execl("/usr/bin/ls", "ls", "-a", "-l", NULL); //程序替换函数

  wait(NULL);
  printf("我的代码正在运行中...\n");
  printf("我的代码正在运行中...\n");
  printf("我的代码正在运行中...\n");
  printf("我的代码正在运行中...\n");
  printf("我的代码正在运行中...\n");
  printf("我的代码正在运行中...\n");
  return 0;
}

那么代码的覆盖是如何做到的呢?为什么父子进程共享数据和代码但是子进程被覆盖了却不影响父进程呢?下面我们来简单介绍一下进程替换原理。

三、替换原理介绍

调用execl函数后:

"代码段+数据段"会被完全替换,从磁盘加载新的ELF程序,把虚拟内存中原有的 "代码段、数据段" 直接替换成新程序的内容(堆、栈会被清空并重新初始化)。

但是"虚拟地址空间+页表框架"会被保留,也就是说虚拟内存的 "结构框架"(比如虚拟地址的分段方式)不变,页表会重新建立 "新代码段 / 数据段" 到物理内存的映射(旧的映射被废弃)。

保留"进程的壳",PCB 里的PID、进程 ID、打开的文件描述符等信息完全不变 ------ 相当于 "同一个进程,换了里面跑的程序"。

替换后的结果:

1.虚拟内存里跑的是新程序的代码和数据

2.物理内存中对应区域被新程序的内容覆盖;

3.外部看,这个进程的 PID 没变化,但执行的程序已经完全不同了。

替换后虚拟地址空间与物理空间的核心变化:

1.虚拟地址的 "具体分段范围" 会随新程序变化;

2.页表的映射关系是完全替换(旧的物理地址映射被丢弃,换成新程序的物理地址);

3.进程的 "虚拟地址空间所有权"(即这个虚拟地址空间属于当前 PID 的进程)是不变的。

四、对比各个替换函数

关于程序替换函数,其实有很多个,常常称为exec系列函数,在终端可以用指令man 3 exec来查看手册相关内容,如下

这里有六个函数,这里主要会讲到四个函数,为什么这些要叫exec系列函数呢?因为每个函数的功能用法与函数名exec后面的字母有关系,下面我来一一讲解

1.execl

这个函数exec后面的字母"l"是"list"的意思,表示参数以列表的形式传递,我们来看第一个参数path,就是要提供要替换的程序的路径(绝对路径 or 相对路径都可以),而后面则是可变参数列表,根据实际的运行指令来填,比如下面的ls指令

这个指令可以拆分为"ls"、"-a"、"-l",因此execl后面的参数列表要填写的就是这些,并且,写完之后要在最后加一个NULL表示结束,否则就会替换失败!

接下来用这段代码来给大家一一介绍这些函数

这段代码相比于之前不同的是可以检验程序替换是否成功,因为当子进程的程序替换成功时,exit代码是不会执行的,此时退出码为0,而当exit被执行的时候,证明程序替换失败,下面打印的退出码就变为1。执行如下:

2.execlp

可以看见这个函数名exec后面为"lp",其中"l"表示"list",和上面的一样,"p"表示path,这里重点讲这个path,之前讲环境变量的时候重点讲过路径PATH,它的存在就是能让系统指令比如ls、top这类指令的执行不需要添加路径的原因,因为它们的路径已经存在与环境变量PATH当中了。

在exelp函数中就是应用了同样的原理,凡是在环境变量PATH中存在路径的程序,替换时都不需要写路径,只需要写程序名就行,这就是exelp函数的第一个参数的填写方式,至于后面的可变参数列表,用法和execl函数一模一样,唯一不同的就是第一个参数一个用写路径名而另一个直接写程序名即可

这里的两个ls第一个是声明程序名,第二个是执行方式(与后面的-a、-l配套出现,属于参数列表的一部分),它们的意义不同,千万别混淆在一起或者简单认为它们相同。

运行结果如下:

与上面的运行结果一致

3.execv

这里的"v"是"vector"的意思,表示一个数组,之前的函数无论是execl还是execlp,它们的功能上都有"l",但是这个函数没有了,改成了vector,即传参的方式从可变参数列表改变成了传数组,由于这个函数不含"p",因此我们的第一个参数依旧要写路径

在argv数组中也千万别忘记了加"NULL",运行结果如下:

4.execvp

这里vp都已经讲解过了,直接演示给大家看

上面演示四个函数写的代码我给大家贴在下面啦~

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>


int main()
{
  printf("我变成了一个进程,PID:%d\n", getpid());
 
  pid_t id = fork();
  if(id == 0)
  {
    //execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
    //execlp("ls", "ls", "-a", "-l", NULL);
    
    char *argv[] = {
     (char*) "ls",
     (char*) "-a",
     (char*) "-l",
     NULL
    };

    //execv("/usr/bin/ls", argv);
    execvp("ls", argv);
    exit(1);
  }

  int status = 0;
  pid_t rid = waitpid(id, &status, 0);
  if(rid > 0)
  {
    printf("wait success, exit_code:%d\n", WEXITSTATUS(status));
  }
  return 0;
}

execvpe(了解)

这是我们最后要讲解的函数,也是理解起来最复杂的,其中"vp"都是刚刚讲解过的,相信大家已经理解,这里主要讲"e",其实e就是envp也就是环境变量表

我们可以发现前面含p的函数都是能根据环境变量中的PATH找到要替换的程序的,因此可以推断,这些函数默认共同继承一张环境变量表environ的

因此带有e的exec系列函数,都是可以根据需求自主定义环境变量的,自己传入的环境变量可以覆盖父进程的全局环境变量表,这里就先不做演示,工作中遇到的场景也很少,用到大家再快速学习就行,目前不需要花费太多精力啃这个,简单了解就行

五、替换自己的程序

那么进程替换函数除了系统指令以外,还可以替换我们自己写的程序

为了方便分开演示看区别,我们自己再写一个cpp文件并替换(除了系统指令,自己写的程序也可以替换,这里用cpp文件替换c文件作为演示),为了更有区分度,我们再创建一个文件夹cmd把cpp文件放进去

确认运行无误

那我们来看看替换自己的程序是否可行

事实证明完全可行

无论是cpp程序还是py甚至是shell脚本,本质上编译后都是进程,都可以被替换,学习了进程程序替换,就可以让我们的程序更高效完成各种复杂任务

至此今天的讲解到此结束,感谢观看~