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! 

相关文章
|
14天前
|
安全 Linux Shell
Linux上执行内存中的脚本和程序
【9月更文挑战第3天】在 Linux 系统中,可以通过多种方式执行内存中的脚本和程序:一是使用 `eval` 命令直接执行内存中的脚本内容;二是利用管道将脚本内容传递给 `bash` 解释器执行;三是将编译好的程序复制到 `/dev/shm` 并执行。这些方法虽便捷,但也需谨慎操作以避免安全风险。
|
21天前
|
网络协议 Linux
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
99 2
|
21天前
|
Linux Python
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
39 2
|
7天前
|
Python
惊!Python进程间通信IPC,让你的程序秒变社交达人,信息畅通无阻
【9月更文挑战第13天】在编程的世界中,进程间通信(IPC)如同一场精彩的社交舞会,每个进程通过优雅的IPC机制交换信息,协同工作。本文将带你探索Python中的IPC奥秘,了解它是如何让程序实现无缝信息交流的。IPC如同隐形桥梁,连接各进程,使其跨越边界自由沟通。Python提供了多种IPC机制,如管道、队列、共享内存及套接字,适用于不同场景。通过一个简单的队列示例,我们将展示如何使用`multiprocessing.Queue`实现进程间通信,使程序如同社交达人般高效互动。掌握IPC,让你的程序在编程舞台上大放异彩。
12 3
|
22天前
|
数据采集 监控 API
如何监控一个程序的运行情况,然后视情况将进程杀死并重启
这篇文章介绍了如何使用Python的psutil和subprocess库监控程序运行情况,并在程序异常时自动重启,包括多进程通信和使用日志文件进行断点重续的方法。
|
23天前
|
消息中间件 Linux
Linux进程间通信
Linux进程间通信
32 1
|
5天前
|
存储 监控 安全
探究Linux操作系统的进程管理机制及其优化策略
本文旨在深入探讨Linux操作系统中的进程管理机制,包括进程调度、内存管理以及I/O管理等核心内容。通过对这些关键组件的分析,我们将揭示它们如何共同工作以提供稳定、高效的计算环境,并讨论可能的优化策略。
12 0
|
18天前
|
Unix Linux
linux中在进程之间传递文件描述符的实现方式
linux中在进程之间传递文件描述符的实现方式
|
19天前
|
开发者 API Windows
从怀旧到革新:看WinForms如何在保持向后兼容性的前提下,借助.NET新平台的力量实现自我进化与应用现代化,让经典桌面应用焕发第二春——我们的WinForms应用转型之路深度剖析
【8月更文挑战第31天】在Windows桌面应用开发中,Windows Forms(WinForms)依然是许多开发者的首选。尽管.NET Framework已演进至.NET 5 及更高版本,WinForms 仍作为核心组件保留,支持现有代码库的同时引入新特性。开发者可将项目迁移至.NET Core,享受性能提升和跨平台能力。迁移时需注意API变更,确保应用平稳过渡。通过自定义样式或第三方控件库,还可增强视觉效果。结合.NET新功能,WinForms 应用不仅能延续既有投资,还能焕发新生。 示例代码展示了如何在.NET Core中创建包含按钮和标签的基本窗口,实现简单的用户交互。
43 0
|
21天前
|
Linux Windows Python
最新 Windows\Linux 后台运行程序注解
本文介绍了在Windows和Linux系统后台运行程序的方法,包括Linux系统中使用nohup命令和ps命令查看进程,以及Windows系统中通过编写bat文件和使用PowerShell启动隐藏窗口的程序,确保即使退出命令行界面程序也继续在后台运行。