/
githubmirror
/
julia
Обзор
Документация
Войти
/
githubmirror
/
julia
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/signal-handling.c
1 070 строк
40 KB
Keno Fischer
cancellation: Hook up ^C (#62655)
08 авг 2026, 03:45
Не верифицирован
08 авг 2026, 03:45
37ef9ad
Код
Авторство
О чём код?
// This file is a part of Julia. License is MIT: https://julialang.org/license #include <stdlib.h> #include <stddef.h> #include <stdio.h> #include <inttypes.h> #include "julia.h" #include "julia_internal.h" #include <unistd.h> #ifndef _OS_WINDOWS_ #include <sys/mman.h> #endif #ifdef _OS_LINUX_ #include <sys/prctl.h> #include <sys/syscall.h> #endif #ifdef __cplusplus extern "C" { #endif #include <threading.h> // Native mutex (not jl_mutex_t): the paths below run on non-Julia threads // (the signal listener, Win32 console-ctrl handler threads), which have no // Julia task to derive lock ownership from. static uv_mutex_t sigint_state_lock; // Profiler control variables uv_mutex_t live_tasks_lock; uv_mutex_t bt_data_prof_lock; volatile jl_bt_element_t *profile_bt_data_prof = NULL; volatile size_t profile_bt_size_max = 0; volatile size_t profile_bt_size_cur = 0; static volatile uint64_t nsecprof = 0; volatile int profile_running = 0; volatile int profile_all_tasks = 0; static const uint64_t GIGA = 1000000000ULL; // Timers to take samples at intervals JL_DLLEXPORT void jl_profile_stop_timer(void) JL_NOTSAFEPOINT; JL_DLLEXPORT int jl_profile_start_timer(uint8_t) JL_NOTSAFEPOINT; /////////////////////// // Utility functions // /////////////////////// JL_DLLEXPORT int jl_profile_init(size_t maxsize, uint64_t delay_nsec) { uv_mutex_lock(&bt_data_prof_lock); if (profile_running) { // the sampler may be writing into the buffer we are about to free. Note this // only rules out re-initializing while running: `jl_profile_stop_timer` doesn't // wait for a sampler iteration already under way, and the thread samplers write // without taking this lock. uv_mutex_unlock(&bt_data_prof_lock); return -2; } profile_bt_size_max = maxsize; nsecprof = delay_nsec; if (profile_bt_data_prof != NULL) free((void*)profile_bt_data_prof); profile_bt_data_prof = (jl_bt_element_t*) calloc(maxsize, sizeof(jl_bt_element_t)); if (profile_bt_data_prof == NULL && maxsize > 0) { uv_mutex_unlock(&bt_data_prof_lock); return -1; } profile_bt_size_cur = 0; uv_mutex_unlock(&bt_data_prof_lock); return 0; } JL_DLLEXPORT uint8_t *jl_profile_get_data(void) { return (uint8_t*) profile_bt_data_prof; } JL_DLLEXPORT size_t jl_profile_len_data(void) { return profile_bt_size_cur; } JL_DLLEXPORT size_t jl_profile_maxlen_data(void) { return profile_bt_size_max; } JL_DLLEXPORT uint64_t jl_profile_delay_nsec(void) { return nsecprof; } JL_DLLEXPORT void jl_profile_clear_data(void) { profile_bt_size_cur = 0; } JL_DLLEXPORT int jl_profile_is_running(void) { return profile_running; } // Any function that acquires this lock must be either an unmanaged thread // or in the GC safe region and must NOT allocate anything through the GC // while holding this lock. // Certain functions in this file might be called from an unmanaged thread // and cannot have any interaction with the julia runtime // They also may be re-entrant, and operating while threads are paused, so we // separately manage the re-entrant count behavior for safety across platforms // Note that we cannot safely upgrade read->write uv_rwlock_t debuginfo_asyncsafe; #ifndef _OS_WINDOWS_ pthread_key_t debuginfo_asyncsafe_held; #else DWORD debuginfo_asyncsafe_held; #endif void jl_init_profile_lock(void) { uv_mutex_init(&sigint_state_lock); uv_rwlock_init(&debuginfo_asyncsafe); #ifndef _OS_WINDOWS_ pthread_key_create(&debuginfo_asyncsafe_held, NULL); #else debuginfo_asyncsafe_held = TlsAlloc(); #endif } static uintptr_t jl_lock_profile_rd_held(void) JL_NOTSAFEPOINT { #ifndef _OS_WINDOWS_ return (uintptr_t)pthread_getspecific(debuginfo_asyncsafe_held); #else return (uintptr_t)TlsGetValue(debuginfo_asyncsafe_held); #endif } void jl_lock_profile(void) { int got = jl_trylock_profile(); assert(got); (void)got; } int jl_trylock_profile(void) { uintptr_t held = jl_lock_profile_rd_held(); if (held == -1) return 0; if (held == 0) { held = -1; #ifndef _OS_WINDOWS_ pthread_setspecific(debuginfo_asyncsafe_held, (void*)held); #else TlsSetValue(debuginfo_asyncsafe_held, (void*)held); #endif uv_rwlock_rdlock(&debuginfo_asyncsafe); held = 0; } held++; #ifndef _OS_WINDOWS_ pthread_setspecific(debuginfo_asyncsafe_held, (void*)held); #else TlsSetValue(debuginfo_asyncsafe_held, (void*)held); #endif return 1; } JL_DLLEXPORT void jl_unlock_profile(void) JL_NO_SAFEPOINT_ANALYSIS { uintptr_t held = jl_lock_profile_rd_held(); assert(held && held != -1); held--; #ifndef _OS_WINDOWS_ pthread_setspecific(debuginfo_asyncsafe_held, (void*)held); #else TlsSetValue(debuginfo_asyncsafe_held, (void*)held); #endif if (held == 0) uv_rwlock_rdunlock(&debuginfo_asyncsafe); } int jl_lock_profile_wr(void) { uintptr_t held = jl_lock_profile_rd_held(); if (held) return 0; held = -1; #ifndef _OS_WINDOWS_ pthread_setspecific(debuginfo_asyncsafe_held, (void*)held); #else TlsSetValue(debuginfo_asyncsafe_held, (void*)held); #endif uv_rwlock_wrlock(&debuginfo_asyncsafe); return 1; } void jl_unlock_profile_wr(void) JL_NO_SAFEPOINT_ANALYSIS { uintptr_t held = jl_lock_profile_rd_held(); assert(held == -1); held = 0; #ifndef _OS_WINDOWS_ pthread_setspecific(debuginfo_asyncsafe_held, (void*)held); #else TlsSetValue(debuginfo_asyncsafe_held, (void*)held); #endif uv_rwlock_wrunlock(&debuginfo_asyncsafe); } static uint64_t profile_cong_rng_seed = 0; static int *profile_round_robin_thread_order = NULL; static int profile_round_robin_thread_order_size = 0; static void jl_shuffle_int_array_inplace(int *carray, int size, uint64_t *seed) { // The "modern Fisher–Yates shuffle" - O(n) algorithm // https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm for (int i = size; i-- > 1; ) { size_t j = cong(i + 1, seed); // cong is an open interval so we add 1 uint64_t tmp = carray[j]; carray[j] = carray[i]; carray[i] = tmp; } } static int *profile_get_randperm(int size) { if (profile_round_robin_thread_order_size < size) { free(profile_round_robin_thread_order); profile_round_robin_thread_order = (int*)malloc_s(size * sizeof(int)); for (int i = 0; i < size; i++) profile_round_robin_thread_order[i] = i; profile_round_robin_thread_order_size = size; profile_cong_rng_seed = jl_rand(); } jl_shuffle_int_array_inplace(profile_round_robin_thread_order, size, &profile_cong_rng_seed); return profile_round_robin_thread_order; } JL_DLLEXPORT int jl_profile_is_buffer_full(void) JL_NOTSAFEPOINT { // Declare buffer full if there isn't enough room to sample even just the // thread metadata and one max-sized frame. The `+ 6` is for the two block // terminator `0`'s plus the 4 metadata entries. return profile_bt_size_cur + ((JL_BT_MAX_ENTRY_SIZE + 1) + 6) > profile_bt_size_max; } #define PROFILE_TASK_DEBUG_FORCE_SAMPLING_FAILURE (0) #define PROFILE_TASK_DEBUG_FORCE_STOP_THREAD_FAILURE (0) void jl_profile_task(void) JL_NOTSAFEPOINT JL_NO_SAFEPOINT_ANALYSIS { if (jl_profile_is_buffer_full()) { // Buffer full: Delete the timer jl_profile_stop_timer(); return; } jl_task_t *t = NULL; int got_mutex = 0; if (uv_mutex_trylock(&live_tasks_lock) != 0) { goto collect_backtrace; } got_mutex = 1; { arraylist_t *tasks = jl_get_all_tasks_arraylist(); uint64_t seed = jl_rand(); const int n_max_random_attempts = 4; // randomly select a task that is not done for (int i = 0; i < n_max_random_attempts; i++) { t = (jl_task_t*)tasks->items[cong(tasks->len, &seed)]; assert(t == NULL || jl_is_task(t)); if (t == NULL) { continue; } int t_state = jl_atomic_load_relaxed(&t->_state); if (t_state == JL_TASK_STATE_DONE) { continue; } break; } arraylist_free(tasks); free(tasks); } collect_backtrace: uv_mutex_lock(&bt_data_prof_lock); if (profile_running == 0) { uv_mutex_unlock(&bt_data_prof_lock); if (got_mutex) { uv_mutex_unlock(&live_tasks_lock); } return; } jl_record_backtrace_result_t r = {0, -1}; jl_bt_element_t *bt_data_prof = (jl_bt_element_t*)(profile_bt_data_prof + profile_bt_size_cur); size_t bt_size_max = profile_bt_size_max - profile_bt_size_cur - 1; if (t == NULL || PROFILE_TASK_DEBUG_FORCE_SAMPLING_FAILURE) { // failed to find a task r.bt_size = failed_to_sample_task_fun(bt_data_prof, bt_size_max, 0); } else { if (!PROFILE_TASK_DEBUG_FORCE_STOP_THREAD_FAILURE) { r = jl_record_backtrace(t, bt_data_prof, bt_size_max, 1); } // we failed to get a backtrace if (r.bt_size == 0) { r.bt_size = failed_to_stop_thread_fun(bt_data_prof, bt_size_max, 0); } } // update the profile buffer size profile_bt_size_cur += r.bt_size; // store threadid but add 1 as 0 is preserved to indicate end of block profile_bt_data_prof[profile_bt_size_cur++].uintptr = r.tid == -1 ? -1 : (uintptr_t)r.tid + 1; // store task id (never null) profile_bt_data_prof[profile_bt_size_cur++].jlvalue = (jl_value_t*)t; // store cpu cycle clock profile_bt_data_prof[profile_bt_size_cur++].uintptr = cycleclock(); // the thread profiler uses this block to record whether the thread is not sleeping (1) or sleeping (2) // let's use a dummy value which is not 1 or 2 to // indicate that we are profiling a task, and therefore, this block is not about the thread state profile_bt_data_prof[profile_bt_size_cur++].uintptr = 3; // Mark the end of this block with two 0's profile_bt_data_prof[profile_bt_size_cur++].uintptr = 0; profile_bt_data_prof[profile_bt_size_cur++].uintptr = 0; uv_mutex_unlock(&bt_data_prof_lock); if (got_mutex) { uv_mutex_unlock(&live_tasks_lock); } } #ifndef _OS_WINDOWS_ // Not thread local, should only be accessed by the signal handler thread. static volatile int jl_sigint_passed = 0; static sigset_t jl_sigint_sset; #endif static int jl_ignore_sigint(void) { // On Unix, we get the SIGINT before the debugger which makes it very // hard to interrupt a running process in the debugger with `Ctrl-C`. // Manually raise a `SIGINT` on current thread with the signal temporarily // unblocked and use its behavior to decide if we need to handle the signal. #ifndef _OS_WINDOWS_ jl_sigint_passed = 0; pthread_sigmask(SIG_UNBLOCK, &jl_sigint_sset, NULL); // This can swallow an external `SIGINT` but it's not an issue // since we don't deliver the same number of signals anyway. pthread_kill(pthread_self(), SIGINT); pthread_sigmask(SIG_BLOCK, &jl_sigint_sset, NULL); if (!jl_sigint_passed) return 1; #endif return 0; } static int exit_on_sigint = 0; JL_DLLEXPORT void jl_exit_on_sigint(int on) { exit_on_sigint = on; } static uintptr_t jl_get_pc_from_ctx(const void *_ctx); static void jl_fprint_sigill(ios_t *s, void *_ctx); #if defined(_CPU_X86_64_) || defined(_CPU_X86_) \ || (defined(_OS_LINUX_) && defined(_CPU_AARCH64_)) \ || (defined(_OS_LINUX_) && defined(_CPU_ARM_)) \ || (defined(_OS_LINUX_) && defined(_CPU_RISCV64_)) static size_t jl_safe_read_mem(const volatile char *ptr, char *out, size_t len) { jl_jmp_buf *old_buf = jl_get_safe_restore(); jl_jmp_buf buf; jl_set_safe_restore(&buf); volatile size_t i = 0; if (!jl_setjmp(buf, 0)) { for (; i < len; i++) { out[i] = ptr[i]; } } jl_set_safe_restore(old_buf); return i; } #endif static double profile_autostop_time = -1.0; static double profile_peek_duration = 1.0; // seconds double jl_get_profile_peek_duration(void) { return profile_peek_duration; } void jl_set_profile_peek_duration(double t) { profile_peek_duration = t; } jl_mutex_t profile_show_peek_cond_lock; static uv_async_t *profile_show_peek_cond_loc; JL_DLLEXPORT void jl_set_peek_cond(uv_async_t *cond) { JL_LOCK_NOGC(&profile_show_peek_cond_lock); profile_show_peek_cond_loc = cond; JL_UNLOCK_NOGC(&profile_show_peek_cond_lock); } static void jl_check_profile_autostop(void) JL_NOTSAFEPOINT { if (profile_show_peek_cond_loc != NULL && profile_autostop_time != -1.0 && jl_hrtime() > profile_autostop_time) { profile_autostop_time = -1.0; jl_profile_stop_timer(); // Disable trace compilation when profile collection ends jl_force_trace_compile_timing_disable(); jl_safe_printf("\n==============================================================\n"); jl_safe_printf("Profile collected. A report will print at the next yield point.\n"); jl_safe_printf("Disabling --trace-compile\n"); jl_safe_printf("==============================================================\n\n"); JL_LOCK_NOGC(&profile_show_peek_cond_lock); if (profile_show_peek_cond_loc != NULL) uv_async_send(profile_show_peek_cond_loc); JL_UNLOCK_NOGC(&profile_show_peek_cond_lock); } } // State for delegating SIGINT handling to a dedicated listener task (similar // to the profile listener above): the signal listener marks the ^C episode // source cancelled and pings this async condition; the Base-side listener // task performs the remaining delivery work. static _Atomic(uv_async_t *) sigint_cond_loc = NULL; JL_DLLEXPORT void jl_set_sigint_cond(uv_async_t *cond) JL_NOTSAFEPOINT { // The lock pairs with deliver_sigint_notification: a notification in // flight completes its async send before exit can clear + close the // handle. (Lock-free readers only probe for NULL.) uv_mutex_lock(&sigint_state_lock); jl_atomic_store_relaxed(&sigint_cond_loc, cond); uv_mutex_unlock(&sigint_state_lock); } // Set while a ^C notification has been posted to the event loop but not yet // picked up by the julia-side sigint listener. While set, idle threads take // over running the event loop if its owning thread cannot (e.g. it is blocked // in a long-running foreign call) - see jl_task_get_next. _Atomic(int) jl_sigint_dispatch_pending = 0; // Atomically claim a pending ^C notification: the sigint listener (one per // threadpool) that wins the claim processes the episode; others re-park. JL_DLLEXPORT int jl_claim_sigint_dispatch(void) JL_NOTSAFEPOINT { return jl_atomic_exchange_relaxed(&jl_sigint_dispatch_pending, 0); } static void deliver_sigint_notification(void) JL_NOTSAFEPOINT { // Runs on a dedicated (non-Julia) thread - the signal listener thread or // a Win32 console ctrl handler thread - so blocking on the lock is fine. uv_mutex_lock(&sigint_state_lock); uv_async_t *cond = jl_atomic_load_relaxed(&sigint_cond_loc); if (cond != NULL) { jl_atomic_store_release(&jl_sigint_dispatch_pending, 1); uv_async_send(cond); // Wake every thread: a parked thread's scheduler loop performs the // dispatch pass inline on waking (see jl_dispatch_sigint_inline), // so delivery does not depend on any particular thread - in // particular not on the loop-owning thread, which may be stuck in // a long-running foreign call. (No preemption of running tasks: // the pass needs only one schedulable thread, and preempting e.g. // the REPL frontend would inject a yield into innocent code on // every press.) int nthreads = jl_atomic_load_acquire(&jl_n_threads); for (int16_t tid = 0; tid < nthreads; tid++) jl_wakeup_thread_from_foreign(tid); } uv_mutex_unlock(&sigint_state_lock); } // The cancellation token source governing the current interactive foreground // evaluation (the "^C episode source"). The REPL backend / script driver // installs a fresh source per episode via jl_set_sigint_source (Base keeps // its own rooted reference for the episode's lifetime); the signal listener // thread reads this mirror lock-free and uses the object only under a GC // exclusion (see jl_sigint_request_cancellation). static _Atomic(jl_value_t *) jl_sigint_source = NULL; // Episode generation, seqlock-style (odd while a swap is in progress): a // press records the generation it targeted, so a press for episode A can // never be delivered to a later-installed episode B (the source pointer // alone would be ABA-prone); consumption compares generations, never // dereferences. Writers are serialized by `sigint_state_lock`. static _Atomic(uint64_t) sigint_episode_gen = 2; // The generation a not-yet-dispatched press targeted; 0 = none pending. static _Atomic(uint64_t) sigint_pending_gen = 0; JL_DLLEXPORT uint64_t jl_set_sigint_source(jl_value_t *src) JL_NOTSAFEPOINT { // The signal thread may load the outgoing source concurrently: its use // is covered by GC exclusion, not rooting, and marking a just-retired // episode's subtree is the very cancellation that episode's press // requested (idempotent, monotonic). The generation brackets the swap // (odd = in progress) so the signal thread can snapshot a consistent // (source, generation) pair. uv_mutex_lock(&sigint_state_lock); uint64_t g = jl_atomic_load_relaxed(&sigint_episode_gen) + 1; // odd jl_atomic_store_release(&sigint_episode_gen, g); jl_atomic_store(&jl_sigint_source, (src == NULL || src == jl_nothing) ? NULL : src); jl_atomic_store_release(&sigint_episode_gen, g + 1); // even: complete uv_mutex_unlock(&sigint_state_lock); // Callers record the returned generation next to their rooted source // reference; a stale press (recorded under an older generation) then // self-invalidates at consumption. return g + 1; } // Consume the pending press if (and only if) it targeted the caller's // episode generation; a press recorded for another generation stays // pending for the pass that owns that episode's view. JL_DLLEXPORT int jl_consume_sigint_pending(uint64_t gen) JL_NOTSAFEPOINT { uint64_t p = jl_atomic_load_acquire(&sigint_pending_gen); if (p == 0 || p != gen) return 0; return jl_atomic_cmpswap(&sigint_pending_gen, &p, (uint64_t)0); } JL_DLLEXPORT int jl_peek_sigint_dispatch(void) JL_NOTSAFEPOINT { return jl_atomic_load_relaxed(&jl_sigint_dispatch_pending); } // Mark every descendant of `root` (including `root`) cancelled at `sev` // (CAS-max, monotonic). Only state bytes are written - waking parked waiters // stays with the julia-side listener - but this eager marking lets the // per-thread cancellation sends identify governed *running* tasks purely by // their bound source's state, including tasks bound to scoped descendant // sources (e.g. the source a `@sync` installs). // // The traversal is lock-free by the child lists' design (mutators only // prepend via CAS on `child_head`; the collector unlinks dead entries with // the world stopped), and linear in the live subtree: a node is descended // into only when our CAS actually advanced its state (state-advance // deduplication, like `Base.cancel!`'s walk), and attachment is // level-triggered (a child linked under a cancelled parent is born marked), // so no descendant is missed. The worklist may malloc: this runs on // ordinary threads (the unix sigwait listener thread, a Windows // console-handler thread), NOT in async-signal context. (The one // nearly-theoretical exception is a Windows CRT `raise(SIGINT)` via // crt_sig_handler, which runs on the raising thread - unsafe only if the // raise itself came from inside malloc.) static void jl_cancel_subtree_mark(jl_cancel_source_t *root, uint8_t sev) JL_NOTSAFEPOINT { arraylist_t worklist; arraylist_new(&worklist, 0); jl_cancel_source_t *node = root; while (1) { // Mark the node before iterating its children (pre-order), pairing // with the seq_cst attach dance (publish link, then read the // parent's state): an attacher this walk misses observes the state. uint8_t st = jl_atomic_load(&node->state); int advanced = 0; while (st < sev) { if (jl_atomic_cmpswap(&node->state, &st, sev)) { advanced = 1; break; } } if (advanced) { jl_value_t *c = jl_atomic_load(&node->child_head); while (c != NULL && c != jl_nothing) { jl_cancel_source_t *cs = (jl_cancel_source_t*)c; // the sibling link lives in the child's link entry for `node` jl_cancel_parent_link_t *links = jl_cancel_source_links(cs); size_t np = cs->nparents; jl_value_t *cnext = jl_nothing; for (size_t i = 0; i < np; i++) { if (links[i].parent == node) { cnext = jl_atomic_load_relaxed(&links[i].next); break; } } arraylist_push(&worklist, cs); c = cnext; } } if (worklist.len == 0) break; node = (jl_cancel_source_t*)arraylist_pop(&worklist); } arraylist_free(&worklist); } // Shared entry point for a user-initiated interrupt (^C): mark the episode // source cancelled, mark the interrupt as pending, and notify the sigint // listener task, which performs the remaining (Julia-side) delivery work. // Callable from non-Julia threads; must not allocate GC memory or take // Julia-side locks. static void jl_sigint_request_cancellation(void) JL_NOTSAFEPOINT { // Mark the episode source cancelled (SAFE severity) here rather than // leaving it to the julia-side listener: the state byte is what every // cancellation point and signal-delivery gate reads, so the signal-based // delivery below is sufficient on its own even if the listener never // gets to run (a single-threaded session, or every thread stuck in a // long foreign call) - exactly the situation ^C must cut through. When // the listener does run, it finds the already-cancelled source and does // the remaining bookkeeping (waking parked waiters; see // `Base.redeliver!`). int have_listener = jl_atomic_load_relaxed(&sigint_cond_loc) != NULL; // The subtree walk dereferences sources reachable only through weak // child links, and this thread does not participate in stop-the-world - // exclude the collector for the walk instead. The mirror load happens // inside the exclusion so the loaded object cannot have been swept. jl_safepoint_exclude_gc_begin(); // Snapshot a consistent (source, generation) pair: seqlock read // against jl_set_sigint_source's bracketed swap. uint64_t gen; jl_cancel_source_t *src; while (1) { gen = jl_atomic_load_acquire(&sigint_episode_gen); if (gen & 1) { jl_cpu_pause(); continue; } src = (jl_cancel_source_t*)jl_atomic_load(&jl_sigint_source); if (jl_atomic_load_acquire(&sigint_episode_gen) == gen) break; } if (src != NULL) { // Mark the whole descendant subtree, so the per-thread sends below // can identify a governed running task - even one bound to a scoped // descendant source - by its bound source's state alone. jl_cancel_subtree_mark(src, 0x1); // The heavy side of the asymmetric fence pairing with the // compiler-order-only publication of task token bindings at // cancellation points (the light side; mirrors // `Threads.atomic_fence_heavy()` in `Base.cancel!`): after this, // either the sends below observe a running task's binding, or the // task's next cancellation point observes the state writes above. jl_membarrier(); } jl_safepoint_exclude_gc_end(); // Everything below reads only per-thread state and tasks' strongly-held // fields - no weakly-reachable source pointers. if (src != NULL) { // Mark the dispatch pending BEFORE the per-thread sends (idle // threads keep the event loop moving until the listener claims it). // Skipped when no listener is registered (e.g. a trimmed binary that // stubs it out): nothing would ever claim the flag, and the // scheduler's stay-awake gate would keep idle threads polling // forever. if (have_listener) jl_atomic_store_release(&jl_sigint_dispatch_pending, 1); // Interrupt asynchronously-interruptible regions right away, on // every thread: the request-5 dispatch delivers to tasks whose own // bound token source is (now) cancelled, and is a no-op for threads // running unrelated (or no) work. int nthreads = jl_atomic_load_acquire(&jl_n_threads); for (int16_t tid = 0; tid < (int16_t)nthreads; tid++) jl_send_cancellation_signal(tid); } jl_atomic_store_release(&sigint_pending_gen, gen); deliver_sigint_notification(); } static void stack_overflow_warning(void) { jl_safe_printf("Warning: detected a stack overflow; program state may be corrupted, so further execution might be unreliable.\n"); } // Async-signal-safe replacement for libc strsignal(). We call this from fatal-signal // handlers, and glibc's strsignal() is not async-signal-safe: it routes through gettext // (to localize the description), which calls malloc(). If the interrupted thread already // held the malloc arena lock, that reentrant malloc() self-deadlocks. A fixed table of // string literals avoids gettext/malloc entirely and is portable across libc flavors // (musl/BSD/macOS lack glibc's sigdescr_np/sigabbrev_np). Cases are #ifdef-guarded so this // compiles wherever a given signal is (or is not) defined. static const char *jl_strsignal(int sig) JL_NOTSAFEPOINT { switch (sig) { #ifdef SIGHUP case SIGHUP: return "Hangup"; #endif #ifdef SIGINT case SIGINT: return "Interrupt"; #endif #ifdef SIGQUIT case SIGQUIT: return "Quit"; #endif #ifdef SIGILL case SIGILL: return "Illegal instruction"; #endif #ifdef SIGTRAP case SIGTRAP: return "Trace/breakpoint trap"; #endif #ifdef SIGABRT case SIGABRT: return "Aborted"; #endif #if defined(SIGABRT_COMPAT) && (!defined(SIGABRT) || SIGABRT_COMPAT != SIGABRT) case SIGABRT_COMPAT: return "Aborted"; #endif #ifdef SIGBUS case SIGBUS: return "Bus error"; #endif #ifdef SIGFPE case SIGFPE: return "Floating point exception"; #endif #ifdef SIGKILL case SIGKILL: return "Killed"; #endif #ifdef SIGUSR1 case SIGUSR1: return "User defined signal 1"; #endif #ifdef SIGSEGV case SIGSEGV: return "Segmentation fault"; #endif #ifdef SIGUSR2 case SIGUSR2: return "User defined signal 2"; #endif #ifdef SIGPIPE case SIGPIPE: return "Broken pipe"; #endif #ifdef SIGALRM case SIGALRM: return "Alarm clock"; #endif #ifdef SIGTERM case SIGTERM: return "Terminated"; #endif #ifdef SIGBREAK case SIGBREAK: return "Break"; #endif #ifdef SIGSTKFLT case SIGSTKFLT: return "Stack fault"; #endif #ifdef SIGCHLD case SIGCHLD: return "Child exited"; #endif #ifdef SIGCONT case SIGCONT: return "Continued"; #endif #ifdef SIGSTOP case SIGSTOP: return "Stopped (signal)"; #endif #ifdef SIGTSTP case SIGTSTP: return "Stopped"; #endif #ifdef SIGTTIN case SIGTTIN: return "Stopped (tty input)"; #endif #ifdef SIGTTOU case SIGTTOU: return "Stopped (tty output)"; #endif #ifdef SIGURG case SIGURG: return "Urgent I/O condition"; #endif #ifdef SIGXCPU case SIGXCPU: return "CPU time limit exceeded"; #endif #ifdef SIGXFSZ case SIGXFSZ: return "File size limit exceeded"; #endif #ifdef SIGVTALRM case SIGVTALRM: return "Virtual timer expired"; #endif #ifdef SIGPROF case SIGPROF: return "Profiling timer expired"; #endif #ifdef SIGWINCH case SIGWINCH: return "Window changed"; #endif #ifdef SIGSYS case SIGSYS: return "Bad system call"; #endif default: return "Unknown signal"; } } #if defined(_WIN32) #include "signals-win.c" #else #include "signals-unix.c" #endif // jl_send_reset_signal is the static per-platform delivery defined by the // file included above; `reset_code` becomes the reset point's setjmp return. JL_DLLEXPORT void jl_send_cancellation_signal(int16_t tid) JL_NOTSAFEPOINT { jl_send_reset_signal(tid, JL_RESET_CODE_CANCEL); } // Request a cooperative yield from the target thread's current task: mark // the task (honored at its next cancellation point) before kicking it out // of any published reset region. JL_DLLEXPORT void jl_send_preempt_signal(int16_t tid) JL_NOTSAFEPOINT { if (tid < 0 || tid >= jl_atomic_load_acquire(&jl_n_threads)) return; jl_ptls_t ptls2 = jl_atomic_load_relaxed(&jl_all_tls_states)[tid]; if (ptls2 == NULL) return; jl_task_t *ct2 = jl_atomic_load_relaxed(&ptls2->current_task); if (ct2 == NULL) return; jl_atomic_store_release(&ct2->preempt_request, 1); jl_send_reset_signal(tid, JL_RESET_CODE_PREEMPT); } // Deliver cancellation shootdowns to every thread whose current task is // bound to a now-cancelled token source; called by `cancel!` after // propagation and a heavy fence. The check is only a hint (the sender // re-validates; a missed task recovers level-triggered), and the unrooted // token read is safe because this thread runs GC-unsafe. JL_DLLEXPORT void jl_shootdown_cancelled_tasks(void) JL_NOTSAFEPOINT { int nthreads = jl_atomic_load_acquire(&jl_n_threads); for (int16_t tid = 0; tid < nthreads; tid++) { jl_ptls_t ptls2 = jl_atomic_load_relaxed(&jl_all_tls_states)[tid]; if (ptls2 == NULL) continue; jl_task_t *ct2 = jl_atomic_load_relaxed(&ptls2->current_task); if (ct2 == NULL) continue; jl_value_t *bound = jl_atomic_load_relaxed(&ct2->bound_cancel_token); if (bound == NULL || bound == jl_nothing || jl_atomic_load_relaxed(&((jl_cancel_source_t*)bound)->state) == 0) continue; jl_send_cancellation_signal(tid); } } static uintptr_t jl_get_pc_from_ctx(const void *_ctx) { #if defined(_OS_LINUX_) && defined(_CPU_X86_64_) return ((ucontext_t*)_ctx)->uc_mcontext.gregs[REG_RIP]; #elif defined(_OS_FREEBSD_) && defined(_CPU_X86_64_) return ((ucontext_t*)_ctx)->uc_mcontext.mc_rip; #elif defined(_OS_LINUX_) && defined(_CPU_X86_) return ((ucontext_t*)_ctx)->uc_mcontext.gregs[REG_EIP]; #elif defined(_OS_FREEBSD_) && defined(_CPU_X86_) return ((ucontext_t*)_ctx)->uc_mcontext.mc_eip; #elif defined(_OS_DARWIN_) && defined(_CPU_x86_64_) return ((ucontext64_t*)_ctx)->uc_mcontext64->__ss.__rip; #elif defined(_OS_DARWIN_) && defined(_CPU_AARCH64_) return ((ucontext64_t*)_ctx)->uc_mcontext64->__ss.__pc; #elif defined(_OS_WINDOWS_) && defined(_CPU_X86_) return ((CONTEXT*)_ctx)->Eip; #elif defined(_OS_WINDOWS_) && defined(_CPU_X86_64_) return ((CONTEXT*)_ctx)->Rip; #elif defined(_OS_LINUX_) && defined(_CPU_AARCH64_) return ((ucontext_t*)_ctx)->uc_mcontext.pc; #elif defined(_OS_FREEBSD_) && defined(_CPU_AARCH64_) return ((ucontext_t*)_ctx)->uc_mcontext.mc_gpregs.gp_elr; #elif defined(_OS_LINUX_) && defined(_CPU_ARM_) return ((ucontext_t*)_ctx)->uc_mcontext.arm_pc; #elif defined(_OS_LINUX_) && defined(_CPU_RISCV64_) return ((ucontext_t*)_ctx)->uc_mcontext.__gregs[REG_PC]; #else // TODO for PPC return 0; #endif } static void jl_fprint_sigill(ios_t *s, void *_ctx) { char *pc = (char*)jl_get_pc_from_ctx(_ctx); // unsupported platform if (!pc) return; #if defined(_CPU_X86_64_) || defined(_CPU_X86_) uint8_t inst[15]; // max length of x86 instruction size_t len = jl_safe_read_mem(pc, (char*)inst, sizeof(inst)); // ud2 if (len >= 2 && inst[0] == 0x0f && inst[1] == 0x0b) { jl_safe_fprintf(s, "Unreachable reached at %p\n", (void*)pc); } else { jl_safe_fprintf(s, "Invalid instruction at %p: ", (void*)pc); for (int i = 0;i < len;i++) { if (i == 0) { jl_safe_fprintf(s, "0x%02" PRIx8, inst[i]); } else { jl_safe_fprintf(s, ", 0x%02" PRIx8, inst[i]); } } jl_safe_fprintf(s, "\n"); } #elif defined(_OS_LINUX_) && defined(_CPU_AARCH64_) uint32_t inst = 0; size_t len = jl_safe_read_mem(pc, (char*)&inst, 4); if (len < 4) jl_safe_fprintf(s, "Fault when reading instruction: %d bytes read\n", (int)len); if (inst == 0xd4200020) { // brk #0x1 // The signal might actually be SIGTRAP instead, doesn't hurt to handle it here though. jl_safe_fprintf(s, "Unreachable reached at %p\n", pc); } else { jl_safe_fprintf(s, "Invalid instruction at %p: 0x%08" PRIx32 "\n", pc, inst); } #elif defined(_OS_LINUX_) && defined(_CPU_ARM_) ucontext_t *ctx = (ucontext_t*)_ctx; if (ctx->uc_mcontext.arm_cpsr & (1 << 5)) { // Thumb uint16_t inst[2] = {0, 0}; size_t len = jl_safe_read_mem(pc, (char*)&inst, 4); if (len < 2) jl_safe_fprintf(s, "Fault when reading Thumb instruction: %d bytes read\n", (int)len); // LLVM and GCC uses different code for the trap... if (inst[0] == 0xdefe || inst[0] == 0xdeff) { // The signal might actually be SIGTRAP instead, doesn't hurt to handle it here though. jl_safe_fprintf(s, "Unreachable reached in Thumb mode at %p: 0x%04" PRIx16 "\n", (void*)pc, inst[0]); } else { jl_safe_fprintf(s, "Invalid Thumb instruction at %p: 0x%04" PRIx16 ", 0x%04" PRIx16 "\n", (void*)pc, inst[0], inst[1]); } } else { uint32_t inst = 0; size_t len = jl_safe_read_mem(pc, (char*)&inst, 4); if (len < 4) jl_safe_fprintf(s, "Fault when reading instruction: %d bytes read\n", (int)len); // LLVM and GCC uses different code for the trap... if (inst == 0xe7ffdefe || inst == 0xe7f000f0) { // The signal might actually be SIGTRAP instead, doesn't hurt to handle it here though. jl_safe_fprintf(s, "Unreachable reached in ARM mode at %p: 0x%08" PRIx32 "\n", (void*)pc, inst); } else { jl_safe_fprintf(s, "Invalid ARM instruction at %p: 0x%08" PRIx32 "\n", (void*)pc, inst); } } #elif defined(_OS_LINUX_) && defined(_CPU_RISCV64_) uint32_t inst = 0; size_t len = jl_safe_read_mem(pc, (char*)&inst, 4); if (len < 2) jl_safe_printf("Fault when reading instruction: %d bytes read\n", (int)len); if (inst == 0x00100073 || // ebreak inst == 0xc0001073 || // unimp (pseudo-instruction for illegal `csrrw x0, cycle, x0`) (inst & ((1 << 16) - 1)) == 0x0000) { // c.unimp (compressed form) // The signal might actually be SIGTRAP instead, doesn't hurt to handle it here though. jl_safe_printf("Unreachable reached at %p\n", pc); } else { jl_safe_printf("Invalid instruction at %p: 0x%08" PRIx32 "\n", pc, inst); } #else // TODO for PPC (void)_ctx; #endif } // make it invalid for a task to return from this point to its stack // this is generally quite a foolish operation, but does free you up to do // arbitrary things on this stack now without worrying about corrupt state that // existed already on it void jl_task_frame_noreturn(jl_task_t *ct) { jl_set_safe_restore(NULL); if (ct) { ct->gcstack = NULL; ct->eh = NULL; ct->world_age = 1; // Force all locks to drop. Is this a good idea? Of course not. But the alternative would probably deadlock instead of crashing. jl_ptls_t ptls = ct->ptls; small_arraylist_t *locks = &ptls->locks; #ifndef __clang_safetyanalysis__ for (size_t i = locks->len; i > 0; i--) jl_mutex_unlock_nogc((jl_mutex_t*)locks->items[i - 1]); #endif locks->len = 0; ptls->in_pure_callback = 0; ptls->in_finalizer = 0; ptls->defer_signal = 0; // forcibly exit GC (if we were in it) or safe into unsafe, without the mandatory safepoint jl_atomic_store_release(&ptls->gc_state, JL_GC_STATE_UNSAFE); surprise_wakeup(ptls); // allow continuing to use a Task that should have already died--unsafe necromancy! jl_atomic_store_relaxed(&ct->_state, JL_TASK_STATE_RUNNABLE); } } // what to do on a critical error on a thread void jl_fprint_critical_error(ios_t *s, int sig, int si_code, bt_context_t *context, jl_task_t *ct) { jl_bt_element_t *bt_data = ct ? ct->ptls->bt_data : NULL; size_t *bt_size = ct ? &ct->ptls->bt_size : NULL; size_t i, n = ct ? *bt_size : 0; // Threads unknown to Julia have no ptls, and hence no pre-allocated // backtrace buffer; a small stack buffer is used for them instead. jl_bt_element_t bt_data_foreign[JL_BT_MAX_ENTRY_SIZE * 8]; if (sig) { // kill this task, so that we cannot get back to it accidentally (via an untimely ^C or jl_fprint_backtrace in jl_exit) // and also resets the state of ct and ptls so that some code can run on this task again jl_task_frame_noreturn(ct); #ifndef _OS_WINDOWS_ sigset_t sset; sigemptyset(&sset); // n.b. In `abort()`, Apple's libSystem "helpfully" blocks all signals // on all threads but SIGABRT. But we also don't know what the thread // was doing, so unblock all critical signals so that they will crash // hard, and not just get stuck. sigaddset(&sset, SIGSEGV); sigaddset(&sset, SIGBUS); sigaddset(&sset, SIGILL); // also unblock fatal signals now, so we won't get back here twice sigaddset(&sset, SIGTERM); sigaddset(&sset, SIGABRT); sigaddset(&sset, SIGQUIT); // and the original signal is now fatal too, in case it wasn't // something already listed (?) if (sig != SIGINT) sigaddset(&sset, sig); pthread_sigmask(SIG_UNBLOCK, &sset, NULL); #endif if (si_code) jl_safe_fprintf(s, "\n[%d] signal %d (%d): %s\n", getpid(), sig, si_code, jl_strsignal(sig)); else jl_safe_fprintf(s, "\n[%d] signal %d: %s\n", getpid(), sig, jl_strsignal(sig)); if (sig == SIGQUIT) { jl_print_task_backtraces(0); } } jl_safe_fprintf(s, "in expression starting at %s:%d\n", jl_atomic_load_relaxed(&jl_filename), jl_atomic_load_relaxed(&jl_lineno)); if (context && ct) { // Must avoid extended backtrace frames here unless we're sure bt_data // is properly rooted. *bt_size = n = rec_backtrace_ctx(bt_data, JL_MAX_BT_SIZE, context, NULL); } else if (context) { // The faulting thread was not created or adopted by Julia (e.g. a // thread started by foreign code that crashed without ever calling // into Julia), so it has no Julia task or backtrace buffer. Record a // native-only backtrace into the local buffer instead, so that at // least the faulting instruction pointer is reported. #ifdef _OS_LINUX_ char thread_name[16]; // the kernel limits thread names to 16 bytes (TASK_COMM_LEN) if (prctl(PR_GET_NAME, (unsigned long)thread_name, 0, 0, 0) != 0) thread_name[0] = '\0'; jl_safe_fprintf(s, "unknown thread \"%s\" (os tid %ld); this thread is not managed by Julia, no Julia backtrace available\n", thread_name, (long)syscall(SYS_gettid)); #else jl_safe_fprintf(s, "unknown thread; this thread is not managed by Julia, no Julia backtrace available\n"); #endif bt_data = bt_data_foreign; n = rec_backtrace_ctx(bt_data, sizeof(bt_data_foreign) / sizeof(jl_bt_element_t), context, NULL); } for (i = 0; i < n; i += jl_bt_entry_size(bt_data + i)) { jl_fprint_bt_entry_codeloc(s, bt_data + i); } if (n == 0 && context) jl_safe_fprintf(s, "no backtrace could be recorded from the signal context\n"); jl_gc_debug_fprint_status(s); jl_gc_debug_fprint_critical_error(s); } #ifdef __cplusplus } #endif