1111#include <stdint.h>
1212#include <time.h>
1313
14+ #include "../interrupt.h"
15+
1416#include "pidfd.c"
1517
1618#include <linux/version.h>
@@ -33,9 +35,21 @@ struct IO_Event_Selector_URing
3335
3436 // Flag indicating whether the selector is currently blocked in a system call.
3537 // 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.
3738 int blocked ;
3839
40+ // Interrupt used to wake the selector from another thread without touching the ring's SQ.
41+ // This allows IORING_SETUP_SINGLE_ISSUER: only the owner thread ever submits SQEs.
42+ // Uses eventfd on Linux, pipe fallback elsewhere.
43+ struct IO_Event_Interrupt interrupt ;
44+
45+ // Whether an async read on interrupt is currently pending in the ring.
46+ // The read is re-submitted before each blocking wait when not registered.
47+ int wakeup_registered ;
48+
49+ // Buffer for the pending async read on the interrupt descriptor.
50+ // Must remain valid for the lifetime of the in-flight SQE.
51+ uint64_t wakeup_value ;
52+
3953 struct timespec idle_duration ;
4054
4155 struct IO_Event_Array completions ;
@@ -101,6 +115,12 @@ void IO_Event_Selector_URing_Type_compact(void *_selector)
101115static
102116void close_internal (struct IO_Event_Selector_URing * selector )
103117{
118+ if (selector -> interrupt .descriptor >= 0 ) {
119+ IO_Event_Interrupt_close (& selector -> interrupt );
120+ selector -> interrupt .descriptor = -1 ;
121+ selector -> wakeup_registered = 0 ;
122+ }
123+
104124 if (selector -> ring .ring_fd >= 0 ) {
105125 io_uring_queue_exit (& selector -> ring );
106126 selector -> ring .ring_fd = -1 ;
@@ -220,6 +240,8 @@ VALUE IO_Event_Selector_URing_allocate(VALUE self) {
220240
221241 selector -> pending = 0 ;
222242 selector -> blocked = 0 ;
243+ selector -> interrupt .descriptor = -1 ;
244+ selector -> wakeup_registered = 0 ;
223245
224246 IO_Event_List_initialize (& selector -> free_list );
225247
@@ -240,14 +262,39 @@ VALUE IO_Event_Selector_URing_initialize(VALUE self, VALUE loop) {
240262 TypedData_Get_Struct (self , struct IO_Event_Selector_URing , & IO_Event_Selector_URing_Type , selector );
241263
242264 IO_Event_Selector_initialize (& selector -> backend , self , loop );
243- int result = io_uring_queue_init (URING_ENTRIES , & selector -> ring , 0 );
265+
266+ unsigned int flags = 0 ;
267+ // IORING_SETUP_SINGLE_ISSUER (kernel 6.0+): only the owner thread submits SQEs.
268+ // Safe here because wakeup() uses eventfd (no ring access from other threads).
269+ #ifdef IORING_SETUP_SINGLE_ISSUER
270+ flags |= IORING_SETUP_SINGLE_ISSUER ;
271+ #endif
272+ // IORING_SETUP_DEFER_TASKRUN (kernel 6.1+, requires SINGLE_ISSUER): defer io_uring
273+ // task work to the application thread rather than a kernel thread, reducing
274+ // cross-CPU signaling overhead. io_uring_get_events() is called before the
275+ // non-blocking CQ check in select() to flush deferred work into the CQ.
276+ #ifdef IORING_SETUP_DEFER_TASKRUN
277+ flags |= IORING_SETUP_DEFER_TASKRUN ;
278+ #endif
279+
280+ int result = io_uring_queue_init (URING_ENTRIES , & selector -> ring , flags );
244281
245282 if (result < 0 ) {
246283 rb_syserr_fail (- result , "IO_Event_Selector_URing_initialize:io_uring_queue_init" );
247284 }
248285
249286 rb_update_max_fd (selector -> ring .ring_fd );
250287
288+ // Interrupt for cross-thread wakeup: another thread calls signal(); the owner
289+ // thread submits an async read before each blocking wait so the ring wakes up
290+ // without the waking thread ever touching the SQ.
291+ IO_Event_Interrupt_open (& selector -> interrupt );
292+ if (selector -> interrupt .descriptor < 0 ) {
293+ io_uring_queue_exit (& selector -> ring );
294+ selector -> ring .ring_fd = -1 ;
295+ rb_sys_fail ("IO_Event_Selector_URing_initialize:IO_Event_Interrupt_open" );
296+ }
297+
251298 return self ;
252299}
253300
@@ -1072,11 +1119,25 @@ void * select_internal(void *_arguments) {
10721119
10731120static
10741121int select_internal_without_gvl (struct select_arguments * arguments ) {
1075- io_uring_submit_flush (arguments -> selector );
1122+ struct IO_Event_Selector_URing * selector = arguments -> selector ;
1123+
1124+ // Submit an async read on the wakeup eventfd before releasing the GVL.
1125+ // When wakeup() writes to the fd the read completes, consuming the counter
1126+ // atomically — no separate poll + drain step required.
1127+ // The address of the interrupt struct serves as a unique sentinel in user_data.
1128+ if (!selector -> wakeup_registered ) {
1129+ struct io_uring_sqe * sqe = io_get_sqe (selector );
1130+ io_uring_prep_read (sqe , IO_Event_Interrupt_descriptor (& selector -> interrupt ), & selector -> wakeup_value , sizeof (selector -> wakeup_value ), 0 );
1131+ io_uring_sqe_set_data (sqe , & selector -> interrupt );
1132+ selector -> wakeup_registered = 1 ;
1133+ selector -> pending += 1 ;
1134+ }
1135+
1136+ io_uring_submit_flush (selector );
10761137
1077- arguments -> selector -> blocked = 1 ;
1138+ selector -> blocked = 1 ;
10781139 rb_thread_call_without_gvl (select_internal , (void * )arguments , RUBY_UBF_IO , 0 );
1079- arguments -> selector -> blocked = 0 ;
1140+ selector -> blocked = 0 ;
10801141
10811142 if (arguments -> result == - ETIME ) {
10821143 arguments -> result = 0 ;
@@ -1115,6 +1176,14 @@ unsigned select_process_completions(struct IO_Event_Selector_URing *selector) {
11151176 continue ;
11161177 }
11171178
1179+ // Interrupt read completion — the read already consumed the counter.
1180+ // Clear the flag so the next blocking wait re-submits the read.
1181+ if (cqe -> user_data == (uint64_t )(uintptr_t )& selector -> interrupt ) {
1182+ selector -> wakeup_registered = 0 ;
1183+ io_uring_cq_advance (ring , 1 );
1184+ continue ;
1185+ }
1186+
11181187 struct IO_Event_Selector_URing_Completion * completion = (void * )cqe -> user_data ;
11191188 struct IO_Event_Selector_URing_Waiting * waiting = completion -> waiting ;
11201189
@@ -1157,6 +1226,16 @@ VALUE IO_Event_Selector_URing_select(VALUE self, VALUE duration) {
11571226 // Flush any pending events:
11581227 io_uring_submit_flush (selector );
11591228
1229+ #ifdef IORING_SETUP_DEFER_TASKRUN
1230+ // With DEFER_TASKRUN the kernel holds completions as "deferred task work"
1231+ // rather than placing them directly into the CQ. io_uring_get_events() calls
1232+ // io_uring_enter with IORING_ENTER_GETEVENTS (without blocking) to flush that
1233+ // work into the CQ so the non-blocking select_process_completions below sees them.
1234+ if (selector -> ring .flags & IORING_SETUP_DEFER_TASKRUN ) {
1235+ io_uring_get_events (& selector -> ring );
1236+ }
1237+ #endif
1238+
11601239 int ready = IO_Event_Selector_ready_flush (& selector -> backend );
11611240
11621241 int result = select_process_completions (selector );
@@ -1200,25 +1279,10 @@ VALUE IO_Event_Selector_URing_wakeup(VALUE self) {
12001279 struct IO_Event_Selector_URing * selector = NULL ;
12011280 TypedData_Get_Struct (self , struct IO_Event_Selector_URing , & IO_Event_Selector_URing_Type , selector );
12021281
1203- // If we are blocking, we can schedule a nop event to wake up the selector:
1282+ // Wake the selector by signalling the interrupt. This is safe from any thread
1283+ // and never touches the ring's SQ, which is required for IORING_SETUP_SINGLE_ISSUER.
12041284 if (selector -> blocked ) {
1205- struct io_uring_sqe * sqe = NULL ;
1206-
1207- while (true) {
1208- sqe = io_uring_get_sqe (& selector -> ring );
1209- if (sqe ) break ;
1210-
1211- rb_thread_schedule ();
1212-
1213- // It's possible we became unblocked already, so we can assume the selector has already cycled at least once:
1214- if (!selector -> blocked ) return Qfalse ;
1215- }
1216-
1217- io_uring_prep_nop (sqe );
1218- // If you don't set this line, the SQE will eventually be recycled and have valid user selector which can cause odd behaviour:
1219- io_uring_sqe_set_data (sqe , NULL );
1220- io_uring_submit (& selector -> ring );
1221-
1285+ IO_Event_Interrupt_signal (& selector -> interrupt );
12221286 return Qtrue ;
12231287 }
12241288
@@ -1229,7 +1293,15 @@ VALUE IO_Event_Selector_URing_wakeup(VALUE self) {
12291293
12301294static int IO_Event_Selector_URing_supported_p (void ) {
12311295 struct io_uring ring ;
1232- int result = io_uring_queue_init (32 , & ring , 0 );
1296+
1297+ unsigned int flags = 0 ;
1298+ #ifdef IORING_SETUP_SINGLE_ISSUER
1299+ flags |= IORING_SETUP_SINGLE_ISSUER ;
1300+ #endif
1301+ #ifdef IORING_SETUP_DEFER_TASKRUN
1302+ flags |= IORING_SETUP_DEFER_TASKRUN ;
1303+ #endif
1304+ int result = io_uring_queue_init (32 , & ring , flags );
12331305
12341306 if (result < 0 ) {
12351307 rb_warn ("io_uring_queue_init() was available at compile time but failed at run time: %s\n" , strerror (- result ));
0 commit comments