揭示/proc/pid/pagemap的力量:在Linux中将虚拟地址映射到物理地址

pagemap的力量:在Linux中将虚拟地址映射到物理地址

  • 一、/proc/pid/pagemap简介
  • 二、了解虚拟地址到物理地址的转换
  • 三、利用/proc/pid/pagemap进行地址转换
    • 3.1、访问/proc/pid/pagemap
    • 3.2、pagemap文件的数据和结构
  • 四、页表、页框架的相关概念
  • 五、总结

一、/proc/pid/pagemap简介

/proc/pid/pagemap是Linux操作系统中的一个特殊文件,它提供了一种机制将虚拟内存地址映射到物理内存地址。在Linux中,每个进程都有一个唯一的进程ID(PID),/proc/pid/pagemap文件存储了与该进程相关联的页面映射信息。访问/proc/pid/pagemap文件需要root权限或具有相应权限的用户身份。对于非活动进程,页面映射信息可能不完整或不可靠。

/proc/pid/pagemap文件包含了进程虚拟地址空间中每个页面的详细信息,包括页面是否被映射、页面的物理地址、页面的访问权限等。通过读取和分析/proc/pid/pagemap文件,可以将虚拟地址转换为物理地址。

Linux内存管理的目的是有效地管理系统的内存资源,以提供可靠的性能和可用性。

  1. 提供虚拟内存:通过虚拟内存,Linux操作系统可以将物理内存和磁盘空间结合起来,使得每个进程都认为它拥有独立的连续地址空间。虚拟内存允许进程使用比实际可用物理内存更大的内存空间,并且提供了更高的灵活性和安全性。

  2. 内存隔离和保护:Linux内存管理确保每个进程在运行时彼此隔离,即使它们共享同一台物理计算机。通过分配独立的虚拟地址空间给每个进程,可以防止进程之间的相互干扰和互相访问数据。

  3. 除了隔离进程,Linux还允许进程共享内存。这通过使用共享内存段、映射相同的文件到多个进程以及使用进程间通信(IPC)机制来实现。内存共享可以提高应用程序之间的通信效率,并减少系统资源的浪费。

  4. Linux内存管理负责分配和回收系统内存。它通过内核内存分配器(如Slab分配器)和垃圾回收机制(如页面回收器)来有效地管理内存资源,并在需要时重新分配和释放内存。

  5. 性能优化:使用高级内存管理技术,如页面置换算法、文件缓存机制和内存压缩,可以最大限度地提高内存的使用效率,并减少访问延迟。

二、了解虚拟地址到物理地址的转换

虚拟地址和物理地址的概念:

  • 虚拟地址是指由CPU生成的地址,用于访问内存中的数据。它在程序执行时由操作系统分配给每个进程,每个进程都认为自己拥有独立的地址空间,其中从0开始的地址都是可用的。虚拟地址空间的最大优势是它提供了一种抽象的地址空间,不受物理内存大小的限制,允许进程使用比实际物理内存更大的内存空间。

  • 物理地址是指内存中存储数据和指令的实际地址。它是指实际的硬件内存地址,由内存管理单元(Memory Management Unit,MMU)将虚拟地址翻译成对应的物理地址。物理地址是实际存储和访问数据的地址,它直接对应于计算机内存模块的实际位置。

在这里插入图片描述

虚拟地址是进程可见的地址,由操作系统管理,使得每个进程拥有独立的地址空间;而物理地址是实际的硬件地址,用于在内存中存储和访问数据。操作系统的内存管理单元负责将虚拟地址转换为对应的物理地址,从而实现进程的正确访问和操作内存。

在内存管理下,虚拟地址到物理地址的转换是由操作系统的内存管理单元(MMU)负责完成的。

操作系统使用一个页表来管理虚拟地址到物理地址的映射。页表是一个数据结构,其中包含虚拟页号(Virtual Page Number,VPN)和物理页号(Physical Page Number,PPN)之间的映射关系。虚拟页号是虚拟地址的一部分,用于定位页表中的对应页项。物理页号指示实际存储页的位置。

