Re: [PATCH] mm/thp: expose deferred split folio memory usage in meminfo

From: liuye

Date: Fri Jul 17 2026 - 05:46:29 EST



在 2026/7/17 17:24, Ye Liu 写道:
>
> 在 2026/7/17 16:08, Lorenzo Stoakes (ARM) 写道:
>> +cc Johannes
>>
>> On Fri, Jul 17, 2026 at 02:30:22PM +0800, Ye Liu wrote:
>>> From: Ye Liu <liuye@xxxxxxxxxx>
>>>
>>> Folios on the deferred split list hold physical memory that is
>>> invisible in meminfo. When a THP becomes partially mapped, the
>>> unmapped pages are removed from AnonPages but remain physically
>>> allocated until the shrinker splits the folio. This creates a
>>> memory accounting gap where used memory cannot be attributed to
>>> any meminfo field.
>> Is this really that much of an issue? You're not giving any use cases here.
>>
>> What real-world use case motivated this?
>>
> I have indeed encountered this situation in a customer environment,
> but the environment is complex, and I cannot clearly explain why this memory
> black hole occurs in that scenario.
> However, I will provide an example to reproduce it below.
> Use case: The system has 100GB of memory. A user-space program requests 80GB of memory.
> For each 2MB block, only one page is retained, and the mapping of all other pages is unmapped,
> while the process continues to run.
> You'll find that in meminfo, MemFree: only 12GB remains,
> but the process actually uses very little memory.
> This creates a memory black hole situation.
> Below is the test program I used, from AI.
>

ps -p $(pgrep -d, test) -o pid,comm,%mem,rss,vsz
    PID COMMAND         %MEM   RSS    VSZ
   9358 test             0.1 165220 166032


MemTotal:       101606276 kB
MemFree:        13172092 kB
MemAvailable:   13569668 kB
Buffers:            5352 kB
Cached:          1169520 kB
SwapCached:            0 kB
Active:         84730404 kB
Inactive:        1129792 kB
Active(anon):   84730404 kB
Inactive(anon):        0 kB
Active(file):          0 kB
Inactive(file):  1129792 kB
Unevictable:        6192 kB
Mlocked:              32 kB
SwapTotal:       4194300 kB
SwapFree:        4194300 kB
Zswap:                 0 kB
Zswapped:              0 kB
Dirty:                24 kB
Writeback:             0 kB
AnonPages:        969280 kB
Mapped:           308848 kB
Shmem:             45076 kB
KReclaimable:      96144 kB
Slab:             269540 kB
SReclaimable:      96144 kB
SUnreclaim:       173396 kB
KernelStack:       27584 kB
PageTables:       187408 kB
SecPageTables:         0 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:    54997436 kB
Committed_AS:    6628776 kB
VmallocTotal:   261087232 kB
VmallocUsed:       38696 kB
VmallocChunk:          0 kB
Percpu:            42800 kB
HardwareCorrupted:     0 kB
AnonHugePages:    413696 kB
ShmemHugePages:        0 kB
ShmemPmdMapped:        0 kB
FileHugePages:    172032 kB
FilePmdMapped:     28672 kB
DeferredSplitPages: 83886080 kB
CmaTotal:         131072 kB
CmaFree:          122880 kB
Balloon:               0 kB
GPUActive:             0 kB
GPUReclaim:            0 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
Hugetlb:               0 kB



/*
 * thp_keep_one_page_per_2mb.c
 *
 * Compile: gcc -O2 -o test thp_keep_one_page_per_2mb.c
 * Run: ./thp_test
 *
 * Description:
 *   Allocate 80GB virtual address space, touch pages and record accessibility.
 *   For each 2MB block (if its first page is accessible), keep only the first
 *   4KB page and unmap all other pages within that block.
 *   The process stays alive for inspection of memory state.
 */

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <errno.h>
#include <signal.h>
#include <stdint.h>
#include <setjmp.h>
#include <stdbool.h>
#include <time.h>

#define PMD_SIZE        (2 * 1024 * 1024)               /* 2 MB */
#define PAGE_SIZE       4096                           /* 4 KB */
#define TOTAL_SIZE      (80ULL * 1024 * 1024 * 1024)  /* 80 GB */
#define NR_PAGES        (TOTAL_SIZE / PAGE_SIZE)       /* ~20.97 million pages */
#define NR_HUGEPAGES    (TOTAL_SIZE / PMD_SIZE)        /* 40960 THP blocks */

static unsigned char *base_addr;
static unsigned char *page_ok;          /* 1 = accessible, 0 = inaccessible / already unmapped */
static sigjmp_buf jmpbuf;

