Skip to content

Commit cf1a894

Browse files
Use eventfd for wakeup and enable IORING_SETUP_SINGLE_ISSUER.
Replace the NOP-SQE wakeup strategy with an eventfd: the owner thread registers a one-shot poll_add on the eventfd before each blocking wait, and wakeup() writes to the fd from any thread without touching the ring's SQ. This satisfies IORING_SETUP_SINGLE_ISSUER (kernel 6.0+, guarded by Also install liburing-dev in the benchmark workflow so the benchmark actually exercises the URing backend. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 218d53a commit cf1a894

1 file changed

Lines changed: 63 additions & 21 deletions

File tree

ext/io/event/selector/uring.c

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <poll.h>
1111
#include <stdint.h>
1212
#include <time.h>
13+
#include <sys/eventfd.h>
1314

1415
#include "pidfd.c"
1516

@@ -33,9 +34,12 @@ struct IO_Event_Selector_URing
3334

3435
// Flag indicating whether the selector is currently blocked in a system call.
3536
// Set to 1 when blocked in io_uring_wait_cqe_timeout() without GVL, 0 otherwise.
36-
// Used by wakeup() to determine if an interrupt signal is needed.
3737
int blocked;
3838

39+
// eventfd used to wake the selector from another thread without touching the ring's SQ.
40+
// This allows IORING_SETUP_SINGLE_ISSUER: only the owner thread ever submits SQEs.
41+
int wakeup_fd;
42+
3943
struct timespec idle_duration;
4044

4145
struct IO_Event_Array completions;
@@ -101,6 +105,11 @@ void IO_Event_Selector_URing_Type_compact(void *_selector)
101105
static
102106
void close_internal(struct IO_Event_Selector_URing *selector)
103107
{
108+
if (selector->wakeup_fd >= 0) {
109+
close(selector->wakeup_fd);
110+
selector->wakeup_fd = -1;
111+
}
112+
104113
if (selector->ring.ring_fd >= 0) {
105114
io_uring_queue_exit(&selector->ring);
106115
selector->ring.ring_fd = -1;
@@ -220,6 +229,7 @@ VALUE IO_Event_Selector_URing_allocate(VALUE self) {
220229

221230
selector->pending = 0;
222231
selector->blocked = 0;
232+
selector->wakeup_fd = -1;
223233

224234
IO_Event_List_initialize(&selector->free_list);
225235

@@ -240,14 +250,33 @@ VALUE IO_Event_Selector_URing_initialize(VALUE self, VALUE loop) {
240250
TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector);
241251

242252
IO_Event_Selector_initialize(&selector->backend, self, loop);
243-
int result = io_uring_queue_init(URING_ENTRIES, &selector->ring, 0);
253+
254+
unsigned int flags = 0;
255+
// IORING_SETUP_SINGLE_ISSUER (kernel 6.0+): only the owner thread submits SQEs.
256+
// Safe here because wakeup() uses eventfd (no ring access from other threads).
257+
#ifdef IORING_SETUP_SINGLE_ISSUER
258+
flags |= IORING_SETUP_SINGLE_ISSUER;
259+
#endif
260+
261+
int result = io_uring_queue_init(URING_ENTRIES, &selector->ring, flags);
244262

245263
if (result < 0) {
246264
rb_syserr_fail(-result, "IO_Event_Selector_URing_initialize:io_uring_queue_init");
247265
}
248266

249267
rb_update_max_fd(selector->ring.ring_fd);
250268

269+
// eventfd for cross-thread wakeup: another thread writes to this fd; the owner
270+
// thread registers a one-shot poll_add before each blocking wait so the ring
271+
// wakes up without the waking thread ever touching the SQ.
272+
selector->wakeup_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
273+
if (selector->wakeup_fd < 0) {
274+
io_uring_queue_exit(&selector->ring);
275+
selector->ring.ring_fd = -1;
276+
rb_sys_fail("IO_Event_Selector_URing_initialize:eventfd");
277+
}
278+
rb_update_max_fd(selector->wakeup_fd);
279+
251280
return self;
252281
}
253282

@@ -1073,11 +1102,25 @@ void * select_internal(void *_arguments) {
10731102

10741103
static
10751104
int select_internal_without_gvl(struct select_arguments *arguments) {
1076-
io_uring_submit_flush(arguments->selector);
1105+
struct IO_Event_Selector_URing *selector = arguments->selector;
10771106

1078-
arguments->selector->blocked = 1;
1107+
// Register a one-shot poll on the wakeup eventfd before releasing the GVL.
1108+
// This allows wakeup() to signal us by writing to the fd from any thread
1109+
// without touching the ring's SQ (required for IORING_SETUP_SINGLE_ISSUER).
1110+
struct io_uring_sqe *sqe = io_get_sqe(selector);
1111+
io_uring_prep_poll_add(sqe, selector->wakeup_fd, POLLIN);
1112+
io_uring_sqe_set_data(sqe, NULL);
1113+
selector->pending += 1;
1114+
1115+
io_uring_submit_flush(selector);
1116+
1117+
selector->blocked = 1;
10791118
rb_thread_call_without_gvl(select_internal, (void *)arguments, RUBY_UBF_IO, 0);
1080-
arguments->selector->blocked = 0;
1119+
selector->blocked = 0;
1120+
1121+
// Drain the wakeup eventfd so the next poll_add doesn't fire immediately.
1122+
uint64_t value;
1123+
while (read(selector->wakeup_fd, &value, sizeof(value)) > 0) {}
10811124

10821125
if (arguments->result == -ETIME) {
10831126
arguments->result = 0;
@@ -1201,25 +1244,18 @@ VALUE IO_Event_Selector_URing_wakeup(VALUE self) {
12011244
struct IO_Event_Selector_URing *selector = NULL;
12021245
TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector);
12031246

1204-
// If we are blocking, we can schedule a nop event to wake up the selector:
1247+
// Wake the selector by writing to the eventfd. This is safe from any thread
1248+
// and never touches the ring's SQ, which is required for IORING_SETUP_SINGLE_ISSUER.
12051249
if (selector->blocked) {
1206-
struct io_uring_sqe *sqe = NULL;
1250+
uint64_t value = 1;
1251+
int result = write(selector->wakeup_fd, &value, sizeof(value));
12071252

1208-
while (true) {
1209-
sqe = io_uring_get_sqe(&selector->ring);
1210-
if (sqe) break;
1211-
1212-
rb_thread_schedule();
1213-
1214-
// It's possible we became unblocked already, so we can assume the selector has already cycled at least once:
1215-
if (!selector->blocked) return Qfalse;
1253+
// EAGAIN means the eventfd counter is already at its maximum (UINT64_MAX - 1),
1254+
// i.e. a wakeup is already pending — that's fine.
1255+
if (result < 0 && errno != EAGAIN) {
1256+
rb_sys_fail("IO_Event_Selector_URing_wakeup:write");
12161257
}
12171258

1218-
io_uring_prep_nop(sqe);
1219-
// If you don't set this line, the SQE will eventually be recycled and have valid user selector which can cause odd behaviour:
1220-
io_uring_sqe_set_data(sqe, NULL);
1221-
io_uring_submit(&selector->ring);
1222-
12231259
return Qtrue;
12241260
}
12251261

@@ -1230,7 +1266,13 @@ VALUE IO_Event_Selector_URing_wakeup(VALUE self) {
12301266

12311267
static int IO_Event_Selector_URing_supported_p(void) {
12321268
struct io_uring ring;
1233-
int result = io_uring_queue_init(32, &ring, 0);
1269+
1270+
unsigned int flags = 0;
1271+
#ifdef IORING_SETUP_SINGLE_ISSUER
1272+
flags |= IORING_SETUP_SINGLE_ISSUER;
1273+
#endif
1274+
1275+
int result = io_uring_queue_init(32, &ring, flags);
12341276

12351277
if (result < 0) {
12361278
rb_warn("io_uring_queue_init() was available at compile time but failed at run time: %s\n", strerror(-result));

0 commit comments

Comments
 (0)