Re:Linux系统篇(二十六)进程篇·十一:从底层原理到 exec* 家族:彻底搞懂 Linux 进程程序替换


◆ 博主名称: 小此方-CSDN博客 大家好,欢迎来到小此方的博客。
⭐️Linux系列个人专栏: 【主题曲】Linux
⭐️此方的GitHub: github_此方
⭐️ Re系列专栏:我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)


文章目录


概要&序論

本文深刻探讨 Linux 进程程序替换机制。

  • 核心内容:剖析程序替换不创建新进程、重映射页表与写时拷贝的底层物理内存原理;
  • 核心内容 :详解 execlexeclpexecvexec* 全系列接口的参数构造与传参差异;
  • 细节补充 :阐述自定义环境变量传递与 putenv 对子进程环境表的影响;
  • 细节补充 :揭示 exec* 仅有失败返回值的独特机制与代码避坑规范;
      好的,我们直接开始。

一、 进程程序替换的原理

1.1 什么是进程程序替换?

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

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

1.2 程序替换的底层原理

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

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

  如何证明进程程序替换没有生成新的进程?

二、 认识 exec* 系列函数

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

2.1 基础接口:execl

2.1.1详细介绍

c 复制代码
int execl(const char *path, const char *arg, ...);
  • 参数含义

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

    c 复制代码
    execl("/usr/bin/ls", "ls", "-a", "-l", NULL);

2.1.2详细测试代码

cpp 复制代码
#include<iostream>
#include<cstring>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
using namespace std;
void Func01(){
        pid_t id = fork();
        if(id==0){
                char* _argv[]={"ls","-a","-l",NULL};
                execl("/bin/ls","ls","-a","-l",NULL);
                exit(0);
        }
        int status = 0;
        pid_t rid = waitpid(id, &status , 0);
        if(rid > 0)
                cout<<"子进程退出成功过"<<"退出码是:"<<WEXITSTATUS(status)<<endl;
        else if(rid < 0)
                cout<<"子进程退出异常"<<endl;

}
int main(){
        Func01();
        return 0;
}

2.2 自动搜索路径:execlp

2.2.1详细介绍

c 复制代码
int execlp(const char *file, const char *arg, ...);
  • 特点 :名字中的 p 代表 PATH 。它不需要输入完整的路径,只需要提供可执行文件的文件名

  • 机制 :系统会自动在环境变量 $PATH 中查找指定的命令。

  • 示例

    c 复制代码
    execlp("ls", "ls", "-l", "-a", NULL);

2.2.2详细测试代码

cpp 复制代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
using namespace std;
void Func01()
{
	pid_t id = fork();
	if(id==0)
	{
		char* _argv[]={"ls","-a","-l",NULL};
		//execv("/bin/ls",_argv);
		//execl("/bin/ls","ls","-a","-l",NULL);
		execlp("ls","-a","-l",NULL);
		execlp("./myscript.py","myscript.py",NULL);
		exit(0);
	}
	int status = 0;
	pid_t rid = waitpid(id, &status , 0);
	if(rid > 0)
		cout<<"子进程退出成功过"<<"退出码是:"<<WEXITSTATUS(status)<<endl;
	else if(rid == 0 )
		cout<<"子进程未退出"<<endl;
	else if(rid < 0)
		cout<<"子进程退出异常"<<endl;
}
int main()
{
	Func01();
	//Func02();
	return 0;
}

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

cpp 复制代码
execlp("/bin/ls","ls","-a","-l",NULL);

2.3 数组传参:execv

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

  • 示例

    c 复制代码
    char *const my_argv[] = {
        "ls",
        "-l",
        "-a",
        NULL // 数组最后同样必须以 NULL 结尾
    };
    execv("/usr/bin/ls", my_argv);

2.4 传递环境变量:execvpe 与 execle

2.4.1 核心接口介绍

c 复制代码
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)会全部丢失。

2.4.1.1利用 putenv 实现追加传递

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

2.4.1.2环境变量的单向传递性

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

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

2.4.2详细测试代码

cpp 复制代码
//proc.cpp
#include<iostream>
#include<cstdio>
#include<cstring>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
using namespace std;
char* newenv = (char* ) "MyEnv=11223344";
void Func()
{

	pid_t id = fork();
	if(id==0)
	{
		putenv(newenv);
		extern char** environ;
		char* _argv[]={(char*)"other",NULL};
		//execv("/bin/ls",_argv);
		//execl("/bin/ls","ls","-a","-l",NULL);
		//execlp("ls","-a","-l",NULL);
		//execlp("./myscript.py","myscript.py",NULL);
		execvpe("./other",_argv,environ);
		exit(0);
	}
	int status = 0;
	pid_t rid = waitpid(id, &status , 0);
	if(rid > 0)
		cout<<"子进程退出成功过"<<"退出码是:"<<WEXITSTATUS(status)<<endl;
	else if(rid == 0 )
		cout<<"子进程未退出"<<endl;
	else if(rid < 0)
		cout<<"子进程退出异常"<<endl;

}

