实现 Windows 应用线上虚拟内存监控

1,106 阅读5分钟

本文正在参加「金石计划」

背景

  • Windows 内存管理知识总结 一文中,我提到了 Win32 程序由于虚拟内存不足导致的 OOM 问题,以及介绍了一些解决方案
  • 如何将 win32 程序虚拟内存扩展到 3GB? 一文中,我介绍了扩展虚拟内存的方案,在线下环境中,我们可以借助官方提供的工具 vmmap 查看具体内存布局 但是,线上环境中,如何验证程序是否应用了扩展方案?我们需要拿到线上具体的内存指标来证明
  • 本文将继续介绍,如何利用 Windows 相关 API 开发一个能够监控线上虚拟内存的工具

虚拟内存线上监控方案

1、直接使用 vmmap

vmmap_usage.png

可以看到 vmmap 提供了命令行 api,可以直接 dump 出一份 vmmap 文件,在应用中可以拿到文件后直接上传,然后再用 vmmap 打开分析,这个方案看起来天衣无缝,但是,

此方案有两个缺陷:

  1. 需要将 vmmap.exe 打包到应用中
  2. 执行命令后,会弹出 vmmap 的 GUI(致命缺陷),并不是后台帮我们 dump 出一份文件,我们不希望用户能感知监控行为

所以,此方案无法使用

2、仿 vmmap 能力,实现自己的监控工具

此方案的成本相比 1 会大出很多,但好在经过一轮调研,有大佬已经实现了类似功能并开源twpol/vmmap,调研过程中,好像还看到有人吐槽 vmmap 作者 Mark Russinovich(微软 Azure CTO)比较吝啬,不想告诉大家 vmmap 是如何实现的(此处一个偷笑表情)

开源库是一个命令行工具,我们仍需要读懂源码,然后想办法集成到自己的应用中,由于我的应用是跑在 Windows 上的 Java 程序,所以还需要额外写 JNI 相关的代码

下面,我就详细介绍一下如何实现自己的监控工具

我们需要的指标是什么?

首先要想清楚我们到底需要什么指标?无论是 vmmap 还是开源库都提供了完整的虚拟内存布局信息,而我们其实只需要:

  1. 虚拟内存总大小,TotalSize
  2. 当前可用的大小,FreeSize
  3. 当前已用的大小,UsedSize
  4. 当前不可用的内存大小(碎片),DisabledSize

具体方案

vm_monitor.png

流程其实不复杂,就是借助 Windows 提供的内存相关的数据结构和 API 来实现,具体看来分为如下步骤:

  1. 获取当前进程 id,CurrentProcessId
  2. 通过 OpenProcess 获得当前进程 Handle
  3. 打开 Debug 权限(如果没有这步,无法获得内存快照信息)
  4. 遍历内存地址空间
    1. 获得 DisabledSize
    2. 获取对应类型的内存信息 Disabled Memory Info Free Memory Info Used Memory Info
  5. 关闭 Debug 权限(不要忘了这步)
  6. 将程序打包成 .dll 供 Java 侧使用
  7. 写 JNI 部分代码
  8. 在 Java 中调用 .dll 获取 VirtualMemoryInfo

下面贴出关键部分代码,供大家参考:

  1. 核心数据结构
// vm_query 是核心数据结构,定义了我们需要的指标
class vm_query
{
private:
	const DWORD _pid;
	unsigned long long _free = 0;
	unsigned long long _total_used = 0;
	unsigned long long _unusable = 0;
	const void collect_vm_result();
public:
	vm_query();
	~vm_query();
	void enable_privilege(const tstring privilege_name);
	void disable_privilege(const tstring privilege_name);
	const unsigned long long get_free_size() { return _free; }
	const unsigned long long get_total_used_size() { return _total_used; }
	const unsigned long long get_unusable_size() { return _unusable; }
};
  1. 开启/关闭权限
// 开启 Debug 权限
void vm_query::enable_privilege(const std::wstring privilege_name)
{
	HANDLE h_token;
	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &h_token))
	{
		return;
	}

	LUID luid;
	if (!LookupPrivilegeValue(NULL, privilege_name.c_str(), &luid))
	{
		return;
	}

	TOKEN_PRIVILEGES tp;
	tp.PrivilegeCount = 1;
	tp.Privileges[0].Luid = luid;
	tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
	if (!AdjustTokenPrivileges(h_token, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), (PTOKEN_PRIVILEGES)NULL, (PDWORD)NULL))
	{
		return;
	}
	if (GetLastError() == ERROR_NOT_ALL_ASSIGNED)
	{
		return;
	}
}

