From 49bf92a7aaf10a4777ea512303e442588f4ce2e5 Mon Sep 17 00:00:00 2001 From: Nathan Ringo Date: Sun, 15 Sep 2024 03:25:30 -0500 Subject: Start of serious allocator work. --- crates/Cargo.lock | 25 +-- crates/alloc_genmalloc/Cargo.toml | 3 + crates/alloc_genmalloc/src/lib.rs | 348 +++++++++++++++++++++++++++++++--- crates/alloc_vma_tree/Cargo.toml | 2 - crates/alloc_vma_tree/src/lib.rs | 11 +- crates/kernel/Cargo.toml | 2 + crates/kernel/src/alloc.rs | 159 ++++++++++++++-- crates/kernel/src/arch/riscv64/mod.rs | 15 +- crates/kernel/src/cpu_locals.rs | 72 +++++++ crates/kernel/src/lib.rs | 5 + crates/utils/src/lib.rs | 4 +- 11 files changed, 587 insertions(+), 59 deletions(-) create mode 100644 crates/kernel/src/cpu_locals.rs (limited to 'crates') diff --git a/crates/Cargo.lock b/crates/Cargo.lock index 41dfbb3..63385f0 100644 --- a/crates/Cargo.lock +++ b/crates/Cargo.lock @@ -94,12 +94,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" -[[package]] -name = "ghost-cell" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8449d342b1c67f49169e92e71deb7b9b27f30062301a16dbc27a4cc8d2351b7" - [[package]] name = "lazy_static" version = "1.5.0" @@ -314,10 +308,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] -name = "static-rc" -version = "0.6.1" +name = "sptr" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91d0104a7b28aeda24b30919f83222570111ac0bf1aab23aaffb8f59330e654" +checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" @@ -354,6 +354,9 @@ version = "0.1.0" dependencies = [ "allocator-api2", "contracts", + "log", + "sptr", + "static_assertions", ] [[package]] @@ -370,8 +373,6 @@ version = "0.1.0" dependencies = [ "allocator-api2", "contracts", - "ghost-cell", - "static-rc", "vernos_utils", ] @@ -401,7 +402,9 @@ dependencies = [ "either", "log", "spin", + "static_assertions", "vernos_alloc_buddy", + "vernos_alloc_genmalloc", "vernos_alloc_physmem_free_list", "vernos_alloc_vma_tree", "vernos_device_tree", diff --git a/crates/alloc_genmalloc/Cargo.toml b/crates/alloc_genmalloc/Cargo.toml index 1099020..945ba0f 100644 --- a/crates/alloc_genmalloc/Cargo.toml +++ b/crates/alloc_genmalloc/Cargo.toml @@ -7,3 +7,6 @@ publish = false [dependencies] allocator-api2 = { version = "0.2.18", default-features = false } contracts = { version = "0.6.3", default-features = false } +log = { version = "0.4.20", default-features = false } +sptr = { version = "0.3.2", default-features = false } +static_assertions = { version = "1.1.0", default-features = false } diff --git a/crates/alloc_genmalloc/src/lib.rs b/crates/alloc_genmalloc/src/lib.rs index 06bc667..bb024a8 100644 --- a/crates/alloc_genmalloc/src/lib.rs +++ b/crates/alloc_genmalloc/src/lib.rs @@ -1,64 +1,282 @@ -//! A general-purpose allocator, based on the design of mimalloc [0, 1]. +//! A general-purpose allocator, based on the design of mimalloc [0, 1]. The design is summarized +//! below. +//! +//! Objects are separated into four classes, by size: +//! +//! - Small objects are less than or equal to 1KiB in size. Small objects get access to a special +//! fast-path for allocation that avoids performing a `.next_power_of_two()` on their size. This +//! fast-path is designed to be inlined. They get stored in 64KiB pages, which are stored in 4MiB +//! segments. +//! - Medium objects are greater than 1KiB in size, but less than or equal to 8KiB. Medium objects +//! have another fast-path, but it isn't inlinable and performs a `.next_power_of_two()`. Like +//! small objects, they get stored in 64KiB pages, which are stored in 4MiB segments. +//! - Large objects are greater than 8KiB in size, but less than or equal to 512KiB. Their pages +//! span the entire 4MiB of a segment. +//! - Huge objects are greater than 512KiB in size. They get a custom segment size with a single +//! page, sized to fit the object. +//! +//! This design has the property that the first address of an object will be less than 4MiB from +//! the start of the segment. By aligning segments to 4MiB, this lets us cheaply get a pointer to +//! the segment from a pointer to the object. //! //! [0]: https://www.microsoft.com/en-us/research/publication/mimalloc-free-list-sharding-in-action/ //! [1]: https://github.com/microsoft/mimalloc #![no_std] -use allocator_api2::alloc::AllocError; +use allocator_api2::alloc::{AllocError, Allocator, Layout}; use contracts::requires; use core::{ - ptr::NonNull, + cell::RefCell, + marker::PhantomData, + mem::MaybeUninit, + ptr::{addr_of_mut, null_mut, slice_from_raw_parts_mut, NonNull}, sync::atomic::{AtomicPtr, AtomicU16, Ordering}, }; +use sptr::invalid_mut; +use static_assertions::const_assert_eq; -/// The per-CPU structure. -struct Heap { +/// The per-CPU structure. This uses a `RefCell` for interior mutability. +#[derive(Debug)] +pub struct Heap(RefCell>); + +#[derive(Debug)] +struct HeapInner { /// Small objects (<=1024 bytes) have pointers directly to the page they allocate into. These /// are non-owning pointers. - pages_direct: [NonNull; 128], + pages_direct: [NonNull; LARGEST_SMALL_OBJECT >> 3], /// The sentinel nodes for the circular doubly-linked lists that compose the size classes, up - /// to and including 512KiB, plus one more for objects over 512KiB. - pages: [PageLink; 18], + /// to and including 512KiB, plus one more for objects over 512KiB (huge objects). + pages: [PageLink; LARGEST_LARGE_OBJECT_SIZE_CLASS + 2], + + /// The sentinel node for segments that store small or medium objects that have free pages. + small_medium_segments_free: SegmentLink, + + /// The sentinel node for segments that store small or medium objects that have no free pages. + small_medium_segments_full: SegmentLink, + + /// The sentinel node for segments that store large or huge objects. + large_huge_segments: SegmentLink, + + _phantom: PhantomData, +} + +/// The log2 of the number of bytes in a segment (other than a segment created for a huge object). +pub const NON_HUGE_SEGMENT_SIZE_BITS: usize = 22; + +/// The number of bytes in a segment (other than a segment created for a huge object). +pub const NON_HUGE_SEGMENT_SIZE: usize = 4 << 20; + +const_assert_eq!(NON_HUGE_SEGMENT_SIZE, 1 << NON_HUGE_SEGMENT_SIZE_BITS); + +const SMALL_MEDIUM_PAGE_SIZE_BITS: u32 = 16; +const SMALL_MEDIUM_PAGE_SIZE: usize = 1 << SMALL_MEDIUM_PAGE_SIZE_BITS; +const SMALL_MEDIUM_PAGES_PER_SEGMENT: usize = + 1 << (NON_HUGE_SEGMENT_SIZE_BITS - SMALL_MEDIUM_PAGE_SIZE_BITS as usize); + +const LARGEST_SMALL_OBJECT: usize = 1024; +const LARGEST_MEDIUM_OBJECT: usize = 8 * 1024; +const LARGEST_LARGE_OBJECT: usize = 512 * 1024; +const LARGEST_SMALL_OBJECT_SIZE_CLASS: usize = 7; +const LARGEST_MEDIUM_OBJECT_SIZE_CLASS: usize = 10; +const LARGEST_LARGE_OBJECT_SIZE_CLASS: usize = 16; + +const_assert_eq!( + size_class(LARGEST_SMALL_OBJECT), + LARGEST_SMALL_OBJECT_SIZE_CLASS +); +const_assert_eq!( + size_class(LARGEST_MEDIUM_OBJECT), + LARGEST_MEDIUM_OBJECT_SIZE_CLASS +); +const_assert_eq!( + size_class(LARGEST_LARGE_OBJECT), + LARGEST_LARGE_OBJECT_SIZE_CLASS +); + +impl Heap { + /// Initializes a `Heap`. After this, the `MaybeUninit` is initialized. + pub fn init(maybe_uninit: &mut MaybeUninit>) { + // Initialize the `RefCell` part. This... might not be sound. Hm. It also might blow the + // stack in debug builds, defeating the point of this whole function... + let refcell = unsafe { + maybe_uninit + .as_mut_ptr() + .cast::>>>>() + .as_mut() + .unwrap_unchecked() + .write(RefCell::new(MaybeUninit::uninit())) + }; + + // Make accessors for the fields of the HeapInner. + let inner_ptr = refcell.get_mut().as_mut_ptr(); + let pages_direct_ptr = |i: usize| { + // SAFETY: For the `maybe_uninit` to have been valid, this must be sound. + unsafe { NonNull::new_unchecked(addr_of_mut!((*inner_ptr).pages_direct[i])) } + }; + let pages_ptr = |i: usize| { + // SAFETY: For the `maybe_uninit` to have been valid, this must be sound. + unsafe { NonNull::new_unchecked(addr_of_mut!((*inner_ptr).pages[i])) } + }; + + // Make a helper to self-link the segment lists. + // + // SAFETY: See the call sites. + let self_link_segments = |ptr: NonNull| unsafe { + (*ptr.as_ptr()).prev = ptr; + (*ptr.as_ptr()).next = ptr; + }; + + // Initialize each field. + for i in 0..128 { + // SAFETY: The returned pointer is valid. + unsafe { pages_direct_ptr(i).write(NonNull::from(&EMPTY_PAGE)) } + } + for i in 0..18 { + let ptr = pages_ptr(i); + // SAFETY: The returned pointer is valid. + unsafe { + ptr.write(PageLink { + prev: ptr, + next: ptr, + }) + } + } + // SAFETY: For the `maybe_uninit` to have been valid, getting these pointers must be sound. + // Self-linking the pointers is also sound as a result. + unsafe { + self_link_segments(NonNull::new_unchecked(addr_of_mut!( + (*inner_ptr).small_medium_segments_free + ))); + self_link_segments(NonNull::new_unchecked(addr_of_mut!( + (*inner_ptr).small_medium_segments_full + ))); + self_link_segments(NonNull::new_unchecked(addr_of_mut!( + (*inner_ptr).large_huge_segments + ))); + } + } + + /// Donates a segment to be used for small or medium objects. + /// + /// # Safety + /// + /// - `ptr` must convey ownership over the entire region. + /// - `thread_id` must be correct. + pub unsafe fn donate_small_medium_segment(&self, ptr: NonNull<[u8; NON_HUGE_SEGMENT_SIZE]>) { + self.0.borrow_mut().add_small_medium_segment(ptr) + } } -impl Heap { +impl HeapInner { + /// Initializes a segment for small or medium objects, adding it to this heap. + unsafe fn add_small_medium_segment(&mut self, ptr: NonNull<[u8; NON_HUGE_SEGMENT_SIZE]>) { + // Initialize the segment to be self-linked. + let maybe_uninit: &mut MaybeUninit = ptr.cast().as_mut(); + maybe_uninit.write(Segment { + link: SegmentLink { + prev: ptr.cast(), + next: ptr.cast(), + }, + header: SegmentHeader { + thread_id: OS::current_thread_id(), + page_shift: SMALL_MEDIUM_PAGE_SIZE_BITS, + }, + }); + + // Add the segment to the list of segments with free pages. + SegmentLink::add_next(NonNull::from(&self.small_medium_segments_free), ptr.cast()); + } + + /// Allocates small blocks of naturally-aligned memory. #[requires(size > 0)] - #[requires(size <= 1024)] - fn alloc_small(&mut self, size: usize) -> Result, AllocError> { - let mut page = self.pages_direct[(size + 7) >> 3]; + #[requires(size <= LARGEST_SMALL_OBJECT)] + #[inline] + fn alloc_small(&mut self, size: usize) -> Result, AllocError> { + let size = (size + 7) & !0b111; + let small_size_class = (size >> 3) - 1; + let mut page = self.pages_direct[small_size_class]; let page = unsafe { page.as_mut() }; match page.header.alloc_list.as_mut() { Some(&mut block) => { let next = unsafe { block.as_ref() }.next.load(Ordering::SeqCst); page.header.alloc_list = NonNull::new(next); page.header.used.fetch_add(1, Ordering::SeqCst); - Ok(block.cast()) + // SAFETY: `block` is a `NonNull`, so the slice pointer will non-null. + Ok(unsafe { + NonNull::new_unchecked(slice_from_raw_parts_mut(block.as_ptr().cast(), size)) + }) } - None => self.alloc_generic(size), + None => self.alloc_generic(size, size), } } - fn alloc_generic(&mut self, size: usize) -> Result, AllocError> { + /// The slow-path of allocation. Allocates a block of any size and alignment, and does some + /// book-keeping tasks along the way. + fn alloc_generic(&mut self, size: usize, align: usize) -> Result, AllocError> { + /* let size_class = size_class(size); let sentinel: NonNull = NonNull::from(&self.pages[size_class]); let mut page = sentinel; loop { - page = unsafe { (*page.as_ptr()).next.unwrap() }; + page = unsafe { (*page.as_ptr()).next }; todo!("{page:#?}") } - todo!() + */ + todo!("{:p}.alloc_generic({size}, {align})", self) } } -/// Returns the index in the `pages` array of objects with the given size. -fn size_class(size: usize) -> usize { - // Clamp the size to the range the bitwise trick will work on. - let size = size.clamp(8, 1024 * 1024); +unsafe impl Allocator for Heap { + #[inline] + fn allocate(&self, layout: Layout) -> Result, AllocError> { + // Special-case ZSTs. + if layout.size() == 0 { + return Ok(unsafe { + NonNull::new_unchecked(slice_from_raw_parts_mut(invalid_mut(layout.align()), 0)) + }); + } + + // An objects with an alignment this big would break our invariants (namely, that we can + // get to the address of the segment by aligning down the address of the object to a 4MiB + // boundary). + if layout.align() >= NON_HUGE_SEGMENT_SIZE { + return Err(AllocError); + } + + let mut inner = self.0.borrow_mut(); + let size_if_small = layout.size().max(layout.align()); + if size_if_small <= LARGEST_SMALL_OBJECT { + // Fast path. + inner.alloc_small(size_if_small) + } else { + // Slow path. + inner.alloc_generic(layout.size(), layout.align()) + } + } - // Figure out the size class. - (size.next_power_of_two().trailing_zeros() - 3) as usize + #[inline] + unsafe fn deallocate(&self, ptr: NonNull, layout: Layout) { + // Special-case ZSTs. + if layout.size() == 0 { + return; + } + + let self_ptr = self as *const Heap; + todo!("{self_ptr:p}.deallocate({ptr:p}, {layout:?})") + } +} + +/// Returns the index in the `pages` array of objects with the given size. +const fn size_class(size: usize) -> usize { + if size < 8 { + 0 + } else if size > LARGEST_LARGE_OBJECT { + 17 + } else { + (size.next_power_of_two().trailing_zeros() - 3) as usize + } } #[test] @@ -79,6 +297,53 @@ fn expected_size_class_values() { assert_eq!(size_class(4 * 1024 * 1024), 17); } +/// A segment is a container for pages, to amortize some of the overhead of allocating new pages. +#[repr(C)] +struct Segment { + /// The intrusive link in the circular doubly-linked list of segments . + link: SegmentLink, + + /// The fields other than the intrusive link in a segment's header. + header: SegmentHeader, +} + +/// The intrusive link in the circular doubly-linked list of pages of each size class. +#[derive(Debug)] +struct SegmentLink { + /// The previous page. + prev: NonNull, + + /// The next page. + next: NonNull, +} + +impl SegmentLink { + #[requires(SegmentLink::self_linked(item))] + pub unsafe fn add_next(list: NonNull, item: NonNull) { + let next = (*list.as_ptr()).next; + (*item.as_ptr()).next = next; + (*item.as_ptr()).prev = list; + + (*list.as_ptr()).next = item; + (*next.as_ptr()).prev = item; + } + + pub unsafe fn self_linked(ptr: NonNull) -> bool { + let SegmentLink { prev, next } = *ptr.as_ptr(); + ptr == prev && ptr == next + } +} + +/// The fields other than the intrusive link in a segment's header. +struct SegmentHeader { + /// The ID of the thread. + thread_id: usize, + + /// The amount to right-shift the offset of an object from the start of the segment by to get + /// its page index. + page_shift: u32, +} + /// A page is a block of memory that contains objects of a single size class. #[repr(C)] struct Page { @@ -89,13 +354,35 @@ struct Page { header: PageHeader, } +unsafe impl Send for Page {} +unsafe impl Sync for Page {} + +/// A fake page that causes allocation to go into the "slow path," so that we can allocate an +/// initial page of that size class. +static EMPTY_PAGE: Page = unsafe { + Page { + link: PageLink { + prev: NonNull::new_unchecked(&EMPTY_PAGE.link as *const PageLink as *mut PageLink), + next: NonNull::new_unchecked(&EMPTY_PAGE.link as *const PageLink as *mut PageLink), + }, + header: PageHeader { + alloc_list: None, + local_free_list: None, + thread_free_list: AtomicPtr::new(null_mut()), + used: AtomicU16::new(0), + thread_freed: AtomicU16::new(0), + }, + } +}; + /// The intrusive link in the circular doubly-linked list of pages of each size class. +#[derive(Debug)] struct PageLink { /// The previous page. - prev: Option>, + prev: NonNull, /// The next page. - next: Option>, + next: NonNull, } /// The fields other than the intrusive link in a page's header. @@ -133,3 +420,14 @@ fn atomic_push(node: &mut Block, list: &AtomicPtr) { } } } + +/// The OS services the allocator needs. +/// +/// # Safety +/// +/// All methods must be implemented according to their doc comments. +pub unsafe trait OSServices { + /// Returns the ID of the current thread. This can be arbitrary, so long as it is consistent + /// within a thread and no two threads with the same thread ID run at the same time. + fn current_thread_id() -> usize; +} diff --git a/crates/alloc_vma_tree/Cargo.toml b/crates/alloc_vma_tree/Cargo.toml index 20b1189..a927769 100644 --- a/crates/alloc_vma_tree/Cargo.toml +++ b/crates/alloc_vma_tree/Cargo.toml @@ -7,6 +7,4 @@ publish = false [dependencies] allocator-api2 = { version = "0.2.18", default-features = false } contracts = { version = "0.6.3", default-features = false } -ghost-cell = { version = "0.2.6", default-features = false } -static-rc = { version = "0.6.1", default-features = false } vernos_utils = { path = "../utils" } diff --git a/crates/alloc_vma_tree/src/lib.rs b/crates/alloc_vma_tree/src/lib.rs index 38dd312..e188ff7 100644 --- a/crates/alloc_vma_tree/src/lib.rs +++ b/crates/alloc_vma_tree/src/lib.rs @@ -21,6 +21,11 @@ pub struct VMATree { } impl VMATree { + /// The layout of a node in the tree. + /// + /// This is used in bootstrapping a mutually dependent allocator and `VMATree`. + pub const NODE_LAYOUT: Layout = Layout::new::>(); + /// Creates a new VMATree that will use the given allocator. pub const fn new_in(allocator: ALLOCATOR) -> VMATree { VMATree { @@ -167,19 +172,19 @@ impl VMANode { let mut ptr: NonNull> = allocator.allocate(layout)?.cast(); // SAFETY: This needs to be OK for the allocator to meet the conditions of its trait. - VMANode::init_in(unsafe { ptr.as_mut() }, addrs); + VMANode::init(unsafe { ptr.as_mut() }, addrs); Ok(ptr.cast()) } - /// Initializes a node. + /// Initializes a node. After this, the `MaybeUninit` is initialized. /// /// The node has the given range of addresses and is in the `Free` state. All of its pointers /// are self-pointers. #[requires(PAGE_SIZE_BITS > 0)] #[requires(addrs.start & ((1 << PAGE_SIZE_BITS) - 1) == 0)] #[requires(addrs.end & ((1 << PAGE_SIZE_BITS) - 1) == 0)] - pub fn init_in(maybe_uninit: &mut MaybeUninit>, addrs: Range) { + pub fn init(maybe_uninit: &mut MaybeUninit>, addrs: Range) { let ptr = NonNull::from(&*maybe_uninit).cast(); maybe_uninit.write(VMANode { addr_and_state: addrs.start, diff --git a/crates/kernel/Cargo.toml b/crates/kernel/Cargo.toml index 5fef50b..a447470 100644 --- a/crates/kernel/Cargo.toml +++ b/crates/kernel/Cargo.toml @@ -14,7 +14,9 @@ contracts = { version = "0.6.3", default-features = false } either = { version = "1.13.0", default-features = false } log = { version = "0.4.20", default-features = false } spin = { version = "0.9.8", default-features = false, features = ["fair_mutex", "use_ticket_mutex"] } +static_assertions = { version = "1.1.0", default-features = false } vernos_alloc_buddy = { path = "../alloc_buddy" } +vernos_alloc_genmalloc = { path = "../alloc_genmalloc" } vernos_alloc_physmem_free_list = { path = "../alloc_physmem_free_list" } vernos_alloc_vma_tree = { path = "../alloc_vma_tree" } vernos_device_tree = { path = "../device_tree" } diff --git a/crates/kernel/src/alloc.rs b/crates/kernel/src/alloc.rs index 203ef5c..920fe0a 100644 --- a/crates/kernel/src/alloc.rs +++ b/crates/kernel/src/alloc.rs @@ -1,13 +1,21 @@ //! Global structures for the allocators. -use crate::paging::{ - BuddyAllocator, MapError, MappingFlags, PageTable, ASID, HIMEM_BOT, LOMEM_TOP, - MAX_PAGE_SIZE_BITS, PAGE_SIZE, PAGE_SIZES, PAGE_SIZE_BITS, +use crate::{ + cpu_locals::CPULocals, + paging::{ + BuddyAllocator, MapError, MappingFlags, PageTable, ASID, HIMEM_BOT, LOMEM_TOP, + MAX_PAGE_SIZE_BITS, PAGE_SIZE, PAGE_SIZES, PAGE_SIZE_BITS, + }, }; -use allocator_api2::alloc::{AllocError, Global, GlobalAlloc, Layout}; +use allocator_api2::alloc::{AllocError, Allocator, GlobalAlloc, Layout}; use contracts::requires; -use core::{num::NonZero, ptr::NonNull}; +use core::{ + mem::MaybeUninit, + num::NonZero, + ptr::{null_mut, NonNull}, +}; use spin::mutex::FairMutex; +use vernos_alloc_genmalloc::{OSServices, NON_HUGE_SEGMENT_SIZE, NON_HUGE_SEGMENT_SIZE_BITS}; use vernos_alloc_vma_tree::VMATree; /// The global instance of the physical page allocator. @@ -17,12 +25,15 @@ static BUDDY_ALLOCATOR: FairMutex> = FairMutex::new(None) static KERNEL_PAGE_TABLE: FairMutex> = FairMutex::new(None); /// The kernel's virtual memory allocator. -static KERNEL_VM_ALLOC: FairMutex> = - FairMutex::new(VMATree::new_in(Global)); +static KERNEL_VM_ALLOC: FairMutex> = + FairMutex::new(VMATree::new_in(CPULocalHeap)); /// The global allocator. #[global_allocator] -static GLOBAL_ALLOC: GlobalGenMalloc = GlobalGenMalloc; +static GLOBAL_ALLOC: CPULocalHeap = CPULocalHeap; + +/// The type of the kernel's allocator. +pub type Heap = vernos_alloc_genmalloc::Heap; /// Initializes the kernel page table and enables paging. /// @@ -82,13 +93,73 @@ pub unsafe fn init_kernel_page_table(buddy_allocator: BuddyAllocator) { #[requires(himem_top & (PAGE_SIZE - 1) == 0)] #[requires(HIMEM_BOT < himem_top)] pub unsafe fn init_kernel_virtual_memory_allocator(himem_top: usize) { - // TODO: Bootstrap the allocator. + let mut himem_bot = HIMEM_BOT; + let mut himem_top = himem_top; + + // To bootstrap the allocator, we make an initial heap. First, we figure out where it should be + // laid out in himem, including putting a guard page beneath it. + let heap_top = himem_top; + himem_top -= size_of::(); + const _: () = assert!(align_of::() < PAGE_SIZE); + himem_top &= !(PAGE_SIZE - 1); + let heap_bot = himem_top; + let heap = (himem_top as *mut MaybeUninit).as_mut().unwrap(); + himem_top -= PAGE_SIZE; + assert!(himem_bot < himem_top); + + // Map memory to back the heap. + for i in (heap_bot >> PAGE_SIZE_BITS)..(heap_top >> PAGE_SIZE_BITS) { + let vaddr = i << PAGE_SIZE_BITS; + let paddr = + alloc_page(PAGE_SIZE).expect("failed to allocate memory to bootstrap hart0's heap"); + kernel_map( + vaddr, + paddr.into(), + PAGE_SIZE, + MappingFlags::R | MappingFlags::W, + ) + .expect("failed to map memory to bootstrap hart0's heap"); + } + + // Next, we initialize the heap, which lets us initialize the CPU-locals as well. + Heap::init(heap); + CPULocals::init(0, heap.assume_init_mut()); + + // We need to initialize the heap with a segment that will let the virtual memory allocator + // allocate nodes. We lay it out at the _bottom_ of himem, since we know that'll be aligned. We + // add a guard page as well. + assert_eq!(himem_bot % NON_HUGE_SEGMENT_SIZE, 0); + let bootstrap_segment = himem_bot; + himem_bot += NON_HUGE_SEGMENT_SIZE; + assert_eq!(himem_bot & (PAGE_SIZE - 1), 0); + himem_bot += PAGE_SIZE; + + // We map the bootstrap segment. + for i in 0..(1 << (NON_HUGE_SEGMENT_SIZE_BITS - PAGE_SIZE_BITS)) { + let vaddr = bootstrap_segment + (i << PAGE_SIZE_BITS); + let paddr = alloc_page(PAGE_SIZE) + .expect("failed to allocate memory for hart0's heap's initial segment"); + kernel_map( + vaddr, + paddr.into(), + PAGE_SIZE, + MappingFlags::R | MappingFlags::W, + ) + .expect("failed to map memory for hart0's heap's initial segment"); + } + + // Donate the bootstrap segment to the heap. + // + // UNWRAP: Himem cannot be null. + CPULocals::get().heap().donate_small_medium_segment( + NonNull::new(bootstrap_segment as *mut [u8; NON_HUGE_SEGMENT_SIZE]).unwrap(), + ); // The error here _really_ ought to be impossible, because we just bootstrapped the allocator! // It definitely has free memory. let mut kernel_vm_alloc = KERNEL_VM_ALLOC.lock(); kernel_vm_alloc - .add(HIMEM_BOT..himem_top) + .add(himem_bot..himem_top) .expect("failed to set up the kernel's virtual memory allocator"); } @@ -136,20 +207,78 @@ pub fn kernel_map( kernel_page_table.map(&mut *buddy_allocator, vaddr, paddr, len, flags)?; vernos_utils::first_time! { log::warn!("TODO: sfence.vma"); - log::warn!("TODO: TLB shootdown"); } Ok(()) } /// A global allocator backed by a hart-local `vernos_alloc_genmalloc::Heap`. -struct GlobalGenMalloc; +struct CPULocalHeap; + +unsafe impl Allocator for CPULocalHeap { + fn allocate(&self, layout: Layout) -> Result, AllocError> { + CPULocals::get().heap().allocate(layout) + } + + unsafe fn deallocate(&self, ptr: NonNull, layout: Layout) { + CPULocals::get().heap().deallocate(ptr, layout) + } -unsafe impl GlobalAlloc for GlobalGenMalloc { + fn allocate_zeroed(&self, layout: Layout) -> Result, AllocError> { + CPULocals::get().heap().allocate_zeroed(layout) + } + + unsafe fn grow( + &self, + ptr: NonNull, + old_layout: Layout, + new_layout: Layout, + ) -> Result, AllocError> { + CPULocals::get().heap().grow(ptr, old_layout, new_layout) + } + + unsafe fn grow_zeroed( + &self, + ptr: NonNull, + old_layout: Layout, + new_layout: Layout, + ) -> Result, AllocError> { + CPULocals::get() + .heap() + .grow_zeroed(ptr, old_layout, new_layout) + } + + unsafe fn shrink( + &self, + ptr: NonNull, + old_layout: Layout, + new_layout: Layout, + ) -> Result, AllocError> { + CPULocals::get().heap().shrink(ptr, old_layout, new_layout) + } +} + +unsafe impl GlobalAlloc for CPULocalHeap { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { - todo!("GlobalGenMalloc.alloc({layout:?})") + match self.allocate(layout) { + Ok(ptr) => ptr.as_ptr().cast(), + Err(AllocError) => null_mut(), + } } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { - todo!("GlobalGenMalloc.dealloc({ptr:?}, {layout:?})") + match NonNull::new(ptr) { + Some(ptr) => self.deallocate(ptr, layout), + None => unreachable!("dealloc({ptr:p}, {layout:?})"), + } + } +} + +/// The OS services provided to the allocator. +#[derive(Debug)] +pub struct HeapOSServices; + +unsafe impl OSServices for HeapOSServices { + fn current_thread_id() -> usize { + CPULocals::get().cpu_number } } diff --git a/crates/kernel/src/arch/riscv64/mod.rs b/crates/kernel/src/arch/riscv64/mod.rs index 011e244..48718c2 100644 --- a/crates/kernel/src/arch/riscv64/mod.rs +++ b/crates/kernel/src/arch/riscv64/mod.rs @@ -1,9 +1,22 @@ +use crate::cpu_locals::CPULocals; +use core::{arch::asm, ptr::NonNull}; + pub mod interrupts; pub mod paging; +/// Returns a pointer to the per-CPU locals. +pub fn get_cpu_locals() -> NonNull { + // SAFETY: The entrypoint sets this up, and safe code cannot invalidate it. + unsafe { + let tp; + asm!("mv {out}, tp", out = out(reg) tp); + NonNull::new_unchecked(tp) + } +} + /// Halts the hart. pub fn sleep_forever() -> ! { loop { - unsafe { core::arch::asm!("wfi") } + unsafe { asm!("wfi") } } } diff --git a/crates/kernel/src/cpu_locals.rs b/crates/kernel/src/cpu_locals.rs new file mode 100644 index 0000000..fd826aa --- /dev/null +++ b/crates/kernel/src/cpu_locals.rs @@ -0,0 +1,72 @@ +use crate::{alloc::Heap, arch}; +use core::{cell::RefCell, marker::PhantomData, ops::DerefMut, ptr::addr_of_mut}; +use static_assertions::{assert_eq_size, assert_not_impl_any}; + +/// The data that is stored in a per-CPU structure. +#[derive(Debug)] +pub struct CPULocals { + /// The index of this CPU. + pub cpu_number: usize, + + /// The heap used by this CPU's allocator. + pub heap: RefCell<&'static mut Heap>, + + /// A canary for the `CPULocals` being initialized. + canary: usize, + + // This ensures that the type is not `Send`. + _phantom: PhantomData<*mut ()>, +} + +impl CPULocals { + /// Creates a new instance of the `CPULocals`, using it to initialize this CPU's `CPULocals`. + /// + /// # Safety + /// + /// - The CPULocals must not have already been initialized. + pub unsafe fn init(cpu_number: usize, heap: &'static mut Heap) { + arch::get_cpu_locals().write(CPULocals { + cpu_number, + heap: RefCell::new(heap), + canary: CANARY, + _phantom: PhantomData, + }); + } + + /// Returns the instance of the `CPULocals` for this CPU. + pub fn get() -> &'static CPULocals { + let ptr = arch::get_cpu_locals(); + // SAFETY: The entrypoint sets this up for hart0, and we allocate this for other harts. + unsafe { + let canary_ptr = addr_of_mut!((*ptr.as_ptr()).canary); + assert_eq!( + *canary_ptr, CANARY, + "CPULocals were not initialized (and we probably just did UB)" + ); + ptr.as_ref() + } + } + + /// Retrieves a reference to the heap. + /// + /// # Panics + /// + /// - Panics if the CPU-local heap was already borrowed. + /// - The returned guard will panic on deref if the heap was not initialized. + pub fn heap(&'static self) -> impl DerefMut { + self.heap.borrow_mut() + } +} + +assert_eq_size!(CPULocals, [usize; 4]); +assert_not_impl_any!(CPULocals: Send); + +cfg_if::cfg_if! { + if #[cfg(target_pointer_width = "32")] { + const CANARY: usize = usize::from_le_bytes(*b"locl"); + } else if #[cfg(target_pointer_width = "64")] { + const CANARY: usize = usize::from_le_bytes(*b"CPULocal"); + } else { + compile_error!("unsupported platform"); + } +} diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index 7421649..fc96950 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -21,6 +21,7 @@ mod panic; pub mod alloc; pub mod arch; pub mod constants; +pub mod cpu_locals; pub mod logger; pub mod paging; @@ -98,6 +99,10 @@ pub unsafe extern "C" fn hart0_early_boot(early_boot_addrs: &mut EarlyBootAddrs) assert!(early_boot_addrs.initial_stack_start.is_aligned()); assert!(early_boot_addrs.stack_end.is_aligned()); assert!(early_boot_addrs.trampoline_start.is_aligned()); + assert_eq!( + arch::get_cpu_locals().as_ptr().wrapping_add(1) as *const (), + early_boot_addrs.stack_end.cast() + ); // Parse the DeviceTree. let flattened_device_tree = unsafe { early_boot_addrs.flattened_device_tree() }; diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 1e8ddfb..2181046 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -134,10 +134,10 @@ impl_FromEndianBytes!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize); /// Runs the body block the first time it is encountered. #[macro_export] macro_rules! first_time { - ($($stmt:stmt)*) => {{ + ($($stmt:stmt;)*) => {{ use spin::lazy::Lazy; static LAZY: Lazy<()> = Lazy::new(|| { - $($stmt)* + $($stmt;)* }); *Lazy::force(&LAZY) }}; -- cgit v1.2.3