内存管理-69-page_owner-1-理论

相关BK:
内存管理-29-page_exit功能 


一、理论介绍

1. page_owner 的作用

基于kernel-6.1

CONFIG_PAGE_OWNER:回答“这页是谁分配出来的”。定义在 Kconfig.debug 中,实现文件 page_owner.c。

config PAGE_OWNER
    bool "Track page owner"
    depends on DEBUG_KERNEL && STACKTRACE_SUPPORT
    select DEBUG_FS
    select STACKTRACE
    select STACKDEPOT
    select PAGE_EXTENSION
    help
        此功能用于跟踪页面所属的调用链,有助于查找裸页面分配(alloc_page)泄漏。即使您在构建中包含此功能,
        默认情况下它也是禁用的####。您需要将"page_owner=on"传递给启动参数才能启用它。启用后会占用相当多
        的内存。有关用户空间辅助实现,请参阅 tools/vm/page_owner_sort.c。

(1) 作用

为每个 page 记录分配者信息(调用栈 + 元数据),用于排查内存泄漏、内存大户、碎片化来源。实现入口在 Kconfig.debug 和 page_owner.c。

(2) 记录的内容有:

a. 分配栈句柄 handle、释放栈句柄 free_handle;
b. 分配阶数 order、gfp_mask;
c. 分配/释放时间戳;
d. 进程名 comm、pid、tgid;
e. 迁移原因 last_migrate_reason;
可见结构体定义在 struct page_owner 中。

(3) 启用方式

编译时打开 CONFIG_PAGE_OWNER, 启动参数加 page_owner=on(默认关闭), 开关对应 early_page_owner_param()。

(4) 查看方式

debugfs 节点 page_owner 文件节点在 pageowner_init() 中创建。官方文档和排序工具见 page_owner.rst

(5) 开销特征

依赖 PAGE_EXTENSION + STACKDEPOT + STACKTRACE,见 Kconfig.debug。开启后会有明显内存开销(每页扩展元数据 + 栈仓库) 和一定运行时开销(采栈)。关闭运行时时,热路径影响较小(static key)。


二、page_owner.rst 翻译

基于 kernel-6.1

==================================================
page owner:追踪每个页面的分配来源
==================================================

1. 简介

page owner 用于追踪每个页面是由谁分配的。它可以用于调试内存泄漏,或者找出内存占用大户。每当发生内存分配时,分配时的调用栈和页面阶数等信息就会被存储到每个页面对应的存储区中。当我们需要了解所有页面的状态时,就可以获取并分析这些信息。

虽然内核已经提供了用于跟踪页面分配/释放的 tracepoint,但用它来分析每个页面的分配来源相当繁琐:需要在用户态程序启动前持续扩大 trace buffer 以防止数据覆盖;而且一旦启动该程序,它会不断地将 trace buffer 转储以供后续分析,这会改变系统行为,不利于调试。

page owner 还可以用于多种用途。例如,可以通过各页面的 gfp 标志获得精确的碎片化统计信息。该功能已经实现,一旦开启 page owner 即会自动激活。其他用途同样欢迎探索。

page owner 默认是关闭的。如果要使用它,需要在启动参数中加入 "page_owner=on"。如果内核编译时包含了 page owner(使能了 CONFIG_PAGE_OWNER) 但运行时未通过启动参数开启,那么运行时开销可以忽略不计。运行时关闭时,不需要为存储所有者信息分配内存,因此没有运行时内存开销。page owner 仅在页面分配器热路径中插入了两个 unlikely 分支,若未启用,分配行为与不带 page owner 的内核完全一致。只要内核支持 static keys 跳转标签补丁功能,这两个 unlikely 分支对分配性能几乎没有影响。下面是启用此功能后内核代码体积的变化情况。

虽然启用 page owner 会使内核体积增加几千字节,但绝大多数代码位于页面分配器的热路径之外。建议将 page owner 编译进内核,需要时再开启,这是调试内核内存问题的理想方式。

有一点需要注意,这源于实现细节:page owner 将信息存储在 struct page extension (struct page_ext )对应的内存中。在稀疏内存系统中,这块内存的初始化时间晚于页面分配器的启动时间,因此在初始化完成之前,许多页面已经被分配,但没有所有者信息。为了弥补这一点,在初始化阶段会对这些早期分配的页面进行排查并标记为"已分配"。虽然这并不意味着它们拥有正确的所有者信息,但至少可以更准确地判断页面是否已被分配。
在一台 2GB 内存的 x86-64 虚拟机上,共发现并标记了 13343 个早期分配的页面(大多来自 struct page extension 功能本身)。无论如何,在那之后就不会再有页面处于未跟踪状态了。


2. 使用方法

(1) 编译用户态辅助工具::

    cd tools/vm
    make page_owner_sort

(2) 开启 page owner:在启动参数中加入 "page_owner=on"。

(3) 执行需要调试的操作。

(4) 分析 page owner 中的信息::

    cat /sys/kernel/debug/page_owner > page_owner_full.txt
    ./page_owner_sort page_owner_full.txt sorted_page_owner.txt

page_owner_full.txt 的一般输出格式如下::

    Page allocated via order XXX, ...
    PFN XXX ...
    // 详细调用栈

    Page allocated via order XXX, ...
    PFN XXX ...
    // 详细调用栈

默认情况下会转储全部 PFN。如果希望从指定 PFN 开始,page_owner 支持 fseek 操作::

    FILE *fp = fopen("/sys/kernel/debug/page_owner", "r");
    fseek(fp, pfn_start, SEEK_SET);

page_owner_sort 工具会忽略 PFN 行,将其余行存入缓冲区,通过正则表达式提取页面阶数,统计次数和页数,最后按指定参数排序。

在 sorted_page_owner.txt 中可以查看每个页面的分配来源,一般输出格式如下::

    XXX times, XXX pages:
    Page allocated via order XXX, ...
    // 详细调用栈

默认按出现次数排序。若要按页数排序,使用 -m 参数。详细参数说明如下:

(1) 基本功能::

Sort:
    -a        按内存分配时间排序。
    -m        按总内存占用排序。
    -p        按 pid 排序。
    -P        按 tgid 排序。
    -n        按任务命令名排序。
    -r        按内存释放时间排序。
    -s        按调用栈排序。
    -t        按出现次数排序(默认)。
    --sort <order>    指定排序规则。排序语法为 [+|-]key[,[+|-]key[,...]]。
            从 **标准格式说明符** 章节中选择键名。"+" 可以省略,
            默认为数字或字典序升序。允许混用简写和完整形式的键名。

示例:

    ./page_owner_sort <input> <output> --sort=n,+pid,-tgid
    ./page_owner_sort <input> <output> --sort=at


附加功能::

(1) Cull(聚合去重)

--cull <rules>: 指定聚合规则。聚合语法为 key[,key[,...]]。从 **标准格式说明符** 章节中选择多字母键名。

<rules> 是以逗号分隔的键名列表,用于指定各聚合规则。支持的键名在下方 **标准格式说明符** 章节中描述。可以用 k1,k2,... 的形式指定键序列,允许混用简写和完整形式的键名。

示例:

./page_owner_sort <input> <output> --cull=stacktrace
./page_owner_sort <input> <output> --cull=st,pid,name
./page_owner_sort <input> <output> --cull=n,f


(2) Filter(过滤)

-f: 过滤掉内存已被释放的块的信息。


(3) Select(筛选):

--pid <pidlist>        按 pid 筛选。选择 pid 在 <pidlist> 中的块。
--tgid <tgidlist>    按 tgid 筛选。选择线程组 ID 在 <tgidlist> 中的块。
--name <cmdlist>    按任务命令名筛选。选择命令名在 <cmdlist> 中的块。

<pidlist>、<tgidlist>、<cmdlist> 均为以逗号分隔的单个参数,用于指定各筛选规则。

示例:

./page_owner_sort <input> <output> --pid=1
./page_owner_sort <input> <output> --tgid=1,2,3
./page_owner_sort <input> <output> --name name1,name2


3. 标准格式说明符

(1) 用于 --sort 选项:

------------------------------------------------------
键名(key)  完整形式      描述
------------------------------------------------------
p          pid           进程 ID
tg         tgid          线程组 ID
n          name          任务命令名
st         stacktrace    页面分配调用栈
T          txt           块的完整文本
ft         free_ts       页面被释放时的时间戳
at         alloc_ts      页面被分配时的时间戳
ator       allocator     页面使用的内存分配器
------------------------------------------------------


(2) 用于 --cull 选项:

------------------------------------------------------
键名(key)  完整形式     描述
------------------------------------------------------
p          pid          进程 ID
tg         tgid         线程组 ID
n          name         任务命令名
f          free         页面是否已被释放
st         stacktrace   页面分配调用栈
ator       allocator    页面使用的内存分配器
------------------------------------------------------

 

posted on 2026-07-09 16:31  Hello-World3  阅读(7)  评论(0)    收藏  举报

导航