int main()
{
	Func();
	return 0;
}
//other.cpp
#include<iostream>
using namespace std;
int main(){
	extern char** environ;
	for(int i =0;environ[i];i++)
		cout<<environ[i];
	return 0;
}

2.5接口的命名规律

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

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

2.6函数返回值与错误处理

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

2.6.1 只有失败返回值

   成功时没有返回值。因为一旦替换成功,原程序的整个代码段、包括接收返回值的逻辑全部都不复存在了,进程会直接去执行新程序的代码。

  • 失败时一定会返回 -1 ,并且系统会设置全局变量 errno 以指示具体的错误原因。

2.6.2 代码编写避坑指南

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

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

2.7谁才是真正的系统调用

  事实上,只有 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没有被使用,使用的是你传递过去的那个"

2.8 支持跨语言替换

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

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

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

2.8.1 环境准备

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

1. Shell 脚本 (myscript.sh)
bash 复制代码
#!/bin/bash
echo "我是一个shell脚本"
2. Python 脚本 (myscript.py)
python 复制代码
#!/usr/bin/env python3
print("我是一个python脚本")

关键细节:绝对路径与可执行权限

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

补充小知识:

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

2.8.2 详细测试代码

  以下 C++ 代码将通过 fork() 创建子进程,并分别演示使用 execvexecl 接口去替换执行上述的 Shell 脚本和 Python 脚本。

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <cstdlib>

using namespace std;

void TestScriptReplace()
{
    // 1. 测试替换为 Shell 脚本
    pid_t id1 = fork();
    if (id1 == 0)
    {
        cout << "[子进程1] 准备替换为 Shell 脚本..." << endl;
        // 使用 execl,通过相对路径直接执行拥有 +x 权限的脚本
        execl("./myscript.sh", "myscript.sh", NULL);
        
        // 如果走到这里,说明替换失败
        perror("exec shell failed");
        exit(1);
    }

    // 2. 测试替换为 Python 脚本
    pid_t id2 = fork();
    if (id2 == 0)
    {
        cout << "[子进程2] 准备替换为 Python 脚本..." << endl;
        // 使用 execv 传参
        char* const py_argv[] = {(char*)"myscript.py", NULL};
        execv("./myscript.py", py_argv);
        
        // 如果走到这里,说明替换失败
        perror("exec python failed");
        exit(2);
    }

    // 父进程回收子进程资源
    int status = 0;
    pid_t rid1 = waitpid(id1, &status, 0);
    if (rid1 > 0 && WIFEXITED(status)) {
        cout << "Shell 脚本子进程退出成功,退出码:" << WEXITSTATUS(status) << endl;
    }

    pid_t rid2 = waitpid(id2, &status, 0);
    if (rid2 > 0 && WIFEXITED(status)) {
        cout << "Python 脚本子进程退出成功,退出码:" << WEXITSTATUS(status) << endl;
    }
}

int main()
{
    TestScriptReplace();
    return 0;
}

好的本期内容就到这里,如果对你有帮助,还不要忘记点赞三联支持。我是此方,我们下期再见。bye!

相关推荐
wgc2k1 小时前
Node.js游戏服务器项目移植 3-手撸简单的内存泄露监控
服务器·游戏·node.js
码农小白AI8 小时前
AI报告审核加速融入自动化实验室:IACheck破解智能设备时代报告管理新挑战
运维·人工智能·自动化
utf8mb4安全女神8 小时前
克隆的虚拟机怎么更改ip地址
运维
赵民勇8 小时前
fuse-overlayfs命令详解
linux·容器
tedcloud1238 小时前
DeepSeek-TUI部署教程:打造CLI AI助手环境
服务器·人工智能·word·excel·dreamweaver
sulikey8 小时前
个人Linux操作系统学习笔记6 - 操作系统与进程初识
linux·笔记·学习·操作系统·进程
无情的西瓜皮9 小时前
MCP协议实战:用Python从零搭建一个AI Agent工具服务器(保姆级教程)
服务器·人工智能·python·mcp
万能的知了10 小时前
服务器托管 vs 云主机 vs 裸金属:一个决策故事
运维·服务器·云计算
杨云龙UP10 小时前
Oracle RAC / ODA 生产环境指定 PDB 启动 SOP
linux·运维·数据库·oracle