Skip to content

Commit 71cbb3e

Browse files
authored
Merge pull request #79 from ryanbreen/feature/cow-fork
feat(fork): Copy-on-Write page sharing with comprehensive test suite
2 parents fddc04f + c793eac commit 71cbb3e

25 files changed

+3221
-132
lines changed

kernel/src/interrupts.rs

Lines changed: 395 additions & 58 deletions
Large diffs are not rendered by default.

kernel/src/main.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,26 @@ fn kernel_main_continue() -> ! {
723723
log::info!("=== SIGNAL TEST: fork pending signal non-inheritance ===");
724724
test_exec::test_fork_pending_signal();
725725

726+
// Test CoW signal delivery (deadlock fix)
727+
log::info!("=== COW TEST: signal delivery on CoW-shared stack ===");
728+
test_exec::test_cow_signal();
729+
730+
// Test CoW cleanup on process exit
731+
log::info!("=== COW TEST: cleanup on process exit ===");
732+
test_exec::test_cow_cleanup();
733+
734+
// Test CoW sole owner optimization
735+
log::info!("=== COW TEST: sole owner optimization ===");
736+
test_exec::test_cow_sole_owner();
737+
738+
// Test CoW at scale with many pages
739+
log::info!("=== COW TEST: stress test with many pages ===");
740+
test_exec::test_cow_stress();
741+
742+
// Test CoW read-only page sharing (code sections)
743+
log::info!("=== COW TEST: read-only page sharing (code sections) ===");
744+
test_exec::test_cow_readonly();
745+
726746
// Test argv support in exec syscall
727747
log::info!("=== EXEC TEST: argv support ===");
728748
test_exec::test_argv();

kernel/src/memory/frame_allocator.rs

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
use alloc::vec::Vec;
12
use bootloader_api::info::{MemoryRegionKind, MemoryRegions};
2-
use core::sync::atomic::{AtomicUsize, Ordering};
3+
use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
34
use spin::Mutex;
45
use x86_64::structures::paging::{FrameAllocator, PhysFrame, Size4KiB};
56
use x86_64::PhysAddr;
@@ -31,6 +32,50 @@ struct MemoryInfo {
3132
static MEMORY_INFO: Mutex<Option<MemoryInfo>> = Mutex::new(None);
3233
static NEXT_FREE_FRAME: AtomicUsize = AtomicUsize::new(0);
3334

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+
3479
/// A simple frame allocator that returns usable frames from the bootloader's memory map
3580
pub struct BootInfoFrameAllocator;
3681

@@ -185,17 +230,79 @@ pub fn init(memory_regions: &'static MemoryRegions) {
185230
}
186231

187232
/// 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.
188248
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
189273
let mut allocator = BootInfoFrameAllocator::new();
190274
allocator.allocate_frame()
191275
}
192276

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+
}
199306
}
200307

201308
/// A wrapper that allows using the global frame allocator with the mapper
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
//! Frame metadata for Copy-on-Write reference counting
2+
//!
3+
//! Each physical frame that can be shared needs metadata tracking:
4+
//! - Reference count (how many page tables point to this frame)
5+
//!
6+
//! Design decisions:
7+
//! - Uses BTreeMap for sparse storage (only track shared frames)
8+
//! - Untracked frames are assumed to have refcount=1 (private)
9+
//! - Single global lock (acceptable for initial implementation)
10+
11+
use alloc::collections::BTreeMap;
12+
use core::sync::atomic::{AtomicU32, Ordering};
13+
use spin::Mutex;
14+
use x86_64::structures::paging::PhysFrame;
15+
16+
/// Global frame metadata storage
17+
/// Uses BTreeMap for sparse storage - only frames that need tracking are stored
18+
static FRAME_METADATA: Mutex<BTreeMap<u64, FrameMetadata>> = Mutex::new(BTreeMap::new());
19+
20+
/// Metadata for a single physical frame
21+
#[derive(Debug)]
22+
struct FrameMetadata {
23+
/// Number of page tables referencing this frame
24+
/// 0 = frame is free (should be removed from map)
25+
/// 1 = frame is private (can be written directly)
26+
/// >1 = frame is shared (CoW semantics apply)
27+
refcount: AtomicU32,
28+
}
29+
30+
impl FrameMetadata {
31+
fn new(initial_count: u32) -> Self {
32+
Self {
33+
refcount: AtomicU32::new(initial_count),
34+
}
35+
}
36+
}
37+
38+
/// Increment reference count for a frame
39+
/// Called when fork() shares a page between parent and child
40+
pub fn frame_incref(frame: PhysFrame) {
41+
let addr = frame.start_address().as_u64();
42+
let mut metadata = FRAME_METADATA.lock();
43+
44+
if let Some(meta) = metadata.get(&addr) {
45+
meta.refcount.fetch_add(1, Ordering::SeqCst);
46+
} else {
47+
// First time tracking this frame - it's being shared
48+
// When we start tracking, the frame is being shared between 2 processes
49+
let meta = FrameMetadata::new(2);
50+
metadata.insert(addr, meta);
51+
}
52+
}
53+
54+
/// Decrement reference count for a frame
55+
/// Returns true if frame can be freed (refcount reached 0)
56+
pub fn frame_decref(frame: PhysFrame) -> bool {
57+
let addr = frame.start_address().as_u64();
58+
let mut metadata = FRAME_METADATA.lock();
59+
60+
if let Some(meta) = metadata.get(&addr) {
61+
let old_count = meta.refcount.fetch_sub(1, Ordering::SeqCst);
62+
if old_count == 1 {
63+
// Was 1, now 0 - remove from tracking and allow free
64+
metadata.remove(&addr);
65+
return true;
66+
} else if old_count == 0 {
67+
// This shouldn't happen - underflow protection
68+
log::error!(
69+
"frame_decref: underflow for frame {:#x}, restoring to 0",
70+
addr
71+
);
72+
meta.refcount.store(0, Ordering::SeqCst);
73+
metadata.remove(&addr);
74+
return false;
75+
}
76+
// old_count > 1, still shared
77+
false
78+
} else {
79+
// Frame wasn't tracked - this is the sole owner
80+
// Return true to indicate it can be freed
81+
true
82+
}
83+
}
84+
85+
/// Get current reference count for a frame
86+
/// Returns 1 if frame is not tracked (assumed private)
87+
pub fn frame_refcount(frame: PhysFrame) -> u32 {
88+
let addr = frame.start_address().as_u64();
89+
let metadata = FRAME_METADATA.lock();
90+
91+
metadata
92+
.get(&addr)
93+
.map(|m| m.refcount.load(Ordering::SeqCst))
94+
.unwrap_or(1) // Untracked frames are private
95+
}
96+
97+
/// Check if a frame is shared (refcount > 1)
98+
pub fn frame_is_shared(frame: PhysFrame) -> bool {
99+
frame_refcount(frame) > 1
100+
}
101+
102+
/// Get statistics about frame metadata tracking
103+
/// Returns (tracked_frames, total_refcount)
104+
#[allow(dead_code)] // Diagnostic API for future CoW debugging
105+
pub fn frame_metadata_stats() -> (usize, u64) {
106+
let metadata = FRAME_METADATA.lock();
107+
let tracked = metadata.len();
108+
let total_refs: u64 = metadata
109+
.values()
110+
.map(|m| m.refcount.load(Ordering::Relaxed) as u64)
111+
.sum();
112+
(tracked, total_refs)
113+
}
114+
115+
#[cfg(test)]
116+
mod tests {
117+
use super::*;
118+
use x86_64::PhysAddr;
119+
120+
fn test_frame(addr: u64) -> PhysFrame {
121+
PhysFrame::containing_address(PhysAddr::new(addr))
122+
}
123+
124+
#[test_case]
125+
fn test_untracked_frame_is_private() {
126+
let frame = test_frame(0x1000_0000);
127+
assert_eq!(frame_refcount(frame), 1);
128+
assert!(!frame_is_shared(frame));
129+
}
130+
131+
#[test_case]
132+
fn test_incref_creates_shared() {
133+
let frame = test_frame(0x2000_0000);
134+
frame_incref(frame);
135+
assert_eq!(frame_refcount(frame), 2);
136+
assert!(frame_is_shared(frame));
137+
138+
// Cleanup
139+
frame_decref(frame);
140+
frame_decref(frame);
141+
}
142+
143+
#[test_case]
144+
fn test_multiple_incref() {
145+
let frame = test_frame(0x3000_0000);
146+
frame_incref(frame); // Now 2
147+
frame_incref(frame); // Now 3
148+
frame_incref(frame); // Now 4
149+
assert_eq!(frame_refcount(frame), 4);
150+
151+
// Cleanup
152+
while frame_refcount(frame) > 1 {
153+
frame_decref(frame);
154+
}
155+
frame_decref(frame);
156+
}
157+
158+
#[test_case]
159+
fn test_decref_to_zero() {
160+
let frame = test_frame(0x4000_0000);
161+
frame_incref(frame); // Now 2
162+
163+
assert!(!frame_decref(frame)); // Now 1, not freeable
164+
assert!(frame_decref(frame)); // Now 0, freeable
165+
assert_eq!(frame_refcount(frame), 1); // Back to untracked
166+
}
167+
}

kernel/src/memory/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod frame_allocator;
2+
pub mod frame_metadata;
23
pub mod heap;
34
pub mod kernel_page_table;
45
pub mod kernel_stack;

0 commit comments

Comments
 (0)