检查进程页表 (翻译 by chatgpt)

发布时间 2023-12-12 21:05:57作者: 摩斯电码

原文:https://www.kernel.org/doc/html/v6.6/admin-guide/mm/pagemap.html

检查进程页表

pagemap是内核中的一组接口,允许用户空间程序通过读取/proc中的文件来检查页面表和相关信息。

pagemap包括以下四个组件:

  • /proc/pid/pagemap:该文件允许用户空间进程查找每个虚拟页面映射到的物理帧。它包含每个虚拟页面的一个64位值,其中包含以下数据:

    • 位0-54:页面帧号(PFN)(如果存在)

    • 位0-4:交换类型(如果已交换)

    • 位5-54:交换偏移(如果已交换)

    • 位55:pte为软脏(参见软脏PTEs)

    • 位56:页面独占映射(自4.2版起)

    • 位57:pte为uffd-wp写保护(自5.13版起)(参见Userfaultfd)

    • 位58-60:零

    • 位61:页面是文件页或共享匿名页(自3.5版起)

    • 位62:页面已交换

    • 位63:页面存在

      自Linux 4.0起,只有具有CAP_SYS_ADMIN权限的用户才能获取PFN。在4.0和4.1中,非特权用户的打开操作会失败并返回-EPERM。从4.2开始,如果用户没有CAP_SYS_ADMIN权限,则PFN字段将被清零。原因是:关于PFN的信息有助于利用Rowhammer漏洞。

      如果页面不在内存中但在交换空间中,则PFN包含交换文件号和页面在交换空间中的偏移的编码。未映射的页面返回空的PFN。这允许精确确定哪些页面被映射(或在交换中),并比较不同进程之间的映射页面。

      使用这个接口的高效用户将使用/proc/pid/maps来确定哪些内存区域实际上被映射,并使用llseek来跳过未映射的区域。

  • /proc/kpagecount:该文件包含每个页面被映射的次数的64位计数,以PFN为索引。

在tools/mm目录中的page-types工具可以用于查询页面被映射的次数。

  • /proc/kpageflags:该文件包含每个页面的64位标志集,以PFN为索引。

    这些标志包括(来自fs/proc/page.c,上面的kpageflags_read):

    • LOCKED
    • ERROR
    • REFERENCED
    • UPTODATE
    • DIRTY
    • LRU
    • ACTIVE
    • SLAB
    • WRITEBACK
    • RECLAIM
    • BUDDY
    • MMAP
    • ANON
    • SWAPCACHE
    • SWAPBACKED
    • COMPOUND_HEAD
    • COMPOUND_TAIL
    • HUGE
    • UNEVICTABLE
    • HWPOISON
    • NOPAGE
    • KSM
    • THP
    • OFFLINE
    • ZERO_PAGE
    • IDLE
    • PGTABLE
  • /proc/kpagecgroup:该文件包含每个页面所属的内存cgroup的64位索引节点号,以PFN为索引。仅在设置了CONFIG_MEMCG时可用。

页面标志的简要描述

  • 0 - LOCKED
    该页面被锁定,用于独占访问,例如正在进行读/写IO操作。

  • 7 - SLAB
    该页面由SLAB/SLUB内核内存分配器管理。当使用复合页面时,只有头页面会设置此标志。

  • 10 - BUDDY
    由伙伴系统分配器管理的空闲内存块。伙伴系统以各种顺序组织空闲内存块。一个N阶块具有2^N个物理连续页面,对于第一个页面,只有设置了BUDDY标志。

  • 15 - COMPOUND_HEAD
    由N阶复合页面由2^N个物理连续页面组成。具有2阶的复合页面采用“HTTT”形式,其中H代表头页面,T代表尾页面。复合页面的主要使用者包括大页(HugeTLB Pages)、SLUB等内存分配器和各种设备驱动程序。然而,在这个接口中,只有大/巨大页面对最终用户可见。

  • 16 - COMPOUND_TAIL
    复合页面的尾部(见上述描述)。

  • 17 - HUGE
    这是大页的一个组成部分。

  • 19 - HWPOISON
    硬件检测到该页面上的内存损坏:不要触摸数据!

  • 20 - NOPAGE
    请求的地址上不存在页面帧。

  • 21 - KSM
    动态共享的相同内存页面,由一个或多个进程共享。

  • 22 - THP
    构成透明大页面的连续页面。

  • 23 - OFFLINE
    该页面在逻辑上处于离线状态。

  • 24 - ZERO_PAGE
    用于pfn_zero或huge_zero页面的零页面。

  • 25 - IDLE
    该页面自从被标记为空闲以来尚未被访问(请参阅空闲页面跟踪)。请注意,如果页面通过PTE被访问,此标志可能已过时。要确保该标志是最新的,必须首先读取/sys/kernel/mm/page_idle/bitmap。

  • 26 - PGTABLE
    该页面正在用作页表。

