• Linux 进程替换深剖


    传统艺能😎

    小编是双非本科大二菜鸟不赘述,欢迎米娜桑来指点江山哦
    在这里插入图片描述
    1319365055

    🎉🎉非科班转码社区诚邀您入驻🎉🎉
    小伙伴们,满怀希望,所向披靡,打码一路向北
    一个人的单打独斗不如一群人的砥砺前行
    这是和梦想合伙人组建的社区,诚邀各位有志之士的加入!!
    社区用户好文均加精(“标兵”文章字数2000+加精,“达人”文章字数1500+加精)
    直达: 社区链接点我


    在这里插入图片描述

    概念🤔

    你是否遇到过这样一个场景,**在一个多人项目中,有人用 Java 实现部分功能,有人用 C++ 实现部分功能,有人用 PHP,go,Python……**那该如何将这些不同的逻辑语言统一进来呢?

    当我们 fork() 生成子进程后,子进程的代码与数据可以来自其他可执行程序。把磁盘上其他程序的数据以覆盖的形式给子进程。这样子进程就可以执行全新的程序了,这种现象称为 程序替换 \color{red} {程序替换} 程序替换

    细则🤔

    首先需要知道进程替换是有原则的:

    1. 进程替换不会创建新进程,因为他只是将该进程数据替换为指定的可执行程序。而进程 PCB 没有改变,所以不是新的进程,进程替换后不会改变 pid

    2. 替换成功后,替换函数后的代码不会执行,因为进程替换是覆盖式的,替换成功后进程原来的代码就消失了,同理替换失败会执行替换函数后的代码

    3. 进程替换函数在进程替换成功后不返回,函数的返回值只会表示替换失败;进程替换成功后,退出码为替换后进程的退出码

    原理🤔

    替换是用的是替换函数: e x e c 函数 \color{red} {exec 函数} exec函数

    该函数类型使用头文件:
    函数原型:int execl(const char *path,const char *arg,…)

    path:可执行程序的路径
    arg:如何执行可执行程序
    … :可变参数,是给执行程序携带的参数,在参数末尾加 NULL 表示参数结束

    返回值:替换失败返回-1,替换成功不返回

    当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的启动例程开始执行,本质上和虚拟内存和写时拷贝机制有点类似,通过在页表上进行映射操作进行替换:

    在这里插入图片描述

    那么子进程程序替换后,会对父进程产生印象吗?

    毫无疑问,子进程被创建时是与父进程共享代码和数据,但当程序替换时,也就意味着需要进行写入操作,这时便需要将父子进程共享的代码和数据进行写时拷贝,此后父子进程的代码和数据也就分离了,因此进行程序替换后不会影响父进程的代码和数据!

    exec 函数🤔

    进程替换有六种替换函数,他们是以 exec 开头的函数,统称为 exec 函数:
    在这里插入图片描述

    execl😋

    int execl(const char *path, const char *arg, ...);
    
    • 1

    第一个参数是要执行程序的路径,第二个参数是可变参数列表,代表我们需要以 list 的形式处理各种操作,内容是各个指令选项,并以NULL结尾

    以 ls 命令为例:

    execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);
    
    • 1

    execlp😋

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

    第一个参数是要执行程序的名字,第二个参数是可变参数列表,代表我们需要以 list 的形式处理各种操作,内容是各个指令选项,并以NULL结尾

    同样以 ls 命令为例:

    execlp("ls", "ls", "-a", "-i", "-l", NULL);
    
    • 1

    execle😋

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

    第一个参数是要执行程序的路径,第二个参数是可变参数列表,代表我们需要以 list 的形式处理各种操作,内容是各个指令选项,并以NULL结尾,第三个参数是你自己设置的环境变量,比如我们设置了自己的 MYVAL 环境变量,在 mypro 程序内就可以使用该环境变量:

    char* myenvp[] = { "MYVAL=2021", NULL };
    execle("./mypro", "mypro", NULL, myenvp);
    
    • 1
    • 2

    execv😋

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

    第一个参数是要执行程序的路径,第二个参数是一个指针数组,代表我们需要以 vector 的形式处理各种操作,数组当中的内容是各个指令选项,数组以 NULL 结尾

    还是以 ls 命令为例:

    char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
    execv("/usr/bin/ls", myargv);
    
    • 1
    • 2

    execvp😋

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

    第一个参数是要执行程序的名字,第二个参数是一个指针数组,代表我们需要以 vector 的形式处理各种操作,数组当中的内容是各个指令选项,数组以 NULL 结尾

    例如,要执行的是ls程序:

    char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
    execvp("ls", myargv);
    
    • 1
    • 2

    execve😋

    int execve(const char *path, char *const argv[], char *const envp[]);
    
    • 1

    第一个参数是要执行程序的路径,第二个参数是一个指针数组,代表我们需要以 vector 的形式处理各种操作,数组以NULL结尾,第三个参数是你自己设置的环境变量,比如我们设置了自己的 MYVAL 环境变量,在 mypro 程序内就可以使用该环境变量:

    char* myargv[] = { "mypro", NULL };
    char* myenvp[] = { "MYVAL=2021", NULL };
    execve("./mypro", myargv, myenvp);
    
    • 1
    • 2
    • 3

    为了方便记忆,这里根据各个函数的后缀进行了归纳:

    l:表示参数采用 list 列表的形式
    v:表示参数采用 vector 数组的形式
    p:表示能自动搜索环境变量 PATH 进行程序查找,即不需要列举程序路径
    e:表示可以传入自己设置的环境(env)变量

    但是事实上, 只有 e x e c v e 才是真正的系统调用 \color{red} {只有 execve 才是真正的系统调用} 只有execve才是真正的系统调用,其它 5 个函数都是调用的execve,也就是说其他 5 个函数实际上是对 execve 的系统调用进行了封装,以满足不同用户的不同调用场景,下图就是各成员间的关系:
    在这里插入图片描述

    实现简易 shell🤔

    shell 也就是之前说的命令行解释器,运行原理就是:当有命令需要执行时,shell 创建子进程,让子进程执行命令,而 shell 只需等待子进程退出即可

    在这里插入图片描述
    我们将 shell 的运行逻辑分为下面的五步:

    1. 获取命令行
    2. 解析命令行
    3. 创建子进程
    4. 替换子进程
    5. 等待子进程退出

    我们之前学习了 fork 函数可以进行进程创建,exec系列函数可以进行子进程替换,wait 或者 waitpid 函数可以等待子进程,那么基础框架就勾勒出来了:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #define LEN 1024 //命令最大长度
    #define NUM 32 //命令拆分后最大个数
    #define SEP " "
    
    char commend_line[NUM];
    char* commend_args[SIZE];
    char env_buffer[128];
    char pwd_buffer[128];
     //实现改变当前工作目录的chdir
    int ChangeDir(const char* new_path)
    {
      chdir(new_path);
      return 0; //调用成功
    }
     //增添自义定环境变量
    void PutEnvInMyShell(char* new_env)
    {
      putenv(new_env);
    }
     
    int main()
    {
      //shell 本质上就是一个死循环
      while(1)
      {
        //显示提示符
        getcwd(pwd_buffer,128);
        printf("yuanlai45@Centos %s# ",pwd_buffer);
        fflush(stdout);
        
        //获取用户输入
        memset(commend_line,'\0',sizeof(commend_line)*sizeof(char)); //初始化为 \0
        fgets(commend_line,NUM,stdin);//获取到的是c风格的字符串 '\0'结尾
        commend_line[strlen(commend_line)-1]='\0';//清除\n 
        
        //字符串分割,比如:"ls -a -l" ->  "ls" "-a" "-l"
        commend_args[0]=strtok(commend_line,SEP);
        int index=1;
        
        //给指令添加颜色
        if(strcmp(commend_args[0],"ls")==0)
        {
          commend_args[index++]=(char*)"--color=auto";
        }
        while(commend_args[index++]=strtok(NULL,SEP));
        
        //内建命令,因为是想改变父进程所处的工作目录,所以并不需要去创建子进程
        if(strcmp(commend_args[0],"cd")==0 && commend_args[1]!=NULL)
        {
          ChangeDir(commend_args[1]);
          continue;
        }
        
        //同理,我们想给父进程添加环境变量,以继承的方式去给子进程
        if(strcmp(commend_args[0],"export")==0 && commend_args[1]!=NULL)
        {
          //目前我们环境变量的信息在commmand_line里面,每次会被清空
          //此处需要自己保存一下环境变量的内容
          //binPutEnvInMyShell(commend_args[1]);
          strcpy(env_buffer,commend_args[1]);
          PutEnvInMyShell(env_buffer);
          continue;
        }
        //创建进程,执行
        pid_t id = fork();
        if(id==0)//子进程
        {
          //程序替换
          execvp(commend_args[0],commend_args);
          exit(-1);//执行到这里说明子进程替换失败
        }
        int status = 0;
        pid_t ret = waitpid(id,&status,0);//阻塞等待
        if(ret>0)
        {
        //打印进程终止信号和退出码
          printf("等待子进程成功,sig:%d,code:%d\n",status&0x7F,status&0xFF);
        }
      }         
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88

    我们自己实现的 shell 在子进程退出后都会打印子进程的退出码,我们可以根据这一点来区分当前使用的是 Linux 的 shell 还是我们自己实现的 shell

    程序替换🤔

    以前我们的程序由很多函数组成,一个函数可以调用另一个函数,同时可以传递一些参数;被调用的函数执行一定的操作,然后返回一个值。

    不同函数通过call/return系统进行通信。这种通过参数和返回值,在拥有私有数据的函数间通信的模式是结构化程序设计的基础,Linux 鼓励将这种应用于程序之内的模式扩展到程序之间

    其次我在开始说过,一个程序如果要做到完美,那么就需要杂糅各种语言来完成不同的功能,因为每种语言的优劣各有不同,所以程序的替换就可以很自然的处理各个语言之间的衔接
    在这里插入图片描述

    在 fork 子进程后,再用 exec 的替换函数进行进程替换即可,从而达到程序之间的相互调用:

    pid_t id = fork();
    if (id == 0){
    	execvp(myargv[0], myargv);
    	exit(1);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这里 exit() 返回进程的调用结果,在原来进程里面是用 wait 或者 waitpid 进行接收即可:

    wait(&status);
    waitpid(id, &status, 0);
    
    • 1
    • 2

    aqa 芭蕾 eqe 亏内,代表着开心代表着快乐,ok 了家人们

  • 相关阅读:
    代码随想录30——回溯:332重新安排行程、51N皇后、37解数独
    PDF页数太多,怎么拆分成几个PDF
    SpringBoot中间件简介
    webpack 静态资源文件加载(assets)
    33:第三章:开发通行证服务:16:使用Redis缓存用户信息;(以减轻数据库的压力)
    Vue 3 基础(二)基础 1
    Android --- 异步操作
    【MySQL篇】初识数据库
    前端面试常见错误
    【C语言基础】结构体中内嵌联合体|联合体中内嵌结构体
  • 原文地址:https://blog.csdn.net/qq_61500888/article/details/127572286