Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/8] mm/page_owner: Add PID/TGID/COMM and cgroup filtering
@ 2026-08-28  3:13 Zhen Ni
  2026-08-28  3:13 ` [PATCH 1/8] mm/page_owner: Add PID filtering support Zhen Ni
                   ` (8 more replies)
  0 siblings, 9 replies; 11+ messages in thread
From: Zhen Ni @ 2026-08-28  3:13 UTC (permalink / raw)
  To: Andrew Morton
  Cc: David Hildenbrand, Lorenzo Stoakes, Liam R . Howlett,
	Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
	Jonathan Corbet, Shuah Khan, Randy Dunlap, Brendan Jackman,
	Johannes Weiner, Zi Yan, linux-mm, linux-doc, linux-kernel,
	Zhen Ni

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #1: Type: text/plain; charset=y, Size: 49857 bytes --]

This patch series adds process and memory cgroup filtering support to
page_owner. Following the previous series that introduced print_mode and
NUMA node filters:
  https://lore.kernel.org/linux-mm/20260707115411.1714314-1-zhen.ni@easystack.cn/

This series adds filtering capabilities to page_owner, allowing users to
filter output by specific processes and memory cgroups. Users can now
filter page_owner output by PID, TGID, COMM (with wildcard support), and
memory cgroup path. This makes page_owner debugging more focused and
efficient for tracking memory allocations in specific contexts.

Targeted filtering provides significant performance benefits on large memory
servers by reducing both execution time and output size. By filtering at the
kernel level before reading, only relevant page allocations are processed,
dramatically reducing the amount of data that needs to be handled in userspace.

This series extends page_owner filtering capabilities with:
- PID filtering
- TGID filtering
- COMM filtering with wildcard support
- Cgroup (memcg) filtering for containerized environments

The series is organized as follows:

Patches 1-3: Add PID, TGID, and COMM filtering support to page_owner
  - Support filtering by process ID
  - Support filtering by thread group ID
  - Support filtering by command name with wildcards

Patch 4: Refactor memcg handling to prepare for cgroup filter support
Patch 5: Add memory cgroup filtering support

Patches 6-7: Update page_owner_filter tool with corresponding features

Patch 8: Update documentation

These filters are particularly useful for:
- Debugging memory leaks in specific processes
- Analyzing memory usage in containerized environments
- Isolating allocations from specific services or applications

Test Setup
===========

The filtering functionality has been tested with the following tools:

1. page_owner_test_alloc.c - Test allocation program

   Build: gcc page_owner_test_alloc.c -o page_owner_test_alloc

   Run: ./page_owner_test_alloc

   Creates 3 threads in separate cgroups (test_a, test_b, test_c), each
   allocating 1MB continuously. Allows verification of PID/TGID/COMM/cgroup
   filtering accuracy by comparing page counts. Automatically cleans up
   cgroups on Ctrl+C (SIGINT) or SIGTERM.

2. test_page_owner_filters.sh - Comprehensive test script

   Run: ./test_page_owner_filters.sh

   Covers invalid inputs, PID 1 filtering, cgroup filtering, combination
   filters, and page count verification using the test program above.

Test Results
============

1. Start test program (keep it running)::

   $ ./page_owner_test_alloc 
   TGID: 792 PID: 793 COMM: po_thread_a CGROUP: /test_a
   TGID: 792 PID: 794 COMM: po_thread_b CGROUP: /test_b
   TGID: 792 PID: 795 COMM: po_thread_c CGROUP: /test_c

   [Keep this terminal running, open a new terminal for the test script]

