Skip to content

unix: Wake sleeping operations immediately on scheduled callbacks.#9

Open
andrewleech wants to merge 3 commits intoreview/unix-sleep-process-pendingfrom
unix-sleep-process-pending
Open

unix: Wake sleeping operations immediately on scheduled callbacks.#9
andrewleech wants to merge 3 commits intoreview/unix-sleep-process-pendingfrom
unix-sleep-process-pending

Conversation

@andrewleech
Copy link
Owner

Summary

On the unix port, time.sleep() and MICROPY_INTERNAL_WFE() were unresponsive to scheduled callbacks — time.sleep() used select() or sleep() with no wakeup mechanism, and WFE busy-polled with a 500us delay.

This adds a signal-based notification so that when something is scheduled (via MICROPY_SCHED_HOOK_SCHEDULED), a signal is sent to the process. A sig_atomic_t flag is set by both mp_hal_signal_event() and the signal handler. mp_unix_sched_sleep() uses pselect() to atomically unblock the signal and enter the wait, avoiding the TOCTOU race where signals for already-queued callbacks could be consumed before the sleep. The signal is blocked process-wide at init so only the thread inside pselect() can receive it, and pthread_sigmask is used on threaded builds for POSIX correctness.

time.sleep() is reworked to loop over mp_unix_sched_sleep() with elapsed-time tracking, processing pending callbacks each iteration. Negative values now raise ValueError to match CPython. WFE uses the same sleep primitive so it blocks properly and wakes on events rather than spinning.

Uses SIGRTMIN+7 where real-time signals are available, falling back to SIGURG.

Testing

Tested on the unix port (Linux), both standard and coverage variants. Windows codepath (_WIN32) is left unchanged — it keeps the existing select()/delay_us behaviour.

Trade-offs and Alternatives

Could have used a pipe/eventfd self-pipe pattern instead of signals, which would avoid any signal-number collision concerns. Signals are simpler and don't require managing an fd, and the chosen signal numbers avoid known conflicts with GC (SIGRTMIN+5) and thread terminate (SIGRTMIN+6).

The time.sleep() rewrite replaces the MICROPY_SELECT_REMAINING_TIME Linux-specific assumption about select() modifying the timeout struct with explicit mp_hal_ticks_ms() tracking, which is portable. Precision drops from microseconds to milliseconds which is acceptable for time.sleep().

Generative AI

I used generative AI tools when creating this PR, but a human has checked the code and is responsible for the description above.

Install an empty signal handler (without SA_RESTART) for a dedicated
signal so that select() calls return EINTR when the scheduler queues a
callback. mp_hal_signal_event() sends this signal via kill(getpid()),
and MICROPY_SCHED_HOOK_SCHEDULED calls it from mp_sched_schedule().

This replaces the need for a self-pipe mechanism while remaining
async-signal-safe.

Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
Rewrite time.sleep() to use mp_unix_sched_sleep(), a select() call with
no file descriptors that returns early on EINTR from the scheduler
signal. The sleep loop recomputes remaining time on each iteration,
handling both signal wakeups and time.sleep(0) correctly.

Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
Replace the fixed 500us delay in MICROPY_INTERNAL_WFE with
mp_unix_sched_sleep(), which sleeps for the full requested timeout but
wakes immediately on EINTR from the scheduler signal.

Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
@andrewleech
Copy link
Owner Author

/review

@andrewleech andrewleech reopened this Feb 25, 2026
Copy link

@mpy-reviewer mpy-reviewer bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The signal-based approach using pselect is the right design here — the atomic unblock-and-wait correctly eliminates the TOCTOU race. A few issues to address before merge.

}

void mp_unix_init_sched_signal(void) {
struct sigaction sa;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sa is a stack variable; sa_restorer (and any padding) is uninitialised. Use:

Suggested change
struct sigaction sa;
struct sigaction sa = {0};


void mp_unix_deinit_sched_signal(void) {
MP_SIGMASK(SIG_UNBLOCK, &sched_signal_mask, NULL);
signal(MP_SCHED_SIGNAL, SIG_DFL);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use sigaction consistently rather than mixing with signal:

Suggested change
signal(MP_SCHED_SIGNAL, SIG_DFL);
struct sigaction sa = {0};
sa.sa_handler = SIG_DFL;
sigemptyset(&sa.sa_mask);
sigaction(MP_SCHED_SIGNAL, &sa, NULL);

// Note that we don't delay for the full TIMEOUT_MS, as execution
// can't be woken from the delay.
// Wait for an event (scheduled callback) or timeout. A signal from
// mp_hal_signal_event() causes select() to return EINTR, waking the wait.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"select()" → "pselect()".

mp_raise_ValueError(MP_ERROR_TEXT("sleep length must be non-negative"));
}
RAISE_ERRNO(res, errno);
uint64_t total_ms = (uint64_t)(val * MICROPY_FLOAT_CONST(1000.0));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This truncates to milliseconds, so time.sleep(0.0001) becomes a no-op (total_ms=0 → loop exits immediately without sleeping). The old code had microsecond precision. At minimum, round up:

Suggested change
uint64_t total_ms = (uint64_t)(val * MICROPY_FLOAT_CONST(1000.0));
uint64_t total_ms = (uint64_t)MICROPY_FLOAT_C_FUN(ceil)(val * MICROPY_FLOAT_CONST(1000.0));

@github-actions
Copy link

Code size report:

Reference:  zephyr/mpconfigport: Remove duplicate builtins.open definition. [1ab9b66]
Comparison: unix: Use sched signal in MICROPY_INTERNAL_WFE. [merge of 3bd023c]
  mpy-cross:    +0 +0.000% 
   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64: +1353 +0.158% standard[incl +40(data) +160(bss)]
      stm32:    +0 +0.000% PYBV10
      esp32:    +0 +0.000% ESP32_GENERIC
     mimxrt:    +0 +0.000% TEENSY40
        rp2:    +0 +0.000% RPI_PICO_W
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:    +0 +0.000% VIRT_RV32

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants