mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v8 0/3] KSM: performance optimizations for rmap_walk_ksm
@ 2026-06-09  4:39 xu.xin16
  2026-06-09  4:40 ` [PATCH v8 1/3] ksm: add linear_page_index into ksm_rmap_item xu.xin16
                   ` (2 more replies)
  0 siblings, 3 replies; 9+ messages in thread
From: xu.xin16 @ 2026-06-09  4:39 UTC (permalink / raw)
  To: akpm, david, xu.xin16
  Cc: chengming.zhou, hughd, wang.yaxin, linux-mm, linux-kernel, ljs

From: xu xin <xu.xin16@zte.com.cn>

This series fixes a severe KSM reverse-mapping performance problem
that can freeze applications for hundreds of milliseconds under
memory pressure especially when a lot of unrelated VMAs sharing a
single anon_vma.

Two key highlights:

1. Lock hold time drops from >500ms to <2ms
   - In our benchmark (20,000 VMAs sharing an anon_vma), worst-case
     anon_vma lock hold time during KSM rmap walk went from 705ms
     down to 1.67ms (max) and 1.44ms (avg).

2. Real user impact
   - The anon_vma lock is also acquired by page faults, reclaim,
     migration, compaction, mlock, exit_mmap, and cgroup accounting.

   - A long hold due to inefficient rmap walks stalls application
     threads, causing latency spikes, reduced throughput, or even
     container timeouts.

   - The problem occurs even without fork() – VMA splitting (e.g.,
     via mprotect or madvise over time) can create tens of thousands
     of VMAs all attached to the same anon_vma.

Real-world examples:

 - JVM / Go runtime: These use mmap for heap regions and later call
mprotect(PROT_NONE) for garbage collection barriers or guard pages,
splitting the original VMA into thousands of small pieces over time.

 - Database engines (MySQL, PostgreSQL): Large shared memory buffers
or anonymous mappings are managed with madvise(MADV_DONTNEED) to release
specific pages, which also splits VMAs.

* Why the benchmark numbers are realistic: We observed ~20,000 VMAs sharing
one anon_vma on a production system running a Java application with KSM
enabled. The lock hold time before the patch was measured at 228 ms (max)
during rmap walks triggered by memory compaction and page migration.
The benchmark reproduces that VMA count and lock‑hold behavior in a
controlled environment.

For systems that do not have thousands of VMAs per anon_vma, the
patch adds negligible overhead (a single pgoff comparison). For systems
that do suffer from this issue, the improvement is dramatic:
1) Worst‑case anon_vma lock hold time drops from hundreds of milliseconds
to under 2 ms.2)This directly reduces blocking of parallel operations that
need the same lock – page faults, reclaim, migration, compaction, mlock, and
exit_mmap.

End‑users will see lower tail latency (fewer application stalls),
higher throughput under memory pressure, and no more spurious
lockup warnings or container timeouts caused by excessive lock hold
times.

In short: workloads that do not hit this pathological pattern are
unaffected; those that do will see a 100x to 500x reduction in lock
hold times, which translates directly into a more responsive system.

Change Log
==========
Changes in v8:
1. Suggested by David:

  * Drop the tracepoint and testbench patches and leave them as OOT patches.

  * Rename pgoff into linear_page_index and update the corresponding commit
  desciption.

2. Fix AI's issue: Use process-self's KSM counter instead of global KSM counters.
https://sashiko.dev/#/patchset/20260530165907829ZSDzDdMc110MnOflRzf9P@zte.com.cn


Changes in v7:
Mainly to fix some issues AI review points out at:
https://sashiko.dev/#/patchset/20260522105234715fKI7KSsjC5XpEVMwoV6rI@zte.com.cn

We have completely correct those possible flaws according to AI useful suggestions.
- Patch 2: There are mainly 3 changes as follows.
	(1) Use COMM-PID filtering during trace parsing to precisely match the right
	    events.
	(2) Graceful handling of single‑NUMA node. trigger_rmap_walk() no longer calls
	    exit(1) when no other NUMA node is available. It returns an error, allowing
	         the caller to clean up (disable tracepoints, restore KSM config) before exiting.
	(3) Fair comparison for anonymous / file tests with KSM. anonymous and file‑backed tests now
	    use fork() to create thousands of child processes, each sharing the same physical
	    page via copy‑on‑write (or MAP_SHARED). This ensures that for all three page types
	    the latency measurement is based on a single physical page mapped by many VMAs (≈ NR_SHARERS).

- Patch 6: There are mainly 3 changes as follows.
	(1) Fix mapping size tracking after mremap and protect the original pointer on failure.
	(2) Use baseline delta comparison to eliminate interference from global KSM counters.
	(3) Fix error-code confusion caused by pread/close interactions.



Changes in v6:
- Patch 1: Defining a single event class once and instantiating the individual
	   tracepoints with DEFINE_EVENT, as AI said:
	https://sashiko.dev/#/patchset/20260519220536792dMIKRMurt3vZ5lXC5pwh8@zte.com.cn

- Patch 2: Suggested-by AI below, three useful changes are done:
	(1) Safe event pairing – Now stores folio and rwc addresses for rmap_walk_start
	    and matches with the same addresses in rmap_walk_end, eliminating
	    cross‑thread interference.

	(2 )KSM configuration preservation – Saves original KSM settings and restores
	    them after the KSM test, avoiding persistent changes to system behaviour.

	(3) unlink in advance to prevent potentialfile leak – unlink(filename) called
	    immediately after mkstemp, so the temporary file is automatically removed
	    even if the program crashes early.

 - Patch 3: a separate, standalone patch to update the MAINTAINERS file.

Changes in v5:
- Patch 1: replaced local_clock() with tracepoints – no overhead
           when tracepoints are disabled.
- Patch 3: switched from vm_pgoff (unstable after VMA split) to a
           linear page offset.
- Patch 4: adapted to the linear page offset; added user-impact
           description (real workloads, lock contention examples,
           VMA splitting scenario).
- Patch 5: simplified to a single process with 32 pages (instead
           of multi-process), as suggested by David.

Changes in v4:
 - Add a tracepoint for rmap_walk
 - Provide a testbench for rmap_walk
 - Add vm_pgoff field in ksm_rmap_item
 - use vm_pgoff instead of address >> PAGE_SHIFT (Suggested by David and Lorenzo)

Changes in v3:
- Fix some typos in commit description
- Replace "pgoff_start" and 'pgoff_end' by 'pgoff'.

Changes in v2:
- Use const variable to initialize 'addr'  "pgoff_start" and 'pgoff_end'
- Let pgoff_end = pgoff_start, since KSM folios are always order-0 (Suggested by David)

xu xin (3):
  ksm: add linear_page_index into ksm_rmap_item
  ksm: Optimize rmap_walk_ksm by passing a suitable page index
  ksm: add mremap selftests for ksm_rmap_walk

 mm/ksm.c                          | 56 +++++++++++++++++---
 tools/testing/selftests/mm/rmap.c | 86 +++++++++++++++++++++++++++++++
 2 files changed, 134 insertions(+), 8 deletions(-)

-- 
2.25.1

^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v8 1/3] ksm: add linear_page_index into ksm_rmap_item
  2026-06-09  4:39 [PATCH v8 0/3] KSM: performance optimizations for rmap_walk_ksm xu.xin16
@ 2026-06-09  4:40 ` xu.xin16
  2026-06-09  7:44   ` David Hildenbrand (Arm)
  2026-06-09  4:47 ` [PATCH v8 2/3] ksm: Optimize rmap_walk_ksm by passing a suitable page index xu.xin16
  2026-06-09  4:47 ` [PATCH v8 3/3] ksm: add mremap selftests for ksm_rmap_walk xu.xin16
  2 siblings, 1 reply; 9+ messages in thread
