* [LTP] [PATCH v9 0/1] cpuset_memory_spread: rewrite as a C test using the LTP cgroup library
@ 2026-11-05 0:55 Changwei Zou via ltp
2026-11-05 0:55 ` [LTP] [PATCH v9 1/1] " Changwei Zou via ltp
0 siblings, 1 reply; 7+ messages in thread
From: Changwei Zou via ltp @ 2026-11-05 0:55 UTC (permalink / raw)
To: ltp; +Cc: Changwei Zou
The cpuset_memory_spread test (from 2009) was split between a shell script
that mounted /dev/cpuset by hand and a C helper (cpuset_mem_hog) driven over
a FIFO and SIGUSR1/SIGUSR2. It checked the cpuset.memory_spread_page policy
by comparing the global per-node FilePages counters in
/sys/devices/system/node/nodeX/meminfo, which also account for unrelated
page-cache activity, so on large or busy NUMA machines the empirical
thresholds became unreliable and the test failed spuriously.
This series rewrites it as a single C test on the new LTP test API, the LTP
cgroup library (tst_cgroup.h) and the NUMA helpers from tse_numa.h, dropping
the shell script, the FIFO/signal handshake and the manual /dev/cpuset
management.
For each case the test pins itself to the local node, sets cpuset.mems and
cpuset.memory_spread_page, faults a file-backed shared mapping and uses
tse_nodemap_count_pages() (get_mempolicy(2)) to learn the NUMA node of each
of the mapping's pages. Only the test mapping's own pages are counted, so
unrelated page-cache activity can no longer perturb the result.
cpuset.memory_spread_page only exists in the cgroup v1 cpuset controller, so
the test requires a v1 cpuset hierarchy (.needs_cgroup_ver = TST_CG_V1) and
is skipped (TCONF) on a unified (v2) hierarchy. The patch also registers
cpuset.memory_spread_page in the LTP cgroup library's cpuset file table so
it can be driven through the SAFE_CG_* helpers.
Tested on a 2-node NUMA machine with a v1 cpuset hierarchy: all six subtests
pass.
Changes in v9 (addresses Cyril's v8 review):
- Use tse_numa.h instead of the deprecated numa_helper.h: node discovery
and per-node free-memory filtering via tse_get_nodemap(), and page
accounting via tse_numa_map()/tse_numa_fault()/tse_nodemap_count_pages().
This also drops the move_pages(2) syscall (counting now uses
get_mempolicy(2)) and the write_node_cpusets() dependency; the task is
pinned to the local node with numa_run_on_node().
- struct tcase: use const char *mems / const char *expect instead of
int[2] arrays with a -1 sentinel.
- Move the tmpfs/ramfs explanation out of the struct into the top-level
test doc comment.
- Makefile: link libltpnuma (LTPLIBS = numa) instead of pulling in the
kernel test lib.
v8:
https://lore.kernel.org/ltp/20261104071908.128726-1-changwei.zou@canonical.com/
v7:
https://lore.kernel.org/ltp/20261104002400.56841-1-changwei.zou@canonical.com/
Changwei Zou (1):
cpuset_memory_spread: rewrite as a C test using the LTP cgroup library
lib/tst_cgroup.c | 1 +
runtest/controllers | 2 +-
.../kernel/controllers/cpuset/.gitignore | 2 +-
.../cpuset/cpuset_memory_spread_test/Makefile | 7 +-
.../cpuset_mem_hog.c | 126 ------
.../cpuset_memory_spread01.c | 214 +++++++++++
.../cpuset_memory_spread_testset.sh | 362 ------------------
7 files changed, 223 insertions(+), 491 deletions(-)
delete mode 100644 testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
create mode 100644 testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread01.c
delete mode 100755 testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
--
2.43.0
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply [flat|nested] 7+ messages in thread* [LTP] [PATCH v9 1/1] cpuset_memory_spread: rewrite as a C test using the LTP cgroup library
2026-11-05 0:55 [LTP] [PATCH v9 0/1] cpuset_memory_spread: rewrite as a C test using the LTP cgroup library Changwei Zou via ltp
@ 2026-11-05 0:55 ` Changwei Zou via ltp
2026-11-05 7:31 ` [LTP] " linuxtestproject.agent
2026-11-11 13:57 ` [LTP] [PATCH v9 1/1] " Petr Vorel
0 siblings, 2 replies; 7+ messages in thread
From: Changwei Zou via ltp @ 2026-11-05 0:55 UTC (permalink / raw)
To: ltp; +Cc: Changwei Zou
The cpuset_memory_spread test, written in 2009, was split between a shell
script (cpuset_memory_spread_testset.sh) that mounted /dev/cpuset by hand
and a C helper (cpuset_mem_hog) driven over a FIFO and SIGUSR1/SIGUSR2.
It checked the cpuset.memory_spread_page policy by comparing the global
per-node FilePages counters in /sys/devices/system/node/nodeX/meminfo,
which also account for unrelated page-cache activity, so on large or busy
NUMA machines the empirical thresholds became unreliable and the test
failed spuriously.
Rewrite the whole thing as a single C test built on the new LTP test API,
the LTP cgroup library (tst_cgroup.h) and the NUMA helpers from
tse_numa.h, dropping the shell script, the FIFO/signal handshake and the
manual /dev/cpuset management.
For each case the test pins itself to the local node, sets cpuset.mems
and cpuset.memory_spread_page, faults a file-backed shared mapping and
uses tse_nodemap_count_pages() (get_mempolicy(2)) to learn the NUMA node
of each of the mapping's pages. Only the test mapping's own pages are
counted, so unrelated page-cache activity can no longer perturb the
result. The distribution is then verified: the pages must land on the
expected node(s), the other nodes may hold at most UNEXPECTED_TOLERANCE
(5%) of them, and when several expected nodes are given each must hold at
least (100 - BALANCE_TOLERANCE, 80%) of its even share.
cpuset.memory_spread_page only exists in the cgroup v1 cpuset controller,
so the test requires a v1 cpuset hierarchy (.needs_cgroup_ver = TST_CG_V1)
and is skipped with TCONF when cpuset is mounted on the unified (v2)
hierarchy. Register cpuset.memory_spread_page in the LTP cgroup library's
cpuset file table so it can be driven through the SAFE_CG_* helpers.
Signed-off-by: Changwei Zou <changwei.zou@canonical.com>
Assisted-By: opencode (claude-opus-4.8)
---
lib/tst_cgroup.c | 1 +
runtest/controllers | 2 +-
.../kernel/controllers/cpuset/.gitignore | 2 +-
.../cpuset/cpuset_memory_spread_test/Makefile | 7 +-
.../cpuset_mem_hog.c | 126 ------
.../cpuset_memory_spread01.c | 214 +++++++++++
.../cpuset_memory_spread_testset.sh | 362 ------------------
7 files changed, 223 insertions(+), 491 deletions(-)
delete mode 100644 testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
create mode 100644 testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread01.c
delete mode 100755 testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
diff --git a/lib/tst_cgroup.c b/lib/tst_cgroup.c
index 7c6ecd4d2..74459138f 100644
--- a/lib/tst_cgroup.c
+++ b/lib/tst_cgroup.c
@@ -206,6 +206,7 @@ static const struct cgroup_file cpuset_ctrl_files[] = {
{ "cpuset.cpus", "cpuset.cpus", CTRL_CPUSET },
{ "cpuset.mems", "cpuset.mems", CTRL_CPUSET },
{ "cpuset.memory_migrate", "cpuset.memory_migrate", CTRL_CPUSET },
+ { "cpuset.memory_spread_page", "cpuset.memory_spread_page", CTRL_CPUSET },
{ }
};
diff --git a/runtest/controllers b/runtest/controllers
index 93c52c439..0af338369 100644
--- a/runtest/controllers
+++ b/runtest/controllers
@@ -357,7 +357,7 @@ cpuset_load_balance cpuset_load_balance_test.sh
cpuset_hotplug cpuset_hotplug_test.sh
cpuset_memory cpuset_memory_testset.sh
cpuset_memory_pressure cpuset_memory_pressure_testset.sh
-cpuset_memory_spread cpuset_memory_spread_testset.sh
+cpuset_memory_spread01 cpuset_memory_spread01
cpuset_regression_test cpuset_regression_test.sh
diff --git a/testcases/kernel/controllers/cpuset/.gitignore b/testcases/kernel/controllers/cpuset/.gitignore
index c65f2fb61..6a8d713c2 100644
--- a/testcases/kernel/controllers/cpuset/.gitignore
+++ b/testcases/kernel/controllers/cpuset/.gitignore
@@ -2,6 +2,6 @@
/cpuset_load_balance_test/cpuset_cpu_hog
/cpuset_load_balance_test/cpuset_sched_domains_check
/cpuset_memory_pressure_test/cpuset_memory_pressure
-/cpuset_memory_spread_test/cpuset_mem_hog
+/cpuset_memory_spread_test/cpuset_memory_spread01
/cpuset_memory_test/cpuset_memory_test
/cpuset_syscall_test/cpuset_syscall_test
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/Makefile b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/Makefile
index b3c76f0de..5b681f77c 100644
--- a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/Makefile
+++ b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/Makefile
@@ -22,6 +22,11 @@
top_srcdir ?= ../../../../..
+LTPLIBS = numa
+
include $(top_srcdir)/include/mk/testcases.mk
-include $(abs_srcdir)/../Makefile.inc
+
+cpuset_memory_spread01: LDLIBS += $(NUMA_LIBS)
+cpuset_memory_spread01: LTPLDLIBS = -lltpnuma
+
include $(top_srcdir)/include/mk/generic_leaf_target.mk
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
deleted file mode 100644
index 56e039eee..000000000
--- a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
+++ /dev/null
@@ -1,126 +0,0 @@
-/******************************************************************************/
-/* */
-/* Copyright (c) 2009 FUJITSU LIMITED */
-/* */
-/* This program is free software; you can redistribute it and/or modify */
-/* it under the terms of the GNU General Public License as published by */
-/* the Free Software Foundation; either version 2 of the License, or */
-/* (at your option) any later version. */
-/* */
-/* This program is distributed in the hope that it will be useful, */
-/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
-/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See */
-/* the GNU General Public License for more details. */
-/* */
-/* You should have received a copy of the GNU General Public License */
-/* along with this program; if not, write to the Free Software */
-/* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */
-/* */
-/* Author: Miao Xie <miaox@cn.fujitsu.com> */
-/* */
-/******************************************************************************/
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <unistd.h>
-#include <string.h>
-#include <ctype.h>
-#include <getopt.h>
-#include <err.h>
-#include <signal.h>
-#include <sys/types.h>
-#include <sys/stat.h>
-#include <fcntl.h>
-
-#include "../cpuset_lib/common.h"
-
-#define BUFFER_SIZE 100
-
-volatile int end;
-
-void sighandler1(UNUSED int signo)
-{
-}
-
-void sighandler2(UNUSED int signo)
-{
- end = 1;
-}
-
-int page_cache_hog(void)
-{
- int fd = -1;
- char buff[BUFFER_SIZE];
- char path[BUFFER_SIZE];
- int ret = 0;
-
- sprintf(path, "%s", "DATAFILE");
- fd = open(path, O_RDONLY);
- if (fd == -1) {
- warn("open %s failed", path);
- return -1;
- }
-
- while ((ret = read(fd, buff, sizeof(buff))) > 0) ;
- if (ret == -1)
- warn("read %s failed", path);
-
- close(fd);
- return ret;
-}
-
-int mem_hog(void)
-{
- sigset_t signalset;
- int fd;
- int ret = 0;
-
- if (sigemptyset(&signalset) < 0)
- err(1, "sigemptyset()");
- sigsuspend(&signalset);
-
- while (!end) {
- ret = page_cache_hog();
-
- fd = open("./myfifo", O_WRONLY);
- if (fd == -1)
- err(1, "open fifo failed");
-
- if (ret) {
- if (write(fd, "0", 1) == -1)
- warn("write fifo failed.");
- } else {
- if (write(fd, "1", 1) == -1)
- warn("write fifo failed.");
- }
-
- close(fd);
-
- sigsuspend(&signalset);
- }
-
- return ret;
-}
-
-int main(void)
-{
- struct sigaction sa1, sa2;
-
- sa1.sa_handler = sighandler1;
- if (sigemptyset(&sa1.sa_mask) < 0)
- err(1, "sigemptyset()");
-
- sa1.sa_flags = 0;
- if (sigaction(SIGUSR1, &sa1, NULL) < 0)
- err(1, "sigaction()");
-
- sa2.sa_handler = sighandler2;
- if (sigemptyset(&sa2.sa_mask) < 0)
- err(1, "sigemptyset()");
-
- sa2.sa_flags = 0;
- if (sigaction(SIGUSR2, &sa2, NULL) < 0)
- err(1, "sigaction()");
-
- return mem_hog();
-}
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread01.c b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread01.c
new file mode 100644
index 000000000..0edaaebb0
--- /dev/null
+++ b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread01.c
@@ -0,0 +1,214 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2009 FUJITSU LIMITED
+ * Author: Miao Xie <miaox@cn.fujitsu.com>
+ *
+ * Rewritten into a single C test using the LTP cgroup library.
+ */
+
+/*\
+ * Verify the ``cpuset.memory_spread_page`` control file.
+ *
+ * A task's file-backed page cache is normally allocated on the node the task
+ * runs on (local allocation). When ``cpuset.memory_spread_page`` is enabled,
+ * the page cache of a task in the cpuset is instead spread evenly over all
+ * the memory nodes allowed by ``cpuset.mems``.
+ *
+ * For every case the test pins itself to the local node, sets ``cpuset.mems``
+ * and ``cpuset.memory_spread_page``, then faults a file-backed shared mapping
+ * and uses :manpage:`get_mempolicy(2)` (through the tse_numa helpers) to learn
+ * the NUMA node each of the mapping's pages resides on. Because only the test
+ * mapping's own pages are counted, unrelated page-cache activity on the system
+ * cannot perturb the result. The resulting distribution is verified:
+ *
+ * - with spreading off the pages must land on the expected node (the local
+ * node, or the only node allowed by ``cpuset.mems``);
+ * - with spreading on over several nodes the pages must be spread evenly.
+ *
+ * ``cpuset.memory_spread_page`` exists only in the cgroup v1 cpuset
+ * controller, so the test requires a v1 cpuset hierarchy and is skipped
+ * (TCONF) when cpuset is mounted on the unified (v2) hierarchy.
+ *
+ * The test is skipped on tmpfs/ramfs, where the mapping's pages are the
+ * backing store itself rather than ordinary page cache, so their placement
+ * is not governed by this knob.
+ */
+
+#include "config.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+
+#ifdef HAVE_NUMA_H
+# include <numa.h>
+#endif
+
+#include "tst_test.h"
+#include "tst_cgroup.h"
+#include "tse_numa.h"
+
+#ifdef HAVE_NUMA_V2
+
+/* Path of the file-backed mapping whose page placement we measure. */
+#define DATAFILE "numa_data_file"
+
+/* Size of the mapping. Large enough to measure an even spread reliably. */
+#define SIZE_MB 100
+#define SIZE ((size_t)SIZE_MB * 1024 * 1024)
+
+/* Each node needs enough free memory to hold the whole mapping plus slack. */
+#define MIN_FREE_KB ((SIZE_MB + 50) * 1024)
+
+/*
+ * The pages must land on the expected node(s); the other nodes may hold at
+ * most this fraction (in percent) of them.
+ */
+#define UNEXPECTED_TOLERANCE 5
+
+/*
+ * When the pages are spread across several expected nodes, the kernel spreads
+ * them evenly. Every expected node must hold at least
+ * (100 - BALANCE_TOLERANCE) percent of its even share.
+ */
+#define BALANCE_TOLERANCE 20
+
+static struct tse_nodemap *nodes;
+
+/*
+ * The task's CPUs are pinned to the local node (nodes->map[0]) for every case
+ * (see setup()); only cpuset.mems and cpuset.memory_spread_page change.
+ *
+ * mems/expect are strings of single-digit indices into nodes->map: '0' is the
+ * local node, '1' the remote one.
+ */
+static const struct tcase {
+ int spread;
+ const char *mems;
+ const char *expect;
+ const char *desc;
+} tcases[] = {
+ {0, "0", "0", "spread off, mems on the local node"},
+ {1, "0", "0", "spread on, mems on the local node"},
+ {0, "1", "1", "spread off, mems on the remote node"},
+ {1, "1", "1", "spread on, mems on the remote node"},
+ {0, "01", "0", "spread off, mems on both nodes (local allocation)"},
+ {1, "01", "01", "spread on, mems on both nodes (even spread)"},
+};
+
+static void check_result(const char *expect, const char *desc)
+{
+ unsigned int i, total = 0, expected_sum = 0, n_expect = 0, unexpected;
+
+ for (i = 0; i < nodes->cnt; i++)
+ total += nodes->counters[i];
+
+ if (!total) {
+ tst_res(TFAIL, "%s: no pages were allocated", desc);
+ return;
+ }
+
+ for (i = 0; expect[i]; i++) {
+ expected_sum += nodes->counters[expect[i] - '0'];
+ n_expect++;
+ }
+
+ unexpected = total - expected_sum;
+
+ tst_res(TINFO,
+ "%s: unexpected nodes hold %.2f%% of the pages (tolerance %d%%)",
+ desc, (double)unexpected * 100 / total, UNEXPECTED_TOLERANCE);
+
+ if (unexpected * 100 > total * UNEXPECTED_TOLERANCE) {
+ tst_res(TFAIL, "%s: too many pages on unexpected node(s)", desc);
+ return;
+ }
+
+ /*
+ * Every expected node must hold at least (100 - BALANCE_TOLERANCE)% of
+ * its even share. This enforces an even spread across several expected
+ * nodes and rejects an expected node that received nothing. A single
+ * expected node always passes this check.
+ */
+ for (i = 0; expect[i]; i++) {
+ unsigned int idx = expect[i] - '0';
+
+ if (nodes->counters[idx] * n_expect * 100 <
+ expected_sum * (100 - BALANCE_TOLERANCE)) {
+ tst_res(TFAIL, "%s: node %u holds too few pages",
+ desc, nodes->map[idx]);
+ return;
+ }
+ }
+
+ tst_res(TPASS, "%s", desc);
+}
+
+static void run(unsigned int n)
+{
+ const struct tcase *tc = &tcases[n];
+ char mems[64];
+ size_t len = 0;
+ void *ptr;
+ unsigned int i;
+ int ret;
+
+ for (i = 0; tc->mems[i]; i++) {
+ ret = snprintf(mems + len, sizeof(mems) - len, "%s%u",
+ i ? "," : "", nodes->map[tc->mems[i] - '0']);
+ if (ret < 0 || (size_t)ret >= sizeof(mems) - len)
+ tst_brk(TBROK, "failed to build cpuset.mems string");
+ len += ret;
+ }
+
+ SAFE_CG_PRINT(tst_cg, "cpuset.mems", mems);
+ SAFE_CG_PRINTF(tst_cg, "cpuset.memory_spread_page", "%d", tc->spread);
+
+ ptr = tse_numa_map(DATAFILE, SIZE);
+ tse_numa_fault(ptr, SIZE);
+ tse_nodemap_reset_counters(nodes);
+ tse_nodemap_count_pages(nodes, ptr, SIZE);
+ tse_numa_unmap(ptr, SIZE);
+
+ check_result(tc->expect, tc->desc);
+}
+
+static void setup(void)
+{
+ nodes = tse_get_nodemap(TST_NUMA_MEM, MIN_FREE_KB);
+ if (nodes->cnt < 2)
+ tst_brk(TCONF, "requires at least 2 NUMA memory nodes");
+
+ if (!SAFE_CG_HAS(tst_cg, "cpuset.memory_spread_page"))
+ tst_brk(TCONF, "cpuset.memory_spread_page not available");
+
+ SAFE_CG_PRINTF(tst_cg, "cgroup.procs", "%d", getpid());
+
+ /* Pin the task to the local node for every case. */
+ if (numa_run_on_node(nodes->map[0]))
+ tst_brk(TCONF, "cannot run on node %u (no CPUs?)",
+ nodes->map[0]);
+}
+
+static void cleanup(void)
+{
+ if (nodes)
+ tse_nodemap_free(nodes);
+}
+
+static struct tst_test test = {
+ .needs_root = 1,
+ .needs_tmpdir = 1,
+ .skip_filesystems = (const char *const []){ "tmpfs", "ramfs", NULL },
+ .setup = setup,
+ .cleanup = cleanup,
+ .test = run,
+ .tcnt = ARRAY_SIZE(tcases),
+ .needs_cgroup_ctrls = (const char *const []){ "cpuset", NULL },
+ .needs_cgroup_ver = TST_CG_V1,
+};
+
+#else
+TST_TEST_TCONF("test requires libnuma >= 2");
+#endif
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
deleted file mode 100755
index 4c49bb8fd..000000000
--- a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
+++ /dev/null
@@ -1,362 +0,0 @@
-#!/bin/sh
-
-################################################################################
-# #
-# Copyright (c) 2009 FUJITSU LIMITED #
-# #
-# This program is free software; you can redistribute it and#or modify #
-# it under the terms of the GNU General Public License as published by #
-# the Free Software Foundation; either version 2 of the License, or #
-# (at your option) any later version. #
-# #
-# This program is distributed in the hope that it will be useful, but #
-# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY #
-# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License #
-# for more details. #
-# #
-# You should have received a copy of the GNU General Public License #
-# along with this program; if not, write to the Free Software #
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #
-# #
-# Author: Miao Xie <miaox@cn.fujitsu.com> #
-# #
-################################################################################
-
-export TCID="cpuset_memory_spread"
-export TST_TOTAL=6
-export TST_COUNT=1
-
-. cpuset_funcs.sh
-
-check
-
-exit_status=0
-nr_cpus=$NR_CPUS
-nr_mems=$N_NODES
-
-# In general, the cache hog will use more than 10000 kb slab space on the nodes
-# on which it is running. The other nodes' slab space has littler change.(less
-# than 1000 kb).
-upperlimit=10000
-
-# set lowerlimit according to pagesize
-# pagesize(bytes) | lowerlimit(kb)
-# ------------------------------------
-# 4096 | 2048
-# 16384 | 8192
-
-PAGE_SIZE=`tst_getconf PAGESIZE`
-lowerlimit=$((PAGE_SIZE * 512 / 1024))
-
-cpus_all="$(seq -s, 0 $((nr_cpus-1)))"
-mems_all="$(seq -s, 0 $((nr_mems-1)))"
-
-nodedir="/sys/devices/system/node"
-
-FIFO="./myfifo"
-
-# memsinfo is an array implementation of the form of a multi-line string
-# _0: value0
-# _1: value1
-# _2: value2
-#
-memsinfo=""
-
-# set value to memsinfo ($1 - index, $2 - value)
-set_memsinfo_val()
-{
- local nl='
-'
- # clearing existent value (if present)
- memsinfo=`echo "$memsinfo" | sed -r "/^\_$1\: /d"`
-
- if [ -z "$memsinfo" ]; then
- memsinfo="_$1: $2"
- else
- memsinfo="$memsinfo${nl}_$1: $2"
- fi
-}
-
-# get value from memsinfo ($1 - index)
-get_memsinfo_val()
-{
- local value=
- value=`echo "$memsinfo" | grep -e "^_$1: "`
- value=`echo "$value" | sed -r "s/^.*\: (.*)$/\1/"`
- echo "$value"
-}
-
-
-init_memsinfo_array()
-{
- local i=
-
- for i in `seq 0 $((nr_mems-1))`
- do
- set_memsinfo_val $i 0
- done
-}
-
-# get_meminfo <nodeid> <item>
-get_meminfo()
-{
- local nodeid="$1"
- local nodepath="$nodedir/node$nodeid"
- local nodememinfo="$nodepath/meminfo"
- local item="$2"
- local info=`cat $nodememinfo | grep $item | awk '{print $4}'`
- set_memsinfo_val $nodeid $info
-}
-
-# freemem_check
-# We need enough memory space on every node to run this test, so we must check
-# whether every node has enough free memory or not.
-# return value: 1 - Some node doesn't have enough free memory
-# 0 - Every node has enough free memory, We can do this test
-freemem_check()
-{
- local i=
-
- for i in `seq 0 $((nr_mems-1))`
- do
- get_meminfo $i "MemFree"
- done
-
- for i in `seq 0 $((nr_mems-1))`
- do
- # I think we need 100MB free memory to run test
- if [ $(get_memsinfo_val $i) -lt 100000 ]; then
- return 1
- fi
- done
-}
-
-# get_memsinfo
-get_memsinfo()
-{
- local i=
-
- for i in `seq 0 $((nr_mems-1))`
- do
- get_meminfo $i "FilePages"
- done
-}
-
-# account_meminfo <nodeId>
-account_meminfo()
-{
- local nodeId="$1"
- local tmp="$(get_memsinfo_val $nodeId)"
- get_meminfo $@ "FilePages"
- set_memsinfo_val $nodeId $(($(get_memsinfo_val $nodeId)-$tmp))
-}
-
-# account_memsinfo
-account_memsinfo()
-{
- local i=
-
- for i in `seq 0 $((nr_mems-1))`
- do
- account_meminfo $i
- done
-}
-
-
-# result_check <nodelist>
-# return 0: success
-# 1: fail
-result_check()
-{
- local nodelist="`echo $1 | sed -e 's/,/ /g'`"
- local i=
-
- for i in $nodelist
- do
- if [ $(get_memsinfo_val $i) -le $upperlimit ]; then
- return 1
- fi
- done
-
- local allnodelist="`echo $mems_all | sed -e 's/,/ /g'`"
- allnodelist=" "$allnodelist" "
- nodelist=" "$nodelist" "
-
- local othernodelist="$allnodelist"
- for i in $nodelist
- do
- othernodelist=`echo "$othernodelist" | sed -e "s/ $i / /g"`
- done
-
- for i in $othernodelist
- do
- if [ $(get_memsinfo_val $i) -gt $lowerlimit ]; then
- return 1
- fi
- done
-}
-
-# general_memory_spread_test <cpusetpath> <is_spread> <cpu_list> <node_list> \
-# <expect_nodes> <test_pid>
-# expect_nodes: we expect to use the slab or cache on which node
-general_memory_spread_test()
-{
- local cpusetpath="$CPUSET/1"
- local is_spread="$1"
- local cpu_list="$2"
- local node_list="$3"
- local expect_nodes="$4"
- local test_pid="$5"
-
- cpuset_set "$cpusetpath" "$cpu_list" "$node_list" "0" 2> $CPUSET_TMP/stderr
- if [ $? -ne 0 ]; then
- cpuset_log_error $CPUSET_TMP/stderr
- tst_resm TFAIL "set general group parameter failed."
- return 1
- fi
-
- /bin/echo "$is_spread" > "$cpusetpath/cpuset.memory_spread_page" 2> $CPUSET_TMP/stderr
- if [ $? -ne 0 ]; then
- cpuset_log_error $CPUSET_TMP/stderr
- tst_resm TFAIL "set spread value failed."
- return 1
- fi
-
- /bin/echo "$test_pid" > "$cpusetpath/tasks" 2> $CPUSET_TMP/stderr
- if [ $? -ne 0 ]; then
- cpuset_log_error $CPUSET_TMP/stderr
- tst_resm TFAIL "attach task failed."
- return 1
- fi
-
- # we'd better drop the caches before we test page cache.
- sync
- /bin/echo 3 > /proc/sys/vm/drop_caches 2> $CPUSET_TMP/stderr
- if [ $? -ne 0 ]; then
- cpuset_log_error $CPUSET_TMP/stderr
- tst_resm TFAIL "drop caches failed."
- return 1
- fi
-
- get_memsinfo
- /bin/kill -s SIGUSR1 $test_pid
- read exit_num < $FIFO
- if [ $exit_num -eq 0 ]; then
- tst_resm TFAIL "hot mem task failed."
- return 1
- fi
-
- account_memsinfo
- result_check $expect_nodes
- if [ $? -ne 0 ]; then
- tst_resm TFAIL "hog the memory on the unexpected node(FilePages_For_Nodes(KB): ${memsinfo}, Expect Nodes: $expect_nodes)."
- return 1
- fi
-}
-
-base_test()
-{
- local pid=
- local result_num=
-
- setup
- if [ $? -ne 0 ]; then
- exit_status=1
- else
- cpuset_mem_hog &
- pid=$!
- general_memory_spread_test "$@" "$pid"
- result_num=$?
- if [ $result_num -ne 0 ]; then
- exit_status=1
- fi
-
- /bin/kill -s SIGUSR2 $pid
- wait $pid
-
- cleanup
- if [ $? -ne 0 ]; then
- exit_status=1
- elif [ $result_num -eq 0 ]; then
- tst_resm TPASS "Cpuset memory spread page test succeeded."
- fi
- fi
- TST_COUNT=$(($TST_COUNT + 1))
-}
-
-# test general spread page cache in a cpuset
-test_spread_page1()
-{
- while read spread cpus nodes exp_nodes
- do
- base_test "$spread" "$cpus" "$nodes" "$exp_nodes"
- done <<- EOF
- 0 0 0 0
- 1 0 0 0
- 0 0 1 1
- 1 0 1 1
- 0 0 0,1 0
- 1 0 0,1 0,1
- EOF
- # while read spread cpus nodes exp_nodes
-}
-
-test_spread_page2()
-{
- local pid=
- local result_num=
-
- setup
- if [ $? -ne 0 ]; then
- exit_status=1
- else
- cpuset_mem_hog &
- pid=$!
- general_memory_spread_test "1" "$cpus_all" "0" "0" "$pid"
- result_num=$?
- if [ $result_num -ne 0 ]; then
- exit_status=1
- else
- general_memory_spread_test "1" "$cpus_all" "1" "1" "$pid"
- result_num=$?
- if [ $result_num -ne 0 ]; then
- exit_status=1
- fi
- fi
-
- /bin/kill -s SIGUSR2 $pid
- wait $pid
-
- cleanup
- if [ $? -ne 0 ]; then
- exit_status=1
- elif [ $result_num -eq 0 ]; then
- tst_resm TPASS "Cpuset memory spread page test succeeded."
- fi
- fi
-}
-
-init_memsinfo_array
-freemem_check
-if [ $? -ne 0 ]; then
- tst_brkm TCONF "Some node doesn't has enough free memory(100MB) to do test(MemFree_For_Nodes(KB): ${memsinfo[*]})."
-fi
-
-dd if=/dev/zero of=./DATAFILE bs=1M count=100
-if [ $? -ne 0 ]; then
- tst_brkm TFAIL "Creating DATAFILE failed."
-fi
-
-rm -f $FIFO
-mkfifo $FIFO
-if [ $? -ne 0 ]; then
- rm -f DATAFILE
- tst_brkm TFAIL "failed to mkfifo $FIFO"
-fi
-
-test_spread_page1
-test_spread_page2
-
-rm -f DATAFILE $FIFO
-
-exit $exit_status
--
2.43.0
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply related [flat|nested] 7+ messages in thread* Re: [LTP] cpuset_memory_spread: rewrite as a C test using the LTP cgroup library
2026-11-05 0:55 ` [LTP] [PATCH v9 1/1] " Changwei Zou via ltp
@ 2026-11-05 7:31 ` linuxtestproject.agent
2026-11-11 13:57 ` [LTP] [PATCH v9 1/1] " Petr Vorel
1 sibling, 0 replies; 7+ messages in thread
From: linuxtestproject.agent @ 2026-11-05 7:31 UTC (permalink / raw)
To: Changwei Zou; +Cc: ltp
Hi Changwei,
On Thu Nov 5 11:55:05 2026 +1100, Changwei Zou wrote:
> cpuset_memory_spread: rewrite as a C test using the LTP cgroup library
Verdict - Reviewed
---
Note:
The agent can sometimes produce false positives although often its
findings are genuine. If you find issues with the review, please
comment this email or ignore the suggestions.
Regards,
LTP AI Reviewer
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [LTP] [PATCH v9 1/1] cpuset_memory_spread: rewrite as a C test using the LTP cgroup library
2026-11-05 0:55 ` [LTP] [PATCH v9 1/1] " Changwei Zou via ltp
2026-11-05 7:31 ` [LTP] " linuxtestproject.agent
@ 2026-11-11 13:57 ` Petr Vorel
2026-11-12 10:00 ` Changwei Zou via ltp
1 sibling, 1 reply; 7+ messages in thread
From: Petr Vorel @ 2026-11-11 13:57 UTC (permalink / raw)
To: Changwei Zou; +Cc: ltp
Hi Changwei Zou,
...
> +static struct tst_test test = {
> + .needs_root = 1,
> + .needs_tmpdir = 1,
> + .skip_filesystems = (const char *const []){ "tmpfs", "ramfs", NULL },
> + .setup = setup,
> + .cleanup = cleanup,
> + .test = run,
> + .tcnt = ARRAY_SIZE(tcases),
> + .needs_cgroup_ctrls = (const char *const []){ "cpuset", NULL },
> + .needs_cgroup_ver = TST_CG_V1,
> +};
> +
> +#else
> +TST_TEST_TCONF("test requires libnuma >= 2");
IMHO at least this suggestion by agent in v7 was wrong. You should have kept
using:
TST_TEST_TCONF(NUMA_ERROR_MSG);
BTW on which Ubuntu version do you test which still uses v1 and have libnuma2?
Kind regards,
Petr
> +#endif
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply [flat|nested] 7+ messages in thread* Re: [LTP] [PATCH v9 1/1] cpuset_memory_spread: rewrite as a C test using the LTP cgroup library
2026-11-11 13:57 ` [LTP] [PATCH v9 1/1] " Petr Vorel
@ 2026-11-12 10:00 ` Changwei Zou via ltp
0 siblings, 0 replies; 7+ messages in thread
From: Changwei Zou via ltp @ 2026-11-12 10:00 UTC (permalink / raw)
To: pvorel; +Cc: Changwei Zou, ltp
Hi Petr,
Thanks for the review.
> +#else
> +TST_TEST_TCONF("test requires libnuma >= 2");
> IMHO at least this suggestion by agent in v7 was wrong. You should have kept
> using:
> TST_TEST_TCONF(NUMA_ERROR_MSG);
Agreed, that was a mistake on my side. I've restored
TST_TEST_TCONF(NUMA_ERROR_MSG);
in v10.
While at it I also dropped ".needs_cgroup_ver = TST_CG_V1". The
SAFE_CG_HAS() check on cpuset.memory_spread_page in setup() already skips
the test with TCONF when cpuset is mounted on the unified (v2) hierarchy,
so forcing the controller version is redundant.
> BTW on which Ubuntu version do you test which still uses v1 and have
> libnuma2?
I tested on an AWS instance running Ubuntu with the 6.8.0-1069-aws kernel
(2 NUMA memory nodes, 96 CPUs, libnuma present).
To be precise, the system does not use cgroup v1 by default -- /sys/fs/cgroup
is cgroup v2 (cgroup2fs). However, systemd does not use the cpuset
controller on the v2 hierarchy there, so cpuset is idle and the LTP cgroup
library is able to mount a v1 cpuset hierarchy on the fly:
tst_cgroup.c:793: TINFO: Mounted V1 cpuset CGroup on /tmp/cgroup_cpuset
cpuset.memory_spread_page then exists in that v1 cpuset and the test runs;
all six subtests pass. On a host where cpuset is actively bound to the v2
hierarchy the test simply skips with TCONF, which is the expected behaviour
since cpuset.memory_spread_page is a cgroup v1 only knob.
I'll submit v10 with these changes later.
Thanks,
Changwei
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply [flat|nested] 7+ messages in thread
* [LTP] [PATCH v8 1/1] cpuset_memory_spread: rewrite as a C test using the LTP cgroup library
@ 2026-11-04 7:19 Changwei Zou via ltp
2026-11-04 11:41 ` [LTP] " linuxtestproject.agent
0 siblings, 1 reply; 7+ messages in thread
From: Changwei Zou via ltp @ 2026-11-04 7:19 UTC (permalink / raw)
To: ltp; +Cc: Changwei Zou
The cpuset_memory_spread test, written in 2009, was split between a shell
script (cpuset_memory_spread_testset.sh) that mounted /dev/cpuset by hand
and a C helper (cpuset_mem_hog) driven over a FIFO and SIGUSR1/SIGUSR2.
It checked the cpuset.memory_spread_page policy by reading a 100 MB
DATAFILE and comparing the global per-node FilePages counters in
/sys/devices/system/node/nodeX/meminfo before and after.
Those counters also account for unrelated page-cache activity elsewhere
on the system, so on large or busy NUMA machines the empirical thresholds
became unreliable and the test failed spuriously.
Rewrite the whole thing as a single C test built on the new LTP test API
and the LTP cgroup library (tst_cgroup.h), dropping the shell script,
the FIFO/signal handshake and the manual /dev/cpuset management.
The test now accounts for only DATAFILE's own page-cache pages: after
dropping the caches and reading the file, it mmaps it, faults every page
in and uses move_pages(2) (with a NULL node array, so nothing is
migrated) to learn the NUMA node each of the file's pages resides on.
Every page must be accounted for -- a per-page error (negative status)
fails the run -- so a partial measurement cannot pass. Because only this
file's pages are counted, unrelated page-cache activity can no longer
perturb the result, and the check is page-size independent.
The node distribution is then verified: the cache must land on the
expected node(s), the other nodes may hold at most UNEXPECTED_TOLERANCE
(5%) of it, and when several expected nodes are given each must hold at
least (100 - BALANCE_TOLERANCE, 80%) of its even share. move_pages(2) is
invoked through tst_syscall(), so the test is skipped with TCONF when it
is unavailable (CONFIG_NUMA_MIGRATION disabled).
cpuset.memory_spread_page only exists in the cgroup v1 cpuset controller,
so the test requires a v1 cpuset hierarchy (.needs_cgroup_ver = TST_CG_V1)
and is skipped with TCONF when cpuset is mounted on the unified (v2)
hierarchy. Register cpuset.memory_spread_page in the LTP cgroup library's
cpuset file table so it can be driven through the SAFE_CG_* helpers.
Signed-off-by: Changwei Zou <changwei.zou@canonical.com>
Assisted-By: opencode (claude-opus-4.8)
---
lib/tst_cgroup.c | 1 +
runtest/controllers | 2 +-
.../kernel/controllers/cpuset/.gitignore | 2 +-
.../cpuset/cpuset_memory_spread_test/Makefile | 2 +-
.../cpuset_mem_hog.c | 126 ------
.../cpuset_memory_spread01.c | 355 +++++++++++++++++
.../cpuset_memory_spread_testset.sh | 362 ------------------
7 files changed, 359 insertions(+), 491 deletions(-)
delete mode 100644 testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
create mode 100644 testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread01.c
delete mode 100755 testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
diff --git a/lib/tst_cgroup.c b/lib/tst_cgroup.c
index 7c6ecd4d2..74459138f 100644
--- a/lib/tst_cgroup.c
+++ b/lib/tst_cgroup.c
@@ -206,6 +206,7 @@ static const struct cgroup_file cpuset_ctrl_files[] = {
{ "cpuset.cpus", "cpuset.cpus", CTRL_CPUSET },
{ "cpuset.mems", "cpuset.mems", CTRL_CPUSET },
{ "cpuset.memory_migrate", "cpuset.memory_migrate", CTRL_CPUSET },
+ { "cpuset.memory_spread_page", "cpuset.memory_spread_page", CTRL_CPUSET },
{ }
};
diff --git a/runtest/controllers b/runtest/controllers
index 93c52c439..0af338369 100644
--- a/runtest/controllers
+++ b/runtest/controllers
@@ -357,7 +357,7 @@ cpuset_load_balance cpuset_load_balance_test.sh
cpuset_hotplug cpuset_hotplug_test.sh
cpuset_memory cpuset_memory_testset.sh
cpuset_memory_pressure cpuset_memory_pressure_testset.sh
-cpuset_memory_spread cpuset_memory_spread_testset.sh
+cpuset_memory_spread01 cpuset_memory_spread01
cpuset_regression_test cpuset_regression_test.sh
diff --git a/testcases/kernel/controllers/cpuset/.gitignore b/testcases/kernel/controllers/cpuset/.gitignore
index c65f2fb61..6a8d713c2 100644
--- a/testcases/kernel/controllers/cpuset/.gitignore
+++ b/testcases/kernel/controllers/cpuset/.gitignore
@@ -2,6 +2,6 @@
/cpuset_load_balance_test/cpuset_cpu_hog
/cpuset_load_balance_test/cpuset_sched_domains_check
/cpuset_memory_pressure_test/cpuset_memory_pressure
-/cpuset_memory_spread_test/cpuset_mem_hog
+/cpuset_memory_spread_test/cpuset_memory_spread01
/cpuset_memory_test/cpuset_memory_test
/cpuset_syscall_test/cpuset_syscall_test
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/Makefile b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/Makefile
index b3c76f0de..40316b29b 100644
--- a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/Makefile
+++ b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/Makefile
@@ -23,5 +23,5 @@
top_srcdir ?= ../../../../..
include $(top_srcdir)/include/mk/testcases.mk
-include $(abs_srcdir)/../Makefile.inc
+include $(top_srcdir)/testcases/kernel/include/lib.mk
include $(top_srcdir)/include/mk/generic_leaf_target.mk
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
deleted file mode 100644
index 56e039eee..000000000
--- a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
+++ /dev/null
@@ -1,126 +0,0 @@
-/******************************************************************************/
-/* */
-/* Copyright (c) 2009 FUJITSU LIMITED */
-/* */
-/* This program is free software; you can redistribute it and/or modify */
-/* it under the terms of the GNU General Public License as published by */
-/* the Free Software Foundation; either version 2 of the License, or */
-/* (at your option) any later version. */
-/* */
-/* This program is distributed in the hope that it will be useful, */
-/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
-/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See */
-/* the GNU General Public License for more details. */
-/* */
-/* You should have received a copy of the GNU General Public License */
-/* along with this program; if not, write to the Free Software */
-/* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */
-/* */
-/* Author: Miao Xie <miaox@cn.fujitsu.com> */
-/* */
-/******************************************************************************/
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <unistd.h>
-#include <string.h>
-#include <ctype.h>
-#include <getopt.h>
-#include <err.h>
-#include <signal.h>
-#include <sys/types.h>
-#include <sys/stat.h>
-#include <fcntl.h>
-
-#include "../cpuset_lib/common.h"
-
-#define BUFFER_SIZE 100
-
-volatile int end;
-
-void sighandler1(UNUSED int signo)
-{
-}
-
-void sighandler2(UNUSED int signo)
-{
- end = 1;
-}
-
-int page_cache_hog(void)
-{
- int fd = -1;
- char buff[BUFFER_SIZE];
- char path[BUFFER_SIZE];
- int ret = 0;
-
- sprintf(path, "%s", "DATAFILE");
- fd = open(path, O_RDONLY);
- if (fd == -1) {
- warn("open %s failed", path);
- return -1;
- }
-
- while ((ret = read(fd, buff, sizeof(buff))) > 0) ;
- if (ret == -1)
- warn("read %s failed", path);
-
- close(fd);
- return ret;
-}
-
-int mem_hog(void)
-{
- sigset_t signalset;
- int fd;
- int ret = 0;
-
- if (sigemptyset(&signalset) < 0)
- err(1, "sigemptyset()");
- sigsuspend(&signalset);
-
- while (!end) {
- ret = page_cache_hog();
-
- fd = open("./myfifo", O_WRONLY);
- if (fd == -1)
- err(1, "open fifo failed");
-
- if (ret) {
- if (write(fd, "0", 1) == -1)
- warn("write fifo failed.");
- } else {
- if (write(fd, "1", 1) == -1)
- warn("write fifo failed.");
- }
-
- close(fd);
-
- sigsuspend(&signalset);
- }
-
- return ret;
-}
-
-int main(void)
-{
- struct sigaction sa1, sa2;
-
- sa1.sa_handler = sighandler1;
- if (sigemptyset(&sa1.sa_mask) < 0)
- err(1, "sigemptyset()");
-
- sa1.sa_flags = 0;
- if (sigaction(SIGUSR1, &sa1, NULL) < 0)
- err(1, "sigaction()");
-
- sa2.sa_handler = sighandler2;
- if (sigemptyset(&sa2.sa_mask) < 0)
- err(1, "sigemptyset()");
-
- sa2.sa_flags = 0;
- if (sigaction(SIGUSR2, &sa2, NULL) < 0)
- err(1, "sigaction()");
-
- return mem_hog();
-}
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread01.c b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread01.c
new file mode 100644
index 000000000..21d3c0205
--- /dev/null
+++ b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread01.c
@@ -0,0 +1,355 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2009 FUJITSU LIMITED
+ * Author: Miao Xie <miaox@cn.fujitsu.com>
+ *
+ * Rewritten into a single C test using the LTP cgroup library.
+ */
+
+/*\
+ * Verify the ``cpuset.memory_spread_page`` control file.
+ *
+ * A task's file-backed page cache is normally allocated on the node the
+ * task is running on (local allocation). When ``cpuset.memory_spread_page``
+ * is enabled, the page cache of a task in the cpuset is instead spread
+ * evenly over all the memory nodes allowed by ``cpuset.mems``.
+ *
+ * The test attaches itself to a cpuset, populates the page cache of a data
+ * file (by dropping the caches and reading the whole file) and then checks
+ * where the file's pages landed. Instead of looking at the noisy global
+ * per-node page-cache counters in ``/sys``, it accounts for only the data
+ * file's own pages: after reading the file it mmaps it, faults every page
+ * in and uses :manpage:`move_pages(2)` (with a NULL node array, so nothing
+ * is migrated) to learn the NUMA node each of the file's pages resides on.
+ * Every page must be accounted for -- a per-page error fails the run -- so
+ * unrelated page-cache activity on the system cannot perturb the result.
+ *
+ * The test is run with and without spreading over a single and over two
+ * memory nodes and the resulting distribution is verified:
+ *
+ * - with spreading off the pages must land on the local (expected) node,
+ * - with spreading on over several nodes the pages must be spread evenly.
+ *
+ * ``cpuset.memory_spread_page`` exists only in the cgroup v1 cpuset
+ * controller, so the test requires a v1 cpuset hierarchy and is skipped
+ * (TCONF) when cpuset is mounted on the unified (v2) hierarchy.
+ */
+
+#include "config.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#ifdef HAVE_NUMA_H
+# include <numa.h>
+#endif
+
+#include "tst_test.h"
+#include "tst_cgroup.h"
+#include "numa_helper.h"
+#include "lapi/syscalls.h"
+
+#ifdef HAVE_NUMA_V2
+
+/* The file whose page-cache placement we test. */
+#define DATAFILE "datafile"
+
+/* Size of the data file. Large enough to measure an even spread. */
+#define FILE_MB 100
+
+/* Each node needs enough free memory to hold the whole file plus slack. */
+#define MIN_FREE_KB (FILE_MB * 1024 + 50 * 1024)
+
+/* Query the residing NUMA node of at most this many pages per syscall. */
+#define MOVE_PAGES_CHUNK 1024
+
+/*
+ * The pages must land on the expected node(s); the other nodes may hold at
+ * most this fraction (in percent) of them.
+ */
+#define UNEXPECTED_TOLERANCE 5
+
+/*
+ * When the page cache is spread across several expected nodes, the kernel
+ * spreads it evenly. Every expected node must hold at least
+ * (100 - BALANCE_TOLERANCE) percent of its even share.
+ */
+#define BALANCE_TOLERANCE 20
+
+static int *nodes;
+static int nnodes;
+static long page_size;
+static unsigned long *counts;
+
+/* Per-node free memory in kB from /sys/.../nodeN/meminfo. */
+static long node_memfree(int nd)
+{
+ long free_kb = 0;
+ char path[PATH_MAX];
+
+ snprintf(path, sizeof(path),
+ PATH_SYS_SYSTEM "/node/node%d/meminfo", nd);
+ SAFE_FILE_LINES_SCANF(path, "Node %*d MemFree: %ld", &free_kb);
+
+ return free_kb;
+}
+
+/*
+ * Populate the page cache of DATAFILE under the current cpuset constraints
+ * by dropping the caches and reading the whole file.
+ */
+static void populate_page_cache(void)
+{
+ int fd;
+ char buf[BUFSIZ];
+
+ sync();
+ SAFE_FILE_PRINTF("/proc/sys/vm/drop_caches", "3");
+
+ fd = SAFE_OPEN(DATAFILE, O_RDONLY);
+ while (SAFE_READ(0, fd, buf, sizeof(buf)) > 0)
+ ;
+ SAFE_CLOSE(fd);
+}
+
+/*
+ * Count only DATAFILE's own page-cache pages per NUMA node.
+ *
+ * move_pages(2) with a NULL node array does not move anything; it only
+ * reports, in status[], the NUMA node each already-present page resides on.
+ */
+static void account_file_pages(void)
+{
+ int fd;
+ struct stat st;
+ char *addr;
+ unsigned long npages, i, off;
+ void **pages;
+ int *status;
+
+ memset(counts, 0, MAXNODES * sizeof(*counts));
+
+ fd = SAFE_OPEN(DATAFILE, O_RDONLY);
+ SAFE_FSTAT(fd, &st);
+ npages = (st.st_size + page_size - 1) / page_size;
+ addr = SAFE_MMAP(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
+ SAFE_CLOSE(fd);
+
+ pages = SAFE_MALLOC(npages * sizeof(*pages));
+ status = SAFE_MALLOC(npages * sizeof(*status));
+
+ /*
+ * Fault in every page so it maps the page-cache page populated by
+ * populate_page_cache(); move_pages() then reports the node that
+ * cache page resides on.
+ */
+ for (i = 0; i < npages; i++) {
+ volatile char c = addr[i * page_size];
+
+ (void)c;
+ pages[i] = addr + i * page_size;
+ status[i] = -1;
+ }
+
+ for (off = 0; off < npages; off += MOVE_PAGES_CHUNK) {
+ unsigned long n = npages - off;
+
+ if (n > MOVE_PAGES_CHUNK)
+ n = MOVE_PAGES_CHUNK;
+
+ /*
+ * tst_syscall() reports TCONF if move_pages() is not
+ * implemented (CONFIG_NUMA_MIGRATION disabled).
+ */
+ TEST(tst_syscall(__NR_move_pages, 0, n, pages + off,
+ NULL, status + off, 0));
+ if (TST_RET == -1)
+ tst_brk(TBROK | TTERRNO, "move_pages");
+ }
+
+ /*
+ * move_pages() can return success while reporting a per-page error
+ * (e.g. -ENOENT or -EFAULT) in status[]. Treat any such page as a
+ * failure: otherwise the distribution would be computed from only a
+ * subset of DATAFILE and could pass incorrectly.
+ */
+ for (i = 0; i < npages; i++) {
+ if (status[i] < 0 || status[i] >= MAXNODES)
+ tst_brk(TBROK, "page %lu not accounted (status %d)",
+ i, status[i]);
+ counts[status[i]]++;
+ }
+
+ SAFE_MUNMAP(addr, st.st_size);
+ free(pages);
+ free(status);
+}
+
+static void check_result(const int *expect, int n_expect, const char *desc)
+{
+ unsigned long total = 0, expected_sum = 0, unexpected;
+ int i;
+
+ for (i = 0; i < MAXNODES; i++)
+ total += counts[i];
+
+ if (!total) {
+ tst_res(TFAIL, "%s: no page cache was populated", desc);
+ return;
+ }
+
+ for (i = 0; i < n_expect; i++)
+ expected_sum += counts[expect[i]];
+
+ unexpected = total - expected_sum;
+
+ tst_res(TINFO,
+ "%s: unexpected nodes hold %.2f%% of the page cache (tolerance %d%%)",
+ desc, (double)unexpected * 100 / total, UNEXPECTED_TOLERANCE);
+
+ if (unexpected * 100 > total * UNEXPECTED_TOLERANCE) {
+ tst_res(TFAIL,
+ "%s: too much page cache on unexpected node(s)", desc);
+ return;
+ }
+
+ /*
+ * Every expected node must hold at least (100 - BALANCE_TOLERANCE)%
+ * of its even share. This enforces an even spread across several
+ * expected nodes and rejects an expected node that received nothing.
+ * A single expected node always passes this check.
+ */
+ for (i = 0; i < n_expect; i++) {
+ if (counts[expect[i]] * n_expect * 100 <
+ expected_sum * (100 - BALANCE_TOLERANCE)) {
+ tst_res(TFAIL,
+ "%s: node %d holds too little page cache",
+ desc, expect[i]);
+ return;
+ }
+ }
+
+ tst_res(TPASS, "%s", desc);
+}
+
+/*
+ * The task's CPUs are bound to the local node (nodes[0]) for every case
+ * (see setup()); only cpuset.mems and cpuset.memory_spread_page change.
+ *
+ * mems/expect hold indices into nodes[]: 0 is the local node, 1 the remote
+ * one; a second entry of -1 means "unused".
+ */
+static const struct tcase {
+ int spread;
+ int mems[2];
+ int expect[2];
+ const char *desc;
+} tcases[] = {
+ {0, {0, -1}, {0, -1}, "spread off, mems on the local node"},
+ {1, {0, -1}, {0, -1}, "spread on, mems on the local node"},
+ {0, {1, -1}, {1, -1}, "spread off, mems on the remote node"},
+ {1, {1, -1}, {1, -1}, "spread on, mems on the remote node"},
+ {0, {0, 1}, {0, -1}, "spread off, mems on both nodes (local allocation)"},
+ {1, {0, 1}, {0, 1}, "spread on, mems on both nodes (even spread)"},
+};
+
+static void run(unsigned int n)
+{
+ const struct tcase *tc = &tcases[n];
+ int expect[2];
+ int n_expect = 1;
+
+ if (tc->mems[1] >= 0)
+ SAFE_CG_PRINTF(tst_cg, "cpuset.mems", "%d,%d",
+ nodes[tc->mems[0]], nodes[tc->mems[1]]);
+ else
+ SAFE_CG_PRINTF(tst_cg, "cpuset.mems", "%d",
+ nodes[tc->mems[0]]);
+
+ SAFE_CG_PRINTF(tst_cg, "cpuset.memory_spread_page", "%d", tc->spread);
+
+ expect[0] = nodes[tc->expect[0]];
+ if (tc->expect[1] >= 0) {
+ expect[1] = nodes[tc->expect[1]];
+ n_expect = 2;
+ }
+
+ populate_page_cache();
+ account_file_pages();
+ check_result(expect, n_expect, tc->desc);
+}
+
+static void setup(void)
+{
+ int i;
+
+ page_size = getpagesize();
+
+ if (get_allowed_nodes_arr(NH_MEMS | NH_CPUS, &nnodes, &nodes) < 0)
+ tst_brk(TBROK | TERRNO, "get_allowed_nodes_arr");
+ if (nnodes < 2)
+ tst_brk(TCONF, "requires at least 2 NUMA nodes with CPUs and memory");
+
+ /*
+ * cpuset.memory_spread_page is a cgroup v1 only knob; it was not
+ * carried over to the unified (v2) hierarchy. .needs_cgroup_ver
+ * below already restricts the test to a v1 cpuset, this is just a
+ * belt-and-suspenders check.
+ */
+ if (!SAFE_CG_HAS(tst_cg, "cpuset.memory_spread_page"))
+ tst_brk(TCONF, "cpuset.memory_spread_page not available");
+
+ for (i = 0; i < 2; i++) {
+ if (node_memfree(nodes[i]) < MIN_FREE_KB)
+ tst_brk(TCONF,
+ "node %d has less than %d kB free memory",
+ nodes[i], MIN_FREE_KB);
+ }
+
+ counts = SAFE_MALLOC(MAXNODES * sizeof(*counts));
+
+ if (tst_fill_file(DATAFILE, 0, 1024 * 1024, FILE_MB))
+ tst_brk(TBROK | TERRNO, "tst_fill_file(%s)", DATAFILE);
+
+ /*
+ * Bind the task's CPUs to the local node (nodes[0]) once; every case
+ * runs on this node and only varies cpuset.mems. write_node_cpusets()
+ * also sets cpuset.mems to nodes[0], which run() overrides per case.
+ */
+ write_node_cpusets(tst_cg, nodes[0]);
+
+ SAFE_CG_PRINTF(tst_cg, "cgroup.procs", "%d", getpid());
+}
+
+static void cleanup(void)
+{
+ free(nodes);
+ free(counts);
+}
+
+static struct tst_test test = {
+ .needs_root = 1,
+ .needs_tmpdir = 1,
+ /*
+ * On tmpfs/ramfs the file data is the page cache itself, so
+ * drop_caches cannot evict it and the pages keep the node they got
+ * at write time rather than the one chosen by the cpuset policy.
+ * The page-cache placement is then not measurable.
+ */
+ .skip_filesystems = (const char *const []){ "tmpfs", "ramfs", NULL },
+ .setup = setup,
+ .cleanup = cleanup,
+ .test = run,
+ .tcnt = ARRAY_SIZE(tcases),
+ .needs_cgroup_ctrls = (const char *const []){ "cpuset", NULL },
+ .needs_cgroup_ver = TST_CG_V1,
+};
+
+#else
+TST_TEST_TCONF("test requires libnuma >= 2");
+#endif
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
deleted file mode 100755
index 4c49bb8fd..000000000
--- a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
+++ /dev/null
@@ -1,362 +0,0 @@
-#!/bin/sh
-
-################################################################################
-# #
-# Copyright (c) 2009 FUJITSU LIMITED #
-# #
-# This program is free software; you can redistribute it and#or modify #
-# it under the terms of the GNU General Public License as published by #
-# the Free Software Foundation; either version 2 of the License, or #
-# (at your option) any later version. #
-# #
-# This program is distributed in the hope that it will be useful, but #
-# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY #
-# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License #
-# for more details. #
-# #
-# You should have received a copy of the GNU General Public License #
-# along with this program; if not, write to the Free Software #
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #
-# #
-# Author: Miao Xie <miaox@cn.fujitsu.com> #
-# #
-################################################################################
-
-export TCID="cpuset_memory_spread"
-export TST_TOTAL=6
-export TST_COUNT=1
-
-. cpuset_funcs.sh
-
-check
-
-exit_status=0
-nr_cpus=$NR_CPUS
-nr_mems=$N_NODES
-
-# In general, the cache hog will use more than 10000 kb slab space on the nodes
-# on which it is running. The other nodes' slab space has littler change.(less
-# than 1000 kb).
-upperlimit=10000
-
-# set lowerlimit according to pagesize
-# pagesize(bytes) | lowerlimit(kb)
-# ------------------------------------
-# 4096 | 2048
-# 16384 | 8192
-
-PAGE_SIZE=`tst_getconf PAGESIZE`
-lowerlimit=$((PAGE_SIZE * 512 / 1024))
-
-cpus_all="$(seq -s, 0 $((nr_cpus-1)))"
-mems_all="$(seq -s, 0 $((nr_mems-1)))"
-
-nodedir="/sys/devices/system/node"
-
-FIFO="./myfifo"
-
-# memsinfo is an array implementation of the form of a multi-line string
-# _0: value0
-# _1: value1
-# _2: value2
-#
-memsinfo=""
-
-# set value to memsinfo ($1 - index, $2 - value)
-set_memsinfo_val()
-{
- local nl='
-'
- # clearing existent value (if present)
- memsinfo=`echo "$memsinfo" | sed -r "/^\_$1\: /d"`
-
- if [ -z "$memsinfo" ]; then
- memsinfo="_$1: $2"
- else
- memsinfo="$memsinfo${nl}_$1: $2"
- fi
-}
-
-# get value from memsinfo ($1 - index)
-get_memsinfo_val()
-{
- local value=
- value=`echo "$memsinfo" | grep -e "^_$1: "`
- value=`echo "$value" | sed -r "s/^.*\: (.*)$/\1/"`
- echo "$value"
-}
-
-
-init_memsinfo_array()
-{
- local i=
-
- for i in `seq 0 $((nr_mems-1))`
- do
- set_memsinfo_val $i 0
- done
-}
-
-# get_meminfo <nodeid> <item>
-get_meminfo()
-{
- local nodeid="$1"
- local nodepath="$nodedir/node$nodeid"
- local nodememinfo="$nodepath/meminfo"
- local item="$2"
- local info=`cat $nodememinfo | grep $item | awk '{print $4}'`
- set_memsinfo_val $nodeid $info
-}
-
-# freemem_check
-# We need enough memory space on every node to run this test, so we must check
-# whether every node has enough free memory or not.
-# return value: 1 - Some node doesn't have enough free memory
-# 0 - Every node has enough free memory, We can do this test
-freemem_check()
-{
- local i=
-
- for i in `seq 0 $((nr_mems-1))`
- do
- get_meminfo $i "MemFree"
- done
-
- for i in `seq 0 $((nr_mems-1))`
- do
- # I think we need 100MB free memory to run test
- if [ $(get_memsinfo_val $i) -lt 100000 ]; then
- return 1
- fi
- done
-}
-
-# get_memsinfo
-get_memsinfo()
-{
- local i=
-
- for i in `seq 0 $((nr_mems-1))`
- do
- get_meminfo $i "FilePages"
- done
-}
-
-# account_meminfo <nodeId>
-account_meminfo()
-{
- local nodeId="$1"
- local tmp="$(get_memsinfo_val $nodeId)"
- get_meminfo $@ "FilePages"
- set_memsinfo_val $nodeId $(($(get_memsinfo_val $nodeId)-$tmp))
-}
-
-# account_memsinfo
-account_memsinfo()
-{
- local i=
-
- for i in `seq 0 $((nr_mems-1))`
- do
- account_meminfo $i
- done
-}
-
-
-# result_check <nodelist>
-# return 0: success
-# 1: fail
-result_check()
-{
- local nodelist="`echo $1 | sed -e 's/,/ /g'`"
- local i=
-
- for i in $nodelist
- do
- if [ $(get_memsinfo_val $i) -le $upperlimit ]; then
- return 1
- fi
- done
-
- local allnodelist="`echo $mems_all | sed -e 's/,/ /g'`"
- allnodelist=" "$allnodelist" "
- nodelist=" "$nodelist" "
-
- local othernodelist="$allnodelist"
- for i in $nodelist
- do
- othernodelist=`echo "$othernodelist" | sed -e "s/ $i / /g"`
- done
-
- for i in $othernodelist
- do
- if [ $(get_memsinfo_val $i) -gt $lowerlimit ]; then
- return 1
- fi
- done
-}
-
-# general_memory_spread_test <cpusetpath> <is_spread> <cpu_list> <node_list> \
-# <expect_nodes> <test_pid>
-# expect_nodes: we expect to use the slab or cache on which node
-general_memory_spread_test()
-{
- local cpusetpath="$CPUSET/1"
- local is_spread="$1"
- local cpu_list="$2"
- local node_list="$3"
- local expect_nodes="$4"
- local test_pid="$5"
-
- cpuset_set "$cpusetpath" "$cpu_list" "$node_list" "0" 2> $CPUSET_TMP/stderr
- if [ $? -ne 0 ]; then
- cpuset_log_error $CPUSET_TMP/stderr
- tst_resm TFAIL "set general group parameter failed."
- return 1
- fi
-
- /bin/echo "$is_spread" > "$cpusetpath/cpuset.memory_spread_page" 2> $CPUSET_TMP/stderr
- if [ $? -ne 0 ]; then
- cpuset_log_error $CPUSET_TMP/stderr
- tst_resm TFAIL "set spread value failed."
- return 1
- fi
-
- /bin/echo "$test_pid" > "$cpusetpath/tasks" 2> $CPUSET_TMP/stderr
- if [ $? -ne 0 ]; then
- cpuset_log_error $CPUSET_TMP/stderr
- tst_resm TFAIL "attach task failed."
- return 1
- fi
-
- # we'd better drop the caches before we test page cache.
- sync
- /bin/echo 3 > /proc/sys/vm/drop_caches 2> $CPUSET_TMP/stderr
- if [ $? -ne 0 ]; then
- cpuset_log_error $CPUSET_TMP/stderr
- tst_resm TFAIL "drop caches failed."
- return 1
- fi
-
- get_memsinfo
- /bin/kill -s SIGUSR1 $test_pid
- read exit_num < $FIFO
- if [ $exit_num -eq 0 ]; then
- tst_resm TFAIL "hot mem task failed."
- return 1
- fi
-
- account_memsinfo
- result_check $expect_nodes
- if [ $? -ne 0 ]; then
- tst_resm TFAIL "hog the memory on the unexpected node(FilePages_For_Nodes(KB): ${memsinfo}, Expect Nodes: $expect_nodes)."
- return 1
- fi
-}
-
-base_test()
-{
- local pid=
- local result_num=
-
- setup
- if [ $? -ne 0 ]; then
- exit_status=1
- else
- cpuset_mem_hog &
- pid=$!
- general_memory_spread_test "$@" "$pid"
- result_num=$?
- if [ $result_num -ne 0 ]; then
- exit_status=1
- fi
-
- /bin/kill -s SIGUSR2 $pid
- wait $pid
-
- cleanup
- if [ $? -ne 0 ]; then
- exit_status=1
- elif [ $result_num -eq 0 ]; then
- tst_resm TPASS "Cpuset memory spread page test succeeded."
- fi
- fi
- TST_COUNT=$(($TST_COUNT + 1))
-}
-
-# test general spread page cache in a cpuset
-test_spread_page1()
-{
- while read spread cpus nodes exp_nodes
- do
- base_test "$spread" "$cpus" "$nodes" "$exp_nodes"
- done <<- EOF
- 0 0 0 0
- 1 0 0 0
- 0 0 1 1
- 1 0 1 1
- 0 0 0,1 0
- 1 0 0,1 0,1
- EOF
- # while read spread cpus nodes exp_nodes
-}
-
-test_spread_page2()
-{
- local pid=
- local result_num=
-
- setup
- if [ $? -ne 0 ]; then
- exit_status=1
- else
- cpuset_mem_hog &
- pid=$!
- general_memory_spread_test "1" "$cpus_all" "0" "0" "$pid"
- result_num=$?
- if [ $result_num -ne 0 ]; then
- exit_status=1
- else
- general_memory_spread_test "1" "$cpus_all" "1" "1" "$pid"
- result_num=$?
- if [ $result_num -ne 0 ]; then
- exit_status=1
- fi
- fi
-
- /bin/kill -s SIGUSR2 $pid
- wait $pid
-
- cleanup
- if [ $? -ne 0 ]; then
- exit_status=1
- elif [ $result_num -eq 0 ]; then
- tst_resm TPASS "Cpuset memory spread page test succeeded."
- fi
- fi
-}
-
-init_memsinfo_array
-freemem_check
-if [ $? -ne 0 ]; then
- tst_brkm TCONF "Some node doesn't has enough free memory(100MB) to do test(MemFree_For_Nodes(KB): ${memsinfo[*]})."
-fi
-
-dd if=/dev/zero of=./DATAFILE bs=1M count=100
-if [ $? -ne 0 ]; then
- tst_brkm TFAIL "Creating DATAFILE failed."
-fi
-
-rm -f $FIFO
-mkfifo $FIFO
-if [ $? -ne 0 ]; then
- rm -f DATAFILE
- tst_brkm TFAIL "failed to mkfifo $FIFO"
-fi
-
-test_spread_page1
-test_spread_page2
-
-rm -f DATAFILE $FIFO
-
-exit $exit_status
--
2.43.0
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply related [flat|nested] 7+ messages in thread* Re: [LTP] cpuset_memory_spread: rewrite as a C test using the LTP cgroup library
2026-11-04 7:19 [LTP] [PATCH v8 " Changwei Zou via ltp
@ 2026-11-04 11:41 ` linuxtestproject.agent
0 siblings, 0 replies; 7+ messages in thread
From: linuxtestproject.agent @ 2026-11-04 11:41 UTC (permalink / raw)
To: Changwei Zou; +Cc: ltp
Hi Changwei,
On Wed Nov 4 18:19:08 2026 +1100, Changwei Zou wrote:
> cpuset_memory_spread: rewrite as a C test using the LTP cgroup library
Verdict - Reviewed
---
Note:
The agent can sometimes produce false positives although often its
findings are genuine. If you find issues with the review, please
comment this email or ignore the suggestions.
Regards,
LTP AI Reviewer
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply [flat|nested] 7+ messages in thread
* [LTP] [PATCH v7 1/1] cpuset_memory_spread: rewrite as a C test using the LTP cgroup library
@ 2026-11-04 0:24 Changwei Zou via ltp
2026-11-04 3:30 ` [LTP] " linuxtestproject.agent
0 siblings, 1 reply; 7+ messages in thread
From: Changwei Zou via ltp @ 2026-11-04 0:24 UTC (permalink / raw)
To: ltp; +Cc: Changwei Zou
The cpuset_memory_spread test, written in 2009, was split between a shell
script (cpuset_memory_spread_testset.sh) that mounted /dev/cpuset by hand
and a C helper (cpuset_mem_hog) driven over a FIFO and SIGUSR1/SIGUSR2.
It checked the cpuset.memory_spread_page policy by reading a 100 MB
DATAFILE and comparing the global per-node FilePages counters in
/sys/devices/system/node/nodeX/meminfo before and after.
Those counters also account for unrelated page-cache activity elsewhere
on the system, so on large or busy NUMA machines the empirical thresholds
became unreliable and the test failed spuriously.
Rewrite the whole thing as a single C test built on the new LTP test API
and the LTP cgroup library (tst_cgroup.h), dropping the shell script,
the FIFO/signal handshake and the manual /dev/cpuset management.
The test now accounts for only DATAFILE's own page-cache pages: after
dropping the caches and reading the file, it mmaps it, faults every page
in and uses move_pages(2) (with a NULL node array, so nothing is
migrated) to learn the NUMA node each of the file's pages resides on.
Every page must be accounted for -- a per-page error (negative status)
fails the run -- so a partial measurement cannot pass. Because only this
file's pages are counted, unrelated page-cache activity can no longer
perturb the result, and the check is page-size independent.
The node distribution is then verified: the cache must land on the
expected node(s), the other nodes may hold at most UNEXPECTED_TOLERANCE
(5%) of it, and when several expected nodes are given each must hold at
least (100 - BALANCE_TOLERANCE, 80%) of its even share. move_pages(2) is
invoked through tst_syscall(), so the test is skipped with TCONF when it
is unavailable (CONFIG_NUMA_MIGRATION disabled).
cpuset.memory_spread_page only exists in the cgroup v1 cpuset controller,
so the test requires a v1 cpuset hierarchy (.needs_cgroup_ver = TST_CG_V1)
and is skipped with TCONF when cpuset is mounted on the unified (v2)
hierarchy. Register cpuset.memory_spread_page in the LTP cgroup library's
cpuset file table so it can be driven through the SAFE_CG_* helpers.
Signed-off-by: Changwei Zou <changwei.zou@canonical.com>
Assisted-By: opencode (claude-opus-4.8)
---
lib/tst_cgroup.c | 1 +
runtest/controllers | 2 +-
.../kernel/controllers/cpuset/.gitignore | 2 +-
.../cpuset/cpuset_memory_spread_test/Makefile | 2 +-
.../cpuset_mem_hog.c | 126 ------
.../cpuset_memory_spread01.c | 394 ++++++++++++++++++
.../cpuset_memory_spread_testset.sh | 362 ----------------
7 files changed, 398 insertions(+), 491 deletions(-)
delete mode 100644 testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
create mode 100644 testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread01.c
delete mode 100755 testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
diff --git a/lib/tst_cgroup.c b/lib/tst_cgroup.c
index 7c6ecd4d2..74459138f 100644
--- a/lib/tst_cgroup.c
+++ b/lib/tst_cgroup.c
@@ -206,6 +206,7 @@ static const struct cgroup_file cpuset_ctrl_files[] = {
{ "cpuset.cpus", "cpuset.cpus", CTRL_CPUSET },
{ "cpuset.mems", "cpuset.mems", CTRL_CPUSET },
{ "cpuset.memory_migrate", "cpuset.memory_migrate", CTRL_CPUSET },
+ { "cpuset.memory_spread_page", "cpuset.memory_spread_page", CTRL_CPUSET },
{ }
};
diff --git a/runtest/controllers b/runtest/controllers
index 93c52c439..0af338369 100644
--- a/runtest/controllers
+++ b/runtest/controllers
@@ -357,7 +357,7 @@ cpuset_load_balance cpuset_load_balance_test.sh
cpuset_hotplug cpuset_hotplug_test.sh
cpuset_memory cpuset_memory_testset.sh
cpuset_memory_pressure cpuset_memory_pressure_testset.sh
-cpuset_memory_spread cpuset_memory_spread_testset.sh
+cpuset_memory_spread01 cpuset_memory_spread01
cpuset_regression_test cpuset_regression_test.sh
diff --git a/testcases/kernel/controllers/cpuset/.gitignore b/testcases/kernel/controllers/cpuset/.gitignore
index c65f2fb61..6a8d713c2 100644
--- a/testcases/kernel/controllers/cpuset/.gitignore
+++ b/testcases/kernel/controllers/cpuset/.gitignore
@@ -2,6 +2,6 @@
/cpuset_load_balance_test/cpuset_cpu_hog
/cpuset_load_balance_test/cpuset_sched_domains_check
/cpuset_memory_pressure_test/cpuset_memory_pressure
-/cpuset_memory_spread_test/cpuset_mem_hog
+/cpuset_memory_spread_test/cpuset_memory_spread01
/cpuset_memory_test/cpuset_memory_test
/cpuset_syscall_test/cpuset_syscall_test
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/Makefile b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/Makefile
index b3c76f0de..40316b29b 100644
--- a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/Makefile
+++ b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/Makefile
@@ -23,5 +23,5 @@
top_srcdir ?= ../../../../..
include $(top_srcdir)/include/mk/testcases.mk
-include $(abs_srcdir)/../Makefile.inc
+include $(top_srcdir)/testcases/kernel/include/lib.mk
include $(top_srcdir)/include/mk/generic_leaf_target.mk
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
deleted file mode 100644
index 56e039eee..000000000
--- a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_mem_hog.c
+++ /dev/null
@@ -1,126 +0,0 @@
-/******************************************************************************/
-/* */
-/* Copyright (c) 2009 FUJITSU LIMITED */
-/* */
-/* This program is free software; you can redistribute it and/or modify */
-/* it under the terms of the GNU General Public License as published by */
-/* the Free Software Foundation; either version 2 of the License, or */
-/* (at your option) any later version. */
-/* */
-/* This program is distributed in the hope that it will be useful, */
-/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
-/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See */
-/* the GNU General Public License for more details. */
-/* */
-/* You should have received a copy of the GNU General Public License */
-/* along with this program; if not, write to the Free Software */
-/* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */
-/* */
-/* Author: Miao Xie <miaox@cn.fujitsu.com> */
-/* */
-/******************************************************************************/
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <unistd.h>
-#include <string.h>
-#include <ctype.h>
-#include <getopt.h>
-#include <err.h>
-#include <signal.h>
-#include <sys/types.h>
-#include <sys/stat.h>
-#include <fcntl.h>
-
-#include "../cpuset_lib/common.h"
-
-#define BUFFER_SIZE 100
-
-volatile int end;
-
-void sighandler1(UNUSED int signo)
-{
-}
-
-void sighandler2(UNUSED int signo)
-{
- end = 1;
-}
-
-int page_cache_hog(void)
-{
- int fd = -1;
- char buff[BUFFER_SIZE];
- char path[BUFFER_SIZE];
- int ret = 0;
-
- sprintf(path, "%s", "DATAFILE");
- fd = open(path, O_RDONLY);
- if (fd == -1) {
- warn("open %s failed", path);
- return -1;
- }
-
- while ((ret = read(fd, buff, sizeof(buff))) > 0) ;
- if (ret == -1)
- warn("read %s failed", path);
-
- close(fd);
- return ret;
-}
-
-int mem_hog(void)
-{
- sigset_t signalset;
- int fd;
- int ret = 0;
-
- if (sigemptyset(&signalset) < 0)
- err(1, "sigemptyset()");
- sigsuspend(&signalset);
-
- while (!end) {
- ret = page_cache_hog();
-
- fd = open("./myfifo", O_WRONLY);
- if (fd == -1)
- err(1, "open fifo failed");
-
- if (ret) {
- if (write(fd, "0", 1) == -1)
- warn("write fifo failed.");
- } else {
- if (write(fd, "1", 1) == -1)
- warn("write fifo failed.");
- }
-
- close(fd);
-
- sigsuspend(&signalset);
- }
-
- return ret;
-}
-
-int main(void)
-{
- struct sigaction sa1, sa2;
-
- sa1.sa_handler = sighandler1;
- if (sigemptyset(&sa1.sa_mask) < 0)
- err(1, "sigemptyset()");
-
- sa1.sa_flags = 0;
- if (sigaction(SIGUSR1, &sa1, NULL) < 0)
- err(1, "sigaction()");
-
- sa2.sa_handler = sighandler2;
- if (sigemptyset(&sa2.sa_mask) < 0)
- err(1, "sigemptyset()");
-
- sa2.sa_flags = 0;
- if (sigaction(SIGUSR2, &sa2, NULL) < 0)
- err(1, "sigaction()");
-
- return mem_hog();
-}
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread01.c b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread01.c
new file mode 100644
index 000000000..91b57778d
--- /dev/null
+++ b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread01.c
@@ -0,0 +1,394 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2009 FUJITSU LIMITED
+ * Author: Miao Xie <miaox@cn.fujitsu.com>
+ *
+ * Rewritten into a single C test using the LTP cgroup library.
+ */
+
+/*\
+ * Verify the ``cpuset.memory_spread_page`` control file.
+ *
+ * A task's file-backed page cache is normally allocated on the node the
+ * task is running on (local allocation). When ``cpuset.memory_spread_page``
+ * is enabled, the page cache of a task in the cpuset is instead spread
+ * evenly over all the memory nodes allowed by ``cpuset.mems``.
+ *
+ * The test attaches itself to a cpuset, populates the page cache of a data
+ * file (by dropping the caches and reading the whole file) and then checks
+ * where the file's pages landed. Instead of looking at the noisy global
+ * per-node page-cache counters in ``/sys``, it accounts for only the data
+ * file's own pages: after reading the file it mmaps it, faults every page
+ * in and uses ``move_pages()`` (with a NULL node array, so nothing is
+ * migrated) to learn the NUMA node each of the file's pages resides on.
+ * Every page must be accounted for -- a per-page error fails the run -- so
+ * unrelated page-cache activity on the system cannot perturb the result.
+ *
+ * The test is run with and without spreading over a single and over two
+ * memory nodes and the resulting distribution is verified:
+ *
+ * - with spreading off the pages must land on the local (expected) node,
+ * - with spreading on over several nodes the pages must be spread evenly.
+ *
+ * ``cpuset.memory_spread_page`` exists only in the cgroup v1 cpuset
+ * controller, so the test requires a v1 cpuset hierarchy and is skipped
+ * (TCONF) when cpuset is mounted on the unified (v2) hierarchy.
+ */
+
+#include "config.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#ifdef HAVE_NUMA_H
+# include <numa.h>
+#endif
+
+#include "tst_test.h"
+#include "tst_cgroup.h"
+#include "numa_helper.h"
+#include "lapi/syscalls.h"
+
+#ifdef HAVE_NUMA_V2
+
+/* The file whose page-cache placement we test. */
+#define DATAFILE "datafile"
+
+/* Size of the data file. Large enough to measure an even spread. */
+#define FILE_MB 100
+
+/* Each node needs enough free memory to hold the whole file plus slack. */
+#define MIN_FREE_KB (FILE_MB * 1024 + 50 * 1024)
+
+/* Query the residing NUMA node of at most this many pages per syscall. */
+#define MOVE_PAGES_CHUNK 1024
+
+/*
+ * The pages must land on the expected node(s); the other nodes may hold at
+ * most this fraction (in percent) of them.
+ */
+#define UNEXPECTED_TOLERANCE 5
+
+/*
+ * When the page cache is spread across several expected nodes, the kernel
+ * spreads it evenly. Every expected node must hold at least
+ * (100 - BALANCE_TOLERANCE) percent of its even share.
+ */
+#define BALANCE_TOLERANCE 20
+
+static int *nodes;
+static int nnodes;
+static long page_size;
+static unsigned long *counts;
+
+/* Per-node free memory in kB from /sys/.../nodeN/meminfo. */
+static long node_memfree(int nd)
+{
+ long free_kb = 0;
+ char path[PATH_MAX];
+
+ snprintf(path, sizeof(path),
+ PATH_SYS_SYSTEM "/node/node%d/meminfo", nd);
+ SAFE_FILE_LINES_SCANF(path, "Node %*d MemFree: %ld", &free_kb);
+
+ return free_kb;
+}
+
+/* Build the cpuset.cpus string listing all online CPUs of node 'nd'. */
+static void node_cpus(int nd, char *cpus, size_t size)
+{
+ int ncpus = 0, i;
+ long online;
+ char path[PATH_MAX], buf[32];
+
+ while (tst_path_exists(PATH_SYS_SYSTEM "/cpu/cpu%d", ncpus))
+ ncpus++;
+
+ cpus[0] = '\0';
+ for (i = 0; i < ncpus; i++) {
+ snprintf(path, sizeof(path),
+ PATH_SYS_SYSTEM "/node/node%d/cpu%d", nd, i);
+ if (!tst_path_exists("%s", path))
+ continue;
+
+ snprintf(path, sizeof(path),
+ PATH_SYS_SYSTEM "/node/node%d/cpu%d/online", nd, i);
+ if (tst_path_exists("%s", path)) {
+ SAFE_FILE_SCANF(path, "%ld", &online);
+ if (!online)
+ continue;
+ }
+
+ snprintf(buf, sizeof(buf), "%d,", i);
+ if (strlen(cpus) + strlen(buf) >= size)
+ tst_brk(TBROK, "cpuset.cpus buffer too small");
+ strcat(cpus, buf);
+ }
+
+ if (!strlen(cpus))
+ tst_brk(TCONF, "node %d has no usable CPUs", nd);
+
+ /* Remove the trailing comma. */
+ cpus[strlen(cpus) - 1] = '\0';
+}
+
+/*
+ * Populate the page cache of DATAFILE under the current cpuset constraints
+ * by dropping the caches and reading the whole file.
+ */
+static void populate_page_cache(void)
+{
+ int fd;
+ char buf[BUFSIZ];
+ ssize_t ret;
+
+ sync();
+ SAFE_FILE_PRINTF("/proc/sys/vm/drop_caches", "3");
+
+ fd = SAFE_OPEN(DATAFILE, O_RDONLY);
+ while ((ret = read(fd, buf, sizeof(buf))) > 0)
+ ;
+ if (ret < 0)
+ tst_brk(TBROK | TERRNO, "read %s", DATAFILE);
+ SAFE_CLOSE(fd);
+}
+
+/*
+ * Count only DATAFILE's own page-cache pages per NUMA node.
+ *
+ * move_pages(2) with a NULL node array does not move anything; it only
+ * reports, in status[], the NUMA node each already-present page resides on.
+ */
+static void account_file_pages(void)
+{
+ int fd;
+ struct stat st;
+ char *addr;
+ unsigned long npages, i, off;
+ void **pages;
+ int *status;
+
+ memset(counts, 0, MAXNODES * sizeof(*counts));
+
+ fd = SAFE_OPEN(DATAFILE, O_RDONLY);
+ SAFE_FSTAT(fd, &st);
+ npages = (st.st_size + page_size - 1) / page_size;
+ addr = SAFE_MMAP(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
+ SAFE_CLOSE(fd);
+
+ pages = SAFE_MALLOC(npages * sizeof(*pages));
+ status = SAFE_MALLOC(npages * sizeof(*status));
+
+ /*
+ * Fault in every page so it maps the page-cache page populated by
+ * populate_page_cache(); move_pages() then reports the node that
+ * cache page resides on.
+ */
+ for (i = 0; i < npages; i++) {
+ volatile char c = addr[i * page_size];
+
+ (void)c;
+ pages[i] = addr + i * page_size;
+ status[i] = -1;
+ }
+
+ for (off = 0; off < npages; off += MOVE_PAGES_CHUNK) {
+ unsigned long n = npages - off;
+
+ if (n > MOVE_PAGES_CHUNK)
+ n = MOVE_PAGES_CHUNK;
+
+ /*
+ * tst_syscall() reports TCONF if move_pages() is not
+ * implemented (CONFIG_NUMA_MIGRATION disabled).
+ */
+ TEST(tst_syscall(__NR_move_pages, 0, n, pages + off,
+ NULL, status + off, 0));
+ if (TST_RET == -1)
+ tst_brk(TBROK | TTERRNO, "move_pages");
+ }
+
+ /*
+ * move_pages() can return success while reporting a per-page error
+ * (e.g. -ENOENT or -EFAULT) in status[]. Treat any such page as a
+ * failure: otherwise the distribution would be computed from only a
+ * subset of DATAFILE and could pass incorrectly.
+ */
+ for (i = 0; i < npages; i++) {
+ if (status[i] < 0 || status[i] >= MAXNODES)
+ tst_brk(TBROK, "page %lu not accounted (status %d)",
+ i, status[i]);
+ counts[status[i]]++;
+ }
+
+ SAFE_MUNMAP(addr, st.st_size);
+ free(pages);
+ free(status);
+}
+
+static void check_result(const int *expect, int n_expect, const char *desc)
+{
+ unsigned long total = 0, expected_sum = 0, unexpected;
+ int i;
+
+ for (i = 0; i < MAXNODES; i++)
+ total += counts[i];
+
+ if (!total) {
+ tst_res(TFAIL, "%s: no page cache was populated", desc);
+ return;
+ }
+
+ for (i = 0; i < n_expect; i++)
+ expected_sum += counts[expect[i]];
+
+ unexpected = total - expected_sum;
+
+ tst_res(TINFO,
+ "%s: unexpected nodes hold %.2f%% of the page cache (tolerance %d%%)",
+ desc, (double)unexpected * 100 / total, UNEXPECTED_TOLERANCE);
+
+ if (unexpected * 100 > total * UNEXPECTED_TOLERANCE) {
+ tst_res(TFAIL,
+ "%s: too much page cache on unexpected node(s)", desc);
+ return;
+ }
+
+ /*
+ * Every expected node must hold at least (100 - BALANCE_TOLERANCE)%
+ * of its even share. This enforces an even spread across several
+ * expected nodes and rejects an expected node that received nothing.
+ * A single expected node always passes this check.
+ */
+ for (i = 0; i < n_expect; i++) {
+ if (counts[expect[i]] * n_expect * 100 <
+ expected_sum * (100 - BALANCE_TOLERANCE)) {
+ tst_res(TFAIL,
+ "%s: node %d holds too little page cache",
+ desc, expect[i]);
+ return;
+ }
+ }
+
+ tst_res(TPASS, "%s", desc);
+}
+
+/*
+ * Configure the cpuset, repopulate the page cache and verify the
+ * distribution.
+ *
+ * cpus_node - bind cpuset.cpus to the CPUs of this node (local node)
+ * second_node - if >= 0, also allow this node in cpuset.mems
+ * spread - value written to cpuset.memory_spread_page
+ * expect - array of nodes that must hold the page cache
+ * n_expect - number of entries in the expect array
+ * desc - human readable description used in the result messages
+ */
+static void do_spread_test(int cpus_node, int second_node, int spread,
+ const int *expect, int n_expect, const char *desc)
+{
+ char cpus[BUFSIZ];
+
+ node_cpus(cpus_node, cpus, sizeof(cpus));
+ SAFE_CG_PRINT(tst_cg, "cpuset.cpus", cpus);
+
+ if (second_node >= 0)
+ SAFE_CG_PRINTF(tst_cg, "cpuset.mems", "%d,%d",
+ cpus_node, second_node);
+ else
+ SAFE_CG_PRINTF(tst_cg, "cpuset.mems", "%d", cpus_node);
+
+ SAFE_CG_PRINTF(tst_cg, "cpuset.memory_spread_page", "%d", spread);
+
+ populate_page_cache();
+ account_file_pages();
+ check_result(expect, n_expect, desc);
+}
+
+static void run(void)
+{
+ int a = nodes[0];
+ int b = nodes[1];
+ const int expect_a[] = { a };
+ const int expect_b[] = { b };
+ const int expect_ab[] = { a, b };
+
+ do_spread_test(a, -1, 0, expect_a, 1,
+ "spread off, single node: local allocation");
+ do_spread_test(a, -1, 1, expect_a, 1,
+ "spread on, single node: all on that node");
+ do_spread_test(b, -1, 0, expect_b, 1,
+ "spread off, single node: local allocation");
+ do_spread_test(b, -1, 1, expect_b, 1,
+ "spread on, single node: all on that node");
+ do_spread_test(a, b, 0, expect_a, 1,
+ "spread off, two nodes: local allocation");
+ do_spread_test(a, b, 1, expect_ab, 2,
+ "spread on, two nodes: even spread");
+}
+
+static void setup(void)
+{
+ int i;
+
+ page_size = getpagesize();
+
+ if (get_allowed_nodes_arr(NH_MEMS | NH_CPUS, &nnodes, &nodes) < 0)
+ tst_brk(TBROK | TERRNO, "get_allowed_nodes_arr");
+ if (nnodes < 2)
+ tst_brk(TCONF, "requires at least 2 NUMA nodes with CPUs and memory");
+
+ /*
+ * cpuset.memory_spread_page is a cgroup v1 only knob; it was not
+ * carried over to the unified (v2) hierarchy. .needs_cgroup_ver
+ * below already restricts the test to a v1 cpuset, this is just a
+ * belt-and-suspenders check.
+ */
+ if (!SAFE_CG_HAS(tst_cg, "cpuset.memory_spread_page"))
+ tst_brk(TCONF, "cpuset.memory_spread_page not available");
+
+ for (i = 0; i < 2; i++) {
+ if (node_memfree(nodes[i]) < MIN_FREE_KB)
+ tst_brk(TCONF,
+ "node %d has less than %d kB free memory",
+ nodes[i], MIN_FREE_KB);
+ }
+
+ counts = SAFE_MALLOC(MAXNODES * sizeof(*counts));
+
+ tst_fill_file(DATAFILE, 0, 1024 * 1024, FILE_MB);
+
+ SAFE_CG_PRINTF(tst_cg, "cgroup.procs", "%d", getpid());
+}
+
+static void cleanup(void)
+{
+ free(nodes);
+ free(counts);
+}
+
+static struct tst_test test = {
+ .needs_root = 1,
+ .needs_tmpdir = 1,
+ /*
+ * On tmpfs/ramfs the file data is the page cache itself, so
+ * drop_caches cannot evict it and the pages keep the node they got
+ * at write time rather than the one chosen by the cpuset policy.
+ * The page-cache placement is then not measurable.
+ */
+ .skip_filesystems = (const char *const []){ "tmpfs", "ramfs", NULL },
+ .setup = setup,
+ .cleanup = cleanup,
+ .test_all = run,
+ .needs_cgroup_ctrls = (const char *const []){ "cpuset", NULL },
+ .needs_cgroup_ver = TST_CG_V1,
+};
+
+#else
+ TST_TEST_TCONF(NUMA_ERROR_MSG);
+#endif
diff --git a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh b/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
deleted file mode 100755
index 4c49bb8fd..000000000
--- a/testcases/kernel/controllers/cpuset/cpuset_memory_spread_test/cpuset_memory_spread_testset.sh
+++ /dev/null
@@ -1,362 +0,0 @@
-#!/bin/sh
-
-################################################################################
-# #
-# Copyright (c) 2009 FUJITSU LIMITED #
-# #
-# This program is free software; you can redistribute it and#or modify #
-# it under the terms of the GNU General Public License as published by #
-# the Free Software Foundation; either version 2 of the License, or #
-# (at your option) any later version. #
-# #
-# This program is distributed in the hope that it will be useful, but #
-# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY #
-# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License #
-# for more details. #
-# #
-# You should have received a copy of the GNU General Public License #
-# along with this program; if not, write to the Free Software #
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #
-# #
-# Author: Miao Xie <miaox@cn.fujitsu.com> #
-# #
-################################################################################
-
-export TCID="cpuset_memory_spread"
-export TST_TOTAL=6
-export TST_COUNT=1
-
-. cpuset_funcs.sh
-
-check
-
-exit_status=0
-nr_cpus=$NR_CPUS
-nr_mems=$N_NODES
-
-# In general, the cache hog will use more than 10000 kb slab space on the nodes
-# on which it is running. The other nodes' slab space has littler change.(less
-# than 1000 kb).
-upperlimit=10000
-
-# set lowerlimit according to pagesize
-# pagesize(bytes) | lowerlimit(kb)
-# ------------------------------------
-# 4096 | 2048
-# 16384 | 8192
-
-PAGE_SIZE=`tst_getconf PAGESIZE`
-lowerlimit=$((PAGE_SIZE * 512 / 1024))
-
-cpus_all="$(seq -s, 0 $((nr_cpus-1)))"
-mems_all="$(seq -s, 0 $((nr_mems-1)))"
-
-nodedir="/sys/devices/system/node"
-
-FIFO="./myfifo"
-
-# memsinfo is an array implementation of the form of a multi-line string
-# _0: value0
-# _1: value1
-# _2: value2
-#
-memsinfo=""
-
-# set value to memsinfo ($1 - index, $2 - value)
-set_memsinfo_val()
-{
- local nl='
-'
- # clearing existent value (if present)
- memsinfo=`echo "$memsinfo" | sed -r "/^\_$1\: /d"`
-
- if [ -z "$memsinfo" ]; then
- memsinfo="_$1: $2"
- else
- memsinfo="$memsinfo${nl}_$1: $2"
- fi
-}
-
-# get value from memsinfo ($1 - index)
-get_memsinfo_val()
-{
- local value=
- value=`echo "$memsinfo" | grep -e "^_$1: "`
- value=`echo "$value" | sed -r "s/^.*\: (.*)$/\1/"`
- echo "$value"
-}
-
-
-init_memsinfo_array()
-{
- local i=
-
- for i in `seq 0 $((nr_mems-1))`
- do
- set_memsinfo_val $i 0
- done
-}
-
-# get_meminfo <nodeid> <item>
-get_meminfo()
-{
- local nodeid="$1"
- local nodepath="$nodedir/node$nodeid"
- local nodememinfo="$nodepath/meminfo"
- local item="$2"
- local info=`cat $nodememinfo | grep $item | awk '{print $4}'`
- set_memsinfo_val $nodeid $info
-}
-
-# freemem_check
-# We need enough memory space on every node to run this test, so we must check
-# whether every node has enough free memory or not.
-# return value: 1 - Some node doesn't have enough free memory
-# 0 - Every node has enough free memory, We can do this test
-freemem_check()
-{
- local i=
-
- for i in `seq 0 $((nr_mems-1))`
- do
- get_meminfo $i "MemFree"
- done
-
- for i in `seq 0 $((nr_mems-1))`
- do
- # I think we need 100MB free memory to run test
- if [ $(get_memsinfo_val $i) -lt 100000 ]; then
- return 1
- fi
- done
-}
-
-# get_memsinfo
-get_memsinfo()
-{
- local i=
-
- for i in `seq 0 $((nr_mems-1))`
- do
- get_meminfo $i "FilePages"
- done
-}
-
-# account_meminfo <nodeId>
-account_meminfo()
-{
- local nodeId="$1"
- local tmp="$(get_memsinfo_val $nodeId)"
- get_meminfo $@ "FilePages"
- set_memsinfo_val $nodeId $(($(get_memsinfo_val $nodeId)-$tmp))
-}
-
-# account_memsinfo
-account_memsinfo()
-{
- local i=
-
- for i in `seq 0 $((nr_mems-1))`
- do
- account_meminfo $i
- done
-}
-
-
-# result_check <nodelist>
-# return 0: success
-# 1: fail
-result_check()
-{
- local nodelist="`echo $1 | sed -e 's/,/ /g'`"
- local i=
-
- for i in $nodelist
- do
- if [ $(get_memsinfo_val $i) -le $upperlimit ]; then
- return 1
- fi
- done
-
- local allnodelist="`echo $mems_all | sed -e 's/,/ /g'`"
- allnodelist=" "$allnodelist" "
- nodelist=" "$nodelist" "
-
- local othernodelist="$allnodelist"
- for i in $nodelist
- do
- othernodelist=`echo "$othernodelist" | sed -e "s/ $i / /g"`
- done
-
- for i in $othernodelist
- do
- if [ $(get_memsinfo_val $i) -gt $lowerlimit ]; then
- return 1
- fi
- done
-}
-
-# general_memory_spread_test <cpusetpath> <is_spread> <cpu_list> <node_list> \
-# <expect_nodes> <test_pid>
-# expect_nodes: we expect to use the slab or cache on which node
-general_memory_spread_test()
-{
- local cpusetpath="$CPUSET/1"
- local is_spread="$1"
- local cpu_list="$2"
- local node_list="$3"
- local expect_nodes="$4"
- local test_pid="$5"
-
- cpuset_set "$cpusetpath" "$cpu_list" "$node_list" "0" 2> $CPUSET_TMP/stderr
- if [ $? -ne 0 ]; then
- cpuset_log_error $CPUSET_TMP/stderr
- tst_resm TFAIL "set general group parameter failed."
- return 1
- fi
-
- /bin/echo "$is_spread" > "$cpusetpath/cpuset.memory_spread_page" 2> $CPUSET_TMP/stderr
- if [ $? -ne 0 ]; then
- cpuset_log_error $CPUSET_TMP/stderr
- tst_resm TFAIL "set spread value failed."
- return 1
- fi
-
- /bin/echo "$test_pid" > "$cpusetpath/tasks" 2> $CPUSET_TMP/stderr
- if [ $? -ne 0 ]; then
- cpuset_log_error $CPUSET_TMP/stderr
- tst_resm TFAIL "attach task failed."
- return 1
- fi
-
- # we'd better drop the caches before we test page cache.
- sync
- /bin/echo 3 > /proc/sys/vm/drop_caches 2> $CPUSET_TMP/stderr
- if [ $? -ne 0 ]; then
- cpuset_log_error $CPUSET_TMP/stderr
- tst_resm TFAIL "drop caches failed."
- return 1
- fi
-
- get_memsinfo
- /bin/kill -s SIGUSR1 $test_pid
- read exit_num < $FIFO
- if [ $exit_num -eq 0 ]; then
- tst_resm TFAIL "hot mem task failed."
- return 1
- fi
-
- account_memsinfo
- result_check $expect_nodes
- if [ $? -ne 0 ]; then
- tst_resm TFAIL "hog the memory on the unexpected node(FilePages_For_Nodes(KB): ${memsinfo}, Expect Nodes: $expect_nodes)."
- return 1
- fi
-}
-
-base_test()
-{
- local pid=
- local result_num=
-
- setup
- if [ $? -ne 0 ]; then
- exit_status=1
- else
- cpuset_mem_hog &
- pid=$!
- general_memory_spread_test "$@" "$pid"
- result_num=$?
- if [ $result_num -ne 0 ]; then
- exit_status=1
- fi
-
- /bin/kill -s SIGUSR2 $pid
- wait $pid
-
- cleanup
- if [ $? -ne 0 ]; then
- exit_status=1
- elif [ $result_num -eq 0 ]; then
- tst_resm TPASS "Cpuset memory spread page test succeeded."
- fi
- fi
- TST_COUNT=$(($TST_COUNT + 1))
-}
-
-# test general spread page cache in a cpuset
-test_spread_page1()
-{
- while read spread cpus nodes exp_nodes
- do
- base_test "$spread" "$cpus" "$nodes" "$exp_nodes"
- done <<- EOF
- 0 0 0 0
- 1 0 0 0
- 0 0 1 1
- 1 0 1 1
- 0 0 0,1 0
- 1 0 0,1 0,1
- EOF
- # while read spread cpus nodes exp_nodes
-}
-
-test_spread_page2()
-{
- local pid=
- local result_num=
-
- setup
- if [ $? -ne 0 ]; then
- exit_status=1
- else
- cpuset_mem_hog &
- pid=$!
- general_memory_spread_test "1" "$cpus_all" "0" "0" "$pid"
- result_num=$?
- if [ $result_num -ne 0 ]; then
- exit_status=1
- else
- general_memory_spread_test "1" "$cpus_all" "1" "1" "$pid"
- result_num=$?
- if [ $result_num -ne 0 ]; then
- exit_status=1
- fi
- fi
-
- /bin/kill -s SIGUSR2 $pid
- wait $pid
-
- cleanup
- if [ $? -ne 0 ]; then
- exit_status=1
- elif [ $result_num -eq 0 ]; then
- tst_resm TPASS "Cpuset memory spread page test succeeded."
- fi
- fi
-}
-
-init_memsinfo_array
-freemem_check
-if [ $? -ne 0 ]; then
- tst_brkm TCONF "Some node doesn't has enough free memory(100MB) to do test(MemFree_For_Nodes(KB): ${memsinfo[*]})."
-fi
-
-dd if=/dev/zero of=./DATAFILE bs=1M count=100
-if [ $? -ne 0 ]; then
- tst_brkm TFAIL "Creating DATAFILE failed."
-fi
-
-rm -f $FIFO
-mkfifo $FIFO
-if [ $? -ne 0 ]; then
- rm -f DATAFILE
- tst_brkm TFAIL "failed to mkfifo $FIFO"
-fi
-
-test_spread_page1
-test_spread_page2
-
-rm -f DATAFILE $FIFO
-
-exit $exit_status
--
2.43.0
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply related [flat|nested] 7+ messages in thread* Re: [LTP] cpuset_memory_spread: rewrite as a C test using the LTP cgroup library
2026-11-04 0:24 [LTP] [PATCH v7 1/1] " Changwei Zou via ltp
@ 2026-11-04 3:30 ` linuxtestproject.agent
0 siblings, 0 replies; 7+ messages in thread
From: linuxtestproject.agent @ 2026-11-04 3:30 UTC (permalink / raw)
To: Changwei Zou; +Cc: ltp
Hi Changwei,
On Wed Nov 4 11:24:00 2026 +1100, Changwei Zou wrote:
> cpuset_memory_spread: rewrite as a C test using the LTP cgroup library
> + * where the file's pages landed. Instead of looking at the noisy global
> + * per-node page-cache counters in ``/sys``, it accounts for only the data
> + * file's own pages: after reading the file it mmaps it, faults every page
> + * in and uses ``move_pages()`` (with a NULL node array, so nothing is
Use :manpage:`move_pages(2)` instead of ``move_pages()`` in the doc comment.
> +/* Build the cpuset.cpus string listing all online CPUs of node 'nd'. */
> +static void node_cpus(int nd, char *cpus, size_t size)
> +{
> + int ncpus = 0, i;
> + long online;
> + char path[PATH_MAX], buf[32];
> +
> + while (tst_path_exists(PATH_SYS_SYSTEM "/cpu/cpu%d", ncpus))
> + ncpus++;
numa_helper.h (linked via testcases/kernel/include/lib.mk) already provides
write_node_cpusets(), which gathers a node's online CPUs and sets
cpuset.cpus and cpuset.mems. Use write_node_cpusets() instead of duplicating
this helper.
> + fd = SAFE_OPEN(DATAFILE, O_RDONLY);
> + while ((ret = read(fd, buf, sizeof(buf))) > 0)
> + ;
> + if (ret < 0)
> + tst_brk(TBROK | TERRNO, "read %s", DATAFILE);
> + SAFE_CLOSE(fd);
Use SAFE_READ(0, fd, buf, sizeof(buf)) in the loop instead of raw read() and
a manual error check.
> +static void run(void)
> +{
> + int a = nodes[0];
> + int b = nodes[1];
> + const int expect_a[] = { a };
> + const int expect_b[] = { b };
> + const int expect_ab[] = { a, b };
> +
> + do_spread_test(a, -1, 0, expect_a, 1,
> + "spread off, single node: local allocation");
> + do_spread_test(a, -1, 1, expect_a, 1,
> + "spread on, single node: all on that node");
> + do_spread_test(b, -1, 0, expect_b, 1,
> + "spread off, single node: local allocation");
> + do_spread_test(b, -1, 1, expect_b, 1,
> + "spread on, single node: all on that node");
> + do_spread_test(a, b, 0, expect_a, 1,
> + "spread off, two nodes: local allocation");
> + do_spread_test(a, b, 1, expect_ab, 2,
> + "spread on, two nodes: even spread");
> +}
Define a struct tcase array and use .test + .tcnt = ARRAY_SIZE(tcases) in
struct tst_test instead of calling do_spread_test() multiple times from
.test_all. Also, in the original test_spread_page1(), cases 3 and 4 kept
cpuset.cpus on node a while setting cpuset.mems to node b to verify
non-local memory node allocation, whereas passing b as cpus_node here binds
both cpuset.cpus and cpuset.mems to node b and repeats cases 1 and 2.
> + counts = SAFE_MALLOC(MAXNODES * sizeof(*counts));
> +
> + tst_fill_file(DATAFILE, 0, 1024 * 1024, FILE_MB);
> +
> + SAFE_CG_PRINTF(tst_cg, "cgroup.procs", "%d", getpid());
tst_fill_file() does not call tst_brk() on failure; it unlinks DATAFILE and
returns -1, which later causes SAFE_OPEN(DATAFILE, O_RDONLY) to fail with
ENOENT. Check the return value of tst_fill_file() and call tst_brk(TBROK |
TERRNO, ...) on non-zero return.
> + .test_all = run,
> + .needs_cgroup_ctrls = (const char *const []){ "cpuset", NULL },
> + .needs_cgroup_ver = TST_CG_V1,
> +};
> +
> +#else
> + TST_TEST_TCONF(NUMA_ERROR_MSG);
> +#endif
Pass a literal string to TST_TEST_TCONF() (e.g.
TST_TEST_TCONF("test requires libnuma >= 2")) without leading indentation
instead of the NUMA_ERROR_MSG macro.
Verdict - Needs revision
---
Note:
The agent can sometimes produce false positives although often its
findings are genuine. If you find issues with the review, please
comment this email or ignore the suggestions.
Regards,
LTP AI Reviewer
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-11-12 10:01 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-05 0:55 [LTP] [PATCH v9 0/1] cpuset_memory_spread: rewrite as a C test using the LTP cgroup library Changwei Zou via ltp
2026-11-05 0:55 ` [LTP] [PATCH v9 1/1] " Changwei Zou via ltp
2026-11-05 7:31 ` [LTP] " linuxtestproject.agent
2026-11-11 13:57 ` [LTP] [PATCH v9 1/1] " Petr Vorel
2026-11-12 10:00 ` Changwei Zou via ltp
-- strict thread matches above, loose matches on Subject: below --
2026-11-04 7:19 [LTP] [PATCH v8 " Changwei Zou via ltp
2026-11-04 11:41 ` [LTP] " linuxtestproject.agent
2026-11-04 0:24 [LTP] [PATCH v7 1/1] " Changwei Zou via ltp
2026-11-04 3:30 ` [LTP] " linuxtestproject.agent
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.