跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C

Linux 进程程序替换原理与 exec 系列函数详解

综述由AI生成Linux 进程程序替换通过 exec 系列系统调用实现,核心在于用磁盘上的新程序覆盖当前进程的地址空间。文章详解了 fork 后子进程如何独立运行新代码,对比了 execl、execv 等六个库函数与底层 execve 的区别,并演示了命令行参数与环境变量的传递技巧,包括自定义环境变量表的使用及跨语言程序调用场景。

全栈工匠发布于 2026/3/15更新于 2026/6/918 浏览
Linux 进程程序替换原理与 exec 系列函数详解

一、进程程序替换

之前我们讲过 fork() 之后,父子进程各自执行父进程代码的一部分,也就是代码共享,数据默认也'共享',但是发生写入后就会以写时拷贝各自私有。那如果子进程想执行一个全新的程序成为一个真正独立的进程呢?这就需要通过进程的程序替换来完成这个功能!

程序替换是通过特定的系统调用接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中。

替换原理

进程替换原理很简单,就是进程调用某种系统调用,从磁盘中加载一份全新的代码和数据到该进程物理内存中,覆盖掉原进程在内存中的代码和数据。程序替换并没有创建新进程,只是改变了进程的物理内存。

在这里插入图片描述

理论讲得再多不如直接上手,我们先看个例子,看看程序替换的效果,之后再回头结合原理讲解。

进程替换需要调用 exec(注意不是 excel 表格)系列接口,一共有六个,还有一个接口我们后面补充:

在这里插入图片描述

我们先看最简单的 execl:

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

我们要执行一个程序首先要找到它,第一个参数就是用来帮助我们找到它,第二个参数是我们要执行程序的程序名,三个点表示可变参数,可填可不填,如果要填这部分参数指的是给程序传递的命令行选项,并且该部分参数传递完毕后必须以 NULL 结尾。

传递参数注意事项:除了 path 外,后面的参数你在命令行中怎么写,就在这里怎么传递。

下面直接上示例:

#include <stdio.h>
#include <unistd.h>

int main() {
    printf("我是一个进程:%d\n", getpid());
    sleep(1);
    execl("/usr/bin/ls", "ls", "-a", "-l", "-n", NULL);
    printf("运行结束\n");
     ;
}
return
10

运行结果:

在这里插入图片描述

我们看到 execl 替换后的执行结果和 ls 命令一样,说明这样确实就可以让这个程序不执行自己的代码和数据,转而去执行 ls 的代码和数据。

但是这里还有个现象,替换后 printf("运行结束\n"); 这条代码为什么没有运行了呢?很容易理解,因为你的程序替换后开始执行另一个程序的代码了,你自己的代码已经被覆盖了。所以程序替换一旦成功,后续代码不再执行,因为没有了!

那既然程序替换有成功,那也一定有失败,我们下面直接让程序替换失败来看现象:(执行一个不存在的指令就会失败)

#include <stdio.h>
#include <unistd.h>

int main() {
    printf("我是一个进程:%d\n", getpid());
    sleep(1);
    int n = execl("/usr/bin/lllls", "ls", "-a", "-l", "-n", NULL);
    printf("运行结束,n = %d\n", n);
    return 10;
}

运行结果:

在这里插入图片描述

我们可以看到程序替换失败后后续代码还会正常执行,所以一旦程序替换后续的代码被执行了,就表示程序替换失败。

我们还可以看到替换失败后 execl 返回 -1,那如果替换成功还需要返回值吗?我们仔细想想,程序替换成功后 execl 的返回值就没有意义了,就算有后续代码也不会执行。所以程序替换如果成功,不需要、也不会有返回值!——>所以 execl 系列函数,一旦返回,必然失败!

所以我们的代码应该这样写:

#include <stdio.h>
#include <unistd.h>

int main() {
    printf("我是一个进程:%d\n", getpid());
    sleep(1);
    execl("/usr/bin/ls", "ls", "-a", "-l", "-n", NULL);
    printf("程序替换失败!\n");
    return 10;
}

子进程程序替换示例

有了上面的认识,我们再回过头看程序替换理论,前面都是替换当前进程的代码和数据,但我们一开始介绍程序替换概念的时候说程序替换是用来让子进程执行全新的代码的,所以接下来将介绍子进程是如何程序替换。

