Linux进程的程序替换

Posted 风起、风落

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux进程的程序替换相关的知识,希望对你有一定的参考价值。

文章目录

1. 程序替换

1.创建子进程的目的是什么?

目标:为了让子进程帮父进程执行特定的任务

  • 具体做法:1. 让子进程执行父进程的一部分代码

红框中的代码实际上是父进程的代码,在没有执行fork之前代码就有了,在没有创建子进程之前,父进程的代码加载到内存了,子进程被创建出来是没有独立的代码,这个代码是父进程的代码,父进程通过if判断分流让子进程去跑了

  • 2.创建一个子进程不执行父进程的代码,而是让子进程在磁盘当中执行全新的程序,这种操作称之为进程的程序替换

2.了解程序是如何进行替换的

程序替换函数 execl
输入 man execl 查看程序替换接口

int execl(const char *path, const char *arg, …);
括号内部的 . . . 称为 可变参数列表,可以给c函数传递任意个数的参数
第一个参数为 要执行什么命令
第二个参数 为 要怎样执行程序
最后以NULL结尾表示参数传完了


创建test.c文件并输入以下内容

#include<stdio.h>
   #include<stdlib.h>
  #include<unistd.h>
   int main()
   
     printf("begin......\\n");
    printf("begin......\\n");
     printf("begin......\\n");
    printf("begin......\\n");
                                                                                                                                                                                          
       execl("/bin/ls", "ls", "-a", "-l", NULL);

                                                                        
    printf("end........\\n");                                            
    printf("end........\\n");                                            
    printf("end........\\n");                                            
   printf("end........\\n");
  
     

运行可执行程序发现,只有begin 以及执行 ls -l -a显示的指令


再次修改test.c文件内容如下


  #include<stdio.h>
   #include<stdlib.h>
  #include<unistd.h>
   int main()
   
     printf("begin......\\n");
    printf("begin......\\n");
     printf("begin......\\n");
    printf("begin......\\n");
     printf("我已经是一个进程啦,我的PID:%d\\n",getpid());                                                                                                                                                                      
       execl("/bin/ls", "ls", "-a", "-l", NULL);

                                                                        
    printf("end........\\n");                                            
    printf("end........\\n");                                            
    printf("end........\\n");                                            
   printf("end........\\n");
  
       

test.c 经过编译形成mytest可执行程序,./可执行程序就变成进程了,CPU调度进程 ,打印出代码中的打印语句,同时调用程序替换execl,将ls程序执行起来了


[yzq@VM-8-8-centos nn]$ file /bin/ls
/bin/ls: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=c8ada1f7095f6b2bb7ddc848e088c2d615c3743e, stripped

使用的 /bin/ls 命令 实际上是一个可执行程序,所以ls程序是在磁盘上的


前面执行的是自己代码的一部分,当调用execl时,将磁盘中可执行程序替换当前进程的代码和数据
后半部分就不执行自己的代码了,执行ls所对应的代码 ,这个现象就叫做程序替换

程序替换就是让一个进程去执行另一个在磁盘中的程序,让一个进程把一个新的程序运行起来

3. 程序替换的基本原理


当前的进程执行当前代码时,如果执行了函数execl等接口,就会根据你所传入的程序的路径以及你要执行的名称及选项,把磁盘当中的一个其他的程序加载到对应的内存,
用新程序的代码替换当前进程的代码段,用当前进程的数据替换老进程的数据段


站在进程的角度
进程的程序替换有没有创建新的进程呢?
没有,只是将新的程序加载到当前进程的代码段和数据段,用CPU去调度当前进程就可以跑起来了


站在程序的角度
程序被加载了内存中,就可以称程序替换的接口(execl) 为加载器

当创建进程的时候,先有进程数据结构,还是先加载代码和数据?

修改test.c文件为以下内容

#include<stdio.h>
  2    #include<stdlib.h>
  3   #include<unistd.h>
  4    int main()
  5    
  6        execl("/bin/ls", "ls", "-a", "-l", NULL);                                                                                                                                          7   

此时运行可执行程序,自己就写了一个ls命令


创建子进程,让子进程调用execl,在调用execl把代码和数据加载到内存
所以当创建进程的时候,先有进程数据结构,再加载代码和数据

程序替换是整体替换,不是局部替换

