* [RFC PATCH v12 0/4] blk-iocost: BPF struct_ops cost model
@ 2026-11-03 13:26 Tao Cui
2026-11-03 13:26 ` [RFC PATCH v12 1/4] blk-iocost: add BPF struct_ops cost model support Tao Cui
` (3 more replies)
0 siblings, 4 replies; 8+ messages in thread
From: Tao Cui @ 2026-11-03 13:26 UTC (permalink / raw)
To: tj, josef, axboe, alexei.starovoitov
Cc: cgroups, linux-block, linux-kernel, bpf, andrii, eddyz87, ast,
daniel, linux-kselftest, cui.tao, cuitao, ameryhung
From: Tao Cui <cuitao@kylinos.cn>
This series lets a BPF program take over the cost model of one device.
The BPF model provides calc_cost() for charged IOs plus two transfer
coefficients for the completion-time sizing, io.cost.model selects
between the attached model and the builtin one, and enabling and
disabling the controller stays with io.cost.qos.
The builtin model is a linear formula derived from a handful of device
parameters, and the right formula is device specific. A BPF model can
be adapted to the device and updated without rebuilding the kernel.
The series is based on next-20261030.
Patches:
1/4 blk-iocost: add BPF struct_ops cost model support
2/4 blk-iocost: add iocost_ioc_tick tracepoint for per-period
device summary
3/4 selftests/bpf: add iocost cost model test
4/4 docs: cgroup-v2: document the iocost BPF cost model attachment
Changes since v11:
- dropped the cgroup callbacks (the models build their state in
calc_cost() anyway)
- dropped the queue freeze from detach
- folded everything into blk-iocost.c
- the selftest now uses cgroup_helpers: the tests create their own
cgroups and remove them on exit; io.cost.qos/io.cost.model are
root-only cgroupfs files and the test reverts them on its exit
paths
Tested on next-20261030 (qemu x86_64): the selftests pass, and the
2x example model doubles the charged cost over the builtin on a
rotational device (2.01x measured), returning to 1.00x after
"model=linear".
Link: https://lore.kernel.org/r/20261028143534.135968-1-cui.tao@linux.dev # v11
^ permalink raw reply [flat|nested] 8+ messages in thread
* [RFC PATCH v12 1/4] blk-iocost: add BPF struct_ops cost model support
2026-11-03 13:26 [RFC PATCH v12 0/4] blk-iocost: BPF struct_ops cost model Tao Cui
@ 2026-11-03 13:26 ` Tao Cui
2026-11-03 16:51 ` Alexei Starovoitov
2026-11-03 13:26 ` [RFC PATCH v12 2/4] blk-iocost: add iocost_ioc_tick tracepoint for per-period device summary Tao Cui
` (2 subsequent siblings)
3 siblings, 1 reply; 8+ messages in thread
From: Tao Cui @ 2026-11-03 13:26 UTC (permalink / raw)
To: tj, josef, axboe, alexei.starovoitov
Cc: cgroups, linux-block, linux-kernel, bpf, andrii, eddyz87, ast,
daniel, linux-kselftest, cui.tao, cuitao, ameryhung
From: Tao Cui <cuitao@kylinos.cn>
iocost prices IO with a linear model derived from a handful of
device parameters. The real cost of an IO depends on device
internals the formula cannot capture and the right formula is
device specific. This lets a BPF program take over pricing for
a device so that the model can be adapted to it without
rebuilding the kernel.
Attaching an iocost_model_ops struct_ops binds the model to one
device: the target is set in the dev member from userspace and
.reg creates the ioc if needed like an io.cost.model write and
publishes the model with the queue frozen and rq_qos_mutex held
like an io.cost.qos write. The attachment records the owning
link in the ioc, so a second attach on the same device fails
with -EBUSY, and device removal ejects the model. Detaching
takes only rq_qos_mutex in a single critical section, and the
map is freed after an RCU grace period so that a calc_cost()
already running can finish. io.cost.model selects between the
attached and builtin models without detaching the struct_ops;
only .unreg or device removal detaches, and .unreg detaches only
its own attachment so a reused dev_t cannot tear down a newer one.
While a model is selected, the automatic profile stepping of
ctrl=auto is suspended; it resumes when the model is switched off
or detached.
While in use, the BPF model prices every charged IO: calc_cost()
is called from the submission path with the bio and returns the
cost in vtime units, clamped to VTIME_PER_SEC by the caller.
The struct_ops also carries the transfer coefficients used by
the completion-time request sizing, so latency tracking and
vrate adjustment follow the model's pricing. Models without
calc_cost or with zero or out-of-range transfer coefficients
are rejected at registration.
The kernel keeps no per-cgroup state for the model; models
which need per-cgroup data keep it in their own maps, created
and freed with the cgroups. Without CONFIG_BLK_CGROUP_IOCOST_BPF
the struct_ops is not built and the builtin model is the only
choice.
Suggested-by: Tejun Heo <tj@kernel.org>
Signed-off-by: Tao Cui <cuitao@kylinos.cn>
---
block/Kconfig | 4 +
block/blk-iocost.c | 260 ++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 259 insertions(+), 5 deletions(-)
diff --git a/block/Kconfig b/block/Kconfig
index 70e4a66d941f..26badf54a477 100644
--- a/block/Kconfig
+++ b/block/Kconfig
@@ -161,6 +161,10 @@ config BLK_CGROUP_IOCOST
distributes IO capacity between different groups based on
their share of the overall weight distribution.
+config BLK_CGROUP_IOCOST_BPF
+ def_bool y
+ depends on BLK_CGROUP_IOCOST && BPF_SYSCALL && BPF_JIT && DEBUG_INFO_BTF
+
config BLK_CGROUP_IOPRIO
bool "Cgroup I/O controller for assigning an I/O priority class"
depends on BLK_CGROUP
diff --git a/block/blk-iocost.c b/block/blk-iocost.c
index 1c1fe07a96ea..22a3d7eb3b1b 100644
--- a/block/blk-iocost.c
+++ b/block/blk-iocost.c
@@ -34,7 +34,7 @@
* device time. If a given IO is estimated to cost 10ms, the device should
* be able to process ~100 of those IOs in a second.
*
- * Currently, there's only one builtin cost model - linear. Each IO is
+ * The builtin cost model is linear. Each IO is
* classified as sequential or random and given a base cost accordingly.
* On top of that, a size cost proportional to the length of the IO is
* added. While simple, this model captures the operational
@@ -179,8 +179,12 @@
#include <linux/time64.h>
#include <linux/parser.h>
#include <linux/sched/signal.h>
+#include <linux/bpf.h>
+#include <linux/bpf_verifier.h>
+#include <linux/btf.h>
#include <asm/local.h>
#include <asm/local64.h>
+#include "blk.h"
#include "blk-rq-qos.h"
#include "blk-stat.h"
#include "blk-wbt.h"
@@ -405,6 +409,27 @@ struct ioc_pcpu_stat {
u64 last_rq_wait_ns;
};
+enum {
+ IOCOST_COST_F_MERGE = 1 << 0, /* called from the merge path */
+};
+
+/* bpf cost model of one device */
+struct iocost_model_ops {
+ /* whole disk, in the new_encode_dev() encoding */
+ dev_t dev;
+
+ /* used by the completion path instead of LCOEF_[RW]PAGE */
+ u64 read_vtime_per_page;
+ u64 write_vtime_per_page;
+
+ /*
+ * Called for every charged bio from the submission path under
+ * rcu_read_lock(); must not sleep. Returns the cost of the bio
+ * in vtime units, clamped to VTIME_PER_SEC by the caller.
+ */
+ u64 (*calc_cost)(struct bio *bio, u64 model_flags);
+};
+
/* per device */
struct ioc {
struct rq_qos rqos;
@@ -448,6 +473,12 @@ struct ioc {
int autop_idx;
bool user_qos_params:1;
bool user_cost_model:1;
+
+ /* holds a reference on the struct_ops map, NULL if none is attached */
+ const struct iocost_model_ops *attached;
+ struct bpf_link *attached_link;
+ /* either NULL (linear) or ->attached */
+ const struct iocost_model_ops __rcu *model;
};
struct iocg_pcpu_stat {
@@ -672,6 +703,13 @@ static struct ioc *q_to_ioc(struct request_queue *q)
return rqos_to_ioc(rq_qos_id(q, RQ_QOS_COST));
}
+static const struct iocost_model_ops *ioc_model(struct ioc *ioc)
+{
+ if (!IS_ENABLED(CONFIG_BLK_CGROUP_IOCOST_BPF))
+ return NULL;
+ return rcu_dereference_check(ioc->model, lockdep_is_held(&ioc->lock));
+}
+
static const char __maybe_unused *ioc_name(struct ioc *ioc)
{
struct gendisk *disk = ioc->rqos.disk;
@@ -857,7 +895,7 @@ static int ioc_autop_idx(struct ioc *ioc, struct gendisk *disk)
return AUTOP_SSD_DFL;
/* if user is overriding anything, maintain what was there */
- if (ioc->user_qos_params || ioc->user_cost_model)
+ if (ioc->user_qos_params || ioc->user_cost_model || ioc_model(ioc))
return idx;
/* step up/down based on the vrate */
@@ -2656,6 +2694,17 @@ static u64 calc_vtime_cost(struct bio *bio, struct ioc_gq *iocg, bool is_merge)
{
u64 cost;
+ if (IS_ENABLED(CONFIG_BLK_CGROUP_IOCOST_BPF)) {
+ const struct iocost_model_ops *model;
+
+ guard(rcu)();
+ model = ioc_model(iocg->ioc);
+ if (model) {
+ cost = model->calc_cost(bio, is_merge ? IOCOST_COST_F_MERGE : 0);
+ return min(cost, VTIME_PER_SEC);
+ }
+ }
+
calc_vtime_cost_builtin(bio, iocg, is_merge, &cost);
return cost;
}
@@ -2682,6 +2731,17 @@ static u64 calc_size_vtime_cost(struct request *rq, struct ioc *ioc)
{
u64 cost;
+ if (IS_ENABLED(CONFIG_BLK_CGROUP_IOCOST_BPF)) {
+ const struct iocost_model_ops *model;
+
+ guard(rcu)();
+ model = ioc_model(ioc);
+ if (model)
+ return (blk_rq_stats_sectors(rq) >> IOC_SECT_TO_PAGE_SHIFT) *
+ (req_op(rq) == REQ_OP_READ ? model->read_vtime_per_page :
+ model->write_vtime_per_page);
+ }
+
calc_size_vtime_cost_builtin(rq, ioc, &cost);
return cost;
}
@@ -2976,10 +3036,33 @@ static void ioc_rqos_queue_depth_changed(struct rq_qos *rqos)
spin_unlock_irq(&ioc->lock);
}
+/* Called by .unreg and by device removal, whichever is first. */
+static void ioc_detach_model(struct ioc *ioc)
+{
+#ifdef CONFIG_BLK_CGROUP_IOCOST_BPF
+ const struct iocost_model_ops *ops = ioc->attached;
+
+ lockdep_assert_held(&ioc->rqos.disk->queue->rq_qos_mutex);
+
+ if (!ops)
+ return;
+
+ spin_lock_irq(&ioc->lock);
+ rcu_assign_pointer(ioc->model, NULL);
+ ioc->attached = NULL;
+ ioc->attached_link = NULL;
+ spin_unlock_irq(&ioc->lock);
+
+ /* calc_cost() can still run. The map is freed after an RCU gp. */
+ bpf_struct_ops_put(ops);
+#endif
+}
+
static void ioc_rqos_exit(struct rq_qos *rqos)
{
struct ioc *ioc = rqos_to_ioc(rqos);
+ ioc_detach_model(ioc);
blkcg_deactivate_policy(rqos->disk, &blkcg_policy_iocost);
spin_lock_irq(&ioc->lock);
@@ -3223,7 +3306,6 @@ static u64 ioc_weight_prfill(struct seq_file *sf, struct blkg_policy_data *pd,
return 0;
}
-
static int ioc_weight_show(struct seq_file *sf, void *v)
{
struct blkcg *blkcg = css_to_blkcg(seq_css(sf));
@@ -3531,11 +3613,12 @@ static u64 ioc_cost_model_prfill(struct seq_file *sf,
return 0;
spin_lock_irq(&ioc->lock);
- seq_printf(sf, "%s ctrl=%s model=linear "
+ seq_printf(sf, "%s ctrl=%s model=%s "
"rbps=%llu rseqiops=%llu rrandiops=%llu "
"wbps=%llu wseqiops=%llu wrandiops=%llu "
"flushiops=%llu\n",
dname, ioc->user_cost_model ? "user" : "auto",
+ ioc_model(ioc) ? "bpf" : "linear",
u[I_LCOEF_RBPS], u[I_LCOEF_RSEQIOPS], u[I_LCOEF_RRANDIOPS],
u[I_LCOEF_WBPS], u[I_LCOEF_WSEQIOPS], u[I_LCOEF_WRANDIOPS],
u[I_LCOEF_FLUSHIOPS]);
@@ -3572,6 +3655,7 @@ static const match_table_t i_lcoef_tokens = {
static ssize_t ioc_cost_model_write(struct kernfs_open_file *of, char *input,
size_t nbytes, loff_t off)
{
+ const struct iocost_model_ops *model;
struct blkg_conf_ctx ctx;
struct request_queue *q;
unsigned int memflags;
@@ -3608,6 +3692,7 @@ static ssize_t ioc_cost_model_write(struct kernfs_open_file *of, char *input,
spin_lock_irq(&ioc->lock);
memcpy(u, ioc->params.i_lcoefs, sizeof(u));
user = ioc->user_cost_model;
+ model = ioc_model(ioc);
ret = -EINVAL;
@@ -3632,7 +3717,11 @@ static ssize_t ioc_cost_model_write(struct kernfs_open_file *of, char *input,
continue;
case COST_MODEL:
match_strlcpy(buf, &args[0], sizeof(buf));
- if (strcmp(buf, "linear"))
+ if (!strcmp(buf, "linear"))
+ model = NULL;
+ else if (!strcmp(buf, "bpf") && ioc->attached)
+ model = ioc->attached;
+ else
goto unlock;
continue;
}
@@ -3652,6 +3741,7 @@ static ssize_t ioc_cost_model_write(struct kernfs_open_file *of, char *input,
} else {
ioc->user_cost_model = false;
}
+ rcu_assign_pointer(ioc->model, model);
ioc_refresh_params(ioc, true);
ret = 0;
@@ -3699,6 +3789,166 @@ static struct blkcg_policy blkcg_policy_iocost = {
.pd_stat_fn = ioc_pd_stat,
};
+#ifdef CONFIG_BLK_CGROUP_IOCOST_BPF
+static int ioc_bpf_reg(void *kdata, struct bpf_link *link)
+{
+ struct iocost_model_ops *ops = kdata;
+ struct block_device *bdev;
+ struct request_queue *q;
+ unsigned int memflags;
+ struct ioc *ioc;
+ int ret;
+
+ bdev = blkdev_get_no_open(new_decode_dev(ops->dev), false);
+ if (!bdev)
+ return -ENODEV;
+
+ q = bdev_get_queue(bdev);
+ if (bdev_is_partition(bdev) || !queue_is_mq(q)) {
+ blkdev_put_no_open(bdev);
+ return -EOPNOTSUPP;
+ }
+
+ /* the order of ioc_qos_write() */
+ memflags = blk_mq_freeze_queue(q);
+ mutex_lock(&q->rq_qos_mutex);
+
+ ret = -ENODEV;
+ if (!disk_live(bdev->bd_disk))
+ goto out;
+
+ ioc = q_to_ioc(q);
+ if (!ioc) {
+ ret = blk_iocost_init(bdev->bd_disk);
+ if (ret)
+ goto out;
+ ioc = q_to_ioc(q);
+ }
+
+ ret = -EBUSY;
+ if (ioc->attached || !bpf_struct_ops_get(ops))
+ goto out;
+
+ spin_lock_irq(&ioc->lock);
+ ioc->attached = ops;
+ ioc->attached_link = link;
+ rcu_assign_pointer(ioc->model, ops);
+ spin_unlock_irq(&ioc->lock);
+ ret = 0;
+out:
+ mutex_unlock(&q->rq_qos_mutex);
+ blk_mq_unfreeze_queue(q, memflags);
+ blkdev_put_no_open(bdev);
+ return ret;
+}
+
+static void ioc_bpf_unreg(void *kdata, struct bpf_link *link)
+{
+ struct iocost_model_ops *ops = kdata;
+ struct block_device *bdev;
+ struct request_queue *q;
+ struct ioc *ioc;
+
+ /* del_gendisk() is in progress or done. ioc_rqos_exit() detaches. */
+ bdev = blkdev_get_no_open(new_decode_dev(ops->dev), false);
+ if (!bdev)
+ return;
+
+ q = bdev_get_queue(bdev);
+ mutex_lock(&q->rq_qos_mutex);
+ ioc = q_to_ioc(q);
+ /* the dev_t can belong to another disk and another model by now */
+ if (ioc && ioc->attached == ops && ioc->attached_link == link)
+ ioc_detach_model(ioc);
+ mutex_unlock(&q->rq_qos_mutex);
+ blkdev_put_no_open(bdev);
+}
+
+static int ioc_bpf_init(struct btf *btf)
+{
+ return 0;
+}
+
+static int ioc_bpf_check_member(const struct btf_type *t,
+ const struct btf_member *member,
+ const struct bpf_prog *prog)
+{
+ return prog->sleepable ? -EINVAL : 0;
+}
+
+static int ioc_bpf_init_member(const struct btf_type *t,
+ const struct btf_member *member,
+ void *kdata, const void *udata)
+{
+ const struct iocost_model_ops *uops = udata;
+ struct iocost_model_ops *ops = kdata;
+
+ switch (__btf_member_bit_offset(t, member) / 8) {
+ case offsetof(struct iocost_model_ops, dev):
+ ops->dev = uops->dev;
+ return 1;
+ case offsetof(struct iocost_model_ops, read_vtime_per_page):
+ ops->read_vtime_per_page = uops->read_vtime_per_page;
+ return 1;
+ case offsetof(struct iocost_model_ops, write_vtime_per_page):
+ ops->write_vtime_per_page = uops->write_vtime_per_page;
+ return 1;
+ }
+ return 0;
+}
+
+static int ioc_bpf_validate(void *kdata)
+{
+ struct iocost_model_ops *ops = kdata;
+
+ /*
+ * Bound the transfer coefficients so that coefficient * pages
+ * cannot wrap u64 for any possible request size.
+ */
+ if (ops->read_vtime_per_page > (VTIME_PER_SEC >> IOC_SECT_TO_PAGE_SHIFT) ||
+ ops->write_vtime_per_page > (VTIME_PER_SEC >> IOC_SECT_TO_PAGE_SHIFT))
+ return -EINVAL;
+
+ if (!ops->dev || !ops->calc_cost ||
+ !ops->read_vtime_per_page || !ops->write_vtime_per_page)
+ return -EINVAL;
+ return 0;
+}
+
+static u64 ioc_bpf_calc_cost_stub(struct bio *bio, u64 model_flags)
+{
+ return 0;
+}
+
+static struct iocost_model_ops ioc_bpf_stubs = {
+ .calc_cost = ioc_bpf_calc_cost_stub,
+};
+
+static const struct bpf_verifier_ops ioc_bpf_verifier_ops = {
+ .get_func_proto = bpf_base_func_proto,
+ .is_valid_access = bpf_tracing_btf_ctx_access,
+};
+
+static struct bpf_struct_ops ioc_bpf_struct_ops = {
+ .verifier_ops = &ioc_bpf_verifier_ops,
+ .init = ioc_bpf_init,
+ .check_member = ioc_bpf_check_member,
+ .init_member = ioc_bpf_init_member,
+ .validate = ioc_bpf_validate,
+ .reg = ioc_bpf_reg,
+ .unreg = ioc_bpf_unreg,
+ .cfi_stubs = &ioc_bpf_stubs,
+ .name = "iocost_model_ops",
+ .owner = THIS_MODULE,
+};
+
+static int __init ioc_bpf_struct_ops_init(void)
+{
+ return register_bpf_struct_ops(&ioc_bpf_struct_ops, iocost_model_ops);
+}
+late_initcall(ioc_bpf_struct_ops_init);
+#endif
+
static int __init ioc_init(void)
{
return blkcg_policy_register(&blkcg_policy_iocost);
--
2.53.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [RFC PATCH v12 2/4] blk-iocost: add iocost_ioc_tick tracepoint for per-period device summary
2026-11-03 13:26 [RFC PATCH v12 0/4] blk-iocost: BPF struct_ops cost model Tao Cui
2026-11-03 13:26 ` [RFC PATCH v12 1/4] blk-iocost: add BPF struct_ops cost model support Tao Cui
@ 2026-11-03 13:26 ` Tao Cui
2026-11-03 14:07 ` bot+bpf-ci
2026-11-03 13:26 ` [RFC PATCH v12 3/4] selftests/bpf: add iocost cost model test Tao Cui
2026-11-03 13:26 ` [RFC PATCH v12 4/4] docs: cgroup-v2: document the iocost BPF cost model attachment Tao Cui
3 siblings, 1 reply; 8+ messages in thread
From: Tao Cui @ 2026-11-03 13:26 UTC (permalink / raw)
To: tj, josef, axboe, alexei.starovoitov
Cc: cgroups, linux-block, linux-kernel, bpf, andrii, eddyz87, ast,
daniel, linux-kselftest, cui.tao, cuitao, ameryhung
From: Tao Cui <cuitao@kylinos.cn>
Add iocost_ioc_tick, emitted once per period from ioc_timer_fn() with
the overall controller state: period_us, vrate, busy_level, active iocg
count and usage percentage. busy_level, cur_period, nr_active and
usage_us_sum are complete at the emission point. usage is wall time
over wall time, summed over iocgs, and is not capped at 100. vrate and
period_us reflect the current controller state, which an io.cost.qos
write may have changed mid-period.
The event fires every period the controller is running, including
steady states, plus one final tick before the controller goes idle, so
the transition to dormancy is directly visible. The active iocg count
is only gathered when the tracepoint is enabled.
Suggested-by: Tejun Heo <tj@kernel.org>
Signed-off-by: Tao Cui <cuitao@kylinos.cn>
---
MAINTAINERS | 1 +
block/blk-iocost.c | 17 ++++++++++++++
include/trace/events/iocost.h | 43 +++++++++++++++++++++++++++++++++++
3 files changed, 61 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index aaf2825ac945..25cd7cd4f17b 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -6832,6 +6832,7 @@ F: block/blk-iocost.c
F: block/blk-iolatency.c
F: block/blk-throttle.c
F: include/linux/blk-cgroup.h
+F: include/trace/events/iocost.h
CONTROL GROUP - CPUSET
M: Waiman Long <longman@redhat.com>
diff --git a/block/blk-iocost.c b/block/blk-iocost.c
index 22a3d7eb3b1b..8b624654992b 100644
--- a/block/blk-iocost.c
+++ b/block/blk-iocost.c
@@ -2349,6 +2349,7 @@ static void ioc_timer_fn(struct timer_list *timer)
struct ioc_now now;
LIST_HEAD(surpluses);
int nr_debtors, nr_shortages = 0, nr_lagging = 0;
+ int nr_active = 0;
u64 usage_us_sum = 0;
u32 ppm_rthr;
u32 ppm_wthr;
@@ -2546,6 +2547,22 @@ static void ioc_timer_fn(struct timer_list *timer)
ioc->busy_level = clamp(ioc->busy_level, -1000, 1000);
+ /*
+ * busy_level, cur_period, nr_active and usage_us_sum are complete
+ * here. usage is wall time over wall time, summed over iocgs, and
+ * is not capped at 100. vrate and period_us reflect the current
+ * controller state, which an io.cost.qos write may have changed
+ * mid-period. Emit before the refresh and the period transition.
+ */
+ if (trace_iocost_ioc_tick_enabled()) {
+ struct ioc_gq *active_iocg;
+
+ list_for_each_entry(active_iocg, &ioc->active_iocgs,
+ active_list)
+ nr_active++;
+ }
+ trace_iocost_ioc_tick(ioc, &now, nr_active, usage_us_sum);
+
ioc_adjust_base_vrate(ioc, rq_wait_pct, nr_lagging, nr_shortages,
prev_busy_level, missed_ppm);
diff --git a/include/trace/events/iocost.h b/include/trace/events/iocost.h
index eb711343ce4f..fb0be4b8c629 100644
--- a/include/trace/events/iocost.h
+++ b/include/trace/events/iocost.h
@@ -181,6 +181,49 @@ TRACE_EVENT(iocost_ioc_vrate_adj,
)
);
+/*
+ * Periodic per-device summary, emitted once per period before the vrate
+ * adjustment and the period transition in ioc_timer_fn(). Unlike the
+ * state-change events above, this fires every period the controller is
+ * running, including steady states, and carries the overall controller state
+ * so basic monitoring doesn't require drgn.
+ */
+TRACE_EVENT(iocost_ioc_tick,
+
+ TP_PROTO(struct ioc *ioc, const struct ioc_now *now,
+ int nr_active, u64 usage_us_sum),
+
+ TP_ARGS(ioc, now, nr_active, usage_us_sum),
+
+ TP_STRUCT__entry (
+ __string(devname, ioc_name(ioc))
+ __field(u64, cur_period)
+ __field(u32, period_us)
+ __field(u64, vrate)
+ __field(int, busy_level)
+ __field(int, nr_active)
+ __field(u32, usage_pct)
+ ),
+
+ TP_fast_assign(
+ __assign_str(devname);
+ __entry->cur_period = atomic64_read(&ioc->cur_period);
+ __entry->period_us = ioc->period_us;
+ __entry->vrate = ioc->vtime_base_rate;
+ __entry->busy_level = ioc->busy_level;
+ __entry->nr_active = nr_active;
+ __entry->usage_pct = now->now > ioc->period_at ?
+ div64_u64(usage_us_sum * 100,
+ now->now - ioc->period_at) : 0;
+ ),
+
+ TP_printk("[%s] period=%llu:%uus vrate=%llu busy=%d active=%d usage=%u%%",
+ __get_str(devname), __entry->cur_period, __entry->period_us,
+ __entry->vrate, __entry->busy_level, __entry->nr_active,
+ __entry->usage_pct
+ )
+);
+
TRACE_EVENT(iocost_iocg_forgive_debt,
TP_PROTO(struct ioc_gq *iocg, const char *path, struct ioc_now *now,
--
2.53.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [RFC PATCH v12 3/4] selftests/bpf: add iocost cost model test
2026-11-03 13:26 [RFC PATCH v12 0/4] blk-iocost: BPF struct_ops cost model Tao Cui
2026-11-03 13:26 ` [RFC PATCH v12 1/4] blk-iocost: add BPF struct_ops cost model support Tao Cui
2026-11-03 13:26 ` [RFC PATCH v12 2/4] blk-iocost: add iocost_ioc_tick tracepoint for per-period device summary Tao Cui
@ 2026-11-03 13:26 ` Tao Cui
2026-11-03 14:07 ` bot+bpf-ci
2026-11-03 13:26 ` [RFC PATCH v12 4/4] docs: cgroup-v2: document the iocost BPF cost model attachment Tao Cui
3 siblings, 1 reply; 8+ messages in thread
From: Tao Cui @ 2026-11-03 13:26 UTC (permalink / raw)
To: tj, josef, axboe, alexei.starovoitov
Cc: cgroups, linux-block, linux-kernel, bpf, andrii, eddyz87, ast,
daniel, linux-kselftest, cui.tao, cuitao, ameryhung
From: Tao Cui <cuitao@kylinos.cn>
Add two example iocost cost models and tests which attach them to a
null_blk instance the tests create through configfs, so everything
runs without external setup.
The first model implements the builtin linear HDD formula at double
cost. The second implements multi-stream sequentiality detection: it
keeps a per-cgroup table of stream slots instead of the single builtin
cursor, so concurrent sequential streams in one cgroup are priced
sequentially. Both set the target device through the struct_ops
member before load, and the maps are declared ".struct_ops.link" so
that a test dying in between does not leave the model attached.
The tests verify the model=bpf readback while attached, that ctrl=bpf
is rejected, that a second model on the same device fails with -EBUSY,
that model=linear and model=bpf switch between the attached and
builtin models without detaching, and that detaching restores the
builtin model. With the controller enabled through io.cost.qos, they
also verify that IOs are priced through calc_cost() and that the
iocost_ioc_tick tracepoint is seen from BPF. They also verify that
device removal under the attached model ejects it, with a recreated
device attachable again.
Suggested-by: Tejun Heo <tj@kernel.org>
Signed-off-by: Tao Cui <cuitao@kylinos.cn>
---
tools/testing/selftests/bpf/config | 2 +
.../selftests/bpf/prog_tests/iocost_model.c | 748 ++++++++++++++++++
.../selftests/bpf/progs/iocost_model.c | 184 +++++
tools/testing/selftests/bpf/progs/iocost_ms.c | 187 +++++
4 files changed, 1121 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/iocost_model.c
create mode 100644 tools/testing/selftests/bpf/progs/iocost_model.c
create mode 100644 tools/testing/selftests/bpf/progs/iocost_ms.c
diff --git a/tools/testing/selftests/bpf/config b/tools/testing/selftests/bpf/config
index 49dfe939467c..78ed59f83a16 100644
--- a/tools/testing/selftests/bpf/config
+++ b/tools/testing/selftests/bpf/config
@@ -141,3 +141,5 @@ CONFIG_SMC_HS_CTRL_BPF=y
CONFIG_DIBS=y
CONFIG_DIBS_LO=y
CONFIG_PM_WAKELOCKS=y
+CONFIG_BLK_CGROUP_IOCOST=y
+CONFIG_BLK_DEV_NULL_BLK=y
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 000000000000..fa9280fe9e34
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/iocost_model.c
@@ -0,0 +1,748 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 KylinSoft Corporation. */
+#define _GNU_SOURCE
+#include <test_progs.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <sys/statfs.h>
+#include <linux/magic.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>
+#include <unistd.h>
+#include "iocost_model.skel.h"
+#include "iocost_ms.skel.h"
+#include "cgroup_helpers.h"
+
+/*
+ * Write val to the configfs attribute at path, returning the errno of the
+ * write negated (0 on success).
+ */
+static int configfs_write(const char *path, const char *val)
+{
+ FILE *fp;
+ int err = 0;
+
+ fp = fopen(path, "w");
+ if (!fp)
+ return -errno;
+ if (fwrite(val, 1, strlen(val), fp) != strlen(val))
+ err = ferror(fp) ? (errno ? -errno : -EIO) : -EIO;
+ if (fclose(fp) && !err)
+ err = -errno;
+ return err;
+}
+
+/*
+ * Read the first token of the file at path, for the dev attribute.
+ */
+static int read_file_token(const char *path, char *tok, size_t tok_sz)
+{
+ char fmt[16];
+ FILE *fp;
+
+ fp = fopen(path, "r");
+ if (!fp)
+ return -errno;
+ snprintf(fmt, sizeof(fmt), "%%%zus", tok_sz - 1);
+ if (fscanf(fp, fmt, tok) != 1) {
+ fclose(fp);
+ return -EIO;
+ }
+ fclose(fp);
+ return 0;
+}
+
+/*
+ * Create a null_blk instance through configfs and copy its maj:min
+ * into @dev. The device appears only once power is written, and is
+ * removed by rmdir of the instance directory; the maj:min comes from
+ * the sysfs directory of the created disk, which is named after the
+ * instance.
+ */
+static int nullb_create(const char *name, char *dev, size_t dev_sz)
+{
+ char path[128];
+ char p2[128];
+ int err;
+
+ snprintf(path, sizeof(path), "/sys/kernel/config/nullb/%s", name);
+ if (mkdir(path, 0755)) {
+ if (errno != EEXIST)
+ return -errno;
+ /*
+ * a leftover instance from an earlier crashed run may
+ * still be powered: tear it down before reusing the
+ * name
+ */
+ snprintf(p2, sizeof(p2),
+ "/sys/kernel/config/nullb/%s/power", name);
+ configfs_write(p2, "0");
+ if (rmdir(path) && errno != ENOENT)
+ return -errno;
+ if (mkdir(path, 0755) && errno != EEXIST)
+ return -errno;
+ }
+
+ snprintf(path, sizeof(path), "/sys/kernel/config/nullb/%s/size", name);
+ err = configfs_write(path, "128");
+ if (err)
+ return err;
+
+ snprintf(path, sizeof(path), "/sys/kernel/config/nullb/%s/power", name);
+ err = configfs_write(path, "1");
+ if (err)
+ return err;
+
+ snprintf(path, sizeof(path), "/sys/block/%s/dev", name);
+ err = read_file_token(path, dev, dev_sz);
+ if (err) {
+ /* do not leave a powered instance behind */
+ snprintf(path, sizeof(path),
+ "/sys/kernel/config/nullb/%s/power", name);
+ configfs_write(path, "0");
+ snprintf(path, sizeof(path),
+ "/sys/kernel/config/nullb/%s", name);
+ rmdir(path);
+ }
+ return err;
+}
+
+static int nullb_remove(const char *name)
+{
+ char path[128];
+
+ /* a powered instance cannot be removed */
+ snprintf(path, sizeof(path), "/sys/kernel/config/nullb/%s/power", name);
+ configfs_write(path, "0");
+ snprintf(path, sizeof(path), "/sys/kernel/config/nullb/%s", name);
+ if (rmdir(path))
+ return -errno;
+ return 0;
+}
+
+#define IOCOST_NULLB "iocost_model_test"
+#define IOCOST_NULLB2 "iocost_model_test2"
+
+/*
+ * The tests run against their own null_blk instance created through configfs,
+ * so they need root, configfs and the null_blk driver.
+ */
+/*
+ * The BPF cost model needs cgroup v2 at /sys/fs/cgroup and
+ * CONFIG_BLK_CGROUP_IOCOST_BPF in the running kernel; both are environment
+ * properties, so their absence is a skip.
+ */
+static bool iocost_bpf_supported(void)
+{
+ FILE *f;
+ char line[128];
+ bool on = false;
+
+ f = popen("zcat /proc/config.gz 2>/dev/null", "r");
+ if (!f)
+ return false;
+ while (fgets(line, sizeof(line), f)) {
+ if (strstr(line, "CONFIG_BLK_CGROUP_IOCOST_BPF=y"))
+ on = true;
+ }
+ pclose(f);
+ return on;
+}
+
+static bool iocost_test_env(void)
+{
+ struct statfs sf;
+
+ if (geteuid() != 0) {
+ printf("%s:SKIP:needs root\n", __func__);
+ test__skip();
+ return false;
+ }
+
+ if (statfs("/sys/fs/cgroup", &sf) ||
+ sf.f_type != CGROUP2_SUPER_MAGIC) {
+ printf("%s:SKIP:needs cgroup v2 at /sys/fs/cgroup\n",
+ __func__);
+ test__skip();
+ return false;
+ }
+
+ if (!iocost_bpf_supported()) {
+ printf("%s:SKIP:needs CONFIG_BLK_CGROUP_IOCOST_BPF\n",
+ __func__);
+ test__skip();
+ return false;
+ }
+
+ /* isolate the configfs and cgroup mounts from the host */
+ if (unshare(CLONE_NEWNS) ||
+ mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)) {
+ printf("%s:SKIP:cannot make mounts private: %s\n",
+ __func__, strerror(errno));
+ test__skip();
+ return false;
+ }
+ mkdir("/sys/kernel/config", 0755);
+ (void)mount("none", "/sys/kernel/config", "configfs", 0, NULL);
+ SYS_NOFAIL("modprobe null_blk nr_devices=0");
+ if (access("/sys/kernel/config/nullb", W_OK)) {
+ printf("%s:SKIP:needs null_blk with configfs support\n",
+ __func__);
+ test__skip();
+ return false;
+ }
+ return true;
+}
+
+/*
+ * Read back the io.cost.model line of dev and copy the model= value into
+ * @model. Returns 0 on success.
+ */
+static int readback_model(const char *dev, char *model, size_t model_sz)
+{
+ char line[256], word[256], *m, *end;
+ FILE *fp;
+ int found = 0;
+
+ fp = fopen("/sys/fs/cgroup/io.cost.model", "r");
+ if (!fp)
+ return -1;
+ while (fgets(line, sizeof(line), fp)) {
+ if (sscanf(line, "%255s", word) == 1 && !strcmp(word, dev)) {
+ found = 1;
+ break;
+ }
+ }
+ fclose(fp);
+ if (!found)
+ return -1;
+
+ m = strstr(line, "model=");
+ if (!m)
+ return -1;
+ m += strlen("model=");
+ end = m;
+ while (*end && *end != ' ')
+ end++;
+ snprintf(model, model_sz, "%.*s", (int)(end - m), m);
+ return 0;
+}
+
+/*
+ * Read the cost.usage accumulator of dev from the cgroup's io.stat. Returns 0
+ * on success, -ENODEV when the device has no line yet (no charge recorded).
+ */
+static int read_cost_usage(const char *cgroup, const char *dev,
+ unsigned long long *usage)
+{
+ char buf[4096], *save = NULL, *linep, *tok, *word;
+ int found = 0, ret;
+
+ ret = read_cgroup_file(cgroup, "io.stat", buf, sizeof(buf));
+ if (ret)
+ return ret;
+ for (linep = strtok_r(buf, "\n", &save); linep;
+ linep = strtok_r(NULL, "\n", &save)) {
+ char *wsave = NULL;
+
+ tok = strtok_r(linep, " \t", &wsave);
+ if (!tok || strcmp(tok, dev))
+ continue;
+ found = 1;
+ while ((word = strtok_r(NULL, " \t", &wsave))) {
+ if (!strncmp(word, "cost.usage=", 11)) {
+ *usage = strtoull(word + 11, NULL, 0);
+ return 0;
+ }
+ }
+ }
+ return found ? -ENOENT : -ENODEV;
+}
+
+/*
+ * Write a line to io.cost.model for dev and return the errno of the write
+ * negated (0 on success), as ASSERT_ERR() expects.
+ */
+static int write_cost_model(const char *dev, const char *what)
+{
+ char buf[128];
+ FILE *fp;
+ int err = 0;
+
+ snprintf(buf, sizeof(buf), "%s %s", dev, what);
+ fp = fopen("/sys/fs/cgroup/io.cost.model", "w");
+ if (!fp)
+ return -EACCES;
+ if (fwrite(buf, 1, strlen(buf), fp) != strlen(buf))
+ err = ferror(fp) ? (errno ? -errno : -EIO) : -EIO;
+ if (fclose(fp) && !err)
+ err = -errno;
+ return err;
+}
+
+/*
+ * Attach the example model to the null_blk instance the test creates: the dev
+ * member is written through the struct_ops map's initial value before load,
+ * and attaching the struct_ops attaches the model to the device. Detaching the
+ * struct_ops restores the builtin model.
+ *
+ * Requires root, cgroup v2 and null_blk with configfs support.
+ */
+void serial_test_iocost_model(void)
+{
+ struct iocost_model *skel, *second, *bad;
+ unsigned int maj, min;
+ char buf[4096], path[64], qbuf[160], dev2[32];
+ int devfd, i, err, cg;
+ char model[32], dev[32];
+ unsigned long long u1 = 0, u2 = 0;
+
+ if (!iocost_test_env())
+ return;
+ if (!ASSERT_OK(nullb_create(IOCOST_NULLB, dev, sizeof(dev)),
+ "nullb_create"))
+ return;
+ if (sscanf(dev, "%u:%u", &maj, &min) != 2) {
+ ASSERT_TRUE(false, "dev_parse");
+ nullb_remove(IOCOST_NULLB);
+ return;
+ }
+
+ skel = iocost_model__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open")) {
+ nullb_remove(IOCOST_NULLB);
+ return;
+ }
+
+ /*
+ * a model with a zero coefficient must fail to attach instead of
+ * silently zeroing the completion sizing (validate rejects it on
+ * the map update the attach performs; the link form of the
+ * struct_ops only reaches it there)
+ */
+ bad = iocost_model__open();
+ if (!ASSERT_OK_PTR(bad, "bad_open")) {
+ iocost_model__destroy(skel);
+ nullb_remove(IOCOST_NULLB);
+ return;
+ }
+ bad->struct_ops.iocost_2x->dev = (__u32)makedev(maj, min);
+ bad->struct_ops.iocost_2x->write_vtime_per_page = 0;
+ if (ASSERT_OK(iocost_model__load(bad), "bad_load"))
+ ASSERT_EQ(iocost_model__attach(bad), -EINVAL,
+ "attach_rejects_zero_coeff");
+ iocost_model__destroy(bad);
+
+ /* same for the read-side transfer coefficient */
+ bad = iocost_model__open();
+ if (!ASSERT_OK_PTR(bad, "bad_read_open")) {
+ iocost_model__destroy(skel);
+ nullb_remove(IOCOST_NULLB);
+ return;
+ }
+ bad->struct_ops.iocost_2x->dev = (__u32)makedev(maj, min);
+ bad->struct_ops.iocost_2x->read_vtime_per_page = 0;
+ if (ASSERT_OK(iocost_model__load(bad), "bad_read_load"))
+ ASSERT_EQ(iocost_model__attach(bad), -EINVAL,
+ "attach_rejects_zero_read");
+ iocost_model__destroy(bad);
+
+ skel->struct_ops.iocost_2x->dev = (__u32)makedev(maj, min);
+ /*
+ * Enable the io controller for the priced-IO cgroup: without it
+ * the cgroup has no io.weight/io.stat and the iocg never
+ * activates, so the model is consulted but nothing is charged.
+ */
+ /*
+ * cgroup_setup_and_join() creates the priced-IO cgroup with the
+ * available controllers enabled, so the cgroup has
+ * io.weight/io.stat and the iocg activates.
+ * cleanup_cgroup_environment() removes it from every exit path
+ * below. io.cost.qos and io.cost.model are root-only cgroupfs
+ * files: they are written at the hierarchy root and reverted on
+ * exit.
+ */
+ cg = cgroup_setup_and_join("iocost_pairing");
+ if (!ASSERT_GE(cg, 0, "setup_cg")) {
+ iocost_model__destroy(skel);
+ nullb_remove(IOCOST_NULLB);
+ return;
+ }
+ close(cg);
+ snprintf(path, sizeof(path), "/dev/%s", IOCOST_NULLB);
+ devfd = open(path, O_RDONLY);
+ if (!ASSERT_OK(devfd < 0 ? -errno : 0, "open_dev")) {
+ cleanup_cgroup_environment();
+ iocost_model__destroy(skel);
+ nullb_remove(IOCOST_NULLB);
+ return;
+ }
+ for (i = 0; i < 32; i++)
+ (void)read(devfd, buf, sizeof(buf));
+ close(devfd);
+
+ err = iocost_model__load(skel);
+ if (!ASSERT_OK(err, "skel_load")) {
+ iocost_model__destroy(skel);
+ cleanup_cgroup_environment();
+ nullb_remove(IOCOST_NULLB);
+ return;
+ }
+
+ err = iocost_model__attach(skel);
+ if (ASSERT_OK(err, "attach")) {
+ /*
+ * attached: the read path reports model=bpf until the
+ * struct_ops is detached; ctrl keeps describing the
+ * builtin coefficients
+ */
+ err = readback_model(dev, model, sizeof(model));
+ if (ASSERT_OK(err, "readback"))
+ ASSERT_STREQ(model, "bpf", "model_bpf");
+
+ /* a second model on the same device fails with -EBUSY */
+ second = iocost_model__open();
+ if (ASSERT_OK_PTR(second, "second_open")) {
+ second->struct_ops.iocost_2x->dev =
+ (__u32)makedev(maj, min);
+ err = iocost_model__load(second);
+ if (ASSERT_OK(err, "second_load")) {
+ struct bpf_link *l2;
+
+ /*
+ * the kernel rejects attaching a second
+ * model to the device with EBUSY
+ */
+ l2 = bpf_map__attach_struct_ops(
+ second->maps.iocost_2x);
+ if (!ASSERT_ERR_PTR(l2, "second_ebusy"))
+ bpf_link__destroy(l2);
+ else
+ ASSERT_EQ(libbpf_get_error(l2), -EBUSY,
+ "second_ebusy_errno");
+ }
+ iocost_model__destroy(second);
+ }
+
+ /*
+ * pricing coverage: root cgroup IOs bypass the model, so
+ * move into the pairing cgroup, enable the controller
+ * and verify IOs actually go through calc_cost() and get
+ * charged
+ */
+ ASSERT_OK(join_cgroup("iocost_pairing"), "join_pairing");
+ snprintf(qbuf, sizeof(qbuf), "%s 1000", dev);
+ ASSERT_OK(write_cgroup_file("iocost_pairing", "io.weight",
+ qbuf), "weight_pairing");
+ snprintf(qbuf, sizeof(qbuf),
+ "%s enable=1 ctrl=user min=100.00 max=100.00", dev);
+ ASSERT_OK(configfs_write("/sys/fs/cgroup/io.cost.qos", qbuf),
+ "qos_enable");
+
+ devfd = open(path, O_RDONLY);
+ if (!ASSERT_OK(devfd < 0 ? -errno : 0, "open_dev_priced")) {
+ iocost_model__destroy(skel);
+ snprintf(qbuf, sizeof(qbuf), "%s enable=0", dev);
+ (void)configfs_write("/sys/fs/cgroup/io.cost.qos", qbuf);
+ nullb_remove(IOCOST_NULLB);
+ cleanup_cgroup_environment();
+ return;
+ }
+ ASSERT_OK(read_cost_usage("iocost_pairing",
+ dev, &u1), "cost_usage_pre");
+ for (i = 0; i < 500; i++)
+ (void)read(devfd, buf, sizeof(buf));
+ close(devfd);
+ ASSERT_GT(skel->bss->calc_costs, 0, "model_priced");
+ /*
+ * the usage stat is flushed by the iocost timer, give
+ * the current period a chance to end before reading
+ */
+ sleep(3);
+ ASSERT_OK(read_cost_usage("iocost_pairing",
+ dev, &u2), "cost_usage_post");
+ ASSERT_GT(u2, u1, "cost_charged");
+ ASSERT_GT(skel->bss->ioc_ticks, 0, "tick_seen");
+
+ /*
+ * a second device is independent: enabling it shows
+ * the builtin model, not the one attached to the
+ * first
+ */
+ if (ASSERT_OK(nullb_create(IOCOST_NULLB2, dev2,
+ sizeof(dev2)), "nullb_create2")) {
+ snprintf(qbuf, sizeof(qbuf),
+ "%s enable=1 ctrl=auto", dev2);
+ ASSERT_OK(configfs_write(
+ "/sys/fs/cgroup/io.cost.qos", qbuf),
+ "qos_nullb2");
+ err = readback_model(dev2, model, sizeof(model));
+ if (ASSERT_OK(err, "readback_nullb2"))
+ ASSERT_STREQ(model, "linear",
+ "model_linear_nullb2");
+ snprintf(qbuf, sizeof(qbuf), "%s enable=0", dev2);
+ ASSERT_OK(configfs_write(
+ "/sys/fs/cgroup/io.cost.qos", qbuf),
+ "qos_nullb2_off");
+ }
+ nullb_remove(IOCOST_NULLB2);
+
+ /*
+ * attachment and model selection are independent:
+ * ctrl=bpf is never accepted, model=linear switches
+ * back to the builtin without detaching, model=bpf
+ * switches to the attached model again
+ */
+ err = write_cost_model(dev, "ctrl=bpf");
+ ASSERT_ERR(err, "ctrl_bpf_rejected");
+
+ err = write_cost_model(dev, "model=linear");
+ ASSERT_OK(err, "model_linear_write");
+ err = readback_model(dev, model, sizeof(model));
+ if (ASSERT_OK(err, "readback_linear"))
+ ASSERT_STREQ(model, "linear", "model_linear");
+
+ /* still attached: the switch must not free the slot */
+ second = iocost_model__open();
+ if (ASSERT_OK_PTR(second, "second_open2")) {
+ second->struct_ops.iocost_2x->dev =
+ (__u32)makedev(maj, min);
+ err = iocost_model__load(second);
+ if (ASSERT_OK(err, "second_load2")) {
+ struct bpf_link *l2;
+
+ l2 = bpf_map__attach_struct_ops(
+ second->maps.iocost_2x);
+ if (!ASSERT_ERR_PTR(l2, "second_ebusy2"))
+ bpf_link__destroy(l2);
+ else
+ ASSERT_EQ(libbpf_get_error(l2), -EBUSY,
+ "second_ebusy2_errno");
+ }
+ iocost_model__destroy(second);
+ }
+
+ /* still attached: switching back to the model works */
+ err = write_cost_model(dev, "model=bpf");
+ ASSERT_OK(err, "model_bpf_write");
+ err = readback_model(dev, model, sizeof(model));
+ if (ASSERT_OK(err, "readback_bpf"))
+ ASSERT_STREQ(model, "bpf", "model_bpf");
+
+ bpf_link__destroy(skel->links.iocost_2x);
+ skel->links.iocost_2x = NULL;
+
+ /* after detach, model=bpf fails: nothing is attached */
+ err = write_cost_model(dev, "model=bpf");
+ ASSERT_ERR(err, "model_bpf_after_detach");
+
+ err = readback_model(dev, model, sizeof(model));
+ if (ASSERT_OK(err, "readback_after_detach"))
+ ASSERT_STREQ(model, "linear", "model_linear");
+
+ }
+
+ iocost_model__destroy(skel);
+ snprintf(qbuf, sizeof(qbuf), "%s enable=0", dev);
+ (void)configfs_write("/sys/fs/cgroup/io.cost.qos", qbuf);
+ nullb_remove(IOCOST_NULLB);
+ cleanup_cgroup_environment();
+}
+/*
+ * Same check for the multi-stream example model. Only one model can be
+ * attached to a device at a time; each test creates its own device and
+ * attaches and detaches, so they are serial.
+ */
+void serial_test_iocost_model_streams(void)
+{
+ struct iocost_ms *skel;
+ unsigned int maj, min;
+ int devfd, i, err, cg;
+ char model[32], dev[32], path[64], qbuf[160];
+
+ if (!iocost_test_env())
+ return;
+ if (!ASSERT_OK(nullb_create(IOCOST_NULLB, dev, sizeof(dev)),
+ "nullb_create"))
+ return;
+ if (sscanf(dev, "%u:%u", &maj, &min) != 2) {
+ ASSERT_TRUE(false, "dev_parse");
+ nullb_remove(IOCOST_NULLB);
+ return;
+ }
+
+ snprintf(path, sizeof(path), "/dev/%s", IOCOST_NULLB);
+
+ skel = iocost_ms__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open")) {
+ nullb_remove(IOCOST_NULLB);
+ return;
+ }
+
+ skel->struct_ops.iocost_ms->dev = (__u32)makedev(maj, min);
+
+ err = iocost_ms__load(skel);
+ if (!ASSERT_OK(err, "skel_load")) {
+ cleanup_cgroup_environment();
+ iocost_ms__destroy(skel);
+ nullb_remove(IOCOST_NULLB);
+ return;
+ }
+
+ err = iocost_ms__attach(skel);
+ if (ASSERT_OK(err, "attach")) {
+ err = readback_model(dev, model, sizeof(model));
+ if (ASSERT_OK(err, "readback"))
+ ASSERT_STREQ(model, "bpf", "model_bpf");
+
+ /*
+ * drive IO through the model: enable the controller, price
+ * from a cgroup, and check the model actually charged them
+ */
+ cg = cgroup_setup_and_join("iocost_ms");
+ if (!ASSERT_GE(cg, 0, "setup_cg_ms")) {
+ iocost_ms__destroy(skel);
+ nullb_remove(IOCOST_NULLB);
+ cleanup_cgroup_environment();
+ return;
+ }
+ close(cg);
+ snprintf(qbuf, sizeof(qbuf), "%s 1000", dev);
+ ASSERT_OK(write_cgroup_file("iocost_ms", "io.weight", qbuf),
+ "weight_ms");
+ snprintf(qbuf, sizeof(qbuf),
+ "%s enable=1 ctrl=user min=100.00 max=100.00", dev);
+ ASSERT_OK(configfs_write("/sys/fs/cgroup/io.cost.qos", qbuf),
+ "qos_enable");
+ devfd = open(path, O_RDONLY);
+ if (ASSERT_OK(devfd < 0 ? -errno : 0, "open_dev_ms")) {
+ char iobuf[4096];
+ unsigned long long u1 = 0, u2 = 0;
+
+ ASSERT_OK(read_cost_usage("iocost_ms",
+ dev, &u1), "cost_usage_pre");
+ for (i = 0; i < 500; i++)
+ (void)read(devfd, iobuf, sizeof(iobuf));
+ close(devfd);
+ sleep(3);
+ ASSERT_OK(read_cost_usage("iocost_ms",
+ dev, &u2), "cost_usage_post");
+ ASSERT_GT(u2, u1, "ms_cost_charged");
+ }
+ ASSERT_OK(join_root_cgroup(), "procs_back");
+ snprintf(qbuf, sizeof(qbuf), "%s enable=0", dev);
+ (void)configfs_write("/sys/fs/cgroup/io.cost.qos", qbuf);
+ bpf_link__destroy(skel->links.iocost_ms);
+ skel->links.iocost_ms = NULL;
+
+ err = readback_model(dev, model, sizeof(model));
+ if (ASSERT_OK(err, "readback_after_detach"))
+ ASSERT_STREQ(model, "linear", "model_linear");
+ }
+
+ iocost_ms__destroy(skel);
+ nullb_remove(IOCOST_NULLB);
+ cleanup_cgroup_environment();
+}
+
+/*
+ * Removal of the device while a model is attached: the test builds
+ * its own null_blk instance through configfs like the other tests,
+ * attaches to it, then removes the device out from under the model.
+ * The ejection must drop the device from the cgroup interface,
+ * down, and a recreated device must be attachable again.
+ */
+void serial_test_iocost_model_removal(void)
+{
+ struct iocost_model *skel, *skel2;
+ unsigned int maj, min;
+ char dev[32], model[32];
+ int err;
+
+ if (!iocost_test_env())
+ return;
+
+ skel = iocost_model__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open"))
+ return;
+
+ err = nullb_create("iocost_removal", dev, sizeof(dev));
+ if (!ASSERT_OK(err, "nullb_create"))
+ goto out;
+ if (sscanf(dev, "%u:%u", &maj, &min) != 2) {
+ ASSERT_TRUE(false, "dev_parse");
+ goto out;
+ }
+
+ skel->struct_ops.iocost_2x->dev = (__u32)makedev(maj, min);
+ err = iocost_model__load(skel);
+ if (!ASSERT_OK(err, "skel_load"))
+ goto out;
+
+ err = iocost_model__attach(skel);
+ if (!ASSERT_OK(err, "attach"))
+ goto out;
+
+ err = readback_model(dev, model, sizeof(model));
+ if (ASSERT_OK(err, "readback"))
+ ASSERT_STREQ(model, "bpf", "model_bpf");
+
+ /* remove the device out from under the attached model */
+ if (!ASSERT_OK(nullb_remove("iocost_removal"), "nullb_remove"))
+ goto detach;
+
+ /* the ejection drops the device from the cgroup files */
+ err = readback_model(dev, model, sizeof(model));
+ ASSERT_ERR(err, "readback_after_removal");
+
+ err = write_cost_model(dev, "model=bpf");
+ ASSERT_ERR(err, "model_bpf_after_removal");
+
+detach:
+ /*
+ * detaching after the device is gone must be a clean no-op;
+ * this exercises the eject-then-detach ordering in the kernel
+ */
+ bpf_link__destroy(skel->links.iocost_2x);
+ skel->links.iocost_2x = NULL;
+
+ /* the same attach and detach against a recreated device */
+ skel2 = iocost_model__open();
+ if (!ASSERT_OK_PTR(skel2, "skel2_open"))
+ goto out;
+
+ err = nullb_create("iocost_removal", dev, sizeof(dev));
+ if (!ASSERT_OK(err, "nullb_create_again"))
+ goto out2;
+ if (sscanf(dev, "%u:%u", &maj, &min) != 2) {
+ ASSERT_TRUE(false, "dev_parse_again");
+ goto out2;
+ }
+
+ skel2->struct_ops.iocost_2x->dev = (__u32)makedev(maj, min);
+ err = iocost_model__load(skel2);
+ if (!ASSERT_OK(err, "skel2_load"))
+ goto out2;
+
+ err = iocost_model__attach(skel2);
+ if (!ASSERT_OK(err, "attach_again"))
+ goto out2;
+
+ err = readback_model(dev, model, sizeof(model));
+ if (ASSERT_OK(err, "readback_again"))
+ ASSERT_STREQ(model, "bpf", "model_bpf_again");
+
+ bpf_link__destroy(skel2->links.iocost_2x);
+ skel2->links.iocost_2x = NULL;
+
+ err = readback_model(dev, model, sizeof(model));
+ if (ASSERT_OK(err, "readback_after_detach_again"))
+ ASSERT_STREQ(model, "linear", "model_linear_again");
+
+out2:
+ iocost_model__destroy(skel2);
+out:
+ nullb_remove("iocost_removal");
+ 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 000000000000..a16ff9a98ad0
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/iocost_model.c
@@ -0,0 +1,184 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 KylinSoft Corporation. */
+/*
+ * Example iocost cost model: the builtin linear HDD formula with all costs
+ * doubled, for one device given by the dev member of the struct_ops.
+ *
+ * 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. On a rotational device still on ctrl=auto, a device with
+ * this model attached charges twice the builtin model under the same
+ * read/write workload (the builtin prices flushes as zero and adds a fixed
+ * flush term to data-bearing preflushes and to FUA writes on devices without
+ * native FUA support, which this model omits; this model prices flushes as
+ * one-page writes), so the doubled cost is a direct check
+ * that accounting goes through the BPF path. On a non-rotational device, or
+ * one with user-pinned coefficients, the ratio to the builtin model is
+ * arbitrary.
+ *
+ * The cursor handling follows the builtin: a zero cursor means "no previous
+ * IO", and the cursor is advanced for the bios the builtin prices
+ * (READ/WRITE/ZONE_APPEND with a non-zero size), so flushes and discards leave
+ * it alone and long merged streams do not drift past the 16MB seek threshold.
+ * One known deviation, mispricing only: the builtin does not move its cursor
+ * for an IO whose cgroup fails to activate; this model does not see the
+ * activation result. Another, narrower one: with "flushiops"
+ * configured, the builtin also advances its cursor for a
+ * preflushed discard, which this model still ignores.
+ *
+ * The model implements the full linear formula itself, including flushes:
+ * there is no fallback to the builtin model, which prices a dataless
+ * WRITE|REQ_PREFLUSH as zero unless flushiops is configured; this model prices
+ * it as a one-page sequential write. What a flush actually costs depends on
+ * the IOs before it, which a static formula cannot express - pricing it as a
+ * small constant is a crude penalty for unnecessarily frequent flushes, and a
+ * stateful model would be the way to do better. Discards and other unpriced
+ * operations pass through at zero cost, like the builtin.
+ */
+
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+
+/*
+ * VTIME_PER_SEC, IOC_PAGE_SIZE/SHIFT, IOC_SECT_TO_PAGE_SHIFT,
+ * LCOEF_RANDIO_PAGES (the 16MB seek threshold) and IOCOST_COST_F_MERGE
+ * come from vmlinux.h (BTF constants; the enum64 ones need pahole
+ * >= 1.23 to be emitted)
+ */
+#define IOCOST_REQ_OP_MASK 0xff /* REQ_OP_MASK, not in BTF */
+
+/*
+ * 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)
+
+/*
+ * per-cgroup cursor storage: keyed by the cgroup and freed with it,
+ * so a cgroup that comes back starts with no cursor. A struct_ops
+ * map attaches to at most one device (a second attach to the same
+ * device fails with -EBUSY), and this example uses one map, so
+ * per-cgroup keys are per (cgroup, device) in effect
+ */
+struct {
+ __uint(type, BPF_MAP_TYPE_CGRP_STORAGE);
+ __uint(map_flags, BPF_F_NO_PREALLOC);
+ __type(key, int);
+ __type(value, __u64);
+} cursor_store SEC(".maps");
+
+/*
+ * incremented by calc_cost(): the test asserts pricing went
+ * through the model while it is attached and the controller is
+ * enabled
+ */
+__u64 calc_costs;
+
+/* counts iocost_ioc_tick emissions observed from BPF side */
+__u64 ioc_ticks;
+
+SEC("struct_ops")
+u64 BPF_PROG(iocost_2x_calc_cost, struct bio *bio, u64 model_flags)
+{
+ u64 opf = bio->bi_opf, nbytes = bio->bi_iter.bi_size;
+ u64 sector = bio->bi_iter.bi_sector;
+ struct blkcg *blkcg = bio->bi_blkg->blkcg;
+ u64 pages, seek_pages = 0, base, coef_page, randio, cost;
+ __u64 *cursor, cur;
+ int priced;
+
+ calc_costs++;
+
+ /* builtin truncates: max(sectors >> IOC_SECT_TO_PAGE_SHIFT, 1) */
+ pages = nbytes >> IOC_PAGE_SHIFT;
+ if (!pages)
+ pages = 1;
+
+ if ((opf & IOCOST_REQ_OP_MASK) == REQ_OP_READ) {
+ base = RSEQIO; coef_page = RPAGE; randio = RRANDIO;
+ } else if ((opf & IOCOST_REQ_OP_MASK) == REQ_OP_WRITE ||
+ (opf & IOCOST_REQ_OP_MASK) == REQ_OP_ZONE_APPEND) {
+ base = WSEQIO; coef_page = WPAGE; randio = WRANDIO;
+ } else {
+ /*
+ * the builtin prices other operations at zero and
+ * leaves them unthrottled; pass them through the same
+ * way instead of inventing costs
+ */
+ return 0;
+ }
+
+ /* mirror the builtin cursor semantics described above */
+ priced = (opf & IOCOST_REQ_OP_MASK) == REQ_OP_READ ||
+ (opf & IOCOST_REQ_OP_MASK) == REQ_OP_WRITE ||
+ (opf & IOCOST_REQ_OP_MASK) == REQ_OP_ZONE_APPEND;
+ cursor = bpf_cgrp_storage_get(&cursor_store,
+ blkcg->css.cgroup, NULL,
+ BPF_LOCAL_STORAGE_GET_F_CREATE);
+ if (!cursor) {
+ if (model_flags & IOCOST_COST_F_MERGE)
+ base = 0;
+ return 2 * (base + pages * coef_page);
+ }
+ cur = *cursor;
+ /*
+ * a size-zero IO never moves the cursor, so it must not be
+ * judged against it either; the builtin skips it entirely,
+ * this model prices it as a one-page sequential write
+ */
+ if (cur && priced && nbytes) {
+ seek_pages = sector > cur ? sector - cur
+ : cur - sector;
+ seek_pages >>= IOC_SECT_TO_PAGE_SHIFT;
+ if (seek_pages > LCOEF_RANDIO_PAGES)
+ base = randio;
+ }
+ /*
+ * the builtin moves the cursor unconditionally on submit, but
+ * on a merge only for a back merge into the request ending at
+ * it; a merged bio starts right at the cursor exactly then,
+ * and a zero cursor means no history so it never matches
+ */
+ if (priced && nbytes &&
+ (!(model_flags & IOCOST_COST_F_MERGE) || (cur && cur == sector)))
+ *cursor = sector + (nbytes >> 9);
+
+ if (model_flags & IOCOST_COST_F_MERGE)
+ base = 0;
+
+ cost = 2 * (base + pages * coef_page);
+ return cost;
+}
+
+/* consumes the new iocost_ioc_tick tracepoint from BPF side */
+SEC("tp_btf/iocost_ioc_tick")
+__u64 BPF_PROG(iocost_2x_tick_count, struct ioc *ioc,
+ const struct ioc_now *now, int nr_active, u64 usage_us_sum)
+{
+ ioc_ticks++;
+ return 0;
+}
+
+SEC(".struct_ops.link") /* detach on close */
+struct iocost_model_ops iocost_2x = {
+ .read_vtime_per_page = 2 * RPAGE,
+ .write_vtime_per_page = 2 * WPAGE,
+ .calc_cost = (void *)iocost_2x_calc_cost,
+};
+
+char LICENSE[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/progs/iocost_ms.c b/tools/testing/selftests/bpf/progs/iocost_ms.c
new file mode 100644
index 000000000000..fe7be4eefb7c
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/iocost_ms.c
@@ -0,0 +1,187 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2026 KylinSoft Corporation. */
+/*
+ * Example multi-stream sequentiality detection cost model.
+ *
+ * The builtin model keeps a single cursor per cgroup, so two interleaved
+ * sequential readers in one cgroup are all priced random (measured 89x
+ * overcharge, 12.9x throughput collapse), while random IO inside a hot window
+ * smaller than the 16MB seek threshold is priced sequential (measured 107x
+ * undercharge). This model replaces the single cursor with a per-cgroup table
+ * of stream slots: an IO is sequential iff its sector matches the expected
+ * next sector of any tracked stream. Interleaved streams keep their own slots,
+ * and windowed random IO rarely matches a moving expectation.
+ *
+ * Stream state lives in a CGRP_STORAGE map, so it is created and freed with
+ * the cgroup. The model implements the builtin linear formula itself; unlike
+ * the builtin it has no flush term, so preflushed IOs are priced as plain
+ * writes (mispricing only, the test never issues flushes). A
+ * struct_ops map attaches to at most one device (a second attach
+ * to the same device fails with -EBUSY), and this example uses one
+ * map, so the per-cgroup keys are per (cgroup, device) in effect.
+ * Two more known deviations:
+ * a dataless READ or WRITE is priced as a one-page access (the
+ * builtin prices it zero), and discards do not advance the
+ * stream expectation while the builtin cursor moves for them.
+ */
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+
+/*
+ * VTIME_PER_SEC, IOC_PAGE_SIZE/SHIFT, IOC_SECT_TO_PAGE_SHIFT and
+ * IOCOST_COST_F_MERGE come from vmlinux.h (BTF enum constants)
+ */
+#define IOCOST_REQ_OP_MASK 0xff /* REQ_OP_MASK, not in BTF */
+
+/*
+ * 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)
+
+#define NSLOTS 4
+
+struct streams {
+ __u64 expected[NSLOTS]; /* next expected sector, per stream */
+ __u64 stamp[NSLOTS]; /* LRU stamp, 0 = empty */
+};
+
+/*
+ * per-cgroup stream table: keyed by the cgroup, freed with it
+ */
+struct {
+ __uint(type, BPF_MAP_TYPE_CGRP_STORAGE);
+ __uint(map_flags, BPF_F_NO_PREALLOC);
+ __type(key, int);
+ __type(value, struct streams);
+} stream_tab SEC(".maps");
+
+SEC("struct_ops")
+u64 BPF_PROG(iocost_ms_calc_cost, struct bio *bio, u64 model_flags)
+{
+ u64 opf = bio->bi_opf, nbytes = bio->bi_iter.bi_size;
+ u64 sector = bio->bi_iter.bi_sector;
+ struct blkcg *blkcg = bio->bi_blkg->blkcg;
+ struct streams *s;
+ u64 pages, base, coef_page, randio, advance, now;
+ u32 i, victim = 0, found = 0xFFFFFFFF;
+
+ if ((opf & IOCOST_REQ_OP_MASK) == REQ_OP_READ) {
+ base = RSEQIO; coef_page = RPAGE; randio = RRANDIO;
+ } else if ((opf & IOCOST_REQ_OP_MASK) == REQ_OP_WRITE ||
+ (opf & IOCOST_REQ_OP_MASK) == REQ_OP_ZONE_APPEND) {
+ base = WSEQIO; coef_page = WPAGE; randio = WRANDIO;
+ } else {
+ /*
+ * the builtin prices other operations at zero and
+ * leaves them unthrottled; pass them through the same
+ * way instead of inventing costs
+ */
+ return 0;
+ }
+ advance = nbytes >> 9; /* sectors, truncated like the builtin */
+
+ /*
+ * only bios the builtin prices participate in stream tracking;
+ * the early return below skips the merge discount, which only
+ * applies to tracked data bios
+ */
+ if (!(((opf & IOCOST_REQ_OP_MASK) == REQ_OP_READ ||
+ (opf & IOCOST_REQ_OP_MASK) == REQ_OP_WRITE ||
+ (opf & IOCOST_REQ_OP_MASK) == REQ_OP_ZONE_APPEND) && nbytes)) {
+ pages = nbytes >> IOC_PAGE_SHIFT;
+ if (!pages)
+ pages = 1;
+ return base + pages * coef_page;
+ }
+
+ s = bpf_cgrp_storage_get(&stream_tab, blkcg->css.cgroup, NULL,
+ BPF_LOCAL_STORAGE_GET_F_CREATE);
+ if (!s) {
+ /*
+ * no storage: price per page, truncating like the
+ * builtin; merges skip the base like the main path
+ */
+ if (model_flags & IOCOST_COST_F_MERGE)
+ base = 0;
+ pages = nbytes >> IOC_PAGE_SHIFT;
+ if (!pages)
+ pages = 1;
+ return base + pages * coef_page;
+ }
+
+ /*
+ * Slot access is lockless, mirroring the builtin single-cursor
+ * update in ioc_rqos_throttle(): concurrent CPUs submitting for
+ * the same cgroup can race on slot updates; mispricing is
+ * bounded and acceptable for an example model.
+ */
+ now = bpf_ktime_get_ns();
+ for (i = 0; i < NSLOTS; i++) {
+ if (s->expected[i] == sector && s->stamp[i]) {
+ found = i;
+ break;
+ }
+ }
+ if (found != 0xFFFFFFFF) {
+ /* sequential: keep the seq base from the op branch */
+ s->expected[found] = sector + advance;
+ s->stamp[found] = now;
+ } else {
+ /*
+ * a merge which matches no slot is not a new stream:
+ * replacing a slot could evict a live stream, so merged
+ * bios are priced without touching the table (and skip
+ * the base cost below)
+ */
+ if (!(model_flags & IOCOST_COST_F_MERGE)) {
+ base = randio;
+ for (i = 1; i < NSLOTS; i++) {
+ if (s->stamp[i] < s->stamp[victim])
+ victim = i;
+ }
+ s->expected[victim] = sector + advance;
+ s->stamp[victim] = now;
+ }
+ }
+
+ /* builtin truncates: max(sectors >> IOC_SECT_TO_PAGE_SHIFT, 1) */
+ pages = nbytes >> IOC_PAGE_SHIFT;
+ if (!pages)
+ pages = 1;
+ if (model_flags & IOCOST_COST_F_MERGE) {
+ /*
+ * merged bios skip the base cost but still advance
+ * the stream position above, so a merge at the
+ * expected sector does not make the following new IO
+ * look random
+ */
+ base = 0;
+ }
+
+ return base + pages * coef_page;
+}
+
+SEC(".struct_ops.link") /* detach on close */
+struct iocost_model_ops iocost_ms = {
+ .read_vtime_per_page = RPAGE,
+ .write_vtime_per_page = WPAGE,
+ .calc_cost = (void *)iocost_ms_calc_cost,
+};
+
+char LICENSE[] SEC("license") = "GPL";
--
2.53.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [RFC PATCH v12 4/4] docs: cgroup-v2: document the iocost BPF cost model attachment
2026-11-03 13:26 [RFC PATCH v12 0/4] blk-iocost: BPF struct_ops cost model Tao Cui
` (2 preceding siblings ...)
2026-11-03 13:26 ` [RFC PATCH v12 3/4] selftests/bpf: add iocost cost model test Tao Cui
@ 2026-11-03 13:26 ` Tao Cui
3 siblings, 0 replies; 8+ messages in thread
From: Tao Cui @ 2026-11-03 13:26 UTC (permalink / raw)
To: tj, josef, axboe, alexei.starovoitov
Cc: cgroups, linux-block, linux-kernel, bpf, andrii, eddyz87, ast,
daniel, linux-kselftest, cui.tao, cuitao, ameryhung
From: Tao Cui <cuitao@kylinos.cn>
Document the BPF cost model attachment in the io.cost.model section
of the cgroup v2 documentation: attaching an iocost_model_ops
struct_ops to a device by its major:minor, the model=bpf readback
while attached (ctrl keeps describing the coefficients), the builtin
model coming back on detach, and ctrl writes never selecting a model.
Suggested-by: Tejun Heo <tj@kernel.org>
Signed-off-by: Tao Cui <cuitao@kylinos.cn>
---
Documentation/admin-guide/cgroup-v2.rst | 45 ++++++++++++++++++++-----
1 file changed, 37 insertions(+), 8 deletions(-)
diff --git a/Documentation/admin-guide/cgroup-v2.rst b/Documentation/admin-guide/cgroup-v2.rst
index 9e3c67b02ae2..0965645b1286 100644
--- a/Documentation/admin-guide/cgroup-v2.rst
+++ b/Documentation/admin-guide/cgroup-v2.rst
@@ -2056,8 +2056,9 @@ IO Interface Files
currently implements "io.weight" proportional control. Lines
are keyed by $MAJ:$MIN device numbers and not ordered. The
line for a given device is populated on the first write for
- the device on "io.cost.qos" or "io.cost.model". The following
- nested keys are defined.
+ the device on "io.cost.qos" or "io.cost.model", or when a BPF
+ cost model is attached to it. The following nested keys are
+ defined.
====== =====================================
enable Weight-based control enable
@@ -2111,13 +2112,40 @@ IO Interface Files
implements "io.weight" proportional control. Lines are keyed
by $MAJ:$MIN device numbers and not ordered. The line for a
given device is populated on the first write for the device on
- "io.cost.qos" or "io.cost.model". The following nested keys
- are defined.
+ "io.cost.qos" or "io.cost.model", or when a BPF cost model is
+ attached to it. The following nested keys are defined.
- ===== ================================
+ ===== ==========================================
ctrl "auto" or "user"
- model The cost model in use - "linear"
- ===== ================================
+ model The cost model in use - "linear" or "bpf"
+ ===== ==========================================
+
+ When BPF cost model support is available, a BPF cost model
+ can be bound to a device by attaching an
+ "iocost_model_ops" struct_ops carrying the whole disk's
+ major:minor as encoded by makedev(3) in its "dev" member (a
+ partition's major:minor is rejected). Attaching creates the
+ controller if needed, like
+ an io.cost.model write does, switches the device's pricing to
+ the model and populates the device's line in this file and in
+ "io.cost.qos" if it did not exist yet. Attaching fails with
+ -EBUSY if the device already has a model attached. The
+ attached model prices IOs only while the controller is
+ enabled through "io.cost.qos"; attaching does not enable it.
+ IOs issued directly by the root cgroup bypass the model.
+
+ While a model is attached, "model" selects between it and the
+ builtin model: "model=bpf" switches to the attached model and
+ "model=linear" switches back to the builtin model, and neither
+ detaches the struct_ops.
+
+ Detaching removes the model, after which "model=bpf"
+ fails and the device prices with the builtin model again.
+ Device removal ejects the attached model as well, while the
+ struct_ops link remains until userspace destroys it. "ctrl"
+ accepts only "auto" and "user" and never selects a model. It
+ keeps describing the builtin coefficients, which take effect
+ again when switched back.
When "ctrl" is "auto", the kernel may change all parameters
dynamically. When "ctrl" is set to "user" or any other
@@ -2125,7 +2153,8 @@ IO Interface Files
automatic changes are disabled.
When "model" is "linear", the following model parameters are
- defined.
+ defined. They remain readable and writable while "model" is
+ "bpf".
============= ========================================
[r|w]bps The maximum sequential IO throughput
--
2.53.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [RFC PATCH v12 2/4] blk-iocost: add iocost_ioc_tick tracepoint for per-period device summary
2026-11-03 13:26 ` [RFC PATCH v12 2/4] blk-iocost: add iocost_ioc_tick tracepoint for per-period device summary Tao Cui
@ 2026-11-03 14:07 ` bot+bpf-ci
0 siblings, 0 replies; 8+ messages in thread
From: bot+bpf-ci @ 2026-11-03 14:07 UTC (permalink / raw)
To: cui.tao, tj, josef, axboe, alexei.starovoitov
Cc: cgroups, linux-block, linux-kernel, bpf, andrii, eddyz87, ast,
daniel, linux-kselftest, cui.tao, cuitao, ameryhung, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2341 bytes --]
> blk-iocost: add iocost_ioc_tick tracepoint for per-period device summary
TL;DR: In iocost_ioc_tick, the tracepoint call sits outside its enabled()
check, so a probe attached in between may log a bogus active count of 0, which
consumers could read as dormancy.
> diff --git a/block/blk-iocost.c b/block/blk-iocost.c
> --- a/block/blk-iocost.c
> +++ b/block/blk-iocost.c
[ ... ]
> @@ -2478,6 +2479,22 @@ static void ioc_timer_fn(struct timer_list *timer)
>
> ioc->busy_level = clamp(ioc->busy_level, -1000, 1000);
>
> + /*
> + * busy_level, cur_period, nr_active and usage_us_sum are complete
> + * here. usage is wall time over wall time, summed over iocgs, and
> + * is not capped at 100. vrate and period_us reflect the current
> + * controller state, which an io.cost.qos write may have changed
> + * mid-period. Emit before the refresh and the period transition.
> + */
> + if (trace_iocost_ioc_tick_enabled()) {
> + struct ioc_gq *active_iocg;
> +
> + list_for_each_entry(active_iocg, &ioc->active_iocgs,
> + active_list)
> + nr_active++;
> + }
> + trace_iocost_ioc_tick(ioc, &now, nr_active, usage_us_sum);
> +
Can this record a bogus nr_active of 0?
trace_iocost_ioc_tick() is called outside the
trace_iocost_ioc_tick_enabled() block, so the two static key tests are
independent. If a probe is attached on another CPU between them, the first
test sees the key disabled and skips the count, while the tracepoint itself
then fires with nr_active still 0 even though ioc->active_iocgs is non-empty.
Documentation/trace/tracepoints.rst says the tracepoint call should always
be within the block of the if (trace_<tracepoint>_enabled()) check to avoid
this race.
Since the commit message describes active=0 as the marker for the transition
to dormancy, a consumer could read this spurious record as that transition,
alongside a non-zero usage value.
Would moving the trace_iocost_ioc_tick() call inside the enabled block
work? Alternatively, nr_active could be counted in the existing
list_for_each_entry() walk over ioc->active_iocgs earlier in
ioc_timer_fn().
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/41646836440
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [RFC PATCH v12 3/4] selftests/bpf: add iocost cost model test
2026-11-03 13:26 ` [RFC PATCH v12 3/4] selftests/bpf: add iocost cost model test Tao Cui
@ 2026-11-03 14:07 ` bot+bpf-ci
0 siblings, 0 replies; 8+ messages in thread
From: bot+bpf-ci @ 2026-11-03 14:07 UTC (permalink / raw)
To: cui.tao, tj, josef, axboe, alexei.starovoitov
Cc: cgroups, linux-block, linux-kernel, bpf, andrii, eddyz87, ast,
daniel, linux-kselftest, cui.tao, cuitao, ameryhung, ast, andrii,
daniel, martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2747 bytes --]
> selftests/bpf: add iocost cost model test
TL;DR: Selftest cgroup names "iocost_pairing" and "iocost_ms" lack a leading
'/', so the cgroups are likely created outside the work directory and not
removed by cleanup_cgroup_environment(), leaving empty cgroups on the host.
> 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
[ ... ]
> + /*
> + * cgroup_setup_and_join() creates the priced-IO cgroup with the
> + * available controllers enabled, so the cgroup has
> + * io.weight/io.stat and the iocg activates.
> + * cleanup_cgroup_environment() removes it from every exit path
> + * below. io.cost.qos and io.cost.model are root-only cgroupfs
> + * files: they are written at the hierarchy root and reverted on
> + * exit.
> + */
> + cg = cgroup_setup_and_join("iocost_pairing");
[ ... ]
> + cg = cgroup_setup_and_join("iocost_ms");
Will these cgroups actually be removed by cleanup_cgroup_environment()?
The name has no leading '/', and format_cgroup_path() in cgroup_helpers.c
joins the pieces with no separator:
#define format_cgroup_path_pid(buf, path, pid) \
snprintf(buf, sizeof(buf), "%s%s%d%s", CGROUP_MOUNT_PATH, \
CGROUP_WORK_DIR, pid, path)
So cgroup_setup_and_join("iocost_pairing") ends up doing mkdir on
"/mnt/cgroup-test-work-dir<pid>iocost_pairing". That is a sibling of the
work directory directly under the cgroup2 root, not a child of it.
join_cgroup(), write_cgroup_file() and read_cgroup_file() build the same
path, so the test itself works.
But __cleanup_cgroup_environment() only walks the work directory:
format_cgroup_path(cgroup_workdir, "");
join_cgroup_from_top(CGROUP_MOUNT_PATH);
nftw(cgroup_workdir, nftwfunc, WALK_FD_LIMIT, FTW_DEPTH | FTW_MOUNT);
so the sibling directory is never visited. The empty cgroup remains on the
host as /sys/fs/cgroup/cgroup-test-work-dir<pid>iocost_pairing, which also
contradicts the comment above ("cleanup_cgroup_environment() removes it from
every exit path below"). serial_test_iocost_model_streams() has the same
problem with "iocost_ms", including the matching write_cgroup_file() and
read_cost_usage() calls.
Other callers that use a fixed name pass a leading slash, for example
"/skb_cgroup_test" and "/cgroup_iter_memcg_test". Would "/iocost_pairing"
and "/iocost_ms" at every place they are passed put the cgroups under the
work directory where the existing cleanup removes them?
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/41646836440
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [RFC PATCH v12 1/4] blk-iocost: add BPF struct_ops cost model support
2026-11-03 13:26 ` [RFC PATCH v12 1/4] blk-iocost: add BPF struct_ops cost model support Tao Cui
@ 2026-11-03 16:51 ` Alexei Starovoitov
0 siblings, 0 replies; 8+ messages in thread
From: Alexei Starovoitov @ 2026-11-03 16:51 UTC (permalink / raw)
To: Tao Cui, tj, josef, axboe
Cc: cgroups, linux-block, linux-kernel, bpf, andrii, eddyz87, daniel,
linux-kselftest, cuitao, ameryhung
On Tue, Nov 03, 2026 at 05:26 AM Tao Cui <cui.tao@linux.dev> wrote:
> @@ -3223,7 +3306,6 @@ static u64 ioc_weight_prfill(struct seq_file *sf, struct blkg_policy_data *pd,
> return 0;
> }
>
> -
> static int ioc_weight_show(struct seq_file *sf, void *v)
Unrelated change. Tejun pointed at this hunk in v11.
Overall makes sense to me:
Acked-by: Alexei Starovoitov <ast@kernel.org>
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-11-03 16:51 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-03 13:26 [RFC PATCH v12 0/4] blk-iocost: BPF struct_ops cost model Tao Cui
2026-11-03 13:26 ` [RFC PATCH v12 1/4] blk-iocost: add BPF struct_ops cost model support Tao Cui
2026-11-03 16:51 ` Alexei Starovoitov
2026-11-03 13:26 ` [RFC PATCH v12 2/4] blk-iocost: add iocost_ioc_tick tracepoint for per-period device summary Tao Cui
2026-11-03 14:07 ` bot+bpf-ci
2026-11-03 13:26 ` [RFC PATCH v12 3/4] selftests/bpf: add iocost cost model test Tao Cui
2026-11-03 14:07 ` bot+bpf-ci
2026-11-03 13:26 ` [RFC PATCH v12 4/4] docs: cgroup-v2: document the iocost BPF cost model attachment Tao Cui
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®