From nobody Fri Sep 25 05:30:54 2026 Received: from mta0.migadu.com (out-190.mta0.migadu.com [91.218.175.190]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 4EBB84D990A for ; Wed, 16 Sep 2026 10:19:47 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.190 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554004; cv=none; b=YgdyCYfKyM4P6HNXRoIPi0xJO3TGtqhmL0rHXYGNhwdehi0YyoPsbsVkDi9TSDv44n0UzvtHDQAKqmqGhCkFEYeRipguqNl4sMGvFAW5VpAmz0ksOHhtgo/TbtJDMkNxavnyTYuo5sXLiej/aEVOXre/TMzFG/2SiCcoaJv4BHY= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554004; c=relaxed/simple; bh=yHUdw2SliBhqyIthTxgdHp+3fEdjzm0Ya/fgObjL84U=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-type; b=isL3n2DdPrUHzC08ydUz9cqx47/12k6XRMlRz0CAhoJErL3KdLwOACba450IOYT8cPPBj4gTxYMqErcT1slACcKHkMNUDSn+g1zQJ6YhOW31Y7s1nZPd8Q2YSPDKUqM+8O1p7NApd3XiWwl2LgBpXwpI9o7qBwqvKe5zLGxNizs= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn; spf=pass smtp.mailfrom=linux.dev; arc=none smtp.client-ip=91.218.175.190 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev X-Envelope-To: linux-kernel@vger.kernel.org X-Envelope-To: linux-kernel@vger.kernel.org Received: by mta12.migadu.com with ESMTPS id dbbdf434853dd29a; Wed, 16 Sep 2026 10:19:43 +0000 X-Mizu-Trace-ID: dbbdf434853dd29a X-Migadu-Flow: FLOW_OUT From: Baoquan He To: linux-mm@kvack.org Cc: akpm@linux-foundation.org, chrisl@kernel.org, kasong@tencent.com, nphamcs@gmail.com, baohua@kernel.org, youngjun.park@lge.com, hannes@cmpxchg.org, yosry@kernel.org, shikemeng@huaweicloud.com, chengming.zhou@linux.dev, baoquan.he@linux.dev, david@kernel.org, linux-kernel@vger.kernel.org, kunwu.chan@gmail.com, Baoquan He Subject: [PATCH v3 01/14] mm: xswap support for zswap Date: Wed, 16 Sep 2026 18:19:08 +0800 Message-ID: <20260916101929.149106-2-hebaoquan@kylinos.cn> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260916101929.149106-1-hebaoquan@kylinos.cn> References: <20260916101929.149106-1-hebaoquan@kylinos.cn> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" From: Chris Li Introduce extendable swap device support - xswap. An xswap device has no backing storage and no swap data section, so it wastes no disk space. Creation is via a sysfs interface added in a later patch. Zswap writeback is gated on whether a real (non-xswap) swap device is active. nr_real_swapfiles counts such devices and is maintained at swapon/swapoff only, so the gate reflects "a device exists to write back to" rather than "a device currently has free slots". This keeps writeback working even when the real swap device is full, and avoids a double decrement when a full device is swapped off. Co-developed-by: Baoquan He Signed-off-by: Baoquan He Signed-off-by: Chris Li --- include/linux/swap.h | 2 ++ mm/page_io.c | 19 +++++++++++++++++++ mm/swap_state.c | 4 ++++ mm/swapfile.c | 28 ++++++++++++++++++++++++---- mm/zswap.c | 7 ++++++- 5 files changed, 55 insertions(+), 5 deletions(-) diff --git a/include/linux/swap.h b/include/linux/swap.h index 43155e122b5c..b4331ca4759a 100644 --- a/include/linux/swap.h +++ b/include/linux/swap.h @@ -201,6 +201,7 @@ enum { SWP_STABLE_WRITES =3D (1 << 11), /* no overwrite PG_writeback pages */ SWP_SYNCHRONOUS_IO =3D (1 << 12), /* synchronous IO is efficient */ SWP_HIBERNATION =3D (1 << 13), /* pinned for hibernation */ + SWP_XSWAP =3D (1 << 14), /* extendable swap device */ /* add others here before... */ }; =20 @@ -375,6 +376,7 @@ void free_folio_and_swap_cache(struct folio *folio); void free_pages_and_swap_cache(struct encoded_page **, int); /* linux/mm/swapfile.c */ extern atomic_long_t nr_swap_pages; +extern atomic_t nr_real_swapfiles; extern long total_swap_pages; extern atomic_t nr_rotate_swap; =20 diff --git a/mm/page_io.c b/mm/page_io.c index 1da4ff484f09..d685c2e2429a 100644 --- a/mm/page_io.c +++ b/mm/page_io.c @@ -248,6 +248,15 @@ int swap_writeout(struct swap_io_ctx *ctx, struct foli= o *folio) } rcu_read_unlock(); =20 + /* + * xswap has no backing store: keep the folio. ctx->sis is not set + * yet, so look the device up from the entry. + */ + if (unlikely(__swap_entry_to_info(folio->swap)->flags & SWP_XSWAP)) { + folio_mark_dirty(folio); + return AOP_WRITEPAGE_ACTIVATE; + } + __swap_writeout(ctx, folio); return 0; out_unlock: @@ -482,6 +491,16 @@ void swap_read_folio(struct swap_io_ctx *ctx, struct f= olio *folio) if (zswap_load(folio) !=3D -ENOENT) goto finish; =20 + if (unlikely(sis->flags & SWP_XSWAP)) { + /* + * An xswap entry only ever lives in zswap, so zswap_load() + * must have found it. Unlock and let the caller retry. + */ + WARN_ON_ONCE(1); + folio_unlock(folio); + goto finish; + } + /* We have to read from slower devices. Increase zswap protection. */ zswap_folio_swapin(folio); swap_add_folio(ctx, folio, READ); diff --git a/mm/swap_state.c b/mm/swap_state.c index 625c185a1ca4..8bba3e533b28 100644 --- a/mm/swap_state.c +++ b/mm/swap_state.c @@ -837,6 +837,10 @@ struct folio *swap_cluster_readahead(swp_entry_t entry= , gfp_t gfp_mask, struct blk_plug plug; swp_entry_t ra_entry; =20 + /* xswap entries live only in zswap; readahead does not help. */ + if (si->flags & SWP_XSWAP) + goto skip; + mask =3D swapin_nr_pages(offset) - 1; if (!mask) goto skip; diff --git a/mm/swapfile.c b/mm/swapfile.c index 280dd906eb18..13ae681acf07 100644 --- a/mm/swapfile.c +++ b/mm/swapfile.c @@ -66,6 +66,7 @@ static void move_cluster(struct swap_info_struct *si, static DEFINE_SPINLOCK(swap_lock); static unsigned int nr_swapfiles; atomic_long_t nr_swap_pages; +atomic_t nr_real_swapfiles; /* * Some modules use swappable objects and may try to swap them out under * memory pressure (via the shrinker). Before doing so, they may wish to @@ -733,7 +734,8 @@ static void free_cluster(struct swap_info_struct *si, s= truct swap_cluster_info * /* * If the swap is discardable, prepare discard the cluster * instead of free it immediately. The cluster will be freed - * after discard. + * after discard. xswap has no bdev and never sets + * SWP_PAGE_DISCARD, so it always takes the free path below. */ if ((si->flags & (SWP_WRITEOK | SWP_PAGE_DISCARD)) =3D=3D (SWP_WRITEOK | SWP_PAGE_DISCARD)) { @@ -1216,6 +1218,9 @@ static void del_from_avail_list(struct swap_info_stru= ct *si, bool swapoff) */ lockdep_assert_held(&si->lock); si->flags &=3D ~SWP_WRITEOK; + /* Count active devices, not merely those on the avail list. */ + if (!(si->flags & SWP_XSWAP)) + atomic_sub(1, &nr_real_swapfiles); atomic_long_or(SWAP_USAGE_OFFLIST_BIT, &si->inuse_pages); } else { /* @@ -1273,6 +1278,8 @@ static void add_to_avail_list(struct swap_info_struct= *si, bool swapon) } =20 plist_add(&si->avail_list, &swap_avail_head); + if (swapon && !(si->flags & SWP_XSWAP)) + atomic_add(1, &nr_real_swapfiles); =20 skip: spin_unlock(&swap_avail_lock); @@ -3266,7 +3273,8 @@ SYSCALL_DEFINE1(swapoff, const char __user *, special= file) =20 destroy_swap_extents(p, p->swap_file); =20 - if (!(p->flags & SWP_SOLIDSTATE)) + if (!(p->flags & SWP_XSWAP) && + !(p->flags & SWP_SOLIDSTATE)) atomic_dec(&nr_rotate_swap); =20 mutex_lock(&swapon_mutex); @@ -3376,6 +3384,19 @@ static void swap_stop(struct seq_file *swap, void *v) mutex_unlock(&swapon_mutex); } =20 +static const char *swap_type_str(struct swap_info_struct *si) +{ + struct file *file =3D si->swap_file; + + if (si->flags & SWP_XSWAP) + return "xswap\t"; + + if (S_ISBLK(file_inode(file)->i_mode)) + return "partition"; + + return "file\t"; +} + static int swap_show(struct seq_file *swap, void *v) { struct swap_info_struct *si =3D v; @@ -3395,8 +3416,7 @@ static int swap_show(struct seq_file *swap, void *v) len =3D seq_file_path(swap, file, " \t\n\\"); seq_printf(swap, "%*s%s\t%lu\t%s%lu\t%s%d\n", len < 40 ? 40 - len : 1, " ", - S_ISBLK(file_inode(file)->i_mode) ? - "partition" : "file\t", + swap_type_str(si), bytes, bytes < 10000000 ? "\t" : "", inuse, inuse < 10000000 ? "\t" : "", si->prio); diff --git a/mm/zswap.c b/mm/zswap.c index 507f2d19fd2a..96fb993d18cb 100644 --- a/mm/zswap.c +++ b/mm/zswap.c @@ -1018,6 +1018,11 @@ static int zswap_writeback_entry(struct zswap_entry = *entry, if (IS_ERR_OR_NULL(si)) return -ENOENT; =20 + if (si->flags & SWP_XSWAP) { + put_swap_device(si); + return -EINVAL; + } + mpol =3D get_task_policy(current); folio =3D swap_cache_alloc_folio(swpentry, GFP_KERNEL, BIT(0), NULL, mpol, NO_INTERLEAVE_INDEX); @@ -1567,7 +1572,7 @@ bool zswap_store(struct folio *folio) zswap_pool_put(pool); put_objcg: obj_cgroup_put(objcg); - if (!ret && zswap_pool_reached_full) + if (!ret && zswap_pool_reached_full && atomic_read(&nr_real_swapfiles)) queue_work(shrink_wq, &zswap_shrink_work); check_old: /* --=20 2.54.0 From nobody Fri Sep 25 05:30:54 2026 Received: from mta0.migadu.com (out-199.mta0.migadu.com [91.218.175.199]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C6C0A4D09E3 for ; Wed, 16 Sep 2026 10:19:53 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.199 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554017; cv=none; b=N9ZD5t9EBN5kXI747sJqG8Cm6OrYhdx2r4vVMZD/BOYvpmtkFBPAWhj+9CMewmo/HYTpwqyDEtuXyHNJTqDKjmlQqYEDnzSgIS2uBMuPUiHm3HhbdSjoEepRUBA08WBNptoh8X126ZEiVlOzSI5EChxWTP/+1U3TBD1cTOICIKw= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554017; c=relaxed/simple; bh=XlYAfe857GrqSDz9vgpM+rs+tEmHVx0+RvAgnQ5G+1E=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-type; b=TH6DCCJynwL33tazGTro3nLME4ybX/KiCgC7riYNBN2idgDy9/C2rzgNqvT1Mv7BTlkASARSgkcSSII21e3IU7HQo6yPo1xJE8YsT2NC8IUTJVp0WH1SJEYVBn9MDy65+uMQaDRObmJAePgXoYlhQn6idalT4UiFRtiWtu/k4QI= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn; spf=pass smtp.mailfrom=linux.dev; arc=none smtp.client-ip=91.218.175.199 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev X-Envelope-To: linux-kernel@vger.kernel.org X-Envelope-To: linux-kernel@vger.kernel.org Received: by mta10.migadu.com with ESMTPS id a96bfd4b4d4e7194; Wed, 16 Sep 2026 10:19:48 +0000 X-Mizu-Trace-ID: a96bfd4b4d4e7194 X-Migadu-Flow: FLOW_OUT From: Baoquan He To: linux-mm@kvack.org Cc: akpm@linux-foundation.org, chrisl@kernel.org, kasong@tencent.com, nphamcs@gmail.com, baohua@kernel.org, youngjun.park@lge.com, hannes@cmpxchg.org, yosry@kernel.org, shikemeng@huaweicloud.com, chengming.zhou@linux.dev, baoquan.he@linux.dev, david@kernel.org, linux-kernel@vger.kernel.org, kunwu.chan@gmail.com, Baoquan He Subject: [PATCH v3 02/14] mm, swap: add CONFIG_XSWAP and xswap fields to swap_info_struct Date: Wed, 16 Sep 2026 18:19:09 +0800 Message-ID: <20260916101929.149106-3-hebaoquan@kylinos.cn> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260916101929.149106-1-hebaoquan@kylinos.cn> References: <20260916101929.149106-1-hebaoquan@kylinos.cn> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add CONFIG_XSWAP Kconfig option for extendable swap device support. It depends on SWAP && 64BIT && ZSWAP, since xswap devices are backed by zswap, and on SYSFS, which is currently the only way to create one. Add three fields to struct swap_info_struct under CONFIG_XSWAP: - cluster_vm: the VM_SPARSE vm_struct backing the cluster_info array - nr_clusters_max: total number of clusters in the xswap address space - nr_clusters_mapped: number of clusters currently mapped (lazy grow) These fields enable lazy vmalloc-based dynamic cluster management. Signed-off-by: Baoquan He --- include/linux/swap.h | 5 +++++ mm/Kconfig | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/include/linux/swap.h b/include/linux/swap.h index b4331ca4759a..b7882aac1eab 100644 --- a/include/linux/swap.h +++ b/include/linux/swap.h @@ -242,6 +242,11 @@ struct swap_info_struct { signed char type; /* strange name for an index */ unsigned int max; /* size of this swap device */ struct swap_cluster_info *cluster_info; /* array, one entry per cluster */ +#ifdef CONFIG_XSWAP + struct vm_struct *cluster_vm; /* VM_SPARSE area for cluster_info */ + unsigned long nr_clusters_max;/* total clusters in the xswap address spa= ce */ + unsigned long nr_clusters_mapped; /* currently mapped cluster count */ +#endif struct list_head free_clusters; /* free clusters list */ struct list_head full_clusters; /* full clusters list */ struct list_head nonfull_clusters[SWAP_NR_ORDERS]; diff --git a/mm/Kconfig b/mm/Kconfig index c180d40cd671..5d6c7845c422 100644 --- a/mm/Kconfig +++ b/mm/Kconfig @@ -122,6 +122,15 @@ config ZSWAP_COMPRESSOR_DEFAULT default "zstd" if ZSWAP_COMPRESSOR_DEFAULT_ZSTD default "" =20 +config XSWAP + bool "Extendable (virtual) swap device" + depends on SWAP && 64BIT && ZSWAP && SYSFS + help + Adds support for extendable swap devices (xswap) that decouple + PTE swap entries from physical backing storage. The cluster_info + array is backed by a sparse vmalloc area that grows and shrinks + on demand, avoiding static pre-allocation overhead. + config ZSMALLOC tristate =20 --=20 2.54.0 From nobody Fri Sep 25 05:30:54 2026 Received: from mta0.migadu.com (out-212.mta0.migadu.com [91.218.175.212]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 67E324A3F0D for ; Wed, 16 Sep 2026 10:20:01 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.212 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554020; cv=none; b=QHBeS7IzzOytk5lb5I2S73gl5MSqxNByOtisl4fjsJ8Zg/AZXE1vXglqyTNlMGYME+XFdkuMpvAItj48ClLD1KQr7e0KlUfAy6Qb9t5czIqQGVECmgEMohtAhlrXLCGYOrC1V1Ck2tRq1rpVOyIi1LsXEu7McT4+kRD3HjfDGOI= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554020; c=relaxed/simple; bh=P0zQpNS9VF3p0YtRHJ7+xNeb7G33aUTMkFOgmA4JJ2g=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-type; b=jM9PsFujK7VtHgWVHcHYm41RciSBuxMgEx1fAQvODj8h90KFXSI+sztGBD6stM5d/N63McktjTTczwVve2SrxM+Jp3M9YNRkfz+N++C++muVHzxCWRcl9O30WvaOHpBJFlwC6pctSP70msahld79cO+heUSuhfQQLA1eggqZJLs= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn; spf=pass smtp.mailfrom=linux.dev; arc=none smtp.client-ip=91.218.175.212 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev X-Envelope-To: linux-kernel@vger.kernel.org X-Envelope-To: linux-kernel@vger.kernel.org Received: by mta11.migadu.com with ESMTPS id 41ccb50d5361293b; Wed, 16 Sep 2026 10:19:56 +0000 X-Mizu-Trace-ID: 41ccb50d5361293b X-Migadu-Flow: FLOW_OUT From: Baoquan He To: linux-mm@kvack.org Cc: akpm@linux-foundation.org, chrisl@kernel.org, kasong@tencent.com, nphamcs@gmail.com, baohua@kernel.org, youngjun.park@lge.com, hannes@cmpxchg.org, yosry@kernel.org, shikemeng@huaweicloud.com, chengming.zhou@linux.dev, baoquan.he@linux.dev, david@kernel.org, linux-kernel@vger.kernel.org, kunwu.chan@gmail.com, Baoquan He Subject: [PATCH v3 03/14] mm, swap: refactor free_swap_cluster_info to take swap_info_struct Date: Wed, 16 Sep 2026 18:19:10 +0800 Message-ID: <20260916101929.149106-4-hebaoquan@kylinos.cn> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260916101929.149106-1-hebaoquan@kylinos.cn> References: <20260916101929.149106-1-hebaoquan@kylinos.cn> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Change free_swap_cluster_info() to take struct swap_info_struct* instead of (cluster_info, maxpages). It now extracts the fields from si and clears si->cluster_info after freeing to avoid a double free on the swapon() error path. The new parameter also lets the xswap path access si->flags in the function. Signed-off-by: Baoquan He --- mm/swapfile.c | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/mm/swapfile.c b/mm/swapfile.c index 13ae681acf07..4b260f9760ac 100644 --- a/mm/swapfile.c +++ b/mm/swapfile.c @@ -3142,14 +3142,17 @@ static void wait_for_allocation(struct swap_info_st= ruct *si) } } =20 -static void free_swap_cluster_info(struct swap_cluster_info *cluster_info, - unsigned long maxpages) +static void free_swap_cluster_info(struct swap_info_struct *si) { + struct swap_cluster_info *cluster_info =3D si->cluster_info; + unsigned long maxpages =3D si->max; struct swap_cluster_info *ci; - int i, nr_clusters =3D DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER); + int i, nr_clusters; =20 if (!cluster_info) return; + + nr_clusters =3D DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER); for (i =3D 0; i < nr_clusters; i++) { ci =3D cluster_info + i; /* Cluster with bad marks count will have a remaining table */ @@ -3161,6 +3164,7 @@ static void free_swap_cluster_info(struct swap_cluste= r_info *cluster_info, spin_unlock(&ci->lock); } kvfree(cluster_info); + si->cluster_info =3D NULL; } =20 /* @@ -3188,11 +3192,9 @@ static void flush_percpu_swap_cluster(struct swap_in= fo_struct *si) SYSCALL_DEFINE1(swapoff, const char __user *, specialfile) { struct swap_info_struct *p =3D NULL; - struct swap_cluster_info *cluster_info; struct file *swap_file, *victim; struct address_space *mapping; struct inode *inode; - unsigned int maxpages; int err, found =3D 0; =20 if (!capable(CAP_SYS_ADMIN)) @@ -3284,10 +3286,6 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specia= lfile) =20 swap_file =3D p->swap_file; p->swap_file =3D NULL; - maxpages =3D p->max; - cluster_info =3D p->cluster_info; - p->max =3D 0; - p->cluster_info =3D NULL; spin_unlock(&p->lock); spin_unlock(&swap_lock); arch_swap_invalidate_area(p->type); @@ -3295,7 +3293,9 @@ SYSCALL_DEFINE1(swapoff, const char __user *, special= file) mutex_unlock(&swapon_mutex); kfree(p->global_cluster); p->global_cluster =3D NULL; - free_swap_cluster_info(cluster_info, maxpages); + free_swap_cluster_info(p); + p->max =3D 0; + p->cluster_info =3D NULL; =20 inode =3D mapping->host; =20 @@ -3662,6 +3662,8 @@ static int setup_swap_clusters_info(struct swap_info_= struct *si, if (!cluster_info) goto err; =20 + si->cluster_info =3D cluster_info; + for (i =3D 0; i < nr_clusters; i++) spin_lock_init(&cluster_info[i].lock); =20 @@ -3725,7 +3727,7 @@ static int setup_swap_clusters_info(struct swap_info_= struct *si, si->cluster_info =3D cluster_info; return 0; err: - free_swap_cluster_info(cluster_info, maxpages); + free_swap_cluster_info(si); return err; } =20 @@ -3947,7 +3949,7 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialf= ile, int, swap_flags) si->global_cluster =3D NULL; inode =3D NULL; destroy_swap_extents(si, swap_file); - free_swap_cluster_info(si->cluster_info, si->max); + free_swap_cluster_info(si); si->cluster_info =3D NULL; /* * Clear the SWP_USED flag after all resources are freed so --=20 2.54.0 From nobody Fri Sep 25 05:30:54 2026 Received: from mta0.migadu.com (out-226.mta0.migadu.com [91.218.175.226]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id AF8F34CE68C for ; Wed, 16 Sep 2026 10:20:07 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.226 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554036; cv=none; b=qErOSUSJq7PhWNE+5AwIhfIVKFpvsas4nZliElar7uL+2bU4ml/uA2F7QJO6lilrPeT+SwXd6cwXOhP1zEE4vAtw75W3pWVHJ+m3IkQwOqI3iod4RcRkYpqmM/TX+md1uVuvWUlGb86hq+EjwI2nykGq/e5ZAwbgWwqF06P1Njo= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554036; c=relaxed/simple; bh=NIv8cbfcW31VTENtbsqFpZxCznW8A4E0Yxd76qhMhRQ=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-type; b=u3HFo5Tl8uCzZmGAXD1YXkhIH585qNV6PgeFrjF4Tprtn3vfw9yEDEKvPFEVpAbrxLbyHqVnQpMb3F2qjx9x9tPF6Tly1vtvqXPUIKCCEZNLOGzciXf26tCz8j45FFOM/6ppX/+NaqGb395NgvZh0YavlRUaNEjo5HajH9C+Jm0= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn; spf=pass smtp.mailfrom=linux.dev; arc=none smtp.client-ip=91.218.175.226 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev X-Envelope-To: linux-kernel@vger.kernel.org X-Envelope-To: linux-kernel@vger.kernel.org Received: by mta11.migadu.com with ESMTPS id 50ed61c3cd174dab; Wed, 16 Sep 2026 10:20:02 +0000 X-Mizu-Trace-ID: 50ed61c3cd174dab X-Migadu-Flow: FLOW_OUT From: Baoquan He To: linux-mm@kvack.org Cc: akpm@linux-foundation.org, chrisl@kernel.org, kasong@tencent.com, nphamcs@gmail.com, baohua@kernel.org, youngjun.park@lge.com, hannes@cmpxchg.org, yosry@kernel.org, shikemeng@huaweicloud.com, chengming.zhou@linux.dev, baoquan.he@linux.dev, david@kernel.org, linux-kernel@vger.kernel.org, kunwu.chan@gmail.com, Baoquan He Subject: [PATCH v3 04/14] mm, swap: add xswap cluster grow via VM_SPARSE vmalloc Date: Wed, 16 Sep 2026 18:19:11 +0800 Message-ID: <20260916101929.149106-5-hebaoquan@kylinos.cn> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260916101929.149106-1-hebaoquan@kylinos.cn> References: <20260916101929.149106-1-hebaoquan@kylinos.cn> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Back the cluster_info array with a sparse vmalloc area that is populated lazily in chunks: pages are allocated and mapped as swap usage grows, and unmapped again on error/teardown. Only an initial chunk is mapped at device setup, and all accesses are bounded to the mapped range. A per-device mutex serializes the map/unmap operations, and the grow path rejects stale ranges and cleans up partial mappings on failure. Signed-off-by: Baoquan He --- include/linux/swap.h | 1 + mm/swapfile.c | 321 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 313 insertions(+), 9 deletions(-) diff --git a/include/linux/swap.h b/include/linux/swap.h index b7882aac1eab..8c62a53667bb 100644 --- a/include/linux/swap.h +++ b/include/linux/swap.h @@ -246,6 +246,7 @@ struct swap_info_struct { struct vm_struct *cluster_vm; /* VM_SPARSE area for cluster_info */ unsigned long nr_clusters_max;/* total clusters in the xswap address spa= ce */ unsigned long nr_clusters_mapped; /* currently mapped cluster count */ + struct mutex xswap_lock; /* serialize map/unmap operations */ #endif struct list_head free_clusters; /* free clusters list */ struct list_head full_clusters; /* full clusters list */ diff --git a/mm/swapfile.c b/mm/swapfile.c index 4b260f9760ac..dac5e0db0e94 100644 --- a/mm/swapfile.c +++ b/mm/swapfile.c @@ -49,6 +49,25 @@ #include "internal.h" #include "swap.h" =20 +#ifdef CONFIG_XSWAP +/* + * xswap: dynamically grow the cluster_info array via a VM_SPARSE area. + * + * XSWAP_GROW_CLUSTERS is the number of clusters to map in one grow + * operation. It is set to the number of cluster_info structs that + * fit in a single page (at least 16), so that the vmalloc page table + * overhead is proportional to the number of clusters mapped. + */ +#define XSWAP_GROW_CLUSTERS \ + max_t(unsigned long, PAGE_SIZE / sizeof(struct swap_cluster_info), 16) + +static int xswap_map_clusters(struct swap_info_struct *si, + unsigned long start_idx, unsigned long nr); +static void xswap_unmap_clusters(struct swap_info_struct *si, + unsigned long start_idx, unsigned long nr); +static int xswap_mapped_end(pte_t *pte, unsigned long addr, void *data); +#endif + static void swap_range_alloc(struct swap_info_struct *si, unsigned int nr_entries); static bool folio_swapcache_freeable(struct folio *folio); @@ -2797,10 +2816,24 @@ static unsigned int find_next_to_unuse(struct swap_= info_struct *si, unsigned int prev) { struct swap_cluster_info *ci; - unsigned long i, end; + unsigned long i, cluster_end, end; unsigned int ci_off; unsigned long swp_tb; =20 + end =3D si->max; +#ifdef CONFIG_XSWAP + /* xswap may have shrunk and unmapped the cluster_info tail. */ + if (si->flags & SWP_XSWAP) { + unsigned long mapped_end; + + /* Pairs with the smp_store_release() in xswap_map_clusters(). */ + mapped_end =3D smp_load_acquire(&si->nr_clusters_mapped) * + SWAPFILE_CLUSTER; + if (mapped_end < end) + end =3D mapped_end; + } +#endif + /* * No need for swap_lock here: we're just looking * for whether an entry is in use, not modifying it; false @@ -2808,11 +2841,11 @@ static unsigned int find_next_to_unuse(struct swap_= info_struct *si, * allocations from this area (while holding swap_lock). */ i =3D prev + 1; - while (i < si->max) { + while (i < end) { ci =3D __swap_offset_to_cluster(si, i); - end =3D min_t(unsigned long, - ALIGN_DOWN(i, SWAPFILE_CLUSTER) + SWAPFILE_CLUSTER, - si->max); + cluster_end =3D min_t(unsigned long, + ALIGN_DOWN(i, SWAPFILE_CLUSTER) + SWAPFILE_CLUSTER, + end); =20 /* * An empty cluster has no slot in use, so skip it whole. @@ -2822,13 +2855,13 @@ static unsigned int find_next_to_unuse(struct swap_= info_struct *si, * enough, unlike in every other cluster_is_empty() caller. */ if (!READ_ONCE(ci->count)) { - i =3D end; + i =3D cluster_end; cond_resched(); continue; } =20 ci_off =3D i % SWAPFILE_CLUSTER; - for (; i < end; ci_off++, i++) { + for (; i < cluster_end; ci_off++, i++) { swp_tb =3D swap_table_get(ci, ci_off); if (!swp_tb_is_null(swp_tb) && !swp_tb_is_bad(swp_tb)) return i; @@ -2838,7 +2871,6 @@ static unsigned int find_next_to_unuse(struct swap_in= fo_struct *si, =20 return 0; } - static int try_to_unuse(unsigned int type) { struct mm_struct *prev_mm; @@ -3136,6 +3168,17 @@ static void wait_for_allocation(struct swap_info_str= uct *si) =20 BUG_ON(si->flags & SWP_WRITEOK); =20 +#ifdef CONFIG_XSWAP + if (si->flags & SWP_XSWAP) { + /* + * Skip the shrinker-unmapped tail; pairs with the + * smp_store_release() in xswap_map_clusters(). + */ + end =3D min(end, smp_load_acquire(&si->nr_clusters_mapped) * + SWAPFILE_CLUSTER); + } +#endif + for (offset =3D 0; offset < end; offset +=3D SWAPFILE_CLUSTER) { ci =3D swap_cluster_lock(si, offset); swap_cluster_unlock(ci); @@ -3147,11 +3190,41 @@ static void free_swap_cluster_info(struct swap_info= _struct *si) struct swap_cluster_info *cluster_info =3D si->cluster_info; unsigned long maxpages =3D si->max; struct swap_cluster_info *ci; - int i, nr_clusters; + unsigned long i, nr_clusters; =20 if (!cluster_info) return; =20 +#ifdef CONFIG_XSWAP + if (si->flags & SWP_XSWAP) { + unsigned long nr_mapped; + + /* + * Cluster 0 keeps the bad header slot, so it never empties + * and __free_cluster() never frees its table. + */ + /* Pairs with the smp_store_release() in xswap_map_clusters(). */ + nr_mapped =3D smp_load_acquire(&si->nr_clusters_mapped); + for (i =3D 0; i < nr_mapped; i++) { + ci =3D &cluster_info[i]; + spin_lock(&ci->lock); + if (cluster_table_is_alloced(ci)) { + swap_cluster_assert_empty(ci, 0, SWAPFILE_CLUSTER, true); + swap_cluster_free_table(ci); + } + spin_unlock(&ci->lock); + } + /* Unmap all mapped clusters and free the VM_SPARSE area */ + if (si->nr_clusters_mapped > 0) + xswap_unmap_clusters(si, 0, si->nr_clusters_mapped); + free_vm_area(si->cluster_vm); + si->cluster_vm =3D NULL; + si->cluster_info =3D NULL; + si->nr_clusters_mapped =3D 0; + return; + } +#endif + nr_clusters =3D DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER); for (i =3D 0; i < nr_clusters; i++) { ci =3D cluster_info + i; @@ -3649,6 +3722,173 @@ static unsigned long read_swap_header(struct swap_i= nfo_struct *si, return maxpages; } =20 +#ifdef CONFIG_XSWAP +static int xswap_map_clusters(struct swap_info_struct *si, + unsigned long start_idx, unsigned long nr) +{ + unsigned long start_addr =3D (unsigned long)si->cluster_info + + (size_t)start_idx * sizeof(struct swap_cluster_info); + unsigned long end_addr =3D start_addr + (size_t)nr * sizeof(struct swap_c= luster_info); + unsigned long vm_start =3D PAGE_ALIGN(start_addr); + unsigned long vm_end =3D PAGE_ALIGN(end_addr); + unsigned int noreclaim_flags; + unsigned long mapped_end; + unsigned long npages; + struct page **pages; + unsigned long i; + int err; + + mutex_lock(&si->xswap_lock); + + /* Refuse a stale range: the boundary moved since the caller read it. */ + if (start_idx !=3D READ_ONCE(si->nr_clusters_mapped)) { + mutex_unlock(&si->xswap_lock); + return -EAGAIN; + } + if (start_idx + nr > si->nr_clusters_max) { + mutex_unlock(&si->xswap_lock); + return -EAGAIN; + } + + /* + * Page-granular mapping can cover clusters past the previous chunk. + * Find the already-mapped prefix and map only the rest. + */ + mapped_end =3D vm_start; + if (vm_start < vm_end) + apply_to_existing_page_range(&init_mm, vm_start, + vm_end - vm_start, + xswap_mapped_end, &mapped_end); + if (vm_start >=3D vm_end || mapped_end =3D=3D vm_end) + goto mapped; + vm_start =3D mapped_end; + + npages =3D (vm_end - vm_start) >> PAGE_SHIFT; + + /* Prevent recursive reclaim during vmap page table allocation. */ + noreclaim_flags =3D memalloc_noreclaim_save(); + + pages =3D kmalloc_array(npages, sizeof(*pages), + __GFP_HIGH | __GFP_NOMEMALLOC | GFP_KERNEL); + if (!pages) { + memalloc_noreclaim_restore(noreclaim_flags); + mutex_unlock(&si->xswap_lock); + return -ENOMEM; + } + + for (i =3D 0; i < npages; i++) { + /* __GFP_ZERO: cluster_info pointer fields must start NULL. */ + pages[i] =3D alloc_page(__GFP_HIGH | __GFP_NOMEMALLOC | + GFP_KERNEL | __GFP_ZERO); + if (!pages[i]) + goto fail; + } + + err =3D vm_area_map_pages(si->cluster_vm, vm_start, vm_end, pages); + if (err) { + /* + * -EBUSY means the range is already mapped; xswap_lock should + * prevent it. Fail instead of recording a mapping whose + * cluster locks were not initialized. + */ + if (err =3D=3D -EBUSY) { + WARN_ON_ONCE(1); + i =3D npages; + goto fail_nounmap; + } + i =3D npages; + goto fail; + } + + for (i =3D start_idx; i < start_idx + nr; i++) + spin_lock_init(&si->cluster_info[i].lock); + + kfree(pages); + memalloc_noreclaim_restore(noreclaim_flags); + + /* + * Publish the new mappings and cluster lock initialization before + * the count; walkers without xswap_lock use smp_load_acquire(). + */ + smp_store_release(&si->nr_clusters_mapped, start_idx + nr); + mutex_unlock(&si->xswap_lock); + return 0; + +mapped: + for (i =3D start_idx; i < start_idx + nr; i++) + spin_lock_init(&si->cluster_info[i].lock); + + /* Publish the advanced count. */ + smp_store_release(&si->nr_clusters_mapped, start_idx + nr); + mutex_unlock(&si->xswap_lock); + return 0; + +fail_nounmap: + /* + * The mapping was not recorded: free our pages and fail so the + * caller does not touch cluster_info for the range. + */ + while (i > 0) { + i--; + if (pages[i]) + __free_page(pages[i]); + } + kfree(pages); + memalloc_noreclaim_restore(noreclaim_flags); + mutex_unlock(&si->xswap_lock); + return -EBUSY; + +fail: + /* Clear PTEs vm_area_map_pages() may have left before freeing pages. */ + vm_area_unmap_pages(si->cluster_vm, vm_start, vm_end); + while (i > 0) { + i--; + if (pages[i]) + __free_page(pages[i]); + } + memalloc_noreclaim_restore(noreclaim_flags); + kfree(pages); + mutex_unlock(&si->xswap_lock); + return -ENOMEM; +} + +static void xswap_unmap_clusters(struct swap_info_struct *si, + unsigned long start_idx, unsigned long nr) +{ + unsigned long start_addr =3D (unsigned long)si->cluster_info + + (size_t)start_idx * sizeof(struct swap_cluster_info); + unsigned long end_addr =3D start_addr + (size_t)nr * sizeof(struct swap_c= luster_info); + unsigned long vm_start =3D PAGE_ALIGN(start_addr); + unsigned long vm_end =3D PAGE_ALIGN(end_addr); + + mutex_lock(&si->xswap_lock); + + if (vm_start >=3D vm_end) { + WRITE_ONCE(si->nr_clusters_mapped, start_idx); + mutex_unlock(&si->xswap_lock); + return; + } + + vm_area_unmap_pages(si->cluster_vm, vm_start, vm_end); + /* vm_area_unmap_pages() clears PTEs but does not free pages. */ + /* TODO: free backing pages via page table walk or tracking bitmap */ + + WRITE_ONCE(si->nr_clusters_mapped, start_idx); + mutex_unlock(&si->xswap_lock); +} + +/* Track the end of the run of pages that is already mapped. */ +static int xswap_mapped_end(pte_t *pte, unsigned long addr, void *data) +{ + unsigned long *mapped_end =3D data; + + if (!pte_present(ptep_get(pte))) + return 0; + *mapped_end =3D addr + PAGE_SIZE; + return 0; +} +#endif /* CONFIG_XSWAP */ + static int setup_swap_clusters_info(struct swap_info_struct *si, union swap_header *swap_header, unsigned long maxpages) @@ -3658,6 +3898,69 @@ static int setup_swap_clusters_info(struct swap_info= _struct *si, int err =3D -ENOMEM; unsigned long i; =20 +#ifdef CONFIG_XSWAP + if (si->flags & SWP_XSWAP) { + unsigned long size =3D PAGE_ALIGN(nr_clusters * sizeof(*cluster_info)); + struct vm_struct *vm; + + vm =3D get_vm_area(size, VM_SPARSE); + if (!vm) + goto err; + + cluster_info =3D vm->addr; + si->cluster_vm =3D vm; + si->nr_clusters_max =3D nr_clusters; + si->cluster_info =3D cluster_info; + + /* Must be initialized before xswap_map_clusters() locks it. */ + mutex_init(&si->xswap_lock); + + if (xswap_map_clusters(si, 0, min_t(unsigned long, + XSWAP_GROW_CLUSTERS, nr_clusters))) + goto err_free_vm; + + /* xswap: only cluster 0 slot 0 is bad */ + err =3D swap_cluster_setup_bad_slot(si, cluster_info, 0, false); + if (err) + goto err_unmap; + + INIT_LIST_HEAD(&si->free_clusters); + INIT_LIST_HEAD(&si->full_clusters); + INIT_LIST_HEAD(&si->discard_clusters); + for (i =3D 0; i < SWAP_NR_ORDERS; i++) { + INIT_LIST_HEAD(&si->nonfull_clusters[i]); + INIT_LIST_HEAD(&si->frag_clusters[i]); + } + + /* + * Cluster 0 holds the header slot and the last one holds the + * holes past si->max; both have slots marked bad, so they are + * not entirely free. The clusters in between are. + */ + for (i =3D 0; i < si->nr_clusters_mapped; i++) { + struct swap_cluster_info *ci =3D &cluster_info[i]; + + if (ci->count) { + ci->flags =3D CLUSTER_FLAG_NONFULL; + list_add_tail(&ci->list, &si->nonfull_clusters[0]); + } else { + ci->flags =3D CLUSTER_FLAG_FREE; + list_add_tail(&ci->list, &si->free_clusters); + } + } + + return 0; + +err_unmap: + xswap_unmap_clusters(si, 0, si->nr_clusters_mapped); +err_free_vm: + free_vm_area(si->cluster_vm); + si->cluster_vm =3D NULL; + si->cluster_info =3D NULL; + return err; + } +#endif /* CONFIG_XSWAP */ + cluster_info =3D kvzalloc_objs(*cluster_info, nr_clusters); if (!cluster_info) goto err; --=20 2.54.0 From nobody Fri Sep 25 05:30:54 2026 Received: from mta0.migadu.com (out-253.mta0.migadu.com [91.218.175.253]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 0DB724E3EEC for ; Wed, 16 Sep 2026 10:20:35 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.253 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554040; cv=none; b=aqdZk6zaz/IH/va2OOVblJrEt3acVPBai7JCeFDgs+xVnukRfbSjfPSLOlEdg1ozNZ3nDI9xD8Qfm0m2fob7h6xK9rD6B1RiDPDRy+yknBb4h3zlOoKThuTG7mPnG1IF36OL7jy06o2a9eNvY3WtbbDmPbbiDxGIVGP04jNFPAQ= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554040; c=relaxed/simple; bh=d+LT5iPAI3k8zqB026CT/v4IuW4UZWGFE1l+zibubBg=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-type; b=VJjI5LgAr/aUS9wLUdAF/aGPFpj2jxl+wxoVZU3ma3Lh4AJHp+wmk0uNgzC4ZMKkXQlVJ1m/g0rW5EdGIv3t5SQgANBrq7ScSoNLYWEGukeH5yIxiAOBiD2xa1zhBAXQDEndzzp9Q+KFT8p2Xa19Ao99/FYrLBNDIw7qkIxNb+U= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn; spf=pass smtp.mailfrom=linux.dev; arc=none smtp.client-ip=91.218.175.253 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev X-Envelope-To: linux-kernel@vger.kernel.org X-Envelope-To: linux-kernel@vger.kernel.org Received: by mta11.migadu.com with ESMTPS id 702b54a055381590; Wed, 16 Sep 2026 10:20:11 +0000 X-Mizu-Trace-ID: 702b54a055381590 X-Migadu-Flow: FLOW_OUT From: Baoquan He To: linux-mm@kvack.org Cc: akpm@linux-foundation.org, chrisl@kernel.org, kasong@tencent.com, nphamcs@gmail.com, baohua@kernel.org, youngjun.park@lge.com, hannes@cmpxchg.org, yosry@kernel.org, shikemeng@huaweicloud.com, chengming.zhou@linux.dev, baoquan.he@linux.dev, david@kernel.org, linux-kernel@vger.kernel.org, kunwu.chan@gmail.com, Baoquan He Subject: [PATCH v3 05/14] mm, swap: add sysfs create interface for xswap Date: Wed, 16 Sep 2026 18:19:12 +0800 Message-ID: <20260916101929.149106-6-hebaoquan@kylinos.cn> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260916101929.149106-1-hebaoquan@kylinos.cn> References: <20260916101929.149106-1-hebaoquan@kylinos.cn> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" xswap devices have no backing storage, so there is no file to swapon. Add a sysfs interface to create them directly: /sys/kernel/mm/xswap/create write an optional priority to create a device (empty =3D DEF_SWAP_PRIO) A new device is created with si->max and nr_clusters_max both equal to twice RAM: the cluster_info array is a sparse VM_SPARSE area mapped lazily, so an idle device costs nothing. The runtime size can be lowered per device afterwards via /sys/kernel/mm/xswap/type/limit (added later in this series). The optional priority follows swapon(2)'s -p semantics (default DEF_SWAP_PRIO, valid range -1..SWAP_FLAG_PRIO_MASK). The device shows up in /proc/swaps as "xswap". Hibernation device discovery skips xswap devices: they have no bdev to carry a resume image. Signed-off-by: Baoquan He --- mm/swapfile.c | 163 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 158 insertions(+), 5 deletions(-) diff --git a/mm/swapfile.c b/mm/swapfile.c index dac5e0db0e94..b0edf5421fc5 100644 --- a/mm/swapfile.c +++ b/mm/swapfile.c @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include #include #include @@ -48,6 +50,7 @@ #include "swap_table.h" #include "internal.h" #include "swap.h" +#define DEF_SWAP_PRIO -1 =20 #ifdef CONFIG_XSWAP /* @@ -66,7 +69,64 @@ static int xswap_map_clusters(struct swap_info_struct *s= i, static void xswap_unmap_clusters(struct swap_info_struct *si, unsigned long start_idx, unsigned long nr); static int xswap_mapped_end(pte_t *pte, unsigned long addr, void *data); -#endif + +static int xswap_create(int prio); + +static ssize_t xswap_create_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + int prio =3D DEF_SWAP_PRIO; + int err; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + /* "[]" is optional; empty means DEF_SWAP_PRIO, i.e. the same + * default as swapon(2) without SWAP_FLAG_PREFER. + */ + if (*skip_spaces(buf)) { + err =3D kstrtoint(buf, 10, &prio); + if (err) + return err; + } + + err =3D xswap_create(prio); + if (err < 0) + return err; + + return count; +} + +static struct kobj_attribute xswap_create_attr =3D __ATTR(create, 0200, NU= LL, + xswap_create_store); + +static struct attribute *xswap_attrs[] =3D { + &xswap_create_attr.attr, + NULL, +}; + +static const struct attribute_group xswap_attr_group =3D { + .attrs =3D xswap_attrs, +}; + +static struct kobject *xswap_kobj; + +static void xswap_sysfs_init(void) +{ + xswap_kobj =3D kobject_create_and_add("xswap", mm_kobj); + if (!xswap_kobj) { + pr_err("xswap: failed to create sysfs kobject\n"); + return; + } + if (sysfs_create_group(xswap_kobj, &xswap_attr_group)) + pr_err("xswap: failed to create sysfs group\n"); +} +#else /* !CONFIG_XSWAP */ +static inline void xswap_sysfs_init(void) +{ +} +#endif /* CONFIG_XSWAP */ =20 static void swap_range_alloc(struct swap_info_struct *si, unsigned int nr_entries); @@ -94,7 +154,6 @@ atomic_t nr_real_swapfiles; EXPORT_SYMBOL_GPL(nr_swap_pages); /* protected with swap_lock. reading in vm_swap_full() doesn't need lock */ long total_swap_pages; -#define DEF_SWAP_PRIO -1 unsigned long swapfile_maximum_size; #ifdef CONFIG_MIGRATION bool swap_migration_ad_supported; @@ -2275,6 +2334,9 @@ static int __find_hibernation_swap_type(dev_t device,= sector_t offset) =20 if (!(sis->flags & SWP_WRITEOK)) continue; + /* xswap has no bdev to match a resume device */ + if (sis->flags & SWP_XSWAP) + continue; =20 if (device =3D=3D sis->bdev->bd_dev) { struct swap_extent *se =3D first_se(sis); @@ -2462,6 +2524,8 @@ int find_first_swap(dev_t *device) =20 if (!(sis->flags & SWP_WRITEOK)) continue; + if (sis->flags & SWP_XSWAP) + continue; *device =3D sis->bdev->bd_dev; spin_unlock(&swap_lock); return type; @@ -3423,7 +3487,7 @@ static void *swap_start(struct seq_file *swap, loff_t= *pos) return SEQ_START_TOKEN; =20 for (type =3D 0; (si =3D swap_type_to_info(type)); type++) { - if (!(si->swap_file)) + if (!(si->swap_file) && !(si->flags & SWP_XSWAP)) continue; if (!--l) return si; @@ -3444,7 +3508,7 @@ static void *swap_next(struct seq_file *swap, void *v= , loff_t *pos) =20 ++(*pos); for (; (si =3D swap_type_to_info(type)); type++) { - if (!(si->swap_file)) + if (!(si->swap_file) && !(si->flags & SWP_XSWAP)) continue; return si; } @@ -3486,7 +3550,14 @@ static int swap_show(struct seq_file *swap, void *v) inuse =3D K(swap_usage_in_pages(si)); =20 file =3D si->swap_file; - len =3D seq_file_path(swap, file, " \t\n\\"); + if (file) + len =3D seq_file_path(swap, file, " \t\n\\"); + else { + char name[16]; + + len =3D scnprintf(name, sizeof(name), "xswap%d", si->type); + seq_puts(swap, name); + } seq_printf(swap, "%*s%s\t%lu\t%s%lu\t%s%d\n", len < 40 ? 40 - len : 1, " ", swap_type_str(si), @@ -4034,6 +4105,86 @@ static int setup_swap_clusters_info(struct swap_info= _struct *si, return err; } =20 +#ifdef CONFIG_XSWAP +/* Create a file-less xswap device. si->max and the initial nr_clusters + * ceiling are both twice RAM; the runtime size can be lowered afterwards + * via /sys/kernel/mm/xswap/type/limit. + */ +static int xswap_create(int prio) +{ + struct swap_info_struct *si; + unsigned long ram, maxpages; + int error; + + if (prio !=3D DEF_SWAP_PRIO && (prio < 0 || prio > SWAP_FLAG_PRIO_MASK)) + return -EINVAL; + + si =3D alloc_swap_info(); + if (IS_ERR(si)) + return PTR_ERR(si); + + INIT_WORK(&si->discard_work, swap_discard_work); + INIT_WORK(&si->reclaim_work, swap_reclaim_work); + + ram =3D totalram_pages(); + maxpages =3D min_t(unsigned long, ram * 2, swapfile_maximum_size); + /* si->max is an unsigned int: don't overflow it. */ + if (maxpages > UINT_MAX) + maxpages =3D UINT_MAX; + /* Cluster-aligned, so no cluster holds a slot past si->max. */ + if (maxpages > SWAPFILE_CLUSTER) + maxpages =3D rounddown(maxpages, SWAPFILE_CLUSTER); + if (maxpages < 2) + maxpages =3D 2; + + si->bdev =3D NULL; + si->flags |=3D SWP_XSWAP | SWP_SOLIDSTATE; + si->max =3D maxpages; + si->pages =3D maxpages - 1; + /* + * No backing file: setup_swap_extents() is only reachable from the + * file-backed swapon() path, so set ops here. Only ops->flags is + * used, by may_enter_fs(); the IO methods are never called because + * swap_writeout()/swap_read_folio() short circuit xswap. + */ + si->ops =3D &swap_bdev_ops; + + error =3D setup_swap_clusters_info(si, NULL, maxpages); + if (error) + goto bad_swap; + + error =3D zswap_swapon(si->type, si->max); + if (error) + goto bad_swap; + + mutex_lock(&swapon_mutex); + si->prio =3D prio; + si->list.prio =3D -si->prio; + si->avail_list.prio =3D -si->prio; + /* si->swap_file stays NULL: this is a file-less device */ + enable_swap_info(si); + mutex_unlock(&swapon_mutex); + + pr_info("xswap: adding extendable swap type %d (prio %d, %u pages, max %l= u)\n", + si->type, prio, si->pages, maxpages); + atomic_inc(&proc_poll_event); + wake_up_interruptible(&proc_poll_wait); + + return si->type; + +bad_swap: + kfree(si->global_cluster); + si->global_cluster =3D NULL; + destroy_swap_extents(si, NULL); /* safe: xswap never sets SWP_ACTIVATED */ + free_swap_cluster_info(si); + si->cluster_info =3D NULL; + spin_lock(&swap_lock); + si->flags =3D 0; + spin_unlock(&swap_lock); + return error; +} +#endif /* CONFIG_XSWAP */ + SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags) { struct swap_info_struct *si; @@ -4380,6 +4531,8 @@ static int __init swapfile_init(void) swap_migration_ad_supported =3D true; #endif /* CONFIG_MIGRATION */ =20 + xswap_sysfs_init(); + return 0; } subsys_initcall(swapfile_init); --=20 2.54.0 From nobody Fri Sep 25 05:30:54 2026 Received: from mta0.migadu.com (out-12.mta0.migadu.com [91.218.175.12]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id E53AA4AF142 for ; Wed, 16 Sep 2026 10:20:44 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.12 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554060; cv=none; b=RvPtRCe3sTC7XTpcqhfZEjQuWAzmXdyyKEDmjRiBt1ygWB9FtGQQLDyw4v1SgGrrM1AAqdxLBgVMvApi0Kb4pIf5F9uciRQUAYKl8mIShbftHQBY6lzZEvvNLGQNlvOhDFu3aCRacVJjrhG+u5g20ouZFyCx4/ArlYFfWQ/OGGU= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554060; c=relaxed/simple; bh=0F87CNzhepZiFr8WQMyBvHS+j+ezFrEBmG0zT8DT8pU=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-type; b=Jgc1gYyouR8jywT7G1sjqOKY+gaEnvtJ903wNaq5HrYAO3heCwmxyrSdhFs4eHf+Id3hFqOina5tgMKZpkuFRJA1aL2POdUbh1YxrXbwQhnAbTecYhRk9DO/qKLxjdLEor9OLSMPl3gitd/FhcIVrNLL/yhOwXoAXj20V0C3N0s= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn; spf=pass smtp.mailfrom=linux.dev; arc=none smtp.client-ip=91.218.175.12 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev X-Envelope-To: linux-kernel@vger.kernel.org X-Envelope-To: linux-kernel@vger.kernel.org Received: by mta12.migadu.com with ESMTPS id e285610b6a99b574; Wed, 16 Sep 2026 10:20:40 +0000 X-Mizu-Trace-ID: e285610b6a99b574 X-Migadu-Flow: FLOW_OUT From: Baoquan He To: linux-mm@kvack.org Cc: akpm@linux-foundation.org, chrisl@kernel.org, kasong@tencent.com, nphamcs@gmail.com, baohua@kernel.org, youngjun.park@lge.com, hannes@cmpxchg.org, yosry@kernel.org, shikemeng@huaweicloud.com, chengming.zhou@linux.dev, baoquan.he@linux.dev, david@kernel.org, linux-kernel@vger.kernel.org, kunwu.chan@gmail.com, Baoquan He Subject: [PATCH v3 06/14] mm, swap: add xswap grow trigger on cluster allocation Date: Wed, 16 Sep 2026 18:19:13 +0800 Message-ID: <20260916101929.149106-7-hebaoquan@kylinos.cn> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260916101929.149106-1-hebaoquan@kylinos.cn> References: <20260916101929.149106-1-hebaoquan@kylinos.cn> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" When cluster_alloc_swap_entry() fails to find a free cluster and the xswap device still has room to grow, expand the mapped range by XSWAP_GROW_CLUSTERS clusters. Since xswap is always SWP_SOLIDSTATE, global_cluster_lock is never held on this path. This makes the xswap cluster space grow transparently as swap usage increases, without any userspace intervention. Growing maps pages into the VM_SPARSE area, which can sleep. The caller holds local_lock(&percpu_swap_cluster.lock) across the whole slow path, so drop it around xswap_map_clusters() and take it again afterwards; it only protects the per-cpu cluster cache, which this path does not touch. Signed-off-by: Baoquan He --- mm/swapfile.c | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/mm/swapfile.c b/mm/swapfile.c index b0edf5421fc5..17e482059d32 100644 --- a/mm/swapfile.c +++ b/mm/swapfile.c @@ -1273,6 +1273,47 @@ static unsigned long cluster_alloc_swap_entry(struct= swap_info_struct *si, if (found) goto done; } + +#ifdef CONFIG_XSWAP + /* For xswap: grow the cluster_info array, then retry. */ + if (!found && (si->flags & SWP_XSWAP) && + READ_ONCE(si->nr_clusters_mapped) < READ_ONCE(si->nr_clusters_max) && + list_empty(&si->free_clusters)) { + unsigned long nr_new =3D min(READ_ONCE(si->nr_clusters_max) - + READ_ONCE(si->nr_clusters_mapped), + XSWAP_GROW_CLUSTERS); + unsigned long start =3D READ_ONCE(si->nr_clusters_mapped); + unsigned long i; + int ret; + + /* + * Mapping pages can sleep. The lock only guards the per-cpu + * cluster cache, which this path does not touch. + */ + local_unlock(&percpu_swap_cluster.lock); + ret =3D xswap_map_clusters(si, start, nr_new); + local_lock(&percpu_swap_cluster.lock); + + if (!ret) { + for (i =3D start; i < start + nr_new; i++) { + struct swap_cluster_info *ci =3D &si->cluster_info[i]; + + /* + * A concurrent grower may have taken these already; + * only add the off-list ones. + */ + spin_lock(&ci->lock); + if (ci->flags =3D=3D CLUSTER_FLAG_NONE) + move_cluster(si, ci, &si->free_clusters, + CLUSTER_FLAG_FREE); + spin_unlock(&ci->lock); + } + + found =3D alloc_swap_scan_list(si, &si->free_clusters, + folio, false); + } + } +#endif done: if (!(si->flags & SWP_SOLIDSTATE)) spin_unlock(&si->global_cluster_lock); --=20 2.54.0 From nobody Fri Sep 25 05:30:54 2026 Received: from mta0.migadu.com (out-26.mta0.migadu.com [91.218.175.26]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 6DCD74DF4AC for ; Wed, 16 Sep 2026 10:20:49 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.26 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554055; cv=none; b=fPw2DM5TWq/a5DkkfWi9sV3Zs731ODzuGJojlHHJGJCnlXu59q3hh5T3kuESsOp1wcR/NNiLHubWWJ8n+d9dS/HK6EwhNAlY16xclnNeZMY9LMMLEcAmY44MLY2pXTcAnx+3H055GIdBFcsoqiVBDx3aE9suo1pxLR4qJYeJ7d0= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554055; c=relaxed/simple; bh=p1spLcHWbZIGQMljIGWtStjdi/jnP7xL0fS82Z2ENIo=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-type; b=sWVwN7zhM11h9DZJGpzlooFnlLcSrb/aChQYCmlV7IB8JFWxwcApC2rzaqPsYTCe0zg6zoZ7nUud+KxHWf6LoLb4dDveN+cvn7tcPONR6o2M6rZZLCagGXIfmikQ3VEAzxdMkzDy5QUnwAdil4zn712GDoOgVPdOpC59aXjFEe4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn; spf=pass smtp.mailfrom=linux.dev; arc=none smtp.client-ip=91.218.175.26 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev X-Envelope-To: linux-kernel@vger.kernel.org X-Envelope-To: linux-kernel@vger.kernel.org Received: by mta10.migadu.com with ESMTPS id 237cca9ece2c04ea; Wed, 16 Sep 2026 10:20:46 +0000 X-Mizu-Trace-ID: 237cca9ece2c04ea X-Migadu-Flow: FLOW_OUT From: Baoquan He To: linux-mm@kvack.org Cc: akpm@linux-foundation.org, chrisl@kernel.org, kasong@tencent.com, nphamcs@gmail.com, baohua@kernel.org, youngjun.park@lge.com, hannes@cmpxchg.org, yosry@kernel.org, shikemeng@huaweicloud.com, chengming.zhou@linux.dev, baoquan.he@linux.dev, david@kernel.org, linux-kernel@vger.kernel.org, kunwu.chan@gmail.com, Baoquan He Subject: [PATCH v3 07/14] mm, swap: add xswap_try_shrink and shrink trigger on cluster free Date: Wed, 16 Sep 2026 18:19:14 +0800 Message-ID: <20260916101929.149106-8-hebaoquan@kylinos.cn> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260916101929.149106-1-hebaoquan@kylinos.cn> References: <20260916101929.149106-1-hebaoquan@kylinos.cn> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add xswap_try_shrink() to unmap the free clusters at the tail of the mapped range. It only reclaims when the range is at most half in use, leaving one chunk of slack for the next allocation. Call it from __free_cluster() after a cluster is released. Signed-off-by: Baoquan He --- mm/swapfile.c | 100 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 94 insertions(+), 6 deletions(-) diff --git a/mm/swapfile.c b/mm/swapfile.c index 17e482059d32..2a03b13c0ed2 100644 --- a/mm/swapfile.c +++ b/mm/swapfile.c @@ -54,12 +54,13 @@ =20 #ifdef CONFIG_XSWAP /* - * xswap: dynamically grow the cluster_info array via a VM_SPARSE area. + * xswap: dynamically grow and shrink the cluster_info array via a + * VM_SPARSE area. * - * XSWAP_GROW_CLUSTERS is the number of clusters to map in one grow - * operation. It is set to the number of cluster_info structs that - * fit in a single page (at least 16), so that the vmalloc page table - * overhead is proportional to the number of clusters mapped. + * XSWAP_GROW_CLUSTERS is the number of clusters to map/unmap in one + * grow/shrink operation: the number of cluster_info structs that fit in + * a single page (at least 16), so that the vmalloc page table overhead + * is proportional to the number of clusters mapped. */ #define XSWAP_GROW_CLUSTERS \ max_t(unsigned long, PAGE_SIZE / sizeof(struct swap_cluster_info), 16) @@ -69,6 +70,7 @@ static int xswap_map_clusters(struct swap_info_struct *si, static void xswap_unmap_clusters(struct swap_info_struct *si, unsigned long start_idx, unsigned long nr); static int xswap_mapped_end(pte_t *pte, unsigned long addr, void *data); +static void xswap_try_shrink(struct swap_info_struct *si); =20 static int xswap_create(int prio); =20 @@ -696,6 +698,9 @@ static void __free_cluster(struct swap_info_struct *si,= struct swap_cluster_info swap_cluster_free_table(ci); move_cluster(si, ci, &si->free_clusters, CLUSTER_FLAG_FREE); ci->order =3D 0; +#ifdef CONFIG_XSWAP + xswap_try_shrink(si); +#endif } =20 /* @@ -1063,6 +1068,9 @@ static unsigned int alloc_swap_scan_cluster(struct sw= ap_info_struct *si, lockdep_assert_held(&ci->lock); VM_WARN_ON(!cluster_is_usable(ci, order)); =20 + /* ci is used without ci->lock; an xswap unmap waits for this. */ + rcu_read_lock(); + if (end < nr_pages || ci->count + nr_pages > SWAPFILE_CLUSTER) goto out; =20 @@ -1091,6 +1099,7 @@ static unsigned int alloc_swap_scan_cluster(struct sw= ap_info_struct *si, out: relocate_cluster(si, ci); swap_cluster_unlock(ci); + rcu_read_unlock(); if (si->flags & SWP_SOLIDSTATE) { this_cpu_write(percpu_swap_cluster.offset[order], next); this_cpu_write(percpu_swap_cluster.si[order], si); @@ -1134,6 +1143,9 @@ static void swap_reclaim_full_clusters(struct swap_in= fo_struct *si, bool force) to_scan =3D swap_usage_in_pages(si) / SWAPFILE_CLUSTER; =20 while ((ci =3D isolate_lock_cluster(si, &si->full_clusters))) { + /* As in alloc_swap_scan_cluster(). */ + rcu_read_lock(); + offset =3D cluster_offset(si, ci); end =3D min(si->max, offset + SWAPFILE_CLUSTER); to_scan--; @@ -1158,6 +1170,7 @@ static void swap_reclaim_full_clusters(struct swap_in= fo_struct *si, bool force) relocate_cluster(si, ci); =20 swap_cluster_unlock(ci); + rcu_read_unlock(); if (to_scan <=3D 0) break; =20 @@ -1506,11 +1519,17 @@ static bool swap_alloc_fast(struct folio *folio) /* * Once allocated, swap_info_struct will never be completely freed, * so checking it's liveness by get_swap_device_info is enough. + * + * The cached offset indexes si->cluster_info, which xswap can + * unmap; cover both the read and the use with RCU. */ + rcu_read_lock(); si =3D this_cpu_read(percpu_swap_cluster.si[order]); offset =3D this_cpu_read(percpu_swap_cluster.offset[order]); - if (!si || !offset || !get_swap_device_info(si)) + if (!si || !offset || !get_swap_device_info(si)) { + rcu_read_unlock(); return false; + } =20 ci =3D swap_cluster_lock(si, offset); if (cluster_is_usable(ci, order)) { @@ -1522,6 +1541,7 @@ static bool swap_alloc_fast(struct folio *folio) } =20 put_swap_device(si); + rcu_read_unlock(); return folio_test_swapcache(folio); } =20 @@ -2312,8 +2332,11 @@ swp_entry_t swap_alloc_hibernation_slot(int type) /* * Try the local cluster first if it matches the device. If * not, try grab a new cluster and override local cluster. + * + * Same RCU requirement as swap_alloc_fast(). */ local_lock(&percpu_swap_cluster.lock); + rcu_read_lock(); pcp_si =3D this_cpu_read(percpu_swap_cluster.si[0]); pcp_offset =3D this_cpu_read(percpu_swap_cluster.offset[0]); if (pcp_si =3D=3D si && pcp_offset) { @@ -2323,6 +2346,7 @@ swp_entry_t swap_alloc_hibernation_slot(int type) else swap_cluster_unlock(ci); } + rcu_read_unlock(); if (!offset) offset =3D cluster_alloc_swap_entry(si, NULL); local_unlock(&percpu_swap_cluster.lock); @@ -3981,6 +4005,15 @@ static void xswap_unmap_clusters(struct swap_info_st= ruct *si, return; } =20 + /* + * A per-cpu cluster cache can still hold an offset in this range. + * Invalidate those references, then wait out the readers that have + * already loaded one, so that nobody can dereference cluster_info + * past this point. swapoff() needs the same before it releases. + */ + flush_percpu_swap_cluster(si); + synchronize_rcu(); + vm_area_unmap_pages(si->cluster_vm, vm_start, vm_end); /* vm_area_unmap_pages() clears PTEs but does not free pages. */ /* TODO: free backing pages via page table walk or tracking bitmap */ @@ -3999,6 +4032,61 @@ static int xswap_mapped_end(pte_t *pte, unsigned lon= g addr, void *data) *mapped_end =3D addr + PAGE_SIZE; return 0; } + +/* + * Automatic reclaim: leave one chunk of the free tail mapped as slack, so + * that the next allocation does not grow the range straight back, and only + * unmap once several chunks can go, so the unmap is worth the RCU grace + * period it costs. + */ +#define XSWAP_SHRINK_SLACK XSWAP_GROW_CLUSTERS +#define XSWAP_SHRINK_MIN (XSWAP_GROW_CLUSTERS * 4) + +/* + * Try to shrink the cluster_info tail: unmap contiguous free clusters + * at the end of the mapped range. + */ +static void xswap_try_shrink(struct swap_info_struct *si) +{ + struct swap_cluster_info *ci; + unsigned long nr_mapped, last, idx; + + if (!(si->flags & SWP_XSWAP)) + return; + + nr_mapped =3D READ_ONCE(si->nr_clusters_mapped); + if (nr_mapped <=3D 1) /* keep cluster 0 */ + return; + + /* + * Reclaim on our own, but only once the mapped range is at most + * half in use: growth is demand driven, so reclaiming on a smaller + * dip would only map the same clusters again, and every unmap costs + * an RCU grace period. + */ + if (swap_usage_in_pages(si) * 2 > nr_mapped * SWAPFILE_CLUSTER) + return; + + /* Find the last non-free cluster from the tail */ + last =3D nr_mapped; + while (last > 1) { + idx =3D last - 1; + ci =3D &si->cluster_info[idx]; + if (ci->count || ci->flags !=3D CLUSTER_FLAG_FREE) + break; + last =3D idx; + } + + if (last =3D=3D nr_mapped) + return; /* nothing to shrink */ + + if (nr_mapped - last < XSWAP_SHRINK_SLACK + XSWAP_SHRINK_MIN) + return; + + last +=3D XSWAP_SHRINK_SLACK; + + xswap_unmap_clusters(si, last, nr_mapped - last); +} #endif /* CONFIG_XSWAP */ =20 static int setup_swap_clusters_info(struct swap_info_struct *si, --=20 2.54.0 From nobody Fri Sep 25 05:30:54 2026 Received: from mta0.migadu.com (out-59.mta0.migadu.com [91.218.175.59]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 2E10B4A4857 for ; Wed, 16 Sep 2026 10:21:04 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.59 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554102; cv=none; b=UehfGsfsWU/qTPcRudtX9GoMsy+FxTYwjtuZpr1IGApddtuUJ4p11DCoxbWQFg0T01EMVVV8gbbKxMdOYJepa693XXyDFlPEyOhMJ06+rwlsLVUIGq8qnOB7wl+aXvyzWIbCGps4rdm7W1kUoNYFloMd6Vexvl4FDXcCUDjbXXk= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554102; c=relaxed/simple; bh=CAlNcB8919pfZFulTu4TOgmmav1vTd8i+vG2Hbaq4+8=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-type; b=GD5+VpYXfRinCcTqSXieV8RtpYMTbP7ruuet7D+BdqfFgQh433ie5GXE7INacusxzS5hZq8Kh+49MGQ+3qVYXbT2DSJBh5JVNqUYLQurtA8+Oaag7FXGEaf9/NofwGs6EvYD5p2BbSd6grbUOH/IBor42CrLhY5xTxNtc+kaKNE= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn; spf=pass smtp.mailfrom=linux.dev; arc=none smtp.client-ip=91.218.175.59 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev X-Envelope-To: linux-kernel@vger.kernel.org X-Envelope-To: linux-kernel@vger.kernel.org Received: by mta12.migadu.com with ESMTPS id 3cf0af0f25f6a445; Wed, 16 Sep 2026 10:20:54 +0000 X-Mizu-Trace-ID: 3cf0af0f25f6a445 X-Migadu-Flow: FLOW_OUT From: Baoquan He To: linux-mm@kvack.org Cc: akpm@linux-foundation.org, chrisl@kernel.org, kasong@tencent.com, nphamcs@gmail.com, baohua@kernel.org, youngjun.park@lge.com, hannes@cmpxchg.org, yosry@kernel.org, shikemeng@huaweicloud.com, chengming.zhou@linux.dev, baoquan.he@linux.dev, david@kernel.org, linux-kernel@vger.kernel.org, kunwu.chan@gmail.com, Baoquan He Subject: [PATCH v3 08/14] mm, swap: free backing pages in xswap_unmap_clusters Date: Wed, 16 Sep 2026 18:19:15 +0800 Message-ID: <20260916101929.149106-9-hebaoquan@kylinos.cn> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260916101929.149106-1-hebaoquan@kylinos.cn> References: <20260916101929.149106-1-hebaoquan@kylinos.cn> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" vm_area_unmap_pages() does not free the backing pages that xswap_map_clusters() allocated, so they leaked on every unmap. Collect the backing pages from the PTEs before unmapping and free them after the PTEs are cleared. The collection array is allocated under memalloc_noreclaim_save(); on failure, return -ENOMEM without unmapping. Signed-off-by: Baoquan He --- mm/swapfile.c | 76 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 11 deletions(-) diff --git a/mm/swapfile.c b/mm/swapfile.c index 2a03b13c0ed2..5b31aacb3ec5 100644 --- a/mm/swapfile.c +++ b/mm/swapfile.c @@ -67,8 +67,8 @@ =20 static int xswap_map_clusters(struct swap_info_struct *si, unsigned long start_idx, unsigned long nr); -static void xswap_unmap_clusters(struct swap_info_struct *si, - unsigned long start_idx, unsigned long nr); +static int xswap_unmap_clusters(struct swap_info_struct *si, + unsigned long start_idx, unsigned long nr); static int xswap_mapped_end(pte_t *pte, unsigned long addr, void *data); static void xswap_try_shrink(struct swap_info_struct *si); =20 @@ -3343,9 +3343,14 @@ static void free_swap_cluster_info(struct swap_info_= struct *si) } spin_unlock(&ci->lock); } - /* Unmap all mapped clusters and free the VM_SPARSE area */ - if (si->nr_clusters_mapped > 0) - xswap_unmap_clusters(si, 0, si->nr_clusters_mapped); + /* + * free_vm_area() drops the mapping without freeing the pages, + * so the unmap has to succeed first. Retry; its only failure + * is a transient -ENOMEM while collecting the backing pages. + */ + while (si->nr_clusters_mapped > 0 && + xswap_unmap_clusters(si, 0, si->nr_clusters_mapped)) + cond_resched(); free_vm_area(si->cluster_vm); si->cluster_vm =3D NULL; si->cluster_info =3D NULL; @@ -3988,21 +3993,44 @@ static int xswap_map_clusters(struct swap_info_stru= ct *si, return -ENOMEM; } =20 -static void xswap_unmap_clusters(struct swap_info_struct *si, - unsigned long start_idx, unsigned long nr) +struct xswap_page_data { + struct page **pages; + int nr; + int max; +}; + +static int xswap_collect_page(pte_t *pte, unsigned long addr, void *data) +{ + struct xswap_page_data *xpd =3D data; + pte_t pteval =3D ptep_get(pte); + + if (!pte_present(pteval)) + return 0; + if (xpd->nr < xpd->max) + xpd->pages[xpd->nr++] =3D pte_page(pteval); + return 0; +} + +static int xswap_unmap_clusters(struct swap_info_struct *si, + unsigned long start_idx, unsigned long nr) { unsigned long start_addr =3D (unsigned long)si->cluster_info + (size_t)start_idx * sizeof(struct swap_cluster_info); unsigned long end_addr =3D start_addr + (size_t)nr * sizeof(struct swap_c= luster_info); unsigned long vm_start =3D PAGE_ALIGN(start_addr); unsigned long vm_end =3D PAGE_ALIGN(end_addr); + unsigned long size; + unsigned long npages; + struct xswap_page_data xpd; + unsigned int noreclaim_flags; + int i; =20 mutex_lock(&si->xswap_lock); =20 if (vm_start >=3D vm_end) { WRITE_ONCE(si->nr_clusters_mapped, start_idx); mutex_unlock(&si->xswap_lock); - return; + return 0; } =20 /* @@ -4014,12 +4042,32 @@ static void xswap_unmap_clusters(struct swap_info_s= truct *si, flush_percpu_swap_cluster(si); synchronize_rcu(); =20 + size =3D vm_end - vm_start; + npages =3D size >> PAGE_SHIFT; + + noreclaim_flags =3D memalloc_noreclaim_save(); + xpd.pages =3D kmalloc_array(npages, sizeof(*xpd.pages), + __GFP_HIGH | __GFP_NOMEMALLOC | GFP_KERNEL); + memalloc_noreclaim_restore(noreclaim_flags); + if (!xpd.pages) { + mutex_unlock(&si->xswap_lock); + return -ENOMEM; + } + + xpd.nr =3D 0; + xpd.max =3D npages; + apply_to_existing_page_range(&init_mm, vm_start, size, + xswap_collect_page, &xpd); + vm_area_unmap_pages(si->cluster_vm, vm_start, vm_end); - /* vm_area_unmap_pages() clears PTEs but does not free pages. */ - /* TODO: free backing pages via page table walk or tracking bitmap */ + + for (i =3D 0; i < xpd.nr; i++) + __free_page(xpd.pages[i]); + kfree(xpd.pages); =20 WRITE_ONCE(si->nr_clusters_mapped, start_idx); mutex_unlock(&si->xswap_lock); + return 0; } =20 /* Track the end of the run of pages that is already mapped. */ @@ -4152,7 +4200,13 @@ static int setup_swap_clusters_info(struct swap_info= _struct *si, return 0; =20 err_unmap: - xswap_unmap_clusters(si, 0, si->nr_clusters_mapped); + /* + * Retry until the unmap succeeds. Its only failure is a transient + * -ENOMEM while collecting the backing pages. + */ + while (si->nr_clusters_mapped > 0 && + xswap_unmap_clusters(si, 0, si->nr_clusters_mapped)) + cond_resched(); err_free_vm: free_vm_area(si->cluster_vm); si->cluster_vm =3D NULL; --=20 2.54.0 From nobody Fri Sep 25 05:30:54 2026 Received: from mta0.migadu.com (out-56.mta0.migadu.com [91.218.175.56]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id A8DEB4D0A16 for ; Wed, 16 Sep 2026 10:21:02 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.56 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554068; cv=none; b=aOJ1vADvMg9BsDgmkNByvsRZdG0uvInVyzHxLtEFAZUWLJiq47+rQ0Zqu+hDe+DZSd5wzJZL9C/3tRd/+dmz4vOPTUe/nLDYvDv2Uyn2CyOvc6aLNcId6mFCQgkBYLY+LV+ogiI6+LVzZg+HdtV66V7zOpf6FfMj4k58j3N1iJs= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554068; c=relaxed/simple; bh=hQFm+ztyqfHm4WmBVA5k4KCVpvALbCnKJ1xGUud3S2o=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-type; b=fQ1HN9RglFCCtPl0hyrour98n09BSTRqYQabEpoFmJS3GIE+cADUvF8oo3BZYy9CJphCQa8UF1NUxqxRABRRgGTkSUTouuO9r7iLU8TipRUfPaei3MZ+bHx+r2UllkD3I+pircmF5Y4xmU3tMSMEq40fcJcxiDYWVX7+eWrtN/k= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn; spf=pass smtp.mailfrom=linux.dev; arc=none smtp.client-ip=91.218.175.56 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev X-Envelope-To: linux-kernel@vger.kernel.org X-Envelope-To: linux-kernel@vger.kernel.org Received: by mta12.migadu.com with ESMTPS id ae7a14221eb78435; Wed, 16 Sep 2026 10:20:57 +0000 X-Mizu-Trace-ID: ae7a14221eb78435 X-Migadu-Flow: FLOW_OUT From: Baoquan He To: linux-mm@kvack.org Cc: akpm@linux-foundation.org, chrisl@kernel.org, kasong@tencent.com, nphamcs@gmail.com, baohua@kernel.org, youngjun.park@lge.com, hannes@cmpxchg.org, yosry@kernel.org, shikemeng@huaweicloud.com, chengming.zhou@linux.dev, baoquan.he@linux.dev, david@kernel.org, linux-kernel@vger.kernel.org, kunwu.chan@gmail.com, Baoquan He Subject: [PATCH v3 09/14] mm, swap: defer xswap shrink to workqueue to avoid lock recursion Date: Wed, 16 Sep 2026 18:19:16 +0800 Message-ID: <20260916101929.149106-10-hebaoquan@kylinos.cn> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260916101929.149106-1-hebaoquan@kylinos.cn> References: <20260916101929.149106-1-hebaoquan@kylinos.cn> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" __free_cluster() called xswap_try_shrink() while holding ci->lock, but shrinking unmaps the backing pages and the subsequent unlock faults on the unmapped address. Run the shrink via schedule_work() instead, so no cluster lock is held. The work is only scheduled for xswap devices and is cancelled on swapoff. Signed-off-by: Baoquan He --- include/linux/swap.h | 1 + mm/swapfile.c | 123 +++++++++++++++++++++++++++++++++---------- 2 files changed, 97 insertions(+), 27 deletions(-) diff --git a/include/linux/swap.h b/include/linux/swap.h index 8c62a53667bb..30642bb481df 100644 --- a/include/linux/swap.h +++ b/include/linux/swap.h @@ -246,6 +246,7 @@ struct swap_info_struct { struct vm_struct *cluster_vm; /* VM_SPARSE area for cluster_info */ unsigned long nr_clusters_max;/* total clusters in the xswap address spa= ce */ unsigned long nr_clusters_mapped; /* currently mapped cluster count */ + struct work_struct xswap_shrink_work; /* deferred shrink trigger */ struct mutex xswap_lock; /* serialize map/unmap operations */ #endif struct list_head free_clusters; /* free clusters list */ diff --git a/mm/swapfile.c b/mm/swapfile.c index 5b31aacb3ec5..351c68bcd70b 100644 --- a/mm/swapfile.c +++ b/mm/swapfile.c @@ -699,7 +699,9 @@ static void __free_cluster(struct swap_info_struct *si,= struct swap_cluster_info move_cluster(si, ci, &si->free_clusters, CLUSTER_FLAG_FREE); ci->order =3D 0; #ifdef CONFIG_XSWAP - xswap_try_shrink(si); + /* Only xswap devices, and not while the device is being torn down. */ + if ((si->flags & SWP_XSWAP) && (si->flags & SWP_WRITEOK)) + schedule_work(&si->xswap_shrink_work); #endif } =20 @@ -3328,6 +3330,7 @@ static void free_swap_cluster_info(struct swap_info_s= truct *si) if (si->flags & SWP_XSWAP) { unsigned long nr_mapped; =20 + cancel_work_sync(&si->xswap_shrink_work); /* * Cluster 0 keeps the bad header slot, so it never empties * and __free_cluster() never frees its table. @@ -3452,6 +3455,11 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specia= lfile) spin_unlock(&p->lock); spin_unlock(&swap_lock); =20 +#ifdef CONFIG_XSWAP + if (p->flags & SWP_XSWAP) + cancel_work_sync(&p->xswap_shrink_work); +#endif + wait_for_allocation(p); =20 set_current_oom_origin(); @@ -4011,8 +4019,9 @@ static int xswap_collect_page(pte_t *pte, unsigned lo= ng addr, void *data) return 0; } =20 -static int xswap_unmap_clusters(struct swap_info_struct *si, - unsigned long start_idx, unsigned long nr) +/* Caller must hold si->xswap_lock; -ENOMEM leaves the mapping intact. */ +static int xswap_unmap_clusters_locked(struct swap_info_struct *si, + unsigned long start_idx, unsigned long nr) { unsigned long start_addr =3D (unsigned long)si->cluster_info + (size_t)start_idx * sizeof(struct swap_cluster_info); @@ -4025,11 +4034,8 @@ static int xswap_unmap_clusters(struct swap_info_str= uct *si, unsigned int noreclaim_flags; int i; =20 - mutex_lock(&si->xswap_lock); - if (vm_start >=3D vm_end) { WRITE_ONCE(si->nr_clusters_mapped, start_idx); - mutex_unlock(&si->xswap_lock); return 0; } =20 @@ -4049,10 +4055,8 @@ static int xswap_unmap_clusters(struct swap_info_str= uct *si, xpd.pages =3D kmalloc_array(npages, sizeof(*xpd.pages), __GFP_HIGH | __GFP_NOMEMALLOC | GFP_KERNEL); memalloc_noreclaim_restore(noreclaim_flags); - if (!xpd.pages) { - mutex_unlock(&si->xswap_lock); + if (!xpd.pages) return -ENOMEM; - } =20 xpd.nr =3D 0; xpd.max =3D npages; @@ -4066,10 +4070,20 @@ static int xswap_unmap_clusters(struct swap_info_st= ruct *si, kfree(xpd.pages); =20 WRITE_ONCE(si->nr_clusters_mapped, start_idx); - mutex_unlock(&si->xswap_lock); return 0; } =20 +static int xswap_unmap_clusters(struct swap_info_struct *si, + unsigned long start_idx, unsigned long nr) +{ + int ret; + + mutex_lock(&si->xswap_lock); + ret =3D xswap_unmap_clusters_locked(si, start_idx, nr); + mutex_unlock(&si->xswap_lock); + return ret; +} + /* Track the end of the run of pages that is already mapped. */ static int xswap_mapped_end(pte_t *pte, unsigned long addr, void *data) { @@ -4090,6 +4104,16 @@ static int xswap_mapped_end(pte_t *pte, unsigned lon= g addr, void *data) #define XSWAP_SHRINK_SLACK XSWAP_GROW_CLUSTERS #define XSWAP_SHRINK_MIN (XSWAP_GROW_CLUSTERS * 4) =20 +static void xswap_shrink_work_fn(struct work_struct *work) +{ + struct swap_info_struct *si =3D container_of(work, + struct swap_info_struct, xswap_shrink_work); + + if (!(READ_ONCE(si->flags) & SWP_WRITEOK)) + return; + xswap_try_shrink(si); +} + /* * Try to shrink the cluster_info tail: unmap contiguous free clusters * at the end of the mapped range. @@ -4097,14 +4121,16 @@ static int xswap_mapped_end(pte_t *pte, unsigned lo= ng addr, void *data) static void xswap_try_shrink(struct swap_info_struct *si) { struct swap_cluster_info *ci; - unsigned long nr_mapped, last, idx; + unsigned long nr_mapped, nr_tail, nr_unmap, start_idx, i; =20 if (!(si->flags & SWP_XSWAP)) return; =20 + mutex_lock(&si->xswap_lock); + nr_mapped =3D READ_ONCE(si->nr_clusters_mapped); - if (nr_mapped <=3D 1) /* keep cluster 0 */ - return; + if (nr_mapped <=3D 1) /* keep cluster 0 */ + goto out_unlock; =20 /* * Reclaim on our own, but only once the mapped range is at most @@ -4113,27 +4139,69 @@ static void xswap_try_shrink(struct swap_info_struc= t *si) * an RCU grace period. */ if (swap_usage_in_pages(si) * 2 > nr_mapped * SWAPFILE_CLUSTER) - return; + goto out_unlock; =20 - /* Find the last non-free cluster from the tail */ - last =3D nr_mapped; - while (last > 1) { - idx =3D last - 1; - ci =3D &si->cluster_info[idx]; - if (ci->count || ci->flags !=3D CLUSTER_FLAG_FREE) + /* + * Count the free clusters at the tail of the mapped range. Scanned, + * not tracked: the count must be exact to size the unmap, and an + * incremental count falls behind on out-of-order frees. + */ + nr_tail =3D 0; + while (nr_mapped - nr_tail > 1) { + ci =3D &si->cluster_info[nr_mapped - nr_tail - 1]; + if (READ_ONCE(ci->count) || + READ_ONCE(ci->flags) !=3D CLUSTER_FLAG_FREE) break; - last =3D idx; + nr_tail++; } + if (nr_tail < XSWAP_SHRINK_SLACK + XSWAP_SHRINK_MIN) + goto out_unlock; =20 - if (last =3D=3D nr_mapped) - return; /* nothing to shrink */ + nr_unmap =3D rounddown(nr_tail - XSWAP_SHRINK_SLACK, XSWAP_GROW_CLUSTERS); + if (!nr_unmap) + goto out_unlock; + start_idx =3D nr_mapped - nr_unmap; =20 - if (nr_mapped - last < XSWAP_SHRINK_SLACK + XSWAP_SHRINK_MIN) - return; + /* + * Only shrink a run that reaches the mapped end; otherwise + * truncating nr_clusters_mapped would orphan the active tail. + */ + spin_lock(&si->lock); + for (i =3D start_idx; i < nr_mapped; i++) { + ci =3D &si->cluster_info[i]; + if (READ_ONCE(ci->flags) !=3D CLUSTER_FLAG_FREE) + break; + if (!spin_trylock(&ci->lock)) { + spin_unlock(&si->lock); + goto out_unlock; + } + spin_unlock(&ci->lock); + } + if (i !=3D nr_mapped) { + spin_unlock(&si->lock); + goto out_unlock; + } =20 - last +=3D XSWAP_SHRINK_SLACK; + for (i =3D start_idx; i < nr_mapped; i++) { + ci =3D &si->cluster_info[i]; + list_del_init(&ci->list); + WRITE_ONCE(ci->flags, CLUSTER_FLAG_NONE); + } + spin_unlock(&si->lock); =20 - xswap_unmap_clusters(si, last, nr_mapped - last); + if (xswap_unmap_clusters_locked(si, start_idx, nr_unmap)) { + spin_lock(&si->lock); + for (i =3D start_idx; i < nr_mapped; i++) { + ci =3D &si->cluster_info[i]; + WRITE_ONCE(ci->flags, CLUSTER_FLAG_FREE); + list_add_tail(&ci->list, &si->free_clusters); + } + spin_unlock(&si->lock); + goto out_unlock; + } + +out_unlock: + mutex_unlock(&si->xswap_lock); } #endif /* CONFIG_XSWAP */ =20 @@ -4197,6 +4265,7 @@ static int setup_swap_clusters_info(struct swap_info_= struct *si, } } =20 + INIT_WORK(&si->xswap_shrink_work, xswap_shrink_work_fn); return 0; =20 err_unmap: --=20 2.54.0 From nobody Fri Sep 25 05:30:54 2026 Received: from mta0.migadu.com (out-72.mta0.migadu.com [91.218.175.72]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 0BE894D09E3 for ; Wed, 16 Sep 2026 10:21:08 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.72 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554089; cv=none; b=QJywrC3ftw9Jt5i+UFm6ughvDSkmouUrqEycLPQHBooVEu37BhLpjAcWwNbVUjr1DcDXsPZ7R6wtzqMK66Vn7fdFGswn6Fi771Sls4eA7whs0Ds47xd7cxvvCECeMZtsVrwS7hqfk3bkYhrCnk50Ynvt12mMhUejxpkIDmxo6qw= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554089; c=relaxed/simple; bh=HQpQjFii12VM67DrgLicu0P6XWPTsuLoPpIRw/KKdok=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-type; b=qBXEg1iFUr2RIs1tFaukFCIrtF3SIaItqoaf4S6faxtoCNQYn2oOVn9R8btOtNBu2grVTRtBK+G/GVdcH5yS6/SnEsSlif5lfJkyDbgof7ZYiqaQtRApZTWxWB4V7h9pmdj4ydS0bokYE3i7L692CakrS9U3rDQCXGqa0xc5ukE= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn; spf=pass smtp.mailfrom=linux.dev; arc=none smtp.client-ip=91.218.175.72 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev X-Envelope-To: linux-kernel@vger.kernel.org X-Envelope-To: linux-kernel@vger.kernel.org Received: by mta12.migadu.com with ESMTPS id 42f42ab4e84ad3fd; Wed, 16 Sep 2026 10:21:05 +0000 X-Mizu-Trace-ID: 42f42ab4e84ad3fd X-Migadu-Flow: FLOW_OUT From: Baoquan He To: linux-mm@kvack.org Cc: akpm@linux-foundation.org, chrisl@kernel.org, kasong@tencent.com, nphamcs@gmail.com, baohua@kernel.org, youngjun.park@lge.com, hannes@cmpxchg.org, yosry@kernel.org, shikemeng@huaweicloud.com, chengming.zhou@linux.dev, baoquan.he@linux.dev, david@kernel.org, linux-kernel@vger.kernel.org, kunwu.chan@gmail.com, Baoquan He Subject: [PATCH v3 10/14] mm, swap: refactor swapoff and add xswap_destroy Date: Wed, 16 Sep 2026 18:19:17 +0800 Message-ID: <20260916101929.149106-11-hebaoquan@kylinos.cn> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260916101929.149106-1-hebaoquan@kylinos.cn> References: <20260916101929.149106-1-hebaoquan@kylinos.cn> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Extract __swapoff() from sys_swapoff() so the teardown logic can be shared, and make it work for file-less devices. Add xswap_destroy() to tear down a file-less xswap device by swap type, exposed via /sys/kernel/mm/xswap/destroy. Writing a swap type tears down that device; it requires CAP_SYS_ADMIN. Signed-off-by: Baoquan He --- mm/swapfile.c | 205 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 151 insertions(+), 54 deletions(-) diff --git a/mm/swapfile.c b/mm/swapfile.c index 351c68bcd70b..cdcbcbaa6d87 100644 --- a/mm/swapfile.c +++ b/mm/swapfile.c @@ -73,6 +73,7 @@ static int xswap_mapped_end(pte_t *pte, unsigned long add= r, void *data); static void xswap_try_shrink(struct swap_info_struct *si); =20 static int xswap_create(int prio); +static int xswap_destroy(int type); =20 static ssize_t xswap_create_store(struct kobject *kobj, struct kobj_attribute *attr, @@ -103,8 +104,35 @@ static ssize_t xswap_create_store(struct kobject *kobj, static struct kobj_attribute xswap_create_attr =3D __ATTR(create, 0200, NU= LL, xswap_create_store); =20 +static ssize_t xswap_destroy_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + unsigned long type; + int err; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + err =3D kstrtoul(buf, 0, &type); + if (err) + return err; + if (type >=3D MAX_SWAPFILES) + return -EINVAL; + + err =3D xswap_destroy(type); + if (err) + return err; + + return count; +} + +static struct kobj_attribute xswap_destroy_attr =3D __ATTR(destroy, 0200, = NULL, + xswap_destroy_store); + static struct attribute *xswap_attrs[] =3D { &xswap_create_attr.attr, + &xswap_destroy_attr.attr, NULL, }; =20 @@ -3399,65 +3427,44 @@ static void flush_percpu_swap_cluster(struct swap_i= nfo_struct *si) } =20 =20 -SYSCALL_DEFINE1(swapoff, const char __user *, specialfile) +/* + * Drop @p from the avail and active lists and undo its accounting. The + * caller must hold swap_lock and have checked that @p is WRITEOK and not + * pinned for hibernation. + * + * Returns 0, or -ENOMEM with swap_lock still held. + */ +static int swap_info_remove(struct swap_info_struct *p) { - struct swap_info_struct *p =3D NULL; - struct file *swap_file, *victim; - struct address_space *mapping; - struct inode *inode; - int err, found =3D 0; - - if (!capable(CAP_SYS_ADMIN)) - return -EPERM; - - BUG_ON(!current->mm); - - CLASS(filename, pathname)(specialfile); - victim =3D file_open_name(pathname, O_RDWR|O_LARGEFILE, 0); - if (IS_ERR(victim)) - return PTR_ERR(victim); - - mapping =3D victim->f_mapping; - spin_lock(&swap_lock); - plist_for_each_entry(p, &swap_active_head, list) { - if (p->flags & SWP_WRITEOK) { - if (p->swap_file->f_mapping =3D=3D mapping) { - found =3D 1; - break; - } - } - } - if (!found) { - err =3D -EINVAL; - spin_unlock(&swap_lock); - goto out_dput; - } - - /* Refuse swapoff while the device is pinned for hibernation */ - if (p->flags & SWP_HIBERNATION) { - err =3D -EBUSY; - spin_unlock(&swap_lock); - goto out_dput; - } - if (!security_vm_enough_memory_mm(current->mm, p->pages)) vm_unacct_memory(p->pages); - else { - err =3D -ENOMEM; - spin_unlock(&swap_lock); - goto out_dput; - } + else + return -ENOMEM; + spin_lock(&p->lock); del_from_avail_list(p, true); plist_del(&p->list, &swap_active_head); atomic_long_sub(p->pages, &nr_swap_pages); total_swap_pages -=3D p->pages; spin_unlock(&p->lock); - spin_unlock(&swap_lock); + return 0; +} + +/* Common swap teardown after list removal; shared by sys_swapoff() and + * xswap_destroy(). + */ +static int __swapoff(struct swap_info_struct *p) +{ + struct file *swap_file =3D NULL; + int err; =20 #ifdef CONFIG_XSWAP - if (p->flags & SWP_XSWAP) + if (p->flags & SWP_XSWAP) { cancel_work_sync(&p->xswap_shrink_work); + /* Wait out a shrink racing us from the sysfs write path. */ + mutex_lock(&p->xswap_lock); + mutex_unlock(&p->xswap_lock); + } #endif =20 wait_for_allocation(p); @@ -3469,7 +3476,7 @@ SYSCALL_DEFINE1(swapoff, const char __user *, special= file) if (err) { /* re-insert swap space back into swap_list */ reinsert_swap_info(p); - goto out_dput; + return err; } =20 /* @@ -3509,15 +3516,21 @@ SYSCALL_DEFINE1(swapoff, const char __user *, speci= alfile) kfree(p->global_cluster); p->global_cluster =3D NULL; free_swap_cluster_info(p); + /* + * The device is off swap_active_head and no longer WRITEOK, so no + * reader can observe these; clearing them here needs no lock. + */ p->max =3D 0; p->cluster_info =3D NULL; =20 - inode =3D mapping->host; + if (swap_file) { + struct inode *inode =3D swap_file->f_mapping->host; =20 - inode_lock(inode); - inode->i_flags &=3D ~S_SWAPFILE; - inode_unlock(inode); - filp_close(swap_file, NULL); + inode_lock(inode); + inode->i_flags &=3D ~S_SWAPFILE; + inode_unlock(inode); + filp_close(swap_file, NULL); + } =20 /* * Clear the SWP_USED flag after all resources are freed so that swapon @@ -3528,10 +3541,61 @@ SYSCALL_DEFINE1(swapoff, const char __user *, speci= alfile) p->flags =3D 0; spin_unlock(&swap_lock); =20 - err =3D 0; atomic_inc(&proc_poll_event); wake_up_interruptible(&proc_poll_wait); =20 + return 0; +} + +SYSCALL_DEFINE1(swapoff, const char __user *, specialfile) +{ + struct swap_info_struct *p =3D NULL; + struct file *victim; + struct address_space *mapping; + int err, found =3D 0; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + BUG_ON(!current->mm); + + CLASS(filename, pathname)(specialfile); + victim =3D file_open_name(pathname, O_RDWR|O_LARGEFILE, 0); + if (IS_ERR(victim)) + return PTR_ERR(victim); + + mapping =3D victim->f_mapping; + spin_lock(&swap_lock); + plist_for_each_entry(p, &swap_active_head, list) { + if (p->flags & SWP_WRITEOK) { + if (p->swap_file && p->swap_file->f_mapping =3D=3D mapping) { + found =3D 1; + break; + } + } + } + if (!found) { + err =3D -EINVAL; + spin_unlock(&swap_lock); + goto out_dput; + } + + /* Refuse swapoff while the device is pinned for hibernation */ + if (p->flags & SWP_HIBERNATION) { + err =3D -EBUSY; + spin_unlock(&swap_lock); + goto out_dput; + } + + err =3D swap_info_remove(p); + if (err) { + spin_unlock(&swap_lock); + goto out_dput; + } + spin_unlock(&swap_lock); + + err =3D __swapoff(p); + out_dput: filp_close(victim, NULL); return err; @@ -4128,6 +4192,10 @@ static void xswap_try_shrink(struct swap_info_struct= *si) =20 mutex_lock(&si->xswap_lock); =20 + /* A swapoff raced us and is about to walk this mapping. */ + if (!(READ_ONCE(si->flags) & SWP_WRITEOK)) + goto out_unlock; + nr_mapped =3D READ_ONCE(si->nr_clusters_mapped); if (nr_mapped <=3D 1) /* keep cluster 0 */ goto out_unlock; @@ -4435,6 +4503,35 @@ static int xswap_create(int prio) spin_unlock(&swap_lock); return error; } + +/* Tear down a file-less xswap device by its swap type. */ +static int xswap_destroy(int type) +{ + struct swap_info_struct *p; + int err; + + p =3D swap_type_to_info(type); + if (!p) + return -EINVAL; + + spin_lock(&swap_lock); + if (!(p->flags & SWP_WRITEOK) || !(p->flags & SWP_XSWAP)) { + spin_unlock(&swap_lock); + return -EINVAL; + } + /* Refuse swapoff while the device is pinned for hibernation */ + if (p->flags & SWP_HIBERNATION) { + spin_unlock(&swap_lock); + return -EBUSY; + } + + err =3D swap_info_remove(p); + spin_unlock(&swap_lock); + if (err) + return err; + + return __swapoff(p); +} #endif /* CONFIG_XSWAP */ =20 SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags) --=20 2.54.0 From nobody Fri Sep 25 05:30:54 2026 Received: from mta0.migadu.com (out-82.mta0.migadu.com [91.218.175.82]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 27CFF4E80B5 for ; Wed, 16 Sep 2026 10:21:11 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.82 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554074; cv=none; b=E+zrSR8d0DdqIBqezlEDi5x1NuMJXJkG4vcJnWBt8ARBe7hNdpI1sq1zNSI9adfry2MnJY7c8n3nkHkSwEm5Xkq0afChYLwSBmAq2l4tgDos03LeRGm36bkRTA7u0Uni60bXK3JcugCjGv4IV6UmVRaM9jEbpE1oCizFGz6bluQ= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554074; c=relaxed/simple; bh=Xxx7vgXehjWHffmM8VWQkI+0anpDGAKRQArL5+lVJp8=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-type; b=s8hkQSyNKGmSriBalKOWmEiWWrdC7uKlT7rUi5R7OVzbGRkP5pV4YRr5X6HnjA7wt7VVmFgyuwJ3WG+IRM18ehm5ToiR2frEBOHKkDGn4T/54P6zm3vGDSnWHtJGCXqe6IGwlW61kGCZYg39JsC3V+/Vw5RkhAuw4SyQyMtRXBs= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn; spf=pass smtp.mailfrom=linux.dev; arc=none smtp.client-ip=91.218.175.82 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev X-Envelope-To: linux-kernel@vger.kernel.org X-Envelope-To: linux-kernel@vger.kernel.org Received: by mta12.migadu.com with ESMTPS id 0d5d1a8d72c58130; Wed, 16 Sep 2026 10:21:08 +0000 X-Mizu-Trace-ID: 0d5d1a8d72c58130 X-Migadu-Flow: FLOW_OUT From: Baoquan He To: linux-mm@kvack.org Cc: akpm@linux-foundation.org, chrisl@kernel.org, kasong@tencent.com, nphamcs@gmail.com, baohua@kernel.org, youngjun.park@lge.com, hannes@cmpxchg.org, yosry@kernel.org, shikemeng@huaweicloud.com, chengming.zhou@linux.dev, baoquan.he@linux.dev, david@kernel.org, linux-kernel@vger.kernel.org, kunwu.chan@gmail.com, Baoquan He Subject: [PATCH v3 11/14] mm, swap: require zswap for xswap devices Date: Wed, 16 Sep 2026 18:19:18 +0800 Message-ID: <20260916101929.149106-12-hebaoquan@kylinos.cn> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260916101929.149106-1-hebaoquan@kylinos.cn> References: <20260916101929.149106-1-hebaoquan@kylinos.cn> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" xswap pages live only in zswap. Without zswap, swapout cannot free the page, but still takes a swap entry. So the device consumes swap entries without freeing memory. Fail to create a device when zswap is unavailable. Signed-off-by: Baoquan He --- mm/swapfile.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mm/swapfile.c b/mm/swapfile.c index cdcbcbaa6d87..cac084bc72d6 100644 --- a/mm/swapfile.c +++ b/mm/swapfile.c @@ -4439,6 +4439,10 @@ static int xswap_create(int prio) if (prio !=3D DEF_SWAP_PRIO && (prio < 0 || prio > SWAP_FLAG_PRIO_MASK)) return -EINVAL; =20 + /* xswap has no backing store, it relies on zswap. */ + if (!zswap_is_enabled()) + return -EOPNOTSUPP; + si =3D alloc_swap_info(); if (IS_ERR(si)) return PTR_ERR(si); --=20 2.54.0 From nobody Fri Sep 25 05:30:54 2026 Received: from mta0.migadu.com (out-89.mta0.migadu.com [91.218.175.89]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id DA61F4DF4C2 for ; Wed, 16 Sep 2026 10:21:16 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.89 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554098; cv=none; b=gVJepeR3B32X1M6KHi+emms2VUn9fJxwzF5pOiOWHmMnoyJOgyjY/LNi+Wbiy/xzRzqohPZTVDLQTHQi6BO/J2GL/xIQzvpqfBpLj04ABjYoFNYOZh4RnFbf522WabGJ9rVgdpkSsCAHrbQSyMUHT43BhDSO//PQOd36l6GK6F0= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554098; c=relaxed/simple; bh=XFP04jrpN6W+7ITyb1pJ6MF6suxcaOgkoPfcJUVQoMI=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-type; b=BgGh5EO2I/TXdB4+aLRyRK00/6wRmSqRgnpBwxCCmwZRcGoMBA/aqSqMOYxyDe+Qhy3Br8eXyU4FXN4BDG/yO4Bfnq9k67wDWIXs8bR/8W3GZe3rOox8F/uxiezxZgipOoXHLItFVIHJcRrlXBnBRwwovEPIqKAoma2wB9Xz0dQ= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn; spf=pass smtp.mailfrom=linux.dev; arc=none smtp.client-ip=91.218.175.89 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev X-Envelope-To: linux-kernel@vger.kernel.org X-Envelope-To: linux-kernel@vger.kernel.org Received: by mta10.migadu.com with ESMTPS id 8cb4c72123f0ed8d; Wed, 16 Sep 2026 10:21:13 +0000 X-Mizu-Trace-ID: 8cb4c72123f0ed8d X-Migadu-Flow: FLOW_OUT From: Baoquan He To: linux-mm@kvack.org Cc: akpm@linux-foundation.org, chrisl@kernel.org, kasong@tencent.com, nphamcs@gmail.com, baohua@kernel.org, youngjun.park@lge.com, hannes@cmpxchg.org, yosry@kernel.org, shikemeng@huaweicloud.com, chengming.zhou@linux.dev, baoquan.he@linux.dev, david@kernel.org, linux-kernel@vger.kernel.org, kunwu.chan@gmail.com, Baoquan He Subject: [PATCH v3 12/14] mm, swap: cap xswap growth at nr_clusters Date: Wed, 16 Sep 2026 18:19:19 +0800 Message-ID: <20260916101929.149106-13-hebaoquan@kylinos.cn> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260916101929.149106-1-hebaoquan@kylinos.cn> References: <20260916101929.149106-1-hebaoquan@kylinos.cn> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add si->nr_clusters as the ceiling for the cluster_info array's growth. It starts at the whole address space, so nothing changes yet. Signed-off-by: Baoquan He --- include/linux/swap.h | 1 + mm/swapfile.c | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/include/linux/swap.h b/include/linux/swap.h index 30642bb481df..9fe82d0f1740 100644 --- a/include/linux/swap.h +++ b/include/linux/swap.h @@ -245,6 +245,7 @@ struct swap_info_struct { #ifdef CONFIG_XSWAP struct vm_struct *cluster_vm; /* VM_SPARSE area for cluster_info */ unsigned long nr_clusters_max;/* total clusters in the xswap address spa= ce */ + unsigned long nr_clusters; /* how far the array may grow */ unsigned long nr_clusters_mapped; /* currently mapped cluster count */ struct work_struct xswap_shrink_work; /* deferred shrink trigger */ struct mutex xswap_lock; /* serialize map/unmap operations */ diff --git a/mm/swapfile.c b/mm/swapfile.c index cac084bc72d6..2cf6ba0bd0c0 100644 --- a/mm/swapfile.c +++ b/mm/swapfile.c @@ -1319,16 +1319,18 @@ static unsigned long cluster_alloc_swap_entry(struc= t swap_info_struct *si, =20 #ifdef CONFIG_XSWAP /* For xswap: grow the cluster_info array, then retry. */ - if (!found && (si->flags & SWP_XSWAP) && - READ_ONCE(si->nr_clusters_mapped) < READ_ONCE(si->nr_clusters_max) && - list_empty(&si->free_clusters)) { - unsigned long nr_new =3D min(READ_ONCE(si->nr_clusters_max) - - READ_ONCE(si->nr_clusters_mapped), - XSWAP_GROW_CLUSTERS); - unsigned long start =3D READ_ONCE(si->nr_clusters_mapped); - unsigned long i; + if (!found && (si->flags & SWP_XSWAP) && list_empty(&si->free_clusters)) { + unsigned long ceiling =3D READ_ONCE(si->nr_clusters); + unsigned long mapped =3D READ_ONCE(si->nr_clusters_mapped); + unsigned long nr_new, start, i; int ret; =20 + if (mapped >=3D ceiling) + goto done; + + nr_new =3D min(ceiling - mapped, XSWAP_GROW_CLUSTERS); + start =3D mapped; + /* * Mapping pages can sleep. The lock only guards the per-cpu * cluster cache, which this path does not touch. @@ -4294,6 +4296,7 @@ static int setup_swap_clusters_info(struct swap_info_= struct *si, cluster_info =3D vm->addr; si->cluster_vm =3D vm; si->nr_clusters_max =3D nr_clusters; + si->nr_clusters =3D nr_clusters; si->cluster_info =3D cluster_info; =20 /* Must be initialized before xswap_map_clusters() locks it. */ --=20 2.54.0 From nobody Fri Sep 25 05:30:54 2026 Received: from mta0.migadu.com (out-109.mta0.migadu.com [91.218.175.109]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id E6A974E3781 for ; Wed, 16 Sep 2026 10:21:22 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.109 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554088; cv=none; b=R7RRv04AXBHkAHtKqVeIjDi0ckCzAz6zJUOtS1/kxRxoAZfBZma5S2pLPWe2WSX+AJqI9y5VTeauqakJryeY3TWmVblVvA/VcQnVVCziV6NmrFW7Zy49SuwIJbK7t8FvjZ/HwdvuFf/gmjwZiR68AJbGz5q6sj6ccgFk0ICaRtc= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554088; c=relaxed/simple; bh=Ua/LKYJCDs0nKfQf5hGtdOEnRU1et3+vh/lbHsB3V1s=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-type; b=MgC6uLaTphieTEV84bkH92sdx/twR6LBaKjM4W3DtwJmcF9wX0lHe7nDKF8nPm1y4gVZ1nGr5yZgN8MHion/jz6yi5aUFI20c6m+z8Dr03phMHlga6Kp38k4wHDhZZiE+roT8yH2AjIF82QJml6D+UVJasyCB26TlVDj9m6Wpl0= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn; spf=pass smtp.mailfrom=linux.dev; arc=none smtp.client-ip=91.218.175.109 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev X-Envelope-To: linux-kernel@vger.kernel.org X-Envelope-To: linux-kernel@vger.kernel.org Received: by mta10.migadu.com with ESMTPS id 152d4e0bf57540cb; Wed, 16 Sep 2026 10:21:19 +0000 X-Mizu-Trace-ID: 152d4e0bf57540cb X-Migadu-Flow: FLOW_OUT From: Baoquan He To: linux-mm@kvack.org Cc: akpm@linux-foundation.org, chrisl@kernel.org, kasong@tencent.com, nphamcs@gmail.com, baohua@kernel.org, youngjun.park@lge.com, hannes@cmpxchg.org, yosry@kernel.org, shikemeng@huaweicloud.com, chengming.zhou@linux.dev, baoquan.he@linux.dev, david@kernel.org, linux-kernel@vger.kernel.org, kunwu.chan@gmail.com, Baoquan He Subject: [PATCH v3 13/14] mm, swap: add sysfs per-device size limit for xswap Date: Wed, 16 Sep 2026 18:19:20 +0800 Message-ID: <20260916101929.149106-14-hebaoquan@kylinos.cn> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260916101929.149106-1-hebaoquan@kylinos.cn> References: <20260916101929.149106-1-hebaoquan@kylinos.cn> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add a per-device sysfs knob to limit the xswap usable size: /sys/kernel/mm/xswap/type/limit read/write, in pages Reading reports the current usable size; writing sets a new ceiling. The ceiling is clamped to cover the pages in use, and enforcement is best effort. The write requires CAP_SYS_ADMIN. Signed-off-by: Baoquan He --- include/linux/swap.h | 18 ++-- mm/swapfile.c | 193 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 179 insertions(+), 32 deletions(-) diff --git a/include/linux/swap.h b/include/linux/swap.h index 9fe82d0f1740..804189b4b4eb 100644 --- a/include/linux/swap.h +++ b/include/linux/swap.h @@ -16,6 +16,8 @@ #include #include =20 +struct kobject; + #define SWAP_FLAG_PREFER 0x8000 /* set if swap priority specified */ #define SWAP_FLAG_PRIO_MASK 0x7fff #define SWAP_FLAG_DISCARD 0x10000 /* enable discard for swap */ @@ -245,8 +247,9 @@ struct swap_info_struct { #ifdef CONFIG_XSWAP struct vm_struct *cluster_vm; /* VM_SPARSE area for cluster_info */ unsigned long nr_clusters_max;/* total clusters in the xswap address spa= ce */ - unsigned long nr_clusters; /* how far the array may grow */ + unsigned long nr_clusters; /* growth ceiling, set by type/limit */ unsigned long nr_clusters_mapped; /* currently mapped cluster count */ + struct kobject *xswap_dev_kobj; /* sysfs: /sys/kernel/mm/xswap/type/ = */ struct work_struct xswap_shrink_work; /* deferred shrink trigger */ struct mutex xswap_lock; /* serialize map/unmap operations */ #endif @@ -256,7 +259,7 @@ struct swap_info_struct { /* list of cluster that contains at least one free slot */ struct list_head frag_clusters[SWAP_NR_ORDERS]; /* list of cluster that are fragmented or contented */ - unsigned int pages; /* total of usable pages of swap */ + unsigned int pages; /* total of usable pages of swap; mutable for xswap = */ atomic_long_t inuse_pages; /* number of those currently in use */ struct swap_sequential_cluster *global_cluster; /* Use one global cluster= for rotating device */ spinlock_t global_cluster_lock; /* Serialize usage of global cluster */ @@ -269,10 +272,13 @@ struct swap_info_struct { * inuse_pages and all cluster lists. * Other fields are only changed * at swapon/swapoff, so are protected - * by swap_lock. changing flags need - * hold this lock and swap_lock. If - * both locks need hold, hold swap_lock - * first. + * by swap_lock, except for pages: + * xswap updates it at runtime from + * type/limit, and readers without + * swap_lock use READ_ONCE(). changing + * flags need hold this lock and + * swap_lock. If both locks need hold, + * hold swap_lock first. */ struct work_struct discard_work; /* discard worker */ struct work_struct reclaim_work; /* reclaim worker */ diff --git a/mm/swapfile.c b/mm/swapfile.c index 2cf6ba0bd0c0..0fcbaf1cf0e2 100644 --- a/mm/swapfile.c +++ b/mm/swapfile.c @@ -71,6 +71,8 @@ static int xswap_unmap_clusters(struct swap_info_struct *= si, unsigned long start_idx, unsigned long nr); static int xswap_mapped_end(pte_t *pte, unsigned long addr, void *data); static void xswap_try_shrink(struct swap_info_struct *si); +static int xswap_dev_kobj_add(struct swap_info_struct *si); +static void xswap_dev_kobj_del(struct swap_info_struct *si); =20 static int xswap_create(int prio); static int xswap_destroy(int type); @@ -1369,8 +1371,6 @@ static unsigned long cluster_alloc_swap_entry(struct = swap_info_struct *si, /* SWAP_USAGE_OFFLIST_BIT can only be set by this helper. */ static void del_from_avail_list(struct swap_info_struct *si, bool swapoff) { - unsigned long pages; - spin_lock(&swap_avail_lock); =20 if (swapoff) { @@ -1388,15 +1388,19 @@ static void del_from_avail_list(struct swap_info_st= ruct *si, bool swapoff) atomic_long_or(SWAP_USAGE_OFFLIST_BIT, &si->inuse_pages); } else { /* - * If not called by swapoff, take it off-list only if it's - * full and SWAP_USAGE_OFFLIST_BIT is not set (strictly - * si->inuse_pages =3D=3D pages), any concurrent slot freeing, - * or device already removed from plist by someone else - * will make this return false. + * Take it off-list only if full and not already off. Use >=3D + * and the current count: xswap can shrink si->pages at + * runtime, so a racing allocation can push inuse_pages past + * it. */ - pages =3D si->pages; - if (!atomic_long_try_cmpxchg(&si->inuse_pages, &pages, - pages | SWAP_USAGE_OFFLIST_BIT)) + long val =3D atomic_long_read(&si->inuse_pages); + + if (val & SWAP_USAGE_OFFLIST_BIT) + goto skip; + if (val < READ_ONCE(si->pages)) + goto skip; + if (!atomic_long_try_cmpxchg(&si->inuse_pages, &val, + val | SWAP_USAGE_OFFLIST_BIT)) goto skip; } =20 @@ -1410,7 +1414,6 @@ static void del_from_avail_list(struct swap_info_stru= ct *si, bool swapoff) static void add_to_avail_list(struct swap_info_struct *si, bool swapon) { long val; - unsigned long pages; =20 spin_lock(&swap_avail_lock); =20 @@ -1429,15 +1432,14 @@ static void add_to_avail_list(struct swap_info_stru= ct *si, bool swapon) val =3D atomic_long_fetch_and_relaxed(~SWAP_USAGE_OFFLIST_BIT, &si->inuse= _pages); =20 /* - * When device is full and device is on the plist, only one updater will - * see (inuse_pages =3D=3D si->pages) and will call del_from_avail_list. = If - * that updater happen to be here, just skip adding. + * Mask off the bit to get the count. Keep the device off-list if + * it is still full; use >=3D because a runtime shrink of si->pages + * can leave it over the limit. */ - pages =3D si->pages; - if (val =3D=3D pages) { - /* Just like the cmpxchg in del_from_avail_list */ - if (atomic_long_try_cmpxchg(&si->inuse_pages, &pages, - pages | SWAP_USAGE_OFFLIST_BIT)) + val &=3D ~SWAP_USAGE_OFFLIST_BIT; + if (val >=3D READ_ONCE(si->pages)) { + if (atomic_long_try_cmpxchg(&si->inuse_pages, &val, + val | SWAP_USAGE_OFFLIST_BIT)) goto skip; } =20 @@ -1462,7 +1464,8 @@ static bool swap_usage_add(struct swap_info_struct *s= i, unsigned int nr_entries) * If device is full, and SWAP_USAGE_OFFLIST_BIT is not set, * remove it from the plist. */ - if (unlikely(val =3D=3D si->pages)) { + if (unlikely(!(val & SWAP_USAGE_OFFLIST_BIT) && + val >=3D READ_ONCE(si->pages))) { del_from_avail_list(si, false); return true; } @@ -3360,6 +3363,7 @@ static void free_swap_cluster_info(struct swap_info_s= truct *si) if (si->flags & SWP_XSWAP) { unsigned long nr_mapped; =20 + xswap_dev_kobj_del(si); cancel_work_sync(&si->xswap_shrink_work); /* * Cluster 0 keeps the bad header slot, so it never empties @@ -3690,7 +3694,7 @@ static int swap_show(struct seq_file *swap, void *v) return 0; } =20 - bytes =3D K(si->pages); + bytes =3D K(READ_ONCE(si->pages)); inuse =3D K(swap_usage_in_pages(si)); =20 file =3D si->swap_file; @@ -4337,6 +4341,9 @@ static int setup_swap_clusters_info(struct swap_info_= struct *si, } =20 INIT_WORK(&si->xswap_shrink_work, xswap_shrink_work_fn); + if (xswap_dev_kobj_add(si)) + pr_warn("xswap: failed to add sysfs interface for type %d\n", + si->type); return 0; =20 err_unmap: @@ -4429,14 +4436,143 @@ static int setup_swap_clusters_info(struct swap_in= fo_struct *si, } =20 #ifdef CONFIG_XSWAP -/* Create a file-less xswap device. si->max and the initial nr_clusters - * ceiling are both twice RAM; the runtime size can be lowered afterwards - * via /sys/kernel/mm/xswap/type/limit. +struct xswap_sysfs_dev { + struct kobject kobj; + struct swap_info_struct *si; +}; + +static ssize_t xswap_limit_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct swap_info_struct *si =3D + container_of(kobj, struct xswap_sysfs_dev, kobj)->si; + + return sysfs_emit(buf, "%u\n", READ_ONCE(si->pages)); +} + +static ssize_t xswap_limit_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct swap_info_struct *si =3D + container_of(kobj, struct xswap_sysfs_dev, kobj)->si; + unsigned long val, clusters, new_pages, used; + int err; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + err =3D kstrtoul(buf, 0, &val); + if (err) + return err; + + spin_lock(&swap_lock); + if (!(si->flags & SWP_WRITEOK)) { + spin_unlock(&swap_lock); + return -ENODEV; + } + + used =3D swap_usage_in_pages(si); + + clusters =3D DIV_ROUND_UP(val, SWAPFILE_CLUSTER); + if (clusters > si->nr_clusters_max) + clusters =3D si->nr_clusters_max; + /* + * The ceiling can never be below the pages in use: the clusters + * covering them stay mapped, and si->pages is the ceiling + * capacity, so the free slots in the partially used top cluster + * are credited instead of being allocatable but unaccounted for. + */ + clusters =3D max_t(unsigned long, clusters, + DIV_ROUND_UP(used + 1, SWAPFILE_CLUSTER)); + + spin_lock(&si->lock); + si->nr_clusters =3D clusters; + spin_unlock(&si->lock); + + new_pages =3D min_t(unsigned long, clusters * SWAPFILE_CLUSTER, si->max); + if (new_pages) + new_pages--; + + if (new_pages < used) + new_pages =3D used; + if (new_pages !=3D si->pages) { + long delta =3D (long)new_pages - (long)si->pages; + + si->pages =3D new_pages; + atomic_long_add(delta, &nr_swap_pages); + total_swap_pages +=3D delta; + } + add_to_avail_list(si, false); + + spin_unlock(&swap_lock); + + return count; +} + +static struct kobj_attribute xswap_limit_attr =3D + __ATTR(limit, 0644, xswap_limit_show, xswap_limit_store); + +static void xswap_dev_release(struct kobject *kobj) +{ + kfree(container_of(kobj, struct xswap_sysfs_dev, kobj)); +} + +static const struct kobj_type xswap_dev_ktype =3D { + .sysfs_ops =3D &kobj_sysfs_ops, + .release =3D xswap_dev_release, +}; + +static int xswap_dev_kobj_add(struct swap_info_struct *si) +{ + struct xswap_sysfs_dev *dev; + int err; + + if (!xswap_kobj) + return 0; + + dev =3D kzalloc_obj(*dev, GFP_KERNEL); + if (!dev) + return -ENOMEM; + dev->si =3D si; + + err =3D kobject_init_and_add(&dev->kobj, &xswap_dev_ktype, xswap_kobj, + "type%d", si->type); + if (err) { + kobject_put(&dev->kobj); + return err; + } + + err =3D sysfs_create_file(&dev->kobj, &xswap_limit_attr.attr); + if (err) { + kobject_del(&dev->kobj); + kobject_put(&dev->kobj); + return err; + } + si->xswap_dev_kobj =3D &dev->kobj; + return 0; +} + +static void xswap_dev_kobj_del(struct swap_info_struct *si) +{ + struct kobject *kobj =3D si->xswap_dev_kobj; + + if (!kobj) + return; + si->xswap_dev_kobj =3D NULL; + sysfs_remove_file(kobj, &xswap_limit_attr.attr); + kobject_del(kobj); + kobject_put(kobj); +} + +/* Create a file-less xswap device. The address space reaches twice RAM; + * the device is created capped at RAM, and type/limit raises that cap + * up to si->max. */ static int xswap_create(int prio) { struct swap_info_struct *si; - unsigned long ram, maxpages; + unsigned long ram, maxpages, nr_clusters; int error; =20 if (prio !=3D DEF_SWAP_PRIO && (prio < 0 || prio > SWAP_FLAG_PRIO_MASK)) @@ -4464,10 +4600,13 @@ static int xswap_create(int prio) if (maxpages < 2) maxpages =3D 2; =20 + nr_clusters =3D DIV_ROUND_UP(ram, SWAPFILE_CLUSTER); + si->bdev =3D NULL; si->flags |=3D SWP_XSWAP | SWP_SOLIDSTATE; si->max =3D maxpages; - si->pages =3D maxpages - 1; + si->pages =3D min_t(unsigned long, nr_clusters * SWAPFILE_CLUSTER, + si->max) - 1; /* * No backing file: setup_swap_extents() is only reachable from the * file-backed swapon() path, so set ops here. Only ops->flags is @@ -4480,6 +4619,8 @@ static int xswap_create(int prio) if (error) goto bad_swap; =20 + si->nr_clusters =3D min(nr_clusters, si->nr_clusters_max); + error =3D zswap_swapon(si->type, si->max); if (error) goto bad_swap; --=20 2.54.0 From nobody Fri Sep 25 05:30:54 2026 Received: from mta0.migadu.com (out-118.mta0.migadu.com [91.218.175.118]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 25C8A3B994A for ; Wed, 16 Sep 2026 10:21:30 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.118 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554122; cv=none; b=PnhtClqqIox+GkkneDlgKwlhs3qrcvuaQG8+GRcjTmDqKbx5vda9aRZutXovo5jBZ7N8OXWu2dUqhMQ3LmJfByPuI3EhbkEQMWhScMlW2Lj0sLJ0prgpkOa8wGrJzs2qWbUyjOBBFxBjtI8XIwJsCxsg0A9xp008iDT1XUTjGz4= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789554122; c=relaxed/simple; bh=zr/6QkN7PGRbAiqv5LyfZL95bWr+x1adRWzQUkTaElM=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-type; b=rZFKVShuC26Q76RvvvcwSxQ4hpveCIdTwwRES21+HmbSFB7M5H80zCz64p/Hu1um2c5ikS6Ottmmgi2U5xzSUy02yugRTiNryBi9fTyqnIY4ApNyhsqsgEVK9rIMLFtPNvJVm3S7zS4OZF3aFoKbty1IcJilvMz88AT6c8e5ynw= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn; spf=pass smtp.mailfrom=linux.dev; arc=none smtp.client-ip=91.218.175.118 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=kylinos.cn Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev X-Envelope-To: linux-kernel@vger.kernel.org X-Envelope-To: linux-kernel@vger.kernel.org Received: by mta11.migadu.com with ESMTPS id c0bf29a0ffc9229e; Wed, 16 Sep 2026 10:21:27 +0000 X-Mizu-Trace-ID: c0bf29a0ffc9229e X-Migadu-Flow: FLOW_OUT From: Baoquan He To: linux-mm@kvack.org Cc: akpm@linux-foundation.org, chrisl@kernel.org, kasong@tencent.com, nphamcs@gmail.com, baohua@kernel.org, youngjun.park@lge.com, hannes@cmpxchg.org, yosry@kernel.org, shikemeng@huaweicloud.com, chengming.zhou@linux.dev, baoquan.he@linux.dev, david@kernel.org, linux-kernel@vger.kernel.org, kunwu.chan@gmail.com, Baoquan He Subject: [PATCH v3 14/14] mm, swap: shrink xswap to the ceiling when it drops Date: Wed, 16 Sep 2026 18:19:21 +0800 Message-ID: <20260916101929.149106-15-hebaoquan@kylinos.cn> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260916101929.149106-1-hebaoquan@kylinos.cn> References: <20260916101929.149106-1-hebaoquan@kylinos.cn> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Make xswap_try_shrink() shrink down to the ceiling when the ceiling is lowered below the mapped range, and trigger the shrink directly from a limit write instead of waiting for the next free cluster. Signed-off-by: Baoquan He --- mm/swapfile.c | 59 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/mm/swapfile.c b/mm/swapfile.c index 0fcbaf1cf0e2..ab64bfc4e0f7 100644 --- a/mm/swapfile.c +++ b/mm/swapfile.c @@ -4190,8 +4190,9 @@ static void xswap_shrink_work_fn(struct work_struct *= work) */ static void xswap_try_shrink(struct swap_info_struct *si) { + unsigned long nr_mapped, nr_ceiling, nr_tail, nr_unmap; + unsigned long start_idx, i; struct swap_cluster_info *ci; - unsigned long nr_mapped, nr_tail, nr_unmap, start_idx, i; =20 if (!(si->flags & SWP_XSWAP)) return; @@ -4203,35 +4204,45 @@ static void xswap_try_shrink(struct swap_info_struc= t *si) goto out_unlock; =20 nr_mapped =3D READ_ONCE(si->nr_clusters_mapped); + nr_ceiling =3D READ_ONCE(si->nr_clusters); + if (nr_mapped <=3D 1) /* keep cluster 0 */ goto out_unlock; =20 /* - * Reclaim on our own, but only once the mapped range is at most - * half in use: growth is demand driven, so reclaiming on a smaller - * dip would only map the same clusters again, and every unmap costs - * an RCU grace period. + * A cap below the mapped range shrinks on its own. Otherwise wait + * until the range is at most half in use: growth follows demand, so + * shrinking above that would only map the same clusters again. */ - if (swap_usage_in_pages(si) * 2 > nr_mapped * SWAPFILE_CLUSTER) + if (nr_ceiling >=3D nr_mapped && + swap_usage_in_pages(si) * 2 > nr_mapped * SWAPFILE_CLUSTER) goto out_unlock; =20 - /* - * Count the free clusters at the tail of the mapped range. Scanned, - * not tracked: the count must be exact to size the unmap, and an - * incremental count falls behind on out-of-order frees. - */ - nr_tail =3D 0; - while (nr_mapped - nr_tail > 1) { - ci =3D &si->cluster_info[nr_mapped - nr_tail - 1]; - if (READ_ONCE(ci->count) || - READ_ONCE(ci->flags) !=3D CLUSTER_FLAG_FREE) - break; - nr_tail++; - } - if (nr_tail < XSWAP_SHRINK_SLACK + XSWAP_SHRINK_MIN) - goto out_unlock; + if (nr_ceiling < nr_mapped) { + /* Take the excess exactly; rounding could zero a small cap. */ + nr_unmap =3D nr_mapped - nr_ceiling; + /* + * Keep cluster 0: it always holds the header slot, so it is + * never free and must not be unmapped. + */ + nr_unmap =3D min(nr_unmap, nr_mapped - 1); + } else { + /* Count the free tail; scanned, not tracked. */ + nr_tail =3D 0; + while (nr_mapped - nr_tail > 1) { + ci =3D &si->cluster_info[nr_mapped - nr_tail - 1]; + if (READ_ONCE(ci->count) || + READ_ONCE(ci->flags) !=3D CLUSTER_FLAG_FREE) + break; + nr_tail++; + } =20 - nr_unmap =3D rounddown(nr_tail - XSWAP_SHRINK_SLACK, XSWAP_GROW_CLUSTERS); + if (nr_tail < XSWAP_SHRINK_SLACK + XSWAP_SHRINK_MIN) + goto out_unlock; + + nr_unmap =3D rounddown(nr_tail - XSWAP_SHRINK_SLACK, + XSWAP_GROW_CLUSTERS); + } if (!nr_unmap) goto out_unlock; start_idx =3D nr_mapped - nr_unmap; @@ -4507,6 +4518,10 @@ static ssize_t xswap_limit_store(struct kobject *kob= j, =20 spin_unlock(&swap_lock); =20 + /* Enforce a lowered ceiling at once; raising needs no shrink. */ + if (clusters < READ_ONCE(si->nr_clusters_mapped)) + xswap_try_shrink(si); + return count; } =20 --=20 2.54.0