目录
一. 进程替换的原理
二. 进程替换的方法
2.1 进程替换的相关函数
2.2 进程替换为其它的C/C++程序或其它语言编写的程序
三. 自主实现简单地命令行解释器
四. 总结
一. 进程替换的原理
进程替换,就是对进程所执行的代码进行替换,让正在运行的一个进程,终止运行其当前的代码,转而执行其他的代码。
在我之前的博文Linux系统编程:详解进程地址空间_【Shine】光芒的博客-CSDN博客中提到,OS需要为每个进程创建一个进程控制块(PCB)、一份地址空间、一张页表,CPU拿到虚拟地址,通过页表映射,找到实际的物理内存,从而访问相应的数据和执行对应的程序。
通过系统调用接口(exec系列),可以实现对进程的替换。如果一个正在运行的进程要去执行其他的可执行程序,那么OS会将那份即将被执行的可执行程序的代码和数据加载到内存中去,并改变页表的映射关系,并释放原来程序代码和数据占用的物理内存空间,从而让PCB可以通过当前进程的地址空间和页表,映射找到替换后的可执行程序的代码和数据。
进程替换原理总结:新程序/数据载入内存 + 释放原来程序数据占用的物理内存空间 + 重新建立页表映射关系。
 
  一般而言,采用创建子进程的方法来进行进程替换,子进程调用exec系列函数替换进程,父进程通过wait/waitpid函数,来监视子进程的状态。
如果不创建子进程,直接在对父进程进行进程替换操作,那么父进程在在于exec系列函数替换进程之后的代码将不再执行。
二. 进程替换的方法
2.1 进程替换的相关函数
有下面六个函数,可以实现进程的替换:
- int execl(const char* path, const char* argv, ...)
 - int execv(const char* path, char* const argv[])
 - int execlp(const char* file, const char* argv, ... )
 - int execvp(const char* file, char* const argv[] )
 - int execle(const char* path, const char* argv, ... , char* const env[])
 - int execve(const char* file, char* const argv[], char* const env[])
 
在上面的函数中1~5为C语言封装后的进程替换函数,6为Linux提供的系统接口函数。函数1~5的底层都是通过封装execve来实现的。
 
  一般来说,我们不会直接调用6号函数execve来替换进程,函数1~5有些需要传递完整的可执行程序/指令路径,有些可以直接给指令名称,有些应当以const char* 格式的数据,依次传入每个命令行参数,有些应当采用指针数组的形式传递命令行参数。表2.1为具体的分类方法,代码2.1分别展示了如何采用1~4号函数,替换进程执行运行Linux内置的指令ls -a -l。
- 函数名中p代表环境变量PATH,即:会去PATH中指定的路径查找指令,如果运行自己编写的代码,还需要指定路径。
 - l表示需要以列表的形式逐个传入命令行参数,v表示以指针数组的形式传入,无论以那种方式传入命令行参数,都需要以NULL结尾。
 - e表示需要用户自己组装环境变量。
 
| 函数名 | 命令行选项格式 | 是否需要带路径 | 是否需要用户组装环境变量 | 
|---|---|---|---|
| execl | 列表 | 是 | 否 | 
| execv | 指针数组 | 是 | 否 | 
| execlp | 列表 | 否 | 否 | 
| execvp | 指针数组 | 否 | 否 | 
| execle | 列表 | 否 | 是 | 
| execve | 指针数组 | 是 | 是 | 
代码2.1:使用进程替换函数运行Linux指令
  #include<stdio.h>    
  #include<stdlib.h>    
  #include<unistd.h>    
  #include<sys/types.h>    
  #include<sys/wait.h>    
      
  #define NUM 10    
      
  int main()    
  {    
      pid_t id = fork();    
      
      if(id == 0)    
      {    
          char* const _argv[NUM] = {    
               (char* const)"ls",    
               (char* const)"-a",    
               (char* const)"-l",    
               NULL    
          };    
      
          //四种方法执行进程替换    
          //execl("/usr/bin/ls", "ls", "-a", "-l", NULL);    
          //execv("/usr/bin/ls", _argv);    
          //execlp("ls", "ls", "-a", "-l", NULL);    
          //execvp("ls", _argv);    
                                                                                                                                                                                                                                                         
          exit(1);    
      }    
      
      int status = 0;    
      pid_t res = waitpid(id, &status, 0);    
      
      if(res > 0)    
      {    
          printf("exit code:%d\n", WEXITSTATUS(status));    
      }    
      
      return 0;    
  }   
2.2 进程替换为其它的C/C++程序或其它语言编写的程序
- 进程替换为其它的C/C++程序
 
这里使用execle函数来进行替换,被替换的可执行程序为mycmd.exe,在父进程中定义环境变量VAL_1=1234 和 VAL_2=5678,作为execle的最后一个参数传入,mycmd的源文件mycmd.cpp中,依次输出每个环境变量的值。
代码2.2:test.cpp文件(父进程源文件)和mycmd.cpp文件
//1.test.cpp文件
#include<stdio.h>    
#include<stdlib.h>    
#include<unistd.h>    
#include<sys/types.h>    
#include<sys/wait.h>    
    
#define NUM 10    
    