在开始编写代码之前,我们要先理解一些概念,子进程确实可以被替换,那么子进程替换后会影响父进程吗?我们知道进程之间具有独立性一定不会影响,但是先前不是讲的父子共用同一段代码吗?子进程替换数据我们知道会发生写时拷贝,其实子进程进行代码替换时操作系统也会进行类似写时拷贝的工作。

所以当子进程进行程序替换时,会把子进程的代码和数据加载进内存,而此时父子进程共享代码和数据,所以就会发生写时拷贝,系统会为子进程开辟新的物理内存,子进程的代码和数据就会加载进物理内存中,这样就保证了进程的独立性。

下面我们直接上代码,注意子进程替换后它本质还是那个子进程,当子进程执行完替换后的程序退出时也需要父进程来等待回收它。

#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) {
        // 子进程
        sleep(2);
        printf("我是一个进程:%d\n", getpid());
        sleep(1);
        execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
        exit(1);
    }
    // 父进程
    pid_t rid = waitpid(id, NULL, 0);
    if (rid > 0) {
        printf("wait: %d success\n", rid);
    }
    return 0;
}

加载器

这里再补充一些和历史知识的勾连,我们知道要运行一个二进制文件要先把它加载到内存,这是冯诺依曼体系结构规定的,因为 CPU 不能直接访问外设,只有加载进内存的代码和数据才能被 CPU 执行。加载是把数据从一个硬件加载到另一个硬件,所以一定需要操作系统来执行加载任务,只有操作系统有这个权力,所以加载底层一定会调用系统调用,那么对于 Linux 而言,加载的本质其实就是调用程序替换的系统调用接口。

(很多人会疑惑:为什么 Linux 不能像 Windows 一样'直接创建进程加载代码'?这是 Linux 的历史设计逻辑:fork()(创建子进程,复制父进程)和 execve()(替换子进程)是分离的两个系统调用,这样可以在 fork() 和 execve() 之间插入额外逻辑(如修改环境变量、重定向输入输出),灵活性更高。而 Windows 的 CreateProcess 是'一站式'接口,将'创建进程'和'加载程序'合并,无需单独的程序替换步骤。)

加载器示例代码:

#include <stdio.h>
#include <unistd.h>

int main(int argc, char* argv[]) {
    printf("我是一个进程:%d\n", getpid());
    sleep(1);
    char** myargv = &argv[1];
    execv(myargv[0], myargv);
    printf("程序替换失败!\n");
    return 10;
}

运行结果:

在这里插入图片描述

示意图:

在这里插入图片描述

以上就是写的一个简易加载器程序,myexec 就是加载器本体,可以通过它加载其他程序。

六个 exec 系列函数串讲

下面我们把 exec 系列系统调用串在一起讲,我们先梳理一下这批参数的共性,第一个参数是指你要执行谁,第二个以及后续参数是指你要如何执行,快速记忆就是在命令行上怎么写就在这里怎么填。

这些函数原型看起来很容易混,但只要掌握了规律就很好记:

在这里插入图片描述

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

execl 我们前面已经介绍过了,execl 中的 l 表示 list,因为传递参数是以一个一个单独的字符串传递的。第一个参数是要执行程序的路径,第二个参数我们以 ls 命令为例,可以写成 ls,也可以写成 /usr/bin/ls,后续可变参数是要 ls 命令的选项,最后一个参数以 NULL 结尾。

exec 系列函数的所有代码小编都只贴上面子进程程序替换中的子进程内部逻辑,因为其他基本都一样。

if (id == 0) {
    // 子进程
    printf("我是一个进程:%d\n", getpid());
    sleep(1);
    execl("/usr/bin/ls", "ls", "-a", "-l", "-n", NULL);
    printf("程序替换失败!\n");
    exit(1);
}
execv
int execv(const char* path, char* const argv[]);

我们可以看到 execv 第二个参数是以字符串数组的方式传递的,所以 execv 中的 v 表示 vector,用法和 execl 类似。数组的最后一个元素也要为 NULL。

