【Linux】Linux进程替换入门:exec函数族使用指南

234 阅读13分钟

在这里插入图片描述

在Linux中,exec函数族能让进程无缝切换到另一个程序,覆盖原有代码但保留PID。它是Shell运行命令、程序动态加载的核心机制。本文详解execl()、execvp()等7种函数的使用场景与陷阱,助你掌握进程替换的精髓。

一、进程替换

假设我们使用 fork 创建一个子进程来帮助完成某个任务,这个任务需要封装成一个函数进行调用。但我们是否每次都要自己编写一个新的函数呢?或者说,子进程是否一定要执行父进程的代码呢?其实,我们完全可以使用一个事先编写好的可执行程序,当需要执行时,让子进程直接运行这个程序,而不是重复写代码。

但问题是,操作系统不能简单地信任任何进程,因此我们需要一些系统调用接口来保证安全和有效的进程切换。这就是进程替换的意义所在:它允许父进程和子进程执行不同的代码,确保各自的任务可以独立执行。

1.1 通过代码看现象

为了实现进程替换,让子进程能够执行与父进程完全不同的代码,操作系统提供了一些exec 系列函数为基础的系统调用接口,通过这些接口,子进程可以加载并运行新的可执行程序。

image.png

image.png

1.2 进程替换原理

首先,我们需要了解,进程 = 内核数据结构 + 代码和数据。当进程被替换时,从被替换进程的角度来看,实际上就是将新的程序加载到内存中。这个过程类似于硬盘到内存的加载,而在 Linux 中,这一过程通过 exec 系列函数实现。通过查看 man 手册,发现 exec 函数属于第三类手册(系统调用),它是一个库函数,封装了底层系统调用接口。

虽然 IO 操作通常是由操作系统底层完成的,但 exec 函数本质上也是通过系统调用来实现进程替换的,它会调用操作系统去加载新的程序代码和数据,从而完成进程的替换。

程序替换的意义在于,内核数据结构本身保持不变,只需要修改其部分属性值。具体来说,进程的原有数据和代码会被新的数据和代码覆盖,完成进程的替换。

image.png

问题】:进程替换是否会新进程被创建?

进程替换并不是创建新的进程,而是直接 使用原进程的“壳”,将其中的代码和数据替换为新的内容。

这个过程其实非常直接:当进程执行到 exec 系列函数时,它会将目标可执行程序(例如 ls)的代码和数据加载进来,直接替换掉原有的代码和数据。当然,这个过程可能会涉及内存空间的申请和释放。因此,并没有创建新的子进程,而是 替换了当前子进程的代码和数据。同时,内核的数据结构(PCB)没有被释放,只会修改其中的一些字段。

问题】:如果进程替换失败怎么办?

返回值不关心,成功就不会执行失败就继续执行了。

1.3 代码修改为多进程

通过 fork() 创建子进程,父进程使用 wait() 来等待子进程的完成,而不影响父进程本身。

image.png

子进程执行任务的方式】:

  1. 让子进程执行父进程的部分代码:子进程继承父进程的代码和数据,继续执行父进程中的一部分任务。
  2. 让子进程执行全新的进程:通过调用 exec() 系列函数,子进程完全替换自己的代码和数据,执行一个全新的程序。

1.4 写时拷贝(Copy-on-Write)

在操作系统的管理下,进程的代码和数据并不是一开始就完全独立的。父子进程在 fork() 后会共享相同的代码和数据区域,这种共享方式称为 写时拷贝(Copy-on-Write, COW)。这意味着,父子进程在开始时使用相同的内存空间,直到其中一个进程尝试修改这些数据时,操作系统才会为该进程分配新的内存空间,并复制原始数据。

image.png

这使得父子进程在初始阶段可以共享同一份代码和数据,从而减少内存的浪费。但当某个进程试图修改这些数据时(例如调用 exec() 函数),操作系统会通过 写时拷贝机制,为该进程分配新的物理内存空间,替换其中的数据。这是一个高效的内存管理方式,确保了父子进程在执行时能够独立,但又不需要在开始时就为每个进程创建完全独立的内存空间。

1.4.1 代码和数据的写时拷贝

