Skip to content

Commit

Permalink
Stop & start the world (undocumented API) (#14729)
Browse files Browse the repository at this point in the history
Add `GC.stop_world` and `GC.start_world` methods to be able to stop and restart the world at will from within Crystal.

- gc/boehm: delegates to `GC_stop_world_external` and `GC_start_world_external`;
- gc/none: implements its own mechanism (tested on UNIX & Windows).

My use case is a [perf-tools](https://github.com/crystal-lang/perf-tools) feature for [RFC 2](crystal-lang/rfcs#2) that must stop the world to print out runtime information of each ExecutionContext with their schedulers and fibers. See crystal-lang/perf-tools#18
  • Loading branch information
ysbaddaden committed Aug 7, 2024
1 parent bddb53f commit 8f26137
Show file tree
Hide file tree
Showing 22 changed files with 282 additions and 25 deletions.
36 changes: 36 additions & 0 deletions src/crystal/system/thread.cr
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ module Crystal::System::Thread
# private def stack_address : Void*

# private def system_name=(String) : String

# def self.init_suspend_resume : Nil

# private def system_suspend : Nil

# private def system_wait_suspended : Nil

# private def system_resume : Nil
end

{% if flag?(:wasi) %}
Expand Down Expand Up @@ -66,6 +74,14 @@ class Thread
@@threads.try(&.unsafe_each { |thread| yield thread })
end

def self.lock : Nil
threads.@mutex.lock
end

def self.unlock : Nil
threads.@mutex.unlock
end

# Creates and starts a new system thread.
def initialize(@name : String? = nil, &@func : Thread ->)
@system_handle = uninitialized Crystal::System::Thread::Handle
Expand Down Expand Up @@ -168,6 +184,26 @@ class Thread

# Holds the GC thread handler
property gc_thread_handler : Void* = Pointer(Void).null

def suspend : Nil
system_suspend
end

def wait_suspended : Nil
system_wait_suspended
end

def resume : Nil
system_resume
end

def self.stop_world : Nil
GC.stop_world
end

def self.start_world : Nil
GC.start_world
end
end

require "./thread_linked_list"
Expand Down
75 changes: 75 additions & 0 deletions src/crystal/system/unix/pthread.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require "c/pthread"
require "c/sched"
require "../panic"

module Crystal::System::Thread
alias Handle = LibC::PthreadT
Expand Down Expand Up @@ -153,6 +154,80 @@ module Crystal::System::Thread
{% end %}
name
end

@suspended = Atomic(Bool).new(false)

def self.init_suspend_resume : Nil
install_sig_suspend_signal_handler
install_sig_resume_signal_handler
end

private def self.install_sig_suspend_signal_handler
action = LibC::Sigaction.new
action.sa_flags = LibC::SA_SIGINFO
action.sa_sigaction = LibC::SigactionHandlerT.new do |_, _, _|
# notify that the thread has been interrupted
Thread.current_thread.@suspended.set(true)

# block all signals but SIG_RESUME
mask = LibC::SigsetT.new
LibC.sigfillset(pointerof(mask))
LibC.sigdelset(pointerof(mask), SIG_RESUME)

# suspend the thread until it receives the SIG_RESUME signal
LibC.sigsuspend(pointerof(mask))
end
LibC.sigemptyset(pointerof(action.@sa_mask))
LibC.sigaction(SIG_SUSPEND, pointerof(action), nil)
end

private def self.install_sig_resume_signal_handler
action = LibC::Sigaction.new
action.sa_flags = 0
action.sa_sigaction = LibC::SigactionHandlerT.new do |_, _, _|
# do nothing (a handler is still required to receive the signal)
end
LibC.sigemptyset(pointerof(action.@sa_mask))
LibC.sigaction(SIG_RESUME, pointerof(action), nil)
end

private def system_suspend : Nil
@suspended.set(false)

if LibC.pthread_kill(@system_handle, SIG_SUSPEND) == -1
System.panic("pthread_kill()", Errno.value)
end
end

private def system_wait_suspended : Nil
until @suspended.get
Thread.yield_current
end
end

private def system_resume : Nil
if LibC.pthread_kill(@system_handle, SIG_RESUME) == -1
System.panic("pthread_kill()", Errno.value)
end
end

# the suspend/resume signals follow BDWGC

private SIG_SUSPEND =
{% if flag?(:linux) %}
LibC::SIGPWR
{% elsif LibC.has_constant?(:SIGRTMIN) %}
LibC::SIGRTMIN + 6
{% else %}
LibC::SIGXFSZ
{% end %}

private SIG_RESUME =
{% if LibC.has_constant?(:SIGRTMIN) %}
LibC::SIGRTMIN + 5
{% else %}
LibC::SIGXCPU
{% end %}
end

# In musl (alpine) the calls to unwind API segfaults
Expand Down
15 changes: 15 additions & 0 deletions src/crystal/system/wasi/thread.cr
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,19 @@ module Crystal::System::Thread
# TODO: Implement
Pointer(Void).null
end

def self.init_suspend_resume : Nil
end

private def system_suspend : Nil
raise NotImplementedError.new("Crystal::System::Thread.system_suspend")
end

private def system_wait_suspended : Nil
raise NotImplementedError.new("Crystal::System::Thread.system_wait_suspended")
end

private def system_resume : Nil
raise NotImplementedError.new("Crystal::System::Thread.system_resume")
end
end
28 changes: 28 additions & 0 deletions src/crystal/system/win32/thread.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require "c/processthreadsapi"
require "c/synchapi"
require "../panic"

module Crystal::System::Thread
alias Handle = LibC::HANDLE
Expand Down Expand Up @@ -87,4 +88,31 @@ module Crystal::System::Thread
{% end %}
name
end

def self.init_suspend_resume : Nil
end

private def system_suspend : Nil
if LibC.SuspendThread(@system_handle) == -1
Crystal::System.panic("SuspendThread()", WinError.value)
end
end

private def system_wait_suspended : Nil
# context must be aligned on 16 bytes but we lack a mean to force the
# alignment on the struct, so we overallocate then realign the pointer:
local = uninitialized UInt8[sizeof(Tuple(LibC::CONTEXT, UInt8[15]))]
thread_context = Pointer(LibC::CONTEXT).new(local.to_unsafe.address &+ 15_u64 & ~15_u64)
thread_context.value.contextFlags = LibC::CONTEXT_FULL

if LibC.GetThreadContext(@system_handle, thread_context) == -1
Crystal::System.panic("GetThreadContext()", WinError.value)
end
end

private def system_resume : Nil
if LibC.ResumeThread(@system_handle) == -1
Crystal::System.panic("ResumeThread()", WinError.value)
end
end
end
13 changes: 13 additions & 0 deletions src/gc/boehm.cr
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ lib LibGC
alias WarnProc = LibC::Char*, Word ->
fun set_warn_proc = GC_set_warn_proc(WarnProc)
$warn_proc = GC_current_warn_proc : WarnProc

fun stop_world_external = GC_stop_world_external
fun start_world_external = GC_start_world_external
end

module GC
Expand Down Expand Up @@ -470,4 +473,14 @@ module GC
GC.unlock_write
end
{% end %}

# :nodoc:
def self.stop_world : Nil
LibGC.stop_world_external
end

# :nodoc:
def self.start_world : Nil
LibGC.start_world_external
end
end
54 changes: 54 additions & 0 deletions src/gc/none.cr
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ require "crystal/tracing"

module GC
def self.init
Crystal::System::Thread.init_suspend_resume
end

# :nodoc:
Expand Down Expand Up @@ -138,4 +139,57 @@ module GC
# :nodoc:
def self.push_stack(stack_top, stack_bottom)
end

# Stop and start the world.
#
# This isn't a GC-safe stop-the-world implementation (it may allocate objects
# while stopping the world), but the guarantees are enough for the purpose of
# gc_none. It could be GC-safe if Thread::LinkedList(T) became a struct, and
# Thread::Mutex either became a struct or provide low level abstraction
# methods that directly interact with syscalls (without allocating).
#
# Thread safety is guaranteed by the mutex in Thread::LinkedList: either a
# thread is starting and hasn't added itself to the list (it will block until
# it can acquire the lock), or is currently adding itself (the current thread
# will block until it can acquire the lock).
#
# In both cases there can't be a deadlock since we won't suspend another
# thread until it has successfuly added (or removed) itself to (from) the
# linked list and released the lock, and the other thread won't progress until
# it can add (or remove) itself from the list.
#
# Finally, we lock the mutex and keep it locked until we resume the world, so
# any thread waiting on the mutex will only be resumed when the world is
# resumed.

# :nodoc:
def self.stop_world : Nil
current_thread = Thread.current

# grab the lock (and keep it until the world is restarted)
Thread.lock

# tell all threads to stop (async)
Thread.unsafe_each do |thread|
thread.suspend unless thread == current_thread
end

# wait for all threads to have stopped
Thread.unsafe_each do |thread|
thread.wait_suspended unless thread == current_thread
end
end

# :nodoc:
def self.start_world : Nil
current_thread = Thread.current

# tell all threads to resume
Thread.unsafe_each do |thread|
thread.resume unless thread == current_thread
end

# finally, we can release the lock
Thread.unlock
end
end
2 changes: 2 additions & 0 deletions src/lib_c/aarch64-android/c/signal.cr
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ lib LibC

fun kill(__pid : PidT, __signal : Int) : Int
fun pthread_sigmask(__how : Int, __new_set : SigsetT*, __old_set : SigsetT*) : Int
fun pthread_kill(__thread : PthreadT, __sig : Int) : Int
fun sigaction(__signal : Int, __new_action : Sigaction*, __old_action : Sigaction*) : Int
fun sigaltstack(__new_signal_stack : StackT*, __old_signal_stack : StackT*) : Int
{% if ANDROID_API >= 21 %}
Expand All @@ -89,5 +90,6 @@ lib LibC
fun sigaddset(__set : SigsetT*, __signal : Int) : Int
fun sigdelset(__set : SigsetT*, __signal : Int) : Int
fun sigismember(__set : SigsetT*, __signal : Int) : Int
fun sigsuspend(__mask : SigsetT*) : Int
{% end %}
end
2 changes: 2 additions & 0 deletions src/lib_c/aarch64-darwin/c/signal.cr
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ lib LibC

fun kill(x0 : PidT, x1 : Int) : Int
fun pthread_sigmask(Int, SigsetT*, SigsetT*) : Int
fun pthread_kill(PthreadT, Int) : Int
fun signal(x0 : Int, x1 : Int -> Void) : Int -> Void
fun sigaction(x0 : Int, x1 : Sigaction*, x2 : Sigaction*) : Int
fun sigaltstack(x0 : StackT*, x1 : StackT*) : Int
Expand All @@ -85,4 +86,5 @@ lib LibC
fun sigaddset(SigsetT*, Int) : Int
fun sigdelset(SigsetT*, Int) : Int
fun sigismember(SigsetT*, Int) : Int
fun sigsuspend(SigsetT*) : Int
end
2 changes: 2 additions & 0 deletions src/lib_c/aarch64-linux-gnu/c/signal.cr
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ lib LibC

fun kill(pid : PidT, sig : Int) : Int
fun pthread_sigmask(Int, SigsetT*, SigsetT*) : Int
fun pthread_kill(PthreadT, Int) : Int
fun signal(sig : Int, handler : Int -> Void) : Int -> Void
fun sigaction(x0 : Int, x1 : Sigaction*, x2 : Sigaction*) : Int
fun sigaltstack(x0 : StackT*, x1 : StackT*) : Int
Expand All @@ -86,4 +87,5 @@ lib LibC
fun sigaddset(SigsetT*, Int) : Int
fun sigdelset(SigsetT*, Int) : Int
fun sigismember(SigsetT*, Int) : Int
fun sigsuspend(SigsetT*) : Int
end
2 changes: 2 additions & 0 deletions src/lib_c/aarch64-linux-musl/c/signal.cr
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ lib LibC

fun kill(x0 : PidT, x1 : Int) : Int
fun pthread_sigmask(Int, SigsetT*, SigsetT*) : Int
fun pthread_kill(PthreadT, Int) : Int
fun signal(x0 : Int, x1 : Int -> Void) : Int -> Void
fun sigaction(x0 : Int, x1 : Sigaction*, x2 : Sigaction*) : Int
fun sigaltstack(x0 : StackT*, x1 : StackT*) : Int
Expand All @@ -85,4 +86,5 @@ lib LibC
fun sigaddset(SigsetT*, Int) : Int
fun sigdelset(SigsetT*, Int) : Int
fun sigismember(SigsetT*, Int) : Int
fun sigsuspend(SigsetT*) : Int
end
2 changes: 2 additions & 0 deletions src/lib_c/arm-linux-gnueabihf/c/signal.cr
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ lib LibC

fun kill(pid : PidT, sig : Int) : Int
fun pthread_sigmask(Int, SigsetT*, SigsetT*) : Int
fun pthread_kill(PthreadT, Int) : Int
fun signal(sig : Int, handler : Int -> Void) : Int -> Void
fun sigaction(x0 : Int, x1 : Sigaction*, x2 : Sigaction*) : Int
fun sigaltstack(x0 : StackT*, x1 : StackT*) : Int
Expand All @@ -85,4 +86,5 @@ lib LibC
fun sigaddset(SigsetT*, Int) : Int
fun sigdelset(SigsetT*, Int) : Int
fun sigismember(SigsetT*, Int) : Int
fun sigsuspend(SigsetT*) : Int
end
2 changes: 2 additions & 0 deletions src/lib_c/i386-linux-gnu/c/signal.cr
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ lib LibC

fun kill(pid : PidT, sig : Int) : Int
fun pthread_sigmask(Int, SigsetT*, SigsetT*) : Int
fun pthread_kill(PthreadT, Int) : Int
fun signal(sig : Int, handler : Int -> Void) : Int -> Void
fun sigaction(x0 : Int, x1 : Sigaction*, x2 : Sigaction*) : Int
fun sigaltstack(x0 : StackT*, x1 : StackT*) : Int
Expand All @@ -85,4 +86,5 @@ lib LibC
fun sigaddset(SigsetT*, Int) : Int
fun sigdelset(SigsetT*, Int) : Int
fun sigismember(SigsetT*, Int) : Int
fun sigsuspend(SigsetT*) : Int
end
2 changes: 2 additions & 0 deletions src/lib_c/i386-linux-musl/c/signal.cr
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ lib LibC

fun kill(x0 : PidT, x1 : Int) : Int
fun pthread_sigmask(Int, SigsetT*, SigsetT*) : Int
fun pthread_kill(PthreadT, Int) : Int
fun signal(x0 : Int, x1 : Int -> Void) : Int -> Void
fun sigaction(x0 : Int, x1 : Sigaction*, x2 : Sigaction*) : Int
fun sigaltstack(x0 : StackT*, x1 : StackT*) : Int
Expand All @@ -84,4 +85,5 @@ lib LibC
fun sigaddset(SigsetT*, Int) : Int
fun sigdelset(SigsetT*, Int) : Int
fun sigismember(SigsetT*, Int) : Int
fun sigsuspend(SigsetT*) : Int
end
Loading

0 comments on commit 8f26137

Please sign in to comment.