深入Linux系列:进程的替换

发布于:2025-03-01 ⋅ 阅读:(14) ⋅ 点赞:(0)

🔥本文专栏Linux
🌸博主主页努力努力再努力wz

在这里插入图片描述

那么我们在之前我们学习了进程的终止与等待的内容,那么今天我们将要学习我们进程的控制相关的另外一个非常重要的内容,那么便是进程的替换,那么在学习完进程的替换之后,那么我们有了之前的创建子进程的系统调用接口fork以及进程的等待waitpid再加上这篇文章所学的进程的替换,那么我们就可以自己来实现一个shell进程也就是命令行解释器,来综合我们前面所学的所有的知识以及系统调用接口,那么接下来我们废话不多说,就让我们进入进程的替换的内容的学习!


本文前置知识:
1.进程的终止与等待
2.进程地址空间
3.进程的概念

进程的替换

1.引入

那么看到进程的替换,想必首先你脑海里面一定会冒出这样的疑惑:
那么就是进程的替换是什么?

那么在介绍我们的进程的替换的原理之前,我们先来看看进程的替换是什么,长什么样子,那么我们可以简单的写一个c语言的代码,那么然后执行这个程序,那么我们的代码的逻辑也很简单:那么就是我们在开头先打印我们该进程的一个PID和PPID,然后紧接着调用一个函数,然后再打印一遍该进程的PID与PPID
在这里插入图片描述
在这里插入图片描述

那么我们执行完我们这个代码之后,我们发现,我们代码的第一个打印执行了,显示在我们的终端上,但是之后的下一个打印就没有执行,反而我们终端给我们显示了一个我们根本不属于我们这个进程的内容,执行的是ls指令的内容。

那么这个现象就是我们进程的替换,那么我们知道出现这个现象的原因一定就我们刚才调用的这个库函数有关,那么就是这个库函数做到了我们的进程的替换,所以接下来我们要掌握进程的替换,那么我们就得一定要掌握进程替换的库函数和系统调用接口

2.进程替换的库函数与系统调用接口

  1. execl

    :函数原型

     int execl(const char *pathname, const char *arg, ... /*, (char *) NULL */); `execlp`
    

    2.execlp

    :函数原型

    •      int execlp(const char *file, const char *arg, ... /*, (char *) NULL */);
           
      

    3.execle

    :函数原型

    •      int execle(const char *pathname, const char *arg, ... /*, (char *) NULL, char *const envp[] */);
           
      

      4.exeecv

      :函数原型

      •           int execv(const char *pathname, char *const argv[]);
             
        

    5.execvp

    :函数原型

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

    6.execve

    :函数原型

         int execve(const char *pathname, char *const argv[], char *const envp[]);
         
    

    3.进程替换的原理

那么我们发现我们上面关于进程替换的库函数以及系统调用接口加起来总共有6个,那么再介绍我们上面那些库函数怎么使用之前,那么我们首先得先了解认识到上面那些库函数它是怎么做到进程的替换的

那么要知道他们如何实现进程替换的原理,那么就得先追溯到我们进程的创建,我们直到进程是由两部分所构成,分别是进程所对应的task_struct结构体以及进程所对应的用户态数据,所以创建一个进程的本质就是在操作系统中创建一份该进程对应的task_struct结构体以及将该进程在外部设备比如磁盘中的用户态数据给加载到内存当中

那么当我们CPU调度执行该进程的时候,我们知道CPU会从该进程的task_struct结构体获取到该进程的地址空间以及页表的内容,然后通过页表将虚拟地址转换为物理地址到内存的相应位置中获取该进程的数据段以及代码段然后加载到CPU中的相应的寄存器中,那么在这个执行过程中,那么一旦我们CPU执行到我们的进程替换的语句,也就是调用我们的execl库函数时,那么通过上文中的该库函数的函数声明也知道,那么我们这个库函数的第一个参数就是我们替换的程序所在的路径,那么有了要替换的程序在磁盘中存放的位置,那么意味着此时就会将该路径下对应的新的程序的用户态数据加载到我们的内存中,但是这里我们只是将替换的程序在外部设备中的用户态数据加载到内存中,但是注意不会为其创建一份该程序对应的task_struct结构体,而我们刚才说了一个进程是由两部分所组成,分别是task_struct结构体以及进程的用户态数据,而此时我们缺少了task_struct结构体,所以我们进程的替换不会创建新的进程