/* Signal handler to catch page fault errors */
static void segv_handler(int sig, siginfo_t *info, void *context)
{
    (void)sig; (void)info; (void)context;
    siglongjmp(jmpbuf, 1);
}

/* Read total AnonHugePages size (in bytes) from /proc/self/smaps */
static unsigned long read_anon_hugepages(void)
{
    FILE *fp = fopen("/proc/self/smaps", "r");
    if (!fp) return 0;
    char line[256];
    unsigned long total = 0;
    while (fgets(line, sizeof(line), fp)) {
        if (strncmp(line, "AnonHugePages:", 14) == 0) {
            unsigned long val;
            if (sscanf(line + 14, "%lu kB", &val) == 1)
                total += val * 1024;
        }
    }
    fclose(fp);
    return total;
}

int main(void)
{
    /* Install signal handlers */
    struct sigaction sa = {
        .sa_sigaction = segv_handler,
        .sa_flags = SA_SIGINFO,
    };
    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGBUS, &sa, NULL);

    printf("=== Keep only one 4KB page per 2MB THP, unmap the rest ===\n");
    printf("Allocation size: %lu MB (%lu 2MB THP blocks)\n",
           TOTAL_SIZE / (1024 * 1024), NR_HUGEPAGES);

    /* 1. Reserve virtual address space (no physical allocation yet) */
    base_addr = mmap(NULL, TOTAL_SIZE,
                     PROT_READ | PROT_WRITE,
                     MAP_PRIVATE | MAP_ANONYMOUS,
                     -1, 0);
    if (base_addr == MAP_FAILED) {
        perror("mmap failed");
        exit(EXIT_FAILURE);
    }
    printf("Virtual address base: %p\n", base_addr);

    /* 2. Hint kernel to use THP (if system policy is madvise) */
    if (madvise(base_addr, TOTAL_SIZE, MADV_HUGEPAGE) == -1)
        perror("madvise (non-fatal)");

    /* 3. Allocate page status array */
    page_ok = calloc(NR_PAGES, sizeof(unsigned char));
    if (!page_ok) {
        perror("calloc");
        exit(EXIT_FAILURE);
    }

    /* 4. Touch pages one by one, capturing faults */
    printf("Touching pages (write each page, skip on failure)...\n");
    const unsigned long print_interval = (256ULL * 1024 * 1024) / PAGE_SIZE;
    for (unsigned long i = 0; i < NR_PAGES; i++) {
        volatile unsigned char *p = base_addr + i * PAGE_SIZE;

        if (sigsetjmp(jmpbuf, 1) == 0) {
            *p = 0xAA;          /* attempt write */
            page_ok[i] = 1;     /* success */
        } else {
            page_ok[i] = 0;     /* fault – page not accessible */
        }

        if ((i + 1) % print_interval == 0) {
            printf("  Processed %lu MB\n", (i + 1) * PAGE_SIZE / (1024 * 1024));
        }
    }

    /* Count successfully mapped pages */
    unsigned long ok_pages = 0;
    for (unsigned long i = 0; i < NR_PAGES; i++)
        if (page_ok[i]) ok_pages++;
    printf("Touch complete. Successfully mapped %lu pages (%.2f MB)\n",
           ok_pages, ok_pages * PAGE_SIZE / (1024.0 * 1024));

    /* 5. THP usage before the operation */
    unsigned long huge_before = read_anon_hugepages();
    printf("AnonHugePages before operation: %.2f MB\n",
           huge_before / (1024.0 * 1024));

    /* 6. For each 2MB block: keep the first page, unmap all other pages */
    printf("\nProcessing each THP: keep first page, unmap the rest...\n");
    int success = 0, fail = 0, skipped = 0;
    for (unsigned long i = 0; i < NR_HUGEPAGES; i++) {
        unsigned long start_page = i * (PMD_SIZE / PAGE_SIZE); /* 512 pages per block */

        if (page_ok[start_page]) {
            /* First page is accessible – keep it, unmap the rest of the block */
            void *addr = base_addr + i * PMD_SIZE + PAGE_SIZE;
            size_t len = PMD_SIZE - PAGE_SIZE; /* 2MB - 4KB */

            /* Try batch munmap first (requires all pages in range to be mapped) */
            if (munmap(addr, len) == 0) {
                /* Batch succeeded; mark all remaining pages in this block as unmapped */
                for (unsigned long j = start_page + 1; j < start_page + 512; j++) {
                    page_ok[j] = 0;
                }
                success++;
            } else {
                /* Batch failed (some pages were never mapped) – fallback to per‑page unmap */
                int sub_success = 0;
                for (unsigned long j = start_page + 1; j < start_page + 512; j++) {
                    if (page_ok[j]) {
                        if (munmap(base_addr + j * PAGE_SIZE, PAGE_SIZE) == 0) {
                            page_ok[j] = 0;
                            sub_success++;
                        } else {
                            /* ignore errors, keep page_ok[j] as 1 */
                        }
                    }
                }
                if (sub_success > 0) {
                    success++;
                } else {
                    fail++;
                }
            }
        } else {
            skipped++;
        }

        if ((i + 1) % 1000 == 0) {
            printf("  Processed %lu/%lu THP (ok:%d, fail:%d, skip:%d)\n",
                   i + 1, NR_HUGEPAGES, success, fail, skipped);
        }
    }
    printf("Operation complete: success %d, fail %d, skipped %d\n", success, fail, skipped);

    /* 7. THP usage after the operation */
    unsigned long huge_after = read_anon_hugepages();
    printf("AnonHugePages after operation: %.2f MB\n",
           huge_after / (1024.0 * 1024));

    /* 8. Verification: access only the kept first page of each block */
    printf("\nVerifying kept first pages (offset 0 of each block)...\n");
    int verify_ok = 0, verify_fail = 0;
    for (unsigned long i = 0; i < NR_HUGEPAGES; i++) {
        unsigned long first_page = i * (PMD_SIZE / PAGE_SIZE);
        if (page_ok[first_page]) {
            volatile unsigned char *p = base_addr + i * PMD_SIZE;
            (void)*p;   /* read the first byte */
            verify_ok++;
        } else {
            verify_fail++;
        }
        if ((i + 1) % 1000 == 0) {
            printf("  Verified %lu/%lu THP\n", i + 1, NR_HUGEPAGES);
        }
    }
    printf("Verification complete: %d first pages accessible, %d inaccessible (skipped)\n",
           verify_ok, verify_fail);

    /* 9. Keep the process alive – all kept pages remain mapped */
    printf("\nEach THP's first page is kept; all other pages have been unmapped.\n");
    printf("Process PID = %d, staying alive (press Ctrl+C to exit).\n", getpid());
    printf("Use these commands to inspect memory state:\n");
    printf("  cat /proc/%d/smaps | grep -A20 '^7f'  # view mapping details\n", getpid());
    printf("  cat /proc/%d/smaps | grep AnonHugePages\n", getpid());

    free(page_ok);   /* release status array */

    while (1) {
        sleep(60);
        /* Optionally print current THP usage (should be near 0) */
        unsigned long cur = read_anon_hugepages();
        printf("[%ld] Current AnonHugePages: %.2f MB\n", time(NULL), cur / (1024.0 * 1024));
    }

    /* never reached */
    munmap(base_addr, TOTAL_SIZE);
    return 0;
}



