【Linux】-- 进程程序替换

目录

引入进程程序替换

进程程序替换

初步使用exec系列函数

原理分析

做一个简易的shell

cd - 内置命令的理解

export - 环境变量的深入理解


引入进程程序替换

        对于fork的学习让我们知道:fork()之后的,父子进程各自执行父进程代码的一部分。但是创建一个子进程的目的,肯定是为了做与父进程不同的事,于是想让子进程执行一个全新进程。就有了进程程序替换。

进程程序替换

        程序替换,是通过特定的接口,加载磁盘上的一个权限的程序(代码和数据),加载到调用的进程地址空间中。

#问:进程替换,有没有创建新的子进程?

        答:没有!进程替换是通过将磁盘中的代码和数据放到内存中,然后改变子进程内核数据结构中的页表映射关系。

是什么,进程程序替换?

        程序替换是通过特定的接口,加载磁盘上的一个程序(代码和数据),即加载到调用进程的地址空间中。

为什么,进程程序替换?

        想让子进程执行其他的程序,即不想让子进程执行父进程的部分而是执行一个全新的程序。

怎么办,进程程序替换?

        进程程序替换的核心在于,如何将程序放入内存当中,即所谓的新程序的加载。Linux中采用exec系列函数解决程序的加载。而exec系列函数进行的操作是在,几乎不变化内核结构PCB的角度,将新的磁盘上的程序加载到内存,并和进程的页表重新建立映射。

初步使用exec系列函数

  • 可知:

        上面的6个函数是man 3,所以是由库提供的。是对系统调用的再次封装。而且其文档中有环境变量时的environ,也可以看出其进程程序替换与环境变量的知识关联。此处可更加深层次的了解环境变量。

int execl(const char *path, const char *arg, ...);
  • path:路径 + 目标文件名
  • arg,. . .:可变参数列表(可以传入多个不定个数参数)

        使用方式与,在命令行上执行一样,参数一个一个对应填即可,最后一个参数必须是NULL,标识参数传递完毕。

复习:

        此处,对应环境变量时的知识,其中的main函数的第二个参数。即命令行参数的存储的char*数组。

#include <stdio.h>

int main(int argc, char *argv[])
{
    int i = 0;
    while(argv[i])
    {
        printf("%d: %s\n", i, argv[i++]);
    }
    return 0;
}

        最后一个参数是NULL,表示命令行参数的结束。

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

int main()
{
    printf("当前进程的开始代码\n");
    execl("/usr/bin/ls","ls","-l",NULL);
    return 0;
}

原理分析

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

int main()
{
    printf("当前进程的开始代码\n");
    execl("/usr/bin/ls","ls",NULL);
    
    printf("当前进程的结束\n");
    return 0;
}

        我们发现该程序所执行的进程,只是将execl函数之前的printf执行了,并未将其后的printf执行。这正是因为execl是程序替换,调用该函数成功后,会将当前进程的所有的代码和数据都进行替换!包括已经执行的与没有执行的。只不过由于execl函数之前的printf已经执行并输出了。所以,一旦调用成功,后续代码全部不执行。这也代表着execl无需进行返回值的判断,成功之后程序被替换,后续判断也会被替换没。所以没有判定返回值的意义。

融汇贯通的理解:

        加载新进程之前,子进程的数据和代码与父进程共享,并且数据具有写时拷贝,这是fork的基本知识。而当子进程加载新进程的时候,在fork时所讲的代码不变而共享的存在,在此时也可以说是和数据一样,称为一种“写入”。所以代码也是需要父子分离,也是需要写时拷贝。如此在不仅仅是数据,代码也是需要写时拷贝的。

        如此在execl之后:父子进程在代码和数据上就彻底分开了,虽然曾经不冲突。

库中的exec系列函数 - 极其相似

  • l(list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索环境变量PATH
  • e(env) : 表示自己维护环境变量
execl系列
函数名参数格式路径提供当前环境变量

execl

列表默认
execlp列表默认
execle列表需自行传

        execlpexecl的区别可以说是多了一个p,即可以理解为execl的基础上多了一个PATH,意思就是会自行在环境变量PATH里查找,不需要使用写明执行的程序在哪个路径下。

int execlp(const char *file, const char *arg, ...);
#include <stdio.h>
#include <unistd.h>

int main()
{
    printf("当前进程的开始代码\n");
    execlp("ls","ls", "-l", NULL);

    printf("当前进程的结束\n");
    return 0;
}

        execlpexecl的区别可以说是多了一个e,即可以理解为execl的基础上多了一个evn,也就是environ。意思就是需要我们自行传环境变量。

int execle(const char *path, const char *arg, ..., char * const envp[]);
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[], char* env[])
{
    //extern char **environ;
    printf("当前进程的开始代码\n");
    execle("/usr/bin/ls", "ls", "-l", NULL, env);
    //execle("/usr/bin/ls", "ls", "-l", NULL, environ);

    printf("当前进程的结束\n");
    return 0;
}
execv系列
函数名参数格式路径提供当前环境变量
execv数组默认
execvp数组默认
execvpe数组需自行传

         execvexecl的区别可以说是l变为了v,即可以理解为l是list、v是vector,即一个是可变的链态传递,一个是定的数组态传递。

