| // SPDX-License-Identifier: GPL-2.0 |
| #define _GNU_SOURCE |
| |
| #include <linux/limits.h> |
| #include <fcntl.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <sys/stat.h> |
| #include <sys/types.h> |
| #include <unistd.h> |
| #include <sys/wait.h> |
| #include <errno.h> |
| #include <sys/sysinfo.h> |
| #include <pthread.h> |
| |
| #include "../kselftest.h" |
| #include "cgroup_util.h" |
| |
| |
| /* |
| * Memory cgroup charging is performed using percpu batches 64 pages |
| * big (look at MEMCG_CHARGE_BATCH), whereas memory.stat is exact. So |
| * the maximum discrepancy between charge and vmstat entries is number |
| * of cpus multiplied by 64 pages. |
| */ |
| #define MAX_VMSTAT_ERROR (4096 * 64 * get_nprocs()) |
| |
| |
| static int alloc_dcache(const char *cgroup, void *arg) |
| { |
| unsigned long i; |
| struct stat st; |
| char buf[128]; |
| |
| for (i = 0; i < (unsigned long)arg; i++) { |
| snprintf(buf, sizeof(buf), |
| "/something-non-existent-with-a-long-name-%64lu-%d", |
| i, getpid()); |
| stat(buf, &st); |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * This test allocates 100000 of negative dentries with long names. |
| * Then it checks that "slab" in memory.stat is larger than 1M. |
| * Then it sets memory.high to 1M and checks that at least 1/2 |
| * of slab memory has been reclaimed. |
| */ |
| static int test_kmem_basic(const char *root) |
| { |
| int ret = KSFT_FAIL; |
| char *cg = NULL; |
| long slab0, slab1, current; |
| |
| cg = cg_name(root, "kmem_basic_test"); |
| if (!cg) |
| goto cleanup; |
| |
| if (cg_create(cg)) |
| goto cleanup; |
| |
| if (cg_run(cg, alloc_dcache, (void *)100000)) |
| goto cleanup; |
| |
| slab0 = cg_read_key_long(cg, "memory.stat", "slab "); |
| if (slab0 < (1 << 20)) |
| goto cleanup; |
| |
| cg_write(cg, "memory.high", "1M"); |
| |
| /* wait for RCU freeing */ |
| sleep(1); |
| |
| slab1 = cg_read_key_long(cg, "memory.stat", "slab "); |
| if (slab1 < 0) |
| goto cleanup; |
| |
| current = cg_read_long(cg, "memory.current"); |
| if (current < 0) |
| goto cleanup; |
| |
| if (slab1 < slab0 / 2 && current < slab0 / 2) |
| ret = KSFT_PASS; |
| cleanup: |
| cg_destroy(cg); |
| free(cg); |
| |
| return ret; |
| } |
| |
| static void *alloc_kmem_fn(void *arg) |
| { |
| alloc_dcache(NULL, (void *)100); |
| return NULL; |
| } |
| |
| static int alloc_kmem_smp(const char *cgroup, void *arg) |
| { |
| int nr_threads = 2 * get_nprocs(); |
| pthread_t *tinfo; |
| unsigned long i; |
| int ret = -1; |
| |
| tinfo = calloc(nr_threads, sizeof(pthread_t)); |
| if (tinfo == NULL) |
| return -1; |
| |
| for (i = 0; i < nr_threads; i++) { |
| if (pthread_create(&tinfo[i], NULL, &alloc_kmem_fn, |
| (void *)i)) { |
| free(tinfo); |
| return -1; |
| } |
| } |
| |
| for (i = 0; i < nr_threads; i++) { |
| ret = pthread_join(tinfo[i], NULL); |
| if (ret) |
| break; |
| } |
| |
| free(tinfo); |
| return ret; |
| } |
| |
| static int cg_run_in_subcgroups(const char *parent, |
| int (*fn)(const char *cgroup, void *arg), |
| void *arg, int times) |
| { |
| char *child; |
| int i; |
| |
| for (i = 0; i < times; i++) { |
| child = cg_name_indexed(parent, "child", i); |
| if (!child) |
| return -1; |
| |
| if (cg_create(child)) { |
| cg_destroy(child); |
| free(child); |
| return -1; |
| } |
| |
| if (cg_run(child, fn, NULL)) { |
| cg_destroy(child); |
| free(child); |
| return -1; |
| } |
| |
| cg_destroy(child); |
| free(child); |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * The test creates and destroys a large number of cgroups. In each cgroup it |
| * allocates some slab memory (mostly negative dentries) using 2 * NR_CPUS |
| * threads. Then it checks the sanity of numbers on the parent level: |
| * the total size of the cgroups should be roughly equal to |
| * anon + file + kernel + sock. |
| */ |
| static int test_kmem_memcg_deletion(const char *root) |
| { |
| long current, anon, file, kernel, sock, sum; |
| int ret = KSFT_FAIL; |
| char *parent; |
| |
| parent = cg_name(root, "kmem_memcg_deletion_test"); |
| if (!parent) |
| goto cleanup; |
| |
| if (cg_create(parent)) |
| goto cleanup; |
| |
| if (cg_write(parent, "cgroup.subtree_control", "+memory")) |
| goto cleanup; |
| |
| if (cg_run_in_subcgroups(parent, alloc_kmem_smp, NULL, 100)) |
| goto cleanup; |
| |
| current = cg_read_long(parent, "memory.current"); |
| anon = cg_read_key_long(parent, "memory.stat", "anon "); |
| file = cg_read_key_long(parent, "memory.stat", "file "); |
| kernel = cg_read_key_long(parent, "memory.stat", "kernel "); |
| sock = cg_read_key_long(parent, "memory.stat", "sock "); |
| if (current < 0 || anon < 0 || file < 0 || kernel < 0 || sock < 0) |
| goto cleanup; |
| |
| sum = anon + file + kernel + sock; |
| if (abs(sum - current) < MAX_VMSTAT_ERROR) { |
| ret = KSFT_PASS; |
| } else { |
| printf("memory.current = %ld\n", current); |
| printf("anon + file + kernel + sock = %ld\n", sum); |
| printf("anon = %ld\n", anon); |
| printf("file = %ld\n", file); |
| printf("kernel = %ld\n", kernel); |
| printf("sock = %ld\n", sock); |
| } |
| |
| cleanup: |
| cg_destroy(parent); |
| free(parent); |
| |
| return ret; |
| } |
| |
| /* |
| * The test reads the entire /proc/kpagecgroup. If the operation went |
| * successfully (and the kernel didn't panic), the test is treated as passed. |
| */ |
| static int test_kmem_proc_kpagecgroup(const char *root) |
| { |
| unsigned long buf[128]; |
| int ret = KSFT_FAIL; |
| ssize_t len; |
| int fd; |
| |
| fd = open("/proc/kpagecgroup", O_RDONLY); |
| if (fd < 0) |
| return ret; |
| |
| do { |
| len = read(fd, buf, sizeof(buf)); |
| } while (len > 0); |
| |
| if (len == 0) |
| ret = KSFT_PASS; |
| |
| close(fd); |
| return ret; |
| } |
| |
| static void *pthread_wait_fn(void *arg) |
| { |
| sleep(100); |
| return NULL; |
| } |
| |
| static int spawn_1000_threads(const char *cgroup, void *arg) |
| { |
| int nr_threads = 1000; |
| pthread_t *tinfo; |
| unsigned long i; |
| long stack; |
| int ret = -1; |
| |
| tinfo = calloc(nr_threads, sizeof(pthread_t)); |
| if (tinfo == NULL) |
| return -1; |
| |
| for (i = 0; i < nr_threads; i++) { |
| if (pthread_create(&tinfo[i], NULL, &pthread_wait_fn, |
| (void *)i)) { |
| free(tinfo); |
| return(-1); |
| } |
| } |
| |
| stack = cg_read_key_long(cgroup, "memory.stat", "kernel_stack "); |
| if (stack >= 4096 * 1000) |
| ret = 0; |
| |
| free(tinfo); |
| return ret; |
| } |
| |
| /* |
| * The test spawns a process, which spawns 1000 threads. Then it checks |
| * that memory.stat's kernel_stack is at least 1000 pages large. |
| */ |
| static int test_kmem_kernel_stacks(const char *root) |
| { |
| int ret = KSFT_FAIL; |
| char *cg = NULL; |
| |
| cg = cg_name(root, "kmem_kernel_stacks_test"); |
| if (!cg) |
| goto cleanup; |
| |
| if (cg_create(cg)) |
| goto cleanup; |
| |
| if (cg_run(cg, spawn_1000_threads, NULL)) |
| goto cleanup; |
| |
| ret = KSFT_PASS; |
| cleanup: |
| cg_destroy(cg); |
| free(cg); |
| |
| return ret; |
| } |
| |
| /* |
| * This test sequentionally creates 30 child cgroups, allocates some |
| * kernel memory in each of them, and deletes them. Then it checks |
| * that the number of dying cgroups on the parent level is 0. |
| */ |
| static int test_kmem_dead_cgroups(const char *root) |
| { |
| int ret = KSFT_FAIL; |
| char *parent; |
| long dead; |
| int i; |
| |
| parent = cg_name(root, "kmem_dead_cgroups_test"); |
| if (!parent) |
| goto cleanup; |
| |
| if (cg_create(parent)) |
| goto cleanup; |
| |
| if (cg_write(parent, "cgroup.subtree_control", "+memory")) |
| goto cleanup; |
| |
| if (cg_run_in_subcgroups(parent, alloc_dcache, (void *)100, 30)) |
| goto cleanup; |
| |
| for (i = 0; i < 5; i++) { |
| dead = cg_read_key_long(parent, "cgroup.stat", |
| "nr_dying_descendants "); |
| if (dead == 0) { |
| ret = KSFT_PASS; |
| break; |
| } |
| /* |
| * Reclaiming cgroups might take some time, |
| * let's wait a bit and repeat. |
| */ |
| sleep(1); |
| } |
| |
| cleanup: |
| cg_destroy(parent); |
| free(parent); |
| |
| return ret; |
| } |
| |
| /* |
| * This test creates a sub-tree with 1000 memory cgroups. |
| * Then it checks that the memory.current on the parent level |
| * is greater than 0 and approximates matches the percpu value |
| * from memory.stat. |
| */ |
| static int test_percpu_basic(const char *root) |
| { |
| int ret = KSFT_FAIL; |
| char *parent, *child; |
| long current, percpu; |
| int i; |
| |
| parent = cg_name(root, "percpu_basic_test"); |
| if (!parent) |
| goto cleanup; |
| |
| if (cg_create(parent)) |
| goto cleanup; |
| |
| if (cg_write(parent, "cgroup.subtree_control", "+memory")) |
| goto cleanup; |
| |
| for (i = 0; i < 1000; i++) { |
| child = cg_name_indexed(parent, "child", i); |
| if (!child) |
| return -1; |
| |
| if (cg_create(child)) |
| goto cleanup_children; |
| |
| free(child); |
| } |
| |
| current = cg_read_long(parent, "memory.current"); |
| percpu = cg_read_key_long(parent, "memory.stat", "percpu "); |
| |
| if (current > 0 && percpu > 0 && abs(current - percpu) < |
| MAX_VMSTAT_ERROR) |
| ret = KSFT_PASS; |
| else |
| printf("memory.current %ld\npercpu %ld\n", |
| current, percpu); |
| |
| cleanup_children: |
| for (i = 0; i < 1000; i++) { |
| child = cg_name_indexed(parent, "child", i); |
| cg_destroy(child); |
| free(child); |
| } |
| |
| cleanup: |
| cg_destroy(parent); |
| free(parent); |
| |
| return ret; |
| } |
| |
| #define T(x) { x, #x } |
| struct kmem_test { |
| int (*fn)(const char *root); |
| const char *name; |
| } tests[] = { |
| T(test_kmem_basic), |
| T(test_kmem_memcg_deletion), |
| T(test_kmem_proc_kpagecgroup), |
| T(test_kmem_kernel_stacks), |
| T(test_kmem_dead_cgroups), |
| T(test_percpu_basic), |
| }; |
| #undef T |
| |
| int main(int argc, char **argv) |
| { |
| char root[PATH_MAX]; |
| int i, ret = EXIT_SUCCESS; |
| |
| if (cg_find_unified_root(root, sizeof(root))) |
| ksft_exit_skip("cgroup v2 isn't mounted\n"); |
| |
| /* |
| * Check that memory controller is available: |
| * memory is listed in cgroup.controllers |
| */ |
| if (cg_read_strstr(root, "cgroup.controllers", "memory")) |
| ksft_exit_skip("memory controller isn't available\n"); |
| |
| if (cg_read_strstr(root, "cgroup.subtree_control", "memory")) |
| if (cg_write(root, "cgroup.subtree_control", "+memory")) |
| ksft_exit_skip("Failed to set memory controller\n"); |
| |
| for (i = 0; i < ARRAY_SIZE(tests); i++) { |
| switch (tests[i].fn(root)) { |
| case KSFT_PASS: |
| ksft_test_result_pass("%s\n", tests[i].name); |
| break; |
| case KSFT_SKIP: |
| ksft_test_result_skip("%s\n", tests[i].name); |
| break; |
| default: |
| ret = EXIT_FAILURE; |
| ksft_test_result_fail("%s\n", tests[i].name); |
| break; |
| } |
| } |
| |
| return ret; |
| } |