Skip to contentSkip to conversation

BPF controlled io_uring

PATCH Applied Pavel Begunkov 3 participants 10 messages Feb 26 – Mar 17

Subsystems (3)
lore
Share & tools
lore

Pavel Begunkov

author maintainer
#0 lore raw

v10 cover BPF controlled io_uring

Introduces a way to override the standard io_uring_enter syscall execution with an extendible event loop, which can be controlled by BPF via new io_uring struct_ops or from within the kernel.

There are multiple use cases I want to cover with this:

- Syscall avoidance. Instead of returning to the userspace for CQE processing, a part of the logic can be moved into BPF to avoid excessive number of syscalls.

- Access to in-kernel io_uring resources. For example, there are registered buffers that can't be directly accessed by the userspace, however we can give BPF the ability to peek at them. It can be used to take a look at in-buffer app level headers to decide what to do with data next and issuing IO using it.

- Smarter request ordering and linking. Request links are pretty limited and inflexible as they can't pass information from one request to another. With BPF we can peek at CQEs and memory and compile a subsequent request.

- Feature semi-deprecation. It can be used to simplify handling of deprecated features by moving it into the callback out core io_uring. For example, it should be trivial to simulate IOSQE_IO_DRAIN. Another target could be request linking logic.

- It can serve as a base for custom algorithms and fine tuning. Often, it'd be impractical to introduce a generic feature because it's either niche or requires a lot of configuration. For example, there is support min-wait, however BPF can help to further fine tune it by doing it in multiple steps with different number of CQEs / timeouts. Another feature people were asking about is allowing to over queue SQEs but make the kernel to maintain a given QD.

- Smarter polling. Napi polling is performed only once per syscall and then it switches to waiting. We can do smarter and intermix polling with waiting using the hook.

It might need more specialised kfuncs in the future, but the core functionality is implemented with just two simple functions. One returns region memory, which gives BPF access to CQ/SQ/etc. And the second is for submitting requests. It's also given a structure as an argument, which is used to pass waiting parameters.

It showed good numbers in a test that sequentially executes N nop requests, where BPF was more than twice as fast than a 2-nop request link implementation.

v10:
- Remove internal wrapper struct around loop params for now
- Improve nr_wait checks
- Kill selftests

v9: - Update mini_liburing
- Clean up the nop test, bound the CQ processing by a separate constant and not CQ_ENTRIES.
- Add helpers for sharing code b/w examples
- Enable IORING_SETUP_SQ_REWIND
- Use io_uring regions for parameter passing.

v8: - Remove an check that is "always true" to silence smatch
- Kill unused variables from selftests

v7: - Fix CQ overflow flushing deadlock and add a selftest

v6: - Fix inversed check on ejection leaving function pointer and
add a selftest checking that.
- Add spdx headers
- Remove sqe reassignment in selftests

v5: - Selftests are now using vmlinux.h
- Checking for unexpected loop return codes
- Remove KF_TRUSTED_ARGS (default)
- Squashed one of the patches, it's more sensible this way

v4: - Separated the event loop from the normal waiting path.
- Improved the selftest.

v3: - Removed most of utility kfuncs and replaced it with a single
helper returning the ring memory.
- Added KF_TRUSTED_ARGS to kfuncs
- Fix ifdef guarding
- Added a selftest
- Adjusted the waiting loop
- Reused the bpf lock section for task_work execution

Pavel Begunkov (4):
io_uring: introduce callback driven main loop io_uring/bpf-ops: implement loop_step with BPF struct_ops io_uring/bpf-ops: add kfunc helpers io_uring/bpf-ops: implement bpf ops registration

 include/linux/io_uring_types.h |  10 ++
 io_uring/Kconfig               |   5 +
 io_uring/Makefile              |   3 +-
 io_uring/bpf-ops.c             | 270 +++++++++++++++++++++++++++++++++
 io_uring/bpf-ops.h             |  28 ++++
 io_uring/io_uring.c            |  13 ++
 io_uring/loop.c                |  91 +++++++++++
 io_uring/loop.h                |  27 ++++
 io_uring/wait.h                |   1 +
 9 files changed, 447 insertions(+), 1 deletion(-)
 create mode 100644 io_uring/bpf-ops.c
 create mode 100644 io_uring/bpf-ops.h
 create mode 100644 io_uring/loop.c
 create mode 100644 io_uring/loop.h
Signature · 1 line
2.53.0
author maintainer
#1 lore raw

PATCH v10 1/4 io_uring: introduce callback driven main loop

The io_uring_enter() has a fixed order of execution: it submits requests, waits for completions, and returns to the user. Allow to optionally replace it with a custom loop driven by a callback called loop_step. The basic requirements to the callback is that it should be able to submit requests, wait for completions, parse them and repeat. Most of the communication including parameter passing can be implemented via shared memory.

The callback should return IOU_LOOP_CONTINUE to continue execution or IOU_LOOP_STOP to return to the user space. Note that the kernel may decide to prematurely terminate it as well, e.g. in case the process was signalled or killed.

The hook takes a structure with parameters. It can be used to ask the kernel to wait for CQEs by setting cq_wait_idx to the CQE index it wants to wait for. Spurious wake ups are possible and even likely, the callback is expected to handle it. There will be more parameters in the future like timeout.

It can be used with kernel callbacks, for example, as a slow path deprecation mechanism overwiting SQEs and emulating the wanted behaviour, however it's more useful together with BPF programs implemented in following patches.

Note that keeping it separately from the normal io_uring wait loop makes things much simpler and cleaner. It keeps it in one place instead of spreading a bunch of checks in different places including disabling the submission path. It holds the lock by default, which is a better fit for BPF synchronisation and the loop execution model. It nicely avoids existing quirks like forced wake ups on timeout request completion. And it should be easier to implement new features.

---
 include/linux/io_uring_types.h |  5 ++
 io_uring/Makefile              |  2 +-
 io_uring/io_uring.c            | 11 ++++
 io_uring/loop.c                | 91 ++++++++++++++++++++++++++++++++++
 io_uring/loop.h                | 27 ++++++++++
 io_uring/wait.h                |  1 +
 6 files changed, 136 insertions(+), 1 deletion(-)
 create mode 100644 io_uring/loop.c
 create mode 100644 io_uring/loop.h
include/linux/io_uring_types.h +5 -0
index 3e4a82a6f817..cceac329fcfd 100644
@@ -41,6 +41,8 @@ enum io_uring_cmd_flags {
 	IO_URING_F_COMPAT		= (1 << 12),
 };
 
+struct iou_loop_params;
+
 struct io_wq_work_node {
 	struct io_wq_work_node *next;
 };
