进程程序替换


🏷️ 引入:

我们之前所创建的子进程,它执行的代码都是父进程的一部分。如果我们想要我们的子进程去执行全新的的代码,访问全新的数据,不要再和父进程有瓜葛我们应该怎么办呢?

这里我们就要使用:程序替换

🏷️ 见一见程序替换-单进程版的程序替换的代码(没有子进程)

代码如下:

#include <stdio.h>
#include <unistd.h>

int main() 
{
    printf("pid: %d, exec command begin\n", getpid());
    // 用 execl 这个函数来调用系统的命令
    execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
    printf("pid: %d, exec command end\n", getpid());
    return 0;
}

上面👆🏻代码的执行结果就和我们在命令行里输入的指令:ls -a -l 相同。

在这里插入图片描述

观察一下上面运行的结果你会发现。
在这里插入图片描述
下面我们简单介绍一下execl 这个函数

函数原型:

int execl(const char *path, const char *arg0, ..., (char *)NULL);

这里的参数意义如下:

  • path:要执行的程序的路径。
  • arg0:传递给新程序的第一个参数,通常是程序的名称。
  • ...:后续的参数列表,这些参数将作为新程序的 argv 参数传递。参数列表必须以 (char *)NULL 结尾,以表示参数列表的结束。
    在这里插入图片描述

好了,execl 介绍完了,我们的问题是:我们的代码里面不是还有一条语句吗,它为什么没有去执行呢?

我们先把这个问题留着,讲解其他知识之后再做解答。

🏷️ 理解和掌握程序替换的原理,更改多进程版的程序替换的代码,扩展理解和掌握程序替换的原理多进程

📌 程序替换的原理一

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

好的,问题来了,那么这个过程有没有创建新的进程呢?

答:并没有。因为当我在替换新程序时,只是把我们目标程序的代码和数据替换到原来进程的壳子当中,也就是说整个进程的 pid 是不会反生任何改变的。

📌 我们把单进程的改成多进程的

我们之前单进程版本的程序替换的代码如下:

#include <stdio.h>
#include <unistd.h>

int main() 
{
    printf("pid: %d, exec command begin\n", getpid());
    // 用 execl 这个函数来调用系统的命令
    execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
    printf("pid: %d, exec command end\n", getpid());
    return 0;
}

我们现在来改成多进程版本的。

我们将代码修改成如下:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
    pid_t id = fork(); // 创建子进程
    if (id == 0)
    {
        // 这里是子进程
        printf("pid: %d, exec command begin\n", getpid());
        sleep(3);
        // 用 execl 这个函数来调用系统的命令
        execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
        printf("pid: %d, exec command end\n", getpid());
        exit(1); // execl 这个函数如果执行成功,就不会来到这下面的语句,如果执行失败才会来到这里,所以我们这里就直接退出
    }
    else 
    {
        // 这里是父进程
        pid_t rid = waitpid(-1, NULL, 0);
        if (rid > 0)
        {
            printf("wait scuess, rid: %d\n", rid);
        }
    }
    return 0;
}

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

✏️如何来理解这个多进程发生程序替换的这个过程?

当我们今天创建子进程的时候,我们自己心里比较清楚的一点是:子进程也有自己的 PCB、地址空间、页表。
我们知道父进程和子进程通过页表的映射指向的同样的代码区和数据区,创建父子进程的时候,子进程要和父进程代码共享,数据以写时拷贝的方式各自私有一份

那么当我们程序替换的时候 实际上是把可执行程序它对应的代码和数据替换到子进程的代码和数据。但是问题来了,子进程的数据和代码是和父进程共享的,你程序替换把子进程的给替换了,那会不会影响到父进程呢?

首先,当你替换对应的数据的时候,因为父子进程的数据是以写时拷贝的方式各种私有一份的,所以并不会影响。
但是如果你替换对应的代码该怎么办呢?同样也是写时拷贝,在这种场景之下,我们也要把代码段进行写时拷贝。

程序替换的本质就是将新程序的代码和数据替换到我们之前进程(调用 execl的那个进程)的代码和数据,在替换的过程当中,如果是单进程就直接替换,如果是在子进程当中替换,我们要发生写时拷贝来保证父子进程的独立性

回答之前遗留的问题:我代码中最后一行还有个 printf ,为啥没给我执行这句代码呢?