与IO相关的页面标志

  • 1 - ERROR
    发生了IO错误。

  • 3 - UPTODATE
    该页面具有最新的数据。即对于文件支持的页面:(内存中的数据修订版 >= 磁盘上的数据修订版)。

  • 4 - DIRTY
    该页面已被写入,因此包含新数据。即对于文件支持的页面:(内存中的数据修订版 > 磁盘上的数据修订版)。

  • 8 - WRITEBACK
    该页面正在同步到磁盘。

LRU相关的页面标志

  • 5 - LRU
    该页面位于LRU列表之一中。

  • 6 - ACTIVE
    该页面位于活动的LRU列表中。

  • 18 - UNEVICTABLE
    该页面位于不可驱逐(非)LRU列表中。它被某种方式固定,并且不是LRU页面回收的候选项,例如ramfs页面、shmctl(SHM_LOCK)和mlock()内存段。

  • 2 - REFERENCED
    该页面自上次LRU列表入队/重新入队以来已被引用。

  • 9 - RECLAIM
    该页面在其页面输出IO完成后将很快被回收。

  • 11 - MMAP
    内存映射页面。

  • 12 - ANON
    不属于文件的内存映射页面。

  • 13 - SWAPCACHE
    该页面映射到交换空间,即具有关联的交换条目。

  • 14 - SWAPBACKED
    该页面由交换/内存支持。

可以使用tools/mm目录中的page-types工具来查询上述标志。

利用pagemap做一些有用的事情

使用pagemap进行有用操作的一般步骤如下:

  1. 阅读/proc/pid/maps以确定内存空间的哪些部分映射到了什么。

  2. 选择您感兴趣的映射 - 可以是所有映射,也可以是特定的库,或者堆栈或堆等。

  3. 打开/proc/pid/pagemap并定位到您想要检查的页面。

  4. 从pagemap中读取每个页面的u64。

  5. 打开/proc/kpagecount和/或/proc/kpageflags。对于刚刚读取的每个PFN,定位到文件中的相应条目,并读取您想要的数据。

例如,要找到"唯一集大小"(USS),即进程使用的与任何其他进程不共享的内存量,您可以遍历进程中的每个映射,找到PFN,在kpagecount中查找它们,并统计仅被引用一次的页面数量。

共享内存的例外情况

共享页面的页表条目在页面被清除或交换出时会被清除。这使得交换出的页面与从未分配的页面无法区分。

在内核空间,交换位置仍然可以从页面缓存中检索到。然而,仅存储在普通PTE上的值在页面被交换出时会被永久丢失(即SOFT_DIRTY)。

在用户空间,可以通过lseek()和/或mincore()系统调用来推断页面是否存在、已交换或不存在。

lseek()可以通过在支持页面的文件上指定SEEK_DATA标志来区分已访问的页面(存在或已交换出)和空洞(不存在/未分配)。对于匿名共享页面,文件可以在/proc/pid/map_files/中找到。

mincore()可以区分内存中的页面(存在,包括交换缓存)和内存外的页面(已交换出或不存在/未分配)。

其他注意事项

如果您在文件中的任何位置开始读取(例如,如果您在文件中寻找了奇数字节),或者读取的大小不是8字节的倍数,那么从任何文件中读取都会返回-EINVAL。

在Linux 3.11之前,pagemap的55-60位用于“页面位移”(在大多数架构上始终为12)。自Linux 3.11以来,它们的含义在第一次清除软脏位之后发生了变化。自Linux 4.2以来,它们无条件地用于标志。

