【Linux学习】Linux中的进程程序替换
大家好,我是程序员小青蛙,今天介绍进程程序替换。
一、什么是进程程序替换?
核心定义
用fork()创建子进程后,子进程默认和父进程执行相同的程序。如果想让子进程执行一个全新的程序,就需要调用exec系列函数,完成进程程序替换。
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
关键特点
- 不创建新进程:调用
exec前后,进程的pid不会改变。 - 替换用户空间:新程序的代码段、数据段会完全覆盖原进程的用户空间,栈、堆也会被重置。
- 成功不返回:
exec调用成功后,原程序的后续代码不会再执行;调用失败时返回-1。
二、exec 系列函数详解
Linux 提供了 6 个以exec开头的函数,统称为 exec 函数族。
1. 函数原型
#include <unistd.h> // l: list(可变参数列表) 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[]); // v: vector(参数数组) int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execve(const char *path, char *const argv[], char *const envp[]);这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
如果调用出错则返回-1
所以exec函数只有出错的返回值而没有成功的返回值。
2. 命名规律(记忆技巧)
| 字母 | 含义 | 示例 |
|---|---|---|
l | list:参数以可变列表形式传递,以NULL结尾 | execl("/bin/ls", "ls", "-l", NULL) |
v | vector:参数以数组形式传递,数组最后一个元素为NULL | execv("/bin/ls", argv) |
p | path:自动搜索PATH环境变量,无需写全路径 | execlp("ls", "ls", "-l", NULL) |
e | env:自定义环境变量,需手动传入envp数组 | execle("/bin/ls", "ls", "-l", NULL, envp) |
3. 函数对比表
| 函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
|---|---|---|---|
execl | 列表 | 必须写全路径 | 是 |
execlp | 列表 | 可写程序名,自动搜PATH | 是 |
execle | 列表 | 必须写全路径 | 否,需手动传入envp |
execv | 数组 | 必须写全路径 | 是 |
execvp | 数组 | 可写程序名,自动搜PATH | 是 |
execve | 数组 | 必须写全路径 | 否,需手动传入envp |
4. 底层关系
只有execve是真正的系统调用,其他 5 个函数都是对execve的封装,以提供更灵活的调用方式。
三、exec 函数使用示例
1.execl示例(全路径 + 列表传参)
#include <unistd.h> int main() { // 执行 ls -l 命令,需要写全路径 execl("/bin/ls", "ls", "-l", NULL); // 只有调用失败才会执行这里 perror("execl"); return 1; }2.execlp示例(自动搜 PATH + 列表传参)
#include <unistd.h> int main() { // 无需写全路径,自动在 PATH 中搜索 ls execlp("ls", "ls", "-l", NULL); perror("execlp"); return 1; }3.execv示例(全路径 + 数组传参)
#include <unistd.h> int main() { char *argv[] = {"ls", "-l", NULL}; execv("/bin/ls", argv); perror("execv"); return 1; }#include <unistd.h> int main() { char *const argv[] = {"ps", "-ef", NULL}; char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL}; execl("/bin/ps", "ps", "-ef", NULL); //带p的,可以使用环境变量PATH,无需写全路径 execlp("ps", "ps", "-ef", NULL); //带e的,需要自己组装环境变量 execle("ps", "ps", "-ef", NULL, envp); execv("/bin/ps", argv); //带p的,可以使用环境变量PATH,无需写全路径 execvp("ps", argv); //带e的,需要自己组装环境变量 execve("/bin/ps", argv, envp); exit(0); }事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve,所以execve在man手册第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。
四、综合应用:简易 Shell 实现
fork+exec+wait是实现 shell 的核心逻辑。
核心流程
- 获取命令行:读取用户输入的命令。
- 解析命令:将命令字符串分割成
argv数组。 - 创建子进程:调用
fork()创建子进程。 - 程序替换:子进程调用
execvp执行命令。 - 等待子进程:父进程调用
waitpid等待子进程退出。
完整代码
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/wait.h> #define MAX_CMD 1024 char command[MAX_CMD]; // 1. 读取用户输入 int do_face() { memset(command, 0x00, MAX_CMD); printf("myshell$ "); fflush(stdout); if (scanf("%[^\n]%*c", command) == 0) { getchar(); return -1; } return 0; } // 2. 解析命令为 argv 数组 char **do_parse(char *buff) { int argc = 0; static char *argv[32]; char *ptr = buff; while (*ptr != '\0') { if (!isspace(*ptr)) { argv[argc++] = ptr; while (*ptr != '\0' && !isspace(*ptr)) ptr++; } else { while (*ptr != '\0' && isspace(*ptr)) ptr++; *ptr = '\0'; } } argv[argc] = NULL; return argv; } // 3. 执行命令 int do_exec(char *buff) { char **argv = do_parse(buff); if (argv[0] == NULL) return -1; pid_t pid = fork(); if (pid == 0) { // 子进程:程序替换 execvp(argv[0], argv); perror("execvp"); exit(1); } else { // 父进程:等待子进程退出 waitpid(pid, NULL, 0); } return 0; } int main() { while (1) { if (do_face() < 0) continue; do_exec(command); } return 0; }思考函数和进程之间的相似性
exec/exit就像call/return
一个C程序有很多函数组成。一个函数可以调用另外一个函数,同时传递给它一些参数。被调用的函数执行一定的操作,然后返回一个值。每个函数都有他的局部变量,不同的函数通过call/return系统进行通信。
这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux鼓励将这种应用于程序之内的模式扩展到程序之间。如下图
一个C程序可以fork/exec另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过exit(n)来返回值。调用它的进程可以通过wait(&ret)来获取exit的返回值。
五、关键知识点总结
- 程序替换的本质:用新程序的代码和数据,覆盖进程原有的用户空间,进程
pid不变。 - exec 函数的核心特点:成功不返回,失败返回
-1。 - 命名规律:
l/v表示参数传递方式,p表示自动搜PATH,e表示自定义环境变量。 - 简易 shell 的核心逻辑:
fork创建子进程 →exec替换程序 →wait等待子进程。
