目录

引言

一、替换原理

1. 什么是程序替换?

2. 为什么需要程序替换?

3. 替换过程

二、替换函数(exec 族)

1. execl

2. execlp

3. execle

4. execv

5. execvp

6. execve

四、命名理解

五、示例代码

1. execl和execv

2. execlp和execvp

3. execle和execve

六、注意事项

七、函数和进程之间的相似性

1. 参数传递方式的相似性

2. 执行逻辑的相似性

3. 返回值与错误处理


引言

在 Linux 多进程编程中,经常遇到这样的场景:父进程创建子进程后,希望子进程去执行一个全新的程序(比如在 Shell 中执行一个外部命令)。这就需要用到进程程序替换。程序替换允许一个进程加载并执行另一个程序,从而改变当前进程的代码、数据、堆栈等,但进程 ID(PID)保持不变

一、替换原理

1. 什么是程序替换?

程序替换是指用一个新的程序替换当前进程的正文段(代码)、数据段、堆和栈,使进程开始执行新程序的入口函数(通常是 main)。替换后,进程的 PID 不变但执行的代码完全改变

2. 为什么需要程序替换?

  • 在 Shell 中执行外部命令:Shell 进程 fork 一个子进程,子进程调用程序替换加载 lsgrep 等命令。

  • 守护进程重新加载新版本:例如通过执行新程序实现平滑升级。

  • 实现作业控制、脚本解释器等。

3. 替换过程

程序替换是通过 exec 系列函数实现的。其核心步骤如下:

  1. 调用 exec 函数。

  2. 内核从指定文件中读入程序代码,替换当前进程的地址空间(代码段、数据段、堆、栈等)。

  3. 新程序从 main 函数开始执行,进程 PID 保持不变,文件描述符(除非设置了 close-on-exec)通常保持打开。

  4. 如果 exec 调用失败,则返回 -1,原进程继续执行;如果成功,exec 不返回(因为原程序已被覆盖)。

二、替换函数(exec 族)

Linux 提供了六个以 exec 开头的函数,统称为 exec 族函数。它们都用于执行程序替换,但参数形式和搜索路径方式不同。函数原型如下(需包含头文件 <unistd.h>):

#include <unistd.h>

extern char **environ;

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 execve(const char *path, char *const argv[], char *const envp[]);

1. execl

int execl(const char *path, const char *arg, ...);
  • 功能:根据指定的路径 path 执行程序。

  • 参数

    • path:要执行的程序的完整路径(如 /bin/ls)。

    • arg:程序的第一个参数(即 argv[0]),通常为程序名本身。

    • ...:变长参数列表,依次传递程序的后续参数,最后必须以 (char *)NULL 结尾。

示例

execl("/bin/ls", "ls", "-l", "/home", NULL);

2. execlp

int execlp(const char *file, const char *arg, ...);
  • 功能:根据文件名 file 在 PATH 环境变量指定的目录中搜索可执行文件,然后执行。

  • 参数

    • file:程序文件名(如 ls),系统会在 PATH 路径下查找

    • arg 和变长参数同 execl

示例

execlp("ls", "ls", "-l", "/home", NULL);

3. execle

int execle(const char *path, const char *arg, ..., char * const envp[]);
  • 功能:根据路径执行程序,并自定义环境变量

  • 参数

    • path:程序路径。

    • arg 和变长参数同 execl,但最后需要额外提供一个环境变量数组 envp

    • envp:字符串数组,每个元素格式为 "key=value",以 NULL 结尾。新程序将使用这个环境表,而不是继承当前进程的 environ

  • 示例

    char *env[] = {"HOME=/tmp", "USER=test", NULL};
    execle("/bin/bash", "bash", NULL, env);

4. execv

int execv(const char *path, char *const argv[]);
  • 功能:根据路径执行程序,参数通过数组传递。

  • 参数

    • path:程序路径。

    • argv:以 NULL 结尾的字符串数组,第一个元素为程序名,后续为参数。

  • 示例

    char *args[] = {"ls", "-l", "/home", NULL};
    execv("/bin/ls", args);

5. execvp

int execvp(const char *file, char *const argv[]);
  • 功能:根据文件名在 PATH 中搜索程序,参数通过数组传递。

  • 参数

    • file:程序文件名。

    • argv:同 execv 的参数数组。

  • 示例

    char *args[] = {"ls", "-l", "/home", NULL};
    execvp("ls", args);

6. execve

int execve(const char *path, char *const argv[], char *const envp[]);
  • 功能:根据路径执行程序,参数通过数组传递,并自定义环境变量

  • 参数

    • path:程序路径。

    • argv:参数数组。

    • envp:环境变量数组。

  • 示例

    char *args[] = {"bash", NULL};
    char *env[] = {"HOME=/tmp", "USER=test", NULL};
    execle("/bin/bash", "bash", NULL, env);

四、命名理解

exec 族函数的名字很有规律,可以帮助记忆:

  • l (list):表示参数以列表形式传递(即 execlexeclpexecle 中的 l)。这类函数使用变长参数逐个列出参数,最后以 NULL 结尾。

  • v (vector):表示参数以数组(向量)形式传递(即 execvexecvpexecve 中的 v)。这类函数接受一个以 NULL 结尾的字符串数组作为参数。

  • p (PATH):表示会在环境变量 PATH 中搜索程序文件(即 execlpexecvp 中的 p)。如果不带 p,则必须提供完整的路径名。

  • e (environment):表示可以传递自定义的环境变量数组(即 execleexecve 中的 e)。如果不带 e,新程序继承当前进程的环境变量。

因此,函数名就暗示了它的参数形式和搜索行为:

  • execl = 参数列表(l) + 路径(无 p)

  • execlp = 参数列表(l) + 搜索 PATH(p)

  • execle = 参数列表(l) + 自定义环境(e)

  • execv = 参数向量(v) + 路径(无 p)

  • execvp = 参数向量(v) + 搜索 PATH(p)

  • execve = 参数向量(v) +  自定义环境(e)

五、示例代码

1. execl和execv

2. execlp和execvp

3. execle和execve

六、注意事项

  1. 程序替换后,原进程的代码不再执行:exec 成功后,新程序从 main 开始,原来的代码段完全被覆盖。

  2. 文件描述符默认保持打开:除非设置了 FD_CLOEXEC 标志,否则 exec 后文件描述符仍然打开,新程序可以继续使用。

  3. 信号处理:exec 会将原先设置为捕获的信号恢复为默认动作,但忽略的信号(SIG_IGN)保持不变。

  4. 进程属性:进程 PID、父进程 PID、进程组、会话、当前工作目录、根目录、资源限制等大多数属性保持不变。

  5. 环境变量继承:默认情况下,新程序继承当前进程的环境变量(通过 environ)。使用带 e 的函数可以完全替换环境。

  6. PATH 搜索:带 p 的函数使用环境变量 PATH 查找程序。如果 PATH 未设置,则使用默认路径(通常为 :/bin:/usr/bin)。注意:出于安全考虑,若程序具有 set-user-ID 权限,可能会忽略 PATH 搜索或限制。

  7. 参数列表必须以 NULL 结尾:无论是变长参数还是数组,最后一个元素必须是 (char *)NULL,否则 exec 无法确定参数终点。

七、函数和进程之间的相似性

在 Linux 操作系统中,函数调用与进程程序替换(exec 家族函数)之间存在着深刻的逻辑相似性。这种相似性体现了操作系统在不同维度上处理“任务执行”的一致性。以下是函数与进程之间相似性的详细对比:

1. 参数传递方式的相似性

  • 函数调用:通过实参向形参传递数据。

  • 进程替换exec 系列函数通过 l(列表)或 v(矢量数组)将命令行参数传递给新程序

  • 环境变量:正如函数可以访问全局变量,进程可以通过 e(env)参数获取或自定义环境变量。

2. 执行逻辑的相似性

  • 入口点

    • 函数从函数名对应的地址开始执行。

    • 进程替换后,新程序从其 main 函数开始执行

  • 代码隔离

    • 函数拥有独立的局部变量空间(栈帧)。

    • 进程通过页表映射拥有独立的虚拟地址空间。

3. 返回值与错误处理

  • 函数:通常通过 return 返回运行结果或错误码。

  • 进程替换函数

    • exec 系列函数在调用成功时不返回,因为原有的执行流已被新程序代码覆盖。

    • 只有在调用失败时才会返回 -1,这与函数在遇到异常时返回错误标识的逻辑一致。

从程序员的视角来看,exec 族函数就像是调用了一个“特殊的函数”,只是这个“函数”运行在独立的程序空间中,并且一旦“调用”成功,就不会再回到原来的代码位置执行。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