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! 

相关文章
|
22天前
|
弹性计算 Linux 区块链
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
30 4
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
|
4天前
|
Linux 调度
Linux源码阅读笔记05-进程优先级与调度策略-实战分析
Linux源码阅读笔记05-进程优先级与调度策略-实战分析
|
4天前
|
Linux API C语言
Linux源码阅读笔记02-进程原理及系统调用
Linux源码阅读笔记02-进程原理及系统调用
|
7天前
|
Linux Shell 调度
【在Linux世界中追寻伟大的One Piece】Linux进程概念
【在Linux世界中追寻伟大的One Piece】Linux进程概念
15 1
|
17天前
|
算法 Linux 调度
探索进程调度:Linux内核中的完全公平调度器
【8月更文挑战第2天】在操作系统的心脏——内核中,进程调度算法扮演着至关重要的角色。本文将深入探讨Linux内核中的完全公平调度器(Completely Fair Scheduler, CFS),一个旨在提供公平时间分配给所有进程的调度器。我们将通过代码示例,理解CFS如何管理运行队列、选择下一个运行进程以及如何对实时负载进行响应。文章将揭示CFS的设计哲学,并展示其如何在现代多任务计算环境中实现高效的资源分配。
|
18天前
|
Python
惊!Python进程间通信IPC,让你的程序秒变社交达人,信息畅通无阻
【8月更文挑战第1天】在编程世界中,进程间通信(IPC)犹如一场社交舞会,各进程通过IPC机制优雅地交换信息,共同完成复杂任务。IPC就像隐形桥梁,连接并行运行的进程,使它们能跨越边界自由沟通。Python提供了多种IPC机制,如管道、队列、共享内存和套接字等,适应不同需求。例如,使用`multiprocessing.Queue`实现进程间通信,生产者向队列添加数据,消费者取出并处理数据,两者虽独立却能有效协作。IPC打破了进程界限,使得程序能像社交达人般自由交流,构建出高效、灵活的应用。掌握IPC,让程序信息畅通无阻。
16 1
|
21天前
|
Java Linux Shell
Linux后台运行jar程序
【7月更文挑战第23天】
|
4天前
|
Oracle Java 关系型数据库
简单记录在Linux上安装JDK环境的步骤,以及解决运行Java程序时出现Error Could not find or load main class XXX问题
本文记录了在Linux系统上安装JDK环境的步骤,并提供了解决运行Java程序时出现的"Error Could not find or load main class XXX"问题的方案,主要是通过重新配置和刷新JDK环境变量来解决。
12 0
|
5天前
|
Linux
Linux 查找进程所在目录
Linux 查找进程所在目录
15 0
|
7天前
|
并行计算 开发者 Python
解锁Python多进程编程的超能力:并行计算的魔法与奇迹,探索处理器核心的秘密,让程序性能飞跃!
【8月更文挑战第12天】在Python编程领域,多进程编程是一项关键技能,能有效提升程序效率。本文通过理论与实践结合,深入浅出地介绍了Python中的多进程编程。首先解释了多进程的概念:即操作系统中能够并发执行的多个独立单元,进而提高整体性能。接着重点介绍了`multiprocessing`模块,演示了如何创建和启动进程,以及进程间的通信方式,如队列等。此外,还提到了更高级的功能,例如进程池管理和同步原语等。通过这些实例,读者能更好地理解如何在实际项目中利用多核处理器的优势,同时注意进程间通信和同步等问题,确保程序稳定高效运行。
19 0