当程序访问虚拟地址时,CPU会将虚拟地址发送给MMU。MMU首先将虚拟地址中的虚拟页号与页表进行匹配,查找对应的物理页号。如果找到对应的物理页号,MMU将物理页号与虚拟地址的偏移量相结合,生成对应的物理地址。然后,CPU可以使用这个物理地址在内存中访问或操作数据。

如果虚拟地址的相应页表项未被加载到内存中,或者虚拟地址超出了进程的地址空间,那么就会发生页面错误(Page Fault)。此时,操作系统会响应页面错误中断,并根据需求来执行相应的页调度算法(如页面置换算法)来将所需的页加载到内存中,并更新页表中的映射关系,以便后续的访问。

重新执行转换
虚拟地址
MMU页表查找
是否有映射关系
获取物理页号
生成物理地址
页面错误
操作系统处理
加载所需页到内存
更新页表映射关系

虚拟到物理地址映射涉及很复杂的问题,包括内存管理、页表设计、页面调度、安全性和保护等。

三、利用/proc/pid/pagemap进行地址转换

在Linux系统下,/proc/pid/pagemap是一个虚拟文件,可以用于获取有关进程虚拟内存页面映射的详细信息。访问/proc/pid/pagemap可以使用类似cat、grep、awk等命令或者编程语言(如Python、C++)的文件I/O功能。

3.1、访问/proc/pid/pagemap

使用命令行工具来访问/proc/pid/pagemap:

# 假设进程ID是12345
pid=12345

# 使用cat命令打印/proc/pid/pagemap文件的内容
cat /proc/$pid/pagemap

使用C++语言的文件I/O功能访问/proc/pid/pagemap:

#include <iostream>
#include <fstream>
#include <sstream>
#include <cstring>
#include <unistd.h>
using namespace std;

// 获取指定进程的页表项
unsigned long long get_page_table_entry(int pid, unsigned long long vaddr) {
    // 构造/proc/pid/pagemap文件路径
    stringstream ss;
    ss << "/proc/" << pid << "/pagemap";
    string pagemap_path = ss.str();

    // 打开/proc/pid/pagemap文件
    ifstream pagemap_file(pagemap_path, ios::binary);
    if (!pagemap_file) {
        cerr << "Failed to open pagemap file" << endl;
        return -1;
    }

    // 计算文件中要读取的偏移量
    off_t offset = (vaddr / getpagesize()) * sizeof(unsigned long long);
    pagemap_file.seekg(offset, pagemap_file.beg);

    // 读取一个unsigned long long大小的数据
    unsigned long long entry;
    pagemap_file.read(reinterpret_cast<char*>(&entry), sizeof(entry));

    // 关闭文件
    pagemap_file.close();

    // 提取页框号
    unsigned long long page_frame_number = entry & 0x7FFFFFFFFFFFFF;
    return page_frame_number;
}

int main() {
    int pid = getpid();  // 获取当前进程ID
    unsigned long long vaddr = reinterpret_cast<unsigned long long>(&main);  // 获取main函数的虚拟地址

    // 获取页表项
    unsigned long long page_frame_number = get_page_table_entry(pid, vaddr);
    if (page_frame_number == -1) {
        cout << "Failed to get page table entry" << endl;
    } else {
        cout << "Page Frame Number: " << page_frame_number << endl;
    }

    return 0;
}

/proc/pid/pagemap 文件的格式相当复杂,包含的信息有关该进程的每一页的物理地址、是否被交换出去、是否脏页等。因此,需要对文件内容进行解析才能得到有效的数据。

3.2、pagemap文件的数据和结构

每个进程拥有一个对应的pagemap文件,在该文件中,每个虚拟内存页都有一个对应的64位页表项。这个文件的大小与进程的虚拟内存大小一致,并且根据页大小进行了对齐。

pagemap文件的数据和结构如下:

  1. 64位页表项(Page Table Entry, PTE):每个PTE有64位,代表对应虚拟内存页的映射情况。

    • Bit 63:如果该页面的物理帧在主存中,则为1;如果被换出到磁盘中,则为0。
    • Bit 62:如果该页面被修改(即脏页),则为1;否则为0。
    • Bits 0-54:物理帧号(Physical Frame Number, PFN),表示虚拟内存页映射的物理页面编号。
    • Bit 61-59:保留位,总是为0。
  2. 文件偏移量计算:要获取特定虚拟地址的PTE,需要根据虚拟地址和页大小计算文件中的偏移量。偏移量计算的公式为:offset = (vaddr / page_size) * 8 。这里的vaddr是虚拟地址,page_size是操作系统的页大小,通常是4KB。

