aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShachaf Ben-Kiki <shachaf@gmail.com>2023-07-02 16:12:21 -0700
committerShachaf Ben-Kiki <shachaf@gmail.com>2024-03-02 16:39:13 -0800
commitf03becf5d7c1bc3fd9a426ba8b013346847f8b9e (patch)
tree49826fc81cf17efa52ecb765182c59db304be3a1
parent2b353a85abed9d19201bd947f3efca6a630e476a (diff)
downloadliburing-f03becf5d7c1bc3fd9a426ba8b013346847f8b9e.tar.gz
Fix memory ordering/atomic access
A few memory accesses don't seem correct to me, and a few seem slightly stricter than necessary. Here are the rules in userspace, I think: sq.ktail is read by the kernel sq.khead is written by the kernel cq.ktail is written by the kernel cq.khead is read by the kernel Locations that are written concurrently by the kernel must be loaded atomically; if they specify (to the user) that some memory is now available, they must be loaded with acquire semantics before the memory is read. Similarly, locations that are read concurrently by the kernel must be written atomically; if they specify (to the kernel) that some memory is now available, they must be stored with release semantics after the memory is written to. Note that without SQPOLL, sq is only accessed by the kernel in response to a system call, so no synchronization is necessary. Note that in practice, on most CPU architectures, a regular aligned load/store is already atomic; in that situation READ_ONCE/WRITE_ONCE is primarily necessary for the compiler, which would otherwise be allowed to do invalid things like load the lower half of an integer and then the upper half. Signed-off-by: Shachaf Ben-Kiki <shachaf@gmail.com>
-rw-r--r--src/include/liburing.h6
-rw-r--r--src/queue.c20
-rw-r--r--test/helpers.c20
3 files changed, 20 insertions, 26 deletions
diff --git a/src/include/liburing.h b/src/include/liburing.h
index dd6b1009..982a8241 100644
--- a/src/include/liburing.h
+++ b/src/include/liburing.h
@@ -1217,7 +1217,7 @@ IOURINGINLINE void io_uring_prep_ftruncate(struct io_uring_sqe *sqe,
*/
IOURINGINLINE unsigned io_uring_sq_ready(const struct io_uring *ring)
{
- unsigned khead = *ring->sq.khead;
+ unsigned khead;
/*
* Without a barrier, we could miss an update and think the SQ wasn't
@@ -1226,6 +1226,8 @@ IOURINGINLINE unsigned io_uring_sq_ready(const struct io_uring *ring)
*/
if (ring->flags & IORING_SETUP_SQPOLL)
khead = io_uring_smp_load_acquire(ring->sq.khead);
+ else
+ khead = *ring->sq.khead;
/* always use real head, to avoid losing sync for short submit */
return ring->sq.sqe_tail - khead;
@@ -1412,7 +1414,7 @@ IOURINGINLINE struct io_uring_sqe *_io_uring_get_sqe(struct io_uring *ring)
if (ring->flags & IORING_SETUP_SQE128)
shift = 1;
if (!(ring->flags & IORING_SETUP_SQPOLL))
- head = IO_URING_READ_ONCE(*sq->khead);
+ head = *sq->khead;
else
head = io_uring_smp_load_acquire(sq->khead);
diff --git a/src/queue.c b/src/queue.c
index b784b10c..c4360614 100644
--- a/src/queue.c
+++ b/src/queue.c
@@ -212,22 +212,18 @@ static unsigned __io_uring_flush_sq(struct io_uring *ring)
* Ensure kernel sees the SQE updates before the tail update.
*/
if (!(ring->flags & IORING_SETUP_SQPOLL))
- IO_URING_WRITE_ONCE(*sq->ktail, tail);
+ *sq->ktail = tail;
else
io_uring_smp_store_release(sq->ktail, tail);
}
/*
- * This _may_ look problematic, as we're not supposed to be reading
- * SQ->head without acquire semantics. When we're in SQPOLL mode, the
- * kernel submitter could be updating this right now. For non-SQPOLL,
- * task itself does it, and there's no potential race. But even for
- * SQPOLL, the load is going to be potentially out-of-date the very
- * instant it's done, regardless or whether or not it's done
- * atomically. Worst case, we're going to be over-estimating what
- * we can submit. The point is, we need to be able to deal with this
- * situation regardless of any perceived atomicity.
- */
- return tail - *sq->khead;
+ * This load needs to be atomic, since sq->khead is written concurrently
+ * by the kernel, but it doesn't need to be load_acquire, since the
+ * kernel doesn't store to the submission queue; it advances khead just
+ * to indicate that it's finished reading the submission queue entries
+ * so they're available for us to write to.
+ */
+ return tail - IO_URING_READ_ONCE(*sq->khead);
}
/*
diff --git a/test/helpers.c b/test/helpers.c
index 24fd4ce3..779347f6 100644
--- a/test/helpers.c
+++ b/test/helpers.c
@@ -286,22 +286,18 @@ unsigned __io_uring_flush_sq(struct io_uring *ring)
* Ensure kernel sees the SQE updates before the tail update.
*/
if (!(ring->flags & IORING_SETUP_SQPOLL))
- IO_URING_WRITE_ONCE(*sq->ktail, tail);
+ *sq->ktail = tail;
else
io_uring_smp_store_release(sq->ktail, tail);
}
/*
- * This _may_ look problematic, as we're not supposed to be reading
- * SQ->head without acquire semantics. When we're in SQPOLL mode, the
- * kernel submitter could be updating this right now. For non-SQPOLL,
- * task itself does it, and there's no potential race. But even for
- * SQPOLL, the load is going to be potentially out-of-date the very
- * instant it's done, regardless or whether or not it's done
- * atomically. Worst case, we're going to be over-estimating what
- * we can submit. The point is, we need to be able to deal with this
- * situation regardless of any perceived atomicity.
- */
- return tail - *sq->khead;
+ * This load needs to be atomic, since sq->khead is written concurrently
+ * by the kernel, but it doesn't need to be load_acquire, since the
+ * kernel doesn't store to the submission queue; it advances khead just
+ * to indicate that it's finished reading the submission queue entries
+ * so they're available for us to write to.
+ */
+ return tail - IO_URING_READ_ONCE(*sq->khead);
}
/*