Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Key Functions

In the kernel, we are going to need a few key functions. Things like fork, exit, kill and wait are all extremely necessary. These are the helpers that prepare the ground for real syscalls in KFS_7 — they do the actual work, the syscall layer will just be the gate that lets user processes call them safely.

create_process()

Creates and allocates space for a Process on the heap and registers it in the process table:

#![allow(unused)]
fn main() {
pub fn create_process() -> *mut Process {
    let mut process: Process = Process::new();
    unsafe {
        process.pid = PID_COUNT;
        PID_COUNT += 1;
        for i in 0..8 {
            match PROCESS_TABLE[i] {
                Some(_) => {}
                None => {
                    let ptr = kmalloc(core::mem::size_of::<Process>());
                    match ptr {
                        Some(p) => {
                            let proc_ptr = p as *mut Process;
                            core::ptr::write(proc_ptr, process);
                            PROCESS_TABLE[i] = Some(proc_ptr);
                            return proc_ptr;
                        }
                        None => {}
                    }
                }
            }
        }
        return core::ptr::null_mut();
    }
}
}

core::ptr::write copies the process data into the kmalloc’d memory without going through Rust’s normal move semantics — necessary because Process contains raw pointers that Rust can’t reason about safely.

exec_fn

exec_fn loads a function as a process. This is the mandatory test function from the subject — it’s how we prove the process system works before we have binary execution in KFS_7:

#![allow(unused)]
fn main() {
pub fn exec_fn(_addr: u32, function: u32, size: u32) {
    unsafe {
        let proc = create_process();
        if proc.is_null() {
            return;
        }
        (*proc).stack = vmalloc_user(size as usize) as u32;
        (*proc).stack_size = size;
        let frame_ptr = kmalloc(core::mem::size_of::<InterruptFrame>());
        if let Some(f) = frame_ptr {
            let frame = f as *mut InterruptFrame;
            core::ptr::write(
                frame,
                InterruptFrame::dummy_frame(function, (*proc).stack as *mut u8, size),
            );
            (*proc).state = frame;
        }
    }
}
}

vmalloc_user gives the process a stack mapped with PAGE_USER flags — ready for ring 3 in KFS_7 without any changes. The dummy_frame builds a fake CPU state that makes the scheduler think the process was interrupted mid-execution:

#![allow(unused)]
fn main() {
pub fn dummy_frame(function: u32, stack_top: *mut u8, size: u32) -> InterruptFrame {
    let mut frame = InterruptFrame::new();
    frame.eip = function;                        // where to start executing
    frame.esp = stack_top as u32 + size - 4;     // top of stack (stacks grow down!)
    frame.eflags = 0x00000202;                   // IF set + reserved bit 1
    frame.cs = 0x08;                             // kernel code segment for now
    frame
}
}

Two things that must be right: esp points to the top of the allocation (stacks grow downward on x86), and eflags has the interrupt flag set — 0x200. Get that wrong and the process runs with interrupts permanently disabled and the scheduler can never fire again.

vmalloc_user

Process stacks need to be mapped with PAGE_USER so ring 3 code can access them. Rather than changing vmalloc itself, we split it into three:

#![allow(unused)]
fn main() {
pub unsafe fn vmalloc(size: usize) -> *mut u8 {
    vmalloc_with_flags(size, KERNEL_PAGE_FLAGS)
}

pub unsafe fn vmalloc_user(size: usize) -> *mut u8 {
    vmalloc_with_flags(size, USER_PAGE_FLAGS)
}

unsafe fn vmalloc_with_flags(size: usize, flags: u32) -> *mut u8 {
    // ... allocates pages with the given flags
}
}

All existing kernel vmalloc calls are unchanged. Process stacks use vmalloc_user. When KFS_7 adds ring 3, the stacks are already correctly mapped.

fork

fork copies an entire process — fresh stack allocation, stack contents copied, fresh InterruptFrame copied from parent, parent/child tree updated:

#![allow(unused)]
fn main() {
pub fn fork(parent: *mut Process) -> *mut Process {
    let proc = create_process();
    unsafe {
        (*proc).parent = parent;
        (*proc).stack = vmalloc_user((*parent).stack_size as usize) as u32;
        mem::memcpy(
            (*proc).stack as *mut u8,
            (*parent).stack as *mut u8,
            (*parent).stack_size as usize,
        );
        (*proc).stack_size = (*parent).stack_size;

        // TODO: Copy heap — processes don't have dynamic heaps yet (KFS_7)
        if !(*parent).heap.is_null() {
            // heap copy goes here when heap allocation is implemented
        }

        (*proc).signal_queue = (*parent).signal_queue;
        (*proc).owner_id = (*parent).owner_id;

        // allocate a fresh frame and copy parent's state into it
        let frame_ptr = kmalloc(core::mem::size_of::<InterruptFrame>());
        if let Some(f) = frame_ptr {
            let frame = f as *mut InterruptFrame;
            core::ptr::write(frame, *(*parent).state);
            (*proc).state = frame;
        }

        // link child into parent's child list
        if (*parent).first_child.is_null() {
            (*parent).first_child = proc;
        } else {
            let mut sibling = (*parent).first_child;
            while !(*sibling).next_sibling.is_null() {
                sibling = (*sibling).next_sibling;
            }
            (*sibling).next_sibling = proc;
        }

        proc
    }
}
}

The critical part is the frame copy — core::ptr::write(frame, *(*parent).state) dereferences the parent’s state pointer to get the actual frame contents, then writes a copy into the freshly allocated frame. If you just copy the pointer, both processes share the same frame and the scheduler corrupts one when saving the other.

The heap copy is deferred — processes don’t have dynamic heaps yet. That arrives with malloc in KFS_7.

memcpy is a klib function we wrote ourselves rather than pulling it from anywhere:

#![allow(unused)]
fn main() {
pub fn memcpy(dest: *mut u8, src: *mut u8, size: usize) -> *mut u8 {
    if src == 0 as *mut u8 || dest == 0 as *mut u8 {
        return 0 as *mut u8;
    }
    unsafe {
        for i in 0..size {
            *dest.add(i) = *src.add(i);
        }
    }
    return dest;
}
}

On Rust raw pointers, dest.add(i) is pointer arithmetic — equivalent to dest + i in C. Then * dereferences it. So *dest.add(i) = *src.add(i) is exactly C’s dest[i] = src[i].

exit

Frees the stack, marks the process as Zombie, notifies the parent with SIGCHLD, and removes the process from the table:

#![allow(unused)]
fn main() {
pub fn exit(pid: u32) {
    unsafe {
        let proc = match get_process(pid) {
            Some(p) => p,
            None => return,
        };

        // free the process's stack
        vfree((*proc).stack as *mut u8);

        // mark as finished
        (*proc).status = ProcessStatus::Zombie;

        // notify parent with SIGCHLD (signal 17)
        let parent = (*proc).parent;
        if !parent.is_null() {
            for i in 0..signals::SIGNAL_QUEUE_SIZE {
                if (*parent).signal_queue[i] == 0 {
                    (*parent).signal_queue[i] = 17; // SIGCHLD
                    break;
                }
            }
        }

        // remove from table
        PROCESS_TABLE[pid as usize] = None;
    }
}
}

SIGCHLD is signal 17 — standard Unix. The parent’s signal queue gets the signal on the next tick so it knows to call wait().

kill

Two cases — SIGKILL (9) terminates immediately, anything else queues the signal for delivery on the next tick:

#![allow(unused)]
fn main() {
pub fn kill(pid: u32, signal: u8) {
    unsafe {
        let proc = match get_process(pid) {
            Some(p) => p,
            None => return,
        };

        if signal == 9 {
            exit(pid);
            return;
        }

        for i in 0..signals::SIGNAL_QUEUE_SIZE {
            if (*proc).signal_queue[i] == 0 {
                (*proc).signal_queue[i] = signal;
                break;
            }
        }
    }
}
}

getuid

Simply returns the owner_id from the process:

#![allow(unused)]
fn main() {
pub fn getuid(pid: u32) -> u32 {
    unsafe {
        let proc = match get_process(pid) {
            Some(p) => p,
            None => return 0,
        };
        (*proc).owner_id
    }
}
}

signal

Registers a per-process signal handler. Each process has 32 handler slots — one per signal number. This is separate from the global SIGNAL_TABLE in the interrupts module which handles kernel-wide interrupt routing:

#![allow(unused)]
fn main() {
pub fn signal(pid: u32, signum: u32, handler: ProcessSignalHandler) {
    unsafe {
        let proc = match get_process(pid) {
            Some(p) => p,
            None => return,
        };
        if signum < 32 {
            (*proc).signal_handlers[signum as usize] = Some(handler);
        }
    }
}
}

When a signal is delivered from the queue, the kernel checks if the process has a handler registered for it and calls it. Without a registered handler, the default kernel behaviour applies.

wait

Blocks until a child of pid becomes a Zombie, then returns that child’s pid:

#![allow(unused)]
fn main() {
pub fn wait(pid: u32) -> u32 {
    unsafe {
        loop {
            for i in 0..8 {
                if let Some(p) = PROCESS_TABLE[i] {
                    if (*p).parent == get_process(pid).unwrap_or(core::ptr::null_mut()) {
                        match (*p).status {
                            ProcessStatus::Zombie => return (*p).pid,
                            _ => {}
                        }
                    }
                }
            }
        }
    }
}
}

This is a busy-wait — it spins checking the process table until a child finishes. The proper implementation puts the waiting process to sleep and wakes it on SIGCHLD, which requires the scheduler to support process sleeping. That’s KFS_5’s scheduler work done properly. For now this satisfies the subject requirement — it works correctly, it’s just not efficient.