Liunx下的进程程序替换

文章目录

  • 前言
  • 1.进程替换
    • 1.为啥要进行进程程序替换
    • 2.如何进程程序替换呢?
    • 3.程序替换失败以及返回值的理解
    • 4.进程程序替换的原理
  • 2.进程程序替换接口
    • 1.execl
    • 2.exclp
    • 3.execv
    • 4.execvp
    • 5.补充说明
    • 6.execle
  • 3.小demo与总结

前言

本文主要对Liunx下的进程程序替换进行讲解,会谈及进程程序替换的原理,同时会将之前讲的环境变量进行进行结合,最后会写一个小demo加深我们对进行程序替换的理解。


1.进程替换

1.为啥要进行进程程序替换

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支).我们知道创建子进程的主要目的是想让进程帮我们执行一部分任务,子进程几乎会继承父进程的所有代码,那么意味着子进程会执行父进程相同的任务,如果我们想让子进程执行一个全新的不同于父进程的任务怎么办呢?这个时候只能更改子进程的代码了,这样子进程才会执行不同于父进程的任务,如何更改子进程代码呢?这个时候就需要我们进行进程程序替换了。

2.如何进程程序替换呢?

系统给我们提供了一批接口进行进程程序替换,当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变.

代码演示

Liunx下的进程程序替换_第1张图片

Liunx下的进程程序替换_第2张图片

当我们从调用execl的位置开始,我们执行了ls指令。之前就讲过这个ls本质也是C语言写的程序。我们通过execl让我们自己的写的程序在成为进程的时候进行程序替换执行了ls指令。其实我们还发现上述代码的end没有打印显示,其实这就是进程替换的时候整体替换,原来老的代码都被取代了。关于这个进程程序替换,进程程序替换的函数接口不止一个,这里只是先见见猪跑,后续会对相关接口的使用进行详细介绍的。

3.程序替换失败以及返回值的理解

我们通过函数调用进行程序替换的,既然是函数调用那么就会有返回值,我们先看看函数调用失败会发生什么吧。

Liunx下的进程程序替换_第3张图片
Liunx下的进程程序替换_第4张图片

我们看到当传入一个不存在指令的时候函数调用进程程序替换的时候会返回-1,也就是说程序替换失败会返回-1,那么调用成功呢?我们知道只有函数把事最后办完了才会返回,也就是说返回的时候,函数执行完了。当进程成功进行程序替换原来的老代码和数据都会被新代码和数据所取代,用n接收返回值,n本身也就是一种数据,但是旧的数据都会被取代,也就是根本看不到返回值 换句话说,进程程序替换成功根本不会有返回值。上述代码中如果进程替换成功,该进程会执行新的代码,也就是说不会走到打印end和n这一步。同时从侧面也可以得到一个结论如果有返回值那就说明程序替换一定失败了。

进程替换成功后子进程虽然会执行全新的代码但是父进程依旧可以拿到子进程的退出码,来查看子进程任务执行的情况。因为要替换的程序也有它自己的退出码,子进程程序替换换后沿用该程序的退出码即可。

Liunx下的进程程序替换_第5张图片

Liunx下的进程程序替换_第6张图片

我们看到程序替换成功了,但是我们用ls指令查看一个不存在的hello.txt文件一定不会找到该文件。我们看到父进程依旧拿到了子进程替换程序后的退出码了,这个退出码就是2。刚好是ls指令没找到对应文件的退出码。

4.进程程序替换的原理

Liunx下的进程程序替换_第7张图片

进程程序替换简单来说:就是让一个进程执行一份全新的代码,这份代码来自于磁盘上的程序文件。站在程序的角度这个函数调用就是加载器将磁盘上的程序文件中的相关代码数据加载到内存中以供进程使用;站在进程的角度,程序的代码和数据被加载到内存中,当该进程要进行程序替换的时候,系统会为该进程创新建立新了页表关系,并且将程序的代码和数据放入对应的页表指向的空间中。这样从调用函数接口的位置开始往后的所有的代码和数据都会被另一个程序的代码和数据所取代。这样进程往后执行的就是新的代码了,也就是相当于执行了一个新任务。所以,我们看到了其实从某种意义上讲代码也能发生写时拷贝。在进程程序替换的过程中并没有创建新的进程,虽然一个新程序的代码和数据被加载到内存中了,但是系统中并没有创建一个新的pcb,只是原来的进程得到了一份新的代码和数据。


