mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Tao Cui <cui.tao@linux.dev>
To: Tejun Heo <tj@kernel.org>
Cc: cui.tao@linux.dev, josef@toxicopanda.com, axboe@kernel.dk,
	cgroups@vger.kernel.org, linux-block@vger.kernel.org,
	linux-kernel@vger.kernel.org, bpf@vger.kernel.org,
	andrii@kernel.org, eddyz87@gmail.com, ast@kernel.org,
	daniel@iogearbox.net, linux-kselftest@vger.kernel.org,
	Tao Cui <cuitao@kylinos.cn>,
	ameryhung@gmail.com, alexei.starovoitov@gmail.com
Subject: Re: [RFC PATCH v10 1/4] blk-iocost: add BPF struct_ops cost model support
Date: Mon, 19 Oct 2026 17:27:40 +0800	[thread overview]
Message-ID: <24699b2b-4637-4872-8706-4f92193fa3ac@linux.dev> (raw)
In-Reply-To: <d6fb17fccb2338485821141c2ddc3d58@kernel.org>

Hello Tejun,

在 2026/10/17 01:49, Tejun Heo 写道:
> Hello, Tao.
> 
> On Sat, 03 Oct 2026 20:40:42 +0800, Tao Cui wrote:
>> +config BLK_CGROUP_IOCOST_BPF
>> +	bool "Enable BPF pluggable cost model support for the cost IO controller"
>> +	depends on BLK_CGROUP_IOCOST && BPF_SYSCALL && BPF_JIT && DEBUG_INFO_BTF
> 
> Does this need to be a separate config option? It doesn't add much code.
> Maybe just enable it when all the dependencies are available?
> bpf_tcp_ca.o, for example, is built whenever CONFIG_BPF_SYSCALL is set.
> 
Agreed, one less option is better. It's now a hidden def_bool with the
dependency list and no prompt, so it's enabled automatically whenever
the dependencies are available.

>> +	 to.  The struct_ops is attached per device, following the
>> +	 hid_bpf_ops model.
> 
> Can you drop the hid_bpf_ops references here, in the comments and in the
> selftest?
> 
Dropped from the Kconfig (which is gone with the hidden option), from
the file comment and from the selftest.

>> +bool blk_get_queue_rcu(struct request_queue *q)
>> +{
>> +	return refcount_inc_not_zero(&q->refs);
>> +}
> 
> I don't like the name. blk_get_queue() fails on a dying queue like
> css_tryget_online(), while this only fails once the refcount reaches zero
> like css_tryget(). Maybe blk_tryget_queue() or blk_queue_tryget()? I'd
> also put it in a separate patch, but it may not be needed at all, see
> below.
> 
It's gone entirely with the rework below; the map reference covers the
queue lifetime now, so the naming question resolves itself.

>> +static int bpf_iocost_validate(void *kdata)
>> +{
>> +	struct iocost_model_ops *ops = kdata;
>> +
>> +	return ops->calc_cost ? 0 : -EINVAL;
>> +}
> 
> read_vtime_per_page and write_vtime_per_page aren't checked. With zero,
> size_nsec in ioc_rqos_done() is zero for every READ and WRITE, and the
> whole on-queue time is judged against the latency targets. Maybe reject
> zero or fall back to the builtin coefficients?
> 
You're right. The zero case would silently change the done path. v11
rejects zero: a model must provide both coefficients explicitly.
Falling back to the builtin silently seemed worse than failing the
load.

>> +#include <linux/blk-iocost.h>
> 
> The header's name doesn't match its content, which is only the BPF model
> interface. Only block/blk-iocost.c and block/blk-iocost-bpf.c include it,
> so it doesn't need to be in include/linux either. Its
> !CONFIG_BLK_CGROUP_IOCOST_BPF stubs for ioc_bpf_attach() and
> ioc_bpf_detach() are never used, as their only caller is built under the
> same option.
> 
Agreed on all three counts. blk-iocost.h moved to block/ as a private
header holding the common definitions, the ops struct lives there now,
and the unused stubs are gone.

>> +static const struct iocost_model_ops *ioc_model_in_use(struct ioc *ioc)
> ...
>> +static const struct iocost_model_ops *ioc_attached_or_null(struct ioc *ioc)
> ...
>> +ioc_model_in_use_locked(struct ioc *ioc)
> 
> Aren't ioc_attached_or_null() and ioc_model_in_use_locked() misnomers?
> ioc_model_in_use() returns NULL while the builtin model is in use, and
> "_or_null" doesn't add anything.
> 
You're right, the names described the call sites rather than what the
functions return. ioc_attached_or_null() is now
ioc_attached_model_locked() and ioc_model_in_use() is
ioc_bpf_model_locked(); the hot-path dispatch moved into the
ioc_model_calc_cost()/ioc_model_size_cost() helpers, which take and
release rcu_read_lock() themselves.

