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

Linux 进程替换原理:从 fork 到 exec 详解

Linux 进程替换指用新可执行程序完全替换当前进程的代码、数据和堆栈,PID 保持不变。核心流程为 fork 创建子进程后调用 exec 系列函数。exec 不会触发写时拷贝,因它丢弃原有地址空间。CPU 通过 ELF 文件头 e_entry 字段获取入口地址。exec 系列库函数封装了底层 execve 系统调用,支持不同参数传递方式(可变参数或数组)。父进程可通过 fork 继承环境变量,或使用 execle/execvpe 显式指定。exec 成功则不返回,失败返回 -1。该机制是 Shell 执行非内建指令的基础。

SqlMaster发布于 2026/3/16更新于 2026/7/2439 浏览
Linux 进程替换原理:从 fork 到 exec 详解

进程替换是什么?

进程替换就是:一个正在运行的进程,用一个全新的可执行程序,完全替换掉自己当前的代码、数据和堆栈,但进程的 PID 保持不变。

简单来说,就像你正在用一个记事本,然后直接把它变成了一个浏览器 —— 窗口(PID)没变,但里面的程序已经完全换了。

我们先来看一个简单的演示,直观感受一下进程替换的效果。

#include <iostream>
#include <unistd.h>
using namespace std;
int main() {
    cout << "before: I am process, Mypid: " << getpid() << " Myppid: " << getppid() << endl;
    execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
    cout << "after: I am process, Mypid: " << getpid() << " Myppid: " << getppid() << endl;
    return 0;
}

从结果能看到,execl 之后程序就变成 ls 了,原进程后续的代码不再执行,这就是进程替换。

进程替换原理

用 fork 创建子进程后,子进程执行的是和父进程相同的程序(但可能执行不同的代码分支)。为了让子进程执行另一个程序,我们通常会在子进程中调用一种 exec 函数。

当进程调用 exec 函数时,该进程的用户空间代码和数据会被新程序完全替换,并从新程序的启动例程开始执行。注意,调用 exec 并不会创建新进程,所以调用 exec 前后,进程的 PID 并未改变。

1. 进程替换会发生写时拷贝吗?

exec 替换不会触发写时拷贝(COW)。

COW 的触发前提:只有 fork() 创建子进程后,父子进程共享内存页,且某一方对共享页执行写入操作时,才会触发 COW。

exec 的本质:它会直接丢弃当前进程的代码段、数据段等所有用户空间内容,重新加载新程序,相当于销毁了原有的地址空间映射,不再与任何进程共享内存页。

子进程场景:fork() 后虽与父进程共享内存,但 exec() 会直接丢弃这些共享资源,无需写入,因此不触发 COW。

父进程场景:直接替换自身时,不存在任何共享内存的前提,自然也不会触发 COW。

2. 普通只读 vs COW 只读

父进程依赖「物理内存页引用计数」触发 COW,无需页表带 COW 标志;子进程依赖「页表中的 COW 标志 + 临时只读属性」触发 COW,因此只有子进程的页表会存在 COW 标志,而父子进程修改共享页,最终都会触发 COW 实现数据隔离。

3. exec 函数执行后,后续代码还会执行吗?

执行成功:程序会被完全替换为新的可执行文件,exec 后面的代码永远不会执行。因为 exec 成功时没有返回值,不会回到原程序的执行流。

执行失败:exec 会返回 -1,程序会继续执行 exec 之后的代码(通常这里会写错误处理逻辑,比如 perror 然后退出)。

4. CPU 如何知道程序的入口地址?

Linux 下的可执行文件是 ELF 格式,文件头部的 e_entry 字段会存储程序的入口地址。当 exec 加载新程序时,内核会读取 ELF 头中的这个地址,并把 CPU 的指令指针(PC)设置到该地址,让程序从这里开始执行。

5. 子进程进行程序替换后,会影响父进程的代码和数据吗?

不会。子进程替换时会丢弃原有资源并构建全新地址空间,与父进程完全独立,因此不会影响父进程的代码和数据。

exec 系列接口

execve() 是内核直接提供的系统调用,是'真身'。

其他 exec 函数都是 C 库提供的封装,是'马甲',最终都会调用 execve() 来完成实际的进程替换。

1. execl

📝 函数原型

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

🔍 参数说明

path:要执行的程序的完整路径(如 /bin/ls),不能只写文件名,因为它不会去 PATH 里查找。

arg:传递给新程序的命令行参数,第一个参数通常是程序名本身,后续是其他参数。

**...**:可变参数列表,最后必须用 NULL 来标志参数结束。

✅ 返回值

调用成功时,函数不会返回(因为当前进程已被新程序替换)。

调用失败时,返回 -1,并设置 errno 来指示错误原因。

