Replies: 2 comments
-
|
The trick with io_uring is that you need to take into account the lifetime of each operation in both userland and the kernel. If the user_data references an object on the stack, you'll have to wait for the corresponding CQE to arrive before returning from your op function. If user_data references an object in the heap then you can mark it as cancelled and free it when the corresponding CQE is processed. |
Beta Was this translation helpful? Give feedback.
-
|
I confirm it's a valid concern. I have the same issue with life-cycle management when using
I would expect that I am trying to come up with a workaround as even if kernel behaviour will be improved, it won't help us as we need the solution for currently released kernels. My next idea:
@axboe @isilence maybe I am thinking all wrong - would love to hear you thoughts as well. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Context: While developing a C++20 Coroutine-based I/O framework using liburing, I encountered a significant challenge regarding reliable cancellation.
The Motivation (Why Sync Cancel?): If a truly synchronous cancellation were possible inside the Awaitable's destructor, the framework implementation would become significantly simpler and more efficient. It would allow us to treat I/O requests with strict RAII semantics: the Awaitable owns the request, and when it goes out of scope, the request is guaranteed to be gone. This eliminates the need for complex, overhead-heavy state management often found in other async frameworks (such as extending object lifetimes via shared_ptr, maintaining "tombstone" states, or incurring the cost of additional runtime state checks inside the event loop's hot path).
The Intended Design & Problem: My design relies on a direct mapping where the user_data submitted to the ring points to a callback specifically bound to the Awaitable instance. Consequently, the Awaitable must remain alive for as long as its corresponding CQE might exist in the ring.
To satisfy this constraint, I intend to use io_uring_register_sync_cancel() within the Awaitable's destructor to strictly finalize the operation before destruction.
However, an asynchronous gap prevents this design: Although io_uring_register_sync_cancel() waits for the cancellation command to be processed, it does not consume or remove the CQE of the original request. By design, the original request will inevitably post a CQE to the ring (either as -ECANCELED or as a successful completion if it finished just in time).
Consequently, even after a successful sync-cancel return, a "Phantom CQE" will eventually appear in the completion queue. If the destructor proceeds to destroy the Awaitable, the main loop will later fetch this Phantom CQE and dereference the invalid pointer, leading to UB.
The "CQE Stealing" Idea (My Question): To enforce strict RAII, I am considering a manual "CQE Stealing" approach inside the destructor.
Since the destructor itself runs within the context of the main event loop (inside an io_uring_for_each_cqe block), simply iterating from the head again is problematic (unknown start point, iterator conflict). Therefore, I am thinking of iterating backwards from the tail:
Logic:
Call io_uring_register_sync_cancel().
Manually scan the CQ ring buffer in reverse order (from tail to head) inside the destructor.
Find the CQE that matches my user_data.
Patch/Swap the user_data in that CQE to a harmless dummy_callback.
Return from the destructor (safely destroying the Awaitable).
Performance Note: While linear scanning might sound inefficient, I expect this to be fast (close to O(1)) in practice. Since the scan is performed immediately after the cancellation, the target CQE should be located at or very close to the tail of the ring.
Questions:
Feasibility: Is this "Reverse Scanning & Swapping" strategy technically feasible and safe? I am concerned about the timing issue where io_uring_register_sync_cancel() returns but the CQE has not yet been visible in the ring buffer.
Safety: Is it valid to access and modify the ring buffer in this manner while the outer loop is iterating?
Alternative: Is there any existing mechanism to perform a cancellation that strictly guarantees "no future CQE will appear for this user_data" before returning?
Beta Was this translation helpful? Give feedback.
All reactions