From: Tao Cui <[email protected]> Add an example cost model implementing iocost_model_ops with the builtin linear HDD formula at double cost, and a runner which registers it as a struct_ops, switches a device to ctrl=bpf through io.cost.model and verifies the readback, restoring ctrl=auto afterwards. The runner first checks the negative case: writing ctrl=bpf while no model is registered is rejected. Under the same workload the doubled model should charge exactly twice the builtin model, which makes it a convenient way to verify that accounting goes through the BPF path; the accounting itself needs real IO and is covered by the kernel-side validation described in the cover letter. The runner is skipped unless $IOCOST_TEST_DEV gives a major:minor of a device with iocost enabled.
Signed-off-by: Tao Cui <[email protected]> --- .../selftests/bpf/prog_tests/iocost_model.c | 124 ++++++++++++++++++ .../selftests/bpf/progs/iocost_model.c | 91 +++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 tools/testing/selftests/bpf/prog_tests/iocost_model.c create mode 100644 tools/testing/selftests/bpf/progs/iocost_model.c diff --git a/tools/testing/selftests/bpf/prog_tests/iocost_model.c b/tools/testing/selftests/bpf/prog_tests/iocost_model.c new file mode 100644 index 0000000000000..386026241d5b4 --- /dev/null +++ b/tools/testing/selftests/bpf/prog_tests/iocost_model.c @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Copyright (c) 2026 Tao Cui */ +#include <test_progs.h> +#include "iocost_model.skel.h" + +/* + * Switch a device to ctrl=bpf through io.cost.model and verify the + * readback, restoring ctrl=auto afterwards. Returns 0 on success. + * Shared by the example-model tests below. + */ +static int switch_to_bpf(const char *dev) +{ + char path[] = "/sys/fs/cgroup/io.cost.model"; + char word[256], line[256], buf[300]; + FILE *fp; + int err, found = 0; + + fp = fopen(path, "r+"); + if (!fp) { + TH_LOG("open %s: %s", path, strerror(errno)); + return -1; + } + + while (fgets(line, sizeof(line), fp)) { + if (sscanf(line, "%255s", word) == 1 && !strcmp(word, dev)) { + found = 1; + break; + } + } + if (!found) { + TH_LOG("%s has no iocost line", dev); + fclose(fp); + return -1; + } + + snprintf(buf, sizeof(buf), "%s ctrl=bpf", dev); + err = fprintf(fp, "%s\n", buf); + if (err <= 0) { + TH_LOG("write ctrl=bpf failed"); + fclose(fp); + return -1; + } + fflush(fp); + + rewind(fp); + found = 0; + while (fgets(line, sizeof(line), fp)) { + if (sscanf(line, "%255s", word) == 1 && !strcmp(word, dev)) { + found = strstr(line, "ctrl=bpf") != NULL; + break; + } + } + if (!found) + TH_LOG("readback does not show ctrl=bpf"); + + fprintf(fp, "%s ctrl=auto\n", dev); + fclose(fp); + return found ? 0 : -1; +} + +static int dev_has_iocost(void) +{ + FILE *fp = fopen("/sys/fs/cgroup/io.cost.qos", "r"); + + if (fp) + fclose(fp); + return fp != NULL; +} + +/* + * Register the example cost model and switch a device to ctrl=bpf. + * IO accounting itself is not checked here; it needs a device doing + * real IO under iocost and is covered by the kernel-side validation + * described in the cover letter. + * + * Requires root, cgroup v2 and a device with iocost support. The + * device must be given as major:minor in $IOCOST_TEST_DEV, otherwise + * the test is skipped. + */ +void serial_test_iocost_model(void) +{ + struct iocost_model *skel; + char word[256], *dev; + int err; + + dev = getenv("IOCOST_TEST_DEV"); + if (!dev || geteuid() != 0) { + test__skip(); + return; + } + if (!ASSERT_TRUE(dev_has_iocost(), "iocost_mounted")) + return; + + /* negative: ctrl=bpf must be rejected while no model is + * registered, so a typo cannot silently disable cost model + * updates */ + if (sscanf(dev, "%255s", word) != 1) + return; + { + char path[] = "/sys/fs/cgroup/io.cost.model"; + char buf[300]; + FILE *fp = fopen(path, "w"); + + snprintf(buf, sizeof(buf), "%s ctrl=bpf\n", word); + err = 0; + if (fp) { + err = fprintf(fp, "%s", buf) <= 0; + fclose(fp); + } + ASSERT_TRUE(err, "ctrl_bpf_without_model_rejected"); + } + + skel = iocost_model__open_and_load(); + if (!ASSERT_OK_PTR(skel, "skel_open_load")) + return; + + /* attaching the struct_ops registers the model; a second + * registration of the same ops would fail with EBUSY */ + err = iocost_model__attach(skel); + if (ASSERT_OK(err, "attach")) + ASSERT_OK(switch_to_bpf(dev), "switch_and_readback"); + + iocost_model__destroy(skel); +} diff --git a/tools/testing/selftests/bpf/progs/iocost_model.c b/tools/testing/selftests/bpf/progs/iocost_model.c new file mode 100644 index 0000000000000..70f7987f8eb6a --- /dev/null +++ b/tools/testing/selftests/bpf/progs/iocost_model.c @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Copyright (c) 2026 Tao Cui */ +/* + * Example iocost cost model: the builtin linear HDD formula with all + * costs doubled. + * + * The constants mirror what calc_lcoefs() derives from the AUTOP_HDD + * defaults (rbps=174019176 rseqiops=41708 rrandiops=370, w-side + * analog) in vtime units where 1s == 2^37, expressed with the same + * round-up divisions so they cannot drift from the kernel, so a + * device switched to ctrl=bpf with this model registered charges + * exactly twice the builtin model under the same workload, which + * makes it a convenient way to verify that accounting goes through + * the BPF path. + */ +#include "vmlinux.h" +#include <bpf/bpf_helpers.h> +#include <bpf/bpf_tracing.h> + +/* VTIME_PER_SEC comes from vmlinux.h (a BTF enum constant) */ +#define IOC_PAGE_SIZE 4096 +#define IOC_SECT_TO_PAGE_SHIFT 3 /* 512B sectors to 4kB pages */ +#define LCOEF_RANDIO_PAGES 4096 /* 16MB seek threshold */ +#define IOCOST_COST_F_MERGE (1ULL << 0) /* not in BTF: a plain macro */ + +/* DIV64_U64_ROUND_UP / DIV_ROUND_UP_ULL equivalents, folded at + * compile time */ +#define RU(x, y) ((x) / (y) + (((x) % (y)) ? 1 : 0)) + +#define RBPS 174019176ULL +#define RSEQIOPS 41708ULL +#define RRANDIOPS 370ULL +#define WBPS 178075866ULL +#define WSEQIOPS 42705ULL +#define WRANDIOPS 378ULL + +#define RPAGE (RU(VTIME_PER_SEC, RU(RBPS, IOC_PAGE_SIZE))) +#define RSEQIO (RU(VTIME_PER_SEC, RSEQIOPS) - RPAGE) +#define RRANDIO (RU(VTIME_PER_SEC, RRANDIOPS) - RPAGE) +#define WPAGE (RU(VTIME_PER_SEC, RU(WBPS, IOC_PAGE_SIZE))) +#define WSEQIO (RU(VTIME_PER_SEC, WSEQIOPS) - WPAGE) +#define WRANDIO (RU(VTIME_PER_SEC, WRANDIOPS) - WPAGE) + +static __always_inline u64 builtin_cost(u64 op, u64 nbytes, u64 sector, + u64 cursor, u64 flags) +{ + u64 pages, seek_pages = 0, base, coef_page; + + if (!nbytes) + return 0; + + switch (op) { + case REQ_OP_READ: + base = RSEQIO; + coef_page = RPAGE; + break; + case REQ_OP_WRITE: + base = WSEQIO; + coef_page = WPAGE; + break; + default: + return 0; + } + + if (cursor) { + seek_pages = sector > cursor ? sector - cursor + : cursor - sector; + seek_pages >>= IOC_SECT_TO_PAGE_SHIFT; + if (seek_pages > LCOEF_RANDIO_PAGES) + base = (op == REQ_OP_READ) ? RRANDIO : WRANDIO; + } + + pages = RU(nbytes, IOC_PAGE_SIZE); + if (flags & IOCOST_COST_F_MERGE) + base = 0; + return base + pages * coef_page; +} + +SEC("struct_ops") +u64 BPF_PROG(iocost_2x_calc_cost, u64 op, u64 nbytes, u64 sector, + u64 cursor, u64 iocg_id, u64 flags) +{ + return 2 * builtin_cost(op, nbytes, sector, cursor, flags); +} + +SEC(".struct_ops") +struct iocost_model_ops iocost_2x = { + .calc_cost = (void *)iocost_2x_calc_cost, +}; + +char LICENSE[] SEC("license") = "GPL"; -- 2.43.0