@@ -355,6 +357,9 @@ struct io_ring_ctx {
 		struct io_alloc_cache	rw_cache;
 		struct io_alloc_cache	cmd_cache;
 
+		int (*loop_step)(struct io_ring_ctx *ctx,
+				 struct iou_loop_params *);
+
 		/*
 		 * Any cancelable uring_cmd is added to this list in
 		 * ->uring_cmd() by io_uring_cmd_insert_cancelable()
io_uring/Makefile +1 -1
index 931f9156132a..1c1f47de32a4 100644
@@ -14,7 +14,7 @@ obj-$(CONFIG_IO_URING)		+= io_uring.o opdef.o kbuf.o rsrc.o notif.o \
 					advise.o openclose.o statx.o timeout.o \
 					cancel.o waitid.o register.o \
 					truncate.o memmap.o alloc_cache.o \
-					query.o
+					query.o loop.o
 
 obj-$(CONFIG_IO_URING_ZCRX)	+= zcrx.o
 obj-$(CONFIG_IO_WQ)		+= io-wq.o
io_uring/io_uring.c +11 -0
index 1e627b7a2f3a..0c8bb4e8480a 100644
@@ -95,6 +95,7 @@
 #include "eventfd.h"
 #include "wait.h"
 #include "bpf_filter.h"
+#include "loop.h"
 
 #define SQE_COMMON_FLAGS (IOSQE_FIXED_FILE | IOSQE_IO_LINK | \
 			  IOSQE_IO_HARDLINK | IOSQE_ASYNC)
@@ -589,6 +590,11 @@ void io_cqring_do_overflow_flush(struct io_ring_ctx *ctx)
 	mutex_unlock(&ctx->uring_lock);
 }
 
+void io_cqring_overflow_flush_locked(struct io_ring_ctx *ctx)
+{
+	__io_cqring_overflow_flush(ctx, false);
+}
+
 /* must to be called somewhat shortly after putting a request */
 static inline void io_put_task(struct io_kiocb *req)
 {
@@ -2582,6 +2588,11 @@ SYSCALL_DEFINE6(io_uring_enter, unsigned int, fd, u32, to_submit,
 	if (unlikely(smp_load_acquire(&ctx->flags) & IORING_SETUP_R_DISABLED))
 		goto out;
 
+	if (io_has_loop_ops(ctx)) {
+		ret = io_run_loop(ctx);
+		goto out;
+	}
+
 	/*
 	 * For SQ polling, the thread will do all submissions and completions.
 	 * Just return the requested submit count, and wake the thread if
io_uring/loop.c new file +91 -0

91 lines not on this page · raw message

io_uring/loop.h new file +27 -0
new file mode 100644
index 000000000000..d7718b9ce61e
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: GPL-2.0
+#ifndef IOU_LOOP_H
+#define IOU_LOOP_H
+
+#include <linux/io_uring_types.h>
+
+struct iou_loop_params {
+	/*
+	 * The CQE index to wait for. Only serves as a hint and can still be
+	 * woken up earlier.
+	 */
+	__u32			cq_wait_idx;
+};
+
+enum {
+	IOU_LOOP_CONTINUE = 0,
+	IOU_LOOP_STOP,
+};
+
+static inline bool io_has_loop_ops(struct io_ring_ctx *ctx)
+{
+	return data_race(ctx->loop_step);
+}
+
+int io_run_loop(struct io_ring_ctx *ctx);
+
+#endif
io_uring/wait.h +1 -0
index 5e236f74e1af..037e512dd80c 100644
@@ -25,6 +25,7 @@ int io_cqring_wait(struct io_ring_ctx *ctx, int min_events, u32 flags,
 		   struct ext_arg *ext_arg);
 int io_run_task_work_sig(struct io_ring_ctx *ctx);
 void io_cqring_do_overflow_flush(struct io_ring_ctx *ctx);
+void io_cqring_overflow_flush_locked(struct io_ring_ctx *ctx);
 
 static inline unsigned int __io_cqring_events(struct io_ring_ctx *ctx)
 {
Signature · 1 line
2.53.0
author maintainer
#2 lore raw

PATCH v10 2/4 io_uring/bpf-ops: implement loop_step with BPF struct_ops

In reply to Pavel Begunkov

Introduce io_uring BPF struct ops implementing the loop_step callback, which will allow BPF to overwrite the default io_uring event loop logic.

The callback takes an io_uring context, the main role of which is to be passed to io_uring kfuncs. The other argument is a struct iou_loop_params, which BPF can use to request CQ waiting and communicate other parameters. See the event loop description in the previous patch for more details.

---
 io_uring/Kconfig    |   5 ++
 io_uring/Makefile   |   1 +
 io_uring/bpf-ops.c  | 127 ++++++++++++++++++++++++++++++++++++++++++++
 io_uring/bpf-ops.h  |  14 +++++
 io_uring/io_uring.c |   1 +
 5 files changed, 148 insertions(+)
 create mode 100644 io_uring/bpf-ops.c
 create mode 100644 io_uring/bpf-ops.h
io_uring/Kconfig +5 -0
index a7ae23cf1035..a283d9e53787 100644
@@ -14,3 +14,8 @@ config IO_URING_BPF
 	def_bool y
 	depends on BPF
 	depends on NET
+
+config IO_URING_BPF_OPS
+	def_bool y
+	depends on IO_URING
+	depends on BPF_SYSCALL && BPF_JIT && DEBUG_INFO_BTF
io_uring/Makefile +1 -0
index 1c1f47de32a4..c54e328d1410 100644
@@ -25,3 +25,4 @@ obj-$(CONFIG_NET) += net.o cmd_net.o
 obj-$(CONFIG_PROC_FS) += fdinfo.o
 obj-$(CONFIG_IO_URING_MOCK_FILE) += mock_file.o
 obj-$(CONFIG_IO_URING_BPF) += bpf_filter.o
+obj-$(CONFIG_IO_URING_BPF_OPS) += bpf-ops.o
io_uring/bpf-ops.c new file +127 -0

127 lines not on this page · raw message

io_uring/bpf-ops.h new file +14 -0
new file mode 100644
index 000000000000..e8a08ae2df0a
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: GPL-2.0
+#ifndef IOU_BPF_OPS_H
+#define IOU_BPF_OPS_H
+
+#include <linux/io_uring_types.h>
+
+struct io_uring_bpf_ops {
+	int (*loop_step)(struct io_ring_ctx *ctx, struct iou_loop_params *lp);
+
+	__u32 ring_fd;
+	void *priv;
+};
+
+#endif /* IOU_BPF_OPS_H */
io_uring/io_uring.c +1 -0
index 0c8bb4e8480a..548ea5a080a0 100644
@@ -87,6 +87,7 @@
 #include "msg_ring.h"
 #include "memmap.h"
 #include "zcrx.h"
+#include "bpf-ops.h"
 
 #include "timeout.h"
 #include "poll.h"
Signature · 1 line
2.53.0
author maintainer
#3 lore raw

PATCH v10 3/4 io_uring/bpf-ops: add kfunc helpers

In reply to Pavel Begunkov

Add two kfuncs that should cover most of the needs:

1. bpf_io_uring_submit_sqes(), which allows to submit io_uring requests. It mirrors the normal user space submission path and follows all related io_uring_enter(2) rules. i.e. SQEs are taken from the SQ according to head/tail values. In case of IORING_SETUP_SQ_REWIND, it'll submit first N entries.

2. bpf_io_uring_get_region() returns a pointer to the specified region, where io_uring regions are kernel-userspace shared chunks of memory. It takes the size as an argument, which should be a load time constant. There are 3 types of regions:
- IOU_REGION_SQ returns the submission queue.
- IOU_REGION_CQ stores the CQ, SQ/CQ headers and the sqarray. In other words, it gives same memory that would normally be mmap'ed with IORING_FEAT_SINGLE_MMAP enabled IORING_OFF_SQ_RING.
- IOU_REGION_MEM represents the memory / parameter region. It can be used to store request indirect parameters and for kernel - user communication.

It intentionally provides a thin but flexible API and expects BPF programs to implement CQ/SQ header parsing, CQ walking, etc. That mirrors how the normal user space works with rings and should help to minimise kernel / kfunc helpers changes while introducing new generic io_uring features.

---
 io_uring/bpf-ops.c | 55 ++++++++++++++++++++++++++++++++++++++++++++++
 io_uring/bpf-ops.h |  6 +++++
 2 files changed, 61 insertions(+)
io_uring/bpf-ops.c +55 -0
index 975db5a78188..17518f4ecca9 100644
@@ -5,11 +5,58 @@
 
 #include "io_uring.h"
 #include "register.h"
+#include "memmap.h"
 #include "bpf-ops.h"
 #include "loop.h"
 
 static const struct btf_type *loop_params_type;
 
+__bpf_kfunc_start_defs();
+
+__bpf_kfunc int bpf_io_uring_submit_sqes(struct io_ring_ctx *ctx, u32 nr)
+{
+	return io_submit_sqes(ctx, nr);
+}
+
+__bpf_kfunc
+__u8 *bpf_io_uring_get_region(struct io_ring_ctx *ctx, __u32 region_id,
+			      const size_t rdwr_buf_size)
+{
+	struct io_mapped_region *r;
+
+	lockdep_assert_held(&ctx->uring_lock);
+
+	switch (region_id) {
+	case IOU_REGION_MEM:
+		r = &ctx->param_region;
+		break;
+	case IOU_REGION_CQ:
+		r = &ctx->ring_region;
+		break;
+	case IOU_REGION_SQ:
+		r = &ctx->sq_region;
+		break;
+	default:
+		return NULL;
+	}
+
+	if (unlikely(rdwr_buf_size > io_region_size(r)))
+		return NULL;
+	return io_region_get_ptr(r);
+}
+
+__bpf_kfunc_end_defs();
+
+BTF_KFUNCS_START(io_uring_kfunc_set)
+BTF_ID_FLAGS(func, bpf_io_uring_submit_sqes, KF_SLEEPABLE);
+BTF_ID_FLAGS(func, bpf_io_uring_get_region, KF_RET_NULL);
+BTF_KFUNCS_END(io_uring_kfunc_set)
+
+static const struct btf_kfunc_id_set bpf_io_uring_kfunc_set = {
+	.owner = THIS_MODULE,
+	.set = &io_uring_kfunc_set,
+};
+
 static int io_bpf_ops__loop_step(struct io_ring_ctx *ctx,
 				 struct iou_loop_params *lp)
 {
@@ -68,12 +115,20 @@ io_lookup_struct_type(struct btf *btf, const char *name)
 
 static int bpf_io_init(struct btf *btf)
 {
+	int ret;
+
 	loop_params_type = io_lookup_struct_type(btf, "iou_loop_params");
 	if (!loop_params_type) {
 		pr_err("io_uring: Failed to locate iou_loop_params\n");
 		return -EINVAL;
 	}
 
+	ret = register_btf_kfunc_id_set(BPF_PROG_TYPE_STRUCT_OPS,
+					&bpf_io_uring_kfunc_set);
+	if (ret) {
+		pr_err("io_uring: Failed to register kfuncs (%d)\n", ret);
+		return ret;
+	}
 	return 0;
 }
 
io_uring/bpf-ops.h +6 -0
index e8a08ae2df0a..b9e589ad519a 100644
@@ -4,6 +4,12 @@
 
 #include <linux/io_uring_types.h>
 
+enum {
+	IOU_REGION_MEM,
+	IOU_REGION_CQ,
+	IOU_REGION_SQ,
+};
+
 struct io_uring_bpf_ops {
 	int (*loop_step)(struct io_ring_ctx *ctx, struct iou_loop_params *lp);
 
Signature · 1 line
2.53.0
author maintainer
#4 lore raw

PATCH v10 4/4 io_uring/bpf-ops: implement bpf ops registration

In reply to Pavel Begunkov

Implement BPF struct ops registration. It's registered off the BPF path, and can be removed by BPF as well as io_uring. To protect it, introduce a global lock synchronising registration. ctx->uring_lock can be nested under it. ctx->bpf_ops is write protected by both locks and so it's safe to read it under either of them.

---
 include/linux/io_uring_types.h |  5 ++
 io_uring/bpf-ops.c             | 92 +++++++++++++++++++++++++++++++++-
 io_uring/bpf-ops.h             |  8 +++
 io_uring/io_uring.c            |  1 +
 4 files changed, 104 insertions(+), 2 deletions(-)
include/linux/io_uring_types.h +5 -0
index cceac329fcfd..976d85f82f86 100644
@@ -8,6 +8,9 @@
 #include <linux/llist.h>
 #include <uapi/linux/io_uring.h>
 
+struct iou_loop_params;
+struct io_uring_bpf_ops;
+
 enum {
 	/*
 	 * A hint to not wake right away but delay until there are enough of
@@ -481,6 +484,8 @@ struct io_ring_ctx {
 	DECLARE_HASHTABLE(napi_ht, 4);
 #endif
 
+	struct io_uring_bpf_ops		*bpf_ops;
+
 	/*
 	 * Protection for resize vs mmap races - both the mmap and resize
 	 * side will need to grab this lock, to prevent either side from
io_uring/bpf-ops.c +90 -2

116 lines not on this page · raw message

io_uring/bpf-ops.h +8 -0
index b9e589ad519a..b39b3fd3acda 100644
@@ -17,4 +17,12 @@ struct io_uring_bpf_ops {
 	void *priv;
 };
 
+#ifdef CONFIG_IO_URING_BPF_OPS
+void io_unregister_bpf_ops(struct io_ring_ctx *ctx);
+#else
+static inline void io_unregister_bpf_ops(struct io_ring_ctx *ctx)
+{
+}
+#endif
+
 #endif /* IOU_BPF_OPS_H */
io_uring/io_uring.c +1 -0
index 548ea5a080a0..b154dac396d3 100644
@@ -2154,6 +2154,7 @@ static __cold void io_req_caches_free(struct io_ring_ctx *ctx)
 
 static __cold void io_ring_ctx_free(struct io_ring_ctx *ctx)
 {
+	io_unregister_bpf_ops(ctx);
 	io_sq_thread_finish(ctx);
 
 	mutex_lock(&ctx->uring_lock);
Signature · 1 line
2.53.0
author maintainer
#5 lore raw
In reply to Pavel Begunkov
Quoted 34 lines · Pavel Begunkov

Introduces a way to override the standard io_uring_enter syscall execution with an extendible event loop, which can be controlled by BPF via new io_uring struct_ops or from within the kernel.

There are multiple use cases I want to cover with this:

- Syscall avoidance. Instead of returning to the userspace for CQE processing, a part of the logic can be moved into BPF to avoid excessive number of syscalls.

- Access to in-kernel io_uring resources. For example, there are registered buffers that can't be directly accessed by the userspace, however we can give BPF the ability to peek at them. It can be used to take a look at in-buffer app level headers to decide what to do with data next and issuing IO using it.

- Smarter request ordering and linking. Request links are pretty limited and inflexible as they can't pass information from one request to another. With BPF we can peek at CQEs and memory and compile a subsequent request.

- Feature semi-deprecation. It can be used to simplify handling of deprecated features by moving it into the callback out core io_uring. For example, it should be trivial to simulate IOSQE_IO_DRAIN. Another target could be request linking logic.

- It can serve as a base for custom algorithms and fine tuning. Often, it'd be impractical to introduce a generic feature because it's either niche or requires a lot of configuration. For example, there is support min-wait, however BPF can help to further fine tune it by doing it in multiple steps with different number of CQEs / timeouts. Another feature people were asking about is allowing to over queue SQEs but make the kernel to maintain a given QD.

Quoted 3 lines

- Smarter polling. Napi polling is performed only once per syscall and then it switches to waiting. We can do smarter and intermix polling with waiting using the hook.

Any comments for the patch set?

Signature · 1 line
Pavel Begunkov
#6 lore raw
Quoted 36 lines · Pavel Begunkov
Quoted 34 lines · Pavel Begunkov

Introduces a way to override the standard io_uring_enter syscall execution with an extendible event loop, which can be controlled by BPF via new io_uring struct_ops or from within the kernel.

There are multiple use cases I want to cover with this:

- Syscall avoidance. Instead of returning to the userspace for CQE processing, a part of the logic can be moved into BPF to avoid excessive number of syscalls.

- Access to in-kernel io_uring resources. For example, there are registered buffers that can't be directly accessed by the userspace, however we can give BPF the ability to peek at them. It can be used to take a look at in-buffer app level headers to decide what to do with data next and issuing IO using it.

- Smarter request ordering and linking. Request links are pretty limited and inflexible as they can't pass information from one request to another. With BPF we can peek at CQEs and memory and compile a subsequent request.

- Feature semi-deprecation. It can be used to simplify handling of deprecated features by moving it into the callback out core io_uring. For example, it should be trivial to simulate IOSQE_IO_DRAIN. Another target could be request linking logic.

- It can serve as a base for custom algorithms and fine tuning. Often, it'd be impractical to introduce a generic feature because it's either niche or requires a lot of configuration. For example, there is support min-wait, however BPF can help to further fine tune it by doing it in multiple steps with different number of CQEs / timeouts. Another feature people were asking about is allowing to over queue SQEs but make the kernel to maintain a given QD.

Quoted 5 lines
Quoted 3 lines

- Smarter polling. Napi polling is performed only once per syscall and then it switches to waiting. We can do smarter and intermix polling with waiting using the hook.

Any comments for the patch set?

I'm not opposed to this feature, but I agree with Ming that it seems largely orthogonal to his patchset allowing BPF programs to access io_uring registered buffers[1]. This patchset doesn't provide any kfuncs for interacting with registered buffers, so we would still need something like the kfuncs implemented by Ming's patchset to allow BPF programs to access registered buffers directly. Although either the ->loop_step() or ->prep()/->issue() interface could allow userspace to run a BPF program in the context of the io_uring, I wouldn't be keen on reimplementing the entire io_uring_enter() loop logic just to intercept a few requests to run BPF programs. The ->prep()/->issue() abstraction seems more straightforward to me, though I understand that it's not as general as the ability to modify the behavior of the entire io_uring_enter() syscall.

Best,
Caleb

[1]: https://lore.kernel.org/io-uring/[email protected]/T/#u

author maintainer
#7 lore raw
Quoted 1 line · Caleb Sander Mateos

On Mon, Mar 9, 2026 at 6:24 AM Pavel Begunkov <[email protected]> wrote:

...

Quoted 9 lines
Quoted 5 lines
Quoted 3 lines

- Smarter polling. Napi polling is performed only once per syscall and then it switches to waiting. We can do smarter and intermix polling with waiting using the hook.

Any comments for the patch set?

I'm not opposed to this feature, but I agree with Ming that it seems largely orthogonal to his patchset allowing BPF programs to access io_uring registered buffers[1]. This patchset doesn't provide any

The idea of giving BPF access to registered buffers is largely orthogonal to underlying ops implementation, I agree.

Quoted 3 lines

kfuncs for interacting with registered buffers, so we would still need something like the kfuncs implemented by Ming's patchset to allow BPF programs to access registered buffers directly. Although either the

Surely

Quoted 4 lines

->loop_step() or ->prep()/->issue() interface could allow userspace to run a BPF program in the context of the io_uring, I wouldn't be keen on reimplementing the entire io_uring_enter() loop logic just to intercept a few requests to run BPF programs.

I don't think some 50 or maybe fewer lines of generic code is that much of a hassle, I'm thinking about adding it as a library code of some sort. In either case, I'm not trying to convince you to do more work.

Signature · 1 line
Pavel Begunkov
maintainer
#8 lore raw
In reply to Caleb Sander Mateos
Quoted 43 lines · Caleb Sander Mateos
Quoted 36 lines · On Mon, Mar 9, 2026 at 6:24?AM Pavel Begunkov <[email protected]> wrote:
Quoted 34 lines · Pavel Begunkov

Introduces a way to override the standard io_uring_enter syscall execution with an extendible event loop, which can be controlled by BPF via new io_uring struct_ops or from within the kernel.

There are multiple use cases I want to cover with this:

- Syscall avoidance. Instead of returning to the userspace for CQE processing, a part of the logic can be moved into BPF to avoid excessive number of syscalls.

- Access to in-kernel io_uring resources. For example, there are registered buffers that can't be directly accessed by the userspace, however we can give BPF the ability to peek at them. It can be used to take a look at in-buffer app level headers to decide what to do with data next and issuing IO using it.

- Smarter request ordering and linking. Request links are pretty limited and inflexible as they can't pass information from one request to another. With BPF we can peek at CQEs and memory and compile a subsequent request.

- Feature semi-deprecation. It can be used to simplify handling of deprecated features by moving it into the callback out core io_uring. For example, it should be trivial to simulate IOSQE_IO_DRAIN. Another target could be request linking logic.

- It can serve as a base for custom algorithms and fine tuning. Often, it'd be impractical to introduce a generic feature because it's either niche or requires a lot of configuration. For example, there is support min-wait, however BPF can help to further fine tune it by doing it in multiple steps with different number of CQEs / timeouts. Another feature people were asking about is allowing to over queue SQEs but make the kernel to maintain a given QD.

Quoted 5 lines
Quoted 3 lines

- Smarter polling. Napi polling is performed only once per syscall and then it switches to waiting. We can do smarter and intermix polling with waiting using the hook.

Any comments for the patch set?

Quoted 13 lines

I'm not opposed to this feature, but I agree with Ming that it seems largely orthogonal to his patchset allowing BPF programs to access io_uring registered buffers[1]. This patchset doesn't provide any kfuncs for interacting with registered buffers, so we would still need something like the kfuncs implemented by Ming's patchset to allow BPF programs to access registered buffers directly. Although either the ->loop_step() or ->prep()/->issue() interface could allow userspace to run a BPF program in the context of the io_uring, I wouldn't be keen on reimplementing the entire io_uring_enter() loop logic just to intercept a few requests to run BPF programs. The ->prep()/->issue() abstraction seems more straightforward to me, though I understand that it's not as general as the ability to modify the behavior of the entire io_uring_enter() syscall.

Caleb/Ming, any objections to merging this patchset? I think we all agree it doesn't cover everything that we would want, so far we have two different patchsets out there, but they also aren't mutually exclusive. In the hopes of getting the ball rolling on the buffer side too, I'd like to move forward and merge this and then we can rebase Ming's stuff on top where it makes sense?

Signature · 1 line
Jens Axboe
maintainer Applied
#9 lore raw
In reply to Pavel Begunkov
Quoted 11 lines · Pavel Begunkov

Introduces a way to override the standard io_uring_enter syscall execution with an extendible event loop, which can be controlled by BPF via new io_uring struct_ops or from within the kernel.

There are multiple use cases I want to cover with this:

- Syscall avoidance. Instead of returning to the userspace for CQE processing, a part of the logic can be moved into BPF to avoid excessive number of syscalls.

[...]

Applied, thanks!

[1/4] io_uring: introduce callback driven main loop

      commit: 033af2b3eb19c5ed96825572105bca3611635ada
[2/4] io_uring/bpf-ops: implement loop_step with BPF struct_ops
      commit: d0e437b76bd3c979ddaa6205f5e9ad3e0f95faef
[3/4] io_uring/bpf-ops: add kfunc helpers
      commit: 890819248a8616558fe12e6c06c918ee1c3a2bc6
[4/4] io_uring/bpf-ops: implement bpf ops registration
      commit: 98f37634b12b17ad5c56db8fb63cf9d7dc55d74c

Best regards,

Signature · 1 line
Jens Axboe