Linux进程程序替换

简介: Linux进程程序替换

什么是进程程序替换?

       Linux进程程序替换是一种操作系统内部的机制,它使得一个正在运行的进程可以将其程序映像替换为另一个指定的可执行程序。具体来说,当我们发出指令后,由shell外壳例如bash这样的任务处理平台创建一个子进程,然后将其替换为对应的指令程序来执行特定的任务。


       值得注意的是,进程程序替换并不会创建新进程。因为这只是将该进程的数据替换为指定的可执行程序,而进程的控制块PCB没有改变,所以这并不是新的进程。因此,进程替换后不会发生进程pid改变。


       此外,进程程序替换常常和进程地址空间关联在一起,其实现依赖于fork函数创建子进程以及exec函数族进行进程程序替换。这些函数是Linux系统提供的用于创建新进程和执行新程序的系统调用接口。需要注意的是:当父进程使用fork创建子进程后,子进程实际上是对父进程PCB的拷贝,也就是说他们开始是指向同一块物理内存的,但是,当发生进程程序替换时,子进程会发生写实拷贝,替换的程序会出现在子进程页表最新指向的物理内存中!


       大致替换过程如下:

引入

首先。认识一下第一个进程程序替换的函数:execl

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

  对于以上各个参数的解析:


第一个参数path是指要执行的程序的路径名

第二个参数arg后续可变参数则代表程序的参数列表。需要注意的是,这些参数必须以空指针NULL结束。

当execl函数执行成功后,它会返回一个非负整数,通常被操作系统用于表示成功的状态。但如果发生错误,比如指定的文件不存在或无法读取,那么就会返回一个负值。

  对此我们结合之前所学知识,尝试着使用execl替换子进程程序为ls:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        printf("pid: %d, exec command begin\n", getpid());
        execl("/usr/bin/ls", "ls", "-a", "-l", "-n", NULL);
        printf("pid: %d, exec command end\n", getpid());
        exit(1);
    }
    else{
        // father
        pid_t rid = waitpid(-1, NULL, 0);
        if(rid > 0)
        {
            printf("wait success, rid: %d\n", rid);
        }
    }
    return 0;
}

根据运行结果我们可知:父进程等待子进程执行完毕才执行,但是子进程缺没有执行execl函数后面的printf语句。这说明了什么?这说明子进程的程序被替换了!因此后续的程序没有执行!再看前后打印出来的子进程pid,发现都是相同的,这说明了什么?这说明进程的PCB没有改变!还是原来的进程!对于父进程,发现也只是运行了父进程的代码,而不是因为子进程的代码和数据被替换了也跟着改变,这也说明了上面所提到的写实拷贝,发生进程程序替换,子进程会指向一块新的物理空间

替换函数

实际上替换函数不止上面我们所提到的execl,其实有六种以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[]);//比较特殊,实际为上面五个函数的底层

       下面分别对以上替换函数详细解析:

execl

       execl函数的定义形式如下:

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

这里有三个参数:

  • 第一个参数path,它是一个字符串,表示要执行的程序的路径名。比如"/bin/ls"就是要执行的程序路径名。
  • 第二个参数arg,也是一个字符串,代表程序的参数列表。比如 "ls -a" 就是参数列表。需要注意的是,这些参数必须以空指针NULL结束。
  • 第三个参数及以后的参数则代表了传递给程序的参数值。

当execl函数被成功调用后,它并不会返回到调用它的进程中,而是直接转到新程序中运行。如果发生错误,比如指定的文件不存在或无法读取,那么就会返回一个负值。

execlp

       execlp函数的定义形式如下:

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

这里有三个参数:

  • 第一个参数file,它是一个字符串,表示要执行的程序的路径名。比如"/bin/ls"就是要执行的程序路径名。如果程序在系统的PATH环境变量所列出的目录中,则可以直接使用程序名作为第一个参数。
  • 第二个参数arg,也是一个字符串,代表程序的参数列表。比如 "ls -a" 就是参数列表。需要注意的是,这些参数必须以空指针NULL结束。
  • 第三个参数及以后的参数则代表了传递给程序的参数值。

当execlp函数被成功调用后,它并不会返回到调用它的进程中,而是直接转到新程序中运行。如果发生错误,比如指定的文件不存在或无法读取,那么就会返回一个负值。

       例子:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        printf("pid: %d, exec command begin\n", getpid());
        execlp("ls", "ls", "-a", "-l", NULL);
        printf("pid: %d, exec command end\n", getpid());
        exit(1);
    }
    else{
        // father
        pid_t rid = waitpid(-1, NULL, 0);
        if(rid > 0)
        {
            printf("wait success, rid: %d\n", rid);
        }
    }
    return 0;
}

execv

       execv函数的定义形式如下:

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

这里有两个参数:

  • 第一个参数path,它是一个字符串,表示要执行的程序的路径名。比如"/bin/ls"就是要执行的程序路径名。
  • 第二个参数argv是一个指向字符指针数组的指针,代表程序的参数列表。例如,如果argv指向一个包含三个元素的数组{char * const arg[] = {"ls", "-a", NULL}},那么"ls -a"就是参数列表。需要注意的是,这些参数必须以空指针NULL结束。

       当execv函数被成功调用后,它将加载指定的可执行程序替换当前进程的代码段、数据段、堆栈等信息,使得当前进程执行其他程序。如果发生错误,比如指定的文件不存在或无法读取,那么就会返回一个负值。


       例子:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
    char *const argv[] = {
            "ls",
            "-a",
            "-l",
            NULL
        };
        printf("pid: %d, exec command begin\n", getpid());
        execv("/usr/bin/ls/", argv);
        printf("pid: %d, exec command end\n", getpid());
        exit(1);
    }
    else{
        // father
        pid_t rid = waitpid(-1, NULL, 0);
        if(rid > 0)
        {
            printf("wait success, rid: %d\n", rid);
        }
    }
    return 0;
}

execvp

       execvp函数的定义形式如下:

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

  这里有两个参数:


  • 第一个参数file,它是一个字符串,表示要执行的程序的路径名。比如"/bin/ls"就是要执行的程序路径名。如果程序在系统的PATH环境变量所列出的目录中,则可以直接使用程序名作为第一个参数。
  • 第二个参数argv是一个指向字符指针数组的指针,代表程序的参数列表。例如,如果argv指向一个包含三个元素的数组{char * const arg[] = {"ls", "-a", NULL}},那么"ls -a"就是参数列表。需要注意的是,这些参数必须以空指针NULL结束。

       当execvp函数被成功调用后,它将加载指定的可执行程序替换当前进程的代码段、数据段、堆栈等信息,使得当前进程执行其他程序。如果发生错误,比如指定的文件不存在或无法读取,那么就会返回一个负值。


       例子:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
    char *const argv[] = {
            "ls",
            "-a",
            "-l",
            NULL
        };
        printf("pid: %d, exec command begin\n", getpid());
        execvp(argv[0], argv);
        printf("pid: %d, exec command end\n", getpid());
        exit(1);
    }
    else{
        // father
        pid_t rid = waitpid(-1, NULL, 0);
        if(rid > 0)
        {
            printf("wait success, rid: %d\n", rid);
        }
    }
    return 0;
}

前置知识—环境变量的继承

       当我们进行程序替换的时候,子进程对应的环境变量是可以直接从父进程来的。下图所示意思为:父进程继承了bash的环境变量,在父进程的时候又增加了环境变量,而子进程会继承bash和父进程的环境变量:

例子:如下我们分两个文件进行操作,程序mytest.cc用于打印argv和环境变量信息,注意编译的时候需要使用 g++ mytest.cc -o mytest 用于myprocess.c中识别,myprocess.c中自定义了一个环境变量env_val,使用 putenv()这个函数将环境变量更新,然后再进行程序替换。结果发现如上图一样子进程继承了环境变量。

#include <iostream>
#include <unistd.h>
int main(int argc, char *argv[], char *env[])
{
    for(int i = 0; i < argc ; i++)
    {
        std::cout << i << "->" <<  argv[i] << std::endl;
    }
    std::cout << "################################" << std::endl;
    for(int i = 0; environ[i]; i++)
    {
        std::cout << i << " : " << env[i] << std::endl;
    }
    return 0;
}
      #include <stdio.h>
      #include <unistd.h>
      #include <stdlib.h>
      #include <sys/types.h>
      #include <sys/wait.h>
      extern char **environ;
     int main()
      {
            char *const myenv[] ={
            "MYVAL1=11111111111111",
            "MYVAL2=11111111111111",
            "MYVAL3=11111111111111",
            "MYVAL4=11111111111111",
              NULL
        };
          char *env_val = "MYVAL5=5555555555555555555555555";
          putenv(env_val);
          pid_t id = fork();
          if(id == 0)
          {
              printf("pid: %d, exec command begin\n", getpid());
              execl("./mytest","mytest",NULL);
              //execle("./mytest", "mytest", "-a", "-b", NULL, myenv);
              //execle("./mytest", "mytest", "-a", "-b", NULL, environ);
              printf("pid: %d, exec command end\n", getpid());
              exit(1);
          }                                                                                                                                                                                
          else{
              // father
              pid_t rid = waitpid(-1, NULL, 0);
              if(rid > 0)
              {
            printf("wait success, rid: %d\n", rid);
              }
          }
          return 0;
      }

       环境变量被子进程继承下去是一种默认的行为,不受进程替换影响,这是因为进程地址空间可以让子进程继承父进程的环境变量数据。当是当我们使用进程替换时,根据进程替换类型的不同也会有所不同:

execle

       函数原型如下:

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

