云计算百科
云计算领域专业知识百科平台

【Linux】程序替换

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

一、进程程序替换的原理

1.1 什么是进程程序替换?

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

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

1.2 程序替换的底层原理

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

在这里插入图片描述

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

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

test.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
printf("替换程序要运行了!\\n");
printf("我的pid: %d\\n", getpid());
printf("替换程序运行完毕了\\n");
return 0;
}

myexec.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
printf("我的程序要运行了!\\n");
printf("我的pid: %d\\n", getpid());
execl("./test", "test", NULL);
printf("我的程序运行完毕了\\n");
return 0;
}

在这里插入图片描述

我们发现两件事:

  • pid 没变,说明是同一个进程;
  • 主进程 execl 后面的内容没有执行。
  • 二、认识 exec* 系列函数

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

    在这里插入图片描述

    2.1 execl

    1. 介绍

    int execl(const char *path, const char *arg, ...);

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

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

    2. 代码

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

    void Func01()
    {
    pid_t id = fork();
    if (id == 0)
    {
    execl("/bin/ls", "ls", "-a", "-l", NULL);
    exit(0);
    }

    int status = 0;
    // 等待 pid 为 id 的子进程,等待方式为阻塞等待
    pid_t wid = waitpid(id, &status, 0);
    if (wid > 0)
    printf("子进程退出成功,退出码是:%d\\n", WEXITSTATUS(status));
    else if (wid < 0)
    printf("子进程退出异常\\n");
    }

    在这里插入图片描述

    2.2 自动搜索路径:execlp

    1. 介绍

    int execlp(const char *file, const char *arg, ...);

    • 特点:名字中的 p 代表 PATH。它不需要输入完整的路径,只需要提供可执行文件的文件名;
    • 机制:系统会自动在环境变量 $PATH 中查找指定的命令;
    • 示例:

    execlp("ls", "ls", "-l", "-a", NULL);

    2. 代码

    void Func01()
    {
    pid_t id = fork();
    if (id == 0)
    {
    // execl("/bin/ls", "ls", "-a", "-l", NULL);
    execlp("ls", "ls", "-l", "-a", NULL);
    exit(0);
    }

    int status = 0;
    // 等待 pid 为 id 的子进程,等待方式为阻塞等待
    pid_t wid = waitpid(id, &status, 0);
    if (wid > 0)
    printf("子进程退出成功,退出码是:%d\\n", WEXITSTATUS(status));
    else if (wid < 0)
    printf("子进程退出异常\\n");
    }

    在这里插入图片描述

    3. 补充

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

    2.3 数组传参:execv

    1. 介绍

    int execv(const char *path, char *const argv[]);

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

    2. 代码

    void Func01()
    {
    pid_t id = fork();
    if (id == 0)
    {
    // 数组最后一位也要带 NULL
    char *_argv[] = {"ls", "-a", "-l", NULL};
    // execl("/bin/ls", "ls", "-a", "-l", NULL);
    // execlp("ls", "ls", "-l", "-a", NULL);
    execv("/bin/ls", _argv);
    exit(0);
    }

    int status = 0;
    // 等待 pid 为 id 的子进程,等待方式为阻塞等待
    pid_t wid = waitpid(id, &status, 0);
    if (wid > 0)
    printf("子进程退出成功,退出码是:%d\\n", WEXITSTATUS(status));
    else if (wid < 0)
    printf("子进程退出异常\\n");
    }

    在这里插入图片描述

    在这里插入图片描述

    2.4 传递环境变量:execvpe 与 execle

    1. 介绍

    int execle(const char *path, const char *arg, ..., char *const envp[]);
    int execvpe(const char *file, char *const argv[], char *const envp[]);

    • 特点:名字中的 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

    int main()
    {
    printf("替换程序要运行了!\\n");
    // printf("我的pid: %d\\n", getpid());

    // 全局的、指向 main 函数 env 表的二级指针
    extern char** environ;
    int i = 0;
    for(i; environ[i]; i++)
    printf("%s\\n", environ[i]);

    printf("替换程序运行完毕了\\n");
    return 0;
    }

    myexec.c

    void Func01()
    {
    pid_t id = fork();
    if (id == 0)
    {
    char env[] = "MYNAME=ljm";
    // 追加导入 env
    putenv(env);

    // 全局的、指向 env 表的二级指针
    extern char** environ;
    // 数组最后一位也要带 NULL
    char *_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);
    }

    int status = 0;
    // 等待 pid 为 id 的子进程,等待方式为阻塞等待
    pid_t wid = 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));
    }
    else if (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. 准备

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

  • Shell 脚本(mytest.sh):
  • #!/bin/bash
    echo "我是一个shell脚本"

  • Python 脚本(mytst.py):
  • #!/usr/bin/env python3
    print("我是一个python脚本")

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

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

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

    4. 代码

    void Func01()
    {
    pid_t id = fork();
    if (id == 0)
    {
    char env[] = "MYNAME=ljm";
    // 追加导入 env
    putenv(env);

    // 全局的、指向 env 表的二级指针
    extern char** environ;
    // 数组最后一位也要带 NULL
    char *_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);
    }

    int status = 0;
    // 等待 pid 为 id 的子进程,等待方式为阻塞等待
    pid_t wid = 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));
    }
    else if (wid < 0)
    printf("子进程退出异常\\n");
    }

    在这里插入图片描述

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【Linux】程序替换
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!