int execv(const char *path, char *const argv[]);
#include <stdio.h>
#include <unistd.h>

#define NUM 16

int main()
{

    printf("当前进程的开始代码\n");
    char *const _argv[NUM] = {
        (char*)"ls",
        (char*)"-a",
        (char*)"-l",
        (char*)"-i",
        NULL
    };
    execv("/usr/bin/ls", _argv);
    printf("当前进程的结束\n");
    return 0;
}

         execvpexecv的区别可以说是多了一个p,即可以理解为execv的基础上多了一个PATH,意思就是会自行在环境变量PATH里查找,不需要使用写明执行的程序在哪个路径下。

int execvp(const char *file, char *const argv[]);
#include <stdio.h>
#include <unistd.h>

#define NUM 16
int main()
{

    printf("当前进程的开始代码\n");
    char *const _argv[NUM] = {
        (char*)"ls",
        (char*)"-a",
        (char*)"-l",
        (char*)"-i",
        NULL
    };
    execvp("ls", _argv);
    printf("当前进程的结束\n");
    return 0;
}

         execvpeexecvp的区别可以说是多了一个e,即可以理解为execl的基础上多了一个evn,也就是environ。意思就是需要我们自行传环境变量。

int execvpe(const char *file, char *const argv[], char *const envp[]);
#include <stdio.h>
#include <unistd.h>

#define NUM 16
int main(int argc, char *argv[], char* env[])
{
    //extern char **environ;
    printf("当前进程的开始代码\n");
    char *const _argv[NUM] = {
        (char*)"ls",
        (char*)"-a",
        (char*)"-l",
        (char*)"-i",
        NULL
    };
    execvpe("/usr/bin/ls", _argv, env);
    //execvpe("/usr/bin/ls", _argv, environ);
    printf("当前进程的结束\n");
    return 0;
}

系统进程程序替换接口execve函数

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

#define NUM 16
int main()
{
    extern char **environ;
    printf("当前进程的开始代码\n");
    char *const _argv[NUM] = {
        (char*)"ls",
        (char*)"-a",
        (char*)"-l",
        (char*)"-i",
        NULL
    };
    execve("/usr/bin/ls", _argv, environ);
    printf("当前进程的结束\n");
}

        用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

融汇贯通的理解:

        进程程序替换原理:结合进程地址空间,通过将新的磁盘数据加载到内存中,通过改变子进程页表的映射关系,以此达到建立进程的时候,先创建子进程再加载代码与数据的操作。

        我们的手机或者电脑,并不是先将代码与数据加载到内存,而是先把进程创建出来。然后子进程通过加载函数exec系列系统接口,将磁盘的软件、app程序加载到内存里。

        所以说:进程是一个运行起来的程序,是对的但是不够准确。因为程序也有可能没运行(并未进程替换),但是进程已经有了。


拓展:

        windows也有进程程序替换的操作,以前我们在windows所用的代码书写工具VS19等,本质上就是一个进程,它们是一个集成开发环境,可以写代码也可以编译代码等。分别对应编辑器、编译器等,其是一个个的模块,它们自身只提供编辑(写代码)功能。而后面的操作需要我们进行安装对应模块,如编译代码时,由如VS19来创建子进程,并使用进程程序替换来让子进程来编译代码。于是乎我们的代码崩亏并不会印象到VS19的运行,因为进程具有相对独立性

做一个简易的shell

        shell执行命令的时候,我们所执行的命令是处于磁盘的、在文件系统里面、在目录结构里面是一个特定路径下的程序,跑起来之后变成一个进程。其运行的本质就是shell给其创建了一个子进程,然后让子进程直接执行一个exec类型的系统接口,将磁盘的内容加载到内存中去跑起来。

简易的shell的核心

  • 需要将从标准输入中获取的支付串进行分析,将其分散为可用的命令(如:"ls -a -l -i" -> "ls" "-a" "-l" "-i")。利用C字符串函数strtok
  • 将需要补充的命令参数进行补充(如:ls命令的颜色显示参数"--color=auto")。利用if语句判断第一个参数
  • 利用fork后执行程序替换,让子进程执行命令,父进程阻塞等待。此时父进程就是shell