虽然代码通常被视为只读的,但并不意味着不能被修改。关键在于谁在尝试修改代码:

  • 如果是子进程想要替换其代码,操作系统会拦截这个行为并进行 写时拷贝。在子进程调用 exec() 系列函数时,操作系统会为它创建独立的内存空间,并将新的程序代码和数据加载到这个空间中。
  • 在这过程中,原本由父子进程共享的代码和数据不会被直接修改,而是为修改的进程分配新的内存,从而保证父进程和子进程的独立性。

这个机制的作用相当于,父进程和子进程共享初始的代码和数据,直到子进程需要修改时,操作系统才会为子进程分配独立的内存空间,确保两者的隔离和独立。

场景理解】:

可以类比为“第二人格”的出现。当子进程通过 exec() 调用加载一个新的程序时,它就像是换了一个全新的“人格”,并完全不再记得父进程的任何状态或数据。这种切换是通过操作系统的管理和内存的 写时拷贝 机制完成的,确保了子进程与父进程在执行新程序时的独立性。

二、程序替换接口

一共有七个接口,其中2号手册中的接口是系统调用接口,而3号手册中的六个接口是库函数

2.1 替换接口理解

这些函数原型看起来很容易混,但只要掌握了规律就很好记。

  • l(list)】 : 表示参数采用列表
  • v(vector)】 : 参数用数组
  • p(path)】 : 有p自动搜索环境变量PATH
  • e(env)】 : 表示自己维护环境变量

在这里插入图片描述

2.2 接口关系

image.png

实际上,只有 execve真正的系统调用,其余五个函数最终都会调用 execve。因此,execve 处于man 手册第2节,而其它五个函数则位于man 手册第3节。这些函数之间的关系可以通过下图来表示。

它们的区别类似于 exit_exit 的关系:库函数是对系统调用接口的进一步封装。最终,这六个库函数都会转化为调用相应的系统调用接口。因此,我们需要更加关注的是3号手册中这六个库函数的具体区别。

2.3 exec* 函数调用(重点)

以下代码方便理解,是整理好的。

image.png

#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);
}

在 Linux 系统中,exec 系列函数用于执行新程序。它们的不同之处在于参数传递的方式、路径的处理方式以及环境变量的使用。下面详细解释每个函数的工作原理。

2.3.1 execl函数

execlexec 系列中的一种,l 代表 "list"(列表)。这意味着你需要将程序的路径、程序名和命令行参数一个一个传递给函数,最后用 NULL 结束参数列表。其行为类似于命令行中的逐个参数传递。

【示例】

execl("/bin/ps", "ps", "-ef", NULL);

这里,execl 会将 /bin/ps 程序路径、ps 命令名和 -ef 参数传递给新程序执行。

2.3.2 execlp函数

execlpp 代表 "path",这意味着你只需要提供程序名,不必指定完整路径。execlp 会在环境变量 $PATH 中查找程序路径,然后执行它。因此,它能自动根据系统环境变量去查找程序,而不需要你手动指定路径。

【示例】

execlp("ps", "ps", "-ef", NULL);

在这里,execlp 会根据系统的环境变量 $PATH 查找 ps 命令并执行。

2.3.3 execv函数函数

execv 中的 v 代表 "vector"(向量)。不同于 execlexecv 需要你先将所有参数放入一个数组中,然后将这个数组传递给函数。这种方式与 main 函数中的 argv[] 参数传递方式类似。

【示例】

char *args[] = {"/bin/ps", "ps", "-ef", NULL};
execv("/bin/ps", args);

这里,args 数组包含了程序路径和各个参数,execv 会使用这个数组执行程序。

2.3.4 execvp

execvpexecvexeclp 的组合,v 代表向量,p 代表路径。它不仅接受一个参数数组,还会在 $PATH 环境变量中查找程序的路径。因此,你只需要提供程序的名称,而不必指定完整路径。

【示例】

char *args[] = {"ps", "-ef", NULL};
execvp("ps", args);

execvp 会在 $PATH 中查找 ps 程序并执行。

2.3.5 execle 和 execvpe函数

execleexecvpe 在功能上与 execlexecvp 类似,但它们接受一个额外的参数 envp[],用来指定一个自定义的环境变量集合,而不是使用父进程的环境变量。这对于需要自定义环境的程序来说非常有用。

【示例】

char *args[] = {"ps", "-ef", NULL};
char *env[] = {"MY_VAR=1", "PATH=/usr/bin", NULL};
execle("/bin/ps", "ps", "-ef", NULL, env);

