欢迎光临
我们一直在努力

Re:Linux 系统篇(二十):进程篇(九):进程替换 exec 全家桶 —— 从原理到 6 个接口逐行拆解,手把手实现程序替换

在这里插入图片描述

观众老爷们大家好 这里是邪修KING的独家频道

本文属于系列Linux系统篇 ——操作指令

一起学Linux的小伙伴可订阅专栏: Linux系统篇

上一篇我们讲透了 fork() 分身术:调用一次,分裂出两个独立进程。但 fork 有个局限:子进程只能复制父进程的代码和数据,跑和父进程一样的逻辑。 如果想让子进程跑一个全新的程序呢?比如执行 ls 命令、跑 python 脚本、启动另一个程序? 这就需要 Linux 系统编程第二大利器:exec 系列程序替换函数。 本篇我们从替换原理讲起,拆解 6 个 exec 函数的命名规律、参数含义、使用场景,最后结合 fork 写出完整的程序替换实战代码,讲透所有坑点。


一、什么是进程程序替换?

1.1 本质:换代码,不换进程

很多人以为 exec 是创建新进程,完全不是。

程序替换的核心:进程 PID 不变,PCB 保留,只把用户态的代码段、数据段,全部替换成新程序(ELF 文件)的内容,重新初始化栈、堆,然后从新程序的入口开始执行。

一句话总结:还是原来那个进程,只是里面跑的代码换成别的程序了。

1.2 核心结构变化

  • ✅ 不变:PID、PPID、PCB、打开的文件描述符、信号处理方式、进程优先级
  • ❌ 全部替换:代码段、数据段、堆、栈、指令指针 PC

替换之后,进程从新程序的 _start 开始执行,原来的代码彻底消失,不会再继续往下走。

1.3 为什么 fork + exec 是黄金搭档?

  • fork:创建子进程,拥有独立地址空间
  • exec:子进程里执行替换,只改子进程自己的代码数据,父进程完全不受影响

