|
| 1 | +use alloc::vec::Vec; |
1 | 2 | use bootloader_api::info::{MemoryRegionKind, MemoryRegions}; |
2 | | -use core::sync::atomic::{AtomicUsize, Ordering}; |
| 3 | +use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; |
3 | 4 | use spin::Mutex; |
4 | 5 | use x86_64::structures::paging::{FrameAllocator, PhysFrame, Size4KiB}; |
5 | 6 | use x86_64::PhysAddr; |
@@ -31,6 +32,50 @@ struct MemoryInfo { |
31 | 32 | static MEMORY_INFO: Mutex<Option<MemoryInfo>> = Mutex::new(None); |
32 | 33 | static NEXT_FREE_FRAME: AtomicUsize = AtomicUsize::new(0); |
33 | 34 |
|
| 35 | +/// Free list for deallocated frames |
| 36 | +/// When frames are deallocated (e.g., after CoW copy reduces refcount to 0), |
| 37 | +/// they are added to this list for reuse |
| 38 | +static FREE_FRAMES: Mutex<Vec<PhysFrame>> = Mutex::new(Vec::new()); |
| 39 | + |
| 40 | +/// Test-only flag to simulate OOM conditions |
| 41 | +/// |
| 42 | +/// When set to true, allocate_frame() will return None to simulate out-of-memory. |
| 43 | +/// This is used to test that CoW fault handling gracefully terminates processes |
| 44 | +/// when memory allocation fails. |
| 45 | +/// |
| 46 | +/// # Safety |
| 47 | +/// Only enable this flag briefly during testing. The flag affects ALL frame |
| 48 | +/// allocations, so enabling it for too long will crash the kernel. |
| 49 | +#[cfg(feature = "testing")] |
| 50 | +static SIMULATE_OOM: AtomicBool = AtomicBool::new(false); |
| 51 | + |
| 52 | +/// Enable OOM simulation for testing |
| 53 | +/// |
| 54 | +/// After calling this, all frame allocations will return None until |
| 55 | +/// `disable_oom_simulation()` is called. |
| 56 | +/// |
| 57 | +/// # Warning |
| 58 | +/// Only use this for brief tests! Extended OOM simulation will crash the kernel. |
| 59 | +#[cfg(feature = "testing")] |
| 60 | +pub fn enable_oom_simulation() { |
| 61 | + log::warn!("OOM simulation ENABLED - all frame allocations will fail"); |
| 62 | + SIMULATE_OOM.store(true, Ordering::SeqCst); |
| 63 | +} |
| 64 | + |
| 65 | +/// Disable OOM simulation |
| 66 | +#[cfg(feature = "testing")] |
| 67 | +pub fn disable_oom_simulation() { |
| 68 | + SIMULATE_OOM.store(false, Ordering::SeqCst); |
| 69 | + log::info!("OOM simulation disabled - frame allocations restored"); |
| 70 | +} |
| 71 | + |
| 72 | +/// Check if OOM simulation is currently active |
| 73 | +#[cfg(feature = "testing")] |
| 74 | +#[allow(dead_code)] // May be useful for future diagnostic output |
| 75 | +pub fn is_oom_simulation_active() -> bool { |
| 76 | + SIMULATE_OOM.load(Ordering::SeqCst) |
| 77 | +} |
| 78 | + |
34 | 79 | /// A simple frame allocator that returns usable frames from the bootloader's memory map |
35 | 80 | pub struct BootInfoFrameAllocator; |
36 | 81 |
|
@@ -185,17 +230,79 @@ pub fn init(memory_regions: &'static MemoryRegions) { |
185 | 230 | } |
186 | 231 |
|
187 | 232 | /// Allocate a physical frame |
| 233 | +/// |
| 234 | +/// First checks the free list for previously deallocated frames, |
| 235 | +/// then falls back to sequential allocation from the memory map. |
| 236 | +/// |
| 237 | +/// # OOM Behavior |
| 238 | +/// |
| 239 | +/// When memory is exhausted (or OOM simulation is active in test builds), |
| 240 | +/// this function returns `None`. Callers must handle this gracefully: |
| 241 | +/// |
| 242 | +/// - **CoW fault handler**: Returns `false`, causing the page fault handler |
| 243 | +/// to terminate the process with SIGSEGV (exit code -11). This is the |
| 244 | +/// correct POSIX behavior for processes that cannot allocate memory |
| 245 | +/// during page faults. |
| 246 | +/// |
| 247 | +/// - **Other kernel code**: Should propagate the error or use fallback paths. |
188 | 248 | pub fn allocate_frame() -> Option<PhysFrame> { |
| 249 | + // Test-only: simulate OOM if flag is set |
| 250 | + #[cfg(feature = "testing")] |
| 251 | + if SIMULATE_OOM.load(Ordering::SeqCst) { |
| 252 | + log::trace!("Frame allocator: OOM simulation active, returning None"); |
| 253 | + return None; |
| 254 | + } |
| 255 | + |
| 256 | + // First, try to reuse a frame from the free list |
| 257 | + { |
| 258 | + if let Some(mut free_list) = FREE_FRAMES.try_lock() { |
| 259 | + if let Some(frame) = free_list.pop() { |
| 260 | + log::trace!( |
| 261 | + "Frame allocator: Reused frame {:#x} from free list ({} remaining)", |
| 262 | + frame.start_address().as_u64(), |
| 263 | + free_list.len() |
| 264 | + ); |
| 265 | + return Some(frame); |
| 266 | + } |
| 267 | + } |
| 268 | + // If we couldn't get the lock, fall through to sequential allocation |
| 269 | + // This avoids deadlock if called from interrupt context |
| 270 | + } |
| 271 | + |
| 272 | + // Fall back to sequential allocation from memory map |
189 | 273 | let mut allocator = BootInfoFrameAllocator::new(); |
190 | 274 | allocator.allocate_frame() |
191 | 275 | } |
192 | 276 |
|
193 | | -/// Deallocate a physical frame (currently a no-op) |
194 | | -/// TODO: Implement proper frame deallocation |
195 | | -#[allow(dead_code)] |
196 | | -pub fn deallocate_frame(_frame: PhysFrame) { |
197 | | - // For now, we don't reclaim frames |
198 | | - // A proper implementation would add the frame back to a free list |
| 277 | +/// Deallocate a physical frame, returning it to the free pool |
| 278 | +/// |
| 279 | +/// The frame will be available for reuse by future allocations. |
| 280 | +/// This is called when a CoW page's reference count drops to zero. |
| 281 | +pub fn deallocate_frame(frame: PhysFrame) { |
| 282 | + // Don't deallocate frames below the low memory floor |
| 283 | + if frame.start_address().as_u64() < LOW_MEMORY_FLOOR { |
| 284 | + log::warn!( |
| 285 | + "Refusing to deallocate frame {:#x} below low memory floor", |
| 286 | + frame.start_address().as_u64() |
| 287 | + ); |
| 288 | + return; |
| 289 | + } |
| 290 | + |
| 291 | + if let Some(mut free_list) = FREE_FRAMES.try_lock() { |
| 292 | + log::trace!( |
| 293 | + "Frame allocator: Deallocated frame {:#x} (free list size: {})", |
| 294 | + frame.start_address().as_u64(), |
| 295 | + free_list.len() + 1 |
| 296 | + ); |
| 297 | + free_list.push(frame); |
| 298 | + } else { |
| 299 | + // If we can't get the lock (e.g., called from interrupt context), |
| 300 | + // we lose this frame. This is a memory leak but prevents deadlock. |
| 301 | + log::warn!( |
| 302 | + "Frame allocator: Could not deallocate frame {:#x} - lock contention", |
| 303 | + frame.start_address().as_u64() |
| 304 | + ); |
| 305 | + } |
199 | 306 | } |
200 | 307 |
|
201 | 308 | /// A wrapper that allows using the global frame allocator with the mapper |
|
0 commit comments