欢迎光临
我们一直在努力

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

个人主页:chian-ocean

文章专栏-Linux

前言:

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

在这里插入图片描述

进程替换原理

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

进程替换的核心是:

  • 清空当前进程的用户态地址空间,包括代码段、数据段、堆、栈等。
  • 加载新程序到当前进程的地址空间,并切换到新程序的入口点执行。
  • 保留进程的内核态资源,如 PID、打开的文件描述符、父子关系等。
  • 如果 exec 调用成功,原进程的代码永远不会被执行。
  • #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; // 父进程正常退出
    }

    执行流程

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

    • 子进程的PID没有变化,发成了进程替换。

    exec系类函数

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

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

    exec 系列函数的成员

    在这里插入图片描述

    L:可以理解list

    V:可以理解Vector

    execl

    int execl(const char *path, const char *arg0, ..., NULL);

    参数说明

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

    execlp

    int execlp(const char *file, const char *arg0, ..., NULL);

    参数说明

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

    execle

    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。

    #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

    int execv(const char *path, char *const argv[]);

    参数说明

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

    #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;
    }

    逐步功能分析

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

    execvp

    int execvp(const char *file, char *const argv[]);

    参数说明

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

    • execv 要求指定程序的完整路径,且不会从环境变量 PATH 中查找。
    • execvp 可以仅提供程序名称,函数会自动从 PATH 中查找程序。

    #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

    int execvpe(const char *file, char *const argv[], char *const envp[]);

    参数说明

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

    #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

    功能分析

  • 父进程输出信息

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

    • 调用

      fork()

      创建子进程:

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

    • envp

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

      • 包括 MY_VAR=HelloWorld 和 PATH=/bin:/usr/bin。
    • argv

      是传递给

      execvpe

      的参数列表:

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

    • 子进程调用

      execvpe("ls", argv, envp)

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

  • 父进程等待子进程完成

    • 调用 waitpid 等待子进程完成。
    • 输出父进程和子进程的 PID 信息。
  • 在这里插入图片描述

    exec 系列函数总结

    函数名称程序路径参数传递环境变量特点
    execl 完整路径 列表传参 继承父进程环境 手动传递每个参数;易用但不适合动态参数数量。
    execlp 搜索 PATH 列表传参 继承父进程环境 在 PATH 中查找程序;适合提供命令名称的情况。
    execle 完整路径 列表传参 自定义环境 与 execl 类似,但支持自定义环境变量。
    execv 完整路径 数组传参 继承父进程环境 参数通过数组传递,适合动态生成参数的情况。
    execvp 搜索 PATH 数组传参 继承父进程环境 在 PATH 中查找程序,适合命令名称和动态参数。
    execve 完整路径 数组传参 自定义环境 底层实现函数;用户可完全控制路径、参数和环境变量。
    execvpe 搜索 PATH 数组传参 自定义环境 GNU 扩展,结合 execvp 和 execve 的优点。
    赞(0)
    未经允许不得转载:171主机测评 » 从理论到实践:Linux 进程替换与 exec 系列函数
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址