那么这里注意了,那么此时我们已经将要替换的程序的用户态加载到内存中了,那么此时我们就修改原本被替换的进程的task_struct体中的相应字段属性,其中就包括了我们的页表,那么我们会将页表中原本的映射关系全部修改,因为原本的映射关系是原来的进程的数据段与代码段的映射,但是此时进程被替换之后,那么原来的页表都得通通更新,其中task_struct结构体的某些字段属性也得更新,其中最重要的就是程序计数器PC,它原本指向的是当前我们原来代码中main函数调用execl函数的位置,那么此时会将其修改指向为新的替换后的程序的main函数的入口,那么一旦替换完之后,我们就会从替换之后的程序的开头来执行它的代码的上下文,这就解释了我们开篇的一个进程替换的一个现象了,那么开篇我们在打印完我们的进程编号与父进程变编号之后,那么我们之后的下文便被替换,此时被替换成了ls程序的上下文,那么我们会从ls程序的开头开始执行它所对应的代码,而不会执行原本代码之后的打印语句了。


而对于进程的替换,我可以打一个形象的比方来帮组你理解,进程的替换就像我们的武侠小说当中,一个人他正在闭关修炼的时候,他突然被别人给夺舍了,虽然身体是自己的,但是灵魂却是另一个人的了,那么这个例子形象的说明了我们的进程替换,我们仔细看看这个例子的细节,其中在这个例子中有一句话是说:“身体是自己的”,那么这句话就意味着我们进程的替换不会创建新的进程,不会有新的task_struct结构体,那么我们只是在原有的task_struct结构体的部分字段进行修改,所以我们进程的替换的过程中,task_struct结构体还是原来的task_struct结构体,但是部分属性会被修改

这里还要注意的是进程的替换没有修改被替换的进程的编号,那么意味着我们该进程如果存在子进程,那么如果该父进程被替换之后,他们的父子关系还是在的,就好比我修炼的时候被夺舍了,虽然我的灵魂是另一个人的,但是身体是还是原本的我的身体,那么既然身体是原本的我的身体,那么我和我儿子的血缘关系肯定是是存在的,不会说被夺舍或者进程被替换了,我子进程就成孤儿进程了。

所以我们在进行进程的替换的时候,我们如果说我们进程的替换之前有调用我们的fork函数这样的场景的话,那么就得注意了,如果我们在进程替换前调用fork函数,并且父子进程是同样的执行流的话,那么一旦执行到execl语句,那么此时父子进的下文都会被替换掉,那么此时对于父进程来说,那么即使他下文有调用wait函数来想要获取子进程的退出情况,那么由于出现替换,那么也不会被执行了

所以当结合我们的fork函数的时候,我们一定得注意,我们一般会根据fork函数的返回值,让父子进程有着不同的执行流,然后让子进程自己的执行流的代码片段中调用execl函数被替换,然后这样父进程就不会被替换,那么此时父进程调用wait或者waitpid函数就不会受到影响,那么在这个场景下,我们结合之前进程替换的原理,我们就知道,此时子进程发生了进程的替换,那么由于子进程有自己的task_struct以及页表,那么此时子进程的页表当中的映射就会被修改更新,并且task_struct结构体的相应的字段也会被更新,而父进程的task_strcut结构体则不会受到影响。

4.进程替换的库函数以及系统调用接口的使用

那么了解了进程替换的原理之后,那么我们认识到这些execl库函数是如何做到的,那么接下来我们就只需要知道如何调用这些库函数了,那么我们的进程替换是exec族的函数,那么其中我们exec后面紧跟的这个字符"l",代表的是list也就是可变参数,其中就包括execl以及execlp和execle,那么有了这个l意味着我们可以传递任意数量的参数,只需要最后加一个NULL来标记结尾

而我们这些exec族函数的第一个参数则是我们要替换的程序它所在的位置,那么第二个参数则是我们替换的程序的程序名,比如我们要替换的程序是ls,那么他的程序名就是"ls",那么这些参数都是字符串

