[LTP] [PATCH v7] memcg/memcontrol05: add cgroup v2 task migration charge accounting test
Cyril Hrubis
chrubis@suse.cz
Fri Oct 30 16:29:37 CET 2026
Hi!
> Verify that in cgroup v2, memory charges remain pinned to the cgroup
> that allocated them when a task migrates, and that new allocations after
> migration are charged to the destination cgroup.
>
> Signed-off-by: Pavithra <pavrampu@linux.ibm.com>
> ---
> v6 -> v7:
> - Replace named checkpoint enum with single index 0
> - Collapse wake/wait pairs into TST_CHECKPOINT_WAKE_AND_WAIT(0)
> - Drop worker_stage tracking
> - Replace manual pass/fail blocks with TST_EXP_LE_LU()
> - Remove MB(4) factor from migration charge check
> - Kill worker with SAFE_KILL/SAFE_WAITPID instead of checkpoint-based exit handshake
> link to v6: https://lore.kernel.org/ltp/20260817155701.943752-1-pavrampu@linux.ibm.com/
> ---
> runtest/controllers | 1 +
> testcases/kernel/controllers/memcg/.gitignore | 1 +
> .../kernel/controllers/memcg/memcontrol05.c | 160 ++++++++++++++++++
> 3 files changed, 162 insertions(+)
> create mode 100644 testcases/kernel/controllers/memcg/memcontrol05.c
>
> diff --git a/runtest/controllers b/runtest/controllers
> index 93c52c439..a9050ef0e 100644
> --- a/runtest/controllers
> +++ b/runtest/controllers
> @@ -24,6 +24,7 @@ memcontrol01 memcontrol01
> memcontrol02 memcontrol02
> memcontrol03 memcontrol03
> memcontrol04 memcontrol04
> +memcontrol05 memcontrol05
>
> cgroup_fj_function_debug cgroup_fj_function.sh debug
> cgroup_fj_function_cpuset cgroup_fj_function.sh cpuset
> diff --git a/testcases/kernel/controllers/memcg/.gitignore b/testcases/kernel/controllers/memcg/.gitignore
> index 3883cede6..8b9f6005c 100644
> --- a/testcases/kernel/controllers/memcg/.gitignore
> +++ b/testcases/kernel/controllers/memcg/.gitignore
> @@ -9,3 +9,4 @@ memcontrol01
> memcontrol02
> memcontrol03
> memcontrol04
> +memcontrol05
> diff --git a/testcases/kernel/controllers/memcg/memcontrol05.c b/testcases/kernel/controllers/memcg/memcontrol05.c
> new file mode 100644
> index 000000000..d452039e6
> --- /dev/null
> +++ b/testcases/kernel/controllers/memcg/memcontrol05.c
> @@ -0,0 +1,160 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Copyright (c) 2026 IBM
> + * Author : Pavithra <pavrampu@linux.ibm.com>
> + */
> +
> +/*\
> + * Origin: ``testcases/kernel/controllers/memctl/memctl_testplan.txt``
> + * Verify memory controller charge accounting during task migration.
> + *
> + * Two child cgroups (``group_a``, ``group_b``) are created under the test cgroup.
> + * A worker process is placed in ``group_a`` and allocates a known amount of
> + * anonymous memory, touching every page to ensure physical allocation.
> + * Root is required to create cgroup subdirectories and to write a PID
> + * into ``cgroup.procs`` to migrate the worker between cgroups.
> + *
> + * Test 1 - No charge transfer on migration:
> + * The worker is migrated to ``group_b`` by writing its PID to
> + * ``group_b/cgroup.procs``. In cgroup v2 memory charges are never
> + * transferred on task migration; they remain pinned to the cgroup
> + * that allocated them. ``group_b/memory.current`` must not increase
> + * beyond its baseline after the migration.
> + *
> + * Test 2 - New allocations charged to destination cgroup:
> + * After migration the worker allocates an additional chunk of memory.
> + * This new allocation must be charged to ``group_b``, so
> + * ``group_b/memory.current`` must increase from the post-migration
> + * baseline.
> + */
> +
> +#define _GNU_SOURCE
> +
> +#include <stdlib.h>
> +#include <sys/types.h>
> +#include <sys/wait.h>
> +#include "tst_test.h"
> +#include "memcontrol_common.h"
> +
> +#define ALLOC_SIZE MB(60)
> +#define ALLOC_SIZE2 MB(20)
> +#define MIN_MEM_AVAIL 128
> +
> +static struct tst_cg_group *group_a;
> +static struct tst_cg_group *group_b;
> +static pid_t worker_pid;
> +
> +static void touch_pages(char *buf, size_t size)
> +{
> + size_t i;
> +
> + for (i = 0; i < size; i += getpagesize())
> + buf[i] = 1;
> +}
> +
> +static void worker(void)
> +{
> + char *buf1, *buf2;
> +
> + SAFE_CG_PRINTF(group_a, "cgroup.procs", "%d", getpid());
> +
> + buf1 = SAFE_MMAP(NULL, ALLOC_SIZE, PROT_READ | PROT_WRITE,
> + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> + touch_pages(buf1, ALLOC_SIZE);
> +
> + TST_CHECKPOINT_WAKE_AND_WAIT(0);
> +
> + buf2 = SAFE_MMAP(NULL, ALLOC_SIZE2, PROT_READ | PROT_WRITE,
> + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> + touch_pages(buf2, ALLOC_SIZE2);
> +
> + TST_CHECKPOINT_WAKE(0);
> +
> + pause();
> +}
> +
> +static void test_memcg_task_migration(void)
> +{
> + long baseline_b, after_migrate, after_migrate_a, after_alloc2, current_a;
> +
> + worker_pid = 0;
> +
> + group_a = tst_cg_group_mk(tst_cg, "group_a");
> + group_b = tst_cg_group_mk(tst_cg, "group_b");
> +
> + if (SAFE_CG_HAS(tst_cg, "memory.swap.max")) {
> + SAFE_CG_PRINT(group_a, "memory.swap.max", "0");
> + SAFE_CG_PRINT(group_b, "memory.swap.max", "0");
> + }
> +
> + SAFE_CG_SCANF(group_b, "memory.current", "%ld", &baseline_b);
> + tst_res(TINFO, "group_b baseline memory.current=%ld", baseline_b);
> +
> + worker_pid = SAFE_FORK();
> + if (!worker_pid) {
> + worker();
> + exit(0);
> + }
> +
> + TST_CHECKPOINT_WAIT(0);
> +
> + SAFE_CG_SCANF(group_a, "memory.current", "%ld", ¤t_a);
> + TST_EXP_LE_LU(ALLOC_SIZE, current_a);
> +
> + SAFE_CG_PRINTF(group_b, "cgroup.procs", "%d", worker_pid);
> + tst_res(TINFO, "Migrated worker PID %d to group_b", worker_pid);
> +
> + SAFE_CG_SCANF(group_b, "memory.current", "%ld", &after_migrate);
> + tst_res(TINFO,
> + "group_b memory.current=%ld after migration (baseline=%ld)",
> + after_migrate, baseline_b);
> +
> + TST_EXP_LE_LU(after_migrate, baseline_b);
I do not think that it's actually guaranteed that nothing is charged in
the cgroup_b after the migration. I guess that we can use values_close()
function here.
> + SAFE_CG_SCANF(group_a, "memory.current", "%ld", &after_migrate_a);
> + tst_res(TINFO, "group_a memory.current=%ld after migration", after_migrate_a);
> + TST_EXP_EXPR(after_migrate_a >= (long)ALLOC_SIZE,
> + "group_a memory.current (%ld) still holds pre-migration charges (>= ALLOC_SIZE %ld)",
> + after_migrate_a, (long)ALLOC_SIZE);
> +
> + TST_CHECKPOINT_WAKE_AND_WAIT(0);
> +
> + SAFE_CG_SCANF(group_b, "memory.current", "%ld", &after_alloc2);
> + tst_res(TINFO, "group_b memory.current=%ld after second alloc (baseline=%ld)",
> + after_alloc2, after_migrate);
> +
> + TST_EXP_EXPR(after_alloc2 >= after_migrate + (long)ALLOC_SIZE2,
> + "group_b memory.current (%ld) increased by >= ALLOC_SIZE2 (%ld)",
> + after_alloc2, (long)ALLOC_SIZE2);
And here as well, I suppose that we can only assert that the
after_alloc2 >= ALLOC_SIZE2.
Also CCing Michal Koutny, our cgroup expert.
--
Cyril Hrubis
chrubis@suse.cz
More information about the ltp
mailing list