2. Run test script in another terminal::

   $ ./test_page_owner_filters.sh

   Test environment: 4-node NUMA system.

   The same script was tested on both cgroup v1 and cgroup v2, and the
   results matched expectations on both.

   The cgroup v1 testing requires CONFIG_MEMCG_V1 to be enabled at build
   time, plus the following kernel boot parameters::

     systemd.unified_cgroup_hierarchy=0
     systemd.legacy_systemd_cgroup_controller=1

   The output below is from the cgroup v2 environment.

   root@ubuntu:~/pid# ./test_page_owner_filters.sh
   =========================================
   Page Owner Filter Tests
   =========================================

   === I. INVALID INPUT TESTS ===

   --- 1.1 PID Invalid Inputs ---
   Test: Negative PID
   ./page_owner_filter -p -1
   Error: Invalid character '-' in pid_list (only digits allowed)

   Test: Non-numeric PID
   ./page_owner_filter -p abc
   Error: Invalid character 'a' in pid_list (only digits allowed)

   Test: Empty PID argument
   ./page_owner_filter -p
   ./page_owner_filter: option requires an argument -- 'p'
   Usage: ./page_owner_filter [OPTIONS]

   Options:
   -m, --mode MODE       : print_mode (stack, handle, stack_handle)
   -n, --nid NID_LIST    : NUMA nodes (comma-separated or ranges)
   -p, --pid PID_LIST    : Process IDs (comma-separated, max 16)
   -t, --tgid TGID_LIST  : Thread Group IDs (comma-separated, max 16)
   -c, --comm COMM_LIST  : Process names (comma-separated, max 8)
                           Supports wildcards: * ? [a-z]
   -g, --cgroup PATH     : Memory cgroup path
   -o, --output FILE     : output file (default: stdout)
   -h, --help            : show this help message

   Examples:
   ./page_owner_filter -m handle -o output.txt
   ./page_owner_filter -n 0,1 -c bash
   ./page_owner_filter -c "python*" -g user.slice

   Test: PID with letters (mixed invalid)
   ./page_owner_filter -p 123abc
   Error: Invalid character 'a' in pid_list (only digits allowed)

   Test: Multiple invalid PIDs
   ./page_owner_filter -p -1,-2,-3
   Error: Invalid character '-' in pid_list (only digits allowed)

   Test: Excessive PID value (> max PID)
   ./page_owner_filter -p 4294967296
   write filter command: Invalid argument

   Test: Too many PIDs (17 PIDs, max is 16)
   ./page_owner_filter -p 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17
   Error: Too many PIDs (max 16)


   --- 1.2 TGID Invalid Inputs ---
   Test: Negative TGID
   ./page_owner_filter -t -1
   Error: Invalid character '-' in pid_list (only digits allowed)

   Test: Non-numeric TGID
   ./page_owner_filter -t xyz
   Error: Invalid character 'x' in pid_list (only digits allowed)

   Test: Empty TGID argument
   ./page_owner_filter -t
   ./page_owner_filter: option requires an argument -- 't'
   Usage: ./page_owner_filter [OPTIONS]

   Options:
   -m, --mode MODE       : print_mode (stack, handle, stack_handle)
   -n, --nid NID_LIST    : NUMA nodes (comma-separated or ranges)
   -p, --pid PID_LIST    : Process IDs (comma-separated, max 16)
   -t, --tgid TGID_LIST  : Thread Group IDs (comma-separated, max 16)
   -c, --comm COMM_LIST  : Process names (comma-separated, max 8)
                           Supports wildcards: * ? [a-z]
   -g, --cgroup PATH     : Memory cgroup path
   -o, --output FILE     : output file (default: stdout)
   -h, --help            : show this help message

   Examples:
   ./page_owner_filter -m handle -o output.txt
   ./page_owner_filter -n 0,1 -c bash
   ./page_owner_filter -c "python*" -g user.slice

   Test: TGID with special characters
   ./page_owner_filter -t '$$'
   Error: Invalid character '$' in pid_list (only digits allowed)

   Test: Mixed valid/invalid TGID
   ./page_owner_filter -t 1,abc,2
   Error: Invalid character 'a' in pid_list (only digits allowed)


   --- 1.3 COMM Invalid Inputs ---
   Test: Empty COMM argument
   ./page_owner_filter -c
   ./page_owner_filter: option requires an argument -- 'c'
   Usage: ./page_owner_filter [OPTIONS]

   Options:
   -m, --mode MODE       : print_mode (stack, handle, stack_handle)
   -n, --nid NID_LIST    : NUMA nodes (comma-separated or ranges)
   -p, --pid PID_LIST    : Process IDs (comma-separated, max 16)
   -t, --tgid TGID_LIST  : Thread Group IDs (comma-separated, max 16)
   -c, --comm COMM_LIST  : Process names (comma-separated, max 8)
                           Supports wildcards: * ? [a-z]
   -g, --cgroup PATH     : Memory cgroup path
   -o, --output FILE     : output file (default: stdout)
   -h, --help            : show this help message

   Examples:
   ./page_owner_filter -m handle -o output.txt
   ./page_owner_filter -n 0,1 -c bash
   ./page_owner_filter -c "python*" -g user.slice

   Test: COMM with only spaces
   ./page_owner_filter -c '   '
   write filter command: Invalid argument

   Test: COMM with path separator
   Verify: Non-existent COMM is accepted but produces no output
   ./page_owner_filter -c 'invalid/name'

   Test: Empty COMM in list (consecutive commas)
   ./page_owner_filter -c 'test,,another'
   Error: Empty COMM in list

   Test: Empty COMM at end
   ./page_owner_filter -c 'test,'
   Error: Empty COMM at end of list

   Test: Empty COMM at start
   ./page_owner_filter -c ',test'
   Error: Empty COMM in list

   Test: Extremely long COMM in a list
   ./page_owner_filter -c 'short,very_long_process_name,another'
   Error: COMM too long (max 15 chars)
   Near: very_long_proce...


   --- 1.4 Cgroup Invalid Inputs ---
   Test: Empty cgroup path
   ./page_owner_filter -g
   ./page_owner_filter: option requires an argument -- 'g'
   Usage: ./page_owner_filter [OPTIONS]

   Options:
   -m, --mode MODE       : print_mode (stack, handle, stack_handle)
   -n, --nid NID_LIST    : NUMA nodes (comma-separated or ranges)
   -p, --pid PID_LIST    : Process IDs (comma-separated, max 16)
   -t, --tgid TGID_LIST  : Thread Group IDs (comma-separated, max 16)
   -c, --comm COMM_LIST  : Process names (comma-separated, max 8)
                           Supports wildcards: * ? [a-z]
   -g, --cgroup PATH     : Memory cgroup path
   -o, --output FILE     : output file (default: stdout)
   -h, --help            : show this help message

   Examples:
   ./page_owner_filter -m handle -o output.txt
   ./page_owner_filter -n 0,1 -c bash
   ./page_owner_filter -c "python*" -g user.slice

   Test: Non-existent cgroup path
   ./page_owner_filter -g /nonexistent/path/that/does/not/exist
   Error: Cgroup path '/nonexistent/path/that/does/not/exist': not found or no memory controller

   Test: Cgroup path with spaces
   ./page_owner_filter -g '/path with spaces'
   Error: Cgroup path '/path with spaces': not found or no memory controller

   Test: Cgroup exists but has no memory controller
   ✓ Created: /sys/fs/cgroup/test_no_memcg_parent_796/test_child
   ✓ Verified: /sys/fs/cgroup/test_no_memcg_parent_796/test_child/memory.stat does not exist
   Note: page_owner_filter should detect missing memory.stat
   ./page_owner_filter -g /test_no_memcg_parent_796/test_child
   Error: Cgroup path '/test_no_memcg_parent_796/test_child': not found or no memory controller

   Test: Cgroup path exceeding PATH_MAX
   Generated path length: 4101
   ./page_owner_filter -g '<very long path>'
   Error: Cgroup path '/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa': not found or no memory controller

   Test: Multiple cgroup paths (last one wins)
   ./page_owner_filter -g /user.slice -g /sys/fs/cgroup
   Verify: Tool accepts multiple -g and uses the last one
   Note: /sys/fs/cgroup is invalid
   Error: Cgroup path '/sys/fs/cgroup': not found or no memory controller


   --- 1.5 General Invalid Options ---
   Test: Invalid option flag
   ./page_owner_filter -x
   ./page_owner_filter: invalid option -- 'x'
   Usage: ./page_owner_filter [OPTIONS]

   Options:
   -m, --mode MODE       : print_mode (stack, handle, stack_handle)
   -n, --nid NID_LIST    : NUMA nodes (comma-separated or ranges)
   -p, --pid PID_LIST    : Process IDs (comma-separated, max 16)
   -t, --tgid TGID_LIST  : Thread Group IDs (comma-separated, max 16)
   -c, --comm COMM_LIST  : Process names (comma-separated, max 8)
                           Supports wildcards: * ? [a-z]
   -g, --cgroup PATH     : Memory cgroup path
   -o, --output FILE     : output file (default: stdout)
   -h, --help            : show this help message

   Examples:
   ./page_owner_filter -m handle -o output.txt
   ./page_owner_filter -n 0,1 -c bash
   ./page_owner_filter -c "python*" -g user.slice

   Test: Unknown filter combination
   ./page_owner_filter -q 123
   ./page_owner_filter: invalid option -- 'q'
   Usage: ./page_owner_filter [OPTIONS]

   Options:
   -m, --mode MODE       : print_mode (stack, handle, stack_handle)
   -n, --nid NID_LIST    : NUMA nodes (comma-separated or ranges)
   -p, --pid PID_LIST    : Process IDs (comma-separated, max 16)
   -t, --tgid TGID_LIST  : Thread Group IDs (comma-separated, max 16)
   -c, --comm COMM_LIST  : Process names (comma-separated, max 8)
                           Supports wildcards: * ? [a-z]
   -g, --cgroup PATH     : Memory cgroup path
   -o, --output FILE     : output file (default: stdout)
   -h, --help            : show this help message

   Examples:
   ./page_owner_filter -m handle -o output.txt
   ./page_owner_filter -n 0,1 -c bash
   ./page_owner_filter -c "python*" -g user.slice

   --- 1.6 Mixed Invalid ---
   Test: Valid PID + valid cgroup + invalid NID (node 4 doesn't exist)
   Verify: Invalid NID should cause rejection even with valid other filters
   ./page_owner_filter -p 1 -g / -n 4
   write filter command: Invalid argument


   === II. PID 1 AND ROOT CGROUP TESTS ===

   --- 2.1 PID Filter with PID 1 ---
   Test: Filter by PID 1 (init/systemd)
   ./page_owner_filter -p 1 | head -20
   Page allocated via order 3, mask 0xd20c0(__GFP_IO|__GFP_FS|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP|__GFP_NOMEMALLOC), pid 1, tgid 1 (swapper/0), ts 5991990 ns
   PFN 0x400c8 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000040(head|node=0|zone=0|lastcpupid=0x1ffff)
   get_page_from_freelist+0x17d8/0x1a60
   __alloc_frozen_pages_noprof+0x198/0x133c
   new_slab+0xd0/0x4e8
   refill_objects+0x204/0x278
   __pcs_replace_empty_main+0x130/0x3ec
   __kmalloc_noprof+0x354/0x42c
   bitmap_zalloc+0x24/0x30
   asids_init+0x64/0xf0
   do_one_initcall+0x70/0x1b8
   kernel_init_freeable+0x108/0x2ec
   kernel_init+0x2c/0x1e0
   ret_from_fork+0x10/0x20

   Page allocated via order 2, mask 0xd20c0(__GFP_IO|__GFP_FS|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP|__GFP_NOMEMALLOC), pid 1, tgid 1 (swapper/0), ts 11666300 ns
   PFN 0x400e0 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000040(head|node=0|zone=0|lastcpupid=0x1ffff)
   get_page_from_freelist+0x17d8/0x1a60
   __alloc_frozen_pages_noprof+0x198/0x133c
   new_slab+0xd0/0x4e8
   Verify: All pages should be from PID 1
   ./page_owner_filter -p 1 | grep -o "pid [0-9]*," | sort | uniq -c
   14919 pid 1,

   --- 2.2 TGID Filter with PID 1's TGID ---
   Test: Filter by TGID of PID 1
   ./page_owner_filter -t 1 | head -20
   Page allocated via order 3, mask 0xd20c0(__GFP_IO|__GFP_FS|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP|__GFP_NOMEMALLOC), pid 1, tgid 1 (swapper/0), ts 5991990 ns
   PFN 0x400c8 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000040(head|node=0|zone=0|lastcpupid=0x1ffff)
   get_page_from_freelist+0x17d8/0x1a60
   __alloc_frozen_pages_noprof+0x198/0x133c
   new_slab+0xd0/0x4e8
   refill_objects+0x204/0x278
   __pcs_replace_empty_main+0x130/0x3ec
   __kmalloc_noprof+0x354/0x42c
   bitmap_zalloc+0x24/0x30
   asids_init+0x64/0xf0
   do_one_initcall+0x70/0x1b8
   kernel_init_freeable+0x108/0x2ec
   kernel_init+0x2c/0x1e0
   ret_from_fork+0x10/0x20

   Page allocated via order 2, mask 0xd20c0(__GFP_IO|__GFP_FS|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP|__GFP_NOMEMALLOC), pid 1, tgid 1 (swapper/0), ts 11666300 ns
   PFN 0x400e0 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000040(head|node=0|zone=0|lastcpupid=0x1ffff)
   get_page_from_freelist+0x17d8/0x1a60
   __alloc_frozen_pages_noprof+0x198/0x133c
   new_slab+0xd0/0x4e8
   Verify: All pages should be from TGID 1
   ./page_owner_filter -t 1 | grep -o "tgid [0-9]* " | sort | uniq -c
   14919 tgid 1 

   --- 2.3 COMM Filter with systemd/init ---
   Test: Filter by systemd (or init if systemd not present)
   ./page_owner_filter -c systemd | head -20
   Page allocated via order 1, mask 0xd20c0(__GFP_IO|__GFP_FS|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP|__GFP_NOMEMALLOC), pid 170, tgid 170 (systemd), ts 4340986430 ns
   PFN 0x40172 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000040(head|node=0|zone=0|lastcpupid=0x1ffff)
   get_page_from_freelist+0x17d8/0x1a60
   __alloc_frozen_pages_noprof+0x198/0x133c
   new_slab+0xd0/0x4e8
   refill_objects+0x204/0x278
   __pcs_replace_empty_main+0x130/0x3ec
   __kmalloc_noprof+0x354/0x42c
   load_elf_phdrs+0x68/0xec
   load_elf_binary+0xac/0x1664
   bprm_execve+0x274/0x4c4
   do_execveat_common.isra.0+0x164/0x1c4
   __arm64_sys_execve+0x44/0x68
   invoke_syscall+0x54/0x110
   el0_svc_common.constprop.0+0x40/0xe8
   do_el0_svc+0x20/0x2c
   el0_svc+0x30/0x198
   el0t_64_sync_handler+0xa0/0xe4

   Page allocated via order 1, mask 0x252800(GFP_NOWAIT|__GFP_NORETRY|__GFP_COMP|__GFP_THISNODE), pid 1, tgid 1 (systemd), ts 3621526030 ns
   Verify: All pages should be from systemd
   ./page_owner_filter -c systemd | grep -o "([a-z]*)," | sort | uniq -c
      2290 (systemd),

   --- 2.4 Cgroup Filter with Root Cgroup ---
   Test: Filter by root cgroup /
   ./page_owner_filter -g / | head -20
   Page allocated via order 0, mask 0x42800(GFP_NOWAIT|__GFP_COMP), pid 1, tgid 1 (swapper/0), ts 162339490 ns
   PFN 0x40b33 type Unmovable Block 517 type Unmovable Flags 0x3fffe0000004124(referenced|lru|active|private|node=0|zone=0|lastcpupid=0x1ffff)
   get_page_from_freelist+0x17d8/0x1a60
   __alloc_frozen_pages_noprof+0x198/0x133c
   alloc_pages_mpol+0x70/0x1c4
   alloc_frozen_pages_noprof+0x4c/0xc8
   folio_alloc_noprof+0x14/0x6c
   filemap_alloc_folio_noprof.part.0+0x134/0x148
   __filemap_get_folio_mpol+0x260/0x50c
   __getblk_slow+0x94/0x2c4
   bdev_getblk+0x68/0x7c
   ext4_sb_breadahead_unmovable+0x1c/0x84
   __ext4_get_inode_loc+0x334/0x520
   __ext4_get_inode_loc_noinmem+0x44/0xb0
   __ext4_iget+0x1cc/0xd38
   ext4_get_journal_inode+0x30/0x110
   ext4_fill_super+0x1624/0x2be0
   get_tree_bdev_flags+0x140/0x1ec
   Charged to memcg /

   Verify: All pages with memcg info should be charged to root cgroup
   Total pages:
      ./page_owner_filter -g / | grep -c "^Page allocated"
   14187
   Charged memcg paths (should only show memcg /):
      ./page_owner_filter -g / | grep "Charged to" | grep -o "memcg [^"]*" | sort | uniq -c
   14187 memcg /


   === III. TEST PROGRAM FILTERS (WITH PAGE COUNT VERIFICATION) ===

   Found test program with main PID: 792
   TGID: 792
   Thread 1: PID=793, COMM=po_thread_a, cgroup=/test_a
   Thread 2: PID=794, COMM=po_thread_b, cgroup=/test_b
   Thread 3: PID=795, COMM=po_thread_c, cgroup=/test_c

   Note: Each thread allocates 1MB (~256 pages)
   --- 3.1 Thread 1 (po_thread_a) Verification ---
   ./page_owner_filter -p 793 | grep -c PFN
   ./page_owner_filter -c po_thread_a | grep -c PFN
   ./page_owner_filter -g test_a | grep -c PFN
   By PID (-p 793):   262 pages
   By COMM (-c po_thread_a): 262 pages
   By cgroup (-g test_a):   267 pages

   --- 3.2 Thread 2 (po_thread_b) ---
   ./page_owner_filter -p 794 | grep -c PFN
   ./page_owner_filter -c po_thread_b | grep -c PFN
   ./page_owner_filter -g test_b | grep -c PFN
   By PID (-p 794):   263 pages
   By COMM (-c po_thread_b): 263 pages
   By cgroup (-g test_b):   268 pages

   --- 3.3 Thread 3 (po_thread_c) ---
   ./page_owner_filter -p 795 | grep -c PFN
   ./page_owner_filter -c po_thread_c | grep -c PFN
   ./page_owner_filter -g test_c | grep -c PFN
   By PID (-p 795):   264 pages
   By COMM (-c po_thread_c): 264 pages
   By cgroup (-g test_c):   257 pages

   --- 3.4 Summary: All Thread Counts ---
   Thread          By PID          By COMM         By Cgroup      
   --------------- --------------- --------------- ---------------
   po_thread_a     262             262             267            
   po_thread_b     263             263             268            
   po_thread_c     264             264             257            
   TOTAL           789             789             792            

   --- 3.6 Multi-PID List Test ---
   Verify: PID list equals sum of individual PIDs
   Expected count: 789
   ./page_owner_filter -p 793,794,795 | grep -c PFN
   789

   ./page_owner_filter -p 792 | grep -c PFN
   50

   ./page_owner_filter -p 792,793,794,795 | grep -c PFN
   839

   Verify: TGID equals sum of all PIDs in thread group (4 PIDs)
   Expected count: -p 792,793,794,795
   ./page_owner_filter -t 792 | grep -c PFN
   839

   --- 3.7 COMM Wildcard Test ---
   Verify: Wildcard matches all thread COMMs (should equal sum of thread counts)
   Expected count: 789
   ./page_owner_filter -c 'po_thread_*' | grep -c PFN
   789

   ./page_owner_filter -c 'page_owner_test' | grep -c PFN
   52

   Verify: COMM list equals sum of individual COMMs
   Expected count: 789 + -c 'page_owner_test'
   ./page_owner_filter -c 'po_thread_*,page_owner_test' | grep -c PFN
   841

   Verify: Should only show current test PIDs
   ./page_owner_filter -c 'po_thread_*,page_owner_test' | grep -o "pid [0-9]*," | sort | uniq -c
         1 pid 684,
         1 pid 685,
         1 pid 686,
         1 pid 785,
         1 pid 786,
         1 pid 787,
      46 pid 792,
      262 pid 793,
      263 pid 794,
      264 pid 795,
   Note: Previous test program PIDs may appear with small page counts (~10-20) due to kernel cache


   === IV. COMBINATION FILTER TESTS ===

   --- 4.1 PID + Cgroup Combination ---
   Test: PID 1 with Root Cgroup
   Verify: Should only show PID 1 pages in root cgroup
   ./page_owner_filter -p 1 -g / | grep -o "pid [0-9]*," | sort | uniq -c
      9779 pid 1,
   Verify: All pages should be charged to root cgroup (total pages = charged pages count)
   Total pages:
      ./page_owner_filter -p 1 -g / | grep -c "^Page allocated"
   9779
   Charged memcg paths:
      ./page_owner_filter -p 1 -g / | grep "Charged to" | grep -o "memcg [^"]*" | sort | uniq -c
      9779 memcg /

   --- 4.2 COMM + NID Combination ---
   Test: systemd with Node 0 filter
   ./page_owner_filter -c systemd -n 0 | head -10
   Page allocated via order 1, mask 0xd20c0(__GFP_IO|__GFP_FS|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP|__GFP_NOMEMALLOC), pid 170, tgid 170 (systemd), ts 4340986430 ns
   PFN 0x40172 type Unmovable Block 512 type Unmovable Flags 0x3fffe0000000040(head|node=0|zone=0|lastcpupid=0x1ffff)
   get_page_from_freelist+0x17d8/0x1a60
   __alloc_frozen_pages_noprof+0x198/0x133c
   new_slab+0xd0/0x4e8
   refill_objects+0x204/0x278
   __pcs_replace_empty_main+0x130/0x3ec
   __kmalloc_noprof+0x354/0x42c
   load_elf_phdrs+0x68/0xec
   load_elf_binary+0xac/0x1664
   Verify: Should only show systemd pages on node 0
      ./page_owner_filter -c systemd -n 0 | grep -o "([a-z]*)," | sort | uniq -c
      191 (systemd),
      ./page_owner_filter -c systemd -n 0 | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c
      191 node=0

   --- 4.3 Test Program Filter Combinations ---
   Note: Process filters (PID/TGID/COMM) use OR logic among themselves,
         but AND logic with other filters (e.g., cgroup)

   expected: TGID page count, max of PID or TGID count)
   ./page_owner_filter -p 793 -t 792 | grep -c PFN
   839

   expected: max of PID or COMM count
   ./page_owner_filter -p 793 -c po_thread_a | grep -c PFN
   264

   expected: TGID page count,max of TGID or COMM count
   ./page_owner_filter -t 792 -c po_thread_a | grep -c PFN
   841

   AND logic: both must match
   ./page_owner_filter -p 793 -g /test_a | grep -c PFN
   257


   =========================================
   Tests completed
   =========================================

   Summary of test categories:
   I.   Invalid inputs for all filters
   II.  PID 1 and root cgroup tests
   III. Test program specific tests (with page count verification)
   IV.  Combination filter tests

   Please review output above for any errors or unexpected behavior.

Appendix - Test Programs
========================

1. page_owner_test_alloc.c - Test allocation program::

// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
#include <sys/prctl.h>
#include <sys/stat.h>

#define ALLOC_MB 1
#define NUM_THREADS 3

static volatile sig_atomic_t shutdown = 0;
static char cgroup_names[NUM_THREADS][16] = {"test_a", "test_b", "test_c"};

static void sig_handler(int signo)
{
	shutdown = 1;
}

static void cleanup(void)
{
	FILE *f;

	/* Move self to root cgroup first */
	f = fopen("/sys/fs/cgroup/cgroup.procs", "w");
	if (!f)
		f = fopen("/sys/fs/cgroup/memory/cgroup.procs", "w");
	if (f) {
		fprintf(f, "%d", getpid());
		fclose(f);
	}

	/* Give threads time to exit cgroup */
	usleep(50000);

	/* Now remove the cgroups */
	for (int i = 0; i < NUM_THREADS; i++) {
		char path[128];
		snprintf(path, sizeof(path), "/sys/fs/cgroup/%s", cgroup_names[i]);
		rmdir(path);
		snprintf(path, sizeof(path), "/sys/fs/cgroup/memory/%s", cgroup_names[i]);
		rmdir(path);
	}
}

static void *thread_func(void *arg)
{
	int id = *(int *)arg;
	char comm[16], path[256];
	FILE *f;

	snprintf(comm, sizeof(comm), "po_thread_%c", 'a' + id);
	prctl(PR_SET_NAME, comm);

	/* Create cgroup and move thread (try v2, fallback to v1) */
	snprintf(path, sizeof(path), "/sys/fs/cgroup/%s", cgroup_names[id]);
	mkdir(path, 0755);
	strcat(path, "/cgroup.procs");
	f = fopen(path, "w");
	if (!f) {
		/* Try cgroup v1 memory controller */
		snprintf(path, sizeof(path), "/sys/fs/cgroup/memory/%s", cgroup_names[id]);
		mkdir(path, 0755);
		strcat(path, "/cgroup.procs");
		f = fopen(path, "w");
	}
	if (f) {
		fprintf(f, "%d", gettid());
		fclose(f);
	}

	/* Allocate memory */
	size_t sz = ALLOC_MB * 1024 * 1024;
	char *p = malloc(sz);
	if (p) {
		for (size_t i = 0; i < sz; i += 4096)
			p[i] = (char)i;
	}

	printf("TGID: %d PID: %d COMM: %s CGROUP: /%s\n",
	       getpid(), gettid(), comm, cgroup_names[id]);
	fflush(stdout);

	while (!shutdown) sleep(1);
	return NULL;
}

int main(void)
{
	pthread_t threads[NUM_THREADS];
	int ids[NUM_THREADS];

	signal(SIGINT, sig_handler);
	signal(SIGTERM, sig_handler);

	for (int i = 0; i < NUM_THREADS; i++) {
		ids[i] = i;
		pthread_create(&threads[i], NULL, thread_func, &ids[i]);
		usleep(100000);
	}

	while (!shutdown) pause();

	shutdown = 1;
	for (int i = 0; i < NUM_THREADS; i++)
		pthread_join(threads[i], NULL);

	cleanup();
	return 0;
}