2.进程程序替换接口

Liunx下的进程程序替换_第8张图片
Liunx下的进程程序替换_第9张图片

我们看到一共有是7个接口,但是其实只有一个系统调用接口,其他都是6个都是基于此接口进行封装的C语言库函数。下面我们就围绕这个系统调用接口是使用进行讲解。

1.execl

Liunx下的进程程序替换_第10张图片

这个函数就是上面最开始用来演示的接口,在讲函数使用之前,我们先思考一个问题如果想进行程序替换是不是要先找到这个程序,找到这个程序之后我们要确定怎么执行这个程序。基于此:我们就很容易理解上述函数的参数了,第一个path就是文程序的所在的路径,用来找到这个程序,后面的参数就是用来确定程序怎么执行的,我们可以联想到在命令行输入的指令,比如ls指令 我们可以直接输入ls 也可以后面跟一些参数 ls -a -l之类的,同样的该函数后面的参数也是这个作用。我们看到这个函数后面的参数是省略号,其实这个就是可变参数列表,我们经常使用的printf函数就是这样的形式,传入的参数个数是不确定的。还需要注意一点的是最后一个参数必须是以NULL结尾。


2.exclp

Liunx下的进程程序替换_第11张图片

其实这个函数的参数含义也很容易猜出来,第一个参数含义就是程序名,系统会自动在环境变量PATH下进行查找,后面的参数就是怎么执行该程序。注意还是以NULL结束

Liunx下的进程程序替换_第12张图片
Liunx下的进程程序替换_第13张图片

我们看到上述的两个参数都是ls 但是它们代表的含义都是不一样的,前者表示在环境变量PATH中搜索名为ls的程序,后者表示ls指令将怎么执行。


3.execv

Liunx下的进程程序替换_第14张图片

这个函数的第一个参数还是用来指定程序的位置,后面是个指针数组,简单来说它这个数组存放怎么运行这个程序的所有指令字符串的地址,不用像之前那样一个一个的传参,这个就相当于打包一起传参

Liunx下的进程程序替换_第15张图片
Liunx下的进程程序替换_第16张图片

这个函数调用也很简单没啥好说的。但是要注意以NULL结尾

4.execvp

Liunx下的进程程序替换_第17张图片

execvp,这个函数我们有了之前的经验,我相信很容易就猜出来这个函数参数的含义。第一个就是程序名系统会在环境变量PATH下进行查找定位,第二个参数就是指定程序怎么运行,不用一个一个传参直接打包在一起传入一个指针数组即可。这里我就不写演示代码了。

5.补充说明

通过观察上述函数接口我们不难发现如果函数名带上了p就不用指定路径指定程序名即可,系统会自动在环境变量PATH下进行查找指定程序。如果没带p就需要我们传入程序的所在的路径。之前我们演示的代码都是调用系统的指令,我们一直都在说系统的指令本质也是可执行程序,那么我们能不能调用自己写的程序进行程序替换呢?其实是可以的,我们看看如下代码。

Liunx下的进程程序替换_第18张图片

Liunx下的进程程序替换_第19张图片
Liunx下的进程程序替换_第20张图片

我们从上述图片看到了这个我们test.c中的子进程成执行了myproc中的代码,mypoc是C++编译生成的可执行程序。所以哪怕是不同的编程语言的生成的可执行程序之间都可以方生进程程序替换。因为在这个程序替换的接口是由操作系统提供的,站在操作系统的角度上看不管何种可执行程序进了内存建立了pcb统统都是进程,都是小弟,操作系统才是老大。这个程序替换的过程是发生在进程这个层次上的,所以不受编程语言的约束。

6.execle

Liunx下的进程程序替换_第21张图片

这个execle我们看到它函数名待e,且最后一个参数是envp。这个参数是用来接收进行程序替换的进程中的环境变量。我们看看如下代码

Liunx下的进程程序替换_第22张图片

