欢迎来到我的频道 【点击跳转专栏】
码云链接 【点此转跳】
文章目录
- [0. fork常规用法回顾](#0. fork常规用法回顾)
- [1. 进程替换原理](#1. 进程替换原理)
- [2. 替换函数的接口](#2. 替换函数的接口)
- [2.1 execl](#2.1 execl)
- [2.2 execlp](#2.2 execlp)
- [2.3 execv](#2.3 execv)
- [2.3.1 加餐:深度解析 char *const argv[]和const char* argv[]](#2.3.1 加餐:深度解析 char const argv[]和const char argv[])
- [2.4 execvp](#2.4 execvp)
- [2.5 execvpe](#2.5 execvpe)
- [2.5.1 putenv](#2.5.1 putenv)
- [2.5.2 结论](#2.5.2 结论)
- [2.6 execle](#2.6 execle)
- [2.7 execve(系统调用)](#2.7 execve(系统调用))
- [2.7.1 execve 与其他函数的对比](#2.7.1 execve 与其他函数的对比)
- [3. 自主shell](#3. 自主shell)
0. fork常规用法回顾
我们之前使用fork创建子进程大多是一个父进程希望复制自己,使父子进程同时执行不同的代码段。但是更多⼀个进程要执⾏⼀个不同的程序。例如⼦进程从fork返回后,调⽤exec函数!使用进程的程序替换来完成这个功能!
1. 进程替换原理
先看现象:
cpp
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("我变成了一个进程:%d\n",getpid());
//执行另一个程序的代码
execl("/usr/bin/ls","ls","-a","-l",NULL);//程序替换函数
printf("我的代码运行中....\n");
printf("我的代码运行中....\n");
printf("我的代码运行中....\n");
printf("我的代码运行中....\n");
printf("我的代码运行中....\n");
}
我么发现
execl后面的printf直接就不执行了,程序之间变成了执行ls -l -a,这就是进程的程序替换
接下来 我们来讲原理层面:
- 进程替换后,有没有创建新的进程?
其实这个答案很简单,进程替换的本质其实是 在物理内存将原本的代码和数据段替换成
ls的,而我们看是否创建新进程的关键是看它的pid,而pid存放在PCB中,而PCB是没有受到影响的,所以并没有创建新的进程。
- 那么进程替换的本质是什么?
进程替换的本质就是把代码和数据拷贝到内存中,然后对应页表的映射关系会发生变化,同时虚拟内存也会根据替换的代码和数据变化程度,进行相应的大小调整
- bash的工作原理
结合上面的知识 我们知道程序在运行前必须要加载到内存中,众所周知,我们的代码或者是
ls等指令,他们的父进程都是bash!那么bash真实创建子进程(新程序)的方式其实也不难想象:bash先fork一个子进程,然后再通过excl将新的程序从硬盘加载到内存中!
- 那么谁有权限把硬盘的数据加载到内存中?
答案是 操作系统!所以OS 必须提供对应系统调用来完成这个工作,所以也不难得知,
execl就是 封装了系统调用的一个函数!
- 那么程序替换可不可以把
shell、java、python这样的语言给调用起来呢?其实这跟语言没关系,程序替换的本质 是替换的可执行程序 而不是替换的源文件!
2. 替换函数的接口
关于替换函数的接口,一共有7个,一个在2号手册中是系统调用;另外6个是在三号手册中的库函数:
二号手册单独的:
cpp
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
int execve(const char *path, char *const argv[], char *const envp[]);
程序替换函数,部分参数确实可以省略 但是本身不复杂 所以不建议省略!
如果所有exec*的返回值是什么?
如果替换成功 则无返回值,替换失败返回
-1同时errno被设置!
那么什么时候会替换失败呢?
很简单 你路径写错 选项写错都会失败
但是我们一般不需要通过返回值写判断
因为一旦替换失败 这个函数就会继续往前走 我们直接
exit()退出即可!
2.1 execl
c
#include <unistd.h>
int execl(const char *path, const char *arg, ... /* (char *) NULL */);
path: 这是一个字符串,指定了要执行程序的完整路径 。例如,要执行ls命令,必须写成"/bin/ls",execl不会自动在PATH环境变量中搜索。arg: 这是传递给新程序的命令行参数列表。- 第一个参数通常是程序名本身。
- 后续参数是具体的命令行选项。
- 参数列表必须以
(char *)NULL结尾,以标识参数结束。
- 进程替换,而非创建 :
execl不会创建新的进程。它只是在当前进程的地址空间里加载新程序,覆盖掉旧的程序内容。因此,调用execl前后,进程 ID (PID) 保持不变。- 成功则不返回 :如果
execl调用成功,当前进程的程序代码就被完全替换,新程序将从它的main函数开始执行。因此,execl成功时没有返回值,其后的代码不会被执行。- 失败则返回 :只有当调用失败时(例如文件路径错误或权限不足),
execl才会返回-1,并设置errno变量来指示错误类型。
l(list) :表示它使用可变参数列表的方式来传递命令行参数
如果用图画 类似是这样的!
cpp
[ "/bin/ls" ] -> [ "ls" ] -> [ "-l" ] -> [ "-a" ] -> [ NULL ]
当你的程序运行起来,CPU 真正执行 execl 时,操作系统会把这一串参数收集起来,打包成一个指针数组(也就是 char *argv[])
cpp
// 内存中的真实结构
char *argv[] = {
"/bin/ls", // argv[0]
"ls", // argv[1]
"-l", // argv[2]
"-a", // argv[3]
NULL // argv[4] (结束标志)
};
这才是它真实的物理长相:一个连续的内存块,里面存了一堆地址。
作为程序员,你在写代码时,是一个一个列出来的(List them out)。你不需要自己先创建一个数组,而是直接罗列参数。这种"罗列"的行为,很像是在列清单(List),所以我们叫它是链表存储。
execl的"链表"只是一个逻辑上的概念,指的是你罗列参数的方式。在计算机内存的深处,它们最终都会变成整齐的数组。
2.2 execlp
execlp 是 Linux/Unix 系统编程中 exec 函数族的一员。它的核心功能是用一个新的程序替换当前进程的映像。
它与 execl 最大的区别在于:execlp 会自动在 PATH 环境变量中查找可执行文件,因此你不需要提供程序的完整路径。
c
#include <unistd.h>
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
file: 这是要执行的程序名称 。execlp会根据PATH环境变量自动在指定的目录中搜索这个文件。例如,你可以直接写"ls",而不必写成"/bin/ls"。(也就是说它会在环境变量表中的PATH变量里面的路径里面去找)arg: 这是传递给新程序的命令行参数列表。- 第一个参数通常是程序名本身 (即
file参数)。 - 后续参数是具体的命令行选项。
- 参数列表必须以
(char *)NULL结尾,以标识参数结束。
- 第一个参数通常是程序名本身 (即
- 自动搜索路径 (
pfor PATH) :这是execlp的标志性特点。它会像 Shell 一样,在PATH变量定义的目录列表中查找可执行文件。- 进程替换 :和
execl一样,execlp不会创建新进程,而是将当前进程的代码、数据、堆栈等完全替换为新程序的内容。进程 ID (PID) 保持不变。- 成功则不返回 :如果调用成功,当前进程的程序就被新程序完全覆盖,
execlp之后的代码将不会被执行。- 失败则返回 :只有当调用失败时(例如在
PATH中找不到文件),execlp才会返回-1,并设置errno变量来指示错误类型。
cpp
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
printf("我变成了一个进程:%d\n",getpid());
pid_t id=fork();
if(id==0)
{
sleep(2);
printf("下面代码都是子进程在执行:\n");
//执行另一个程序的代码
execlp("ls","ls","-a","-l","-n",NULL);
// -n 就是把用户信息变成数字
exit(1);
}
int status=0;
pid_t rid=waitpid(id,&status,0);
if(rid>0)
{
printf("wait success, exit code: %d\n", WEXITSTATUS(status));
}
}
我们发现在没有相对路径的情况下 依然成功执行了代码:
有个问题:
execlp("ls","ls","-a","-l","-n",NULL);这里有两个
ls重复吗?不重复 第一个
ls表示你要执行谁 第二个ls表示你要怎么执行!
execlp("ls",,"-l","-n",NULL);当然这么写也是对的 但是这是一种省略写法没必要记!反而容易出错!!
2.3 execv
c
#include <unistd.h>
int execv(const char *pathname, char *const argv[]);
pathname:- 这是一个字符串,指向要执行的程序的完整路径。
- 例如:
"/bin/ls"或"/usr/bin/python3"。它不会像 Shell 那样自动在PATH环境变量中搜索。
argv:- 这是一个
字符串指针数组,用于向新程序传递命令行参数。 - 这个数组必须以
NULL指针结尾。 - 按照惯例,数组的第一个元素
argv[0]应该是程序本身的名称。
- 这是一个
- 成功时 :不会返回 。当前进程的映像被新程序替换,从新程序的
main函数开始执行。- 失败时 :返回
-1,并设置全局变量errno来指示具体的错误原因(如文件不存在、权限不足等)。
vvsl:execv使用一个指针数组来传递参数,而execl则是将参数一个一个列出来(以链表的形式)。
execv 是构建 Shell、容器技术(如 Docker)等复杂系统的基础机制之一。
| 特性 | execl (List) | execv (Vector) |
|---|---|---|
| 你写代码的样子 | execl(path, arg1, arg2, NULL) |
execv(path, argv_array) |
| 形象比喻 | 报菜名:红烧肉、鱼香肉丝、米饭,没了。 | 递菜单:给,这是一张写好的菜单(数组)。 |
| 内存里的真身 | 最终还是会被组装成数组 | 本来就是数组 |
| 适用场景 | 参数固定,写死在代码里很方便。 | 参数数量不确定(比如 Shell 接收用户输入),必须用数组。 |
cpp
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
printf("我变成了一个进程:%d\n",getpid());
pid_t id=fork();
if(id==0)
{
sleep(2);
printf("下面代码都是子进程在执行:\n");
//执行另一个程序的代码
char* argv[]={
(char*)"ls",
(char*)"-a",
(char*)"-l",
NULL
};
execv("/usr/bin/ls",argv);
// -n 就是把用户信息变成数字
exit(1);
}
int status=0;
pid_t rid=waitpid(id,&status,0);
if(rid>0)
{
printf("wait success, exit code: %d\n", WEXITSTATUS(status));
}
}
上图:这么写会报警 那是因为
"ls"是字符串常量!
强转一下就没事了!
效果:
那么这个
argv传给了谁?本质就是传给了
"ls"的main中的命令行参数!!
2.3.1 加餐:深度解析 char const argv[]和const char argv[]
这两个类型的核心区别在于:const 修饰的位置不同,导致"不可变"的对象也不同。
简单来说:
char *const argv[]:指针本身是常量(不能换地址),但指向的内容可以改。const char* argv[]:指向的内容是常量(不能改字符),但指针本身可以换地址。
char *const argv[] (execv 的用法)
为什么 execv 要这样设计?
execv需要读取你传入的参数数组来启动新程序。
- 防止篡改指针 :操作系统需要确保你传给它的参数列表结构是稳定的。如果允许
execv内部修改argv[1]的指向,可能会导致逻辑混乱(比如把参数"-l"偷偷换成了"hello")。- 允许修改内容:在某些底层实现或特殊场景下,被调用的程序可能需要临时修改参数内容的缓冲区(虽然不常见,但 C 语言保留了这种可能性)。
代码演示:
c
char *args[] = {"ls", "-l", NULL};
// ✅ 合法:修改内容
args[0][0] = 'L'; // 把 "ls" 变成 "Ls",这是允许的
// ❌ 非法:修改指针指向
args[1] = "-a"; // 编译报错!因为 args 里的指针是 const 的
const char* argv[](只读数据)
这通常用于处理字符串常量。
- 含义 :
argv数组里的元素是指向常量字符的指针。 - 你可以做什么 :你可以让指针指向别的地方(例如
argv[1] = other_string)。 - 你不能做什么 :你绝对不能修改字符串的内容(例如不能把
"hello"改成"hallo")。
代码演示:
c
const char *args[] = {"ls", "-l", NULL};
// ❌ 非法:修改内容
args[0][0] = 'L'; // 运行时错误(段错误)!因为 "ls" 在只读内存区
// ✅ 合法:修改指针指向
args[1] = "-a"; // 这是允许的,指针本身不是 const
总结对比表
| 特性 | char *const argv[] |
const char* argv[] |
|---|---|---|
| Const 修饰谁 | 修饰指针本身 (指针的值即指向的地址) | 修饰指向的内容 (字符串字符) |
| 能改字符串内容吗 | ✅ 能 (如 args[0][0] = 'A') |
❌ 不能 (会导致崩溃) |
| 能换指针指向吗 | ❌ 不能 (如 args[1] = "x") |
✅ 能 |
2.4 execvp
c
#include <unistd.h>
int execvp(const char *file, char *const argv[]);
file:- 这是要执行的文件名 (例如
"ls"、"gcc")。 - 关键点 :你不需要写
/bin/ls,只需要写ls。execvp会自动去PATH环境变量里找ls在哪里。
- 这是要执行的文件名 (例如
argv:- 这是一个字符串指针数组,用于传递命令行参数。
- 关键点 :数组的最后一个元素必须是
NULL。 - 按照惯例,
argv[0]应该是程序名本身。
返回值
- 成功时 :不返回 。当前进程被新程序完全覆盖,从新程序的
main函数开始执行。- 失败时 :返回
-1,并设置errno错误码(例如文件没找到、权限不足)。
| 特性 | execv | execvp |
|---|---|---|
| 路径搜索 | 不搜索 。必须提供完整路径(如 /bin/ls)。 |
自动搜索 。只需提供文件名(如 ls),系统去 PATH 找。 |
| 灵活性 | 较低。如果程序换了安装位置,代码就得改。 | 高。只要环境变量配对了,程序在哪都能找到。 |
| 典型场景 | 执行系统内部固定路径的工具。 | Shell 实现、执行用户输入的命令(因为用户通常只输命令名)。 |
2.5 execvpe
c
int execvpe(const char *file, char *const argv[], char *const envp[]);
file: 要执行的程序文件名。execvpe会根据PATH环境变量在指定的目录中搜索该文件。argv: 传递给新程序的命令行参数数组。这个数组的最后一个元素必须是空指针NULL。envp: 传递给新程序的环境变量数组。这个数组的最后一个元素也必须是空指针NULL。
- 成功时:函数不会返回,因为当前进程的映像已经被新程序完全替换。
- 失败时 :函数返回 -1,并设置全局变量
errno来指示错误原因。
环境变量
execvpe 与 execvp 的主要区别在于,execvpe 允许你通过 envp 参数为新程序指定一套全新的环境变量,而不是继承调用进程的环境变量。
- 当你想使用父进程自带的环境变量
可以通过
extern char** environ直接传递environ
- 当你想使用自己的环境变量
你可以自己写一个
myenv[]然后传递进函数!而我们传递自己环境变量的本质,其实就是对系统环境变量做的一个覆盖!
这是一个在 Linux 环境下使用 execvpe 的简单示例:
这是一个我们自己写的程序
othercmd:
cpp
#include <iostream>
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
for(int i = 0; i < argc; i++)
{
printf("argv[%d]: %s\n", i, argv[i]);
}
std::cout << "\r\n";
for(int j = 0; env[j]; j++)
{
printf("env[%d]->%s\n", j, env[j]);
}
std::cout << "我是自己的C++程序!" << std::endl;
return 0;
}
效果如下:
1 . 使用自己的环境变量:
cpp
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
printf("我变成了一个进程:%d\n",getpid());
pid_t id=fork();
if(id==0)
{
sleep(2);
printf("下面代码都是子进程在执行:\n");
//执行另一个程序的代码
char* argv[]={
(char*)"othercmd",
(char*)"-a",
(char*)"-b",
NULL
};
char *env[]={
(char*)"PATH=/home/fcy/linux_code/lesson22"
,NULL
};
extern char** environ;
execvpe("othercmd",argv,env);
exit(1);
}
int status=0;
pid_t rid=waitpid(id,&status,0);
if(rid>0)
{
printf("wait success, exit code: %d\n", WEXITSTATUS(status));
}
}
运行结果:
2.5.1 putenv
当你又想使用系统的环境变量表,并且添加一些新的环境变量,可以使用
putenv函数!
putenv 是一个用于动态修改或添加当前进程环境变量的 C 语言标准库函数。它定义在 <stdlib.h> 头文件中。

c
int putenv(char *string);
string: 一个格式为"name=value"的字符串。name: 要设置或修改的环境变量名。value: 环境变量的新值。
- 成功时:返回 0。
- 失败时 :返回 -1,并设置全局变量
errno来指示错误原因(例如ENOMEM表示内存不足)。
- 作用范围
putenv函数仅影响调用它的当前进程 的环境变量。它不会修改父进程或操作系统的全局环境设置。由当前进程通过fork创建的子进程会继承修改后的环境变量。- 内存管理 (关键)
putenv函数不会复制 你传入的字符串,而是直接将这个字符串指针添加到环境变量数组中。这意味着:
- 你不能传入一个栈上的局部数组,因为函数返回后该内存会失效。
- 如果你后续修改了这个字符串的内容,环境变量的值也会随之改变。
- 你不能释放这个字符串的内存,否则环境变量会指向一块无效内存。
- 推荐做法 :通常建议使用更安全的
setenv函数来代替,因为它会复制字符串,避免了这些内存管理陷阱。- 删除环境变量
删除环境变量的方法在不同系统上存在差异:
- 在 Linux/Unix 上 :
putenv通常不用于删除变量,应使用unsetenv函数。
案例:
cpp
//把这一块代码进行些修改
extern char** environ;
putenv((char*)"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/fcy/linux_code/lesson22");
execvpe("othercmd",argv,envion);
效果:
2.5.2 结论
结合上面的内容,其实我们可以得到一个结论:命令行参数表和环境变量表,都是父进程通过
exec*给你传递的!
2.6 execle
c
#include <unistd.h>
int execle(const char *path, const char *arg0, ..., char * const envp[]);
path: 要执行的程序的完整路径 。与execlp或execvp不同,execle不会 在PATH环境变量中搜索程序,所以你必须提供准确的路径(如"/bin/ls"或"./myapp")。arg0, ...: 这是传递给新程序的命令行参数列表。arg0通常是程序名本身。- 后续参数是具体的命令行选项。
- 这个列表必须 以一个空指针
NULL结尾。
envp: 这是一个指向字符串数组的指针,数组中的每个字符串都是一个"KEY=VALUE"格式的环境变量。这个数组也必须 以NULL结尾。新程序将使用这个全新的环境,而不是继承调用进程的环境。
- 成功时 :函数不会返回。因为当前进程的代码、数据、堆栈等已被新程序完全覆盖。
- 失败时 :函数返回
-1,并设置全局变量errno来指示错误原因(如ENOENT文件未找到,EACCES权限不足等)。
2.7 execve(系统调用)

execve是 Linux/Unix 系统中最基础、最核心 的系统调用,它是整个exec函数族(包括execl,execvp,execle等)的底层实现基础 。如果说
execl和execvp是为了方便程序员使用的"高级封装",那么execve就是直接和操作系统内核打交道的"原生接口"。
c
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
filename:- 要执行的程序的完整路径 (例如
"/bin/ls"或"./myapp")。 - 注意 :
execve不会 像execlp那样去PATH环境变量里查找文件,你必须指定确切的路径。
- 要执行的程序的完整路径 (例如
argv:- 传递给新程序的命令行参数数组。
argv[0]通常是程序名本身。- 数组必须 以
NULL指针结尾。
envp:- 传递给新程序的环境变量数组(例如
{"PATH=/bin", "HOME=/home/user", NULL})。 - 数组必须 以
NULL指针结尾。 - 新程序将使用这个全新的环境,不会继承当前进程的环境变量
- 传递给新程序的环境变量数组(例如
- 成功时 :不会返回。当前进程的代码段、数据段、堆栈等会被新程序完全覆盖("夺舍")。
- 失败时 :返回
-1,并设置errno错误码(如ENOENT文件未找到,EACCES权限不足)。
2.7.1 execve 与其他函数的对比
execve 是系统调用,其他函数(如 execl, execvp)都是 C 库函数,它们最终都会调用 execve 来完成任务。
| 函数 | 参数形式 | 是否自动搜索 PATH | 是否自定义环境 | 底层实现 |
|---|---|---|---|---|
execve |
数组 (v) |
否 | 是 | 系统调用 |
execl |
列表 (l) |
否 | 否 | 调用 execve |
execle |
列表 (l) |
否 | 是 | 调用 execve |
execlp |
列表 (l) |
是 (p) |
否 | 调用 execve |
execv |
数组 (v) |
否 | 否 | 调用 execve |
execvp |
数组 (v) |
是 (p) |
否 | 调用 execve |
⚠️:在底层封装中,如果没有传环境变量,在execve中默认传environ,这也就是为什么 分装的6个库函数在使用手册中 都与entern char** environ高度绑定的原因!
总结:
如果你需要完全控制 新程序的环境变量(例如为了安全或隔离),或者你正在编写底层的系统代码,直接使用 execve 是最佳选择。如果你只是想在脚本中简单运行一个命令,使用 库函数!
3. 自主shell
当然光说 肯定是空洞的 我们还是得写一个自主
shell来好好感受一下程序替换的魅力!!篇幅原因 请期待