2. test_page_owner_filters.sh - Comprehensive test script::

#!/bin/bash
# Comprehensive test script for page_owner filters
# Tests: PID/TGID/COMM/Cgroup filters (invalid cases, PID 1, test program, combinations)

cd "$(dirname "$0")"

# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

echo "========================================="
echo "Page Owner Filter Tests"
echo "========================================="
echo

# ============================================================================
# Section I: Invalid Input Tests
# ============================================================================
echo -e "${YELLOW}=== I. INVALID INPUT TESTS ===${NC}"
echo

echo "--- 1.1 PID Invalid Inputs ---"
echo "Test: Negative PID"
echo "  ./page_owner_filter -p -1"
./page_owner_filter -p -1
echo

echo "Test: Non-numeric PID"
echo "  ./page_owner_filter -p abc"
./page_owner_filter -p abc
echo

echo "Test: Empty PID argument"
echo "  ./page_owner_filter -p"
./page_owner_filter -p
echo

echo "Test: PID with letters (mixed invalid)"
echo "  ./page_owner_filter -p 123abc"
./page_owner_filter -p 123abc
echo

echo "Test: Multiple invalid PIDs"
echo "  ./page_owner_filter -p -1,-2,-3"
./page_owner_filter -p -1,-2,-3
echo

echo "Test: Excessive PID value (> max PID)"
echo "  ./page_owner_filter -p 4294967296"
./page_owner_filter -p 4294967296
echo

echo "Test: Too many PIDs (17 PIDs, max is 16)"
echo "  ./page_owner_filter -p 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17"
./page_owner_filter -p 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17
echo

echo
echo "--- 1.2 TGID Invalid Inputs ---"
echo "Test: Negative TGID"
echo "  ./page_owner_filter -t -1"
./page_owner_filter -t -1
echo

echo "Test: Non-numeric TGID"
echo "  ./page_owner_filter -t xyz"
./page_owner_filter -t xyz
echo

echo "Test: Empty TGID argument"
echo "  ./page_owner_filter -t"
./page_owner_filter -t
echo

echo "Test: TGID with special characters"
echo "  ./page_owner_filter -t '\$\$'"
./page_owner_filter -t '$$'
echo

echo "Test: Mixed valid/invalid TGID"
echo "  ./page_owner_filter -t 1,abc,2"
./page_owner_filter -t 1,abc,2
echo

echo
echo "--- 1.3 COMM Invalid Inputs ---"
echo "Test: Empty COMM argument"
echo "  ./page_owner_filter -c"
./page_owner_filter -c
echo

echo "Test: COMM with only spaces"
echo "  ./page_owner_filter -c '   '"
./page_owner_filter -c "   "
echo

echo "Test: COMM with path separator"
echo "  Verify: Non-existent COMM is accepted but produces no output"
echo "  ./page_owner_filter -c 'invalid/name'"
./page_owner_filter -c "invalid/name"
echo

echo "Test: Empty COMM in list (consecutive commas)"
echo "  ./page_owner_filter -c 'test,,another'"
./page_owner_filter -c "test,,another"
echo

echo "Test: Empty COMM at end"
echo "  ./page_owner_filter -c 'test,'"
./page_owner_filter -c "test,"
echo

echo "Test: Empty COMM at start"
echo "  ./page_owner_filter -c ',test'"
./page_owner_filter -c ",test"
echo

echo "Test: Extremely long COMM in a list"
echo "  ./page_owner_filter -c 'short,very_long_process_name,another'"
./page_owner_filter -c "short,very_long_process_name,another"
echo

echo
echo "--- 1.4 Cgroup Invalid Inputs ---"
echo "Test: Empty cgroup path"
echo "  ./page_owner_filter -g"
./page_owner_filter -g
echo

echo "Test: Non-existent cgroup path"
echo "  ./page_owner_filter -g /nonexistent/path/that/does/not/exist"
./page_owner_filter -g /nonexistent/path/that/does/not/exist
echo

echo "Test: Cgroup path with spaces"
echo "  ./page_owner_filter -g '/path with spaces'"
./page_owner_filter -g "/path with spaces"
echo

echo "Test: Cgroup exists but has no memory controller"
TEST_PARENT_DIR="test_no_memcg_parent_$$"
TEST_CHILD_DIR="test_child"

if [ -d "/sys/fs/cgroup/memory" ]; then
	# cgroup v1: /cpuset exists but lacks memory controller
	echo "  ./page_owner_filter -g /cpuset"
	echo "  Note: /cpuset exists under memory/ but lacks memory.stat (cgroup v1)"
	./page_owner_filter -g /cpuset
else
	# cgroup v2: create parent without memory in subtree_control
	TEST_PARENT_PATH="/sys/fs/cgroup/$TEST_PARENT_DIR"
	TEST_CHILD_PATH="$TEST_PARENT_PATH/$TEST_CHILD_DIR"

	if mkdir -p "$TEST_PARENT_PATH" 2>/dev/null && \
	   echo "-memory" > "$TEST_PARENT_PATH/cgroup.subtree_control" 2>/dev/null && \
	   mkdir "$TEST_CHILD_PATH" 2>/dev/null; then
		if [ -d "$TEST_CHILD_PATH" ]; then
			echo "  ✓ Created: $TEST_CHILD_PATH"
			if [ -f "$TEST_CHILD_PATH/memory.stat" ]; then
				echo "  ✗ Test setup failed: $TEST_CHILD_PATH/memory.stat exists (should not)"
			else
				echo "  ✓ Verified: $TEST_CHILD_PATH/memory.stat does not exist"
			fi
			echo "  Note: page_owner_filter should detect missing memory.stat"
			echo "  ./page_owner_filter -g /$TEST_PARENT_DIR/$TEST_CHILD_DIR"
			./page_owner_filter -g "/$TEST_PARENT_DIR/$TEST_CHILD_DIR"
		else
			echo "  ✗ Failed to create: $TEST_CHILD_PATH"
		fi
		rmdir "$TEST_CHILD_PATH" 2>/dev/null
		rmdir "$TEST_PARENT_PATH" 2>/dev/null
	else
		echo "  (skipped - cannot create test cgroup)"
	fi
fi
echo

echo "Test: Cgroup path exceeding PATH_MAX"
LONG_PATH="/$(head -c 4100 /dev/zero | tr '\0' 'a')"
echo "  Generated path length: ${#LONG_PATH}"
echo "  ./page_owner_filter -g '<very long path>'"
./page_owner_filter -g "$LONG_PATH"
echo

echo "Test: Multiple cgroup paths (last one wins)"
echo "  ./page_owner_filter -g /user.slice -g /sys/fs/cgroup"
echo "  Verify: Tool accepts multiple -g and uses the last one"
echo "  Note: /sys/fs/cgroup is invalid"
./page_owner_filter -g /user.slice -g /sys/fs/cgroup
echo

echo
echo "--- 1.5 General Invalid Options ---"
echo "Test: Invalid option flag"
echo "  ./page_owner_filter -x"
./page_owner_filter -x
echo

echo "Test: Unknown filter combination"
echo "  ./page_owner_filter -q 123"
./page_owner_filter -q 123
echo

echo "--- 1.6 Mixed Invalid ---"
echo "Test: Valid PID + valid cgroup + invalid NID (node 4 doesn't exist)"
echo "  Verify: Invalid NID should cause rejection even with valid other filters"
echo "  ./page_owner_filter -p 1 -g / -n 4"
./page_owner_filter -p 1 -g / -n 4
echo

# ============================================================================
# Section II: PID 1 and Root Cgroup Tests
# ============================================================================
echo
echo -e "${YELLOW}=== II. PID 1 AND ROOT CGROUP TESTS ===${NC}"
echo

echo "--- 2.1 PID Filter with PID 1 ---"
echo "Test: Filter by PID 1 (init/systemd)"
echo "  ./page_owner_filter -p 1 | head -20"
./page_owner_filter -p 1 | head -20
echo "Verify: All pages should be from PID 1"
echo "  ./page_owner_filter -p 1 | grep -o \"pid [0-9]*,\" | sort | uniq -c"
./page_owner_filter -p 1 | grep -o "pid [0-9]*," | sort | uniq -c
echo

echo "--- 2.2 TGID Filter with PID 1's TGID ---"
echo "Test: Filter by TGID of PID 1"
echo "  ./page_owner_filter -t 1 | head -20"
./page_owner_filter -t 1 | head -20
echo "Verify: All pages should be from TGID 1"
echo "  ./page_owner_filter -t 1 | grep -o \"tgid [0-9]* \" | sort | uniq -c"
./page_owner_filter -t 1 | grep -o "tgid [0-9]* " | sort | uniq -c
echo

echo "--- 2.3 COMM Filter with systemd/init ---"
echo "Test: Filter by systemd (or init if systemd not present)"
echo "  ./page_owner_filter -c systemd | head -20"
./page_owner_filter -c systemd | head -20
echo "Verify: All pages should be from systemd"
echo "  ./page_owner_filter -c systemd | grep -o \"([a-z]*),\" | sort | uniq -c"
./page_owner_filter -c systemd | grep -o "([a-z]*)," | sort | uniq -c
echo

echo "--- 2.4 Cgroup Filter with Root Cgroup ---"
echo "Test: Filter by root cgroup /"
echo "  ./page_owner_filter -g / | head -20"
./page_owner_filter -g / | head -20
echo "Verify: All pages with memcg info should be charged to root cgroup"
echo "  Total pages:"
echo "    ./page_owner_filter -g / | grep -c \"^Page allocated\""
./page_owner_filter -g / | grep -c '^Page allocated'
echo "  Charged memcg paths (should only show memcg /):"
echo "    ./page_owner_filter -g / | grep \"Charged to\" | grep -o \"memcg [^\"]*\" | sort | uniq -c"
./page_owner_filter -g / | grep "Charged to" | grep -o "memcg [^\"]*" | sort | uniq -c
echo

# ============================================================================
# Section III: Test Program (page_owner_test_alloc) Tests
# ============================================================================
echo
echo -e "${YELLOW}=== III. TEST PROGRAM FILTERS (WITH PAGE COUNT VERIFICATION) ===${NC}"
echo

# Find the running test_alloc process and get all 5 variables
TEST_PID=$(pgrep -f "page_owner_test_alloc" | head -1)
if [ -z "$TEST_PID" ]; then
    echo -e "${RED}ERROR: page_owner_test_alloc not running!${NC}"
    echo "Please start it first: ./page_owner_test_alloc &"
    echo
else
    echo "Found test program with main PID: $TEST_PID"

    # Get TGID (main process TGID, same for all threads)
    TEST_TGID=$(cat /proc/$TEST_PID/status 2>/dev/null | grep -i "^Tgid:" | awk '{print $2}')
    echo "TGID: $TEST_TGID"

    # Get the 3 thread PIDs (po_thread_a, po_thread_b, po_thread_c)
    THREAD_PIDS=$(ls -1 /proc/$TEST_PID/task/ | grep -v "^$TEST_PID$" | sort -n)
    TEST_PID_1=$(echo "$THREAD_PIDS" | sed -n '1p')
    TEST_PID_2=$(echo "$THREAD_PIDS" | sed -n '2p')
    TEST_PID_3=$(echo "$THREAD_PIDS" | sed -n '3p')

    # Get COMM names for each thread
    TEST_COMM_1=$(cat /proc/$TEST_PID_1/comm 2>/dev/null | tr -d '\n')
    TEST_COMM_2=$(cat /proc/$TEST_PID_2/comm 2>/dev/null | tr -d '\n')
    TEST_COMM_3=$(cat /proc/$TEST_PID_3/comm 2>/dev/null | tr -d '\n')

    echo "Thread 1: PID=$TEST_PID_1, COMM=$TEST_COMM_1, cgroup=/test_a"
    echo "Thread 2: PID=$TEST_PID_2, COMM=$TEST_COMM_2, cgroup=/test_b"
    echo "Thread 3: PID=$TEST_PID_3, COMM=$TEST_COMM_3, cgroup=/test_c"
    echo

    # --- 3.1 Individual Filter Tests with Page Counts ---
    echo "Note: Each thread allocates 1MB (~256 pages)"

    echo "--- 3.1 Thread 1 (po_thread_a) Verification ---"
    echo "  ./page_owner_filter -p $TEST_PID_1 | grep -c PFN"
    PID1_COUNT=$(./page_owner_filter -p "$TEST_PID_1" 2>/dev/null | grep -c "PFN" || echo "0")
    echo "  ./page_owner_filter -c $TEST_COMM_1 | grep -c PFN"
    COMM1_COUNT=$(./page_owner_filter -c "$TEST_COMM_1" 2>/dev/null | grep -c "PFN" || echo "0")
    echo "  ./page_owner_filter -g test_a | grep -c PFN"
    CGROUP1_COUNT=$(./page_owner_filter -g test_a 2>/dev/null | grep -c "PFN" || echo "0")

    echo "  By PID (-p $TEST_PID_1):   $PID1_COUNT pages"
    echo "  By COMM (-c $TEST_COMM_1): $COMM1_COUNT pages"
    echo "  By cgroup (-g test_a):   $CGROUP1_COUNT pages"
    echo

    echo "--- 3.2 Thread 2 (po_thread_b) ---"
    echo "  ./page_owner_filter -p $TEST_PID_2 | grep -c PFN"
    PID2_COUNT=$(./page_owner_filter -p "$TEST_PID_2" 2>/dev/null | grep -c "PFN" || echo "0")
    echo "  ./page_owner_filter -c $TEST_COMM_2 | grep -c PFN"
    COMM2_COUNT=$(./page_owner_filter -c "$TEST_COMM_2" 2>/dev/null | grep -c "PFN" || echo "0")
    echo "  ./page_owner_filter -g test_b | grep -c PFN"
    CGROUP2_COUNT=$(./page_owner_filter -g test_b 2>/dev/null | grep -c "PFN" || echo "0")

    echo "  By PID (-p $TEST_PID_2):   $PID2_COUNT pages"
    echo "  By COMM (-c $TEST_COMM_2): $COMM2_COUNT pages"
    echo "  By cgroup (-g test_b):   $CGROUP2_COUNT pages"
    echo

    echo "--- 3.3 Thread 3 (po_thread_c) ---"
    echo "  ./page_owner_filter -p $TEST_PID_3 | grep -c PFN"
    PID3_COUNT=$(./page_owner_filter -p "$TEST_PID_3" 2>/dev/null | grep -c "PFN" || echo "0")
    echo "  ./page_owner_filter -c $TEST_COMM_3 | grep -c PFN"
    COMM3_COUNT=$(./page_owner_filter -c "$TEST_COMM_3" 2>/dev/null | grep -c "PFN" || echo "0")
    echo "  ./page_owner_filter -g test_c | grep -c PFN"
    CGROUP3_COUNT=$(./page_owner_filter -g test_c 2>/dev/null | grep -c "PFN" || echo "0")

    echo "  By PID (-p $TEST_PID_3):   $PID3_COUNT pages"
    echo "  By COMM (-c $TEST_COMM_3): $COMM3_COUNT pages"
    echo "  By cgroup (-g test_c):   $CGROUP3_COUNT pages"
    echo

    # --- 3.4 Summary Table ---
    TOTAL_PID_COUNT=$((PID1_COUNT + PID2_COUNT + PID3_COUNT))
    TOTAL_COMM_COUNT=$((COMM1_COUNT + COMM2_COUNT + COMM3_COUNT))
    TOTAL_CGROUP_COUNT=$((CGROUP1_COUNT + CGROUP2_COUNT + CGROUP3_COUNT))
    echo "--- 3.4 Summary: All Thread Counts ---"
    printf "%-15s %-15s %-15s %-15s\n" "Thread" "By PID" "By COMM" "By Cgroup"
    printf "%-15s %-15s %-15s %-15s\n" "---------------" "---------------" "---------------" "---------------"
    printf "%-15s %-15s %-15s %-15s\n" "$TEST_COMM_1" "$PID1_COUNT" "$COMM1_COUNT" "$CGROUP1_COUNT"
    printf "%-15s %-15s %-15s %-15s\n" "$TEST_COMM_2" "$PID2_COUNT" "$COMM2_COUNT" "$CGROUP2_COUNT"
    printf "%-15s %-15s %-15s %-15s\n" "$TEST_COMM_3" "$PID3_COUNT" "$COMM3_COUNT" "$CGROUP3_COUNT"
    printf "%-15s %-15s %-15s %-15s\n" "TOTAL" "$TOTAL_PID_COUNT" "$TOTAL_COMM_COUNT" "$TOTAL_CGROUP_COUNT"
    echo

    echo "--- 3.6 Multi-PID List Test ---"
    echo "  Verify: PID list equals sum of individual PIDs"
    echo "  Expected count: $TOTAL_PID_COUNT"
    ALL_PIDS="$TEST_PID_1,$TEST_PID_2,$TEST_PID_3"
    echo "  ./page_owner_filter -p $ALL_PIDS | grep -c PFN"
    ./page_owner_filter -p "$ALL_PIDS"| grep -c "PFN"
    echo

    echo "  ./page_owner_filter -p $TEST_PID | grep -c PFN"
    ./page_owner_filter -p "$TEST_PID" | grep -c "PFN"
    echo

    FOUR_PIDS="$TEST_PID,$ALL_PIDS"
    echo "  ./page_owner_filter -p $FOUR_PIDS | grep -c PFN"
    ./page_owner_filter -p "$FOUR_PIDS" | grep -c "PFN"
    echo

    echo "  Verify: TGID equals sum of all PIDs in thread group (4 PIDs)"
    echo "  Expected count: -p $FOUR_PIDS"
    echo "  ./page_owner_filter -t $TEST_TGID | grep -c PFN"
    ./page_owner_filter -t "$TEST_TGID" | grep -c "PFN"
    echo

    echo "--- 3.7 COMM Wildcard Test ---"
    echo "  Verify: Wildcard matches all thread COMMs (should equal sum of thread counts)"
    echo "  Expected count: $TOTAL_COMM_COUNT"
    echo "  ./page_owner_filter -c 'po_thread_*' | grep -c PFN"
    ./page_owner_filter -c 'po_thread_*' | grep -c "PFN"
    echo

    echo "  ./page_owner_filter -c 'page_owner_test' | grep -c PFN"
    ./page_owner_filter -c 'page_owner_test' | grep -c "PFN"
    echo

    echo "  Verify: COMM list equals sum of individual COMMs"
    echo "  Expected count: $TOTAL_COMM_COUNT + -c 'page_owner_test'"
    echo "  ./page_owner_filter -c 'po_thread_*,page_owner_test' | grep -c PFN"
    ./page_owner_filter -c 'po_thread_*,page_owner_test' | grep -c "PFN"
    echo
    echo "  Verify: Should only show current test PIDs"
    echo "  ./page_owner_filter -c 'po_thread_*,page_owner_test' | grep -o \"pid [0-9]*,\" | sort | uniq -c"
    ./page_owner_filter -c 'po_thread_*,page_owner_test' | grep -o "pid [0-9]*," | sort | uniq -c
    echo "  Note: Previous test program PIDs may appear with small page counts (~10-20) due to kernel cache"
    echo