C++解析和处理/proc/pid/pagemap文件的数据:

#include <iostream>
#include <fstream>
#include <sstream>
#include <cstring>
#include <unistd.h>
#include <bitset>
using namespace std;

// 获取指定进程的页表项
unsigned long long get_page_table_entry(int pid, unsigned long long vaddr) {
    // 构造/proc/pid/pagemap文件路径
    stringstream ss;
    ss << "/proc/" << pid << "/pagemap";
    string pagemap_path = ss.str();

    // 打开/proc/pid/pagemap文件
    ifstream pagemap_file(pagemap_path, ios::binary);
    if (!pagemap_file) {
        cerr << "Failed to open pagemap file" << endl;
        return -1;
    }

    // 计算文件中要读取的偏移量
    off_t offset = (vaddr / getpagesize()) * sizeof(unsigned long long);
    pagemap_file.seekg(offset, pagemap_file.beg);

    // 读取一个unsigned long long大小的数据
    unsigned long long entry;
    pagemap_file.read(reinterpret_cast<char*>(&entry), sizeof(entry));

    // 关闭文件
    pagemap_file.close();

    // 提取页框号
    const unsigned long long PFN_MASK = 0x7FFFFFFFFFFFFF;  // 物理帧号掩码
    unsigned long long page_frame_number = entry & PFN_MASK;
    return page_frame_number;
}

// 获取物理地址
unsigned long long get_physical_address(int pid, unsigned long long vaddr) {
    unsigned long long page_frame_number = get_page_table_entry(pid, vaddr);
    if (page_frame_number == -1) {
        cerr << "Failed to get page table entry" << endl;
        return -1;
    }

    // 获取页大小
    long page_size = sysconf(_SC_PAGESIZE);

    // 计算物理地址
    unsigned long long physical_address = (page_frame_number * page_size) + (vaddr % page_size);
    return physical_address;
}

int main() {
    int pid = getpid();  // 获取当前进程ID
    unsigned long long vaddr = reinterpret_cast<unsigned long long>(&main);  // 获取main函数的虚拟地址

    // 获取物理地址
    unsigned long long physical_address = get_physical_address(pid, vaddr);
    if (physical_address == -1) {
        cout << "Failed to get physical address" << endl;
    } else {
        cout << "Physical Address: " << physical_address << endl;
    }

    return 0;
}

提取更多页表项或执行其他操作,例如遍历进程的虚拟内存空间并打印每个虚拟地址对应的物理地址:

// 遍历虚拟内存空间并打印每个虚拟地址对应的物理地址
void print_virtual_to_physical_mapping(int pid) {
    // 获取页大小
    long page_size = sysconf(_SC_PAGESIZE);

    // 打开/proc/pid/maps文件
    stringstream maps_ss;
    maps_ss << "/proc/" << pid << "/maps";
    string maps_path = maps_ss.str();
    ifstream maps_file(maps_path);
    if (!maps_file) {
        cerr << "Failed to open maps file" << endl;
        return;
    }

    // 读取maps文件,逐行处理
    string line;
    while (getline(maps_file, line)) {
        istringstream iss(line);
        string addr_range, perms, offset, dev, inode, pathname;

        // 解析maps文件中的行
        iss >> addr_range >> perms >> offset >> dev >> inode >> pathname;

        // 解析地址范围
        size_t dash_pos = addr_range.find('-');
        if (dash_pos == string::npos) {
            cerr << "Invalid address range: " << addr_range << endl;
            continue;
        }
        string start_addr_str = addr_range.substr(0, dash_pos);
        unsigned long long start_addr = stoull(start_addr_str, nullptr, 16);

        // 遍历地址范围,打印虚拟地址和对应的物理地址
        while (start_addr < stoull(addr_range.substr(dash_pos + 1), nullptr, 16)) {
            unsigned long long physical_address = get_physical_address(pid, start_addr);
            if (physical_address == -1) {
                cerr << "Failed to get physical address for virtual address: " << hex << start_addr << endl;
            } else {
                cout << "Virtual Address: " << hex << start_addr << " => Physical Address: " << physical_address << endl;
            }
            start_addr += page_size;  // 移动到下一个页面
        }
    }

    // 关闭maps文件
    maps_file.close();
}

