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! 

相关文章
|
10天前
|
消息中间件 分布式计算 Java
Linux环境下 java程序提交spark任务到Yarn报错
Linux环境下 java程序提交spark任务到Yarn报错
73 4
|
6月前
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
259 67
|
5月前
|
Web App开发 Linux 程序员
获取和理解Linux进程以及其PID的基础知识。
总的来说,理解Linux进程及其PID需要我们明白,进程就如同汽车,负责执行任务,而PID则是独特的车牌号,为我们提供了管理的便利。知道这个,我们就可以更好地理解和操作Linux系统,甚至通过对进程的有效管理,让系统运行得更加顺畅。
150 16
|
5月前
|
Unix Linux
对于Linux的进程概念以及进程状态的理解和解析
现在,我们已经了解了Linux进程的基础知识和进程状态的理解了。这就像我们理解了城市中行人的行走和行为模式!希望这个形象的例子能帮助我们更好地理解这个重要的概念,并在实际应用中发挥作用。
114 20
|
4月前
|
监控 Shell Linux
Linux进程控制(详细讲解)
进程等待是系统通过调用特定的接口(如waitwaitpid)来实现的。来进行对子进程状态检测与回收的功能。
102 0
|
4月前
|
存储 负载均衡 算法
Linux2.6内核进程调度队列
本篇文章是Linux进程系列中的最后一篇文章,本来是想放在上一篇文章的结尾的,但是想了想还是单独写一篇文章吧,虽然说这部分内容是比较难的,所有一般来说是简单的提及带过的,但是为了让大家对进程有更深的理解与认识,还是看了一些别人的文章,然后学习了学习,然后对此做了总结,尽可能详细的介绍明白。最后推荐一篇文章Linux的进程优先级 NI 和 PR - 简书。
127 0
|
4月前
|
存储 Linux Shell
Linux进程概念-详细版(二)
在Linux进程概念-详细版(一)中我们解释了什么是进程,以及进程的各种状态,已经对进程有了一定的认识,那么这篇文章将会继续补全上篇文章剩余没有说到的,进程优先级,环境变量,程序地址空间,进程地址空间,以及调度队列。
91 0
|
4月前
|
Linux 调度 C语言
Linux进程概念-详细版(一)
子进程与父进程代码共享,其子进程直接用父进程的代码,其自己本身无代码,所以子进程无法改动代码,平时所说的修改是修改的数据。为什么要创建子进程:为了让其父子进程执行不同的代码块。子进程的数据相对于父进程是会进行写时拷贝(COW)。
90 0
|
7月前
|
Linux 数据库 Perl
【YashanDB 知识库】如何避免 yasdb 进程被 Linux OOM Killer 杀掉
本文来自YashanDB官网,探讨Linux系统中OOM Killer对数据库服务器的影响及解决方法。当内存接近耗尽时,OOM Killer会杀死占用最多内存的进程,这可能导致数据库主进程被误杀。为避免此问题,可采取两种方法:一是在OS层面关闭OOM Killer,通过修改`/etc/sysctl.conf`文件并重启生效;二是豁免数据库进程,由数据库实例用户借助`sudo`权限调整`oom_score_adj`值。这些措施有助于保护数据库进程免受系统内存管理机制的影响。
|
7月前
|
存储 Linux 调度
【Linux】进程概念和进程状态
本文详细介绍了Linux系统中进程的核心概念与管理机制。从进程的定义出发,阐述了其作为操作系统资源管理的基本单位的重要性,并深入解析了task_struct结构体的内容及其在进程管理中的作用。同时,文章讲解了进程的基本操作(如获取PID、查看进程信息等)、父进程与子进程的关系(重点分析fork函数)、以及进程的三种主要状态(运行、阻塞、挂起)。此外,还探讨了Linux特有的进程状态表示和孤儿进程的处理方式。通过学习这些内容,读者可以更好地理解Linux进程的运行原理并优化系统性能。
252 4