这就是 shell 的工作原理:

  • 你输入命令,shell 调用 fork 创建子进程
  • 子进程调用 exec,把自己替换成你输入的命令程序
  • 父进程 shell 等待子进程退出,退出后继续等待下一条命令
  • 💡 为什么不直接 exec 替换自己? 替换了 shell 自己,命令跑完 shell 就没了。所以必须 fork 出子进程来替换,父进程 shell 保留,继续接收命令。


    二、exec 系列函数命名规律(核心记忆法)

    exec 一共 6 个常用函数,不用死记硬背,名字里的每个字母都有明确含义,掌握规律一眼就能看懂。

    表格

    后缀字母全称含义
    l list 参数用列表形式传递,逐个写参数,最后以 NULL 结尾
    v vector 参数用数组形式传递,把所有参数放进数组,传数组首地址
    p path 自动在 PATH 环境变量里搜索可执行文件,不用写全路径
    e environment 传入自定义环境变量,不继承父进程的环境变量表

    6 个函数就是这几个特征的组合:

    • l / v 二选一:参数怎么传
    • p 可选:要不要自动搜 PATH
    • e 可选:要不要自定义环境变量

    底层真相: 只有 execve 是真正的系统调用,剩下 5 个都是 C 标准库封装的库函数,最终都会调用 execve。


    三、逐个接口详解:原型 + 参数 + 场景

    3.1 execl:列表传参,需写全路径

    函数原型

    #include <unistd.h>

    int execl(const char *path, const char *arg, ...);

    参数说明
  • path:可执行文件的完整路径,必须写全,不会自动搜索
  • arg:命令行参数列表,第一个参数约定是程序名本身,后面依次传参数
  • 最后必须以 NULL 结尾,标记参数列表结束
  • 适用场景

    知道程序的完整路径,参数不多,逐个写比较清晰。

    示例

    // 执行 /usr/bin/ls -l -a
    execl("/usr/bin/ls", "ls", "-l", "-a", NULL);


    3.2 execlp:列表传参,自动搜索 PATH

    函数原型

    int execlp(const char *file, const char *arg, ...);

    参数说明
  • file:可执行文件名,不需要写全路径,系统自动去 PATH 环境变量的目录里查找
  • 后面参数和 execl 一样,列表形式,NULL 结尾
  • 适用场景

    执行系统自带命令(ls、cat、gcc),不用找路径,直接写命令名。

    示例

    // 执行 ls -l -a,自动在 PATH 里找 ls
    execlp("ls", "ls", "-l", "-a", NULL);

    💡 注意:第二个参数 ls 是传给新程序的 argv [0],和第一个参数文件名可以不一样,但约定写成一样的。


    3.3 execle:列表传参,自定义环境变量

    函数原型

    int execle(const char *path, const char *arg, ..., char *const envp[]);

    参数说明
  • path:可执行文件全路径
  • arg…:参数列表,NULL 结尾
  • envp[]:自定义环境变量数组,格式为 KEY=VALUE 字符串,最后 NULL 结尾。新程序只会拿到这个数组里的环境变量,不继承父进程的。
  • 适用场景

    需要给新程序单独设置环境变量,隔离父进程环境。

    示例

    char *my_env[] = {
    "PATH=/usr/bin",
    "USER=test",
    NULL
    };

    // 执行 ls,只传入自定义环境变量
    execle("/usr/bin/ls", "ls", "-l", NULL, my_env);


    3.4 execv:数组传参,需写全路径

    函数原型

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

    参数说明
  • path:可执行文件全路径
  • argv[]:参数数组,第一个是程序名,后面依次是参数,最后 NULL 结尾
  • 适用场景

    参数多、或者参数是动态生成的,放数组里更方便。

    示例

    char *const argv[] = {
    "ls",
    "-l",
    "-a",
    NULL
    };

    execv("/usr/bin/ls", argv);


    3.5 execvp:数组传参,自动搜索 PATH

    函数原型

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

    参数说明
  • file:可执行文件名,自动搜 PATH
  • argv[]:参数数组
  • 适用场景

    动态拼接参数,又不想写全路径,是最常用的组合之一。

    示例

    char *const argv[] = {
    "ls",
    "-l",
    "-a",
    NULL
    };

    execvp("ls", argv);


    3.6 execve:系统调用本尊

    函数原型

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

    参数说明
  • path:可执行文件全路径
  • argv[]:参数数组
  • envp[]:自定义环境变量数组
  • 这是最完整的一个,也是真正的系统调用。前面 5 个库函数,内部最终都是调用它。

    适用场景

    需要完全控制参数和环境变量,或者想直接调用系统调用。


    四、完整实战:fork + exec 程序替换

    4.1 完整代码 proc.c

    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>

    int main()
    {
    printf("我的程序要运行了!\\n");

    pid_t pid = fork();

    if (pid == 0)
    {
    // ========== 子进程:执行程序替换 ==========
    printf("[子进程] pid=%d,准备替换成 ls 程序\\n", getpid());
    sleep(1);

    // 方式1:execl 列表传参 + 全路径
    // execl("/usr/bin/ls", "ls", "-ln", "-a", NULL);

    // 方式2:execv 数组传参 + 全路径(推荐写法)
    char *const argv[] = {
    "ls",
    "-l",
    "-a",
    NULL
    };
    execv("/usr/bin/ls", argv);

    // ===== 注意:下面的代码只有 exec 失败才会执行 =====
    // 成功的话,代码已经被替换,永远走不到这里
    perror("exec 失败");
    exit(1);
    }
    else
    {
    // ========== 父进程:等待子进程 ==========
    waitpid(1, NULL, 0);
    printf("我的程序运行完毕了\\n");
    }

    return 0;
    }

    4.2 编译运行

    gcc proc.c o proc
    ./proc

    4.3 运行结果

    我的程序要运行了!
    [子进程] pid=xxxx,准备替换成 ls 程序
    总用量 xx
    drwxrwxr-x ...
    ...(ls 命令的输出)
    我的程序运行完毕了

    4.4 逐行理解

  • 父进程 fork 出子进程
  • 子进程调用 execv,把自己的代码替换成 ls 程序
  • 替换成功后,子进程开始执行 ls 的代码,输出目录内容
  • ls 执行完,子进程退出
  • 父进程 wait 回收,打印结束提示
  • 💡 关键观察: exec 之后,子进程还是那个 PID,但跑的已经是 ls 的代码了。原来子进程里 exec 后面的代码,只要替换成功就永远不会执行。


    五、核心特性与常见坑点

    5.1 返回值:成功不返回,失败才返回

    这是最反直觉的一点:

    exec 系列函数,成功了就不会返回了,直接跑新程序去了。 只有出错的时候,才会返回 -1,并且设置 errno 错误码。

    所以写法一定是:

    execxxx(…);
    // 走到这里一定是失败了
    perror("exec fail");
    exit(1);

    不要写 if(execxxx() > 0) 这种判断,成功根本不会回来。

    5.2 参数列表必须以 NULL 结尾

    不管是 l 系列的列表,还是 v 系列的数组,最后必须加 NULL,告诉系统参数传完了。 不加 NULL 会导致越界访问,结果不可预期。

    5.3 替换后原代码全部失效

    exec 成功后,原进程后面的所有代码都被覆盖了,不会执行。 所以错误处理、清理工作一定要写在 exec 之前,或者失败分支里。

    5.4 环境变量继承规则

    • 不带 e 的函数(execl、execlp、execv、execvp):继承父进程的全部环境变量
    • 带 e 的函数(execle、execve):只用你传入的环境变量,完全不继承父进程

    六、6 个函数对比总结表

    表格

    函数名传参方式自动搜 PATH环境变量本质推荐场景
    execl 列表 ❌ 要写全路径 继承父进程 库函数 参数少、知道完整路径
    execlp 列表 ✅ 自动搜索 继承父进程 库函数 执行系统命令、参数少
    execle 列表 ❌ 要写全路径 ✅ 自定义 库函数 需要自定义环境变量
    execv 数组 ❌ 要写全路径 继承父进程 库函数 参数多、动态参数
    execvp 数组 ✅ 自动搜索 继承父进程 库函数 最常用,动态参数 + 系统命令
    execve 数组 ❌ 要写全路径 ✅ 自定义 系统调用 底层完全控制

    全文总结

  • 程序替换本质:不创建新进程,PID 不变,替换代码段数据段,跑新程序。
  • 命名规律:l 列表、v 数组、p 搜 PATH、e 自定义环境,组合出 6 个函数。
  • 返回值特性:成功不返回,失败返回 – 1;exec 后面的代码只有失败才执行。
  • 黄金搭档:fork 创建子进程,子进程 exec 替换,父进程 wait 回收,这就是 shell 的核心原理。
  • 底层真相:只有 execve 是系统调用,其他都是库函数封装。
  • 在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » Re:Linux 系统篇(二十):进程篇(九):进程替换 exec 全家桶 —— 从原理到 6 个接口逐行拆解,手把手实现程序替换
    分享到: 更多 (0)

    评论 抢沙发

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