【Linux】进程程序替换 + 模拟实现简易shell

前言

上一节我们介绍了 **进程终止**和 **进程等待**等一系列问题,并做了相应的验证,本章将继续对进程控制进行介绍,重点学习进程程序替换,并进行相应验证,在此基础上,自己模拟实现一个shell,该shell能够实现执行命令操作。。

1.进程程序替换

概念引入:

将可执行程序加载到内存,并且重新调整 子进程的页表映射,使之指向新的进程的代码和数据段,这种过程就叫做程序替换

  • 用fork创建子进程后执行的是和父进程相同的程序,因为代码共享
  • 但,如果我们想让创建出来的子进程,执行全新的程序呢,此时就需要用到进程的程序替换
  • 进程程序替换就相当于一个加载器的角色

1.1为什么要做进程程序替换?

原因:

  • 原因是我们想让我们的子进程执行一个全新的程序。
  • 不同语言写的功能(比如python shell) 互相调用,这就是为什么要有程序替换的原因。

我们一般在服务器设计(Linux编程)的时候,往往需要子进程干两件种类事情

  • 1.让子进程执行父进程的代码片段(代码共享)
  • 2.让子进程执行磁盘中一个全新的程序(使用shell脚本, 想让客户端执行对应的程序,通过我们的进程,执行其他人写的进程代码等等)

1.2 进程程序替换的原理

程序替换的原理:

  • 将磁盘中的待执行的程序,加载入内存结构
  • 重新建立页表映射,谁执行程序替换,就重新建立子进程的映射关系

效果:让我们的父进程和子进程彻底分离,并让子进程执行一个全新的程序!

![

](https://i-blog.csdnimg.cn/direct/f4c725a0e19742b0831dec30e48d9525.png)

在这里插入图片描述
调整子进程的页表,让其不再与父进程代码和数据有任何关系,而是指向自己的代码和自己的数据区

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

总结:

  • 说白了就是让fork创建子进程,不想让子进程执行父进程代码片段。
  • 我们想让子进程执行磁盘当中全新的程序,而且我们没有创建新的进程。
  • 因为子进程的内核数据结构基本没变,只是重新建立了虚拟到物理的映射关系罢了

1.3 六个exec替换函数

程序替换的是子进程:(重点)
进程替换永远影响的是进程的本身,子进程的替换永远不会影响父进程,因为进程具有独立性
重新建立的是页表映射但并不影响内核数据结构的具体情况.

其实有六种以exec开头的函数,统称exec函数:

#include <unistd.h>  %需要包含头文件

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);

