Skip to content

Commit 2575d7e

Browse files
committed
add fork with copy-on-write for riscv64
Unlike x86_64/aarch64, no kernel stack has to be cloned: the user- mode dispatcher owns the complete trap context, so fork is intercepted in user_loop (the syscall-table entry stays as an ENOSYS fallback), the child gets a copy of the parent's UserContext with a0 = 0, a fresh kernel stack, and re-enters user mode through its own user_loop_resume via fork_child_entry. The COW machinery mirrors the other targets: RSW bit 8 marks shared frames, mark_user_pages_copy_on_write drops the write bit before copy_current_root_page_table deep-copies the user slots (kernel slots stay shared), and do_cow_fault clones a frame on the first write — or re-marks it writable in place when the last reference faults. The frame refcounting protocol is completed on riscv64: every user mapping (code, TLS, stack, lazily faulted pages) takes a reference and clear_user_slots only releases frames whose count drops to zero, which also covers exec after fork. Kernel-mode stores through sstatus.SUM to COW pages are resolved in trap_handler. Verified in QEMU with the fork example: fork, exec with argv/envp in the child, waitpid, and 16k fork/exec/join cycles complete cleanly.
1 parent ab37371 commit 2575d7e

8 files changed

Lines changed: 467 additions & 14 deletions

File tree

