PraxisOS is a minimalist, monolithic operating system kernel designed for the RISC-V 32-bit architecture. This is a study project implementing concepts from operating-system-in-1000-lines.
PraxisOS is built on a classic monolithic kernel model. While minimalist, it implements several core operating system concepts to support a functional, albeit simple, user-space environment.
The kernel uses the RISC-V Sv32 virtual memory scheme, which implements a two-level page table structure. The address space is partitioned between the kernel and a single user process.
-
Address Space Layout:
- Kernel Space: Loaded at the physical and virtual address
0x80200000. The kernel's code, data, and a 64MB region for dynamic page allocation (__free_ram) are mapped with a 1:1 virtual-to-physical correspondence. This direct mapping simplifies the kernel's memory management logic. - User Space: Each process is given a distinct address space starting at the virtual address
0x1000000. This is defined in theuser.ldlinker script.
- Kernel Space: Loaded at the physical and virtual address
-
Page Tables:
- Each process has its own root page table (Level 1). Upon a context switch, the
satp(Supervisor Address Translation and Protection) register is updated to point to the next process's root page table, effectively activating its address space. - The
create_user_pagetablefunction allocates a new page table for a process. It crucially maps the entire kernel space into the user process's address space, allowing the CPU to access kernel code and data when a trap occurs. It then maps the application's binary image and allocates pages for its stack.
- Each process has its own root page table (Level 1). Upon a context switch, the
-
Physical Allocation:
- Physical pages are allocated using
alloc_pages, a simple bump allocator that carves out 4KB chunks from the__free_ramregion. There is no corresponding deallocation mechanism, reflecting the system's simple lifecycle.
- Physical pages are allocated using
PraxisOS implements cooperative multitasking for a fixed number of processes (PROCS_MAX).
-
Process Control Block: The state of each process is maintained in a
struct process, which contains its ID, state (PROC_RUNNABLE,PROC_EXITED, etc.), a pointer to its kernel stack, and a pointer to its root page table. -
Scheduling:
- The scheduler is implemented in the
yield()function and follows a basic round-robin policy. It iterates through the globalprocsarray to find the nextPROC_RUNNABLEprocess to execute. - There is a designated
idle_procthat runs when no other processes are runnable.
- The scheduler is implemented in the
-
Context Switching:
- The
switch_contextfunction, written in assembly, performs the low-level machine state switch. It saves callee-saved registers to the current process's kernel stack and restores them for the next process. - Upon a trap from user-space, the
sscratchregister is used to swap the user stack pointer with the process's kernel stack pointer, ensuring a safe and isolated execution environment for the trap handler.
- The
System calls provide the interface between user-space applications and kernel services.
- Trap Entry: The
stvecregister is configured to point tokernel_entry, the unified assembly routine for handling all traps (interrupts, exceptions, and syscalls). - Trap Handling:
kernel_entrysaves the complete user register context onto the process's kernel stack, creating astruct trap_frame.- It then calls
handle_trap, a C function that reads thescauseregister to determine the cause of the trap. - For syscalls (
scause= 8),handle_trapdispatches tohandle_syscall.
- Syscall Dispatch:
handle_syscalluses the value in registera3as an index intosyscall_table, an array of function pointers, to invoke the correct handler (e.g.,handle_putchar,handle_readfile). Arguments are passed from user space in registersa0-a2.
A key feature of PraxisOS is its driver for the VirtIO block device, the standard para-virtualized I/O mechanism in QEMU.
- Initialization: The
virtio_blk_initfunction performs the standard VirtIO device initialization sequence, negotiating with the device and setting up the required virtqueue. - Virtqueues: Communication is managed via a single virtqueue, which consists of three main components:
- Descriptor Table: An array of
struct virtq_descthat describes the memory buffers (address, length, flags) the driver wants to share with the device. - Available Ring: The driver places the index of a descriptor chain into this ring to notify the device of a new request.
- Used Ring: The device places the index of a completed descriptor chain here to signal completion to the driver.
- Descriptor Table: An array of
- I/O Operations:
- To perform a read or write, the driver constructs a
struct virtio_blk_reqheader and assembles a three-descriptor chain: one for the request header, one for the data buffer, and one for the device to write a status byte. - The driver "kicks" the device by writing to the
VIRTIO_REG_QUEUE_NOTIFYregister. It then busy-waits by polling theused_indexof the virtqueue to detect when the operation has finished.
- To perform a read or write, the driver constructs a
The filesystem is not a traditional on-disk structure but rather an in-memory cache of files loaded from a TAR archive at boot.
- Initialization: At startup,
fs_initreads the first few sectors from the VirtIO block device. It parses this data as a USTAR-formatted TAR archive, extracting each file's name and content into the in-memoryfilesarray. - File Operations: The
readfileandwritefilesyscalls operate directly on the data buffers in this array. - Persistence: When
writefileis called, thefs_flushfunction is triggered. It reconstructs the entire TAR archive in a memory buffer from the current state of thefilesarray and writes it back to the block device. This makes changes persistent for the duration of the QEMU session but is highly inefficient.
Follow these instructions to set up the toolchain and run PraxisOS.
PraxisOS requires a specific toolchain for building the RISC-V 32-bit kernel and user-space applications. You will need:
- Clang Compiler: To compile C code for the
riscv32-unknown-elftarget. - LLD Linker: The LLVM linker, used by Clang.
- LLVM Objcopy: To manipulate the compiled ELF binaries.
- QEMU: To emulate the RISC-V machine and run the OS.
Below are installation instructions for common operating systems.
brew install llvm qemuThe llvm package includes clang, lld, and llvm-objcopy. Since run.sh invokes these tools by name, you must add LLVM's bin directory to your PATH:
export PATH="$(brew --prefix llvm)/bin:$PATH"Add this to your shell profile (.zshrc, .bash_profile) to persist across sessions.
sudo apt update
sudo apt install clang lld llvm qemu-system-miscThese packages will provide the necessary clang, lld, llvm-objcopy, and qemu-system-riscv32 tools.
Once the prerequisites are installed, you can build and run the operating system with a single script.
-
Execute the
run.shscript:./run.sh
This script handles everything:
- Compiling the user-space shell.
- Compiling the kernel.
- Creating a
disk.tarfile system image from the contents of thedisk/directory. - Launching the OS in QEMU.
-
Interact with the Shell:
After running the script, QEMU will launch, and you will be greeted by the PraxisOS shell prompt:
>Available commands:
hello: Prints a test message.readfile: Reads and displays the content ofhello.txtfrom the embedded disk image.writefile: Writes data tohello.txton the embedded disk.exit: Terminates the shell and stops the OS.
To exit QEMU, press
Ctrl+Afollowed byX.