1. 基础知识

什么是进程替换

用 fork() 创建子进程后,子进程和父进程执行的是同一个程序(虽然虚拟地址空间复制了一份),此时可以通过 if 分流来使得父子进程完成不同的功能。但事实上更加常见的做法是通过 execve 系统调用来改变当前进程所执行的程序,使得当前进程转而去执行另一个程序(注意区分程序和函数等概念)。这个过程就被称为进程替换。

进程替换:execve()

系统调用 execve() 可以将新程序加载到某一进程的虚拟内存空间。在这一操作过程中,将丢弃旧有程序,而该进程的虚拟地址空间被新程序重新初始化,更新页表,更新PCB相关条件,但注意PID不变,因为这还是同一个进程

进程替换最常见的应用是用于 fork() 后的子进程,使得子进程转而去执行不同的程序。

基于系统调用 execve() ,还实现了一组高层的库函数封装,这组库函数接口各异但最终功能相同,并且最终都会调用系统调用 execve() 。下面先来介绍最重要的系统调用 execve()。

int execve(const char *filename, char *const argv[], char *const envp[]);
  • 参数 filename 指向要替换的程序的绝对路径或者相对路径名。
  • 参数 argv 指针数组制定了传递给新进程的命令行参数。该数组对应于C语言 main 函数的第二个参数 argv,数组中的第一个元素指向要运行的程序名,该数组以NULL标志参数列举完毕。
  • 最后一个参数 envp 指定了新程序的环境变量列表。参数 envp 对应新程序的 environ 全局变量。

由于这个系统调用会替换掉原程序,所以调用成功该函数永不返回,而如果调用失败会返回-1。可以通过查看 errno 来判断出错原因。

exec() 库函数族

库函数实现了六个对 execve() 的封装函数,其中的区别仅在于调用API的不同。

extern char **environ;

int execl(const char *path, const char *arg, ... /* (char  *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char  *) NULL */);
int execle(const char *path, const char *arg, ... /*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

命名规则:

  • l(list):表示参数用参数列表的形式给出。
  • v(vector):表示参数用字符指针数组的形式给出。
  • p(path):带 p 则表示第一个参数可以直接给出程序名,函数会自动到环境变量PATH中去寻找程序名相同的可执行程序。
  • e(environment):带 e 则表示可以使用字符指针数组指定替换后的新程序的环境变量,不带 e 则会自动使用当前的环境变量列表来传递给替换后的程序。

函数返回值与 execve() 相同。

2. 文件描述符与 exec()

替换后的程序继承了文件描述符

默认情况下,exec() 替换后的新程序会继承旧程序的打开文件的文件描述符。这通常很实用,例如 shell 利用这一特性来实现I/O重定向。例如,输入如下shell 命令。

ls /tmp > dit.txt

shell 运行这条命令的时候会依次执行以下步骤:

  1. shell 主程序利用 fork() 创建一个子进程。
  2. 子进程用文件描述符1打开文件 dit.txt ,这可能会采取如下两种方式之一:先关闭文件描述符1,然后利用打开文件后的默认文件描述符为最小未用数字这一特性,使得文件描述1指向 dit.txt。或者现直接打开文件 dit.txt,然后用 dup() 将文件描述符1重定向到该文件,之后关闭掉多余的文件描述符(原来指向 dit.txt 的文件描述符)。第二种方法较前者更加安全,因为它不依赖于文件描述符的低值取数原则。
  3. 子进程调用程序替换,将程序替换成 ls,替换后的程序继承了文件描述符,所以默认向文件描述符1写入数据的时候就直接写入了 dit.txt。

执行时关闭(close-on-exec)标志

在执行 exec() 之前,程序有时需要确保关闭某些特定的文件描述符。这可能是从安全的角度出发,不想让新程序访问到之前程序的打开文件。也有可能只是需要程序替换前关闭不再会用到的文件描述符。对这些文件描述符施以 close() 也可以达到目的,然而这一做法存在如下局限性:

  • 有些文件描述符可能是库函数打开的并且返回时由于某种原因并未关闭该文件描述符,故程序替换之前库函数就算想关闭这些文件描述符也是鞭长莫及。
  • 如果 exec() 因为某种原因调用失败,但此时所有文件描述符已经被 close() 显示关闭了,这种情况下再想打开之前那些文件描述符就很难了。
    为此,内核为每个文件描述符提供了执行时关闭标志。如果设置了这个标志,则该文件描述符会在 exec() 调用成功之后自动被关闭掉,而如果 exec() 调用失败,这些文件描述符将不会受到影响。

想设置 close-on-exec 标志可以在调用 open() 的时候给flags参数加上 O_CLOEXEC。也可以通过系统调用 fcntl() 在程序中访问执行时关闭标志。

3. 信号与 exec()

exec() 调用时会丢掉现有的进程代码段,而该代码段中必然包括了由调用进程设置的信号处理器程序。既然信号处理器程序已经丢失,内核就会对所有设置了信号处理器程序的信号的处置方式改为 SIG_DFL。而对其他信号的处置方式则保持不变。

最后修改:2019 年 11 月 11 日
如果觉得我的文章对你有用,请随意赞赏