检查进程页表

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-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 之上)

    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。此文件包含每个页面所属内存 cgroup 的 64 位 inode 号,按 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

该页面用作页表。

共享内存的例外情况

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

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

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

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

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

其他注意事项

如果读取不是从 8 字节边界开始(例如,如果你在文件中搜索了奇数个字节),或者读取的大小不是 8 字节的倍数,则从任何文件读取都会返回 -EINVAL。

在 Linux 3.11 之前,pagemap 位 55-60 用于 “page-shift”(在大多数架构上始终为 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 - 页面是软脏的

struct pm_scan_arg 用作 IOCTL 的参数。

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

  2. 可以在 flags 字段中指定标志。此时,PM_SCAN_WP_MATCHINGPM_SCAN_CHECK_WPASYNC 是唯一添加的标志。根据是否提供输出缓冲区,可以选择性地执行 get 操作。

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

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

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

  2. UFFD_FEATURE_WP_UNPOPULATEDUFFD_FEATURE_WP_ASYNC 特性由 UFFDIO_API IOCTL 设置。

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

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

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