* [PATCH 2/4] ksm: skip rmap_item allocation for non-zero pages under only_merge_zero_pages
2026-11-10 7:44 ` [PATCH 1/4] ksm: add only_merge_zero_pages to merge only empty anonymous pages Xu Xin (vivo)
@ 2026-11-10 7:44 ` Xu Xin (vivo)
2026-11-10 7:44 ` [PATCH 3/4] selftests/ksm: consolidate ksm_stat readers into ksm_get_self_stat() Xu Xin (vivo)
2026-11-10 7:44 ` [PATCH 4/4] selftests/ksm: add test for only_merge_zero_pages Xu Xin (vivo)
2 siblings, 0 replies; 5+ messages in thread
From: Xu Xin (vivo) @ 2026-11-10 7:44 UTC (permalink / raw)
To: xu.xin
Cc: akpm, chengming.zhou, corbet, david, link, linux-doc,
linux-kernel, linux-kselftest, linux-mm, ljs, odys.yuan, rppt,
surenb, tujinjiang, vbabka, xiao.wang
When only_merge_zero_pages is enabled, ksmd only ever merges empty
(zeroed) anonymous pages with the kernel zero page. A page that carries
data can never be merged, yet the current code still allocates an
rmap_item for it in scan_get_next_rmap_item() before cmp_and_merge_page()
decides to skip it.
On workloads with a large amount of non-mergeable anonymous memory this
is a real cost: every scanned non-zero page pays for an rmap_item
allocation, and the rmap_items accumulate in the per-mm pending list.
For example, a MySQL instance with hundreds of gigabytes of anonymous
memory (mostly buffer pool pages that cannot be merged) ends up with
ksmd creating rmap_item metadata for each candidate page; the metadata
overhead can wipe out any gain from the zero-page merging and even turn
the net benefit negative.
Add should_skip_non_zero_page(), called in scan_get_next_rmap_item() before
get_next_rmap_item(), which, when only_merge_zero_pages is enabled,
checks whether the candidate page is all zeros. A non-zero page is
skipped entirely: no rmap_item is allocated for it, and any stale
pending rmap_items below it are freed (as get_next_rmap_item() would).
The page is reported back to ksm_do_scan() as ERR_PTR(-EAGAIN), which
counts it as scanned and continues, so the pages_to_scan budget is
preserved.
The all-zero check (pages_identical() against the kernel zero page) reads
the whole page. For a non-zero page this is a net win: the extra page read
is cheaper than the rmap_item allocation and the stable/unstable tree
operations it would otherwise trigger.
A page that is zero now but is written before cmp_and_merge_page() runs
is still handled correctly, because the checksum-based decision in
cmp_and_merge_page() (added by the previous patch) is the authoritative
one.
Signed-off-by: Xu Xin (vivo) <xu.xin@linux.dev>
Co-developed-by: Jinjiang Tu <tujinjiang@huawei.com>
Signed-off-by: Jinjiang Tu <tujinjiang@huawei.com>
---
mm/ksm.c | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 82 insertions(+)
diff --git a/mm/ksm.c b/mm/ksm.c
index 49f3728a3d03..e641c37ee096 100644
--- a/mm/ksm.c
+++ b/mm/ksm.c
@@ -2535,6 +2535,69 @@ static bool should_skip_rmap_item(struct folio *folio,
return true;
}
+/*
+ * Determines whether a candidate page should be skipped entirely when
+ * only_merge_zero_pages is enabled, before an rmap_item is allocated for
+ * it.
+ *
+ * A page that is not all zeros carries data and can never be merged with
+ * the kernel zero page, so there is no point in allocating an rmap_item
+ * for it. Skipping it here avoids the rmap_item metadata cost that full
+ * KSM would otherwise pay for every non-mergeable page.
+ *
+ * @rmap_list: head of the per-mm pending rmap_item list
+ * @addr: address of the candidate page
+ * @folio: folio containing the candidate page
+ * @page: the candidate page
+ *
+ * Returns true if the page should be skipped (and any stale pending
+ * rmap_items below @addr are freed), false if it should be processed.
+ */
+static bool should_skip_non_zero_page(struct ksm_rmap_item **rmap_list,
+ unsigned long addr, struct folio *folio,
+ struct page *page)
+{
+ struct ksm_rmap_item *rmap_item;
+
+ if (!ksm_only_merge_zero_pages)
+ return false;
+
+ /*
+ * Pages already merged by KSM are handled by cmp_and_merge_page();
+ * do not skip them here.
+ */
+ if (folio_test_ksm(folio))
+ return false;
+
+ /*
+ * An all-zero page is exactly what we want to merge with the kernel
+ * zero page; process it.
+ */
+ if (pages_identical(page, ZERO_PAGE(0)))
+ return false;
+
+ /*
+ * The page carries data: free any pending rmap_items that are below
+ * it (they are stale, as get_next_rmap_item() would), and skip it so
+ * no rmap_item is allocated for it.
+ *
+ * Compare the masked address, unlike get_next_rmap_item() which
+ * compares the raw rmap_item->address (which may carry the
+ * UNSTABLE/STABLE_FLAG bits). That is intentional: a pending
+ * rmap_item at the same address as a non-zero page is stale and must
+ * be freed here, not returned for processing.
+ */
+ while (*rmap_list) {
+ rmap_item = *rmap_list;
+ if ((rmap_item->address & PAGE_MASK) > addr)
+ break;
+ *rmap_list = rmap_item->rmap_list;
+ remove_rmap_item_from_tree(rmap_item);
+ free_rmap_item(rmap_item);
+ }
+ return true;
+}
+
struct ksm_next_page_arg {
struct folio *folio;
struct page *page;
@@ -2737,6 +2800,16 @@ static struct ksm_rmap_item *scan_get_next_rmap_item(struct page **page)
if (tmp_page) {
flush_anon_page(vma, tmp_page, ksm_scan.address);
flush_dcache_page(tmp_page);
+
+ if (should_skip_non_zero_page(ksm_scan.rmap_list,
+ ksm_scan.address, folio,
+ tmp_page)) {
+ folio_put(folio);
+ ksm_scan.address += PAGE_SIZE;
+ mmap_read_unlock(mm);
+ return ERR_PTR(-EAGAIN);
+ }
+
rmap_item = get_next_rmap_item(mm_slot,
ksm_scan.rmap_list, ksm_scan.address);
if (rmap_item) {
@@ -2839,6 +2912,15 @@ static void ksm_do_scan(unsigned int scan_npages)
rmap_item = scan_get_next_rmap_item(&page);
if (!rmap_item)
return;
+ if (rmap_item == ERR_PTR(-EAGAIN)) {
+ /*
+ * A non-zero page was skipped under
+ * only_merge_zero_pages: it was examined but not
+ * merged, so count it as scanned and keep going.
+ */
+ ksm_pages_scanned++;
+ continue;
+ }
cmp_and_merge_page(page, rmap_item);
put_page(page);
ksm_pages_scanned++;
--
2.34.1
^ permalink raw reply [flat|nested] 5+ messages in thread* [PATCH 3/4] selftests/ksm: consolidate ksm_stat readers into ksm_get_self_stat()
2026-11-10 7:44 ` [PATCH 1/4] ksm: add only_merge_zero_pages to merge only empty anonymous pages Xu Xin (vivo)
2026-11-10 7:44 ` [PATCH 2/4] ksm: skip rmap_item allocation for non-zero pages under only_merge_zero_pages Xu Xin (vivo)
@ 2026-11-10 7:44 ` Xu Xin (vivo)
2026-11-10 7:44 ` [PATCH 4/4] selftests/ksm: add test for only_merge_zero_pages Xu Xin (vivo)
2 siblings, 0 replies; 5+ messages in thread
From: Xu Xin (vivo) @ 2026-11-10 7:44 UTC (permalink / raw)
To: xu.xin
Cc: akpm, chengming.zhou, corbet, david, link, linux-doc,
linux-kernel, linux-kselftest, linux-mm, ljs, odys.yuan, rppt,
surenb, tujinjiang, vbabka, xiao.wang
ksm_get_self_zero_pages() and ksm_get_self_merging_pages() both read
/proc/self/ksm_stat and differ only in the key they look up. Replace
them with a single ksm_get_self_stat(key) helper in vm_util.c, which
reads one value from /proc/self/ksm_stat by key name and accepts only
the keys used by the tests.
This is a pure refactor with no functional change; it prepares for a
new test that also needs to read ksm_rmap_items from /proc/self/ksm_stat.
No functional change intended.
Signed-off-by: Xu Xin (vivo) <xu.xin@linux.dev>
---
.../selftests/mm/ksm_functional_tests.c | 18 ++++-----
tools/testing/selftests/mm/vm_util.c | 40 ++++++-------------
tools/testing/selftests/mm/vm_util.h | 3 +-
3 files changed, 22 insertions(+), 39 deletions(-)
diff --git a/tools/testing/selftests/mm/ksm_functional_tests.c b/tools/testing/selftests/mm/ksm_functional_tests.c
index 135191f99962..2ffb97ae3120 100644
--- a/tools/testing/selftests/mm/ksm_functional_tests.c
+++ b/tools/testing/selftests/mm/ksm_functional_tests.c
@@ -85,7 +85,7 @@ static char *__mmap_and_merge_range(char val, unsigned long size, int prot,
return err_map;
}
- if (ksm_get_self_merging_pages() > 0) {
+ if (ksm_get_self_stat("ksm_merging_pages") > 0) {
ksft_print_msg("Still pages merged\n");
return err_map;
}
@@ -144,7 +144,7 @@ static char *__mmap_and_merge_range(char val, unsigned long size, int prot,
* Check if anything was merged at all. Ignore the zero page that is
* accounted differently (depending on kernel support).
*/
- if (val && !ksm_get_self_merging_pages()) {
+ if (val && !ksm_get_self_stat("ksm_merging_pages")) {
ksft_print_msg("No pages got merged\n");
goto unmap;
}
@@ -204,7 +204,7 @@ static void test_unmerge_zero_pages(void)
ksft_print_msg("[RUN] %s\n", __func__);
- if (ksm_get_self_zero_pages() < 0) {
+ if (ksm_get_self_stat("ksm_zero_pages") < 0) {
ksft_test_result_skip("accessing \"/proc/self/ksm_stat\" failed\n");
return;
}
@@ -221,7 +221,7 @@ static void test_unmerge_zero_pages(void)
/* Check if ksm_zero_pages is updated correctly after KSM merging */
pages_expected = size / pagesize;
- if (pages_expected != ksm_get_self_zero_pages()) {
+ if (pages_expected != ksm_get_self_stat("ksm_zero_pages")) {
ksft_test_result_fail("'ksm_zero_pages' updated after merging\n");
goto unmap;
}
@@ -234,7 +234,7 @@ static void test_unmerge_zero_pages(void)
/* Check if ksm_zero_pages is updated correctly after unmerging */
pages_expected /= 2;
- if (pages_expected != ksm_get_self_zero_pages()) {
+ if (pages_expected != ksm_get_self_stat("ksm_zero_pages")) {
ksft_test_result_fail("'ksm_zero_pages' updated after unmerging\n");
goto unmap;
}
@@ -244,7 +244,7 @@ static void test_unmerge_zero_pages(void)
*((unsigned int *)&map[offs]) = offs;
/* Now we should have no zeropages remaining. */
- if (ksm_get_self_zero_pages()) {
+ if (ksm_get_self_stat("ksm_zero_pages")) {
ksft_test_result_fail("'ksm_zero_pages' updated after write fault\n");
goto unmap;
}
@@ -671,7 +671,7 @@ static void test_fork_ksm_merging_page_count(void)
child_pid = fork();
if (!child_pid) {
init_global_file_handles();
- exit(ksm_get_self_merging_pages());
+ exit(ksm_get_self_stat("ksm_merging_pages"));
} else if (child_pid < 0) {
ksft_test_result_fail("fork() failed\n");
goto unmap;
@@ -771,7 +771,7 @@ static void test_merge_aged_pairs(void)
goto unmap;
}
- ksft_test_result(ksm_get_self_merging_pages() == 2 * pairs,
+ ksft_test_result(ksm_get_self_stat("ksm_merging_pages") == 2 * pairs,
"Pages aged by smart scan got merged\n");
unmap:
ksm_stop();
@@ -793,7 +793,7 @@ static void init_global_file_handles(void)
pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
if (pagemap_fd < 0)
ksft_exit_skip("open(\"/proc/self/pagemap\") failed\n");
- if (ksm_get_self_merging_pages() < 0)
+ if (ksm_get_self_stat("ksm_merging_pages") < 0)
ksft_exit_skip("accessing \"/proc/self/ksm_merging_pages\") failed\n");
pages_to_scan_fd = open("/sys/kernel/mm/ksm/pages_to_scan", O_RDWR);
diff --git a/tools/testing/selftests/mm/vm_util.c b/tools/testing/selftests/mm/vm_util.c
index f26f9e4bfdbe..8f1ee1f81e36 100644
--- a/tools/testing/selftests/mm/vm_util.c
+++ b/tools/testing/selftests/mm/vm_util.c
@@ -904,14 +904,20 @@ bool detect_huge_zeropage(void)
return enabled;
}
-long ksm_get_self_zero_pages(void)
+long ksm_get_self_stat(const char *key)
{
int proc_self_ksm_stat_fd;
char buf[200];
- char *substr_ksm_zero;
+ char *substr;
size_t value_pos;
ssize_t read_size;
+ /* Only the keys actually used by the tests are accepted. */
+ if (strcmp(key, "ksm_rmap_items") &&
+ strcmp(key, "ksm_zero_pages") &&
+ strcmp(key, "ksm_merging_pages"))
+ return -EINVAL;
+
proc_self_ksm_stat_fd = open("/proc/self/ksm_stat", O_RDONLY);
if (proc_self_ksm_stat_fd < 0)
return -errno;
@@ -925,34 +931,12 @@ long ksm_get_self_zero_pages(void)
buf[read_size] = 0;
- substr_ksm_zero = strstr(buf, "ksm_zero_pages");
- if (!substr_ksm_zero)
+ substr = strstr(buf, key);
+ if (!substr)
return 0;
- value_pos = strcspn(substr_ksm_zero, "0123456789");
- return strtol(substr_ksm_zero + value_pos, NULL, 10);
-}
-
-long ksm_get_self_merging_pages(void)
-{
- int proc_self_ksm_merging_pages_fd;
- char buf[10];
- ssize_t ret;
-
- proc_self_ksm_merging_pages_fd = open("/proc/self/ksm_merging_pages",
- O_RDONLY);
- if (proc_self_ksm_merging_pages_fd < 0)
- return -errno;
-
- ret = pread(proc_self_ksm_merging_pages_fd, buf, sizeof(buf) - 1, 0);
- close(proc_self_ksm_merging_pages_fd);
- if (ret < 0)
- return -errno;
- if (!ret)
- return -EIO;
- buf[ret] = 0;
-
- return strtol(buf, NULL, 10);
+ value_pos = strcspn(substr, "0123456789");
+ return strtol(substr + value_pos, NULL, 10);
}
long ksm_get_full_scans(void)
diff --git a/tools/testing/selftests/mm/vm_util.h b/tools/testing/selftests/mm/vm_util.h
index ab1325dc6580..2b25a9eab30c 100644
--- a/tools/testing/selftests/mm/vm_util.h
+++ b/tools/testing/selftests/mm/vm_util.h
@@ -159,8 +159,7 @@ static inline int sz2ord(size_t size, size_t pagesize)
void *sys_mremap(void *old_address, unsigned long old_size,
unsigned long new_size, int flags, void *new_address);
-long ksm_get_self_zero_pages(void);
-long ksm_get_self_merging_pages(void);
+long ksm_get_self_stat(const char *key);
long ksm_get_full_scans(void);
int ksm_wait_full_scans(long nr);
int ksm_get_smart_scan(void);
--
2.34.1
^ permalink raw reply [flat|nested] 5+ messages in thread* [PATCH 4/4] selftests/ksm: add test for only_merge_zero_pages
2026-11-10 7:44 ` [PATCH 1/4] ksm: add only_merge_zero_pages to merge only empty anonymous pages Xu Xin (vivo)
2026-11-10 7:44 ` [PATCH 2/4] ksm: skip rmap_item allocation for non-zero pages under only_merge_zero_pages Xu Xin (vivo)
2026-11-10 7:44 ` [PATCH 3/4] selftests/ksm: consolidate ksm_stat readers into ksm_get_self_stat() Xu Xin (vivo)
@ 2026-11-10 7:44 ` Xu Xin (vivo)
2 siblings, 0 replies; 5+ messages in thread
From: Xu Xin (vivo) @ 2026-11-10 7:44 UTC (permalink / raw)
To: xu.xin
Cc: akpm, chengming.zhou, corbet, david, link, linux-doc,
linux-kernel, linux-kselftest, linux-mm, ljs, odys.yuan, rppt,
surenb, tujinjiang, vbabka, xiao.wang
Add a functional test that enables use_zero_pages and
only_merge_zero_pages, maps a 2 MiB region with the first half filled
with non-zero data and the second half with zeroes, and verifies that
after KSM scanning reaches steady state:
- ksm_zero_pages equals the zero half (merged with the kernel zero
page)
- ksm_merging_pages is 0 (non-zero pages never reach the stable tree)
- ksm_rmap_items is 0 (non-zero pages are skipped before rmap_item
allocation, and the zero pages' rmap_items are freed once a scan
finds the merged pages are no longer anonymous)
The test skips if the only_merge_zero_pages attribute is absent.
Signed-off-by: Xu Xin (vivo) <xu.xin@linux.dev>
---
.../selftests/mm/ksm_functional_tests.c | 117 ++++++++++++++++++
1 file changed, 117 insertions(+)
diff --git a/tools/testing/selftests/mm/ksm_functional_tests.c b/tools/testing/selftests/mm/ksm_functional_tests.c
index 2ffb97ae3120..764ead3425cb 100644
--- a/tools/testing/selftests/mm/ksm_functional_tests.c
+++ b/tools/testing/selftests/mm/ksm_functional_tests.c
@@ -257,6 +257,122 @@ static void test_unmerge_zero_pages(void)
munmap(map, size);
}
+/*
+ * Enable or disable /sys/kernel/mm/ksm/only_merge_zero_pages. Returns 0 on
+ * success, -errno on failure.
+ */
+static int ksm_set_only_merge_zero_pages(int enable)
+{
+ int fd;
+ ssize_t ret;
+
+ fd = open("/sys/kernel/mm/ksm/only_merge_zero_pages", O_RDWR);
+ if (fd < 0)
+ return -errno;
+
+ ret = write(fd, enable ? "1" : "0", 1);
+ close(fd);
+ return ret == 1 ? 0 : -errno;
+}
+
+static void test_only_merge_zero_pages(void)
+{
+ const unsigned int size = 2 * MiB;
+ const unsigned int half = size / 2;
+ const unsigned int half_pages = half / pagesize;
+ char *map;
+ int i;
+ long zero_pages, merging_pages, rmap_items;
+
+ ksft_print_msg("[RUN] %s\n", __func__);
+
+ if (ksm_use_zero_pages() < 0) {
+ ksft_test_result_skip("write /sys/kernel/mm/ksm/use_zero_pages failed\n");
+ return;
+ }
+ if (ksm_set_only_merge_zero_pages(1) < 0) {
+ ksft_test_result_skip("write /sys/kernel/mm/ksm/only_merge_zero_pages failed\n");
+ return;
+ }
+
+ /* Stabilize accounting by disabling KSM completely. */
+ if (ksm_stop() < 0) {
+ ksft_test_result_fail("Disabling (unmerging) KSM failed\n");
+ goto restore;
+ }
+
+ map = mmap(NULL, size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANON, -1, 0);
+ if (map == MAP_FAILED) {
+ ksft_test_result_fail("mmap() failed\n");
+ goto restore;
+ }
+
+ /* Don't use THP. Ignore if THP are not around on a kernel. */
+ if (madvise(map, size, MADV_NOHUGEPAGE) && errno != EINVAL) {
+ ksft_test_result_fail("MADV_NOHUGEPAGE failed\n");
+ goto unmap;
+ }
+
+ /*
+ * Fill the first half with non-zero data (all 0x01) and the second
+ * half with zeroes. Under only_merge_zero_pages the zero half should
+ * be merged with the kernel zero page, while the non-zero half must be
+ * skipped entirely (no rmap_item allocated, no stable-tree insertion).
+ */
+ memset(map, 0x01, half);
+ memset(map + half, 0x00, half);
+
+ if (madvise(map, size, MADV_MERGEABLE)) {
+ ksft_test_result_fail("MADV_MERGEABLE failed\n");
+ goto unmap;
+ }
+
+ /*
+ * Run KSM until steady state. Each ksm_start() waits for two full
+ * scans: the first records the checksums, the second merges the zero
+ * half with the kernel zero page. The second ksm_start() waits for
+ * more scans so that the rmap_items of the (now merged, no longer
+ * anonymous) zero pages are freed and ksm_rmap_items settles at 0.
+ */
+ for (i = 0; i < 2; i++) {
+ if (ksm_start() < 0) {
+ ksft_test_result_fail("Running KSM failed\n");
+ goto unmap;
+ }
+ }
+
+ /*
+ * Expected results (steady state):
+ * - ksm_zero_pages: the zero half (size/2 pages) is merged with the
+ * kernel zero page.
+ * - ksm_merging_pages: 0, because the non-zero half is skipped before
+ * it can reach the stable tree, and zero pages are accounted in
+ * ksm_zero_pages, not ksm_merging_pages.
+ * - ksm_rmap_items: 0. The non-zero half is skipped before
+ * get_next_rmap_item(), so it never allocates an rmap_item; and the
+ * zero half's rmap_items are freed once a scan finds the (now
+ * merged) pages are no longer anonymous.
+ */
+ zero_pages = ksm_get_self_stat("ksm_zero_pages");
+ merging_pages = ksm_get_self_stat("ksm_merging_pages");
+ rmap_items = ksm_get_self_stat("ksm_rmap_items");
+
+ ksft_print_msg("ksm_zero_pages=%ld ksm_merging_pages=%ld ksm_rmap_items=%ld"
+ " (expected %u/0/0)\n", zero_pages, merging_pages, rmap_items,
+ half_pages);
+
+ ksft_test_result(zero_pages == half_pages && merging_pages == 0 &&
+ rmap_items == 0, "only_merge_zero_pages: zero half merged, "
+ "non-zero half skipped\n");
+
+unmap:
+ ksm_stop();
+ munmap(map, size);
+restore:
+ ksm_set_only_merge_zero_pages(0);
+}
+
static void test_unmerge_discarded(void)
{
const unsigned int size = 2 * MiB;
@@ -827,6 +943,7 @@ int main(int argc, char **argv)
test_unmerge();
test_unmerge_zero_pages();
+ test_only_merge_zero_pages();
test_unmerge_discarded();
#ifdef __NR_userfaultfd
test_unmerge_uffd_wp();
--
2.34.1
^ permalink raw reply [flat|nested] 5+ messages in thread