Ruby 4.1.0dev (2026-11-12 revision acc624e47b178e3514d848a949f8e25d4e49e175)
thread_sched_mn.c (acc624e47b178e3514d848a949f8e25d4e49e175)
1/* -*-c-*- */
2/**********************************************************************
3
4 thread_sched_mn.c - the M:N scheduler
5
6 Included by the platform implementation (currently only thread_pthread.c)
7 when USE_MN_THREADS is 1. A platform that cannot run coroutine threads
8 defines it to 0 and supplies the stubs at the bottom of this file itself
9 (see thread_win32.c).
10
11 Most of what is here is platform independent: the coroutine threads
12 themselves, the native thread stack pool, the timer wheel, and the
13 fd -> waiters map. The part that is not is the readiness backend --
14 arming an fd and waiting for events -- which is epoll on Linux and kqueue
15 elsewhere. Those pieces are marked "backend" below; they are the natural
16 seam for a thread_sched_epoll.c / thread_sched_kqueue.c split, and for an
17 IOCP backend that would let Windows run M:N threads too.
18
19**********************************************************************/
20
21#if USE_MN_THREADS
22
23#if HAVE_SYS_EPOLL_H || HAVE_SYS_EVENT_H
24static void timer_thread_unregister_waiting(rb_thread_t *th, int fd, enum thread_sched_waiting_flag flags);
25#endif
26
27static bool
28timer_thread_check_exceed(rb_hrtime_t abs, rb_hrtime_t now)
29{
30 return abs <= now;
31}
32
33static rb_thread_t *
34thread_sched_waiting_thread(struct rb_thread_sched_waiting *w)
35{
36 if (w) {
37 return (rb_thread_t *)((size_t)w - offsetof(rb_thread_t, sched.waiting_reason));
38 }
39 else {
40 return NULL;
41 }
42}
43
44#define FD_WAIT_IO_MASK (thread_sched_waiting_io_read | thread_sched_waiting_io_write)
45
46// Guards the fd map entries and the flags of io waits for its fds. Whoever
47// clears an io wait's flags under its shard owns that wakeup.
48// Lock order: shard -> waiting_lock -> wake_pending_lock.
49static void
50fd_shard_lock(int fd)
51{
52 rb_native_mutex_lock(&timer_th.fd_shard_locks[(unsigned int)fd % IO_WAIT_SHARDS]);
53}
54
55static void
56fd_shard_unlock(int fd)
57{
58 rb_native_mutex_unlock(&timer_th.fd_shard_locks[(unsigned int)fd % IO_WAIT_SHARDS]);
59}
60
61#define TIMER_WHEEL_NO_EXPIRY RB_HRTIME_MAX
62#ifndef TIMER_WHEEL_TICK_MS
63#define TIMER_WHEEL_TICK_MS 1 // L0 slot width; coarser trades sleep accuracy for fewer drains
64#endif
65
66/* Timer wheel over the timed waiters. Every operation runs under
67 * timer_th.waiting_lock.
68 *
69 * Level L buckets deadlines into 64 slots of 64^L ms each; a deadline is
70 * placed by its distance from the drain cursor, so its slot is always
71 * strictly ahead of the cursor at that level. The drain refines a
72 * not-yet-due waiter onto a finer level instead of cascading whole slots,
73 * so one waiter moves at most once per level over its lifetime.
74 *
75 * This is the hierarchical timing wheel of Varghese & Lauck, "Hashed and
76 * Hierarchical Timing Wheels" (SOSP '87). */
77
78static uint64_t
79timer_wheel_tick(rb_hrtime_t hrt)
80{
81 return hrt / (RB_HRTIME_PER_MSEC * TIMER_WHEEL_TICK_MS);
82}
83
84static inline int
85timer_wheel_ctz64(uint64_t v)
86{
87#if defined(__GNUC__) || defined(__clang__)
88 return __builtin_ctzll(v);
89#else
90 int n = 0;
91 while (!(v & 1)) { v >>= 1; n++; }
92 return n;
93#endif
94}
95
96static int
97timer_wheel_level(uint64_t dist_ms)
98{
99 if (dist_ms < ((uint64_t)1 << TIMER_WHEEL_SLOT_BITS)) return 0;
100 if (dist_ms < ((uint64_t)1 << 2 * TIMER_WHEEL_SLOT_BITS)) return 1;
101 if (dist_ms < ((uint64_t)1 << 3 * TIMER_WHEEL_SLOT_BITS)) return 2;
102 return TIMER_WHEEL_LEVELS - 1;
103}
104
105static void
106timer_wheel_insert(struct rb_thread_sched_waiting *w)
107{
108 uint64_t dl_tick = timer_wheel_tick(w->data.timeout);
109 uint64_t cur = timer_th.wheel_cursor_tick;
110
111 /* A deadline at or behind the cursor parks one tick ahead: its own slot
112 * is already drained and would otherwise wait out a full wheel turn. */
113 uint64_t target = dl_tick > cur ? dl_tick : cur + 1;
114 int lvl = timer_wheel_level(target - cur);
115 int shift = lvl * TIMER_WHEEL_SLOT_BITS;
116 uint64_t slot_tick = target >> shift;
117
118 if (lvl == TIMER_WHEEL_LEVELS - 1) {
119 /* Beyond the wheel range: park on the farthest slot; the drain
120 * re-inserts it with the then-smaller distance. */
121 uint64_t far = (cur >> shift) + TIMER_WHEEL_SLOTS - 1;
122 if (slot_tick > far) slot_tick = far;
123 }
124
125 int slot = (int)(slot_tick & (TIMER_WHEEL_SLOTS - 1));
126
127 ccan_list_add_tail(&timer_th.wheel[lvl].slots[slot], &w->node);
128 timer_th.wheel[lvl].occupied |= UINT64_C(1) << slot;
129 w->wheel_lvl = (uint8_t)lvl;
130 w->wheel_slot = (uint8_t)slot;
131
132 if (w->data.timeout < timer_th.next_expiry) {
133 timer_th.next_expiry = w->data.timeout;
134 }
135}
136
137// Unlink a waiter from the wheel slot or the untimed list it is on.
138static void
139timer_wheel_del(struct rb_thread_sched_waiting *w)
140{
141 ccan_list_del_init(&w->node);
142
143 if (w->flags & thread_sched_waiting_timeout) {
144 struct timer_wheel_level *lv = &timer_th.wheel[w->wheel_lvl];
145 if (ccan_list_empty(&lv->slots[w->wheel_slot])) {
146 lv->occupied &= ~(UINT64_C(1) << w->wheel_slot);
147 }
148 }
149}
150
151/* A lower bound on the earliest deadline: the start time of the nearest
152 * occupied slot per level. Never later than any real deadline, so waking
153 * by it is at worst early, which is harmless. */
154static rb_hrtime_t
155timer_wheel_next_expiry(void)
156{
157 rb_hrtime_t best = TIMER_WHEEL_NO_EXPIRY;
158
159 for (int lvl = 0; lvl < TIMER_WHEEL_LEVELS; lvl++) {
160 uint64_t occ = timer_th.wheel[lvl].occupied;
161 if (!occ) continue;
162
163 int shift = lvl * TIMER_WHEEL_SLOT_BITS;
164 uint64_t cur_tick = timer_th.wheel_cursor_tick >> shift;
165 unsigned base = (unsigned)((cur_tick + 1) & (TIMER_WHEEL_SLOTS - 1));
166 // rotate so bit k = slot for tick cur_tick+1+k
167 uint64_t rot = (occ >> base) | (base ? (occ << (TIMER_WHEEL_SLOTS - base)) : 0);
168 uint64_t tick = cur_tick + 1 + timer_wheel_ctz64(rot);
169 rb_hrtime_t start = (rb_hrtime_t)(tick << shift) * RB_HRTIME_PER_MSEC * TIMER_WHEEL_TICK_MS;
170
171 if (start < best) best = start;
172 }
173
174 return best;
175}
176
177/* Drain every slot whose tick moved behind `now`, collecting due waiters
178 * onto `expired` and re-bucketing not-yet-due ones onto a finer level.
179 * timer_th.waiting_lock must be held. */
180static void
181timer_wheel_drain(rb_hrtime_t now, uint64_t now_tick, struct ccan_list_head *expired)
182{
183 uint64_t prev_tick = timer_th.wheel_cursor_tick;
184 timer_th.wheel_cursor_tick = now_tick; // re-inserts below map against the new cursor
185 /* A deadline inside the current tick parks one tick ahead, so its slot
186 * start is later than the deadline. Keep the exact value: the recompute
187 * below only knows slot starts, and next_expiry must not exceed a deadline. */
188 rb_hrtime_t reinserted = TIMER_WHEEL_NO_EXPIRY;
189
190 for (int lvl = 0; lvl < TIMER_WHEEL_LEVELS; lvl++) {
191 int shift = lvl * TIMER_WHEEL_SLOT_BITS;
192 uint64_t from = prev_tick >> shift;
193 uint64_t to = now_tick >> shift;
194
195 if (to == from) break; // no boundary crossed; coarser levels crossed none either
196
197 uint64_t steps = to - from;
198 if (steps > TIMER_WHEEL_SLOTS) steps = TIMER_WHEEL_SLOTS;
199 struct timer_wheel_level *lv = &timer_th.wheel[lvl];
200
201 for (uint64_t tick = to - steps + 1; tick <= to; tick++) {
202 int slot = (int)(tick & (TIMER_WHEEL_SLOTS - 1));
203
204 if (!(lv->occupied & (UINT64_C(1) << slot))) continue;
205 lv->occupied &= ~(UINT64_C(1) << slot);
206
207 /* Detach first: a far-future waiter re-clamped by the insert below
208 * can land back on this very slot and must not be drained again. */
209 struct ccan_list_head pending;
210 ccan_list_head_init(&pending);
211 ccan_list_append_list(&pending, &lv->slots[slot]);
212
213 struct rb_thread_sched_waiting *w;
214 while ((w = ccan_list_pop(&pending, struct rb_thread_sched_waiting, node)) != NULL) {
215 if (timer_thread_check_exceed(w->data.timeout, now)) {
216 RUBY_DEBUG_LOG("expired th:%u", rb_th_serial(thread_sched_waiting_thread(w)));
217
218 /* flags stay set until the wakeup takes them under the
219 * owning lock (the fd shard for io waits, this one
220 * otherwise): a waiter whose flags are already cleared may
221 * run and re-register through this same `w`, which would
222 * relink the node we are still holding on `expired`. */
223 ccan_list_add_tail(expired, &w->node);
224 }
225 else {
226 /* arrived early: the distance shrank, so this lands on a
227 * finer level (or a farther slot of the coarsest one) */
228 if (w->data.timeout < reinserted) reinserted = w->data.timeout;
229 timer_wheel_insert(w);
230 }
231 }
232 }
233 }
234
235 timer_th.next_expiry = timer_wheel_next_expiry();
236 if (reinserted < timer_th.next_expiry) timer_th.next_expiry = reinserted;
237}
238
239/* Merge the wheel's next expiry into the poll timeout (ms; -1 = none).
240 * Checked even when the scheduler has other work (grq_cnt > 0). */
241static int
242timer_wheel_timeout(int timeout)
243{
244 rb_native_mutex_lock(&timer_th.waiting_lock);
245 {
246 if (timer_th.next_expiry != TIMER_WHEEL_NO_EXPIRY) {
247 rb_hrtime_t now = rb_hrtime_now();
248 rb_hrtime_t hrrel = rb_hrtime_sub(timer_th.next_expiry, now);
249
250 RUBY_DEBUG_LOG("now:%lu rel:%lu", (unsigned long)now, (unsigned long)hrrel);
251
252 rb_hrtime_t msec = (hrrel + RB_HRTIME_PER_MSEC - 1) / RB_HRTIME_PER_MSEC;
253 // A deadline further away than INT_MAX ms must clamp, not truncate:
254 // a negative timeout would be an untimed epoll_wait.
255 int thread_timeout = msec > INT_MAX ? INT_MAX : (int)msec; // ms
256
257 // Use minimum of scheduler timeout and thread sleep timeout
258 if (timeout < 0 || thread_timeout < timeout) {
259 timeout = thread_timeout;
260 }
261 }
262 }
263 rb_native_mutex_unlock(&timer_th.waiting_lock);
264
265 return timeout;
266}
267
268static void
269timer_thread_wakeup_thread_locked(struct rb_thread_sched *sched, rb_thread_t *th, uint32_t event_serial)
270{
271 if (sched->running != th && th->sched.event_serial == event_serial) {
272 thread_sched_to_ready_common(sched, th, true, false);
273 }
274}
275
276static void
277timer_thread_wakeup_thread(rb_thread_t *th, uint32_t event_serial)
278{
279 RUBY_DEBUG_LOG("th:%u", rb_th_serial(th));
280 struct rb_thread_sched *sched = TH_SCHED(th);
281
282 thread_sched_lock(sched, th);
283 {
284 timer_thread_wakeup_thread_locked(sched, th, event_serial);
285 }
286 thread_sched_unlock(sched, th);
287}
288
289#define TIMEOUT_WAKE_BATCH 16
290
291// One thread the timer thread is about to wake, with the serial it was armed at.
292struct timer_wake { rb_thread_t *th; uint32_t serial; };
293
294// Count a pending wake against a thread, so a dying thread can wait them out
295// (timer_thread_wake_fence). A count, not a flag: an expiry hold and an fd
296// event's wake can be pending on one thread at once. Take it while the lock
297// that pinned the thread (shard or waiting_lock) is still held.
298static void
299timer_wake_pending_inc(rb_thread_t *th)
300{
301 rb_native_mutex_lock(&timer_th.wake_pending_lock);
302 th->sched.wake_pending_cnt++;
303 rb_native_mutex_unlock(&timer_th.wake_pending_lock);
304}
305
306static void
307timer_wake_pending_dec(rb_thread_t *th)
308{
309 rb_native_mutex_lock(&timer_th.wake_pending_lock);
310 VM_ASSERT(th->sched.wake_pending_cnt > 0);
311 th->sched.wake_pending_cnt--;
312 rb_native_cond_broadcast(&timer_th.wake_pending_cond);
313 rb_native_mutex_unlock(&timer_th.wake_pending_lock);
314}
315
316static void
317timer_wake_pending_clear(struct timer_wake *batch, int n)
318{
319 for (int i = 0; i < n; i++) {
320 timer_wake_pending_dec(batch[i].th);
321 }
322}
323
324// Wait out pending wakes before a thread is freed: they would touch freed
325// memory, or wake a reused thread whose first serial matches a stale entry.
326static void
327timer_thread_wake_fence(rb_thread_t *th)
328{
329 if (!TIMER_THREAD_CREATED_P()) return;
330
331 rb_native_mutex_lock(&timer_th.wake_pending_lock);
332 while (th->sched.wake_pending_cnt > 0) {
333 rb_native_cond_wait(&timer_th.wake_pending_cond, &timer_th.wake_pending_lock);
334 }
335 rb_native_mutex_unlock(&timer_th.wake_pending_lock);
336}
337
338static void
339timer_thread_check_timeout(rb_vm_t *vm)
340{
341 rb_hrtime_t now = rb_hrtime_now();
342 uint64_t now_tick = timer_wheel_tick(now);
343 struct ccan_list_head expired;
344
345 ccan_list_head_init(&expired);
346
347 struct timer_wake batch[TIMEOUT_WAKE_BATCH];
348 bool more = true;
349
350 while (more) {
351 int n = 0;
352 struct { rb_thread_t *th; uint32_t serial; int fd; } io_claims[TIMEOUT_WAKE_BATCH];
353 int n_io = 0;
354
355 rb_native_mutex_lock(&timer_th.waiting_lock);
356 {
357 // A second pass finds the cursor already at now_tick and drains nothing.
358 if (now_tick > timer_th.wheel_cursor_tick) {
359 timer_wheel_drain(now, now_tick, &expired);
360 }
361
362 struct rb_thread_sched_waiting *w;
363 while (n + n_io < TIMEOUT_WAKE_BATCH &&
364 (w = ccan_list_pop(&expired, struct rb_thread_sched_waiting, node)) != NULL) {
365 // Name the thread and its serial here, then release it: once the
366 // flags are clear the thread may run and re-register through `w`,
367 // and a serial read after that would match the new registration.
368 // the pop leaves the node dangling; a concurrent claimer's
369 // wheel del (under this lock) must find it self-linked
370 ccan_list_node_init(&w->node);
371
372 if (w->flags & FD_WAIT_IO_MASK) {
373 // The fd shard owns these flags; claim below, once this
374 // lock is dropped. The pending count pins the thread: it
375 // was parked when we popped its node (a claimed entry
376 // leaves the wheel before its thread can wake).
377 io_claims[n_io].th = thread_sched_waiting_thread(w);
378 io_claims[n_io].serial = w->data.event_serial;
379 io_claims[n_io].fd = w->data.fd;
380 timer_wake_pending_inc(io_claims[n_io].th);
381 n_io++;
382 }
383 else {
384 // pin before the flags clear; see timer_thread_wake_fd_waiters
385 batch[n].th = thread_sched_waiting_thread(w);
386 batch[n].serial = w->data.event_serial;
387 timer_wake_pending_inc(batch[n].th);
388 w->flags = thread_sched_waiting_none;
389 w->data.result = 0;
390 n++;
391 }
392 }
393 more = !ccan_list_empty(&expired);
394 }
395 rb_native_mutex_unlock(&timer_th.waiting_lock);
396
397 for (int i = 0; i < n; i++) {
398 timer_thread_wakeup_thread(batch[i].th, batch[i].serial);
399 }
400 timer_wake_pending_clear(batch, n);
401
402 // The io entries race the fd event and the ubf for their flags.
403 for (int i = 0; i < n_io; i++) {
404 rb_thread_t *th = io_claims[i].th;
405 int fd = io_claims[i].fd;
406 bool claimed = false;
407
408 fd_shard_lock(fd);
409 {
410 struct rb_thread_sched_waiting *w = &th->sched.waiting_reason;
411
412 if (w->flags != thread_sched_waiting_none &&
413 w->data.event_serial == io_claims[i].serial) {
414 VM_ASSERT(w->data.fd == fd);
415 timer_thread_unregister_waiting(th, fd, w->flags);
416 w->flags = thread_sched_waiting_none;
417 w->data.result = 0;
418 claimed = true;
419 }
420 }
421 fd_shard_unlock(fd);
422
423 if (claimed) {
424 timer_thread_wakeup_thread(th, io_claims[i].serial);
425 }
426 timer_wake_pending_dec(th);
427 }
428 }
429}
430
431static bool
432timer_thread_cancel_waiting(rb_thread_t *th)
433{
434 struct rb_thread_sched_waiting *w = &th->sched.waiting_reason;
435
436 while (1) {
437 // Racy routing read; the claim is re-verified under the owning lock.
438 enum thread_sched_waiting_flag flags = w->flags;
439
440 if (flags == thread_sched_waiting_none) {
441 return false;
442 }
443 else if (flags & FD_WAIT_IO_MASK) {
444 int fd = w->data.fd;
445
446 fd_shard_lock(fd);
447 if ((w->flags & FD_WAIT_IO_MASK) && w->data.fd == fd) {
448 if (w->flags & thread_sched_waiting_timeout) {
449 rb_native_mutex_lock(&timer_th.waiting_lock);
450 timer_wheel_del(w);
451 rb_native_mutex_unlock(&timer_th.waiting_lock);
452 }
453 timer_thread_unregister_waiting(th, fd, w->flags);
454 w->flags = thread_sched_waiting_none;
455 fd_shard_unlock(fd);
456 return true;
457 }
458 fd_shard_unlock(fd);
459 }
460 else {
461 rb_native_mutex_lock(&timer_th.waiting_lock);
462 if (w->flags && !(w->flags & FD_WAIT_IO_MASK)) {
463 timer_wheel_del(w);
464 w->flags = thread_sched_waiting_none;
465 rb_native_mutex_unlock(&timer_th.waiting_lock);
466 return true;
467 }
468 rb_native_mutex_unlock(&timer_th.waiting_lock);
469 }
470 // lost the routing race; look again
471 }
472}
473
474static void
475ubf_event_waiting(void *ptr)
476{
477 rb_thread_t *th = (rb_thread_t *)ptr;
478 struct rb_thread_sched *sched = TH_SCHED(th);
479
480 RUBY_DEBUG_LOG("th:%u", rb_th_serial(th));
481
482 VM_ASSERT(th->nt == NULL || !th_has_dedicated_nt(th));
483
484 // only once. it is safe because th->interrupt_lock is already acquired.
485 th->unblock.func = NULL;
486 th->unblock.arg = NULL;
487
488 thread_sched_lock(sched, th);
489 {
490 bool canceled = timer_thread_cancel_waiting(th);
491
492 if (sched->running == th) {
493 RUBY_DEBUG_LOG("not waiting yet");
494 }
495 else if (canceled) {
496 thread_sched_to_ready_common(sched, th, true, false);
497 }
498 else {
499 RUBY_DEBUG_LOG("already not waiting");
500 }
501 }
502 thread_sched_unlock(sched, th);
503}
504
505// Why timer_thread_register_waiting() did or did not take over the wait. Both
506// "not registered" cases used to share one value; only the first means ready.
507enum timer_thread_register_result {
508 timer_thread_registered, // the timer thread owns this wait now
509 timer_thread_already_ready, // no wait needed: the fd is ready, or no timeout
510 timer_thread_unavailable, // cannot be registered; the caller must fall back
511};
512
513static enum timer_thread_register_result
514timer_thread_register_waiting(rb_thread_t *th, int fd, enum thread_sched_waiting_flag flags, rb_hrtime_t *rel, uint32_t event_serial);
515
516// Arm a timeout-only wake on the timer thread for a Ractor wait. Returns false
517// if the deadline has already passed, in which case nothing was registered.
518static bool
519ractor_sched_timeout_arm(rb_thread_t *th, const rb_hrtime_t *rel)
520{
521 rb_hrtime_t rel_copy = *rel;
522
523 return timer_thread_register_waiting(th, -1, thread_sched_waiting_timeout, &rel_copy,
524 ++th->sched.event_serial) == timer_thread_registered;
525}
526
527// Returns true if the timeout was still armed, i.e. it did not fire.
528static bool
529ractor_sched_timeout_disarm(rb_thread_t *th)
530{
531 return timer_thread_cancel_waiting(th);
532}
533
534// return how the wait ended; see enum thread_sched_wait_result
535static enum thread_sched_wait_result
536thread_sched_wait_events(struct rb_thread_sched *sched, rb_thread_t *th, int fd, enum thread_sched_waiting_flag events, rb_hrtime_t *rel)
537{
538 VM_ASSERT(!th_has_dedicated_nt(th)); // on SNT
539
540 volatile bool timedout = false, need_cancel = false;
541 volatile enum timer_thread_register_result reg = timer_thread_unavailable;
542
543 uint32_t event_serial = ++th->sched.event_serial; // overflow is okay
544
545
546 thread_sched_lock(sched, th);
547 {
548 // NOTE: there's a lock ordering inversion here with the ubf call, but it's benign.
549 if (ubf_set(th, ubf_event_waiting, (void *)th, NULL)) {
550 // Already interrupted: the fd was never looked at, so this is no event.
551 thread_sched_unlock(sched, th);
552 return thread_sched_wait_interrupted;
553 }
554
555 reg = timer_thread_register_waiting(th, fd, events, rel, event_serial);
556
557 if (reg == timer_thread_registered) {
558 RUBY_DEBUG_LOG("wait fd:%d", fd);
559
560 RB_VM_SAVE_MACHINE_CONTEXT(th);
561
562 RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_SUSPENDED, th);
563
564 if (th->sched.waiting_reason.flags == thread_sched_waiting_none) {
565 th->sched.event_serial++;
566 // timer thread has dequeued us already, but it won't try to wake us because we bumped our serial
567 }
568 else if (RUBY_VM_INTERRUPTED(th->ec)) {
569 th->sched.event_serial++; // make sure timer thread doesn't try to wake us
570 need_cancel = true;
571 }
572 else {
573 RUBY_DEBUG_LOG("sleep");
574
575 // A sleeper's status belongs to the caller: sleep_hrtime
576 // re-sleeps while it stays THREAD_STOPPED and only a waker may
577 // change it, as with native_cond_sleep on a dedicated nt. An
578 // io wait enters as THREAD_RUNNABLE and shows "sleep" while
579 // parked, as a dedicated nt's blocking region does.
580 enum rb_thread_status prev_status = th->status;
581 if (prev_status == THREAD_RUNNABLE) th->status = THREAD_STOPPED_FOREVER;
582 thread_sched_wakeup_next_thread(sched, th, true);
583 thread_sched_wait_running_turn(sched, th, true, NULL);
584 if (prev_status == THREAD_RUNNABLE) th->status = THREAD_RUNNABLE;
585
586 RUBY_DEBUG_LOG("wakeup");
587 }
588
589 timedout = th->sched.waiting_reason.data.result == 0;
590
591 if (need_cancel) {
592 timer_thread_cancel_waiting(th);
593 }
594 }
595 else {
596 // Ready right now, or not registerable at all -- only the former may
597 // be reported as readiness.
598 RUBY_DEBUG_LOG("did not wait fd:%d reg:%d", fd, (int)reg);
599 }
600 }
601 thread_sched_unlock(sched, th);
602
603 // if ubf triggered between sched unlock and ubf clear, sched->running == th here
604 ubf_clear(th, false);
605
606 VM_ASSERT(sched->running == th);
607
608 if (reg == timer_thread_unavailable) return thread_sched_wait_unavailable;
609 // A ready fd never registered, so it never timed out either.
610 if (reg == timer_thread_already_ready) return thread_sched_wait_event;
611 // No event fired and we are interrupted: that is an interrupt, not a timeout.
612 if (timedout && RUBY_VM_INTERRUPTED(th->ec)) return thread_sched_wait_interrupted;
613 return timedout ? thread_sched_wait_timeout : thread_sched_wait_event;
614}
615
617
618static int
619get_sysconf_page_size(void)
620{
621 static long page_size = 0;
622
623 if (UNLIKELY(page_size == 0)) {
624 page_size = sysconf(_SC_PAGESIZE);
625 VM_ASSERT(page_size < INT_MAX);
626 }
627 return (int)page_size;
628}
629
630#define MSTACK_CHUNK_SIZE (512 * 1024 * 1024) // 512MB
631#define MSTACK_PAGE_SIZE get_sysconf_page_size()
632#define MSTACK_CHUNK_PAGE_NUM (MSTACK_CHUNK_SIZE / MSTACK_PAGE_SIZE - 1) // 1 is start redzone
633
634// 512MB chunk
635// 131,072 pages (> 65,536)
636// Head pages hold the chunk header (see start_page); stacks follow.
637
638/*
639 * <--> machine stack + vm stack
640 * ----------------------------------
641 * |HD...|RZ| ... |RZ| ... ... |RZ|
642 * <------------- 512MB ------------->
643 */
644
645static struct nt_stack_chunk_header {
646 struct nt_stack_chunk_header *prev_chunk;
647 struct nt_stack_chunk_header *prev_free_chunk;
648 // prev_free_chunk == NULL cannot double as the membership test: the free
649 // list's tail also has it NULL, and re-pushing the tail self-cycles it.
650 bool on_free_list;
651
652 uint16_t start_page;
653 uint16_t stack_count;
654 uint16_t uninitialized_stack_count;
655
656 uint16_t free_stack_pos;
657 uint16_t free_stack[];
658} *nt_stack_chunks = NULL,
659 *nt_free_stack_chunks = NULL;
660
661struct nt_machine_stack_footer {
662 struct nt_stack_chunk_header *ch;
663 size_t index;
664};
665
666static rb_nativethread_lock_t nt_machine_stack_lock = RB_NATIVETHREAD_LOCK_INIT;
667
668// The holder at the fork moment does not exist in the child; start over.
669static void
670nt_machine_stack_atfork(void)
671{
672 rb_native_mutex_initialize(&nt_machine_stack_lock);
673}
674
675#include <sys/mman.h>
676
677// Page-align the configured sizes: the layout puts a guard page and a
678// MAP_FIXED machine stack behind the VM stack, and both must land on page
679// boundaries whatever RUBY_THREAD_VM_STACK_SIZE and
680// RUBY_THREAD_MACHINE_STACK_SIZE hold (those align only to 4KB).
681static inline size_t
682nt_vm_stack_area(const rb_vm_t *vm)
683{
684 return (size_t)roomof(vm->default_params.thread_vm_stack_size, MSTACK_PAGE_SIZE) * MSTACK_PAGE_SIZE;
685}
686
687static inline size_t
688nt_machine_stack_area(const rb_vm_t *vm)
689{
690 return (size_t)roomof(vm->default_params.thread_machine_stack_size, MSTACK_PAGE_SIZE) * MSTACK_PAGE_SIZE;
691}
692
693// vm stack area + guard page + machine stack area
694static inline size_t
695nt_thread_stack_size(void)
696{
697 static size_t msz;
698 if (LIKELY(msz > 0)) return msz;
699
700 rb_vm_t *vm = GET_VM();
701 msz = nt_vm_stack_area(vm) + MSTACK_PAGE_SIZE + nt_machine_stack_area(vm);
702 return msz;
703}
704
705static struct nt_stack_chunk_header *
706nt_alloc_thread_stack_chunk(void)
707{
708 const char *m = (void *)mmap(NULL, MSTACK_CHUNK_SIZE, PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
709 if (m == MAP_FAILED) {
710 return NULL;
711 }
712
713 ruby_annotate_mmap(m, MSTACK_CHUNK_SIZE, "Ruby:nt_alloc_thread_stack_chunk");
714
715 size_t msz = nt_thread_stack_size();
716 int header_page_cnt = 1;
717 int stack_count = ((MSTACK_CHUNK_PAGE_NUM - header_page_cnt) * MSTACK_PAGE_SIZE) / msz;
718 int ch_size = sizeof(struct nt_stack_chunk_header) + sizeof(uint16_t) * stack_count;
719
720 if (ch_size > MSTACK_PAGE_SIZE * header_page_cnt) {
721 header_page_cnt = (ch_size + MSTACK_PAGE_SIZE - 1) / MSTACK_PAGE_SIZE;
722 stack_count = ((MSTACK_CHUNK_PAGE_NUM - header_page_cnt) * MSTACK_PAGE_SIZE) / msz;
723 }
724
725 VM_ASSERT(stack_count <= UINT16_MAX);
726
727 // Enable read/write for the header pages
728 if (mprotect((void *)m, (size_t)header_page_cnt * MSTACK_PAGE_SIZE, PROT_READ | PROT_WRITE) != 0) {
729 munmap((void *)m, MSTACK_CHUNK_SIZE);
730 return NULL;
731 }
732
733 struct nt_stack_chunk_header *ch = (struct nt_stack_chunk_header *)m;
734
735 ch->start_page = header_page_cnt;
736 ch->prev_chunk = nt_stack_chunks;
737 ch->prev_free_chunk = nt_free_stack_chunks;
738 ch->on_free_list = true; // the caller makes it the free-list head
739 ch->uninitialized_stack_count = ch->stack_count = (uint16_t)stack_count;
740 ch->free_stack_pos = 0;
741
742 RUBY_DEBUG_LOG("ch:%p start_page:%d stack_cnt:%d stack_size:%d", ch, (int)ch->start_page, (int)ch->stack_count, (int)msz);
743
744 return ch;
745}
746
747static void *
748nt_stack_chunk_get_stack_start(struct nt_stack_chunk_header *ch, size_t idx)
749{
750 const char *m = (char *)ch;
751 return (void *)(m + ch->start_page * MSTACK_PAGE_SIZE + idx * nt_thread_stack_size());
752}
753
754static struct nt_machine_stack_footer *
755nt_stack_chunk_get_msf(const rb_vm_t *vm, const char *mstack)
756{
757 // TODO: stack direction
758 const size_t msz = vm->default_params.thread_machine_stack_size;
759 return (struct nt_machine_stack_footer *)&mstack[msz - sizeof(struct nt_machine_stack_footer)];
760}
761
762static void
763nt_stack_chunk_get_stack(const rb_vm_t *vm, struct nt_stack_chunk_header *ch, size_t idx, void **vm_stack, void **machine_stack)
764{
765 // TODO: only support stack going down
766 // [VM ... <GUARD> machine stack ...]
767
768 const char *vstack, *mstack;
769 const char *guard_page;
770 vstack = nt_stack_chunk_get_stack_start(ch, idx);
771 guard_page = vstack + nt_vm_stack_area(vm);
772 mstack = guard_page + MSTACK_PAGE_SIZE;
773
774 struct nt_machine_stack_footer *msf = nt_stack_chunk_get_msf(vm, mstack);
775 msf->ch = ch;
776 msf->index = idx;
777
778#if 0
779 RUBY_DEBUG_LOG("msf:%p vstack:%p-%p guard_page:%p-%p mstack:%p-%p", msf,
780 vstack, (void *)(guard_page-1),
781 guard_page, (void *)(mstack-1),
782 mstack, (void *)(msf));
783#endif
784
785 *vm_stack = (void *)vstack;
786 *machine_stack = (void *)mstack;
787}
788
790static void
791nt_stack_chunk_dump(void)
792{
793 struct nt_stack_chunk_header *ch;
794 int i;
795
796 fprintf(stderr, "** nt_stack_chunks\n");
797 ch = nt_stack_chunks;
798 for (i=0; ch; i++, ch = ch->prev_chunk) {
799 fprintf(stderr, "%d %p free_pos:%d\n", i, (void *)ch, (int)ch->free_stack_pos);
800 }
801
802 fprintf(stderr, "** nt_free_stack_chunks\n");
803 ch = nt_free_stack_chunks;
804 for (i=0; ch; i++, ch = ch->prev_free_chunk) {
805 fprintf(stderr, "%d %p free_pos:%d\n", i, (void *)ch, (int)ch->free_stack_pos);
806 }
807}
808
809static int
810nt_alloc_stack(rb_vm_t *vm, void **vm_stack, void **machine_stack)
811{
812 int err = 0;
813
814 rb_native_mutex_lock(&nt_machine_stack_lock);
815 {
816 retry:
817 if (nt_free_stack_chunks) {
818 struct nt_stack_chunk_header *ch = nt_free_stack_chunks;
819 if (ch->free_stack_pos > 0) {
820 RUBY_DEBUG_LOG("free_stack_pos:%d", ch->free_stack_pos);
821 nt_stack_chunk_get_stack(vm, ch, ch->free_stack[--ch->free_stack_pos], vm_stack, machine_stack);
822 }
823 else if (ch->uninitialized_stack_count > 0) {
824 RUBY_DEBUG_LOG("uninitialized_stack_count:%d", ch->uninitialized_stack_count);
825
826 size_t idx = ch->stack_count - ch->uninitialized_stack_count--;
827
828 // The chunk was mapped PROT_NONE; enable the VM stack and
829 // machine stack pages, leaving the guard page as PROT_NONE.
830 char *stack_start = nt_stack_chunk_get_stack_start(ch, idx);
831 size_t vm_stack_area = nt_vm_stack_area(vm);
832 size_t mstack_size = nt_thread_stack_size() - vm_stack_area - MSTACK_PAGE_SIZE;
833 char *mstack_start = stack_start + vm_stack_area + MSTACK_PAGE_SIZE;
834
835 int mstack_flags = MAP_FIXED | MAP_ANONYMOUS | MAP_PRIVATE;
836#if defined(MAP_STACK) && !defined(__FreeBSD__)
837 mstack_flags |= MAP_STACK;
838#endif
839
840 if (mprotect(stack_start, vm_stack_area, PROT_READ | PROT_WRITE) != 0 ||
841 mmap(mstack_start, mstack_size, PROT_READ | PROT_WRITE, mstack_flags, -1, 0) == MAP_FAILED) {
842 err = errno;
843 ch->uninitialized_stack_count++; // the slot was not consumed
844 }
845 else {
846 nt_stack_chunk_get_stack(vm, ch, idx, vm_stack, machine_stack);
847 }
848 }
849 else {
850 nt_free_stack_chunks = ch->prev_free_chunk;
851 ch->prev_free_chunk = NULL;
852 ch->on_free_list = false;
853 goto retry;
854 }
855 }
856 else {
857 struct nt_stack_chunk_header *p = nt_alloc_thread_stack_chunk();
858 if (p == NULL) {
859 err = errno;
860 }
861 else {
862 nt_free_stack_chunks = nt_stack_chunks = p;
863 goto retry;
864 }
865 }
866 }
867 rb_native_mutex_unlock(&nt_machine_stack_lock);
868
869 return err;
870}
871
872static void
873nt_madvise_free_or_dontneed(void *addr, size_t len)
874{
875 /* There is no real way to perform error handling here. Both MADV_FREE
876 * and MADV_DONTNEED are both documented to pretty much only return EINVAL
877 * for a huge variety of errors. It's indistinguishable if madvise fails
878 * because the parameters were bad, or because the kernel we're running on
879 * does not support the given advice. This kind of free-but-don't-unmap
880 * is best-effort anyway, so don't sweat it.
881 *
882 * n.b. A very common case of "the kernel doesn't support MADV_FREE and
883 * returns EINVAL" is running under the `rr` debugger; it makes all
884 * MADV_FREE calls return EINVAL. */
885
886#if defined(MADV_FREE)
887 int r = madvise(addr, len, MADV_FREE);
888 // Return on success, or else try MADV_DONTNEED
889 if (r == 0) return;
890#endif
891#if defined(MADV_DONTNEED)
892 madvise(addr, len, MADV_DONTNEED);
893#endif
894}
895
896static void
897nt_free_stack(void *mstack)
898{
899 if (!mstack) return;
900
901 rb_native_mutex_lock(&nt_machine_stack_lock);
902 {
903 struct nt_machine_stack_footer *msf = nt_stack_chunk_get_msf(GET_VM(), mstack);
904 struct nt_stack_chunk_header *ch = msf->ch;
905 int idx = (int)msf->index;
906 void *stack = nt_stack_chunk_get_stack_start(ch, idx);
907
908 RUBY_DEBUG_LOG("stack:%p mstack:%p ch:%p index:%d", stack, mstack, ch, idx);
909
910 if (!ch->on_free_list) {
911 ch->on_free_list = true;
912 ch->prev_free_chunk = nt_free_stack_chunks;
913 nt_free_stack_chunks = ch;
914 }
915 ch->free_stack[ch->free_stack_pos++] = idx;
916
917 // clear the stack pages
918 nt_madvise_free_or_dontneed(stack, nt_thread_stack_size());
919 }
920 rb_native_mutex_unlock(&nt_machine_stack_lock);
921}
922
923static bool
924mn_threads_enabled_p(void)
925{
926 return mn_threads_mode >= 0;
927}
928
929static int
930native_thread_check_and_create_shared(rb_vm_t *vm)
931{
932 bool need_to_make = false;
933
934 if (!mn_threads_enabled_p()) return 0; // no thread is M:N: the pool serves nobody
935
936 ractor_sched_lock(vm, NULL); // NULL: the timer thread also calls this
937 {
938 unsigned int schedulable_ractor_cnt = vm->ractor.cnt;
939 RUBY_ASSERT(schedulable_ractor_cnt >= 1);
940
941 if (!vm->ractor.main_ractor->threads.sched.enable_mn_threads)
942 schedulable_ractor_cnt--; // do not need snt for main ractor
943
944 // CAS keeps a concurrent rejoin from pushing snt_cnt past the cap
945 rb_atomic_t snt_cnt = RUBY_ATOMIC_LOAD(vm->ractor.sched.snt_cnt);
946 while (((int)snt_cnt < MINIMUM_SNT) ||
947 (snt_cnt < schedulable_ractor_cnt &&
948 snt_cnt < vm->ractor.sched.max_cpu)) {
949 rb_atomic_t prev = RUBY_ATOMIC_CAS(vm->ractor.sched.snt_cnt, snt_cnt, snt_cnt + 1);
950 if (prev == snt_cnt) {
951 need_to_make = true;
952 break;
953 }
954 snt_cnt = prev;
955 }
956
957 if (need_to_make) {
958 RUBY_DEBUG_LOG("added snt:%u dnt:%u ractor_cnt:%u grq_cnt:%u",
959 vm->ractor.sched.snt_cnt,
960 vm->ractor.sched.dnt_cnt,
961 vm->ractor.cnt,
962 vm->ractor.sched.grq_cnt);
963 }
964 else {
965 RUBY_DEBUG_LOG("snt:%d ractor_cnt:%d", (int)vm->ractor.sched.snt_cnt, (int)vm->ractor.cnt);
966 }
967 }
968 ractor_sched_unlock(vm, NULL);
969
970 if (need_to_make) {
971 struct rb_native_thread *nt = native_thread_alloc();
972 nt->vm = vm;
973 int err = native_thread_create0(nt);
974 if (err) {
975 // Roll back, or this function would conclude forever that the
976 // pool is wide enough and never try again.
977 ractor_sched_lock(vm, NULL);
978 RUBY_ATOMIC_DEC(vm->ractor.sched.snt_cnt);
979 ractor_sched_unlock(vm, NULL);
980 native_thread_destroy(nt);
981 }
982 return err;
983 }
984 else {
985 return 0;
986 }
987}
988
989// A coroutine thread's epilogue, run from thread_start_func_2 while th is
990// still valid. Everything that touches th happens here; co_start then only
991// makes the final transfer, touching the execution-owned tctx alone.
992static void
993coroutine_thread_terminated(rb_thread_t *th)
994{
995 struct rb_thread_context *tctx = (struct rb_thread_context *)th->sched.context;
996 struct rb_thread_sched *sched = TH_SCHED(th);
997 rb_ractor_t *r = th->ractor;
998 bool last = (th->invoke_type == thread_invoke_type_ractor_proc);
999 bool is_dnt = th_has_dedicated_nt(th);
1000
1001 // VM destruct tears down the heap and then unsets ruby_current_vm_ptr;
1002 // this native thread still frees the dead context afterwards (the nt
1003 // loop's reclaim uses ruby_xfree and the stack-pool geometry via
1004 // GET_VM()). Make destruct wait until the reclaim finished. (Observed:
1005 // an assert_separately child exiting right after a Ractor finished
1006 // crashed at GET_VM()->default_params, offset 0x2600, on two arches.)
1007 rb_vm_t *const vm = th->vm; // survives th; the tail below must not read th
1008 RUBY_ATOMIC_INC(vm->ractor.sched.winding_cnt);
1009
1010 rb_thread_t *wake_th;
1011 bool wake_mn = false;
1012
1013 // Leave the living set here, while th is still barrier-registered and no
1014 // successor can run: the GC's root scan walks r->threads.set without the
1015 // Ractor lock, so the unlink must not race with it. Off the set th would
1016 // be unreachable although the handoff below keeps using it (and the GC can
1017 // run: to_dead_common() deregisters th, so no barrier waits for it) --
1018 // dying_th keeps it marked until its last use.
1019 VM_ASSERT(sched->running == th); // th owns the slot through the handoff
1020 if (!last) {
1021 RUBY_ATOMIC_PTR_SET(r->threads.dying_th, th);
1022 rb_ractor_living_threads_remove(r, th);
1023 }
1024
1025 thread_sched_lock(sched, th);
1026 {
1027 // designate the successor (running = next from readyq, or NULL); for
1028 // a dedicated nt (will_switch false) this also enqueues the Ractor.
1029 thread_sched_to_dead_common(sched, th);
1030
1031 // Only the successor WE designated here may be enqueued by this
1032 // epilogue (below). If readyq was empty, running is now NULL and a
1033 // waker (e.g. the timer thread) that later installs a runnable
1034 // thread enqueues the Ractor itself -- enqueuing "whatever is
1035 // running" at that point would duplicate its entry.
1036 wake_th = is_dnt ? NULL : sched->running;
1037 // Read wake_th->nt under the lock: a dedicated successor was already
1038 // woken by to_dead_common and may die (freeing wake_th) as soon as we
1039 // unlock. An M:N successor (nt == NULL) cannot run or be assigned an
1040 // nt before our enqueue below, so the value cannot go stale.
1041 wake_mn = (wake_th != NULL && wake_th->nt == NULL);
1042
1043 tctx->nt = th->nt; // stash the final transfer target for co_start
1044 native_thread_assign(NULL, th);
1045 th->sched.context = NULL; // the wrapper's dfree must not reclaim tctx
1046
1047 if (!last) {
1048 // Still under the sched lock: a successor (even a dedicated one
1049 // woken by to_dead_common) starts by taking it, so it cannot
1050 // observe or overwrite these until we unlock. th was last used
1051 // above and running_ec no longer points into it; now it may be
1052 // collected.
1053 rb_ractor_set_current_ec(r, NULL); // r alive: it has other threads
1054 VM_ASSERT(RUBY_ATOMIC_PTR_LOAD(r->threads.dying_th) == th);
1055 RUBY_ATOMIC_PTR_SET(r->threads.dying_th, NULL);
1056 }
1057 }
1058 if (last) {
1059 thread_sched_unlock(sched, th); // th is still on the living set here
1060
1061 VM_ASSERT(sched->running == NULL);
1062 VM_ASSERT(wake_th == NULL);
1063 // Last access to th/r: the removal may unlink the Ractor, after
1064 // which the GC may collect th and r.
1065 rb_ractor_living_threads_remove(r, th);
1066 rb_current_ec_set(NULL); // TLS only; r may be collectable already
1067 }
1068 else {
1069 // th lost its root at the clear above; the plain unlock's debug log
1070 // would read th->serial.
1071 thread_sched_unlock_no_log(sched, th);
1072
1073 if (wake_mn) {
1074 // enqueue the successor designated above -- exactly once per
1075 // "runnable but unserved" period, by its designator.
1076 thread_sched_lock(sched, NULL);
1077 ractor_sched_enq(vm, r);
1078 thread_sched_unlock(sched, NULL);
1079 }
1080 }
1081}
1082
1083#ifdef __APPLE__
1084# define co_start ruby_coroutine_start
1085#else
1086static
1087#endif
1088COROUTINE
1089co_start(struct coroutine_context *from, struct coroutine_context *self)
1090{
1091#ifdef RUBY_ASAN_ENABLED
1092 __sanitizer_finish_switch_fiber(self->fake_stack,
1093 (const void**)&from->stack_base, &from->stack_size);
1094#endif
1095
1096 rb_thread_t *th = (rb_thread_t *)self->argument;
1097 struct rb_thread_sched *sched = TH_SCHED(th);
1098 VM_ASSERT(th->nt != NULL);
1099 VM_ASSERT(th == sched->running);
1100 VM_ASSERT(sched->lock_owner == NULL);
1101
1102 // RUBY_DEBUG_LOG("th:%u", rb_th_serial(th));
1103
1104 thread_sched_set_locked(sched, th);
1105 thread_sched_add_running_thread(TH_SCHED(th), th);
1106 thread_sched_unlock(sched, th);
1107 {
1108 RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_RESUMED, th);
1109 call_thread_start_func_2(th);
1110 }
1111 // Thread is terminated. coroutine_thread_terminated (run from
1112 // thread_start_func_2 while th was still valid) already left the living
1113 // set and stashed the transfer target, so th / its Ractor may already be
1114 // collected. Only tctx is touched here: mark it dead and transfer back to
1115 // this native thread's own context, where the nt loop reclaims tctx.
1116 struct rb_thread_context *tctx = (struct rb_thread_context *)self;
1117 tctx->dead = true;
1118 // Hand this context to the nt's loop, which reclaims it right after the
1119 // final transfer returns from switch0.
1120 tctx->nt->dead_co = &tctx->co;
1121 coroutine_transfer0(&tctx->co, tctx->nt->nt_context, true);
1122
1123 rb_bug("unreachable");
1124}
1125
1126static int
1127native_thread_create_shared(rb_thread_t *th)
1128{
1129 // setup coroutine
1130 rb_vm_t *vm = th->vm;
1131 void *vm_stack = NULL, *machine_stack = NULL;
1132 int err = nt_alloc_stack(vm, &vm_stack, &machine_stack);
1133 if (err) return err;
1134
1135 VM_ASSERT(vm_stack < machine_stack);
1136
1137 // setup vm stack
1138 size_t vm_stack_words = th->vm->default_params.thread_vm_stack_size/sizeof(VALUE);
1139 rb_ec_initialize_vm_stack(th->ec, vm_stack, vm_stack_words);
1140
1141 // setup machine stack
1142 size_t machine_stack_size = vm->default_params.thread_machine_stack_size - sizeof(struct nt_machine_stack_footer);
1143 th->ec->machine.stack_start = (void *)((uintptr_t)machine_stack + machine_stack_size);
1144 th->ec->machine.stack_maxsize = machine_stack_size; // TODO
1145 th->sched.context_stack = machine_stack;
1146 th->sched.context_stack_size = machine_stack_size;
1147
1148 struct rb_thread_context *tctx = ruby_xmalloc(sizeof(struct rb_thread_context));
1149 tctx->stack = machine_stack;
1150 tctx->dead = false;
1151 tctx->nt = NULL;
1152 th->sched.context = &tctx->co;
1153 coroutine_initialize(&tctx->co, co_start, machine_stack, machine_stack_size);
1154 tctx->co.argument = th;
1155
1156 RUBY_DEBUG_LOG("th:%u vm_stack:%p machine_stack:%p", rb_th_serial(th), vm_stack, machine_stack);
1157
1158 // Widen the pool before publishing th. Once ready, a Ractor's thread that
1159 // runs to its end frees its own rb_thread_t (rb_ractor_postmortem_free),
1160 // and the caller's create-failure path assumes th never became runnable.
1161 int create_err = native_thread_check_and_create_shared(vm);
1162 if (create_err) return create_err;
1163
1164 thread_sched_to_ready(TH_SCHED(th), th);
1165 return 0;
1166}
1167
1169#if HAVE_SYS_EPOLL_H || HAVE_SYS_EVENT_H
1170
1174
1175// (FD_WAIT_IO_MASK and the fd shard helpers are defined near the top.)
1176
1177#define FDMAP_CHUNK_BITS 10
1178#define FDMAP_CHUNK_SIZE (1u << FDMAP_CHUNK_BITS)
1179#define FDMAP_CHUNK_MASK (FDMAP_CHUNK_SIZE - 1)
1180
1181// Callable under any fd shard lock: a chunk spans every shard, so the chunk
1182// table is not guarded by them; slots install by CAS and are never freed.
1183static struct rb_fd_waiters *
1184fd_waiters_lookup(int fd, bool create)
1185{
1186 if (fd < 0) return NULL;
1187
1188 unsigned int ci = (unsigned int)fd >> FDMAP_CHUNK_BITS;
1189 if (ci >= FDMAP_MAX_CHUNKS) return NULL; // the caller falls back to a blocking wait
1190
1191 struct rb_fd_waiters *chunk = RUBY_ATOMIC_PTR_LOAD(timer_th.fdmap_chunks[ci]);
1192
1193 if (chunk == NULL) {
1194 if (!create) return NULL;
1195
1196 chunk = calloc(FDMAP_CHUNK_SIZE, sizeof(*chunk));
1197 if (chunk == NULL) rb_bug("fd_waiters_lookup: calloc failed");
1198
1199 for (unsigned int i = 0; i < FDMAP_CHUNK_SIZE; i++) {
1200 ccan_list_head_init(&chunk[i].waiters);
1201 }
1202
1203 struct rb_fd_waiters *prev = RUBY_ATOMIC_PTR_CAS(timer_th.fdmap_chunks[ci], NULL, chunk);
1204 if (prev != NULL) {
1205 free(chunk); // another shard installed it first
1206 chunk = prev;
1207 }
1208 }
1209
1210 return &chunk[(unsigned int)fd & FDMAP_CHUNK_MASK];
1211}
1212
1213// The fd's shard lock must be held.
1214static uint32_t
1215fd_waiters_union(struct rb_fd_waiters *e)
1216{
1217 uint32_t want = 0;
1218 struct rb_thread_sched_waiting *w;
1219
1220 ccan_list_for_each(&e->waiters, w, fd_node) {
1221 want |= (uint32_t)(w->flags & FD_WAIT_IO_MASK);
1222 }
1223 return want;
1224}
1225
1226// Events name an fd and the generation it was armed with, never a thread: a
1227// stale event then resolves to a table lookup instead of a freed thread.
1228static inline uint64_t
1229fd_event_tag(int fd, uint32_t generation)
1230{
1231 return ((uint64_t)generation << 32) | (uint32_t)fd;
1232}
1233
1234#define FD_EVENT_TAG_FD(tag) ((int)((tag) & 0xffffffffu))
1235#define FD_EVENT_TAG_GEN(tag) ((uint32_t)((tag) >> 32))
1236
1237// Make the backend match `want`. Returns false if the fd cannot be registered
1238// at all (closed, or unsupported by the backend), leaving the entry untouched.
1239// The fd's shard lock must be held. `consumed` says an epoll event for the
1240// current arming was just delivered, so EPOLLONESHOT has already disarmed it.
1241static bool
1242fd_waiters_arm(int fd, struct rb_fd_waiters *e, uint32_t want, bool consumed)
1243{
1244 // After a delivery the kernel side is disarmed even when the flags agree,
1245 // so a consumed call must fall through to re-arm.
1246 if (want == e->armed_flags && !consumed) return true;
1247
1248#if HAVE_SYS_EVENT_H
1249 struct kevent ke[2];
1250 int n = 0;
1251 uint32_t add = want & ~e->armed_flags;
1252 uint32_t del = e->armed_flags & ~want;
1253 void *tag = (void *)(uintptr_t)fd_event_tag(fd, e->generation);
1254
1255 if (del & thread_sched_waiting_io_read) { EV_SET(&ke[n], fd, EVFILT_READ, EV_DELETE, 0, 0, NULL); n++; }
1256 if (del & thread_sched_waiting_io_write) { EV_SET(&ke[n], fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL); n++; }
1257
1258 if (n > 0 && kevent(timer_th.event_fd, ke, n, NULL, 0, NULL) == -1) {
1259 // The fd may already be gone; that is not an error for a removal.
1260 if (errno != ENOENT && errno != EBADF) {
1261 perror("kevent");
1262 rb_bug("fd_waiters_arm/kevent delete failed (fd:%d errno:%d)", fd, errno);
1263 }
1264 }
1265
1266 n = 0;
1267 if (add & thread_sched_waiting_io_read) { EV_SET(&ke[n], fd, EVFILT_READ, EV_ADD, 0, 0, tag); n++; }
1268 if (add & thread_sched_waiting_io_write) { EV_SET(&ke[n], fd, EVFILT_WRITE, EV_ADD, 0, 0, tag); n++; }
1269
1270 if (n > 0 && kevent(timer_th.event_fd, ke, n, NULL, 0, NULL) == -1) {
1271 switch (errno) {
1272 case EBADF:
1273 case EINVAL:
1274 return false;
1275 default:
1276 perror("kevent");
1277 rb_bug("fd_waiters_arm/kevent add failed (fd:%d errno:%d)", fd, errno);
1278 }
1279 }
1280#elif HAVE_SYS_EPOLL_H
1281 if (want == 0) {
1282 // A delivered oneshot event has already disarmed the fd; otherwise DEL
1283 // it. MOD to no events would not do: EPOLLHUP and EPOLLERR are
1284 // reported whatever the mask asks for, and without EPOLLONESHOT they
1285 // are reported level-triggered, so a registration left behind by a
1286 // waiter that gave up (kill, interrupt, timeout) spins the timer
1287 // thread once the fd hangs up -- and a spinning timer thread never
1288 // reaches its timeout branch, the only place that mints an snt.
1289 if (!consumed && e->registered) {
1290 if (epoll_ctl(timer_th.event_fd, EPOLL_CTL_DEL, fd, NULL) == -1) {
1291 switch (errno) {
1292 case EBADF:
1293 case ENOENT:
1294 // the fd is already closed or gone from the set
1295 break;
1296 default:
1297 perror("epoll_ctl");
1298 rb_bug("fd_waiters_arm/epoll_ctl disarm failed (fd:%d errno:%d)", fd, errno);
1299 }
1300 }
1301 e->registered = false;
1302 }
1303 // Anything epoll_wait already queued for the old arming is stale now.
1304 e->generation++;
1305 e->armed_flags = 0;
1306 return true;
1307 }
1308
1309 uint32_t epoll_events = EPOLLONESHOT;
1310 if (want & thread_sched_waiting_io_read) epoll_events |= EPOLLIN;
1311 if (want & thread_sched_waiting_io_write) epoll_events |= EPOLLOUT;
1312
1313 struct epoll_event event = {
1314 .events = epoll_events,
1315 .data = { .u64 = fd_event_tag(fd, e->generation) },
1316 };
1317
1318 int op = e->registered ? EPOLL_CTL_MOD : EPOLL_CTL_ADD;
1319
1320 if (epoll_ctl(timer_th.event_fd, op, fd, &event) == -1) {
1321 switch (errno) {
1322 case ENOENT:
1323 // Not registered after all: the fd was closed and reopened. Add it.
1324 if (op == EPOLL_CTL_MOD &&
1325 epoll_ctl(timer_th.event_fd, EPOLL_CTL_ADD, fd, &event) == 0) {
1326 break;
1327 }
1328 e->registered = false;
1329 return false;
1330 case EEXIST:
1331 // Likewise in the other direction.
1332 if (op == EPOLL_CTL_ADD &&
1333 epoll_ctl(timer_th.event_fd, EPOLL_CTL_MOD, fd, &event) == 0) {
1334 break;
1335 }
1336 return false;
1337 case EBADF:
1338 case EPERM:
1339 // closed, or the fd does not support epoll
1340 e->registered = false;
1341 return false;
1342 default:
1343 perror("epoll_ctl");
1344 rb_bug("fd_waiters_arm/epoll_ctl failed (fd:%d op:%d errno:%d)", fd, op, errno);
1345 }
1346 }
1347 e->registered = true;
1348#else
1349# error "neither kqueue nor epoll"
1350#endif
1351
1352 e->armed_flags = want;
1353 return true;
1354}
1355
1356static bool
1357fd_ready_nonblock(int fd, short events)
1358{
1359 struct pollfd pfd = {
1360 .fd = fd,
1361 .events = events,
1362 };
1363
1364 // A "ready" answer makes the caller report the requested event, so a
1365 // closed fd (POLLNVAL) must not count: it owes the caller EBADF.
1366 return poll(&pfd, 1, 0) > 0 && !(pfd.revents & POLLNVAL);
1367}
1368
1369static void
1370verify_waiting_list(void)
1371{
1372#if VM_CHECK_MODE > 0
1373 struct rb_thread_sched_waiting *w;
1374
1375 for (int lvl = 0; lvl < TIMER_WHEEL_LEVELS; lvl++) {
1376 const struct timer_wheel_level *lv = &timer_th.wheel[lvl];
1377
1378 for (int slot = 0; slot < TIMER_WHEEL_SLOTS; slot++) {
1379 bool occupied = (lv->occupied >> slot) & 1;
1380 VM_ASSERT(occupied == !ccan_list_empty(&lv->slots[slot]));
1381
1382 ccan_list_for_each(&lv->slots[slot], w, node) {
1383 // an io entry's flags belong to its fd shard: do not read them here
1384 if (!(w->flags & FD_WAIT_IO_MASK)) {
1385 VM_ASSERT(w->flags & thread_sched_waiting_timeout);
1386 VM_ASSERT(w->data.timeout != 0);
1387 }
1388 VM_ASSERT(w->wheel_lvl == lvl);
1389 VM_ASSERT(w->wheel_slot == slot);
1390 }
1391 }
1392 }
1393#endif
1394}
1395
1396/* ------------------------------------------------------------------------
1397 * backend: the readiness notification mechanism (epoll / kqueue).
1398 *
1399 * Everything below that names epoll or kqueue is this backend; the rest of
1400 * the M:N scheduler only asks it to arm an fd (fd_waiters_arm) and to wait
1401 * for what fired (event_wait / timer_thread_polling).
1402 * ------------------------------------------------------------------------ */
1403
1404#if HAVE_SYS_EVENT_H // kqueue helpers
1405
1406static enum thread_sched_waiting_flag
1407kqueue_translate_filter_to_flags(int16_t filter)
1408{
1409 switch (filter) {
1410 case EVFILT_READ:
1411 return thread_sched_waiting_io_read;
1412 case EVFILT_WRITE:
1413 return thread_sched_waiting_io_write;
1414 case EVFILT_TIMER:
1415 return thread_sched_waiting_timeout;
1416 default:
1417 rb_bug("kevent filter:%d not supported", filter);
1418 }
1419}
1420
1421static int
1422kqueue_wait(rb_vm_t *vm)
1423{
1424 struct timespec calculated_timeout;
1425 struct timespec *timeout = NULL;
1426 int timeout_ms = timer_thread_set_timeout(vm);
1427
1428 if (timeout_ms > 0) {
1429 calculated_timeout.tv_sec = timeout_ms / 1000;
1430 calculated_timeout.tv_nsec = (timeout_ms % 1000) * 1000000;
1431 timeout = &calculated_timeout;
1432 }
1433 else if (timeout_ms == 0) {
1434 // Relying on the absence of other members of struct timespec is not strictly portable,
1435 // and kevent needs a 0-valued timespec to mean immediate timeout.
1436 memset(&calculated_timeout, 0, sizeof(struct timespec));
1437 timeout = &calculated_timeout;
1438 }
1439
1440 return kevent(timer_th.event_fd, NULL, 0, timer_th.finished_events, KQUEUE_EVENTS_MAX, timeout);
1441}
1442
1443static void
1444kqueue_create(void)
1445{
1446 if ((timer_th.event_fd = kqueue()) == -1) rb_bug("kqueue creation failed (errno:%d)", errno);
1447 int flags = fcntl(timer_th.event_fd, F_GETFD);
1448 if (flags == -1) {
1449 rb_bug("kqueue GETFD failed (errno:%d)", errno);
1450 }
1451
1452 flags |= FD_CLOEXEC;
1453 if (fcntl(timer_th.event_fd, F_SETFD, flags) == -1) {
1454 rb_bug("kqueue SETFD failed (errno:%d)", errno);
1455 }
1456}
1457
1458#endif // HAVE_SYS_EVENT_H
1459
1460// return false if the fd is not waitable or not need to wait.
1461static enum timer_thread_register_result
1462timer_thread_register_waiting(rb_thread_t *th, int fd, enum thread_sched_waiting_flag flags, rb_hrtime_t *rel, uint32_t event_serial)
1463{
1464 RUBY_DEBUG_LOG("th:%u fd:%d flag:%d rel:%lu", rb_th_serial(th), fd, flags, rel ? (unsigned long)*rel : 0);
1465
1466 VM_ASSERT(th == NULL || TH_SCHED(th)->running == th);
1467 VM_ASSERT(flags != 0);
1468
1469 rb_hrtime_t abs = 0; // 0 means no timeout
1470
1471 if (rel) {
1472 if (*rel > 0) {
1473 flags |= thread_sched_waiting_timeout;
1474 }
1475 else {
1476 return timer_thread_already_ready; // zero timeout: nothing to wait for
1477 }
1478 }
1479
1480 if (flags & thread_sched_waiting_timeout) {
1481 VM_ASSERT(rel != NULL);
1482 abs = rb_hrtime_add(rb_hrtime_now(), *rel);
1483 }
1484
1485 if (flags & thread_sched_waiting_io_read) {
1486 if (!(flags & thread_sched_waiting_io_force) && fd_ready_nonblock(fd, POLLIN)) {
1487 RUBY_DEBUG_LOG("fd readable");
1488 return timer_thread_already_ready;
1489 }
1490 VM_ASSERT(fd >= 0);
1491 }
1492
1493 if (flags & thread_sched_waiting_io_write) {
1494 if (!(flags & thread_sched_waiting_io_force) && fd_ready_nonblock(fd, POLLOUT)) {
1495 RUBY_DEBUG_LOG("fd writable");
1496 return timer_thread_already_ready;
1497 }
1498 VM_ASSERT(fd >= 0);
1499 }
1500
1501 if (flags & FD_WAIT_IO_MASK) {
1502 fd_shard_lock(fd);
1503 {
1504 struct rb_fd_waiters *e = fd_waiters_lookup(fd, true);
1505
1506 if (e == NULL) { // fd beyond the map: fall back to a blocking wait
1507 fd_shard_unlock(fd);
1508 return timer_thread_unavailable;
1509 }
1510
1511 // Arm the union of what this fd's waiters want, so a second waiter
1512 // on the same fd extends the arming instead of colliding with it.
1513 if (!fd_waiters_arm(fd, e, fd_waiters_union(e) | (uint32_t)(flags & FD_WAIT_IO_MASK), false)) {
1514 fd_shard_unlock(fd);
1515 return timer_thread_unavailable;
1516 }
1517
1518 if (th) {
1519 ccan_list_add_tail(&e->waiters, &th->sched.waiting_reason.fd_node);
1520
1521 VM_ASSERT(th->sched.waiting_reason.flags == thread_sched_waiting_none);
1522 th->sched.waiting_reason.flags = flags;
1523 th->sched.waiting_reason.data.timeout = abs;
1524 th->sched.waiting_reason.data.fd = fd;
1525 th->sched.waiting_reason.data.result = 0;
1526 th->sched.waiting_reason.data.event_serial = event_serial;
1527
1528 if (abs != 0) {
1529 RUBY_DEBUG_LOG("abs:%lu", (unsigned long)abs);
1530 VM_ASSERT(flags & thread_sched_waiting_timeout);
1531
1532 rb_native_mutex_lock(&timer_th.waiting_lock);
1533 {
1534 rb_hrtime_t prev_expiry = timer_th.next_expiry;
1535 timer_wheel_insert(&th->sched.waiting_reason);
1536 verify_waiting_list();
1537 if (timer_th.next_expiry < prev_expiry) {
1538 // an earlier deadline than the timer thread is armed for
1539 timer_thread_wakeup_force();
1540 }
1541 }
1542 rb_native_mutex_unlock(&timer_th.waiting_lock);
1543 }
1544 }
1545 RUBY_DEBUG_LOG("armed fd:%d want:%u", fd, e->armed_flags);
1546 }
1547 fd_shard_unlock(fd);
1548 }
1549 else if (th) {
1550 // fd-less timed wait: the wheel lock owns it end to end
1551 VM_ASSERT(abs != 0 && (flags & thread_sched_waiting_timeout));
1552
1553 rb_native_mutex_lock(&timer_th.waiting_lock);
1554 {
1555 VM_ASSERT(th->sched.waiting_reason.flags == thread_sched_waiting_none);
1556 th->sched.waiting_reason.flags = flags;
1557 th->sched.waiting_reason.data.timeout = abs;
1558 th->sched.waiting_reason.data.fd = fd;
1559 th->sched.waiting_reason.data.result = 0;
1560 th->sched.waiting_reason.data.event_serial = event_serial;
1561
1562 rb_hrtime_t prev_expiry = timer_th.next_expiry;
1563 timer_wheel_insert(&th->sched.waiting_reason);
1564 verify_waiting_list();
1565 if (timer_th.next_expiry < prev_expiry) {
1566 timer_thread_wakeup_force();
1567 }
1568 }
1569 rb_native_mutex_unlock(&timer_th.waiting_lock);
1570 }
1571 else {
1572 VM_ASSERT(abs == 0);
1573 }
1574
1575 return timer_thread_registered;
1576}
1577
1578// Drop `th` from its fd's waiter list and re-arm the backend for whoever is
1579// left. The fd's shard lock must be held.
1580static void
1581timer_thread_unregister_waiting(rb_thread_t *th, int fd, enum thread_sched_waiting_flag flags)
1582{
1583 if (!(th->sched.waiting_reason.flags & FD_WAIT_IO_MASK)) {
1584 return;
1585 }
1586
1587 RUBY_DEBUG_LOG("th:%u fd:%d", rb_th_serial(th), fd);
1588
1589 ccan_list_del_init(&th->sched.waiting_reason.fd_node);
1590
1591 struct rb_fd_waiters *e = fd_waiters_lookup(fd, false);
1592 if (e) {
1593 fd_waiters_arm(fd, e, fd_waiters_union(e), false);
1594 }
1595}
1596
1597// The timer thread's own wakeup pipe has no waiter: it stays armed forever and
1598// is recognised by its fd when an event arrives.
1599static void
1600timer_thread_arm_comm_pipe(void)
1601{
1602 int fd = timer_th.comm_fds[0];
1603
1604#if HAVE_SYS_EVENT_H
1605 struct kevent ke;
1606 EV_SET(&ke, fd, EVFILT_READ, EV_ADD, 0, 0, (void *)(uintptr_t)fd_event_tag(fd, 0));
1607 if (kevent(timer_th.event_fd, &ke, 1, NULL, 0, NULL) == -1) {
1608 rb_bug("timer_thread_arm_comm_pipe/kevent failed (errno:%d)", errno);
1609 }
1610#elif HAVE_SYS_EPOLL_H
1611 struct epoll_event event = {
1612 .events = EPOLLIN,
1613 .data = { .u64 = fd_event_tag(fd, 0) },
1614 };
1615 if (epoll_ctl(timer_th.event_fd, EPOLL_CTL_ADD, fd, &event) == -1) {
1616 rb_bug("timer_thread_arm_comm_pipe/epoll_ctl failed (errno:%d)", errno);
1617 }
1618#else
1619# error "neither kqueue nor epoll"
1620#endif
1621}
1622
1623static void
1624timer_thread_setup_mn(void)
1625{
1626#if HAVE_SYS_EVENT_H
1627 kqueue_create();
1628 RUBY_DEBUG_LOG("kqueue_fd:%d", timer_th.event_fd);
1629#else
1630 if ((timer_th.event_fd = epoll_create1(EPOLL_CLOEXEC)) == -1) rb_bug("epoll_create (errno:%d)", errno);
1631 RUBY_DEBUG_LOG("epoll_fd:%d", timer_th.event_fd);
1632#endif
1633 RUBY_DEBUG_LOG("comm_fds:%d/%d", timer_th.comm_fds[0], timer_th.comm_fds[1]);
1634
1635 timer_thread_arm_comm_pipe();
1636}
1637
1638static int
1639event_wait(rb_vm_t *vm)
1640{
1641#if HAVE_SYS_EVENT_H
1642 int r = kqueue_wait(vm);
1643#else
1644 int r = epoll_wait(timer_th.event_fd, timer_th.finished_events, EPOLL_EVENTS_MAX, timer_thread_set_timeout(vm));
1645#endif
1646 return r;
1647}
1648
1649// How many waiters one pass may unlink before it stops holding waiting_lock.
1650#define FD_WAKE_BATCH 16
1651
1652
1653// Deliver an fd event to every thread waiting for it. Waiters are unlinked
1654// under waiting_lock but woken after releasing it (the scheduler lock is taken
1655// before it, never after); event_serial, captured under the lock, guards them.
1656static void
1657timer_thread_wake_fd_waiters(int fd, uint32_t generation, uint32_t wake_flags, int result)
1658{
1659 struct timer_wake batch[FD_WAKE_BATCH];
1660
1661 if (wake_flags == 0) return;
1662
1663 while (1) {
1664 int n = 0;
1665 bool more = false;
1666
1667 fd_shard_lock(fd);
1668 {
1669 struct rb_fd_waiters *e = fd_waiters_lookup(fd, false);
1670
1671 // A newer generation means the fd was disarmed after this event was
1672 // queued, and the number may name a different file by now.
1673 if (e != NULL && e->generation == generation) {
1674 struct rb_thread_sched_waiting *w, *nxt;
1675
1676 ccan_list_for_each_safe(&e->waiters, w, nxt, fd_node) {
1677 if (!(w->flags & wake_flags)) continue;
1678
1679 if (n == FD_WAKE_BATCH) {
1680 more = true;
1681 break;
1682 }
1683
1684 ccan_list_del_init(&w->fd_node);
1685
1686 if (w->flags & thread_sched_waiting_timeout) {
1687 // also leaves the timer wheel (or the expiry pass's
1688 // batch, whose claim will then find the flags gone)
1689 rb_native_mutex_lock(&timer_th.waiting_lock);
1690 timer_wheel_del(w);
1691 rb_native_mutex_unlock(&timer_th.waiting_lock);
1692 }
1693
1694 // The pin must be visible before the flags clear: a waiter
1695 // that sees the clear may skip parking, finish and die,
1696 // and the fence only waits on the pending count.
1697 batch[n].th = thread_sched_waiting_thread(w);
1698 batch[n].serial = w->data.event_serial;
1699 timer_wake_pending_inc(batch[n].th);
1700 n++;
1701
1702 w->flags = thread_sched_waiting_none;
1703 w->data.fd = -1;
1704 w->data.result = result;
1705 }
1706
1707 // Re-arm for whoever is still waiting on this fd (nothing, if
1708 // they all just woke up).
1709 fd_waiters_arm(fd, e, fd_waiters_union(e), true);
1710 }
1711 }
1712 fd_shard_unlock(fd);
1713
1714 for (int i = 0; i < n; i++) {
1715 timer_thread_wakeup_thread(batch[i].th, batch[i].serial);
1716 }
1717 timer_wake_pending_clear(batch, n);
1718
1719 if (!more) break;
1720 }
1721}
1722
1723/*
1724 * The purpose of the timer thread:
1725 *
1726 * (1) Periodic checking
1727 * (1-1) Provide time slice for active NTs
1728 * (1-2) Check NT shortage
1729 * (1-3) Periodic UBF (global)
1730 * (1-4) Lazy GRQ deq start
1731 * (2) Receive notification
1732 * (2-1) async I/O termination
1733 * (2-2) timeout
1734 * (2-2-1) sleep(n)
1735 * (2-2-2) timeout(n), I/O, ...
1736 */
1737static void
1738timer_thread_polling(rb_vm_t *vm)
1739{
1740 int r = event_wait(vm);
1741
1742 RUBY_DEBUG_LOG("r:%d errno:%d", r, errno);
1743
1744 switch (r) {
1745 case 0: // timeout
1746 RUBY_DEBUG_LOG("timeout%s", "");
1747
1748 ractor_sched_lock(vm, NULL);
1749 {
1750 // (1-1) timeslice
1751 timer_thread_check_timeslice(vm);
1752
1753 // (1-4) lazy grq deq
1754 if (vm->ractor.sched.grq_cnt > 0) {
1755 RUBY_DEBUG_LOG("GRQ cnt: %u", vm->ractor.sched.grq_cnt);
1756 rb_native_cond_signal(&vm->ractor.sched.cond);
1757 }
1758 }
1759 ractor_sched_unlock(vm, NULL);
1760
1761 // (1-2)
1762 native_thread_check_and_create_shared(vm);
1763
1764 break;
1765
1766 case -1:
1767 switch (errno) {
1768 case EINTR:
1769 // simply retry
1770 break;
1771 default:
1772 perror("event_wait");
1773 rb_bug("event_wait errno:%d", errno);
1774 }
1775 break;
1776
1777 default:
1778 RUBY_DEBUG_LOG("%d event(s)", r);
1779
1780#if HAVE_SYS_EVENT_H
1781 for (int i=0; i<r; i++) {
1782 uint64_t tag = (uint64_t)(uintptr_t)timer_th.finished_events[i].udata;
1783 int fd = (int)timer_th.finished_events[i].ident;
1784 int16_t filter = timer_th.finished_events[i].filter;
1785
1786 if (fd == timer_th.comm_fds[0]) {
1787 RUBY_DEBUG_LOG("comm from fd:%d", timer_th.comm_fds[1]);
1788 consume_communication_pipe(timer_th.comm_fds[0]);
1789 continue;
1790 }
1791
1792 uint32_t wake_flags = kqueue_translate_filter_to_flags(filter) & FD_WAIT_IO_MASK;
1793 if (timer_th.finished_events[i].flags & (EV_EOF | EV_ERROR)) {
1794 wake_flags = FD_WAIT_IO_MASK; // end of file or error concerns everyone
1795 }
1796
1797 timer_thread_wake_fd_waiters(fd, FD_EVENT_TAG_GEN(tag), wake_flags, filter);
1798 }
1799#elif HAVE_SYS_EPOLL_H
1800 for (int i=0; i<r; i++) {
1801 uint64_t tag = timer_th.finished_events[i].data.u64;
1802 int fd = FD_EVENT_TAG_FD(tag);
1803 uint32_t events = timer_th.finished_events[i].events;
1804
1805 if (fd == timer_th.comm_fds[0]) {
1806 RUBY_DEBUG_LOG("comm from fd:%d", timer_th.comm_fds[1]);
1807 consume_communication_pipe(timer_th.comm_fds[0]);
1808 continue;
1809 }
1810
1811 RUBY_DEBUG_LOG("io event. fd:%d event:%s%s%s%s%s%s", fd,
1812 (events & EPOLLIN) ? "in/" : "",
1813 (events & EPOLLOUT) ? "out/" : "",
1814 (events & EPOLLRDHUP) ? "RDHUP/" : "",
1815 (events & EPOLLPRI) ? "pri/" : "",
1816 (events & EPOLLERR) ? "err/" : "",
1817 (events & EPOLLHUP) ? "hup/" : "");
1818
1819 uint32_t wake_flags = 0;
1820 if (events & (EPOLLIN | EPOLLPRI | EPOLLRDHUP)) wake_flags |= thread_sched_waiting_io_read;
1821 if (events & EPOLLOUT) wake_flags |= thread_sched_waiting_io_write;
1822 // An error or hangup ends every wait on this fd, in either direction.
1823 if (events & (EPOLLERR | EPOLLHUP)) wake_flags |= FD_WAIT_IO_MASK;
1824
1825 timer_thread_wake_fd_waiters(fd, FD_EVENT_TAG_GEN(tag), wake_flags, (int)events);
1826 }
1827#else
1828# error "neither kqueue nor epoll"
1829#endif
1830 }
1831}
1832
1833#endif // HAVE_SYS_EPOLL_H || HAVE_SYS_EVENT_H
1834
1835#else // USE_MN_THREADS
1836
1837static void
1838timer_thread_setup_mn(void)
1839{
1840 // do nothing
1841}
1842
1843static void
1844timer_thread_polling(rb_vm_t *vm)
1845{
1846 int timeout = timer_thread_set_timeout(vm);
1847
1848 struct pollfd pfd = {
1849 .fd = timer_th.comm_fds[0],
1850 .events = POLLIN,
1851 };
1852
1853 int r = poll(&pfd, 1, timeout);
1854
1855 switch (r) {
1856 case 0: // timeout
1857 ractor_sched_lock(vm, NULL);
1858 {
1859 // (1-1) timeslice
1860 timer_thread_check_timeslice(vm);
1861 }
1862 ractor_sched_unlock(vm, NULL);
1863 break;
1864
1865 case -1: // error
1866 switch (errno) {
1867 case EINTR:
1868 // simply retry
1869 break;
1870 default:
1871 perror("poll");
1872 rb_bug("poll errno:%d", errno);
1873 break;
1874 }
1875
1876 case 1:
1877 consume_communication_pipe(timer_th.comm_fds[0]);
1878 break;
1879
1880 default:
1881 rb_bug("unreachbale");
1882 }
1883}
1884
1885static int
1886native_thread_create_shared(rb_thread_t *th)
1887{
1888 rb_bug("unreachable");
1889}
1890
1891static enum thread_sched_wait_result
1892thread_sched_wait_events(struct rb_thread_sched *sched, rb_thread_t *th, int fd, enum thread_sched_waiting_flag events, rb_hrtime_t *rel)
1893{
1894 rb_bug("unreachable");
1895}
1896
1897// Without the wheel every thread is dedicated, so a Ractor wait takes its
1898// deadline on its own condvar and never reaches these.
1899static bool
1900ractor_sched_timeout_arm(rb_thread_t *th, const rb_hrtime_t *rel)
1901{
1902 rb_bug("unreachable");
1903}
1904
1905static bool
1906ractor_sched_timeout_disarm(rb_thread_t *th)
1907{
1908 rb_bug("unreachable");
1909}
1910
1911static int
1912timer_wheel_timeout(int timeout)
1913{
1914 return timeout; // no M:N threads, no timed waiters
1915}
1916
1917static void
1918timer_thread_wake_fence(rb_thread_t *th)
1919{
1920 // no timer wheel, no wake batches
1921}
1922
1923static void
1924timer_thread_check_timeout(rb_vm_t *vm)
1925{
1926 // no M:N threads, no timed waiters
1927}
1928
1929#endif // USE_MN_THREADS
#define RUBY_ATOMIC_PTR_LOAD(var)
Identical to RUBY_ATOMIC_LOAD, except it expects its arguments are void*.
Definition atomic.h:348
#define RUBY_ATOMIC_INC(var)
Atomically increments the value pointed by var.
Definition atomic.h:214
#define RUBY_ATOMIC_PTR_CAS(var, oldval, newval)
Identical to RUBY_ATOMIC_CAS, except it expects its arguments are void*.
Definition atomic.h:375
#define RUBY_ATOMIC_CAS(var, oldval, newval)
Atomic compare-and-swap.
Definition atomic.h:165
std::atomic< unsigned > rb_atomic_t
Type that is eligible for atomic operations.
Definition atomic.h:69
#define RUBY_ATOMIC_DEC(var)
Atomically decrements the value pointed by var.
Definition atomic.h:223
#define RUBY_ATOMIC_LOAD(var)
Atomic load.
Definition atomic.h:175
#define RUBY_ATOMIC_PTR_SET(var, val)
Identical to RUBY_ATOMIC_SET, except it expects its arguments are void*.
Definition atomic.h:361
#define RUBY_ASSERT(...)
Asserts that the given expression is truthy if and only if RUBY_DEBUG is truthy.
Definition assert.h:219
int len
Length of the buffer.
Definition io.h:8
#define RUBY_INTERNAL_THREAD_EVENT_RESUMED
Triggered when a thread successfully acquired the GVL.
Definition thread.h:249
#define RUBY_INTERNAL_THREAD_EVENT_SUSPENDED
Triggered when a thread released the GVL.
Definition thread.h:256
#define RBIMPL_ATTR_MAYBE_UNUSED()
Wraps (or simulates) [[maybe_unused]]
#define errno
Ractor-aware version of errno.
Definition ruby.h:388
void rb_native_mutex_lock(rb_nativethread_lock_t *lock)
Just another name of rb_nativethread_lock_lock.
void rb_native_cond_broadcast(rb_nativethread_cond_t *cond)
Signals a condition variable.
void rb_native_mutex_initialize(rb_nativethread_lock_t *lock)
Just another name of rb_nativethread_lock_initialize.
void rb_native_mutex_unlock(rb_nativethread_lock_t *lock)
Just another name of rb_nativethread_lock_unlock.
void rb_native_cond_signal(rb_nativethread_cond_t *cond)
Signals a condition variable.
void rb_native_cond_wait(rb_nativethread_cond_t *cond, rb_nativethread_lock_t *mutex)
Waits for the passed condition variable to be signalled.
uintptr_t VALUE
Type that represents a Ruby object.
Definition value.h:40