int main()    
{    
    pid_t id = fork();    
    
    if(id == 0)    
    {    
        char* const env[NUM] = {    
            (char* const)"VAL_1=1234",    
            (char* const)"VAL_2=5678",    
            NULL    
        };    
    
        execle("./mycmd.exe", "mycmd.exe", NULL, env);                                                                                                                                                                                                   
    }    
    
    int status = 0;    
    pid_t res = waitpid(id, &status, 0);    
    
    if(res > 0)    
    {    
        printf("exit code:%d\n", WEXITSTATUS(status));    
    }    
    
    return 0;    
} 
//2. mycmd.cpp文件
#include<stdio.h>    
int main(int argc, char* argv[], char* env[])    
{    
    for(int i = 0; env[i]; ++i)    
    {    
        printf("env[%d]:%s\n", i, env[i]);                                                                                                                                                                                                             
    }    
      
    return 0;    
}  
 
  - 替换为其它语言生成的可执行程序(python为例)
 
我们编写test.py文件和test.cpp文件,在test.cpp文件中,使用execl指令,让进程替换为运行test.py的代码,在命令行中,可以使用python test.py运行程序,如果test.py文件具有可执行权限,那么可直接在命令行中输入./test.py运行程序。
代码2.3:test.cpp文件和test.py文件
// test.cpp 文件
#include<stdio.h>    
#include<stdlib.h>    
#include<unistd.h>    
#include<sys/types.h>    
#include<sys/wait.h>    
                                                                                                                                                                                                                                                         
int main()    
{             
    pid_t id = fork();    
                          
    if(id == 0)    
    {              
        execl("/usr/bin/python", "python", "test.py", NULL);    
        exit(1);                                                
    }               
         
    int status = 0;    
    pid_t res = waitpid(id, &status, 0);    
                                            
    if(res > 0)    
    {              
        printf("exit code:%d\n", WEXITSTATUS(status));    
    }                                                     
         
    return 0;    
}    
// test.py 文件
#! /usr/bin/python3.6                                                                                                                                            
print("hello Python")                                                               
print("hello Python")                                                               
print("hello Python")                                                               
print("hello Python")  
三. 自主实现简单地命令行解释器
命令行解释器是常驻系统运行的,需要循环执行下面的操作:
- 打印提示信息,假设为[root@local-address-CentOS]#
 - 读入用户指令,可以使用fgets函数。
 - 从用户的指令中分离出命令行的每个选项(以' '为间隔),通过strtok函数分割。
 - 判断是否为内置指令,内置指令应当在父进程中执行,否则在子进程运行。(这里简化为只考虑cd指令为内置指令)
 - 创建子进程,在子进程中调用execv函数运行指令。
 - 父进程阻塞等待子进程退出码,进入下一层循环等待用户输入下一条指令。
 
这里对命令行解释器中涉及到的系统接口和函数进行简单解读:
- 系统接口chdir:int chdir(const char* path),将当前进程的路径变为path,如果成功改变路径返回0,否则返回-1。
 - 以文本方式读取字符串函数fgets:char* fgets(char* str, size_t size, FILE* stream),从stream流中去读size_t个字符到str指向的空间中去,函数返回值就是str。
 - strtok函数,char* strtok(char* str, const char* delim),通过指定分割符的方式,将一个字符串分割为若干个。如果调用时第一个参数为NULL,那么str就等价于上次找到的分隔符的后面那一个字符的位置,因为strtok具有记忆功能,能够记录下来每次进行分割的位置。
 
代码3.1为命令行解释器的简易实现程序,由于Linux系统是用C语言编写的,所以这里也采用C语言模拟实现命令行解释器。
代码3.1:命令行解释器的简单模拟实现 -- C语言代码
      //所以需要死循环    
      while(1)    
      {    
          //1.打印提示信息    
          printf("[root@local-address-CentOS]# ");    
          fflush(stdout);  //强制刷新缓冲区    
      
          //2.获取用户输入的指令    
          fgets(g_cmd, NUM * sizeof(char), stdin);    
      
          //fgets会引入换行符/0,因此要将/n改为'/0'    
          g_cmd[strlen(g_cmd) - 1] = '\0';    
      
          //3.将指令的每个选项读入到g_argv中去    
          int index = 0;    
          g_argv[index++] = strtok(g_cmd, g_seq);    
      
          while(g_argv[index++] = strtok(NULL, g_seq));    
      
          //for(int i = 0; g_argv[i]; ++i)    
          //{    
          //    printf("%s\n", g_argv[i]);    
          //}    
      
          //4.处理内置命令(直接在父进程中运行,不切换子进程)    
          if(strcmp(g_argv[0], "cd") == 0)    
          {    
              if(g_argv[1] != NULL)    
              {    
                  chdir(g_argv[1]);    
              }    
      
              continue;    
          }    
      
          //5.创建子进程,运行指令    
          pid_t id = fork();    
      
          if(id == 0)  //子进程代码    
          {    
              execvp(g_argv[0], g_argv);    
              exit(1);  //如果进程替换成功,就不会运行exit(1)    
          }    
          else if(id < 0)  //如果子进程创建失败                                                                                                                                                                                                          
          {    
              perror("fork");    
              exit(1);    
          }    
      
          //6.阻塞等待指令运行的结果
          int status = 0;   //接收子进程运行结果
          pid_t ans = waitpid(id, &status, 0);
          
          if(ans > 0)
          {
              printf("exit code: %d\n", WEXITSTATUS(status));
          }
      }
  
      return 0;
  }
 
四. 总结
- 进程替换的底层实现原理是改变页表的映射关系,一般在子进程中进行进程替换操作。
 - 通过exec系列函数,可以实现进程的替换,可以是替换为Linux系统内置的指令,可以替换为其他的C/C++程序,也可以替换为其它语言的程序。
 



