(下列代码点是1,2,3,5,省略了4,由于4更难,完成简单的后再讲解)

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#define NUM 1024
#define SIZE 32
#define SEP " "

//保存完整的命令行字符串
static char cmd_line[NUM];
//保存打散之后的命令行字符串
static char *g_argv[SIZE];

int main()
{
    while(1)
    {
        //1. 打印出提示信息 [qcr@我的系统 myshell]#
        printf("[qcr@我的系统 myshell]#");
        fflush(stdout);
        memset(cmd_line, '0', sizeof cmd_line);

        //2. 获取用户的键盘输入(如:输入的是各种指令和选项: "ls -a -l -i")
        if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
            continue;
        //意义:"-a -l -i\n\0"

        //3. 命令行字符串解析:"ls -a -l -i" -> "ls" "-a" "-l" "-i"
        cmd_line[strlen(cmd_line) - 1] = '\0';
        g_argv[0] = strtok(cmd_line, SEP); //第一次调用,要传入原始字符串
        int index = 1;
        if(strcmp(g_argv[0], "ls") == 0)
            g_argv[index++] = "--color=auto";
        while(g_argv[index++] = strtok(NULL, SEP)); //第二次调用,如果还要解析原始字符串,传入NULL

        //5. fork()
        pid_t id = fork();
        if(id == 0)
        {
            //child
            printf("下面功能让子进程进行的\n");
            execvp(g_argv[0], g_argv); // ls -a -l -i
            exit(1);
        }
        //father
        int status = 0;
        pid_t ret = waitpid(id, &status, 0); //阻塞等待
        if(ret > 0) printf("exit code: %d\n", WEXITSTATUS(status));
    }
    return 0;
}

 

        上面利用50多行就简单实现了一个我们的shell,由于功能实现不全,所以只能实现部分命令(后续博客会随着知识上升,会随之完整,但限于为理解,所以会很简陋)。此处的我们的shell是一个while循环打造的,所以结束需要使用CTRL + c。由于输入分析不够完整,所以对于Delete需要CTRL + Delete。

cd - 内置命令的理解

当我们使用myshell执行cd会发现:

       当我们执行cd的时候,会发现使用cd成功了,但是myshell中的当前地址并且改变。这是因为我们将cd的操作放在了子进程上,子进程进行了cd并不会改变父进程的当前地址。

        我们执行的命令是通过父进程也就是myshell创出的子进程,经过进程替换成我们所输入的命令的可执行程序的代码和数据,其执行起来的进程是子进程角度上的。我们需要在父进程上。

内置命令:让父进程(myshell)自己执行的命令,叫做内置命令,内建命令。

        cd由于是内置命令,所以其是与普通的如"ls"命令是不同的,其并不是一个存储在硬盘的可执行程序,而是由系统提供的系统接口实现。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#define NUM 1024
#define SIZE 32
#define SEP " "

//保存完整的命令行字符串
static char cmd_line[NUM];
//保存打散之后的命令行字符串
static char *g_argv[SIZE];

int main()
{
    while(1)
    {
        //1. 打印出提示信息 [qcr@我的系统 myshell]#
        printf("[qcr@我的系统 myshell]#");
        fflush(stdout);
        memset(cmd_line, '0', sizeof cmd_line);
        //2. 获取用户的键盘输入(如:输入的是各种指令和选项: "ls -a -l -i")
        if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
            continue;
        //意义:"-a -l -i\n\0"
        cmd_line[strlen(cmd_line) - 1] = '\0';
        g_argv[0] = strtok(cmd_line, SEP); //第一次调用,要传入原始字符串
        int index = 1;
        if(strcmp(g_argv[0], "ls") == 0)
            g_argv[index++] = "--color=auto";
        while(g_argv[index++] = strtok(NULL, SEP)); //第二次调用,如果还要解析原始字符串,传入NULL
        
        //4.内置命令
        if(strcmp(g_argv[0], "cd") == 0) //父进程执行
        {
            if(g_argv[1] != NULL) chdir(g_argv[1]);
            continue;
        }
        
        //fork
        pid_t id = fork();
        if(id == 0)
        {
            //child
            printf("下面功能让子进程进行的\n");
            execvp(g_argv[0], g_argv); // ls -a -l -i
            exit(1);
        }
        //5.father
        int status = 0;
        pid_t ret = waitpid(id, &status, 0); //阻塞等待
        if(ret > 0) printf("exit code: %d\n", WEXITSTATUS(status));
    }
    return 0;
}

export - 环境变量的深入理解

makefile 

.PHONY:all
all:myshell mytest

myshell:myshell.c
	gcc -o $@ $^