参数说明:

  • const char *path:要执行的程序的路径名。
  • const char *arg:程序的参数列表。
  • ...:可变参数列表,表示传递给程序的其他参数值。
  • char *const envp[]:环境变量列表,用于设置程序运行的环境。

       函数返回值为int类型,表示执行结果。如果执行成功,返回0;否则返回-1。

       需要注意的是通过这个函数我们可以做到三种传递环境变量的情况:

1、将父进程的环境变量原封不动传递给子进程(直接传递environ给第四个参数)

2、新增传递(如上面前置知识例子差不多,只不过需要多传递一个全局变量environ第四个参数)

3、覆盖传递,传递我们自己的环境变量,我们可以直接构造环境变量表,给子进程传递。如下:

#include <iostream>
#include <unistd.h>
int main(int argc, char *argv[], char *env[])
{
    for(int i = 0; i < argc ; i++)
    {
        std::cout << i << "->" <<  argv[i] << std::endl;
    }
    std::cout << "################################" << std::endl;
    for(int i = 0; environ[i]; i++)
    {
        std::cout << i << " : " << env[i] << std::endl;
    }
    return 0;
}
      #include <stdio.h>
      #include <unistd.h>
      #include <stdlib.h>
      #include <sys/types.h>
      #include <sys/wait.h>
      extern char **environ;
     int main()
      {
            char *const myenv[] ={
            "MYVAL1=11111111111111",
            "MYVAL2=11111111111111",
            "MYVAL3=11111111111111",
            "MYVAL4=11111111111111",
              NULL
        };
          char *env_val = "MYVAL5=5555555555555555555555555";
          putenv(env_val);
          pid_t id = fork();
          if(id == 0)
          {
              printf("pid: %d, exec command begin\n", getpid());
              execle("./mytest", "mytest", "-a", "-b", NULL, myenv);
              //execle("./mytest", "mytest", "-a", "-b", NULL, environ);
              printf("pid: %d, exec command end\n", getpid());
              exit(1);
          }                                                                                                                                                                                
          else{
              // father
              pid_t rid = waitpid(-1, NULL, 0);
              if(rid > 0)
              {
            printf("wait success, rid: %d\n", rid);
              }
          }
          return 0;
      }

execve

       函数原型如下:

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

 参数说明:

  • const char *path:要执行的程序的路径名。
  • char *const argv[]:程序的命令行参数列表。
  • char *const envp[]:环境变量列表,用于设置程序运行的环境。

       execve函数实际上是其他五个exec函数(execl, execlp, execv, execvp, execle)的底层实现。当你调用这些函数时,它们最终都会调用execve函数来执行指定的程序。这五个exec函数的主要区别在于参数传递的方式。


 感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o! 

相关文章
|
3天前
|
算法 Linux 调度
深度解析:Linux内核的进程调度机制
【4月更文挑战第12天】 在多任务操作系统如Linux中,进程调度机制是系统的核心组成部分之一,它决定了处理器资源如何分配给多个竞争的进程。本文深入探讨了Linux内核中的进程调度策略和相关算法,包括其设计哲学、实现原理及对系统性能的影响。通过分析进程调度器的工作原理,我们能够理解操作系统如何平衡效率、公平性和响应性,进而优化系统表现和用户体验。
13 3
|
7天前
|
监控 Linux Shell
初识Linux下进程2
初识Linux下进程2
|
7天前
|
Linux 编译器 Windows
【Linux】10. 进程地址空间
【Linux】10. 进程地址空间
19 4
|
12天前
|
Web App开发 人工智能 Ubuntu
【Linux】Linux启动/查看/结束进程命令(详细讲解)
【Linux】Linux启动/查看/结束进程命令(详细讲解)
|
17天前
|
Linux Shell 调度
【Linux】进程排队的理解&&进程状态的表述&&僵尸进程和孤儿进程的理解
【Linux】进程排队的理解&&进程状态的表述&&僵尸进程和孤儿进程的理解
|
19天前
|
监控 Linux Shell
Linux 进程问题调查探秘:分析和排查频繁创建进程问题
Linux 进程问题调查探秘:分析和排查频繁创建进程问题
39 0
|
19天前
|
存储 Linux 程序员
【Linux C/C++ 堆内存分布】深入理解Linux进程的堆空间管理
【Linux C/C++ 堆内存分布】深入理解Linux进程的堆空间管理
66 0
|
19天前
|
消息中间件 存储 网络协议
Linux IPC 进程间通讯方式的深入对比与分析和权衡
Linux IPC 进程间通讯方式的深入对比与分析和权衡
65 0
|
19天前
|
存储 算法 Linux
【Linux 系统标准 进程资源】Linux 创建一个最基本的进程所需的资源分析,以及线程资源与之的差异
【Linux 系统标准 进程资源】Linux 创建一个最基本的进程所需的资源分析,以及线程资源与之的差异
24 0
|
24天前
|
Unix Shell Linux
Linux 终端和进程的关系,以及在终端前后台切换进程
Linux 终端和进程的关系,以及在终端前后台切换进程
26 1