因为当它调用exec 程序替换 ,只要程序替换成功了,其实 eip都被改掉了,所以你的子进程就转而去执行新的程序了, 如果是在单进程的场景下,你调用 execl 成功之后,你的子进程的代码和数据都被替换掉了,所以也无法执行那个printf

当我们执行 exec* 这样的函数,如果当前进程执行成功,则后续代码没有机会在执行了!因为被替换掉了!

exec* 只有失败的返回值,没有成功的返回值

🏷️大量使用其他的程序替换的方法——父子进程场景中

任何程序替换必须解决的两个问题:

  • 必须找到这个可执行程序
  • 必须告诉exec* 怎么执行

✏️ 我们再来认识一下:execl 这个函数

在这里插入图片描述

✏️ execlp 函数

在这里插入图片描述

execlp("ls", "ls", "-a", "-l", NULL); // 注意,第一个“ls”表示的是文件名,第二个“ls”表示的是命令行里的参数

✏️ execv 函数

在这里插入图片描述

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
    pid_t id = fork(); // 创建子进程
    if (id == 0)       // 这里是子进程
    {
        char *const argv[] = {
            "ls",
            "-a",
            "-l",
            NULL};
        printf("pid: %d, exec command begin\n", getpid());
        sleep(1);
        execv("/usr/bin/ls", argv);
        // execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
        // execlp("ls", "ls", "-a", "-l", NULL); // 注意,第一个“ls”表示的是文件名,第二个“ls”表示的是命令行里的参数

        printf("pid: %d, exec command end\n", getpid());
        exit(1);
    }
    else
    {
        // 这里是父进程
        pid_t rid = waitpid(-1, NULL, 0);
        if (rid > 0)
        {
            printf("wait scuess, rid: %d\n", rid);
        }
    }
    return 0;
}

✏️ execvp 函数

在这里插入图片描述

✏️ 问题:我们的程序替换,能替换系统指令程序,能替换我写的程序吗?

在这里插入图片描述
.cc 后缀的文件是 c++ 程序文件。

我们可以在这个mytest.cc 写下如下 c++ 代码:

#include <iostream>

int main()
{
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    
    return 0;
}

同时,我们也要相应的去修改我们的Makefile 文件,


.PHONY:all

all:mytest myprocess
// 这样我们才能在 make 的时候一次形成两个可执行程序

mytest:mytest.cc

g++ -o $@ $^ -std=c++11

  
myprocess:myprocess.c

gcc -o $@ $^ -std=c99

.PHONY:clean

clean:

rm -f myprocess mytest

看看我们今天写的程序:

在这里插入图片描述

我们现在的实验是:用上面的c程序替换掉我们的 c++程序。(用今天学的程序替换的方式)

在此之前,我们也要对我们写的 C 程序做出相应的修改:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
    pid_t id = fork(); // 创建子进程
    if (id == 0)       // 这里是子进程
    {
        execl("./mytest", "mytest", NULL); 
        printf("pid: %d, exec command end\n", getpid());
        exit(1);
    }
    else
    {
        // 这里是父进程
        pid_t rid = waitpid(-1, NULL, 0);
        if (rid > 0)
        {
            printf("wait scuess, rid: %d\n", rid);
        }
    }
    return 0;
}

好,我们做好上面的准备工作之后,开始跑一下:

在这里插入图片描述
哎,也能行!

✏️ 一个程序是怎么加载到内存里的

通过今天的学习你会发现:exec*的这些函数的调用的过程不就是一个程序加载到内存里的那个过程吗?
想想我们之前的那一张图:

在这里插入图片描述

这不就是把磁盘中的程序加载到内存中的过程吗,不就是

📌 解释一下:./mytest 这个程序从开始加载到调度运行到最后退出的整个过程

我们当前的系统识别到了对应的可执行程序,它要创建对应的进程,它会先创建进程的内核数据结构(什么 PCB,地址空间,页表之类的),然后把磁盘中的相应的代码和数据通过类似 exec* 这样的接口加载到物理内存之中。

🏷️接口学习:execle

假设我们现在有以下文件:

📄 myprocess.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

int main()

{

    pid_t id = fork(); // 创建子进程

    if (id == 0)       // 这里是子进程

    {

        printf("pid: %d, exec command begin\n", getpid());

        execl("./mytest", "mytest", NULL);

        printf("pid: %d, exec command end\n", getpid());

        exit(1);

    }

    else

    {

        // 这里是父进程

        pid_t rid = waitpid(-1, NULL, 0);

        if (rid > 0)

        {

            printf("wait sucess, rid: %d\n", rid);

        }

    }

    return 0;

}
📄 test.cc
#include <iostream>
int main()

{

    std::cout << "hello c++" << std::endl;

    std::cout << "hello c++" << std::endl;

    std::cout << "hello c++" << std::endl;

    std::cout << "hello c++" << std::endl;

    std::cout << "hello c++" << std::endl;

    return 0;

}

上面的myprocess.c文件中,我们是使用的execl 这个函数来进行程序替换的。

现在我们来修改一下上面👆🏻的代码,我想要mytest.cc 这个文件来实现一个打印环境变量的功能,我们可以做如下修改:

#include<iostream>

int main(int argc, char* argv[], char* env[])
{
    for (int i = 0; env[i]; i++)
    {
        std::cout << i << ":" << env[i] << std::endl;
    }
    return 0;
}

对上面代码的解释

int argc, char* argv[], char* env[] 
// `int argc`:       命令行参数的数量。
// `char* argv[]`:   一个字符串数组,包含了命令行参数。
// `char* env[]`:    一个字符串数组,包含了环境变量。
for (int i = 0; env[i]; i++)
//这是一个`for`循环,用于遍历环境变量数组。循环的条件是`env[i]`不为`NULL`,这意味着当前索引`i`处有环境变量。因为环境变量也是一张表,环境变量所对应的这张表是一个指针数组,这个指针数组最终以 NULL结尾,所以当我们在遍历的时候,退出循环的条件就是当 env[i]走到 NULL 的时候。

好了,修改之后,我们使用./myprocess ,运行上面的代码(运行之前记得重新 make一下),我们就可以看到打印出来的环境变量了。
通过观察打印出来的环境变量,我们可以得出这样的一个结论:

当我们进行程序替换的时候,子进程对应的环境变量是从父进程那里得来的,而父进程的环境变量是从当前对应的 shell 得来的。

我们可以验证一下:

使用export MYVAL=6666666666666666666666666666666666666666666 命令来自定义一个环境变量到当前 bash 这个 shell 中,名字叫:MYVAL。对应的内容是:6666666666666666666666666666666666666666666

我们可以使用 echo 命令查看一下:echo $MYVAL;

然后重新再运行一下./myprocess ,就可以看到程序打印的环境变量中 有我们自定义的环境变量了。

在这里插入图片描述

在这里插入图片描述

我们还可以进行一次尝试,上面我们导入的环境变量是直接导入到 bash 中的,这次我们在父进程中,导入一个环境变量,看看子进程还拿不拿得到,这个时候我们要修改一下我们的代码。

❓ 我们如何让我们的父进程自己导入一个环境变量呢?

这个时候我们要引入一个新的函数:putenv 头文件是: stdlib.h ,这个函数的作用就是导入一个环境变量

# include <stdlib.h>
int putenv(char* string);

好,知道了上面的知识,我们把我们的myprocess.c 代码修改成如下:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
    char *env_val = "MYVAL2=88888888888888888888888888888";
    putenv(env_val);

    pid_t id = fork(); // 创建子进程
    if (id == 0)       // 这里是子进程
    {
        printf("pid: %d, exec command begin\n", getpid());
        execl("./mytest", "mytest", NULL);
        printf("pid: %d, exec command end\n", getpid());
        exit(1);
    }
    else
    {
        // 这里是父进程
        pid_t rid = waitpid(-1, NULL, 0);
        if (rid > 0)
        {
            printf("wait sucess, rid: %d\n", rid);
        }
    }
    return 0;
}

make 之后,我们运行一下,也可以发现子进程中有父进程自己导入的环境变量:

在这里插入图片描述

在我们上面的代码之中,我们没有主动地传递过任何环境变量给子进程,但是子进程中会出现父进程的环境变量,这说明环境变量被子进程继承下去是一种默认行为,不受程序替换的影响,### 为什么呢?

通过地址空间可以让子进程继承父进程的环境变量数据。
但是环境变量和命令行参数也是数据呀,我们之前讲程序替换的时候不是说要把相应进程的数据段和代码段都替换掉吗?那为什么这里说环境变量被子进程继承下去不受到程序替换的影响

答案很简单: 程序替换确实是替换掉之前程序的代码段和数据段,但是环境变量不会被替换掉。

环境变量具有全局属性

✏️子进程执行的时候,获得环境变量的方法

🧲execle函数的相关知识:

execle函数是Linux系统编程中exec函数族的一员,它用于在当前进程中执行一个新的程序,替换当前进程的映像。这个函数特别之处在于它允许你指定一个新的环境变量列表来代替当前进程的环境变量。

函数原型:

int execle(const char *path, const char *arg, ..., char *const envp[]);

参数:

  • path:要执行的文件的路径。
  • arg:第一个参数是新程序的名称,后面跟着传递给新程序的参数,参数列表必须以NULL结束。
  • envp:这是一个指向环境变量数组的指针数组,新程序的环境变量设置将由这个数组决定。

返回值:

  • 如果执行成功,execle不会返回。
  • 如果执行失败,返回-1,并设置errno以指示错误。

注意事项:

  • 参数列表必须以NULL结束,这表示参数列表的结束。
  • envp参数允许你为新程序定义一个全新的环境变量集合,这在执行需要特定环境配置的程序时非常有用。
  • 使用execle时,需要注意文件路径、权限等问题,确保可执行文件是存在的,并且有适当的执行权限。
🧲 方法一:将父进程的环境变量原封不动的传递给子进程
1.直接用
2.直接传
看例子🌰, 这个时候我们要修改一下我们的代码:
``myprocess.c``要改成这样,我们用的是``execle``函数
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

extern char **environ; // 在这里声明一下。

int main()
{

    pid_t id = fork(); // 创建子进程
    if (id == 0)       // 这里是子进程
    {
        printf("pid: %d, exec command begin\n", getpid());
        execle("./mytest", "mytest", "-a", "-b", NULL, environ); // 这里要报错,说 environ 未定义,但是 environ 是被包在了头文件 unistd.h 中,没办法,我们可以去上面声明一下
        printf("pid: %d, exec command end\n", getpid());
        exit(1);
    }
    else
    {
        // 这里是父进程
        pid_t rid = waitpid(-1, NULL, 0);
        if (rid > 0)
        {
            printf("wait sucess, rid: %d\n", rid);
        }
    }
    return 0;
}

mytest.cc ,要修改为下面这样:

#include <iostream>

int main(int argc, char *argv[], char *env[])
{
    for (int i = 0; i < argc; i++)
    {
        std::cout << i << " -> "  << argv[i] << std::endl; // 打印参数
    }
    std::cout << "#####################################" << std::endl; // 分割线而已
    for (int i = 0; env[i]; i++)
    {
        std::cout << i << ":" << env[i] << std::endl;   // 打印环境变量
    }
    return 0;
}
🧲 方法二:传我们自己定义的环境变量—我们可以直接构造环境变量表给子进程传递

这个时候我们修改一下我们的代码,自定义环境变量。myprocess.c

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

extern char **environ; // 在这里声明一下。

int main()
{

    // 自己定义一个环境变量
    char *const myenv[] = {
        "MYVAL1=11111111111111111111111111",
        "MYVAL2=11111111111111111111111111",
        "MYVAL3=11111111111111111111111111",
        "MYVAL4=11111111111111111111111111", NULL // 注意不要忘了,以 NULL 结尾。
    };
    pid_t id = fork(); // 创建子进程
    if (id == 0)       // 这里是子进程
    {
        printf("pid: %d, exec command begin\n", getpid());
        execle("./mytest", "mytest", "-a", "-b", NULL, myenv);
        printf("pid: %d, exec command end\n", getpid());
        exit(1);
    }
    else
    {
        // 这里是父进程
        pid_t rid = waitpid(-1, NULL, 0);
        if (rid > 0)
        {
            printf("wait sucess, rid: %d\n", rid);
        }
    }
    return 0;
}

编译之后,我们运行一下来试试。(没说明的话 mytest.cc 就不做改变)

在这里插入图片描述

通过观察运行结果我们知道,子进程拿到的只有我们自己定义的环境变量表了。

注意:通过上面的运行结果我们可以知道,execle 函数来传递环境变量时,不是新增环境变量,而是覆盖掉之前的环境变量,同理可以推导出其他 exec 族函数中也一样,带e的,不是新增,而是覆盖

🏷️ 本章图集:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值