From: Kiryl Shutsemau <kirill@shutemov.name>
To: akpm@linux-foundation.org, david@kernel.org, ljs@kernel.org,
nico.pache@linux.dev
Cc: baolin.wang@linux.alibaba.com, baohua@kernel.org,
dev.jain@arm.com, hughd@google.com, lance.yang@linux.dev,
liam@infradead.org, mhocko@suse.com, rppt@kernel.org,
ryan.roberts@arm.com, shuah@kernel.org, surenb@google.com,
usama.arif@linux.dev, vbabka@kernel.org, ziy@nvidia.com,
usama.anjum@arm.com, agordeev@linux.ibm.com, linux-mm@kvack.org,
linux-kselftest@vger.kernel.org, linux-kernel@vger.kernel.org,
kas@kernel.org, jannh@google.com, willy@infradead.org,
pfalcato@suse.de, rostedt@goodmis.org, mhiramat@kernel.org,
linux-trace-kernel@vger.kernel.org, bpf@vger.kernel.org
Subject: [RFC PATCH 18/57] mm/collapse: install the destinations at PTE level
Date: Sun, 16 Aug 2026 23:45:30 +0100 [thread overview]
Message-ID: <20260816224609.308019-19-kirill@shutemov.name> (raw)
In-Reply-To: <20260816224609.308019-1-kirill@shutemov.name>
From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>
Fill in the install for sub-PMD candidates, and with it the two things
every install needs: a verify, and an abort.
The verify decides whether the window is still the round's to publish.
Under the ptl, every slot that had a source must still hold the round's
migration entry, and every slot that had none must still be none. A hole
some fault refilled is not the round's to overwrite. Verify and install
share one ptl hold, so a verified candidate cannot lose a slot before it
is published.
The abort undoes a frozen candidate that cannot be published: the copy
took a machine check, the provision pass could not spare it a
destination, or the verify refused it. Slot by slot:
- a slot still holding the round's migration entry is restored from the
saved value. No TLB flush: the translation is identical.
- a slot that does not is left exactly as found, since restoring it
would resurrect memory the user zapped. Its rmap is dropped here,
because the zapper fixed up rss for what it cleared but could not drop
the rmap a frozen source keeps.
The table itself going away is that same rule at whole-table scale. A
racing MADV_DONTNEED over the whole table, and the empty-table reclaim
behind it, can free the table between freeze and install. Every slot
then reads as foreign, and what is left to undo is exactly the half of
the teardown a zapper cannot do for a frozen source.
Publishing is the fault path's own helper, so the destination gets rmap
and LRU insertion before its PTEs, fresh bits from the VMA, contpte
painting, and no TLB flush -- every transition is non-present to present.
Slots that had no source become anon memory no zap ever accounted for, so
rss is corrected by hand.
PMD-order candidates need a terminal layer of their own, a stub here.
Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
---
mm/collapse.c | 226 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 226 insertions(+)
diff --git a/mm/collapse.c b/mm/collapse.c
index a3882d897d11..842adc30aeb0 100644
--- a/mm/collapse.c
+++ b/mm/collapse.c
@@ -129,6 +129,7 @@ enum collapse_candidate_state {
CAND_SELECTED, /* collected; nothing held on its behalf yet */
CAND_SKIPPED, /* refused; nothing of it left to undo */
CAND_FROZEN, /* sources displaced and frozen */
+ CAND_INSTALLED, /* the destination is mapped */
};
/*
@@ -1082,10 +1083,235 @@ static void collapse_copy(struct vm_area_struct *vma,
}
}
+/*
+ * Undo one frozen slot: restore the saved PTE if our migration entry is still
+ * there, or drop the rmap the freeze took if a racing zap already replaced it.
+ * Returns true when the slot was zapped -- its mapping reference is then ours to
+ * release.
+ */
+static bool collapse_abort_slot(struct vm_area_struct *vma, struct folio *folio,
+ pte_t *slot, unsigned long addr, pte_t saved)
+{
+ /* No table left: the slot cannot still be holding our entry */
+ if (slot) {
+ softleaf_t entry = softleaf_from_pte(ptep_get(slot));
+
+ if (softleaf_is_migration(entry) &&
+ softleaf_to_pfn(entry) == pte_pfn(saved)) {
+ set_pte_at(vma->vm_mm, addr, slot, saved);
+ return false;
+ }
+ }
+ folio_remove_rmap_pte(folio, pte_page(saved), vma);
+ return true;
+}
+
+/*
+ * Abort one frozen candidate at install time: it took a machine check during the
+ * copy, or some of its slots no longer hold our migration entries. mmap_read
+ * (held freeze..putback) blocks fork, mremap and munmap, and faults wait on the
+ * migration entries -- but madvise-class operations run under mmap_read too, so a
+ * concurrent MADV_DONTNEED may have zapped frozen slots, and a fault may have
+ * refilled a zapped one.
+ *
+ * Slots still holding our entries are restored from the saved values (no TLB
+ * flush: identical translation). Foreign slots are left exactly as found --
+ * restoring them would resurrect memory the user zapped -- but their rmap is
+ * dropped here: the zapper fixed up rss for the slots it cleared, yet could not
+ * drop the rmap a frozen source keeps, unlike a migrating one, which unmaps at
+ * freeze time. Slots with no source follow the same rule with no rmap to drop: a
+ * cleared zeropage is restored only while its slot is still none, and a hole was
+ * never touched at all.
+ *
+ * @pte is NULL when the table itself is gone: a racing whole-table MADV_DONTNEED
+ * zapped every entry, frozen slots included, and the empty-table reclaim
+ * (CONFIG_PT_RECLAIM) freed it, clearing the pmd under the pmd lock and the pte
+ * ptl, neither of which excludes it between our freeze and install. Every slot
+ * then reads as foreign, which is exactly right: nothing of ours survives to
+ * restore or verify, and what is left is the half of the teardown the zapper
+ * cannot perform for a frozen source -- the kept rmap, the freeze, the folio
+ * locks and the references. The caller holds no page-table lock in that case,
+ * there being no table to lock.
+ */
+static void collapse_abort_candidate(struct vm_area_struct *vma,
+ struct collapse_candidate *cand,
+ pte_t *pte)
+{
+ const unsigned int nr_pages = candidate_nr_pages(cand);
+ struct mm_struct *mm = vma->vm_mm;
+ unsigned long addr = cand->addr;
+ unsigned int i, nr;
+
+ for (i = 0; i < nr_pages; i += nr, addr += nr * PAGE_SIZE) {
+ pte_t saved = cand->saved_ptes[i];
+ unsigned int k, nr_dropped;
+ struct folio *folio;
+
+ nr = 1; /* skip stride; a span overrides it */
+ if (pte_none(saved))
+ continue;
+ if (is_zero_pfn(pte_pfn(saved))) {
+ if (pte && pte_none(ptep_get(pte + i)))
+ set_pte_at(mm, addr, pte + i, saved);
+ continue;
+ }
+
+ folio = pte_folio(saved);
+ nr = collapse_saved_span_len(cand, i, nr_pages);
+
+ /*
+ * Unfreeze before any rmap drop: rmap removal munlocks under
+ * VM_LOCKED, and munlock_folio() takes a reference a frozen folio
+ * forbids. The expected count still holds every slot's mapping
+ * reference; restored slots keep theirs, and the zapped slots'
+ * references become ours to drop with the rmap.
+ */
+ folio_ref_unfreeze(folio, folio_expected_ref_count(folio) + 1);
+
+ nr_dropped = 0;
+ for (k = 0; k < nr; k++) {
+ pte_t *slot = pte ? pte + i + k : NULL;
+
+ nr_dropped += collapse_abort_slot(vma, folio, slot,
+ addr + k * PAGE_SIZE,
+ cand->saved_ptes[i + k]);
+ }
+
+ folio_unlock(folio);
+ folio_put_refs(folio, nr_dropped + 1);
+ }
+
+ /*
+ * Not installed; collapse_finish() releases the destination, which has to
+ * wait for the ptl to be dropped.
+ */
+ cand->state = CAND_SKIPPED;
+}
+
+/*
+ * Nothing may have shifted under the round: every source slot must still hold our
+ * migration entry, and every slot with no source must still be none -- the freeze
+ * cleared the zeropage ones, so both read as none by then, and a slot some fault
+ * has refilled, with a page or with a zeropage, is not ours to overwrite.
+ * @nr_populated returns how many source-less slots the install is about to make
+ * present, which is rss no zap ever accounted for.
+ */
+static bool collapse_verify_candidate(struct collapse_candidate *cand,
+ pte_t *pte, unsigned int *nr_populated)
+{
+ const unsigned int nr_pages = candidate_nr_pages(cand);
+ unsigned int k, populated = 0;
+
+ for (k = 0; k < nr_pages; k++) {
+ pte_t live = ptep_get(pte + k);
+ softleaf_t entry;
+
+ if (pte_none_or_zero(cand->saved_ptes[k])) {
+ if (!pte_none(live))
+ return false;
+ populated++;
+ continue;
+ }
+
+ entry = softleaf_from_pte(live);
+ if (!softleaf_is_migration(entry) ||
+ softleaf_to_pfn(entry) != pte_pfn(cand->saved_ptes[k]))
+ return false;
+ }
+ *nr_populated = populated;
+ return true;
+}
+
+/*
+ * The PMD terminal layer: verify, detach the table, deposit a fresh one and
+ * install the leaf, as one atomic section under the pmd lock. A pmd_none window
+ * never exists -- faults stay held at pte level by the migration entries
+ * throughout -- which is what lets PMD collapse run under mmap_read like the rest
+ * of the engine.
+ */
+static void collapse_install_pmd(struct vm_area_struct *vma,
+ struct collapse_control *cc, pmd_t *pmd)
+{
+}
+
/* Publish each destination folio in place of the sources it replaces */
static void collapse_install(struct vm_area_struct *vma,
struct collapse_control *cc, pmd_t *pmd)
{
+ struct mm_struct *mm = vma->vm_mm;
+ pte_t *pte, *table;
+ spinlock_t *ptl;
+ unsigned int i;
+
+ if (is_pmd_order(cc->candidates[0].order)) {
+ /* A PMD candidate fills the slot pool: always alone */
+ VM_WARN_ON_ONCE(cc->nr_candidates != 1);
+ collapse_install_pmd(vma, cc, pmd);
+ return;
+ }
+
+ pte = pte_offset_map_lock(mm, pmd, cc->candidates[0].addr, &ptl);
+ if (!pte) {
+ /*
+ * Table gone under us (see collapse_abort_candidate() on @pte).
+ * Tear down every frozen candidate -- stranding them would leak
+ * frozen, locked sources.
+ */
+ for (i = 0; i < cc->nr_candidates; i++) {
+ struct collapse_candidate *cand = &cc->candidates[i];
+
+ if (cand->state != CAND_FROZEN)
+ continue;
+
+ cand->result = SCAN_NO_PTE_TABLE;
+ collapse_abort_candidate(vma, cand, NULL);
+ }
+ return;
+ }
+ table = pte - pte_index(cc->candidates[0].addr);
+
+ for (i = 0; i < cc->nr_candidates; i++) {
+ struct collapse_candidate *cand = &cc->candidates[i];
+ pte_t *cand_pte = table + pte_index(cand->addr);
+ unsigned int nr_populated;
+
+ if (cand->state != CAND_FROZEN)
+ continue;
+
+ if (cand->result != SCAN_SUCCEED) {
+ /* Machine check during the copy */
+ collapse_abort_candidate(vma, cand, cand_pte);
+ continue;
+ }
+
+ /* No destination: the provision pass could not spare one */
+ if (!cand->new_folio) {
+ collapse_abort_candidate(vma, cand, cand_pte);
+ continue;
+ }
+
+ if (!collapse_verify_candidate(cand, cand_pte, &nr_populated)) {
+ cand->result = SCAN_PTE_NON_PRESENT;
+ collapse_abort_candidate(vma, cand, cand_pte);
+ continue;
+ }
+
+ /*
+ * The smp_wmb() in __folio_mark_uptodate() orders the copied
+ * data before the set_ptes() that publishes it.
+ */
+ __folio_mark_uptodate(cand->new_folio);
+ map_anon_folio_pte_nopf(cand->new_folio, cand_pte, vma,
+ cand->addr, /*uffd_wp=*/ false);
+
+ /* Slots with no source gain anon memory that no zap accounted */
+ if (nr_populated)
+ add_mm_counter(mm, MM_ANONPAGES, nr_populated);
+ cand->new_folio = NULL; /* ownership: the mappings */
+ cand->state = CAND_INSTALLED;
+ }
+
+ pte_unmap_unlock(pte, ptl);
}
/*
--
2.54.0
next prev parent reply other threads:[~2026-08-16 22:46 UTC|newest]
Thread overview: 64+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-16 22:45 [RFC PATCH 00/57] mm/collapse: rebuild collapse on migration primitives Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 01/57] mm: add pte_folio() Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 02/57] mm: add pte_none_or_zero() Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 03/57] mm/collapse: add collapse.h for the shared collapse state Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 04/57] mm/collapse: rename mthp_present_ptes to eligible_ptes Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 05/57] mm/collapse: state what a collapse may do in the policy Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 06/57] mm/collapse: move the smallest collapse order to collapse.h Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 07/57] mm/collapse: sketch the new anonymous collapse engine Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 08/57] mm/collapse: scan a table for what a collapse could use Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 09/57] mm/collapse: collect candidate windows into a round Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 10/57] mm/collapse: run a round and feed the outcomes back Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 11/57] mm/collapse: sketch the passes of a round Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 12/57] mm/collapse: allocate a destination per candidate Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 13/57] mm/collapse: revalidate a round against the VMA Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 14/57] mm/collapse: fault the sources in before the freeze Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 15/57] mm/collapse: check what a candidate would freeze Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 16/57] mm/collapse: freeze the sources behind migration entries Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 17/57] mm/collapse: copy the sources into the destinations Kiryl Shutsemau
2026-08-16 22:45 ` Kiryl Shutsemau [this message]
2026-08-16 22:45 ` [RFC PATCH 19/57] mm/collapse: install a PMD leaf as the terminal layer Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 20/57] mm/collapse: put the sources back Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 21/57] mm/collapse: settle whatever the round reached Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 22/57] mm/collapse: walk a table with a selection cursor Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 23/57] mm/collapse: give a refused region a second chance Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 24/57] mm/collapse: report each candidate's outcome to tracing Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 25/57] mm/collapse: collapse anonymous memory with the new engine Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 26/57] mm/collapse: give collapse_single_pmd() the range to work on Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 27/57] mm/collapse: scan the windows a VMA can hold Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 28/57] mm/collapse: remove the mechanism the engine replaces Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 29/57] mm/collapse: move what a collapse is judged on into collapse.c Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 30/57] mm/collapse: name the max_ptes ceiling after collapse Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 31/57] mm/khugepaged: count collapses where khugepaged makes them Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 32/57] mm/collapse: move the file collapse into collapse.c Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 33/57] mm/collapse: split collapse into a scan and a run Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 34/57] mm/collapse: implement MADV_COLLAPSE in madvise.c Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 35/57] mm/madvise: drop MADV_COLLAPSE's redundant mm reference Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 36/57] mm/collapse: report what the scan found Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 37/57] mm/collapse: report what the fault-in pass paid Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 38/57] mm/collapse: report the round, and what it made faulters wait Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 39/57] mm/collapse: name the file collapse's tracepoints after collapse Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 40/57] mm/collapse: remove the tracepoints of the mechanism that is gone Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 41/57] mm/collapse: give collapse its own trace header Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 42/57] mm/collapse: allow error injection into the freeze Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 43/57] mm/khugepaged: check the scan budget before the work, not after Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 44/57] mm/khugepaged: hold the address space open across a scan Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 45/57] mm/collapse: take a per-VMA read lock for the round Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 46/57] mm/khugepaged: scan under a per-VMA read lock Kiryl Shutsemau
2026-08-16 22:45 ` [RFC PATCH 47/57] mm/madvise: collapse " Kiryl Shutsemau
2026-08-16 22:46 ` [RFC PATCH 48/57] mm/collapse: assert the mm reference the engine relies on Kiryl Shutsemau
2026-08-16 22:46 ` [RFC PATCH 49/57] mm/khugepaged: drop the mmap_lock barrier from __khugepaged_exit() Kiryl Shutsemau
2026-08-16 22:46 ` [RFC PATCH 50/57] selftests/mm: attribute collapses by candidate event alone Kiryl Shutsemau
2026-08-16 22:46 ` [RFC PATCH 51/57] selftests/mm: cover collapse inside a sub-PMD VMA Kiryl Shutsemau
2026-08-16 22:46 ` [RFC PATCH 52/57] selftests/mm: cover a hole-y window in " Kiryl Shutsemau
2026-08-16 22:46 ` [RFC PATCH 53/57] selftests/mm: cover collapse of mlocked ranges Kiryl Shutsemau
2026-08-16 22:46 ` [RFC PATCH 54/57] selftests/mm: cover collapse beside a MADV_FREE'd page Kiryl Shutsemau
2026-08-16 22:46 ` [RFC PATCH 55/57] selftests/mm: cover collapse beside a pinned page Kiryl Shutsemau
2026-08-16 22:46 ` [RFC PATCH 56/57] selftests/mm: cover the scaled max_ptes_shared limit Kiryl Shutsemau
2026-08-16 22:46 ` [RFC PATCH 57/57] MAINTAINERS: add an entry for collapse Kiryl Shutsemau
2026-08-17 8:04 ` Lorenzo Stoakes (ARM)
2026-08-17 8:08 ` David Hildenbrand (Arm)
2026-08-17 10:12 ` Kiryl Shutsemau
2026-08-17 2:02 ` [RFC PATCH 00/57] mm/collapse: rebuild collapse on migration primitives Zi Yan
2026-08-17 10:07 ` Kiryl Shutsemau
2026-08-17 8:52 ` Lorenzo Stoakes (ARM)
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260816224609.308019-19-kirill@shutemov.name \
--to=kirill@shutemov.name \
--cc=agordeev@linux.ibm.com \
--cc=akpm@linux-foundation.org \
--cc=baohua@kernel.org \
--cc=baolin.wang@linux.alibaba.com \
--cc=bpf@vger.kernel.org \
--cc=david@kernel.org \
--cc=dev.jain@arm.com \
--cc=hughd@google.com \
--cc=jannh@google.com \
--cc=kas@kernel.org \
--cc=lance.yang@linux.dev \
--cc=liam@infradead.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=linux-trace-kernel@vger.kernel.org \
--cc=ljs@kernel.org \
--cc=mhiramat@kernel.org \
--cc=mhocko@suse.com \
--cc=nico.pache@linux.dev \
--cc=pfalcato@suse.de \
--cc=rostedt@goodmis.org \
--cc=rppt@kernel.org \
--cc=ryan.roberts@arm.com \
--cc=shuah@kernel.org \
--cc=surenb@google.com \
--cc=usama.anjum@arm.com \
--cc=usama.arif@linux.dev \
--cc=vbabka@kernel.org \
--cc=willy@infradead.org \
--cc=ziy@nvidia.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox