Skip to content

Commit

Permalink
Initialize Fiber with an explicit stack (#15409)
Browse files Browse the repository at this point in the history
Doesn't change the public API (the stack is still taken from the current
scheduler's stack pool), but introduces an undocumented initializer that
takes the stack and stack_bottom pointers.

This will allow a few scenarios, mostly for RFC 2:

- start fibers with a fake stack when we don't need to run the fibers,
  for example during specs that only need fiber objects (and would leak
  memory since we only release stacks after the fiber has run);
- during a cross execution context spawn, we can pick a stack from the
  destination context instead of the current context (so we can recycle
  stacks from fibers that terminated in the desination context).

* Add Fiber::Stack

Holds the stack limits and whether the stack can be reused (i.e.
released back into Fiber::StackPool).

Also abstracts accessing the first addressable pointer with 16-bytes
alignment (as required by most architectures) to pass to `makecontext`.

Co-authored-by: Johannes Müller <[email protected]>
  • Loading branch information
ysbaddaden and straight-shoota authored Feb 11, 2025
1 parent 6e80a8a commit cb7782d
Show file tree
Hide file tree
Showing 9 changed files with 60 additions and 40 deletions.
4 changes: 2 additions & 2 deletions src/compiler/crystal/interpreter/context.cr
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,10 @@ class Crystal::Repl::Context
# Once the block returns, the stack is returned to the pool.
# The stack is not cleared after or before it's used.
def checkout_stack(& : UInt8* -> _)
stack, _ = @stack_pool.checkout
stack = @stack_pool.checkout

begin
yield stack.as(UInt8*)
yield stack.pointer.as(UInt8*)
ensure
@stack_pool.release(stack)
end
Expand Down
2 changes: 1 addition & 1 deletion src/crystal/scheduler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ class Crystal::Scheduler
{% elsif flag?(:interpreted) %}
# No need to change the stack bottom!
{% else %}
GC.set_stackbottom(fiber.@stack_bottom)
GC.set_stackbottom(fiber.@stack.bottom)
{% end %}

current, @thread.current_fiber = @thread.current_fiber, fiber
Expand Down
4 changes: 2 additions & 2 deletions src/crystal/system/unix/signal.cr
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,8 @@ module Crystal::System::Signal

is_stack_overflow =
begin
stack_top = Pointer(Void).new(::Fiber.current.@stack.address - 4096)
stack_bottom = ::Fiber.current.@stack_bottom
stack_top = ::Fiber.current.@stack.pointer - 4096
stack_bottom = ::Fiber.current.@stack.bottom
stack_top <= addr < stack_bottom
rescue e
Crystal::System.print_error "Error while trying to determine if a stack overflow has occurred. Probable memory corruption\n"
Expand Down
36 changes: 19 additions & 17 deletions src/fiber.cr
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require "crystal/system/thread_linked_list"
require "crystal/print_buffered"
require "./fiber/context"
require "./fiber/stack"

# :nodoc:
@[NoInline]
Expand Down Expand Up @@ -56,12 +57,11 @@ class Fiber
end

@context : Context
@stack : Void*
@stack : Stack
@resume_event : Crystal::EventLoop::Event?
@timeout_event : Crystal::EventLoop::Event?
# :nodoc:
property timeout_select_action : Channel::TimeoutAction?
protected property stack_bottom : Void*

# The name of the fiber, used as internal reference.
property name : String?
Expand Down Expand Up @@ -97,31 +97,30 @@ class Fiber
# When the fiber is executed, it runs *proc* in its context.
#
# *name* is an optional and used only as an internal reference.
def initialize(@name : String? = nil, &@proc : ->)
@context = Context.new
@stack, @stack_bottom =
def self.new(name : String? = nil, &proc : ->)
stack =
{% if flag?(:interpreted) %}
{Pointer(Void).null, Pointer(Void).null}
# the interpreter is managing the stacks
Stack.new(Pointer(Void).null, Pointer(Void).null)
{% else %}
Crystal::Scheduler.stack_pool.checkout
{% end %}
new(name, stack, &proc)
end

fiber_main = ->(f : Fiber) { f.run }

# point to first addressable pointer on the stack (@stack_bottom points past
# the stack because the stack grows down):
stack_ptr = @stack_bottom - sizeof(Void*)

# align the stack pointer to 16 bytes:
stack_ptr = Pointer(Void*).new(stack_ptr.address & ~0x0f_u64)
# :nodoc:
def initialize(@name : String?, @stack : Stack, &@proc : ->)
@context = Context.new

fiber_main = ->(f : Fiber) { f.run }
stack_ptr = @stack.first_addressable_pointer
makecontext(stack_ptr, fiber_main)

Fiber.fibers.push(self)
end

# :nodoc:
def initialize(@stack : Void*, thread)
def initialize(stack : Void*, thread)
@proc = Proc(Void).new { }

# TODO: should creating a new context for the main fiber also be platform specific?
Expand All @@ -133,7 +132,10 @@ class Fiber
{% else %}
Context.new(_fiber_get_stack_top)
{% end %}
thread.gc_thread_handler, @stack_bottom = GC.current_thread_stack_bottom

thread.gc_thread_handler, stack_bottom = GC.current_thread_stack_bottom
@stack = Stack.new(stack, stack_bottom)

@name = "main"
{% if flag?(:preview_mt) %} @current_thread.set(thread) {% end %}
Fiber.fibers.push(self)
Expand Down Expand Up @@ -317,7 +319,7 @@ class Fiber
# :nodoc:
def push_gc_roots : Nil
# Push the used section of the stack
GC.push_stack @context.stack_top, @stack_bottom
GC.push_stack @context.stack_top, @stack.bottom
end

{% if flag?(:preview_mt) %}
Expand Down
8 changes: 4 additions & 4 deletions src/fiber/context/aarch64-microsoft.cr
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ class Fiber

# actual stack top, not including guard pages and reserved pages
LibC.GetNativeSystemInfo(out system_info)
stack_top = @stack_bottom - system_info.dwPageSize
stack_top = @stack.bottom - system_info.dwPageSize

stack_ptr[-4] = self.as(Void*) # x0 (r0): puts `self` as first argument for `fiber_main`
stack_ptr[-16] = fiber_main.pointer # x30 (lr): initial `resume` will `ret` to this address

# The following three values are stored in the Thread Information Block (NT_TIB)
# and are used by Windows to track the current stack limits
stack_ptr[-3] = @stack # [x18, #0x1478]: Win32 DeallocationStack
stack_ptr[-2] = stack_top # [x18, #16]: Stack Limit
stack_ptr[-1] = @stack_bottom # [x18, #8]: Stack Base
stack_ptr[-3] = @stack.pointer # [x18, #0x1478]: Win32 DeallocationStack
stack_ptr[-2] = stack_top # [x18, #16]: Stack Limit
stack_ptr[-1] = @stack.bottom # [x18, #8]: Stack Base
end

# :nodoc:
Expand Down
8 changes: 4 additions & 4 deletions src/fiber/context/x86_64-microsoft.cr
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ class Fiber

# actual stack top, not including guard pages and reserved pages
LibC.GetNativeSystemInfo(out system_info)
stack_top = @stack_bottom - system_info.dwPageSize
stack_top = @stack.bottom - system_info.dwPageSize

stack_ptr -= 4 # shadow space (or home space) before return address
stack_ptr[0] = fiber_main.pointer # %rbx: Initial `resume` will `ret` to this address
stack_ptr[-1] = self.as(Void*) # %rcx: puts `self` as first argument for `fiber_main`

# The following three values are stored in the Thread Information Block (NT_TIB)
# and are used by Windows to track the current stack limits
stack_ptr[-2] = @stack # %gs:0x1478: Win32 DeallocationStack
stack_ptr[-3] = stack_top # %gs:0x10: Stack Limit
stack_ptr[-4] = @stack_bottom # %gs:0x08: Stack Base
stack_ptr[-2] = @stack.pointer # %gs:0x1478: Win32 DeallocationStack
stack_ptr[-3] = stack_top # %gs:0x10: Stack Limit
stack_ptr[-4] = @stack.bottom # %gs:0x08: Stack Base
end

# :nodoc:
Expand Down
17 changes: 17 additions & 0 deletions src/fiber/stack.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class Fiber
# :nodoc:
struct Stack
getter pointer : Void*
getter bottom : Void*
getter? reusable : Bool

def initialize(@pointer, @bottom, *, @reusable = false)
end

def first_addressable_pointer : Void**
ptr = @bottom # stacks grow down
ptr -= sizeof(Void*) # point to first addressable pointer
Pointer(Void*).new(ptr.address & ~15_u64) # align to 16 bytes
end
end
end
19 changes: 10 additions & 9 deletions src/fiber/stack_pool.cr
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ class Fiber
# Interpreter stacks grow upwards (pushing values increases the stack
# pointer value) rather than downwards, so *protect* must be false.
def initialize(@protect : Bool = true)
@deque = Deque(Void*).new
@deque = Deque(Stack).new
end

def finalize
@deque.each do |stack|
Crystal::System::Fiber.free_stack(stack, STACK_SIZE)
Crystal::System::Fiber.free_stack(stack.pointer, STACK_SIZE)
end
end

Expand All @@ -26,7 +26,7 @@ class Fiber
def collect(count = lazy_size // 2) : Nil
count.times do
if stack = @deque.shift?
Crystal::System::Fiber.free_stack(stack, STACK_SIZE)
Crystal::System::Fiber.free_stack(stack.pointer, STACK_SIZE)
else
return
end
Expand All @@ -41,18 +41,19 @@ class Fiber
end

# Removes a stack from the bottom of the pool, or allocates a new one.
def checkout : {Void*, Void*}
def checkout : Stack
if stack = @deque.pop?
Crystal::System::Fiber.reset_stack(stack, STACK_SIZE, @protect)
Crystal::System::Fiber.reset_stack(stack.pointer, STACK_SIZE, @protect)
stack
else
stack = Crystal::System::Fiber.allocate_stack(STACK_SIZE, @protect)
pointer = Crystal::System::Fiber.allocate_stack(STACK_SIZE, @protect)
Stack.new(pointer, pointer + STACK_SIZE, reusable: true)
end
{stack, stack + STACK_SIZE}
end

# Appends a stack to the bottom of the pool.
def release(stack) : Nil
@deque.push(stack)
def release(stack : Stack) : Nil
@deque.push(stack) if stack.reusable?
end

# Returns the approximated size of the pool. It may be equal or slightly
Expand Down
2 changes: 1 addition & 1 deletion src/gc/boehm.cr
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ module GC
{% if flag?(:preview_mt) %}
Thread.unsafe_each do |thread|
if fiber = thread.current_fiber?
GC.set_stackbottom(thread.gc_thread_handler, fiber.@stack_bottom)
GC.set_stackbottom(thread.gc_thread_handler, fiber.@stack.bottom)
end
end
{% end %}
Expand Down

0 comments on commit cb7782d

Please sign in to comment.