From nobody Sat Jul 25 21:19:11 2026 Received: from out-173.mta0.migadu.com (out-173.mta0.migadu.com [91.218.175.173]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 7D5AC42E8FE for ; Mon, 13 Jul 2026 13:36:29 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.173 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949792; cv=none; b=GDDzDnwSY3Tk824FWxuCWHoTy+Izj57raGPBhic4MMFSWirtI0q+ZF5X2WsxeGsbb3VaMvQWkMLddGk09KPNzr4mg9exVHAv3wKoz8zetEk8+DXY45mZzFQ1HxjLrUlN/LrcpC9ftfX4Tm5HdN4akC8jmqCM6UKaVhVxbfEyJsQ= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949792; c=relaxed/simple; bh=Hi0+xTzwtGh0gPsjdZDFQ4GbtKuYdaZEuqgaDPdudbA=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=WyKcZsRI06W5V0DPY71OqVSejsnJ4zjWamyNAfp4+DKtvHD07UvqzZyWiJEOhJsV8jtGeiMEvcr2+Fv9stf7m7bCSVdZedAY3EiRe/VjkZ9YJr5LsVIS7wts3X5jjXoR7S03S7t0ViQnwNN2yst5YlFyNfvS/GJZJkeVamT1qYA= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=DcwT1fZP; arc=none smtp.client-ip=91.218.175.173 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="DcwT1fZP" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1783949786; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=tUgA6wQcfP1IvEKm+e/GRsSnX/F4Dv4Bzc78RNPqdmU=; b=DcwT1fZPpMKPqiLd5rSn8mnpKpGJ9Q6JT2j5O2Si+GNVm15oKn+Ol6/Fqs53X6xaAxMTcd ZiaHo5TljcU9gFYCulrB3Z69li5qiG1V5H4tWL99qUXrJG/ZFwedC14AeWvYWEfjW4ZU95 bwhL3R2IXTyWdXuq71Y0V2Sv+gipddE= From: Usama Arif To: Andrew Morton , david@kernel.org, chrisl@kernel.org, kasong@tencent.com, ljs@kernel.org, ziy@nvidia.com, linux-mm@kvack.org Cc: ying.huang@linux.alibaba.com, Baoquan He , willy@infradead.org, youngjun.park@lge.com, hannes@cmpxchg.org, riel@surriel.com, shakeel.butt@linux.dev, alex@ghiti.fr, kas@kernel.org, baohua@kernel.org, dev.jain@arm.com, baolin.wang@linux.alibaba.com, npache@redhat.com, Liam R. Howlett , ryan.roberts@arm.com, Vlastimil Babka , lance.yang@linux.dev, linux-kernel@vger.kernel.org, nphamcs@gmail.com, shikemeng@huaweicloud.com, yosry@kernel.org, kernel-team@meta.com, Usama Arif Subject: [PATCH v4 01/11] mm: add PMD swap entry detection support Date: Mon, 13 Jul 2026 06:35:36 -0700 Message-ID: <20260713133613.2707815-2-usama.arif@linux.dev> In-Reply-To: <20260713133613.2707815-1-usama.arif@linux.dev> References: <20260713133613.2707815-1-usama.arif@linux.dev> 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 X-Migadu-Flow: FLOW_OUT Content-Type: text/plain; charset="utf-8" Currently when a PMD-mapped THP is swapped out, the PMD is always split into 512 PTE-level swap entries. To preserve huge page information across swap cycles, later patches will install a single PMD-level swap entry instead. This patch adds the infrastructure to detect those entries. Teach the softleaf layer to recognise PMD swap entries: pmd_is_swap_entry() detects them and softleaf_is_valid_pmd_entry() accepts them as a valid non-present type. Clear the exclusive overlay bit in softleaf_from_pmd() before decoding, matching how soft_dirty and uffd_wp bits are already stripped. Add pmd_swp_mkexclusive(), pmd_swp_exclusive(), and pmd_swp_clear_exclusive() helpers to each architecture that supports PMD softleaf entries (x86, arm64, s390, riscv, loongarch, powerpc), mirroring the existing PTE swap exclusive helpers in each arch's pgtable.h. Provide generic no-op PMD swap exclusive fallbacks for architectures without PMD softleaf support, matching the generic PMD swap soft-dirty fallbacks. Signed-off-by: Usama Arif --- arch/arm64/include/asm/pgtable.h | 6 +++++ arch/loongarch/include/asm/pgtable.h | 19 ++++++++++++++++ arch/powerpc/include/asm/book3s/64/pgtable.h | 17 ++++++++++++++ arch/riscv/include/asm/pgtable.h | 17 ++++++++++++++ arch/s390/include/asm/pgtable.h | 17 ++++++++++++++ arch/x86/include/asm/pgtable.h | 17 ++++++++++++++ include/linux/leafops.h | 24 ++++++++++++++++---- include/linux/pgtable.h | 17 ++++++++++++++ 8 files changed, 129 insertions(+), 5 deletions(-) diff --git a/arch/arm64/include/asm/pgtable.h b/arch/arm64/include/asm/pgta= ble.h index a2681d755358..860f95573d1f 100644 --- a/arch/arm64/include/asm/pgtable.h +++ b/arch/arm64/include/asm/pgtable.h @@ -598,6 +598,12 @@ static inline int pmd_protnone(pmd_t pmd) #define pmd_swp_clear_uffd(pmd) \ pte_pmd(pte_swp_clear_uffd(pmd_pte(pmd))) #endif /* CONFIG_HAVE_ARCH_USERFAULTFD_WP */ +#ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES +#define pmd_swp_exclusive(pmd) pte_swp_exclusive(pmd_pte(pmd)) +#define pmd_swp_mkexclusive(pmd) pte_pmd(pte_swp_mkexclusive(pmd_pte(pmd))) +#define pmd_swp_clear_exclusive(pmd) \ + pte_pmd(pte_swp_clear_exclusive(pmd_pte(pmd))) +#endif =20 #define pmd_write(pmd) pte_write(pmd_pte(pmd)) =20 diff --git a/arch/loongarch/include/asm/pgtable.h b/arch/loongarch/include/= asm/pgtable.h index 1952e34bc8ee..aa8e1223d397 100644 --- a/arch/loongarch/include/asm/pgtable.h +++ b/arch/loongarch/include/asm/pgtable.h @@ -357,6 +357,25 @@ static inline pte_t pte_swp_clear_exclusive(pte_t pte) return pte; } =20 +#ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES +static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd) +{ + pmd_val(pmd) |=3D _PAGE_SWP_EXCLUSIVE; + return pmd; +} + +static inline bool pmd_swp_exclusive(pmd_t pmd) +{ + return pmd_val(pmd) & _PAGE_SWP_EXCLUSIVE; +} + +static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd) +{ + pmd_val(pmd) &=3D ~_PAGE_SWP_EXCLUSIVE; + return pmd; +} +#endif + #define pte_none(pte) (!(pte_val(pte) & ~_PAGE_GLOBAL)) #define pte_present(pte) (pte_val(pte) & (_PAGE_PRESENT | _PAGE_PROTNONE)) #define pte_no_exec(pte) (pte_val(pte) & _PAGE_NO_EXEC) diff --git a/arch/powerpc/include/asm/book3s/64/pgtable.h b/arch/powerpc/in= clude/asm/book3s/64/pgtable.h index f4db7d7fbd5c..6a899d0793b3 100644 --- a/arch/powerpc/include/asm/book3s/64/pgtable.h +++ b/arch/powerpc/include/asm/book3s/64/pgtable.h @@ -699,6 +699,23 @@ static inline pte_t pte_swp_clear_exclusive(pte_t pte) return __pte_raw(pte_raw(pte) & cpu_to_be64(~_PAGE_SWP_EXCLUSIVE)); } =20 +#ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES +static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd) +{ + return __pmd_raw(pmd_raw(pmd) | cpu_to_be64(_PAGE_SWP_EXCLUSIVE)); +} + +static inline bool pmd_swp_exclusive(pmd_t pmd) +{ + return !!(pmd_raw(pmd) & cpu_to_be64(_PAGE_SWP_EXCLUSIVE)); +} + +static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd) +{ + return __pmd_raw(pmd_raw(pmd) & cpu_to_be64(~_PAGE_SWP_EXCLUSIVE)); +} +#endif + static inline bool check_pte_access(unsigned long access, unsigned long pt= ev) { /* diff --git a/arch/riscv/include/asm/pgtable.h b/arch/riscv/include/asm/pgta= ble.h index 7f21742b2b6a..558bb99cdaca 100644 --- a/arch/riscv/include/asm/pgtable.h +++ b/arch/riscv/include/asm/pgtable.h @@ -927,6 +927,23 @@ static inline pmd_t pmd_swp_clear_uffd(pmd_t pmd) } #endif /* CONFIG_HAVE_ARCH_USERFAULTFD_WP */ =20 +#ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES +static inline bool pmd_swp_exclusive(pmd_t pmd) +{ + return pte_swp_exclusive(pmd_pte(pmd)); +} + +static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd) +{ + return pte_pmd(pte_swp_mkexclusive(pmd_pte(pmd))); +} + +static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd) +{ + return pte_pmd(pte_swp_clear_exclusive(pmd_pte(pmd))); +} +#endif + #ifdef CONFIG_HAVE_ARCH_SOFT_DIRTY static inline bool pmd_soft_dirty(pmd_t pmd) { diff --git a/arch/s390/include/asm/pgtable.h b/arch/s390/include/asm/pgtabl= e.h index e882663a58e7..490e4a3464b1 100644 --- a/arch/s390/include/asm/pgtable.h +++ b/arch/s390/include/asm/pgtable.h @@ -870,6 +870,23 @@ static inline pte_t pte_swp_clear_exclusive(pte_t pte) return clear_pte_bit(pte, __pgprot(_PAGE_SWP_EXCLUSIVE)); } =20 +#ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES +static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd) +{ + return set_pmd_bit(pmd, __pgprot(_PAGE_SWP_EXCLUSIVE)); +} + +static inline bool pmd_swp_exclusive(pmd_t pmd) +{ + return pmd_val(pmd) & _PAGE_SWP_EXCLUSIVE; +} + +static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd) +{ + return clear_pmd_bit(pmd, __pgprot(_PAGE_SWP_EXCLUSIVE)); +} +#endif + static inline int pte_soft_dirty(pte_t pte) { return pte_val(pte) & _PAGE_SOFT_DIRTY; diff --git a/arch/x86/include/asm/pgtable.h b/arch/x86/include/asm/pgtable.h index 8e0018fadd14..b5da5447e83d 100644 --- a/arch/x86/include/asm/pgtable.h +++ b/arch/x86/include/asm/pgtable.h @@ -1525,6 +1525,23 @@ static inline pte_t pte_swp_clear_exclusive(pte_t pt= e) return pte_clear_flags(pte, _PAGE_SWP_EXCLUSIVE); } =20 +#ifdef CONFIG_ARCH_HAS_PMD_SOFTLEAVES +static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd) +{ + return pmd_set_flags(pmd, _PAGE_SWP_EXCLUSIVE); +} + +static inline int pmd_swp_exclusive(pmd_t pmd) +{ + return pmd_flags(pmd) & _PAGE_SWP_EXCLUSIVE; +} + +static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd) +{ + return pmd_clear_flags(pmd, _PAGE_SWP_EXCLUSIVE); +} +#endif + #ifdef CONFIG_HAVE_ARCH_SOFT_DIRTY static inline pte_t pte_swp_mksoft_dirty(pte_t pte) { diff --git a/include/linux/leafops.h b/include/linux/leafops.h index 4c1476ae3234..2beee484a87d 100644 --- a/include/linux/leafops.h +++ b/include/linux/leafops.h @@ -102,6 +102,8 @@ static inline softleaf_t softleaf_from_pmd(pmd_t pmd) pmd =3D pmd_swp_clear_soft_dirty(pmd); if (pmd_swp_uffd(pmd)) pmd =3D pmd_swp_clear_uffd(pmd); + if (pmd_swp_exclusive(pmd)) + pmd =3D pmd_swp_clear_exclusive(pmd); arch_entry =3D __pmd_to_swp_entry(pmd); =20 /* Temporary until swp_entry_t eliminated. */ @@ -634,18 +636,30 @@ static inline bool pmd_is_migration_entry(pmd_t pmd) */ static inline bool softleaf_is_valid_pmd_entry(softleaf_t entry) { - /* Only device private, migration entries valid for PMD. */ + /* Device private, migration, and swap entries valid for PMD. */ return softleaf_is_device_private(entry) || - softleaf_is_migration(entry); + softleaf_is_migration(entry) || + softleaf_is_swap(entry); +} + +/** + * pmd_is_swap_entry() - Does this PMD entry encode an actual swap entry? + * @pmd: PMD entry. + * + * Returns: true if the PMD encodes a swap entry, otherwise false. + */ +static inline bool pmd_is_swap_entry(pmd_t pmd) +{ + return softleaf_is_swap(softleaf_from_pmd(pmd)); } =20 /** * pmd_is_valid_softleaf() - Is this PMD entry a valid softleaf entry? * @pmd: PMD entry. * - * PMD leaf entries are valid only if they are device private or migration - * entries. This function asserts that a PMD leaf entry is valid in this - * respect. + * PMD leaf entries are valid only if they are device private, migration, + * or swap entries. This function asserts that a PMD leaf entry is valid + * in this respect. * * Returns: true if the PMD entry is a valid leaf entry, otherwise false. */ diff --git a/include/linux/pgtable.h b/include/linux/pgtable.h index 8c093c119e5a..e10a7e91e426 100644 --- a/include/linux/pgtable.h +++ b/include/linux/pgtable.h @@ -1917,6 +1917,23 @@ static inline pmd_t pmd_swp_clear_soft_dirty(pmd_t p= md) } #endif =20 +#ifndef CONFIG_ARCH_HAS_PMD_SOFTLEAVES +static inline pmd_t pmd_swp_mkexclusive(pmd_t pmd) +{ + return pmd; +} + +static inline bool pmd_swp_exclusive(pmd_t pmd) +{ + return false; +} + +static inline pmd_t pmd_swp_clear_exclusive(pmd_t pmd) +{ + return pmd; +} +#endif + #ifndef __HAVE_PFNMAP_TRACKING /* * Interfaces that can be used by architecture code to keep track of --=20 2.53.0-Meta From nobody Sat Jul 25 21:19:11 2026 Received: from out-177.mta0.migadu.com (out-177.mta0.migadu.com [91.218.175.177]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id BA4A842DA47 for ; Mon, 13 Jul 2026 13:36:34 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.177 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949796; cv=none; b=AF7Z2pgRF9C2N3XPCtegHoOMa2DR+4TG1MvUjJXSBtcFfdbWQtMkcw1nPi+GVtIkWMkakyU0eaoXcRTuMZcrYq9HUcBGFfSi4fSLmlPEgWENK2S+y7doJe2n1lJnqZINFnE/ZOnbbe4js8IB6a6AA1btM6R/xFDqCO2ynw2+QCU= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949796; c=relaxed/simple; bh=+jFyYxKO7p27oyDCYMBlSlBoHCxMm84X/SqyvMXHXs0=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=QZ7S80YpKDCYXsKkRQwZuPOozPwBSB6+PTbRfDJ0FmTZaBdDAwCt1MRk377uUVap6ENn3tYbbne7JcdyQbSk09+fCfteGOMT+g184wnrSVkXNVaxSJcGbU0nZIYhRQiqDcOk05Ds5M+w+DKdqdypWt1K3dBCrxJwObzWARJbcq8= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=M7MNGg3Q; arc=none smtp.client-ip=91.218.175.177 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="M7MNGg3Q" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1783949792; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=zUERJd2UgNsh+Iab1xJmAl5cWHaF6LdlBWi3pKgh33Y=; b=M7MNGg3QmSrpvjpN81xLgRREOq5NGH5mBPFZVto+uLgOuCYdeOMhbgRXRN8URC53lOHyp5 xTHp2tF/E2JmUwM8Ogg/ImqQw8ns/f8O1HtWB05FN2s/0t0aonKnK6f4KdabnYkWhpivjb I5Z9jNuzu7w+Rv4/ehVDrG3fGJZ8xJk= From: Usama Arif To: Andrew Morton , david@kernel.org, chrisl@kernel.org, kasong@tencent.com, ljs@kernel.org, ziy@nvidia.com, linux-mm@kvack.org Cc: ying.huang@linux.alibaba.com, Baoquan He , willy@infradead.org, youngjun.park@lge.com, hannes@cmpxchg.org, riel@surriel.com, shakeel.butt@linux.dev, alex@ghiti.fr, kas@kernel.org, baohua@kernel.org, dev.jain@arm.com, baolin.wang@linux.alibaba.com, npache@redhat.com, Liam R. Howlett , ryan.roberts@arm.com, Vlastimil Babka , lance.yang@linux.dev, linux-kernel@vger.kernel.org, nphamcs@gmail.com, shikemeng@huaweicloud.com, yosry@kernel.org, kernel-team@meta.com, Usama Arif Subject: [PATCH v4 02/11] mm: add PMD swap entry splitting support Date: Mon, 13 Jul 2026 06:35:37 -0700 Message-ID: <20260713133613.2707815-3-usama.arif@linux.dev> In-Reply-To: <20260713133613.2707815-1-usama.arif@linux.dev> References: <20260713133613.2707815-1-usama.arif@linux.dev> 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 X-Migadu-Flow: FLOW_OUT Content-Type: text/plain; charset="utf-8" Add a swap branch in __split_huge_pmd_locked() that splits a PMD swap entry into 512 PTE swap entries. Unlike migration splits, no folio reference is needed because swap entries point to swap slots, not pages. Each PTE inherits the correct sub-slot offset and preserves soft_dirty, uffd_wp, and exclusive flags. The folio_remove_rmap_pmd() gate at the end must inspect old_pmd rather than *pmd: for a present THP split, *pmd has already been cleared by pmdp_invalidate() by the time the gate runs, and the invalidated bit pattern can decode as a plausible swap entry via softleaf_from_pmd() (any type_num < MAX_SWAPFILES reads as SWAP). This branch is reached from the explicit __split_huge_pmd() callers that hit a non-present PMD: partial-range mprotect / munmap, the wp_huge_pmd() PMD-COW fallback, and the swap-in / swapoff fallbacks added in later patches when the cached folio is no longer PMD-sized. page_vma_mapped_walk() does not iterate PMD swap entries, so try_to_unmap_one() and try_to_migrate_one() do not reach this branch and freeze=3Dtrue cannot occur in this branch today. page and folio are therefore left uninitialized in the swap branch; a VM_WARN_ON_ONCE(freeze) catches any future caller that breaks this invariant before the freeze path dereferences page_to_pfn(page + i) or put_page(page). Signed-off-by: Usama Arif --- mm/huge_memory.c | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/mm/huge_memory.c b/mm/huge_memory.c index 0dc6d630570f..e602cf2b48db 100644 --- a/mm/huge_memory.c +++ b/mm/huge_memory.c @@ -3208,6 +3208,12 @@ static void __split_huge_pmd_locked(struct vm_area_s= truct *vma, pmd_t *pmd, folio_add_anon_rmap_ptes(folio, page, HPAGE_PMD_NR, vma, haddr, rmap_flags); } + } else if (pmd_is_swap_entry(*pmd)) { + VM_WARN_ON_ONCE(freeze); + old_pmd =3D *pmd; + soft_dirty =3D pmd_swp_soft_dirty(old_pmd); + uffd_wp =3D pmd_swp_uffd_wp(old_pmd); + anon_exclusive =3D pmd_swp_exclusive(old_pmd); } else { /* * Up to this point the pmd is present and huge and userland has @@ -3344,6 +3350,25 @@ static void __split_huge_pmd_locked(struct vm_area_s= truct *vma, pmd_t *pmd, VM_WARN_ON(!pte_none(ptep_get(pte + i))); set_pte_at(mm, addr, pte + i, entry); } + } else if (pmd_is_swap_entry(old_pmd)) { + softleaf_t sl_entry =3D softleaf_from_pmd(old_pmd); + pte_t swp_pte; + swp_entry_t sub_entry; + + for (i =3D 0, addr =3D haddr; i < HPAGE_PMD_NR; + i++, addr +=3D PAGE_SIZE) { + sub_entry =3D swp_entry(swp_type(sl_entry), + swp_offset(sl_entry) + i); + swp_pte =3D swp_entry_to_pte(sub_entry); + if (soft_dirty) + swp_pte =3D pte_swp_mksoft_dirty(swp_pte); + if (uffd_wp) + swp_pte =3D pte_swp_mkuffd(swp_pte); + if (anon_exclusive) + swp_pte =3D pte_swp_mkexclusive(swp_pte); + VM_WARN_ON(!pte_none(ptep_get(pte + i))); + set_pte_at(mm, addr, pte + i, swp_pte); + } } else { pte_t entry; =20 @@ -3371,7 +3396,7 @@ static void __split_huge_pmd_locked(struct vm_area_st= ruct *vma, pmd_t *pmd, } pte_unmap(pte); =20 - if (!pmd_is_migration_entry(*pmd)) + if (!pmd_is_migration_entry(old_pmd) && !pmd_is_swap_entry(old_pmd)) folio_remove_rmap_pmd(folio, page, vma); if (freeze) put_page(page); --=20 2.53.0-Meta From nobody Sat Jul 25 21:19:11 2026 Received: from out-170.mta1.migadu.com (out-170.mta1.migadu.com [95.215.58.170]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 35EA242B31A for ; Mon, 13 Jul 2026 13:36:42 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.170 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949804; cv=none; b=Hspeehy13R7WmuleQkaEPLcLFZAYQTcpWmGurQm+jY9Emy1ACpy62xv0jJJDWLucDrhyrWSn1NAAo7oisG2dnu6RLXg9l2CGUhSwyHQ75PGSvZEqhzJsqh6wXvvlu5sn4bh/IEnMr7qvdjbtxGWeL1xns0u3JoW7rRxBnwBt1EU= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949804; c=relaxed/simple; bh=b+K3lFHdkICKTJZoJ2AT66nLod0wFztija0t8T+TdGM=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=K7oIsGHAXxMSH7JZbRuDiEwA4hD5Ezbu3IP1gEv5yUwkeAfpmcofaAHez4w5AbQh69X5S4EJyPMqI6L+MzR3BaJhcqtxlInsUyqit2/S+OI1pEU+RD7nH1QZm8dN2tUx8QWE1b2q4qhJyyaasUg5cVf2GwZSsX3tN+p2c82WL9M= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=MBfD/6cE; arc=none smtp.client-ip=95.215.58.170 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="MBfD/6cE" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1783949800; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=/fZYAXoSj6PRbx/ktnt4V+LdEYD8kBj8ni3o5E8GfhY=; b=MBfD/6cErRByPlmJ5XfZhrPzxJnt1aKQA82EgiYHbsKk/0QzsI6E57BTeupqWiHQxIZoaw AcMkOF+iZIvz4ndKBL5yZeW3au6BToSMJ1u46ACcnz3QwIoQ737Yb/0mXPzrBj0TQwyijX EIiNG25EZcZGc/ZQWT7U+fQI9ymQRpI= From: Usama Arif To: Andrew Morton , david@kernel.org, chrisl@kernel.org, kasong@tencent.com, ljs@kernel.org, ziy@nvidia.com, linux-mm@kvack.org Cc: ying.huang@linux.alibaba.com, Baoquan He , willy@infradead.org, youngjun.park@lge.com, hannes@cmpxchg.org, riel@surriel.com, shakeel.butt@linux.dev, alex@ghiti.fr, kas@kernel.org, baohua@kernel.org, dev.jain@arm.com, baolin.wang@linux.alibaba.com, npache@redhat.com, Liam R. Howlett , ryan.roberts@arm.com, Vlastimil Babka , lance.yang@linux.dev, linux-kernel@vger.kernel.org, nphamcs@gmail.com, shikemeng@huaweicloud.com, yosry@kernel.org, kernel-team@meta.com, Usama Arif Subject: [PATCH v4 03/11] mm: handle PMD swap entries in fork path Date: Mon, 13 Jul 2026 06:35:38 -0700 Message-ID: <20260713133613.2707815-4-usama.arif@linux.dev> In-Reply-To: <20260713133613.2707815-1-usama.arif@linux.dev> References: <20260713133613.2707815-1-usama.arif@linux.dev> 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 X-Migadu-Flow: FLOW_OUT Content-Type: text/plain; charset="utf-8" Teach copy_huge_pmd()/copy_huge_non_present_pmd() about swap entries, mirroring copy_nonpresent_pte(). swap_dup_entry_direct() gains a nr parameter (and is renamed to swap_dup_entries_direct()) so it can duplicate a contiguous range of swap slots in one call, matching the existing swap_put_entries_direct(entry, nr) API. Existing callers pass 1. swap_retry_table_alloc() likewise gains a nr parameter so the outer retry knows how many slots the caller was trying to duplicate. The underlying swap_extend_table_alloc() now scans every slot in [ci_off, ci_off + nr) to confirm that at least one still needs the per-cluster extend table before committing an allocation. copy_huge_non_present_pmd() "copies" PMD swap entries during fork instead of splitting, preserving the THP. This mirrors copy_nonpresent_pte() which duplicates the swap slot refcount, clears the exclusive bit on the source, and adds the destination mm to mmlist. If swap_dup_entries_direct() fails (GFP_ATOMIC table alloc), copy_huge_pmd() retries after swap_retry_table_alloc(entry, HPAGE_PMD_NR, GFP_KERNEL), matching the PTE retry in copy_pte_range(). The PMD is stable across the retry because dup_mmap() holds write mmap_lock on both mm_structs. Signed-off-by: Usama Arif --- include/linux/swap.h | 4 ++-- mm/huge_memory.c | 54 ++++++++++++++++++++++++++++++++++++++------ mm/memory.c | 4 ++-- mm/swap.h | 5 ++-- mm/swapfile.c | 41 +++++++++++++++++++++------------ 5 files changed, 81 insertions(+), 27 deletions(-) diff --git a/include/linux/swap.h b/include/linux/swap.h index 4427c7aa16dc..de1c3a897184 100644 --- a/include/linux/swap.h +++ b/include/linux/swap.h @@ -388,7 +388,7 @@ sector_t swap_folio_sector(struct folio *folio); * All entries must be allocated by folio_alloc_swap(). And they must have * a swap count > 1. See comments of folio_*_swap helpers for more info. */ -int swap_dup_entry_direct(swp_entry_t entry); +int swap_dup_entries_direct(swp_entry_t entry, int nr); void swap_put_entries_direct(swp_entry_t entry, int nr); =20 /* @@ -432,7 +432,7 @@ static inline void free_swap_cache(struct folio *folio) { } =20 -static inline int swap_dup_entry_direct(swp_entry_t ent) +static inline int swap_dup_entries_direct(swp_entry_t ent, int nr) { return 0; } diff --git a/mm/huge_memory.c b/mm/huge_memory.c index e602cf2b48db..d16cdf684b41 100644 --- a/mm/huge_memory.c +++ b/mm/huge_memory.c @@ -1805,7 +1805,7 @@ bool touch_pmd(struct vm_area_struct *vma, unsigned l= ong addr, return false; } =20 -static void copy_huge_non_present_pmd( +static int copy_huge_non_present_pmd( struct mm_struct *dst_mm, struct mm_struct *src_mm, pmd_t *dst_pmd, pmd_t *src_pmd, unsigned long addr, struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma, @@ -1851,14 +1851,35 @@ static void copy_huge_non_present_pmd( */ folio_try_dup_anon_rmap_pmd(src_folio, &src_folio->page, dst_vma, src_vma); + } else if (softleaf_is_swap(entry)) { + int err; + + /* + * PMD swap entry: duplicate swap references and clear + * exclusive on source, matching copy_nonpresent_pte(). + */ + err =3D swap_dup_entries_direct(entry, HPAGE_PMD_NR); + if (err < 0) + return err; + + mm_prepare_for_swap_entries(dst_mm); + + if (pmd_swp_exclusive(pmd)) { + pmd =3D pmd_swp_clear_exclusive(pmd); + set_pmd_at(src_mm, addr, src_pmd, pmd); + } } =20 - add_mm_counter(dst_mm, MM_ANONPAGES, HPAGE_PMD_NR); + if (softleaf_is_swap(entry)) + add_mm_counter(dst_mm, MM_SWAPENTS, HPAGE_PMD_NR); + else + add_mm_counter(dst_mm, MM_ANONPAGES, HPAGE_PMD_NR); mm_inc_nr_ptes(dst_mm); pgtable_trans_huge_deposit(dst_mm, dst_pmd, pgtable); if (!userfaultfd_protected(dst_vma)) pmd =3D pmd_swp_clear_uffd(pmd); set_pmd_at(dst_mm, addr, dst_pmd, pmd); + return 0; } =20 int copy_huge_pmd(struct mm_struct *dst_mm, struct mm_struct *src_mm, @@ -1899,6 +1920,7 @@ int copy_huge_pmd(struct mm_struct *dst_mm, struct mm= _struct *src_mm, if (unlikely(!pgtable)) goto out; =20 +retry: dst_ptl =3D pmd_lock(dst_mm, dst_pmd); src_ptl =3D pmd_lockptr(src_mm, src_pmd); spin_lock_nested(src_ptl, SINGLE_DEPTH_NESTING); @@ -1906,11 +1928,29 @@ int copy_huge_pmd(struct mm_struct *dst_mm, struct = mm_struct *src_mm, ret =3D -EAGAIN; pmd =3D *src_pmd; =20 - if (unlikely(thp_migration_supported() && - pmd_is_valid_softleaf(pmd))) { - copy_huge_non_present_pmd(dst_mm, src_mm, dst_pmd, src_pmd, addr, - dst_vma, src_vma, pmd, pgtable); - ret =3D 0; + if (unlikely(pmd_is_valid_softleaf(pmd))) { + ret =3D copy_huge_non_present_pmd(dst_mm, src_mm, dst_pmd, src_pmd, + addr, dst_vma, src_vma, pmd, + pgtable); + if (ret) { + spin_unlock(src_ptl); + spin_unlock(dst_ptl); + /* + * For PMD swap entries -ENOMEM means the per-cluster + * swap-extend table couldn't be GFP_ATOMIC-allocated. + * try the GFP_KERNEL fallback once before giving up. + */ + if (ret =3D=3D -ENOMEM) { + softleaf_t entry =3D softleaf_from_pmd(pmd); + + if (softleaf_is_swap(entry) && + !swap_retry_table_alloc(entry, HPAGE_PMD_NR, + GFP_KERNEL)) + goto retry; + } + pte_free(dst_mm, pgtable); + goto out; + } goto out_unlock; } =20 diff --git a/mm/memory.c b/mm/memory.c index d5e87624f692..21ea76bb4c37 100644 --- a/mm/memory.c +++ b/mm/memory.c @@ -1010,7 +1010,7 @@ copy_nonpresent_pte(struct mm_struct *dst_mm, struct = mm_struct *src_mm, struct page *page; =20 if (likely(softleaf_is_swap(entry))) { - if (swap_dup_entry_direct(entry) < 0) + if (swap_dup_entries_direct(entry, 1) < 0) return -EIO; =20 mm_prepare_for_swap_entries(dst_mm); @@ -1425,7 +1425,7 @@ copy_pte_range(struct vm_area_struct *dst_vma, struct= vm_area_struct *src_vma, =20 if (ret =3D=3D -EIO) { VM_WARN_ON_ONCE(!entry.val); - if (swap_retry_table_alloc(entry, GFP_KERNEL) < 0) { + if (swap_retry_table_alloc(entry, 1, GFP_KERNEL) < 0) { ret =3D -ENOMEM; goto out; } diff --git a/mm/swap.h b/mm/swap.h index b51ad3071a73..c524a7670ac0 100644 --- a/mm/swap.h +++ b/mm/swap.h @@ -206,7 +206,7 @@ static inline void swap_cluster_unlock_irq(struct swap_= cluster_info *ci) spin_unlock_irq(&ci->lock); } =20 -extern int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp); +extern int swap_retry_table_alloc(swp_entry_t entry, unsigned int nr, gfp_= t gfp); =20 /* * Below are the core routines for doing swap for a folio. @@ -431,7 +431,8 @@ static inline int swap_writeout(struct folio *folio, return 0; } =20 -static inline int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp) +static inline int swap_retry_table_alloc(swp_entry_t entry, unsigned int n= r, + gfp_t gfp) { return -EINVAL; } diff --git a/mm/swapfile.c b/mm/swapfile.c index 72952491e9cf..2dbe52e1dfe0 100644 --- a/mm/swapfile.c +++ b/mm/swapfile.c @@ -1456,9 +1456,11 @@ static bool swap_sync_discard(void) =20 static int swap_extend_table_alloc(struct swap_info_struct *si, struct swap_cluster_info *ci, - unsigned int ci_off, gfp_t gfp) + unsigned int ci_off, unsigned int nr, + gfp_t gfp) { int count; + unsigned int i; void *table; =20 table =3D kzalloc(sizeof(ci->extend_table[0]) * SWAPFILE_CLUSTER, gfp); @@ -1474,15 +1476,23 @@ static int swap_extend_table_alloc(struct swap_info= _struct *si, */ if (!cluster_table_is_alloced(ci)) goto out_free; - count =3D swp_tb_get_count(__swap_table_get(ci, ci_off)); - if (count < (SWP_TB_COUNT_MAX - 1)) - goto out_free; if (ci->extend_table) goto out_free; - - ci->extend_table =3D table; - spin_unlock(&ci->lock); - return 0; + /* + * The caller may not know which slot in [ci_off, ci_off + nr) hit + * SWP_TB_COUNT_MAX - 1 (e.g., the PMD fork retry only holds the + * range base after an atomic-alloc failure at a subslot). Confirm + * at least one slot in the range still needs the extend table + * before committing the allocation. + */ + for (i =3D 0; i < nr; i++) { + count =3D swp_tb_get_count(__swap_table_get(ci, ci_off + i)); + if (count >=3D (SWP_TB_COUNT_MAX - 1)) { + ci->extend_table =3D table; + spin_unlock(&ci->lock); + return 0; + } + } =20 out_free: spin_unlock(&ci->lock); @@ -1490,7 +1500,7 @@ static int swap_extend_table_alloc(struct swap_info_s= truct *si, return 0; } =20 -int swap_retry_table_alloc(swp_entry_t entry, gfp_t gfp) +int swap_retry_table_alloc(swp_entry_t entry, unsigned int nr, gfp_t gfp) { int ret; struct swap_info_struct *si; @@ -1502,7 +1512,8 @@ int swap_retry_table_alloc(swp_entry_t entry, gfp_t g= fp) return 0; =20 ci =3D __swap_offset_to_cluster(si, offset); - ret =3D swap_extend_table_alloc(si, ci, swp_cluster_offset(entry), gfp); + ret =3D swap_extend_table_alloc(si, ci, swp_cluster_offset(entry), nr, + gfp); =20 put_swap_device(si); return ret; @@ -1703,7 +1714,8 @@ static int swap_dup_entries_cluster(struct swap_info_= struct *si, if (unlikely(err)) { if (err =3D=3D -ENOMEM) { spin_unlock(&ci->lock); - err =3D swap_extend_table_alloc(si, ci, ci_off, GFP_ATOMIC); + err =3D swap_extend_table_alloc(si, ci, ci_off, 1, + GFP_ATOMIC); spin_lock(&ci->lock); if (!err) goto restart; @@ -3904,8 +3916,9 @@ void si_swapinfo(struct sysinfo *val) } =20 /* - * swap_dup_entry_direct() - Increase reference count of a swap entry by o= ne. + * swap_dup_entries_direct() - Increase reference count of swap entries by= one. * @entry: first swap entry from which we want to increase the refcount. + * @nr: number of contiguous swap entries to duplicate. * * Returns 0 for success, or -ENOMEM if the extend table is required * but could not be atomically allocated. Returns -EINVAL if the swap @@ -3917,7 +3930,7 @@ void si_swapinfo(struct sysinfo *val) * Also the swap entry must have a count >=3D 1. Otherwise folio_dup_swap = should * be used. */ -int swap_dup_entry_direct(swp_entry_t entry) +int swap_dup_entries_direct(swp_entry_t entry, int nr) { struct swap_info_struct *si; =20 @@ -3934,7 +3947,7 @@ int swap_dup_entry_direct(swp_entry_t entry) */ VM_WARN_ON_ONCE(!swap_entry_swapped(si, entry)); =20 - return swap_dup_entries_cluster(si, swp_offset(entry), 1); + return swap_dup_entries_cluster(si, swp_offset(entry), nr); } =20 #if defined(CONFIG_MEMCG) && defined(CONFIG_BLK_CGROUP) --=20 2.53.0-Meta From nobody Sat Jul 25 21:19:11 2026 Received: from out-184.mta0.migadu.com (out-184.mta0.migadu.com [91.218.175.184]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 915FE42B321 for ; Mon, 13 Jul 2026 13:36:47 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.184 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949809; cv=none; b=KrNbGtoDwMJV6tJx0eV88panVgoVqtqX9P75Xst74xx1O9mzgCSqQnYfAVz+2sc0q5lyqiw1sCLcjebxyqjLLbitcm2/2oUIqPS+91BGv47Ss0ER/YDwSpPc0MaFEhOOg6knZLOGrprDaCr1iY1KIg0kBR74eIral6WmZKJIPYw= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949809; c=relaxed/simple; bh=1p3ECseIu0s6UtY3ebMlR+O+eN0hM04YXUGZrF0f8M4=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=XyyYqttCYzWFDzLLx0i7xNDdc7Z0BzSdNU3pzLA5QRCDDOiCpl2EHQCW6p3pYfSHRv+ZMMBUVx+ECHnkYyfdMYCU3OCj1QyaagZGJ3MTilemyaXEb5l3D0Qu7nj7UuXSwdqGa+uuHT1trN20BfYlYCqslYLAnoeGr9BqKF7YKPs= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=dYl+FzHp; arc=none smtp.client-ip=91.218.175.184 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="dYl+FzHp" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1783949805; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=iCAxdLGRZoPIYCHBM8J8Z4pztbi/KGbus1Bg67l/Xrs=; b=dYl+FzHp6TMHIh2Fv08qoz05g/mAafxzHTh/ppIuoohM2LJsJEYcQfCO00GBve7rljfwWJ lhnLeb9+Uv6wJwKrg3Kek5FonRR9JZK5fVQmmyCG272MIBoQXNzNUlbd1MHk2Zft/6w8J6 9XADr1copjLBlPB3g7/+xQtk6SAzK4s= From: Usama Arif To: Andrew Morton , david@kernel.org, chrisl@kernel.org, kasong@tencent.com, ljs@kernel.org, ziy@nvidia.com, linux-mm@kvack.org Cc: ying.huang@linux.alibaba.com, Baoquan He , willy@infradead.org, youngjun.park@lge.com, hannes@cmpxchg.org, riel@surriel.com, shakeel.butt@linux.dev, alex@ghiti.fr, kas@kernel.org, baohua@kernel.org, dev.jain@arm.com, baolin.wang@linux.alibaba.com, npache@redhat.com, Liam R. Howlett , ryan.roberts@arm.com, Vlastimil Babka , lance.yang@linux.dev, linux-kernel@vger.kernel.org, nphamcs@gmail.com, shikemeng@huaweicloud.com, yosry@kernel.org, kernel-team@meta.com, Alexandre Ghiti , Usama Arif Subject: [PATCH v4 04/11] mm: zswap: add range lookup for large-folio swapin Date: Mon, 13 Jul 2026 06:35:39 -0700 Message-ID: <20260713133613.2707815-5-usama.arif@linux.dev> In-Reply-To: <20260713133613.2707815-1-usama.arif@linux.dev> References: <20260713133613.2707815-1-usama.arif@linux.dev> 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 X-Migadu-Flow: FLOW_OUT Content-Type: text/plain; charset="utf-8" From: Alexandre Ghiti A large folio reaches zswap_load() only when the caller expects the whole range to be on disk. Zswap still stores large folios as independent order-0 entries, so reconstructing a large folio from zswap entries would risk returning partially initialized data. Teach zswap_load() to scan the covered range. If no slot is in zswap, return -ENOENT so swap_read_folio() reads the backing device. If any slot is still in zswap, fail the large-folio read so the caller can fall back to per-page swapin. Add zswap_range_has_entry() so PMD swap-entry consumers can make the same range decision before attempting PMD-order swapin. Signed-off-by: Alexandre Ghiti Signed-off-by: Usama Arif --- include/linux/zswap.h | 6 ++++++ mm/zswap.c | 42 ++++++++++++++++++++++++++++++++---------- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/include/linux/zswap.h b/include/linux/zswap.h index 30c193a1207e..cd9efcf9dec9 100644 --- a/include/linux/zswap.h +++ b/include/linux/zswap.h @@ -35,6 +35,7 @@ void zswap_lruvec_state_init(struct lruvec *lruvec); void zswap_folio_swapin(struct folio *folio); bool zswap_is_enabled(void); bool zswap_never_enabled(void); +bool zswap_is_present(swp_entry_t entry, unsigned int nr); #else =20 struct zswap_lruvec_state {}; @@ -69,6 +70,11 @@ static inline bool zswap_never_enabled(void) return true; } =20 +static inline bool zswap_is_present(swp_entry_t entry, unsigned int nr) +{ + return false; +} + #endif =20 #endif /* _LINUX_ZSWAP_H */ diff --git a/mm/zswap.c b/mm/zswap.c index b5a17ea20237..e1202fb164a0 100644 --- a/mm/zswap.c +++ b/mm/zswap.c @@ -1559,6 +1559,23 @@ bool zswap_store(struct folio *folio) return ret; } =20 +/** + * zswap_is_present() - is any slot in [entry, entry + nr) in zswap? + * @entry: base swap entry of the range + * @nr: number of contiguous slots to check (pass 1 for a single-slot quer= y) + */ +bool zswap_is_present(swp_entry_t entry, unsigned int nr) +{ + pgoff_t offset =3D swp_offset(entry); + struct xarray *tree =3D swap_zswap_tree(entry); + unsigned long index =3D offset; + + if (!nr || zswap_never_enabled()) + return false; + + return xa_find(tree, &index, offset + nr - 1, XA_PRESENT); +} + /** * zswap_load() - load a folio from zswap * @folio: folio to load @@ -1571,10 +1588,9 @@ bool zswap_store(struct folio *folio) * NOT marked up-to-date, so that an IO error is emitted (e.g. do_swap_pa= ge() * will SIGBUS). * - * -EINVAL: if the swapped out content was in zswap, but the page belongs - * to a large folio, which is not supported by zswap. The folio is unlock= ed, - * but NOT marked up-to-date, so that an IO error is emitted (e.g. - * do_swap_page() will SIGBUS). + * -EIO: if a slot in a large-folio range is unexpectedly still in zswap. + * The folio is unlocked, but NOT marked up-to-date, so that an IO + * error is emitted (e.g. do_swap_page() will SIGBUS). * * -ENOENT: if the swapped out content was not in zswap. The folio remains * locked on return. @@ -1593,13 +1609,19 @@ int zswap_load(struct folio *folio) return -ENOENT; =20 /* - * Large folios should not be swapped in while zswap is being used, as - * they are not properly handled. Zswap does not properly load large - * folios, and a large folio may only be partially in zswap. + * A large folio reaches zswap_load() only when its whole range is + * expected to be on disk: PMD swap-entry consumers split before + * calling into PMD-order swapin whenever any slot is still in zswap. + * Confirm the range is entirely absent from zswap and return -ENOENT + * so the caller reads it from disk; if a slot is unexpectedly still in + * zswap, fail the read rather than return partially-initialized data. */ - if (WARN_ON_ONCE(folio_test_large(folio))) { - folio_unlock(folio); - return -EINVAL; + if (folio_test_large(folio)) { + if (zswap_is_present(swp, folio_nr_pages(folio))) { + folio_unlock(folio); + return -EIO; + } + return -ENOENT; } =20 entry =3D xa_load(tree, offset); --=20 2.53.0-Meta From nobody Sat Jul 25 21:19:11 2026 Received: from out-176.mta1.migadu.com (out-176.mta1.migadu.com [95.215.58.176]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C54C825B0B8 for ; Mon, 13 Jul 2026 13:36:58 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.176 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949820; cv=none; b=SXi4Earbb3kGUDDcpn2BSxS39YYco/M/denmbVE2RsKc1vOGbdhQIEN44RvIz8Hg3Og5FezluDKAqV3cjW6NPiwS8EjAQlG/9UrMSS5oDG8s1W42jBrgBhlSVF9Lh3D31G/TQEKMZ3z+GGtuldsR5EjH7ZlTpDGGXiOAbKHaxqY= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949820; c=relaxed/simple; bh=yIdwjwMMuCO0xgQI52gTvF381Gl776zvESb+7WpjAGc=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=PkwocUu02XGx4IC7WqOywUi0/NbfgL9XapkuNHksbEOavXwVKOAoLevBbWyPrxLtNcMVlLOlX2cr6zpPKkjlZyw4pYxRYF+9pJEfr/4xeGnmjaau4TF5N/ZdQv0GbmKUIlvuSJjVxyR9Lhwkd7vmpNmAXRl8Vg9BPDrG6IvR5+M= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=p4Hk+RA3; arc=none smtp.client-ip=95.215.58.176 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="p4Hk+RA3" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1783949816; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=T7DkTB0bXHt+ucyJcKopVdEnI+IigGMPukHBvjGJ66o=; b=p4Hk+RA3mpDog1LPVrOmTDzh68o0k5l2+1IGXhySerFdzhMVgjVgBbdiNfm6jtnw5cFXmh nVIuHX2C1NE8fhpqL6FFx52IISzGtOWeynXig3i1vUV1/I64Ku9R/FYaTFo/EX2D1eXgoC LMGaoAo/hKlHBZzchE/kHJP2kvZLcco= From: Usama Arif To: Andrew Morton , david@kernel.org, chrisl@kernel.org, kasong@tencent.com, ljs@kernel.org, ziy@nvidia.com, linux-mm@kvack.org Cc: ying.huang@linux.alibaba.com, Baoquan He , willy@infradead.org, youngjun.park@lge.com, hannes@cmpxchg.org, riel@surriel.com, shakeel.butt@linux.dev, alex@ghiti.fr, kas@kernel.org, baohua@kernel.org, dev.jain@arm.com, baolin.wang@linux.alibaba.com, npache@redhat.com, Liam R. Howlett , ryan.roberts@arm.com, Vlastimil Babka , lance.yang@linux.dev, linux-kernel@vger.kernel.org, nphamcs@gmail.com, shikemeng@huaweicloud.com, yosry@kernel.org, kernel-team@meta.com, Usama Arif Subject: [PATCH v4 05/11] mm: swap in PMD swap entries as whole THPs during swapoff Date: Mon, 13 Jul 2026 06:35:40 -0700 Message-ID: <20260713133613.2707815-6-usama.arif@linux.dev> In-Reply-To: <20260713133613.2707815-1-usama.arif@linux.dev> References: <20260713133613.2707815-1-usama.arif@linux.dev> 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 X-Migadu-Flow: FLOW_OUT Content-Type: text/plain; charset="utf-8" Add swap_pmd_cache_lookup() to classify the swap cache behind a PMD swap entry as empty, backed by one PMD-sized folio, or requiring per-page handling because at least one covered slot has a smaller folio in the swap cache. PMD swap entries are handled at PMD granularity only while the covered cache range is empty or backed by a PMD-sized folio; a split cache forces the entry to be split and retried through the PTE path. Add unuse_pmd() and call it from unuse_pmd_range() to swap in PMD-level swap entries as whole THPs during swapoff. This mirrors the existing unuse_pte_range() but operates at PMD granularity. If the PMD-order folio cannot be allocated, the swap cache already contains per-page folios in the covered range (e.g. split in the swap cache by deferred_split_scan() or memory_failure() while the PMD swap entry was installed), or the folio is not uptodate, the PMD swap entry is split into PTE-level entries via __split_huge_pmd() and a non-zero error is returned so unuse_pmd_range() falls through to unuse_pte_range(), which handles the individual entries at order-0. Signed-off-by: Usama Arif --- mm/swap.h | 17 ++++++ mm/swap_state.c | 44 ++++++++++++++ mm/swapfile.c | 154 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 215 insertions(+) diff --git a/mm/swap.h b/mm/swap.h index c524a7670ac0..e37fb01623b4 100644 --- a/mm/swap.h +++ b/mm/swap.h @@ -301,6 +301,23 @@ static inline bool folio_matches_swap_entry(const stru= ct folio *folio, bool swap_cache_has_folio(swp_entry_t entry); struct folio *swap_cache_get_folio(swp_entry_t entry); void *swap_cache_get_shadow(swp_entry_t entry); +enum swap_pmd_cache { + SWAP_PMD_CACHE_EMPTY, + SWAP_PMD_CACHE_HUGE, + SWAP_PMD_CACHE_SPLIT, +}; + +#ifdef CONFIG_THP_SWAP +enum swap_pmd_cache swap_pmd_cache_lookup(swp_entry_t entry, + struct folio **foliop); +#else +static inline enum swap_pmd_cache swap_pmd_cache_lookup(swp_entry_t entry, + struct folio **foliop) +{ + *foliop =3D NULL; + return SWAP_PMD_CACHE_EMPTY; +} +#endif void swap_cache_del_folio(struct folio *folio); struct folio *swap_cache_alloc_folio(swp_entry_t target_entry, gfp_t gfp_m= ask, unsigned long orders, struct vm_fault *vmf, diff --git a/mm/swap_state.c b/mm/swap_state.c index 1444d20a40e9..4197304e2a4f 100644 --- a/mm/swap_state.c +++ b/mm/swap_state.c @@ -123,6 +123,50 @@ bool swap_cache_has_folio(swp_entry_t entry) return swp_tb_is_folio(swp_tb); } =20 +#ifdef CONFIG_THP_SWAP +/** + * swap_pmd_cache_lookup - classify the swap cache behind a PMD swap entry + * @entry: first swap slot encoded by the PMD swap entry + * @foliop: returned PMD-sized folio, with a reference, if present + * + * A PMD swap entry is a compact page-table encoding for HPAGE_PMD_NR + * consecutive swap slots. The swap cache behind those slots can be empty, + * one PMD-sized folio, or per-slot folios after the original folio was sp= lit. + * + * Context: Caller must keep @entry valid using the usual swap cache rules. + * Return: SWAP_PMD_CACHE_EMPTY if no slot in the PMD range has a cached f= olio, + * SWAP_PMD_CACHE_HUGE if one PMD-sized folio covers the range, or + * SWAP_PMD_CACHE_SPLIT if the range needs per-page handling. + */ +enum swap_pmd_cache swap_pmd_cache_lookup(swp_entry_t entry, + struct folio **foliop) +{ + unsigned int type =3D swp_type(entry); + pgoff_t offset =3D swp_offset(entry); + struct folio *folio; + int i; + + *foliop =3D NULL; + + folio =3D swap_cache_get_folio(entry); + if (folio) { + if (folio_nr_pages(folio) =3D=3D HPAGE_PMD_NR) { + *foliop =3D folio; + return SWAP_PMD_CACHE_HUGE; + } + folio_put(folio); + return SWAP_PMD_CACHE_SPLIT; + } + + for (i =3D 1; i < HPAGE_PMD_NR; i++) { + if (swap_cache_has_folio(swp_entry(type, offset + i))) + return SWAP_PMD_CACHE_SPLIT; + } + + return SWAP_PMD_CACHE_EMPTY; +} +#endif + /** * swap_cache_get_shadow - Looks up a shadow in the swap cache. * @entry: swap entry used for the lookup. diff --git a/mm/swapfile.c b/mm/swapfile.c index 2dbe52e1dfe0..7d9703bbf251 100644 --- a/mm/swapfile.c +++ b/mm/swapfile.c @@ -42,6 +42,7 @@ #include #include #include +#include =20 #include #include @@ -2658,6 +2659,147 @@ static int unuse_pte_range(struct vm_area_struct *v= ma, pmd_t *pmd, return 0; } =20 +/* + * unuse_pmd - Map a locked folio at PMD granularity during swapoff. + * + * The caller provides a locked, swapped-in folio. Returns 0 on success + * (PMD was mapped). Returns -EAGAIN if the swap cache folio no longer + * matches the entry or the PMD changed under the lock (try_to_unuse will + * rescan). Returns -EIO if the folio is not uptodate; in that case the + * PMD is split so unuse_pte_range() can handle individual pages. + */ +static int unuse_pmd(struct vm_area_struct *vma, pmd_t *pmd, + unsigned long addr, softleaf_t entry, + struct folio *folio) +{ + struct mm_struct *mm =3D vma->vm_mm; + struct page *page; + pmd_t new_pmd, old_pmd; + spinlock_t *ptl; + rmap_t rmap_flags =3D RMAP_NONE; + bool exclusive; + + if (unlikely(!folio_matches_swap_entry(folio, entry))) + return -EAGAIN; + + if (unlikely(!folio_test_uptodate(folio))) { + __split_huge_pmd(vma, pmd, addr, false); + return -EIO; + } + + page =3D folio_page(folio, 0); + + ptl =3D pmd_lock(mm, pmd); + old_pmd =3D pmdp_get(pmd); + + if (!pmd_is_swap_entry(old_pmd) || + softleaf_from_pmd(old_pmd).val !=3D entry.val) { + spin_unlock(ptl); + return -EAGAIN; + } + + exclusive =3D pmd_swp_exclusive(old_pmd); + + /* + * Some architectures may have to restore extra metadata to the folio + * when reading from swap. This metadata may be indexed by swap entry + * so this must be called before folio_put_swap(). + */ + arch_swap_restore(folio_swap(entry, folio), folio); + + add_mm_counter(mm, MM_ANONPAGES, HPAGE_PMD_NR); + add_mm_counter(mm, MM_SWAPENTS, -HPAGE_PMD_NR); + + new_pmd =3D folio_mk_pmd(folio, vma->vm_page_prot); + new_pmd =3D pmd_mkold(new_pmd); + if (pmd_swp_soft_dirty(old_pmd)) + new_pmd =3D pmd_mksoft_dirty(new_pmd); + if (pmd_swp_uffd(old_pmd)) + new_pmd =3D pmd_mkuffd(new_pmd); + + if (exclusive) + rmap_flags |=3D RMAP_EXCLUSIVE; + + folio_get(folio); + if (!folio_test_anon(folio)) + folio_add_new_anon_rmap(folio, vma, addr, rmap_flags); + else + folio_add_anon_rmap_pmd(folio, page, vma, addr, rmap_flags); + + set_pmd_at(mm, addr, pmd, new_pmd); + folio_put_swap(folio, NULL); + + spin_unlock(ptl); + + folio_free_swap(folio); + return 0; +} + +/* + * Try to swap in a PMD swap entry as a whole THP. Returns 0 on success. + * If the swap cache no longer has one PMD-sized folio, zswap may require + * per-page loading, or a PMD-order allocation/read fails, split the PMD so + * the caller can fall back to unuse_pte_range(). Otherwise propagates the + * error from unuse_pmd(). + */ +static int unuse_pmd_entry(struct vm_area_struct *vma, pmd_t *pmd, + unsigned long addr, softleaf_t entry) +{ + struct folio *folio; + enum swap_pmd_cache cache_state; + int ret; + + cache_state =3D swap_pmd_cache_lookup(entry, &folio); + if (cache_state =3D=3D SWAP_PMD_CACHE_SPLIT) { + ret =3D -EAGAIN; + goto split_fallback; + } + if (!folio) { + struct vm_fault vmf =3D { + .vma =3D vma, + .address =3D addr, + .real_address =3D addr, + .pmd =3D pmd, + }; + + if (zswap_is_present(entry, HPAGE_PMD_NR)) { + ret =3D -EAGAIN; + goto split_fallback; + } + + folio =3D swapin_sync(entry, GFP_HIGHUSER_MOVABLE, + BIT(HPAGE_PMD_ORDER), &vmf, NULL, 0); + if (IS_ERR_OR_NULL(folio)) { + ret =3D folio ? PTR_ERR(folio) : -ENOMEM; + goto split_fallback; + } + } + + folio_lock(folio); + folio_wait_writeback(folio); + /* + * If the cached folio is no longer PMD-sized (e.g. split in the + * swap cache by deferred_split_scan() or memory_failure() while + * the PMD swap entry was installed), the PMD swap entry no longer + * maps a single contiguous folio. Split the PMD swap entry so + * unuse_pte_range() can swap the per-slot folios in individually. + */ + if (folio_nr_pages(folio) !=3D HPAGE_PMD_NR) { + folio_unlock(folio); + folio_put(folio); + ret =3D -EAGAIN; + goto split_fallback; + } + ret =3D unuse_pmd(vma, pmd, addr, entry, folio); + folio_unlock(folio); + folio_put(folio); + return ret; + +split_fallback: + __split_huge_pmd(vma, pmd, addr, false); + return ret; +} + static inline int unuse_pmd_range(struct vm_area_struct *vma, pud_t *pud, unsigned long addr, unsigned long end, unsigned int type) @@ -2670,6 +2812,18 @@ static inline int unuse_pmd_range(struct vm_area_str= uct *vma, pud_t *pud, do { cond_resched(); next =3D pmd_addr_end(addr, end); + + pmd_t pmdval =3D pmdp_get(pmd); + + if (pmd_is_swap_entry(pmdval)) { + softleaf_t sl =3D softleaf_from_pmd(pmdval); + + if (swp_type(sl) =3D=3D type) { + if (!unuse_pmd_entry(vma, pmd, addr, sl)) + continue; + } + } + ret =3D unuse_pte_range(vma, pmd, addr, next, type); if (ret) return ret; --=20 2.53.0-Meta From nobody Sat Jul 25 21:19:11 2026 Received: from out-183.mta0.migadu.com (out-183.mta0.migadu.com [91.218.175.183]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id E6A23423784 for ; Mon, 13 Jul 2026 13:37:03 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.183 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949826; cv=none; b=ABrXG5cXsjbzfn0NDooMO/vcTuh1Sp46rZmdQp6ruZOuIov0nNeDh7q83XesYnPSQBA6Dq1tJ8DSuv7DcZ+/Z8GfPfzy2NonKSQwLcTPo7vVHBKi/Lm6Cz2MO1NIbjEUmIkkHp4mvkXBY3rjr6ucSDsjHvNwqpNNemMf5yAstWA= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949826; c=relaxed/simple; bh=wLHH7W87BHDe2wH17P8gvpRcNJuHqXbYEEAM0G/blII=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=Tm2dtsHDh9Ig98WVZnHuCEYX/GpqYOAnTVFXdFO1XOLQbANXc3fSVaMv7VzdabbdV4mqBcmW3vOyfHeZ4xXxljMNLEeo8SyIRnWh3qKRFQqzNGVn8zL/HbQxv1gkecCmyQlac3di2zeBhnwarqoBEyOzi3ur3PgM4MmP47pMwVY= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=MqI8VGCs; arc=none smtp.client-ip=91.218.175.183 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="MqI8VGCs" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1783949821; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=tUTto7lYDoWJdkIdAt5j73vjHXx1jSGnYfGwAlOneRQ=; b=MqI8VGCsiVhUTQQ8ZdjFQlLqI6/XdaWm9C3mE/cSzyq9eywkgyaWEHh1yBhhrvoDkNs0yx VKhooAm5VsdnWQubAng/DBLc6QSiHOes++NV3lp26h1sHSUC0WPnHLaqm1zj/326IodiSK 4j3fnjvzoZPDXTZxwrOl5ekDzYr7XqA= From: Usama Arif To: Andrew Morton , david@kernel.org, chrisl@kernel.org, kasong@tencent.com, ljs@kernel.org, ziy@nvidia.com, linux-mm@kvack.org Cc: ying.huang@linux.alibaba.com, Baoquan He , willy@infradead.org, youngjun.park@lge.com, hannes@cmpxchg.org, riel@surriel.com, shakeel.butt@linux.dev, alex@ghiti.fr, kas@kernel.org, baohua@kernel.org, dev.jain@arm.com, baolin.wang@linux.alibaba.com, npache@redhat.com, Liam R. Howlett , ryan.roberts@arm.com, Vlastimil Babka , lance.yang@linux.dev, linux-kernel@vger.kernel.org, nphamcs@gmail.com, shikemeng@huaweicloud.com, yosry@kernel.org, kernel-team@meta.com, Usama Arif Subject: [PATCH v4 06/11] mm: handle PMD swap entries in non-present PMD walkers Date: Mon, 13 Jul 2026 06:35:41 -0700 Message-ID: <20260713133613.2707815-7-usama.arif@linux.dev> In-Reply-To: <20260713133613.2707815-1-usama.arif@linux.dev> References: <20260713133613.2707815-1-usama.arif@linux.dev> 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 X-Migadu-Flow: FLOW_OUT Content-Type: text/plain; charset="utf-8" Teach the remaining non-present PMD walkers about swap entries, mirroring the PTE-level equivalents. smaps_pmd_entry() accounts swap and swap_pss via a new shared smaps_account_swap() helper used by both PTE and PMD paths. move_soft_dirty_pmd(), clear_soft_dirty_pmd(), and make_uffd_wp_pmd(), pagemap_pmd_range_thp() and change_huge_pmd() handle swap entries alongside migration entries. hmm_vma_handle_absent_pmd() faults in PMD swap entries via hmm_vma_fault() instead of returning -EFAULT. The first per-page handle_mm_fault() call triggers do_huge_pmd_swap_page(), which maps the entire folio; subsequent calls become harmless huge_pmd_set_accessed() and the walker retries with a present PMD. madvise_free_huge_pmd() handles PMD swap entries directly: for a full-range MADV_FREE it clears the PMD, frees the deposited page table, and releases the swap slots; for a partial range it splits to PTE swap entries. Without this, MADV_FREE silently becomes a no-op on swapped-out THPs, leaking swap slots. zap_huge_pmd() frees swap slots via swap_put_entries_direct(), matching zap_nonpresent_ptes(). change_non_present_huge_pmd() skips write-permission changes for swap entries and only updates uffd_wp, matching change_softleaf_pte(). madvise_cold_or_pageout_pte_range() skips PMD swap entries early. MADV_COLD and MADV_PAGEOUT operate on resident folios, so a swapped-out THP has nothing to deactivate or reclaim; skipping also prevents the walker from descending into or splitting the PMD swap entry. The locked THP path also treats a racing PMD swap entry as handled before checking for other non-present PMD types. mincore_pte_range() routes the pmd_trans_huge_lock() branch through mincore_swap() for non-present PMDs, matching how the PTE path already calls mincore_swap() for non-present PTEs. Without this a swapped-out PMD-mapped THP would be reported as resident, because pmd_is_huge() (and therefore pmd_trans_huge_lock()) accepts any non-present non-none PMD and the old branch unconditionally did memset(vec, 1, nr). mincore_swap() returns 1 for migration / device-private entries (preserving the prior behavior for those) and checks swap-cache residency for swap entries. queue_folios_pmd() in mempolicy silently skips swap entries, matching the PTE walker which only counts migration entries as failures. Without this, mbind(MPOL_MF_STRICT) would spuriously return -EIO on a swapped-out THP. check_pmd_state() in khugepaged returns SCAN_PMD_MAPPED for PMD swap entries, treating a swapped-out THP as still being a THP from khugepaged's perspective and matching the existing migration-entry handling. Signed-off-by: Usama Arif --- fs/proc/task_mmu.c | 43 +++++++++++++++++++++------------- mm/hmm.c | 3 ++- mm/huge_memory.c | 58 +++++++++++++++++++++++++++++++++++----------- mm/khugepaged.c | 6 +++++ mm/madvise.c | 14 ++++++++++- mm/mincore.c | 45 ++++++++++++++++++++++++++++++++++- 6 files changed, 137 insertions(+), 32 deletions(-) diff --git a/fs/proc/task_mmu.c b/fs/proc/task_mmu.c index 8285f497178b..bf0fd594be59 100644 --- a/fs/proc/task_mmu.c +++ b/fs/proc/task_mmu.c @@ -1046,6 +1046,23 @@ static void smaps_pte_hole_lookup(unsigned long addr= , struct mm_walk *walk) #endif } =20 +static void smaps_account_swap(struct mem_size_stats *mss, + softleaf_t entry, unsigned long size) +{ + int mapcount; + + mss->swap +=3D size; + mapcount =3D swp_swapcount(entry); + if (mapcount >=3D 2) { + u64 pss_delta =3D (u64)size << PSS_SHIFT; + + do_div(pss_delta, mapcount); + mss->swap_pss +=3D pss_delta; + } else { + mss->swap_pss +=3D (u64)size << PSS_SHIFT; + } +} + static void smaps_pte_entry(pte_t *pte, unsigned long addr, struct mm_walk *walk) { @@ -1067,18 +1084,7 @@ static void smaps_pte_entry(pte_t *pte, unsigned lon= g addr, const softleaf_t entry =3D softleaf_from_pte(ptent); =20 if (softleaf_is_swap(entry)) { - int mapcount; - - mss->swap +=3D PAGE_SIZE; - mapcount =3D swp_swapcount(entry); - if (mapcount >=3D 2) { - u64 pss_delta =3D (u64)PAGE_SIZE << PSS_SHIFT; - - do_div(pss_delta, mapcount); - mss->swap_pss +=3D pss_delta; - } else { - mss->swap_pss +=3D (u64)PAGE_SIZE << PSS_SHIFT; - } + smaps_account_swap(mss, entry, PAGE_SIZE); } else if (softleaf_has_pfn(entry)) { if (softleaf_is_device_private(entry)) present =3D true; @@ -1108,9 +1114,13 @@ static void smaps_pmd_entry(pmd_t *pmd, unsigned lon= g addr, if (pmd_present(*pmd)) { page =3D vm_normal_page_pmd(vma, addr, *pmd); present =3D true; - } else if (unlikely(thp_migration_supported())) { + } else { const softleaf_t entry =3D softleaf_from_pmd(*pmd); =20 + if (softleaf_is_swap(entry)) { + smaps_account_swap(mss, entry, HPAGE_PMD_SIZE); + return; + } if (softleaf_has_pfn(entry)) page =3D softleaf_to_page(entry); } @@ -1755,7 +1765,7 @@ static inline void clear_soft_dirty_pmd(struct vm_are= a_struct *vma, pmd =3D pmd_clear_soft_dirty(pmd); =20 set_pmd_at(vma->vm_mm, addr, pmdp, pmd); - } else if (pmd_is_migration_entry(pmd)) { + } else if (pmd_is_migration_entry(pmd) || pmd_is_swap_entry(pmd)) { pmd =3D pmd_swp_clear_soft_dirty(pmd); set_pmd_at(vma->vm_mm, addr, pmdp, pmd); } @@ -2115,7 +2125,8 @@ static int pagemap_pmd_range_thp(pmd_t *pmdp, unsigne= d long addr, flags |=3D PM_UFFD_WP; if (pm->show_pfn) frame =3D pmd_pfn(pmd) + idx; - } else if (thp_migration_supported()) { + } else if (pmd_is_swap_entry(pmd) || + (thp_migration_supported() && pmd_is_migration_entry(pmd))) { const softleaf_t entry =3D softleaf_from_pmd(pmd); unsigned long offset; =20 @@ -2581,7 +2592,7 @@ static void make_uffd_wp_pmd(struct vm_area_struct *v= ma, old =3D pmdp_invalidate_ad(vma, addr, pmdp); pmd =3D pmd_mkuffd(old); set_pmd_at(vma->vm_mm, addr, pmdp, pmd); - } else if (pmd_is_migration_entry(pmd)) { + } else if (pmd_is_migration_entry(pmd) || pmd_is_swap_entry(pmd)) { pmd =3D pmd_swp_mkuffd(pmd); set_pmd_at(vma->vm_mm, addr, pmdp, pmd); } diff --git a/mm/hmm.c b/mm/hmm.c index fc2e1cd0cb22..6da069c0dbfd 100644 --- a/mm/hmm.c +++ b/mm/hmm.c @@ -376,7 +376,8 @@ static int hmm_vma_handle_absent_pmd(struct mm_walk *wa= lk, unsigned long start, required_fault =3D hmm_range_need_fault(hmm_vma_walk, hmm_pfns, npages, 0); if (required_fault) { - if (softleaf_is_device_private(entry)) + if (softleaf_is_device_private(entry) || + softleaf_is_swap(entry)) return hmm_record_fault(addr, end, required_fault, walk); else return -EFAULT; diff --git a/mm/huge_memory.c b/mm/huge_memory.c index d16cdf684b41..991929ec17d8 100644 --- a/mm/huge_memory.c +++ b/mm/huge_memory.c @@ -2347,6 +2347,14 @@ vm_fault_t do_huge_pmd_numa_page(struct vm_fault *vm= f) return 0; } =20 +static inline void zap_deposited_table(struct mm_struct *mm, pmd_t *pmd) +{ + pgtable_t pgtable; + + pgtable =3D pgtable_trans_huge_withdraw(mm, pmd); + pte_free(mm, pgtable); + mm_dec_nr_ptes(mm); +} /* * Return true if we do MADV_FREE successfully on entire pmd page. * Otherwise, return false. @@ -2371,6 +2379,21 @@ bool madvise_free_huge_pmd(struct mmu_gather *tlb, s= truct vm_area_struct *vma, goto out; =20 if (unlikely(!pmd_present(orig_pmd))) { + if (pmd_is_swap_entry(orig_pmd)) { + if (next - addr !=3D HPAGE_PMD_SIZE) { + spin_unlock(ptl); + __split_huge_pmd(vma, pmd, addr, false); + goto out_unlocked; + } + softleaf_t sl =3D softleaf_from_pmd(orig_pmd); + + pmdp_huge_get_and_clear(mm, addr, pmd); + zap_deposited_table(mm, pmd); + spin_unlock(ptl); + swap_put_entries_direct(sl, HPAGE_PMD_NR); + add_mm_counter(mm, MM_SWAPENTS, -HPAGE_PMD_NR); + return true; + } VM_WARN_ON_ONCE(!pmd_is_migration_entry(orig_pmd) && !pmd_is_device_private_entry(orig_pmd)); goto out; @@ -2421,15 +2444,6 @@ bool madvise_free_huge_pmd(struct mmu_gather *tlb, s= truct vm_area_struct *vma, return ret; } =20 -static inline void zap_deposited_table(struct mm_struct *mm, pmd_t *pmd) -{ - pgtable_t pgtable; - - pgtable =3D pgtable_trans_huge_withdraw(mm, pmd); - pte_free(mm, pgtable); - mm_dec_nr_ptes(mm); -} - static void zap_huge_pmd_folio(struct mm_struct *mm, struct vm_area_struct= *vma, pmd_t pmdval, struct folio *folio, bool is_present) { @@ -2522,6 +2536,16 @@ bool zap_huge_pmd(struct mmu_gather *tlb, struct vm_= area_struct *vma, arch_check_zapped_pmd(vma, orig_pmd); tlb_remove_pmd_tlb_entry(tlb, pmd, addr); =20 + if (pmd_is_swap_entry(orig_pmd)) { + softleaf_t sl =3D softleaf_from_pmd(orig_pmd); + + zap_deposited_table(mm, pmd); + spin_unlock(ptl); + swap_put_entries_direct(sl, HPAGE_PMD_NR); + add_mm_counter(mm, MM_SWAPENTS, -HPAGE_PMD_NR); + return true; + } + is_present =3D pmd_present(orig_pmd); folio =3D normal_or_softleaf_folio_pmd(vma, addr, orig_pmd, is_present); has_deposit =3D has_deposited_pgtable(vma, orig_pmd, folio); @@ -2554,7 +2578,8 @@ static inline int pmd_move_must_withdraw(spinlock_t *= new_pmd_ptl, static pmd_t move_soft_dirty_pmd(pmd_t pmd) { if (pgtable_supports_soft_dirty()) { - if (unlikely(pmd_is_migration_entry(pmd))) + if (unlikely(pmd_is_migration_entry(pmd) || + pmd_is_swap_entry(pmd))) pmd =3D pmd_swp_mksoft_dirty(pmd); else if (pmd_present(pmd)) pmd =3D pmd_mksoft_dirty(pmd); @@ -2645,7 +2670,14 @@ static void change_non_present_huge_pmd(struct mm_st= ruct *mm, pmd_t newpmd; =20 VM_WARN_ON(!pmd_is_valid_softleaf(*pmd)); - if (softleaf_is_migration_write(entry)) { + + /* + * PMD swap entries don't encode write permission in the entry type, + * so only uffd_wp flag changes apply. No folio lookup needed. + */ + if (softleaf_is_swap(entry)) { + newpmd =3D *pmd; + } else if (softleaf_is_migration_write(entry)) { const struct folio *folio =3D softleaf_to_folio(entry); =20 /* @@ -2705,7 +2737,7 @@ int change_huge_pmd(struct mmu_gather *tlb, struct vm= _area_struct *vma, if (!ptl) return 0; =20 - if (thp_migration_supported() && pmd_is_valid_softleaf(*pmd)) { + if (pmd_is_valid_softleaf(*pmd)) { change_non_present_huge_pmd(mm, addr, pmd, uffd_prot, uffd_prot_resolve); goto unlock; @@ -3252,7 +3284,7 @@ static void __split_huge_pmd_locked(struct vm_area_st= ruct *vma, pmd_t *pmd, VM_WARN_ON_ONCE(freeze); old_pmd =3D *pmd; soft_dirty =3D pmd_swp_soft_dirty(old_pmd); - uffd_wp =3D pmd_swp_uffd_wp(old_pmd); + uffd_wp =3D pmd_swp_uffd(old_pmd); anon_exclusive =3D pmd_swp_exclusive(old_pmd); } else { /* diff --git a/mm/khugepaged.c b/mm/khugepaged.c index 45e8245d80da..3a44bf8206c0 100644 --- a/mm/khugepaged.c +++ b/mm/khugepaged.c @@ -1102,6 +1102,12 @@ static inline enum scan_result check_pmd_state(pmd_t= *pmd) */ if (pmd_is_migration_entry(pmde)) return SCAN_PMD_MAPPED; + /* + * A PMD-mapped THP that has been swapped out is still a THP from + * khugepaged's perspective; treat it like a present huge PMD. + */ + if (pmd_is_swap_entry(pmde)) + return SCAN_PMD_MAPPED; if (!pmd_present(pmde)) return SCAN_NO_PTE_TABLE; if (pmd_trans_huge(pmde)) diff --git a/mm/madvise.c b/mm/madvise.c index bf9ce199935a..e24166500ac5 100644 --- a/mm/madvise.c +++ b/mm/madvise.c @@ -374,6 +374,15 @@ static int madvise_cold_or_pageout_pte_range(pmd_t *pm= d, !can_do_file_pageout(vma); =20 #ifdef CONFIG_TRANSPARENT_HUGEPAGE + /* + * Swapped-out THPs have no resident folio to deactivate or reclaim. + * Avoid descending into or splitting a PMD swap entry. + */ + if (pmd_is_swap_entry(*pmd)) { + walk->action =3D ACTION_CONTINUE; + return 0; + } + if (pmd_trans_huge(*pmd)) { pmd_t orig_pmd; unsigned long next =3D pmd_addr_end(addr, end); @@ -384,6 +393,9 @@ static int madvise_cold_or_pageout_pte_range(pmd_t *pmd, return 0; =20 orig_pmd =3D *pmd; + if (pmd_is_swap_entry(orig_pmd)) + goto huge_unlock; + if (is_huge_zero_pmd(orig_pmd)) goto huge_unlock; =20 @@ -665,7 +677,7 @@ static int madvise_free_pte_range(pmd_t *pmd, unsigned = long addr, int nr, max_nr; =20 next =3D pmd_addr_end(addr, end); - if (pmd_trans_huge(*pmd)) + if (pmd_trans_huge(*pmd) || pmd_is_swap_entry(*pmd)) if (madvise_free_huge_pmd(tlb, vma, pmd, addr, next)) return 0; =20 diff --git a/mm/mincore.c b/mm/mincore.c index 53b982803771..ddf7c96964b0 100644 --- a/mm/mincore.c +++ b/mm/mincore.c @@ -99,6 +99,41 @@ static unsigned char mincore_swap(swp_entry_t entry, boo= l shmem) return present; } =20 +#ifdef CONFIG_THP_SWAP +static void mincore_pmd_swap(swp_entry_t entry, unsigned long addr, + unsigned long end, unsigned char *vec) +{ + unsigned long haddr =3D addr & HPAGE_PMD_MASK; + unsigned long start =3D (addr - haddr) >> PAGE_SHIFT; + unsigned long nr =3D (end - addr) >> PAGE_SHIFT; + struct folio *folio; + enum swap_pmd_cache state; + int i; + + state =3D swap_pmd_cache_lookup(entry, &folio); + if (state =3D=3D SWAP_PMD_CACHE_HUGE) { + memset(vec, folio_test_uptodate(folio), nr); + folio_put(folio); + return; + } + + if (state =3D=3D SWAP_PMD_CACHE_EMPTY) { + memset(vec, 0, nr); + return; + } + + /* + * The PMD swap entry is only a compact encoding for consecutive swap + * slots. If the PMD-sized swapcache folio was split, report residency + * from the individual slots covered by this mincore() range. + */ + for (i =3D 0; i < nr; i++) + vec[i] =3D mincore_swap(swp_entry(swp_type(entry), + swp_offset(entry) + start + i), + false); +} +#endif + /* * Later we can get more picky about what "in core" means precisely. * For now, simply check to see if the page is in the page cache, @@ -172,7 +207,15 @@ static int mincore_pte_range(pmd_t *pmd, unsigned long= addr, unsigned long end, =20 ptl =3D pmd_trans_huge_lock(pmd, vma); if (ptl) { - memset(vec, 1, nr); + if (pmd_is_swap_entry(*pmd)) { +#ifdef CONFIG_THP_SWAP + mincore_pmd_swap(softleaf_from_pmd(*pmd), addr, end, vec); +#else + memset(vec, 0, nr); +#endif + } else { + memset(vec, 1, nr); + } spin_unlock(ptl); goto out; } --=20 2.53.0-Meta From nobody Sat Jul 25 21:19:11 2026 Received: from out-174.mta1.migadu.com (out-174.mta1.migadu.com [95.215.58.174]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 625F025B0B8 for ; Mon, 13 Jul 2026 13:37:12 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.174 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949833; cv=none; b=jPf8pys+Sfwra0B19U1Mis3MDtip3CbgGtlG6NjDjKEw8VqYVpIXIV6TWVwbFZPx4L75wuGAd2/cY9ZAhbRJ48rsTiQIJYhazd9ugorIy8KOFxv9AwGBMXM8pNzHzePaRsivLviwgPcdHINbpAxldezdNjlaQHrZdpppbKn4VI8= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949833; c=relaxed/simple; bh=bNu9RFkDGlGkjfaSy9l9qBCkMrwb1O/YdtOZyEubtLM=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=PNnV4QGLWdDLHPEX51PxfVhlKX8QYXTXwzzrPjai0bKzDR6Do1a1VllTb267frzEBbXMdXMr6Nl7tjX58w0malxXfNyPJuYLyA/3ftHYKgcMCWcwcr5qKQjjA/VKTHoF6C31DoftGgW6lfXHJTK2AqxGSTIBnR3yu3JqX3xK8Y4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=oVe9ga5N; arc=none smtp.client-ip=95.215.58.174 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="oVe9ga5N" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1783949830; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=kvfvkCrmtZHr53aH3z5soGWWpLJ3f1epIMSekVEVwok=; b=oVe9ga5NwSI+QjrRnMyOZsfDhx5boGW7Uib0YK95Ky0Hs5RzGM0wkqy/DYq464ztEFoCu7 azGe6pgJaHs5F/LWaiAfLnO/KQNBpBySpFJapoy+kABYwd9861jmSF5PbFTJATsYyYJR1h c7z03WDv0e1nPWeEw+dTa1VrmlJyLio= From: Usama Arif To: Andrew Morton , david@kernel.org, chrisl@kernel.org, kasong@tencent.com, ljs@kernel.org, ziy@nvidia.com, linux-mm@kvack.org Cc: ying.huang@linux.alibaba.com, Baoquan He , willy@infradead.org, youngjun.park@lge.com, hannes@cmpxchg.org, riel@surriel.com, shakeel.butt@linux.dev, alex@ghiti.fr, kas@kernel.org, baohua@kernel.org, dev.jain@arm.com, baolin.wang@linux.alibaba.com, npache@redhat.com, Liam R. Howlett , ryan.roberts@arm.com, Vlastimil Babka , lance.yang@linux.dev, linux-kernel@vger.kernel.org, nphamcs@gmail.com, shikemeng@huaweicloud.com, yosry@kernel.org, kernel-team@meta.com, Usama Arif Subject: [PATCH v4 07/11] mm: handle PMD swap entries in MADV_WILLNEED Date: Mon, 13 Jul 2026 06:35:42 -0700 Message-ID: <20260713133613.2707815-8-usama.arif@linux.dev> In-Reply-To: <20260713133613.2707815-1-usama.arif@linux.dev> References: <20260713133613.2707815-1-usama.arif@linux.dev> 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 X-Migadu-Flow: FLOW_OUT Content-Type: text/plain; charset="utf-8" swapin_walk_pmd_entry() walks PTEs and skips non-present PMDs, so MADV_WILLNEED is a no-op on a PMD swap entry. Handle PMD swap entries under pmd_trans_huge_lock(). If the covered swap-cache range already has a PMD-sized folio, there is nothing left to prefetch. If the range has split cache state, or any covered slot currently has a zswap entry, split the PMD swap entry and ask the walker to retry so the PTE path can handle the individual slots. Otherwise pin the swap device and read the folio in at PMD order via swapin_sync(BIT(HPAGE_PMD_ORDER)). This keeps the subsequent fault on the do_huge_pmd_swap_page() path and avoids order-0 readahead needlessly splitting the PMD swap entry. If PMD-order swapin races with per-slot swap-cache population after dropping the PMD lock, split and retry through the PTE path instead. Signed-off-by: Usama Arif --- mm/madvise.c | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/mm/madvise.c b/mm/madvise.c index e24166500ac5..987966dbe927 100644 --- a/mm/madvise.c +++ b/mm/madvise.c @@ -32,6 +32,7 @@ #include #include #include +#include =20 #include =20 @@ -193,6 +194,79 @@ static int swapin_walk_pmd_entry(pmd_t *pmd, unsigned = long start, spinlock_t *ptl; unsigned long addr; =20 + ptl =3D pmd_trans_huge_lock(pmd, vma); + if (ptl) { + pmd_t pmdval =3D *pmd; + + if (pmd_is_swap_entry(pmdval)) { + softleaf_t entry =3D softleaf_from_pmd(pmdval); + struct vm_fault vmf =3D { + .vma =3D vma, + .address =3D start, + .real_address =3D start, + .pmd =3D pmd, + }; + struct swap_info_struct *si; + struct folio *folio; + enum swap_pmd_cache cache_state; + bool split =3D false; + + cache_state =3D swap_pmd_cache_lookup(entry, &folio); + if (cache_state =3D=3D SWAP_PMD_CACHE_HUGE) { + folio_put(folio); + spin_unlock(ptl); + goto ret; + } + if (cache_state =3D=3D SWAP_PMD_CACHE_SPLIT || + zswap_is_present(entry, HPAGE_PMD_NR)) { + spin_unlock(ptl); + __split_huge_pmd(vma, pmd, start, false); + walk->action =3D ACTION_AGAIN; + goto ret; + } + + /* + * Pin the swap device under the PMD lock so the + * PMD-swap-entry observation keeps the entry valid for + * swapin_sync(). + */ + si =3D get_swap_device(entry); + spin_unlock(ptl); + if (!si) + goto ret; + + folio =3D swapin_sync(entry, GFP_HIGHUSER_MOVABLE, + BIT(HPAGE_PMD_ORDER), &vmf, + NULL, 0); + /* + * The empty-cache observation was made under the PMD + * lock, but swap cache can change after dropping it. If + * PMD-order swapin lost a race to per-slot cache state, + * retry through the PTE path. + */ + if (IS_ERR(folio)) { + if (PTR_ERR(folio) =3D=3D -EBUSY) + split =3D true; + } else if (folio) { + if (folio_nr_pages(folio) !=3D HPAGE_PMD_NR) + split =3D true; + else if (!folio_test_locked(folio) && + !folio_test_uptodate(folio) && + zswap_is_present(entry, + HPAGE_PMD_NR)) + split =3D true; + folio_put(folio); + } + put_swap_device(si); + if (split) { + __split_huge_pmd(vma, pmd, start, false); + walk->action =3D ACTION_AGAIN; + } + goto ret; + } + spin_unlock(ptl); + } + for (addr =3D start; addr < end; addr +=3D PAGE_SIZE) { pte_t pte; softleaf_t entry; @@ -221,6 +295,7 @@ static int swapin_walk_pmd_entry(pmd_t *pmd, unsigned l= ong start, if (ptep) pte_unmap_unlock(ptep, ptl); swap_read_unplug(splug); +ret: cond_resched(); =20 return 0; --=20 2.53.0-Meta From nobody Sat Jul 25 21:19:11 2026 Received: from out-172.mta1.migadu.com (out-172.mta1.migadu.com [95.215.58.172]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 51DB92F7EF8 for ; Mon, 13 Jul 2026 13:37:19 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.172 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949841; cv=none; b=B94WY5rT/Lhf00zKaIVnceBhb5C4y6vjG0jKBvZu/qguOQHr6hiJ5MtaIqtuHRGLK8UIl7ACHL4c2DBHBUJ/g1ZAMzgpsOTaK4j3r+rrUGzMAC6JiQCNW/G7Y0VEBBcRYe69vcRW7wmi0bqUVIMtxr91+S82FnTYGiyp/kTS55w= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949841; c=relaxed/simple; bh=NxOgYgOc7gOj1k/Fa/Vlp3vH1DbbNj/NsV7V+leBRxA=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=tcWdZyotyARhmbac9DJbtM1cfnknZPoXRPpwebGnuzqftCCl8q91ms+28mRFHt+tEQPs4VFVeEHyj+Z6FnX+eMBnHI/qVqHMHADl/7EqsharHPnptt/ObjG1vE3OHrwEGuXyUvKf+bF8qK8drYI2QTEClkVn1voKKbj4xW5dBeM= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=Emoa6QQ3; arc=none smtp.client-ip=95.215.58.172 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="Emoa6QQ3" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1783949836; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=JtWebUj//4zHLEaAYlPy0cK7ae9CikSRWhDrI9+mSdA=; b=Emoa6QQ3ocNBAiKqF9g2zYAZ7ndnflpEbY26WPCX1tR4De1HNkRlkEcO+Fg1p+ozYwaIeR 6lzsU0UotNO4TECLbEvTy6RakTdXxF+5RmQOVcFbYmo0jx3ISldGbQsoiqyM534NRIcCAh 3MwHfAvxWxCBrLUH35pJ+8IUiJMmPi4= From: Usama Arif To: Andrew Morton , david@kernel.org, chrisl@kernel.org, kasong@tencent.com, ljs@kernel.org, ziy@nvidia.com, linux-mm@kvack.org Cc: ying.huang@linux.alibaba.com, Baoquan He , willy@infradead.org, youngjun.park@lge.com, hannes@cmpxchg.org, riel@surriel.com, shakeel.butt@linux.dev, alex@ghiti.fr, kas@kernel.org, baohua@kernel.org, dev.jain@arm.com, baolin.wang@linux.alibaba.com, npache@redhat.com, Liam R. Howlett , ryan.roberts@arm.com, Vlastimil Babka , lance.yang@linux.dev, linux-kernel@vger.kernel.org, nphamcs@gmail.com, shikemeng@huaweicloud.com, yosry@kernel.org, kernel-team@meta.com, Usama Arif Subject: [PATCH v4 08/11] mm: handle PMD swap entries in UFFDIO_MOVE Date: Mon, 13 Jul 2026 06:35:43 -0700 Message-ID: <20260713133613.2707815-9-usama.arif@linux.dev> In-Reply-To: <20260713133613.2707815-1-usama.arif@linux.dev> References: <20260713133613.2707815-1-usama.arif@linux.dev> 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 X-Migadu-Flow: FLOW_OUT Content-Type: text/plain; charset="utf-8" move_pages_huge_pmd() returned -ENOENT for any non-trans_huge, non-migration PMD, which fails aligned UFFDIO_MOVE on a swapped-out THP -- the PMD swap entry is a perfectly valid mapping that should move whole. Splitting via the move_pages_ptes() fallback isn't a substitute either: __split_huge_pmd_locked() splits a PMD swap entry into HPAGE_PMD_NR PTE swap entries pointing at the same swap-cache folio, but move_swap_pte() refuses any swap-cache folio that is still large and returns -EBUSY. Add move_swap_pmd(), modeled on move_swap_pte(), that moves the swap entry whole-PMD and re-anchors a PMD-sized swap-cache folio's anon rmap to the destination VMA. Reject !pmd_swp_exclusive() entries with -EBUSY to preserve UFFDIO_MOVE's single-owner semantics, propagate soft-dirty, and carry the deposited page table across with the entry. The dispatcher in move_pages_huge_pmd() now waits for migration on a PMD migration entry (matching the PTE path) and routes PMD swap entries through move_swap_pmd() after pinning the swap device and arming an mmu_notifier range so secondary MMUs see the move. Before moving, classify the whole PMD swap-cache range with swap_pmd_cache_lookup(). A PMD swap entry can be moved whole only if the covered range is empty or backed by one PMD-sized folio. If the range already has per-slot cache state, split the PMD swap entry and return -EAGAIN so the caller retries through the PTE path. If a PMD-sized folio is cached, lock and revalidate that it still matches the PMD swap entry. If no folio is cached, recheck all HPAGE_PMD_NR slots under both PMD locks before moving the entry; any per-slot folio that appears needs the PTE move path to update its rmap metadata. This avoids moving the PMD while cached folios still point at the old anon_vma/index. Signed-off-by: Usama Arif --- mm/huge_memory.c | 133 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 1 deletion(-) diff --git a/mm/huge_memory.c b/mm/huge_memory.c index 991929ec17d8..b8e3566bd502 100644 --- a/mm/huge_memory.c +++ b/mm/huge_memory.c @@ -2864,6 +2864,72 @@ int change_huge_pud(struct mmu_gather *tlb, struct v= m_area_struct *vma, #endif =20 #ifdef CONFIG_USERFAULTFD +/* + * Move a PMD-level swap entry from src_pmd to dst_pmd. Both PMD locks are + * acquired here; src_folio (if present) must already be locked. The depos= ited + * page table backing the source THP is moved across with the entry. + */ +static int move_swap_pmd(struct mm_struct *mm, struct vm_area_struct *dst_= vma, + unsigned long dst_addr, unsigned long src_addr, + pmd_t *dst_pmd, pmd_t *src_pmd, + pmd_t orig_dst_pmd, pmd_t orig_src_pmd, + spinlock_t *dst_ptl, spinlock_t *src_ptl, + struct folio *src_folio, swp_entry_t entry) +{ + pgtable_t src_pgtable; + pmd_t moved_pmd; + + /* + * The folio may have been freed and reused for a different swap entry + * while it was unlocked. Re-verify the association. + */ + if (src_folio && unlikely(!folio_matches_swap_entry(src_folio, entry) || + folio_nr_pages(src_folio) !=3D HPAGE_PMD_NR)) + return -EAGAIN; + + double_pt_lock(dst_ptl, src_ptl); + + if (!pmd_same(*src_pmd, orig_src_pmd) || + !pmd_same(*dst_pmd, orig_dst_pmd)) { + double_pt_unlock(dst_ptl, src_ptl); + return -EAGAIN; + } + + /* + * If the folio is in the swap cache, re-anchor its anon rmap to the + * destination VMA so a future swap-in fault at dst_addr finds it. + * Otherwise, re-check the whole PMD swap range: a PMD swap entry is + * only a compact encoding for 512 swap slots, and any per-slot cached + * folio would need the PTE move path to update its rmap metadata. + */ + if (src_folio) { + folio_move_anon_rmap(src_folio, dst_vma); + src_folio->index =3D linear_page_index(dst_vma, dst_addr); + } else { + unsigned int type =3D swp_type(entry); + pgoff_t offset =3D swp_offset(entry); + int i; + + for (i =3D 0; i < HPAGE_PMD_NR; i++) { + if (swap_cache_has_folio(swp_entry(type, offset + i))) { + double_pt_unlock(dst_ptl, src_ptl); + return -EAGAIN; + } + } + } + + moved_pmd =3D pmdp_huge_get_and_clear(mm, src_addr, src_pmd); + if (pgtable_supports_soft_dirty()) + moved_pmd =3D pmd_swp_mksoft_dirty(moved_pmd); + set_pmd_at(mm, dst_addr, dst_pmd, moved_pmd); + + src_pgtable =3D pgtable_trans_huge_withdraw(mm, src_pmd); + pgtable_trans_huge_deposit(mm, dst_pmd, src_pgtable); + + double_pt_unlock(dst_ptl, src_ptl); + return 0; +} + /* * The PT lock for src_pmd and dst_vma/src_vma (for reading) are locked by * the caller, but it must return after releasing the page_table_lock. @@ -2898,11 +2964,76 @@ int move_pages_huge_pmd(struct mm_struct *mm, pmd_t= *dst_pmd, pmd_t *src_pmd, pm } =20 if (!pmd_trans_huge(src_pmdval)) { - spin_unlock(src_ptl); if (pmd_is_migration_entry(src_pmdval)) { + spin_unlock(src_ptl); pmd_migration_entry_wait(mm, src_pmd); return -EAGAIN; } + if (pmd_is_swap_entry(src_pmdval)) { + swp_entry_t entry; + struct swap_info_struct *si; + enum swap_pmd_cache cache_state; + + /* + * UFFDIO_MOVE on anon mappings requires single-owner + * semantics; refuse to move a shared swap entry. + */ + if (!pmd_swp_exclusive(src_pmdval)) { + spin_unlock(src_ptl); + return -EBUSY; + } + + entry =3D softleaf_from_pmd(src_pmdval); + spin_unlock(src_ptl); + + /* Pin the swap device against a racing swapoff. */ + si =3D get_swap_device(entry); + if (unlikely(!si)) + return -EAGAIN; + + src_folio =3D NULL; + cache_state =3D swap_pmd_cache_lookup(entry, &src_folio); + if (cache_state =3D=3D SWAP_PMD_CACHE_SPLIT) { + put_swap_device(si); + __split_huge_pmd(src_vma, src_pmd, src_addr, false); + return -EAGAIN; + } + + mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, + mm, src_addr, + src_addr + HPAGE_PMD_SIZE); + mmu_notifier_invalidate_range_start(&range); + + if (src_folio) { + folio_lock(src_folio); + if (!folio_matches_swap_entry(src_folio, entry) || + folio_nr_pages(src_folio) !=3D HPAGE_PMD_NR) { + err =3D -EAGAIN; + folio_unlock(src_folio); + folio_put(src_folio); + mmu_notifier_invalidate_range_end(&range); + put_swap_device(si); + __split_huge_pmd(src_vma, src_pmd, + src_addr, false); + return err; + } + } + + dst_ptl =3D pmd_lockptr(mm, dst_pmd); + err =3D move_swap_pmd(mm, dst_vma, dst_addr, src_addr, + dst_pmd, src_pmd, dst_pmdval, + src_pmdval, dst_ptl, src_ptl, + src_folio, entry); + + mmu_notifier_invalidate_range_end(&range); + if (src_folio) { + folio_unlock(src_folio); + folio_put(src_folio); + } + put_swap_device(si); + return err; + } + spin_unlock(src_ptl); return -ENOENT; } =20 --=20 2.53.0-Meta From nobody Sat Jul 25 21:19:11 2026 Received: from out-186.mta0.migadu.com (out-186.mta0.migadu.com [91.218.175.186]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 12AEE42DA47 for ; Mon, 13 Jul 2026 13:37:24 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.186 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949847; cv=none; b=PVFNN+kDUTLEfYBjUod6xNxB6T4BM13IfZeuLCp1eGbsPyV94kwoHk5vZ+0O677WGLmzfT6ZNrS/zCEo690uDyNB/1jzexFprQsXdw58lXzdUb+AcSHFJM4sgIq+FrIMAHO6fqC3tRtGgfxno/UXbCi3tNpCR5WuDIlQAukLE1U= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949847; c=relaxed/simple; bh=SWGWLxDOZRTW0VaHKRwOPiaDQp/UzPvp+kX7Sw+5Txo=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=kbMhj0sts6EMLNeCYDBmtgXJQXR2B0q/86kM5WWAt3ln2SVaZzrFMT544n4QBJ4ptFaYL1JNes3/R5BUyL8M8kFrf35U4TID3OmLl5Iip9JdPU96GPax2wo5OKZCKmjYmwqMgeWFBukzLNH8tdqrEf7bRRjlUQIN6N9tZGjvZaw= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=DxnVzgoF; arc=none smtp.client-ip=91.218.175.186 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="DxnVzgoF" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1783949842; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=HBpFn91MvbR629AVCu7mmit7ydRcm4nx9l1DfjdpkDY=; b=DxnVzgoFccfS88yAHrsQu+PB9gljv8nn7Q+19HNhW7RyL5nGf4inPCk8jI6VZZaErYeJuU 6xYqtxmkEPJB8m9+cAVKR/ESfmQ5C2n/g3B3E64CBnEqdcdu7c8qwLsB+gwZEMGxbUGh6o MTWAYeUvuzExsggUC0iIuJI4FfnqxzA= From: Usama Arif To: Andrew Morton , david@kernel.org, chrisl@kernel.org, kasong@tencent.com, ljs@kernel.org, ziy@nvidia.com, linux-mm@kvack.org Cc: ying.huang@linux.alibaba.com, Baoquan He , willy@infradead.org, youngjun.park@lge.com, hannes@cmpxchg.org, riel@surriel.com, shakeel.butt@linux.dev, alex@ghiti.fr, kas@kernel.org, baohua@kernel.org, dev.jain@arm.com, baolin.wang@linux.alibaba.com, npache@redhat.com, Liam R. Howlett , ryan.roberts@arm.com, Vlastimil Babka , lance.yang@linux.dev, linux-kernel@vger.kernel.org, nphamcs@gmail.com, shikemeng@huaweicloud.com, yosry@kernel.org, kernel-team@meta.com, Usama Arif Subject: [PATCH v4 09/11] mm: handle PMD swap entry faults on swap-in Date: Mon, 13 Jul 2026 06:35:44 -0700 Message-ID: <20260713133613.2707815-10-usama.arif@linux.dev> In-Reply-To: <20260713133613.2707815-1-usama.arif@linux.dev> References: <20260713133613.2707815-1-usama.arif@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable X-Migadu-Flow: FLOW_OUT Add do_huge_pmd_swap_page() and dispatch to it from __handle_mm_fault() when vmf->orig_pmd encodes a swap entry. The handler resolves the entire 2 MB mapping in one shot, mirroring do_swap_page() (PTE path) at PMD granularity: - Look up the folio in the swap cache; on a miss, allocate a PMD-order folio via swapin_sync(BIT(HPAGE_PMD_ORDER)) and read from swap. This deliberately skips the existing order-0 swap readahead paths: the fault already asks for the whole PMD range, while order-0 readahead would populate per-page swap cache state and force the PMD swap entry to split. - After locking, re-validate that the folio still corresponds to our entry and is still PMD-sized. Between the unlocked cache lookup and the lock, a racing swap-in on the same entry may have removed it from the cache via folio_free_swap(), or reclaim / memory_failure / deferred-split may have split the folio into smaller folios. - Refuse to map a folio that contains a hardware-poisoned subpage: folio_contain_hwpoisoned_page() covers PG_hwpoison on the head and PG_has_hwpoisoned on a large folio. On a hit, split the PMD swap entry so do_swap_page() can return VM_FAULT_HWPOISON per subpage, matching the PTE swap-in PageHWPoison check. - Restore soft_dirty and uffd_wp from the swap PMD. Map writable only when the entry was exclusive, the VMA permits writes, and uffd-wp is not armed. Drop the exclusive marker when the cached folio is under writeback to an SWP_STABLE_WRITES backend (zram, encrypted) so the PMD is mapped read-only; a later write COWs into a fresh folio rather than corrupting the in-flight writeback. Mirrors do_swap_page(). - When the resulting PMD is read-only but the fault was a write, update vmf->orig_pmd and call wp_huge_pmd() in the same handler to COW immediately rather than forcing a second fault. Mask VM_FAULT_FALLBACK from its return: a PMD-COW that splits to PTE-level is normal, but the bit is part of VM_FAULT_ERROR and arch fault handlers BUG() on it without SIGBUS/HWPOISON/SIGSEGV. Requires exposing wp_huge_pmd() via mm/internal.h. - Free the swap slot via should_try_to_free_swap() (hoisted from mm/memory.c into mm/internal.h so PTE- and PMD-level swap-in share the heuristic). When PMD-order resources are unavailable (folio allocation fails, the cached folio was split, memcg charge fails, or swapin_folio() races) split the PMD swap entry into 512 PTE swap entries via __split_huge_pmd() and return 0. The fault retries and do_swap_page() takes over per-PTE. This avoids returning VM_FAULT_OOM for transient PMD-order allocation failures. Signed-off-by: Usama Arif --- include/linux/huge_mm.h | 9 ++ mm/huge_memory.c | 228 ++++++++++++++++++++++++++++++++++++++++ mm/internal.h | 34 ++++++ mm/memory.c | 38 +------ 4 files changed, 275 insertions(+), 34 deletions(-) diff --git a/include/linux/huge_mm.h b/include/linux/huge_mm.h index cae97c307280..b6ab28ff7eee 100644 --- a/include/linux/huge_mm.h +++ b/include/linux/huge_mm.h @@ -534,6 +534,15 @@ vm_fault_t do_huge_pmd_uffd_rwp(struct vm_fault *vmf); =20 vm_fault_t do_huge_pmd_device_private(struct vm_fault *vmf); =20 +#ifdef CONFIG_THP_SWAP +vm_fault_t do_huge_pmd_swap_page(struct vm_fault *vmf); +#else +static inline vm_fault_t do_huge_pmd_swap_page(struct vm_fault *vmf) +{ + return 0; +} +#endif + extern struct folio *huge_zero_folio; extern unsigned long huge_zero_pfn; =20 diff --git a/mm/huge_memory.c b/mm/huge_memory.c index b8e3566bd502..60f42d136945 100644 --- a/mm/huge_memory.c +++ b/mm/huge_memory.c @@ -41,6 +41,7 @@ #include #include #include +#include =20 #include #include "internal.h" @@ -2347,6 +2348,233 @@ vm_fault_t do_huge_pmd_numa_page(struct vm_fault *v= mf) return 0; } =20 +#ifdef CONFIG_THP_SWAP +/** + * do_huge_pmd_swap_page() - Handle a fault on a PMD-level swap entry. + * @vmf: Fault context. vmf->orig_pmd contains the swap PMD. + * + * A PMD swap entry is a compact encoding for HPAGE_PMD_NR consecutive swap + * slots. If the swap cache still has one PMD-sized folio covering the ran= ge, + * map it directly at PMD level. If the range has been split into per-page + * cache state, or zswap may have per-page state for it, split the PMD swap + * entry and retry at PTE granularity. + * + * Return: VM_FAULT_* flags. + */ +vm_fault_t do_huge_pmd_swap_page(struct vm_fault *vmf) +{ + struct vm_area_struct *vma =3D vmf->vma; + struct mm_struct *mm =3D vma->vm_mm; + struct folio *folio; + struct page *page; + struct swap_info_struct *si; + unsigned long haddr =3D vmf->address & HPAGE_PMD_MASK; + softleaf_t entry; + swp_entry_t swp_entry; + pmd_t pmd; + vm_fault_t ret =3D 0; + bool exclusive; + rmap_t rmap_flags =3D RMAP_NONE; + enum swap_pmd_cache cache_state; + + entry =3D softleaf_from_pmd(vmf->orig_pmd); + if (unlikely(!softleaf_is_swap(entry))) + return 0; + + swp_entry =3D entry; + + /* Prevent swapoff from happening to us. */ + si =3D get_swap_device(swp_entry); + if (unlikely(!si)) + return 0; + + cache_state =3D swap_pmd_cache_lookup(swp_entry, &folio); + if (cache_state =3D=3D SWAP_PMD_CACHE_SPLIT) + goto split_fallback; + if (!folio) { + /* + * PMD swap entries encode ordinary per-page swap slots. If any + * slot is in zswap, split and let the PTE swap path load the + * range per page. Otherwise the range is all on disk and can be + * read back as one PMD-sized folio. + */ + if (zswap_is_present(swp_entry, HPAGE_PMD_NR)) + goto split_fallback; + + folio =3D swapin_sync(swp_entry, GFP_HIGHUSER_MOVABLE, + BIT(HPAGE_PMD_ORDER), vmf, NULL, 0); + if (IS_ERR_OR_NULL(folio)) + goto split_fallback; + + /* Had to read from swap area: Major fault */ + ret =3D VM_FAULT_MAJOR; + count_vm_event(PGMAJFAULT); + count_memcg_event_mm(mm, PGMAJFAULT); + } + + ret |=3D folio_lock_or_retry(folio, vmf); + if (ret & VM_FAULT_RETRY) + goto out_release; + + /* Verify the folio is still in swap cache and matches our entry */ + if (unlikely(!folio_matches_swap_entry(folio, swp_entry))) + goto out_page; + + /* + * Folio should be PMD-sized; if not (e.g. split in swap cache), + * split the PMD swap entry and retry at PTE level. + */ + if (folio_nr_pages(folio) !=3D HPAGE_PMD_NR) { + folio_unlock(folio); + folio_put(folio); + goto split_fallback; + } + + if (unlikely(!folio_test_uptodate(folio))) { + if (zswap_is_present(swp_entry, HPAGE_PMD_NR)) { + folio_unlock(folio); + folio_put(folio); + goto split_fallback; + } + ret =3D VM_FAULT_SIGBUS; + goto out_page; + } + + /* + * If any subpage is hardware-poisoned, split the PMD swap entry and + * let the PTE swap-in path handle each page individually so + * do_swap_page() can return VM_FAULT_HWPOISON for the poisoned + * subpage rather than mapping the corrupted memory as one THP. + */ + if (unlikely(folio_contain_hwpoisoned_page(folio))) { + folio_unlock(folio); + folio_put(folio); + goto split_fallback; + } + + page =3D folio_page(folio, 0); + arch_swap_restore(folio_swap(swp_entry, folio), folio); + + if ((vmf->flags & FAULT_FLAG_WRITE) && !folio_test_lru(folio)) + lru_add_drain(); + + folio_throttle_swaprate(folio, GFP_KERNEL); + + /* Lock the PMD and verify it hasn't changed */ + vmf->ptl =3D pmd_lock(mm, vmf->pmd); + if (unlikely(!pmd_same(vmf->orig_pmd, pmdp_get(vmf->pmd)))) { + spin_unlock(vmf->ptl); + goto out_page; + } + + exclusive =3D pmd_swp_exclusive(vmf->orig_pmd); + + /* + * Some swap backends (e.g. zram) don't support concurrent page + * modifications while under writeback. If we map exclusive on such + * a backend while the folio is still under writeback, the writeback + * may see partial modifications and corrupt the swap slot. Drop the + * exclusive marker and only map R/O for that case; further GUP + * references can't appear once the page is fully unmapped, so this + * is safe. + */ + if (exclusive && folio_test_writeback(folio) && + data_race(si->flags & SWP_STABLE_WRITES)) + exclusive =3D false; + + /* + * Set up the PMD mapping. Similar to do_swap_page() but at PMD level. + */ + add_mm_counter(mm, MM_ANONPAGES, HPAGE_PMD_NR); + add_mm_counter(mm, MM_SWAPENTS, -HPAGE_PMD_NR); + + pmd =3D folio_mk_pmd(folio, vma->vm_page_prot); + pmd =3D pmd_mkyoung(pmd); + + if (pmd_swp_soft_dirty(vmf->orig_pmd)) + pmd =3D pmd_mksoft_dirty(pmd); + if (pmd_swp_uffd(vmf->orig_pmd)) + pmd =3D pmd_mkuffd(pmd); + + /* + * Check exclusivity to determine if we can map writable. + */ + if (exclusive || folio_ref_count(folio) =3D=3D 1) { + if ((vma->vm_flags & VM_WRITE) && + !userfaultfd_huge_pmd_wp(vma, pmd) && + !pmd_needs_soft_dirty_wp(vma, pmd)) { + pmd =3D pmd_mkwrite(pmd, vma); + if (vmf->flags & FAULT_FLAG_WRITE) { + pmd =3D pmd_mkdirty(pmd); + vmf->flags &=3D ~FAULT_FLAG_WRITE; + } + } + rmap_flags |=3D RMAP_EXCLUSIVE; + } + + flush_icache_pages(vma, page, HPAGE_PMD_NR); + + if (!folio_test_anon(folio)) + folio_add_new_anon_rmap(folio, vma, haddr, rmap_flags); + else + folio_add_anon_rmap_pmd(folio, page, vma, haddr, rmap_flags); + + folio_put_swap(folio, NULL); + + set_pmd_at(mm, haddr, vmf->pmd, pmd); + update_mmu_cache_pmd(vma, haddr, vmf->pmd); + + /* Update orig_pmd for any follow-up wp_huge_pmd() below. */ + vmf->orig_pmd =3D pmd; + + /* + * Conditionally try to free up the swap cache. Do it after mapping, + * so raced page faults will likely see the folio in swap cache and + * wait on the folio lock. + */ + if (should_try_to_free_swap(si, folio, vma, exclusive, vmf->flags)) + folio_free_swap(folio); + + spin_unlock(vmf->ptl); + + folio_unlock(folio); + put_swap_device(si); + + /* + * If the write fault wasn't satisfied above (folio is shared without + * exclusivity), fall through to wp_huge_pmd to handle COW or + * userfaultfd-wp without forcing a second fault. + * + * wp_huge_pmd() may return VM_FAULT_FALLBACK if it had to split the + * PMD; that's a normal outcome =E2=80=94 the natural PTE-level refault w= ill + * complete the COW. Mask it so callers (and the arch fault handler) + * don't see VM_FAULT_FALLBACK as a fatal VM_FAULT_ERROR. + */ + if (vmf->flags & FAULT_FLAG_WRITE) { + vm_fault_t wp_ret =3D wp_huge_pmd(vmf); + + wp_ret &=3D ~VM_FAULT_FALLBACK; + ret |=3D wp_ret; + if (ret & VM_FAULT_ERROR) + ret &=3D VM_FAULT_ERROR; + } + + return ret; + +out_page: + folio_unlock(folio); +out_release: + folio_put(folio); + put_swap_device(si); + return ret; + +split_fallback: + __split_huge_pmd(vma, vmf->pmd, haddr, false); + put_swap_device(si); + return 0; +} +#endif /* CONFIG_THP_SWAP */ + static inline void zap_deposited_table(struct mm_struct *mm, pmd_t *pmd) { pgtable_t pgtable; diff --git a/mm/internal.h b/mm/internal.h index 874be94cf257..660cb27c216a 100644 --- a/mm/internal.h +++ b/mm/internal.h @@ -573,6 +573,40 @@ static inline vm_fault_t vmf_anon_prepare(struct vm_fa= ult *vmf) } =20 vm_fault_t do_swap_page(struct vm_fault *vmf); +vm_fault_t wp_huge_pmd(struct vm_fault *vmf); + +/* + * Check if we should call folio_free_swap to free the swap cache. + * folio_free_swap only frees the swap cache to release the slot if swap + * count is zero, so we don't need to check the swap count here. + */ +static inline bool should_try_to_free_swap(struct swap_info_struct *si, + struct folio *folio, + struct vm_area_struct *vma, + bool exclusive, + unsigned int fault_flags) +{ + if (!folio_test_swapcache(folio)) + return false; + /* + * Always try to free swap cache for SWP_SYNCHRONOUS_IO devices. Swap + * cache can help save some IO or memory overhead, but these devices + * are fast, and meanwhile, swap cache pinning the slot deferring the + * release of metadata or fragmentation is a more critical issue. + */ + if (data_race(si->flags & SWP_SYNCHRONOUS_IO)) + return true; + if (mem_cgroup_swap_full(folio) || (vma->vm_flags & VM_LOCKED) || + folio_test_mlocked(folio)) + return true; + + /* + * Free the swapcache only if we are the exclusive user and + * this is a write fault. + */ + return (fault_flags & FAULT_FLAG_WRITE) && exclusive; +} + void folio_rotate_reclaimable(struct folio *folio); bool __folio_end_writeback(struct folio *folio); void deactivate_file_folio(struct folio *folio); diff --git a/mm/memory.c b/mm/memory.c index 21ea76bb4c37..08baf528d4a4 100644 --- a/mm/memory.c +++ b/mm/memory.c @@ -4585,38 +4585,6 @@ static vm_fault_t remove_device_exclusive_entry(stru= ct vm_fault *vmf) return 0; } =20 -/* - * Check if we should call folio_free_swap to free the swap cache. - * folio_free_swap only frees the swap cache to release the slot if swap - * count is zero, so we don't need to check the swap count here. - */ -static inline bool should_try_to_free_swap(struct swap_info_struct *si, - struct folio *folio, - struct vm_area_struct *vma, - bool exclusive, - unsigned int fault_flags) -{ - if (!folio_test_swapcache(folio)) - return false; - /* - * Always try to free swap cache for SWP_SYNCHRONOUS_IO devices. Swap - * cache can help save some IO or memory overhead, but these devices - * are fast, and meanwhile, swap cache pinning the slot deferring the - * release of metadata or fragmentation is a more critical issue. - */ - if (data_race(si->flags & SWP_SYNCHRONOUS_IO)) - return true; - if (mem_cgroup_swap_full(folio) || (vma->vm_flags & VM_LOCKED) || - folio_test_mlocked(folio)) - return true; - - /* - * Free the swapcache only if we are the exclusive user and - * this is a write fault. - */ - return (fault_flags & FAULT_FLAG_WRITE) && exclusive; -} - static vm_fault_t pte_marker_clear(struct vm_fault *vmf) { vmf->pte =3D pte_offset_map_lock(vmf->vma->vm_mm, vmf->pmd, @@ -6331,8 +6299,7 @@ static inline vm_fault_t create_huge_pmd(struct vm_fa= ult *vmf) return VM_FAULT_FALLBACK; } =20 -/* `inline' is required to avoid gcc 4.1.2 build error */ -static inline vm_fault_t wp_huge_pmd(struct vm_fault *vmf) +vm_fault_t wp_huge_pmd(struct vm_fault *vmf) { struct vm_area_struct *vma =3D vmf->vma; const bool unshare =3D vmf->flags & FAULT_FLAG_UNSHARE; @@ -6625,6 +6592,9 @@ static vm_fault_t __handle_mm_fault(struct vm_area_st= ruct *vma, =20 if (pmd_is_migration_entry(vmf.orig_pmd)) pmd_migration_entry_wait(mm, vmf.pmd); + else if (IS_ENABLED(CONFIG_THP_SWAP) && + pmd_is_swap_entry(vmf.orig_pmd)) + return do_huge_pmd_swap_page(&vmf); return 0; } if (pmd_trans_huge(vmf.orig_pmd)) { --=20 2.53.0-Meta From nobody Sat Jul 25 21:19:11 2026 Received: from out-181.mta1.migadu.com (out-181.mta1.migadu.com [95.215.58.181]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id B632F42B740 for ; Mon, 13 Jul 2026 13:37:31 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.181 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949853; cv=none; b=AVt55myayxZnaoLBP5gPApEGwhUvcaokk6Y1XMLHgjSfLD8aeqpPT1SQmj5a4ZI+WbrIZY0e1CgR7oGP42+2U/VTenfCQamENtLzLKf2mX3GMK/monkT4riqf4akFZUlYkaTiF6UcDQUILz2C4E/BbD7wI6lmr343DfJVfpDtlE= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949853; c=relaxed/simple; bh=agKzF1Ji/a6Ljr/emletUNIb6mdJi84MCWA+maY/lto=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=pbHNonyJBypSQiky+Q/b8c+kB7cuD0gR0qEcoB1RL5Vcc6fbuvTrX2rRZ9HtVFx0jhxSNniDH8loA8rnLmpmR/Xk0PuY1ag82k1WEeMnwwE4bAVeXTLr3pvutVm6h45lilAabWso9aHkZGY2+8q0W2QuNX4Yl6zPCsNaYiBdXIg= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=fBbkAIFQ; arc=none smtp.client-ip=95.215.58.181 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="fBbkAIFQ" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1783949849; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=kquTgKG2tUftdhkjvrowfIPLMW91GHe6VxDcWet49Dg=; b=fBbkAIFQS3zQFlWxPb/v61B6TMHh3+fV7PqurWDyL/D/GL+VDJsFFmswg59dmBUY10cbU8 oiE2PBPYl7M/3Trby0a5ntlgn3WLY7SI5cmCmIKvTXu9AsInS7WtgplVLruizirQUtdy2m ZUglO3A16XD9o3YF1hGgelR2kzGYmUM= From: Usama Arif To: Andrew Morton , david@kernel.org, chrisl@kernel.org, kasong@tencent.com, ljs@kernel.org, ziy@nvidia.com, linux-mm@kvack.org Cc: ying.huang@linux.alibaba.com, Baoquan He , willy@infradead.org, youngjun.park@lge.com, hannes@cmpxchg.org, riel@surriel.com, shakeel.butt@linux.dev, alex@ghiti.fr, kas@kernel.org, baohua@kernel.org, dev.jain@arm.com, baolin.wang@linux.alibaba.com, npache@redhat.com, Liam R. Howlett , ryan.roberts@arm.com, Vlastimil Babka , lance.yang@linux.dev, linux-kernel@vger.kernel.org, nphamcs@gmail.com, shikemeng@huaweicloud.com, yosry@kernel.org, kernel-team@meta.com, Usama Arif Subject: [PATCH v4 10/11] mm: install PMD swap entries on swap-out Date: Mon, 13 Jul 2026 06:35:45 -0700 Message-ID: <20260713133613.2707815-11-usama.arif@linux.dev> In-Reply-To: <20260713133613.2707815-1-usama.arif@linux.dev> References: <20260713133613.2707815-1-usama.arif@linux.dev> 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 X-Migadu-Flow: FLOW_OUT Content-Type: text/plain; charset="utf-8" Reclaim today splits a PMD-mapped anonymous THP into 512 PTE swap entries before unmap, losing the huge mapping across the swap round-trip and forcing khugepaged to rebuild it later. The contiguous swap range was already secured when the folio was added to the swap cache (a non-contiguous allocation would have split the folio earlier), so the PMD can be replaced by a single PMD-level swap entry instead. This patch mirrors the existing PTE swap-out path at PMD granularity: - shrink_folio_list() drops TTU_SPLIT_HUGE_PMD for PMD-mappable swapcache folios. zswap is handled by the PMD swap-in users: if any covered slot currently has a zswap entry, they split the PMD swap entry and fall back to the per-PTE path. - try_to_unmap_one() now has a PMD branch that calls set_pmd_swap_entry() and adjusts MM_ANONPAGES / MM_SWAPENTS by HPAGE_PMD_NR before walk_done. TTU_SPLIT_HUGE_PMD remains the fallback. - set_pmd_swap_entry() is the installer. Mirroring the PTE swap-out sequence at PMD granularity, it clears the present mapping (keeping the original for rollback), bumps the swap_map refcount for the folio's 512 slots, transfers the exclusive state in the swap entry, propagates the dirty bit to the folio so writeback is not lost, and installs a swap PMD that preserves the original soft-dirty / uffd-wp / exclusive bits. Any failing step rolls back the present mapping. The swap entry value matches what 512 PTE swap entries would encode, so swap_map refcounting is unchanged: each of the 512 slots carries a count of 1, released individually on later split or together on swap-in. Signed-off-by: Usama Arif --- include/linux/huge_mm.h | 2 + include/linux/vm_event_item.h | 1 + mm/huge_memory.c | 80 +++++++++++++++++++++++++++++++++++ mm/rmap.c | 19 +++++++++ mm/vmscan.c | 9 +++- mm/vmstat.c | 1 + 6 files changed, 111 insertions(+), 1 deletion(-) diff --git a/include/linux/huge_mm.h b/include/linux/huge_mm.h index b6ab28ff7eee..6d294d446f7b 100644 --- a/include/linux/huge_mm.h +++ b/include/linux/huge_mm.h @@ -536,6 +536,8 @@ vm_fault_t do_huge_pmd_device_private(struct vm_fault *= vmf); =20 #ifdef CONFIG_THP_SWAP vm_fault_t do_huge_pmd_swap_page(struct vm_fault *vmf); +int set_pmd_swap_entry(struct page_vma_mapped_walk *pvmw, + struct folio *folio); #else static inline vm_fault_t do_huge_pmd_swap_page(struct vm_fault *vmf) { diff --git a/include/linux/vm_event_item.h b/include/linux/vm_event_item.h index 03fe95f5a020..7267c06674c0 100644 --- a/include/linux/vm_event_item.h +++ b/include/linux/vm_event_item.h @@ -108,6 +108,7 @@ enum vm_event_item { PGPGIN, PGPGOUT, PSWPIN, PSWPOUT, THP_ZERO_PAGE_ALLOC_FAILED, THP_SWPOUT, THP_SWPOUT_FALLBACK, + THP_SWPOUT_PMD, #endif #ifdef CONFIG_BALLOON BALLOON_INFLATE, diff --git a/mm/huge_memory.c b/mm/huge_memory.c index 60f42d136945..2f5c01a043fe 100644 --- a/mm/huge_memory.c +++ b/mm/huge_memory.c @@ -5544,3 +5544,83 @@ void remove_migration_pmd(struct page_vma_mapped_wal= k *pvmw, struct page *new) trace_remove_migration_pmd(address, pmd_val(pmde)); } #endif + +#ifdef CONFIG_THP_SWAP +/** + * set_pmd_swap_entry() - Replace a PMD mapping with a PMD-level swap entr= y. + * @pvmw: Page vma mapped walk context, must have pvmw->pmd set and + * pvmw->pte NULL (i.e. PMD-mapped). + * @folio: The folio being swapped out. Must be in the swap cache. + * + * This installs a PMD-level swap entry in place of a present PMD mapping, + * avoiding the need to split the PMD into PTE-level swap entries. + * + * Return: 0 on success, negative error code on failure. + */ +int set_pmd_swap_entry(struct page_vma_mapped_walk *pvmw, + struct folio *folio) +{ + struct vm_area_struct *vma =3D pvmw->vma; + struct mm_struct *mm =3D vma->vm_mm; + unsigned long address =3D pvmw->address; + unsigned long haddr =3D address & HPAGE_PMD_MASK; + struct page *page =3D folio_page(folio, 0); + bool anon_exclusive; + pmd_t pmdval; + swp_entry_t entry; + pmd_t pmdswp; + + if (!(pvmw->pmd && !pvmw->pte)) + return 0; + + VM_BUG_ON_FOLIO(!folio_test_swapcache(folio), folio); + VM_BUG_ON_FOLIO(!folio_test_anon(folio), folio); + + if (unlikely(folio_test_swapbacked(folio) !=3D + folio_test_swapcache(folio))) { + WARN_ON_ONCE(1); + return -EBUSY; + } + + flush_cache_range(vma, haddr, haddr + HPAGE_PMD_SIZE); + + pmdval =3D pmdp_invalidate(vma, haddr, pvmw->pmd); + + /* Update high watermark before we lower rss */ + update_hiwater_rss(mm); + + if (folio_dup_swap(folio, NULL) < 0) { + set_pmd_at(mm, haddr, pvmw->pmd, pmdval); + return -ENOMEM; + } + + /* See folio_try_share_anon_rmap_pmd(): invalidate PMD first. */ + anon_exclusive =3D PageAnonExclusive(page); + if (anon_exclusive && folio_try_share_anon_rmap_pmd(folio, page)) { + folio_put_swap(folio, NULL); + set_pmd_at(mm, haddr, pvmw->pmd, pmdval); + return -EBUSY; + } + + mm_prepare_for_swap_entries(mm); + + if (pmd_dirty(pmdval)) + folio_mark_dirty(folio); + + entry =3D folio->swap; + pmdswp =3D softleaf_to_pmd(entry); + if (pmd_soft_dirty(pmdval)) + pmdswp =3D pmd_swp_mksoft_dirty(pmdswp); + if (pmd_uffd(pmdval)) + pmdswp =3D pmd_swp_mkuffd(pmdswp); + if (anon_exclusive) + pmdswp =3D pmd_swp_mkexclusive(pmdswp); + set_pmd_at(mm, haddr, pvmw->pmd, pmdswp); + + folio_remove_rmap_pmd(folio, page, vma); + folio_put(folio); + + count_vm_event(THP_SWPOUT_PMD); + return 0; +} +#endif /* CONFIG_THP_SWAP */ diff --git a/mm/rmap.c b/mm/rmap.c index e854679553b9..b431d52cc2e3 100644 --- a/mm/rmap.c +++ b/mm/rmap.c @@ -2079,6 +2079,25 @@ static bool try_to_unmap_one(struct folio *folio, st= ruct vm_area_struct *vma, goto walk_abort; } =20 +#ifdef CONFIG_THP_SWAP + /* + * If the folio is in the swap cache and we're not + * asked to split, install a PMD-level swap entry. + */ + if (!(flags & TTU_SPLIT_HUGE_PMD) && + folio_test_anon(folio) && + folio_test_swapcache(folio)) { + if (set_pmd_swap_entry(&pvmw, folio)) + goto walk_abort; + + add_mm_counter(mm, MM_ANONPAGES, + -HPAGE_PMD_NR); + add_mm_counter(mm, MM_SWAPENTS, + HPAGE_PMD_NR); + goto walk_done; + } +#endif + if (flags & TTU_SPLIT_HUGE_PMD) { /* * We temporarily have to drop the PTL and diff --git a/mm/vmscan.c b/mm/vmscan.c index 986dde8e7429..ffc72a2f635b 100644 --- a/mm/vmscan.c +++ b/mm/vmscan.c @@ -1322,7 +1322,14 @@ static unsigned int shrink_folio_list(struct list_he= ad *folio_list, enum ttu_flags flags =3D TTU_BATCH_FLUSH; bool was_swapbacked =3D folio_test_swapbacked(folio); =20 - if (folio_test_pmd_mappable(folio)) + /* + * With THP_SWAP, PMD-mappable folios already in the + * swap cache can be unmapped with a PMD-level swap + * entry, avoiding the cost of splitting the PMD. + */ + if (folio_test_pmd_mappable(folio) && + !(IS_ENABLED(CONFIG_THP_SWAP) && + folio_test_swapcache(folio))) flags |=3D TTU_SPLIT_HUGE_PMD; /* * Without TTU_SYNC, try_to_unmap will only begin to diff --git a/mm/vmstat.c b/mm/vmstat.c index 3b5cb1031f72..08af18f2b4dc 100644 --- a/mm/vmstat.c +++ b/mm/vmstat.c @@ -1422,6 +1422,7 @@ const char * const vmstat_text[] =3D { [I(THP_ZERO_PAGE_ALLOC_FAILED)] =3D "thp_zero_page_alloc_failed", [I(THP_SWPOUT)] =3D "thp_swpout", [I(THP_SWPOUT_FALLBACK)] =3D "thp_swpout_fallback", + [I(THP_SWPOUT_PMD)] =3D "thp_swpout_pmd", #endif #ifdef CONFIG_BALLOON [I(BALLOON_INFLATE)] =3D "balloon_inflate", --=20 2.53.0-Meta From nobody Sat Jul 25 21:19:11 2026 Received: from out-178.mta1.migadu.com (out-178.mta1.migadu.com [95.215.58.178]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 36BD5423784 for ; Mon, 13 Jul 2026 13:37:40 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.178 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949862; cv=none; b=C66w9ZGsW+VS/w+Tvso6O3M9DI/61Vlpx6HVK6yfAdFkwtgGiW2EX6V05tmLrv+nCCdziujwg9RyOwBP8wuMnYKN7d/DsQ1BxWtq+0tYaRS0o65mBehZxtyc2c1oT/bc0Gl5QnJwo+qZ9QmMkewIeygMJgOSW6m1yhN29N0CS44= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1783949862; c=relaxed/simple; bh=424rzmvzqBAX4rg+M3GfLT3EvTLsvNv/9D0gNYHcrcQ=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=rGvM2kOQI1mt05hH1VUgLTWLm764njRVvQ+yuVgg6luPNmvGGUgyUFCsHYtmOjjUjIe7PdHSalk5GpV/UibAtBL7LGigfs0atYcKdj0/KvRhIfnOX89CFWCvqD6ksJGahNk/nUfzhz647sKlCJAp/C4DO/PBnnSmcaB3W7OLMAc= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=Nh8NaM69; arc=none smtp.client-ip=95.215.58.178 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="Nh8NaM69" X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=linux.dev; s=key1; t=1783949858; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=JuqmUztraTK8Ryv3Qi+D1x6p87qO6gPQXm8v1yQ0mQY=; b=Nh8NaM69vJM9Zlozay8ws/i203YSn/rqlF37Z7HV+2bONcw35JTa1MlW3COVswwiLOfVAs /OAXTkfduUtHigKYhlghqFyd9koG3xRq6gDI9L3hd4tU8z5jdcD1mDvHDfx/71B/XnrVzo 3VvFTvlmzFRNjfJAoJgJLtQHpLuqGPs= From: Usama Arif To: Andrew Morton , david@kernel.org, chrisl@kernel.org, kasong@tencent.com, ljs@kernel.org, ziy@nvidia.com, linux-mm@kvack.org Cc: ying.huang@linux.alibaba.com, Baoquan He , willy@infradead.org, youngjun.park@lge.com, hannes@cmpxchg.org, riel@surriel.com, shakeel.butt@linux.dev, alex@ghiti.fr, kas@kernel.org, baohua@kernel.org, dev.jain@arm.com, baolin.wang@linux.alibaba.com, npache@redhat.com, Liam R. Howlett , ryan.roberts@arm.com, Vlastimil Babka , lance.yang@linux.dev, linux-kernel@vger.kernel.org, nphamcs@gmail.com, shikemeng@huaweicloud.com, yosry@kernel.org, kernel-team@meta.com, Usama Arif Subject: [PATCH v4 11/11] selftests/mm: add PMD swap entry tests Date: Mon, 13 Jul 2026 06:35:46 -0700 Message-ID: <20260713133613.2707815-12-usama.arif@linux.dev> In-Reply-To: <20260713133613.2707815-1-usama.arif@linux.dev> References: <20260713133613.2707815-1-usama.arif@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable X-Migadu-Flow: FLOW_OUT Exercise the PMD swap entry paths. The tests allocate a PMD-mapped THP, write a known pattern, swap it out via MADV_PAGEOUT, and then exercise different code paths: - swap-out / swap-in round-trip with data verification - fork with read-only access from both parent and child - fork with writes in both processes to verify COW isolation - repeated swap cycles to catch reference counting issues - write fault on a swapped PMD to verify dirty handling and PMD mapping restoration - munmap of a swapped PMD (zap_huge_pmd swap slot cleanup) - mprotect on a swapped PMD (change_non_present_huge_pmd) - UFFDIO_MOVE on a swapped PMD (move_pages_huge_pmd swap path) - mremap of a swapped PMD (move_soft_dirty_pmd) - pagemap reading (pagemap_pmd_range_thp softleaf_has_pfn guard) - mincore on a swapped PMD without faulting it in - MADV_FREE on a swapped PMD: verifies swap slots are freed via pagemap and the memory reads back as zero - MADV_WILLNEED on a swapped PMD - swapoff with active PMD swap entries When zswap is enabled, PMD-order consumers may split a PMD swap entry and retry through the PTE path because zswap stores the range as per-page entries. In that configuration, the tests still verify data correctness and log that the PMD mapping assertion is skipped. With zswap disabled, the tests assert that write faults, UFFDIO_MOVE, MADV_WILLNEED, and swapoff restore a PMD-mapped THP where expected. Signed-off-by: Usama Arif --- tools/testing/selftests/mm/Makefile | 1 + tools/testing/selftests/mm/pmd_swap.c | 702 ++++++++++++++++++++++++++ 2 files changed, 703 insertions(+) create mode 100644 tools/testing/selftests/mm/pmd_swap.c diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/= mm/Makefile index ee8def9b4c31..ec20e3aa7f46 100644 --- a/tools/testing/selftests/mm/Makefile +++ b/tools/testing/selftests/mm/Makefile @@ -104,6 +104,7 @@ TEST_GEN_FILES +=3D guard-regions TEST_GEN_FILES +=3D merge TEST_GEN_FILES +=3D rmap TEST_GEN_FILES +=3D folio_split_race_test +TEST_GEN_FILES +=3D pmd_swap =20 ifneq ($(ARCH),arm64) TEST_GEN_FILES +=3D soft-dirty diff --git a/tools/testing/selftests/mm/pmd_swap.c b/tools/testing/selftest= s/mm/pmd_swap.c new file mode 100644 index 000000000000..b4a60a6b50d9 --- /dev/null +++ b/tools/testing/selftests/mm/pmd_swap.c @@ -0,0 +1,702 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Test PMD-level swap entries. + * + * Verifies that when a PMD-mapped THP is swapped out the kernel installs + * a single PMD-level swap entry (instead of splitting into 512 PTE-level + * entries), and that operations on the swapped region behave correctly: + * basic - swap out + swap in preserves data + * fork - parent and child both see the data + * fork_cow - COW after fork keeps parent's data isolated + * cycles - repeated swap out/in does not corrupt data + * write - faulting in via a write restores a PMD-mapped THP + * munmap - munmap on a PMD swap entry frees swap slots cleanly + * mprotect - mprotect on a PMD swap entry preserves data + * mremap - mremap on a PMD swap entry preserves data + * pagemap - pagemap reports the entries as swapped + * mincore - mincore walks a PMD swap entry without faulting it in + * madvise_free - MADV_FREE on a PMD swap entry does not crash + * madvise_willneed - MADV_WILLNEED handles a PMD swap entry + * uffdio_move - UFFDIO_MOVE moves a PMD swap entry + * swapoff - swapoff handles PMD swap entries (needs PMD_SWAP_DEV= ICE) + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "kselftest_harness.h" +#include "vm_util.h" + +#define ZSWAP_ENABLED_PATH "/sys/module/zswap/parameters/enabled" + +static bool check_swapped(int pagemap_fd, char *addr, unsigned long size) +{ + unsigned long off; + + for (off =3D 0; off < size; off +=3D getpagesize()) + if (!pagemap_is_swapped(pagemap_fd, addr + off)) + return false; + return true; +} + +static bool zswap_enabled(void) +{ + char enabled =3D 0; + FILE *f; + + f =3D fopen(ZSWAP_ENABLED_PATH, "r"); + if (!f) + return false; + + if (fscanf(f, " %c", &enabled) !=3D 1) + enabled =3D 0; + fclose(f); + + return enabled =3D=3D 'Y' || enabled =3D=3D 'y' || enabled =3D=3D '1'; +} + +static bool swap_available(int pagemap_fd) +{ + char *p; + bool ret; + + p =3D mmap(NULL, getpagesize(), PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (p =3D=3D MAP_FAILED) + return false; + + memset(p, 0xab, getpagesize()); + madvise(p, getpagesize(), MADV_PAGEOUT); + ret =3D pagemap_is_swapped(pagemap_fd, p); + munmap(p, getpagesize()); + return ret; +} + +static unsigned long read_vm_event(const char *name) +{ + char line[256]; + size_t name_len =3D strlen(name); + unsigned long val =3D 0; + FILE *f; + + f =3D fopen("/proc/vmstat", "r"); + if (!f) + return 0; + while (fgets(line, sizeof(line), f)) { + if (!strncmp(line, name, name_len) && line[name_len] =3D=3D ' ') { + val =3D strtoul(line + name_len + 1, NULL, 10); + break; + } + } + fclose(f); + return val; +} + +static unsigned int random_seed(void) +{ + unsigned int seed; + + if (getrandom(&seed, sizeof(seed), 0) !=3D sizeof(seed)) + seed =3D (unsigned int)time(NULL); + return seed; +} + +static unsigned char pattern_byte(unsigned int seed, unsigned long off) +{ + return (unsigned char)(seed + off); +} + +static void fill_pattern(char *buf, unsigned long size, unsigned int seed) +{ + unsigned long i; + + for (i =3D 0; i < size; i++) + buf[i] =3D (char)pattern_byte(seed, i); +} + +static bool verify_pattern(char *buf, unsigned long size, unsigned int see= d) +{ + unsigned long i; + + for (i =3D 0; i < size; i++) + if ((unsigned char)buf[i] !=3D pattern_byte(seed, i)) + return false; + return true; +} + +/* + * mmap an anonymous PMD-aligned region of pmd_size bytes. Over-allocates + * by one PMD and trims the unaligned head/tail so the returned address is + * PMD-aligned (required for whole-PMD UFFDIO_MOVE). + */ +static char *mmap_pmd_aligned(unsigned long pmd_size) +{ + unsigned long pad =3D pmd_size; + char *raw, *aligned; + + raw =3D mmap(NULL, pmd_size + pad, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (raw =3D=3D MAP_FAILED) + return MAP_FAILED; + + aligned =3D (char *)(((uintptr_t)raw + pmd_size - 1) & ~(pmd_size - 1)); + if (aligned !=3D raw) + munmap(raw, aligned - raw); + if (aligned + pmd_size !=3D raw + pmd_size + pad) + munmap(aligned + pmd_size, + (raw + pmd_size + pad) - (aligned + pmd_size)); + return aligned; +} + +/* + * mmap a PMD-aligned PMD-sized region, request THP, fill with a pattern, + * and swap it out. Verifies via the thp_swpout_pmd vmstat counter that + * the swap-out installed a PMD swap entry rather than splitting to PTEs. + */ +static char *alloc_fill_swap_thp(unsigned long pmd_size, int pagemap_fd, + unsigned int seed) +{ + unsigned long pmd_before, pmd_after; + char *mem; + + mem =3D mmap_pmd_aligned(pmd_size); + if (mem =3D=3D MAP_FAILED) + return MAP_FAILED; + + madvise(mem, pmd_size, MADV_HUGEPAGE); + fill_pattern(mem, pmd_size, seed); + + pmd_before =3D read_vm_event("thp_swpout_pmd"); + + if (madvise(mem, pmd_size, MADV_PAGEOUT) || + !check_swapped(pagemap_fd, mem, pmd_size)) { + munmap(mem, pmd_size); + return MAP_FAILED; + } + + pmd_after =3D read_vm_event("thp_swpout_pmd"); + printf("# thp_swpout_pmd: %lu -> %lu\n", pmd_before, pmd_after); + if (pmd_after - pmd_before < 1) { + munmap(mem, pmd_size); + return MAP_FAILED; + } + return mem; +} + +FIXTURE(pmd_swap) +{ + unsigned long pmd_size; + int pagemap_fd; + unsigned int seed; + bool zswap_enabled; +}; + +FIXTURE_SETUP(pmd_swap) +{ + self->pagemap_fd =3D -1; + + self->pmd_size =3D read_pmd_pagesize(); + if (!self->pmd_size) + SKIP(return, "Cannot determine PMD size\n"); + + self->pagemap_fd =3D open("/proc/self/pagemap", O_RDONLY); + if (self->pagemap_fd < 0) + SKIP(return, "Cannot open /proc/self/pagemap\n"); + + if (!swap_available(self->pagemap_fd)) + SKIP(return, "Swap not available or not working\n"); + + self->seed =3D random_seed(); + self->zswap_enabled =3D zswap_enabled(); +} + +FIXTURE_TEARDOWN(pmd_swap) +{ + if (self->pagemap_fd >=3D 0) + close(self->pagemap_fd); +} + +/* + * Allocate a PMD-sized THP, write a pattern, swap it out, read it back, + * verify the pattern. + */ +TEST_F(pmd_swap, basic) +{ + char *mem; + + mem =3D alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, self->seed); + if (mem =3D=3D MAP_FAILED) + SKIP(return, "Could not create swapped THP\n"); + + ASSERT_TRUE(verify_pattern(mem, self->pmd_size, self->seed)); + + munmap(mem, self->pmd_size); +} + +/* + * Allocate a THP, swap it out, fork, verify both parent and child see + * the correct data. + */ +TEST_F(pmd_swap, fork) +{ + char *mem; + pid_t pid; + int status; + + mem =3D alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, self->seed); + if (mem =3D=3D MAP_FAILED) + SKIP(return, "Could not create swapped THP\n"); + + pid =3D fork(); + ASSERT_GE(pid, 0); + + if (pid =3D=3D 0) + _exit(verify_pattern(mem, self->pmd_size, self->seed) ? 0 : 1); + + ASSERT_TRUE(verify_pattern(mem, self->pmd_size, self->seed)); + + ASSERT_EQ(waitpid(pid, &status, 0), pid); + ASSERT_TRUE(WIFEXITED(status)); + ASSERT_EQ(WEXITSTATUS(status), 0); + + munmap(mem, self->pmd_size); +} + +/* + * Swap out, fork, then have parent and child write different patterns. + * Exercises COW on shared PMD swap entries: writes after fork must + * trigger copy-on-write so the parent's data stays isolated. + */ +TEST_F(pmd_swap, fork_cow) +{ + unsigned int parent_seed =3D self->seed; + unsigned int child_seed =3D ~self->seed; + char *mem; + pid_t pid; + int status; + + mem =3D alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, parent_seed= ); + if (mem =3D=3D MAP_FAILED) + SKIP(return, "Could not create swapped THP\n"); + + pid =3D fork(); + ASSERT_GE(pid, 0); + + if (pid =3D=3D 0) { + fill_pattern(mem, self->pmd_size, child_seed); + _exit(verify_pattern(mem, self->pmd_size, child_seed) ? 0 : 1); + } + + ASSERT_EQ(waitpid(pid, &status, 0), pid); + + ASSERT_TRUE(verify_pattern(mem, self->pmd_size, parent_seed)); + ASSERT_TRUE(WIFEXITED(status)); + ASSERT_EQ(WEXITSTATUS(status), 0); + + munmap(mem, self->pmd_size); +} + +/* + * Swap a THP out and in repeatedly without data corruption. + */ +TEST_F(pmd_swap, cycles) +{ + const int num_cycles =3D 5; + char *mem; + int cycle; + + for (cycle =3D 0; cycle < num_cycles; cycle++) { + unsigned int seed =3D self->seed + cycle; + + mem =3D alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, seed); + if (mem =3D=3D MAP_FAILED) + SKIP(return, "Could not create swapped THP at cycle %d\n", + cycle); + + ASSERT_TRUE(verify_pattern(mem, self->pmd_size, seed)); + + munmap(mem, self->pmd_size); + } +} + +/* + * Swap out, fault in via a write to the first page, verify the write + * reinstates a THP mapping and the rest of the THP is preserved. + */ +TEST_F(pmd_swap, write) +{ + unsigned int seed =3D self->seed; + char *mem; + unsigned long i; + + mem =3D alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, seed); + if (mem =3D=3D MAP_FAILED) + SKIP(return, "Could not create swapped THP\n"); + + mem[0] =3D 0xbb; + ASSERT_EQ(mem[0], (char)0xbb); + + if (self->zswap_enabled) { + TH_LOG("zswap is enabled, so PMD mapping is not checked"); + } else { + ASSERT_TRUE(check_huge_anon(mem, 1, self->pmd_size)); + } + + for (i =3D 1; i < self->pmd_size; i++) + ASSERT_EQ((unsigned char)mem[i], pattern_byte(seed, i)); + + munmap(mem, self->pmd_size); +} + +/* + * munmap while the folio is swapped out. Exercises zap_huge_pmd() on a + * PMD swap entry =E2=80=94 must free the swap slots without trying to loo= k up + * a folio. + */ +TEST_F(pmd_swap, munmap) +{ + char *mem; + + mem =3D alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, self->seed); + if (mem =3D=3D MAP_FAILED) + SKIP(return, "Could not create swapped THP\n"); + + munmap(mem, self->pmd_size); +} + +/* + * Change protection on a swapped PMD entry, then fault back in and + * verify data. Exercises change_non_present_huge_pmd(). + */ +TEST_F(pmd_swap, mprotect) +{ + unsigned int seed =3D self->seed; + char *mem; + + mem =3D alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, seed); + if (mem =3D=3D MAP_FAILED) + SKIP(return, "Could not create swapped THP\n"); + + ASSERT_EQ(mprotect(mem, self->pmd_size, PROT_READ), 0); + ASSERT_EQ(mprotect(mem, self->pmd_size, PROT_READ | PROT_WRITE), 0); + + ASSERT_TRUE(verify_pattern(mem, self->pmd_size, seed)); + + munmap(mem, self->pmd_size); +} + +/* + * UFFDIO_MOVE a PMD swap entry from src to a registered dst. Exercises + * move_pages_huge_pmd() handling of pmd_is_swap_entry: the whole PMD swap + * entry must move to dst without splitting, and the destination must + * read back the original pattern after a swap-in fault. + */ +TEST_F(pmd_swap, uffdio_move) +{ + unsigned int seed =3D self->seed; + struct uffdio_register reg =3D {}; + struct uffdio_move move =3D {}; + struct uffdio_api api =3D {}; + char *src, *dst; + int uffd; + + dst =3D mmap_pmd_aligned(self->pmd_size); + if (dst =3D=3D MAP_FAILED) + SKIP(return, "Could not mmap aligned dst\n"); + + src =3D alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, seed); + if (src =3D=3D MAP_FAILED) { + munmap(dst, self->pmd_size); + SKIP(return, "Could not create swapped THP\n"); + } + if ((uintptr_t)src & (self->pmd_size - 1)) { + munmap(src, self->pmd_size); + munmap(dst, self->pmd_size); + SKIP(return, "src not PMD-aligned\n"); + } + + uffd =3D syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); + if (uffd < 0) { + munmap(src, self->pmd_size); + munmap(dst, self->pmd_size); + SKIP(return, "userfaultfd unavailable\n"); + } + + api.api =3D UFFD_API; + api.features =3D UFFD_FEATURE_MOVE; + if (ioctl(uffd, UFFDIO_API, &api) || + !(api.features & UFFD_FEATURE_MOVE)) { + close(uffd); + munmap(src, self->pmd_size); + munmap(dst, self->pmd_size); + SKIP(return, "UFFD_FEATURE_MOVE unsupported\n"); + } + + reg.range.start =3D (unsigned long)dst; + reg.range.len =3D self->pmd_size; + reg.mode =3D UFFDIO_REGISTER_MODE_MISSING; + if (ioctl(uffd, UFFDIO_REGISTER, ®)) { + close(uffd); + munmap(src, self->pmd_size); + munmap(dst, self->pmd_size); + SKIP(return, "UFFDIO_REGISTER failed\n"); + } + + move.dst =3D (unsigned long)dst; + move.src =3D (unsigned long)src; + move.len =3D self->pmd_size; + if (ioctl(uffd, UFFDIO_MOVE, &move)) { + close(uffd); + munmap(src, self->pmd_size); + munmap(dst, self->pmd_size); + ASSERT_EQ(errno, 0); + } + ASSERT_EQ(move.move, self->pmd_size); + + /* dst inherits the PMD swap entry; reading it must restore the data. */ + ASSERT_TRUE(check_swapped(self->pagemap_fd, dst, self->pmd_size)); + ASSERT_TRUE(verify_pattern(dst, self->pmd_size, seed)); + if (self->zswap_enabled) { + TH_LOG("zswap is enabled, so PMD mapping is not checked"); + } else { + /* The whole-PMD path must reinstate a THP, not 512 PTE folios. */ + ASSERT_TRUE(check_huge_anon(dst, 1, self->pmd_size)); + } + + close(uffd); + munmap(src, self->pmd_size); + munmap(dst, self->pmd_size); +} + +/* + * Move a swapped PMD entry to a new address, fault in, verify data. + * Exercises move_huge_pmd() and move_soft_dirty_pmd(). + */ +TEST_F(pmd_swap, mremap) +{ + unsigned int seed =3D self->seed; + char *mem, *new_mem; + + mem =3D alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, seed); + if (mem =3D=3D MAP_FAILED) + SKIP(return, "Could not create swapped THP\n"); + + new_mem =3D mremap(mem, self->pmd_size, self->pmd_size, MREMAP_MAYMOVE); + if (new_mem =3D=3D MAP_FAILED) { + munmap(mem, self->pmd_size); + ASSERT_NE(new_mem, MAP_FAILED); + } + + ASSERT_TRUE(verify_pattern(new_mem, self->pmd_size, seed)); + + munmap(new_mem, self->pmd_size); +} + +/* + * Read /proc/self/pagemap on a PMD swap entry. Exercises the pagemap + * PMD walker which must handle PMD swap entries without trying to + * convert them to a page via softleaf_to_page(). + */ +TEST_F(pmd_swap, pagemap) +{ + char *mem; + uint64_t entry; + unsigned long off; + + mem =3D alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, self->seed); + if (mem =3D=3D MAP_FAILED) + SKIP(return, "Could not create swapped THP\n"); + + for (off =3D 0; off < self->pmd_size; off +=3D getpagesize()) { + entry =3D pagemap_get_entry(self->pagemap_fd, mem + off); + /* Bit 62 =3D swapped */ + ASSERT_TRUE(entry & (1ULL << 62)); + } + + munmap(mem, self->pmd_size); +} + +/* + * mincore() on a swapped-out PMD-mapped THP must handle the non-present P= MD + * entry in place. The call must not fault the PMD back in or split the en= try. + */ +TEST_F(pmd_swap, mincore) +{ + unsigned long pages =3D self->pmd_size / getpagesize(); + unsigned char *vec; + char *mem; + + mem =3D alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, self->seed); + if (mem =3D=3D MAP_FAILED) + SKIP(return, "Could not create swapped THP\n"); + + vec =3D calloc(pages, sizeof(*vec)); + ASSERT_NE(vec, NULL) { + munmap(mem, self->pmd_size); + } + + ASSERT_EQ(mincore(mem, self->pmd_size, vec), 0) { + free(vec); + munmap(mem, self->pmd_size); + } + ASSERT_TRUE(check_swapped(self->pagemap_fd, mem, self->pmd_size)) { + free(vec); + munmap(mem, self->pmd_size); + } + + free(vec); + munmap(mem, self->pmd_size); +} + +/* + * MADV_FREE on a swapped-out PMD must free the swap slots and clear the + * entry. After the call, pagemap must no longer report the pages as + * swapped, and accessing the region must yield zero pages. + */ +TEST_F(pmd_swap, madvise_free) +{ + char *mem; + unsigned long i; + + mem =3D alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, self->seed); + if (mem =3D=3D MAP_FAILED) + SKIP(return, "Could not create swapped THP\n"); + + ASSERT_TRUE(check_swapped(self->pagemap_fd, mem, self->pmd_size)); + ASSERT_EQ(madvise(mem, self->pmd_size, MADV_FREE), 0); + ASSERT_FALSE(check_swapped(self->pagemap_fd, mem, self->pmd_size)); + + for (i =3D 0; i < self->pmd_size; i +=3D getpagesize()) + ASSERT_EQ(mem[i], 0); + + munmap(mem, self->pmd_size); +} + +/* + * MADV_WILLNEED on a swapped-out PMD-mapped THP may schedule PMD-order + * swapin I/O, find the PMD-sized folio already resident in the swap cache, + * or split to the PTE path when zswap has per-page state for the range. + */ +TEST_F(pmd_swap, madvise_willneed) +{ + char *mem; + + mem =3D alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, self->seed); + if (mem =3D=3D MAP_FAILED) + SKIP(return, "Could not create swapped THP\n"); + + ASSERT_EQ(madvise(mem, self->pmd_size, MADV_WILLNEED), 0); + ASSERT_TRUE(check_swapped(self->pagemap_fd, mem, self->pmd_size)); + + /* First touch faults the data back in. */ + ASSERT_TRUE(verify_pattern(mem, self->pmd_size, self->seed)); + + if (self->zswap_enabled) + TH_LOG("zswap is enabled, so PMD mapping is not checked"); + else + ASSERT_TRUE(check_huge_anon(mem, 1, self->pmd_size)); + + munmap(mem, self->pmd_size); +} + +/* + * swapoff requires a dedicated swap device path. Use a separate fixture + * that picks the device up from the PMD_SWAP_DEVICE environment variable + * and skips when unset. + */ +FIXTURE(pmd_swap_swapoff) +{ + unsigned long pmd_size; + int pagemap_fd; + const char *swap_dev; + unsigned int seed; + bool zswap_enabled; +}; + +FIXTURE_SETUP(pmd_swap_swapoff) +{ + self->pagemap_fd =3D -1; + self->swap_dev =3D getenv("PMD_SWAP_DEVICE"); + if (!self->swap_dev) + SKIP(return, "PMD_SWAP_DEVICE env var not set\n"); + + self->pmd_size =3D read_pmd_pagesize(); + if (!self->pmd_size) + SKIP(return, "Cannot determine PMD size\n"); + + self->pagemap_fd =3D open("/proc/self/pagemap", O_RDONLY); + if (self->pagemap_fd < 0) + SKIP(return, "Cannot open /proc/self/pagemap\n"); + + if (!swap_available(self->pagemap_fd)) + SKIP(return, "Swap not available or not working\n"); + + self->seed =3D random_seed(); + self->zswap_enabled =3D zswap_enabled(); +} + +FIXTURE_TEARDOWN(pmd_swap_swapoff) +{ + if (self->pagemap_fd >=3D 0) + close(self->pagemap_fd); +} + +/* + * Swap out a THP, then turn off swap. Verify data is intact. When zswap is + * not active, the PMD-order swapoff path should preserve the huge mapping. + */ +TEST_F(pmd_swap_swapoff, basic) +{ + unsigned int seed =3D self->seed; + char *mem; + int ret, err; + + mem =3D alloc_fill_swap_thp(self->pmd_size, self->pagemap_fd, seed); + if (mem =3D=3D MAP_FAILED) + SKIP(return, "Could not create swapped THP\n"); + + ret =3D swapoff(self->swap_dev); + err =3D errno; + ASSERT_EQ(ret, 0) { + TH_LOG("swapoff(%s) failed: %s", self->swap_dev, strerror(err)); + munmap(mem, self->pmd_size); + } + + ASSERT_TRUE(verify_pattern(mem, self->pmd_size, seed)) { + swapon(self->swap_dev, 0); + munmap(mem, self->pmd_size); + } + + if (self->zswap_enabled) { + TH_LOG("zswap is enabled, so PMD mapping is not checked"); + } else { + ASSERT_TRUE(check_huge_anon(mem, 1, self->pmd_size)) { + swapon(self->swap_dev, 0); + munmap(mem, self->pmd_size); + } + } + + ret =3D swapon(self->swap_dev, 0); + err =3D errno; + ASSERT_EQ(ret, 0) { + TH_LOG("swapon(%s) failed: %s", self->swap_dev, strerror(err)); + munmap(mem, self->pmd_size); + } + + munmap(mem, self->pmd_size); +} + +TEST_HARNESS_MAIN --=20 2.53.0-Meta