int main() {
    int pid = getpid();  // 获取当前进程ID

    // 打印每个虚拟地址对应的物理地址
    print_virtual_to_physical_mapping(pid);

    return 0;
}

提取虚拟地址并将其转换为物理地址的步骤:

  1. 打开/proc/pid/maps文件,该文件包含了进程的虚拟地址空间的映射信息,可以读取并解析这些信息来获取进程的虚拟地址范围。

  2. 通过位操作和系统调用来处理/proc/pid/pagemap文件,该文件包含了页表项的信息,可以根据需要提取页表项并解析从而获取虚拟地址对应的物理地址或页框号。

  3. 使用C++或其他编程语言编写代码来读取/proc/pid/maps文件,解析其中的数据并获取虚拟地址范围。然后通过位操作和系统调用来读取/proc/pid/pagemap文件,解析页表项并计算虚拟地址对应的物理地址。

  4. 对于每个虚拟地址,通过位操作提取页表项中的页框号,然后计算物理地址。

  5. 进行反转地址映射。

开发语言实现从虚拟地址到物理地址转换的步骤:

  1. 读取/proc/pid/maps文件并解析出虚拟地址范围。
  2. 使用位操作和系统调用来读取/proc/pid/pagemap文件,并根据虚拟地址获取页表项中的页框号。
  3. 根据页框号和页大小计算出物理地址。

四、页表、页框架的相关概念

在计算机系统中,逻辑地址空间被划分为固定大小的页,每个页有自己的逻辑页号。这些逻辑页通过页表被映射到物理内存中的页框(也称页块)。物理内存同样被划分为大小相同的页框,每个页框都有一个物理页框号。

页表是一个数据结构,它存储了一系列页表项,每个页表项对应一个逻辑页号,并指向相应的物理页框号。当CPU发出一个逻辑地址时,会通过提取该地址中的页号部分来查找页表,找到对应的页表项后,获取该页所映射的物理页框号。最后,结合页内偏移量,即可形成完整的物理地址。

  1. 页表(Page table):页表是操作系统中的一种数据结构,用于将虚拟地址映射到物理地址。每个进程都有自己的页表,用于将进程的虚拟地址空间映射到物理内存中的实际物理地址。页表通常是一个二级或三级的树状结构,以支持大型地址空间。
  2. 页(Page):页是操作系统和计算机内存管理中的最小单位,通常大小为 4KB 或 4MB。虚拟地址空间和物理内存都被划分为大小相同的页。虚拟内存中的页被映射到物理内存中的页框架。
  3. 页框架(Page frame):页框架是物理内存中的最小单位,与虚拟内存中的页大小相同。页框架是用于存储进程数据和指令的物理内存块。当虚拟内存中的页被映射到物理内存时,它们将占用一个页框架。

在这里插入图片描述

通过使用页表和页框架,操作系统可以提供虚拟内存功能,让每个进程拥有独立的地址空间并能够访问比物理内存更大的内存空间。页表和页框架的映射使得操作系统可以动态地将进程的虚拟地址映射到物理地址,从而实现内存管理和地址转换。

五、总结

/proc/pid/pagemap 是一个特殊的文件,其中包含了有关进程的虚拟内存页与物理内存页框架之间映射的信息。它可以在 Linux 系统中实现虚拟地址到物理地址的转换。

功能:

  1. 提供虚拟地址到物理地址的转换:/proc/pid/pagemap 文件存储了进程的虚拟内存页到物理内存页框架的映射关系。通过读取和解析该文件,可以获得任意虚拟地址对应的物理地址。
  2. 支持动态映射更新:当进程的虚拟内存页与物理内存页框架之间的映射关系发生变化时(如内存分配、页面置换等),/proc/pid/pagemap 文件可以提供最新的映射信息,以实现地址转换的更新。
  3. 支持查看辅助信息:/proc/pid/pagemap 文件可以提供有关每个虚拟内存页的其他信息,例如是否被加载到物理内存、是否被修改等。
    在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/439027.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

