FS-DAX registers persistent-memory ranges as ZONE_DEVICE memory, and the
kernel normally allocates and initializes vmemmap storage for every
advertised PFN up front. Sparse pmem images and workloads that only use the
DAX direct-access path may never need writable per-PFN state for most of
that range, but still pay the memory and initialization cost.

Add an opt-in dev_pagemap mode that populates FS-DAX vmemmap PTEs from a
shared read-only metadata page. The shared page is initialized with the
common ZONE_DEVICE and dev_pagemap state, so every PFN still has a valid
struct page representation while private metadata allocation is deferred.

This relies on sizeof(struct page) being a power of two, so each vmemmap
page contains a naturally aligned and repeatable set of struct page slots.
It also requires architecture support for runtime vmemmap remapping,
because shared mappings must be replaced with private writable pages before
a PFN can enter userspace mappings.

The initial implementation is deliberately limited to a single
memory-block-aligned range. That is not a fundamental requirement, but keeps
the registration and teardown paths simple; support for multiple ranges or
less strict alignment can be added later.

Provide vmemmap_materialize_page() to replace shared mappings in the
requested metadata range with private writable copies. A later patch will
call it from the FS-DAX fault path.

No caller enables the mode yet.

Signed-off-by: Muchun Song <[email protected]>
---
 include/linux/memremap.h | 11 ++++++-
 mm/memremap.c            | 38 ++++++++++++++++++++++--
 mm/mm_init.c             | 11 +++++++
 mm/sparse-vmemmap.c      | 64 +++++++++++++++++++++++++++++++++++++---
 4 files changed, 116 insertions(+), 8 deletions(-)

diff --git a/include/linux/memremap.h b/include/linux/memremap.h
index e3c2ccf872a8..21c9b6aeef67 100644
--- a/include/linux/memremap.h
+++ b/include/linux/memremap.h
@@ -9,6 +9,7 @@
 
 struct resource;
 struct device;
+struct page;
 
 /**
  * struct vmem_altmap - pre-allocated storage for vmemmap_populate
@@ -108,7 +109,8 @@ struct dev_pagemap_ops {
        void (*folio_split)(struct folio *head, struct folio *tail);
 };
 
-#define PGMAP_ALTMAP_VALID     (1 << 0)
+#define PGMAP_ALTMAP_VALID                             BIT(0)
+#define PGMAP_VMEMMAP_OPTIMIZATION                     BIT(1)
 
 /**
  * struct dev_pagemap - metadata for ZONE_DEVICE mappings
@@ -122,6 +124,7 @@ struct dev_pagemap_ops {
  *     A zero value (default) uses base pages as the vmemmap metadata
  *     representation. A bigger value will set up compound struct pages
  *     of the requested order value.
+ * @vmemmap_shared_page: shared read-only vmemmap page for optimized FS-DAX
  * @ops: method table
  * @owner: an opaque pointer identifying the entity that manages this
  *     instance.  Used by various helpers to make sure that no
@@ -137,6 +140,7 @@ struct dev_pagemap {
        enum memory_type type;
        unsigned int flags;
        unsigned long vmemmap_shift;
+       struct page *vmemmap_shared_page;
        const struct dev_pagemap_ops *ops;
        void *owner;
        int nr_range;
@@ -232,6 +236,7 @@ void *devm_memremap_pages(struct device *dev, struct 
dev_pagemap *pgmap);
 void devm_memunmap_pages(struct device *dev, struct dev_pagemap *pgmap);
 struct dev_pagemap *get_dev_pagemap(unsigned long pfn);
 bool pgmap_pfn_valid(struct dev_pagemap *pgmap, unsigned long pfn);
+int vmemmap_materialize_page(struct page *page, unsigned int order);
 
 unsigned long memremap_compat_align(void);
 
@@ -307,4 +312,8 @@ static inline void put_dev_pagemap(struct dev_pagemap 
*pgmap)
                percpu_ref_put(&pgmap->ref);
 }
 
+static inline bool pgmap_vmemmap_optimizable(struct dev_pagemap *pgmap)
+{
+       return pgmap && pgmap->vmemmap_shared_page != NULL;
+}
 #endif /* _LINUX_MEMREMAP_H_ */
diff --git a/mm/memremap.c b/mm/memremap.c
index accba23aef28..a53d09b84eaa 100644
--- a/mm/memremap.c
+++ b/mm/memremap.c
@@ -3,6 +3,7 @@
 #include <linux/device.h>
 #include <linux/io.h>
 #include <linux/kasan.h>
+#include <linux/memory.h>
 #include <linux/memory_hotplug.h>
 #include <linux/memremap.h>
 #include <linux/swap.h>
@@ -83,6 +84,30 @@ static unsigned long pfn_len(struct dev_pagemap *pgmap, 
unsigned long range_id)
                pfn_first(pgmap, range_id)) >> pgmap->vmemmap_shift;
 }
 