fi

# ============================================================================
# Section IV: Combination Filter Tests
# ============================================================================
echo
echo -e "${YELLOW}=== IV. COMBINATION FILTER TESTS ===${NC}"
echo

echo "--- 4.1 PID + Cgroup Combination ---"
echo "Test: PID 1 with Root Cgroup"
echo "  Verify: Should only show PID 1 pages in root cgroup"
echo "  ./page_owner_filter -p 1 -g / | grep -o \"pid [0-9]*,\" | sort | uniq -c"
./page_owner_filter -p 1 -g / | grep -o "pid [0-9]*," | sort | uniq -c
echo "  Verify: All pages should be charged to root cgroup (total pages = charged pages count)"
echo "  Total pages:"
echo "    ./page_owner_filter -p 1 -g / | grep -c \"^Page allocated\""
./page_owner_filter -p 1 -g / | grep -c "^Page allocated"
echo "  Charged memcg paths:"
echo "    ./page_owner_filter -p 1 -g / | grep \"Charged to\" | grep -o \"memcg [^\"]*\" | sort | uniq -c"
./page_owner_filter -p 1 -g / | grep "Charged to" | grep -o "memcg [^\"]*" | sort | uniq -c
echo

echo "--- 4.2 COMM + NID Combination ---"
echo "Test: systemd with Node 0 filter"
echo "  ./page_owner_filter -c systemd -n 0 | head -10"
./page_owner_filter -c systemd -n 0 | head -10
echo "  Verify: Should only show systemd pages on node 0"
echo "    ./page_owner_filter -c systemd -n 0 | grep -o \"([a-z]*),\" | sort | uniq -c"
./page_owner_filter -c systemd -n 0 | grep -o "([a-z]*)," | sort | uniq -c
echo "    ./page_owner_filter -c systemd -n 0 | grep \"PFN\" | grep -o \"node=[0-9]\" | sort | uniq -c"
./page_owner_filter -c systemd -n 0 | grep "PFN" | grep -o "node=[0-9]" | sort | uniq -c
echo

if [ -n "$TEST_PID_1" ]; then
    echo "--- 4.3 Test Program Filter Combinations ---"
    echo "  Note: Process filters (PID/TGID/COMM) use OR logic among themselves,"
    echo "        but AND logic with other filters (e.g., cgroup)"
    echo
    echo "  expected: TGID page count, max of PID or TGID count)"
    echo "  ./page_owner_filter -p $TEST_PID_1 -t $TEST_TGID | grep -c PFN"
    ./page_owner_filter -p "$TEST_PID_1" -t "$TEST_TGID" | grep -c "PFN"
    echo

    echo "  expected: max of PID or COMM count"
    echo "  ./page_owner_filter -p $TEST_PID_1 -c $TEST_COMM_1 | grep -c PFN"
    ./page_owner_filter -p "$TEST_PID_1" -c "$TEST_COMM_1" | grep -c "PFN"
    echo

    echo " expected: TGID page count,max of TGID or COMM count"
    echo " ./page_owner_filter -t $TEST_TGID -c $TEST_COMM_1 | grep -c PFN"
    ./page_owner_filter -t "$TEST_TGID" -c "$TEST_COMM_1" | grep -c "PFN"
    echo

    echo " AND logic: both must match"
    echo "  ./page_owner_filter -p $TEST_PID_1 -g /test_a | grep -c PFN"
    ./page_owner_filter -p "$TEST_PID_1" -g /test_a | grep -c "PFN"
    echo
fi

# ============================================================================
# Summary
# ============================================================================
echo
echo "========================================="
echo -e "${YELLOW}Tests completed${NC}"
echo "========================================="
echo
echo "Summary of test categories:"
echo "  I.   Invalid inputs for all filters"
echo "  II.  PID 1 and root cgroup tests"
echo "  III. Test program specific tests (with page count verification)"
echo "  IV.  Combination filter tests"
echo
echo "Please review output above for any errors or unexpected behavior."

Zhen Ni (8):
  mm/page_owner: Add PID filtering support
  mm/page_owner: Add TGID filtering support
  mm/page_owner: Add COMM filtering with wildcard support
  mm/page_owner: Refactor memcg handling for cgroup filter support
  mm/page_owner: Add memcg filter support
  tools/mm: Add PID/TGID/COMM filtering support to page_owner_filter
  tools/mm: Add memory cgroup filtering support to page_owner_filter
  Documentation: page_owner: Document PID/TGID/COMM and cgroup filters

 Documentation/mm/page_owner.rst |  25 ++-
 mm/page_owner.c                 | 327 +++++++++++++++++++++++++++++---
 tools/mm/page_owner_filter.c    | 220 ++++++++++++++++++---
 3 files changed, 526 insertions(+), 46 deletions(-)

-- 
2.20.1



^ permalink raw reply	[flat|nested] 11+ messages in thread

* [PATCH 1/8] mm/page_owner: Add PID filtering support
  2026-08-28  3:13 [PATCH 0/8] mm/page_owner: Add PID/TGID/COMM and cgroup filtering Zhen Ni
@ 2026-08-28  3:13 ` Zhen Ni
  2026-08-28  3:13 ` [PATCH 2/8] mm/page_owner: Add TGID " Zhen Ni
                   ` (7 subsequent siblings)
  8 siblings, 0 replies; 11+ messages in thread
From: Zhen Ni @ 2026-08-28  3:13 UTC (permalink / raw)
  To: Andrew Morton
  Cc: David Hildenbrand, Lorenzo Stoakes, Liam R . Howlett,
	Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
	Jonathan Corbet, Shuah Khan, Randy Dunlap, Brendan Jackman,
	Johannes Weiner, Zi Yan, linux-mm, linux-doc, linux-kernel,
	Zhen Ni

Add PID filtering support. Users can filter page_owner output by process
IDs using the "pid=<pid_list>" command format. The filter supports up to
16 PIDs specified as a comma-separated list. PIDs are stored in sorted
order for efficient binary search matching during page owner iteration.

Signed-off-by: Zhen Ni <zhen.ni@easystack.cn>
---
 mm/page_owner.c | 85 +++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 83 insertions(+), 2 deletions(-)

diff --git a/mm/page_owner.c b/mm/page_owner.c
index fbbda7ba914b..33b9e12a019b 100644
--- a/mm/page_owner.c
+++ b/mm/page_owner.c
@@ -12,6 +12,8 @@
 #include <linux/seq_file.h>
 #include <linux/memcontrol.h>
 #include <linux/sched/clock.h>
+#include <linux/bsearch.h>
+#include <linux/sort.h>
 
 #include "page_alloc.h"
 
@@ -66,12 +68,24 @@ static const char * const page_owner_print_mode_strings[] = {
 	[PAGE_OWNER_PRINT_STACK_HANDLE]	= "stack_handle",
 };
 
+/* PID_MAX_LIMIT = 4,194,304 (7 decimal digits) */
+#define PID_MAX_DIGITS  	7
+#define MAX_FILTER_PIDS 	16
+
 struct page_owner_filter_state {
 	enum page_owner_print_mode print_mode;
-	nodemask_t nid_filter;
 	bool nid_filter_enabled;
+	bool proc_filter_enabled;
+	nodemask_t nid_filter;
+	int pid_count;
+	pid_t pid_list[MAX_FILTER_PIDS];
 };
 
+static int cmp_pid_t(const void *a, const void *b)
+{
+	return *(pid_t *)a - *(pid_t *)b;
+}
+
 static bool page_owner_enabled __initdata;
 DEFINE_STATIC_KEY_FALSE(page_owner_inited);
 
@@ -820,6 +834,19 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
 				goto ext_put_continue;
 		}
 
+		if (state->proc_filter_enabled) {
+			bool proc_match = false;
+
+			proc_match = bsearch(&page_owner->pid,
+					    state->pid_list,
+					    state->pid_count,
+					    sizeof(pid_t),
+					    cmp_pid_t) != NULL;
+
+			if (!proc_match)
+				goto ext_put_continue;
+		}
+
 		/* Record the next PFN to read in the file offset */
 		*ppos = pfn + 1;
 
@@ -927,6 +954,7 @@ static int page_owner_open(struct inode *inode, struct file *file)
 	state->print_mode = PAGE_OWNER_PRINT_STACK;
 	nodes_clear(state->nid_filter);
 	state->nid_filter_enabled = false;
+	state->proc_filter_enabled = false;
 	file->private_data = state;
 	return 0;
 }
@@ -937,6 +965,37 @@ static int page_owner_release(struct inode *inode, struct file *file)
 	return 0;
 }
 
+static int parse_pid_t_list(const char *str, pid_t *list, int *count)
+{
+	char *str_copy, *token;
+	int i = 0;
+	unsigned int pid;
+	int ret = 0;
+
+	str_copy = kstrdup(str, GFP_KERNEL);
+	if (!str_copy)
+		return -ENOMEM;
+
+	while ((token = strsep(&str_copy, ",")) != NULL) {
+		if (*token == '\0')
+			continue;
+		if (i >= MAX_FILTER_PIDS) {
+			ret = -E2BIG;
+			goto out_free;
+		}
+		if (kstrtouint(token, 10, &pid) != 0) {
+			ret = -EINVAL;
+			goto out_free;
+		}
+		list[i++] = (pid_t)pid;
+	}
+
+	*count = i;
+out_free:
+	kfree(str_copy);
+	return ret;
+}
+
 static ssize_t page_owner_write(struct file *file,
 				 const char __user *buf,
 				 size_t count, loff_t *ppos)
@@ -949,6 +1008,9 @@ static ssize_t page_owner_write(struct file *file,
 	enum page_owner_print_mode new_print_mode;
 	nodemask_t new_nid_filter;
 	bool new_nid_filter_enabled;
+	bool new_proc_filter_enabled;
+	pid_t new_pid_list[MAX_FILTER_PIDS];
+	int new_pid_count = 0;
 
 	/*
 	 * Maximum input length for filter commands:
@@ -956,8 +1018,10 @@ static ssize_t page_owner_write(struct file *file,
 	 *        with sufficient buffer
 	 * - 6 * MAX_NUMNODES: worst case for nid list
 	 *   Worst case per node: ",NNNNN" (comma + 5-digit node number) = 6 bytes
+	 * - For list filters: (digit+comma) * count + prefix
 	 */
-	if (count > 32 + 6 * MAX_NUMNODES)
+	if (count > 32 + 6 * MAX_NUMNODES +
+			(PID_MAX_DIGITS + 1) * MAX_FILTER_PIDS + 4)
 		return -EINVAL;
 
 	kbuf = memdup_user_nul(buf, count);
@@ -969,6 +1033,11 @@ static ssize_t page_owner_write(struct file *file,
 	new_print_mode = state->print_mode;
 	new_nid_filter = state->nid_filter;
 	new_nid_filter_enabled = state->nid_filter_enabled;
+	new_proc_filter_enabled = state->proc_filter_enabled;
+	if (state->pid_count > 0) {
+		memcpy(new_pid_list, state->pid_list, sizeof(state->pid_list));
+		new_pid_count = state->pid_count;
+	}
 
 	while ((token = strsep(&kbuf, " \t\n")) != NULL) {
 		if (*token == '\0')
@@ -1000,6 +1069,10 @@ static ssize_t page_owner_write(struct file *file,
 			}
 
 			new_nid_filter_enabled = true;
+		} else if (!strncmp(token, "pid=", 4)) {
+			ret = parse_pid_t_list(token + 4, new_pid_list, &new_pid_count);
+			if (ret < 0)
+				goto out_free;
 		} else {
 			ret = -EINVAL;
 			goto out_free;
@@ -1010,6 +1083,14 @@ static ssize_t page_owner_write(struct file *file,
 	state->print_mode = new_print_mode;
 	state->nid_filter = new_nid_filter;
 	state->nid_filter_enabled = new_nid_filter_enabled;
+	state->proc_filter_enabled = new_pid_count > 0;
+	if (new_pid_count > 0) {
+		memcpy(state->pid_list, new_pid_list, sizeof(state->pid_list));
+		state->pid_count = new_pid_count;
+	}
+	if (state->pid_count > 1)
+		sort(state->pid_list, state->pid_count, sizeof(pid_t),
+		     cmp_pid_t, NULL);
 
 	ret = count;
 
-- 
2.20.1



^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH 2/8] mm/page_owner: Add TGID filtering support
  2026-08-28  3:13 [PATCH 0/8] mm/page_owner: Add PID/TGID/COMM and cgroup filtering Zhen Ni
  2026-08-28  3:13 ` [PATCH 1/8] mm/page_owner: Add PID filtering support Zhen Ni
@ 2026-08-28  3:13 ` Zhen Ni
  2026-08-28  3:13 ` [PATCH 3/8] mm/page_owner: Add COMM filtering with wildcard support Zhen Ni
                   ` (6 subsequent siblings)
  8 siblings, 0 replies; 11+ messages in thread
From: Zhen Ni @ 2026-08-28  3:13 UTC (permalink / raw)
  To: Andrew Morton
  Cc: David Hildenbrand, Lorenzo Stoakes, Liam R . Howlett,
	Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
	Jonathan Corbet, Shuah Khan, Randy Dunlap, Brendan Jackman,
	Johannes Weiner, Zi Yan, linux-mm, linux-doc, linux-kernel,
	Zhen Ni

Extend filter to support thread group ID (TGID) filtering alongside
PID filtering. Reuses existing PID parsing and binary search
infrastructure with separate TGID list and count fields.

Signed-off-by: Zhen Ni <zhen.ni@easystack.cn>
---
 mm/page_owner.c | 43 ++++++++++++++++++++++++++++++++++++-------
 1 file changed, 36 insertions(+), 7 deletions(-)

diff --git a/mm/page_owner.c b/mm/page_owner.c
index 33b9e12a019b..077867e9e572 100644
--- a/mm/page_owner.c
+++ b/mm/page_owner.c
@@ -71,6 +71,7 @@ static const char * const page_owner_print_mode_strings[] = {
 /* PID_MAX_LIMIT = 4,194,304 (7 decimal digits) */
 #define PID_MAX_DIGITS  	7
 #define MAX_FILTER_PIDS 	16
+#define MAX_FILTER_TGIDS	16
 
 struct page_owner_filter_state {
 	enum page_owner_print_mode print_mode;
@@ -78,7 +79,9 @@ struct page_owner_filter_state {
 	bool proc_filter_enabled;
 	nodemask_t nid_filter;
 	int pid_count;
+	int tgid_count;
 	pid_t pid_list[MAX_FILTER_PIDS];
+	pid_t tgid_list[MAX_FILTER_TGIDS];
 };
 
 static int cmp_pid_t(const void *a, const void *b)
@@ -837,11 +840,19 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
 		if (state->proc_filter_enabled) {
 			bool proc_match = false;
 
-			proc_match = bsearch(&page_owner->pid,
-					    state->pid_list,
-					    state->pid_count,
-					    sizeof(pid_t),
-					    cmp_pid_t) != NULL;
+			if (state->pid_count > 0)
+				proc_match = bsearch(&page_owner->pid,
+						    state->pid_list,
+						    state->pid_count,
+						    sizeof(pid_t),
+						    cmp_pid_t) != NULL;
+
+			if (!proc_match && state->tgid_count > 0)
+				proc_match = bsearch(&page_owner->tgid,
+						    state->tgid_list,
+						    state->tgid_count,
+						    sizeof(pid_t),
+						    cmp_pid_t) != NULL;
 
 			if (!proc_match)
 				goto ext_put_continue;
@@ -1010,7 +1021,9 @@ static ssize_t page_owner_write(struct file *file,
 	bool new_nid_filter_enabled;
 	bool new_proc_filter_enabled;
 	pid_t new_pid_list[MAX_FILTER_PIDS];
+	pid_t new_tgid_list[MAX_FILTER_TGIDS];
 	int new_pid_count = 0;
+	int new_tgid_count = 0;
 
 	/*
 	 * Maximum input length for filter commands:
@@ -1021,7 +1034,8 @@ static ssize_t page_owner_write(struct file *file,
 	 * - For list filters: (digit+comma) * count + prefix
 	 */
 	if (count > 32 + 6 * MAX_NUMNODES +
-			(PID_MAX_DIGITS + 1) * MAX_FILTER_PIDS + 4)
+			(PID_MAX_DIGITS + 1) * MAX_FILTER_PIDS + 4 +
+			(PID_MAX_DIGITS + 1) * MAX_FILTER_TGIDS + 5)
 		return -EINVAL;
 
 	kbuf = memdup_user_nul(buf, count);
@@ -1038,6 +1052,10 @@ static ssize_t page_owner_write(struct file *file,
 		memcpy(new_pid_list, state->pid_list, sizeof(state->pid_list));
 		new_pid_count = state->pid_count;
 	}
+	if (state->tgid_count > 0) {
+		memcpy(new_tgid_list, state->tgid_list, sizeof(state->tgid_list));
+		new_tgid_count = state->tgid_count;
+	}
 
 	while ((token = strsep(&kbuf, " \t\n")) != NULL) {
 		if (*token == '\0')
@@ -1073,6 +1091,10 @@ static ssize_t page_owner_write(struct file *file,
 			ret = parse_pid_t_list(token + 4, new_pid_list, &new_pid_count);
 			if (ret < 0)
 				goto out_free;
+		} else if (!strncmp(token, "tgid=", 5)) {
+			ret = parse_pid_t_list(token + 5, new_tgid_list, &new_tgid_count);
+			if (ret < 0)
+				goto out_free;
 		} else {
 			ret = -EINVAL;
 			goto out_free;
@@ -1083,7 +1105,7 @@ static ssize_t page_owner_write(struct file *file,
 	state->print_mode = new_print_mode;
 	state->nid_filter = new_nid_filter;
 	state->nid_filter_enabled = new_nid_filter_enabled;
-	state->proc_filter_enabled = new_pid_count > 0;
+	state->proc_filter_enabled = new_pid_count > 0 || new_tgid_count > 0;
 	if (new_pid_count > 0) {
 		memcpy(state->pid_list, new_pid_list, sizeof(state->pid_list));
 		state->pid_count = new_pid_count;
@@ -1091,6 +1113,13 @@ static ssize_t page_owner_write(struct file *file,
 	if (state->pid_count > 1)
 		sort(state->pid_list, state->pid_count, sizeof(pid_t),
 		     cmp_pid_t, NULL);
+	if (new_tgid_count > 0) {
+		memcpy(state->tgid_list, new_tgid_list, sizeof(state->tgid_list));
+		state->tgid_count = new_tgid_count;
+	}
+	if (state->tgid_count > 1)
+		sort(state->tgid_list, state->tgid_count, sizeof(pid_t),
+		     cmp_pid_t, NULL);
 
 	ret = count;
 
-- 
2.20.1



^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH 3/8] mm/page_owner: Add COMM filtering with wildcard support
  2026-08-28  3:13 [PATCH 0/8] mm/page_owner: Add PID/TGID/COMM and cgroup filtering Zhen Ni
  2026-08-28  3:13 ` [PATCH 1/8] mm/page_owner: Add PID filtering support Zhen Ni
  2026-08-28  3:13 ` [PATCH 2/8] mm/page_owner: Add TGID " Zhen Ni
@ 2026-08-28  3:13 ` Zhen Ni
  2026-08-28  3:13 ` [PATCH 4/8] mm/page_owner: Refactor memcg handling for cgroup filter support Zhen Ni
                   ` (5 subsequent siblings)
  8 siblings, 0 replies; 11+ messages in thread
From: Zhen Ni @ 2026-08-28  3:13 UTC (permalink / raw)
  To: Andrew Morton
  Cc: David Hildenbrand, Lorenzo Stoakes, Liam R . Howlett,
	Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
	Jonathan Corbet, Shuah Khan, Randy Dunlap, Brendan Jackman,
	Johannes Weiner, Zi Yan, linux-mm, linux-doc, linux-kernel,
	Zhen Ni

Add process name (COMM) filtering to page_owner with glob-style wildcard
pattern matching support. Users can now filter page_owner output by
process names using flexible patterns.

Supported wildcards:
  *    : matches any sequence of characters
  ?    : matches any single character
  [abc]: matches any character in the set
  [a-z]: matches any character in the range

Examples:
  comm=python*  : matches python, python3, python3.9, etc.
  comm=*sh      : matches bash, zsh, dash, etc.

Signed-off-by: Zhen Ni <zhen.ni@easystack.cn>
---
 mm/page_owner.c | 82 +++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 79 insertions(+), 3 deletions(-)

diff --git a/mm/page_owner.c b/mm/page_owner.c
index 077867e9e572..7cd533682054 100644
--- a/mm/page_owner.c
+++ b/mm/page_owner.c
@@ -14,6 +14,7 @@
 #include <linux/sched/clock.h>
 #include <linux/bsearch.h>
 #include <linux/sort.h>
+#include <linux/glob.h>
 
 #include "page_alloc.h"
 
@@ -72,6 +73,7 @@ static const char * const page_owner_print_mode_strings[] = {
 #define PID_MAX_DIGITS  	7
 #define MAX_FILTER_PIDS 	16
 #define MAX_FILTER_TGIDS	16
+#define MAX_FILTER_COMMS	8
 
 struct page_owner_filter_state {
 	enum page_owner_print_mode print_mode;
@@ -80,8 +82,10 @@ struct page_owner_filter_state {
 	nodemask_t nid_filter;
 	int pid_count;
 	int tgid_count;
+	int comm_count;
 	pid_t pid_list[MAX_FILTER_PIDS];
 	pid_t tgid_list[MAX_FILTER_TGIDS];
+	char comm_list[MAX_FILTER_COMMS][TASK_COMM_LEN];
 };
 
 static int cmp_pid_t(const void *a, const void *b)
@@ -854,6 +858,20 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
 						    sizeof(pid_t),
 						    cmp_pid_t) != NULL;
 
+			if (!proc_match && state->comm_count > 0) {
+				bool comm_match = false;
+				int i;
+
+				for (i = 0; i < state->comm_count; i++) {
+					if (glob_match(state->comm_list[i],
+						       page_owner->comm)) {
+						comm_match = true;
+						break;
+					}
+				}
+				proc_match = comm_match;
+			}
+
 			if (!proc_match)
 				goto ext_put_continue;
 		}
@@ -1007,6 +1025,38 @@ static int parse_pid_t_list(const char *str, pid_t *list, int *count)
 	return ret;
 }
 
+static int parse_comm_list(const char *str, char (*list)[TASK_COMM_LEN], int *count)
+{
+	char *str_copy, *token;
+	int i = 0;
+	int ret = 0;
+
+	str_copy = kstrdup(str, GFP_KERNEL);
+	if (!str_copy)
+		return -ENOMEM;
+
+	while ((token = strsep(&str_copy, ",")) != NULL) {
+		token = strstrip(token);
+		if (*token == '\0')
+			continue;
+		if (i >= MAX_FILTER_COMMS) {
+			ret = -E2BIG;
+			goto out_free;
+		}
+		strscpy(list[i], token, TASK_COMM_LEN);
+		i++;
+	}
+
+	*count = i;
+
+	if (i == 0)
+		ret = -EINVAL;
+
+out_free:
+	kfree(str_copy);
+	return ret;
+}
+
 static ssize_t page_owner_write(struct file *file,
 				 const char __user *buf,
 				 size_t count, loff_t *ppos)
@@ -1022,8 +1072,10 @@ static ssize_t page_owner_write(struct file *file,
 	bool new_proc_filter_enabled;
 	pid_t new_pid_list[MAX_FILTER_PIDS];
 	pid_t new_tgid_list[MAX_FILTER_TGIDS];
+	char (*new_comm_list)[TASK_COMM_LEN] = NULL;
 	int new_pid_count = 0;
 	int new_tgid_count = 0;
+	int new_comm_count = 0;
 
 	/*
 	 * Maximum input length for filter commands:
@@ -1035,12 +1087,19 @@ static ssize_t page_owner_write(struct file *file,
 	 */
 	if (count > 32 + 6 * MAX_NUMNODES +
 			(PID_MAX_DIGITS + 1) * MAX_FILTER_PIDS + 4 +
-			(PID_MAX_DIGITS + 1) * MAX_FILTER_TGIDS + 5)
+			(PID_MAX_DIGITS + 1) * MAX_FILTER_TGIDS + 5 +
+			TASK_COMM_LEN * MAX_FILTER_COMMS + 5)
 		return -EINVAL;
 
+	new_comm_list = kmalloc_array(MAX_FILTER_COMMS, TASK_COMM_LEN, GFP_KERNEL);
+	if (!new_comm_list)
+		return -ENOMEM;
+
 	kbuf = memdup_user_nul(buf, count);
-	if (IS_ERR(kbuf))
+	if (IS_ERR(kbuf)) {
+		kfree(new_comm_list);
 		return PTR_ERR(kbuf);
+	}
 
 	orig = kbuf;
 
@@ -1056,6 +1115,11 @@ static ssize_t page_owner_write(struct file *file,
 		memcpy(new_tgid_list, state->tgid_list, sizeof(state->tgid_list));
 		new_tgid_count = state->tgid_count;
 	}
