ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

【Linux】程序替换

【Linux】程序替换

一、进程程序替换的原理

1.1 什么是进程程序替换?

在 Linux 中,用fork()创建子进程后,子进程往往会执行与父进程相同的代码(或者是通过 if-else 分流执行父进程代码的一部分)。如果我们想让子进程执行一个全新的程序,就需要用到进程程序替换。

进程程序替换是通过exec*系列系统调用/库函数实现的。当进程调用这类函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的初始化代码开始执行。

1.2 程序替换的底层原理

  • 不创建新进程:程序替换前后,进程的 PID 并没有发生改变。它依然是原来的那个进程,只是内核把新程序的代码和数据加载到了该进程的物理内存中;
  • 页表与物理内存的映射改变:原本进程的虚拟地址空间通过页表映射到旧的代码和数据。当调用exec*函数时,磁盘上要执行的新程序会被加载器加载到物理内存中。接着,操作系统会重新更新该进程的页表映射关系,让虚拟地址空间的各个区域指向新加载的代码和数据;
  • 写时拷贝保证独立性:父子进程原本共享代码段,一旦子进程调用exec*,发生大范围的代码和数据覆盖,操作系统会触发写时拷贝。此时,子进程会拥有属于自己独立的物理内存空间,从而绝对不会影响父进程的代码和数据。这也完美体现了进程的独立性。

操作系统内核的加载器就是 exec 的这个原理。

代码证明进程程序替换没有生成新的进程(先看运行结果,后面解释):

test.c

#include<stdio.h>#include<unistd.h>#include<stdlib.h>intmain(){printf("替换程序要运行了!\n");printf("我的pid: %d\n",getpid());printf("替换程序运行完毕了\n");return0;}

myexec.c

#include<stdio.h>#include<unistd.h>#include<stdlib.h>intmain(){printf("我的程序要运行了!\n");printf("我的pid: %d\n",getpid());execl("./test","test",NULL);printf("我的程序运行完毕了\n");return0;}

我们发现两件事:

  1. pid 没变,说明是同一个进程;
  2. 主进程execl后面的内容没有执行。

二、认识 exec* 系列函数

Linux 提供了多种 exec 版本的函数,它们的底层功能相同,但参数传递的方式有所差异。

2.1 execl

1. 介绍

intexecl(constchar*path,constchar*arg,...);
  • 参数含义:
    • path:要执行程序的绝对路径或相对路径(回答了"我要执行谁");
    • arg:命令行参数列表。我们在终端怎么写命令,就怎么传参数(回答了"我想怎么执行它");
    • ...:可变参数列表,必须以 NULL 结尾,用以表明参数传递结束。
  • 示例:
execl("/usr/bin/ls","ls","-a","-l",NULL);

2. 代码

我们都知道指令就是一个程序,都是存放在/usr/bin/ls下的可执行程序,那我们把它替换进来试试:

voidFunc01(){pid_tid=fork();if(id==0){execl("/bin/ls","ls","-a","-l",NULL);exit(0);}intstatus=0;// 等待 pid 为 id 的子进程,等待方式为阻塞等待pid_twid=waitpid(id,&status,0);if(wid>0)printf("子进程退出成功,退出码是:%d\n",WEXITSTATUS(status));elseif(wid<0)printf("子进程退出异常\n");}

2.2 自动搜索路径:execlp

1. 介绍

intexeclp(constchar*file,constchar*arg,...);
  • 特点:名字中的p 代表 PATH。它不需要输入完整的路径,只需要提供可执行文件的文件名;
  • 机制:系统会自动在环境变量$PATH中查找指定的命令;
  • 示例:
execlp("ls","ls","-l","-a",NULL);

2. 代码

voidFunc01(){pid_tid=fork();if(id==0){// execl("/bin/ls", "ls", "-a", "-l", NULL);execlp("ls","ls","-l","-a",NULL);exit(0);}intstatus=0;// 等待 pid 为 id 的子进程,等待方式为阻塞等待pid_twid=waitpid(id,&status,0);if(wid>0)printf("子进程退出成功,退出码是:%d\n",WEXITSTATUS(status));elseif(wid<0)printf("子进程退出异常\n");}

3. 补充

虽然它说可以不用带上地址,但是我们带上地址也没事,其他的同理。这里的接口设计师在设计的时候考量的问题挺多的,当然写的时候尽可能严谨一点。

2.3 数组传参:execv

1. 介绍

intexecv(constchar*path,char*constargv[]);
  • 特点:名字中的v 代表 vector(数组)。它不再使用可变参数一个一个传,而是把所有的命令行参数装进一个指针数组中统一传递。

2. 代码

voidFunc01(){pid_tid=fork();if(id==0){// 数组最后一位也要带 NULLchar*_argv[]={"ls","-a","-l",NULL};// execl("/bin/ls", "ls", "-a", "-l", NULL);// execlp("ls", "ls", "-l", "-a", NULL);execv("/bin/ls",_argv);exit(0);}intstatus=0;// 等待 pid 为 id 的子进程,等待方式为阻塞等待pid_twid=waitpid(id,&status,0);if(wid>0)printf("子进程退出成功,退出码是:%d\n",WEXITSTATUS(status));elseif(wid<0)printf("子进程退出异常\n");}

2.4 传递环境变量:execvpe 与 execle

1. 介绍

intexecle(constchar*path,constchar*arg,...,char*constenvp[]);intexecvpe(constchar*file,char*constargv[],char*constenvp[]);
  • 特点:名字中的e 代表 environment。它允许开发者向被替换后的新进程传递一张全新的环境变量表envp;
  • 应用场景:如果我们希望被替换的子进程在一个纯净的、或者完全由我们定制的环境变量下运行,就可以使用带 e 的接口;
  • 核心冲突:

正常情况下,子进程不需手动传递就能默认获取父进程的环境变量。但带有 e 的接口传递环境变量是覆盖式的,而不是追加式的。一旦使用,新进程原有的环境变量(如 PATH)会全部丢失。

  • 利用putenv实现追加传递:

如果我们只想新增一两个环境变量给子进程,可以在调用exec*之前,使用putenv(char *string)函数将新变量导入到当前进程的全局environ中,然后再通过environ整体传递给子进程。

  • 环境变量的单向传递性:

假设存在进程链:A → B → C(B 是 A 的子进程,C 是 B 的子进程)。

如果 B 进程putenv了一个环境变量,C 看得到,A 看不到。这是因为子进程的环境变量拷贝自父进程,修改只能向下单向传递,无法逆向影响父进程。

2. 代码

test.c

intmain(){printf("替换程序要运行了!\n");// printf("我的pid: %d\n", getpid());// 全局的、指向 main 函数 env 表的二级指针externchar**environ;inti=0;for(i;environ[i];i++)printf("%s\n",environ[i]);printf("替换程序运行完毕了\n");return0;}

myexec.c

voidFunc01(){pid_tid=fork();if(id==0){charenv[]="MYNAME=ljm";// 追加导入 envputenv(env);// 全局的、指向 env 表的二级指针externchar**environ;// 数组最后一位也要带 NULLchar*_argv[]={"ls","-a","-l",NULL};// execl("/bin/ls", "ls", "-a", "-l", NULL);// execlp("ls", "ls", "-l", "-a", NULL);// execv("/bin/ls", _argv);execle("./test",NULL,environ);execve("./test",_argv,environ);exit(0);}intstatus=0;// 等待 pid 为 id 的子进程,等待方式为阻塞等待pid_twid=waitpid(id,&status,0);if(wid>0){inti=0;externchar**environ;// 验证环境变量只能向下传for(i;environ[i];i++)printf("%s\n",environ[i]);printf("子进程退出成功,退出码是:%d\n",WEXITSTATUS(status));}elseif(wid<0)printf("子进程退出异常\n");}

三、接口其他问题

3.1 接口的命名规律

只要理解了接口后缀的英文字母,就能轻松记忆和选用所有接口:

  • l(list):参数采用列表形式(可变参数);
  • v(vector):参数采用指针数组形式;
  • p(path):能自动在$PATH环境变量中搜索可执行文件;
  • e(env):可以传入自定义的环境变量数组。

3.2 函数返回值与错误处理

1. 函数返回值

exec*系列函数的返回值设计非常特殊,与绝大多数系统调用不同。

  • 成功时:没有返回值。因为一旦替换成功,原程序的整个代码段、包括接收返回值的逻辑全部都不复存在了,进程会直接去执行新程序的代码;
  • 失败时:一定会返回 -1,并且系统会设置全局变量errno以指示具体的错误原因。

2. 错误处理

因为exec*函数只要成功就绝不返回,所以在实际编写代码时,我们不需要对exec*成功的情况做任何返回值判断。只要exec*后面的代码被执行了,那必然是替换失败了!

// 经典规范写法execvp("ls",argv);// 如果能走到下一行,说明 exec 必然失败了!perror("exec failed");exit(1);

3.3 底层是谁

事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve,所以execve在 man 手册第 2 节,其它函数在 man 手册第 3 节。这些函数之间的关系如下图所示。

其他的诸如execl、execlp、execle、execv、execvp、execvpe等,全部都是在execve之上进行的 C 语言库函数封装。

那么这个封装体现在哪里?我举一个例子,我们 C 语言库函数带 e 的 exec 族函数可以传递环境变量,但是实际上没有带 e 的 exec 族函数其实底层使用了环境变量的缺省值(environ)。原因是底层封装的系统调用execve支持它这么干。

这也是为什么带 e 的 exec 族函数会覆盖式地传递环境变量的根本原因:“缺省参数environ没有被使用,使用的是你传递过去的那个”。

3.4 支持跨语言替换

进程程序替换不仅可以替换由 C/C++ 编译出来的二进制可执行程序,还可以替换成 Shell、Python 等任意脚本语言。

核心原理:无论是二进制程序还是脚本文件,在操作系统看来都是一个可执行文件。对于脚本文件,只要它拥有可执行权限,且其首行指定了正确的解释器路径,内核就会自动调用相应的解释器来执行该脚本。

只要是进程就可以进行进程程序替换。

1. 准备

在进行代码测试前,我们需要在同级目录下准备好相应的脚本文件。脚本通过解释器运行,不需要进行编译。

  1. Shell 脚本(mytest.sh):
#!/bin/bashecho"我是一个shell脚本"
  1. Python 脚本(mytst.py):
#!/usr/bin/env python3print("我是一个python脚本")

2. 绝对路径与可执行权限

  1. Shebang 的意义:#!/bin/bash和#!/usr/bin/env python3告知内核该用哪个路径下的解释器去加载后面的脚本代码;
  2. 赋予可执行权限:脚本编写完成后,必须在终端通过chmod +x myscript.sh myscript.py命令为它们加上可执行权限,否则子进程在执行程序替换时会报Permission denied的错误。

3. 解释型语言

Python 等解释型语言在运行时,本质上是在执行解释器程序。解释器负责将源代码动态翻译并执行。同理,Shell 脚本的解释器就是系统中的 Shell 终端程序本身。

4. 代码

voidFunc01(){pid_tid=fork();if(id==0){charenv[]="MYNAME=ljm";// 追加导入 envputenv(env);// 全局的、指向 env 表的二级指针externchar**environ;// 数组最后一位也要带 NULLchar*_argv[]={"ls","-a","-l",NULL};// execl("/bin/ls", "ls", "-a", "-l", NULL);// execlp("ls", "ls", "-l", "-a", NULL);// execv("/bin/ls", _argv);// execle("./test", NULL, environ);// execve("./test", _argv, environ);// execl("./mytest.sh", NULL);execl("./mytst.py",NULL);exit(0);}intstatus=0;// 等待 pid 为 id 的子进程,等待方式为阻塞等待pid_twid=waitpid(id,&status,0);if(wid>0){// int i = 0;// extern char** environ;// 验证环境变量只能向下传// for(i; environ[i]; i++)// printf("%s\n", environ[i]);printf("子进程退出成功,退出码是:%d\n",WEXITSTATUS(status));}elseif(wid<0)printf("子进程退出异常\n");}

返回列表