从理论到实践:Linux 进程替换与 exec 系列函数

个人主页:chian-ocean

文章专栏-Linux

前言:

在Linux中,进程替换(Process Substitution)是一个非常强大的特性,它允许将一个进程的输出直接当作一个文件来处理。这种技术通常用于Shell脚本和命令行操作中。

进程替换原理

进程替换(Process Replacement)是操作系统用来用一个新程序完全替换当前进程用户态内容的机制,其本质是清空当前进程的用户态内容并加载新程序,同时保留内核态资源(如 PID、文件描述符等)。它通过 exec 系列系统调用实现,以下是进程替换的详细原理。

进程替换的核心是:

  1. 清空当前进程的用户态地址空间,包括代码段、数据段、堆、栈等。
  2. 加载新程序到当前进程的地址空间,并切换到新程序的入口点执行。
  3. 保留进程的内核态资源,如 PID、打开的文件描述符、父子关系等。
  4. 如果 exec 调用成功,原进程的代码永远不会被执行。
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <cstdlib>  

using namespace std;

int main() {
    // 输出当前进程的 PID(进程 ID)和 PPID(父进程 ID)
    cout << "I'm a process: " << "PID: " << getpid() << " PPID: " << getppid() << endl;

    // 调用 fork() 创建子进程
    pid_t id = fork();

    // 子进程逻辑
    if (id == 0) {
        
        cout <<"Child PID: "<< getpid() << endl;//打印子进程的PID
        // 使用 execl() 替换当前子进程为 /usr/bin/ls 程序
        // 第一个参数是程序路径,第二个参数是程序名称(通常为 argv[0]),后面是命令行参数
        execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
        
        // 如果 execl() 执行失败(例如文件不存在),会执行以下代码
        perror("execl failed"); // 输出错误信息
        exit(1); // 子进程以退出码 1 结束
    }
    // 父进程逻辑
    // 使用 waitpid() 等待子进程结束
    int ret = waitpid(id, NULL, 0); // 第二个参数为 NULL,表示忽略子进程的退出状态
    if (ret > 0) {
        // 如果 waitpid() 成功返回,表示子进程已结束
        cout << "Father PID: " << getpid() << " " << "Child PID: " << ret << endl;

    return 0; // 父进程正常退出
}

执行流程

  1. 程序开始
    • 父进程运行,打印自己的 PID 和 PPID(错误地显示 PPID 为自己的 PID)。
  2. 创建子进程
    • fork 创建一个子进程。
  3. 子进程执行 execl
    • 子进程替换为 /usr/bin/ls 程序,并执行 ls -l -a 命令,列出当前目录中所有文件(包括隐藏文件)的详细信息。
    • 如果 execl 成功,子进程的地址空间完全被 ls 程序覆盖。
    • 如果 execl 失败,执行 exit(1),子进程退出,返回码为 1
  4. 父进程等待子进程
    • 父进程调用 waitpid,阻塞等待子进程终止。
    • 当子进程完成后,waitpid 返回子进程的 PID。
  5. 父进程打印结果
    • 父进程输出自己的 PID 和已终止的子进程的 PID。
  • 子进程的PID没有变化,发成了进程替换。

exec系类函数

exec 系列函数是 UNIX/Linux 系统中用于进程替换 的函数集合。通过 exec 系列函数,当前进程的用户态内容(如代码段、数据段、堆、栈等)会被新程序替换,而进程的内核态资源(如 PID、打开的文件描述符等)被保留。

exec 系列函数不创建新进程,只是在当前进程中加载并运行一个新程序。

exec 系列函数的成员

L:可以理解list

V:可以理解Vector

execl

cpp 复制代码
int execl(const char *path, const char *arg0, ..., NULL);

参数说明

  1. path
    • 新程序的文件路径(可以是绝对路径或相对路径)。
    • /bin/ls./myprogram
  2. arg0, ..., NULL
    • 传递给新程序的参数列表,按照顺序传递给新程序的 argv 数组。
    • arg0 通常是程序名,相当于 argv[0]
    • 后续的参数是传递给新程序的命令行参数,相当于 argv[1], argv[2], ...
    • 参数列表必须以 NULL 结束。
  3. 示例:
cpp 复制代码
execl("/bin/ls", "ls", "-l", "-a", NULL);

execlp

cpp 复制代码
int execlp(const char *file, const char *arg0, ..., NULL);

参数说明

  1. file
    • 新程序的文件名。
    • 如果 file 不包含斜杠(/),execlp 会根据 PATH 环境变量搜索可执行文件。
    • 如果 file 包含斜杠,则直接视为路径,无需搜索 PATH
  2. arg0, ..., NULL
    • 传递给新程序的参数列表,必须以 NULL 结束。
    • arg0 通常是程序名,相当于 argv[0]
    • 后续参数为程序的命令行参数,相当于 argv[1]argv[2] 等。
  3. 示例
cpp 复制代码
execlp("ls", "ls", "-l", "-a", NULL);

execle

cpp 复制代码
int execle(const char *path, const char *arg0, ..., NULL, char *const envp[]);

参数说明:

path

  • 新程序的文件路径,可以是绝对路径或相对路径。
  • /bin/ls./myprogram

arg0, ..., NULL

  • 传递给新程序的参数列表,必须以 NULL 结束。
  • arg0 通常是程序名,相当于 argv[0]
  • 后续参数为程序的命令行参数,相当于 argv[1], argv[2], ...

envp

  • 一个指向环境变量字符串数组的指针。
  • 每个环境变量字符串的格式为 key=value(例如,PATH=/usr/bin)。
  • 如果希望新程序继承当前进程的环境变量,可以手动传递当前进程的 environ
cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
  
using namespace std;
  
int main()    
{     
      cout << "I'm a process: "<<"PID:"<<getpid()<< " PPID: "<< getpid()<< endl;    
      pid_t id = fork();    
      char *envp[] = {    
        "MY_VAR=HelloWorld",    
        "PATH=/bin:/usr/bin",    
          NULL    
      };     
      if(id == 0)    
      {    
          cout <<"Child PID: "<< getpid() << endl;    
          execle("/usr/bin/env","env",NULL,envp);                                  
          exit(1);    
      }    
      int ret = waitpid(id,NULL,0);    
      if(ret > 0)    
      cout << "Father PID: "<<getpid()<< " " <<"Child PID: "<< ret << endl;    
      
      
      return 0;    
}

execv

cpp 复制代码
int execv(const char *path, char *const argv[]);

参数说明

  1. path : 指向可执行文件路径的字符串(以 \0 结尾)。
  2. argv : 一个字符串指针数组,用于传递给新程序的参数列表。数组的第一个元素通常为程序名称(argv[0]),最后一个元素必须为 NULL,以标记参数列表结束。

示例:

cpp 复制代码
#include<iostream> 
#include<unistd.h> 
#include<stdlib.h> 
#include<sys/wait.h> 。
#include<sys/types.h> 

int main()
{
    // 输出当前进程的 PID(进程 ID)和 PPID(父进程 ID)
    std::cout << "I'm a process: "
              << "PID:" << getpid() 
              << " PPID: " << getppid() << std::endl;
    // 创建子进程
    pid_t id = fork();
    // 定义一个字符指针数组,用于存储传递给 `execv` 的参数
    char *argv[] = {    
        "ls",    // argv[0]: 通常是程序名称
        "-l",    // argv[1]: 参数,表示以长格式列出文件
        "-a",    // argv[2]: 参数,显示隐藏文件
        NULL     // 终止符,必须为 NULL
    };
    if(id == 0) // 子进程执行的代码块
    {    
        // 子进程输出自己的 PID
        std::cout << "Child PID: " << getpid() << std::endl; 
        // 用 execv 替换当前进程的执行映像
        execv("/usr/bin/ls", argv);

        // 如果 execv 返回,说明执行失败
        exit(1); // 退出子进程,返回非零值表示错误
    }
    // 父进程等待子进程完成
    int ret = waitpid(id, NULL, 0);
    if(ret > 0) // 如果 `waitpid` 成功返回
        std::cout << "Father PID: " << getpid() 
                  << " " << "Child PID: " << ret 
                  << std::endl;
    return 0;
}

逐步功能分析

  1. 主进程输出信息
    使用 getpid()getppid() 分别获取当前进程 ID 和父进程 ID,并输出信息。
  2. 创建子进程
    使用 fork() 创建一个子进程:
    • 返回值 id == 0:表示当前是子进程。
    • 返回值 id > 0:表示当前是父进程,id 为子进程的 PID。
  3. 子进程执行新程序
    在子进程中调用 execv
    • 替换当前进程映像为 /usr/bin/ls
    • 参数数组 argv 指定了程序名称和选项。
    • 如果 execv 成功,后续代码不会执行;否则会继续执行并调用 exit(1) 终止子进程。
  4. 父进程等待子进程
    父进程调用 waitpid
    • 阻塞当前进程,直到子进程终止。
    • 返回值 ret 是子进程的 PID。
  5. 父进程输出信息
    输出父进程和子进程的 PID 信息。

execvp

cpp 复制代码
int execvp(const char *file, char *const argv[]);

参数说明

  1. file
    • 要执行的程序名称或路径。
    • 如果提供的是程序名称(非路径),execvp 会根据环境变量 PATH 中的目录列表查找该程序。
  2. argv
    • 一个字符串数组,表示传递给新程序的参数。
    • argv[0] 通常是程序名称,最后一个元素必须为 NULL

execvpexecv 的区别

  • execv
    要求指定程序的完整路径,且不会从环境变量 PATH 中查找。
  • execvp
    可以仅提供程序名称,函数会自动从 PATH 中查找程序。
cpp 复制代码
#include<iostream> 
#include<unistd.h> 
#include<stdlib.h> 
#include<sys/wait.h> 。
#include<sys/types.h> 

int main()
{
    // 输出当前进程的 PID(进程 ID)和 PPID(父进程 ID)
    std::cout << "I'm a process: "
              << "PID:" << getpid() 
              << " PPID: " << getppid() << std::endl;
    // 创建子进程
    pid_t id = fork();
    // 定义一个字符指针数组,用于存储传递给 `execv` 的参数
    char *argv[] = {    
        "ls",    // argv[0]: 通常是程序名称
        "-l",    // argv[1]: 参数,表示以长格式列出文件
        "-a",    // argv[2]: 参数,显示隐藏文件
        NULL     // 终止符,必须为 NULL
    };
    if(id == 0) // 子进程执行的代码块
    {    
        // 子进程输出自己的 PID
        std::cout << "Child PID: " << getpid() << std::endl; 
        // 用 execvp 替换当前进程的执行映像
        execvp("ls", argv); // 区别于execv

        // 如果 execv 返回,说明执行失败
        exit(1); // 退出子进程,返回非零值表示错误
    }
    // 父进程等待子进程完成
    int ret = waitpid(id, NULL, 0);
    if(ret > 0) // 如果 `waitpid` 成功返回
        std::cout << "Father PID: " << getpid() 
                  << " " << "Child PID: " << ret 
                  << std::endl;
    return 0;
}

ecexvpe

cpp 复制代码
int execvpe(const char *file, char *const argv[], char *const envp[]);

参数说明

  1. file
    • 要执行的程序名称或路径。
    • 如果提供的是程序名称,execvpe 会根据环境变量 PATH 自动查找该程序。
  2. argv
    • 一个字符串数组,用于传递给新程序的参数。
    • argv[0] 通常是程序的名称,最后一个元素必须是 NULL
  3. envp
    • 一个字符串数组,用于指定新程序的环境变量。
    • 每个字符串的格式为 KEY=VALUE,例如 "PATH=/usr/bin"
    • 最后一个元素必须为 NULL

示例

cpp 复制代码
#include<iostream>  
#include<unistd.h>   
#include<stdlib.h>   
#include<sys/wait.h> 
#include<sys/types.h>

using namespace std;

int main()                                                                    
{                     
    // 输出当前进程的 PID 和父进程 ID(PPID)
    cout << "I'm a process: "
         << "PID:" << getpid() 
         << " PPID: " << getpid() << endl;

    // 创建子进程
    pid_t id = fork();      

    // 自定义环境变量数组
    char *envp[] = {
        "MY_VAR=HelloWorld", // 自定义变量 MY_VAR,值为 "HelloWorld"
        "PATH=/bin:/usr/bin", // 自定义 PATH,确保能找到可执行文件
        NULL                 // 终止标志
    };       

    // 命令参数数组,传递给 `ls` 命令
    char *argv[] = {
        "ls",   // argv[0] 通常为程序名称
        "-l",   // 参数:长格式输出
        "-a",   // 参数:显示隐藏文件
        NULL    // 终止标志
    };                                          

    if(id == 0) // 子进程
    {
        cout <<"Child PID: " << getpid() << endl;

        // 使用 execvpe 执行 ls 命令,传递自定义环境变量
        execvpe("ls", argv, envp);

        // 如果 execvpe 执行失败
        exit(1); // 退出子进程,返回非零值表示错误
    }                                                                    

    // 父进程等待子进程完成
    int ret = waitpid(id, NULL, 0);
    if(ret > 0) // 如果子进程正常退出
        cout << "Father PID: " << getpid() 
             << " " << "Child PID: " << ret << endl;

    return 0

功能分析

  1. 父进程输出信息

    • 使用 getpid() 获取当前进程的 ID。
    • 使用 getpid() 显示父进程的 PPID(此处写错,正确用法应是 getppid())。
  2. 创建子进程

    • 调用

      复制代码
      fork()

      创建子进程:

      • 返回值 id == 0:表示当前是子进程。
      • 返回值 id > 0:表示当前是父进程,id 为子进程的 PID。
  3. 定义环境变量和参数

    复制代码
      envp

    是自定义的环境变量数组:

    • 包括 MY_VAR=HelloWorldPATH=/bin:/usr/bin
    复制代码
      argv

    是传递给

    复制代码
      execvpe

    的参数列表:

    • 包括 ls 命令及其参数 -l-a
  4. 子进程执行新程序

    • 子进程调用

      复制代码
      execvpe("ls", argv, envp)
      • 替换当前子进程的映像为 ls 命令。
      • 使用自定义的环境变量。
    • 如果 execvpe 失败,子进程调用 exit(1) 退出。

  5. 父进程等待子进程完成

    • 调用 waitpid 等待子进程完成。
    • 输出父进程和子进程的 PID 信息。

exec 系列函数总结

函数名称 程序路径 参数传递 环境变量 特点
execl 完整路径 列表传参 继承父进程环境 手动传递每个参数;易用但不适合动态参数数量。
execlp 搜索 PATH 列表传参 继承父进程环境 PATH 中查找程序;适合提供命令名称的情况。
execle 完整路径 列表传参 自定义环境 execl 类似,但支持自定义环境变量。
execv 完整路径 数组传参 继承父进程环境 参数通过数组传递,适合动态生成参数的情况。
execvp 搜索 PATH 数组传参 继承父进程环境 PATH 中查找程序,适合命令名称和动态参数。
execve 完整路径 数组传参 自定义环境 底层实现函数;用户可完全控制路径、参数和环境变量。
execvpe 搜索 PATH 数组传参 自定义环境 GNU 扩展,结合 execvpexecve 的优点。
相关推荐
Gofarlic_OMS14 小时前
Windchill的license合规使用报告自动化生成与审计追踪系统
大数据·运维·人工智能·云原生·自动化·云计算
迷途之人不知返14 小时前
shell相关知识与Linux权限
linux
SPC的存折14 小时前
3、主从复制实现同步数据过滤
linux·运维·服务器
SPC的存折14 小时前
openEuler 24.03 MariaDB Galera 集群部署指南(cz)
linux·运维·服务器·数据库·mysql
xcbrand14 小时前
文旅行业品牌策划公司找哪家
大数据·运维·人工智能·python
SPC的存折14 小时前
MySQL 8.0 分库分表
linux·运维·服务器·数据库·mysql
风吹迎面入袖凉14 小时前
【Redis】Redisson分布式锁原理
java·服务器·开发语言
cyber_两只龙宝14 小时前
【Oracle】Oracle之DQL中WHERE限制条件查询
linux·运维·数据库·云原生·oracle
斌味代码15 小时前
Shell 性能监控:指标采集、告警规则与可视化大盘设计
运维
22信通小白15 小时前
USRP初学者使用手册(基础配置及bug记录)——Linux+Clion(单台X310收发)
linux·运维·c++·5g·bug·信息与通信