检查进程页表

pagemap 是内核中一套新(自 2.6.25 版本起)的接口,它允许用户空间程序通过读取 /proc 中的文件来检查页表及相关信息。

pagemap 有四个组成部分

  • /proc/pid/pagemap。此文件允许用户空间进程找出每个虚拟页映射到的物理帧。它为每个虚拟页包含一个 64 位值,包含以下数据(来自 fs/proc/task_mmu.c,在 pagemap_read 之上)

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

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

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

    • 位 55 pte 是软脏的(请参阅 软脏 PTE

    • 位 56 页被独占映射(自 4.2 版本起)

    • 位 57 pte 受 uffd-wp 写保护(自 5.13 版本起)(请参阅 Userfaultfd

    • 位 58 pte 是一个保护区域(自 6.15 版本起)(请参阅 madvise (2) 手册页)

    • 位 59-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。这允许精确地确定哪些页已映射(或处于交换状态)并比较进程之间映射的页。

    传统上,位 56 表示页只被精确映射一次,当页被多次映射时(即使在同一进程中被多次映射),位 56 会被清除。在某些内核配置中,作为较大分配(例如 THP)一部分的页的语义可能会有所不同:如果相应较大分配的所有页都确定地映射在同一进程中,即使该页在该进程中被多次映射,位 56 也会被设置。当较大分配的任何页可能映射在不同进程中时,位 56 会被清除。在某些情况下,即使不再是这种情况,较大的分配也可能被视为“可能被多个进程映射”。

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

  • /proc/kpagecount。此文件包含一个 64 位计数,表示每个页被映射的次数,并按 PFN 索引。某些内核配置不跟踪作为较大分配(例如 THP)一部分的页被映射的精确次数。在这些配置中,将返回此较大分配中每个页的平均映射次数。但是,如果较大分配的任何页已映射,则返回的值将至少为 1。

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

  • /proc/kpageflags。此文件包含一个 64 位标志集,用于每个页,并按 PFN 索引。

    这些标志是(来自 fs/proc/page.c,在 kpageflags_read 之上)

    1. LOCKED(锁定)

    2. ERROR(错误)

    3. REFERENCED(已引用)

    4. UPTODATE(最新)

    5. DIRTY(脏)

    6. LRU

    7. ACTIVE(活跃)

    8. SLAB

    9. WRITEBACK(回写)

    10. RECLAIM(回收)

    11. BUDDY

    12. MMAP(内存映射)

    13. ANON(匿名)

    14. SWAPCACHE(交换缓存)

    15. SWAPBACKED(交换支持)

    16. COMPOUND_HEAD(复合页头)

    17. COMPOUND_TAIL(复合页尾)

    18. HUGE(巨页)

    19. UNEVICTABLE(不可回收)

    20. HWPOISON(硬件中毒)

    21. NOPAGE(无页)

    22. KSM

    23. THP

    24. OFFLINE(离线)

    25. ZERO_PAGE(零页)

    26. IDLE(空闲)

    27. PGTABLE(页表)

  • /proc/kpagecgroup。此文件包含一个 64 位 inode 号,表示每个页所属的内存 cgroup,并按 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 页(HugeTLB 页)、SLUB 等内存分配器和各种设备驱动程序。然而,在此接口中,只有巨页/千兆页对最终用户可见。

16 - COMPOUND_TAIL(复合页尾)

复合页尾(见上述描述)。

17 - HUGE(巨页)

这是 HugeTLB 页的组成部分。

19 - HWPOISON(硬件中毒)

硬件检测到此页上的内存损坏:请勿触碰数据!

20 - NOPAGE(无页)

请求的地址处不存在页帧。

21 - KSM

一个或多个进程之间动态共享的相同内存页。

22 - THP

构造任意大小 THP 并以任意粒度映射的连续页。

23 - OFFLINE(离线)

该页逻辑上处于离线状态。

24 - ZERO_PAGE(零页)

pfn_zero 或 huge_zero 页的零页。

25 - IDLE(空闲)

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

26 - PGTABLE(页表)

该页正在用作页表。

共享内存的例外情况

当共享页被清理或换出时,其页表条目会被清除。这使得换出页与从未分配的页无法区分。

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

在用户空间中,可以借助 lseek 和/或 mincore 系统调用来推断页是存在、已交换还是不存在。

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

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

其他注意事项

如果您没有在 8 字节边界上开始读取(例如,如果您在文件中查找了奇数个字节),或者如果读取的大小不是 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 - 页是 PMD 映射的 THP 或 HugeTLB 支持的

  • PAGE_IS_SOFT_DIRTY - 页是软脏的

  • PAGE_IS_GUARD - 页是保护区域的一部分

struct pm_scan_arg 用作 IOCTL 的参数。

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

  2. 标志可以在 flags 字段中指定。目前只有 PM_SCAN_WP_MATCHINGPM_SCAN_CHECK_WPASYNC 是新增的标志。获取操作是可选执行的,取决于是否提供了输出缓冲区。

  3. 范围通过 startend 指定。

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

  5. struct page_region 数组的输出缓冲区及其大小在 vecvec_len 中指定。

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

  7. 掩码在 category_maskcategory_anyof_maskcategory_invertedreturn_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 标志可以被认为是 soft-dirty 标志的一个性能更好的替代方案。它不受内核 VMA 合并的影响,因此用户可以在普通页的情况下找到真正的软脏页。(对于 THP 或 Hugetlb 页,仍可能报告额外的脏页。)

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

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

  2. UFFD_FEATURE_WP_UNPOPULATEDUFFD_FEATURE_WP_ASYNC 功能通过 UFFDIO_API IOCTL 设置。

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

  4. 然后,注册内存的任何部分或整个内存区域必须使用带有 PM_SCAN_WP_MATCHING 标志的 PAGEMAP_SCAN IOCTL 或 UFFDIO_WRITEPROTECT IOCTL 进行写保护。两者执行相同的操作。前者在性能方面更好。

  5. 现在,PAGEMAP_SCAN IOCTL 可以用于查找自上次标记以来已被写入的页,和/或可选地写保护这些页。