
文章目录
-
- [一、先破误区:进程替换不是 "创建新进程"](#一、先破误区:进程替换不是 “创建新进程”)
-
-
- [代码演示:单进程执行 exec,验证 PID 不变](#代码演示:单进程执行 exec,验证 PID 不变)
-
- [二、exec 系列函数:6 个函数的 "命名密码" 与用法](#二、exec 系列函数:6 个函数的 “命名密码” 与用法)
-
- [2.1 先记 "命名密码":l、v、p、e 分别代表什么?](#2.1 先记 “命名密码”:l、v、p、e 分别代表什么?)
- [2.2 6 个 exec 函数的原型与基础用法](#2.2 6 个 exec 函数的原型与基础用法)
-
- [1. execlp:列表参数 + 自动查 PATH(最常用之一)](#1. execlp:列表参数 + 自动查 PATH(最常用之一))
- [2. execvp:数组参数 + 自动查 PATH(最常用之一)](#2. execvp:数组参数 + 自动查 PATH(最常用之一))
- [3. execl:列表参数 + 手动写全路径](#3. execl:列表参数 + 手动写全路径)
- [4. execv:数组参数 + 手动写全路径](#4. execv:数组参数 + 手动写全路径)
- [5. execle:列表参数 + 手动路径 + 自定义环境](#5. execle:列表参数 + 手动路径 + 自定义环境)
- [6. execvpe:数组参数 + 自动查 PATH + 自定义环境](#6. execvpe:数组参数 + 自动查 PATH + 自定义环境)
- [2.3 6 个 exec 函数的核心对比表](#2.3 6 个 exec 函数的核心对比表)
- [三、进程替换的底层原理:虚拟地址空间的 "大换血"](#三、进程替换的底层原理:虚拟地址空间的 “大换血”)
-
- [3.1 虚拟地址空间的变化过程](#3.1 虚拟地址空间的变化过程)
- [3.2 为什么 exec 成功后没有返回值?](#3.2 为什么 exec 成功后没有返回值?)
- [四、实战核心:fork + exec + wait 的组合(Shell 的核心逻辑)](#四、实战核心:fork + exec + wait 的组合(Shell 的核心逻辑))
-
- [4.1 完整代码示例:模拟 Shell 执行 ls 命令](#4.1 完整代码示例:模拟 Shell 执行 ls 命令)
- [4.2 运行效果与核心逻辑解析](#4.2 运行效果与核心逻辑解析)
- 五、扩展知识点:实战中的常见问题与解决方案
-
- [5.1 问题 1:exec 执行脚本(Python/Shell)失败](#5.1 问题 1:exec 执行脚本(Python/Shell)失败)
- [5.2 问题 2:如何追加环境变量(而非覆盖)?](#5.2 问题 2:如何追加环境变量(而非覆盖)?)
- [5.3 问题 3:exec 失败后子进程必须退出](#5.3 问题 3:exec 失败后子进程必须退出)
- 六、总结与下一篇预告
上一篇我们解决了 "子进程如何回收" 的问题 ------ 通过 wait/waitpid避免僵尸进程并获取退出信息。但实际开发中,子进程很少会执行和父进程相同的代码:比如 Shell 创建子进程后,要执行 ls、 cd等全新命令;服务器创建子进程后,要执行处理请求的专用程序。这就需要 进程替换 技术 ------ 让子进程 "脱胎换骨",用新程序的代码和数据覆盖自己的地址空间,却不改变进程身份(PID 不变)。今天我们就深入讲解 exec 系列函数的用法、原理和实战场景,搞懂进程替换如何让子进程 "改头换面"。
一、先破误区:进程替换不是 "创建新进程"
在讲具体函数前,必须先纠正一个常见误区:进程替换不会创建新进程。
我们回顾进程的本质:进程 = 内核数据结构(PCB/task_struct + 页表) + 用户态代码/数据。进程替换的核心是:
- 保留内核数据结构(PID、进程组、打开的文件描述符等不变);
- 彻底替换用户态资源(代码段、数据段、堆、栈被新程序覆盖);
- 从新程序的 "启动入口"(如
main函数)开始执行,原进程的剩余代码不再执行。
举个通俗的例子:进程就像 "演员",PID 是演员的 "身份证",用户态代码 / 数据是 "剧本"。进程替换相当于 "演员不换(身份证不变),但换了一本全新的剧本,只演新剧本的内容"------ 不是换了个演员,而是同一个演员换了要演的内容。
代码演示:单进程执行 exec,验证 PID 不变
我们用一个简单例子,看 exec 替换后 PID 是否变化:
c
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
printf("替换前:进程PID = %d,即将执行ls命令\n", getpid());
// 调用execlp替换为ls命令(ls会列出当前目录文件)
// 若替换成功,下面的printf不会执行(代码被覆盖)
int ret = execlp("ls", "ls", "-l", NULL);
if (ret == -1) { // 只有替换失败才会执行到这里
perror("execlp失败");
exit(1);
}
// 替换成功后,这行代码永远不会执行
printf("替换后:这行不会打印\n");
return 0;
}
编译运行(gcc exec_demo1.c -o exec_demo1 && ./exec_demo1),会看到类似输出:
plaintext
plaintext
替换前:进程PID = 12345,即将执行ls命令
total 16
-rwxrwxr-x 1 ubuntu ubuntu 8960 10月 1 10:00 exec_demo1
-rw-rw-r-- 1 ubuntu ubuntu 456 10月 1 09:59 exec_demo1.c
然后用echo $?查看退出码(ls 执行成功退出码为 0),再用ps -ef | grep 12345查看 ------ 会发现 PID 为 12345 的进程已经消失(ls 执行完就退出了),但替换过程中 PID 始终是 12345,没有新建进程。
二、exec 系列函数:6 个函数的 "命名密码" 与用法
Linux 提供了 6 个以exec开头的函数(统称 exec 函数簇),它们功能相似但参数格式不同。很多初学者会被这些函数的名字和参数搞混,其实只要掌握 "命名规则",就能轻松区分 ------ 每个函数名的后缀(l/v/p/e)都对应特定含义。
2.1 先记 "命名密码":l、v、p、e 分别代表什么?
exec 函数簇的命名遵循严格规则,后缀字母对应参数格式或功能,我们先拆解这 4 个关键字母:
| 后缀字母 | 英文全称 | 核心含义 |
|---|---|---|
l |
List | 参数采用 "列表形式"(可变参数,最后必须以NULL结尾,标记参数结束) |
v |
Vector | 参数采用 "数组形式"(传入字符指针数组,最后一个元素必须是NULL) |
p |
Path | 自动从环境变量PATH中查找新程序的路径,无需手动写全路径(如ls而非/bin/ls) |
e |
Environment | 自定义环境变量(传入环境变量数组,覆盖进程默认继承的环境变量) |
根据这 4 个字母的组合,6 个 exec 函数的关系如下(核心是execve,其他 5 个都是它的封装):
- 无
e:使用进程默认继承的环境变量(从父进程继承,如PATH、HOME); - 有
e:必须手动传入环境变量数组,覆盖默认环境; - 无
p:必须写新程序的完整路径 (如/bin/ls); - 有
p:只需写程序名(如ls),自动查PATH。
2.2 6 个 exec 函数的原型与基础用法
我们按 "常用程度" 排序,逐一讲解每个函数的原型、参数含义和代码示例,重点关注execlp、execvp(日常开发最常用)。
1. execlp:列表参数 + 自动查 PATH(最常用之一)
原型:
c
c
#include <unistd.h>
int execlp(const char *file, const char *arg, ...);
file:新程序的名称(如ls、ps),会自动从PATH查找路径;arg:命令行参数列表(第一个参数必须是程序名,最后以NULL结尾);- 返回值:只有失败返回 - 1(成功无返回,代码已覆盖)。
代码示例:用 execlp 执行 ls -l 命令
c
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
printf("执行execlp(ls -l),PID = %d\n", getpid());
// 参数说明:
// 1. "ls":要执行的程序名(自动查PATH找到/bin/ls)
// 2. "ls":第一个命令行参数(惯例是程序名)
// 3. "-l":第二个命令行参数(ls的选项)
// 4. NULL:标记参数列表结束
int ret = execlp("ls", "ls", "-l", NULL);
if (ret == -1) {
perror("execlp执行ls失败"); // 失败原因:如命令不存在、权限不足
exit(1);
}
printf("替换成功后不会执行这行\n");
return 0;
}
关键注意点 :参数列表必须以NULL结尾,否则 exec 函数会读取到垃圾数据,导致执行失败。
2. execvp:数组参数 + 自动查 PATH(最常用之一)
当命令行参数较多时,用 "列表形式"(execlp)会写很多参数,而 "数组形式"(execvp)更简洁 ------ 把参数存到数组里,直接传入函数。
原型:
c
c
#include <unistd.h>
int execvp(const char *file, char *const argv[]);
file:同 execlp,程序名(自动查 PATH);argv:字符指针数组(每个元素是命令行参数,最后一个元素是NULL);- 返回值:同 execlp,失败返回 - 1。
代码示例:用 execvp 执行 ps -ef 命令
c
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
// 命令行参数数组:ps -ef,最后一个元素必须是NULL
char *const argv[] = {
"ps", // 第一个参数:程序名
"-ef", // 第二个参数:选项
NULL // 标记数组结束
};
printf("执行execvp(ps -ef),PID = %d\n", getpid());
int ret = execvp("ps", argv); // 传入程序名和参数数组
if (ret == -1) {
perror("execvp执行ps失败");
exit(1);
}
printf("替换成功后不会执行这行\n");
return 0;
}
适用场景:参数数量不确定(如从用户输入动态生成参数),用数组存储更灵活。
3. execl:列表参数 + 手动写全路径
execl和execlp的唯一区别是:execl必须写新程序的完整路径 (无p,不查PATH),其他用法完全相同。
原型:
c
c
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
path:新程序的完整路径(如/bin/ls、/usr/bin/ps);arg:同 execlp,参数列表以NULL结尾。
代码示例:用 execl 执行 /bin/ls -a
c
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
// 必须写全路径:/bin/ls,不能只写ls
int ret = execl("/bin/ls", "ls", "-a", NULL);
if (ret == -1) {
perror("execl执行ls失败"); // 若路径写错(如/bin/lss),会提示"没有那个文件或目录"
exit(1);
}
return 0;
}
注意点 :如果路径写错(如把/bin/ls写成/bin/lss),execl 会失败,错误信息是 "No such file or directory"。
4. execv:数组参数 + 手动写全路径
execv和execvp的区别与execl和execlp一致:execv必须写全路径,execvp自动查PATH。
原型:
c
c
#include <unistd.h>
int execv(const char *path, char *const argv[]);
代码示例:用 execv 执行 /usr/bin/ps -aux
c
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
char *const argv[] = {
"ps",
"-aux",
NULL
};
// 写全路径:/usr/bin/ps
int ret = execv("/usr/bin/ps", argv);
if (ret == -1) {
perror("execv执行ps失败");
exit(1);
}
return 0;
}
5. execle:列表参数 + 手动路径 + 自定义环境
execle的后缀有e,表示 "自定义环境变量"------ 需要手动传入环境变量数组,覆盖进程默认继承的环境(如PATH、HOME)。
原型:
c
c
#include <unistd.h>
int execle(const char *path, const char *arg, ..., char *const envp[]);
envp:环境变量数组(每个元素是 "KEY=VALUE" 格式,最后以NULL结尾);- 注意:参数列表必须以
NULL结尾,然后再跟envp(可变参数的特殊处理)。
代码示例:自定义环境变量,执行 echo $MY_ENV我们先写一个简单的 "打印环境变量" 程序(echo_myenv.c),再用 execle 执行它并传入自定义环境:
- 编写 echo_myenv.c(打印 MY_ENV 环境变量):
c
c
#include <stdio.h>
#include <stdlib.h>
int main() {
// 获取环境变量MY_ENV的值
char *my_env = getenv("MY_ENV");
if (my_env) {
printf("MY_ENV = %s\n", my_env);
} else {
printf("MY_ENV未设置\n");
}
return 0;
}
- 编译 echo_myenv:
gcc echo_myenv.c -o echo_myenv; - 用 execle 执行 echo_myenv,传入自定义环境:
c
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
// 自定义环境变量数组(KEY=VALUE格式,最后NULL结尾)
char *const envp[] = {
"MY_ENV=hello_execle",
"PATH=/bin:/usr/bin", // 必须设置PATH,否则新程序依赖的命令可能找不到
NULL
};
// 执行echo_myenv,传入自定义环境
int ret = execle(
"./echo_myenv", // 全路径(当前目录下的程序)
"echo_myenv", // 第一个参数:程序名
NULL, // 参数列表结束
envp // 自定义环境变量数组
);
if (ret == -1) {
perror("execle执行失败");
exit(1);
}
return 0;
}
运行结果:
plaintext
plaintext
MY_ENV = hello_execle
关键注意点 :自定义环境变量时,建议包含PATH------ 否则新程序中若执行其他命令(如ls),会因找不到路径而失败。
6. execvpe:数组参数 + 自动查 PATH + 自定义环境
execvpe是 "数组参数(v)+ 自动查 PATH(p)+ 自定义环境(e)" 的组合,用法是 execvp 和 execle 的结合。
原型:
c
c
#include <unistd.h>
int execvpe(const char *file, char *const argv[], char *const envp[]);
代码示例:用 execvpe 执行 echo_myenv,自动查 PATH + 自定义环境 假设我们把echo_myenv放到/usr/local/bin(该目录在PATH中),则无需写全路径:
c
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
char *const argv[] = {
"echo_myenv",
NULL
};
char *const envp[] = {
"MY_ENV=hello_execvpe",
"PATH=/bin:/usr/bin:/usr/local/bin", // 包含echo_myenv所在路径
NULL
};
// 只需写程序名"echo_myenv",自动查PATH
int ret = execvpe("echo_myenv", argv, envp);
if (ret == -1) {
perror("execvpe执行失败");
exit(1);
}
return 0;
}
运行结果:
plaintext
plaintext
MY_ENV = hello_execvpe
2.3 6 个 exec 函数的核心对比表
为了方便查阅,我们用表格总结 6 个函数的关键差异,帮你快速选择合适的函数:
| 函数名 | 参数格式 | 是否需全路径 | 是否自定义环境 | 核心场景 |
|---|---|---|---|---|
| execl | 列表 | 是 | 否(默认继承) | 简单命令,参数少且固定 |
| execlp | 列表 | 否(查 PATH) | 否(默认继承) | 简单命令,不想写全路径(如 ls) |
| execle | 列表 | 是 | 是(覆盖) | 需自定义环境,参数少 |
| execv | 数组 | 是 | 否(默认继承) | 参数多或动态生成,需全路径 |
| execvp | 数组 | 否(查 PATH) | 否(默认继承) | 参数多,不想写全路径(最常用) |
| execvpe | 数组 | 否(查 PATH) | 是(覆盖) | 需自定义环境,参数多 |
选择口诀 :参数少用l,参数多用v;不想写路径用p,自定义环境用e。
三、进程替换的底层原理:虚拟地址空间的 "大换血"
我们结合之前讲的 "虚拟地址空间" 知识,拆解进程替换时内核和用户态的变化 ------ 为什么代码被覆盖后,原进程的代码就不执行了?
3.1 虚拟地址空间的变化过程
进程替换的核心是 "覆盖用户态地址空间的所有区域",具体过程如下(以 32 位系统为例):
- 读取新程序的 ELF 文件 :exec 函数会先从磁盘读取新程序的 ELF 可执行文件(如
/bin/ls),解析其代码段、数据段的大小和位置。 - 释放原进程的用户态资源:内核释放原进程虚拟地址空间中 "代码段、数据段、堆、栈" 的映射关系,回收对应的物理内存(若其他进程不共享)。
- 分配新的虚拟地址区域:根据 ELF 文件的解析结果,在进程的虚拟地址空间中,为新程序的 "代码段、数据段、堆、栈" 划分对应的虚拟地址范围(如代码段从 0x08048000 开始)。
- 建立新的页表映射:内核为新程序的代码段、数据段分配物理内存页,建立 "虚拟地址→物理地址" 的页表映射(代码段标记为 "只读 + 可执行",数据段标记为 "可读 + 可写")。
- 设置程序计数器(PC) :将 CPU 的程序计数器(PC)指向新程序的 "入口地址"(ELF 文件中定义的
_start函数,最终会调用main)。
至此,进程的用户态资源已完全替换 ------ 下一条执行的指令是新程序的入口,原进程的代码和数据再也不会被执行(已被释放或覆盖)。
3.2 为什么 exec 成功后没有返回值?
这是初学者最常问的问题 ------ 答案就藏在上述过程中:
- 若 exec 替换成功,原进程的代码段已被新程序覆盖,
exec函数本身的返回指令(原代码的一部分)也被删除了,根本无法返回任何值; - 只有当 exec 替换失败(如找不到程序、权限不足)时,原进程的代码段才没被覆盖,
exec函数才会返回 - 1,让我们能处理错误。
这也是为什么 exec 函数的错误处理很简单:"只要返回,就是失败",无需判断返回值是否为 0。
四、实战核心:fork + exec + wait 的组合(Shell 的核心逻辑)
单独使用 exec 函数意义不大(会覆盖当前进程的代码,比如父进程 exec 后就无法 wait 子进程了)。实际开发中,进程替换几乎都和fork、wait配合使用 ------父进程 fork 子进程,子进程 exec 替换为新程序,父进程 wait 回收子进程,这正是 Shell 执行命令的核心逻辑。
4.1 完整代码示例:模拟 Shell 执行 ls 命令
我们写一个简化版的 "迷你 Shell",实现 "输入 ls,执行 ls 命令" 的逻辑:
c
c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
#define BUF_SIZE 1024
int main() {
char command[BUF_SIZE]; // 存储用户输入的命令
while (1) {
// 1. 打印命令提示符(模拟Shell的[user@host dir]$)
printf("[mini-shell]$ ");
fflush(stdout); // 刷新缓冲区,确保提示符立即显示
// 2. 获取用户输入的命令(如"ls -l")
if (fgets(command, BUF_SIZE, stdin) == NULL) {
perror("获取命令失败");
continue;
}
// 处理换行符:fgets会把回车符\n读入,需替换为\0
command[strcspn(command, "\n")] = '\0';
if (strlen(command) == 0) {
continue; // 空命令,跳过
}
// 3. 拆分命令和参数(简化版:按空格拆分,仅支持单个空格分隔)
char *argv[BUF_SIZE];
int argc = 0;
char *token = strtok(command, " "); // 第一次调用strtok,传入命令字符串
while (token != NULL) {
argv[argc++] = token;
token = strtok(NULL, " "); // 后续调用传入NULL,继续拆分
}
argv[argc] = NULL; // 数组最后必须是NULL
// 4. fork子进程,子进程exec替换,父进程wait回收
pid_t pid = fork();
if (pid == -1) {
perror("fork失败");
continue;
}
if (pid == 0) {
// 子进程:exec替换为新程序(用execvp,自动查PATH)
execvp(argv[0], argv);
// 只有exec失败才会执行到这里
perror("命令执行失败");
exit(1); // 子进程必须退出,否则会继续执行父进程的循环
} else {
// 父进程:wait回收子进程,避免僵尸进程
int status;
waitpid(pid, &status, 0);
// 可选:打印子进程退出码
if (WIFEXITED(status)) {
printf("命令执行完毕,退出码 = %d\n", WEXITSTATUS(status));
}
}
}
return 0;
}
4.2 运行效果与核心逻辑解析
编译运行(gcc mini_shell.c -o mini_shell && ./mini_shell),输入ls -l,会看到类似输出:
plaintext
plaintext
[mini-shell]$ ls -l
total 48
-rwxrwxr-x 1 ubuntu ubuntu 8960 10月 1 14:00 exec_demo1
-rw-rw-r-- 1 ubuntu ubuntu 456 10月 1 09:59 exec_demo1.c
-rwxrwxr-x 1 ubuntu ubuntu 8896 10月 1 13:30 echo_myenv
-rw-rw-r-- 1 ubuntu ubuntu 287 10月 1 13:28 echo_myenv.c
-rwxrwxr-x 1 ubuntu ubuntu 9088 10月 1 14:30 mini_shell
-rw-rw-r-- 1 ubuntu ubuntu 1568 10月 1 14:29 mini_shell.c
命令执行完毕,退出码 = 0
核心逻辑拆解:
- 命令输入与解析 :用
fgets获取用户输入,strtok按空格拆分命令和参数(如 "ls -l" 拆分为argv = ["ls", "-l", NULL]); - fork 子进程:父进程保留原代码(继续等待用户输入),子进程准备替换;
- 子进程 exec 替换 :用
execvp执行命令(自动查 PATH,无需写全路径),若失败则打印错误并退出; - 父进程 wait 回收:等待子进程执行完毕,获取退出码,避免僵尸进程。
这正是 Linux 中 bash、zsh 等 Shell 的核心工作流程 ------ 你每天输入的ls、cd、gcc等命令,背后都是这个 "fork→exec→wait" 的循环。
五、扩展知识点:实战中的常见问题与解决方案
5.1 问题 1:exec 执行脚本(Python/Shell)失败
exec 函数不仅能执行 C 语言编译的 ELF 程序,还能执行 Python、Shell 等脚本文件 ------ 但需要注意脚本的 "解释器声明"(第一行#!/usr/bin/env python3或#!/bin/bash),否则 exec 会因 "无法识别可执行格式" 而失败。
代码示例:用 execvp 执行 Python 脚本
- 编写 Python 脚本(test.py):
python
python
#!/usr/bin/env python3 # 关键:声明解释器路径
print("Hello from Python script!")
print("Script PID:", __import__('os').getpid())
- 给脚本加执行权限:
chmod +x test.py; - 用 execvp 执行:
c
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
char *const argv[] = {
"./test.py",
NULL
};
// 执行Python脚本(自动查PATH,脚本有解释器声明)
int ret = execvp("./test.py", argv);
if (ret == -1) {
perror("execvp执行Python脚本失败");
exit(1);
}
return 0;
}
运行结果:
plaintext
plaintext
Hello from Python script!
Script PID: 12345 # 和原进程PID相同,验证未新建进程
5.2 问题 2:如何追加环境变量(而非覆盖)?
exec 函数中带e的函数(execle、execvpe)会覆盖默认环境变量,若想 "追加" 新环境变量(保留原有环境,新增变量),可按以下步骤操作:
- 用
extern char **environ获取当前进程的环境变量数组(environ是全局变量,存储所有环境变量); - 新建一个环境变量数组,先复制
environ的所有元素,再追加新变量; - 将新数组传入 execle/execvpe。
代码示例:追加环境变量 MY_ENV
c
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main() {
extern char **environ; // 获取当前环境变量数组
int env_len = 0;
// 1. 计算原有环境变量的数量
while (environ[env_len] != NULL) {
env_len++;
}
// 2. 新建环境变量数组(原有数量 + 1个新变量 + 1个NULL)
char *new_env[env_len + 2];
int i;
// 复制原有环境变量
for (i = 0; i < env_len; i++) {
new_env[i] = environ[i];
}
// 追加新环境变量
new_env[i++] = "MY_ENV=append_env";
new_env[i] = NULL; // 标记结束
// 3. 执行echo_myenv,传入追加后的环境
int ret = execle(
"./echo_myenv",
"echo_myenv",
NULL,
new_env
);
if (ret == -1) {
perror("execle失败");
exit(1);
}
return 0;
}
运行结果:
plaintext
plaintext
MY_ENV = append_env # 新变量生效,原有环境变量(如PATH)也保留
5.3 问题 3:exec 失败后子进程必须退出
如果子进程 exec 失败(如命令不存在),子进程会继续执行父进程的代码 ------ 这会导致 "子进程进入父进程的循环,也开始打印提示符、获取命令",出现 "多个提示符重叠" 的错误。
错误示例(子进程未 exit):
c
c
if (pid == 0) {
execvp(argv[0], argv);
perror("命令执行失败");
// 没有exit,子进程会继续执行下面的循环
}
运行错误效果:
plaintext
plaintext
[mini-shell]$ lss # 输入不存在的命令
命令执行失败: No such file or directory
[mini-shell]$ [mini-shell]$ # 两个提示符重叠(父进程和子进程都打印)
解决方案 :exec 失败后,子进程必须调用exit退出,终止执行流:
c
c
if (pid == 0) {
execvp(argv[0], argv);
perror("命令执行失败");
exit(1); // 关键:子进程退出,避免执行父进程代码
}
六、总结与下一篇预告
本篇文章我们从 "子进程如何执行新程序" 切入,讲清了进程替换的本质(不新建进程,只换用户态资源),拆解了 exec 系列 6 个函数的命名规则、原型和用法,最后通过 "迷你 Shell" 实战,展示了fork+exec+wait的核心组合 ------ 这是 Linux 中多任务执行的基石。核心要点可以总结为 3 句话:
- 进程替换的核心是 "换代码数据,不换 PID",exec 成功无返回,失败返回 - 1;
- 6 个 exec 函数的区别在 "参数格式(l/v)、路径查找(p)、环境变量(e)",日常开发优先用
execlp(简单命令)和execvp(参数多); - 实战中必须配合
fork和wait:父进程 fork 子进程,子进程 exec 替换,父进程 wait 回收,避免覆盖父进程代码或产生僵尸进程。
通过前四篇文章,我们已经掌握了进程控制的完整流程:创建(fork)→ 替换(exec)→ 等待(wait)→ 终止(exit)。但这些知识如何落地成一个能实际使用的工具?下一篇文章《实战 ------ 微型 Shell 命令行解释器实现》,我们会基于前面的所有知识,写一个支持 "内建命令(cd/export)、外部命令、环境变量管理" 的完整 Shell,让你真正做到 "学以致用"。