mytest:mytest.c
	gcc -o $@ $^
.PHONY:clean
clean:
	rm -rf myshell mytest

        all没有目标文件有两个路径文件,于是会分别执行两个路径,其又作为目标文件有各自对应的路径,于是就有了两个可执行程序。

myshell.c

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#define NUM 1024
#define SIZE 32
#define SEP " "

//保存完整的命令行字符串
static char cmd_line[NUM];
//保存打散之后的命令行字符串
static char *g_argv[SIZE];
//利用缓冲区,将环境变量数据传递给子进程
static char g_myargv[64];

int main()
{
    while(1)
    {
        //1. 打印出提示信息 [qcr@我的系统 myshell]#
        printf("[qcr@我的系统 myshell]#");
        fflush(stdout);
        memset(cmd_line, '0', sizeof cmd_line);
        //2. 获取用户的键盘输入(如:输入的是各种指令和选项: "ls -a -l -i")
        if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
            continue;
        //意义:"-a -l -i\n\0"
        cmd_line[strlen(cmd_line) - 1] = '\0';
        g_argv[0] = strtok(cmd_line, SEP); //第一次调用,要传入原始字符串
        int index = 1;
        if(strcmp(g_argv[0], "ls") == 0)
            g_argv[index++] = "--color=auto";
        while(g_argv[index++] = strtok(NULL, SEP)); //第二次调用,如果还要解析原始字符串,传入NULL
        
        //4.内置命令
        if(strcmp(g_argv[0], "cd") == 0) //父进程执行
        {
            if(g_argv[1] != NULL) chdir(g_argv[1]);
            continue;
        }

        //在myshell中添加环境变量 - 是父进程上的行为,所以是内置命令
        //如:g_val[0] = export;g_val[1] = Hello="你好"
        if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)
        {
            strcpy(g_myargv, g_argv[1]);
            int ret = putenv(g_myargv);
            if(ret == 0) printf("export success\n");
            continue;
        }
        
        
        //5.fork
        pid_t id = fork();
        if(id == 0)
        {
            //child
            printf("%s\n", getenv("Hello"));
            printf("下面功能让子进程进行的\n");
            execvp(g_argv[0], g_argv); // ls -a -l -i
            exit(1);
        }
        //father
        int status = 0;
        pid_t ret = waitpid(id, &status, 0); //阻塞等待
        if(ret > 0) printf("exit code: %d\n", WEXITSTATUS(status));
    }
    return 0;
}
//利用缓冲区,将环境变量数据传递给子进程
static char g_myargv[64];

//在myshell中添加环境变量 - 是父进程上的行为,所以是内置命令
//如:g_val[0] = export;g_val[1] = Hello="你好"
if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)
{
    strcpy(g_myargv, g_argv[1]);
    int ret = putenv(g_myargv);
    if(ret == 0) printf("export success\n");
    continue;
}

复习:

        环境变量表中,存储的是环境变量字符串的地址,通过使用字符串地址的方式找到环境变量。

        此处所写的myshell是用while为核心的,而在经过export环境变量操作后continue,将会进行下一次while,而上一个while的环境变量的实际存储位置是在g_argv中,再次循环会因为命令的输入而刷新掉实际环境变量的数据。于是环境变量表中存储的地址就会变为野指针。所以我们需要写一个全局的变量,进行拷贝存放。

mytest.c

#include <stdio.h>
#include <stdlib.h>
int main()
{
    printf("Hello=%s\n",getenv("Hello"));
    return 0;
}

        用于检验我们所创建的shell是否export了环境变量。因为由程序mytest在myshell中运行,是属于myshell的子进程,而环境变量具有全局属性,所以test会继承myshell环境变量。

融汇贯通的理解:
        在环境变量时,有所谓的环境变量以及本地变量,有export的是环境变量,无export的是本地变量,环境变量具有全局属性,能被子进程继承。本地变量不能被子进程继承,而在此处我们可以更加深刻的理解。

        进程程序替换本质上:是将磁盘中的一个程序全部执行起来,其实是一个加载器的角色,而当它加载的时候,我们可以手动的导入我们自定义的环境变量,也可以使用默认的环境变量(父进程的继承给子进程)


shell执行命令的方式通常有两种:

  1. 第三方提供的对应的在磁盘中有具体的二进制文件的可执行程序(由子进程执行)。
  2. shell内部,自己实现的方法,由自己(父进程)来进行执行。 (因为有一些命令就是要影响到shell本身,如:cd,export)

        从myshell的角度更可以直接的解释,程序替换的必要性,一定是和应用场景有关的,有时候就是需要子进程去执行一个全新的程序。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

川入

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

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

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

打赏作者

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

抵扣说明:

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

余额充值