From: xu.xin16 @ 2026-06-09  4:40 UTC (permalink / raw)
  To: akpm, david
  Cc: chengming.zhou, hughd, wang.yaxin, linux-mm, linux-kernel, ljs

From: xu xin <xu.xin16@zte.com.cn>

As preparation for KSM rmap optimizations, let's track the original
linear_page_index() of a de-duplicated page in its ksm_rmap_item, so we can
efficiently search for the page in an address space, avoiding scanning the
entire address space. This was previously discussed in [1, 2].

To avoid growing ksm_rmap_item, let's squeeze it into the existing
structure by overlying some members (oldchecksum, age, remaining_skips)
that are only relevant while on the unstable tree. The new entry will
only be relevant for entries in the stable tree.

However, as the age information is read by should_skip_rmap_item() with the
smart-scanning approach even while we have an entry in the stable tree, but
the page changes (no longer a KSM page, for example due to COW), we have to
change the handling there a bit.

We'll calculate the linear page index in try_to_merge_with_ksm_page(), when
adding it to the stable tree, and reset the index (to reset overlayed data)
when removing an item from the stable tree -- in
remove_rmap_item_from_tree(), remove_node_from_stable_tree() and
break_cow().

To be specially clarified, the reason for resetting the stored index at
break_cow() is:

- When a page successfully becomes a KSM page (i.e., after
  stable_tree_append() sets STABLE_FLAG), both anon_vma and the index are
  stored and remain valid.

- However, during the merging process, there are several failure paths
  where we already prepared an rmap item to be added to the stable tree,
  but must revert that as some part of the merge process failed. Examples
  include:
    * The second call to try_to_merge_with_ksm_page() fails in
      try_to_merge_two_pages().
    * stable_tree_insert() fails in cmp_and_merge_page().
  In such cases, break_cow() is invoked to break the COW mapping and
  discard the KSM state.

Currently, break_cow() already contains a put_anon_vma(rmap_item->anon_vma)
to release the reference taken during the aborted merge. Because the index
is logically paired with anon_vma (both are only meaningful when the
rmap_item is in a stable state), it must also be cleared (or reset) in
break_cow() to avoid leaving stale linear_page_index values that could
confuse subsequent rmap walks or scanning logic.

[1] https://lore.kernel.org/all/adTPQSb-qSSHviJN@lucifer/
[2] https://lore.kernel.org/all/202604091806051535BJWZ_FTtdIm3Snk24ei_@zte.com.cn/

Signed-off-by: xu xin <xu.xin16@zte.com.cn>
---
 mm/ksm.c | 49 ++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 42 insertions(+), 7 deletions(-)

