| /* SPDX-License-Identifier: GPL-2.0 */ |
| #define _GNU_SOURCE |
| |
| #include <linux/limits.h> |
| #include <linux/oom.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/socket.h> |
| #include <sys/wait.h> |
| #include <arpa/inet.h> |
| #include <netinet/in.h> |
| #include <netdb.h> |
| #include <errno.h> |
| |
| #include "../kselftest.h" |
| #include "cgroup_util.h" |
| |
| /* |
| * This test creates two nested cgroups with and without enabling |
| * the memory controller. |
| */ |
| static int test_memcg_subtree_control(const char *root) |
| { |
| char *parent, *child, *parent2, *child2; |
| int ret = KSFT_FAIL; |
| char buf[PAGE_SIZE]; |
| |
| /* Create two nested cgroups with the memory controller enabled */ |
| parent = cg_name(root, "memcg_test_0"); |
| child = cg_name(root, "memcg_test_0/memcg_test_1"); |
| if (!parent || !child) |
| goto cleanup; |
| |
| if (cg_create(parent)) |
| goto cleanup; |
| |
| if (cg_write(parent, "cgroup.subtree_control", "+memory")) |
| goto cleanup; |
| |
| if (cg_create(child)) |
| goto cleanup; |
| |
| if (cg_read_strstr(child, "cgroup.controllers", "memory")) |
| goto cleanup; |
| |
| /* Create two nested cgroups without enabling memory controller */ |
| parent2 = cg_name(root, "memcg_test_1"); |
| child2 = cg_name(root, "memcg_test_1/memcg_test_1"); |
| if (!parent2 || !child2) |
| goto cleanup; |
| |
| if (cg_create(parent2)) |
| goto cleanup; |
| |
| if (cg_create(child2)) |
| goto cleanup; |
| |
| if (cg_read(child2, "cgroup.controllers", buf, sizeof(buf))) |
| goto cleanup; |
| |
| if (!cg_read_strstr(child2, "cgroup.controllers", "memory")) |
| goto cleanup; |
| |
| ret = KSFT_PASS; |
| |
| cleanup: |
| cg_destroy(child); |
| cg_destroy(parent); |
| free(parent); |
| free(child); |
| |
| cg_destroy(child2); |
| cg_destroy(parent2); |
| free(parent2); |
| free(child2); |
| |
| return ret; |
| } |
| |
| static int alloc_anon_50M_check(const char *cgroup, void *arg) |
| { |
| size_t size = MB(50); |
| char *buf, *ptr; |
| long anon, current; |
| int ret = -1; |
| |
| buf = malloc(size); |
| for (ptr = buf; ptr < buf + size; ptr += PAGE_SIZE) |
| *ptr = 0; |
| |
| current = cg_read_long(cgroup, "memory.current"); |
| if (current < size) |
| goto cleanup; |
| |
| if (!values_close(size, current, 3)) |
| goto cleanup; |
| |
| anon = cg_read_key_long(cgroup, "memory.stat", "anon "); |
| if (anon < 0) |
| goto cleanup; |
| |
| if (!values_close(anon, current, 3)) |
| goto cleanup; |
| |
| ret = 0; |
| cleanup: |
| free(buf); |
| return ret; |
| } |
| |
| static int alloc_pagecache_50M_check(const char *cgroup, void *arg) |
| { |
| size_t size = MB(50); |
| int ret = -1; |
| long current, file; |
| int fd; |
| |
| fd = get_temp_fd(); |
| if (fd < 0) |
| return -1; |
| |
| if (alloc_pagecache(fd, size)) |
| goto cleanup; |
| |
| current = cg_read_long(cgroup, "memory.current"); |
| if (current < size) |
| goto cleanup; |
| |
| file = cg_read_key_long(cgroup, "memory.stat", "file "); |
| if (file < 0) |
| goto cleanup; |
| |
| if (!values_close(file, current, 10)) |
| goto cleanup; |
| |
| ret = 0; |
| |
| cleanup: |
| close(fd); |
| return ret; |
| } |
| |
| /* |
| * This test create a memory cgroup, allocates |
| * some anonymous memory and some pagecache |
| * and check memory.current and some memory.stat values. |
| */ |
| static int test_memcg_current(const char *root) |
| { |
| int ret = KSFT_FAIL; |
| long current; |
| char *memcg; |
| |
| memcg = cg_name(root, "memcg_test"); |
| if (!memcg) |
| goto cleanup; |
| |
| if (cg_create(memcg)) |
| goto cleanup; |
| |
| current = cg_read_long(memcg, "memory.current"); |
| if (current != 0) |
| goto cleanup; |
| |
| if (cg_run(memcg, alloc_anon_50M_check, NULL)) |
| goto cleanup; |
| |
| if (cg_run(memcg, alloc_pagecache_50M_check, NULL)) |
| goto cleanup; |
| |
| ret = KSFT_PASS; |
| |
| cleanup: |
| cg_destroy(memcg); |
| free(memcg); |
| |
| return ret; |
| } |
| |
| static int alloc_pagecache_50M(const char *cgroup, void *arg) |
| { |
| int fd = (long)arg; |
| |
| return alloc_pagecache(fd, MB(50)); |
| } |
| |
| static int alloc_pagecache_50M_noexit(const char *cgroup, void *arg) |
| { |
| int fd = (long)arg; |
| int ppid = getppid(); |
| |
| if (alloc_pagecache(fd, MB(50))) |
| return -1; |
| |
| while (getppid() == ppid) |
| sleep(1); |
| |
| return 0; |
| } |
| |
| static int alloc_anon_noexit(const char *cgroup, void *arg) |
| { |
| int ppid = getppid(); |
| |
| if (alloc_anon(cgroup, arg)) |
| return -1; |
| |
| while (getppid() == ppid) |
| sleep(1); |
| |
| return 0; |
| } |
| |
| /* |
| * Wait until processes are killed asynchronously by the OOM killer |
| * If we exceed a timeout, fail. |
| */ |
| static int cg_test_proc_killed(const char *cgroup) |
| { |
| int limit; |
| |
| for (limit = 10; limit > 0; limit--) { |
| if (cg_read_strcmp(cgroup, "cgroup.procs", "") == 0) |
| return 0; |
| |
| usleep(100000); |
| } |
| return -1; |
| } |
| |
| /* |
| * First, this test creates the following hierarchy: |
| * A memory.min = 50M, memory.max = 200M |
| * A/B memory.min = 50M, memory.current = 50M |
| * A/B/C memory.min = 75M, memory.current = 50M |
| * A/B/D memory.min = 25M, memory.current = 50M |
| * A/B/E memory.min = 500M, memory.current = 0 |
| * A/B/F memory.min = 0, memory.current = 50M |
| * |
| * Usages are pagecache, but the test keeps a running |
| * process in every leaf cgroup. |
| * Then it creates A/G and creates a significant |
| * memory pressure in it. |
| * |
| * A/B memory.current ~= 50M |
| * A/B/C memory.current ~= 33M |
| * A/B/D memory.current ~= 17M |
| * A/B/E memory.current ~= 0 |
| * |
| * After that it tries to allocate more than there is |
| * unprotected memory in A available, and checks |
| * checks that memory.min protects pagecache even |
| * in this case. |
| */ |
| static int test_memcg_min(const char *root) |
| { |
| int ret = KSFT_FAIL; |
| char *parent[3] = {NULL}; |
| char *children[4] = {NULL}; |
| long c[4]; |
| int i, attempts; |
| int fd; |
| |
| fd = get_temp_fd(); |
| if (fd < 0) |
| goto cleanup; |
| |
| parent[0] = cg_name(root, "memcg_test_0"); |
| if (!parent[0]) |
| goto cleanup; |
| |
| parent[1] = cg_name(parent[0], "memcg_test_1"); |
| if (!parent[1]) |
| goto cleanup; |
| |
| parent[2] = cg_name(parent[0], "memcg_test_2"); |
| if (!parent[2]) |
| goto cleanup; |
| |
| if (cg_create(parent[0])) |
| goto cleanup; |
| |
| if (cg_read_long(parent[0], "memory.min")) { |
| ret = KSFT_SKIP; |
| goto cleanup; |
| } |
| |
| if (cg_write(parent[0], "cgroup.subtree_control", "+memory")) |
| goto cleanup; |
| |
| if (cg_write(parent[0], "memory.max", "200M")) |
| goto cleanup; |
| |
| if (cg_write(parent[0], "memory.swap.max", "0")) |
| goto cleanup; |
| |
| if (cg_create(parent[1])) |
| goto cleanup; |
| |
| if (cg_write(parent[1], "cgroup.subtree_control", "+memory")) |
| goto cleanup; |
| |
| if (cg_create(parent[2])) |
| goto cleanup; |
| |
| for (i = 0; i < ARRAY_SIZE(children); i++) { |
| children[i] = cg_name_indexed(parent[1], "child_memcg", i); |
| if (!children[i]) |
| goto cleanup; |
| |
| if (cg_create(children[i])) |
| goto cleanup; |
| |
| if (i == 2) |
| continue; |
| |
| cg_run_nowait(children[i], alloc_pagecache_50M_noexit, |
| (void *)(long)fd); |
| } |
| |
| if (cg_write(parent[0], "memory.min", "50M")) |
| goto cleanup; |
| if (cg_write(parent[1], "memory.min", "50M")) |
| goto cleanup; |
| if (cg_write(children[0], "memory.min", "75M")) |
| goto cleanup; |
| if (cg_write(children[1], "memory.min", "25M")) |
| goto cleanup; |
| if (cg_write(children[2], "memory.min", "500M")) |
| goto cleanup; |
| if (cg_write(children[3], "memory.min", "0")) |
| goto cleanup; |
| |
| attempts = 0; |
| while (!values_close(cg_read_long(parent[1], "memory.current"), |
| MB(150), 3)) { |
| if (attempts++ > 5) |
| break; |
| sleep(1); |
| } |
| |
| if (cg_run(parent[2], alloc_anon, (void *)MB(148))) |
| goto cleanup; |
| |
| if (!values_close(cg_read_long(parent[1], "memory.current"), MB(50), 3)) |
| goto cleanup; |
| |
| for (i = 0; i < ARRAY_SIZE(children); i++) |
| c[i] = cg_read_long(children[i], "memory.current"); |
| |
| if (!values_close(c[0], MB(33), 10)) |
| goto cleanup; |
| |
| if (!values_close(c[1], MB(17), 10)) |
| goto cleanup; |
| |
| if (!values_close(c[2], 0, 1)) |
| goto cleanup; |
| |
| if (!cg_run(parent[2], alloc_anon, (void *)MB(170))) |
| goto cleanup; |
| |
| if (!values_close(cg_read_long(parent[1], "memory.current"), MB(50), 3)) |
| goto cleanup; |
| |
| ret = KSFT_PASS; |
| |
| cleanup: |
| for (i = ARRAY_SIZE(children) - 1; i >= 0; i--) { |
| if (!children[i]) |
| continue; |
| |
| cg_destroy(children[i]); |
| free(children[i]); |
| } |
| |
| for (i = ARRAY_SIZE(parent) - 1; i >= 0; i--) { |
| if (!parent[i]) |
| continue; |
| |
| cg_destroy(parent[i]); |
| free(parent[i]); |
| } |
| close(fd); |
| return ret; |
| } |
| |
| /* |
| * First, this test creates the following hierarchy: |
| * A memory.low = 50M, memory.max = 200M |
| * A/B memory.low = 50M, memory.current = 50M |
| * A/B/C memory.low = 75M, memory.current = 50M |
| * A/B/D memory.low = 25M, memory.current = 50M |
| * A/B/E memory.low = 500M, memory.current = 0 |
| * A/B/F memory.low = 0, memory.current = 50M |
| * |
| * Usages are pagecache. |
| * Then it creates A/G an creates a significant |
| * memory pressure in it. |
| * |
| * Then it checks actual memory usages and expects that: |
| * A/B memory.current ~= 50M |
| * A/B/ memory.current ~= 33M |
| * A/B/D memory.current ~= 17M |
| * A/B/E memory.current ~= 0 |
| * |
| * After that it tries to allocate more than there is |
| * unprotected memory in A available, |
| * and checks low and oom events in memory.events. |
| */ |
| static int test_memcg_low(const char *root) |
| { |
| int ret = KSFT_FAIL; |
| char *parent[3] = {NULL}; |
| char *children[4] = {NULL}; |
| long low, oom; |
| long c[4]; |
| int i; |
| int fd; |
| |
| fd = get_temp_fd(); |
| if (fd < 0) |
| goto cleanup; |
| |
| parent[0] = cg_name(root, "memcg_test_0"); |
| if (!parent[0]) |
| goto cleanup; |
| |
| parent[1] = cg_name(parent[0], "memcg_test_1"); |
| if (!parent[1]) |
| goto cleanup; |
| |
| parent[2] = cg_name(parent[0], "memcg_test_2"); |
| if (!parent[2]) |
| goto cleanup; |
| |
| if (cg_create(parent[0])) |
| goto cleanup; |
| |
| if (cg_read_long(parent[0], "memory.low")) |
| goto cleanup; |
| |
| if (cg_write(parent[0], "cgroup.subtree_control", "+memory")) |
| goto cleanup; |
| |
| if (cg_write(parent[0], "memory.max", "200M")) |
| goto cleanup; |
| |
| if (cg_write(parent[0], "memory.swap.max", "0")) |
| goto cleanup; |
| |
| if (cg_create(parent[1])) |
| goto cleanup; |
| |
| if (cg_write(parent[1], "cgroup.subtree_control", "+memory")) |
| goto cleanup; |
| |
| if (cg_create(parent[2])) |
| goto cleanup; |
| |
| for (i = 0; i < ARRAY_SIZE(children); i++) { |
| children[i] = cg_name_indexed(parent[1], "child_memcg", i); |
| if (!children[i]) |
| goto cleanup; |
| |
| if (cg_create(children[i])) |
| goto cleanup; |
| |
| if (i == 2) |
| continue; |
| |
| if (cg_run(children[i], alloc_pagecache_50M, (void *)(long)fd)) |
| goto cleanup; |
| } |
| |
| if (cg_write(parent[0], "memory.low", "50M")) |
| goto cleanup; |
| if (cg_write(parent[1], "memory.low", "50M")) |
| goto cleanup; |
| if (cg_write(children[0], "memory.low", "75M")) |
| goto cleanup; |
| if (cg_write(children[1], "memory.low", "25M")) |
| goto cleanup; |
| if (cg_write(children[2], "memory.low", "500M")) |
| goto cleanup; |
| if (cg_write(children[3], "memory.low", "0")) |
| goto cleanup; |
| |
| if (cg_run(parent[2], alloc_anon, (void *)MB(148))) |
| goto cleanup; |
| |
| if (!values_close(cg_read_long(parent[1], "memory.current"), MB(50), 3)) |
| goto cleanup; |
| |
| for (i = 0; i < ARRAY_SIZE(children); i++) |
| c[i] = cg_read_long(children[i], "memory.current"); |
| |
| if (!values_close(c[0], MB(33), 10)) |
| goto cleanup; |
| |
| if (!values_close(c[1], MB(17), 10)) |
| goto cleanup; |
| |
| if (!values_close(c[2], 0, 1)) |
| goto cleanup; |
| |
| if (cg_run(parent[2], alloc_anon, (void *)MB(166))) { |
| fprintf(stderr, |
| "memory.low prevents from allocating anon memory\n"); |
| goto cleanup; |
| } |
| |
| for (i = 0; i < ARRAY_SIZE(children); i++) { |
| oom = cg_read_key_long(children[i], "memory.events", "oom "); |
| low = cg_read_key_long(children[i], "memory.events", "low "); |
| |
| if (oom) |
| goto cleanup; |
| if (i < 2 && low <= 0) |
| goto cleanup; |
| if (i >= 2 && low) |
| goto cleanup; |
| } |
| |
| ret = KSFT_PASS; |
| |
| cleanup: |
| for (i = ARRAY_SIZE(children) - 1; i >= 0; i--) { |
| if (!children[i]) |
| continue; |
| |
| cg_destroy(children[i]); |
| free(children[i]); |
| } |
| |
| for (i = ARRAY_SIZE(parent) - 1; i >= 0; i--) { |
| if (!parent[i]) |
| continue; |
| |
| cg_destroy(parent[i]); |
| free(parent[i]); |
| } |
| close(fd); |
| return ret; |
| } |
| |
| static int alloc_pagecache_max_30M(const char *cgroup, void *arg) |
| { |
| size_t size = MB(50); |
| int ret = -1; |
| long current; |
| int fd; |
| |
| fd = get_temp_fd(); |
| if (fd < 0) |
| return -1; |
| |
| if (alloc_pagecache(fd, size)) |
| goto cleanup; |
| |
| current = cg_read_long(cgroup, "memory.current"); |
| if (current <= MB(29) || current > MB(30)) |
| goto cleanup; |
| |
| ret = 0; |
| |
| cleanup: |
| close(fd); |
| return ret; |
| |
| } |
| |
| /* |
| * This test checks that memory.high limits the amount of |
| * memory which can be consumed by either anonymous memory |
| * or pagecache. |
| */ |
| static int test_memcg_high(const char *root) |
| { |
| int ret = KSFT_FAIL; |
| char *memcg; |
| long high; |
| |
| memcg = cg_name(root, "memcg_test"); |
| if (!memcg) |
| goto cleanup; |
| |
| if (cg_create(memcg)) |
| goto cleanup; |
| |
| if (cg_read_strcmp(memcg, "memory.high", "max\n")) |
| goto cleanup; |
| |
| if (cg_write(memcg, "memory.swap.max", "0")) |
| goto cleanup; |
| |
| if (cg_write(memcg, "memory.high", "30M")) |
| goto cleanup; |
| |
| if (cg_run(memcg, alloc_anon, (void *)MB(100))) |
| goto cleanup; |
| |
| if (!cg_run(memcg, alloc_pagecache_50M_check, NULL)) |
| goto cleanup; |
| |
| if (cg_run(memcg, alloc_pagecache_max_30M, NULL)) |
| goto cleanup; |
| |
| high = cg_read_key_long(memcg, "memory.events", "high "); |
| if (high <= 0) |
| goto cleanup; |
| |
| ret = KSFT_PASS; |
| |
| cleanup: |
| cg_destroy(memcg); |
| free(memcg); |
| |
| return ret; |
| } |
| |
| /* |
| * This test checks that memory.max limits the amount of |
| * memory which can be consumed by either anonymous memory |
| * or pagecache. |
| */ |
| static int test_memcg_max(const char *root) |
| { |
| int ret = KSFT_FAIL; |
| char *memcg; |
| long current, max; |
| |
| memcg = cg_name(root, "memcg_test"); |
| if (!memcg) |
| goto cleanup; |
| |
| if (cg_create(memcg)) |
| goto cleanup; |
| |
| if (cg_read_strcmp(memcg, "memory.max", "max\n")) |
| goto cleanup; |
| |
| if (cg_write(memcg, "memory.swap.max", "0")) |
| goto cleanup; |
| |
| if (cg_write(memcg, "memory.max", "30M")) |
| goto cleanup; |
| |
| /* Should be killed by OOM killer */ |
| if (!cg_run(memcg, alloc_anon, (void *)MB(100))) |
| goto cleanup; |
| |
| if (cg_run(memcg, alloc_pagecache_max_30M, NULL)) |
| goto cleanup; |
| |
| current = cg_read_long(memcg, "memory.current"); |
| if (current > MB(30) || !current) |
| goto cleanup; |
| |
| max = cg_read_key_long(memcg, "memory.events", "max "); |
| if (max <= 0) |
| goto cleanup; |
| |
| ret = KSFT_PASS; |
| |
| cleanup: |
| cg_destroy(memcg); |
| free(memcg); |
| |
| return ret; |
| } |
| |
| static int alloc_anon_50M_check_swap(const char *cgroup, void *arg) |
| { |
| long mem_max = (long)arg; |
| size_t size = MB(50); |
| char *buf, *ptr; |
| long mem_current, swap_current; |
| int ret = -1; |
| |
| buf = malloc(size); |
| for (ptr = buf; ptr < buf + size; ptr += PAGE_SIZE) |
| *ptr = 0; |
| |
| mem_current = cg_read_long(cgroup, "memory.current"); |
| if (!mem_current || !values_close(mem_current, mem_max, 3)) |
| goto cleanup; |
| |
| swap_current = cg_read_long(cgroup, "memory.swap.current"); |
| if (!swap_current || |
| !values_close(mem_current + swap_current, size, 3)) |
| goto cleanup; |
| |
| ret = 0; |
| cleanup: |
| free(buf); |
| return ret; |
| } |
| |
| /* |
| * This test checks that memory.swap.max limits the amount of |
| * anonymous memory which can be swapped out. |
| */ |
| static int test_memcg_swap_max(const char *root) |
| { |
| int ret = KSFT_FAIL; |
| char *memcg; |
| long max; |
| |
| if (!is_swap_enabled()) |
| return KSFT_SKIP; |
| |
| memcg = cg_name(root, "memcg_test"); |
| if (!memcg) |
| goto cleanup; |
| |
| if (cg_create(memcg)) |
| goto cleanup; |
| |
| if (cg_read_long(memcg, "memory.swap.current")) { |
| ret = KSFT_SKIP; |
| goto cleanup; |
| } |
| |
| if (cg_read_strcmp(memcg, "memory.max", "max\n")) |
| goto cleanup; |
| |
| if (cg_read_strcmp(memcg, "memory.swap.max", "max\n")) |
| goto cleanup; |
| |
| if (cg_write(memcg, "memory.swap.max", "30M")) |
| goto cleanup; |
| |
| if (cg_write(memcg, "memory.max", "30M")) |
| goto cleanup; |
| |
| /* Should be killed by OOM killer */ |
| if (!cg_run(memcg, alloc_anon, (void *)MB(100))) |
| goto cleanup; |
| |
| if (cg_read_key_long(memcg, "memory.events", "oom ") != 1) |
| goto cleanup; |
| |
| if (cg_read_key_long(memcg, "memory.events", "oom_kill ") != 1) |
| goto cleanup; |
| |
| if (cg_run(memcg, alloc_anon_50M_check_swap, (void *)MB(30))) |
| goto cleanup; |
| |
| max = cg_read_key_long(memcg, "memory.events", "max "); |
| if (max <= 0) |
| goto cleanup; |
| |
| ret = KSFT_PASS; |
| |
| cleanup: |
| cg_destroy(memcg); |
| free(memcg); |
| |
| return ret; |
| } |
| |
| /* |
| * This test disables swapping and tries to allocate anonymous memory |
| * up to OOM. Then it checks for oom and oom_kill events in |
| * memory.events. |
| */ |
| static int test_memcg_oom_events(const char *root) |
| { |
| int ret = KSFT_FAIL; |
| char *memcg; |
| |
| memcg = cg_name(root, "memcg_test"); |
| if (!memcg) |
| goto cleanup; |
| |
| if (cg_create(memcg)) |
| goto cleanup; |
| |
| if (cg_write(memcg, "memory.max", "30M")) |
| goto cleanup; |
| |
| if (cg_write(memcg, "memory.swap.max", "0")) |
| goto cleanup; |
| |
| if (!cg_run(memcg, alloc_anon, (void *)MB(100))) |
| goto cleanup; |
| |
| if (cg_read_strcmp(memcg, "cgroup.procs", "")) |
| goto cleanup; |
| |
| if (cg_read_key_long(memcg, "memory.events", "oom ") != 1) |
| goto cleanup; |
| |
| if (cg_read_key_long(memcg, "memory.events", "oom_kill ") != 1) |
| goto cleanup; |
| |
| ret = KSFT_PASS; |
| |
| cleanup: |
| cg_destroy(memcg); |
| free(memcg); |
| |
| return ret; |
| } |
| |
| struct tcp_server_args { |
| unsigned short port; |
| int ctl[2]; |
| }; |
| |
| static int tcp_server(const char *cgroup, void *arg) |
| { |
| struct tcp_server_args *srv_args = arg; |
| struct sockaddr_in6 saddr = { 0 }; |
| socklen_t slen = sizeof(saddr); |
| int sk, client_sk, ctl_fd, yes = 1, ret = -1; |
| |
| close(srv_args->ctl[0]); |
| ctl_fd = srv_args->ctl[1]; |
| |
| saddr.sin6_family = AF_INET6; |
| saddr.sin6_addr = in6addr_any; |
| saddr.sin6_port = htons(srv_args->port); |
| |
| sk = socket(AF_INET6, SOCK_STREAM, 0); |
| if (sk < 0) |
| return ret; |
| |
| if (setsockopt(sk, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)) < 0) |
| goto cleanup; |
| |
| if (bind(sk, (struct sockaddr *)&saddr, slen)) { |
| write(ctl_fd, &errno, sizeof(errno)); |
| goto cleanup; |
| } |
| |
| if (listen(sk, 1)) |
| goto cleanup; |
| |
| ret = 0; |
| if (write(ctl_fd, &ret, sizeof(ret)) != sizeof(ret)) { |
| ret = -1; |
| goto cleanup; |
| } |
| |
| client_sk = accept(sk, NULL, NULL); |
| if (client_sk < 0) |
| goto cleanup; |
| |
| ret = -1; |
| for (;;) { |
| uint8_t buf[0x100000]; |
| |
| if (write(client_sk, buf, sizeof(buf)) <= 0) { |
| if (errno == ECONNRESET) |
| ret = 0; |
| break; |
| } |
| } |
| |
| close(client_sk); |
| |
| cleanup: |
| close(sk); |
| return ret; |
| } |
| |
| static int tcp_client(const char *cgroup, unsigned short port) |
| { |
| const char server[] = "localhost"; |
| struct addrinfo *ai; |
| char servport[6]; |
| int retries = 0x10; /* nice round number */ |
| int sk, ret; |
| |
| snprintf(servport, sizeof(servport), "%hd", port); |
| ret = getaddrinfo(server, servport, NULL, &ai); |
| if (ret) |
| return ret; |
| |
| sk = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); |
| if (sk < 0) |
| goto free_ainfo; |
| |
| ret = connect(sk, ai->ai_addr, ai->ai_addrlen); |
| if (ret < 0) |
| goto close_sk; |
| |
| ret = KSFT_FAIL; |
| while (retries--) { |
| uint8_t buf[0x100000]; |
| long current, sock; |
| |
| if (read(sk, buf, sizeof(buf)) <= 0) |
| goto close_sk; |
| |
| current = cg_read_long(cgroup, "memory.current"); |
| sock = cg_read_key_long(cgroup, "memory.stat", "sock "); |
| |
| if (current < 0 || sock < 0) |
| goto close_sk; |
| |
| if (current < sock) |
| goto close_sk; |
| |
| if (values_close(current, sock, 10)) { |
| ret = KSFT_PASS; |
| break; |
| } |
| } |
| |
| close_sk: |
| close(sk); |
| free_ainfo: |
| freeaddrinfo(ai); |
| return ret; |
| } |
| |
| /* |
| * This test checks socket memory accounting. |
| * The test forks a TCP server listens on a random port between 1000 |
| * and 61000. Once it gets a client connection, it starts writing to |
| * its socket. |
| * The TCP client interleaves reads from the socket with check whether |
| * memory.current and memory.stat.sock are similar. |
| */ |
| static int test_memcg_sock(const char *root) |
| { |
| int bind_retries = 5, ret = KSFT_FAIL, pid, err; |
| unsigned short port; |
| char *memcg; |
| |
| memcg = cg_name(root, "memcg_test"); |
| if (!memcg) |
| goto cleanup; |
| |
| if (cg_create(memcg)) |
| goto cleanup; |
| |
| while (bind_retries--) { |
| struct tcp_server_args args; |
| |
| if (pipe(args.ctl)) |
| goto cleanup; |
| |
| port = args.port = 1000 + rand() % 60000; |
| |
| pid = cg_run_nowait(memcg, tcp_server, &args); |
| if (pid < 0) |
| goto cleanup; |
| |
| close(args.ctl[1]); |
| if (read(args.ctl[0], &err, sizeof(err)) != sizeof(err)) |
| goto cleanup; |
| close(args.ctl[0]); |
| |
| if (!err) |
| break; |
| if (err != EADDRINUSE) |
| goto cleanup; |
| |
| waitpid(pid, NULL, 0); |
| } |
| |
| if (err == EADDRINUSE) { |
| ret = KSFT_SKIP; |
| goto cleanup; |
| } |
| |
| if (tcp_client(memcg, port) != KSFT_PASS) |
| goto cleanup; |
| |
| waitpid(pid, &err, 0); |
| if (WEXITSTATUS(err)) |
| goto cleanup; |
| |
| if (cg_read_long(memcg, "memory.current") < 0) |
| goto cleanup; |
| |
| if (cg_read_key_long(memcg, "memory.stat", "sock ")) |
| goto cleanup; |
| |
| ret = KSFT_PASS; |
| |
| cleanup: |
| cg_destroy(memcg); |
| free(memcg); |
| |
| return ret; |
| } |
| |
| /* |
| * This test disables swapping and tries to allocate anonymous memory |
| * up to OOM with memory.group.oom set. Then it checks that all |
| * processes in the leaf (but not the parent) were killed. |
| */ |
| static int test_memcg_oom_group_leaf_events(const char *root) |
| { |
| int ret = KSFT_FAIL; |
| char *parent, *child; |
| |
| parent = cg_name(root, "memcg_test_0"); |
| child = cg_name(root, "memcg_test_0/memcg_test_1"); |
| |
| if (!parent || !child) |
| goto cleanup; |
| |
| if (cg_create(parent)) |
| goto cleanup; |
| |
| if (cg_create(child)) |
| goto cleanup; |
| |
| if (cg_write(parent, "cgroup.subtree_control", "+memory")) |
| goto cleanup; |
| |
| if (cg_write(child, "memory.max", "50M")) |
| goto cleanup; |
| |
| if (cg_write(child, "memory.swap.max", "0")) |
| goto cleanup; |
| |
| if (cg_write(child, "memory.oom.group", "1")) |
| goto cleanup; |
| |
| cg_run_nowait(parent, alloc_anon_noexit, (void *) MB(60)); |
| cg_run_nowait(child, alloc_anon_noexit, (void *) MB(1)); |
| cg_run_nowait(child, alloc_anon_noexit, (void *) MB(1)); |
| if (!cg_run(child, alloc_anon, (void *)MB(100))) |
| goto cleanup; |
| |
| if (cg_test_proc_killed(child)) |
| goto cleanup; |
| |
| if (cg_read_key_long(child, "memory.events", "oom_kill ") <= 0) |
| goto cleanup; |
| |
| if (cg_read_key_long(parent, "memory.events", "oom_kill ") != 0) |
| goto cleanup; |
| |
| ret = KSFT_PASS; |
| |
| cleanup: |
| if (child) |
| cg_destroy(child); |
| if (parent) |
| cg_destroy(parent); |
| free(child); |
| free(parent); |
| |
| return ret; |
| } |
| |
| /* |
| * This test disables swapping and tries to allocate anonymous memory |
| * up to OOM with memory.group.oom set. Then it checks that all |
| * processes in the parent and leaf were killed. |
| */ |
| static int test_memcg_oom_group_parent_events(const char *root) |
| { |
| int ret = KSFT_FAIL; |
| char *parent, *child; |
| |
| parent = cg_name(root, "memcg_test_0"); |
| child = cg_name(root, "memcg_test_0/memcg_test_1"); |
| |
| if (!parent || !child) |
| goto cleanup; |
| |
| if (cg_create(parent)) |
| goto cleanup; |
| |
| if (cg_create(child)) |
| goto cleanup; |
| |
| if (cg_write(parent, "memory.max", "80M")) |
| goto cleanup; |
| |
| if (cg_write(parent, "memory.swap.max", "0")) |
| goto cleanup; |
| |
| if (cg_write(parent, "memory.oom.group", "1")) |
| goto cleanup; |
| |
| cg_run_nowait(parent, alloc_anon_noexit, (void *) MB(60)); |
| cg_run_nowait(child, alloc_anon_noexit, (void *) MB(1)); |
| cg_run_nowait(child, alloc_anon_noexit, (void *) MB(1)); |
| |
| if (!cg_run(child, alloc_anon, (void *)MB(100))) |
| goto cleanup; |
| |
| if (cg_test_proc_killed(child)) |
| goto cleanup; |
| if (cg_test_proc_killed(parent)) |
| goto cleanup; |
| |
| ret = KSFT_PASS; |
| |
| cleanup: |
| if (child) |
| cg_destroy(child); |
| if (parent) |
| cg_destroy(parent); |
| free(child); |
| free(parent); |
| |
| return ret; |
| } |
| |
| /* |
| * This test disables swapping and tries to allocate anonymous memory |
| * up to OOM with memory.group.oom set. Then it checks that all |
| * processes were killed except those set with OOM_SCORE_ADJ_MIN |
| */ |
| static int test_memcg_oom_group_score_events(const char *root) |
| { |
| int ret = KSFT_FAIL; |
| char *memcg; |
| int safe_pid; |
| |
| memcg = cg_name(root, "memcg_test_0"); |
| |
| if (!memcg) |
| goto cleanup; |
| |
| if (cg_create(memcg)) |
| goto cleanup; |
| |
| if (cg_write(memcg, "memory.max", "50M")) |
| goto cleanup; |
| |
| if (cg_write(memcg, "memory.swap.max", "0")) |
| goto cleanup; |
| |
| if (cg_write(memcg, "memory.oom.group", "1")) |
| goto cleanup; |
| |
| safe_pid = cg_run_nowait(memcg, alloc_anon_noexit, (void *) MB(1)); |
| if (set_oom_adj_score(safe_pid, OOM_SCORE_ADJ_MIN)) |
| goto cleanup; |
| |
| cg_run_nowait(memcg, alloc_anon_noexit, (void *) MB(1)); |
| if (!cg_run(memcg, alloc_anon, (void *)MB(100))) |
| goto cleanup; |
| |
| if (cg_read_key_long(memcg, "memory.events", "oom_kill ") != 3) |
| goto cleanup; |
| |
| if (kill(safe_pid, SIGKILL)) |
| goto cleanup; |
| |
| ret = KSFT_PASS; |
| |
| cleanup: |
| if (memcg) |
| cg_destroy(memcg); |
| free(memcg); |
| |
| return ret; |
| } |
| |
| |
| #define T(x) { x, #x } |
| struct memcg_test { |
| int (*fn)(const char *root); |
| const char *name; |
| } tests[] = { |
| T(test_memcg_subtree_control), |
| T(test_memcg_current), |
| T(test_memcg_min), |
| T(test_memcg_low), |
| T(test_memcg_high), |
| T(test_memcg_max), |
| T(test_memcg_oom_events), |
| T(test_memcg_swap_max), |
| T(test_memcg_sock), |
| T(test_memcg_oom_group_leaf_events), |
| T(test_memcg_oom_group_parent_events), |
| T(test_memcg_oom_group_score_events), |
| }; |
| #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"); |
| |
| 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; |
| } |