Some of the main features of xv6 are covered here:
Memory and Caching
xv6 has 128MB of main memory, which is hardwired into the kernel with aderive. In real life systems, this is dynamic. As well, the kernel has to use caching (L1, L2, L3 caches) to optimize performance. xv6 does not.
Device Support
- xv6 has UART (Universal Asynchronous Receiver-Transmitter) support for connecting to devices.
- xv6 only supports a single disk drive, which is emulated as a file on the host machine.
- Real world processors have interrupt controllers like PLIC (Platform-Level Interrupt Controllers) which manage interrupts and assigns them to the appropriate core, and CLINT (Core-Local Interrupt Controllers) which handle interrupts per core.
Memory Allocation
-
Physical Memory in xv6 is divided into 4KB pages. The kernel manages memory using a free list, a linked list of available pages.
-
When memory is needed, the kernel allocates a page from the free list.
-
When no longer needed, the page is returned to the front of the free list.
-
There is a three-level page table to manage virtual memory.
-
Kernel Page Table (Maps physical memory for all cores)
-
per-process Page table (Maps virtual address space for each process)
Pages can be marked as:
- R (Readable)
- W (Writable)
- X (Executable)
- U (User-accessible)
- V (Valid)
Where user-mode processes can only access pages marked as U.
Scheduling
xv6 has a semi round-robin scheduler.
- Each process has a time slice of 1 million cycles
- After the time slice expires, the process is returned to the ready queue.
- The next process is selected for execution, which could be on a different core
All cores share a single ready queue, where each core scans through the array linearly, searching for a runnable process.
Booting
xv6 also simplifies the boot process:
The emulator loads the kernel directly into a fixed memory location. And starts execution from there.
No bootloader, boot block or BIOS
Synchronization & Concurrency
xv6 has spinlocks where:
- 0 = free
- 1 = held
With two accompanying functions:
acquire()
wait in a loop until the lock is free, then lock.release()
unlocks the acquired lock by setting variable to 0.
As well, there’s sleep()
and wakeup()
sleep()
puts a process in a blocked statewake_up()
changes a sleeping process back to runnable for scheduling
There’s also interrupt disabling. Each core can enable/disable interrupts. However, this only works on the local core, so other cores can still modify shared memory, so locking is necessary.
System Limits
- The process table is an array.
- The maximum number of open files is predefined.
- Linear searches are used for killing processes.
User Address Space
A program’s virtual memory is laid out as so:
- Code and Data section (loaded from the binary during execution)
- Guard Page (prevents stack overflow by mdrking a page as
- Stack (Allocated one 4KB page). If the program exceeds this, it crashes.
- Heap (Grows dynamically in page-sized increments) inaccessible)
- Trap/Trampoline pages (traps are per process, trampolines for all
processes)
- Trap frames store process state during exceptions
- Trampoline for handling traps
Environment variables are not supported.
Risc-V Memory Model
- xv6 uses Sv39, which has 39-bit virtual address space, and xv6 uses 38 bits, limiting the virtual address space to 256GB. There are Sv32 and Sv48 variants as well.