if (id == 0) {
    // 子进程
    printf("我是一个进程:%d\n", getpid());
    sleep(1);
    //"pwd"本质是 const char* 类型,
    // 需要把它强转为 char* 类型以匹配 char* const 类型的 myargv 数组
    char* const myargv[] = {(char*)"pwd", NULL};
    execv("/usr/bin/pwd", myargv);
    printf("程序替换失败!\n");
    exit(1);
}
execlp
int execlp(const char* file, const char* arg, ...);

execlp 除了第一个参数其他参数和 execl 一样。execl 第一个参数传要执行程序的路径,而 execlp 第一个参数只用传要执行程序的程序名就行(比如 ls 和 ./mycmd),代表的依旧是你要执行谁。原理就是使用 execlp 我们只用传要执行命令的名字,execlp 自己会去环境变量 path 中寻找指定的程序并执行。

#include <stdio.h>
#include <unistd.h>

int main() {
    printf("我是一个进程:%d\n", getpid());
    sleep(1);
    execlp("ls", "ls", "-a", "-l", "-n", NULL);
    printf("程序替换失败!\n");
    return 10;
}
execvp
int execvp(const char* file, char* const argv[]);

有了前面的三个的介绍想必 execvp 各位都能理解了把,直接上代码。

if (id == 0) {
    // 子进程
    printf("我是一个进程:%d\n", getpid());
    sleep(1);
    execlp("ls", "ls", "-a", "-l", "-n", NULL);
    printf("程序替换失败!\n");
    exit(1);
}
execvpe
int execvpe(const char* file, char* const argv[], char* const envp[]);

带 e 的程序替换接口可以让程序员灵活地控制传递给替换后程序的环境变量,无论是自定义的环境变量还是系统的环境变量。

下面示例代码逻辑是 myexec 程序里创建一个子进程,然后子进程程序替换为 mycmd 程序,mycmd 程序会打印它的命令行参数和环境变量。当我们直接运行 mycmd 时它会打印从父进程 bash 拿到的命令行参数和环境变量:

// mycmd.c
#include <stdio.h>

int main(int argc, char* argv[], char* env[]) {
    for (int i = 0; argv[i]; i++) {
        printf("argv[%d], %s\n", i, argv[i]);
    }
    for (int i = 0; env[i]; i++) {
        printf("env[%d], %s\n", i, env[i]);
    }
    return 0;
}

直接编译 mycmd.c 并运行:

在这里插入图片描述

当我们把自定义的命令行参数和环境变量通过 execvpe 传递给程序替换后的 mycmd 程序后 mycmd 就会打印出它拿到的我们自定义的命令行参数和环境变量:

// myexec.c
#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 myargv[] = {"wusaqi", "cmd", NULL};
        char* const myenv[] = {"strggle=888", "luck=666", NULL};
        execvpe("./mycmd", myargv, myenv);
        exit(0);
    }
    // 父进程
    pid_t rid = waitpid(id, NULL, 0);
    if (rid > 0) {
        printf("wait: %d success\n", rid);
    }
    return 0;
}

myexec.c 编译后的运行结果:

在这里插入图片描述

如果我们想把系统的环境变量传给替换后的程序,execvpe 第三个参数就可以传 environ。所以通过程序替换接口传递环境变量表默认意义是摒弃掉老的环境变量表,使用你自己设置的全新的环境变量表。如果程序替换不传环境变量表,替换后的新程序会默认使用调用 exec 函数的当前进程(即被替换的原进程)的环境变量表。

除了只传自定义的环境变量和传系统的环境变量,我们还可以既传系统环境变量,又传自定义的环境变量。这需要我们事先调用 putenv 传递自己的环境变量到系统的环境变量 environ 中,然后程序替换时传递 environ。示例代码如下:

在这里插入图片描述

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

extern char** environ;

char* my = "wusaqi=12345";

int main() {
    pid_t id = fork();
    if (id == 0) {
        // 子进程
        char* const myargv[] = {"wusaqi", "cmd", NULL};
        putenv(my);
        execvpe("./mycmd", myargv, environ);
        exit(0);
    }
    // 父进程
    pid_t rid = waitpid(id, NULL, 0);
    if (rid > 0) {
        printf("wait: %d success\n", rid);
    }
    return 0;
}
execle
int execle(const char* path, const char* arg, ..., char* const envp[]);

这个接口就不细讲了,原理类似。

第七个程序替换接口

我们之前介绍的六个 exec 系列函数都是 man 3 号手册的库函数,而接下来的这个 execve 是 man 2 号手册的系统调用,所有程序替换操作最后都会调用这个系统调用,把当前进程的命令行参数和环境变量传递给被替换的程序。也就是说上面介绍的 6 个程序替换库函数底层都会调用 execve。所以这六个库函数只是传参形式不同,设计这六个库函数的目的是为了满足未来不同场景的需求。

在这里插入图片描述

子进程执行用户写的程序

子进程不仅可以替换系统命令,也可以通过程序替换执行我们自己写的程序,只要能找到就行了,示例如下,让子程序执行我们自己用 C++ 写的 mycmd 程序:

mycmd.cc

#include <iostream>

int main() {
    std::cout << "hello C++" << std::endl;
    return 0;
}

myexec.c:

#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) {
        // 子进程
        sleep(2);
        printf("我是一个进程:%d\n", getpid());
        sleep(1);
        execl("./mycmd", "./mycmd", NULL);
        exit(1);
    }
    // 父进程
    pid_t rid = waitpid(id, NULL, 0);
    if (rid > 0) {
        printf("wait: %d success\n", rid);
    }
    return 0;
}

运行结果:

在这里插入图片描述

程序替换可以调用任意语言的程序

有了上面用 C 语言替换 C++ 程序的铺垫后,我想说不论是什么语言编写的代码运行后都会变成进程,而程序替换其实就是替换进程,所以我们可以用 C 语言写的程序替换代码调用其他语言运行起来的程序。

程序替换是操作系统的功能,所以不止 C 语言,任何语言编写的程序都能完成程序替换。

传递命令行参数和环境变量的 2 种方式

  1. 有了上面关于程序替换的认识,我们学习到了命令行参数和环境变量有两种传递方式,第一种方式是通过程序替换接口(exec**e)的方式将当前程序的环境变量或者其他任意环境变量灵活传递给替换该当前程序的程序,所有 exec 函数都会把命令行参数传递给替换后的程序。
  2. 第二种方式是父子进程之间通过虚拟地址空间传递,因为在父进程的进程地址空间中会存在它的命令行参数和环境变量,就如同全局变量一样,子进程会继承到父进程的进程虚拟空间和页表,自然子进程也能拿到命令行参数和环境变量。

目录

  1. 一、进程程序替换
  2. 替换原理
  3. 子进程程序替换示例
  4. 加载器
  5. 六个 exec 系列函数串讲
  6. execl
  7. execv
  8. execlp
  9. execvp
  10. execvpe
  11. execle
  12. 第七个程序替换接口
  13. 子进程执行用户写的程序
  14. 程序替换可以调用任意语言的程序
  15. 传递命令行参数和环境变量的 2 种方式
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • CentOS 7 Docker 安装指南
  • 在 Cursor 中配置和使用 MCP 服务指南
  • C++ 核心特性解析:引用、内联函数与 nullptr
  • Python 临床知识问答与检索系统架构及实现
  • AI 智能答题助手:Chrome 扩展实时解析实践
  • K-means 聚类算法原理与实现详解
  • C++ 工程师在 AIGC 模型加载中的技术挑战与解决方案
  • 本地大模型部署优化:国内镜像加速与 LLama-Factory 微调实战
  • 利用浏览器插件 Web Scraper 爬取知乎评论数据
  • Python 二级考试真题及参考代码解析(简单应用题)
  • PPT 嵌入 VR 全景图与空间照片的实操方法
  • FPGA 实现 CIC 抽取滤波器设计与仿真
  • 基于大模型和 RAG 的智能 Text2SQL 问答系统:SQLBot
  • 动态规划:环绕字符串中唯一的子字符串
  • 无监督学习:K-Means 聚类算法 MATLAB 实现
  • Python 网络爬虫实战:从基础请求到数据可视化
  • noteDigger 纯前端音频扒谱工具技术解析
  • 脉脉功能实测:后端开发者如何利用 AI 创作与职场社交
  • RTX 5090D 部署 LLaMA-Factory:PyTorch 与 CUDA 版本兼容性实战
  • 小需求设计:如何用 Redis 实现协议勾选状态管理

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online