019 进程控制 —— 进程程序替换
🦄 个人主页: 小米里的大麦-CSDN博客
🎏 所属专栏: Linux_小米里的大麦的博客-CSDN博客
🎁 GitHub主页: 小米里的大麦的 GitHub
⚙️ 操作环境: Visual Studio 2022
文章目录
- 进程控制 —— 进程程序替换
- 1. 替换原理
- 2. execl 的单进程使用
- 1. 函数原型
- 3. execl 的多进程使用
- 4. `exec` 函数“全家桶”
- 1. `man` 手册查询
- 2. 核心函数详解(实战场景)
- ① `execl`(列表传参 + 绝对路径)
- ② `execlp`(列表传参 + 自动搜索 `PATH`)
- ③ `execle`(列表传参 + 自定义环境变量)
- ④ `execv`(数组传参 + 绝对路径)
- ⑤ `execvp`(数组传参 + 自动搜索 `PATH`)
- ⑥ `execvpe`(数组传参 + 搜索 `PATH` + 自定义环境变量)
- ⑦ `execve`(系统调用 + 完全控制)
- 5. 部分代码实战
- 1. 使用 `execvp` 执行动态命令
- 2. 使用 `execle` 自定义环境变量
- 6. 代码验证 `exec` 执行系统命令和自定义命令
- 1. 执行系统命令(如 `ls`)
- 2. 执行自定义程序(如编译后的 `my_program`)
- 7. 向子进程传递环境变量
- 1. 使用 `execle` 或 `execvpe` 自定义环境变量
- 8. 环境变量是覆盖还是追加?
- 1. 核心规则
- 2. 示例验证
- 9. 总结
- 1. 核心原理
- 2. 关键函数实战场景
- 3. 环境变量规则
- 4. 实战口诀
- 共勉
进程控制 —— 进程程序替换
1. 替换原理
进程程序替换是指在一个已经存在的进程中,通过系统调用将当前进程的代码、数据等全部替换为新程序的内容,也就是说,新程序加载到当前进程的地址空间中,原来进程的内容被完全覆盖。在这一过程中,进程的 PID
保持不变,但内存空间、寄存器的内容和代码逻辑均变为新程序的内容。
【关键点】
- 进程替换不创建新进程,而是复用现有进程的
PID
,这对需要快速切换任务、降低资源开销有很大好处。 - 一旦替换成功,原进程的代码 立即终止,从新程序的
main
函数开始执行。 - 替换过程会关闭原进程中打开的文件描述符(除非这些描述符被标记为在 exec 之后保留),
fcntl(fd, F_SETFD, FD_CLOEXEC)
标记文件描述符在exec
后关闭(了解)。
2. execl 的单进程使用
1. 函数原型
int execl(const char* path, const char* arg, ..., (char*)NULL);注意:execl 接受变长参数,需要以 NULL 结束参数列表,参数顺序要求严格,第一个参数通常写为程序文件的绝对路径,第二个参数是新程序的“名称”,随后是实际参数。
int execl(const char* path, // 新程序的路径(核心!)const char* arg0, // 第一个参数(通常为程序名)const char* arg1, // 第二个参数(如命令行选项)..., // 可变参数(灵活传递)(char*)NULL // 参数结束标志(必须!忘记会导致崩溃或参数错乱)
);
参数解析:
path
:新程序的绝对路径(如/bin/ls
),必须明确指定位置!arg
:命令行参数列表,第一个参数通常是程序名,最后必须加NULL
表示结束。
#include <unistd.h>
#include <stdio.h>int main()
{printf("原进程即将被替换!\n");execl("/bin/ls", "ls", "-l", NULL); // 执行 ls -l 命令printf("这里不会被执行!你看不到我!\n");perror("execl failed"); // 若替换失败才会执行到这里return 1;
}
- 替换成功后,
printf("原进程...")
之后的代码 不再执行。 - 若替换失败(如路径错误),
perror
会输出错误信息。
运行示例:
3. execl 的多进程使用
我们先 fork()
创建一个 子进程,在子进程中使用 execl()
替换自身为另一个程序,而 父进程继续执行原有逻辑。这种方式不会影响父进程,同时能让子进程跑一个完全不同的程序,是 非常经典的进程控制模式。
#include <stdio.h>
#include <unistd.h> // 包含 fork() 的头文件
#include <stdlib.h> // 包含 exit() 的头文件int main()
{pid_t pid = fork(); // 创建子进程if (pid == 0) // 子进程:使用 execl 替换为新程序{printf("我是子进程,PID: %d,现在执行 execl 替换为 ls 命令\n", getpid());execl("/bin/ls", "ls", "-l", NULL);perror("execl failed"); // 若 execl 出错,才会继续执行下面语句exit(1);}else if (pid > 0) // 父进程继续执行原有逻辑{printf("我是父进程,PID: %d,创建了子进程: %d\n", getpid(), pid);sleep(2);printf("父进程结束\n");}else{perror("fork failed");return 1;}return 0;
}
运行示例:
我是父进程,PID: 18882,创建了子进程: 18883
我是子进程,PID: 18883,现在执行 execl 替换为 ls 命令
total 32
-rwxrwxr-x 1 hcc hcc 8720 Apr 16 13:47 Multi-process
-rw-rw-r-- 1 hcc hcc 833 Apr 16 13:44 Multi-process.c
-rwxrwxr-x 1 hcc hcc 8464 Apr 16 13:25 test1
-rw-rw-r-- 1 hcc hcc 325 Apr 16 13:24 test1.c
父进程结束
4. exec
函数“全家桶”
Linux 下有 7 种以 exec
开头的函数,统称 exec
函数。
- 系统调用类:
exec
是由内核提供的系统调用,属于man
手册的第 2 章节。 - 库函数类:其他
exec
函数均是 C 标准库对execve
的封装,属于man
手册第 3 章节。
exec
函数族成员一览(记熟)
函数名 | 参数类型 | 使用说明 |
---|---|---|
execl | 列出参数(arg0, arg1, …, NULL) | 最常用,适合参数个数固定 ✅ |
execv | 参数数组(char * argv []) | 参数可变,用数组表示(* *程序参数动态构造(任务调度器)**) |
execle | 列出参数 + 环境变量(envp) | 适合需要指定环境变量 |
execve | 参数数组 + 环境变量 | 最底层函数,系统调用接口 ✅ |
execlp | 自动查 $PATH + 列出参数 | 不指定绝对路径,依赖环境 PATH ✅ |
execvp | 自动查 $PATH + 参数数组 | 最常见 Shell 结构 ✅(支持 PATH 查找 + 动态参数) |
execvpe | 自动查 $PATH + 参数数组 + envp | 非标准,GNU 扩展 |
记忆:
- l(list):表示参数采用列表。
- v(vector):参数用数组。
- p(path):有 p 自动搜索环境变量 PATH。
- e(env):表示自己维护环境变量。
1. man
手册查询
一般来说使用 man 3 exec
(6 个)和 man 2 execve
(1 个)就能进行查询,但是不妨有朋友像我一样被提示“No manual entry for execl
”,如何解决?根据搜索,使用命令 sudo yum install man-pages man-pages-posix
即可(如果是其他情况还请自行搜索)。
2. 核心函数详解(实战场景)
① execl
(列表传参 + 绝对路径)
execl("/bin/ls", "ls", "-l", NULL); // 执行 /bin/ls -l
- 重点:必须指定完整路径,参数以列表形式传递,末尾必须加
NULL
。
② execlp
(列表传参 + 自动搜索 PATH
)
execlp("ls", "ls", "-l", NULL); // 执行系统命令 ls(自动搜索 PATH 环境变量)
- 优势:直接使用命令名(如
ls
),无需写绝对路径。
③ execle
(列表传参 + 自定义环境变量)
// 自定义环境变量并执行程序
char *env[] = {"MY_ENV=hello", NULL};
execle("/path/to/my_program", "my_program", NULL, env);
- 应用场景:需要为子进程指定独立环境变量(如容器化任务)。
④ execv
(数组传参 + 绝对路径)
// 参数以数组形式传递
char *argv[] = {"ls", "-l", NULL};
execv("/bin/ls", argv);
- 适用场景:参数动态生成(如从用户输入或配置文件读取)。
⑤ execvp
(数组传参 + 自动搜索 PATH
)
// 动态参数数组 + 自动搜索 PATH
char *argv[] = {"ls", "-l", NULL};
execvp("ls", argv);
- 高频用法:实现类似 Shell 的功能(动态解析命令参数)。
⑥ execvpe
(数组传参 + 搜索 PATH
+ 自定义环境变量)
// GNU 扩展,非所有系统支持
char *argv[] = {"my_program", NULL};
char *env[] = {"CUSTOM_ENV=1", NULL};
execvpe("my_program", argv, env);
- 注意:
execvpe
是GNU
扩展函数,需确认系统支持(如 Linux 可用)。
⑦ execve
(系统调用 + 完全控制)
// 系统调用级函数,直接控制环境变量和参数
char *argv[] = {"ls", "-l", NULL};
char *env[] = {"PATH=/bin", NULL};
execve("/bin/ls", argv, env);
- 本质:其他
exec
函数最终调用execve
实现功能。
[!NOTE]
所有
exec
函数 成功时不返回,失败时返回-1
! 检查返回值并处理错误:if (execl(...) == -1) {perror("执行失败!");exit(1); }
#include <unistd.h> #include <stdio.h> #include <stdlib.h>int main() {char* const argv[] = { "ps", "-ef", NULL };char* const envp[] = { "PATH=/bin:/usr/bin", "TERM=console", NULL };// execl: 需要完整路径if (execl("/bin/ps", "ps", "-ef", NULL) == -1){perror("execl");exit(1);}// execlp: 使用 PATH 环境变量if (execlp("ps", "ps", "-ef", NULL) == -1){perror("execlp");exit(1);}// execle: 需要自己设置环境变量if (execle("ps", "ps", "-ef", NULL, envp) == -1){perror("execle");exit(1);}// execv: 参数通过数组传递if (execv("/bin/ps", argv) == -1){perror("execv");exit(1);}// execvp: 使用 PATH 环境变量if (execvp("ps", argv) == -1){perror("execvp");exit(1);}// execve: 需要自己设置环境变量if (execve("/bin/ps", argv, envp) == -1){perror("execve");exit(1);}return 0; }
5. 部分代码实战
1. 使用 execvp
执行动态命令
#include <unistd.h>
#include <stdio.h>int main()
{char *args[] = {"ls", "-l", "-a", NULL}; // 参数数组execvp("ls", args);perror("execvp 失败!");return 1;
}
- 输出:执行
ls -l -a
,参数通过数组动态传递。
2. 使用 execle
自定义环境变量
#include <unistd.h>int main()
{char *env[] = {"USER=test", "PATH=/usr/bin", NULL};execle("/bin/ls", "ls", "-l", NULL, env);return 1; // 只有出错才会执行
}
- 作用:子进程的环境变量被替换为
env
数组中的内容。
6. 代码验证 exec
执行系统命令和自定义命令
1. 执行系统命令(如 ls
)
#include <unistd.h>
#include <stdio.h>int main()
{execlp("ls", "ls", "-l", NULL); // 执行系统命令 ls -l,自动搜索 PATH 环境变量perror("execlp 失败!"); // 若替换失败才会执行以下代码return 1;
}
执行结果:输出当前目录的文件列表(与终端直接运行 ls -l
效果一致)。
2. 执行自定义程序(如编译后的 my_program
)
#include <unistd.h>
#include <stdio.h>int main()
{// 执行我们用户自己的程序(假设 my_program 在 /home/user/bin 下)execl("/home/user/bin/my_program", "my_program", "arg1", "arg2", NULL);perror("execl失败!");return 1;
}
关键点:
- 路径必须正确:需指定自定义程序的绝对路径或确保其在
PATH
环境变量中。 - 参数自由传递:可传递任意参数给自定义程序(
my_program
的main
函数接收这些参数)。
7. 向子进程传递环境变量
1. 使用 execle
或 execvpe
自定义环境变量
#include <unistd.h>
#include <stdio.h>int main()
{char* env[] = { "MY_ENV=hello", "PATH=/usr/bin", NULL }; // 自定义环境变量数组(必须以 NULL 结尾)execle("/bin/ls", "ls", "-l", NULL, env); // 执行程序并传递自定义环境变量perror("execle失败!");return 1;
}
验证方法:在 ls
程序中无法直接看到环境变量(因为 ls
不读取这些变量),但可通过以下方式验证:
// 编写一个测试程序 test_env.c
#include <stdio.h>
#include <stdlib.h>int main()
{printf("MY_ENV=%s\n", getenv("MY_ENV")); // 输出自定义环境变量return 0;
}// 编译后执行:
execle("./test_env", "test_env", NULL, env); // 输出 "MY_ENV = hello"
8. 环境变量是覆盖还是追加?
1. 核心规则
- 若显式传递环境变量(如
execle
或execve
):完全覆盖 父进程的环境变量,子进程仅保留传递的环境变量。 - 若不传递环境变量(如
execlp
或execv
):继承父进程的所有环境变量。
2. 示例验证
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>int main()
{setenv("PARENT_ENV", "parent_value", 1); // 父进程设置一个环境变量char* env[] = { "CHILD_ENV=child_value", NULL }; // 自定义子进程环境变量if (fork() == 0) // 子进程使用 execle 传递自定义环境变量{execle("./test_env", "test_env", NULL, env);perror("execle failed");_exit(1);}else{wait(NULL);}return 0;
}
test_env
程序输出:
CHILD_ENV=child_value
PARENT_ENV=(null) # 父进程的环境变量被覆盖!
9. 总结
1. 核心原理
- 不创建新进程:复用现有进程的
PID
和资源(文件描述符等),仅替换代码和数据段。 - 执行即替换:成功后原进程代码立即终止,从新程序的
main
开始执行。 - 资源处理:默认关闭未标记的文件描述符,避免资源泄漏。
2. 关键函数实战场景
函数名 | 参数形式 | 路径搜索 | 环境变量 | 适用场景 |
---|---|---|---|---|
execl | 列表传参 | 否 | 继承父进程 | 参数固定 + 绝对路径(如 /bin/ls ) |
execlp | 列表传参 | 是 | 继承父进程 | 执行系统命令(如 ls ,依赖 PATH ) |
execle | 列表传参 | 否 | 自定义覆盖 | 需要独立环境变量(如容器化任务) |
execv | 数组传参 | 否 | 继承父进程 | 动态生成参数(如用户输入解析) |
execvp | 数组传参 | 是 | 继承父进程 | Shell 类动态命令执行(如 ls -l -a ) |
execve | 数组传参 | 否 | 自定义覆盖 | 系统级控制(底层实现) |
3. 环境变量规则
- 覆盖规则:使用
execle
或execve
时,子进程环境变量完全替换为传入的数组。 - 继承规则:默认继承父进程环境变量,适用于
execl
、execv
、execlp
、execvp
。
4. 实战口诀
- 列表传参用
l
,数组传参用v
- 自动搜索加
p
,环境变量加e
- 路径要对、参数要全、NULL 收尾、错误要检!
共勉