src/arch/riscv64/kernel/interrupts.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,28 @@ pub extern "C" fn trap_handler(tf: &mut TrapFrame) {
248248
Trap::Interrupt(Interrupt::SupervisorTimer) => {
249249
crate::arch::riscv64::kernel::scheduler::timer_handler();
250250
}
251+
// The kernel writes to user pages through `sstatus.SUM` (program
252+
// loading, argv/envp setup, syscall buffers). Such a store can
253+
// fault on a COW-marked or not-yet-faulted user page and must be
254+
// resolved exactly like the corresponding user-mode fault.
255+
#[cfg(feature = "common-os")]
256+
Trap::Exception(Exception::LoadPageFault | Exception::StorePageFault) => {
257+
let fault_addr = stval;
258+
259+
#[cfg(feature = "fork")]
260+
if matches!(cause, Trap::Exception(Exception::StorePageFault))
261+
&& crate::arch::riscv64::mm::paging::do_cow_fault(memory_addresses::VirtAddr::new(
262+
fault_addr as u64,
263+
)) {
264+
return;
265+
}
266+
267+
if !crate::arch::riscv64::kernel::do_user_page_fault(fault_addr) {
268+
error!("Unhandled kernel page fault at {fault_addr:#x} ({cause:?})");
269+
error!("sepc = {sepc:x}");
270+
scheduler::abort();
271+
}
272+
}
251273
cause => {
252274
error!("Interrupt: {cause:?}");
253275
error!("tf = {tf:x?} ");

src/arch/riscv64/kernel/mod.rs

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,10 @@ where
230230
let layout = PageLayout::from_size_align(code_size, BasePageSize::SIZE as usize).unwrap();
231231
let frame_range = FrameAlloc::allocate(layout).unwrap();
232232
let physaddr = PhysAddr::from(frame_range.start());
233+
#[cfg(feature = "fork")]
234+
for i in 0..code_size / BasePageSize::SIZE as usize {
235+
crate::mm::frame_ref_inc(physaddr + i * BasePageSize::SIZE as usize);
236+
}
233237

234238
let mut flags = PageTableEntryFlags::empty();
235239
flags.normal().writable().user();
@@ -272,6 +276,10 @@ where
272276
let layout = PageLayout::from_size(tls_memsz).unwrap();
273277
let frame_range = FrameAlloc::allocate(layout).unwrap();
274278
let physaddr = PhysAddr::from(frame_range.start());
279+
#[cfg(feature = "fork")]
280+
for i in 0..tls_memsz / BasePageSize::SIZE as usize {
281+
crate::mm::frame_ref_inc(physaddr + i * BasePageSize::SIZE as usize);
282+
}
275283

276284
let mut flags = PageTableEntryFlags::empty();
277285
flags.normal().writable().user().execute_disable();
@@ -364,7 +372,7 @@ fn run_user(ctx: &mut trapframe::UserContext) {
364372
/// Returns `true` if the fault was resolved and the faulting instruction
365373
/// can be retried.
366374
#[cfg(feature = "common-os")]
367-
fn do_user_page_fault(fault_addr: usize) -> bool {
375+
pub(crate) fn do_user_page_fault(fault_addr: usize) -> bool {
368376
use core::ops::Bound;
369377

370378
use align_address::Align;
@@ -401,6 +409,8 @@ fn do_user_page_fault(fault_addr: usize) -> bool {
401409
}
402410

403411
paging::map::<BasePageSize>(addr, physaddr, 1, flags);
412+
#[cfg(feature = "fork")]
413+
crate::mm::frame_ref_inc(physaddr);
404414

405415
// Clear the page through the identity mapping of physical memory,
406416
// so this works independently of `sstatus.SUM`.
@@ -489,9 +499,11 @@ pub(crate) fn user_loop(
489499
| SSTATUS_FS_INITIAL
490500
| SSTATUS_SUM;
491501

492-
let mut ctx = trapframe::UserContext::default();
493-
ctx.sepc = entry;
494-
ctx.sstatus = user_sstatus;
502+
let mut ctx = trapframe::UserContext {
503+
sepc: entry,
504+
sstatus: user_sstatus,
505+
..Default::default()
506+
};
495507
ctx.general.sp = stack_pointer;
496508
ctx.general.tp = thread_pointer as usize;
497509
ctx.general.a0 = args[0];
@@ -500,6 +512,15 @@ pub(crate) fn user_loop(
500512

501513
debug!("Jump to user space at {entry:#x}, stack pointer {stack_pointer:#x}");
502514

515+
user_loop_resume(ctx)
516+
}
517+
518+
/// Service all traps the user code raises, starting (or resuming, for a
519+
/// fork child) user execution from `ctx`. Never returns; the process
520+
/// leaves through the `exit` system call (or is torn down after a fatal
521+
/// fault).
522+
#[cfg(feature = "common-os")]
523+
pub(crate) fn user_loop_resume(mut ctx: trapframe::UserContext) -> ! {
503524
use riscv::interrupt::{Exception, Interrupt, Trap};
504525
use riscv::register::{scause, stval};
505526

@@ -513,13 +534,35 @@ pub(crate) fn user_loop(
513534
let cause = Trap::<Interrupt, Exception>::try_from(scause.cause()).unwrap();
514535

515536
match cause {
516-
Trap::Exception(Exception::UserEnvCall) => dispatch_syscall(&mut ctx),
537+
Trap::Exception(Exception::UserEnvCall) => {
538+
// `fork` needs the full user context to seed the child's
539+
// user loop, so it is dispatched here instead of through
540+
// the syscall table.
541+
#[cfg(feature = "fork")]
542+
if ctx.general.a7 == crate::syscalls::table::SYSNO_FORK {
543+
ctx.sepc += 4;
544+
let pid: i32 = unsafe { crate::scheduler::fork_from_user_context(&ctx) }.into();
545+
ctx.general.a0 = pid as usize;
546+
continue;
547+
}
548+
549+
dispatch_syscall(&mut ctx);
550+
}
517551
Trap::Exception(
518552
Exception::InstructionPageFault
519553
| Exception::LoadPageFault
520554
| Exception::StorePageFault,
521555
) => {
522556
let fault_addr = stval::read();
557+
558+
#[cfg(feature = "fork")]
559+
if matches!(cause, Trap::Exception(Exception::StorePageFault))
560+
&& crate::arch::riscv64::mm::paging::do_cow_fault(
561+
memory_addresses::VirtAddr::new(fault_addr as u64),
562+
) {
563+
continue;
564+
}
565+
523566
if !do_user_page_fault(fault_addr) {
524567
error!("Unhandled user page fault at {fault_addr:#x} ({cause:?})");
525568
error!("sepc = {:#x}", ctx.sepc);
@@ -565,6 +608,10 @@ pub unsafe fn jump_to_user_land(entry_point: usize, args: Vec<CString>, envs: Ve
565608
let layout = PageLayout::from_size(USER_STACK_SIZE).unwrap();
566609
let frame_range = FrameAlloc::allocate(layout).unwrap();
567610
let phys_addr = PhysAddr::from(frame_range.start());
611+
#[cfg(feature = "fork")]
612+
for i in 0..USER_STACK_SIZE / BasePageSize::SIZE as usize {
613+
crate::mm::frame_ref_inc(phys_addr + i * BasePageSize::SIZE as usize);
614+
}
568615
let mut flags = PageTableEntryFlags::empty();
569616
flags.normal().writable().user().execute_disable();
570617
paging::map::<BasePageSize>(

src/arch/riscv64/kernel/scheduler.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,50 @@ extern "C" fn user_task_entry(entry: usize, arg: usize, user_stack: usize, tp: u
293293
crate::arch::riscv64::kernel::user_loop(entry, user_stack, tp as u64, [arg, 0, 0])
294294
}
295295

296+
/// First code of a fork child, running in kernel mode on the child's
297+
/// fresh kernel stack. Re-enters user mode with the parent's saved
298+
/// register state (`a0` already patched to 0 by `fork_from_user_context`).
299+
#[cfg(all(feature = "common-os", feature = "fork"))]
300+
extern "C" fn fork_child_entry(ctx_box: usize) -> ! {
301+
let ctx = unsafe {
302+
alloc::boxed::Box::from_raw(core::ptr::with_exposed_provenance_mut::<
303+
trapframe::UserContext,
304+
>(ctx_box))
305+
};
306+
crate::arch::riscv64::kernel::user_loop_resume(*ctx)
307+
}
308+
309+
/// Craft the initial kernel-stack frame for a fork child. When the
310+
/// scheduler first picks the child, `switch_to_task` pops the `State`
311+
/// and returns into `fork_child_entry` with the boxed `UserContext` as
312+
/// argument. Returns the child's initial `last_stack_pointer`.
313+
#[cfg(all(feature = "common-os", feature = "fork"))]
314+
pub(crate) fn create_fork_stack_frame(
315+
stacks: &TaskStacks,
316+
ctx: alloc::boxed::Box<trapframe::UserContext>,
317+
) -> VirtAddr {
318+
unsafe {
319+
// Set a marker for debugging at the very top.
320+
let mut stack = stacks.get_kernel_stack() + stacks.get_kernel_stack_size() - 0x10u64;
321+
*stack.as_mut_ptr::<u64>() = 0xdead_beefu64;
322+
323+
// Put the State structure expected by the ASM switch() function on the stack.
324+
stack -= size_of::<State>();
325+
326+
let state = stack.as_mut_ptr::<State>();
327+
state.cast::<u8>().write_bytes(0, size_of::<State>());
328+
329+
(*state).ra = core::mem::transmute::<
330+
extern "C" fn(usize) -> !,
331+
unsafe extern "C" fn(extern "C" fn(usize), usize, u64),
332+
>(fork_child_entry);
333+
(*state).sp = stack.as_usize();
334+
(*state).a0 = alloc::boxed::Box::into_raw(ctx).expose_provenance();
335+
336+
stack
337+
}
338+
}
339+
296340
#[cfg(feature = "common-os")]
297341
impl Task {
298342
/// Craft the initial kernel-stack frame for a new user-space thread.

src/arch/riscv64/mm/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
pub mod paging;
22

3+
#[cfg(all(feature = "common-os", feature = "fork"))]
4+
pub use paging::{copy_current_root_page_table, prepare_mem_copy_on_write};
35
#[cfg(feature = "common-os")]
46
pub use paging::{create_new_root_page_table, drop_user_space};
57

0 commit comments

Comments
 (0)