summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZhen Ni <zhen.ni@easystack.cn>2026-07-07 19:54:08 +0800
committerAndrew Morton <akpm@linux-foundation.org>2026-07-28 21:11:54 -0700
commit8570b65220e80f0dafa517edd07fb5e63eff5344 (patch)
tree7658919bb6f4152dbe4d28da438983ed0c556daa
parent3dd5a633a0941eb818d4647e1f904252ca028dc6 (diff)
downloadlinux-next-8570b65220e80f0dafa517edd07fb5e63eff5344.tar.gz
linux-next-8570b65220e80f0dafa517edd07fb5e63eff5344.zip
mm/page_owner: add print_mode filter
Patch series "mm/page_owner: add per-fd filter infrastructure for print_mode and NUMA filtering", v12. This patch series introduces per-file-descriptor filtering capabilities to the page_owner feature. Problem Statement ================= In production environments with large memory configurations (e.g., 250GB+), collecting page_owner information often results in files ranging from several gigabytes to over 10GB. This creates significant challenges: 1. Storage pressure on production systems 2. Difficulty transferring large files from production environments 3. Post-processing overhead with tools/mm/page_owner_sort.c The primary contributor to file size is redundant stack trace information. While the kernel already deduplicates stacks via stackdepot, page_owner retrieves and stores full stack traces for each page, only to deduplicate them again during post-processing. Additionally, in NUMA-aware environments (e.g., DPDK-based cloud deployments where QEMU processes are bound to specific NUMA nodes), OOM events are often node-specific rather than system-wide. Previously, page_owner could not filter by NUMA node, forcing users to collect and analyze data for all nodes. Solution ======== This patch series introduces a per-file-descriptor filter infrastructure with two initial filters: 1. **Print Mode Filter**: Outputs only stack handles instead of full stack traces. The handle-to-stack mapping can be retrieved from the existing show_stacks_handles interface. This dramatically reduces output size while preserving all allocation metadata. 2. **NUMA Node Filter**: Allows filtering pages by specific NUMA node(s) using flexible nodelist format, enabling targeted analysis of memory issues in NUMA-aware deployments. The per-fd design allows multiple concurrent page_owner reads with different filters, solving coordination issues in multi-user production environments. Implementation ============== The series is structured as follows: - Patch 1: Implement print_mode filter infrastructure * Add file->private_data to store per-fd filter state * Add .open, .release, and .write file operations * Support "stack", "handle", and "stack_handle" modes via "mode=" write commands - Patch 2: Implement NUMA node filter infrastructure * Add nid_filter field to per-fd state * Support flexible nodelist format via "nid=" write commands (single, multiple, ranges) * Validate nodes and reject non-existent nodes using nodes_subset() - Patch 3: Add page_owner_filter userspace tool * Manages per-fd filters via write() interface * Provides user-friendly command-line interface * Includes comprehensive input validation - Patch 4: Document filter features and usage Usage Example ============= Using the page_owner_filter tool with per-fd filters: # ./page_owner_filter -m stack_handle -n "0,2-3" -o page_owner.txt The tool opens /sys/kernel/debug/page_owner, sets filters via write(), then reads the filtered output to the specified file (or stdout). Sample print_mode output (showing handles only): Page allocated via order 0, mask 0x0(), pid 0, tgid 0 (swapper), ts 0 ns PFN 0x40000 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000000(node=0|zone=0|lastcpupid=0x1ffff) handle: 1048577 Page allocated via order 0, mask 0x252000(__GFP_NOWARN| __GFP_NORETRY|__GFP_COMP|__GFP_THISNODE), pid 0, tgid 0 (swapper), ts 0 ns PFN 0x40002 type Unmovable Block 512 type Unmovable Flags 0x23fffe0000000200(workingset|node=0|zone=0|lastcpupid=0x1ffff) handle: 1048577 This patch (of 4): Add a print_mode filter to page_owner that allows users to choose between printing stack traces, stack handles, or both, providing flexibility for different debugging and analysis scenarios. The filter provides three modes via page_owner: - Writing "mode=stack" prints stack traces for each page (default) - Writing "mode=handle" prints only the handle number - Writing "mode=stack_handle" prints both stack traces and handles The default stack mode maintains backward compatibility with existing usage, displaying complete stack traces for each page allocation. The handle mode dramatically reduces log size and improves performance by showing only the handle number instead of the full stack trace. Testing shows handle mode reduces output size by ~66% (84MB vs 244MB) and improves read performance by ~4.4x compared to full stack output. The mapping from handles to actual stack traces can be obtained via the show_stacks_handles interface. The stack_handle mode prints both stack traces and handles, making it easier to identify pages with the same allocation pattern by comparing handle numbers instead of comparing large stack traces. Example usage: # Using the page_owner_filter tool (recommended) ./page_owner_filter -m stack # Print only stack traces (default) ./page_owner_filter -m handle # Print only handles ./page_owner_filter -m stack_handle # Print both stack and handles Sample output (handle mode): Page allocated via order 0, migratetype Unmovable, gfp_mask 0x1100ca, pid 1, tgid 1 (systemd), ts 123456789 ns PFN 0x1000 type Unmovable Block 1 type Unmovable Flags 0x3fffe800000084(referenced|lru|active|private|node=0|zone=1) handle: 17432583 ... This implementation uses per-file-descriptor filter state stored in file->private_data, allowing each opener to have independent filter configuration. Link: https://lore.kernel.org/20260707115411.1714314-1-zhen.ni@easystack.cn Link: https://lore.kernel.org/20260707115411.1714314-2-zhen.ni@easystack.cn Signed-off-by: Zhen Ni <zhen.ni@easystack.cn> Tested-by: Zi Yan <ziy@nvidia.com> Acked-by: Zi Yan <ziy@nvidia.com> Reviewed-by: Vlastimil Babka (SUSE) <vbabka@kernel.org> Cc: Brendan Jackman <jackmanb@google.com> Cc: Johannes Weiner <hannes@cmpxchg.org> Cc: Michal Hocko <mhocko@suse.com> Cc: Suren Baghdasaryan <surenb@google.com> Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
-rw-r--r--mm/page_owner.c116
1 files changed, 110 insertions, 6 deletions
diff --git a/mm/page_owner.c b/mm/page_owner.c
index e18512a49e38..5c475e655818 100644
--- a/mm/page_owner.c
+++ b/mm/page_owner.c
@@ -54,6 +54,22 @@ struct stack_print_ctx {
u8 flags;
};
+enum page_owner_print_mode {
+ PAGE_OWNER_PRINT_STACK,
+ PAGE_OWNER_PRINT_HANDLE,
+ PAGE_OWNER_PRINT_STACK_HANDLE,
+};
+
+static const char * const page_owner_print_mode_strings[] = {
+ [PAGE_OWNER_PRINT_STACK] = "stack",
+ [PAGE_OWNER_PRINT_HANDLE] = "handle",
+ [PAGE_OWNER_PRINT_STACK_HANDLE] = "stack_handle",
+};
+
+struct page_owner_filter_state {
+ enum page_owner_print_mode print_mode;
+};
+
static bool page_owner_enabled __initdata;
DEFINE_STATIC_KEY_FALSE(page_owner_inited);
@@ -584,16 +600,20 @@ static inline int print_page_owner_memcg(char *kbuf, size_t count, int ret,
static ssize_t
print_page_owner(char __user *buf, size_t count, unsigned long pfn,
struct page *page, struct page_owner *page_owner,
- depot_stack_handle_t handle)
+ depot_stack_handle_t handle,
+ struct page_owner_filter_state *state)
{
int ret, pageblock_mt, page_mt;
char *kbuf;
+ enum page_owner_print_mode print_mode;
count = min_t(size_t, count, PAGE_SIZE);
kbuf = kmalloc(count, GFP_KERNEL);
if (!kbuf)
return -ENOMEM;
+ print_mode = state->print_mode;
+
ret = scnprintf(kbuf, count,
"Page allocated via order %u, mask %#x(%pGg), pid %d, tgid %d (%s), ts %llu ns\n",
page_owner->order, page_owner->gfp_mask,
@@ -612,9 +632,18 @@ print_page_owner(char __user *buf, size_t count, unsigned long pfn,
migratetype_names[pageblock_mt],
&page->flags.f);
- ret += stack_depot_snprint(handle, kbuf + ret, count - ret, 0);
- if (ret >= count)
- goto err;
+ if (print_mode != PAGE_OWNER_PRINT_HANDLE) {
+ ret += stack_depot_snprint(handle, kbuf + ret, count - ret, 0);
+ if (ret >= count)
+ goto err;
+ }
+
+ if (print_mode != PAGE_OWNER_PRINT_STACK) {
+ ret += scnprintf(kbuf + ret, count - ret, "handle: %u\n",
+ handle);
+ if (ret >= count)
+ goto err;
+ }
if (page_owner->last_migrate_reason != MR_NEVER) {
ret += scnprintf(kbuf + ret, count - ret,
@@ -702,6 +731,7 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
struct page_ext *page_ext;
struct page_owner *page_owner;
depot_stack_handle_t handle;
+ struct page_owner_filter_state *state = file->private_data;
if (!static_branch_unlikely(&page_owner_inited))
return -EINVAL;
@@ -779,7 +809,7 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
page_owner_tmp = *page_owner;
page_ext_put(page_ext);
return print_page_owner(buf, count, pfn, page,
- &page_owner_tmp, handle);
+ &page_owner_tmp, handle, state);
ext_put_continue:
page_ext_put(page_ext);
}
@@ -868,7 +898,81 @@ static void init_early_allocated_pages(void)
init_pages_in_zone(zone);
}
+static int page_owner_open(struct inode *inode, struct file *file)
+{
+ struct page_owner_filter_state *state;
+
+ state = kzalloc_obj(*state);
+ if (!state)
+ return -ENOMEM;
+
+ state->print_mode = PAGE_OWNER_PRINT_STACK;
+ file->private_data = state;
+ return 0;
+}
+
+static int page_owner_release(struct inode *inode, struct file *file)
+{
+ kfree(file->private_data);
+ return 0;
+}
+
+static ssize_t page_owner_write(struct file *file,
+ const char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ char *kbuf;
+ char *orig;
+ char *token;
+ int ret;
+ struct page_owner_filter_state *state = file->private_data;
+ enum page_owner_print_mode new_print_mode;
+
+ /*
+ * Maximum input length for filter commands:
+ * 32: print_mode command max length is 17 ("mode=stack_handle").
+ */
+ if (count > 32)
+ return -EINVAL;
+
+ kbuf = memdup_user_nul(buf, count);
+ if (IS_ERR(kbuf))
+ return PTR_ERR(kbuf);
+
+ orig = kbuf;
+
+ new_print_mode = state->print_mode;
+
+ while ((token = strsep(&kbuf, " \t\n")) != NULL) {
+ if (*token == '\0')
+ continue;
+
+ if (!strncmp(token, "mode=", 5)) {
+ ret = sysfs_match_string(page_owner_print_mode_strings,
+ token + 5);
+ if (ret < 0)
+ goto out_free;
+ new_print_mode = ret;
+ } else {
+ ret = -EINVAL;
+ goto out_free;
+ }
+ }
+
+ state->print_mode = new_print_mode;
+
+ ret = count;
+
+out_free:
+ kfree(orig);
+ return ret;
+}
+
static const struct file_operations page_owner_fops = {
+ .owner = THIS_MODULE,
+ .open = page_owner_open,
+ .release = page_owner_release,
+ .write = page_owner_write,
.read = read_page_owner,
.llseek = lseek_page_owner,
};
@@ -999,7 +1103,7 @@ static int __init pageowner_init(void)
return 0;
}
- debugfs_create_file("page_owner", 0400, NULL, NULL, &page_owner_fops);
+ debugfs_create_file("page_owner", 0600, NULL, NULL, &page_owner_fops);
dir = debugfs_create_dir("page_owner_stacks", NULL);
debugfs_create_file("show_stacks", 0400, dir,
(void *)(STACK_PRINT_FLAG_STACK |