>>> Add NR_DEFERRED_SPLIT_PAGES to track the total memory consumed by
>>> folios currently on the deferred_split_lru, updated via
>>> mod_node_page_state() at all enqueue/dequeue points. The new field
>>> DeferredSplitPages is visible in /proc/meminfo, /proc/vmstat, and
>>> per-node /sys/devices/system/node/node*/meminfo.
>> Again you're not justifying anything, why do you need a per-node breakdown?
>>
> Initially, the only plan was to add it to /proc/meminfo;
> the others were more of a transitional design.
>
>>> Signed-off-by: Ye Liu <liuye@xxxxxxxxxx>
>> You're updating these stats in a number of places and you've given zero
>> explanataion or evidence that you're accounting this correctly.
>>
> The counter tracks the current total pages of partially_mapped folios on deferred_split_lru.
> The six hook points:
> - Add: deferred_split_folio() when partially_mapped → the only first-enqueue path
> - Add: deferred_split_scan() requeue → net effect is zero paired with isolate's del
> - Del: deferred_split_isolate() → temporary removal for shrinker processing
> - Del: __folio_unqueue_deferred_split() → permanent removal when folio is freed
> - Del: split path → permanent removal when folio is split into sub-folios
>
>> This is very subtle code and you're a very new contributor to this so I
>> wouldn't want us to take someting like this even if it was justified
>> without a very strong argument.
> True – I’m new to the kernel code, so I’ll definitely benefit from checking in
> with you guys more regularly.
>
>> You'd probably want to do some refactoring stuff first also to combine any
>> such updates with moves.
>>
> Indeed, it would be better to integrate it with folio updates.
>>> ---
>>> drivers/base/node.c | 4 +++-
>>> fs/proc/meminfo.c | 2 ++
>>> include/linux/mmzone.h | 1 +
>>> mm/huge_memory.c | 24 +++++++++++++++++++++---
>>> mm/vmstat.c | 1 +
>>> 5 files changed, 28 insertions(+), 4 deletions(-)
>>>
>>> diff --git a/drivers/base/node.c b/drivers/base/node.c
>>> index 3da91929ad4e..69463438300e 100644
>>> --- a/drivers/base/node.c
>>> +++ b/drivers/base/node.c
>>> @@ -519,6 +519,7 @@ static ssize_t node_read_meminfo(struct device *dev,
>>> "Node %d ShmemPmdMapped: %8lu kB\n"
>>> "Node %d FileHugePages: %8lu kB\n"
>>> "Node %d FilePmdMapped: %8lu kB\n"
>>> + "Node %d DeferredSplitPages: %8lu kB\n"
>>> #endif
>>> #ifdef CONFIG_UNACCEPTED_MEMORY
>>> "Node %d Unaccepted: %8lu kB\n"
>>> @@ -553,7 +554,8 @@ static ssize_t node_read_meminfo(struct device *dev,
>>> nid, K(node_page_state(pgdat, NR_SHMEM_THPS)),
>>> nid, K(node_page_state(pgdat, NR_SHMEM_PMDMAPPED)),
>>> nid, K(node_page_state(pgdat, NR_FILE_THPS)),
>>> - nid, K(node_page_state(pgdat, NR_FILE_PMDMAPPED))
>>> + nid, K(node_page_state(pgdat, NR_FILE_PMDMAPPED)),
>>> + nid, K(node_page_state(pgdat, NR_DEFERRED_SPLIT_PAGES))
>>> #endif
>>> #ifdef CONFIG_UNACCEPTED_MEMORY
>>> ,
>>> diff --git a/fs/proc/meminfo.c b/fs/proc/meminfo.c
>>> index b2813ff13cb2..76dbf51ad1e0 100644
>>> --- a/fs/proc/meminfo.c
>>> +++ b/fs/proc/meminfo.c
>>> @@ -149,6 +149,8 @@ static int meminfo_proc_show(struct seq_file *m, void *v)
>>> global_node_page_state(NR_FILE_THPS));
>>> show_val_kb(m, "FilePmdMapped: ",
>>> global_node_page_state(NR_FILE_PMDMAPPED));
>>> + show_val_kb(m, "DeferredSplitPages: ",
>>> + global_node_page_state(NR_DEFERRED_SPLIT_PAGES));
>>> #endif
>>>
>>> #ifdef CONFIG_CMA
>>> diff --git a/include/linux/mmzone.h b/include/linux/mmzone.h
>>> index 0507193b3ae3..1e75adb73d00 100644
>>> --- a/include/linux/mmzone.h
>>> +++ b/include/linux/mmzone.h
>>> @@ -267,6 +267,7 @@ enum node_stat_item {
>>> NR_FILE_THPS,
>>> NR_FILE_PMDMAPPED,
>>> NR_ANON_THPS,
>>> + NR_DEFERRED_SPLIT_PAGES, /* THP/mTHP pages pending deferred split */
>>> NR_VMSCAN_WRITE,
>>> NR_VMSCAN_IMMEDIATE, /* Prioritise for reclaim when writeback ends */
>>> NR_DIRTIED, /* page dirtyings since bootup */
>>> diff --git a/mm/huge_memory.c b/mm/huge_memory.c
>>> index c642bec967fa..1094b844675c 100644
>>> --- a/mm/huge_memory.c
>>> +++ b/mm/huge_memory.c
>>> @@ -77,6 +77,14 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
>>> struct shrink_control *sc);
>>> static bool split_underused_thp = true;
>>>
>>> +#define deferred_split_pages_add(folio) \
>>> + mod_node_page_state(NODE_DATA(folio_nid(folio)), \
>>> + NR_DEFERRED_SPLIT_PAGES, folio_nr_pages(folio))
>>> +
>>> +#define deferred_split_pages_del(folio) \
>>> + mod_node_page_state(NODE_DATA(folio_nid(folio)), \
>>> + NR_DEFERRED_SPLIT_PAGES, -folio_nr_pages(folio))
>> Why are these macros?
>>
>> I really dislike the naming too, these read like they're actually
>> adding/deleting pages.
>>
> Rename the helpers to avoid the "add/del pages" confusion
> (e.g. mod_deferred_split_stat() or just inline mod_node_page_state).
>
>>> +
>>> static atomic_t huge_zero_refcount;
>>> struct folio *huge_zero_folio __read_mostly;
>>> unsigned long huge_zero_pfn __read_mostly = ~0UL;
>>> @@ -3913,8 +3921,10 @@ static int __folio_freeze_and_split_unmapped(struct folio *folio, unsigned int n
>>> struct lruvec *lruvec;
>>>
>>> if (dequeue_deferred) {
>>> - __list_lru_del(&deferred_split_lru, lru,
>>> - &folio->_deferred_list, folio_nid(folio));
>>> + if (__list_lru_del(&deferred_split_lru, lru,
>>> + &folio->_deferred_list, folio_nid(folio)) &&
>>> + folio_test_partially_mapped(folio))
>>> + deferred_split_pages_del(folio);
>>> if (folio_test_partially_mapped(folio)) {
>>> folio_clear_partially_mapped(folio);
>>> mod_mthp_stat(old_order,
>>> @@ -4415,6 +4425,7 @@ bool __folio_unqueue_deferred_split(struct folio *folio)
>>> lru = list_lru_lock_irqsave(&deferred_split_lru, nid, &memcg, &flags);
>>> if (__list_lru_del(&deferred_split_lru, lru, &folio->_deferred_list, nid)) {
>>> if (folio_test_partially_mapped(folio)) {
>>> + deferred_split_pages_del(folio);
>>> folio_clear_partially_mapped(folio);
>>> mod_mthp_stat(folio_order(folio),
>>> MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, -1);
>>> @@ -4473,6 +4484,8 @@ void deferred_split_folio(struct folio *folio, bool partially_mapped)
>>> VM_WARN_ON_FOLIO(folio_test_partially_mapped(folio), folio);
>>> }
>>> __list_lru_add(&deferred_split_lru, lru, &folio->_deferred_list, nid, memcg);
>>> + if (partially_mapped)
>>> + deferred_split_pages_add(folio);
>>> list_lru_unlock_irqrestore(lru, &flags);
>>> rcu_read_unlock();
>>> }
>>> @@ -4524,8 +4537,11 @@ static enum lru_status deferred_split_isolate(struct list_head *item,
>>> {
>>> struct folio *folio = container_of(item, struct folio, _deferred_list);
>>> struct list_head *freeable = cb_arg;
>>> + bool partially_mapped = folio_test_partially_mapped(folio);
>>>
>>> if (folio_try_get(folio)) {
>>> + if (partially_mapped)
>>> + deferred_split_pages_del(folio);
>>> list_lru_isolate_move(lru, item, freeable);
>>> return LRU_REMOVED;
>>> }
>>> @@ -4535,7 +4551,8 @@ static enum lru_status deferred_split_isolate(struct list_head *item,
>>> * isolate: folio_unqueue_deferred_split() checks list_empty()
>>> * locklessly, so once removed the folio can be freed any time.
>>> */
>>> - if (folio_test_partially_mapped(folio)) {
>>> + if (partially_mapped) {
>>> + deferred_split_pages_del(folio);
>>> folio_clear_partially_mapped(folio);
>>> mod_mthp_stat(folio_order(folio),
>>> MTHP_STAT_NR_ANON_PARTIALLY_MAPPED, -1);
>>> @@ -4596,6 +4613,7 @@ static unsigned long deferred_split_scan(struct shrinker *shrink,
>>> &folio->_deferred_list,
>>> folio_nid(folio),
>>> folio_memcg(folio));
>>> + deferred_split_pages_add(folio);
>>> rcu_read_unlock();
>>> }
>>> folio_put(folio);
>>> diff --git a/mm/vmstat.c b/mm/vmstat.c
>>> index 3b5cb1031f72..ba71cc3e4c30 100644
>>> --- a/mm/vmstat.c
>>> +++ b/mm/vmstat.c
>>> @@ -1248,6 +1248,7 @@ const char * const vmstat_text[] = {
>>> [I(NR_FILE_THPS)] = "nr_file_hugepages",
>>> [I(NR_FILE_PMDMAPPED)] = "nr_file_pmdmapped",
>>> [I(NR_ANON_THPS)] = "nr_anon_transparent_hugepages",
>>> + [I(NR_DEFERRED_SPLIT_PAGES)] = "nr_deferred_split_pages",
>>> [I(NR_VMSCAN_WRITE)] = "nr_vmscan_write",
>>> [I(NR_VMSCAN_IMMEDIATE)] = "nr_vmscan_immediate_reclaim",
>>> [I(NR_DIRTIED)] = "nr_dirtied",
>>> --
>>> 2.43.0
>>>
>> Cheers, Lorenzo

--
Thanks,
Ye Liu