演示

#include <iostream>
#include <unistd.h>
using namespace std;
int main() {
    cout << "before: I am process, Mypid: " << getpid() << " Myppid: " << getppid() << endl;
    // 调用 execl 执行 ls -l -a
    execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
    // 如果 execl 执行成功,下面的代码永远不会执行
    cout << "after: I am process, Mypid: " << getpid() << " Myppid: " << getppid() << endl;
    return 0;
}
2. execlp

📝 函数原型

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

🔍 参数说明

**file**:要执行的程序文件名(如 "ls"),它会自动在系统 PATH 环境变量中查找该程序,无需写完整路径。

arg:传递给新程序的命令行参数,第一个参数通常是程序名本身,后续是其他参数。

**...**:可变参数列表,最后必须用 NULL 来标志参数结束。

✅ 返回值

调用成功时,函数不会返回(当前进程已被新程序替换)。

调用失败时,返回 -1,并设置 errno 来指示错误原因。

演示

#include <iostream>
#include <unistd.h>
#include <cstdio>
using namespace std;
int main() {
    cout << "Before execlp, PID: " << getpid() << endl;
    // 执行 ls -l -a,通过 PATH 环境变量自动查找 ls
    int ret = execlp("ls", "ls", "-l", "-a", NULL);
    // 如果 execlp 成功,下面的代码永远不会执行
    if (ret == -1) {
        perror("execlp failed");
    }
    cout << "After execlp (won't print if success)" << endl;
    return 0;
}
3. execv

📝 函数原型

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

🔍 参数说明

path:要执行的程序的完整路径(例如 /bin/ls),不会自动在 PATH 中查找。

**argv**:传递给新程序的命令行参数数组,格式为 {"程序名", "参数 1", "参数 2", ..., NULL},必须以 NULL 结尾。

✅ 返回值

调用成功时,函数不会返回(进程已被新程序替换)。

调用失败时,返回 -1,并设置 errno 指示错误原因。

演示

#include <iostream>
#include <unistd.h>
using namespace std;
int main() {
    cout << "Before execv, PID: " << getpid() << endl;
    // 构造参数数组
    char *const argv[] = {"ls", "-l", "-a", NULL};
    // 执行 /bin/ls -l -a
    int ret = execv("/bin/ls", argv);
    // 如果 execv 成功,下面的代码不会执行
    if (ret == -1) {
        perror("execv failed");
    }
    cout << "After execv (won't print if success)" << endl;
    return 0;
}
4. execvp

📝 函数原型

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

🔍 参数说明

**file**:要执行的程序文件名(如 "ls"),会自动在系统 PATH 环境变量中查找该程序,无需写完整路径。

**argv**:传递给新程序的命令行参数数组,格式为 {"程序名", "参数 1", "参数 2", ..., NULL},必须以 NULL 结尾。

✅ 返回值

调用成功时,函数不会返回(进程已被新程序替换)。

调用失败时,返回 -1,并设置 errno 指示错误原因。

演示

#include <iostream>
#include <unistd.h>
using namespace std;
int main() {
    cout << "Before execvp, PID: " << getpid() << endl;
    // 构造参数数组
    char *const argv[] = {"ls", "-l", "-a", NULL};
    // 执行 ls -l -a,通过 PATH 自动查找 ls
    int ret = execvp("ls", argv);
    // 如果 execvp 成功,下面的代码不会执行
    if (ret == -1) {
        perror("execvp failed");
    }
    cout << "After execvp (won't print if success)" << endl;
    return 0;
}
5. execle

📝 execle 函数原型

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

✅ 函数参数

**path**:要执行的程序的完整路径(比如 "/bin/ls")。

**arg**:传递给新程序的命令行参数,第一个参数通常是程序名,后续是选项或参数,最后必须以 NULL 结尾。

**envp**:你自定义的环境变量数组,格式为 KEY=VALUE,同样以 NULL 结尾,用来完全替换子进程的环境变量表。

✅ 返回值

调用成功时,函数不会返回(进程已被新程序替换)。

调用失败时,返回 -1,并设置 errno 指示错误原因。

演示

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
// 编译器默认定义的变量,指向当前进程环境变量表
extern char** environ;
int main() {
    // 创建子进程
    pid_t pid = fork();
    if (pid == -1) {
        // fork 创建子进程失败
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程:使用 execle 显式传递 environ
        int ret = execle("./testc", "./testc", "-a", "-b", "-c", NULL, environ);
        // 只有 execle 失败时,下面的代码才会执行
        if (ret == -1) {
            perror("execle failed");
            return 1;
        }
    } else {
        // 父进程:等待子进程执行完毕,避免子进程变成僵尸进程
        wait(NULL);
    }
    return 0;
}