信号处理-探索相邻数据点之间的变化和关联性的操作方法

当前值减去前一个值&#xff0c;乘上当前值与前一个值差值的绝对值 当前值减去后一个值&#xff0c;乘上当前值与后一个值差值的绝对值。 意义何在&#xff1f; 当前值减去前一个值&#xff1a;表示当前数据点与前一个数据点之间的变化量。当前值与前一个值差值的绝对值&…

【Linux】软件管理器yum和编辑器vim

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f3a5;系列专栏&#xff1a;《C语言》 《数据结构》 《C》 《Linux》 《Cpolar》 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 文章目录 一、Linux下安装软件的方案1.1 源代码安装1.2 rpm安装1.3 yum安装 二、Linux软件…

外贸常用的出口认证 | 全球外贸数据服务平台 | 箱讯科技

出口认证是一种贸易信任背书&#xff0c;对许多外贸从业者而言,产品的出口认证和当前的国际贸易环境一样复杂多变&#xff0c;不同的目标市场、不同的产品类别,所需要的认证及标准也不同。 国际认证 01 IECEE-CB IECEE-CB体系的中文含义是“关于电工产品测试证书的相互认可体…

记一次简单的获取虚拟机|伪终端shell权限

场景描述 某个系统是ova文件&#xff0c;导入虚拟机启动&#xff0c;但是启动后只有一个伪终端权限&#xff0c;即权限很小&#xff0c;如何拿到这个虚拟机的shell权限呢&#xff1f; 实际操作 这次运气比较好&#xff0c;所遇到的系统磁盘并没有被加密&#xff0c;所以直接…

吴恩达深度学习笔记:神经网络的编程基础2.1-2.3

目录 第一门课&#xff1a;神经网络和深度学习 (Neural Networks and Deep Learning)第二周&#xff1a;神经网络的编程基础 (Basics of Neural Network programming)2.1 二分类(Binary Classification)2.2 逻辑回归(Logistic Regression) 第一门课&#xff1a;神经网络和深度学…

c++ 11 新特性 不同数据类型之间转换函数之reinterpret_cast

一.不同数据类型之间转换函数reinterpret_cast介绍 reinterpret_cast是C中的一种类型转换操作符&#xff0c;用于执行低级别的位模式转换。具体来说&#xff0c;reinterpret_cast可以实现以下功能&#xff1a; 指针和整数之间的转换&#xff1a;这种转换通常用于在指针中存储额…

如何学习、上手点云算法(三):用VsCode、Visual Studio来debug基于PCL、Open3D的代码

写在前面 本文内容 以PCL 1.14.0&#xff0c;Open3D0.14.1为例&#xff0c;对基于PCL、Open3D开发的代码进行源码debug&#xff1b; 如何学习、上手点云算法系列&#xff1a; 如何学习、上手点云算法(一)&#xff1a;点云基础 如何学习、上手点云算法(二)&#xff1a;点云处理相…

【数据结构】二、线性表:6.顺序表和链表的对比不同(从数据结构三要素讨论:逻辑结构、物理结构(存储结构)、数据运算(基本操作))

文章目录 6.对比&#xff1a;顺序表&链表6.1逻辑结构6.2物理结构&#xff08;存储结构&#xff09;6.2.1顺序表6.2.2链表 6.3数据运算&#xff08;基本操作&#xff09;6.3.1初始化6.3.2销毁表6.3.3插入、删除6.3.4查找 6.对比&#xff1a;顺序表&链表 6.1逻辑结构 顺…

基于pytest的证券清算系统功能测试工具开发

需求 1.造测试数据&#xff1a;根据测试需要&#xff0c;自动化构造各业务场景的中登清算数据与清算所需起来数据 2.测试清算系统功能&#xff1a; 自动化测试方案 工具设计 工具框架图 工具流程图 实现技术 python, pytest, allure, 多进程&#xff0c;mysql, 前端 效果 测…

Web开发介绍,制作小网站流程和需要的技术【详解】

