use crate::{
AllocatorStats, PageFrame, PageOrder, PhysAddr, PhysMemError,
MAX_ORDER, PAGE_SIZE, order_to_pages, pages_to_order,
};
const MAX_FREE_BLOCKS: usize = 1024;
pub struct BuddyAllocator {
base_addr: PhysAddr,
total_pages: usize,
free_lists: [FreeList; MAX_ORDER],
stats: AllocatorStats,
initialized: bool,
}
#[derive(Clone)]
struct FreeList {
blocks: [PhysAddr; MAX_FREE_BLOCKS],
count: usize,
}
impl Default for FreeList {
fn default() -> Self {
Self::new()
}
}
impl FreeList {
const fn new() -> Self {
Self {
blocks: [PhysAddr::NULL; MAX_FREE_BLOCKS],
count: 0,
}
}
#[inline]
const fn len(&self) -> usize {
self.count
}
#[inline]
#[allow(dead_code)]
const fn is_empty(&self) -> bool {
self.count == 0
}
#[inline]
fn push(&mut self, addr: PhysAddr) -> Result<(), PhysMemError> {
if self.count >= MAX_FREE_BLOCKS {
return Err(PhysMemError::InternalCorruption);
}
self.blocks[self.count] = addr;
self.count += 1;
Ok(())
}
#[inline]
fn pop(&mut self) -> Option<PhysAddr> {
if self.count == 0 {
None
} else {
self.count -= 1;
Some(self.blocks[self.count])
}
}
fn remove(&mut self, addr: PhysAddr) -> bool {
for i in 0..self.count {
if self.blocks[i] == addr {
self.count -= 1;
if i < self.count {
self.blocks[i] = self.blocks[self.count];
}
return true;
}
}
false
}
#[allow(dead_code)]
fn contains(&self, addr: PhysAddr) -> bool {
for i in 0..self.count {
if self.blocks[i] == addr {
return true;
}
}
false
}
}
impl BuddyAllocator {
#[must_use]
pub fn new(base_addr: PhysAddr, total_pages: usize) -> Self {
assert!(
base_addr.is_page_aligned(),
"Base address must be page-aligned"
);
let mut allocator = Self {
base_addr,
total_pages,
free_lists: core::array::from_fn(|_| FreeList::new()),
stats: AllocatorStats::new(total_pages, 0),
initialized: false,
};
allocator.init_free_lists();
allocator.initialized = true;
allocator
}
#[must_use]
pub const fn uninit() -> Self {
Self {
base_addr: PhysAddr::NULL,
total_pages: 0,
free_lists: [
FreeList::new(), FreeList::new(), FreeList::new(), FreeList::new(),
FreeList::new(), FreeList::new(), FreeList::new(), FreeList::new(),
FreeList::new(), FreeList::new(),
],
stats: AllocatorStats::new(0, 0),
initialized: false,
}
}
pub fn init(&mut self, base_addr: PhysAddr, total_pages: usize) -> Result<(), PhysMemError> {
if !base_addr.is_page_aligned() {
return Err(PhysMemError::UnalignedAddress);
}
self.base_addr = base_addr;
self.total_pages = total_pages;
self.stats = AllocatorStats::new(total_pages, 0);
for list in &mut self.free_lists {
*list = FreeList::new();
}
self.init_free_lists();
self.initialized = true;
Ok(())
}
fn init_free_lists(&mut self) {
let mut remaining_pages = self.total_pages;
let mut current_addr = self.base_addr;
while remaining_pages > 0 {
let order = self.largest_fitting_order(remaining_pages, current_addr);
let pages = order_to_pages(order);
let _ = self.free_lists[order].push(current_addr);
current_addr = current_addr.add_pages(pages);
remaining_pages -= pages;
}
self.stats = AllocatorStats::new(self.total_pages, self.total_pages);
}
fn largest_fitting_order(&self, remaining_pages: usize, addr: PhysAddr) -> usize {
for order in (0..MAX_ORDER).rev() {
let pages = order_to_pages(order);
if pages <= remaining_pages && addr.is_order_aligned(order) {
return order;
}
}
0
}
#[inline]
#[must_use]
pub const fn base_addr(&self) -> PhysAddr {
self.base_addr
}
#[inline]
#[must_use]
pub const fn total_pages(&self) -> usize {
self.total_pages
}
#[inline]
#[must_use]
pub fn free_page_count(&self) -> usize {
let mut free = 0;
for order in 0..MAX_ORDER {
free += self.free_lists[order].len() * order_to_pages(order);
}
free
}
#[inline]
#[must_use]
pub fn used_pages(&self) -> usize {
self.total_pages.saturating_sub(self.free_page_count())
}
#[inline]
#[must_use]
pub const fn stats(&self) -> &AllocatorStats {
&self.stats
}
#[inline]
#[must_use]
pub fn end_addr(&self) -> PhysAddr {
self.base_addr.add_pages(self.total_pages)
}
#[inline]
#[must_use]
pub fn free_blocks_at_order(&self, order: usize) -> usize {
if order < MAX_ORDER {
self.free_lists[order].len()
} else {
0
}
}
#[must_use]
pub fn alloc_pages(&mut self, count: usize) -> Option<PhysAddr> {
if !self.initialized {
return None;
}
if count == 0 {
return None;
}
let required_order = pages_to_order(count);
if required_order >= MAX_ORDER {
self.stats.record_allocation(count, false);
return None;
}
let result = self.alloc_order(required_order);
self.stats.record_allocation(count, result.is_some());
result
}
fn alloc_order(&mut self, order: usize) -> Option<PhysAddr> {
if let Some(addr) = self.free_lists[order].pop() {
return Some(addr);
}
for higher_order in (order + 1)..MAX_ORDER {
if let Some(addr) = self.free_lists[higher_order].pop() {
self.split_to_order(addr, higher_order, order);
return self.free_lists[order].pop();
}
}
None
}
fn split_to_order(&mut self, addr: PhysAddr, from_order: usize, to_order: usize) {
let base_addr = addr;
let mut current_order = from_order;
while current_order > to_order {
let new_order = current_order - 1;
let buddy_addr = base_addr.add_pages(order_to_pages(new_order));
let _ = self.free_lists[new_order].push(buddy_addr);
self.stats.record_split();
current_order = new_order;
}
let _ = self.free_lists[to_order].push(base_addr);
}
pub fn dealloc_pages(&mut self, addr: PhysAddr, count: usize) {
if !self.initialized || count == 0 {
return;
}
debug_assert!(
addr.is_page_aligned(),
"Address must be page-aligned: {addr:?}"
);
debug_assert!(
addr.is_in_range(self.base_addr, self.end_addr()),
"Address out of range: {addr:?}"
);
let order = pages_to_order(count);
if order >= MAX_ORDER {
return;
}
self.free_order(addr, order);
self.stats.record_free(count);
}
fn free_order(&mut self, addr: PhysAddr, order: usize) {
let mut current_addr = addr;
let mut current_order = order;
while current_order < MAX_ORDER - 1 {
let buddy_addr = self.buddy_addr(current_addr, current_order);
if !buddy_addr.is_in_range(self.base_addr, self.end_addr()) {
break;
}
if !self.free_lists[current_order].remove(buddy_addr) {
break;
}
current_addr = if current_addr.as_u64() < buddy_addr.as_u64() {
current_addr
} else {
buddy_addr
};
self.stats.record_coalesce();
current_order += 1;
}
let _ = self.free_lists[current_order].push(current_addr);
}
fn buddy_addr(&self, addr: PhysAddr, order: usize) -> PhysAddr {
let block_size = (order_to_pages(order) * PAGE_SIZE) as u64;
PhysAddr::new(addr.as_u64() ^ block_size)
}
#[must_use]
pub fn alloc_frame(&mut self, count: usize) -> Option<PageFrame> {
let order = PageOrder::from_pages(count)?;
let addr = self.alloc_pages(count)?;
Some(PageFrame::new(addr, order))
}
pub fn free_frame(&mut self, frame: &PageFrame) {
self.dealloc_pages(frame.addr(), frame.pages());
}
pub fn try_alloc_pages(&mut self, count: usize) -> Result<PhysAddr, PhysMemError> {
if !self.initialized {
return Err(PhysMemError::NotInitialized);
}
if count == 0 {
return Err(PhysMemError::ZeroAllocation);
}
let required_order = pages_to_order(count);
if required_order >= MAX_ORDER {
return Err(PhysMemError::AllocationTooLarge);
}
self.alloc_pages(count).ok_or(PhysMemError::OutOfMemory)
}
pub fn try_free_pages(&mut self, addr: PhysAddr, count: usize) -> Result<(), PhysMemError> {
if !self.initialized {
return Err(PhysMemError::NotInitialized);
}
if count == 0 {
return Err(PhysMemError::ZeroAllocation);
}
if !addr.is_page_aligned() {
return Err(PhysMemError::UnalignedAddress);
}
if !addr.is_in_range(self.base_addr, self.end_addr()) {
return Err(PhysMemError::AddressOutOfRange);
}
let order = pages_to_order(count);
if order >= MAX_ORDER {
return Err(PhysMemError::AllocationTooLarge);
}
self.dealloc_pages(addr, count);
Ok(())
}
#[inline]
#[must_use]
pub fn contains(&self, addr: PhysAddr) -> bool {
addr.is_in_range(self.base_addr, self.end_addr())
}
pub fn reset(&mut self) {
if !self.initialized {
return;
}
for list in &mut self.free_lists {
*list = FreeList::new();
}
self.init_free_lists();
self.stats.reset_counters();
}
#[must_use]
pub fn order_stats(&self) -> [crate::stats::OrderStats; MAX_ORDER] {
let mut stats = [crate::stats::OrderStats::new(); MAX_ORDER];
for (i, stat) in stats.iter_mut().enumerate() {
stat.free_blocks = self.free_lists[i].len();
}
stats
}
#[cfg(feature = "std")]
pub fn dump_free_lists(&self) {
std::println!("Buddy Allocator State:");
std::println!(" Base: {}", self.base_addr);
std::println!(" Total: {} pages ({} bytes)", self.total_pages, self.total_pages * PAGE_SIZE);
std::println!(" Free: {} pages", self.free_page_count());
std::println!(" Used: {} pages", self.used_pages());
std::println!();
std::println!("Free Lists:");
for order in 0..MAX_ORDER {
let count = self.free_lists[order].len();
if count > 0 {
let pages = order_to_pages(order);
let bytes = pages * PAGE_SIZE;
std::println!(" Order {}: {} blocks ({} pages each, {} bytes)",
order, count, pages, bytes);
}
}
}
}
impl Default for BuddyAllocator {
fn default() -> Self {
Self::uninit()
}
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_BASE: u64 = 0x1000_0000;
const TEST_PAGES: usize = 1024;
fn create_allocator() -> BuddyAllocator {
BuddyAllocator::new(PhysAddr::new(TEST_BASE), TEST_PAGES)
}
#[test]
fn test_new_allocator() {
let alloc = create_allocator();
assert_eq!(alloc.total_pages(), TEST_PAGES);
assert_eq!(alloc.free_page_count(), TEST_PAGES);
assert_eq!(alloc.used_pages(), 0);
assert_eq!(alloc.base_addr().as_u64(), TEST_BASE);
}
#[test]
fn test_alloc_single_page() {
let mut alloc = create_allocator();
let addr = alloc.alloc_pages(1).expect("alloc failed");
assert!(addr.is_page_aligned());
assert!(alloc.contains(addr));
assert_eq!(alloc.used_pages(), 1);
alloc.dealloc_pages(addr, 1);
assert_eq!(alloc.used_pages(), 0);
}
#[test]
fn test_alloc_multiple_pages() {
let mut alloc = create_allocator();
let addr = alloc.alloc_pages(4).expect("alloc failed");
assert!(addr.is_page_aligned());
assert!(addr.is_order_aligned(2)); assert_eq!(alloc.used_pages(), 4);
let addr2 = alloc.alloc_pages(8).expect("alloc failed");
assert!(addr2.is_order_aligned(3));
assert_eq!(alloc.used_pages(), 12);
alloc.dealloc_pages(addr, 4);
alloc.dealloc_pages(addr2, 8);
assert_eq!(alloc.used_pages(), 0);
}
#[test]
fn test_alloc_max_block() {
let mut alloc = BuddyAllocator::new(PhysAddr::new(TEST_BASE), 512);
let addr = alloc.alloc_pages(512).expect("alloc failed");
assert!(addr.is_page_aligned());
assert_eq!(alloc.used_pages(), 512);
assert_eq!(alloc.free_page_count(), 0);
alloc.dealloc_pages(addr, 512);
assert_eq!(alloc.free_page_count(), 512);
}
#[test]
fn test_alloc_too_large() {
let mut alloc = create_allocator();
let result = alloc.alloc_pages(513);
assert!(result.is_none());
}
#[test]
fn test_alloc_zero() {
let mut alloc = create_allocator();
let result = alloc.alloc_pages(0);
assert!(result.is_none());
}
#[test]
fn test_out_of_memory() {
let mut alloc = BuddyAllocator::new(PhysAddr::new(TEST_BASE), 8);
let addr1 = alloc.alloc_pages(4).expect("alloc failed");
let addr2 = alloc.alloc_pages(4).expect("alloc failed");
assert_eq!(alloc.free_page_count(), 0);
let result = alloc.alloc_pages(1);
assert!(result.is_none());
alloc.dealloc_pages(addr1, 4);
let addr3 = alloc.alloc_pages(1).expect("alloc failed");
assert!(addr3.is_page_aligned());
alloc.dealloc_pages(addr2, 4);
alloc.dealloc_pages(addr3, 1);
}
#[test]
fn test_block_splitting() {
let mut alloc = BuddyAllocator::new(PhysAddr::new(TEST_BASE), 8);
let addr = alloc.alloc_pages(1).expect("alloc failed");
assert!(alloc.stats().splits() > 0);
assert_eq!(alloc.free_page_count(), 7);
alloc.dealloc_pages(addr, 1);
}
#[test]
fn test_block_coalescing() {
let mut alloc = BuddyAllocator::new(PhysAddr::new(TEST_BASE), 8);
let addr1 = alloc.alloc_pages(4).expect("alloc failed");
let addr2 = alloc.alloc_pages(4).expect("alloc failed");
alloc.dealloc_pages(addr1, 4);
alloc.dealloc_pages(addr2, 4);
assert!(alloc.stats().coalesces() > 0);
assert_eq!(alloc.free_page_count(), 8);
}
#[test]
fn test_allocation_patterns() {
let mut alloc = create_allocator();
let mut addrs = [PhysAddr::NULL; 64];
for addr in &mut addrs {
*addr = alloc.alloc_pages(1).expect("alloc failed");
}
assert_eq!(alloc.used_pages(), 64);
for (i, addr) in addrs.iter().enumerate() {
if i % 2 == 0 {
alloc.dealloc_pages(*addr, 1);
}
}
assert_eq!(alloc.used_pages(), 32);
for (i, addr) in addrs.iter().enumerate() {
if i % 2 == 1 {
alloc.dealloc_pages(*addr, 1);
}
}
assert_eq!(alloc.used_pages(), 0);
}
#[test]
fn test_try_alloc() {
let mut alloc = create_allocator();
let result = alloc.try_alloc_pages(4);
assert!(result.is_ok());
let result = alloc.try_alloc_pages(0);
assert_eq!(result, Err(PhysMemError::ZeroAllocation));
let result = alloc.try_alloc_pages(1024);
assert_eq!(result, Err(PhysMemError::AllocationTooLarge));
}
#[test]
fn test_try_free() {
let mut alloc = create_allocator();
let addr = alloc.alloc_pages(4).unwrap();
let result = alloc.try_free_pages(addr, 4);
assert!(result.is_ok());
let result = alloc.try_free_pages(addr, 0);
assert_eq!(result, Err(PhysMemError::ZeroAllocation));
let result = alloc.try_free_pages(PhysAddr::new(1), 4);
assert_eq!(result, Err(PhysMemError::UnalignedAddress));
let result = alloc.try_free_pages(PhysAddr::new(0), 4);
assert_eq!(result, Err(PhysMemError::AddressOutOfRange));
}
#[test]
fn test_alloc_frame() {
let mut alloc = create_allocator();
let frame = alloc.alloc_frame(4).expect("alloc failed");
assert_eq!(frame.pages(), 4);
assert!(frame.addr().is_page_aligned());
alloc.free_frame(&frame);
assert_eq!(alloc.used_pages(), 0);
}
#[test]
fn test_reset() {
let mut alloc = create_allocator();
let _ = alloc.alloc_pages(100);
let _ = alloc.alloc_pages(50);
assert!(alloc.used_pages() > 0);
alloc.reset();
assert_eq!(alloc.free_page_count(), TEST_PAGES);
assert_eq!(alloc.used_pages(), 0);
}
#[test]
fn test_uninit() {
let mut alloc = BuddyAllocator::uninit();
let result = alloc.try_alloc_pages(1);
assert_eq!(result, Err(PhysMemError::NotInitialized));
alloc.init(PhysAddr::new(TEST_BASE), 64).unwrap();
let addr = alloc.alloc_pages(1).expect("alloc failed");
alloc.dealloc_pages(addr, 1);
}
#[test]
fn test_init_invalid_alignment() {
let mut alloc = BuddyAllocator::uninit();
let result = alloc.init(PhysAddr::new(0x1001), 64);
assert_eq!(result, Err(PhysMemError::UnalignedAddress));
}
#[test]
fn test_statistics() {
let mut alloc = create_allocator();
assert_eq!(alloc.stats().allocations(), 0);
assert_eq!(alloc.stats().frees(), 0);
let addr = alloc.alloc_pages(4).unwrap();
assert_eq!(alloc.stats().successful_allocations(), 1);
let mut small_alloc = BuddyAllocator::new(PhysAddr::new(TEST_BASE), 1);
let _ = small_alloc.alloc_pages(1); let _ = small_alloc.alloc_pages(1); assert_eq!(small_alloc.stats().failed_allocations(), 1);
alloc.dealloc_pages(addr, 4);
assert_eq!(alloc.stats().frees(), 1);
}
#[test]
fn test_order_stats() {
let alloc = BuddyAllocator::new(PhysAddr::new(TEST_BASE), 512);
let stats = alloc.order_stats();
assert_eq!(stats[9].free_blocks, 1);
for i in 0..9 {
assert_eq!(stats[i].free_blocks, 0);
}
}
#[test]
fn test_contains() {
let alloc = create_allocator();
assert!(alloc.contains(PhysAddr::new(TEST_BASE)));
assert!(alloc.contains(PhysAddr::new(TEST_BASE + 0x1000)));
assert!(!alloc.contains(PhysAddr::new(TEST_BASE - 1)));
assert!(!alloc.contains(PhysAddr::new(TEST_BASE + TEST_PAGES as u64 * PAGE_SIZE as u64)));
}
#[test]
#[should_panic(expected = "page-aligned")]
fn test_new_unaligned_panics() {
let _ = BuddyAllocator::new(PhysAddr::new(0x1001), 64);
}
#[test]
fn test_free_list_operations() {
let mut list = FreeList::new();
assert!(list.is_empty());
assert_eq!(list.len(), 0);
list.push(PhysAddr::new(0x1000)).unwrap();
list.push(PhysAddr::new(0x2000)).unwrap();
assert_eq!(list.len(), 2);
assert!(!list.is_empty());
assert!(list.contains(PhysAddr::new(0x1000)));
assert!(!list.contains(PhysAddr::new(0x3000)));
assert!(list.remove(PhysAddr::new(0x1000)));
assert_eq!(list.len(), 1);
assert!(!list.contains(PhysAddr::new(0x1000)));
let addr = list.pop().unwrap();
assert_eq!(addr.as_u64(), 0x2000);
assert!(list.is_empty());
assert!(list.pop().is_none());
}
}