+static int pgmap_vmemmap_shared_page_alloc(struct dev_pagemap *pgmap, int nid)
+{
+       const struct range *range = &pgmap->range;
+
+       if (!is_power_of_2(sizeof(struct page)) ||
+           !IS_ENABLED(CONFIG_ARCH_SUPPORTS_VMEMMAP_REMAP) ||
+           !(pgmap->flags & PGMAP_VMEMMAP_OPTIMIZATION))
+               return 0;
+
+       if (pgmap->nr_range != 1 ||
+           !IS_ALIGNED(range->start | range_len(range), MIN_MEMORY_BLOCK_SIZE))
+               return 0;
+
+       pgmap->vmemmap_shared_page = alloc_pages_node(nid, GFP_KERNEL, 0);
+
+       return pgmap->vmemmap_shared_page ? 0 : -ENOMEM;
+}
+
+static inline void pgmap_vmemmap_shared_page_free(struct dev_pagemap *pgmap)
+{
+       if (pgmap->vmemmap_shared_page)
+               put_page(pgmap->vmemmap_shared_page);
+}
+
 static void pageunmap_range(struct dev_pagemap *pgmap, int range_id)
 {
        struct range *range = &pgmap->ranges[range_id];
@@ -93,8 +118,9 @@ static void pageunmap_range(struct dev_pagemap *pgmap, int 
range_id)
 
        /* pages are dead and unused, undo the arch mapping */
        mem_hotplug_begin();
-       remove_pfn_range_from_zone(page_zone(first_page), 
PHYS_PFN(range->start),
-                                  PHYS_PFN(range_len(range)));
+       if (!pgmap_vmemmap_optimizable(pgmap))
+               remove_pfn_range_from_zone(page_zone(first_page), 
PHYS_PFN(range->start),
+                                          PHYS_PFN(range_len(range)));
        if (pgmap->type == MEMORY_DEVICE_PRIVATE) {
                __remove_pages(PHYS_PFN(range->start),
                               PHYS_PFN(range_len(range)), NULL, pgmap);
@@ -123,6 +149,7 @@ void memunmap_pages(struct dev_pagemap *pgmap)
 
        for (i = 0; i < pgmap->nr_range; i++)
                pageunmap_range(pgmap, i);
+       pgmap_vmemmap_shared_page_free(pgmap);
        percpu_ref_exit(&pgmap->ref);
 
        WARN_ONCE(pgmap->altmap.alloc, "failed to free all reserved pages\n");
@@ -310,6 +337,9 @@ void *memremap_pages(struct dev_pagemap *pgmap, int nid)
                break;
        case MEMORY_DEVICE_FS_DAX:
                params.pgprot = pgprot_decrypted(params.pgprot);
+               error = pgmap_vmemmap_shared_page_alloc(pgmap, nid);
+               if (error)
+                       return ERR_PTR(error);
                break;
        case MEMORY_DEVICE_GENERIC:
                break;
@@ -324,8 +354,10 @@ void *memremap_pages(struct dev_pagemap *pgmap, int nid)
        init_completion(&pgmap->done);
        error = percpu_ref_init(&pgmap->ref, dev_pagemap_percpu_release, 0,
                                GFP_KERNEL);
-       if (error)
+       if (error) {
+               pgmap_vmemmap_shared_page_free(pgmap);
                return ERR_PTR(error);
+       }
 
        /*
         * Clear the pgmap nr_range as it will be incremented for each
diff --git a/mm/mm_init.c b/mm/mm_init.c
index 2ed17cc707ed..7dd03b8a8d28 100644
--- a/mm/mm_init.c
+++ b/mm/mm_init.c
@@ -33,6 +33,7 @@
 #include <linux/vmstat.h>
 #include <linux/kexec_handover.h>
 #include <linux/hugetlb.h>
+#include <linux/memremap.h>
 #include "internal.h"
 #include "mm_init.h"
 #include "page_alloc.h"
@@ -1133,6 +1134,15 @@ void __ref memmap_init_zone_device(struct zone *zone,
        if (!nr_pages)
                return;
 
+       if (pgmap_vmemmap_optimizable(pgmap)) {
+               struct page *page = page_address(pgmap->vmemmap_shared_page);
+
+               for (int i = 0; i < PAGE_SIZE / sizeof(struct page); i++)
+                       __init_zone_device_page(page + i, start_pfn + i,
+                                               ZONE_DEVICE, nid, pgmap);
+               goto pageblock_init;
+       }
+
        /*
         * Seed the reusable head-page template from the first real struct
         * page. The normal page-init and refcount helpers must operate on
@@ -1164,6 +1174,7 @@ void __ref memmap_init_zone_device(struct zone *zone,
                                     compound_nr_pages(pfn, altmap, pgmap));
        }
 
+pageblock_init:
        pageblock_migratetype_init_range(start_pfn, nr_pages, MIGRATE_MOVABLE,
                                         /* isolate */ false, /* atomic */ 
false);
 
diff --git a/mm/sparse-vmemmap.c b/mm/sparse-vmemmap.c
index e62e6aa07f12..4dc7f020ed10 100644
--- a/mm/sparse-vmemmap.c
+++ b/mm/sparse-vmemmap.c
@@ -37,6 +37,8 @@
  */
 /* Get a ref on the head page struct page, for ZONE_DEVICE compound pages */
 #define VMEMMAP_POPULATE_PAGEREF       0x0001
+/* Read-only shared vmemmap mappings for FS-DAX base pages */
+#define VMEMMAP_POPULATE_FSDAX_SHARED  0x0002
 
 #include "internal.h"
 #include "mm_init.h"
@@ -262,10 +264,11 @@ static pte_t * __meminit vmemmap_pte_populate(pmd_t *pmd, 
unsigned long addr, in
                         * and through vmemmap_populate_compound_pages() when
                         * slab is available.
                         */
-                       if (flags & VMEMMAP_POPULATE_PAGEREF)
+                       if (flags & (VMEMMAP_POPULATE_PAGEREF | 
VMEMMAP_POPULATE_FSDAX_SHARED))
                                get_page(pfn_to_page(ptpfn));
                }
-               entry = pfn_pte(ptpfn, PAGE_KERNEL);
+               entry = pfn_pte(ptpfn, flags & VMEMMAP_POPULATE_FSDAX_SHARED ?
+                               PAGE_KERNEL_RO : PAGE_KERNEL);
                set_pte_at(&init_mm, addr, pte, entry);
        } else if (WARN_ON_ONCE(vmemmap_optimizable_pfn(pfn)))
                return NULL;