Liunx下的进程程序替换_第23张图片
Liunx下的进程程序替换_第24张图片

这我们看到了程序替换后,新的代码也接收的到了原来进程的环境变量。在来看看系统的环境变量和自定义环境变量的对比

Liunx下的进程程序替换_第25张图片
Liunx下的进程程序替换_第26张图片
Liunx下的进程程序替换_第27张图片

我们发现程序替换以后虽然会接收到进程自定义的环境变量,但是是覆盖式传入的程序原来的环境变量没有了,只有进程中的自定义环境变量。如何在进行进程替换的时候自定义环境变量和系统环境变量都能保存下来呢?我们使用putenv即可,putenv会将自定义的环境变量添加到当前进程的系统环境变量中,我们传入系统环境变量用getenv接收即可。

Liunx下的进程程序替换_第28张图片
Liunx下的进程程序替换_第29张图片

之前提到了环境变量具有全局属性 子进程可以继承父进程的环境变量这是怎么办到的呢?我们知道基本上所有的指令都是bash的子进程,bash在创建子进程的时候可以调用上述的函数接口进行对应指令的程序替换,在替换的时候可以传入这个系统环境变量对应的全局变量environ,这样不就行了吗。

有了前面的几种接口介绍后面剩下的几个接口,这里就不在介绍了。我们可以发现一个规律 带p的就不用传入系统环境变量,没带的就要传入系统环境变量,参数是指针数组的就可以把程序执行方式打包带传入,如果是字符串那就一个个传入,如果带e了就是可以程序替换后进环境变量也可以被继承下去。


3.小demo与总结

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define MAX 1024
#define ARGC 64
#define SEP " "

int split(char* commandstr, char* argv[])
{
    assert(commandstr);
    assert(argv);

    argv[0] = strtok(commandstr, SEP);
    if (argv[0] == NULL) return -1;
    int i = 1;
    while ((argv[i++] = strtok(NULL, SEP)));
    //int i = 1;
    //while(1)
    //{
    //    argv[i] = strtok(NULL, SEP);
    //    if(argv[i] == NULL) break;
    //    i++;
    //}
    return 0;
}

void debugPrint(char* argv[])
{
    for (int i = 0; argv[i]; i++)
    {
        printf("%d: %s\n", i, argv[i]);
    }
}

int main()
{
    while (1)
    {
        char commandstr[MAX] = { 0 };
        char* argv[ARGC] = { NULL };
        printf("[Ly@mymachine currpath]# ");
        fflush(stdout);
        char* s = fgets(commandstr, sizeof(commandstr), stdin);
        assert(s);
        (void)s; 
       /* 保证在release方式发布的时候,因为去掉assert了
        所以s就没有被使用, 而带来的编译告警, 什么都没做,
        但是充当一次使用*/
        commandstr[strlen(commandstr) - 1] = '\0';
        int n = split(commandstr, argv);
        if (n != 0) 
          continue;
        pid_t id = fork();
        assert(id >= 0);
        (void)id;
        if (id == 0)
        {
            //child
            execvp(argv[0], argv);
            exit(1);
        }
        int status = 0;
        waitpid(id, &status, 0);
        return 0;
    }
}

这个代码就实现了一个简易版的bash,主要是思路就是创建一个字进程进行程序替换将输入的字符串进行切割成对应的指令,将这些切割好的字符串按序填入对应的参数位置即可。我们这里是使用的execvp不用传入环境变量,只输入好对应的指令即可。

总结

exec/exit就像call/return 一个C程序有很多函数组成。一个函数可以调用另外一个函数,同时传递给它一些参数。被调用的函数执行一定的操作,然后返回一个值。每个函数都有他的局部变量,不同的函数通过call/return系统进行通信。这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux鼓励将这种应用于程序之内的模式扩展到程序之间,一个C程序可以fork/exec另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过exit(n)来返回值。调用它的进程可以通过wait(&ret)来获取exit的返回值.只不过代码中的函数调我们写程序的时候设计好的,这个程序之间的程序相互调用是从进程角度切入的。

你可能感兴趣的:(Liunx操作系统,linux,c++,学习)