Re: [RFC PATCH 03/11] mm, swap: add xswap cluster grow via VM_SPARSE vmalloc

Baoquan He <[email protected]>
Newsgroups gmane.linux.kernel,gmane.linux.kernel.mm
Message-ID <anLvfyS3S_uc6OYx@MiWiFi-R3L-srv>
On 07/29/26 at 11:12pm, Baoquan He wrote:
> On 07/27/26 at 08:05am, Nhat Pham wrote:
> > On Mon, Jul 27, 2026 at 7:05 AM Baoquan He <[email protected]> wrote:
...snip...
> > > +#ifdef CONFIG_XSWAP
> > > +static int xswap_map_clusters(struct swap_info_struct *si,
> > > +                             unsigned long start_idx, unsigned long nr)
> > > +{
> > > +       unsigned long start_addr = (unsigned long)si->cluster_info +
> > > +                                  (size_t)start_idx * sizeof(struct swap_cluster_info);
> > > +       unsigned long end_addr = start_addr + (size_t)nr * sizeof(struct swap_cluster_info);
> > > +       /*
> > > +        * vm_area_map_pages() requires that start and end be page-aligned.
> > > +        * If start_addr falls within a page that was already mapped by a
> > > +        * previous batch (grow path), round it up to skip the already-mapped
> > > +        * partial page.  Always round end_addr up so the vmap page table walk
> > > +        * terminates correctly (the walk loop exits when addr == end, and addr
> > > +        * advances by PAGE_SIZE each iteration).
> > > +        */
> > > +       unsigned long vm_start = PAGE_ALIGN(start_addr);
> > > +       unsigned long vm_end = PAGE_ALIGN(end_addr);
> > > +       unsigned long npages;
> > > +       struct page **pages;
> > > +       unsigned long i;
> > > +
> > > +       if (vm_start >= vm_end) {
> > > +               /* All requested clusters fall within already-mapped pages. */
> > > +               for (i = start_idx; i < start_idx + nr; i++)
> > > +                       spin_lock_init(&si->cluster_info[i].lock);
> > > +               WRITE_ONCE(si->nr_clusters_mapped, start_idx + nr);
> > > +               return 0;
> > > +       }
> > > +
> > > +       npages = (vm_end - vm_start) >> PAGE_SHIFT;
> > > +
> > > +       pages = kmalloc_array(npages, sizeof(*pages), GFP_KERNEL);
> > > +       if (!pages)
> > > +               return -ENOMEM;
> > > +
> > > +       for (i = 0; i < npages; i++) {
> > > +               /*
> > > +                * __GFP_ZERO is critical: cluster_info structs contain pointer
> > > +                * fields (extend_table, zero_bitmap, memcg_table, table) that
> > > +                * must start as NULL.  Without zeroing, stale data from a
> > > +                * previous user of the page would look like valid pointers.
> > > +                */
> > > +               pages[i] = alloc_page(GFP_KERNEL | __GFP_ZERO);
> > > +               if (!pages[i])
> > > +                       goto fail;
> > > +       }
> > > +
> > > +       if (vm_area_map_pages(si->cluster_vm, vm_start, vm_end, pages)) {
> > > +               i = npages; /* free all pages on failure */
> > > +               goto fail;
> > 
> > I was evaluating whether your vmalloc array can be extended to support
> > kernel-driven dynamic growth at least (either as a slot-in replacement
> > for xarray, or as a follow-up optimization if it's too complicated),
> > and I stumble this mapping action.
> > 
> > Seems like it does not take any GFP flag argument, and under the hood
> > it calls GFP_KERNEL. Would this be safe in the swap allocation path
> > (where you slotted it in in patch 4)? Kairui used a more precise set
> > of flags in this path, for e.g for swap table allocation:
> > 
> > ret = swap_cluster_alloc_table(ci, __GFP_HIGH | __GFP_NOMEMALLOC |
> >    GFP_KERNEL);
> > 
> > 
> > I'm guessing this has to do with the fact that we often entered
> > swapping out paths with PF_MEMALLOC... Is there any risk of deadlock
> > etc.?

Hi Chris,

I gave the kmem_cache approach a try, but ended up keeping VM_SPARSE.
The kmem_cache approach has two structural problems:

1. With individually kmem_cache_alloc()'d clusters, ci - si->cluster_info
   no longer works — cluster_info is a pointer array, not a contiguous
   array.  Each struct swap_cluster_info would need a new field to store
   its own index. VM_SPARSE preserves the contiguous array layout, so
   ci - si->cluster_info continues to work for O(1) index lookup everywhere.

2. The struct swap_cluster_info ** pointer array itself costs extra
   memory: 8 bytes per cluster.  That's 8 KB for a 1 GB swap device,
   and 8 MB for a 1 TB device — paid upfront at swapon, regardless of
   how many clusters are actually used.  VM_SPARSE needs no such
   indirection array; the cluster_info is addressed directly through
   the vmalloc area.

By comparison, the introduced change in struct swap_cluster_info isn't
that great.

How the GFP concern is addressed
--------------------------------
Hi Nhat,

You were right about the three layers of hardcoded GFP_KERNEL in v1.
In the current code (the version I'll post as RFC v2), each is fixed:

1. alloc_page() and kmalloc_array() now use:

     __GFP_HIGH | __GFP_NOMEMALLOC | GFP_KERNEL

   This matches the pattern Kairui established in
   swap_cluster_alloc_table().  __GFP_NOMEMALLOC prevents the
   pfmemalloc reserve bypass during reclaim.

2. The internal page-table allocations inside vmap_pages_range()
   use GFP_PGTABLE_KERNEL and are not directly controllable from
   the caller.  To prevent these from recursing into swap reclaim,
   the entire allocation block is wrapped with:

     noreclaim_flags = memalloc_noreclaim_save();
     ... alloc_page / kmalloc_array / vm_area_map_pages ...
     memalloc_noreclaim_restore(noreclaim_flags);

   memalloc_noreclaim_save() ensures __GFP_FS and __GFP_IO are
   cleared for all allocations within the scope, so the page-table
   allocation cannot recurse into filesystem or swap reclaim.

3. The residual risk is that the page-table allocations still lack
   __GFP_NOMEMALLOC, meaning they could theoretically dip into
   emergency reserves under PF_MEMALLOC.  However, each grow
   operation maps at most one PTE page (XSWAP_GROW_CLUSTERS
   clusters per page), so the total order-0 allocation is tiny —
   typically a single page-table page.  This seems acceptable
   compared to the complexity of plumbing a GFP parameter through
   the entire vmap_pages_range() call chain.

A variant vm_area_map_pages_gfp that passes the caller's GFP context
down to the page-table allocations. That cleanly solves the problem
for all VM_SPARSE users, not just xswap.  I'm happy to work on that
if people think it's the right direction, but I'd prefer to keep it
as a separate improvement rather than blocking this series on it.

The shrink path also avoids this problem entirely: it runs in a
workqueue context where PF_MEMALLOC is never set, so GFP_KERNEL
is correct there.

Thanks
Baoquan

> 
> Thanks for catching this.  You're right — there are actually three
> layers of hardcoded GFP_KERNEL in the VM_SPARSE path:
> 
> 1. alloc_page(GFP_KERNEL | __GFP_ZERO) in xswap_map_clusters() for the
> backing pages
> 2. vmap_pages_range() hardcodes GFP_KERNEL (passed only to KMSAN, but
> still semantically wrong)
> 3. The page-table allocations inside vmap_pages_range_noflush_walk()
> (pte_alloc_kernel_track etc.) use GFP_PGTABLE_KERNEL, which is
> GFP_KERNEL | __GFP_ZERO — also lacking __GFP_NOMEMALLOC, and
> completely invisible to the caller
> 
> When PF_MEMALLOC is set (as it is during reclaim), __gfp_pfmemalloc_flags()
> returns ALLOC_NO_WATERMARKS for any allocation without __GFP_NOMEMALLOC,
> meaning those allocations can consume all emergency reserves — including
> the reserves that the reclaim machinery itself needs to make forward
> progress.  That is a real deadlock risk.
> 
> One way is to fix them one by one. Another way is to drop the VM_SPARSE
> approach entirely and implemented Chris's suggestion instead: cluster_info
> is now a struct swap_cluster_info ** pointer array, with each cluster
> individually kmem_cache_zalloc()'d. The grow path (xswap_grow_clusters)
> now uses:
> 
> kmem_cache_zalloc(swap_cluster_cachep,
>         __GFP_HIGH | __GFP_NOMEMALLOC | GFP_KERNEL)
> matching the pattern Kairui established in swap_cluster_alloc_table().
> All allocations are fully GFP-controllable — no hidden vmap page-table
> allocations, no hardcoded GFP_PGTABLE_KERNEL.
> 
> With kmem_cache, shrink is just kmem_cache_free() — each cluster is
> independently allocated and freed. Seems the logic is simpler. If no
> objection, I will give it a shot soon.
> 
> 
> Thanks
> Baoquan
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.