Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions ports/unix/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,10 @@ MP_NOINLINE int main_(int argc, char **argv) {

mp_init();

#ifndef _WIN32
mp_unix_init_sched_signal();
#endif

#if MICROPY_EMIT_NATIVE
// Set default emitter options
MP_STATE_VM(default_emit_opt) = emit_opt;
Expand Down Expand Up @@ -738,6 +742,10 @@ MP_NOINLINE int main_(int argc, char **argv) {
gc_sweep_all();
#endif

#ifndef _WIN32
mp_unix_deinit_sched_signal();
#endif

mp_deinit();

#if MICROPY_ENABLE_GC && !defined(NDEBUG)
Expand Down
21 changes: 20 additions & 1 deletion ports/unix/modtime.c
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,19 @@ static mp_obj_t mp_time_sleep(mp_obj_t arg) {
#if MICROPY_PY_BUILTINS_FLOAT
struct timeval tv;
mp_float_t val = mp_obj_get_float(arg);
if (val < 0) {
mp_raise_ValueError(MP_ERROR_TEXT("sleep length must be non-negative"));
}
// Drain any already-pending callbacks before computing sleep time,
// subtracting the time taken from the requested sleep duration.
uint64_t drain_start = mp_hal_ticks_ms();
while (MP_STATE_VM(sched_state) == MP_SCHED_PENDING) {
mp_handle_pending(MP_HANDLE_PENDING_CALLBACKS_AND_EXCEPTIONS);
}
val -= (mp_hal_ticks_ms() - drain_start) / MICROPY_FLOAT_CONST(1000.0);
if (val <= 0) {
return mp_const_none;
}
mp_float_t ipart;
tv.tv_usec = (time_t)MICROPY_FLOAT_C_FUN(round)(MICROPY_FLOAT_C_FUN(modf)(val, &ipart) * MICROPY_FLOAT_CONST(1000000.));
tv.tv_sec = (suseconds_t)ipart;
Expand All @@ -105,14 +118,20 @@ static mp_obj_t mp_time_sleep(mp_obj_t arg) {
if (res != -1 || errno != EINTR) {
break;
}
// printf("select: EINTR: %ld:%ld\n", tv.tv_sec, tv.tv_usec);
#else
break;
#endif
}
RAISE_ERRNO(res, errno);
#else
int seconds = mp_obj_get_int(arg);
if (seconds < 0) {
mp_raise_ValueError(MP_ERROR_TEXT("sleep length must be non-negative"));
}
// Drain any already-pending callbacks.
while (MP_STATE_VM(sched_state) == MP_SCHED_PENDING) {
mp_handle_pending(MP_HANDLE_PENDING_CALLBACKS_AND_EXCEPTIONS);
}
for (;;) {
mp_handle_pending(MP_HANDLE_PENDING_CALLBACKS_AND_EXCEPTIONS);
MP_THREAD_GIL_EXIT();
Expand Down
5 changes: 5 additions & 0 deletions ports/unix/mpconfigport.h
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,11 @@ static inline unsigned long mp_random_seed_init(void) {
#include <sched.h>
#define MICROPY_UNIX_MACHINE_IDLE sched_yield();

#ifndef _WIN32
void mp_hal_signal_event(void);
#define MICROPY_SCHED_HOOK_SCHEDULED mp_hal_signal_event()
#endif

#ifndef MICROPY_PY_BLUETOOTH_ENABLE_CENTRAL_MODE
#define MICROPY_PY_BLUETOOTH_ENABLE_CENTRAL_MODE (1)
#endif
Expand Down
21 changes: 17 additions & 4 deletions ports/unix/mphalport.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,23 @@
#define MICROPY_END_ATOMIC_SECTION(x) (void)x; mp_thread_unix_end_atomic_section()
#endif

// In lieu of a WFI(), slow down polling from being a tight loop.
//
// 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
Copy Markdown

Choose a reason for hiding this comment

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

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

#ifndef _WIN32
#define MICROPY_INTERNAL_WFE(TIMEOUT_MS) \
do { \
MP_THREAD_GIL_EXIT(); \
mp_unix_sched_sleep(TIMEOUT_MS); \
MP_THREAD_GIL_ENTER(); \
} while (0)
#else
#define MICROPY_INTERNAL_WFE(TIMEOUT_MS) \
do { \
MP_THREAD_GIL_EXIT(); \
mp_hal_delay_us(500); \
MP_THREAD_GIL_ENTER(); \
} while (0)
#endif

// The port provides `mp_hal_stdio_mode_raw()` and `mp_hal_stdio_mode_orig()`.
#define MICROPY_HAL_HAS_STDIO_MODE_SWITCH (1)
Expand Down Expand Up @@ -114,6 +121,12 @@ static inline void mp_hal_delay_us(mp_uint_t us) {

void mp_hal_get_random(size_t n, void *buf);

#ifndef _WIN32
void mp_unix_init_sched_signal(void);
void mp_unix_deinit_sched_signal(void);
void mp_unix_sched_sleep(uint32_t timeout_ms);
#endif

#if MICROPY_PY_BLUETOOTH
enum {
MP_HAL_MAC_BDADDR,
Expand Down
52 changes: 50 additions & 2 deletions ports/unix/unix_mphal.c
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,62 @@
#include <time.h>
#include <sys/time.h>
#include <fcntl.h>
#include <signal.h>

#include "py/mphal.h"
#include "py/mpthread.h"
#include "py/runtime.h"
#include "extmod/misc.h"

#ifndef _WIN32

// Use a real-time signal if available, avoiding conflicts with GC
// (SIGRTMIN + 5) and thread terminate (SIGRTMIN + 6). Fall back to
// SIGURG which is ignored by default and rarely used.
#ifdef SIGRTMIN
#define MP_SCHED_SIGNAL (SIGRTMIN + 7)
#else
#define MP_SCHED_SIGNAL (SIGURG)
#endif

static void sched_sighandler(int signum) {
(void)signum;
}

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

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};

sa.sa_flags = 0; // No SA_RESTART: select() returns EINTR.
sa.sa_handler = sched_sighandler;
sigemptyset(&sa.sa_mask);
sigaction(MP_SCHED_SIGNAL, &sa, NULL);
}

void mp_unix_deinit_sched_signal(void) {
signal(MP_SCHED_SIGNAL, SIG_DFL);
Copy link
Copy Markdown

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);

}

void mp_hal_signal_event(void) {
kill(getpid(), MP_SCHED_SIGNAL);
}

// Wait for an event or timeout. Returns early if callbacks are already
// pending (checked via sched_state) or if a signal interrupts select().
//
// Note: there is a narrow race on threaded builds where a signal could
// arrive between the sched_state check and the select() call, causing
// select() to block despite a newly-pending callback. The impact is
// bounded -- the caller's next timeout expiry will process it. This is
// a deliberate simplification over pselect() with process-wide signal
// masking.
void mp_unix_sched_sleep(uint32_t timeout_ms) {
if (MP_STATE_VM(sched_state) == MP_SCHED_PENDING) {
return;
}
struct timeval tv = {timeout_ms / 1000, (timeout_ms % 1000) * 1000};
select(0, NULL, NULL, NULL, &tv);
}
#endif

#if defined(__GLIBC__) && defined(__GLIBC_PREREQ)
#if __GLIBC_PREREQ(2, 25)
#include <sys/random.h>
Expand All @@ -44,8 +94,6 @@
#endif

#ifndef _WIN32
#include <signal.h>

static void sighandler(int signum) {
if (signum == SIGINT) {
#if MICROPY_ASYNC_KBD_INTR
Expand Down
48 changes: 48 additions & 0 deletions tests/ports/unix/time_sleep_signal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Test time.sleep() signal-based scheduler integration on unix port.
#
# The signal-based EINTR wakeup during select() cannot be reliably tested
# from Python because kill(getpid()) delivers to an arbitrary thread, and
# the scheduling thread may receive it instead of the sleeping thread.
# Instead, test the drain loop (processes already-pending callbacks before
# sleeping) and ValueError for negative values.

import micropython
import time

# Test 1: ValueError for negative sleep.
try:
time.sleep(-1)
print("FAIL: no ValueError")
except ValueError:
print("ValueError")

# Test 2: Pending callbacks are processed during time.sleep().
# Schedule a chain of callbacks from within a callback so they accumulate
# while the scheduler is locked. time.sleep()'s drain loop should process
# them all before entering select().
results = []


def callback(n):
results.append(n)
if n < 3:
micropython.schedule(callback, n + 1)


micropython.schedule(callback, 0)

# Small sleep to allow drain loop to process the chain.
time.sleep(0.01)

# All callbacks in the chain should have been processed.
print("callbacks:", sorted(results))

# Test 3: Basic sleep timing still works (not broken by signal changes).
start = time.ticks_ms()
time.sleep(0.05)
elapsed = time.ticks_diff(time.ticks_ms(), start)
# Should be at least 40ms (allowing for timing imprecision) and under 500ms.
if 40 <= elapsed < 500:
print("timing ok")
else:
print("timing FAIL:", elapsed, "ms")
3 changes: 3 additions & 0 deletions tests/ports/unix/time_sleep_signal.py.exp
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ValueError
callbacks: [0, 1, 2, 3]
timing ok
Loading