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! 

相关文章
|
13天前
|
资源调度 Linux 调度
Linux c/c++之进程基础
这篇文章主要介绍了Linux下C/C++进程的基本概念、组成、模式、运行和状态,以及如何使用系统调用创建和管理进程。
26 0
|
8天前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
【10月更文挑战第9天】本文将深入浅出地介绍Linux系统中的进程管理机制,包括进程的概念、状态、调度以及如何在Linux环境下进行进程控制。我们将通过直观的语言和生动的比喻,让读者轻松掌握这一核心概念。文章不仅适合初学者构建基础,也能帮助有经验的用户加深对进程管理的理解。
12 1
|
8天前
|
运维 Java Linux
【运维基础知识】Linux服务器下手写启停Java程序脚本start.sh stop.sh及详细说明
### 启动Java程序脚本 `start.sh` 此脚本用于启动一个Java程序,设置JVM字符集为GBK,最大堆内存为3000M,并将程序的日志输出到`output.log`文件中,同时在后台运行。 ### 停止Java程序脚本 `stop.sh` 此脚本用于停止指定名称的服务(如`QuoteServer`),通过查找并终止该服务的Java进程,输出操作结果以确认是否成功。
14 1
|
13天前
|
消息中间件 Linux API
Linux c/c++之IPC进程间通信
这篇文章详细介绍了Linux下C/C++进程间通信(IPC)的三种主要技术:共享内存、消息队列和信号量,包括它们的编程模型、API函数原型、优势与缺点,并通过示例代码展示了它们的创建、使用和管理方法。
16 0
Linux c/c++之IPC进程间通信
|
13天前
|
Linux C++
Linux c/c++进程间通信(1)
这篇文章介绍了Linux下C/C++进程间通信的几种方式,包括普通文件、文件映射虚拟内存、管道通信(FIFO),并提供了示例代码和标准输入输出设备的应用。
15 0
Linux c/c++进程间通信(1)
|
13天前
|
Linux C++
Linux c/c++之进程的创建
这篇文章介绍了在Linux环境下使用C/C++创建进程的三种方式:system函数、fork函数以及exec族函数,并展示了它们的代码示例和运行结果。
17 0
Linux c/c++之进程的创建
|
1月前
|
消息中间件 分布式计算 Java
Linux环境下 java程序提交spark任务到Yarn报错
Linux环境下 java程序提交spark任务到Yarn报错
34 5
|
1月前
|
Linux Shell
6-9|linux查询现在运行的进程
6-9|linux查询现在运行的进程
|
13天前
|
Linux C++
Linux c/c++进程之僵尸进程和守护进程
这篇文章介绍了Linux系统中僵尸进程和守护进程的概念、产生原因、解决方法以及如何创建守护进程。
15 0
|
16天前
|
安全 API C#
C# 如何让程序后台进程不被Windows任务管理器强制结束
C# 如何让程序后台进程不被Windows任务管理器强制结束
34 0