// 关闭 Debug 权限
void vm_query::disable_privilege(const std::wstring privilege_name)
{
	HANDLE h_token;
	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &h_token))
	{
		return;
	}

	LUID luid;
	if (!LookupPrivilegeValue(NULL, privilege_name.c_str(), &luid))
	{
		return;
	}

	TOKEN_PRIVILEGES tp;
	tp.PrivilegeCount = 1;
	tp.Privileges[0].Luid = luid;
	tp.Privileges[0].Attributes = 0;
	if (!AdjustTokenPrivileges(h_token, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), (PTOKEN_PRIVILEGES)NULL, (PDWORD)NULL))
	{
		return;
	}
	if (GetLastError() == ERROR_NOT_ALL_ASSIGNED)
	{
		return;
	}
}
  1. 遍历地址空间,并收集数据
// Collect virtual memory allocations
vm_info vm_info;
{
  BOOL is_wow_64;
  IsWow64Process(h_process, &is_wow_64);

  SYSTEM_INFO system_info;
  GetNativeSystemInfo(&system_info);

  unsigned long long max_address = 0x100000000; //4GB+1

  //后文会详细介绍此数据结构是什么
  MEMORY_BASIC_INFORMATION mem_info = { 0 };
  for (unsigned long long addr = 0; addr < max_address; addr += mem_info.RegionSize)
  {
    //使用 VirtualQueryEx 查询当前内存地址空间内容,后文会详细介绍此 API 如何使用
    SIZE_T size = VirtualQueryEx(h_process, (void*)addr, &mem_info, sizeof(mem_info));
    if (size == 0) break;

    // 当前进程不可用的虚拟内存
    if (mem_info.State == MEM_FREE && system_info.dwAllocationGranularity > system_info.dwPageSize)
    {
      unsigned long long disable_end =
        ((addr + system_info.dwAllocationGranularity - 1) /
          system_info.dwAllocationGranularity) * system_info.dwAllocationGranularity;

      if (disable_end > addr)
      {
        memory_group group;
        group.process_disable_group(addr, disable_end - addr);
        vm_info.put_group(addr, group);

        addr = disable_end - mem_info.RegionSize;
        continue;
      }
    }

    if ((unsigned long long)mem_info.AllocationBase + mem_info.RegionSize > max_address) break;


    unsigned long long alloc_base = (unsigned long long)mem_info.AllocationBase;
    if (mem_info.State == MEM_FREE)
    {
      alloc_base = (unsigned long long)mem_info.BaseAddress;
    }

    // 将不同指标的内存分组存放
    memory_group& group = vm_info.get_group(alloc_base);

    if (group.type() == VMGPT__LAST)
    {
      group = vm_info.get_group(alloc_base) = memory_group(&mem_info);
    }

    group.add_block(memory_block(&mem_info));
  }
}
enum memory_group_type
{
	VMGPT__FIRST = 0,
	VMGPT_TOTAL = VMGPT__FIRST, //已使用的
	VMGPT_FREE, //可用的
	VMGPT_UNUSABLE, //不可用的
	VMGPT__LAST
};
memory_block::memory_block(const PMEMORY_BASIC_INFORMATION p_info)
{
  //MEM_COMMIT 表示【已提交的】
  //MEM_RESERVED 表示【预留分配的】
  //如果对上述内容有疑问,可以参考之前写过的【Windows 内存管理知识总结】
	memory_block_type t = p_info->State == MEM_COMMIT ? VMBLT_COMMITTED : p_info->State == MEM_RESERVE ? VMBLT_RESERVED : VMBLT_FREE;
	for (int i = VMBLDT__FIRST; i < VMBLDT__LAST; i++)
	{
		_data[i] = 0;
	}
	_data[VMBLDT_BASE] = (unsigned long long) p_info->BaseAddress;
	_data[VMBLDT_SIZE] = (unsigned long long) p_info->RegionSize;
	_type = t;
}

memory_block::memory_block(memory_block_type type, unsigned long long base, unsigned long long size)
{
	for (int i = VMBLDT__FIRST; i < VMBLDT__LAST; i++)
	{
		_data[i] = 0;
	}
	_data[VMBLDT_BASE] = base;
	_data[VMBLDT_SIZE] = size;
	_type = type;
}

Windows 内存相关数据结构和 API

下面记录了 Windows 提供的 API,并做了简要解释,结合上面的代码应该能比较容易的理解

VirtualAlloc

VirtualAlloc function (memoryapi.h)

LPVOID VirtualAlloc(
  [in, optional] LPVOID lpAddress, //分配的初始地址,如果要分配的地址已经被预留,则向下取最近的一个页面边界
  [in]           SIZE_T dwSize, // region size
  [in]           DWORD  flAllocationType, // 内存权限类型
  [in]           DWORD  flProtect
);

VirtualQuery

virtual_query.png

数据结构

MEMORY_BASIC_INFORMATION structure (winnt.h)

vitypedef struct _MEMORY_BASIC_INFORMATION {
  PVOID  BaseAddress; // base address of the region of pages
  PVOID  AllocationBase; 
  DWORD  AllocationProtect; // 内存保护权限(可执行、可读、可写)
  WORD   PartitionId;
  SIZE_T RegionSize; // pages size in Bytes, from BaseAdress
  DWORD  State;
  DWORD  Protect;
  DWORD  Type;
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;

State,描述当前虚拟内存段的状态

StateMeaning
MEM_COMMIT 0x1000Indicates committed pages for which physical storage has been allocated, either in memory or in the paging file on disk.
MEM_FREE 0x10000Indicates free pages not accessible to the calling process and available to be allocated. For free pages, the information in the AllocationBase, AllocationProtect, Protect, and Type members is undefined.
MEM_RESERVE 0x2000Indicates reserved pages where a range of the process's virtual address space is reserved without any physical storage being allocated. For reserved pages, the information in the Protect member is undefined.
  • MEM_COMMIT,物理内存地址已经分配,无论数据是在内存中或是缓存在磁盘中
  • MEM_FREE,还未分配的虚拟地址空间
  • MEM_RESERVE,已经预留的虚拟内存地址空间,但还没有映射到物理内存中

Type,描述 region 中 pages 的类型

TypeMeaning
MEM_IMAGE 0x1000000Indicates that the memory pages within the region are mapped into the view of an image section.
MEM_MAPPED 0x40000Indicates that the memory pages within the region are mapped into the view of a section.
MEM_PRIVATE 0x20000Indicates that the memory pages within the region are private (that is, not shared by other processes).
  • MEM_IMAGE,镜像文件,一般指 DLL
  • MEM_MAPPED,映射某个段,比如代码段、数据段等
  • MEM_PRIVATE,进程私有的内存,比如堆和栈

SYSTEM_INFO

SYSTEM_INFO structure (sysinfoapi.h)

Contains information about the current computer system. This includes the architecture and type of the processor, the number of processors in the system, the page size, and other such information.

typedef struct _SYSTEM_INFO {
  union {
    DWORD dwOemId;
    struct {
      WORD wProcessorArchitecture;
      WORD wReserved;
    } DUMMYSTRUCTNAME;
  } DUMMYUNIONNAME;
  DWORD     dwPageSize;
  LPVOID    lpMinimumApplicationAddress;
  LPVOID    lpMaximumApplicationAddress;
  DWORD_PTR dwActiveProcessorMask;
  DWORD     dwNumberOfProcessors;
  DWORD     dwProcessorType;
  DWORD     dwAllocationGranularity;
  WORD      wProcessorLevel;
  WORD      wProcessorRevision;
} SYSTEM_INFO, *LPSYSTEM_INFO;
  • dwPageSize The page size and the granularity of page protection and commitment. This is the page size used by the VirtualAlloc function.

  • dwAllocationGranularity The granularity for the starting address at which virtual memory can be allocated. For more information, see VirtualAlloc.

ChatGPT 关于【Granularity】的解答: Granularity(粒度)是虚拟内存分配中重要的概念之一,它是指内存页面大小的单位。在 Windows 中,虚拟内存分配时,系统会把实际的物理内存空间分成大小相等的块,称为页面。这些页面的大小通常是2的幂次方,如4KB、8KB、16KB等。粒度越小,可以更精细地管理内存分配,但也会增加内存分配的开销;而粒度越大,则可以节省内存分配的开销,但可能会浪费部分内存空间。

在 Windows 中,虚拟内存的基本单位是虚拟页(Virtual Page),虚拟页与物理页(Physical Page)是一一对应的。当进程需要访问某个虚拟地址时,系统会根据该地址对应的虚拟页号,找到对应的物理页号,从而访问相应的物理内存。

在虚拟内存的分配和管理中,粒度的大小对系统性能和资源利用效率都有一定的影响。通常情况下,Windows 中的默认页面大小为4KB,这是一种比较折中的选择,能够平衡内存管理的效率和开销,同时也便于与其他操作系统进行交互。但在特定场景下,可能需要调整页面大小,以更好地适应应用程序的需求。

参考