Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
@@ -0,0 +1,12 @@
# SPDX-License-Identifier: GPL-2.0-only
test_core
test_cpu
test_cpuset
test_freezer
test_hugetlb_memcg
test_kill
test_kmem
test_memcontrol
test_pids
test_zswap
wait_inotify
@@ -0,0 +1,35 @@
# SPDX-License-Identifier: GPL-2.0
CFLAGS += -Wall -pthread
all: ${HELPER_PROGS}
TEST_FILES := with_stress.sh
TEST_PROGS := test_stress.sh test_cpuset_prs.sh test_cpuset_v1_hp.sh
TEST_GEN_FILES := wait_inotify
# Keep the lists lexicographically sorted
TEST_GEN_PROGS = test_core
TEST_GEN_PROGS += test_cpu
TEST_GEN_PROGS += test_cpuset
TEST_GEN_PROGS += test_freezer
TEST_GEN_PROGS += test_hugetlb_memcg
TEST_GEN_PROGS += test_kill
TEST_GEN_PROGS += test_kmem
TEST_GEN_PROGS += test_memcontrol
TEST_GEN_PROGS += test_pids
TEST_GEN_PROGS += test_zswap
LOCAL_HDRS += $(selfdir)/clone3/clone3_selftests.h $(selfdir)/pidfd/pidfd.h
include ../lib.mk
include lib/libcgroup.mk
$(OUTPUT)/test_core: $(LIBCGROUP_O)
$(OUTPUT)/test_cpu: $(LIBCGROUP_O)
$(OUTPUT)/test_cpuset: $(LIBCGROUP_O)
$(OUTPUT)/test_freezer: $(LIBCGROUP_O)
$(OUTPUT)/test_hugetlb_memcg: $(LIBCGROUP_O)
$(OUTPUT)/test_kill: $(LIBCGROUP_O)
$(OUTPUT)/test_kmem: $(LIBCGROUP_O)
$(OUTPUT)/test_memcontrol: $(LIBCGROUP_O)
$(OUTPUT)/test_pids: $(LIBCGROUP_O)
$(OUTPUT)/test_zswap: $(LIBCGROUP_O)
@@ -0,0 +1,6 @@
CONFIG_CGROUPS=y
CONFIG_CGROUP_CPUACCT=y
CONFIG_CGROUP_FREEZER=y
CONFIG_CGROUP_SCHED=y
CONFIG_MEMCG=y
CONFIG_PAGE_COUNTER=y
@@ -0,0 +1,679 @@
/* SPDX-License-Identifier: GPL-2.0 */
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/limits.h>
#include <poll.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "cgroup_util.h"
#include "../../clone3/clone3_selftests.h"
bool cg_test_v1_named;
/* Returns read len on success, or -errno on failure. */
ssize_t read_text(const char *path, char *buf, size_t max_len)
{
ssize_t len;
int fd;
fd = open(path, O_RDONLY);
if (fd < 0)
return -errno;
len = read(fd, buf, max_len - 1);
if (len >= 0)
buf[len] = 0;
close(fd);
return len < 0 ? -errno : len;
}
/* Returns written len on success, or -errno on failure. */
ssize_t write_text(const char *path, char *buf, ssize_t len)
{
int fd;
fd = open(path, O_WRONLY | O_APPEND);
if (fd < 0)
return -errno;
len = write(fd, buf, len);
close(fd);
return len < 0 ? -errno : len;
}
char *cg_name(const char *root, const char *name)
{
size_t len = strlen(root) + strlen(name) + 2;
char *ret = malloc(len);
if (ret)
snprintf(ret, len, "%s/%s", root, name);
return ret;
}
char *cg_name_indexed(const char *root, const char *name, int index)
{
size_t len = strlen(root) + strlen(name) + 10;
char *ret = malloc(len);
if (ret)
snprintf(ret, len, "%s/%s_%d", root, name, index);
return ret;
}
char *cg_control(const char *cgroup, const char *control)
{
size_t len = strlen(cgroup) + strlen(control) + 2;
char *ret = malloc(len);
if (ret)
snprintf(ret, len, "%s/%s", cgroup, control);
return ret;
}
/* Returns 0 on success, or -errno on failure. */
int cg_read(const char *cgroup, const char *control, char *buf, size_t len)
{
char path[PATH_MAX];
ssize_t ret;
snprintf(path, sizeof(path), "%s/%s", cgroup, control);
ret = read_text(path, buf, len);
return ret >= 0 ? 0 : ret;
}
int cg_read_strcmp(const char *cgroup, const char *control,
const char *expected)
{
size_t size;
char *buf;
int ret;
/* Handle the case of comparing against empty string */
if (!expected)
return -1;
/* needs size > 1, otherwise cg_read() reads 0 bytes */
size = (expected[0] == '\0') ? 2 : strlen(expected) + 1;
buf = malloc(size);
if (!buf)
return -1;
if (cg_read(cgroup, control, buf, size)) {
free(buf);
return -1;
}
ret = strcmp(expected, buf);
free(buf);
return ret;
}
int cg_read_strcmp_wait(const char *cgroup, const char *control,
const char *expected)
{
int i, ret;
for (i = 0; i < 100; i++) {
ret = cg_read_strcmp(cgroup, control, expected);
if (!ret)
return ret;
usleep(10000);
}
return ret;
}
int cg_read_strstr(const char *cgroup, const char *control, const char *needle)
{
char buf[BUF_SIZE];
if (cg_read(cgroup, control, buf, sizeof(buf)))
return -1;
return strstr(buf, needle) ? 0 : -1;
}
long cg_read_long(const char *cgroup, const char *control)
{
char buf[128];
if (cg_read(cgroup, control, buf, sizeof(buf)))
return -1;
return atol(buf);
}
long cg_read_long_fd(int fd)
{
char buf[128];
if (pread(fd, buf, sizeof(buf), 0) <= 0)
return -1;
return atol(buf);
}
long cg_read_key_long(const char *cgroup, const char *control, const char *key)
{
char buf[BUF_SIZE];
char *ptr;
if (cg_read(cgroup, control, buf, sizeof(buf)))
return -1;
ptr = strstr(buf, key);
if (!ptr)
return -1;
return atol(ptr + strlen(key));
}
long cg_read_key_long_poll(const char *cgroup, const char *control,
const char *key, long expected, int retries,
useconds_t wait_interval_us)
{
long val = -1;
int i;
for (i = 0; i < retries; i++) {
val = cg_read_key_long(cgroup, control, key);
if (val < 0)
return val;
if (val == expected)
break;
usleep(wait_interval_us);
}
return val;
}
long cg_read_lc(const char *cgroup, const char *control)
{
char buf[BUF_SIZE];
const char delim[] = "\n";
char *line;
long cnt = 0;
if (cg_read(cgroup, control, buf, sizeof(buf)))
return -1;
for (line = strtok(buf, delim); line; line = strtok(NULL, delim))
cnt++;
return cnt;
}
/* Returns 0 on success, or -errno on failure. */
int cg_write(const char *cgroup, const char *control, char *buf)
{
char path[PATH_MAX];
ssize_t len = strlen(buf), ret;
snprintf(path, sizeof(path), "%s/%s", cgroup, control);
ret = write_text(path, buf, len);
return ret == len ? 0 : ret;
}
/*
* Returns fd on success, or -1 on failure.
* (fd should be closed with close() as usual)
*/
int cg_open(const char *cgroup, const char *control, int flags)
{
char path[PATH_MAX];
snprintf(path, sizeof(path), "%s/%s", cgroup, control);
return open(path, flags);
}
int cg_write_numeric(const char *cgroup, const char *control, long value)
{
char buf[64];
int ret;
ret = sprintf(buf, "%lu", value);
if (ret < 0)
return ret;
return cg_write(cgroup, control, buf);
}
static int cg_find_root(char *root, size_t len, const char *controller,
bool *nsdelegate)
{
char buf[10 * BUF_SIZE];
char *fs, *mount, *type, *options;
const char delim[] = "\n\t ";
if (read_text("/proc/self/mounts", buf, sizeof(buf)) <= 0)
return -1;
/*
* Example:
* cgroup /sys/fs/cgroup cgroup2 rw,seclabel,noexec,relatime 0 0
*/
for (fs = strtok(buf, delim); fs; fs = strtok(NULL, delim)) {
mount = strtok(NULL, delim);
type = strtok(NULL, delim);
options = strtok(NULL, delim);
strtok(NULL, delim);
strtok(NULL, delim);
if (strcmp(type, "cgroup") == 0) {
if (!controller || !strstr(options, controller))
continue;
} else if (strcmp(type, "cgroup2") == 0) {
if (controller &&
cg_read_strstr(mount, "cgroup.controllers", controller))
continue;
} else {
continue;
}
strncpy(root, mount, len);
if (nsdelegate)
*nsdelegate = !!strstr(options, "nsdelegate");
return 0;
}
return -1;
}
int cg_find_controller_root(char *root, size_t len, const char *controller)
{
return cg_find_root(root, len, controller, NULL);
}
int cg_find_unified_root(char *root, size_t len, bool *nsdelegate)
{
return cg_find_root(root, len, NULL, nsdelegate);
}
int cg_create(const char *cgroup)
{
return mkdir(cgroup, 0755);
}
int cg_wait_for_proc_count(const char *cgroup, int count)
{
char buf[10 * BUF_SIZE] = {0};
int attempts;
char *ptr;
for (attempts = 10; attempts >= 0; attempts--) {
int nr = 0;
if (cg_read(cgroup, "cgroup.procs", buf, sizeof(buf)))
break;
for (ptr = buf; *ptr; ptr++)
if (*ptr == '\n')
nr++;
if (nr >= count)
return 0;
usleep(100000);
}
return -1;
}
int cg_killall(const char *cgroup)
{
char buf[BUF_SIZE];
char *ptr = buf;
/* If cgroup.kill exists use it. */
if (!cg_write(cgroup, "cgroup.kill", "1"))
return 0;
if (cg_read(cgroup, "cgroup.procs", buf, sizeof(buf)))
return -1;
while (ptr < buf + sizeof(buf)) {
int pid = strtol(ptr, &ptr, 10);
if (pid == 0)
break;
if (*ptr)
ptr++;
else
break;
if (kill(pid, SIGKILL))
return -1;
}
return 0;
}
int cg_destroy(const char *cgroup)
{
int ret;
if (!cgroup)
return 0;
retry:
ret = rmdir(cgroup);
if (ret && errno == EBUSY) {
cg_killall(cgroup);
usleep(100);
goto retry;
}
if (ret && errno == ENOENT)
ret = 0;
return ret;
}
int cg_enter(const char *cgroup, int pid)
{
char pidbuf[64];
snprintf(pidbuf, sizeof(pidbuf), "%d", pid);
return cg_write(cgroup, "cgroup.procs", pidbuf);
}
int cg_enter_current(const char *cgroup)
{
return cg_write(cgroup, "cgroup.procs", "0");
}
int cg_enter_current_thread(const char *cgroup)
{
return cg_write(cgroup, CG_THREADS_FILE, "0");
}
int cg_run(const char *cgroup,
int (*fn)(const char *cgroup, void *arg),
void *arg)
{
int pid, retcode;
pid = fork();
if (pid < 0) {
return pid;
} else if (pid == 0) {
char buf[64];
snprintf(buf, sizeof(buf), "%d", getpid());
if (cg_write(cgroup, "cgroup.procs", buf))
exit(EXIT_FAILURE);
exit(fn(cgroup, arg));
} else {
waitpid(pid, &retcode, 0);
if (WIFEXITED(retcode))
return WEXITSTATUS(retcode);
else
return -1;
}
}
pid_t clone_into_cgroup(int cgroup_fd)
{
#ifdef CLONE_ARGS_SIZE_VER2
pid_t pid;
struct __clone_args args = {
.flags = CLONE_INTO_CGROUP,
.exit_signal = SIGCHLD,
.cgroup = cgroup_fd,
};
pid = sys_clone3(&args, sizeof(struct __clone_args));
/*
* Verify that this is a genuine test failure:
* ENOSYS -> clone3() not available
* E2BIG -> CLONE_INTO_CGROUP not available
*/
if (pid < 0 && (errno == ENOSYS || errno == E2BIG))
goto pretend_enosys;
return pid;
pretend_enosys:
#endif
errno = ENOSYS;
return -ENOSYS;
}
int clone_reap(pid_t pid, int options)
{
int ret;
siginfo_t info = {
.si_signo = 0,
};
again:
ret = waitid(P_PID, pid, &info, options | __WALL | __WNOTHREAD);
if (ret < 0) {
if (errno == EINTR)
goto again;
return -1;
}
if (options & WEXITED) {
if (WIFEXITED(info.si_status))
return WEXITSTATUS(info.si_status);
}
if (options & WSTOPPED) {
if (WIFSTOPPED(info.si_status))
return WSTOPSIG(info.si_status);
}
if (options & WCONTINUED) {
if (WIFCONTINUED(info.si_status))
return 0;
}
return -1;
}
int dirfd_open_opath(const char *dir)
{
return open(dir, O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW | O_PATH);
}
#define close_prot_errno(fd) \
if (fd >= 0) { \
int _e_ = errno; \
close(fd); \
errno = _e_; \
}
static int clone_into_cgroup_run_nowait(const char *cgroup,
int (*fn)(const char *cgroup, void *arg),
void *arg)
{
int cgroup_fd;
pid_t pid;
cgroup_fd = dirfd_open_opath(cgroup);
if (cgroup_fd < 0)
return -1;
pid = clone_into_cgroup(cgroup_fd);
close_prot_errno(cgroup_fd);
if (pid == 0)
exit(fn(cgroup, arg));
return pid;
}
int cg_run_nowait(const char *cgroup,
int (*fn)(const char *cgroup, void *arg),
void *arg)
{
int pid;
pid = clone_into_cgroup_run_nowait(cgroup, fn, arg);
if (pid > 0)
return pid;
/* Genuine test failure. */
if (pid < 0 && errno != ENOSYS)
return -1;
pid = fork();
if (pid == 0) {
char buf[64];
snprintf(buf, sizeof(buf), "%d", getpid());
if (cg_write(cgroup, "cgroup.procs", buf))
exit(EXIT_FAILURE);
exit(fn(cgroup, arg));
}
return pid;
}
int proc_mount_contains(const char *option)
{
char buf[4 * BUF_SIZE];
ssize_t read;
read = read_text("/proc/mounts", buf, sizeof(buf));
if (read < 0)
return read;
return strstr(buf, option) != NULL;
}
int cgroup_feature(const char *feature)
{
char buf[BUF_SIZE];
ssize_t read;
read = read_text("/sys/kernel/cgroup/features", buf, sizeof(buf));
if (read < 0)
return read;
return strstr(buf, feature) != NULL;
}
ssize_t proc_read_text(int pid, bool thread, const char *item, char *buf, size_t size)
{
char path[PATH_MAX];
ssize_t ret;
if (!pid)
snprintf(path, sizeof(path), "/proc/%s/%s",
thread ? "thread-self" : "self", item);
else
snprintf(path, sizeof(path), "/proc/%d/%s", pid, item);
ret = read_text(path, buf, size);
return ret < 0 ? -1 : ret;
}
int proc_read_strstr(int pid, bool thread, const char *item, const char *needle)
{
char buf[BUF_SIZE];
if (proc_read_text(pid, thread, item, buf, sizeof(buf)) < 0)
return -1;
return strstr(buf, needle) ? 0 : -1;
}
int clone_into_cgroup_run_wait(const char *cgroup)
{
int cgroup_fd;
pid_t pid;
cgroup_fd = dirfd_open_opath(cgroup);
if (cgroup_fd < 0)
return -1;
pid = clone_into_cgroup(cgroup_fd);
close_prot_errno(cgroup_fd);
if (pid < 0)
return -1;
if (pid == 0)
exit(EXIT_SUCCESS);
/*
* We don't care whether this fails. We only care whether the initial
* clone succeeded.
*/
(void)clone_reap(pid, WEXITED);
return 0;
}
static int __prepare_for_wait(const char *cgroup, const char *filename)
{
int fd, ret = -1;
fd = inotify_init1(0);
if (fd == -1)
return fd;
ret = inotify_add_watch(fd, cg_control(cgroup, filename), IN_MODIFY);
if (ret == -1) {
close(fd);
fd = -1;
}
return fd;
}
int cg_prepare_for_wait(const char *cgroup)
{
return __prepare_for_wait(cgroup, "cgroup.events");
}
int memcg_prepare_for_wait(const char *cgroup)
{
return __prepare_for_wait(cgroup, "memory.events");
}
int cg_wait_for(int fd)
{
int ret = -1;
struct pollfd fds = {
.fd = fd,
.events = POLLIN,
};
while (true) {
ret = poll(&fds, 1, 10000);
if (ret == -1) {
if (errno == EINTR)
continue;
break;
}
if (ret > 0 && fds.revents & POLLIN) {
ret = 0;
break;
}
}
return ret;
}
@@ -0,0 +1,100 @@
/* SPDX-License-Identifier: GPL-2.0 */
#include <stdbool.h>
#include <stdlib.h>
#ifndef BUF_SIZE
#define BUF_SIZE 4096
#endif
#define MB(x) (x << 20)
#define USEC_PER_SEC 1000000L
#define NSEC_PER_SEC 1000000000L
#define TEST_UID 65534 /* usually nobody, any !root is fine */
#define CG_THREADS_FILE (!cg_test_v1_named ? "cgroup.threads" : "tasks")
#define CG_NAMED_NAME "selftest"
#define CG_PATH_FORMAT (!cg_test_v1_named ? "0::%s" : (":name=" CG_NAMED_NAME ":%s"))
#define DEFAULT_WAIT_INTERVAL_US (100 * 1000) /* 100 ms */
/*
* Checks if two given values differ by less than err% of their sum.
*/
static inline int values_close(long a, long b, int err)
{
return labs(a - b) <= (a + b) / 100 * err;
}
/*
* Checks if two given values differ by less than err% of their sum and assert
* with detailed debug info if not.
*/
static inline int values_close_report(long a, long b, int err)
{
long diff = labs(a - b);
long limit = (a + b) / 100 * err;
double actual_err = (a + b) ? (100.0 * diff / (a + b)) : 0.0;
int close = diff <= limit;
if (!close)
fprintf(stderr,
"[FAIL] actual=%ld expected=%ld | diff=%ld | limit=%ld | "
"tolerance=%d%% | actual_error=%.2f%%\n",
a, b, diff, limit, err, actual_err);
return close;
}
extern ssize_t read_text(const char *path, char *buf, size_t max_len);
extern ssize_t write_text(const char *path, char *buf, ssize_t len);
extern int cg_find_controller_root(char *root, size_t len, const char *controller);
extern int cg_find_unified_root(char *root, size_t len, bool *nsdelegate);
extern char *cg_name(const char *root, const char *name);
extern char *cg_name_indexed(const char *root, const char *name, int index);
extern char *cg_control(const char *cgroup, const char *control);
extern int cg_create(const char *cgroup);
extern int cg_destroy(const char *cgroup);
extern int cg_read(const char *cgroup, const char *control,
char *buf, size_t len);
extern int cg_read_strcmp(const char *cgroup, const char *control,
const char *expected);
extern int cg_read_strcmp_wait(const char *cgroup, const char *control,
const char *expected);
extern int cg_read_strstr(const char *cgroup, const char *control,
const char *needle);
extern long cg_read_long(const char *cgroup, const char *control);
extern long cg_read_long_fd(int fd);
long cg_read_key_long(const char *cgroup, const char *control, const char *key);
long cg_read_key_long_poll(const char *cgroup, const char *control,
const char *key, long expected, int retries,
useconds_t wait_interval_us);
extern long cg_read_lc(const char *cgroup, const char *control);
extern int cg_write(const char *cgroup, const char *control, char *buf);
extern int cg_open(const char *cgroup, const char *control, int flags);
int cg_write_numeric(const char *cgroup, const char *control, long value);
extern int cg_run(const char *cgroup,
int (*fn)(const char *cgroup, void *arg),
void *arg);
extern int cg_enter(const char *cgroup, int pid);
extern int cg_enter_current(const char *cgroup);
extern int cg_enter_current_thread(const char *cgroup);
extern int cg_run_nowait(const char *cgroup,
int (*fn)(const char *cgroup, void *arg),
void *arg);
extern int cg_wait_for_proc_count(const char *cgroup, int count);
extern int cg_killall(const char *cgroup);
int proc_mount_contains(const char *option);
int cgroup_feature(const char *feature);
extern ssize_t proc_read_text(int pid, bool thread, const char *item, char *buf, size_t size);
extern int proc_read_strstr(int pid, bool thread, const char *item, const char *needle);
extern pid_t clone_into_cgroup(int cgroup_fd);
extern int clone_reap(pid_t pid, int options);
extern int clone_into_cgroup_run_wait(const char *cgroup);
extern int dirfd_open_opath(const char *dir);
extern int cg_prepare_for_wait(const char *cgroup);
extern int memcg_prepare_for_wait(const char *cgroup);
extern int cg_wait_for(int fd);
extern bool cg_test_v1_named;
@@ -0,0 +1,19 @@
CGROUP_DIR := $(selfdir)/cgroup
LIBCGROUP_C := lib/cgroup_util.c
LIBCGROUP_O := $(patsubst %.c, $(OUTPUT)/%.o, $(LIBCGROUP_C))
LIBCGROUP_O_DIRS := $(shell dirname $(LIBCGROUP_O) | uniq)
CFLAGS += -I$(CGROUP_DIR)/lib/include
EXTRA_HDRS := $(selfdir)/clone3/clone3_selftests.h
$(LIBCGROUP_O_DIRS):
mkdir -p $@
$(LIBCGROUP_O): $(OUTPUT)/%.o : $(CGROUP_DIR)/%.c $(EXTRA_HDRS) $(LIBCGROUP_O_DIRS)
$(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c $< -o $@
EXTRA_CLEAN += $(LIBCGROUP_O)
@@ -0,0 +1,89 @@
% SPDX-License-Identifier: GPL-2.0
%
% run as: octave-cli memcg_protection.m
%
% This script simulates reclaim protection behavior on a single level of memcg
% hierarchy to illustrate how overcommitted protection spreads among siblings
% (as it depends also on their current consumption).
%
% Simulation assumes siblings consumed the initial amount of memory (w/out
% reclaim) and then the reclaim starts, all memory is reclaimable, i.e. treated
% same. It simulates only non-low reclaim and assumes all memory.min = 0.
%
% Input configurations
% --------------------
% E number parent effective protection
% n vector nominal protection of siblings set at the given level (memory.low)
% c vector current consumption -,,- (memory.current)
% example from testcase (values in GB)
E = 50 / 1024;
n = [75 25 0 500 ] / 1024;
c = [50 50 50 0] / 1024;
% Reclaim parameters
% ------------------
% Minimal reclaim amount (GB)
cluster = 32*4 / 2**20;
% Reclaim coefficient (think as 0.5^sc->priority)
alpha = .1
% Simulation parameters
% ---------------------
epsilon = 1e-7;
timeout = 1000;
% Simulation loop
% ---------------
ch = [];
eh = [];
rh = [];
for t = 1:timeout
% low_usage
u = min(c, n);
siblings = sum(u);
% effective_protection()
protected = min(n, c); % start with nominal
e = protected * min(1, E / siblings); % normalize overcommit
% recursive protection
unclaimed = max(0, E - siblings);
parent_overuse = sum(c) - siblings;
if (unclaimed > 0 && parent_overuse > 0)
overuse = max(0, c - protected);
e += unclaimed * (overuse / parent_overuse);
endif
% get_scan_count()
r = alpha * c; % assume all memory is in a single LRU list
% commit 1bc63fb1272b ("mm, memcg: make scan aggression always exclude protection")
sz = max(e, c);
r .*= (1 - (e+epsilon) ./ (sz+epsilon));
% uncomment to debug prints
% e, c, r
% nothing to reclaim, reached equilibrium
if max(r) < epsilon
break;
endif
% SWAP_CLUSTER_MAX roundup
r = max(r, (r > epsilon) .* cluster);
% XXX here I do parallel reclaim of all siblings
% in reality reclaim is serialized and each sibling recalculates own residual
c = max(c - r, 0);
ch = [ch ; c];
eh = [eh ; e];
rh = [rh ; r];
endfor
t
c, e
@@ -0,0 +1,959 @@
/* SPDX-License-Identifier: GPL-2.0 */
#define _GNU_SOURCE
#include <linux/limits.h>
#include <linux/sched.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <stdio.h>
#include <errno.h>
#include <signal.h>
#include <string.h>
#include <pthread.h>
#include "kselftest.h"
#include "cgroup_util.h"
static bool nsdelegate;
#ifndef CLONE_NEWCGROUP
#define CLONE_NEWCGROUP 0
#endif
static int touch_anon(char *buf, size_t size)
{
int fd;
char *pos = buf;
fd = open("/dev/urandom", O_RDONLY);
if (fd < 0)
return -1;
while (size > 0) {
ssize_t ret = read(fd, pos, size);
if (ret < 0) {
if (errno != EINTR) {
close(fd);
return -1;
}
} else {
pos += ret;
size -= ret;
}
}
close(fd);
return 0;
}
static int alloc_and_touch_anon_noexit(const char *cgroup, void *arg)
{
int ppid = getppid();
size_t size = (size_t)arg;
void *buf;
buf = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON,
0, 0);
if (buf == MAP_FAILED)
return -1;
if (touch_anon((char *)buf, size)) {
munmap(buf, size);
return -1;
}
while (getppid() == ppid)
sleep(1);
munmap(buf, size);
return 0;
}
/*
* Create a child process that allocates and touches 100MB, then waits to be
* killed. Wait until the child is attached to the cgroup, kill all processes
* in that cgroup and wait until "cgroup.procs" is empty. At this point try to
* destroy the empty cgroup. The test helps detect race conditions between
* dying processes leaving the cgroup and cgroup destruction path.
*/
static int test_cgcore_destroy(const char *root)
{
int ret = KSFT_FAIL;
char *cg_test = NULL;
int child_pid;
char buf[BUF_SIZE];
cg_test = cg_name(root, "cg_test");
if (!cg_test)
goto cleanup;
for (int i = 0; i < 10; i++) {
if (cg_create(cg_test))
goto cleanup;
child_pid = cg_run_nowait(cg_test, alloc_and_touch_anon_noexit,
(void *) MB(100));
if (child_pid < 0)
goto cleanup;
/* wait for the child to enter cgroup */
if (cg_wait_for_proc_count(cg_test, 1))
goto cleanup;
if (cg_killall(cg_test))
goto cleanup;
/* wait for cgroup to be empty */
while (1) {
if (cg_read(cg_test, "cgroup.procs", buf, sizeof(buf)))
goto cleanup;
if (buf[0] == '\0')
break;
usleep(1000);
}
if (rmdir(cg_test))
goto cleanup;
if (waitpid(child_pid, NULL, 0) < 0)
goto cleanup;
}
ret = KSFT_PASS;
cleanup:
if (cg_test)
cg_destroy(cg_test);
free(cg_test);
return ret;
}
/*
* A(0) - B(0) - C(1)
* \ D(0)
*
* A, B and C's "populated" fields would be 1 while D's 0.
* test that after the one process in C is moved to root,
* A,B and C's "populated" fields would flip to "0" and file
* modified events will be generated on the
* "cgroup.events" files of both cgroups.
*/
static int test_cgcore_populated(const char *root)
{
int ret = KSFT_FAIL;
int err;
char *cg_test_a = NULL, *cg_test_b = NULL;
char *cg_test_c = NULL, *cg_test_d = NULL;
int cgroup_fd = -EBADF;
pid_t pid;
if (cg_test_v1_named)
return KSFT_SKIP;
cg_test_a = cg_name(root, "cg_test_a");
cg_test_b = cg_name(root, "cg_test_a/cg_test_b");
cg_test_c = cg_name(root, "cg_test_a/cg_test_b/cg_test_c");
cg_test_d = cg_name(root, "cg_test_a/cg_test_b/cg_test_d");
if (!cg_test_a || !cg_test_b || !cg_test_c || !cg_test_d)
goto cleanup;
if (cg_create(cg_test_a))
goto cleanup;
if (cg_create(cg_test_b))
goto cleanup;
if (cg_create(cg_test_c))
goto cleanup;
if (cg_create(cg_test_d))
goto cleanup;
if (cg_enter_current(cg_test_c))
goto cleanup;
if (cg_read_strcmp(cg_test_a, "cgroup.events", "populated 1\n"))
goto cleanup;
if (cg_read_strcmp(cg_test_b, "cgroup.events", "populated 1\n"))
goto cleanup;
if (cg_read_strcmp(cg_test_c, "cgroup.events", "populated 1\n"))
goto cleanup;
if (cg_read_strcmp(cg_test_d, "cgroup.events", "populated 0\n"))
goto cleanup;
if (cg_enter_current(root))
goto cleanup;
if (cg_read_strcmp(cg_test_a, "cgroup.events", "populated 0\n"))
goto cleanup;
if (cg_read_strcmp(cg_test_b, "cgroup.events", "populated 0\n"))
goto cleanup;
if (cg_read_strcmp(cg_test_c, "cgroup.events", "populated 0\n"))
goto cleanup;
if (cg_read_strcmp(cg_test_d, "cgroup.events", "populated 0\n"))
goto cleanup;
/* Test that we can directly clone into a new cgroup. */
cgroup_fd = dirfd_open_opath(cg_test_d);
if (cgroup_fd < 0)
goto cleanup;
pid = clone_into_cgroup(cgroup_fd);
if (pid < 0) {
if (errno == ENOSYS)
goto cleanup_pass;
goto cleanup;
}
if (pid == 0) {
if (raise(SIGSTOP))
exit(EXIT_FAILURE);
exit(EXIT_SUCCESS);
}
err = cg_read_strcmp(cg_test_d, "cgroup.events", "populated 1\n");
(void)clone_reap(pid, WSTOPPED);
(void)kill(pid, SIGCONT);
(void)clone_reap(pid, WEXITED);
if (err)
goto cleanup;
if (cg_read_strcmp_wait(cg_test_d, "cgroup.events",
"populated 0\n"))
goto cleanup;
/* Remove cgroup. */
if (cg_test_d) {
cg_destroy(cg_test_d);
free(cg_test_d);
cg_test_d = NULL;
}
pid = clone_into_cgroup(cgroup_fd);
if (pid < 0)
goto cleanup_pass;
if (pid == 0)
exit(EXIT_SUCCESS);
(void)clone_reap(pid, WEXITED);
goto cleanup;
cleanup_pass:
ret = KSFT_PASS;
cleanup:
if (cg_test_d)
cg_destroy(cg_test_d);
if (cg_test_c)
cg_destroy(cg_test_c);
if (cg_test_b)
cg_destroy(cg_test_b);
if (cg_test_a)
cg_destroy(cg_test_a);
free(cg_test_d);
free(cg_test_c);
free(cg_test_b);
free(cg_test_a);
if (cgroup_fd >= 0)
close(cgroup_fd);
return ret;
}
/*
* A (domain threaded) - B (threaded) - C (domain)
*
* test that C can't be used until it is turned into a
* threaded cgroup. "cgroup.type" file will report "domain (invalid)" in
* these cases. Operations which fail due to invalid topology use
* EOPNOTSUPP as the errno.
*/
static int test_cgcore_invalid_domain(const char *root)
{
int ret = KSFT_FAIL;
char *grandparent = NULL, *parent = NULL, *child = NULL;
if (cg_test_v1_named)
return KSFT_SKIP;
grandparent = cg_name(root, "cg_test_grandparent");
parent = cg_name(root, "cg_test_grandparent/cg_test_parent");
child = cg_name(root, "cg_test_grandparent/cg_test_parent/cg_test_child");
if (!parent || !child || !grandparent)
goto cleanup;
if (cg_create(grandparent))
goto cleanup;
if (cg_create(parent))
goto cleanup;
if (cg_create(child))
goto cleanup;
if (cg_write(parent, "cgroup.type", "threaded"))
goto cleanup;
if (cg_read_strcmp(child, "cgroup.type", "domain invalid\n"))
goto cleanup;
if (!cg_enter_current(child))
goto cleanup;
if (errno != EOPNOTSUPP)
goto cleanup;
if (!clone_into_cgroup_run_wait(child))
goto cleanup;
if (errno == ENOSYS)
goto cleanup_pass;
if (errno != EOPNOTSUPP)
goto cleanup;
cleanup_pass:
ret = KSFT_PASS;
cleanup:
cg_enter_current(root);
if (child)
cg_destroy(child);
if (parent)
cg_destroy(parent);
if (grandparent)
cg_destroy(grandparent);
free(child);
free(parent);
free(grandparent);
return ret;
}
/*
* Test that when a child becomes threaded
* the parent type becomes domain threaded.
*/
static int test_cgcore_parent_becomes_threaded(const char *root)
{
int ret = KSFT_FAIL;
char *parent = NULL, *child = NULL;
if (cg_test_v1_named)
return KSFT_SKIP;
parent = cg_name(root, "cg_test_parent");
child = cg_name(root, "cg_test_parent/cg_test_child");
if (!parent || !child)
goto cleanup;
if (cg_create(parent))
goto cleanup;
if (cg_create(child))
goto cleanup;
if (cg_write(child, "cgroup.type", "threaded"))
goto cleanup;
if (cg_read_strcmp(parent, "cgroup.type", "domain threaded\n"))
goto cleanup;
ret = KSFT_PASS;
cleanup:
if (child)
cg_destroy(child);
if (parent)
cg_destroy(parent);
free(child);
free(parent);
return ret;
}
/*
* Test that there's no internal process constrain on threaded cgroups.
* You can add threads/processes on a parent with a controller enabled.
*/
static int test_cgcore_no_internal_process_constraint_on_threads(const char *root)
{
int ret = KSFT_FAIL;
char *parent = NULL, *child = NULL;
if (cg_test_v1_named ||
cg_read_strstr(root, "cgroup.controllers", "cpu") ||
cg_write(root, "cgroup.subtree_control", "+cpu")) {
ret = KSFT_SKIP;
goto cleanup;
}
parent = cg_name(root, "cg_test_parent");
child = cg_name(root, "cg_test_parent/cg_test_child");
if (!parent || !child)
goto cleanup;
if (cg_create(parent))
goto cleanup;
if (cg_create(child))
goto cleanup;
if (cg_write(parent, "cgroup.type", "threaded"))
goto cleanup;
if (cg_write(child, "cgroup.type", "threaded"))
goto cleanup;
if (cg_write(parent, "cgroup.subtree_control", "+cpu"))
goto cleanup;
if (cg_enter_current(parent))
goto cleanup;
ret = KSFT_PASS;
cleanup:
cg_enter_current(root);
cg_enter_current(root);
if (child)
cg_destroy(child);
if (parent)
cg_destroy(parent);
free(child);
free(parent);
return ret;
}
/*
* Test that you can't enable a controller on a child if it's not enabled
* on the parent.
*/
static int test_cgcore_top_down_constraint_enable(const char *root)
{
int ret = KSFT_FAIL;
char *parent = NULL, *child = NULL;
if (cg_test_v1_named)
return KSFT_SKIP;
parent = cg_name(root, "cg_test_parent");
child = cg_name(root, "cg_test_parent/cg_test_child");
if (!parent || !child)
goto cleanup;
if (cg_create(parent))
goto cleanup;
if (cg_create(child))
goto cleanup;
if (!cg_write(child, "cgroup.subtree_control", "+memory"))
goto cleanup;
ret = KSFT_PASS;
cleanup:
if (child)
cg_destroy(child);
if (parent)
cg_destroy(parent);
free(child);
free(parent);
return ret;
}
/*
* Test that you can't disable a controller on a parent
* if it's enabled in a child.
*/
static int test_cgcore_top_down_constraint_disable(const char *root)
{
int ret = KSFT_FAIL;
char *parent = NULL, *child = NULL;
if (cg_test_v1_named)
return KSFT_SKIP;
parent = cg_name(root, "cg_test_parent");
child = cg_name(root, "cg_test_parent/cg_test_child");
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, "cgroup.subtree_control", "+memory"))
goto cleanup;
if (!cg_write(parent, "cgroup.subtree_control", "-memory"))
goto cleanup;
ret = KSFT_PASS;
cleanup:
if (child)
cg_destroy(child);
if (parent)
cg_destroy(parent);
free(child);
free(parent);
return ret;
}
/*
* Test internal process constraint.
* You can't add a pid to a domain parent if a controller is enabled.
*/
static int test_cgcore_internal_process_constraint(const char *root)
{
int ret = KSFT_FAIL;
char *parent = NULL, *child = NULL;
if (cg_test_v1_named)
return KSFT_SKIP;
parent = cg_name(root, "cg_test_parent");
child = cg_name(root, "cg_test_parent/cg_test_child");
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_enter_current(parent))
goto cleanup;
if (!clone_into_cgroup_run_wait(parent))
goto cleanup;
ret = KSFT_PASS;
cleanup:
if (child)
cg_destroy(child);
if (parent)
cg_destroy(parent);
free(child);
free(parent);
return ret;
}
static void *dummy_thread_fn(void *arg)
{
return (void *)(size_t)pause();
}
/*
* Test threadgroup migration.
* All threads of a process are migrated together.
*/
static int test_cgcore_proc_migration(const char *root)
{
int ret = KSFT_FAIL;
int t, c_threads = 0, n_threads = 13;
char *src = NULL, *dst = NULL;
pthread_t threads[n_threads];
src = cg_name(root, "cg_src");
dst = cg_name(root, "cg_dst");
if (!src || !dst)
goto cleanup;
if (cg_create(src))
goto cleanup;
if (cg_create(dst))
goto cleanup;
if (cg_enter_current(src))
goto cleanup;
for (c_threads = 0; c_threads < n_threads; ++c_threads) {
if (pthread_create(&threads[c_threads], NULL, dummy_thread_fn, NULL))
goto cleanup;
}
cg_enter_current(dst);
if (cg_read_lc(dst, CG_THREADS_FILE) != n_threads + 1)
goto cleanup;
ret = KSFT_PASS;
cleanup:
for (t = 0; t < c_threads; ++t) {
pthread_cancel(threads[t]);
}
for (t = 0; t < c_threads; ++t) {
pthread_join(threads[t], NULL);
}
cg_enter_current(root);
if (dst)
cg_destroy(dst);
if (src)
cg_destroy(src);
free(dst);
free(src);
return ret;
}
static void *migrating_thread_fn(void *arg)
{
int g, i, n_iterations = 1000;
char **grps = arg;
char lines[3][PATH_MAX];
for (g = 1; g < 3; ++g)
snprintf(lines[g], sizeof(lines[g]), CG_PATH_FORMAT, grps[g] + strlen(grps[0]));
for (i = 0; i < n_iterations; ++i) {
cg_enter_current_thread(grps[(i % 2) + 1]);
if (proc_read_strstr(0, 1, "cgroup", lines[(i % 2) + 1]))
return (void *)-1;
}
return NULL;
}
/*
* Test single thread migration.
* Threaded cgroups allow successful migration of a thread.
*/
static int test_cgcore_thread_migration(const char *root)
{
int ret = KSFT_FAIL;
char *dom = NULL;
char line[PATH_MAX];
char *grps[3] = { (char *)root, NULL, NULL };
pthread_t thr;
void *retval;
dom = cg_name(root, "cg_dom");
grps[1] = cg_name(root, "cg_dom/cg_src");
grps[2] = cg_name(root, "cg_dom/cg_dst");
if (!grps[1] || !grps[2] || !dom)
goto cleanup;
if (cg_create(dom))
goto cleanup;
if (cg_create(grps[1]))
goto cleanup;
if (cg_create(grps[2]))
goto cleanup;
if (!cg_test_v1_named) {
if (cg_write(grps[1], "cgroup.type", "threaded"))
goto cleanup;
if (cg_write(grps[2], "cgroup.type", "threaded"))
goto cleanup;
}
if (cg_enter_current(grps[1]))
goto cleanup;
if (pthread_create(&thr, NULL, migrating_thread_fn, grps))
goto cleanup;
if (pthread_join(thr, &retval))
goto cleanup;
if (retval)
goto cleanup;
snprintf(line, sizeof(line), CG_PATH_FORMAT, grps[1] + strlen(grps[0]));
if (proc_read_strstr(0, 1, "cgroup", line))
goto cleanup;
ret = KSFT_PASS;
cleanup:
cg_enter_current(root);
if (grps[2])
cg_destroy(grps[2]);
if (grps[1])
cg_destroy(grps[1]);
if (dom)
cg_destroy(dom);
free(grps[2]);
free(grps[1]);
free(dom);
return ret;
}
/*
* cgroup migration permission check should be performed based on the
* credentials at the time of open instead of write.
*/
static int test_cgcore_lesser_euid_open(const char *root)
{
const uid_t test_euid = TEST_UID;
int ret = KSFT_FAIL;
char *cg_test_a = NULL, *cg_test_b = NULL;
char *cg_test_a_procs = NULL, *cg_test_b_procs = NULL;
int cg_test_b_procs_fd = -1;
uid_t saved_uid;
cg_test_a = cg_name(root, "cg_test_a");
cg_test_b = cg_name(root, "cg_test_b");
if (!cg_test_a || !cg_test_b)
goto cleanup;
cg_test_a_procs = cg_name(cg_test_a, "cgroup.procs");
cg_test_b_procs = cg_name(cg_test_b, "cgroup.procs");
if (!cg_test_a_procs || !cg_test_b_procs)
goto cleanup;
if (cg_create(cg_test_a) || cg_create(cg_test_b))
goto cleanup;
if (cg_enter_current(cg_test_a))
goto cleanup;
if (chown(cg_test_a_procs, test_euid, -1) ||
chown(cg_test_b_procs, test_euid, -1))
goto cleanup;
saved_uid = geteuid();
if (seteuid(test_euid))
goto cleanup;
cg_test_b_procs_fd = open(cg_test_b_procs, O_RDWR);
if (seteuid(saved_uid))
goto cleanup;
if (cg_test_b_procs_fd < 0)
goto cleanup;
if (write(cg_test_b_procs_fd, "0", 1) >= 0 || errno != EACCES)
goto cleanup;
ret = KSFT_PASS;
cleanup:
cg_enter_current(root);
if (cg_test_b_procs_fd >= 0)
close(cg_test_b_procs_fd);
if (cg_test_b)
cg_destroy(cg_test_b);
if (cg_test_a)
cg_destroy(cg_test_a);
free(cg_test_b_procs);
free(cg_test_a_procs);
free(cg_test_b);
free(cg_test_a);
return ret;
}
struct lesser_ns_open_thread_arg {
const char *path;
int fd;
int err;
};
static int lesser_ns_open_thread_fn(void *arg)
{
struct lesser_ns_open_thread_arg *targ = arg;
targ->fd = open(targ->path, O_RDWR);
targ->err = errno;
return 0;
}
/*
* cgroup migration permission check should be performed based on the cgroup
* namespace at the time of open instead of write.
*/
static int test_cgcore_lesser_ns_open(const char *root)
{
static char stack[65536];
const uid_t test_euid = 65534; /* usually nobody, any !root is fine */
int ret = KSFT_FAIL;
char *cg_test_a = NULL, *cg_test_b = NULL;
char *cg_test_a_procs = NULL, *cg_test_b_procs = NULL;
int cg_test_b_procs_fd = -1;
struct lesser_ns_open_thread_arg targ = { .fd = -1 };
pid_t pid;
int status;
if (!nsdelegate)
return KSFT_SKIP;
cg_test_a = cg_name(root, "cg_test_a");
cg_test_b = cg_name(root, "cg_test_b");
if (!cg_test_a || !cg_test_b)
goto cleanup;
cg_test_a_procs = cg_name(cg_test_a, "cgroup.procs");
cg_test_b_procs = cg_name(cg_test_b, "cgroup.procs");
if (!cg_test_a_procs || !cg_test_b_procs)
goto cleanup;
if (cg_create(cg_test_a) || cg_create(cg_test_b))
goto cleanup;
if (cg_enter_current(cg_test_b))
goto cleanup;
if (chown(cg_test_a_procs, test_euid, -1) ||
chown(cg_test_b_procs, test_euid, -1))
goto cleanup;
targ.path = cg_test_b_procs;
pid = clone(lesser_ns_open_thread_fn, stack + sizeof(stack),
CLONE_NEWCGROUP | CLONE_FILES | CLONE_VM | SIGCHLD,
&targ);
if (pid < 0)
goto cleanup;
if (waitpid(pid, &status, 0) < 0)
goto cleanup;
if (!WIFEXITED(status))
goto cleanup;
cg_test_b_procs_fd = targ.fd;
if (cg_test_b_procs_fd < 0)
goto cleanup;
if (cg_enter_current(cg_test_a))
goto cleanup;
if ((status = write(cg_test_b_procs_fd, "0", 1)) >= 0 || errno != ENOENT)
goto cleanup;
ret = KSFT_PASS;
cleanup:
cg_enter_current(root);
if (cg_test_b_procs_fd >= 0)
close(cg_test_b_procs_fd);
if (cg_test_b)
cg_destroy(cg_test_b);
if (cg_test_a)
cg_destroy(cg_test_a);
free(cg_test_b_procs);
free(cg_test_a_procs);
free(cg_test_b);
free(cg_test_a);
return ret;
}
static int setup_named_v1_root(char *root, size_t len, const char *name)
{
char options[PATH_MAX];
int r;
r = snprintf(root, len, "/mnt/cg_selftest");
if (r < 0)
return r;
r = snprintf(options, sizeof(options), "none,name=%s", name);
if (r < 0)
return r;
r = mkdir(root, 0755);
if (r < 0 && errno != EEXIST)
return r;
r = mount("none", root, "cgroup", 0, options);
if (r < 0)
return r;
return 0;
}
static void cleanup_named_v1_root(char *root)
{
if (!cg_test_v1_named)
return;
umount(root);
rmdir(root);
}
#define T(x) { x, #x }
struct corecg_test {
int (*fn)(const char *root);
const char *name;
} tests[] = {
T(test_cgcore_internal_process_constraint),
T(test_cgcore_top_down_constraint_enable),
T(test_cgcore_top_down_constraint_disable),
T(test_cgcore_no_internal_process_constraint_on_threads),
T(test_cgcore_parent_becomes_threaded),
T(test_cgcore_invalid_domain),
T(test_cgcore_populated),
T(test_cgcore_proc_migration),
T(test_cgcore_thread_migration),
T(test_cgcore_destroy),
T(test_cgcore_lesser_euid_open),
T(test_cgcore_lesser_ns_open),
};
#undef T
int main(int argc, char *argv[])
{
char root[PATH_MAX];
int i;
ksft_print_header();
ksft_set_plan(ARRAY_SIZE(tests));
if (cg_find_unified_root(root, sizeof(root), &nsdelegate)) {
if (setup_named_v1_root(root, sizeof(root), CG_NAMED_NAME))
ksft_exit_skip("cgroup v2 isn't mounted and could not setup named v1 hierarchy\n");
cg_test_v1_named = true;
goto post_v2_setup;
}
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");
post_v2_setup:
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:
ksft_test_result_fail("%s\n", tests[i].name);
break;
}
}
cleanup_named_v1_root(root);
ksft_finished();
}
@@ -0,0 +1,825 @@
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#include <linux/limits.h>
#include <sys/param.h>
#include <sys/sysinfo.h>
#include <sys/wait.h>
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#include "kselftest.h"
#include "cgroup_util.h"
enum hog_clock_type {
// Count elapsed time using the CLOCK_PROCESS_CPUTIME_ID clock.
CPU_HOG_CLOCK_PROCESS,
// Count elapsed time using system wallclock time.
CPU_HOG_CLOCK_WALL,
};
struct cpu_hogger {
char *cgroup;
pid_t pid;
long usage;
};
struct cpu_hog_func_param {
int nprocs;
struct timespec ts;
enum hog_clock_type clock_type;
};
/*
* This test creates two nested cgroups with and without enabling
* the cpu controller.
*/
static int test_cpucg_subtree_control(const char *root)
{
char *parent = NULL, *child = NULL, *parent2 = NULL, *child2 = NULL;
int ret = KSFT_FAIL;
// Create two nested cgroups with the cpu controller enabled.
parent = cg_name(root, "cpucg_test_0");
if (!parent)
goto cleanup;
if (cg_create(parent))
goto cleanup;
if (cg_write(parent, "cgroup.subtree_control", "+cpu"))
goto cleanup;
child = cg_name(parent, "cpucg_test_child");
if (!child)
goto cleanup;
if (cg_create(child))
goto cleanup;
if (cg_read_strstr(child, "cgroup.controllers", "cpu"))
goto cleanup;
// Create two nested cgroups without enabling the cpu controller.
parent2 = cg_name(root, "cpucg_test_1");
if (!parent2)
goto cleanup;
if (cg_create(parent2))
goto cleanup;
child2 = cg_name(parent2, "cpucg_test_child");
if (!child2)
goto cleanup;
if (cg_create(child2))
goto cleanup;
if (!cg_read_strstr(child2, "cgroup.controllers", "cpu"))
goto cleanup;
ret = KSFT_PASS;
cleanup:
cg_destroy(child);
free(child);
cg_destroy(child2);
free(child2);
cg_destroy(parent);
free(parent);
cg_destroy(parent2);
free(parent2);
return ret;
}
static void *hog_cpu_thread_func(void *arg)
{
while (1)
;
return NULL;
}
static struct timespec
timespec_sub(const struct timespec *lhs, const struct timespec *rhs)
{
struct timespec zero = {
.tv_sec = 0,
.tv_nsec = 0,
};
struct timespec ret;
if (lhs->tv_sec < rhs->tv_sec)
return zero;
ret.tv_sec = lhs->tv_sec - rhs->tv_sec;
if (lhs->tv_nsec < rhs->tv_nsec) {
if (ret.tv_sec == 0)
return zero;
ret.tv_sec--;
ret.tv_nsec = NSEC_PER_SEC - rhs->tv_nsec + lhs->tv_nsec;
} else
ret.tv_nsec = lhs->tv_nsec - rhs->tv_nsec;
return ret;
}
static int hog_cpus_timed(const char *cgroup, void *arg)
{
const struct cpu_hog_func_param *param =
(struct cpu_hog_func_param *)arg;
struct timespec ts_run = param->ts;
struct timespec ts_remaining = ts_run;
struct timespec ts_start;
int i, ret;
ret = clock_gettime(CLOCK_MONOTONIC, &ts_start);
if (ret != 0)
return ret;
for (i = 0; i < param->nprocs; i++) {
pthread_t tid;
ret = pthread_create(&tid, NULL, &hog_cpu_thread_func, NULL);
if (ret != 0)
return ret;
}
while (ts_remaining.tv_sec > 0 || ts_remaining.tv_nsec > 0) {
struct timespec ts_total;
ret = nanosleep(&ts_remaining, NULL);
if (ret && errno != EINTR)
return ret;
if (param->clock_type == CPU_HOG_CLOCK_PROCESS) {
ret = clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts_total);
if (ret != 0)
return ret;
} else {
struct timespec ts_current;
ret = clock_gettime(CLOCK_MONOTONIC, &ts_current);
if (ret != 0)
return ret;
ts_total = timespec_sub(&ts_current, &ts_start);
}
ts_remaining = timespec_sub(&ts_run, &ts_total);
}
return 0;
}
/*
* Creates a cpu cgroup, burns a CPU for a few quanta, and verifies that
* cpu.stat shows the expected output.
*/
static int test_cpucg_stats(const char *root)
{
int ret = KSFT_FAIL;
long usage_usec, user_usec, system_usec;
long usage_seconds = 2;
long expected_usage_usec = usage_seconds * USEC_PER_SEC;
char *cpucg;
cpucg = cg_name(root, "cpucg_test");
if (!cpucg)
goto cleanup;
if (cg_create(cpucg))
goto cleanup;
usage_usec = cg_read_key_long(cpucg, "cpu.stat", "usage_usec");
user_usec = cg_read_key_long(cpucg, "cpu.stat", "user_usec");
system_usec = cg_read_key_long(cpucg, "cpu.stat", "system_usec");
if (usage_usec != 0 || user_usec != 0 || system_usec != 0)
goto cleanup;
struct cpu_hog_func_param param = {
.nprocs = 1,
.ts = {
.tv_sec = usage_seconds,
.tv_nsec = 0,
},
.clock_type = CPU_HOG_CLOCK_PROCESS,
};
if (cg_run(cpucg, hog_cpus_timed, (void *)&param))
goto cleanup;
usage_usec = cg_read_key_long(cpucg, "cpu.stat", "usage_usec");
user_usec = cg_read_key_long(cpucg, "cpu.stat", "user_usec");
if (user_usec <= 0)
goto cleanup;
if (!values_close_report(usage_usec, expected_usage_usec, 1))
goto cleanup;
ret = KSFT_PASS;
cleanup:
cg_destroy(cpucg);
free(cpucg);
return ret;
}
/*
* Creates a nice process that consumes CPU and checks that the elapsed
* usertime in the cgroup is close to the expected time.
*/
static int test_cpucg_nice(const char *root)
{
int ret = KSFT_FAIL;
int status;
long user_usec, nice_usec;
long usage_seconds = 2;
long expected_nice_usec = usage_seconds * USEC_PER_SEC;
char *cpucg;
pid_t pid;
cpucg = cg_name(root, "cpucg_test");
if (!cpucg)
goto cleanup;
if (cg_create(cpucg))
goto cleanup;
user_usec = cg_read_key_long(cpucg, "cpu.stat", "user_usec");
nice_usec = cg_read_key_long(cpucg, "cpu.stat", "nice_usec");
if (nice_usec == -1)
ret = KSFT_SKIP;
if (user_usec != 0 || nice_usec != 0)
goto cleanup;
/*
* We fork here to create a new process that can be niced without
* polluting the nice value of other selftests
*/
pid = fork();
if (pid < 0) {
goto cleanup;
} else if (pid == 0) {
struct cpu_hog_func_param param = {
.nprocs = 1,
.ts = {
.tv_sec = usage_seconds,
.tv_nsec = 0,
},
.clock_type = CPU_HOG_CLOCK_PROCESS,
};
char buf[64];
snprintf(buf, sizeof(buf), "%d", getpid());
if (cg_write(cpucg, "cgroup.procs", buf))
exit(EXIT_FAILURE);
/* Try to keep niced CPU usage as constrained to hog_cpu as possible */
nice(1);
hog_cpus_timed(cpucg, &param);
exit(0);
} else {
waitpid(pid, &status, 0);
if (!WIFEXITED(status))
goto cleanup;
user_usec = cg_read_key_long(cpucg, "cpu.stat", "user_usec");
nice_usec = cg_read_key_long(cpucg, "cpu.stat", "nice_usec");
if (!values_close_report(nice_usec, expected_nice_usec, 1))
goto cleanup;
ret = KSFT_PASS;
}
cleanup:
cg_destroy(cpucg);
free(cpucg);
return ret;
}
static int
run_cpucg_weight_test(
const char *root,
pid_t (*spawn_child)(const struct cpu_hogger *child),
int (*validate)(const struct cpu_hogger *children, int num_children))
{
int ret = KSFT_FAIL, i;
char *parent = NULL;
struct cpu_hogger children[3] = {};
parent = cg_name(root, "cpucg_test_0");
if (!parent)
goto cleanup;
if (cg_create(parent))
goto cleanup;
if (cg_write(parent, "cgroup.subtree_control", "+cpu"))
goto cleanup;
for (i = 0; i < ARRAY_SIZE(children); i++) {
children[i].cgroup = cg_name_indexed(parent, "cpucg_child", i);
if (!children[i].cgroup)
goto cleanup;
if (cg_create(children[i].cgroup))
goto cleanup;
if (cg_write_numeric(children[i].cgroup, "cpu.weight",
50 * (i + 1)))
goto cleanup;
}
for (i = 0; i < ARRAY_SIZE(children); i++) {
pid_t pid = spawn_child(&children[i]);
if (pid <= 0)
goto cleanup;
children[i].pid = pid;
}
for (i = 0; i < ARRAY_SIZE(children); i++) {
int retcode;
waitpid(children[i].pid, &retcode, 0);
if (!WIFEXITED(retcode))
goto cleanup;
if (WEXITSTATUS(retcode))
goto cleanup;
}
for (i = 0; i < ARRAY_SIZE(children); i++)
children[i].usage = cg_read_key_long(children[i].cgroup,
"cpu.stat", "usage_usec");
if (validate(children, ARRAY_SIZE(children)))
goto cleanup;
ret = KSFT_PASS;
cleanup:
for (i = 0; i < ARRAY_SIZE(children); i++) {
cg_destroy(children[i].cgroup);
free(children[i].cgroup);
}
cg_destroy(parent);
free(parent);
return ret;
}
static pid_t weight_hog_ncpus(const struct cpu_hogger *child, int ncpus)
{
long usage_seconds = 10;
struct cpu_hog_func_param param = {
.nprocs = ncpus,
.ts = {
.tv_sec = usage_seconds,
.tv_nsec = 0,
},
.clock_type = CPU_HOG_CLOCK_WALL,
};
return cg_run_nowait(child->cgroup, hog_cpus_timed, (void *)&param);
}
static pid_t weight_hog_all_cpus(const struct cpu_hogger *child)
{
return weight_hog_ncpus(child, get_nprocs());
}
static int
overprovision_validate(const struct cpu_hogger *children, int num_children)
{
int ret = KSFT_FAIL, i;
for (i = 0; i < num_children - 1; i++) {
long delta;
if (children[i + 1].usage <= children[i].usage)
goto cleanup;
delta = children[i + 1].usage - children[i].usage;
if (!values_close_report(delta, children[0].usage, 35))
goto cleanup;
}
ret = KSFT_PASS;
cleanup:
return ret;
}
/*
* First, this test creates the following hierarchy:
* A
* A/B cpu.weight = 50
* A/C cpu.weight = 100
* A/D cpu.weight = 150
*
* A separate process is then created for each child cgroup which spawns as
* many threads as there are cores, and hogs each CPU as much as possible
* for some time interval.
*
* Once all of the children have exited, we verify that each child cgroup
* was given proportional runtime as informed by their cpu.weight.
*/
static int test_cpucg_weight_overprovisioned(const char *root)
{
return run_cpucg_weight_test(root, weight_hog_all_cpus,
overprovision_validate);
}
static pid_t weight_hog_one_cpu(const struct cpu_hogger *child)
{
return weight_hog_ncpus(child, 1);
}
static int
underprovision_validate(const struct cpu_hogger *children, int num_children)
{
int ret = KSFT_FAIL, i;
for (i = 0; i < num_children - 1; i++) {
if (!values_close_report(children[i + 1].usage, children[0].usage, 15))
goto cleanup;
}
ret = KSFT_PASS;
cleanup:
return ret;
}
/*
* First, this test creates the following hierarchy:
* A
* A/B cpu.weight = 50
* A/C cpu.weight = 100
* A/D cpu.weight = 150
*
* A separate process is then created for each child cgroup which spawns a
* single thread that hogs a CPU. The testcase is only run on systems that
* have at least one core per-thread in the child processes.
*
* Once all of the children have exited, we verify that each child cgroup
* had roughly the same runtime despite having different cpu.weight.
*/
static int test_cpucg_weight_underprovisioned(const char *root)
{
// Only run the test if there are enough cores to avoid overprovisioning
// the system.
if (get_nprocs() < 4)
return KSFT_SKIP;
return run_cpucg_weight_test(root, weight_hog_one_cpu,
underprovision_validate);
}
static int
run_cpucg_nested_weight_test(const char *root, bool overprovisioned)
{
int ret = KSFT_FAIL, i;
char *parent = NULL, *child = NULL;
struct cpu_hogger leaf[3] = {};
long nested_leaf_usage, child_usage;
int nprocs = get_nprocs();
if (!overprovisioned) {
if (nprocs < 4)
/*
* Only run the test if there are enough cores to avoid overprovisioning
* the system.
*/
return KSFT_SKIP;
nprocs /= 4;
}
parent = cg_name(root, "cpucg_test");
child = cg_name(parent, "cpucg_child");
if (!parent || !child)
goto cleanup;
if (cg_create(parent))
goto cleanup;
if (cg_write(parent, "cgroup.subtree_control", "+cpu"))
goto cleanup;
if (cg_create(child))
goto cleanup;
if (cg_write(child, "cgroup.subtree_control", "+cpu"))
goto cleanup;
if (cg_write(child, "cpu.weight", "1000"))
goto cleanup;
for (i = 0; i < ARRAY_SIZE(leaf); i++) {
const char *ancestor;
long weight;
if (i == 0) {
ancestor = parent;
weight = 1000;
} else {
ancestor = child;
weight = 5000;
}
leaf[i].cgroup = cg_name_indexed(ancestor, "cpucg_leaf", i);
if (!leaf[i].cgroup)
goto cleanup;
if (cg_create(leaf[i].cgroup))
goto cleanup;
if (cg_write_numeric(leaf[i].cgroup, "cpu.weight", weight))
goto cleanup;
}
for (i = 0; i < ARRAY_SIZE(leaf); i++) {
pid_t pid;
struct cpu_hog_func_param param = {
.nprocs = nprocs,
.ts = {
.tv_sec = 10,
.tv_nsec = 0,
},
.clock_type = CPU_HOG_CLOCK_WALL,
};
pid = cg_run_nowait(leaf[i].cgroup, hog_cpus_timed,
(void *)&param);
if (pid <= 0)
goto cleanup;
leaf[i].pid = pid;
}
for (i = 0; i < ARRAY_SIZE(leaf); i++) {
int retcode;
waitpid(leaf[i].pid, &retcode, 0);
if (!WIFEXITED(retcode))
goto cleanup;
if (WEXITSTATUS(retcode))
goto cleanup;
}
for (i = 0; i < ARRAY_SIZE(leaf); i++) {
leaf[i].usage = cg_read_key_long(leaf[i].cgroup,
"cpu.stat", "usage_usec");
if (leaf[i].usage <= 0)
goto cleanup;
}
nested_leaf_usage = leaf[1].usage + leaf[2].usage;
if (overprovisioned) {
if (!values_close_report(leaf[0].usage, nested_leaf_usage, 15))
goto cleanup;
} else if (!values_close_report(leaf[0].usage * 2, nested_leaf_usage, 15))
goto cleanup;
child_usage = cg_read_key_long(child, "cpu.stat", "usage_usec");
if (child_usage <= 0)
goto cleanup;
if (!values_close_report(child_usage, nested_leaf_usage, 1))
goto cleanup;
ret = KSFT_PASS;
cleanup:
for (i = 0; i < ARRAY_SIZE(leaf); i++) {
cg_destroy(leaf[i].cgroup);
free(leaf[i].cgroup);
}
cg_destroy(child);
free(child);
cg_destroy(parent);
free(parent);
return ret;
}
/*
* First, this test creates the following hierarchy:
* A
* A/B cpu.weight = 1000
* A/C cpu.weight = 1000
* A/C/D cpu.weight = 5000
* A/C/E cpu.weight = 5000
*
* A separate process is then created for each leaf, which spawn nproc threads
* that burn a CPU for a few seconds.
*
* Once all of those processes have exited, we verify that each of the leaf
* cgroups have roughly the same usage from cpu.stat.
*/
static int
test_cpucg_nested_weight_overprovisioned(const char *root)
{
return run_cpucg_nested_weight_test(root, true);
}
/*
* First, this test creates the following hierarchy:
* A
* A/B cpu.weight = 1000
* A/C cpu.weight = 1000
* A/C/D cpu.weight = 5000
* A/C/E cpu.weight = 5000
*
* A separate process is then created for each leaf, which nproc / 4 threads
* that burns a CPU for a few seconds.
*
* Once all of those processes have exited, we verify that each of the leaf
* cgroups have roughly the same usage from cpu.stat.
*/
static int
test_cpucg_nested_weight_underprovisioned(const char *root)
{
return run_cpucg_nested_weight_test(root, false);
}
/*
* This test creates a cgroup with some maximum value within a period, and
* verifies that a process in the cgroup is not overscheduled.
*/
static int test_cpucg_max(const char *root)
{
int ret = KSFT_FAIL;
long quota_usec = 1000;
long default_period_usec = 100000; /* cpu.max's default period */
long duration_seconds = 1;
long duration_usec = duration_seconds * USEC_PER_SEC;
long usage_usec, n_periods, remainder_usec, expected_usage_usec;
char *cpucg;
char quota_buf[32];
snprintf(quota_buf, sizeof(quota_buf), "%ld", quota_usec);
cpucg = cg_name(root, "cpucg_test");
if (!cpucg)
goto cleanup;
if (cg_create(cpucg))
goto cleanup;
if (cg_write(cpucg, "cpu.max", quota_buf))
goto cleanup;
struct cpu_hog_func_param param = {
.nprocs = 1,
.ts = {
.tv_sec = duration_seconds,
.tv_nsec = 0,
},
.clock_type = CPU_HOG_CLOCK_WALL,
};
if (cg_run(cpucg, hog_cpus_timed, (void *)&param))
goto cleanup;
usage_usec = cg_read_key_long(cpucg, "cpu.stat", "usage_usec");
if (usage_usec <= 0)
goto cleanup;
/*
* The following calculation applies only since
* the cpu hog is set to run as per wall-clock time
*/
n_periods = duration_usec / default_period_usec;
remainder_usec = duration_usec - n_periods * default_period_usec;
expected_usage_usec
= n_periods * quota_usec + MIN(remainder_usec, quota_usec);
if (!values_close_report(usage_usec, expected_usage_usec, 10))
goto cleanup;
ret = KSFT_PASS;
cleanup:
cg_destroy(cpucg);
free(cpucg);
return ret;
}
/*
* This test verifies that a process inside of a nested cgroup whose parent
* group has a cpu.max value set, is properly throttled.
*/
static int test_cpucg_max_nested(const char *root)
{
int ret = KSFT_FAIL;
long quota_usec = 1000;
long default_period_usec = 100000; /* cpu.max's default period */
long duration_seconds = 1;
long duration_usec = duration_seconds * USEC_PER_SEC;
long usage_usec, n_periods, remainder_usec, expected_usage_usec;
char *parent, *child;
char quota_buf[32];
snprintf(quota_buf, sizeof(quota_buf), "%ld", quota_usec);
parent = cg_name(root, "cpucg_parent");
child = cg_name(parent, "cpucg_child");
if (!parent || !child)
goto cleanup;
if (cg_create(parent))
goto cleanup;
if (cg_write(parent, "cgroup.subtree_control", "+cpu"))
goto cleanup;
if (cg_create(child))
goto cleanup;
if (cg_write(parent, "cpu.max", quota_buf))
goto cleanup;
struct cpu_hog_func_param param = {
.nprocs = 1,
.ts = {
.tv_sec = duration_seconds,
.tv_nsec = 0,
},
.clock_type = CPU_HOG_CLOCK_WALL,
};
if (cg_run(child, hog_cpus_timed, (void *)&param))
goto cleanup;
usage_usec = cg_read_key_long(child, "cpu.stat", "usage_usec");
if (usage_usec <= 0)
goto cleanup;
/*
* The following calculation applies only since
* the cpu hog is set to run as per wall-clock time
*/
n_periods = duration_usec / default_period_usec;
remainder_usec = duration_usec - n_periods * default_period_usec;
expected_usage_usec
= n_periods * quota_usec + MIN(remainder_usec, quota_usec);
if (!values_close_report(usage_usec, expected_usage_usec, 10))
goto cleanup;
ret = KSFT_PASS;
cleanup:
cg_destroy(child);
free(child);
cg_destroy(parent);
free(parent);
return ret;
}
#define T(x) { x, #x }
struct cpucg_test {
int (*fn)(const char *root);
const char *name;
} tests[] = {
T(test_cpucg_subtree_control),
T(test_cpucg_stats),
T(test_cpucg_nice),
T(test_cpucg_weight_overprovisioned),
T(test_cpucg_weight_underprovisioned),
T(test_cpucg_nested_weight_overprovisioned),
T(test_cpucg_nested_weight_underprovisioned),
T(test_cpucg_max),
T(test_cpucg_max_nested),
};
#undef T
int main(int argc, char *argv[])
{
char root[PATH_MAX];
int i;
ksft_print_header();
ksft_set_plan(ARRAY_SIZE(tests));
if (cg_find_unified_root(root, sizeof(root), NULL))
ksft_exit_skip("cgroup v2 isn't mounted\n");
if (cg_read_strstr(root, "cgroup.subtree_control", "cpu"))
if (cg_write(root, "cgroup.subtree_control", "+cpu"))
ksft_exit_skip("Failed to set cpu 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:
ksft_test_result_fail("%s\n", tests[i].name);
break;
}
}
ksft_finished();
}
@@ -0,0 +1,276 @@
// SPDX-License-Identifier: GPL-2.0
#include <linux/limits.h>
#include <signal.h>
#include "kselftest.h"
#include "cgroup_util.h"
static int idle_process_fn(const char *cgroup, void *arg)
{
(void)pause();
return 0;
}
static int do_migration_fn(const char *cgroup, void *arg)
{
int object_pid = (int)(size_t)arg;
if (setuid(TEST_UID))
return EXIT_FAILURE;
// XXX checking /proc/$pid/cgroup would be quicker than wait
if (cg_enter(cgroup, object_pid) ||
cg_wait_for_proc_count(cgroup, 1))
return EXIT_FAILURE;
return EXIT_SUCCESS;
}
static int do_controller_fn(const char *cgroup, void *arg)
{
const char *child = cgroup;
const char *parent = arg;
if (setuid(TEST_UID))
return EXIT_FAILURE;
if (!cg_read_strstr(child, "cgroup.controllers", "cpuset"))
return EXIT_FAILURE;
if (cg_write(parent, "cgroup.subtree_control", "+cpuset"))
return EXIT_FAILURE;
if (cg_read_strstr(child, "cgroup.controllers", "cpuset"))
return EXIT_FAILURE;
if (cg_write(parent, "cgroup.subtree_control", "-cpuset"))
return EXIT_FAILURE;
if (!cg_read_strstr(child, "cgroup.controllers", "cpuset"))
return EXIT_FAILURE;
return EXIT_SUCCESS;
}
/*
* Migrate a process between two sibling cgroups.
* The success should only depend on the parent cgroup permissions and not the
* migrated process itself (cpuset controller is in place because it uses
* security_task_setscheduler() in cgroup v1).
*
* Deliberately don't set cpuset.cpus in children to avoid definining migration
* permissions between two different cpusets.
*/
static int test_cpuset_perms_object(const char *root, bool allow)
{
char *parent = NULL, *child_src = NULL, *child_dst = NULL;
char *parent_procs = NULL, *child_src_procs = NULL, *child_dst_procs = NULL;
const uid_t test_euid = TEST_UID;
int object_pid = 0;
int ret = KSFT_FAIL;
parent = cg_name(root, "cpuset_test_0");
if (!parent)
goto cleanup;
parent_procs = cg_name(parent, "cgroup.procs");
if (!parent_procs)
goto cleanup;
if (cg_create(parent))
goto cleanup;
child_src = cg_name(parent, "cpuset_test_1");
if (!child_src)
goto cleanup;
child_src_procs = cg_name(child_src, "cgroup.procs");
if (!child_src_procs)
goto cleanup;
if (cg_create(child_src))
goto cleanup;
child_dst = cg_name(parent, "cpuset_test_2");
if (!child_dst)
goto cleanup;
child_dst_procs = cg_name(child_dst, "cgroup.procs");
if (!child_dst_procs)
goto cleanup;
if (cg_create(child_dst))
goto cleanup;
if (cg_write(parent, "cgroup.subtree_control", "+cpuset"))
goto cleanup;
if (cg_read_strstr(child_src, "cgroup.controllers", "cpuset") ||
cg_read_strstr(child_dst, "cgroup.controllers", "cpuset"))
goto cleanup;
/* Enable permissions along src->dst tree path */
if (chown(child_src_procs, test_euid, -1) ||
chown(child_dst_procs, test_euid, -1))
goto cleanup;
if (allow && chown(parent_procs, test_euid, -1))
goto cleanup;
/* Fork a privileged child as a test object */
object_pid = cg_run_nowait(child_src, idle_process_fn, NULL);
if (object_pid < 0)
goto cleanup;
/* Carry out migration in a child process that can drop all privileges
* (including capabilities), the main process must remain privileged for
* cleanup.
* Child process's cgroup is irrelevant but we place it into child_dst
* as hacky way to pass information about migration target to the child.
*/
if (allow ^ (cg_run(child_dst, do_migration_fn, (void *)(size_t)object_pid) == EXIT_SUCCESS))
goto cleanup;
ret = KSFT_PASS;
cleanup:
if (object_pid > 0) {
(void)kill(object_pid, SIGTERM);
(void)clone_reap(object_pid, WEXITED);
}
cg_destroy(child_dst);
free(child_dst_procs);
free(child_dst);
cg_destroy(child_src);
free(child_src_procs);
free(child_src);
cg_destroy(parent);
free(parent_procs);
free(parent);
return ret;
}
static int test_cpuset_perms_object_allow(const char *root)
{
return test_cpuset_perms_object(root, true);
}
static int test_cpuset_perms_object_deny(const char *root)
{
return test_cpuset_perms_object(root, false);
}
/*
* Migrate a process between parent and child implicitely
* Implicit migration happens when a controller is enabled/disabled.
*
*/
static int test_cpuset_perms_subtree(const char *root)
{
char *parent = NULL, *child = NULL;
char *parent_procs = NULL, *parent_subctl = NULL, *child_procs = NULL;
const uid_t test_euid = TEST_UID;
int object_pid = 0;
int ret = KSFT_FAIL;
parent = cg_name(root, "cpuset_test_0");
if (!parent)
goto cleanup;
parent_procs = cg_name(parent, "cgroup.procs");
if (!parent_procs)
goto cleanup;
parent_subctl = cg_name(parent, "cgroup.subtree_control");
if (!parent_subctl)
goto cleanup;
if (cg_create(parent))
goto cleanup;
child = cg_name(parent, "cpuset_test_1");
if (!child)
goto cleanup;
child_procs = cg_name(child, "cgroup.procs");
if (!child_procs)
goto cleanup;
if (cg_create(child))
goto cleanup;
/* Enable permissions as in a delegated subtree */
if (chown(parent_procs, test_euid, -1) ||
chown(parent_subctl, test_euid, -1) ||
chown(child_procs, test_euid, -1))
goto cleanup;
/* Put a privileged child in the subtree and modify controller state
* from an unprivileged process, the main process remains privileged
* for cleanup.
* The unprivileged child runs in subtree too to avoid parent and
* internal-node constraing violation.
*/
object_pid = cg_run_nowait(child, idle_process_fn, NULL);
if (object_pid < 0)
goto cleanup;
if (cg_run(child, do_controller_fn, parent) != EXIT_SUCCESS)
goto cleanup;
ret = KSFT_PASS;
cleanup:
if (object_pid > 0) {
(void)kill(object_pid, SIGTERM);
(void)clone_reap(object_pid, WEXITED);
}
cg_destroy(child);
free(child_procs);
free(child);
cg_destroy(parent);
free(parent_subctl);
free(parent_procs);
free(parent);
return ret;
}
#define T(x) { x, #x }
struct cpuset_test {
int (*fn)(const char *root);
const char *name;
} tests[] = {
T(test_cpuset_perms_object_allow),
T(test_cpuset_perms_object_deny),
T(test_cpuset_perms_subtree),
};
#undef T
int main(int argc, char *argv[])
{
char root[PATH_MAX];
int i;
ksft_print_header();
ksft_set_plan(ARRAY_SIZE(tests));
if (cg_find_unified_root(root, sizeof(root), NULL))
ksft_exit_skip("cgroup v2 isn't mounted\n");
if (cg_read_strstr(root, "cgroup.subtree_control", "cpuset"))
if (cg_write(root, "cgroup.subtree_control", "+cpuset"))
ksft_exit_skip("Failed to set cpuset 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:
ksft_test_result_fail("%s\n", tests[i].name);
break;
}
}
ksft_finished();
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,77 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Basc test for cpuset v1 interfaces write/read
#
skip_test() {
echo "$1"
echo "Test SKIPPED"
exit 4 # ksft_skip
}
write_test() {
dir=$1
interface=$2
value=$3
original=$(cat $dir/$interface)
echo "testing $interface $value"
echo $value > $dir/$interface
new=$(cat $dir/$interface)
[[ "$value" != "$new" ]] && {
echo "$interface write $value failed: new:$new"
exit 1
}
}
[[ $(id -u) -eq 0 ]] || skip_test "Test must be run as root!"
# Find cpuset v1 mount point
CPUSET=$(mount -t cgroup | grep cpuset | head -1 | awk '{print $3}')
[[ -n "$CPUSET" ]] || skip_test "cpuset v1 mount point not found!"
#
# Create a test cpuset, read write test
#
TDIR=test$$
[[ -d $CPUSET/$TDIR ]] || mkdir $CPUSET/$TDIR
ITF_MATRIX=(
#interface value expect root_only
'cpuset.cpus 0-1 0-1 0'
'cpuset.mem_exclusive 1 1 0'
'cpuset.mem_exclusive 0 0 0'
'cpuset.mem_hardwall 1 1 0'
'cpuset.mem_hardwall 0 0 0'
'cpuset.memory_migrate 1 1 0'
'cpuset.memory_migrate 0 0 0'
'cpuset.memory_spread_page 1 1 0'
'cpuset.memory_spread_page 0 0 0'
'cpuset.memory_spread_slab 1 1 0'
'cpuset.memory_spread_slab 0 0 0'
'cpuset.mems 0 0 0'
'cpuset.sched_load_balance 1 1 0'
'cpuset.sched_load_balance 0 0 0'
'cpuset.sched_relax_domain_level 2 2 0'
'cpuset.memory_pressure_enabled 1 1 1'
'cpuset.memory_pressure_enabled 0 0 1'
)
run_test()
{
cnt="${ITF_MATRIX[@]}"
for i in "${ITF_MATRIX[@]}" ; do
args=($i)
root_only=${args[3]}
[[ $root_only -eq 1 ]] && {
write_test "$CPUSET" "${args[0]}" "${args[1]}" "${args[2]}"
continue
}
write_test "$CPUSET/$TDIR" "${args[0]}" "${args[1]}" "${args[2]}"
done
}
run_test
rmdir $CPUSET/$TDIR
echo "Test PASSED"
exit 0
@@ -0,0 +1,46 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Test the special cpuset v1 hotplug case where a cpuset become empty of
# CPUs will force migration of tasks out to an ancestor.
#
skip_test() {
echo "$1"
echo "Test SKIPPED"
exit 4 # ksft_skip
}
[[ $(id -u) -eq 0 ]] || skip_test "Test must be run as root!"
# Find cpuset v1 mount point
CPUSET=$(mount -t cgroup | grep cpuset | head -1 | awk -e '{print $3}')
[[ -n "$CPUSET" ]] || skip_test "cpuset v1 mount point not found!"
#
# Create a test cpuset, put a CPU and a task there and offline that CPU
#
TDIR=test$$
[[ -d $CPUSET/$TDIR ]] || mkdir $CPUSET/$TDIR
echo 1 > $CPUSET/$TDIR/cpuset.cpus
echo 0 > $CPUSET/$TDIR/cpuset.mems
sleep 10&
TASK=$!
echo $TASK > $CPUSET/$TDIR/tasks
NEWCS=$(cat /proc/$TASK/cpuset)
[[ $NEWCS != "/$TDIR" ]] && {
echo "Unexpected cpuset $NEWCS, test FAILED!"
exit 1
}
echo 0 > /sys/devices/system/cpu/cpu1/online
sleep 0.5
echo 1 > /sys/devices/system/cpu/cpu1/online
NEWCS=$(cat /proc/$TASK/cpuset)
rmdir $CPUSET/$TDIR
[[ $NEWCS != "/" ]] && {
echo "cpuset $NEWCS, test FAILED!"
exit 1
}
echo "Test PASSED"
exit 0
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,242 @@
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#include <linux/limits.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include "kselftest.h"
#include "cgroup_util.h"
#define ADDR ((void *)(0x0UL))
#define FLAGS (MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB)
/* mapping 8 MBs == 4 hugepages */
#define LENGTH (8UL*1024*1024)
#define PROTECTION (PROT_READ | PROT_WRITE)
/* borrowed from mm/hmm-tests.c */
static long get_hugepage_size(void)
{
int fd;
char buf[2048];
int len;
char *p, *q, *path = "/proc/meminfo", *tag = "Hugepagesize:";
long val;
fd = open(path, O_RDONLY);
if (fd < 0) {
/* Error opening the file */
return -1;
}
len = read(fd, buf, sizeof(buf));
close(fd);
if (len < 0) {
/* Error in reading the file */
return -1;
}
if (len == sizeof(buf)) {
/* Error file is too large */
return -1;
}
buf[len] = '\0';
/* Search for a tag if provided */
if (tag) {
p = strstr(buf, tag);
if (!p)
return -1; /* looks like the line we want isn't there */
p += strlen(tag);
} else
p = buf;
val = strtol(p, &q, 0);
if (*q != ' ') {
/* Error parsing the file */
return -1;
}
return val;
}
static int set_file(const char *path, long value)
{
FILE *file;
int ret;
file = fopen(path, "w");
if (!file)
return -1;
ret = fprintf(file, "%ld\n", value);
fclose(file);
return ret;
}
static int set_nr_hugepages(long value)
{
return set_file("/proc/sys/vm/nr_hugepages", value);
}
static unsigned int check_first(char *addr)
{
return *(unsigned int *)addr;
}
static void write_data(char *addr)
{
unsigned long i;
for (i = 0; i < LENGTH; i++)
*(addr + i) = (char)i;
}
static int hugetlb_test_program(const char *cgroup, void *arg)
{
char *test_group = (char *)arg;
void *addr;
long old_current, expected_current, current;
int ret = EXIT_FAILURE;
old_current = cg_read_long(test_group, "memory.current");
set_nr_hugepages(20);
current = cg_read_long(test_group, "memory.current");
if (current - old_current >= MB(2)) {
ksft_print_msg(
"setting nr_hugepages should not increase hugepage usage.\n");
ksft_print_msg("before: %ld, after: %ld\n", old_current, current);
return EXIT_FAILURE;
}
addr = mmap(ADDR, LENGTH, PROTECTION, FLAGS, 0, 0);
if (addr == MAP_FAILED) {
ksft_print_msg("fail to mmap.\n");
return EXIT_FAILURE;
}
current = cg_read_long(test_group, "memory.current");
if (current - old_current >= MB(2)) {
ksft_print_msg("mmap should not increase hugepage usage.\n");
ksft_print_msg("before: %ld, after: %ld\n", old_current, current);
goto out_failed_munmap;
}
old_current = current;
/* read the first page */
check_first(addr);
expected_current = old_current + MB(2);
current = cg_read_long(test_group, "memory.current");
if (!values_close(expected_current, current, 5)) {
ksft_print_msg("memory usage should increase by around 2MB.\n");
ksft_print_msg(
"expected memory: %ld, actual memory: %ld\n",
expected_current, current);
goto out_failed_munmap;
}
/* write to the whole range */
write_data(addr);
current = cg_read_long(test_group, "memory.current");
expected_current = old_current + MB(8);
if (!values_close(expected_current, current, 5)) {
ksft_print_msg("memory usage should increase by around 8MB.\n");
ksft_print_msg(
"expected memory: %ld, actual memory: %ld\n",
expected_current, current);
goto out_failed_munmap;
}
/* unmap the whole range */
munmap(addr, LENGTH);
current = cg_read_long(test_group, "memory.current");
expected_current = old_current;
if (!values_close(expected_current, current, 5)) {
ksft_print_msg("memory usage should go back down.\n");
ksft_print_msg(
"expected memory: %ld, actual memory: %ld\n",
expected_current, current);
return ret;
}
ret = EXIT_SUCCESS;
return ret;
out_failed_munmap:
munmap(addr, LENGTH);
return ret;
}
static int test_hugetlb_memcg(char *root)
{
int ret = KSFT_FAIL;
char *test_group;
test_group = cg_name(root, "hugetlb_memcg_test");
if (!test_group || cg_create(test_group)) {
ksft_print_msg("fail to create cgroup.\n");
goto out;
}
if (cg_write(test_group, "memory.max", "100M")) {
ksft_print_msg("fail to set cgroup memory limit.\n");
goto out;
}
/* disable swap */
if (cg_write(test_group, "memory.swap.max", "0")) {
ksft_print_msg("fail to disable swap.\n");
goto out;
}
if (!cg_run(test_group, hugetlb_test_program, (void *)test_group))
ret = KSFT_PASS;
out:
cg_destroy(test_group);
free(test_group);
return ret;
}
int main(int argc, char **argv)
{
char root[PATH_MAX];
int ret = EXIT_SUCCESS, has_memory_hugetlb_acc;
has_memory_hugetlb_acc = proc_mount_contains("memory_hugetlb_accounting");
if (has_memory_hugetlb_acc < 0)
ksft_exit_skip("Failed to query cgroup mount option\n");
else if (!has_memory_hugetlb_acc)
ksft_exit_skip("memory hugetlb accounting is disabled\n");
/* Unit is kB! */
if (get_hugepage_size() != 2048) {
ksft_print_msg("test_hugetlb_memcg requires 2MB hugepages\n");
ksft_test_result_skip("test_hugetlb_memcg\n");
return ret;
}
if (cg_find_unified_root(root, sizeof(root), NULL))
ksft_exit_skip("cgroup v2 isn't mounted\n");
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");
}
switch (test_hugetlb_memcg(root)) {
case KSFT_PASS:
ksft_test_result_pass("test_hugetlb_memcg\n");
break;
case KSFT_SKIP:
ksft_test_result_skip("test_hugetlb_memcg\n");
break;
default:
ret = EXIT_FAILURE;
ksft_test_result_fail("test_hugetlb_memcg\n");
break;
}
return ret;
}
@@ -0,0 +1,299 @@
/* SPDX-License-Identifier: GPL-2.0 */
#include <errno.h>
#include <linux/limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include "kselftest.h"
#include "../pidfd/pidfd.h"
#include "cgroup_util.h"
/*
* Kill the given cgroup and wait for the inotify signal.
* If there are no events in 10 seconds, treat this as an error.
* Then check that the cgroup is in the desired state.
*/
static int cg_kill_wait(const char *cgroup)
{
int fd, ret = -1;
fd = cg_prepare_for_wait(cgroup);
if (fd < 0)
return fd;
ret = cg_write(cgroup, "cgroup.kill", "1");
if (ret)
goto out;
ret = cg_wait_for(fd);
if (ret)
goto out;
out:
close(fd);
return ret;
}
/*
* A simple process running in a sleep loop until being
* re-parented.
*/
static int child_fn(const char *cgroup, void *arg)
{
int ppid = getppid();
while (getppid() == ppid)
usleep(1000);
return getppid() == ppid;
}
static int test_cgkill_simple(const char *root)
{
pid_t pids[100];
int ret = KSFT_FAIL;
char *cgroup = NULL;
int i;
cgroup = cg_name(root, "cg_test_simple");
if (!cgroup)
goto cleanup;
if (cg_create(cgroup))
goto cleanup;
for (i = 0; i < 100; i++)
pids[i] = cg_run_nowait(cgroup, child_fn, NULL);
if (cg_wait_for_proc_count(cgroup, 100))
goto cleanup;
if (cg_read_strcmp(cgroup, "cgroup.events", "populated 1\n"))
goto cleanup;
if (cg_kill_wait(cgroup))
goto cleanup;
ret = KSFT_PASS;
cleanup:
for (i = 0; i < 100; i++)
wait_for_pid(pids[i]);
if (ret == KSFT_PASS &&
cg_read_strcmp_wait(cgroup, "cgroup.events", "populated 0\n"))
ret = KSFT_FAIL;
if (cgroup)
cg_destroy(cgroup);
free(cgroup);
return ret;
}
/*
* The test creates the following hierarchy:
* A
* / / \ \
* B E I K
* /\ |
* C D F
* |
* G
* |
* H
*
* with a process in C, H and 3 processes in K.
* Then it tries to kill the whole tree.
*/
static int test_cgkill_tree(const char *root)
{
pid_t pids[5];
char *cgroup[10] = {0};
int ret = KSFT_FAIL;
int i;
cgroup[0] = cg_name(root, "cg_test_tree_A");
if (!cgroup[0])
goto cleanup;
cgroup[1] = cg_name(cgroup[0], "B");
if (!cgroup[1])
goto cleanup;
cgroup[2] = cg_name(cgroup[1], "C");
if (!cgroup[2])
goto cleanup;
cgroup[3] = cg_name(cgroup[1], "D");
if (!cgroup[3])
goto cleanup;
cgroup[4] = cg_name(cgroup[0], "E");
if (!cgroup[4])
goto cleanup;
cgroup[5] = cg_name(cgroup[4], "F");
if (!cgroup[5])
goto cleanup;
cgroup[6] = cg_name(cgroup[5], "G");
if (!cgroup[6])
goto cleanup;
cgroup[7] = cg_name(cgroup[6], "H");
if (!cgroup[7])
goto cleanup;
cgroup[8] = cg_name(cgroup[0], "I");
if (!cgroup[8])
goto cleanup;
cgroup[9] = cg_name(cgroup[0], "K");
if (!cgroup[9])
goto cleanup;
for (i = 0; i < 10; i++)
if (cg_create(cgroup[i]))
goto cleanup;
pids[0] = cg_run_nowait(cgroup[2], child_fn, NULL);
pids[1] = cg_run_nowait(cgroup[7], child_fn, NULL);
pids[2] = cg_run_nowait(cgroup[9], child_fn, NULL);
pids[3] = cg_run_nowait(cgroup[9], child_fn, NULL);
pids[4] = cg_run_nowait(cgroup[9], child_fn, NULL);
/*
* Wait until all child processes will enter
* corresponding cgroups.
*/
if (cg_wait_for_proc_count(cgroup[2], 1) ||
cg_wait_for_proc_count(cgroup[7], 1) ||
cg_wait_for_proc_count(cgroup[9], 3))
goto cleanup;
/*
* Kill A and check that we get an empty notification.
*/
if (cg_kill_wait(cgroup[0]))
goto cleanup;
ret = KSFT_PASS;
cleanup:
for (i = 0; i < 5; i++)
wait_for_pid(pids[i]);
if (ret == KSFT_PASS &&
cg_read_strcmp_wait(cgroup[0], "cgroup.events",
"populated 0\n"))
ret = KSFT_FAIL;
for (i = 9; i >= 0 && cgroup[i]; i--) {
cg_destroy(cgroup[i]);
free(cgroup[i]);
}
return ret;
}
static int forkbomb_fn(const char *cgroup, void *arg)
{
int ppid;
fork();
fork();
ppid = getppid();
while (getppid() == ppid)
usleep(1000);
return getppid() == ppid;
}
/*
* The test runs a fork bomb in a cgroup and tries to kill it.
*/
static int test_cgkill_forkbomb(const char *root)
{
int ret = KSFT_FAIL;
char *cgroup = NULL;
pid_t pid = -ESRCH;
cgroup = cg_name(root, "cg_forkbomb_test");
if (!cgroup)
goto cleanup;
if (cg_create(cgroup))
goto cleanup;
pid = cg_run_nowait(cgroup, forkbomb_fn, NULL);
if (pid < 0)
goto cleanup;
usleep(100000);
if (cg_kill_wait(cgroup))
goto cleanup;
if (cg_wait_for_proc_count(cgroup, 0))
goto cleanup;
ret = KSFT_PASS;
cleanup:
if (pid > 0)
wait_for_pid(pid);
if (ret == KSFT_PASS &&
cg_read_strcmp_wait(cgroup, "cgroup.events", "populated 0\n"))
ret = KSFT_FAIL;
if (cgroup)
cg_destroy(cgroup);
free(cgroup);
return ret;
}
#define T(x) { x, #x }
struct cgkill_test {
int (*fn)(const char *root);
const char *name;
} tests[] = {
T(test_cgkill_simple),
T(test_cgkill_tree),
T(test_cgkill_forkbomb),
};
#undef T
int main(int argc, char *argv[])
{
char root[PATH_MAX];
int i;
ksft_print_header();
ksft_set_plan(ARRAY_SIZE(tests));
if (cg_find_unified_root(root, sizeof(root), NULL))
ksft_exit_skip("cgroup v2 isn't mounted\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:
ksft_test_result_fail("%s\n", tests[i].name);
break;
}
}
ksft_finished();
}
@@ -0,0 +1,459 @@
// 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 (sysconf(_SC_PAGESIZE) * 64 * get_nprocs())
#define KMEM_DEAD_WAIT_RETRIES 80
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 (labs(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 = -1;
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;
/*
* Allow up to ~8s for reclaim of dying descendants to complete.
* This is a generous upper bound derived from stress testing, not
* from a specific kernel constant, and can be adjusted if reclaim
* behavior changes in the future.
*/
dead = cg_read_key_long_poll(parent, "cgroup.stat",
"nr_dying_descendants ", 0, KMEM_DEAD_WAIT_RETRIES,
DEFAULT_WAIT_INTERVAL_US);
if (dead)
goto cleanup;
ret = KSFT_PASS;
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, slab;
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) {
ret = -1;
goto cleanup_children;
}
if (cg_create(child)) {
free(child);
goto cleanup_children;
}
free(child);
}
current = cg_read_long(parent, "memory.current");
percpu = cg_read_key_long(parent, "memory.stat", "percpu ");
slab = cg_read_key_long(parent, "memory.stat", "slab ");
if (current > 0 && percpu > 0 && slab >= 0 &&
labs(current - (percpu + slab)) < MAX_VMSTAT_ERROR)
ret = KSFT_PASS;
else
printf("memory.current %ld\npercpu %ld\nslab %ld\ndelta %ld\n",
current, percpu, slab, current - (percpu + slab));
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;
ksft_print_header();
ksft_set_plan(ARRAY_SIZE(tests));
if (cg_find_unified_root(root, sizeof(root), NULL))
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:
ksft_test_result_fail("%s\n", tests[i].name);
break;
}
}
ksft_finished();
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,181 @@
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#include <errno.h>
#include <linux/limits.h>
#include <signal.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include "kselftest.h"
#include "cgroup_util.h"
static int run_success(const char *cgroup, void *arg)
{
return 0;
}
static int run_pause(const char *cgroup, void *arg)
{
return pause();
}
/*
* This test checks that pids.max prevents forking new children above the
* specified limit in the cgroup.
*/
static int test_pids_max(const char *root)
{
int ret = KSFT_FAIL;
char *cg_pids;
int pid;
cg_pids = cg_name(root, "pids_test");
if (!cg_pids)
goto cleanup;
if (cg_create(cg_pids))
goto cleanup;
if (cg_read_strcmp(cg_pids, "pids.max", "max\n"))
goto cleanup;
if (cg_write(cg_pids, "pids.max", "2"))
goto cleanup;
if (cg_enter_current(cg_pids))
goto cleanup;
pid = cg_run_nowait(cg_pids, run_pause, NULL);
if (pid < 0)
goto cleanup;
if (cg_run_nowait(cg_pids, run_success, NULL) != -1 || errno != EAGAIN)
goto cleanup;
if (kill(pid, SIGINT))
goto cleanup;
ret = KSFT_PASS;
cleanup:
cg_enter_current(root);
cg_destroy(cg_pids);
free(cg_pids);
return ret;
}
/*
* This test checks that pids.events are counted in cgroup associated with pids.max
*/
static int test_pids_events(const char *root)
{
int ret = KSFT_FAIL;
char *cg_parent = NULL, *cg_child = NULL;
int pid;
if (cgroup_feature("pids_localevents") <= 0)
return KSFT_SKIP;
cg_parent = cg_name(root, "pids_parent");
cg_child = cg_name(cg_parent, "pids_child");
if (!cg_parent || !cg_child)
goto cleanup;
if (cg_create(cg_parent))
goto cleanup;
if (cg_write(cg_parent, "cgroup.subtree_control", "+pids"))
goto cleanup;
if (cg_create(cg_child))
goto cleanup;
if (cg_write(cg_parent, "pids.max", "2"))
goto cleanup;
if (cg_read_strcmp(cg_child, "pids.max", "max\n"))
goto cleanup;
if (cg_enter_current(cg_child))
goto cleanup;
pid = cg_run_nowait(cg_child, run_pause, NULL);
if (pid < 0)
goto cleanup;
if (cg_run_nowait(cg_child, run_success, NULL) != -1 || errno != EAGAIN)
goto cleanup;
if (kill(pid, SIGINT))
goto cleanup;
if (cg_read_key_long(cg_child, "pids.events", "max ") != 0)
goto cleanup;
if (cg_read_key_long(cg_parent, "pids.events", "max ") != 1)
goto cleanup;
ret = KSFT_PASS;
cleanup:
cg_enter_current(root);
if (cg_child)
cg_destroy(cg_child);
if (cg_parent)
cg_destroy(cg_parent);
free(cg_child);
free(cg_parent);
return ret;
}
#define T(x) { x, #x }
struct pids_test {
int (*fn)(const char *root);
const char *name;
} tests[] = {
T(test_pids_max),
T(test_pids_events),
};
#undef T
int main(int argc, char **argv)
{
char root[PATH_MAX];
ksft_print_header();
ksft_set_plan(ARRAY_SIZE(tests));
if (cg_find_unified_root(root, sizeof(root), NULL))
ksft_exit_skip("cgroup v2 isn't mounted\n");
/*
* Check that pids controller is available:
* pids is listed in cgroup.controllers
*/
if (cg_read_strstr(root, "cgroup.controllers", "pids"))
ksft_exit_skip("pids controller isn't available\n");
if (cg_read_strstr(root, "cgroup.subtree_control", "pids"))
if (cg_write(root, "cgroup.subtree_control", "+pids"))
ksft_exit_skip("Failed to set pids controller\n");
for (int 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:
ksft_test_result_fail("%s\n", tests[i].name);
break;
}
}
ksft_finished();
}
@@ -0,0 +1,4 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
./with_stress.sh -s subsys -s fork ${OUTPUT:-.}/test_core
@@ -0,0 +1,845 @@
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#include <linux/limits.h>
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/sysinfo.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <sys/random.h>
#include "kselftest.h"
#include "cgroup_util.h"
static int page_size;
#define PATH_ZSWAP "/sys/module/zswap"
#define PATH_ZSWAP_ENABLED "/sys/module/zswap/parameters/enabled"
static int read_int(const char *path, size_t *value)
{
FILE *file;
int ret = 0;
file = fopen(path, "r");
if (!file)
return -1;
if (fscanf(file, "%ld", value) != 1)
ret = -1;
fclose(file);
return ret;
}
static int set_min_free_kb(size_t value)
{
FILE *file;
int ret;
file = fopen("/proc/sys/vm/min_free_kbytes", "w");
if (!file)
return -1;
ret = fprintf(file, "%ld\n", value);
fclose(file);
return ret;
}
static int read_min_free_kb(size_t *value)
{
return read_int("/proc/sys/vm/min_free_kbytes", value);
}
static int get_zswap_stored_pages(size_t *value)
{
return read_int("/sys/kernel/debug/zswap/stored_pages", value);
}
static long get_cg_wb_count(const char *cg)
{
return cg_read_key_long(cg, "memory.stat", "zswpwb");
}
static long get_zswpout(const char *cgroup)
{
return cg_read_key_long(cgroup, "memory.stat", "zswpout ");
}
static int allocate_and_read_bytes(const char *cgroup, void *arg)
{
size_t size = (size_t)arg;
char *mem = (char *)malloc(size);
int ret = 0;
if (!mem)
return -1;
for (int i = 0; i < size; i += page_size)
mem[i] = 'a';
/* Go through the allocated memory to (z)swap in and out pages */
for (int i = 0; i < size; i += page_size) {
if (mem[i] != 'a')
ret = -1;
}
free(mem);
return ret;
}
static int allocate_bytes(const char *cgroup, void *arg)
{
size_t size = (size_t)arg;
char *mem = (char *)malloc(size);
if (!mem)
return -1;
for (int i = 0; i < size; i += page_size)
mem[i] = 'a';
free(mem);
return 0;
}
static char *setup_test_group_1M(const char *root, const char *name)
{
char *group_name = cg_name(root, name);
if (!group_name)
return NULL;
if (cg_create(group_name))
goto fail;
if (cg_write(group_name, "memory.max", "1M")) {
cg_destroy(group_name);
goto fail;
}
return group_name;
fail:
free(group_name);
return NULL;
}
/*
* Writeback is asynchronous; poll until at least one writeback has
* been recorded for @cg, or until @timeout_ms has elapsed.
*/
static long wait_for_writeback(const char *cg, int timeout_ms)
{
long elapsed, count;
for (elapsed = 0; elapsed < timeout_ms; elapsed += 100) {
count = get_cg_wb_count(cg);
if (count < 0)
return -1;
if (count > 0)
return count;
usleep(100000);
}
return 0;
}
/*
* Sanity test to check that pages are written into zswap.
*/
static int test_zswap_usage(const char *root)
{
long zswpout_before, zswpout_after;
int ret = KSFT_FAIL;
char *test_group;
test_group = cg_name(root, "no_shrink_test");
if (!test_group)
goto out;
if (cg_create(test_group))
goto out;
if (cg_write(test_group, "memory.max", "1M"))
goto out;
zswpout_before = get_zswpout(test_group);
if (zswpout_before < 0) {
ksft_print_msg("Failed to get zswpout\n");
goto out;
}
/* Allocate more than memory.max to push memory into zswap */
if (cg_run(test_group, allocate_bytes, (void *)MB(4)))
goto out;
/* Verify that pages come into zswap */
zswpout_after = get_zswpout(test_group);
if (zswpout_after <= zswpout_before) {
ksft_print_msg("zswpout does not increase after test program\n");
goto out;
}
ret = KSFT_PASS;
out:
cg_destroy(test_group);
free(test_group);
return ret;
}
/*
* Check that when memory.zswap.max = 0, no pages can go to the zswap pool for
* the cgroup.
*/
static int test_swapin_nozswap(const char *root)
{
int ret = KSFT_FAIL;
char *test_group, mem_max_buf[32];
long swap_peak, zswpout, min_swap;
size_t allocation_size = page_size * 512;
min_swap = allocation_size / 4;
snprintf(mem_max_buf, sizeof(mem_max_buf), "%zu", allocation_size * 3/4);
test_group = cg_name(root, "no_zswap_test");
if (!test_group)
goto out;
if (cg_create(test_group))
goto out;
if (cg_write(test_group, "memory.max", mem_max_buf))
goto out;
if (cg_write(test_group, "memory.zswap.max", "0"))
goto out;
/* Allocate and read more than memory.max to trigger swapin */
if (cg_run(test_group, allocate_and_read_bytes, (void *)allocation_size))
goto out;
/* Verify that pages are swapped out, but no zswap happened */
swap_peak = cg_read_long(test_group, "memory.swap.peak");
if (swap_peak < 0) {
ksft_print_msg("failed to get cgroup's swap_peak\n");
goto out;
}
if (swap_peak < min_swap) {
ksft_print_msg("at least %ldKB of memory should be swapped out\n",
min_swap / 1024);
goto out;
}
zswpout = get_zswpout(test_group);
if (zswpout < 0) {
ksft_print_msg("failed to get zswpout\n");
goto out;
}
if (zswpout > 0) {
ksft_print_msg("zswapout > 0 when memory.zswap.max = 0\n");
goto out;
}
ret = KSFT_PASS;
out:
cg_destroy(test_group);
free(test_group);
return ret;
}
/* Simple test to verify the (z)swapin code paths */
static int test_zswapin(const char *root)
{
int ret = KSFT_FAIL;
char *test_group;
long zswpin;
test_group = cg_name(root, "zswapin_test");
if (!test_group)
goto out;
if (cg_create(test_group))
goto out;
if (cg_write(test_group, "memory.max", "8M"))
goto out;
if (cg_write(test_group, "memory.zswap.max", "max"))
goto out;
/* Allocate and read more than memory.max to trigger (z)swap in */
if (cg_run(test_group, allocate_and_read_bytes, (void *)MB(32)))
goto out;
zswpin = cg_read_key_long(test_group, "memory.stat", "zswpin ");
if (zswpin < 0) {
ksft_print_msg("failed to get zswpin\n");
goto out;
}
if (zswpin < MB(24) / page_size) {
ksft_print_msg("at least 24MB should be brought back from zswap\n");
goto out;
}
ret = KSFT_PASS;
out:
cg_destroy(test_group);
free(test_group);
return ret;
}
/*
* Attempt writeback with the following steps:
* 1. Allocate memory.
* 2. Reclaim memory equal to the amount that was allocated in step 1.
This will move it into zswap.
* 3. Save current zswap usage.
* 4. Move the memory allocated in step 1 back in from zswap.
* 5. Set zswap.max to 1/4 of the amount that was recorded in step 3.
* 6. Attempt to reclaim memory equal to the amount that was allocated,
this will either trigger writeback if it's enabled, or reclamation
will fail if writeback is disabled as there isn't enough zswap space.
*/
static int attempt_writeback(const char *cgroup, void *arg)
{
size_t memsize = page_size * 1024;
char buf[page_size];
long zswap_usage;
bool wb_enabled = *(bool *) arg;
int ret = -1;
char *mem;
mem = (char *)malloc(memsize);
if (!mem)
return ret;
/*
* Fill half of each page with increasing data, and keep other
* half empty, this will result in data that is still compressible
* and ends up in zswap, with material zswap usage.
*/
for (int i = 0; i < page_size; i++)
buf[i] = i < page_size/2 ? (char) i : 0;
for (int i = 0; i < memsize; i += page_size)
memcpy(&mem[i], buf, page_size);
/* Try and reclaim allocated memory */
if (cg_write_numeric(cgroup, "memory.reclaim", memsize)) {
ksft_print_msg("Failed to reclaim all of the requested memory\n");
goto out;
}
zswap_usage = cg_read_long(cgroup, "memory.zswap.current");
/* zswpin */
for (int i = 0; i < memsize; i += page_size) {
if (memcmp(&mem[i], buf, page_size)) {
ksft_print_msg("invalid memory\n");
goto out;
}
}
if (cg_write_numeric(cgroup, "memory.zswap.max", zswap_usage/4))
goto out;
/*
* If writeback is enabled, trying to reclaim memory now will trigger a
* writeback as zswap.max is 1/4 of what was needed when reclaim ran the first time.
* If writeback is disabled, memory reclaim will fail as zswap is limited and
* it can't writeback to swap.
*/
ret = cg_write_numeric(cgroup, "memory.reclaim", memsize);
if (!wb_enabled)
ret = (ret == -EAGAIN) ? 0 : -1;
out:
free(mem);
return ret;
}
static int test_zswap_writeback_one(const char *cgroup, bool wb)
{
long zswpwb_before, zswpwb_after;
zswpwb_before = get_cg_wb_count(cgroup);
if (zswpwb_before != 0) {
ksft_print_msg("zswpwb_before = %ld instead of 0\n", zswpwb_before);
return -1;
}
if (cg_run(cgroup, attempt_writeback, (void *) &wb))
return -1;
/* Verify that zswap writeback occurred only if writeback was enabled */
if (wb)
zswpwb_after = wait_for_writeback(cgroup, 5000);
else
zswpwb_after = get_cg_wb_count(cgroup);
if (zswpwb_after < 0)
return -1;
if (wb != !!zswpwb_after) {
ksft_print_msg("zswpwb_after is %ld while wb is %s\n",
zswpwb_after, wb ? "enabled" : "disabled");
return -1;
}
return 0;
}
/* Test to verify the zswap writeback path */
static int test_zswap_writeback(const char *root, bool wb)
{
int ret = KSFT_FAIL;
char *test_group, *test_group_child = NULL;
if (cg_read_strcmp(root, "memory.zswap.writeback", "1"))
return KSFT_SKIP;
test_group = cg_name(root, "zswap_writeback_test");
if (!test_group)
goto out;
if (cg_create(test_group))
goto out;
if (cg_write(test_group, "memory.zswap.writeback", wb ? "1" : "0"))
goto out;
if (test_zswap_writeback_one(test_group, wb))
goto out;
/* Reset memory.zswap.max to max (modified by attempt_writeback), and
* set up child cgroup, whose memory.zswap.writeback is hardcoded to 1.
* Thus, the parent's setting shall be what's in effect. */
if (cg_write(test_group, "memory.zswap.max", "max"))
goto out;
if (cg_write(test_group, "cgroup.subtree_control", "+memory"))
goto out;
test_group_child = cg_name(test_group, "zswap_writeback_test_child");
if (!test_group_child)
goto out;
if (cg_create(test_group_child))
goto out;
if (cg_write(test_group_child, "memory.zswap.writeback", "1"))
goto out;
if (test_zswap_writeback_one(test_group_child, wb))
goto out;
ret = KSFT_PASS;
out:
if (test_group_child) {
cg_destroy(test_group_child);
free(test_group_child);
}
cg_destroy(test_group);
free(test_group);
return ret;
}
static int test_zswap_writeback_enabled(const char *root)
{
return test_zswap_writeback(root, true);
}
static int test_zswap_writeback_disabled(const char *root)
{
return test_zswap_writeback(root, false);
}
/*
* When trying to store a memcg page in zswap, if the memcg hits its memory
* limit in zswap, writeback should affect only the zswapped pages of that
* memcg.
*/
static int test_no_invasive_cgroup_shrink(const char *root)
{
int ret = KSFT_FAIL;
unsigned int off;
size_t allocation_size = page_size * 1024;
unsigned int nr_pages = allocation_size / page_size;
char zswap_max_buf[32], mem_max_buf[32];
char *zw_allocation = NULL, *wb_allocation = NULL;
char *zw_group = NULL, *wb_group = NULL;
snprintf(zswap_max_buf, sizeof(zswap_max_buf), "%d", page_size);
snprintf(mem_max_buf, sizeof(mem_max_buf), "%zu", allocation_size / 2);
wb_group = setup_test_group_1M(root, "per_memcg_wb_test1");
if (!wb_group)
return KSFT_FAIL;
if (cg_write(wb_group, "memory.zswap.max", zswap_max_buf))
goto out;
if (cg_write(wb_group, "memory.max", mem_max_buf))
goto out;
zw_group = setup_test_group_1M(root, "per_memcg_wb_test2");
if (!zw_group)
goto out;
if (cg_write(zw_group, "memory.max", mem_max_buf))
goto out;
/* Push some zw_group memory into zswap (simple data, easy to compress) */
if (cg_enter_current(zw_group))
goto out;
zw_allocation = malloc(allocation_size);
for (int i = 0; i < nr_pages; i++) {
off = (unsigned long)i * page_size;
memset(&zw_allocation[off], 0, page_size);
memset(&zw_allocation[off], 'a', page_size/4);
}
if (cg_read_key_long(zw_group, "memory.stat", "zswapped") < 1)
goto out;
/* Push wb_group memory into zswap with hard-to-compress data to trigger wb */
if (cg_enter_current(wb_group))
goto out;
wb_allocation = malloc(allocation_size);
if (!wb_allocation)
goto out;
for (int i = 0; i < nr_pages; i++) {
off = (unsigned long)i * page_size;
memset(&wb_allocation[off], 0, page_size);
getrandom(&wb_allocation[off], page_size/4, 0);
}
/* Verify that only zswapped memory from gwb_group has been written back */
if (wait_for_writeback(wb_group, 5000) > 0 && get_cg_wb_count(zw_group) == 0)
ret = KSFT_PASS;
out:
cg_enter_current(root);
if (zw_group) {
cg_destroy(zw_group);
free(zw_group);
}
if (wb_group) {
cg_destroy(wb_group);
free(wb_group);
}
if (zw_allocation)
free(zw_allocation);
if (wb_allocation)
free(wb_allocation);
return ret;
}
struct no_kmem_bypass_child_args {
size_t target_alloc_bytes;
size_t child_allocated;
};
static int no_kmem_bypass_child(const char *cgroup, void *arg)
{
struct no_kmem_bypass_child_args *values = arg;
void *allocation;
allocation = malloc(values->target_alloc_bytes);
if (!allocation) {
values->child_allocated = true;
return -1;
}
for (long i = 0; i < values->target_alloc_bytes; i += page_size)
((char *)allocation)[i] = 'a';
values->child_allocated = true;
pause();
free(allocation);
return 0;
}
/*
* When pages owned by a memcg are pushed to zswap by kswapd, they should be
* charged to that cgroup. This wasn't the case before commit
* cd08d80ecdac("mm: correctly charge compressed memory to its memcg").
*
* The test first allocates memory in a memcg, then raises min_free_kbytes to
* a very high value so that the allocation falls below low wm, then makes
* another allocation to trigger kswapd that should push the memcg-owned pages
* to zswap and verifies that the zswap pages are correctly charged.
*
* To be run on a VM with at most 4G of memory.
*/
static int test_no_kmem_bypass(const char *root)
{
size_t min_free_kb_high, min_free_kb_low, min_free_kb_original;
struct no_kmem_bypass_child_args *values;
size_t trigger_allocation_size;
int wait_child_iteration = 0;
long stored_pages_threshold;
struct sysinfo sys_info;
int ret = KSFT_FAIL;
int child_status;
char *test_group = NULL;
pid_t child_pid;
/* Read sys info and compute test values accordingly */
if (sysinfo(&sys_info) != 0)
return KSFT_FAIL;
if (sys_info.totalram > 5000000000)
return KSFT_SKIP;
values = mmap(0, sizeof(struct no_kmem_bypass_child_args), PROT_READ |
PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (values == MAP_FAILED)
return KSFT_FAIL;
if (read_min_free_kb(&min_free_kb_original))
return KSFT_FAIL;
min_free_kb_high = sys_info.totalram / 2000;
min_free_kb_low = sys_info.totalram / 500000;
values->target_alloc_bytes = (sys_info.totalram - min_free_kb_high * 1000) +
sys_info.totalram * 5 / 100;
stored_pages_threshold = sys_info.totalram / 5 / page_size;
trigger_allocation_size = sys_info.totalram / 20;
/* Set up test memcg */
test_group = cg_name(root, "kmem_bypass_test");
if (!test_group)
goto out;
/* Spawn memcg child and wait for it to allocate */
set_min_free_kb(min_free_kb_low);
if (cg_create(test_group))
goto out;
values->child_allocated = false;
child_pid = cg_run_nowait(test_group, no_kmem_bypass_child, values);
if (child_pid < 0)
goto out;
while (!values->child_allocated && wait_child_iteration++ < 10000)
usleep(1000);
/* Try to wakeup kswapd and let it push child memory to zswap */
set_min_free_kb(min_free_kb_high);
for (int i = 0; i < 20; i++) {
size_t stored_pages;
char *trigger_allocation = malloc(trigger_allocation_size);
if (!trigger_allocation)
break;
for (int i = 0; i < trigger_allocation_size; i += page_size)
trigger_allocation[i] = 'b';
usleep(100000);
free(trigger_allocation);
if (get_zswap_stored_pages(&stored_pages))
break;
if (stored_pages < 0)
break;
/* If memory was pushed to zswap, verify it belongs to memcg */
if (stored_pages > stored_pages_threshold) {
int zswapped = cg_read_key_long(test_group, "memory.stat", "zswapped ");
int delta = stored_pages * page_size - zswapped;
int result_ok = delta < stored_pages * page_size / 4;
ret = result_ok ? KSFT_PASS : KSFT_FAIL;
break;
}
}
kill(child_pid, SIGTERM);
waitpid(child_pid, &child_status, 0);
out:
set_min_free_kb(min_free_kb_original);
cg_destroy(test_group);
free(test_group);
return ret;
}
struct incomp_child_args {
size_t size;
int pipefd[2];
int madvise_ret;
int madvise_errno;
};
static int allocate_random_and_wait(const char *cgroup, void *arg)
{
struct incomp_child_args *values = arg;
size_t size = values->size;
char *mem;
int fd;
ssize_t n;
close(values->pipefd[0]);
mem = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mem == MAP_FAILED)
return -1;
/* Fill with random data from /dev/urandom - incompressible */
fd = open("/dev/urandom", O_RDONLY);
if (fd < 0) {
munmap(mem, size);
return -1;
}
for (size_t i = 0; i < size; ) {
n = read(fd, mem + i, size - i);
if (n <= 0)
break;
i += n;
}
close(fd);
/* Touch all pages to ensure they're faulted in */
for (size_t i = 0; i < size; i += page_size)
mem[i] = mem[i];
/* Use MADV_PAGEOUT to push pages into zswap */
values->madvise_ret = madvise(mem, size, MADV_PAGEOUT);
values->madvise_errno = errno;
/* Notify parent that allocation and pageout are done */
write(values->pipefd[1], "x", 1);
close(values->pipefd[1]);
/* Keep memory alive for parent to check stats */
pause();
munmap(mem, size);
return 0;
}
static long get_zswap_incomp(const char *cgroup)
{
return cg_read_key_long(cgroup, "memory.stat", "zswap_incomp ");
}
/*
* Test that incompressible pages (random data) are tracked by zswap_incomp.
*
* The child process allocates random data within memory.max, then uses
* MADV_PAGEOUT to push pages into zswap. The parent waits on a pipe for
* the child to finish, then checks the zswap_incomp stat before the child
* exits (zswap_incomp is a gauge that decreases on free).
*/
static int test_zswap_incompressible(const char *root)
{
int ret = KSFT_FAIL;
struct incomp_child_args *values;
char *test_group;
long zswap_incomp;
pid_t child_pid;
int child_status;
char buf;
values = mmap(0, sizeof(struct incomp_child_args), PROT_READ |
PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (values == MAP_FAILED)
return KSFT_FAIL;
if (pipe(values->pipefd)) {
munmap(values, sizeof(struct incomp_child_args));
return KSFT_FAIL;
}
test_group = cg_name(root, "zswap_incompressible_test");
if (!test_group)
goto out;
if (cg_create(test_group))
goto out;
if (cg_write(test_group, "memory.max", "32M"))
goto out;
values->size = MB(4);
child_pid = cg_run_nowait(test_group, allocate_random_and_wait, values);
if (child_pid < 0)
goto out;
close(values->pipefd[1]);
/* Wait for child to finish allocating and pageout */
read(values->pipefd[0], &buf, 1);
close(values->pipefd[0]);
zswap_incomp = get_zswap_incomp(test_group);
if (zswap_incomp <= 0) {
long zswpout = get_zswpout(test_group);
long zswapped = cg_read_key_long(test_group, "memory.stat", "zswapped ");
long zswap_b = cg_read_key_long(test_group, "memory.stat", "zswap ");
ksft_print_msg("zswap_incomp not increased: %ld\n", zswap_incomp);
ksft_print_msg("debug: zswpout=%ld zswapped=%ld zswap_b=%ld\n",
zswpout, zswapped, zswap_b);
ksft_print_msg("debug: madvise ret=%d errno=%d\n",
values->madvise_ret, values->madvise_errno);
goto out_kill;
}
ret = KSFT_PASS;
out_kill:
kill(child_pid, SIGTERM);
waitpid(child_pid, &child_status, 0);
out:
cg_destroy(test_group);
free(test_group);
munmap(values, sizeof(struct incomp_child_args));
return ret;
}
#define T(x) { x, #x }
struct zswap_test {
int (*fn)(const char *root);
const char *name;
} tests[] = {
T(test_zswap_usage),
T(test_swapin_nozswap),
T(test_zswapin),
T(test_zswap_writeback_enabled),
T(test_zswap_writeback_disabled),
T(test_no_kmem_bypass),
T(test_no_invasive_cgroup_shrink),
T(test_zswap_incompressible),
};
#undef T
static void check_zswap_enabled(void)
{
char value[2];
if (access(PATH_ZSWAP, F_OK))
ksft_exit_skip("zswap isn't configured\n");
if (read_text(PATH_ZSWAP_ENABLED, value, sizeof(value)) <= 0)
ksft_exit_fail_msg("Failed to read " PATH_ZSWAP_ENABLED "\n");
if (value[0] == 'N')
ksft_exit_skip("zswap is disabled (hint: echo 1 > " PATH_ZSWAP_ENABLED ")\n");
}
int main(int argc, char **argv)
{
char root[PATH_MAX];
int i;
page_size = sysconf(_SC_PAGE_SIZE);
if (page_size <= 0)
page_size = BUF_SIZE;
ksft_print_header();
ksft_set_plan(ARRAY_SIZE(tests));
if (cg_find_unified_root(root, sizeof(root), NULL))
ksft_exit_skip("cgroup v2 isn't mounted\n");
check_zswap_enabled();
/*
* 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:
ksft_test_result_fail("%s\n", tests[i].name);
break;
}
}
ksft_finished();
}
@@ -0,0 +1,87 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Wait until an inotify event on the given cgroup file.
*/
#include <linux/limits.h>
#include <sys/inotify.h>
#include <sys/mman.h>
#include <sys/ptrace.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static const char usage[] = "Usage: %s [-v] <cgroup_file>\n";
static char *file;
static int verbose;
static inline void fail_message(char *msg)
{
fprintf(stderr, msg, file);
exit(1);
}
int main(int argc, char *argv[])
{
char *cmd = argv[0];
int c, fd;
struct pollfd fds = { .events = POLLIN, };
while ((c = getopt(argc, argv, "v")) != -1) {
switch (c) {
case 'v':
verbose++;
break;
}
argv++, argc--;
}
if (argc != 2) {
fprintf(stderr, usage, cmd);
return -1;
}
file = argv[1];
fd = open(file, O_RDONLY);
if (fd < 0)
fail_message("Cgroup file %s not found!\n");
close(fd);
fd = inotify_init();
if (fd < 0)
fail_message("inotify_init() fails on %s!\n");
if (inotify_add_watch(fd, file, IN_MODIFY) < 0)
fail_message("inotify_add_watch() fails on %s!\n");
fds.fd = fd;
/*
* poll waiting loop
*/
for (;;) {
int ret = poll(&fds, 1, 10000);
if (ret < 0) {
if (errno == EINTR)
continue;
perror("poll");
exit(1);
}
if ((ret > 0) && (fds.revents & POLLIN))
break;
}
if (verbose) {
struct inotify_event events[10];
long len;
usleep(1000);
len = read(fd, events, sizeof(events));
printf("Number of events read = %ld\n",
len/sizeof(struct inotify_event));
}
close(fd);
return 0;
}
+101
View File
@@ -0,0 +1,101 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
# Kselftest framework requirement - SKIP code is 4.
ksft_skip=4
stress_fork()
{
while true ; do
/usr/bin/true
sleep 0.01
done
}
stress_subsys()
{
local verb=+
while true ; do
echo $verb$subsys_ctrl >$sysfs/cgroup.subtree_control
[ $verb = "+" ] && verb=- || verb=+
# incommensurable period with other stresses
sleep 0.011
done
}
init_and_check()
{
sysfs=`mount -t cgroup2 | head -1 | awk '{ print $3 }'`
if [ ! -d "$sysfs" ]; then
echo "Skipping: cgroup2 is not mounted" >&2
exit $ksft_skip
fi
if ! echo +$subsys_ctrl >$sysfs/cgroup.subtree_control ; then
echo "Skipping: cannot enable $subsys_ctrl in $sysfs" >&2
exit $ksft_skip
fi
if ! echo -$subsys_ctrl >$sysfs/cgroup.subtree_control ; then
echo "Skipping: cannot disable $subsys_ctrl in $sysfs" >&2
exit $ksft_skip
fi
}
declare -a stresses
declare -a stress_pids
duration=5
rc=0
subsys_ctrl=cpuset
sysfs=
while getopts c:d:hs: opt; do
case $opt in
c)
subsys_ctrl=$OPTARG
;;
d)
duration=$OPTARG
;;
h)
echo "Usage $0 [ -s stress ] ... [ -d duration ] [-c controller] cmd args .."
echo -e "\t default duration $duration seconds"
echo -e "\t default controller $subsys_ctrl"
exit
;;
s)
func=stress_$OPTARG
if [ "x$(type -t $func)" != "xfunction" ] ; then
echo "Unknown stress $OPTARG"
exit 1
fi
stresses+=($func)
;;
esac
done
shift $((OPTIND - 1))
init_and_check
for s in ${stresses[*]} ; do
$s &
stress_pids+=($!)
done
time=0
start=$(date +%s)
while [ $time -lt $duration ] ; do
$*
rc=$?
[ $rc -eq 0 ] || break
time=$(($(date +%s) - $start))
done
for pid in ${stress_pids[*]} ; do
kill -SIGTERM $pid
wait $pid
done
exit $rc