-
Notifications
You must be signed in to change notification settings - Fork 140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
open question about race with kernel that liburing had to fix #197
Comments
I suspect that using |
This comment was marked as outdated.
This comment was marked as outdated.
After having another think about this and re-re-reading the diagrams in the original issue, I don't think I really understood the cross-language-atomic-semantics properly yet. That might mean there are not the proper tools to properly solve this in Rust. Sorry for the noise. A newer kernel which I was reading does use appear to be using an extra acquire fence. However it establishes another order than the one required for AcqRel to be definitely sufficient: https://github.com/torvalds/linux/blob/a93289b830ce783955b22fbe5d1274a464c05acf/io_uring/sqpoll.c#L334-L352 |
I had a look at the code here and the same bug is present here as in liburing/the kernel. The code here is almost the same as in liburing, the only difference is that liburing is using C/C++11 whereas Rust follows the C++20 memory model but the differences between the two are not relevant to this bug.
The code you link to is using a full memory barrier like the documentation mentioned in your previous comment talked about, not an acquire barrier. That As for why |
"making some assumptions" of the equivalence of these two operations is the confusing part (and this comment is the first I see that assumption spelled out as such precisely). Quite obviously Under the assumption that
This might be owed to the kernel's difference in approach to that terminology. The descriptions of their ordering terminology focus more on _how is X observed by other threads_ whereas the C/C++/Rust model operate on _what other things can our thread observe_ (thus requiring describing the operations on kernel thread to make _any_ kind of statement on it). My confusion is owed to that [`smp_mb__after_atomics` ends up being the default in the macro to an `acquire` fence](https://github.com/torvalds/linux/blob/5eb4573ea63d0c83bf58fb7c243fc2c2b6966c02/include/linux/atomic.h#L43).
To put it into the semantic model for Rust and visualize happens-before relationships:
If K2 > U2 then K1 > K2 > U2 > U3 and user space gets the flag, whereas when K2 < U2 then U1 > U2 > K2 > K3 and the kernel observes the updated head. Since there's a synchronizing ordering relationship between any pair of SeqCst operations, including a fence, this provides the necessary correctness guarantee. As an aside to vindicate AcqRel a little bit, replacing the load with It's surprising how a SeqCst is sufficient here, but it is. That kind of non-deterministic correctness guarantee is quite unexpected on its own, no? (Do you happen to know of any good CS paper on the transfer of invariants in this manner—the responsibility of waking might as well be some other affine state? It's fascinating.)
Expanding on the red-herring solution of fetch_xor
As by your quoted section: There is an ordering relationship between two atomic writes to the same memory location ("in the modification order of M") in any case. The problem of applying it is that in the code as written only one atomic write to separate locations each occurs—so that portion can't be used for reasoning. This is what |
Is there a fix for I've been involved in other work since about this issue was raised in |
I think you can apply the same fix here that was used in liburing:
The actual reason
From the above, if neither side sees the other's write (the kernel doesn't see the store to tail and the application doesn't see the store to flags) then each side's read is coherence ordered before the other's write which means each side's respective |
Never mind the AcqRel, it's admittedly a stupid solution that shouldn't be necessary and I've omitted the details on purpose. (Though: edited in above, discuss in another thread if you want¹) The kernel should just ensure that So yes, I've been convinced enough this library should be using |
Big thanks to you for looking into this again. In the past year, I've gained a new appreciation for just how many crates are building their io_uring features off of this one. The Rust on Linux community really benefits from getting this right. Can I reopen this just long enough to raise a related question given the work and PR you-all did yesterday? There are these two reads now: https://github.com/tokio-rs/io-uring/blob/4e52bca1f2c3ee24b2a51f4b5d9f3d8652f26cab/src/submit.rs#L57C5-L72C1 /// Whether the kernel thread has gone to sleep because it waited for too long without
/// submission queue entries.
#[inline]
fn sq_need_wakeup(&self) -> bool {
unsafe {
(*self.sq_flags).load(atomic::Ordering::Relaxed) & sys::IORING_SQ_NEED_WAKEUP != 0
}
}
/// CQ ring is overflown
fn sq_cq_overflow(&self) -> bool {
unsafe {
(*self.sq_flags).load(atomic::Ordering::Acquire) & sys::IORING_SQ_CQ_OVERFLOW != 0
}
} Without a comment explaining why, it is glaring that they perform the loads with different atomic ordering. Fair to say the three of you know this stuff way better than I so I'm just in a position to ask the question. Is the second one These two functions are not public so their documentation or comments don't have to say much but perhaps while this context is still fresh for you, one of you could suggest a comment for one or the other that says why their load instructions are different? |
The load in I believe the load in Edit: |
Thank you for these details. At least for now to me, it seems much clearer. @quininer Would you be amenable to a PR for the |
@FrankReh of course. |
Change the atomic read of the SQ flags field to be `relaxed`. Refer to discussions in tokio-rs#197 and in particular, as pointed out in tokio-rs#197, refer to the liburing library loads of this same field. The liburing library names this field sq.kflags and all its atomic loads are performed with their IO_URING_READ_ONCE macro, which it defines to be the `relaxed` atomic load.
Change the atomic read of the SQ flags field to be `relaxed`. Refer to discussions in tokio-rs#197 and in particular, as pointed out in tokio-rs#197, refer to the liburing library loads of this same field. The liburing library names this field sq.kflags and all its atomic loads are performed with their IO_URING_READ_ONCE macro, which it defines to be the `relaxed` atomic load.
Change the atomic read of the SQ flags field to be `relaxed`. Refer to discussions in #197 and in particular, as pointed out in #197, refer to the liburing library loads of this same field. The liburing library names this field sq.kflags and all its atomic loads are performed with their IO_URING_READ_ONCE macro, which it defines to be the `relaxed` atomic load.
I found this commit to liburing some time ago. It may have no bearing on this repo because the atomic code is necessarily different anyway. But didn't want to lose track of this before I or someone else had made sure.
axboe/liburing@744f415,
fixing issue
The text was updated successfully, but these errors were encountered: