Skip to content

Commit cb7782d

Browse files
Initialize Fiber with an explicit stack (crystal-lang#15409)
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]>
1 parent 6e80a8a commit cb7782d

File tree

9 files changed

+60
-40
lines changed

9 files changed

+60
-40
lines changed

src/compiler/crystal/interpreter/context.cr

+2-2
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,10 @@ class Crystal::Repl::Context
106106
# Once the block returns, the stack is returned to the pool.
107107
# The stack is not cleared after or before it's used.
108108
def checkout_stack(& : UInt8* -> _)
109-
stack, _ = @stack_pool.checkout
109+
stack = @stack_pool.checkout
110110

111111
begin
112-
yield stack.as(UInt8*)
112+
yield stack.pointer.as(UInt8*)
113113
ensure
114114
@stack_pool.release(stack)
115115
end

src/crystal/scheduler.cr

+1-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ class Crystal::Scheduler
124124
{% elsif flag?(:interpreted) %}
125125
# No need to change the stack bottom!
126126
{% else %}
127-
GC.set_stackbottom(fiber.@stack_bottom)
127+
GC.set_stackbottom(fiber.@stack.bottom)
128128
{% end %}
129129

130130
current, @thread.current_fiber = @thread.current_fiber, fiber

src/crystal/system/unix/signal.cr

+2-2
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,8 @@ module Crystal::System::Signal
183183

184184
is_stack_overflow =
185185
begin
186-
stack_top = Pointer(Void).new(::Fiber.current.@stack.address - 4096)
187-
stack_bottom = ::Fiber.current.@stack_bottom
186+
stack_top = ::Fiber.current.@stack.pointer - 4096
187+
stack_bottom = ::Fiber.current.@stack.bottom
188188
stack_top <= addr < stack_bottom
189189
rescue e
190190
Crystal::System.print_error "Error while trying to determine if a stack overflow has occurred. Probable memory corruption\n"

src/fiber.cr

+19-17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require "crystal/system/thread_linked_list"
22
require "crystal/print_buffered"
33
require "./fiber/context"
4+
require "./fiber/stack"
45

56
# :nodoc:
67
@[NoInline]
@@ -56,12 +57,11 @@ class Fiber
5657
end
5758

5859
@context : Context
59-
@stack : Void*
60+
@stack : Stack
6061
@resume_event : Crystal::EventLoop::Event?
6162
@timeout_event : Crystal::EventLoop::Event?
6263
# :nodoc:
6364
property timeout_select_action : Channel::TimeoutAction?
64-
protected property stack_bottom : Void*
6565

6666
# The name of the fiber, used as internal reference.
6767
property name : String?
@@ -97,31 +97,30 @@ class Fiber
9797
# When the fiber is executed, it runs *proc* in its context.
9898
#
9999
# *name* is an optional and used only as an internal reference.
100-
def initialize(@name : String? = nil, &@proc : ->)
101-
@context = Context.new
102-
@stack, @stack_bottom =
100+
def self.new(name : String? = nil, &proc : ->)
101+
stack =
103102
{% if flag?(:interpreted) %}
104-
{Pointer(Void).null, Pointer(Void).null}
103+
# the interpreter is managing the stacks
104+
Stack.new(Pointer(Void).null, Pointer(Void).null)
105105
{% else %}
106106
Crystal::Scheduler.stack_pool.checkout
107107
{% end %}
108+
new(name, stack, &proc)
109+
end
108110

109-
fiber_main = ->(f : Fiber) { f.run }
110-
111-
# point to first addressable pointer on the stack (@stack_bottom points past
112-
# the stack because the stack grows down):
113-
stack_ptr = @stack_bottom - sizeof(Void*)
114-
115-
# align the stack pointer to 16 bytes:
116-
stack_ptr = Pointer(Void*).new(stack_ptr.address & ~0x0f_u64)
111+
# :nodoc:
112+
def initialize(@name : String?, @stack : Stack, &@proc : ->)
113+
@context = Context.new
117114

115+
fiber_main = ->(f : Fiber) { f.run }
116+
stack_ptr = @stack.first_addressable_pointer
118117
makecontext(stack_ptr, fiber_main)
119118

120119
Fiber.fibers.push(self)
121120
end
122121

123122
# :nodoc:
124-
def initialize(@stack : Void*, thread)
123+
def initialize(stack : Void*, thread)
125124
@proc = Proc(Void).new { }
126125