diff --git a/mm/ksm.c b/mm/ksm.c
index 7d5b76478f0b..e0ba29e3c0a4 100644
--- a/mm/ksm.c
+++ b/mm/ksm.c
@@ -195,22 +195,28 @@ struct ksm_stable_node {
  * @node: rb node of this rmap_item in the unstable tree
  * @head: pointer to stable_node heading this list in the stable tree
  * @hlist: link into hlist of rmap_items hanging off that stable_node
- * @age: number of scan iterations since creation
- * @remaining_skips: how many scans to skip
+ * @age: number of scan iterations since creation (unstable node)
+ * @remaining_skips: how many scans to skip (unstable node)
+ * @linear_page_index: the original page's index before merged by KSM (stable node)
  */
 struct ksm_rmap_item {
 	struct ksm_rmap_item *rmap_list;
 	union {
-		struct anon_vma *anon_vma;	/* when stable */
+		struct anon_vma *anon_vma;	/* for reverse mapping, when stable */
 #ifdef CONFIG_NUMA
 		int nid;		/* when node of unstable tree */
 #endif
 	};
 	struct mm_struct *mm;
 	unsigned long address;		/* + low bits used for flags below */
-	unsigned int oldchecksum;	/* when unstable */
-	rmap_age_t age;
-	rmap_age_t remaining_skips;
+	union {
+		struct {
+			unsigned int oldchecksum;
+			rmap_age_t age;
+			rmap_age_t remaining_skips;
+		};			/* when unstable */
+		unsigned long linear_page_index;    /* for reverse mapping, when stable */
+	};
 	union {
 		struct rb_node node;	/* when node of unstable tree */
 		struct {		/* when listed from stable tree */
@@ -776,6 +782,11 @@ static struct vm_area_struct *find_mergeable_vma(struct mm_struct *mm,
 	return vma;
 }

+/*
+ * break_cow: actively break COW, replacing the KSM page by a fresh anonymous
+ * page. This is called when rmap_item has not yet become stable, but page
+ * has been merged.
+ */
 static void break_cow(struct ksm_rmap_item *rmap_item)
 {
 	struct mm_struct *mm = rmap_item->mm;
@@ -787,6 +798,11 @@ static void break_cow(struct ksm_rmap_item *rmap_item)
 	 * to undo, we also need to drop a reference to the anon_vma.
 	 */
 	put_anon_vma(rmap_item->anon_vma);
+	/*
+	 * Reset linear_page_index that might overlay age-related
+	 * information. (it's still unstable node)
+	 */
+	rmap_item->linear_page_index = 0;

 	mmap_read_lock(mm);
 	vma = find_mergeable_vma(mm, addr);
@@ -899,6 +915,8 @@ static void remove_node_from_stable_tree(struct ksm_stable_node *stable_node)
 		VM_BUG_ON(stable_node->rmap_hlist_len <= 0);
 		stable_node->rmap_hlist_len--;
 		put_anon_vma(rmap_item->anon_vma);
+		/* Reset linear_page_index that might overlay age-related information. */
+		rmap_item->linear_page_index = 0;
 		rmap_item->address &= PAGE_MASK;
 		cond_resched();
 	}
@@ -1052,6 +1070,8 @@ static void remove_rmap_item_from_tree(struct ksm_rmap_item *rmap_item)
 		stable_node->rmap_hlist_len--;

 		put_anon_vma(rmap_item->anon_vma);
+		/* Reset linear_page_index that might overlay age-related information. */
+		rmap_item->linear_page_index = 0;
 		rmap_item->head = NULL;
 		rmap_item->address &= PAGE_MASK;

@@ -1598,8 +1618,16 @@ static int try_to_merge_with_ksm_page(struct ksm_rmap_item *rmap_item,
 	/* Unstable nid is in union with stable anon_vma: remove first */
 	remove_rmap_item_from_tree(rmap_item);

-	/* Must get reference to anon_vma while still holding mmap_lock */
+	/*
+	 * Must get reference to anon_vma while still holding mmap_lock.
+	 * Must can only reference the VMA while still holding the mmap
+	 * lock, so reference the anon_vma and calculate the linear page
+	 * index early, before stable_tree_append(). If anything goes
+	 * wrong that prevents the rmap_item from being added to the
+	 * stable_tree, break_cow() will clean it up.
+	 */
 	rmap_item->anon_vma = vma->anon_vma;
+	rmap_item->linear_page_index = linear_page_index(vma, rmap_item->address);
 	get_anon_vma(vma->anon_vma);
 out:
 	mmap_read_unlock(mm);
@@ -2458,6 +2486,13 @@ static bool should_skip_rmap_item(struct folio *folio,
 	if (folio_test_ksm(folio))
 		return false;

+	/*
+	 * There is no age information in stable-tree nodes. We might end up
+	 * here without a KSM page for example after COW.
+	 */
+	if (rmap_item->address & STABLE_FLAG)
+		return false;
+
 	age = rmap_item->age;
 	if (age != U8_MAX)
 		rmap_item->age++;
-- 
2.25.1

^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v8 2/3] ksm: Optimize rmap_walk_ksm by passing a suitable page index
  2026-06-09  4:39 [PATCH v8 0/3] KSM: performance optimizations for rmap_walk_ksm xu.xin16
  2026-06-09  4:40 ` [PATCH v8 1/3] ksm: add linear_page_index into ksm_rmap_item xu.xin16
@ 2026-06-09  4:47 ` xu.xin16
  2026-06-09  8:13   ` David Hildenbrand (Arm)
  2026-06-09  4:47 ` [PATCH v8 3/3] ksm: add mremap selftests for ksm_rmap_walk xu.xin16
  2 siblings, 1 reply; 9+ messages in thread
From: xu.xin16 @ 2026-06-09  4:47 UTC (permalink / raw)
  To: akpm, david
  Cc: chengming.zhou, hughd, wang.yaxin, linux-mm, linux-kernel, ljs

From: xu xin <xu.xin16@zte.com.cn>

User impact / Why this matters to Linux users
=============================================
When a system runs with KSM enabled and memory becomes tight, KSM pages
may be swapped out or migrated. The kernel then performs a reverse map
walk by rmap_walk_ksm to locate all page table entries that reference
these pages. If A large number of unrelated VMAs can attach to a single
anon_vma related with this KSM page, then rmap_walk might be severe
performance bottleneck.  In our embedded test environment, we observed
~20,000 VMAs sharing one anon_vma without any fork – purely from VMA
splits, which cause 200~700ms duration of rmap_walk_ksm.

When one of those VMAs mapped a KSM page, then this KSM page's rmapping
will become bottleneck with hold its anon_vma lock for a long time. The
anon_vma lock is not only used by KSM; it is a core lock protecting the
VMA interval tree and is acquired by many critical memory operations:

  • Page faults: do_anonymous_page(), do_wp_page() (during COW)
  • Memory reclaim: try_to_unmap()
  • Page migration & compaction: migrate_pages(), compact_zone()
  • mlock / munlock: mlock_fixup()
  • Process exit: exit_mmap() (tearing down VMAs)
  • Cgroup memory accounting: mem_cgroup_move_charge()

If one thread holds the anon_vma lock for hundreds of milliseconds
because of an inefficient KSM rmap walk, any other thread that tries to
acquire the same lock (e.g., an application taking a page fault, kswapd
reclaiming pages, or a migration thread) will block.  This leads to
stalled application threads, increased latency spikes, and in extreme
cases container timeouts or watchdog triggers.

This patch reduces the worst-case anon_vma lock hold time during KSM
rmap walk from >500 ms to <1 ms, thereby almost eliminating this
source of lock contention and improving system responsiveness under
memory pressure.

Real-world examples:
====================
 - JVM / Go runtime: These use mmap for heap regions and later call
mprotect(PROT_NONE) for garbage collection barriers or guard pages,
splitting the original VMA into thousands of small pieces over time.

 - Database engines (MySQL, PostgreSQL): Large shared memory buffers
or anonymous mappings are managed with madvise(MADV_DONTNEED) to
release specific pages, which also splits VMAs.

* Why the benchmark numbers are realistic: We observed ~20,000 VMAs
sharing one anon_vma on a production system running a Java application
with KSM enabled. The lock hold time before the patch was measured at
228 ms (max) during rmap walks triggered by memory compaction and page
migration. The benchmark reproduces that VMA count and lock‑hold
behavior in a controlled environment.

Root Cause
==========
Through local debugging trace analysis, we found that most of the latency
of rmap_walk_ksm occurs within anon_vma_interval_tree_foreach(), leading
to an excessively long hold time on the anon_vma lock (even reaching 500ms
or more), which in turn causes upper-layer applications (waiting for the
anon_vma lock) to be blocked for extended periods.

Further investigation revealed that 99.9% of iterations inside the
anon_vma_interval_tree_foreach loop are skipped due to the first check
"if (addr < vma->vm_start || addr >= vma->vm_end)), indicating that a large
number of loop iterations are ineffective. This inefficiency arises because
the start page index and the end page index parameters passed to
anon_vma_interval_tree_foreach span the entire address space from 0 to
ULONG_MAX, resulting in very poor loop efficiency.

Solution
========
We cannot rely solely on anon_vma to locate all PTEs mapping this page
but also need to have the original page's linear_page_index. Since the
implementation of anon_vma_interval_tree_foreach — it essentially
iterates to find a suitable VMA such that the provided page index falls
within the candidate's vm_pgoff range.

vm_pgoff <= original linear page offset <= (vm_pgoff + vma_pages(v) - 1)

Fortunately, we have already linear_page_index. in ksm_rmap_item in the
previos patch of series, so that we use it to get the index to accelerate
the searching.

Test results
============
A rmap testbench can be obtained with two Out-Of-Tree patches at [1][2].
After applying the OOT patches and building rmap_benchmark from:
tools/testing/rmap/rmap_benchmark.c, we can start the performance test.

The testing result in QEMU is shown as follows:

KSM rmapping	Maximum duration		Average duration

Before:		705.12 ms (705119858 ns)	532.04 ms (532041586 ns)
After:		1.67 ms (1665917 ns)		1.44 ms (1443784 ns)

[1] https://lore.kernel.org/all/202605301703094695zmVgcSC27BNR0rH0N8_x@zte.com.cn
[2] https://lore.kernel.org/all/20260530170404509QpJmBtpSjn3uQHeVKA2iA@zte.com.cn/

Co-developed-by: Wang Yaxin <wang.yaxin@zte.com.cn>
Signed-off-by: Wang Yaxin <wang.yaxin@zte.com.cn>
Signed-off-by: xu xin <xu.xin16@zte.com.cn>
---
 mm/ksm.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/mm/ksm.c b/mm/ksm.c
index e0ba29e3c0a4..9e1879d96751 100644
--- a/mm/ksm.c
+++ b/mm/ksm.c
@@ -3208,6 +3208,7 @@ void rmap_walk_ksm(struct folio *folio, struct rmap_walk_control *rwc)
 	hlist_for_each_entry(rmap_item, &stable_node->hlist, hlist) {
 		/* Ignore the stable/unstable/sqnr flags */
 		const unsigned long addr = rmap_item->address & PAGE_MASK;
+		const unsigned long index = rmap_item->linear_page_index;
 		struct anon_vma *anon_vma = rmap_item->anon_vma;
 		struct anon_vma_chain *vmac;
 		struct vm_area_struct *vma;
@@ -3221,8 +3222,12 @@ void rmap_walk_ksm(struct folio *folio, struct rmap_walk_control *rwc)
 			anon_vma_lock_read(anon_vma);
 		}

+		/*
+		 * Currently KSM folios are order-0 normal pages, so the end
+		 * page's index should be the same as the start page's index.
+		 */
 		anon_vma_interval_tree_foreach(vmac, &anon_vma->rb_root,
-					       0, ULONG_MAX) {
+					       index, index) {

 			cond_resched();
 			vma = vmac->vma;
-- 
2.25.1

^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v8 3/3] ksm: add mremap selftests for ksm_rmap_walk
  2026-06-09  4:39 [PATCH v8 0/3] KSM: performance optimizations for rmap_walk_ksm xu.xin16
  2026-06-09  4:40 ` [PATCH v8 1/3] ksm: add linear_page_index into ksm_rmap_item xu.xin16
  2026-06-09  4:47 ` [PATCH v8 2/3] ksm: Optimize rmap_walk_ksm by passing a suitable page index xu.xin16
@ 2026-06-09  4:47 ` xu.xin16
  2026-06-09  9:18   ` David Hildenbrand (Arm)
  2 siblings, 1 reply; 9+ messages in thread
From: xu.xin16 @ 2026-06-09  4:47 UTC (permalink / raw)
  To: akpm, david
  Cc: chengming.zhou, hughd, wang.yaxin, linux-mm, linux-kernel, ljs

From: xu xin <xu.xin16@zte.com.cn>

The existing tools/testing/selftests/mm/rmap.c has already one testcase
for ksm_rmap_walk in TEST_F(migrate, ksm), which takes use of migration
of page from one NUMA node to another NUMA node. However, it just lacks
the scenario of mremapped VMAs.

We add the calling of mremap() and then trigger KSM to merge pages before
migrating, which is specifically to test an optimization which is
introduced by this patch ("ksm: Optimize rmap_walk_ksm by passing a
suitable address pgoff").

This test can reproduce the issue that Hugh points out at
https://lore.kernel.org/all/02e1b8df-d568-8cbb-b8f6-46d5476d9d75@google.com/

Signed-off-by: xu xin <xu.xin16@zte.com.cn>
---
 tools/testing/selftests/mm/rmap.c | 86 +++++++++++++++++++++++++++++++
 1 file changed, 86 insertions(+)

diff --git a/tools/testing/selftests/mm/rmap.c b/tools/testing/selftests/mm/rmap.c
index 53f2058b0ef2..1cdc4beb48c2 100644
--- a/tools/testing/selftests/mm/rmap.c
+++ b/tools/testing/selftests/mm/rmap.c
@@ -430,4 +430,90 @@ TEST_F(migrate, ksm)
 	propagate_children(_metadata, data);
 }

+static void prepare_pages(struct global_data *data, int nr_pages)
+{
+	/* Allocate exactly pages for the test */
+	data->mapsize = nr_pages * getpagesize();
+	data->region = mmap(NULL, data->mapsize, PROT_READ | PROT_WRITE,
+			    MAP_PRIVATE | MAP_ANON, -1, 0);
+	if (data->region == MAP_FAILED)
+		ksft_exit_fail_perror("mmap failed");
+
+	/* Fill all pages with identical content to encourage KSM merging */
+	memset(data->region, 0x77, data->mapsize);
+}
+
+static int mremap_merge_and_migrate(struct global_data *data)
+{
+	int ret;
+	void *old_region;
+	void *new_region;
+	int nr_pages = 32;
+	long merging_pages;
+
+	prepare_pages(data, nr_pages);
+
+	if (ksm_start() < 0)
+		return FAIL_ON_CHECK;
+
+	old_region = data->region;
+	/*
+	 * Mremap the second half region to the first half location (FIXED).
+	 */
+	new_region = mremap(old_region + data->mapsize / 2, data->mapsize / 2,
+			    data->mapsize / 2, MREMAP_MAYMOVE | MREMAP_FIXED,
+			    old_region);
+	if (new_region == MAP_FAILED) {
+		ksft_print_msg("mremap failed: %s\n", strerror(errno));
+		return FAIL_ON_CHECK;
+	}
+	data->region = new_region;
+	data->mapsize /= 2;	/* mapping is now half of original */
+
+	if (ksm_start() < 0)
+		return FAIL_ON_CHECK;
+
+	/* Attempt to migrate the merged KSM page */
+	ret = try_to_move_page(data->region);
+	if (ret != 0) {
+		ksft_print_msg("migration of KSM page after mremap failed\n");
+		return FAIL_ON_CHECK;
+	}
+
+	/* Ensure ksmd scan two turns at least to update ksm counters */
+	if (ksm_start() < 0)
+		return FAIL_ON_CHECK;
+
+	merging_pages = ksm_get_self_merging_pages();
+	printf("merging_pages:%ld\n", merging_pages);
+	if (merging_pages != nr_pages / 2) {
+		ksft_print_msg("Unexpected KSM counters: ksm_merging_pages=%ld,expected=%d\n",
+			       merging_pages, nr_pages / 2);
+		return FAIL_ON_CHECK;
+	}
+
+	return 0;
+}
+
+
+TEST_F(migrate, ksm_and_mremap)
+{
+	struct global_data *data = &self->data;
+	int ret;
+
+	/* Skip if KSM is not available */
+	if (ksm_stop() < 0)
+		SKIP(return, "accessing \"/sys/kernel/mm/ksm/run\" failed");
+	if (ksm_get_full_scans() < 0)
+		SKIP(return, "accessing \"/sys/kernel/mm/ksm/full_scan\" failed");
+
+	ret = prctl(PR_SET_MEMORY_MERGE, 1, 0, 0, 0);
+	if (ret < 0 && errno == EINVAL)
+		SKIP(return, "PR_SET_MEMORY_MERGE not supported");
+	else if (ret)
+		ksft_exit_fail_perror("PR_SET_MEMORY_MERGE=1 failed");
+
+	ASSERT_EQ(mremap_merge_and_migrate(data), 0);
+}
+
 TEST_HARNESS_MAIN
-- 
2.25.1

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v8 1/3] ksm: add linear_page_index into ksm_rmap_item
  2026-06-09  4:40 ` [PATCH v8 1/3] ksm: add linear_page_index into ksm_rmap_item xu.xin16
@ 2026-06-09  7:44   ` David Hildenbrand (Arm)
  2026-06-09 11:45     ` xu.xin16
  0 siblings, 1 reply; 9+ messages in thread
From: David Hildenbrand (Arm) @ 2026-06-09  7:44 UTC (permalink / raw)
  To: xu.xin16, akpm
  Cc: chengming.zhou, hughd, wang.yaxin, linux-mm, linux-kernel, ljs

On 6/9/26 06:40, xu.xin16@zte.com.cn wrote:
> From: xu xin <xu.xin16@zte.com.cn>
> 
> As preparation for KSM rmap optimizations, let's track the original
> linear_page_index() of a de-duplicated page in its ksm_rmap_item, so we can
> efficiently search for the page in an address space, avoiding scanning the
> entire address space. This was previously discussed in [1, 2].
> 
> To avoid growing ksm_rmap_item, let's squeeze it into the existing
> structure by overlying some members (oldchecksum, age, remaining_skips)
> that are only relevant while on the unstable tree. The new entry will
> only be relevant for entries in the stable tree.
> 
> However, as the age information is read by should_skip_rmap_item() with the
> smart-scanning approach even while we have an entry in the stable tree, but
> the page changes (no longer a KSM page, for example due to COW), we have to
> change the handling there a bit.
> 
> We'll calculate the linear page index in try_to_merge_with_ksm_page(), when
> adding it to the stable tree, and reset the index (to reset overlayed data)
> when removing an item from the stable tree -- in
> remove_rmap_item_from_tree(), remove_node_from_stable_tree() and
> break_cow().
> 
> To be specially clarified, the reason for resetting the stored index at
> break_cow() is:
> 
> - When a page successfully becomes a KSM page (i.e., after
>   stable_tree_append() sets STABLE_FLAG), both anon_vma and the index are
>   stored and remain valid.
> 
> - However, during the merging process, there are several failure paths
>   where we already prepared an rmap item to be added to the stable tree,
>   but must revert that as some part of the merge process failed. Examples
>   include:
>     * The second call to try_to_merge_with_ksm_page() fails in
>       try_to_merge_two_pages().
>     * stable_tree_insert() fails in cmp_and_merge_page().
>   In such cases, break_cow() is invoked to break the COW mapping and
>   discard the KSM state.
> 
> Currently, break_cow() already contains a put_anon_vma(rmap_item->anon_vma)
> to release the reference taken during the aborted merge. Because the index
> is logically paired with anon_vma (both are only meaningful when the
> rmap_item is in a stable state), it must also be cleared (or reset) in
> break_cow() to avoid leaving stale linear_page_index values that could
> confuse subsequent rmap walks or scanning logic.
> 
> [1] https://lore.kernel.org/all/adTPQSb-qSSHviJN@lucifer/
> [2] https://lore.kernel.org/all/202604091806051535BJWZ_FTtdIm3Snk24ei_@zte.com.cn/


[...]

> +/*
> + * break_cow: actively break COW, replacing the KSM page by a fresh anonymous
> + * page. This is called when rmap_item has not yet become stable, but page
> + * has been merged.
> + */
>  static void break_cow(struct ksm_rmap_item *rmap_item)
>  {
>  	struct mm_struct *mm = rmap_item->mm;
> @@ -787,6 +798,11 @@ static void break_cow(struct ksm_rmap_item *rmap_item)
>  	 * to undo, we also need to drop a reference to the anon_vma.
>  	 */
>  	put_anon_vma(rmap_item->anon_vma);
> +	/*
> +	 * Reset linear_page_index that might overlay age-related
> +	 * information. (it's still unstable node)
> +	 */
> +	rmap_item->linear_page_index = 0;

Sashiko comments that, on 32bit, it is not overlaying age-related information.
So setting it to 0 won't clear the age.

Which is what we document with the "might".

On 32bit, it simply behaves the way it was before (no reset of age-related
information here).

We could move oldchecksum below remaining_skips to clear age-related information
consistently. Not sure whether that is really worth it.

I would even say that we can just stop supporting KSM on 32bit completely.

> 
>  	mmap_read_lock(mm);
>  	vma = find_mergeable_vma(mm, addr);
> @@ -899,6 +915,8 @@ static void remove_node_from_stable_tree(struct ksm_stable_node *stable_node)
>  		VM_BUG_ON(stable_node->rmap_hlist_len <= 0);
>  		stable_node->rmap_hlist_len--;
>  		put_anon_vma(rmap_item->anon_vma);
> +		/* Reset linear_page_index that might overlay age-related information. */
> +		rmap_item->linear_page_index = 0;
>  		rmap_item->address &= PAGE_MASK;
>  		cond_resched();
>  	}
> @@ -1052,6 +1070,8 @@ static void remove_rmap_item_from_tree(struct ksm_rmap_item *rmap_item)
>  		stable_node->rmap_hlist_len--;
> 
>  		put_anon_vma(rmap_item->anon_vma);
> +		/* Reset linear_page_index that might overlay age-related information. */
> +		rmap_item->linear_page_index = 0;
>  		rmap_item->head = NULL;
>  		rmap_item->address &= PAGE_MASK;
> 
> @@ -1598,8 +1618,16 @@ static int try_to_merge_with_ksm_page(struct ksm_rmap_item *rmap_item,
>  	/* Unstable nid is in union with stable anon_vma: remove first */
>  	remove_rmap_item_from_tree(rmap_item);
> 
> -	/* Must get reference to anon_vma while still holding mmap_lock */
> +	/*
> +	 * Must get reference to anon_vma while still holding mmap_lock.

I think this sentence should go, and instead ...

> +	 * Must can only reference the VMA while still holding the mmap

This one should become:

"We can consider the VMA only while still holding the mmap lock, so ...

> +	 * lock, so reference the anon_vma and calculate the linear page
> +	 * index early, before stable_tree_append(). If anything goes
> +	 * wrong that prevents the rmap_item from being added to the
> +	 * stable_tree, break_cow() will clean it up.
> +	 */
>  	rmap_item->anon_vma = vma->anon_vma;
> +	rmap_item->linear_page_index = linear_page_index(vma, rmap_item->address);
>  	get_anon_vma(vma->anon_vma);
>  out:
>  	mmap_read_unlock(mm);
> @@ -2458,6 +2486,13 @@ static bool should_skip_rmap_item(struct folio *folio,
>  	if (folio_test_ksm(folio))
>  		return false;

Thinking about the overlay once more, I'm trying to assess what it means when we
clear rmap_item->checksum. We'd do that now in:

(a) remove_node_from_stable_tree(): We had a stable node -> KSM page, but
something changed.

(b) remove_rmap_item_from_tree(): Same as (a)

(c) break_cow(): we have to unshare, either because insertion into the stable
    tree failed, or because we would have only a single PTE mapping the KSM
    folio after a failed merge.

For (c), I guess if we'd have to, we could remember the checksum while
processing the rmap_item.

Clearing rmap_item->checksum implies that cmp_and_merge_page() would refuse to
merge one round.

Having rmap_item->checksum cleared is just like allocating a fresh rmap_item. So
it will fix itself up during the next scan.

So my best guess is that this is alright.


Hoping for no surprises in corner cases

Acked-by: David Hildenbrand (Arm) <david@kernel.org>

-- 
Cheers,

David

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v8 2/3] ksm: Optimize rmap_walk_ksm by passing a suitable page index
  2026-06-09  4:47 ` [PATCH v8 2/3] ksm: Optimize rmap_walk_ksm by passing a suitable page index xu.xin16
@ 2026-06-09  8:13   ` David Hildenbrand (Arm)
  0 siblings, 0 replies; 9+ messages in thread
From: David Hildenbrand (Arm) @ 2026-06-09  8:13 UTC (permalink / raw)
  To: xu.xin16, akpm
  Cc: chengming.zhou, hughd, wang.yaxin, linux-mm, linux-kernel, ljs

On 6/9/26 06:47, xu.xin16@zte.com.cn wrote:
> From: xu xin <xu.xin16@zte.com.cn>
> 
> User impact / Why this matters to Linux users
> =============================================
> When a system runs with KSM enabled and memory becomes tight, KSM pages
> may be swapped out or migrated. The kernel then performs a reverse map
> walk by rmap_walk_ksm to locate all page table entries that reference
> these pages. If A large number of unrelated VMAs can attach to a single
> anon_vma related with this KSM page, then rmap_walk might be severe
> performance bottleneck.  In our embedded test environment, we observed
> ~20,000 VMAs sharing one anon_vma without any fork – purely from VMA
> splits, which cause 200~700ms duration of rmap_walk_ksm.
> 
> When one of those VMAs mapped a KSM page, then this KSM page's rmapping
> will become bottleneck with hold its anon_vma lock for a long time. The
> anon_vma lock is not only used by KSM; it is a core lock protecting the
> VMA interval tree and is acquired by many critical memory operations:
> 
>   • Page faults: do_anonymous_page(), do_wp_page() (during COW)
>   • Memory reclaim: try_to_unmap()
>   • Page migration & compaction: migrate_pages(), compact_zone()
>   • mlock / munlock: mlock_fixup()
>   • Process exit: exit_mmap() (tearing down VMAs)
>   • Cgroup memory accounting: mem_cgroup_move_charge()
> 
> If one thread holds the anon_vma lock for hundreds of milliseconds
> because of an inefficient KSM rmap walk, any other thread that tries to
> acquire the same lock (e.g., an application taking a page fault, kswapd
> reclaiming pages, or a migration thread) will block.  This leads to
> stalled application threads, increased latency spikes, and in extreme
> cases container timeouts or watchdog triggers.
> 
> This patch reduces the worst-case anon_vma lock hold time during KSM
> rmap walk from >500 ms to <1 ms, thereby almost eliminating this
> source of lock contention and improving system responsiveness under
> memory pressure.
> 
> Real-world examples:
> ====================
>  - JVM / Go runtime: These use mmap for heap regions and later call
> mprotect(PROT_NONE) for garbage collection barriers or guard pages,
> splitting the original VMA into thousands of small pieces over time.
> 
>  - Database engines (MySQL, PostgreSQL): Large shared memory buffers
> or anonymous mappings are managed with madvise(MADV_DONTNEED) to
> release specific pages, which also splits VMAs.
> 
> * Why the benchmark numbers are realistic: We observed ~20,000 VMAs

The "*" at the start looks odd (the list above used "-", and I assume this
should be separate from the list).

> sharing one anon_vma on a production system running a Java application
> with KSM enabled. The lock hold time before the patch was measured at
> 228 ms (max) during rmap walks triggered by memory compaction and page
> migration. The benchmark reproduces that VMA count and lock‑hold
> behavior in a controlled environment.
> 
> Root Cause
> ==========
> Through local debugging trace analysis, we found that most of the latency
> of rmap_walk_ksm occurs within anon_vma_interval_tree_foreach(), leading
> to an excessively long hold time on the anon_vma lock (even reaching 500ms
> or more), which in turn causes upper-layer applications (waiting for the
> anon_vma lock) to be blocked for extended periods.
> 
> Further investigation revealed that 99.9% of iterations inside the
> anon_vma_interval_tree_foreach loop are skipped due to the first check
> "if (addr < vma->vm_start || addr >= vma->vm_end)), indicating that a large
> number of loop iterations are ineffective. This inefficiency arises because
> the start page index and the end page index parameters passed to
> anon_vma_interval_tree_foreach span the entire address space from 0 to
> ULONG_MAX, resulting in very poor loop efficiency.
> 
> Solution
> ========
> We cannot rely solely on anon_vma to locate all PTEs mapping this page
> but also need to have the original page's linear_page_index. Since the
> implementation of anon_vma_interval_tree_foreach — it essentially
> iterates to find a suitable VMA such that the provided page index falls
> within the candidate's vm_pgoff range.
> 
> vm_pgoff <= original linear page offset <= (vm_pgoff + vma_pages(v) - 1)
> 
> Fortunately, we have already linear_page_index. in ksm_rmap_item in the

Misplaced "."

> previos patch of series, so that we use it to get the index to accelerate
> the searching.

Avoid talking a about "previous patch" in a series. Something like:

"Fortunately, an earlier commit introduced the linear_page_index to struct
ksm_rmap_item, allowing for optimizing the RMAP walk."

> 
> Test results
> ============
> A rmap testbench can be obtained with two Out-Of-Tree patches at [1][2].
> After applying the OOT patches and building rmap_benchmark from:
> tools/testing/rmap/rmap_benchmark.c, we can start the performance test.
> 
> The testing result in QEMU is shown as follows:
> 
> KSM rmapping	Maximum duration		Average duration
> 
> Before:		705.12 ms (705119858 ns)	532.04 ms (532041586 ns)
> After:		1.67 ms (1665917 ns)		1.44 ms (1443784 ns)
> 
> [1] https://lore.kernel.org/all/202605301703094695zmVgcSC27BNR0rH0N8_x@zte.com.cn
> [2] https://lore.kernel.org/all/20260530170404509QpJmBtpSjn3uQHeVKA2iA@zte.com.cn/
> 
> Co-developed-by: Wang Yaxin <wang.yaxin@zte.com.cn>
> Signed-off-by: Wang Yaxin <wang.yaxin@zte.com.cn>
> Signed-off-by: xu xin <xu.xin16@zte.com.cn>
> ---
>  mm/ksm.c | 7 ++++++-
>  1 file changed, 6 insertions(+), 1 deletion(-)
> 
> diff --git a/mm/ksm.c b/mm/ksm.c
> index e0ba29e3c0a4..9e1879d96751 100644
> --- a/mm/ksm.c
> +++ b/mm/ksm.c
> @@ -3208,6 +3208,7 @@ void rmap_walk_ksm(struct folio *folio, struct rmap_walk_control *rwc)
>  	hlist_for_each_entry(rmap_item, &stable_node->hlist, hlist) {
>  		/* Ignore the stable/unstable/sqnr flags */
>  		const unsigned long addr = rmap_item->address & PAGE_MASK;
> +		const unsigned long index = rmap_item->linear_page_index;

remove_rmap_item_from_tree() does the hlist_del(&rmap_item->hlist); once we
clear STABLE_FLAG + rmap_item->linear_page_index.

So, this should always be valid (just like rmap_item->anon_vma). Good.

>  		struct anon_vma *anon_vma = rmap_item->anon_vma;
>  		struct anon_vma_chain *vmac;
>  		struct vm_area_struct *vma;
> @@ -3221,8 +3222,12 @@ void rmap_walk_ksm(struct folio *folio, struct rmap_walk_control *rwc)
>  			anon_vma_lock_read(anon_vma);
>  		}
> 
> +		/*
> +		 * Currently KSM folios are order-0 normal pages, so the end
> +		 * page's index should be the same as the start page's index.

Maybe best to say "Currently, KSM folios are always small folios, so it's
sufficient to search for a single page."

I don't even want to imagine how large-folio support could look like, and how it
interacts with rmap_items :/ Let's hope we'll never have to go there, and if so,
this code here will be the least of our concerns.

Do we want to explain a bit how this works and on which properties this relies on?

"We can simply use the linear_page_index of the de-duplicated anonymous page
that we remembered in the rmap_item while de-duplicating. Note that
mremap() always de-duplicates KSM folios: so if there was mremap() in our parent
or our child, we wouldn't have the KSM folio mapped in these processes anymore."

> +		 */
>  		anon_vma_interval_tree_foreach(vmac, &anon_vma->rb_root,
> -					       0, ULONG_MAX) {
> +					       index, index) {
> 
>  			cond_resched();
>  			vma = vmac->vma;

I hope we're not missing some other weird corner cases. Sashiko seems to be
happy, but I don't trust that ;)

Hoping other people can also give this another look before we move this upstream.

Acked-by: David Hildenbrand (Arm) <david@kernel.org>

-- 
Cheers,

David

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v8 3/3] ksm: add mremap selftests for ksm_rmap_walk
  2026-06-09  4:47 ` [PATCH v8 3/3] ksm: add mremap selftests for ksm_rmap_walk xu.xin16
@ 2026-06-09  9:18   ` David Hildenbrand (Arm)
  2026-06-10  6:21     ` xu.xin16
  0 siblings, 1 reply; 9+ messages in thread
From: David Hildenbrand (Arm) @ 2026-06-09  9:18 UTC (permalink / raw)
  To: xu.xin16, akpm
  Cc: chengming.zhou, hughd, wang.yaxin, linux-mm, linux-kernel, ljs

On 6/9/26 06:47, xu.xin16@zte.com.cn wrote:
> From: xu xin <xu.xin16@zte.com.cn>
> 
> The existing tools/testing/selftests/mm/rmap.c has already one testcase
> for ksm_rmap_walk in TEST_F(migrate, ksm), which takes use of migration
> of page from one NUMA node to another NUMA node. However, it just lacks
> the scenario of mremapped VMAs.
> 
> We add the calling of mremap() and then trigger KSM to merge pages before
> migrating, which is specifically to test an optimization which is
> introduced by this patch ("ksm: Optimize rmap_walk_ksm by passing a
> suitable address pgoff").
> 
> This test can reproduce the issue that Hugh points out at
> https://lore.kernel.org/all/02e1b8df-d568-8cbb-b8f6-46d5476d9d75@google.com/
> 
> Signed-off-by: xu xin <xu.xin16@zte.com.cn>
> ---
>  tools/testing/selftests/mm/rmap.c | 86 +++++++++++++++++++++++++++++++
>  1 file changed, 86 insertions(+)
> 
> diff --git a/tools/testing/selftests/mm/rmap.c b/tools/testing/selftests/mm/rmap.c
> index 53f2058b0ef2..1cdc4beb48c2 100644
> --- a/tools/testing/selftests/mm/rmap.c
> +++ b/tools/testing/selftests/mm/rmap.c
> @@ -430,4 +430,90 @@ TEST_F(migrate, ksm)
>  	propagate_children(_metadata, data);
>  }
> 
> +static void prepare_pages(struct global_data *data, int nr_pages)
> +{
> +	/* Allocate exactly pages for the test */
> +	data->mapsize = nr_pages * getpagesize();
> +	data->region = mmap(NULL, data->mapsize, PROT_READ | PROT_WRITE,
> +			    MAP_PRIVATE | MAP_ANON, -1, 0);
> +	if (data->region == MAP_FAILED)
> +		ksft_exit_fail_perror("mmap failed");
> +
> +	/* Fill all pages with identical content to encourage KSM merging */
> +	memset(data->region, 0x77, data->mapsize);
> +}
> +
> +static int mremap_merge_and_migrate(struct global_data *data)
> +{
> +	int ret;
> +	void *old_region;
> +	void *new_region;
> +	int nr_pages = 32;

Likely you should do

const unsigned int nr_pages = 16;

and then simply multiply it by 2 for the temporary larger mmap.

> +	long merging_pages;
> +
> +	prepare_pages(data, nr_pages);

Is it really worth moving that into a helper function?

> +
> +	if (ksm_start() < 0)
> +		return FAIL_ON_CHECK;

Why the merge here? Wouldn't we want to merge after mremap()? mremap() will
just unshare either way?

> +
> +	old_region = data->region;
> +	/*
> +	 * Mremap the second half region to the first half location (FIXED).
> +	 */

Okay, so we create a VMA and populated anonymous pages. Might be from THPs, do
we care? I guess due to the split-on-dedup, this should be ok.

Now we mremap half of it over the other half.


> +	new_region = mremap(old_region + data->mapsize / 2, data->mapsize / 2,
> +			    data->mapsize / 2, MREMAP_MAYMOVE | MREMAP_FIXED,
> +			    old_region);
> +	if (new_region == MAP_FAILED) {
> +		ksft_print_msg("mremap failed: %s\n", strerror(errno));
> +		return FAIL_ON_CHECK;
> +	}
> +	data->region = new_region;
> +	data->mapsize /= 2;	/* mapping is now half of original */

Let's avoid the use of trailing comments where possible.

> +
> +	if (ksm_start() < 0)
> +		return FAIL_ON_CHECK;
> +
> +	/* Attempt to migrate the merged KSM page */
> +	ret = try_to_move_page(data->region);
> +	if (ret != 0) {
> +		ksft_print_msg("migration of KSM page after mremap failed\n");
> +		return FAIL_ON_CHECK;
> +	}
> +
> +	/* Ensure ksmd scan two turns at least to update ksm counters */
> +	if (ksm_start() < 0)
> +		return FAIL_ON_CHECK;

Why do we have to re-run KSM? ksm_start() already performed two runs. To remove
any leftover rmap entries?

But fundamentally, wouldn't it better to query before+after the
try_to_move_page() the PFN of all pages, to (a) make sure that they are the same
before (all merged, if not -> skip) and (b) make sure that all were migrated (if
still old PFN -> not migrated -> BUG).

> +
> +	merging_pages = ksm_get_self_merging_pages();
> +	printf("merging_pages:%ld\n", merging_pages);
> +	if (merging_pages != nr_pages / 2) {


[...]

> +TEST_F(migrate, ksm_and_mremap)
> +{
> +	struct global_data *data = &self->data;
> +	int ret;
> +
> +	/* Skip if KSM is not available */
> +	if (ksm_stop() < 0)
> +		SKIP(return, "accessing \"/sys/kernel/mm/ksm/run\" failed");
> +	if (ksm_get_full_scans() < 0)
> +		SKIP(return, "accessing \"/sys/kernel/mm/ksm/full_scan\" failed");
> +
> +	ret = prctl(PR_SET_MEMORY_MERGE, 1, 0, 0, 0);
> +	if (ret < 0 && errno == EINVAL)
> +		SKIP(return, "PR_SET_MEMORY_MERGE not supported");
> +	else if (ret)
> +		ksft_exit_fail_perror("PR_SET_MEMORY_MERGE=1 failed");

Why are we not simply using MADV_MERGEABLE instead of the prctl?

-- 
Cheers,

David

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v8 1/3] ksm: add linear_page_index into ksm_rmap_item
  2026-06-09  7:44   ` David Hildenbrand (Arm)
@ 2026-06-09 11:45     ` xu.xin16
  0 siblings, 0 replies; 9+ messages in thread
From: xu.xin16 @ 2026-06-09 11:45 UTC (permalink / raw)
  To: david
  Cc: akpm, chengming.zhou, hughd, wang.yaxin, linux-mm, linux-kernel, ljs

>> +/*
>> + * break_cow: actively break COW, replacing the KSM page by a fresh anonymous
>> + * page. This is called when rmap_item has not yet become stable, but page
>> + * has been merged.
>> + */
>>  static void break_cow(struct ksm_rmap_item *rmap_item)
>>  {
>>  	struct mm_struct *mm = rmap_item->mm;
>> @@ -787,6 +798,11 @@ static void break_cow(struct ksm_rmap_item *rmap_item)
>>  	 * to undo, we also need to drop a reference to the anon_vma.
>>  	 */
>>  	put_anon_vma(rmap_item->anon_vma);
>> +	/*
>> +	 * Reset linear_page_index that might overlay age-related
>> +	 * information. (it's still unstable node)
>> +	 */
>> +	rmap_item->linear_page_index = 0;
>
>Sashiko comments that, on 32bit, it is not overlaying age-related information.
>So setting it to 0 won't clear the age.
>
>Which is what we document with the "might".
>
>On 32bit, it simply behaves the way it was before (no reset of age-related
>information here).
>
>We could move oldchecksum below remaining_skips to clear age-related information
>consistently. Not sure whether that is really worth it.

Yes, On 32-bit systems, although we do not clean the age, we also did not overwrite it.
So the behavior is the same as it used to be.

>> @@ -1598,8 +1618,16 @@ static int try_to_merge_with_ksm_page(struct ksm_rmap_item *rmap_item,
>>  	/* Unstable nid is in union with stable anon_vma: remove first */
>>  	remove_rmap_item_from_tree(rmap_item);
>> 
>> -	/* Must get reference to anon_vma while still holding mmap_lock */
>> +	/*
>> +	 * Must get reference to anon_vma while still holding mmap_lock.
>
>I think this sentence should go, and instead ...
>
>> +	 * Must can only reference the VMA while still holding the mmap
>
>This one should become:
>
>"We can consider the VMA only while still holding the mmap lock, so ...

Oh, my bad. I'll correct it right away.

> +	 * lock, so reference the anon_vma and calculate the linear page
> +	 * index early, before stable_tree_append(). If anything goes
> +	 * wrong that prevents the rmap_item from being added to the
> +	 * stable_tree, break_cow() will clean it up.
> +	 */
>  	rmap_item->anon_vma = vma->anon_vma;
> +	rmap_item->linear_page_index = linear_page_index(vma, rmap_item->address);
>  	get_anon_vma(vma->anon_vma);
>  out:
>  	mmap_read_unlock(mm);
> @@ -2458,6 +2486,13 @@ static bool should_skip_rmap_item(struct folio *folio,
>  	if (folio_test_ksm(folio))
>  		return false;

> Thinking about the overlay once more, I'm trying to assess what it means when we
> clear rmap_item->checksum. We'd do that now in:
> 
> (a) remove_node_from_stable_tree(): We had a stable node -> KSM page, but
> something changed.
> 
> (b) remove_rmap_item_from_tree(): Same as (a)
> 
> (c) break_cow(): we have to unshare, either because insertion into the stable
>     tree failed, or because we would have only a single PTE mapping the KSM
>     folio after a failed merge.
> 
> For (c), I guess if we'd have to, we could remember the checksum while
> processing the rmap_item.
> 
> Clearing rmap_item->checksum implies that cmp_and_merge_page() would refuse to
> merge one round.
> 
> Having rmap_item->checksum cleared is just like allocating a fresh rmap_item. So
> it will fix itself up during the next scan.
> 
> So my best guess is that this is alright.

You've thought this through carefully. For case c, clearing rmap_item->checksum will
indeed cause cmp_and_merge_page to reject the first merge. The original intent of
rejecting the merge is because the page changes too frequently (somewhat similar to
LRU), and this rationale still hold. Making this page scanned in your case (c) one
more time is acceptable.

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v8 3/3] ksm: add mremap selftests for ksm_rmap_walk
  2026-06-09  9:18   ` David Hildenbrand (Arm)
@ 2026-06-10  6:21     ` xu.xin16
  0 siblings, 0 replies; 9+ messages in thread
From: xu.xin16 @ 2026-06-10  6:21 UTC (permalink / raw)
  To: david
  Cc: akpm, chengming.zhou, hughd, wang.yaxin, linux-mm, linux-kernel, ljs

> > +static void prepare_pages(struct global_data *data, int nr_pages)
> > +{
> > +	/* Allocate exactly pages for the test */
> > +	data->mapsize = nr_pages * getpagesize();
> > +	data->region = mmap(NULL, data->mapsize, PROT_READ | PROT_WRITE,
> > +			    MAP_PRIVATE | MAP_ANON, -1, 0);
> > +	if (data->region == MAP_FAILED)
> > +		ksft_exit_fail_perror("mmap failed");
> > +
> > +	/* Fill all pages with identical content to encourage KSM merging */
> > +	memset(data->region, 0x77, data->mapsize);
> > +}
> > +
> > +static int mremap_merge_and_migrate(struct global_data *data)
> > +{
> > +	int ret;
> > +	void *old_region;
> > +	void *new_region;
> > +	int nr_pages = 32;
> 
> Likely you should do
> 
> const unsigned int nr_pages = 16;
> 
> and then simply multiply it by 2 for the temporary larger mmap.

Ok, thanks.

> 
> > +	long merging_pages;
> > +
> > +	prepare_pages(data, nr_pages);
> 
> Is it really worth moving that into a helper function?
> 
> > +
> > +	if (ksm_start() < 0)
> > +		return FAIL_ON_CHECK;
> 
> Why the merge here? Wouldn't we want to merge after mremap()? mremap() will
> just unshare either way?

It maight be redundant, which was introduced from the last version v7 to calculate
the base KSM counters.

> 
> > +
> > +	old_region = data->region;
> > +	/*
> > +	 * Mremap the second half region to the first half location (FIXED).
> > +	 */
> 
> Okay, so we create a VMA and populated anonymous pages. Might be from THPs, do
> we care? I guess due to the split-on-dedup, this should be ok.
> 
> Now we mremap half of it over the other half.
> 
> 
> > +	new_region = mremap(old_region + data->mapsize / 2, data->mapsize / 2,
> > +			    data->mapsize / 2, MREMAP_MAYMOVE | MREMAP_FIXED,
> > +			    old_region);
> > +	if (new_region == MAP_FAILED) {
> > +		ksft_print_msg("mremap failed: %s\n", strerror(errno));
> > +		return FAIL_ON_CHECK;
> > +	}
> > +	data->region = new_region;
> > +	data->mapsize /= 2;	/* mapping is now half of original */
> 
> Let's avoid the use of trailing comments where possible.

OK, thanks.
> 
> > +
> > +	if (ksm_start() < 0)
> > +		return FAIL_ON_CHECK;
> > +
> > +	/* Attempt to migrate the merged KSM page */
> > +	ret = try_to_move_page(data->region);
> > +	if (ret != 0) {
> > +		ksft_print_msg("migration of KSM page after mremap failed\n");
> > +		return FAIL_ON_CHECK;
> > +	}
> > +
> > +	/* Ensure ksmd scan two turns at least to update ksm counters */
> > +	if (ksm_start() < 0)
> > +		return FAIL_ON_CHECK;
> 
> Why do we have to re-run KSM? ksm_start() already performed two runs. To remove
> any leftover rmap entries?

The second run-ksm is necessary since we want to check if the counter
ksm_merging_pages is correct after mremap.

> 
> But fundamentally, wouldn't it better to query before+after the
> try_to_move_page() the PFN of all pages, to (a) make sure that they are the same
> before (all merged, if not -> skip) and (b) make sure that all were migrated (if
> still old PFN -> not migrated -> BUG).

That makes sense.

> 
> > +
> > +	merging_pages = ksm_get_self_merging_pages();
> > +	printf("merging_pages:%ld\n", merging_pages);
> > +	if (merging_pages != nr_pages / 2) {
> 
> 
> [...]
> 
> > +TEST_F(migrate, ksm_and_mremap)
> > +{
> > +	struct global_data *data = &self->data;
> > +	int ret;
> > +
> > +	/* Skip if KSM is not available */
> > +	if (ksm_stop() < 0)
> > +		SKIP(return, "accessing \"/sys/kernel/mm/ksm/run\" failed");
> > +	if (ksm_get_full_scans() < 0)
> > +		SKIP(return, "accessing \"/sys/kernel/mm/ksm/full_scan\" failed");
> > +
> > +	ret = prctl(PR_SET_MEMORY_MERGE, 1, 0, 0, 0);
> > +	if (ret < 0 && errno == EINVAL)
> > +		SKIP(return, "PR_SET_MEMORY_MERGE not supported");
> > +	else if (ret)
> > +		ksft_exit_fail_perror("PR_SET_MEMORY_MERGE=1 failed");
> 
> Why are we not simply using MADV_MERGEABLE instead of the prctl?

No special reason here, I can switch to use MADV_MERGEABLE instead of the prctl if you think it's better.

^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2026-06-10  6:21 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-06-09  4:39 [PATCH v8 0/3] KSM: performance optimizations for rmap_walk_ksm xu.xin16
2026-06-09  4:40 ` [PATCH v8 1/3] ksm: add linear_page_index into ksm_rmap_item xu.xin16
2026-06-09  7:44   ` David Hildenbrand (Arm)
2026-06-09 11:45     ` xu.xin16
2026-06-09  4:47 ` [PATCH v8 2/3] ksm: Optimize rmap_walk_ksm by passing a suitable page index xu.xin16
2026-06-09  8:13   ` David Hildenbrand (Arm)
2026-06-09  4:47 ` [PATCH v8 3/3] ksm: add mremap selftests for ksm_rmap_walk xu.xin16
2026-06-09  9:18   ` David Hildenbrand (Arm)
2026-06-10  6:21     ` xu.xin16

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®