A minimal operating system for RISC-V 64-bit architecture running on QEMU virt platform.
For a file-by-file reader map, see Architecture Notes.
- Platform: QEMU virt, RISC-V 64 (rv64gc), S-mode bare metal
- Boot: OpenSBI firmware (
-bios default) - Console: NS16550A UART driver with interactive shell
- Timer: SBI-based timer with 100 Hz tick rate (~10ms)
- Tasks: Kernel threads with context switching
- Scheduling:
- Round-Robin (RR) - Preemptive with 5-tick quantum
- Shortest Job First (SJF) - Non-preemptive with exponential burst estimation
- Benchmarking: Compare RR vs SJF performance metrics
- QEMU 7.0+ with RISC-V support
riscv64-unknown-elf-gcctoolchaindtc(Device Tree Compiler) for DTB inspection
brew install qemu
brew install riscv-gnu-toolchain
brew install dtc# Ubuntu/Debian
sudo apt-get install qemu-system-misc gcc-riscv64-unknown-elf device-tree-compiler
# Arch
sudo pacman -S qemu-system-riscv dtc
yay -S riscv64-elf-gcc# Build the kernel
make
# Run in QEMU
make run
# Clean build artifacts
make clean
# Boot and exercise the shell in QEMU
make smoke
# Dump device tree blob (for debugging)
make dtbOnce booted, the system presents an interactive shell prompt: HeliOS>
- help - Display list of available commands
- about - Show a compact capability summary for demos
- ps - List all tasks with PID, state, CPU ticks used, and burst estimate
- run cpu - Create a CPU-bound task (burns CPU cycles)
- run io - Create an I/O-bound task (simulates I/O with sleeps)
- kill <pid> - Terminate task with given PID
- sched rr - Switch to Round-Robin scheduler
- sched sjf - Switch to Shortest Job First scheduler
- sched preempt on|off - Enable or disable timer-driven RR preemption
- sleep <ticks> - Sleep for a number of timer ticks
- pcdemo - Run the producer-consumer synchronization demo
- bench - Run scheduling benchmark and compare RR vs SJF
- uptime - Display system uptime in seconds and ticks
- meminfo - Show kernel heap memory usage
- intstats - Show timer/interrupt CSR state
- Type: Preemptive
- Quantum: 5 ticks (~50ms at 100 Hz)
- Behavior: Each task runs for its quantum, then the scheduler switches to the next ready task in FIFO order
- Use Case: Fair CPU time distribution, good for interactive workloads
- Type: Non-preemptive
- Selection: Task with smallest estimated burst time runs to completion
- Estimation: Exponential averaging with alpha=0.5
- tau_new = 0.5 * actual_burst + 0.5 * tau_old
- Tie-breaking: Arrival time (FCFS), then PID
- Use Case: Minimizes average wait time when burst times are known/predictable
The bench command runs two rounds of the same task set:
- First under Round-Robin
- Then under SJF
Metrics reported:
-
Wait time (avg): Average time tasks spend in ready queue before first execution
- Formula: sum(start_time - arrival_time) / N
-
Turnaround time (avg): Average total time from arrival to completion
- Formula: sum(finish_time - arrival_time) / N
-
Throughput: Tasks completed per second
- Formula: N / total_duration
HeliOS provides basic synchronization primitives for coordinating concurrent tasks:
Counting semaphores with cooperative busy-wait implementation:
sem_t sem;
sem_init(&sem, initial_count); // Initialize with count
sem_wait(&sem); // Decrement (wait if count <= 0)
sem_post(&sem); // IncrementImplementation notes:
- Uses cooperative busy-wait with
task_yield()while waiting - Safe in both preemptive and cooperative modes
- Critical sections protected with
disable_irq()/enable_irq()
Binary semaphores for mutual exclusion:
mutex_t mutex;
mutex_init(&mutex); // Initialize
mutex_lock(&mutex); // Acquire (wait if locked)
mutex_unlock(&mutex); // ReleaseThe pcdemo command demonstrates classic producer-consumer synchronization:
Buffer: 16-item circular buffer Semaphores:
empty- tracks available slots (initially 16)full- tracks filled slots (initially 0)mutex- protects buffer access
Algorithm:
Producer:
sem_wait(&empty) // Wait for empty slot
mutex_lock(&mutex) // Enter critical section
[produce item]
mutex_unlock(&mutex) // Exit critical section
sem_post(&full) // Signal item available
Consumer:
sem_wait(&full) // Wait for item
mutex_lock(&mutex) // Enter critical section
[consume item]
mutex_unlock(&mutex) // Exit critical section
sem_post(&empty) // Signal slot available
Running the demo:
HeliOS> pcdemo
=== Producer-Consumer Demo ===
Buffer size: 16 items
Creating producer and consumer tasks...
Producer task created (PID 1)
Consumer task created (PID 2)
Demo running... (will produce/consume 10 items)
Watch the alternating output!
Producer: produced item 1 at index 0
Consumer: consumed item 1 from index 0
Producer: produced item 2 at index 1
Consumer: consumed item 2 from index 1
...
Producer: finished producing 10 items
Consumer: finished consuming 10 itemsObservations:
- Preemptive mode (
sched preempt on): More interleaved execution, smoother alternation - Cooperative mode (
sched preempt off): Works correctly, alternates at yield points - No race conditions: Mutex ensures buffer integrity
- No deadlock: Proper semaphore ordering prevents deadlock
- Visible delays: 5-tick sleeps make synchronization observable
HeliOS uses a free-list allocator with first-fit allocation and coalescing for efficient memory reuse.
Data Structure:
struct mem_header {
size_t size; // Block size (excluding header)
int free; // 1 = free, 0 = allocated
struct mem_header *next; // Next block in list
};Allocation (kmalloc):
- Search free list for first block with
size >= requested - If block is large enough, split it:
- Allocate requested size from beginning
- Create new free block with remainder
- Mark block as allocated
- Return pointer (after header)
Deallocation (kfree):
- Mark block as free
- Coalesce with next block if it's free
- Coalesce with previous block if it's free
- Reduces fragmentation
Memory Stats:
HeliOS> meminfo
=== Memory Usage ===
Heap total: 262144 bytes
Heap used: 24576 bytes (9%)
Heap free: 237568 bytes (90%)
Free blocks: 5
Fragmentation: moderate- First-fit: Fast allocation (O(n) where n = number of blocks)
- Splitting: Minimizes waste for small allocations
- Coalescing: Merges adjacent free blocks to prevent fragmentation
- Aligned: All allocations aligned to 16 bytes
- Thread-safe: Uses
disable_irq()/enable_irq()
- No best-fit or worst-fit: Only first-fit implemented
- Linear search: O(n) allocation time
- No compaction: Fragmentation can occur over time
- Fixed heap size: 256KB total
Task stacks: Each task allocates 4KB stack on creation
void *stack = kmalloc(STACK_SIZE); // 4096 bytes
// ... use stack ...
kfree(stack); // When task exitsDynamic buffers: Shell and synchronization structures
int *buffer = kmalloc(16 * sizeof(int));
// ... use buffer ...
kfree(buffer);# Before creating tasks
HeliOS> meminfo
Heap used: 8192 bytes
# After pcdemo
HeliOS> pcdemo
...
HeliOS> meminfo
Heap used: 24576 bytes # Two 4KB stacks + buffers
# After tasks finish (become ZOMBIE)
HeliOS> meminfo
Heap used: 24576 bytes # Stacks still allocated
# After killing tasks (with task_reap)
HeliOS> kill 1
HeliOS> kill 2
HeliOS> meminfo
Heap used: 8192 bytes # Stacks freed!- Header overhead: 24 bytes per block
- Minimum split size: 16 bytes (prevents tiny fragments)
- Coalescing: Both forward and backward
- Initial state: One large free block (256KB - header)
HeliOS (rv64gc, QEMU virt) - console ready
ticks=0
HeliOS> help
Available commands:
help - Show this help
ps - List tasks
run cpu - Create CPU-bound task
run io - Create I/O-bound task
kill <pid> - Kill a task
sched rr - Switch to Round-Robin
sched sjf - Switch to SJF
bench - Run scheduler benchmark
uptime - Show system uptime
meminfo - Show memory usage
HeliOS> sched rr
Scheduler: Round-Robin (quantum=5 ticks)
HeliOS> run cpu
Created CPU task with PID 0
HeliOS> run io
Created I/O task with PID 1
HeliOS> ps
PID STATE TICKS BURST_EST ARRIVAL
0 RUNNING 23 20 10
1 READY 8 15 15
HeliOS> bench
Running benchmark...
Round 1: Round-Robin...
RR done in 142 ticks
Round 2: SJF...
SJF done in 125 ticks
Benchmark Results (6 tasks):
RR SJF
Wait (avg): 45 28 ticks
Turnaround (avg): 68 48 ticks
Throughput: 4.22 4.80 tasks/sec
HeliOS> uptime
Uptime: 32.45 seconds (3245 ticks)
- Base Address: 0x80200000
- Kernel Stack: 16 KB
- Task Stacks: 8 KB each (up to 32 tasks)
- Heap: 256 KB (free-list allocator with coalescing)
All general-purpose registers (x1-x31), sstatus, and sepc are saved/restored on context switch. Stack pointer is aligned to 16 bytes per RISC-V ABI requirements.
- Timer interrupts occur every 10ms (100 Hz)
- Interrupts are disabled during critical sections (queue/context manipulation)
- Minimal work in IRQ handler - just schedule next tick and set flags
Tasks transition through these states:
- NEW -> READY -> RUNNING -> SLEEPING/back to READY -> ZOMBIE
The codebase is organized as follows:
HeliOS/
include/helios.h Main header with types and prototypes
linker.ld Linker script
boot/start.S Boot assembly and context switch
kernel/kmain.c Kernel entry point
kernel/trap.c Trap/interrupt handling
kernel/task.c Task management
kernel/kmem.c Free-list allocator
kernel/sched.c Scheduler (RR and SJF)
kernel/shell.c Interactive shell
drivers/uart.c NS16550A UART driver
drivers/timer.c SBI timer driver
lib/printf.c Minimal printf and string utilities
scripts/run-qemu.sh QEMU launch script
scripts/demo.sh Automated demo script
- No MMU/paging (bare metal S-mode)
- No user mode (all tasks run in kernel mode)
- No file system or persistent storage
- Single CPU core only
- Add user mode with syscalls
- Implement basic paging/virtual memory
- Multi-level feedback queue scheduling
- Real I/O device drivers (block devices, network)
- Add realloc and allocator stress tests
CI builds the kernel and runs scripts/smoke.sh, which boots QEMU headless,
sends a small shell command sequence, and checks for expected output. This keeps
the repository demonstrable instead of only compile-clean.
Educational project for OS course.
- Thomas