函数解释

  • 这些函数如果调用成功,则加载新的程序从启动代码开始执行,不再返回。
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值(注意:此函数调用失败,才有返回值

函数命名理解

  • (list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索环境变量PATH
  • e(env) : 表示自己维护环境变量
    在这里插入图片描述
1.3.1 execl函数:
int execl(const char *path, const char *arg, ...);
path:这个是路径,可执行程序的路径
arg: 可变参数,可以传多个参数,参数要以列表形式写,比如(1s -1 -a) ,就要写成"ls",“-l”,“-a”,NULL, 
注意:最后必须是NULL结尾,表示参数传递完毕。
 

代码演示:

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

int main()
{
    //让我的程序执行系统上的: ls -a -i这样的一个命令
    printf("我是一个进程,我的pid是 : %d\n", getpid());

    //int ret = execl("/usr/bin/ls", "ls", "-l", "-a", NULL); //带选项
 
    //替换失败的情况
    int ret = execl("/usr/bin/lsssss", "ls", "-l", "-a", NULL); //带选项
    printf("我执行完毕了,我的pid : %d, ret = %d\n", getpid(), ret);

    return 0;
}
  • 一旦替换成功,是将当前进程的代码和数据全部替换了!!
  • 前一个printf被执行是因为程序替换并没有执行。
1.3.2 execv 函数:
int execv(const char *path, char *const argv[]); %%
实现的功能和execl一模一样。
path:这个是路径,可执行程序的路径
argv[]: 和execl的唯一区别就是传参方式的不一样,这个要传入数组har* const argv_[] = {"ls","-l","-a","-i", NULL};
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

int main()
{
    printf("我是父进程,我的pid是:%d\n", getpid());
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        //我们要子进程执行全新的程序,以前我们是子进程执行父进程的代码片段
        
        printf("我是子进程,我的pid是:%d\n", getpid());
        //char* const argv_[] = {
        //    (char*)"ls",
        //    (char*)"-l",
        //    (char*)"-a",
        //    (char*)"-i",
        //    NULL
        //};
        
        char* const argv_[] = {
            (char*)"top",
            NULL 
        };
       //execv("/usr/bin/ls", argv_);

       execv("/usr/bin/top", argv_);   
    }

    //一定是父进程
    int status = 0;
    int ret = waitpid(id, &status, 0);
    if(ret == id)
    {
        sleep(2);
        printf("进程等待成功!\n");
    }

    return 0;
}
1.3.3 execlp函数:
int execlp(const char *file, const char *arg, ...);
file: 你想执行程序的名字,注意:命名中带P的,可以不用带可执行程序的路径。
arg:以列表形式传参

代码演示:

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

//有时候不想让父进程做一件事,只想让子进程做一件事
//将进程创建引入进来

int main()
{
    printf("我是父进程,我的pid是:%d\n", getpid());
    pid_t id = fork();
    if(id == 0)
    {
        printf("我是子进程,我的pid是:%d\n", getpid());
        execlp("ls", "ls", "-a", "-l", "-i", NULL);//这里出现了两个ls,含义一样吗?-- 不一样!
        //第一个参数是供系统去找要执行谁的指令,后面一坨是表示如何执行该指令
        
        exit(100); //只要执行了exit,就意味着,execl系列的函数失败了 -- 进程替换失败了
    }

    //一定是父进程
    int status = 0;
    int ret = waitpid(id, &status, 0);%0代表阻塞等待
    if(ret == id)
    {
        sleep(2);
        printf("wait success, ret : %d, 我所等待子进程的退出码: %d, 退出信号是: %d\n", ret, (status >> 8) & 0xFF, status & 0x7F);
    }

    return 0;
}
1.3.4 execvp函数:

作用和execIp是一样的,只不过传参形式不一样。。

int execvp(const char *file, char *const argv[]);
file: 你想执行程序的名字,注意:命名中带P的,可以不用带可执行程序的路径。
arg:以数组形式传参

代码演示:

    char* const argv_[] = {
        (char*)"top",
        NULL 
    };
   execvp("ls", argv_ );
1.3.5 execle函数:

多了一个参数,是环境变量。

int execle(const char *path, const char *arg, ..., char * const envp[]);
path:这个是路径,可执行程序的路径
arg: 可变参数,可以传多个参数,参数要以列表形式写,比如(1s -1 -a) ,就要写成"ls",“-l”,“-a”,NULL, 
注意:最后必须是NULL结尾,表示参数传递完毕。
 envp:环境变量

代码演示:

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

int main()
{
    //环境变量的指针声明
    extern char** environ;

    printf("我是父进程,我的pid是:%d\n", getpid());
    pid_t id = fork();
    if(id == 0)
    { 
        printf("我是子进程,我的pid是:%d\n", getpid());
        
        //我们来手动导入一个环境变量
        char* const env_[] = {
            (char*)"MYPATH=You Can See Me!!",
            NULL 
        };

        //e: 添加环境变量给目标进程,是覆盖式的!
        //execle("./mycmd", "mycmd", NULL, env_);
        
        //execle("/usr/bin/ls", "ls", NULL, env_);

        execle("./mycmd", "mycmd", NULL, environ);

        exit(100); //只要执行了exit,就意味着,execl系列的函数失败了 -- 进程替换失败了
    }

    //一定是父进程
    int status = 0;
    int ret = waitpid(id, &status, 0);
    if(ret == id)
    {
        sleep(2);
        printf("进程等待成功!\n");
    }

    return 0;
}

使用execle()添加环境变量给目标进程,是覆盖式的!会把原来的environ环境变量都改掉
所以环境变量只剩下MYPATHT

正确做法是:将全部环境变量传过去,将environ传过去。

在这里插入图片描述

补充重点1:

  • 子进程会继承父进程的环境变量的,当父进程调用fork()创建子进程时,子进程会继承父进程的所有环境变量。
  • 当子进程调用execlp()等函数执行其他程序时,子进程也会继承父进程的环境变量.
  • 如果需要在子进程中更改环境变量,可以使用setenv()或putenv()等函数进行更改.
  • 更改的环境变量只会影响当前进程和它的子进程,并不会影响父进程或其他进程的环境变量

补充重点2:
ls 是一个常见的系统命令, 它通常位于系统的某个标准路径(如 /bin 或 /usr/bin),即使 PATH 为空,execlp() 会检查这些标准路径,找到 ls 的可执行文件并执行它。

  1. 如果PATH环境变量为空,execlp()函数会无法在环境变量中查找可执行文件的路径。但是,execle()函数会检查一些默认路径,例如/bin、/usr/bin等,来查找可执行文件。因此,即使PATH为空execle()函数也可能会在这些默认路径中找到可执行文件并执行它

  2. 但是,如果在默认路径中也找不到可执行文件,则execle()函数会执行失败,并将errno设置为ENOENT,表示无法找到可执行文件。因此,如果需要执行特定路径下的可执行文件,最好使用execv()或execve()等函数,并指定可执行文件的完整路径。这样可以避免依赖PATH环境变量来查找可执行文件的路径。

1.3.6 execve函数:
int execve(const char *path, char *const argv[], char *const envp[]);

有了上面的execle()函数的讲解,理解就不复杂了,只是第二个参数传的不同,这里传的是一个指针数组。
在这里插入图片描述

为什么有那么多的接口?

  • 目的是:适配应用场景。
  • 其实上述函数都是对系统调用接口的封装。

1.4 实现简易版shell

只要我们懂得了程序替换的原理,会用程序替换的接口,就很好理解:

  • shell本身执行起来就是个死循环
  • shell创建子进程,将子进程给替换掉就ok了

要写一个shell,需要循环以下过程

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork)
  4. 替换子进程(execvp)
  5. 父等待子进程退出(wait)