目前,我们在调用 execle 时,第三个参数传递的是当前进程的 environ 变量,也就是把父进程的完整环境变量表传给子进程(如何传自定义环境变量表在文章最后会有讲解)

6. execvpe

这个接口我就不介绍了,execvpe 与 execle 的唯一区别在于,它使用数组来传递命令行参数,其余功能与 execle 完全一致。

exec 系列库函数与系统调用的关系

exec 系列库函数(如 execl、execle)都是对底层系统调用 execve 的封装,它们通过不同的参数传递方式(可变参数或数组)来提供更易用的接口。

其他问题

问题:exec 系列函数只能执行系统命令吗?能不能执行自己写的程序?

exec 系列函数不仅能执行系统命令(比如 ls、cat),也能执行你自己写的可执行程序;

只要你把程序编译成可执行文件,然后把它的完整路径(或文件名,如果在 PATH 里)传给 exec 函数,就能像执行系统命令一样运行它;

演示

#include <iostream>
#include <unistd.h>
using namespace std;
int main() {
    int ret = execl("./testc", "testc", NULL);
    if (ret == -1) {
        cout << "execl failed" << endl;
    }
    return 0;
}

这段演示清晰地证明了 exec 系列函数的强大能力:它不仅能执行 ls、cat 这类系统命令,也能无缝运行我们自己编写并编译好的程序。

很多人会疑惑:为什么在命令行执行自己的程序时需要加 ./(比如 ./testc),但在 exec 函数的参数里只传了 testc 就能运行?

这是因为:

在外部终端执行程序时,系统需要明确路径来找到可执行文件,所以必须加上 ./ 表示当前目录。

而在 exec 函数中,第一个参数是程序的完整路径(如 ./testc),它已经告诉系统去哪里找文件;后面的 testc 只是传递给程序的 argv[0] 参数,不是用来定位文件的,因此不需要再加路径(当然,加了也可以运行)

问题:为什么我们的可执行程序、脚本,都能跨语言调用呢?

因为所有语言运行起来,本质都是进程。不管是 C/C++ 编译出的可执行文件、Shell 脚本,还是 Python 脚本,最终都是由操作系统创建为独立的进程来执行的。调用的本质都是让操作系统加载并执行目标程序,和它原本用什么语言编写无关。

exec*有着加载器的作用

exec 系列函数是操作系统层面的程序加载与替换接口,核心作用是加载指定可执行文件,将当前进程的内存空间完全替换为新程序内容,从而启动新程序执行。

在终端中,我们输入的非内建指令(如 ls、自定义编译程序),本质上都是 Shell 先通过 fork() 创建子进程,再在子进程中调用 exec 系列函数加载并执行目标程序。因此,exec 是终端非内建指令执行的核心环节,扮演'加载器'的关键角色。

问题:父进程如何通过 exec*给子进程传递命令行参数和环境变量
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
int main() {
    pid_t pid = fork();
    if (pid == -1) {
        // fork 创建子进程失败
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程执行逻辑
        char* const argv[] = { "./testc", "-a", "-b", "-c", NULL };
        int ret = execv("./testc", argv);
        // 只有 execv 失败时,下面的代码才会执行
        if (ret == -1) {
            perror("execv failed");
            return 1;
        }
    }
    // 等待子进程执行完毕,避免子进程变成僵尸进程
    wait(NULL);
    return 0;
}
#include <stdio.h>
int main(int argc, char *argv[], char *env[]) {
    printf("-------------------这是命令行参数-------------------\n");
    for (int i = 0; argv[i]; i++) {
        printf("[%d]: %s\n", i, argv[i]);
    }
    printf("-------------------这是环境变量-------------------\n");
    for (int i = 0; env[i]; i++) {
        printf("[%d]: %s\n", i, env[i]);
    }
    return 0;
}

从运行结果来看,我们传递的命令行参数被完整打印,但是我们没有传递环境变量为啥也能打印出来呢?

这是因为环境变量是在 fork() 创建子进程时,由操作系统自动从父进程复制并继承给子进程的,并非需要我们手动传递。后续调用 execv 替换子进程时,这份环境变量会被完整保留,所以新程序依然可以通过 main 的 env[] 参数或全局变量 environ 访问并打印它们。

**env[]**:是 main 函数的第三个参数,它直接指向子进程继承来的环境变量表,是函数参数级别的访问入口。

environ:是系统为每个进程预设的全局变量,也指向同一份环境变量表,是全局级别的访问入口。

putenv(新增环境变量)

✅ putenv 是一个用于新增或修改当前进程环境变量的函数。

作用:将一个格式为 KEY=VALUE 的字符串添加到当前进程的环境变量表中;如果 KEY 已存在,则会覆盖原有值。