修改test.c文件内容如下

 #include<stdio.h>
  2    #include<stdlib.h>
  3   #include<unistd.h>
  4 #include<sys/wait.h>
  5    int main()
  6    
  7      pid_t id=fork();
  8      if(id==0)
  9      
 10        //child
 11        printf("我是子进程:%d\\n",getpid());
 12        execl("/bin/ls", "ls", "-a", "-l", NULL);                        
 13      
          sleep(5);
 14      printf("我是父进程:%d\\n",getpid());
 15      waitpid(id,NULL,0);                                                                                                                                                                
 16     

查看子进程完成替换后会不会影响父进程,如果影响父进程,就不应该打印父进程的这句话了


过了5秒钟,父进程结果打印出来,说明父进程不受子进程影响


程序替换只会影响调用进程,进程具有独立性
父子进程都有自己独立的PCB 地址空间 页表 也会自己的映射关系
虽然代码有可能是跟父进程共享,当子进程进行程序替换的时候,子进程会加载新进程的代码和数据
操作系统会发生写时拷贝,将代码和数据进行区分 ,使子进程形成新的映射关系,从而使子进程不会影响到父进程

execl 返回值

如果出错了,execl返回值为-1


修改test.c文件内容如下

#include<stdio.h>
  2    #include<stdlib.h>
  3   #include<unistd.h>
  4 #include<sys/wait.h>
  5    int main()
  6    
  7      pid_t id=fork();
  8      if(id==0)
  9      
 10        //child
 11        printf("我是子进程:%d\\n",getpid());
 12       int n=execl("/bin/lsss", "lsss", "-a", "-l", NULL);//lsss命令不存在     
 13        printf("you can see me:%d\\n",n);
 14        exit(0);
 15      
 16      sleep(5);
 17      printf("我是父进程:%d\\n",getpid());                                                                                                                                                
 18      waitpid(id,NULL,0);                                                                                                                            
 19                                                                                                                                                    
 20      

输入的lsss命令不存在,查询报错后的execl的返回值


程序替换只要成功,就会跑去执行新程序,失败了就会继续向后运行
所以execl程序替换成功不会有返回值——>如果替换失败,一定有返回值——>如果失败了,必定返回——>只要有返回值就失败了
说明不用对execl函数的返回值进行判断,只要继续向后运行一定失败

4. 替换函数

1. execl

int execl(const char *path, const char *arg, …);
l 代表 list 链表
path:代表你想执行谁 (需要带路径)
执行一个程序最基本的原则为:找到它,加载执行它
arg:你想怎么执行它(若想执行ls指令,是只执行ls,还是执行ls- l 、ls -l -a指令
在命令行怎么执行这个命令,就把参数一个一个的传递给execl就可以了
最终以NULL结尾


具体的实现以及返回值问题上面在演示程序替换时已经使用过啦

2. execv

int execv(const char *path, char *const argv[]);
v代表vector 容器
path:代表你想执行谁 (需要带路径)
把原来需要一个一个传的参数放在argv[]数组中


修改test.c文件内容

1    #include<stdio.h>
    2    #include<stdlib.h>
    3   #include<unistd.h>
    4 #include<sys/wait.h>
    5    int main()
    6    
    7      pid_t id=fork();
    8      if(id==0)
    9      
   10        //child
   11        printf("我是子进程:%d\\n",getpid());
 12        char *const myargv[]="ls", "-l", "-a",NULL;
13       execv("/bin/ls",myargv);
   14        exit(0);
   15      
   16      sleep(5);
   17      printf("我是父进程:%d\\n",getpid());
   18      waitpid(id,NULL,0);
   19   
   20                                                                                                                                                                                       
  


执行可执行程序,依旧可以执行ls指令

3. execlp

int execlp(const char *file, const char *arg, …);
带p:代表当执行程序的时候,只需要指定程序名即可,系统会自动在PATH环境变量中查找
file: 不需要传路径,只需要把在PATH环境变量的指令传过来
最后以NULL结尾


   #include<stdio.h>  
  2    #include<stdlib.h>  
  3   #include<unistd.h>  
  4 #include<sys/wait.h>  
  5    int main()  
  6      
  7      pid_t id=fork();  
  8      if(id==0)  
  9        
 10        //child  
 11        printf("我是子进程:%d\\n",getpid());  
 12        execlp("ls", "ls", "-l", "-a",NULL);
 13        exit(0);
 14      
 15      sleep(5);                                                                                                                                                                          
 16      printf("我是父进程:%d\\n",getpid());                                                              
 17      waitpid(id,NULL,0);                                                                              
 18      

执行可执行程序,依旧可以执行ls指令


4. execvp

int execvp(const char *file, char *const argv[]);
带p:代表当执行程序的时候,只需要指定程序名即可,系统会自动在PATH环境变量中查找
v代表vector 容器


     #include<stdio.h>    
     #include<stdlib.h>    
    #include<unistd.h>    
  #include<sys/wait.h>    
     int main()    
         
       pid_t id=fork();    
       if(id==0)    
           
         //child    
         printf("我是子进程:%d\\n",getpid());    
       char* const myargv[]="ls", "-l", "-a",NULL;    
         execvp("ls", myargv);    
         exit(0);    
           
       sleep(5);    
       printf("我是父进程:%d\\n",getpid());    
       waitpid(id,NULL,0);                                                                                                                                                                  
        
      


5. execle

int execle(const char *path, const char *arg,
…, char * const envp[]);
path:代表你想执行谁 (需要带路径)
envp[]:代表自定义环境变量
如果调用程序替换时,若不想让子进程使用父进程的环境列表,想自定义环境变量,就可以自己传一个环境变量


在另一个目录中创建other.cc (以cc为结尾说明是一个c++程序),并输入以下内容

#include <iostream>    
#include <unistd.h>    
#include<stdlib.h>    
using namespace std;    
    
int main()    
    
    for(int i = 0; i < 5; i++)    
        
      cout<< "我是另一个程序,我的pid是:"<< getpid()<<endl;                                                                                                                                 
        cout << " MYENV: " << (getenv("MYENV")==NULL?"NULL":getenv("MYENV")) << endl;    
        sleep(1);                                        
                                                        
                                                         
    return 0;                                            
               

修改test.c文件为以下内容

     #include<stdio.h>    
     #include<stdlib.h>    
    #include<unistd.h>    
  #include<sys/wait.h>    
     int main()    
         
       pid_t id=fork();    
       if(id==0)    
           
         //child    
         printf("我是子进程:%d\\n",getpid());    
W>       char*const myenv[]="MYENV=YouCanSeeMe", NULL;    
         execle("/home/mydir/my/mm/myother", "myother", NULL,myenv);    
         exit(0);    
           
       sleep(1);    
       printf("我是父进程:%d\\n",getpid());    
       int  status=0;    
       waitpid(id,&status,0);    
       return 0;                                                                                                                                                                            
                                                                                                                                                                       
        

第一个路径为other.cc生成的可执行程序 myother 所在的 绝对路径



2. 自定义shell

编写极简版本的shell(bash)
目标:为了深刻的理解shell的运行原理


输入 ps ajx |grep bash ,发现bash就是一个进程


由于shell是一个进程,所以用while死循环

缓冲区问题



正常来说,运行可执行程序会显示命令行,但是由于没有\\n刷新缓冲区,也没有使用相关的刷新库函数,所以命令行会一直在缓冲区中 直到 程序结束才显示,但是这是个死循环,所以什么都不会显示




执行可执行程序后即可显示命令行

fgets 使用出现空格问题

fgets 标准输入 按行获取
char *fgets(char *s, int size, FILE *stream);
从特定的标准输入当中获取对应的命令行输入,把对应的数据放在缓冲区中




执行可执行程序后,发现在命令行中输入 ls -a ,就会打印出 ls -a,但是输入时会多出一个空行


正常来说,都会使用回车来到下一行,而这个回车被fgets读取到了



将最后的回车符替换成’\\0’



此时就没有空格出现了

完整代码

: mybash.c ? ?                                                                                                                                                                ?? buffers 
  #include<stdio.h>
  #include<unistd.h>
  #include<string.h>
  #include<assert.h>
  #include<sys/types.h>
  #include<sys/wait.h>
  #include<stdlib.h>
  #define MAX 1024
  #define ARGC 64
  #define SEP " "
  int split (char*commandstr,char*argv[])
  
      assert(commandstr);
      assert(argv);
      argv[0]=strtok(commandstr,SEP);//在commandstr以空格作为分割符
      if(argv[0]==NULL)//切割失败
      
        return -1;
      
      int i=1;
      while(1)
      
       argv[i]=strtok(NULL,SEP);//继续切割空格
       if(argv[i]==NULL)
      
      break; 
      
       i++;                                                                                                                                                                                 
      
W>
    
  void print(char*argv[])
  
 int i=0;
    for(i=0;argv[i];i++)
                                                                                                                                                                                           
      printf("%d:%s\\n",i,argv[i]);
    
  
  int main()
     
     while(1)
    
    char commandstr[MAX]=
        
                

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

1.进程替换注意事项

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

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

3.进程替换函数在进程替换成功后不返回,函数的返回值表示替换失败

4.进程替换成功后,退出码为替换后的进程的退出码

2.替换函数(unistd.h)

execl

函数原型
int execl(const char *path,const char *arg,…)

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

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

eg:


观察上图发现进程替换成功后,替换函数下的打印没有执行,原因与注意事项的第二条相同

如果替换失败:

execlp

函数原型:
int execlp(const char *file, const char *arg,…)
参数解释:
file:要替换的目标程序,arg:如何执行这个程序,…为给这个程序传的参数。最后以NULL结束表示传参结束

返回值:进程替换失败后返回-1。

相比于execl :execp默认在Linux环境变量PATH中查找可执行程序

Linux环境变量

eg:

execv

函数原型:
int execv(const char* path,char* const argv[ ]);

参数解释:
argv数组:保存的是参数列表,将如何执行 可执行程序 和可执行程序需要的参数保存到字符串数组中,最后以NULL结尾表示参数结束,给execv
path:替换目标程序路径,

返回值:进程替换失败返回-1

eg:



可以发现argv数组与main函数的命令行参数相同
Linux_main函数命令行参数

execle

函数原型:
int execle(const char* path, const char* arg, …,char* const envp[ ])
参数解释:
path为替换的目标程序路径,arg与…表示如何执行替换后的程序以NULL结尾。
envp数组为要导入的环境变量
返回值:
替换失败返回-1
eg:

因为此时我们没有环境变量MYSTR所以第一行打印为空

这里在myProc子进程中用execle函数来导入环境变量MYSTR

注意:
1.导环境变量的数组最后以NULL结尾
2.导入环境变量后原系统环境变量的值被清空,这种导入环境变量的方式为覆盖式导入

替换函数的命名理解(execvp,execve)

替换函数前面的exec不变
l:参数采用列表
v:参数采用数组
p:不需要输入路径,在环境变量自动搜索
e:要导入自己的环境变量

所以execvp表示不需要输入路径,参数用数组传
execve表示需要输入路径,参数用数组传,自己维护环境变量

execvp拓展_简易shell

我们在命令行输入的命令可以用函数fgets来获取
定义一个字符数组
char Shell[100]
fgets(Shell,100,stdin);这样就可以将我们的命令存起来。
因为不同参数之间用空格相隔,所以我们可以用字符串分割函数strtok来分割处理成字符串存到数组中。
strtok函数在没有找到或者最后一次找到时返回NULL,这个函数可以将字符串分割为多个字符串。
execvp:不需要路径,参数用数组传,而strtok分割的正好为命令的参数,我们输入的命令又在环境变量中
所以我们利用进程替换可以实现简易的shell
代码如下:

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

#define MAXLEN 1024
#define LEN 32

int main()
{
  char shell[MAXLEN]={0};
  char* DOS[LEN]={0};//保存处理后的参数
  while(1)
  {
    printf("[dodamce@My_centos dir] ");
    fgets(shell,MAXLEN,stdin);
    shell[strlen(shell)-1]='\\0';
    DOS[0]=strtok(shell," ");
    int i=1;
    while(DOS[i]=strtok(NULL," "))//strtok按照空格拆分字符串,并且将其存到DOS字符串数组中
    {
      i++;
    }
  
  pid_t id=fork();//创建子进程
  if(id==0)
  {
    //child
    execvp(DOS[0],DOS);//不需要路径,DOS第一个元素就是我们要替换的可执行程序
    exit(1);
  }
  int status=0;//父进程阻塞等待,接受并打印子进程的退出码,防止僵尸进程
  pid_t wait=waitpid(id,&status,0);
  if(wait>0)
  {
    printf("Exit Code=%d\\n",WEXITSTATUS(status));
  }
  }
  return 0;
}

3.替换函数总结

函数名参数传递形式路径是否导入环境变量
execl列表需要可执行程序路径不导入 使用当前环境变量
execlp列表默认在环境变量中找不导入 使用当前环境变量
execle列表需要可执行程序路径导入 使用导入的环境变量
execv数组需要可执行程序路径不导入 使用当前环境变量
execvp数组默认在环境变量中找不导入 使用当前环境变量
execve数组需要可执行程序路径导入 使用导入的环境变量

以上是关于Linux进程的程序替换的主要内容,如果未能解决你的问题,请参考以下文章

Linux运维之进程管理

Linux中的进程管理

linux云自动化运维基础知识8(进程)

《前端运维》一Linux基础--10定时任务

linux的运维管理UNIT7

Linux进程的程序替换