【Linux我做主】进程程序替换和exec函数族

【Linux我做主】进程程序替换和exec函数族

进程程序替换和exec函数族

进程程序替换和exec函数族

github地址

有梦想的电信狗

0. 前言

​ 在 Linux 中,进程除了能通过 fork 创建子进程外,还可以通过 exec 系列函数进行进程替换。所谓替换,就是让一个正在运行的进程丢掉原来的程序映像,转而执行另一个可执行文件。

​ 这正是命令行运行程序、Shell 调用脚本的底层机制。本文将通过实例,从单进程替换到 fork+exec 的组合,再到不同的 exec 接口,逐步剖析这一机制的原理与用法。


1. 单进程的进程替换

  • 先简单看一下单进程进程替换的现象,代码如下:

Linux为我们提供了一些列系统调用,用于进行进程替换!

在这里插入图片描述
#include<stdio.h>#include<stdlib.h>#include<unistd.h>intmain(){printf("before: I am a process pid: %d, ppid: %d\n",getpid(),getppid());execl("/usr/bin/ls","ls","-a","-l",NULL);printf("after: I am a process pid: %d, ppid: %d\n",getpid(),getppid());return0;}

注意:这里 execl("/usr/bin/ls", "ls", "-a", "-l", NULL);exec系列函数的标准写法,方便记忆

  • execl("/usr/bin/ls", "ls", "-a", "-l", NULL); : 第一个参数是可执行文件的路径,中间是可变参数列表,用于指明程序执行的参数,最后一个参数必须是 NULL
  • 执行现象如下
在这里插入图片描述

总结现象

  • execl参数中的命令和选项被执行了
  • 只执行了execl调用前的printf("before ...")函数,调用execl之后的printf("after ...")没有执行
  • 我们自己的进程,可以在运行时,运行其他路径的程序,这种现象就叫进程替换

2. 进程替换的原理

在 Linux 中,当我们在命令行运行一个程序时,本质上经历了以下几个步骤

1. 进程的创建

  1. 用户在 bash 中输入一条命令。
  2. bash 进程调用 fork() 创建一个子进程,这个子进程就是将要执行用户命令的进程。
  3. 操作系统为该子进程分配必要的资源,包括:
    • PCB(进程控制块):保存进程的标识、状态、寄存器等信息。
    • 进程地址空间:包含代码段、数据段、堆、栈等区域。
    • 页表:建立虚拟地址与物理地址的映射关系。

2. 可执行程序的加载

当子进程被创建后,bash 会调用 exec 系列函数,加载用户指定的可执行程序。
在这一过程中:

  • 原进程在物理内存中的 代码段和数据段会被新程序替换,发生了写时拷贝
  • 操作系统会 重新建立页表,将新程序的虚拟地址映射到新的物理内存页。
  • 虚拟地址空间本身的结构保持不变(依然分为代码段、数据段、堆、栈),但内容会根据新程序进行调整。
  • PCB 不会被替换,它仍然是原来的进程控制块,只是内部记录的信息被更新。

因此,调用 exec 系列函数不会创建新进程,而是让当前进程“脱胎换骨”,从此开始运行新的程序。

3. 程序入口地址的确定

一个关键问题是:CPU 如何知道新程序应该从哪里开始执行?

答案在于 ELF(Executable and Linkable Format,可执行与可链接格式)

Linux中形成的可执行程序,是有特定格式的,Linux中的可执行程序的格式为 ELF
  • 在程序源代码编译生成 ELF 文件时,编译器会将程序的 入口地址(entry point) 写入到 ELF 文件的头部(表头),可执行程序的入口地址存在表头中
  • exec 将 ELF 加载到内存后,操作系统会读取 ELF 头部信息,从而得到程序的入口地址。
  • CPU 在调度该进程运行时,会将指令寄存器(IP/EIP/RIP)设置为入口地址,从这个位置开始执行程序代码。

4. 总结

在这里插入图片描述
  • fork() 用于创建子进程。
  • exec() 用于替换子进程的内存映像,加载并运行新程序。
    • exec系列函数的做法十分简单粗暴
      • 调用exec系列函数时,直接用新程序的代码替换原来进程的代码,用新程序的数据,替换原来进程的数据,并让CPU执行新程序的代码开始
      • 该过程中没有创建新进程,原进程的PCB、进程地址空间都保持不变,页表中的相应字段被更新
  • 进程替换后:
    • PCB 保留(进程还是原来的进程)。
    • 虚拟地址空间结构不变,但内容被新程序覆盖。
    • 页表更新,建立新的虚拟地址到物理地址的映射。
    • 入口地址由 ELF 文件头部提供,CPU 从此位置开始执行新程序。

最终,用户在命令行中输入的程序就能在 CPU 上开始运行。


3. 多进程的程序替换

  • 代码演示
// 多进程 程序替换intmain(){ pid_t id =fork();if(id ==0){// childprintf("before: I am a process pid: %d, ppid: %d\n",getpid(),getppid());sleep(5);execl("/usr/bin/ls","ls","-a","-l",NULL);printf("after: I am a process pid: %d, ppid: %d\n",getpid(),getppid());exit(0);}// father pid_t ret =waitpid(id,NULL,0);// 等待子进程,暂不关心进程退出状态,阻塞等待if(ret >0){printf("wait success, father: %d, ret %d\n",getpid(), ret);sleep(5);}return0;}
在这里插入图片描述

问题与总结

1. 子进程被替换会不会影响父进程?

不会。

  • 父子进程是独立的两个进程,它们各自拥有独立的虚拟地址空间。
  • 当子进程调用 exec 系列函数时,替换的只是该子进程的用户空间代码和数据不会对父进程的执行造成影响
  • 父进程依然可以通过 wait/waitpid 等方式监控子进程的退出状态。

2. 进程替换是否创建新进程?

进程替换不创建新进程只进行进程代码和数据的替换

  • exec 并不会创建新进程,它只会在当前进程的上下文中加载并运行一个新的程序。
  • 因此:
    • 进程 ID(PID)保持不变
    • PCB 仍然是原来的,只是其中的代码段、数据段、堆、栈等信息被新程序替换。
  • 所以,exec 本质上是“用另一个程序替换自己”,而不是启动一个新的进程。

3. forkexec 的关系

  • 调用 fork() 后,子进程和父进程最初运行的是相同的程序(代码空间一致,但地址空间独立)。
  • 在很多场景下,子进程会立刻调用 exec,以执行一个全新的程序。

这样形成了经典的模式:

父进程: 继续原有逻辑

子进程:fork → exec,替换为用户指定的程序

4. exec 调用后的代码执行情况

  • 替换成功时
    • 进程的代码和数据完全被新程序替换。
    • CPU 的指令寄存器被设置为新程序的入口地址。
    • exec 调用点之后的代码不会被执行
  • 替换失败时
    • exec 会返回 -1,并设置 errno 表示错误原因。
    • 此时,调用 exec 之后的代码才有机会被执行。

5. exec 系列函数的返回值

  • exec没有成功返回值
  • 如果成功执行了替换,原来的程序逻辑已经不存在,因此不可能返回到原调用点。
  • 只有在加载失败时,exec 才会返回 -1

总结

  • 子进程的替换不会影响父进程,它们的执行空间相互独立。
  • 进程替换不创建新进程,只是在原有进程中加载新程序。
  • forkexec 常常配合使用:fork 创建子进程,exec 让子进程运行新程序。
  • exec 成功执行后,调用点之后的代码不会被执行;只有失败时才会返回 -1 并继续向下执行,exec系列函数无成功时的返回值

4. 验证各种程序替换接口exec

1. exec 系列函数概述

在 Linux 中,exec 系列函数用于 用一个新的程序替换当前进程的映像

  • 调用成功后,当前进程的代码段、数据段、堆、栈都会被新程序替换,不会创建新进程(这点和 fork 不同)。
  • 调用成功时 不会返回;若调用失败,才会返回 -1。

它们主要定义在头文件:

#include<unistd.h>
在这里插入图片描述

2. exec 系列函数族

常见的 exec 系列库函数有:

// 以下为库函数intexecl(constchar*path,constchar*arg,.../* (char *) NULL */);intexeclp(constchar*file,constchar*arg,.../* (char *) NULL */);intexecle(constchar*path,constchar*arg,.../* (char *) NULL, char *const envp[] */);intexecv(constchar*path,char*const argv[]);intexecvp(constchar*file,char*const argv[]);intexecve(constchar*path,char*const argv[],char*const envp[]);// 以下为系统调用intexecvpe(constchar*file,char*const argv[],char*const envp[]);
  • 以上函数,如果调用成功加载新的程序从启动代码开始执行,不再返回。
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值,没有成功的返回值

命名规律总结

exec 系列函数的后缀有规律

后缀含义
l参数以列表(list)的形式传递(execl, execlp, execle),传参时直接写 "arg1", "arg2", ..., NULL必须以NULL结尾
v参数以向量(vector,数组)的形式传递(execv, execvp, execvpe),传入 char *argv[]
p函数名中的p:即PATH。代表默认在 PATH 环境变量搜索可执行文件(execlp, execvp, execlvpe),传参时无需传入路径,只需传入要执行的程序名
e允许指定新的环境变量 envp[]execle, execvpe)。
记忆口诀l = listv = vectorp = pathe = environment

3. 函数参数说明和使用

  • exec系列所有函数
    • 第一个参数:帮助函数找到该程序,因此传入程序的绝对路径或相对路径或程序名
    • 第二个参数:告诉程序如何执行,因此传入程序执行所需的参数

(1) pathfile

  • path:需要给出 可执行文件的绝对路径或相对路径,例如 /usr/bin/ls或。
  • file:只需要给出文件名,例如ls函数会在 PATH 环境变量 指定的路径中搜索程序。

例如

execl("/usr/bin/ls","ls","-a","-l",NULL);// 直接指定路径execlp("ls","ls","-a","-l",NULL);// 默认在环境变量 PATH 搜索,只需传入程序名

(2) argv / arg

  • argvarg 都表示 传递给新程序的参数列表
  • NULL 结尾 表示参数结束。

示例

char*const myargv[]={"ls","-a","-l",NULL};execv("/usr/bin/ls", myargv);

(3) envp

  • execleexecvpe 可以显式指定环境变量;
  • 其他函数(如 execl, execv, execlp, execvp)会默认继承调用进程的环境变量。

envp 表示 环境变量列表,即一个以 NULL 结尾的字符串数组:

// 自定义的环境变量char*envp[]={"PATH=/usr/bin","USER=guest",NULL};execle("/usr/bin/ls","ls","-a","-l",NULL, envp);char* myagrv[]={"ls","-a","-l",NULL};execvpe("ls", myargv, envp)

基本用法

#include<unistd.h>#include<stdio.h>#include<stdlib.h>intmain(){printf("Before exec...\n");// ... 进程替换的接口perror("exec failed");return1;}

execl

// 方法1: execl (指定路径 + 列表传参)execl("/usr/bin/ls","ls","-a","-l",NULL);

execlp

// 方法2: execlp (用 PATH 搜索)execlp("ls","ls","-a","-l",NULL);

execv

// 方法3: execv (指定路径 + 向量传参)char*args[]={"ls","-a","-l",NULL};execv("/usr/bin/ls", args);

execle

// 方法4: execle (指定路径 + 参数 + 环境变量)char*envp[]={"PATH=/bin",NULL};execle("/usr/bin/ls","ls","-a","-l",NULL, envp);

总结

  • 第一个参数pathname/file:传入程序的绝对路径或相对路径或程序名
  • 后续如果是可变参数列表:命令行中怎么写,参数列表就怎么写,只不过空格分隔变成了逗号分隔,选项放在了双引号中
  • 根据需要可传入指定的环境变量
  • ls以及其他程序是C/C++写的程序,也有命令行参数,exec系列函数调用时,会把后面参数列表agrv[]中的选项envp[],形成命令行参数表和环境变量表,传递给ls或其他程序的main函数
在 Linux 中,所有进程都是由已有进程 fork 出来的子进程。命令行运行程序时,Bash 先 fork 出子进程,再用 exec 系列函数把目标程序加载进内存运行。
exec 的作用是清空当前进程占用的物理内存空间,把磁盘上的可执行文件读入内存,并让 CPU 从新程序的入口开始执行,相当于内核提供的“程序加载器”。

4. Makefile 一次编译多个可执行程序

.PHONY:all all:otherExe mycommand mycommand:mycommand.c gcc -o $@ $^ -std=c99 otherExe:otherExe.cpp g++ -o $@ $^ -std=c++11 .PHONY:clean clean: rm -f mycommand otherExe 
  • .PHONY: all
    • 声明 伪目标all,表示 all 不是一个文件名,而是一个逻辑目标。
    • 如果没有声明 .PHONY,当目录下存在一个叫 all 的文件时,make all 会误认为已经生成了目标而不执行命令。
  • all: otherExe mycommand
    • all 目标依赖于 otherExemycommand
    • 执行 make all 时,会先尝试生成 otherExe,再生成 mycommand(顺序由 make 自行决定,但通常按依赖书写顺序来执行)。

  • 总结依赖关系图
all ├── otherExe (依赖于 otherExe.cpp) └── mycommand (依赖于 mycommand.c) 
  • makemake all:同时编译 mycommandotherExe
  • make mycommand:只编译 mycommand
  • make otherExe:只编译 otherExe
  • make clean:清理编译产物。

跨语言调用

  • exec接口调用我们自己的写的可执行程序以及调用其他语言形成的可执行程序
// 执行我们自己写的程序 用C语言程序 调用C++程序execl("./otherExe","otherExe",NULL);// C语言程序调用 shell 脚本execl("/usr/bin/bash","bash","test.sh",NULL);// C语言程序调用 python 脚本execl("/usr/bin/python3","python3","test.py",NULL);

“所有语言编写的程序,运行起来本质都是进程”


为什么可执行程序或脚本能跨语言调用?

  • 程序运行后在操作系统看来都是 进程,无论是 C、Go 编译的二进制,还是 Python、Shell 脚本。
  • 跨语言调用的本质就是 一个进程调用或替换成另一个进程
  • 操作系统不关心语言,只负责加载和运行进程。

👉 所以所有语言写的程序都能互相调用。

5. 一个程序调另一个程序验证命令行参数的传递

  • mycommand程序向otherExe传递命令行参数
// mycommand.cintmain(){ pid_t id =fork();if(id ==0){// childprintf("before: I am a process pid: %d, ppid: %d\n",getpid(),getppid());sleep(3);char*const myargv[]={"otherExe","-a","-b",NULL};execv("./otherExe", myargv);printf("after: I am a process pid: %d, ppid: %d\n",getpid(),getppid());exit(0);}// father pid_t ret =waitpid(id,NULL,0);// 等待子进程,暂不关心进程退出状态,阻塞等待if(ret >0){printf("wait success, father: %d, ret %d\n",getpid(), ret);sleep(3);}return0;}
// otherExe.cpp#include<iostream>usingnamespace std;intmain(int argc,char* argv[]){ cout << argv[0]<<" begin running"<< endl;for(int i =0; argv[i];++i){ cout << i <<" : "<< argv[i]<< endl;} cout << argv[0]<<"otherExe stop running"<< endl;return0;}
在这里插入图片描述

6. 一个程序调另一个程序验证环境变量的传递

  • mycommand程序向otherExe传递环境变量
externchar** environ;execle("./otherExe","otherExe","-a","-b",NULL, environ);// 传递系统的环境变量
#include<iostream>usingnamespace std;intmain(int argc,char* argv[],char* env[]){ cout <<"这是命令行参数"<< endl; cout << argv[0]<<" begin running"<< endl;for(int i =0; argv[i];++i){ cout << i <<" : "<< argv[i]<< endl;} cout <<"这是环境变量"<< endl;for(int i =0; env[i];++i){ cout << i <<" : "<< env[i]<< endl;} cout << argv[0]<<"otherExe stop running"<< endl;return0;}
在这里插入图片描述

7. 给子进程传递新的环境变量

传递新的环境变量有两种方式
  • 添加新的环境变量
  • 完全替换原来的环境变量

putenv添加新的环境变量

putenv("MYPRIVATE_ENV=123456");putenv为当前进程添加环境变量,不影响父进程中的环境变量

在这里插入图片描述
intmain(){ pid_t id =fork();putenv("MYPRIVATE_ENV=123456");if(id ==0){// childprintf("before: I am a process pid: %d, ppid: %d\n",getpid(),getppid());sleep(3);char*const myargv[]={"otherExe","-a","-b",NULL};execv("./otherExe", myargv);printf("after: I am a process pid: %d, ppid: %d\n",getpid(),getppid());exit(0);}// father pid_t ret =waitpid(id,NULL,0);// 等待子进程,暂不关心进程退出状态,阻塞等待if(ret >0){printf("wait success, father: %d, ret %d\n",getpid(), ret);sleep(3);}return0;}
在这里插入图片描述

完全替换掉从父进程继承下来的环境变量

intmain(){ pid_t id =fork();externchar** environ;if(id ==0){// childprintf("before: I am a process pid: %d, ppid: %d\n",getpid(),getppid());sleep(3);// execle("./otherExe", "otherExe", "-a", "-b", NULL, environ); // 传系统的环境变量// 传自定义的环境变量 会完全覆盖从系统继承下来的环境变量char*const myenv[]={"MYVAL=123456","MYPATH=/usr/bin/xxx",NULL};execle("./otherExe","otherExe","-a","-b",NULL, myenv);printf("after: I am a process pid: %d, ppid: %d\n",getpid(),getppid());exit(0);}// father pid_t ret =waitpid(id,NULL,0);// 等待子进程,暂不关心进程退出状态,阻塞等待if(ret >0){printf("wait success, father: %d, ret %d\n",getpid(), ret);sleep(3);}return0;}
在这里插入图片描述
  • 使用exec系列函数中带e的接口时(execle,execvpe),手动传入新的环境变量,会覆盖掉从父进程继承下来的环境变量
  • exec系列函数所有接口的调用关系
    • execve系统调用,头文件为<unistd.h>
    • 其他exec函数是库函数,头文件为<stdlib.h>,底层调用execve
在这里插入图片描述

5. 环境变量与进程的关系

1. 环境变量的本质

  • 环境变量也是数据,存放在进程的用户空间中。
  • 每个进程在运行时,都有一份属于自己的环境变量表。

2. 环境变量的继承

  • 当父进程通过 fork 创建子进程时,环境变量表会被一同复制到子进程的地址空间。
  • 因此,子进程天生就继承了父进程的环境变量。
  • 这意味着:环境变量是在 子进程创建阶段 就已经传递下去的,而不是运行后再赋值的。

3. 环境变量的访问方式

  • main 函数中,可以通过第三个参数 char* envp[] 访问环境变量;
  • 即使不依赖 main 的参数,也可以通过 全局变量
externchar**environ;

直接获取和操作当前进程的环境变量表。


总结一句话
环境变量是进程运行时的一部分数据,创建子进程时会自动继承父进程的环境变量表;无论通过 main 参数还是 environ 变量,都可以访问和修改它。


5. 结语

​ 进程替换并不会创建新进程,而是在原进程中装载新程序。配合 fork 使用,就形成了“父进程继续执行,子进程运行新程序”的经典模式。

​ 理解 exec 系列函数,有助于把握 Linux 程序执行的本质:所有程序运行到最后都是进程,而进程既可以继承,也可以替换。这正是 Linux 灵活而高效的关键所在。


以上就是本文的所有内容了,如果觉得文章对你有帮助,欢迎 点赞⭐收藏 支持!如有疑问或建议,请在评论区留言交流,我们一起进步

分享到此结束啦
一键三连,好运连连!
你的每一次互动,都是对作者最大的鼓励!征程尚未结束,让我们在广阔的世界里继续前行! 🚀

Read more

用 10% GPU 跑通万亿参数 RL!马骁腾拆解万亿参数大模型的后训练实战

用 10% GPU 跑通万亿参数 RL!马骁腾拆解万亿参数大模型的后训练实战

整理 | 梦依丹 出品 | ZEEKLOG(ID:ZEEKLOGnews) 左手是提示词的工程化约束,右手是 Context Learning 的自我进化。 在 OpenAI 新发布的《Prompt guidance for GPT-5.4》中,反复提到了 Prompt Contracts(提示词合约)。要求开发者像编写代码一样,严谨地定义 Agent 的输入边界、输出格式与工具调用逻辑,进而换取 AI 行为的确定性。 但在现实操作中,谁又能日复一日地去维护那些冗长、脆弱的“提示词代码”? 真正的 Agent,不应只靠阅读 Context Engineering,更应该具备 Context Learning 的能力。 为此,在 4 月 17-18

By Ne0inhk
当OpenClaw引爆全网,谁来解决企业AI Agent的“落地焦虑”?

当OpenClaw引爆全网,谁来解决企业AI Agent的“落地焦虑”?

2026 年 3 月,开源 AI Agent 框架 OpenClaw 在 GitHub 上的星标突破28万,并一度超越 React,成为 GitHub 最受关注的软件项目之一。短时间内,开发者利用它构建了大量实验性应用:从全栈开发辅助,到自动化营销脚本,再到桌面操作自动化,AI Agent 的能力边界正在迅速被拓展。 这股热潮也带动了另一个趋势——本地部署与算力硬件需求的快速增长。越来越多开发者尝试在个人设备或企业服务器上运行 Agent 系统,以获得更高的控制权和数据安全性。 从表面上看,AI Agent 似乎正从“概念验证”走向更广泛的开发实践。但在企业环境中,情况却没有想象中乐观。当企业负责人开始追问—— “它能直接解决我的业务问题吗?” 很多演示级产品仍难以给出令人满意的答案。 如何让 Agent 真正融入企业既有系统、适配复杂业务流程,正成为大模型产业落地必须跨越的一道门槛。 与此同时,中国不同城市的产业结构差异明显:互联网、

By Ne0inhk
遭“美国政府封杀”后,Anthropic正式提起诉讼!

遭“美国政府封杀”后,Anthropic正式提起诉讼!

整理 | 苏宓 出品 | ZEEKLOG(ID:ZEEKLOGnews) 据路透社报道,当地时间周一,AI 初创公司 Anthropic 正式对美国国防部及特朗普政府提起诉讼,抗议五角大楼将其列为“国家安全供应链风险”主体的决定。 Anthropic 在向美国加州北区地方法院提交的诉讼文件中表示,这一认定“史无前例且非法”,已对公司造成“不可挽回的损害”。公司希望法院撤销该决定,并指示联邦机构停止执行相关认定。 划定 AI 应用红线,双方观点不一 正如我们此前报道,这场争端的核心在于 Anthropic 为其核心 AI 模型 Claude 设定的两条技术使用红线,与美国国防部的使用需求发生根本冲突。 此前,Anthropic 曾与五角大楼签署一份价值最高可达 2 亿美元的合作合同,Claude 也成为少数被纳入美国机密网络环境进行测试的 AI 系统之一。 对此,Anthropic 一直坚持两条底线: * Claude 等技术不得被用于对美国民众的大规模国内监控;

By Ne0inhk
二手平台出现OpenClaw卸载服务,299元可上门“帮卸”;2026年春招AI人才身价暴涨:平均月薪超6万;Meta辟谣亚历山大·王离职 | 极客头条

二手平台出现OpenClaw卸载服务,299元可上门“帮卸”;2026年春招AI人才身价暴涨:平均月薪超6万;Meta辟谣亚历山大·王离职 | 极客头条

「极客头条」—— 技术人员的新闻圈! ZEEKLOG 的读者朋友们好,「极客头条」来啦,快来看今天都有哪些值得我们技术人关注的重要新闻吧。(投稿或寻求报道:[email protected]) 整理 | 苏宓 出品 | ZEEKLOG(ID:ZEEKLOGnews) 一分钟速览新闻点! * 微信员工辟谣“小龙虾可自动发红包”:不要以讹传讹 * 蚂蚁集团启动春招,超 70% 为 AI 相关岗位 * 受贿 208 万!拼多多一员工被抓 * 2026 年春招 AI 人才身价暴涨: 平均月薪超 6 万元 * 二手平台出现 OpenClaw 上门卸载服务 * 权限太高,国家互联网应急中心发布 OpenClaw 安全应用的风险提示 * 字节豆包内测 AI 电商功能:无需跳转抖音,日活用户数超

By Ne0inhk