生效范围:仅对当前进程有效,并且会被后续 fork() 创建的子进程继承。

注意点:参数必须是 KEY=VALUE 格式,等号两边不能有空格。

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
int main() {
    // 设置环境变量,注意格式 KEY=VALUE 不能有空格
    putenv("MY_ENV=666666");
    // 创建子进程
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程:执行 testc 程序
        char* const argv[] = { "./testc", "-a", "-b", "-c", NULL };
        int ret = execv("./testc", argv);
        // 如果 execv 失败才会执行到这里
        if (ret == -1) {
            perror("execv failed");
            return 1;
        }
    } else {
        // 父进程:等待子进程结束
        wait(NULL);
        // 父进程打印自己的环境变量
        cout << "-------------------父进程新增 env-------------------" << endl;
        cout << getenv("MY_ENV") << endl;
    }
    return 0;
}
  1. 子进程成功获取并输出了通过 putenv 设置的环境变量。
  2. 当前进程成功获取并输出了通过 putenv 设置的环境变量。
  3. bash 父进程的环境变量并未新增该条目。
问题:如何彻底替换环境变量?
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
int main() {
    // 创建子进程
    pid_t pid = fork();
    if (pid == -1) {
        // fork 创建子进程失败
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程:用 execle 显式传递自定义环境变量
        char* const argv[] = { "./testc", "-a", "-b", "-c", NULL };
        char* const myenv[] = { "MYAVL=111111", "HELLO=222222", NULL };
        // 显式传递自定义环境变量数组 myenv
        int ret = execle("./testc", "./testc", "-a", "-b", "-c", NULL, myenv);
        // 只有 execle 失败时才会执行到这里
        if (ret == -1) {
            perror("execle failed");
            return 1;
        }
    } else {
        // 父进程:等待子进程执行完毕,避免子进程变成僵尸进程
        wait(NULL);
    }
    return 0;
}

子进程环境变量被自定义 myenv 数组完全替换,仅能访问显式传递的 MYAVL/HELLO,未继承父进程的 PATH 等原有环境变量。

目录

  1. 进程替换是什么?
  2. 进程替换原理
  3. 1. 进程替换会发生写时拷贝吗?
  4. 2. 普通只读 vs COW 只读
  5. 3. exec 函数执行后,后续代码还会执行吗?
  6. 4. CPU 如何知道程序的入口地址?
  7. 5. 子进程进行程序替换后,会影响父进程的代码和数据吗?
  8. exec 系列接口
  9. 1. execl
  10. 2. execlp
  11. 3. execv
  12. 4. execvp
  13. 5. execle
  14. 6. execvpe
  15. exec 系列库函数与系统调用的关系
  16. 其他问题
  17. 问题:exec 系列函数只能执行系统命令吗?能不能执行自己写的程序?
  18. 问题:为什么我们的可执行程序、脚本,都能跨语言调用呢?
  19. exec*有着加载器的作用
  20. 问题:父进程如何通过 exec*给子进程传递命令行参数和环境变量
  21. putenv(新增环境变量)
  22. 问题:如何彻底替换环境变量?
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • ruoyi-vue-pro 数据大屏纯前端单点登录方案
  • 苍穹外卖前端开发实战:Vue + TypeScript + ElementUI
  • SCADA Engine:基于 Vue3 的开源工业级组态引擎
  • Android MVVM 架构实战:DataBinding 中 BindingAdapter 的使用与原理
  • 2026 年主流 AI 生成 PPT 工具横评:6 款实测与选择指南
  • Milvus 向量数据库:核心原理与 Python 实战
  • 前端异常捕获与统一格式化:从 console.log 到服务端上报
  • Ubuntu 国内镜像源更换指南:阿里/清华/中科大对比
  • 基于 LLama-Factory 打造个性化 AI 角色的微调实战
  • VSCode Copilot 配置文件提示“未知工具”警告排查与解决
  • Windows 环境下 faster-whisper 部署:CUDA 加速与 PyAV 依赖配置
  • 前端状态管理实战:Redux Toolkit、Zustand 与 Jotai 对比
  • 前端加密 encrypt-labs 靶场:环境搭建与关卡解析
  • Flutter 组件 Spry 适配鸿蒙 HarmonyOS 实战:轻量化端侧 Web 框架
  • IntelliJ IDEA 配置 AI 插件实现 Java 俄罗斯方块游戏开发
  • WebODM完全指南:零基础掌握开源无人机地图制作
  • Java ArrayList 底层原理与核心方法手写实现
  • 树莓派智能家居中控系统构建指南
  • Manacher(马拉车)算法详解:求解最长回文子串
  • 2026 年主流 AI Agent 框架横评:OpenClaw 及九大衍生产品对比与场景推荐

相关免费在线工具

  • 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