Re: [RFC PATCH 09/10] mm/swapfile: implement swap_ghost_extend_max() for dynamic growth

From: Chris Li

Date: Sat Jul 11 2026 - 05:03:06 EST


On Tue, Jul 7, 2026 at 1:27 AM Baoquan He <baoquan.he@xxxxxxxxx> wrote:
>
> Allocate additional physical pages and map them at the end of the
> already-reserved sparse vmalloc area via vm_area_map_pages(). The flat
> cluster_info pointer never moves — new clusters appear contiguously at
> higher indices. New clusters are fully initialized (spinlocks, list
> heads, trailing BAD mask) before si->max is published via WRITE_ONCE.
>
> Page-pointer tracking in cluster_vm->pages is reallocated to accommodate
> additional entries. Returns -E2BIG if the requested size exceeds the
> reserved VA range. Extension is serialized by swapon_mutex.
>
> Signed-off-by: Baoquan He <baoquan.he@xxxxxxxxx>
> ---
> mm/swapfile.c | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 110 insertions(+)
>
> diff --git a/mm/swapfile.c b/mm/swapfile.c
> index da085a75d7b5..8bf336678887 100644
> --- a/mm/swapfile.c
> +++ b/mm/swapfile.c
> @@ -58,6 +58,8 @@ static void move_cluster(struct swap_info_struct *si,
> enum swap_cluster_flags new_flags);
> #ifdef CONFIG_ZSWAP
> #define GHOST_MAX_CLUSTER_BYTES (64UL * 1024 * 1024)
> +static int swap_ghost_extend_max(struct swap_info_struct *si,
> + unsigned int new_maxpages);
> #endif
>
> /*
> @@ -4247,6 +4249,114 @@ swp_entry_t ghost_redirect_to_phys(struct swap_info_struct *si, pgoff_t offset)
>
> return (swp_entry_t){};
> }
> +
> +/*
> + * swap_ghost_extend_max — Extend an active ghost swap device.
> + * @si: ghost device (SWP_GHOST set)
> + * @new: new max pages (> si->max)
> + *
> + * Allocates physical pages and maps them at the end of the reserved
> + * sparse vmalloc area. si->cluster_info never moves. New clusters
> + * are fully initialized before si->max is published via WRITE_ONCE.
> + *
> + * Returns 0, -ENOMEM, -EINVAL, or -E2BIG. Caller holds swapon_mutex.
> + */
> +static int swap_ghost_extend_max(struct swap_info_struct *si,
> + unsigned int new)
> +{
> + unsigned long old_max = si->max, old_nr, new_nr;
> + unsigned long old_bytes, new_bytes, nr_new_pg;
> + struct page **new_pglist;
> + unsigned long i;
> +
> + if (!(si->flags & SWP_GHOST) ||
> + !(si->flags & (SWP_USED | SWP_WRITEOK)) ||
> + new <= si->max)
> + return -EINVAL;
> +
> + old_nr = DIV_ROUND_UP(si->max, SWAPFILE_CLUSTER);
> + new_nr = DIV_ROUND_UP(new, SWAPFILE_CLUSTER);
> + old_bytes = ALIGN(old_nr * sizeof(struct swap_cluster_info), PAGE_SIZE);
> + new_bytes = ALIGN(new_nr * sizeof(struct swap_cluster_info), PAGE_SIZE);
> + nr_new_pg = (new_bytes - old_bytes) / PAGE_SIZE;
> +
> + if (old_bytes + nr_new_pg * PAGE_SIZE > si->cluster_vm->size) {
> + pr_warn("ghost: extend to %u exceeds reserved VA (%lu bytes)\n",
> + new, si->cluster_vm->size);
> + return -E2BIG;
> + }
> +
> + new_pglist = kvmalloc_array(si->cluster_vm->nr_pages + nr_new_pg,
> + sizeof(struct page *), GFP_KERNEL);
> + if (!new_pglist)
> + return -ENOMEM;
> + if (si->cluster_vm->pages) {
> + memcpy(new_pglist, si->cluster_vm->pages,
> + si->cluster_vm->nr_pages * sizeof(struct page *));
> + kvfree(si->cluster_vm->pages);

Can you use kvrealloc() here? I was hoping that could get rid of the
memcpy on your side.

I was considering another approach: allowing kvmalloc internally to
take two arguments for size. One is the initial size, and the other is
the vma size. That way you can specify the vma->pages with the max
size it can support but with a small inti size. Then it can grow the
backing pages incrementally to the maximum size without reallocating
the vma. I did some experiments; I might send out an RFC if there is
interest.

Chris

> + }
> + si->cluster_vm->pages = (void *)new_pglist;
> +
> + for (i = 0; i < nr_new_pg; i++) {
> + struct page *page = alloc_page(GFP_KERNEL | __GFP_ZERO);
> + if (!page) {
> + while (i-- > 0)
> + __free_page(new_pglist[si->cluster_vm->nr_pages + i]);
> + return -ENOMEM;
> + }
> + new_pglist[si->cluster_vm->nr_pages + i] = page;
> + }
> +
> + if (vm_area_map_pages(si->cluster_vm,
> + (unsigned long)si->cluster_vm->addr + old_bytes,
> + (unsigned long)si->cluster_vm->addr + new_bytes,
> + &new_pglist[si->cluster_vm->nr_pages])) {
> + for (i = 0; i < nr_new_pg; i++)
> + __free_page(new_pglist[si->cluster_vm->nr_pages + i]);
> + return -ENOMEM;
> + }
> + si->cluster_vm->nr_pages += nr_new_pg;
> +
> + for (i = old_nr; i < new_nr; i++) {
> + struct swap_cluster_info *ci = &si->cluster_info[i];
> + spin_lock_init(&ci->lock);
> + INIT_LIST_HEAD(&ci->list);
> + }
> +
> + if (new % SWAPFILE_CLUSTER) {
> + unsigned long base = (new - 1) / SWAPFILE_CLUSTER *
> + SWAPFILE_CLUSTER;
> + unsigned long idx = base / SWAPFILE_CLUSTER;
> + struct swap_cluster_info *ci = &si->cluster_info[idx];
> +
> + if (!ci->count && swap_cluster_alloc_table(ci, GFP_KERNEL)) {
> + for (i = 0; i < nr_new_pg; i++)
> + __free_page(new_pglist[si->cluster_vm->nr_pages + i]);
> + return -ENOMEM;
> + }
> + spin_lock(&ci->lock);
> + for (i = new % SWAPFILE_CLUSTER; i < SWAPFILE_CLUSTER; i++)
> + __swap_table_set(ci, i, SWP_TB_BAD);
> + spin_unlock(&ci->lock);
> + }
> +
> + WRITE_ONCE(si->max, new);
> +
> + for (i = old_nr; i < new_nr; i++) {
> + struct swap_cluster_info *ci = &si->cluster_info[i];
> + ci->flags = CLUSTER_FLAG_FREE;
> + spin_lock(&si->lock);
> + list_add_tail(&ci->list, &si->free_clusters);
> + spin_unlock(&si->lock);
> + }
> +
> + si->pages += new - old_max;
> + atomic_long_add(new - old_max, &nr_swap_pages);
> + spin_lock(&swap_lock);
> + total_swap_pages += new - old_max;
> + spin_unlock(&swap_lock);
> + return 0;
> +}
> #endif /* CONFIG_ZSWAP */
>
> static int __init swapfile_init(void)
> --
> 2.54.0
>