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)
101105static
102106void 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
10741103static
10751104int 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
12311267static 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