Nov 2024 - Jan 2025 · C, RISC-V assembly, QEMU · University of Illinois, ECE 391 · built with Yixuan Li live demo ·
IllinixOS is a UNIX-like kernel for 64-bit RISC-V, written in C and assembly for QEMU's RISC-V CPU emulator. It has Sv39 virtual memory, preemptive multitasking, fork and exec, a filesystem on a VirtIO disk, a system call interface, and a shell on a serial port. From the shell you can run programs stored on the disk. Each one is a process with its own address space, scheduled preemptively off a timer interrupt, and a process can fork and exec like it would on UNIX. The OS kernel runs an a single hart and with no C library. The string functions, heap allocator, and console are implemented in the kernel.
The kernel is a stack of layers with one I/O interface running through it.
A user program calls read via a syscall. The syscall handler triggers an exception, and
the trap handler saves its registers and switches to a kernel thread to execute the syscall.
It then checks that the pointer passed in really belongs to that process, and the
request goes to an io_intf struct, which can be a file on the filesystem or a device
such as a UART. All files and devices have an io_intf associated with them that
provides four operations: read, write, ctl, and close. The filesystem fetches its blocks through
the VirtIO block driver, which posts a descriptor on a virtqueue and sleeps on a condition variable
until the disk's interrupt arrives through the PLIC and wakes it. Each layer only knows the
interface of the one below it, so we had to design the interfaces so that they agree with each other.
The virtual memory is Sv39. Virtual addresses are 39-bits walked through three levels of page table.
The kernel maps the device registers with 1 GB pages, its own RAM with 2 MB pages, and its image
with 4 KB pages so that text is executable but not writable and data is writable but not
executable. Each user process gets its own subtree at 0xC000_0000 with the
user bit set, so the same virtual address means something different in every process, and
fork works by cloning that subtree. A page fault in user space that the kernel cannot resolve
kills the process instead of the machine.
The filesystem is made up of 4 KB blocks. A boot block holding the
directory of up to 63 entries, one inode block per file listing its length and data blocks,
then the data. Up to 32 files can be open at once, each behind the same io_intf
as a device. Programs are ELF files on that disk: exec unmaps the old user space, builds a
fresh page table, loads each program segment with the right permissions (R/W/X) and begins
execution in user mode from the prog entry point.
Two of us built it. I wrote the device drivers (PLIC, UART, VirtIO block), the timer and preemption, threads and processes, fork and exec, the virtual memory system, trap handling, and syscalls. Yixuan wrote the filesystem, the ELF loader and the locking,and I worked on the filesystem side with Yixuan to make communicate with the block device correctly.
After the course I compiled our kernel + QEMU to WebAssembly (using qemu-wasm and emscripten) so it can run in a web browser. There are user programs loaded onto its filesystem, such as Star Trek and Rule30 (provided by the course), one of my fork+thread switching stress test programs, and some other C programs I found on GitHub and compiled to RV64 (slightly modified to work with our kernel ABI). Demo
The kernel source stays private under course policy.