+	if (state->comm_count > 0) {
+		memcpy(new_comm_list, state->comm_list,
+				state->comm_count * TASK_COMM_LEN);
+		new_comm_count = state->comm_count;
+	}
 
 	while ((token = strsep(&kbuf, " \t\n")) != NULL) {
 		if (*token == '\0')
@@ -1095,6 +1159,10 @@ static ssize_t page_owner_write(struct file *file,
 			ret = parse_pid_t_list(token + 5, new_tgid_list, &new_tgid_count);
 			if (ret < 0)
 				goto out_free;
+		} else if (!strncmp(token, "comm=", 5)) {
+			ret = parse_comm_list(token + 5, new_comm_list, &new_comm_count);
+			if (ret < 0)
+				goto out_free;
 		} else {
 			ret = -EINVAL;
 			goto out_free;
@@ -1105,7 +1173,9 @@ static ssize_t page_owner_write(struct file *file,
 	state->print_mode = new_print_mode;
 	state->nid_filter = new_nid_filter;
 	state->nid_filter_enabled = new_nid_filter_enabled;
-	state->proc_filter_enabled = new_pid_count > 0 || new_tgid_count > 0;
+	state->proc_filter_enabled = new_pid_count > 0 ||
+				     new_tgid_count > 0 ||
+				     new_comm_count > 0;
 	if (new_pid_count > 0) {
 		memcpy(state->pid_list, new_pid_list, sizeof(state->pid_list));
 		state->pid_count = new_pid_count;
@@ -1120,10 +1190,16 @@ static ssize_t page_owner_write(struct file *file,
 	if (state->tgid_count > 1)
 		sort(state->tgid_list, state->tgid_count, sizeof(pid_t),
 		     cmp_pid_t, NULL);
+	if (new_comm_count > 0) {
+		memcpy(state->comm_list, new_comm_list,
+				new_comm_count * TASK_COMM_LEN);
+		state->comm_count = new_comm_count;
+	}
 
 	ret = count;
 
 out_free:
+	kfree(new_comm_list);
 	kfree(orig);
 	return ret;
 }
-- 
2.20.1



^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH 4/8] mm/page_owner: Refactor memcg handling for cgroup filter support
  2026-08-28  3:13 [PATCH 0/8] mm/page_owner: Add PID/TGID/COMM and cgroup filtering Zhen Ni
                   ` (2 preceding siblings ...)
  2026-08-28  3:13 ` [PATCH 3/8] mm/page_owner: Add COMM filtering with wildcard support Zhen Ni
@ 2026-08-28  3:13 ` Zhen Ni
  2026-08-28  3:13 ` [PATCH 5/8] mm/page_owner: Add memcg " Zhen Ni
                   ` (4 subsequent siblings)
  8 siblings, 0 replies; 11+ messages in thread
From: Zhen Ni @ 2026-08-28  3:13 UTC (permalink / raw)
  To: Andrew Morton
  Cc: David Hildenbrand, Lorenzo Stoakes, Liam R . Howlett,
	Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
	Jonathan Corbet, Shuah Khan, Randy Dunlap, Brendan Jackman,
	Johannes Weiner, Zi Yan, linux-mm, linux-doc, linux-kernel,
	Zhen Ni

Extract memcg information retrieval from printing logic to prepare for
cgroup filtering support. Introduce struct memcg_info to hold cgroup
data that can be reused for both display output and filtering
decisions.

No functional change - output behavior unchanged.

Signed-off-by: Zhen Ni <zhen.ni@easystack.cn>
---
 mm/page_owner.c | 62 +++++++++++++++++++++++++++++++++++--------------
 1 file changed, 44 insertions(+), 18 deletions(-)

diff --git a/mm/page_owner.c b/mm/page_owner.c
index 7cd533682054..09cfad10a7df 100644
--- a/mm/page_owner.c
+++ b/mm/page_owner.c
@@ -69,6 +69,13 @@ static const char * const page_owner_print_mode_strings[] = {
 	[PAGE_OWNER_PRINT_STACK_HANDLE]	= "stack_handle",
 };
 
+struct memcg_info {
+	char name[80];
+	bool is_slab;
+	bool is_objcg;
+	bool is_online;
+};
+
 /* PID_MAX_LIMIT = 4,194,304 (7 decimal digits) */
 #define PID_MAX_DIGITS  	7
 #define MAX_FILTER_PIDS 	16
@@ -573,16 +580,13 @@ void pagetypeinfo_showmixedcount_print(struct seq_file *m,
 
 #ifdef CONFIG_MEMCG
 /*
- * Looking for memcg information and print it out
+ * Get memcg information from page
  */
-static inline int print_page_owner_memcg(char *kbuf, size_t count, int ret,
-					 struct page *page)
+static void get_page_memcg_info(struct page *page, struct memcg_info *info)
 {
 	unsigned long memcg_data;
 	struct obj_cgroup *objcg;
 	struct mem_cgroup *memcg;
-	bool online;
-	char name[80];
 
 	rcu_read_lock();
 	memcg_data = READ_ONCE(page->memcg_data);
@@ -590,8 +594,7 @@ static inline int print_page_owner_memcg(char *kbuf, size_t count, int ret,
 		goto out_unlock;
 
 	if (memcg_data & MEMCG_DATA_OBJEXTS) {
-		ret += scnprintf(kbuf + ret, count - ret,
-				"Slab cache page\n");
+		info->is_slab = true;
 		goto out_unlock;
 	}
 
@@ -600,21 +603,38 @@ static inline int print_page_owner_memcg(char *kbuf, size_t count, int ret,
 	if (!memcg)
 		goto out_unlock;
 
-	online = css_is_online(&memcg->css);
-	cgroup_name(memcg->css.cgroup, name, sizeof(name));
-	ret += scnprintf(kbuf + ret, count - ret,
-			"Charged %sto %smemcg %s\n",
-			(memcg_data & MEMCG_DATA_KMEM) ? "(via objcg) " : "",
-			online ? "" : "offline ",
-			name);
+	info->is_objcg = (memcg_data & MEMCG_DATA_KMEM) != 0;
+	info->is_online = css_is_online(&memcg->css);
+	cgroup_name(memcg->css.cgroup, info->name, sizeof(info->name));
 out_unlock:
 	rcu_read_unlock();
+}
+
+/*
+ * Print memcg information from memcg_info
+ */
+static inline int print_page_owner_memcg(char *kbuf, size_t count, int ret,
+					 const struct memcg_info *info)
+{
+	if (!info)
+		return ret;
+
+	if (info->is_slab)
+		ret += scnprintf(kbuf + ret, count - ret,
+				"Slab cache page\n");
+
+	if (info->name[0])
+		ret += scnprintf(kbuf + ret, count - ret,
+				"Charged %sto %smemcg %s\n",
+				info->is_objcg ? "(via objcg) " : "",
+				info->is_online ? "" : "offline ",
+				info->name);
 
 	return ret;
 }
 #else
 static inline int print_page_owner_memcg(char *kbuf, size_t count, int ret,
-					 struct page *page)
+					 const struct memcg_info *info)
 {
 	return ret;
 }
@@ -624,7 +644,8 @@ 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,
-		struct page_owner_filter_state *state)
+		struct page_owner_filter_state *state,
+		const struct memcg_info *memcg_info)
 {
 	int ret, pageblock_mt, page_mt;
 	char *kbuf;
@@ -674,7 +695,7 @@ print_page_owner(char __user *buf, size_t count, unsigned long pfn,
 			migrate_reason_names[page_owner->last_migrate_reason]);
 	}
 
-	ret = print_page_owner_memcg(kbuf, count, ret, page);
+	ret = print_page_owner_memcg(kbuf, count, ret, memcg_info);
 
 	ret += snprintf(kbuf + ret, count - ret, "\n");
 	if (ret >= count)
@@ -777,6 +798,7 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
 		 * user through copy_to_user() or GFP_KERNEL allocations.
 		 */
 		struct page_owner page_owner_tmp;
+		struct memcg_info memcg_info = {};
 
 		/*
 		 * If the new page is in a new MAX_ORDER_NR_PAGES area,
@@ -876,13 +898,17 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
 				goto ext_put_continue;
 		}
 
+#ifdef CONFIG_MEMCG
+		get_page_memcg_info(page, &memcg_info);
+#endif
+
 		/* Record the next PFN to read in the file offset */
 		*ppos = pfn + 1;
 
 		page_owner_tmp = *page_owner;
 		page_ext_put(page_ext);
 		return print_page_owner(buf, count, pfn, page,
-				&page_owner_tmp, handle, state);
+				&page_owner_tmp, handle, state, &memcg_info);
 ext_put_continue:
 		page_ext_put(page_ext);
 		cond_resched();
-- 
2.20.1



^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH 5/8] mm/page_owner: Add memcg filter support
  2026-08-28  3:13 [PATCH 0/8] mm/page_owner: Add PID/TGID/COMM and cgroup filtering Zhen Ni
                   ` (3 preceding siblings ...)
  2026-08-28  3:13 ` [PATCH 4/8] mm/page_owner: Refactor memcg handling for cgroup filter support Zhen Ni
@ 2026-08-28  3:13 ` Zhen Ni
  2026-08-28  3:13 ` [PATCH 6/8] tools/mm: Add PID/TGID/COMM filtering support to page_owner_filter Zhen Ni
                   ` (3 subsequent siblings)
  8 siblings, 0 replies; 11+ messages in thread
From: Zhen Ni @ 2026-08-28  3:13 UTC (permalink / raw)
  To: Andrew Morton
  Cc: David Hildenbrand, Lorenzo Stoakes, Liam R . Howlett,
	Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
	Jonathan Corbet, Shuah Khan, Randy Dunlap, Brendan Jackman,
	Johannes Weiner, Zi Yan, linux-mm, linux-doc, linux-kernel,
	Zhen Ni

Add memory cgroup filtering to page_owner to allow filtering pages by
their memcg path. This helps debug memory usage patterns for specific
cgroups. Users can now filter page_owner output to show only pages
belonging to a particular memory cgroup.

Collect cgroup path in memcg_info using cgroup_path() and store the
filter state in page_owner_filter_state. When the user sets memcg filter
via "memcg=<path>" command, compare each page's cgroup path against
the specified path and skip non-matching pages using strcmp.

Signed-off-by: Zhen Ni <zhen.ni@easystack.cn>
---
 mm/page_owner.c | 79 +++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 74 insertions(+), 5 deletions(-)

diff --git a/mm/page_owner.c b/mm/page_owner.c
index 09cfad10a7df..21b82e927eaf 100644
--- a/mm/page_owner.c
+++ b/mm/page_owner.c
@@ -71,6 +71,7 @@ static const char * const page_owner_print_mode_strings[] = {
 
 struct memcg_info {
 	char name[80];
+	char *path;
 	bool is_slab;
 	bool is_objcg;
 	bool is_online;
@@ -86,6 +87,7 @@ struct page_owner_filter_state {
 	enum page_owner_print_mode print_mode;
 	bool nid_filter_enabled;
 	bool proc_filter_enabled;
+	bool memcg_filter_enabled;
 	nodemask_t nid_filter;
 	int pid_count;
 	int tgid_count;
@@ -93,6 +95,7 @@ struct page_owner_filter_state {
 	pid_t pid_list[MAX_FILTER_PIDS];
 	pid_t tgid_list[MAX_FILTER_TGIDS];
 	char comm_list[MAX_FILTER_COMMS][TASK_COMM_LEN];
+	char *memcg_path;
 };
 
 static int cmp_pid_t(const void *a, const void *b)
@@ -582,12 +585,21 @@ void pagetypeinfo_showmixedcount_print(struct seq_file *m,
 /*
  * Get memcg information from page
  */
-static void get_page_memcg_info(struct page *page, struct memcg_info *info)
+static void get_page_memcg_info(struct page *page, struct memcg_info *info,
+				 bool need_path)
 {
 	unsigned long memcg_data;
 	struct obj_cgroup *objcg;
 	struct mem_cgroup *memcg;
 
+	if (need_path) {
+		info->path = kzalloc(PATH_MAX, GFP_KERNEL);
+		if (!info->path)
+			return;
+	} else {
+		info->path = NULL;
+	}
+
 	rcu_read_lock();
 	memcg_data = READ_ONCE(page->memcg_data);
 	if (!memcg_data || PageTail(page))
@@ -606,8 +618,14 @@ static void get_page_memcg_info(struct page *page, struct memcg_info *info)
 	info->is_objcg = (memcg_data & MEMCG_DATA_KMEM) != 0;
 	info->is_online = css_is_online(&memcg->css);
 	cgroup_name(memcg->css.cgroup, info->name, sizeof(info->name));
+	if (need_path)
+		cgroup_path(memcg->css.cgroup, info->path, PATH_MAX);
 out_unlock:
 	rcu_read_unlock();
+	if (info->path && !info->path[0]) {
+		kfree(info->path);
+		info->path = NULL;
+	}
 }
 
 /*
@@ -776,6 +794,7 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
 	struct page_owner *page_owner;
 	depot_stack_handle_t handle;
 	struct page_owner_filter_state *state = file->private_data;
+	ssize_t ret;
 
 	if (!static_branch_unlikely(&page_owner_inited))
 		return -EINVAL;
@@ -899,7 +918,12 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
 		}
 
 #ifdef CONFIG_MEMCG
-		get_page_memcg_info(page, &memcg_info);
+		get_page_memcg_info(page, &memcg_info,
+				    state->memcg_filter_enabled);
+		if (state->memcg_filter_enabled)
+			if (!memcg_info.path ||
+			    strcmp(memcg_info.path, state->memcg_path) != 0)
+				goto ext_put_continue;
 #endif
 
 		/* Record the next PFN to read in the file offset */
@@ -907,9 +931,12 @@ 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,
+		ret = print_page_owner(buf, count, pfn, page,
 				&page_owner_tmp, handle, state, &memcg_info);
+		kfree(memcg_info.path);
+		return ret;
 ext_put_continue:
+		kfree(memcg_info.path);
 		page_ext_put(page_ext);
 		cond_resched();
 	}
@@ -1016,7 +1043,10 @@ static int page_owner_open(struct inode *inode, struct file *file)
 
 static int page_owner_release(struct inode *inode, struct file *file)
 {
-	kfree(file->private_data);
+	struct page_owner_filter_state *state = file->private_data;
+
+	kfree(state->memcg_path);
+	kfree(state);
 	return 0;
 }
 
@@ -1096,9 +1126,11 @@ static ssize_t page_owner_write(struct file *file,
 	nodemask_t new_nid_filter;
 	bool new_nid_filter_enabled;
 	bool new_proc_filter_enabled;
+	bool new_memcg_filter_enabled;
 	pid_t new_pid_list[MAX_FILTER_PIDS];
 	pid_t new_tgid_list[MAX_FILTER_TGIDS];
 	char (*new_comm_list)[TASK_COMM_LEN] = NULL;
+	char *new_memcg_path;
 	int new_pid_count = 0;
 	int new_tgid_count = 0;
 	int new_comm_count = 0;
@@ -1114,16 +1146,24 @@ static ssize_t page_owner_write(struct file *file,
 	if (count > 32 + 6 * MAX_NUMNODES +
 			(PID_MAX_DIGITS + 1) * MAX_FILTER_PIDS + 4 +
 			(PID_MAX_DIGITS + 1) * MAX_FILTER_TGIDS + 5 +
-			TASK_COMM_LEN * MAX_FILTER_COMMS + 5)
+			TASK_COMM_LEN * MAX_FILTER_COMMS + 5 +
+			PATH_MAX + 6)
 		return -EINVAL;
 
 	new_comm_list = kmalloc_array(MAX_FILTER_COMMS, TASK_COMM_LEN, GFP_KERNEL);
 	if (!new_comm_list)
 		return -ENOMEM;
 
+	new_memcg_path = kmalloc(PATH_MAX, GFP_KERNEL);
+	if (!new_memcg_path) {
+		kfree(new_comm_list);
+		return -ENOMEM;
+	}
+
 	kbuf = memdup_user_nul(buf, count);
 	if (IS_ERR(kbuf)) {
 		kfree(new_comm_list);
+		kfree(new_memcg_path);
 		return PTR_ERR(kbuf);
 	}
 
@@ -1146,6 +1186,9 @@ static ssize_t page_owner_write(struct file *file,
 				state->comm_count * TASK_COMM_LEN);
 		new_comm_count = state->comm_count;
 	}
+	new_memcg_filter_enabled = state->memcg_filter_enabled;
+	if (state->memcg_filter_enabled && state->memcg_path)
+		strscpy(new_memcg_path, state->memcg_path, PATH_MAX);
 
 	while ((token = strsep(&kbuf, " \t\n")) != NULL) {
 		if (*token == '\0')
@@ -1189,12 +1232,33 @@ static ssize_t page_owner_write(struct file *file,
 			ret = parse_comm_list(token + 5, new_comm_list, &new_comm_count);
 			if (ret < 0)
 				goto out_free;
+		} else if (!strncmp(token, "memcg=", 6)) {
+			if (token[6] == '\0') {
+				ret = -EINVAL;
+				goto out_free;
+			}
+			ret = strscpy(new_memcg_path, token + 6, PATH_MAX);
+			if (ret < 0)
+				goto out_free;
+			new_memcg_filter_enabled = true;
 		} else {
 			ret = -EINVAL;
 			goto out_free;
 		}
 	}
 
+	if (new_memcg_filter_enabled) {
+		if (!state->memcg_path) {
+			state->memcg_path = kzalloc(PATH_MAX, GFP_KERNEL);
+			if (!state->memcg_path) {
+				ret = -ENOMEM;
+				goto out_free;
+			}
+		} else {
+			memset(state->memcg_path, 0, PATH_MAX);
+		}
+	}
+
 	/* Commit all filter changes */
 	state->print_mode = new_print_mode;
 	state->nid_filter = new_nid_filter;
@@ -1221,11 +1285,16 @@ static ssize_t page_owner_write(struct file *file,
 				new_comm_count * TASK_COMM_LEN);
 		state->comm_count = new_comm_count;
 	}
+	if (new_memcg_filter_enabled) {
+		strscpy(state->memcg_path, new_memcg_path, PATH_MAX);
+		state->memcg_filter_enabled = true;
+	}
 
 	ret = count;
 
 out_free:
 	kfree(new_comm_list);
+	kfree(new_memcg_path);
 	kfree(orig);
 	return ret;
 }
-- 
2.20.1



^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH 6/8] tools/mm: Add PID/TGID/COMM filtering support to page_owner_filter
  2026-08-28  3:13 [PATCH 0/8] mm/page_owner: Add PID/TGID/COMM and cgroup filtering Zhen Ni
                   ` (4 preceding siblings ...)
  2026-08-28  3:13 ` [PATCH 5/8] mm/page_owner: Add memcg " Zhen Ni
@ 2026-08-28  3:13 ` Zhen Ni
  2026-08-28  3:13 ` [PATCH 7/8] tools/mm: Add memory cgroup " Zhen Ni
                   ` (2 subsequent siblings)
  8 siblings, 0 replies; 11+ messages in thread
From: Zhen Ni @ 2026-08-28  3:13 UTC (permalink / raw)
  To: Andrew Morton
  Cc: David Hildenbrand, Lorenzo Stoakes, Liam R . Howlett,
	Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
	Jonathan Corbet, Shuah Khan, Randy Dunlap, Brendan Jackman,
	Johannes Weiner, Zi Yan, linux-mm, linux-doc, linux-kernel,
	Zhen Ni

Add command-line options for filtering by process ID (PID), thread group
ID (TGID), and process name (COMM) to the page_owner_filter userspace tool.

New options:
  -p, --pid PID_LIST    : Process IDs (comma-separated, max 16)
  -t, --tgid TGID_LIST  : Thread Group IDs (comma-separated, max 16)
  -c, --comm COMM_LIST  : Process names (comma-separated, max 8)
                         Supports wildcards: * ? [a-z]

Usage examples:
  page_owner_filter -p 1234,5678
  page_owner_filter -c "python*"
  page_owner_filter -n 0 -c kworker* -o output.txt

Signed-off-by: Zhen Ni <zhen.ni@easystack.cn>
---
 tools/mm/page_owner_filter.c | 168 ++++++++++++++++++++++++++++++-----
 1 file changed, 147 insertions(+), 21 deletions(-)

diff --git a/tools/mm/page_owner_filter.c b/tools/mm/page_owner_filter.c
index 1d1f0a38678a..9256ffad4939 100644
--- a/tools/mm/page_owner_filter.c
+++ b/tools/mm/page_owner_filter.c
@@ -21,22 +21,24 @@
 #include <signal.h>
 
 #define MAX_CMD_LEN	512
+#define TASK_COMM_LEN	16
 
 static void usage(const char *prog)
 {
 	fprintf(stderr, "Usage: %s [OPTIONS]\n", prog);
 	fprintf(stderr, "\nOptions:\n");
-	fprintf(stderr, "  -m, --mode MODE      : print_mode (stack, handle, or stack_handle)\n");
-	fprintf(stderr, "  -n, --nid NID_LIST   : NUMA node IDs (comma-separated or ranges)\n");
-	fprintf(stderr, "  -o, --output FILE    : output file (default: stdout)\n");
-	fprintf(stderr, "  -h, --help           : show this help message\n");
+	fprintf(stderr, "  -m, --mode MODE       : print_mode (stack, handle, stack_handle)\n");
+	fprintf(stderr, "  -n, --nid NID_LIST    : NUMA nodes (comma-separated or ranges)\n");
+	fprintf(stderr, "  -p, --pid PID_LIST    : Process IDs (comma-separated, max 16)\n");
+	fprintf(stderr, "  -t, --tgid TGID_LIST  : Thread Group IDs (comma-separated, max 16)\n");
+	fprintf(stderr, "  -c, --comm COMM_LIST  : Process names (comma-separated, max 8)\n");
+	fprintf(stderr, "                          Supports wildcards: * ? [a-z]\n");
+	fprintf(stderr, "  -o, --output FILE     : output file (default: stdout)\n");
+	fprintf(stderr, "  -h, --help            : show this help message\n");
 	fprintf(stderr, "\nExamples:\n");
-	fprintf(stderr, "  %s -m stack\n", prog);
-	fprintf(stderr, "  %s -m handle\n", prog);
-	fprintf(stderr, "  %s -m stack_handle\n", prog);
-	fprintf(stderr, "  %s -m stack -o output.txt\n", prog);
-	fprintf(stderr, "  %s -n 0,1,2\n", prog);
-	fprintf(stderr, "  %s -m stack -n 0\n", prog);
+	fprintf(stderr, "  %s -m handle -o output.txt\n", prog);
+	fprintf(stderr, "  %s -n 0,1 -c bash\n", prog);
+	fprintf(stderr, "  %s -c \"python*\" -t 1\n", prog);
 }
 
 static int validate_mode(const char *mode)
@@ -132,6 +134,93 @@ static int validate_nid_list(const char *nid_list)
 	return 0;
 }
 
+static int validate_pid_list(const char *pid_list)
+{
+	const char *p;
+	int count = 0;
+
+	if (!pid_list || strlen(pid_list) == 0)
+		return -1;
+
+	for (p = pid_list; *p; p++) {
+		if (*p == ',') {
+			count++;
+			continue;
+		}
+		if (!isdigit((unsigned char)*p)) {
+			fprintf(stderr,
+				"Error: Invalid character '%c' in pid_list (only digits allowed)\n",
+				*p);
+			return -1;
+		}
+	}
+
+	if (++count > 16) {
+		fprintf(stderr, "Error: Too many PIDs (max 16)\n");
+		return -1;
+	}
+
+	return 0;
+}
+
+static int validate_tgid_list(const char *tgid_list)
+{
+	return validate_pid_list(tgid_list);
+}
+
+static int validate_comm_list(const char *comm_list)
+{
+	const char *p;
+	const char *comm_start;
+	int count = 0;
+	int comm_len = 0;
+
+	if (!comm_list || strlen(comm_list) == 0)
+		return -1;
+
+	comm_start = comm_list;
+	for (p = comm_list; *p; p++) {
+		if (*p == ',') {
+			/* Check COMM length before separator */
+			if (comm_len == 0) {
+				fprintf(stderr, "Error: Empty COMM in list\n");
+				return -1;
+			}
+			if (comm_len >= TASK_COMM_LEN) {
+				fprintf(stderr,
+					"Error: COMM too long (max %d chars)\n",
+					TASK_COMM_LEN - 1);
+				fprintf(stderr, "  Near: %.15s...\n", comm_start);
+				return -1;
+			}
+			count++;
+			comm_len = 0;
+			comm_start = p + 1;
+			continue;
+		}
+		comm_len++;
+	}
+
+	/* Check last COMM */
+	if (comm_len == 0) {
+		fprintf(stderr, "Error: Empty COMM at end of list\n");
+		return -1;
+	}
+	if (comm_len >= TASK_COMM_LEN) {
+		fprintf(stderr, "Error: COMM too long (max %d chars)\n",
+			TASK_COMM_LEN - 1);
+		fprintf(stderr, "  Near: %.15s...\n", comm_start);
+		return -1;
+	}
+
+	if (++count > 8) {
+		fprintf(stderr, "Error: Too many COMMs (max 8)\n");
+		return -1;
+	}
+
+	return 0;
+}
+
 int main(int argc, char *argv[])
 {
 	const char *output_file = NULL;
@@ -148,6 +237,9 @@ int main(int argc, char *argv[])
 	static struct option long_options[] = {
 		{"mode",	required_argument, 0, 'm'},
 		{"nid",		required_argument, 0, 'n'},
+		{"pid",		required_argument, 0, 'p'},
+		{"tgid",	required_argument, 0, 't'},
+		{"comm",	required_argument, 0, 'c'},
 		{"output",	required_argument, 0, 'o'},
 		{"help",	no_argument,	   0, 'h'},
 		{0, 0, 0, 0}
@@ -174,7 +266,7 @@ int main(int argc, char *argv[])
 		return 1;
 	}
 
-	while ((opt = getopt_long(argc, argv, "m:n:o:h", long_options, NULL)) != -1) {
+	while ((opt = getopt_long(argc, argv, "m:n:p:t:c:o:h", long_options, NULL)) != -1) {
 		int len;
 
 		switch (opt) {
@@ -206,6 +298,48 @@ int main(int argc, char *argv[])
 			cmd_len += len;
 			break;
 		}
+		case 'p': {
+			const char *pid_list = optarg;
+
+			if (validate_pid_list(pid_list) < 0)
+				return 1;
+			len = snprintf(filter_cmd + cmd_len, MAX_CMD_LEN - cmd_len,
+				       "%spid=%s", cmd_len > 0 ? " " : "", pid_list);
+			if (len < 0 || cmd_len + len >= MAX_CMD_LEN) {
+				fprintf(stderr, "Error: Command too long\n");
+				return 1;
+			}
+			cmd_len += len;
+			break;
+		}
+		case 't': {
+			const char *tgid_list = optarg;
+
+			if (validate_tgid_list(tgid_list) < 0)
+				return 1;
+			len = snprintf(filter_cmd + cmd_len, MAX_CMD_LEN - cmd_len,
+				       "%stgid=%s", cmd_len > 0 ? " " : "", tgid_list);
+			if (len < 0 || cmd_len + len >= MAX_CMD_LEN) {
+				fprintf(stderr, "Error: Command too long\n");
+				return 1;
+			}
+			cmd_len += len;
+			break;
+		}
+		case 'c': {
+			const char *comm_list = optarg;
+
+			if (validate_comm_list(comm_list) < 0)
+				return 1;
+			len = snprintf(filter_cmd + cmd_len, MAX_CMD_LEN - cmd_len,
+				       "%scomm=%s", cmd_len > 0 ? " " : "", comm_list);
+			if (len < 0 || cmd_len + len >= MAX_CMD_LEN) {
+				fprintf(stderr, "Error: Command too long\n");
+				return 1;
+			}
+			cmd_len += len;
+			break;
+		}
 		case 'o':
 			output_file = optarg;
 			break;
@@ -220,7 +354,7 @@ int main(int argc, char *argv[])
 
 	/* At least one filter must be specified */
 	if (cmd_len == 0) {
-		fprintf(stderr, "Error: At least one filter (-m or -n) must be specified\n\n");
+		fprintf(stderr, "Error: At least one filter must be specified\n\n");
 		usage(argv[0]);
 		return 1;
 	}
@@ -255,15 +389,7 @@ int main(int argc, char *argv[])
 	ret = write(fd, filter_cmd, strlen(filter_cmd));
 
 	if (ret < 0) {
-		if (errno == EINVAL) {
-			fprintf(stderr, "Error: Kernel rejected the filter command.\n");
-			fprintf(stderr, "Possible causes:\n");
-			fprintf(stderr, "  - Kernel does not support per-fd filtering\n");
-			fprintf(stderr, "  - NUMA node has no memory\n");
-			fprintf(stderr, "  - Unknown reason\n");
-		} else {
-			perror("write filter command");
-		}
+		perror("write filter command");
 		goto out;
 	}
 
-- 
2.20.1



^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH 7/8] tools/mm: Add memory cgroup filtering support to page_owner_filter
  2026-08-28  3:13 [PATCH 0/8] mm/page_owner: Add PID/TGID/COMM and cgroup filtering Zhen Ni
                   ` (5 preceding siblings ...)
  2026-08-28  3:13 ` [PATCH 6/8] tools/mm: Add PID/TGID/COMM filtering support to page_owner_filter Zhen Ni
@ 2026-08-28  3:13 ` Zhen Ni
  2026-08-28  3:13 ` [PATCH 8/8] Documentation: page_owner: Document PID/TGID/COMM and cgroup filters Zhen Ni
  2026-08-28  7:03 ` [PATCH 0/8] mm/page_owner: Add PID/TGID/COMM and cgroup filtering Lorenzo Stoakes (ARM)
  8 siblings, 0 replies; 11+ messages in thread
From: Zhen Ni @ 2026-08-28  3:13 UTC (permalink / raw)
  To: Andrew Morton
  Cc: David Hildenbrand, Lorenzo Stoakes, Liam R . Howlett,
	Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
	Jonathan Corbet, Shuah Khan, Randy Dunlap, Brendan Jackman,
	Johannes Weiner, Zi Yan, linux-mm, linux-doc, linux-kernel,
	Zhen Ni

Add filtering capability for page_owner to allow filtering by
memory cgroup path.

Filter page_owner output by cgroup path:
./page_owner_filter -g /
./page_owner_filter -g /user.slice
./page_owner_filter -g /user.slice -c systemd

Signed-off-by: Zhen Ni <zhen.ni@easystack.cn>
---
 tools/mm/page_owner_filter.c | 56 ++++++++++++++++++++++++++++++++++--
 1 file changed, 53 insertions(+), 3 deletions(-)

diff --git a/tools/mm/page_owner_filter.c b/tools/mm/page_owner_filter.c
index 9256ffad4939..1ffbe4387854 100644
--- a/tools/mm/page_owner_filter.c
+++ b/tools/mm/page_owner_filter.c
@@ -20,7 +20,7 @@
 #include <getopt.h>
 #include <signal.h>
 
-#define MAX_CMD_LEN	512
+#define MAX_CMD_LEN	2048
 #define TASK_COMM_LEN	16
 
 static void usage(const char *prog)
@@ -33,12 +33,13 @@ static void usage(const char *prog)
 	fprintf(stderr, "  -t, --tgid TGID_LIST  : Thread Group IDs (comma-separated, max 16)\n");
 	fprintf(stderr, "  -c, --comm COMM_LIST  : Process names (comma-separated, max 8)\n");
 	fprintf(stderr, "                          Supports wildcards: * ? [a-z]\n");
+	fprintf(stderr, "  -g, --cgroup PATH     : Memory cgroup path\n");
 	fprintf(stderr, "  -o, --output FILE     : output file (default: stdout)\n");
 	fprintf(stderr, "  -h, --help            : show this help message\n");
 	fprintf(stderr, "\nExamples:\n");
 	fprintf(stderr, "  %s -m handle -o output.txt\n", prog);
 	fprintf(stderr, "  %s -n 0,1 -c bash\n", prog);
-	fprintf(stderr, "  %s -c \"python*\" -t 1\n", prog);
+	fprintf(stderr, "  %s -c \"python*\" -g user.slice\n", prog);
 }
 
 static int validate_mode(const char *mode)
@@ -221,6 +222,38 @@ static int validate_comm_list(const char *comm_list)
 	return 0;
 }
 
+static int validate_cgroup_path(const char *path)
+{
+	char cgroup_path[512];
+	const char *input_path = path;
+	int is_cgroup_v2 = 0;
+
+	if (!path || strlen(path) == 0)
+		return -1;
+
+	if (path[0] == '/')
+		input_path++;
+
+	/* Check if v1 memory controller exists */
+	if (access("/sys/fs/cgroup/memory", F_OK) != 0)
+		is_cgroup_v2 = 1;
+
+	if (is_cgroup_v2)
+		snprintf(cgroup_path, sizeof(cgroup_path),
+			"/sys/fs/cgroup/%s/memory.stat", input_path);
+	else
+		snprintf(cgroup_path, sizeof(cgroup_path),
+			"/sys/fs/cgroup/memory/%s/memory.stat", input_path);
+
+	if (access(cgroup_path, F_OK) != 0) {
+		fprintf(stderr, "Error: Cgroup path '%s': not found or no memory controller\n",
+			path);
+		return -1;
+	}
+
+	return 0;
+}
+
 int main(int argc, char *argv[])
 {
 	const char *output_file = NULL;
@@ -240,6 +273,7 @@ int main(int argc, char *argv[])
 		{"pid",		required_argument, 0, 'p'},
 		{"tgid",	required_argument, 0, 't'},
 		{"comm",	required_argument, 0, 'c'},
+		{"cgroup",	required_argument, 0, 'g'},
 		{"output",	required_argument, 0, 'o'},
 		{"help",	no_argument,	   0, 'h'},
 		{0, 0, 0, 0}
@@ -266,7 +300,7 @@ int main(int argc, char *argv[])
 		return 1;
 	}
 
-	while ((opt = getopt_long(argc, argv, "m:n:p:t:c:o:h", long_options, NULL)) != -1) {
+	while ((opt = getopt_long(argc, argv, "m:n:p:t:c:g:o:h", long_options, NULL)) != -1) {
 		int len;
 
 		switch (opt) {
@@ -340,6 +374,22 @@ int main(int argc, char *argv[])
 			cmd_len += len;
 			break;
 		}
+		case 'g': {
+			const char *cgroup_path = optarg;
+
+			if (validate_cgroup_path(cgroup_path) < 0)
+				return 1;
+			const char *path = (cgroup_path[0] == '/') ? cgroup_path + 1 : cgroup_path;
+
+			len = snprintf(filter_cmd + cmd_len, MAX_CMD_LEN - cmd_len,
+				       "%smemcg=/%s", cmd_len > 0 ? " " : "", path);
+			if (len < 0 || cmd_len + len >= MAX_CMD_LEN) {
+				fprintf(stderr, "Error: Command too long\n");
+				return 1;
+			}
+			cmd_len += len;
+			break;
+		}
 		case 'o':
 			output_file = optarg;
 			break;
-- 
2.20.1



^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH 8/8] Documentation: page_owner: Document PID/TGID/COMM and cgroup filters
  2026-08-28  3:13 [PATCH 0/8] mm/page_owner: Add PID/TGID/COMM and cgroup filtering Zhen Ni
                   ` (6 preceding siblings ...)
  2026-08-28  3:13 ` [PATCH 7/8] tools/mm: Add memory cgroup " Zhen Ni
@ 2026-08-28  3:13 ` Zhen Ni
  2026-08-28  7:03 ` [PATCH 0/8] mm/page_owner: Add PID/TGID/COMM and cgroup filtering Lorenzo Stoakes (ARM)
  8 siblings, 0 replies; 11+ messages in thread
From: Zhen Ni @ 2026-08-28  3:13 UTC (permalink / raw)
  To: Andrew Morton
  Cc: David Hildenbrand, Lorenzo Stoakes, Liam R . Howlett,
	Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
	Jonathan Corbet, Shuah Khan, Randy Dunlap, Brendan Jackman,
	Johannes Weiner, Zi Yan, linux-mm, linux-doc, linux-kernel,
	Zhen Ni

Update page_owner.rst to document process and cgroup filtering
support in page_owner_filter tool. Add usage examples for PID, TGID,
COMM (with wildcard support), and cgroup filters along with their
respective limits.

Signed-off-by: Zhen Ni <zhen.ni@easystack.cn>
---
 Documentation/mm/page_owner.rst | 25 ++++++++++++++++++++++++-
 1 file changed, 24 insertions(+), 1 deletion(-)

diff --git a/Documentation/mm/page_owner.rst b/Documentation/mm/page_owner.rst
index a6bd3fe6423a..c8a7e6c242c5 100644
--- a/Documentation/mm/page_owner.rst
+++ b/Documentation/mm/page_owner.rst
@@ -283,7 +283,7 @@ page_owner supports filtering output at the kernel level before reading,
 which reduces the amount of data that needs to be processed in userspace.
 
 The page_owner_filter tool provides a convenient interface for this filtering
-capability. It supports two types of filters:
+capability. It supports the following types of filters:
 
 1. **print_mode filter**: Control what information is printed for each page
 	- ``stack``: Print full stack traces (default, compatible with existing usage)
@@ -300,6 +300,16 @@ capability. It supports two types of filters:
 	- Ranges: ``-n 0-3``
 	- Mixed format: ``-n 0,2-3,5``
 
+3. **Process filters**: Filter pages by process identifiers
+	- Filter by process ID: ``-p PID_LIST`` (comma-separated, max 16)
+	- Filter by thread group ID: ``-t TGID_LIST`` (comma-separated, max 16)
+	- Filter by task command name: ``-c COMM_LIST`` (comma-separated, max 8)
+	- Name matching supports wildcards: ``*``, ``?``, ``[a-z]``
+
+4. **Cgroup (memcg) filter**: Filter pages by memory cgroup
+	- Filter by cgroup path: ``-g CGROUP``
+	- Useful for containerized environments and multi-tenant systems
+
 Usage examples::
 
 	# Filter by print mode
@@ -310,9 +320,19 @@ Usage examples::
 	./page_owner_filter -n 0
 	./page_owner_filter -n 0-3
 
+	# Filter by process
+	./page_owner_filter -p 1234
+	./page_owner_filter -t 1,2,3
+	./page_owner_filter -c python*
+
+	# Filter by cgroup
+	./page_owner_filter -g system.slice
+	./page_owner_filter -g kubepods/besteffort/pod123
+
 	# Combined filters
 	./page_owner_filter -m stack -n 0,1,2
 	./page_owner_filter -m handle -n 0,2-3
+	./page_owner_filter -g user.slice -c bash -n 0
 
 	# Save to file
 	./page_owner_filter -m handle -o filtered_output.txt
@@ -323,6 +343,9 @@ reduce output size by ~66% (84MB vs 244MB) and improve read performance by ~4.4x
 compared to full stack output.
 
 The NUMA node filter is useful for NUMA-aware memory allocation analysis and debugging.
+Process filters help isolate memory allocations for specific processes or tasks.
+The cgroup filter is essential for containerized environments where you need to
+analyze memory usage per container or service.
 
 Behind the scenes, page_owner_filter opens /sys/kernel/debug/page_owner and
 writes filter commands before reading the filtered output. The filtering uses
-- 
2.20.1



^ permalink raw reply related	[flat|nested] 11+ messages in thread

* Re: [PATCH 0/8] mm/page_owner: Add PID/TGID/COMM and cgroup filtering
  2026-08-28  3:13 [PATCH 0/8] mm/page_owner: Add PID/TGID/COMM and cgroup filtering Zhen Ni
                   ` (7 preceding siblings ...)
  2026-08-28  3:13 ` [PATCH 8/8] Documentation: page_owner: Document PID/TGID/COMM and cgroup filters Zhen Ni
@ 2026-08-28  7:03 ` Lorenzo Stoakes (ARM)
  2026-08-28 18:36   ` Andrew Morton
  8 siblings, 1 reply; 11+ messages in thread
From: Lorenzo Stoakes (ARM) @ 2026-08-28  7:03 UTC (permalink / raw)
  To: Zhen Ni
  Cc: Andrew Morton, David Hildenbrand, Liam R . Howlett,
	Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
	Jonathan Corbet, Shuah Khan, Randy Dunlap, Brendan Jackman,
	Johannes Weiner, Zi Yan, linux-mm, linux-doc, linux-kernel

On Fri, Aug 28, 2026 at 11:13:31AM +0800, Zhen Ni wrote:
> This patch series adds process and memory cgroup filtering support to
> page_owner. Following the previous series that introduced print_mode and
> NUMA node filters:
>   https://lore.kernel.org/linux-mm/20260707115411.1714314-1-zhen.ni@easystack.cn/
>
> This series adds filtering capabilities to page_owner, allowing users to
> filter output by specific processes and memory cgroups. Users can now
> filter page_owner output by PID, TGID, COMM (with wildcard support), and
> memory cgroup path. This makes page_owner debugging more focused and
> efficient for tracking memory allocations in specific contexts.

The majority of this cover letter feels like it should have been
documentation put somewhere :)

I mean I like to be verbose, but this is slightly excessive :P

Remember that mm places cover letters in the first commit in merged
series...

Andrew - could we not do that here, or at least heavily truncate it?

[...]

>    Test: Cgroup path exceeding PATH_MAX
>    Generated path length: 4101
>    ./page_owner_filter -g '<very long path>'
>    Error: Cgroup path '/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa': not found or no memory controller

Also let's maybe refrain from >4000 char 'aaa...[rgh]'s, I think people get
the picture without needing all the chars :)