127126
# TODO: should creating a new context for the main fiber also be platform specific?
@@ -133,7 +132,10 @@ class Fiber
133132
{% else %}
134133
Context.new(_fiber_get_stack_top)
135134
{% end %}
136-
thread.gc_thread_handler, @stack_bottom = GC.current_thread_stack_bottom
135+
136+
thread.gc_thread_handler, stack_bottom = GC.current_thread_stack_bottom
137+
@stack = Stack.new(stack, stack_bottom)
138+
137139
@name = "main"
138140
{% if flag?(:preview_mt) %} @current_thread.set(thread) {% end %}
139141
Fiber.fibers.push(self)
@@ -317,7 +319,7 @@ class Fiber
317319
# :nodoc:
318320
def push_gc_roots : Nil
319321
# Push the used section of the stack
320-
GC.push_stack @context.stack_top, @stack_bottom
322+
GC.push_stack @context.stack_top, @stack.bottom
321323
end
322324

323325
{% if flag?(:preview_mt) %}

src/fiber/context/aarch64-microsoft.cr

+4-4
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,16 @@ class Fiber
1313

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

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

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

2828
# :nodoc:

src/fiber/context/x86_64-microsoft.cr

+4-4
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,17 @@ class Fiber
1212

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

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

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

2828
# :nodoc:

src/fiber/stack.cr

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
class Fiber
2+
# :nodoc:
3+
struct Stack
4+
getter pointer : Void*
5+
getter bottom : Void*
6+
getter? reusable : Bool
7+
8+
def initialize(@pointer, @bottom, *, @reusable = false)
9+
end
10+
11+
def first_addressable_pointer : Void**
12+
ptr = @bottom # stacks grow down
13+
ptr -= sizeof(Void*) # point to first addressable pointer
14+
Pointer(Void*).new(ptr.address & ~15_u64) # align to 16 bytes
15+
end
16+
end
17+
end

src/fiber/stack_pool.cr

+10-9
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ class Fiber
1212
# Interpreter stacks grow upwards (pushing values increases the stack
1313
# pointer value) rather than downwards, so *protect* must be false.
1414
def initialize(@protect : Bool = true)
15-
@deque = Deque(Void*).new
15+
@deque = Deque(Stack).new
1616
end
1717

1818
def finalize
1919
@deque.each do |stack|
20-
Crystal::System::Fiber.free_stack(stack, STACK_SIZE)
20+
Crystal::System::Fiber.free_stack(stack.pointer, STACK_SIZE)
2121
end
2222
end
2323

@@ -26,7 +26,7 @@ class Fiber
2626
def collect(count = lazy_size // 2) : Nil
2727
count.times do
2828
if stack = @deque.shift?
29-
Crystal::System::Fiber.free_stack(stack, STACK_SIZE)
29+
Crystal::System::Fiber.free_stack(stack.pointer, STACK_SIZE)
3030
else
3131
return
3232
end
@@ -41,18 +41,19 @@ class Fiber
4141
end
4242

4343
# Removes a stack from the bottom of the pool, or allocates a new one.
44-
def checkout : {Void*, Void*}
44+
def checkout : Stack
4545
if stack = @deque.pop?
46-
Crystal::System::Fiber.reset_stack(stack, STACK_SIZE, @protect)
46+
Crystal::System::Fiber.reset_stack(stack.pointer, STACK_SIZE, @protect)
47+
stack
4748
else
48-
stack = Crystal::System::Fiber.allocate_stack(STACK_SIZE, @protect)
49+
pointer = Crystal::System::Fiber.allocate_stack(STACK_SIZE, @protect)
50+
Stack.new(pointer, pointer + STACK_SIZE, reusable: true)
4951
end
50-
{stack, stack + STACK_SIZE}
5152
end
5253

5354
# Appends a stack to the bottom of the pool.
54-
def release(stack) : Nil
55-
@deque.push(stack)
55+
def release(stack : Stack) : Nil
56+
@deque.push(stack) if stack.reusable?
5657
end
5758

5859
# Returns the approximated size of the pool. It may be equal or slightly

src/gc/boehm.cr

+1-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ module GC
223223
{% if flag?(:preview_mt) %}
224224
Thread.unsafe_each do |thread|
225225
if fiber = thread.current_fiber?
226-
GC.set_stackbottom(thread.gc_thread_handler, fiber.@stack_bottom)
226+
GC.set_stackbottom(thread.gc_thread_handler, fiber.@stack.bottom)
227227
end
228228
end
229229
{% end %}

0 commit comments

Comments
 (0)