Pagemap扫描IOCTL

在pagemap文件上的PAGEMAP_SCAN IOCTL可用于获取或可选地清除有关页表条目的信息。此IOCTL支持以下操作:

  • 扫描地址范围并获取与提供的条件匹配的内存范围。当指定输出缓冲区时执行此操作。

  • 写保护页面。PM_SCAN_WP_MATCHING用于写保护感兴趣的页面。PM_SCAN_CHECK_WPASYNC如果发现非异步写保护页面,则中止操作。PM_SCAN_WP_MATCHING可以与或不与PM_SCAN_CHECK_WPASYNC一起使用。

  • 这两种操作可以合并为一个原子操作,其中我们可以获取并写保护页面。

当前支持有关页面的以下标志:

  • PAGE_IS_WPALLOWED - 页面启用了异步写保护

  • PAGE_IS_WRITTEN - 从写保护以来已对页面进行了写入

  • PAGE_IS_FILE - 页面由文件支持

  • PAGE_IS_PRESENT - 页面存在于内存中

  • PAGE_IS_SWAPPED - 页面已被交换

  • PAGE_IS_PFNZERO - 页面具有零PFN

  • PAGE_IS_HUGE - 页面是THP或HugeTLB支持的

struct pm_scan_arg用作IOCTL的参数。

  1. 必须在size字段中指定struct pm_scan_arg的大小。如果以后进行扩展,此字段将有助于识别结构。

  2. 可以在flags字段中指定标志。目前,PM_SCAN_WP_MATCHING和PM_SCAN_CHECK_WPASYNC是唯一添加的标志。根据是否提供了输出缓冲区,执行获取操作是可选的。

  3. 通过start和end指定范围。

  4. 遍历可能在访问完整范围之前中止,例如用户缓冲区可能已满等。遍历结束地址在end_walk中指定。

  5. struct page_region数组的输出缓冲区和大小在vec和vec_len中指定。

  6. 可选的最大请求页面在max_pages中指定。

  7. 掩码在category_mask、category_anyof_mask、category_inverted和return_mask中指定。

查找已写入的页面并对其进行写保护:

struct pm_scan_arg arg = {
.size = sizeof(arg),
.flags = PM_SCAN_CHECK_WPASYNC | PM_SCAN_CHECK_WPASYNC,
..
.category_mask = PAGE_IS_WRITTEN,
.return_mask = PAGE_IS_WRITTEN,
};

查找已写入、由文件支持、未交换且存在或巨大的页面:

struct pm_scan_arg arg = {
.size = sizeof(arg),
.flags = 0,
..
.category_mask = PAGE_IS_WRITTEN | PAGE_IS_SWAPPED,
.category_inverted = PAGE_IS_SWAPPED,
.category_anyof_mask = PAGE_IS_PRESENT | PAGE_IS_HUGE,
.return_mask = PAGE_IS_WRITTEN | PAGE_IS_SWAPPED |
               PAGE_IS_PRESENT | PAGE_IS_HUGE,
};

PAGE_IS_WRITTEN标志可以被视为软脏标志的性能更佳的替代品。它不受内核VMA合并的影响,因此用户可以在普通页面的情况下找到真正的软脏页面。(对于THP或HugeTLB页面,仍可能报告额外的脏页面。)

"PAGE_IS_WRITTEN"类别与启用了uffd写保护的范围一起使用,以在用户空间实现内存脏页跟踪:

  1. 使用userfaultfd系统调用创建userfaultfd文件描述符。

  2. 通过UFFDIO_API IOCTL设置UFFD_FEATURE_WP_UNPOPULATED和UFFD_FEATURE_WP_ASYNC功能。

  3. 使用UFFDIO_REGISTER IOCTL通过UFFDIO_REGISTER_MODE_WP模式注册内存范围。

  4. 然后可以使用PAGEMAP_SCAN IOCTL并使用标志PM_SCAN_WP_MATCHING或UFFDIO_WRITEPROTECT IOCTL来写保护注册的内存的任何部分或整个内存区域。这两者执行相同的操作。就性能而言,前者更好。

  5. 现在可以使用PAGEMAP_SCAN IOCTL仅查找自上次标记以来已写入的页面和/或可选地对页面进行写保护。