|
module Kernel |
|
module Memory |
|
|
|
# Provides high-level memory management functionality for dynamic memory |
|
# allocations. |
|
# |
|
# The two public methods are {#allocate} and {#free}. |
|
module Allocator |
|
extend self |
|
|
|
CHILD_MAGIC = 0xAF96324Au32 |
|
PARENT_MAGIC = 0x7EAC0FFEu32 |
|
|
|
CHILD_FREE_MAGIC = CHILD_MAGIC - 1u32 |
|
|
|
# Forward declarations |
|
struct Parent; end |
|
struct Child; end |
|
|
|
# Manages a small memory region inside a {Parent} |
|
struct Child |
|
@available : Bool |
|
@size : UInt32 |
|
@parent : Parent* |
|
@next_child : Child*? |
|
@magic : UInt32 = CHILD_MAGIC |
|
|
|
def available?; @available; end |
|
def size; @size; end |
|
def next_child; @next_child; end |
|
def parent; @parent; end |
|
|
|
def available=(v); @available = v; end |
|
def size=(v); @size = v; end |
|
def next_child=(v); @next_child = v; end |
|
|
|
def initialize(@parent, @size, @available = false, @next_child = nil) |
|
end |
|
|
|
def dispose |
|
@magic = CHILD_FREE_MAGIC |
|
end |
|
|
|
def verify |
|
(@magic == CHILD_MAGIC) |
|
end |
|
|
|
def verify! |
|
if @magic == CHILD_FREE_MAGIC |
|
kernel_panic "Memory Corruption: Double free detected" |
|
elsif @magic != CHILD_MAGIC |
|
kernel_panic "Memory Corruption: Wrong MAGIC in child: ", @magic |
|
end |
|
end |
|
end |
|
|
|
# Manages a large memory region using many children. |
|
# The available size of the region is {Physical::PAGE_SIZE} minus the |
|
# size of the Parent managing it and at least one {Child}. |
|
# |
|
# A parent, each managing one or more virtual memory pages, uses a |
|
# linear memory region. |
|
struct Parent |
|
@page_count : UInt32 |
|
@size_available : UInt32 |
|
@child : Child* |
|
@next_parent : Parent*? |
|
@magic : UInt32 = PARENT_MAGIC |
|
|
|
def page_count; @page_count; end |
|
def size_available; @size_available; end |
|
def child; @child; end |
|
def next_parent; @next_parent; end |
|
|
|
def size_available=(v); @size_available = v; end |
|
def child=(v); @child = v; end |
|
def next_parent=(v); @next_parent = v; end |
|
|
|
def initialize( |
|
@child, |
|
@page_count, |
|
@size_available, |
|
@next_parent = nil, |
|
) |
|
end |
|
|
|
def verify |
|
(@magic == PARENT_MAGIC) |
|
end |
|
|
|
def verify! |
|
unless @magic == PARENT_MAGIC |
|
kernel_panic "Memory Corruption: Wrong MAGIC in parent: ", @magic |
|
end |
|
end |
|
end |
|
|
|
# If the size of the child minus the size of the allocation is greater or |
|
# equal to this amount, the child will in reality have the full size |
|
# instead of it being split into smaller chunks. |
|
WASTE_THRESHOLD = sizeof(Child) * 2 |
|
|
|
# Minimum overhead per parent. |
|
BASE_OVERHEAD = (sizeof(Parent) + sizeof(Child)).to_u32 |
|
|
|
# Start of the linked-list |
|
@@first_parent : Pointer(Parent) = Pointer(Parent).null |
|
|
|
# First entry in the linked-list, which actually has space available. |
|
@@first_with_available : Pointer(Parent) = Pointer(Parent).null |
|
|
|
# Count of currently allocated pages in total |
|
@@total_page_count : UInt32 = 0u32 |
|
|
|
def first_parent; @@first_parent; end |
|
def total_page_count; @@total_page_count; end |
|
|
|
# Initializes the allocator. |
|
def start |
|
@@total_page_count = 0u32 |
|
@@first_parent = create_new_parent |
|
@@first_with_available = @@first_parent |
|
end |
|
|
|
private def create_new_parent(page_count : UInt32 = 1u32) : Pointer(Parent) |
|
size_avail = (Physical::PAGE_SIZE * page_count) - BASE_OVERHEAD |
|
|
|
parent = allocate_and_map_pages(page_count).as(Parent*) |
|
child = (parent + 1).as(Child*) |
|
|
|
child.value = Child.new( |
|
available: true, |
|
parent: parent, |
|
size: size_avail.to_u32, |
|
) |
|
|
|
parent.value = Parent.new( |
|
child: child, |
|
page_count: page_count, |
|
size_available: size_avail.to_u32, |
|
) |
|
|
|
parent |
|
end |
|
|
|
# Allocates a memory region with size in *bytes* and returns it as |
|
# void pointer. |
|
def allocate(bytes : UInt32) : Pointer(Void) |
|
# Fast-path |
|
if alloc = try_allocation_in_parent(@@first_with_available, bytes) |
|
return alloc |
|
end |
|
|
|
# Find parent with enough space |
|
previous = parent = @@first_parent |
|
while parent |
|
if alloc = try_allocation_in_parent(parent, bytes) |
|
return alloc |
|
end |
|
|
|
previous, parent = parent, parent.value.next_parent |
|
end |
|
|
|
# If we end up here, there are no parents with enough available space |
|
pages = (bytes + BASE_OVERHEAD) / Physical::PAGE_SIZE |
|
new_parent = create_new_parent(pages + 1) # Create new parent |
|
@@first_with_available = new_parent # Remember this one |
|
previous.value.next_parent = new_parent # Hook it up ... |
|
do_allocation(new_parent, new_parent.value.child, bytes) # Allocate! |
|
end |
|
|
|
# ditto |
|
def allocate(bytes) |
|
allocate bytes.to_u32 |
|
end |
|
|
|
private def try_allocation_in_parent(parent, bytes) |
|
parent.value.verify! |
|
|
|
if parent.value.size_available >= bytes |
|
try_allocation(parent, bytes) |
|
end |
|
end |
|
|
|
# Unallocates the given memory region. It must not be used afterwards for |
|
# *anything* at all anymore. |
|
def free(ptr : Pointer(Void)) : Nil |
|
child = ptr.as(Child*) - 1 |
|
parent = child.value.parent |
|
|
|
child.value.verify! # Check for memory corruption |
|
parent.value.verify! |
|
|
|
if child.value.available? |
|
kernel_panic "Memory Corruption: Double-free detected at ", ptr |
|
end |
|
|
|
child.value.available = true # Mark as available |
|
parent.value.size_available += child.value.size |
|
|
|
# Check if we can merge memory regions |
|
# Merge with previous child if it is available |
|
prev_child = find_previous_child(child) |
|
if prev_child && prev_child.value.available? |
|
child = prev_child |
|
merge_children(child) |
|
end |
|
|
|
# Merge with the next child if it is available |
|
next_child = child.value.next_child |
|
if next_child |
|
next_child.value.verify! |
|
merge_children(child) if next_child.value.available? |
|
end |
|
|
|
if is_parent_empty(parent) |
|
destroy_parent(parent) |
|
@@first_with_available = @@first_parent |
|
else @@first_with_available.value.size_available < parent.value.size_available |
|
@@first_with_available = parent |
|
end |
|
end |
|
|
|
# ditto |
|
def free(ptr : Pointer) |
|
free(ptr.as(Void*)) |
|
end |
|
|
|
# ditto |
|
def free(ptr : UInt64) |
|
free(Pointer(Void).new(ptr)) |
|
end |
|
|
|
# Verifies all parents and children. If one fails, the method |
|
# will panic. If all structures are fine, it does nothing. |
|
def verify_data_structures! : Nil |
|
parent = @@first_parent |
|
while parent |
|
parent.value.verify! |
|
child = parent.value.child |
|
available = 0u32 |
|
|
|
while child |
|
child.value.verify! |
|
available += child.value.size if child.value.available? |
|
child = child.value.next_child |
|
end |
|
|
|
if available != parent.value.size_available |
|
kernel_panic "Wrong available size for parent ", parent, ": ", available, " != ", parent.value.size_available |
|
end |
|
|
|
parent = parent.value.next_parent |
|
end |
|
end |
|
|
|
private def destroy_parent(parent) |
|
previous = find_previous_parent(parent) |
|
|
|
# Remove `parent` from the linked-list |
|
previous.value.next_parent = parent.value.next_parent |
|
|
|
free_parent(parent) |
|
end |
|
|
|
private def free_parent(parent) : Nil |
|
@@total_page_count -= parent.value.page_count |
|
|
|
# Unmap the virtual memory for it |
|
vmm = Virtual.kernel_instance |
|
parent.value.page_count.times do |page_off| |
|
vmm.value.unmap(parent.address + page_off * Physical::PAGE_SIZE) |
|
end |
|
end |
|
|
|
private def find_previous_parent(parent) : Pointer(Parent) |
|
current = previous = @@first_parent |
|
|
|
return parent if parent == current |
|
|
|
while current != parent |
|
current.value.verify! |
|
current, previous = current.value.next_parent, current |
|
return @@first_parent unless current |
|
end |
|
|
|
previous || @@first_parent |
|
end |
|
|
|
private def is_parent_empty(parent) : Bool |
|
|
|
# Make sure we never deallocate the first parent |
|
return false if parent == @@first_parent |
|
|
|
max_available = (parent.value.page_count * Physical::PAGE_SIZE) - BASE_OVERHEAD |
|
has_available = parent.value.size_available |
|
|
|
if max_available == has_available |
|
true |
|
elsif has_available > max_available |
|
kernel_panic "Parent has more space available than possible: ", has_available, " > ", max_available |
|
else |
|
false |
|
end |
|
end |
|
|
|
private def find_previous_child(child) : Pointer(Child) | Nil |
|
parent = child.value.parent |
|
current = previous = parent.value.child |
|
|
|
# Break if this is the first child. |
|
return nil if current == child |
|
|
|
while current != child |
|
current.value.verify! |
|
current, previous = current.value.next_child, current |
|
return nil unless current |
|
end |
|
|
|
previous |
|
end |
|
|
|
private def merge_children(child) : Nil |
|
partner = child.value.next_child |
|
|
|
# We need a next_child to merge with |
|
kernel_panic "Allocator#merge_children: next_child is nil" unless partner |
|
|
|
# They must have the same parents |
|
if child.value.parent != partner.value.parent |
|
kernel_panic "Allocator#merge_children: Attempting to merge children of different parents" |
|
end |
|
|
|
# Update child |
|
child.value.size += sizeof(Child) + partner.value.size |
|
|
|
# Remove `partner` out of the linked list |
|
child.value.next_child = partner.value.next_child |
|
child.value.parent.value.size_available += sizeof(Child) |
|
|
|
# Mark partner as disposed |
|
partner.value.dispose |
|
end |
|
|
|
private def try_allocation(parent, bytes) : Pointer(Void) | Nil |
|
child, previous = find_available_child(parent, bytes) |
|
|
|
if child # Have we found a fitting child? |
|
do_allocation(parent, child, bytes) |
|
end |
|
end |
|
|
|
private def do_allocation(parent, child, bytes) : Pointer(Void) |
|
child.value.verify! |
|
child.value.available = false # This child is now in use |
|
|
|
# Assertions |
|
if child.value.size.to_u64.to_i64 - bytes.to_u64.to_i64 < 0 |
|
kernel_panic "Logic error in Allocator#do_allocation: Child ", child.value.size, " - ", bytes, " < 0!" |
|
end |
|
|
|
if parent.value.size_available.to_u64.to_i64 - bytes.to_u64.to_i64 < 0 |
|
kernel_panic "Logic error in Allocator#do_allocation: Parent ", parent.value.size_available, " < 0!" |
|
end |
|
|
|
# Split memory region if it makes sense |
|
if child.value.size - bytes > WASTE_THRESHOLD |
|
parent.value.size_available -= bytes |
|
split_child(parent, child, bytes) |
|
else |
|
parent.value.size_available -= child.value.size |
|
end |
|
|
|
# Is the page full? |
|
if parent == @@first_with_available && parent.value.size_available == 0 |
|
@@first_with_available = @@first_parent |
|
end |
|
|
|
# Return data pointer |
|
(child + 1).as(Void*) |
|
end |
|
|
|
private def split_child(parent, child, bytes) : Nil |
|
remaining = child.value.size.to_u64.to_i64 - bytes.to_u64.to_i64 - sizeof(Child) |
|
|
|
if remaining < 0 |
|
kernel_panic "Logic error in Allocator#split_child: ", remaining, " < 0!" |
|
end |
|
|
|
# Update previous child |
|
new_child = (child.as(UInt8*) + sizeof(Child) + bytes).as(Child*) |
|
child.value.size = bytes |
|
partner = child.value.next_child |
|
child.value.next_child = new_child |
|
|
|
# Update parent |
|
parent.value.size_available -= sizeof(Child) |
|
|
|
# Initialize new child |
|
new_child.value = Child.new( |
|
available: true, |
|
size: remaining.to_u32, |
|
parent: child.value.parent, |
|
next_child: partner, |
|
) |
|
end |
|
|
|
private def find_available_child(parent, min_size) |
|
previous = child = parent.value.child |
|
|
|
while child |
|
child.value.verify! |
|
|
|
if child.value.available? && child.value.size >= min_size |
|
return { child, previous } |
|
end |
|
|
|
previous, child = child, child.value.next_child |
|
end |
|
|
|
{ nil, previous } |
|
end |
|
|
|
private def allocate_and_map_pages(count : Int = 1) : Pointer(Void) |
|
@@total_page_count += count.to_u32 |
|
Virtual.kernel_instance.value.allocate_pages(count) |
|
end |
|
end |
|
end |
|
end |