>> +	rcu_read_lock();
>> +	model = ioc_model_in_use(iocg->ioc);
>> +	if (model) {
>> +		cost = model->calc_cost(bio,
>> +				is_merge ? IOCOST_COST_F_MERGE : 0);
> 
> Can the model cost calculation be wrapped in an inline helper in the
> header so that it compiles out completely when disabled?
> 
Done; it's an inline helper in the private header next to the ops
struct, and with the BPF support compiled out it degenerates to the
builtin dispatch and the whole path compiles away.

>> +	if (model && (req_op(rq) == REQ_OP_READ ||
>> +		      req_op(rq) == REQ_OP_WRITE)) {
>> +		unsigned int pages =
>> +			blk_rq_stats_sectors(rq) >> IOC_SECT_TO_PAGE_SHIFT;
> 
> The model-specific calculation probably belongs on the BPF model side
> instead of being open-coded here. Also, ioc_rqos_done(), the only caller,
> returns early for anything but READ and WRITE, so the op test isn't
> needed.
> 
Moved t

>> +static void ioc_bpf_eject(struct ioc *ioc)
> 
> The source file split doesn't make sense as is. ioc_bpf_eject() and
> ioc_bpf_walk_iocgs() are BPF-only but sit in blk-iocost.c because they
> need struct ioc and helpers which are private to it. If the BPF parts are
> to live in a separate file, can a prep patch move the common definitions
> into a private header so that everything can go where it belongs?
> 
Yes, the split grew organically and ended up misplaced. The series now
starts with a prep patch which moves the enums, struct ioc/ioc_gq and
the accessors into block/blk-iocost.h, and the whole BPF side lives in
blk-iocost-bpf.c now: attach, detach, the ejection and the callback
delivery.

>> +	ops = (struct iocost_model_ops *)rcu_dereference_protected(
>> +			ioc->attached, lockdep_is_held(&ioc->lock));
> 
> The const cast is still here.
> 
Sorry, missed that one. Gone with the rework; the recorded owner is a
plain pointer.

>> +	/*
>> +	 * ioc->attached and ioc->model are left set: the ioc is freed
>> +	 * right below, so clearing them buys nothing, and any
>> +	 * ioc_pd_free() which still runs finds the attachment and
>> +	 * delivers its iocg_free() through the struct_ops image grace
>> +	 * period instead of silently losing it.
>> +	 */
> 
> blkcg_deactivate_policy() has already freed and cleared the pd of every
> blkg on q->blkg_list, and nothing keeps the map alive once ops->q is
> cleared, so there is no such grace period.
> 
You're right, and thank you for the correction: that comment was just
wrong, there is no grace period there. The v11 removal path clears the
ioc pointers and drops the map reference, and the comment describes
that.

>> +	smp_store_release(&ops->q, NULL);
> 
> What's the smp_store_release() for? More on this below.
> 
>> +	rcu_read_lock();
>> +	model = ioc_attached_or_null(ioc);
>> +	if (model && model->iocg_init) {
>> +		model->iocg_init(blkg->blkcg, ioc->rqos.disk->queue);
>> +		iocg->bpf_iocg_inited = true;
>> +	}
>> +	rcu_read_unlock();
> 
> ioc_pd_init() isn't a hot path, and both of its callers hold queue_lock,
> which attach and detach also hold while changing ioc->attached. Can the
> synchronization be simplified?
> 
Simplified as you suggest. The callbacks now read ioc->attached
directly. ioc_pd_init() runs under queue_lock, the free path under
blkcg_mutex, and attach/detach update the pointer while holding the
corresponding lock.

>> +		if (iocg->bpf_iocg_inited) {
>> +			rcu_read_lock();
>> +			model = ioc_attached_or_null(ioc);
> 
> Ditto. This runs under blkcg_mutex, which attach and detach also hold. Is
> the lockless dance necessary?
> 
> The pairing also has holes. blkg_create() calls pd_init before
> radix_tree_insert(), and a blkg whose insert fails never reaches
> q->blkg_list. If model A is detached and model B attached before that blkg
> is freed, neither walk sees it and ioc_pd_free() delivers B's iocg_free()
> for a cgroup which only got A's iocg_init(). The attach walk also delivers
> iocg_init() to blkgs which blkg_destroy() already took offline but which
> stay on q->blkg_list until blkg_free_workfn().
> 
> blkg_create() calls pd_online_fn only after a successful insert and
> list_add(), blkg_destroy() and blkcg_policy_teardown_pds() call
> pd_offline_fn, and pd->online tracks the state, all under queue_lock.
> Would delivering iocg_init() from a new ioc_pd_online() and iocg_free()
> from ioc_pd_offline(), with the walks skipping blkgs whose pd->online is
> clear, work? That would pair the callbacks exactly without bpf_iocg_inited
> or the radix_tree_insert() exception.
> 
> The catch is that iocg_free() then runs at blkg destruction, and bios
> still holding the blkg can be priced afterwards, e.g. the ones
> throtl_pd_offline() flushes. Moving only iocg_init() to pd_online and
> keeping the flag for iocg_free() avoids that.
> 
You're right about the hole, and v11 follows that approach:
iocg_init() is delivered from a new ioc_pd_online(), which
blkg_create() calls only after the successful radix_tree_insert() and
list_add(), the walks skip blkgs whose pd->online is clear, and
iocg_free() keeps the delivered flag so it is not delivered while bios
can still be priced against the blkg.

>> +static void ioc_bpf_walk_iocgs(struct ioc *ioc,
>> +			       const struct iocost_model_ops *ops, bool init)
> 
> The name is a misnomer, this delivers the cgroup callbacks.
> 
Renamed to ioc_bpf_deliver_iocg_callbacks().

>> +	 * blkg_create() holds blkcg_mutex across ioc_pd_init() and the
>> +	 * list_add(), and blkg_free_workfn() holds blkcg_mutex across
> 
> On the bio path, blkg_create() runs from blkg_tryget_closest() under
> queue_lock only. The walk is correct because it also holds queue_lock, but
> this comment, the description and the cover letter still give the v8
> reasoning.
> 
Updated; the comment and the description now say the walk is correct
because it holds queue_lock, which is what the bio path uses.

>> +	 * the freeze is taken before rq_qos_mutex; taking the mutex
>> +	 * around the freeze instead cycles against the io.cost.qos
>> +	 * write path, which freezes with the mutex held.  Holding
> 
> ioc_qos_write() drops rq_qos_mutex before freezing. It's io.cost.model
> writes which freeze with the mutex held, so the cover letter's "under the
> same freeze and quiesce those writes go through" doesn't hold for the
> order either.
> 
That's right, the comment was wrong. It now says the attach and detach
order matches ioc_qos_write()'s, which is what the code does, and the
cover letter no longer claims the io.cost.model write ordering.

>> +	if (smp_load_acquire(&ops->q)) {
>> +		mutex_unlock(&q->rq_qos_mutex);
>> +		blk_mq_unfreeze_queue(q, memflags);
>> +		ret = -EBUSY;
>> +		goto put;
>> +	}
> 
> What's going on with the smp_load_acquire() here and the
> smp_store_release() above? The comment doesn't make clear what they're
> ordering against. Is this necessary?
> 
> They order the ops->link updates across different devices, which share no
> lock. However, ops->dev is set by init_member() while the map is still
> being initialized, so it can't change once .reg runs, and every attach of
> a map resolves the same dev_t. That should allow dropping bdev, q and link
> from the ops:
> 
> 1. Attach looks up ops->dev and checks disk_live() under rq_qos_mutex as
>    now, records the owning link in the ioc next to ioc->attached, takes a
>    map reference with bpf_struct_ops_get(), and puts the bdev before
>    returning.
> 
> 2. .unreg looks up ops->dev the same way and detaches only if
>    ioc->attached and the recorded link match. If the lookup fails, there
>    is nothing to do, as removal ejects the model on its own.
> 
> 3. Removal clears the ioc pointers and puts the map reference without
>    touching the ops.
> 
> The map reference keeps ioc->attached valid when .unreg can't find a
> device which del_gendisk() has unhashed but not yet torn down. This would
> remove blk_get_queue_rcu(), the const cast and the acquire/release
> pairing.
> 
Thank you for spelling this out; it's a much cleaner shape. Reworked
accordingly: attach looks up ops->dev and checks disk_live() under
rq_qos_mutex, records the owning link next to ioc->attached and takes
a reference on the map with bpf_struct_ops_get(), putting the bdev
before returning; .unreg looks the device up the same way and
detaches only when ioc->attached and the recorded link both match,
treating a failed lookup as nothing to do; the removal path clears
the ioc pointers and drops the map reference without touching the
ops. bdev, q and link are gone from the ops along with the
acquire/release pairing.

> Also, the error paths repeat the unlock sequences. Can this use guard()?
> 
Switched to guard() for the mutex.

>> +void ioc_bpf_detach(struct iocost_model_ops *ops, struct bpf_link *link,
>> +		   struct request_queue *q)
> ...
>> +	memflags = blk_mq_freeze_queue(q);
>> +	blk_mq_quiesce_queue(q);
>> +
>> +	mutex_lock(&q->rq_qos_mutex);
>> +
>> +	ioc = q_to_ioc(q);
> 
> .unreg holds only a queue reference here, which doesn't keep the tag set
> around. If the device is removed after blk_get_queue_rcu(), e.g.
> virtblk_remove() running del_gendisk() and blk_mq_free_tag_set() and then
> freeing vblk, blk_mq_quiesce_queue() reads the freed q->tag_set in
> blk_mq_wait_quiesce_done(). This hasn't been reproduced.
> 
> Can the ioc and the owning link be checked under rq_qos_mutex before
> quiescing? Removal ejects the model under rq_qos_mutex before the driver
> frees the tag set, so a matching attachment under the mutex means the tag
> set is still there.
> 
Good catch. v11 now performs an initial check under
rq_qos_mutex before freezing, then re-checks after freezing before
quiescing, so a successful second check guarantees that the tag set
is still alive.

>> +				/* staged and committed below, so a parse
>> +				 * error later in the same write leaves
>> +				 * the model selection untouched
>> +				 */
> 
> This and the comment in bpf_iocost_unreg() put text on the /* or */ line.
> Also, let's not use double spaces after periods. I know there are some in
> these files already, but let's not spread them further. Lines are also
> broken prematurely in many places, e.g. the calc_cost() call above fits in
> 79 columns and the ioc_cost_model_prfill() hunk rewraps existing lines
> which fit, although I'm not sure what the block tree's policy is here.
> 
The two comment openings are fixed in v11 already; the double spaces
and the premature line breaks are cleaned up throughout, and the
ioc_cost_model_prfill() hunk no longer rewraps lines that fit.

> Overall, this still looks sloppy. Can you go through the code and make
> sure that each part is necessary and sits in the right place?
> 
Fair. I went through the whole series again rather than just fixing
the individual comments. A number of comments and helpers had drifted
during the v8-v10 rework, so I cleaned those up as well. Hopefully
v11 is a better reflection of the code now.

Thanks again for the review.

Tao

> Thanks.
> 


  reply	other threads:[~2026-10-19  9:27 UTC|newest]

Thread overview: 18+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-03 12:40 [RFC PATCH v10 0/4] " Tao Cui
2026-10-03 12:40 ` [RFC PATCH v10 1/4] " Tao Cui
2026-10-03 13:21   ` bot+bpf-ci
2026-10-03 20:45   ` Alexei Starovoitov
2026-10-16 17:49   ` Tejun Heo
2026-10-19  9:27     ` Tao Cui [this message]
2026-10-03 12:40 ` [RFC PATCH v10 2/4] selftests/bpf: add iocost cost model test Tao Cui
2026-10-16 17:49   ` Tejun Heo
2026-10-19  9:38     ` Tao Cui
2026-10-03 12:40 ` [RFC PATCH v10 3/4] blk-iocost: add iocost_ioc_tick tracepoint for per-period device summary Tao Cui
2026-10-16 17:49   ` Tejun Heo
2026-10-19  9:44     ` Tao Cui
2026-10-03 12:40 ` [RFC PATCH v10 4/4] docs: cgroup-v2: document the iocost BPF cost model attachment Tao Cui
2026-10-16 17:49   ` Tejun Heo
2026-10-19  9:50     ` Tao Cui
2026-10-15 12:00 ` [RFC PATCH v10 0/4] blk-iocost: add BPF struct_ops cost model support Tao Cui
2026-10-16 17:49   ` Tejun Heo
2026-10-19  8:47     ` Tao Cui

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=24699b2b-4637-4872-8706-4f92193fa3ac@linux.dev \
    --to=cui.tao@linux.dev \
    --cc=alexei.starovoitov@gmail.com \
    --cc=ameryhung@gmail.com \
    --cc=andrii@kernel.org \
    --cc=ast@kernel.org \
    --cc=axboe@kernel.dk \
    --cc=bpf@vger.kernel.org \
    --cc=cgroups@vger.kernel.org \
    --cc=cuitao@kylinos.cn \
    --cc=daniel@iogearbox.net \
    --cc=eddyz87@gmail.com \
    --cc=josef@toxicopanda.com \
    --cc=linux-block@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=tj@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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®