而之后的几个参数就是这个程序的命令行参数,那么我们之前也知道在我们的Linux下,我们可以通过命令行来传参,而我们的main函数就可以接收命令行参数,那么main函数就有两种参数列表,分别是:

1:  int mian(int argc,char* argv[])
2: int main(int argc,char* argv[],char* env[])

其中第一个参数就是命令行参数的个数,那么后面的字符数组就是将解析后的命令行参数的字符串放进该数组的不同位置,最后以NULL结尾,而argc[0]则是程序名

而其中我们的execl的"p"则表示我们则表示我们如果第一个参数如果不给指定的路径的话,那么我们默认会从该进程的环境变量中的PATH中去搜寻匹配

而"e"则表示我们可以自定义替换后该进程的环境变量,那么我们会用一个字符函数组来传递,那么注意我们该字符数组的自定义的环境变量会直接覆盖原本进程的环境变量,所以说如果我们进程的所有属性的环境变量没有定义完的话,那么我们会导致该进程的部分环境变量属性未定义

所以说我们可以用exec族的函数来替换为我们Linux的指令,因为我们知道指令的本质也是一个编写好的可执行文件,当然我们也可以替换为自己定义的文件,那么只需要告诉execl函数我们文件的位置以及文件名和我们要向该文件传递的参数,那么参数就决定我们该文件进行什么样的行为,比如我们该向ls程序中传入一个“-l”参数,意味着我们接下来要打印该路径下所有文件的详细属性

注意的是我们exec族的函数第一个参数的路径可以是绝对路径也可以是相对路径,一般建议写绝对路径

而这些execl以及execlp函数,他们都是库函数,而我们的execve才是我们的系统调用接口,而这点我们可以通过我们的man手册来验证,我们发现除了execve在第三号手册也就是系统接口手册中,其余都在2号手册也就是库函数手册当中,那么它要传递的命令行参数以及环境变量都得以字符数组的形式传递给该系统调用接口,而我们的exec库函数内部封装了execve接口,那么我们传递的各个命令行参数以及环境变量,内部它就已经帮组我们实现了将其转换为数组的过程

而我们如果我们给execl传递的程序的路径不存在或者该程序存在,那么execl函数会失效,会继续执行execl之后的代码,所以一定要注意一个参数也就是关于替换的程序的路径的正确性,因为一旦错误了,我们该进程不会出现什么异常,只是直接执行后面的代码去了,所以我们可以在exec函数后面定义一定的检查逻辑比如打印操作,如果失败,那么它就会执行之后的打印操作,我们就能在终端中看到,然后知道我们execl函数是调用失败的

5.应用

那么我们就可以自己用c语言代码来应用一下我们进程的替换,那么我们首先自己在其他路径定义一个要替换的该进程的可执行文件,然后接下来我们用while循环创建5个子进程通过调用fork函数,然后利用fork函数的返回值,让父子进程有着自己的执行流,然后我们让子进程自己的执行流代码段中调用execl函数来替换为我们在另一个路径下定义的一个test.exe的可执行文件,然后父进程则等待子进程的退出

#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
     printf("我是一个父进程,我的PID是%d,我的父进程的PID是%d\n",getpid(),getppid());
    int N=5;
while(N--)
{
     pid_t id=fork();
    if(id==0)
{
     printf("子进程开始启动\n");
      execl("/home/WangZhe/dir3/test.exe","test.exe",NULL);
       printf("子进程执行失败\n");
}else
{
   while(1)
{
int status;
  int i= waitpid(id,&status,WNOHANG);
if(i>0)
{
 printf("该子进程%d执行成功\n",i);
 sleep(1);
break;
 }else if(i<0)
{
  printf("等待失败\n");
  sleep(1);
break;
 }else
{
  printf("在等待\n");
 sleep(1);
 }
   }
   }
}
    return 0;
}

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

结语

那么这就是本篇文章关于进程替换的全部内容,那么我的下一篇文章将是综合我们之前学到过的进程的全部的知识来自己实现一个命令行解释器,那么我会持续更新,希望你能够多多关照与支持,如果本篇文章让你有所收获的话,还希望你能够一键三连加关注哦,你的支持就是我创作的最大的动力!
在这里插入图片描述