1.什么是web开发 Web&#xff1a;全球广域网&#xff0c;也称为万维网(www World Wide Web)&#xff0c;能够通过浏览器访问的网站。 所以Web开发说白了&#xff0c;就是开发网站的&#xff0c;例如网站&#xff1a;淘宝&#xff0c;京东等等 2. 网站的工作流程 1.首先我们需…

【Godot4自学手册】第二十一节掉落金币和收集

这一节我们主要学习敌人死亡后随机掉落金币&#xff0c;主人公可以进行拾取功能。 一、新建金币场景 新建场景&#xff0c;节点选择CharacterBody2D&#xff0c;命名为Coins&#xff0c;将场景保存到Scenes目录下。 1.新建节点 为根节点依次添加CollisionShape2D节点&#…

阿里云服务器使用教程_2024建站教程_10分钟网站搭建流程

使用阿里云服务器快速搭建网站教程&#xff0c;先为云服务器安装宝塔面板&#xff0c;然后在宝塔面板上新建站点&#xff0c;阿里云服务器网aliyunfuwuqi.com以搭建WordPress网站博客为例&#xff0c;来详细说下从阿里云服务器CPU内存配置选择、Web环境、域名解析到网站上线全流…

Dubbo基础入门二

8、Dubbo协议 服务调用 8.1 服务端 启动过程深入分析 我们查看一下服务启动的过程 ProtocolFilterWrapper.export 好我们进入DubboProtocol.export 创建服务 分析我们的Handler 我们接着返回刚才位置 下面的super方法里面会创建服务&#xff0c;ChannelHandlers.wrap会对hand…

2024年3月8日蚂蚁新村今日答案:以下哪一项传统武术项目入选了人类非物质文化遗产代表作名录?太极拳还是咏春拳

蚂蚁新村是一个虚拟社区。在这个虚拟社区中&#xff0c;用户可以参与各种活动&#xff0c;比如生产能量豆、做慈善捐赠等。同时&#xff0c;蚂蚁新村也提供了一些知识问答环节&#xff0c;用户在参与的过程中可以增进知识。这些问答内容往往涉及广泛的主题&#xff0c;如文化、…

idea Gradle 控制台中文乱码

如下图所示&#xff0c;idea 中的 Gradle 控制台中文乱码&#xff1a; 解决方法&#xff0c;如下图所示&#xff1a; 注意&#xff1a;如果你的 idea 使用 crack 等方式破解了&#xff0c;那么你可能需要在文件 crack-2023\jetbra\vmoptions\idea.vmoptions 中进行配置&#xf…

git分布式管理-头歌实验标签

一、创建标签 任务描述 现在你已经成了项目负责人&#xff0c;由你负责发布版本&#xff0c;你需要在发布一个版本之前&#xff0c;给该版本对应的代码打上标签&#xff0c;以便于管理和标识。 本关任务&#xff1a;为最近一次提交打上标签。 相关知识 在开发过程中&#xff0c…

Android14之禁止vbmeta.img签名校验(一百九十)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

七彩虹八渐变 外贸建站公司wordpress模板

进出口水果wordpress外贸模板 漂亮水果wordpress外贸模板&#xff0c;做水果进出品生意的外贸公司自建站官网模板。 https://www.jianzhanpress.com/?p3516 玩具wordpress外贸模板 简洁玩具wordpress外贸模板&#xff0c;适合做跨境电商外贸公司使用的wordpres外贸s网站主题…

追寻工作与生活的和谐之道

在现代社会&#xff0c;人们往往被快节奏的工作和生活所困扰&#xff0c;如何在这两者之间找到平衡点&#xff0c;成为许多人关注的焦点。本文将为您介绍一些实用的方法和建议&#xff0c;帮助您实现工作与生活的和谐共处。 一、合理规划时间&#xff0c;提高工作效率 时间是实…

day37 贪心算法part6

738. 单调递增的数字 中等 提示 当且仅当每个相邻位数上的数字 x 和 y 满足 x < y 时&#xff0c;我们称这个整数是单调递增的。 给定一个整数 n &#xff0c;返回 小于或等于 n 的最大数字&#xff0c;且数字呈 单调递增 。 不知道怎么讲思路……以9287举例&#xff0c;…