#include <stdio.h>
#include <string.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

#define SEP " "
#define MAX_CMD 1024
#define SIZE 128

char command_line[MAX_CMD];
char* command_args[SIZE];

char env_buffer[NUM];

extern char** environ;

//对应上层的内建命令
int ChangeDir(const char* new_path)
{
    chdir(new_path);

    return 0;//调用成功
}

void PutEnvInMyShell(char* new_env)
{
    putenv(new_env);
}

int main()
{
    //shell本质就是一个死循环
    while(1)
    {
        //不关心获取这些属性的接口,搜索一下都有
        
        //1.显示提示符
        printf("[用户名@我的主机名 当前目录]# ");
        fflush(stdout);

        //2.获取用户输入
        memset(command_line, '\0', sizeof(command_line)); //初始化

        //从键盘获取,标准输入,stdin,获取到的是C风格的字符串(stdio.h结尾的),'\0'结尾
        fgets(command_line, NUM, stdin);
        command_line[strlen(command_line) - 1] = '\0';//清空\n回车
        //printf("%s\n", command_line);

        //3. "ls -a -l -i" -> "ls" "-a" "-l" "-i" 字符串切分 -- 因为这些参数一定得以列表或者数组方式传递给程序替换接口
        //shell必须切分,因为必须调用execl函数
        
        //将第一个字符串地址用0号下标指向,第二个字符串地址用1号下标指向 
        command_args[0] = strtok(command_line, SEP);

        int index = 1;

        //给ls命令添加颜色: 如果提取出来的程序名是ls -- 1下标设置成改颜色的
        if(strcmp(command_args[0], "ls") == 0) command_args[index++] = (char*)"--color=auto";

        //strtok截取成功返回字符串起始地址
        //截取失败,返回NULL
        while(command_args[index++] = strtok(NULL, SEP));
        
        //for debug
        //int i = 0;
        //for(i = 0; i < index; i++)
        //{
        //    printf("%d : %s\n", i, command_args[i]);
        //}
        
        //4.TODO -- 编写后面的逻辑,内建命令(由父Shell自己实现的自己调用的一个函数)
        if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL)
        {
            //让调用方进行路径切换,父进程
            ChangeDir(command_args[1]);
            continue;
        }
        
        //走到这里一定是将命令行参数解析完了,包括命令 + 选项
        
        //将环境变量的信息导入在了父进程的上下文当中
        if(strcmp(command_args[0], "export") == 0 && command_args[1] != NULL)
        {
            //环境变量列表(是个指针数组,每个元素是个指针指向一个环境变量)
            //我们传的是一个字符串首地址,但是环境变量的内容还是我们自己维护的
            //目前,环境变量信息在comman_line,会被清空,那么环境变量当然就没有了
            //所以此处我们需要自己保存一下环境变量的内容
            
            strcpy(env_buffer, command_args[1]);
            PutEnvInMyShell(env_buffer);
            //PutEnvInMyShell(command_args[1]);//MYENV=112233
            continue;
        }

        //5.创建进程Fork,执行
        //如果自己直接程序替换的话,就把自己写的shell给替换了
        
        pid_t id = fork();
        if(id == 0)
        {
            //子进程
            //6.程序替换
           
            //execvpe(command_args[0], command_args, environ);
            execvp(command_args[0], command_args);

            exit(1);//执行到这里,子进程一定替换失败了
        }

        int status = 0;
        
        pid_t ret = waitpid(id, &status, 0);

        if(ret > 0)
        {
            printf("等待子进程成功: sig: %d, code: %d\n", status & 0x007F, (status & 0xFF00) >> 8);
        }

    }//end while

    return 0;
}
1.4.1 Shell 内建命令等问题的解决
  • cd命令的处理
    在命令行中使用cd指令,会跳转路径,如果使用绝对命令,就不行了

  • 一个进程也存在对应路径,进程对应的路径可以理解成:当进程启动的时候,在那个路径启动,这个进程所在的路径就是当前进程所启动的路径。

  • 如果我们不对cd进行特殊处理,则子进程路径切换后,并不影响父进程的路径,会发现命令路径还是没变化。

所以,对应这个命令,就不能用程序替换的方式,来执行一些特殊的命令了。
而是,在父进程中将一些命令,单独处理。

重点:

程序替换影响的是子进程和父进程没关系,子进程一 跑就完了,曾经所有的操作就没有意义,路径切换就没意义了,所以我们要让父进程的路径发生变化。
如果有些行为,是必须让父进程shell执行的,不想让子进程执行,绝对不能创建子进程!只能是父进程自己实现对应的代码!

正确做法:
使用系统中,更改工作目录的函数。
在这里插入图片描述

 if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL)
        {
            //让调用方进行路径切换,父进程
            ChangeDir(command_args[1]);
            continue;
        }
  • 内建命令:
    由shell自己执行的命令,我们称之为内建(内置bind- in)命令。

  • export的处理:
    导入环境变量:

  • export不是一个可执行程序和cd,ls,cat等指令不同。

  • export是一个shell内置命令,用于设置环境变量。它并不是一个可执行程序,而是由shell解释器直接执行的命令

所以,在使用execvp进行程序替换的时候,是不能替换成功的!
在这里插入图片描述
注意:

  • 环境变量是属于系统的数据,子进程在执行程序替换时,当前进程的环境变量数据,不会被替换掉,而且是以父进程为模版继承下来的
  • 所以才会让父进程以内建命令的方式putenv,子进程就能直接获取了.
  • 环境变量会被子进程继承下去,所以他会有全局属性。
  • 必须,Export放在父进程的内建命令中实现,因为放在子进程中,父进程内的环境变量,就没有改变,是有问题的。。

尾声
看到这里,相信大家对这个Linux有了解了。
如果你感觉这篇博客对你有帮助,不要忘了一键三连哦

  • 29
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值