@@ -379,6 +382,54 @@ int __meminit vmemmap_populate_basepages(unsigned long 
start, unsigned long end,
        return vmemmap_populate_range(start, end, node, altmap, -1, 0);
 }
 
+#ifdef CONFIG_ZONE_DEVICE
+static int __vmemmap_materialize_page(struct page *page)
+{
+       unsigned long addr = PAGE_ALIGN_DOWN((unsigned long)page);
+       struct dev_pagemap *pgmap = page_pgmap(page);
+       struct page *candidate, *template = pgmap->vmemmap_shared_page;
+       pte_t *pte = virt_to_kpte(addr);
+
+       if (pte_page(ptep_get(pte)) != template)
+               return 0;
+
+       candidate = alloc_pages_node(page_to_nid(page), GFP_KERNEL, 0);
+       if (!candidate)
+               return -ENOMEM;
+       copy_page(page_address(candidate), page_address(template));
+
+       spin_lock(&init_mm.page_table_lock);
+       if (pte_page(ptep_get(pte)) != template) {
+               __free_page(candidate);
+               goto out;
+       }
+       /* Make the copied struct page contents visible before the PTE update. 
*/
+       smp_wmb();
+       set_pte_at(&init_mm, addr, pte, mk_pte(candidate, PAGE_KERNEL));
+       flush_tlb_kernel_range(addr, addr + PAGE_SIZE);
+       put_page(template);
+out:
+       spin_unlock(&init_mm.page_table_lock);
+
+       return 0;
+}
+
+int vmemmap_materialize_page(struct page *page, unsigned int order)
+{
+       struct dev_pagemap *pgmap = page_pgmap(page);
+       unsigned long end = (unsigned long)(page + (1UL << order));
+
+       if (!pgmap_vmemmap_optimizable(pgmap))
+               return 0;
+
+       for (unsigned long addr = (unsigned long)page; addr < end; addr += 
PAGE_SIZE)
+               if (__vmemmap_materialize_page((struct page *)addr))
+                       return -ENOMEM;
+
+       return 0;
+}
+#endif /* CONFIG_ZONE_DEVICE */
+
 /*
  * Write protect the mirrored tail page structs for HVO. This will be
  * called from the hugetlb code when gathering and initializing the
@@ -581,7 +632,11 @@ struct page * __meminit __populate_section_memmap(unsigned 
long pfn,
                !IS_ALIGNED(nr_pages, PAGES_PER_SUBSECTION)))
                return NULL;
 
-       if (vmemmap_can_optimize(altmap, pgmap))
+       if (pgmap_vmemmap_optimizable(pgmap))
+               r = vmemmap_populate_range(start, end, nid, NULL,
+                                          
page_to_pfn(pgmap->vmemmap_shared_page),
+                                          VMEMMAP_POPULATE_FSDAX_SHARED);
+       else if (vmemmap_can_optimize(altmap, pgmap))
                r = vmemmap_populate_compound_pages(pfn, start, end, nid, 
pgmap);
        else
                r = vmemmap_populate(start, end, nid, altmap);
@@ -887,7 +942,8 @@ int __meminit sparse_add_section(int nid, unsigned long 
start_pfn,
         * Poison uninitialized struct pages in order to catch invalid flags
         * combinations.
         */
-       page_init_poison(memmap, sizeof(struct page) * nr_pages);
+       if (!pgmap_vmemmap_optimizable(pgmap))
+               page_init_poison(memmap, sizeof(struct page) * nr_pages);
 
        ms = __nr_to_section(section_nr);
        __section_mark_present(ms, section_nr);
-- 
2.54.0


Reply via email to