019 进程控制 —— 进程程序替换

进程控制 —— 进程程序替换

1. 替换原理

进程程序替换是指在一个已经存在的进程中,通过系统调用将当前进程的代码、数据等全部替换为新程序的内容,也就是说,新程序加载到当前进程的地址空间中,原来进程的内容被完全覆盖。在这一过程中,进程的 PID 保持不变,但内存空间、寄存器的内容和代码逻辑均变为新程序的内容。

image-20250416124958825

【关键点】

  1. 进程替换不创建新进程,而是复用现有进程的 PID,这对需要快速切换任务、降低资源开销有很大好处。
  2. 一旦替换成功,原进程的代码 立即终止,从新程序的 main 函数开始执行。
  3. 替换过程会关闭原进程中打开的文件描述符(除非这些描述符被标记为在 exec 之后保留),fcntl(fd, F_SETFD, FD_CLOEXEC) 标记文件描述符在 exec 后关闭(了解)。

2. execl 的单进程使用

1. 函数原型

1
2
3
4
5
6
7
8
9
10
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 表示结束。
1
2
3
4
5
6
7
8
9
10
11
#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 会输出错误信息。

运行示例:

image-20250416132648832


3. execl 的多进程使用

我们先 fork() 创建一个 子进程,在子进程中使用 execl() 替换自身为另一个程序,而 父进程继续执行原有逻辑。这种方式不会影响父进程,同时能让子进程跑一个完全不同的程序,是 非常经典的进程控制模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#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;
}

运行示例:

image-20250416134826280

1
2
3
4
5
6
7
8
我是父进程,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 扩展

image-20250416185202635

记忆:

  • 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 即可(如果是其他情况还请自行搜索)。

image-20250416170533773

image-20250416171812992

2. 核心函数详解(实战场景)

execl(列表传参 + 绝对路径)
1
execl("/bin/ls", "ls", "-l", NULL); 	// 执行 /bin/ls -l
  • 重点:必须指定完整路径,参数以列表形式传递,末尾必须加 NULL
execlp(列表传参 + 自动搜索 PATH
1
execlp("ls", "ls", "-l", NULL); 	// 执行系统命令 ls(自动搜索 PATH 环境变量)
  • 优势:直接使用命令名(如 ls),无需写绝对路径。
execle(列表传参 + 自定义环境变量)
1
2
3
// 自定义环境变量并执行程序
char *env[] = {"MY_ENV=hello", NULL};
execle("/path/to/my_program", "my_program", NULL, env);
  • 应用场景:需要为子进程指定独立环境变量(如容器化任务)。
execv(数组传参 + 绝对路径)
1
2
3
// 参数以数组形式传递
char *argv[] = {"ls", "-l", NULL};
execv("/bin/ls", argv);
  • 适用场景:参数动态生成(如从用户输入或配置文件读取)。
execvp(数组传参 + 自动搜索 PATH
1
2
3
// 动态参数数组 + 自动搜索 PATH
char *argv[] = {"ls", "-l", NULL};
execvp("ls", argv);
  • 高频用法:实现类似 Shell 的功能(动态解析命令参数)。
execvpe(数组传参 + 搜索 PATH + 自定义环境变量)
1
2
3
4
// GNU 扩展,非所有系统支持
char *argv[] = {"my_program", NULL};
char *env[] = {"CUSTOM_ENV=1", NULL};
execvpe("my_program", argv, env);
  • 注意execvpeGNU 扩展函数,需确认系统支持(如 Linux 可用)。
execve(系统调用 + 完全控制)
1
2
3
4
// 系统调用级函数,直接控制环境变量和参数
char *argv[] = {"ls", "-l", NULL};
char *env[] = {"PATH=/bin", NULL};
execve("/bin/ls", argv, env);
  • 本质:其他 exec 函数最终调用 execve 实现功能。

[!NOTE]

所有 exec 函数 成功时不返回,失败时返回 -1 检查返回值并处理错误:

1
2
3
4
5
if (execl(...) == -1)
{
perror("执行失败!");
exit(1);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#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 执行动态命令

1
2
3
4
5
6
7
8
9
10
#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 自定义环境变量

1
2
3
4
5
6
7
8
#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

1
2
3
4
5
6
7
8
9
#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

1
2
3
4
5
6
7
8
9
10
#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_programmain 函数接收这些参数)。

7. 向子进程传递环境变量

1. 使用 execleexecvpe 自定义环境变量

1
2
3
4
5
6
7
8
9
10
#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 不读取这些变量),但可通过以下方式验证:

1
2
3
4
5
6
7
8
9
10
11
12
// 编写一个测试程序 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. 核心规则

  • 若显式传递环境变量(如 execleexecve):完全覆盖 父进程的环境变量,子进程仅保留传递的环境变量。
  • 若不传递环境变量(如 execlpexecv):继承父进程的所有环境变量

2. 示例验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#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 程序输出

1
2
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. 环境变量规则

  • 覆盖规则:使用 execleexecve 时,子进程环境变量完全替换为传入的数组。
  • 继承规则:默认继承父进程环境变量,适用于 execlexecvexeclpexecvp

4. 实战口诀

  • 列表传参用 l,数组传参用 v
  • 自动搜索加 p,环境变量加 e
  • 路径要对、参数要全、NULL 收尾、错误要检!