--
Cheers, Lorenzo


^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH 0/8] mm/page_owner: Add PID/TGID/COMM and cgroup filtering
  2026-08-28  7:03 ` [PATCH 0/8] mm/page_owner: Add PID/TGID/COMM and cgroup filtering Lorenzo Stoakes (ARM)
@ 2026-08-28 18:36   ` Andrew Morton
  0 siblings, 0 replies; 11+ messages in thread
From: Andrew Morton @ 2026-08-28 18:36 UTC (permalink / raw)
  To: Lorenzo Stoakes (ARM)
  Cc: Zhen Ni, David Hildenbrand, Liam R . Howlett, Vlastimil Babka,
	Mike Rapoport, Suren Baghdasaryan, Michal Hocko, Jonathan Corbet,
	Shuah Khan, Randy Dunlap, Brendan Jackman, Johannes Weiner,
	Zi Yan, linux-mm, linux-doc, linux-kernel

On Fri, 28 Aug 2026 08:03:38 +0100 "Lorenzo Stoakes (ARM)" <ljs@kernel.org> wrote:

> On Fri, Aug 28, 2026 at 11:13:31AM +0800, Zhen Ni wrote:
> > This patch series adds process and memory cgroup filtering support to
> > page_owner. Following the previous series that introduced print_mode and
> > NUMA node filters:
> >   https://lore.kernel.org/linux-mm/20260707115411.1714314-1-zhen.ni@easystack.cn/
> >
> > This series adds filtering capabilities to page_owner, allowing users to
> > filter output by specific processes and memory cgroups. Users can now
> > filter page_owner output by PID, TGID, COMM (with wildcard support), and
> > memory cgroup path. This makes page_owner debugging more focused and
> > efficient for tracking memory allocations in specific contexts.
> 
> The majority of this cover letter feels like it should have been
> documentation put somewhere :)