在这个示例中,execle 会使用提供的 env[] 环境变量,而不是父进程的环境变量。

2.3.6 选择合适的exec函数

在使用 exec 系列函数时,选择哪一个函数取决于你的需求。具体来说:

  • 如果你知道程序的完整路径并希望直接传递命令行参数,使用 execlexecv
  • 如果你不希望手动指定完整路径,可以使用 execlpexecvp,它们会自动在 $PATH 环境变量中查找程序。
  • 如果你需要自定义环境变量而不是使用父进程的环境,使用 execleexecvpe

通过这些函数,程序可以灵活地执行新程序,并根据需要传递不同的参数和环境配置。

2.4 进程替换与内存管理

exec 函数系列的执行过程实际上并不是创建一个全新的进程,而是替换当前进程的代码和数据。换句话说,子进程在调用 exec 时,操作系统会使用新的程序内容替换原先的程序,从而实现了“进程替换”而非“进程创建”。此时,进程的内存空间被清空并重新加载新的程序,这意味着当前进程会在替换后运行新的代码,完全不再记得原来父进程的状态。

这一过程与 fork() 系列中的写时拷贝(COW)机制密切相关,因为父子进程在创建时共享同一内存,而当子进程进行替换时,操作系统才会为子进程分配新的内存空间。

三、进程替换实践

在 Linux 系统中,当我们想要执行自己的程序时,通常会涉及多个文件的编译和执行。为了更好地管理这些操作,可以利用 Makefileexec 系列函数来简化执行流程。

image.png

3.1 伪目标处理多文件编译问题

处理前】:

在这里插入图片描述

在传统的编译过程中,我们通常会处理一个文件,直到这个文件找到它所依赖的函数并执行完成。这意味着每个程序和文件之间必须存在依赖关系,才能逐步执行。通常,我们在执行时只会处理一个文件,其他文件的编译和执行需要等待该文件执行完毕。

处理后

为了支持多文件同时编译,我们通常使用 Makefile 来进行管理。没有明确指定目标文件时,make 会根据依赖链推导执行第一个可执行目标,并顺序编译其他文件。

在这里插入图片描述

解决方案:通过在 Makefile 中添加伪目标 all,并将其放在文件前面,make 会根据目标的依赖关系顺序执行编译,确保多个文件能够一次性被编译。这个方式为我们同时编译多个源文件提供了高效的方法。

3.2 命令行参数和环境变量进行传递

在进程替换的过程中,命令行参数环境变量的传递非常重要。理解它们的工作原理能帮助我们更好地掌控子进程的行为和配置。

3.2.1 环境变量的继承

环境变量是进程在创建时继承自父进程的。即使没有显式传递环境变量,子进程也能通过继承父进程的地址空间来访问这些变量。因此,在进程替换时,环境变量默认不会被替换,除非我们显式地指定新的环境变量。

环境变量示意

image.png

image.png

在日常运行中,程序的命令行参数和环境变量通常由父进程传递给子进程。例如,当我们运行 bash 作为父进程时,它会维护两张表:命令行参数表环境变量表。在创建子进程时,这些信息会被直接传递给子进程,确保子进程能够继续访问到父进程的环境配置。

3.2.2 子进程环境变量处理

有时我们希望在子进程中为其设置新的环境变量,而不影响父进程或全局环境。我们可以使用 putenv 函数来动态添加新的环境变量,这样做会使得新的变量仅对当前进程及其子进程有效。

在这里插入图片描述

示例:

  • putenv("MY_VAR=1"):会将 MY_VAR 环境变量添加到当前进程及其子进程中。

应用场景:如果我们不希望影响系统的全局环境或父进程的环境,可以选择在当前进程内使用 putenv 来动态配置子进程的环境。

3.2.3 彻底替换环境变量execle、execve

如果我们需要完全替换环境变量,可以使用 execleexecve 函数。这两个函数允许我们指定一个自定义的环境变量数组,完全覆盖父进程的环境变量。

char *env[] = {"MY_VAR=1", "PATH=/usr/bin", NULL};
execle("/bin/ps", "ps", "-ef", NULL, env);

在这里插入图片描述

以上就是本篇文章的所有内容,在此感谢大家的观看!这里是Linux笔记,希望对你在学习Linux旅途中有所帮助!