yes please.

The only thing longer than the cover letter is the Sashiko report ;)

	https://sashiko.dev/#/patchset/20260828031339.1270699-1-zhen.ni@easystack.cn


^ permalink raw reply	[flat|nested] 11+ messages in thread

end of thread, other threads:[~2026-08-28 18:37 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-28  3:13 [PATCH 0/8] mm/page_owner: Add PID/TGID/COMM and cgroup filtering Zhen Ni
2026-08-28  3:13 ` [PATCH 1/8] mm/page_owner: Add PID filtering support Zhen Ni
2026-08-28  3:13 ` [PATCH 2/8] mm/page_owner: Add TGID " Zhen Ni
2026-08-28  3:13 ` [PATCH 3/8] mm/page_owner: Add COMM filtering with wildcard support Zhen Ni
2026-08-28  3:13 ` [PATCH 4/8] mm/page_owner: Refactor memcg handling for cgroup filter support Zhen Ni
2026-08-28  3:13 ` [PATCH 5/8] mm/page_owner: Add memcg " Zhen Ni
2026-08-28  3:13 ` [PATCH 6/8] tools/mm: Add PID/TGID/COMM filtering support to page_owner_filter Zhen Ni
2026-08-28  3:13 ` [PATCH 7/8] tools/mm: Add memory cgroup " Zhen Ni
2026-08-28  3:13 ` [PATCH 8/8] Documentation: page_owner: Document PID/TGID/COMM and cgroup filters Zhen Ni
2026-08-28  7:03 ` [PATCH 0/8] mm/page_owner: Add PID/TGID/COMM and cgroup filtering Lorenzo Stoakes (ARM)
2026-08-28 18:36   ` Andrew Morton

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox