A small operating system kernel, written from scratch, that runs several programs at once on an ARM processor.
It boots on a real BeagleBone Black board or in the QEMU emulator, takes turns between programs on a timer, keeps them out of each other's memory, and survives when one of them crashes.
A real boot in QEMU. Blue lines come from the OS process, green from the user programs, grey lines are the kernel logging every switch, and orange is a program crashing on purpose while the rest keep running.
- Several programs, one processor. A timer interrupts the running program at a fixed interval and the kernel hands the processor to the next one in line (round-robin scheduling). A program can also give up its turn early.
- Programs can't touch each other. Each one lives in its own 1 MB slice of memory, and the processor's memory-protection unit blocks any access outside it.
- A controlled way in. Programs ask the kernel for things (write text, give up the CPU, exit) through system calls, and the kernel checks every request: a bad file number or a pointer outside the program's memory is refused, not obeyed.
- Crashes stay contained. If a program faults, for example by reading memory at a misaligned address, the kernel records why, ends that program, and keeps running the others.
- Two targets. The same code builds for a VersatilePB board emulated by QEMU and for the real BeagleBone Black board (ARM Cortex-A8).
- Everything is logged. Every switch between a program and the kernel is printed with the reason, so you can watch the scheduler work.
| Process | What it does |
|---|---|
| Kernel | Boots, sets up memory protection, the timer and interrupts, then schedules everyone else. |
| OS process | Prints the alphabet, one letter at a time (----From OS: A, B, ...). Runs whenever no one else needs the CPU. |
| Process 1 | Tries two bad requests to prove the kernel rejects them ([A] Protections Verified!), then prints [B] tick 0 to [B] tick 19, giving up its turn after each one. |
| Process 2 | Deliberately breaks a rule to test fault handling. By default it reads memory at a misaligned address; set TEST in src/user/p2/process_2.c to try a permission fault, privilege escalation or a debug event instead. |
- Boot. The kernel starts in privileged mode, sets up the memory protection, turns off the board's watchdog, starts a hardware timer and enables interrupts.
- Launch. It loads the OS process and the two user programs, each into its own memory slice, and switches to the first one in unprivileged user mode.
- Take turns. Every timer tick drops back into the kernel, which saves the running program's registers, picks the next ready program, restores its registers and jumps back. To each program it looks like it never stopped.
- Serve requests. When a program makes a system call, the kernel checks the arguments, does the work and returns a result code.
- Handle crashes. When the processor reports a fault, the kernel identifies the kind of fault, marks that program as terminated, and moves on to the next one.
You need Linux, make, the ARM cross-compiler gcc-arm-none-eabi (13.2.1 tested) and qemu-system-arm. Tested on Ubuntu and Fedora.
make qemu # build and boot in QEMU (VersatilePB) make qemu-debug # same, waiting for a debugger on port 3333 make bbb # build for the BeagleBone Black make clean
Connect to the board's serial console at 115200 baud (with CoolTerm, minicom or similar), then in U-Boot:
loady 0x82000000
Send build/bin/os.bin over YMODEM, then start it with go 0x82000000.
After make qemu-debug, in another terminal:
gdb-multiarch build/bin/os.elf (gdb) target remote localhost:3333
Useful commands: layout regs, break <function>, continue.
Made by Carlos Alvarez (@23004004) and Gabriel Garcia (@GabGP). This is a fork of 23004004/001Multiprogramming.
| Output | Event |
|---|---|
! |
Kernel Panic |
. |
Yield Scheduling |
... |
Normal Scheduling |
MODE_SWITCH KERNEL_TO_USER pid=<first> reason=initial_launch |
Initial Boot Path (Kernel → User) |
MODE_SWITCH USER_TO_KERNEL pid=<n> reason=timer_irq |
Interrupt Path (User → Kernel) |
MODE_SWITCH KERNEL_TO_USER pid=<m> reason=dispatch |
Interrupt Path (Kernel → User) |
MODE_SWITCH USER_TO_KERNEL pid=<n> reason=syscall id=<id> |
Syscall Path (User → Kernel) |
MODE_SWITCH KERNEL_TO_USER pid=<m> reason=syscall_return id=<id> rc=<rc> |
Syscall Path (Kernel → User) |
MODE_SWITCH USER_TO_KERNEL pid=<n> reason=fault type=<type> |
Exception Path (User → Kernel) |
MODE_SWITCH KERNEL_TO_USER pid=<m> reason=fault_recovery |
Exception Path (Kernel → User) |
The scheduler enforces a Round-Robin scheduling policy. It manages CPU time allocation by monitoring a quantum and handling process transitions based on three main triggers:
- Quantum Expiry: When the
quantumreaches zero, the current process is preempted to allow another process to run. - Process Termination: If the current process enters the
PROCESS_TERMINATEDstate, a new process is immediately scheduled. - Voluntary Yield: A process can choose to give up its remaining time via the
SYS_YIELDsystem call.
If any of these conditions are met, the scheduler moves to the next process in the ready queue and resets the quantum. When idling, the OS process will continue running.
Each process is allocated a dedicated memory region starting at a base address determined by the platform and its PID. The entry point for each process is at the beginning of its allocated 1MB region.
| PID | Process Name | Memory Start Address (VersatilePB) | Memory Start Address (BeagleBone Black) |
|---|---|---|---|
| 0 | Kernel | 0x00000000 |
0x82000000 |
| 1 | OS Process | 0x00100000 |
0x82100000 |
| 2 | User Process 1 | 0x00200000 |
0x82200000 |
| 3 | User Process 2 | 0x00300000 |
0x82300000 |
| ... | ... | ... | ... |
The PCB stores the context and metadata for each process, ensuring that the system can save and restore its state during context switches.
| Variable | Description |
|---|---|
pid |
Unique numeric identifier for the process. |
state |
Current lifecycle state (e.g., READY, RUNNING, WAITING). |
regs[13] |
Array storing the general-purpose registers (R0 to R12). |
pc |
Program Counter; the address of the next instruction to be executed. |
sp |
Stack Pointer; points to the top of the process's private stack. |
lr |
Link Register; stores the return address for function calls and exceptions. |
spsr |
Saved Processor Status Register; used to restore the CPU mode and flags. |
syscall_id |
Stores the ID of the current or most recent system call. |
fault_type |
Records the type of hardware exception encountered by the process. |
termination_reason |
Indicates why a process ended (Normal exit, Syscall, or Fault). |
exit_code |
The status code returned to the kernel upon termination. |
The kernel manages the lifecycle of each task through a set of defined states stored in the PCB.
| State | Value | Description |
|---|---|---|
PROCESS_NEW |
0 | The process is being created and its PCB is being initialized. |
PROCESS_READY |
1 | The process is waiting in the ready_queue to be assigned to the CPU. |
PROCESS_RUNNING |
2 | The process instructions are currently being executed by the processor. |
PROCESS_WAITING |
3 | The process is waiting for an event to occur. |
PROCESS_SUSPENDED |
4 | The process execution is paused but the context is preserved in memory. |
PROCESS_TERMINATED |
5 | The process has completed execution or has been aborted due to a fault. |
The system uses registers R0-R3 for system call interfacing. R0 is used to pass the Syscall ID and also stores the return code upon completion.
| Syscall Name | ID (R0) | Arg 1 (R1) | Arg 2 (R2) | Arg 3 (R3) | Return Code (R0) | Description |
|---|---|---|---|---|---|---|
SYS_YIELD |
1 | - | - | - | RC_SUCCES |
Voluntarily yield CPU to the next process. |
SYS_EXIT |
2 | Exit Code | - | - | Exit Code | Terminate the current process with an exit code. |
SYS_WRITE |
3 | File Descriptor | Buffer | Length | Bytes / Error | Write string to UART (FD 1 supported). |
| Return Code | Value | Description |
|---|---|---|
RC_SUCCESS |
0 | Operation completed successfully. |
RC_INVALID_SYSCALL |
-1 | The requested Syscall ID is not recognized. |
RC_INVALID_ARGUMENT |
-2 | Provided arguments are invalid. (FD != 1or 0 > Length > 256) |
RC_INVALID_USR_PTR |
-3 | Memory access violation (Buffer outside process boundary). |
RC_INVALID_ORIGIN |
-4 | Invalid syscall origin (Triggered by kernel). |
If a process requests an unknown Syscall ID, the kernel will immediately transition the process to the PROCESS_TERMINATED state, set the return register to RC_INVALID_SYSCALL, and trigger the scheduler to pick the next available task.
If a process triggers a syscall while on kernel mode, then the same procedure as unknown system behavior follows, except that the return code is set to RC_INVALID_ORIGIN. This is because syscalls are meant to be triggered while on user mode, and if someone with kernel privileges triggers it, then it means something went wrong.
When a hardware exception or processor fault occurs, the fault_dispatcher categorizes the error and enforces a system safety policy.
| Fault Classification / Type | Value | Outcome | Comment |
|---|---|---|---|
FAULT_NONE |
0 | - | Default Value |
FAULT_ALIGNMENT_ERROR |
1 | Terminate | MMU |
FAULT_ACCESS_FLAG |
2 | Terminate | MMU |
FAULT_INVALID_MAPPING |
3 | Terminate | Translation Fault / MMU |
FAULT_PRIV_VIOLATION |
4 | Terminate | Domain Fault / MMU |
FAULT_PERMISSION |
5 | Terminate | MMU |
FAULT_SYNC_EXT_ABORT |
6 | Terminate | Synchronous External Abort |
FAULT_ASYNC_EXT_ABORT |
7 | Terminate | Asynchronous External Abort |
FAULT_TTB_WALK_SEA |
8 | Terminate | Translation Table Walk (Sync External Abort) |
FAULT_TTB_WALK_SPE |
9 | Terminate | Translation Table Walk (Sync Parity Error) |
FAULT_MEM_ACCESS_SPE |
10 | Terminate | Memory Access (Synchronous Parity Error) |
FAULT_MEM_ACCESS_APE |
11 | Terminate | Memory Access (Asynchronous Parity Error) |
FAULT_DEBUG_EVENT |
12 | Terminate | - |
FAULT_INST_CACHE_MAINT |
13 | Terminate | Instruction Cache Maintenance |
FAULT_IMP_DEF_LD |
14 | Terminate | Implementation Defined (Lockdown) |
FAULT_IMP_DEF_CA |
15 | Terminate | Implementation Defined (Coprocessor Abort) |
FAULT_UNKNOWN |
-1 | Terminate | - |
FAULT_UND_INST |
-2 | Terminate | Undefined Instruction |
Recovery Strategy
The current kernel policy is Fail-Stop for individual processes. If any fault is detected:
- The current process is marked as
PROCESS_TERMINATED. - The
fault_typeandtermination_reasonare recorded in the process's PCB. - The scheduler swaps out the current process and selects the next
PROCESS_READYprocess. - The system continues running other processes.
Notes:
- Test on BeagleBone Black, otherwise the exceptions might not trigger on qemu.
- MMU faults require the MMU to be enabled and set up.
| Termination Reason | Value | Description |
|---|---|---|
EXIT_NORMAL |
0 | Process terminated normally. Default Value. |
EXIT_SYSCALL |
1 | Process terminated after a syscall (Successful or Failed). |
EXIT_FAULT |
2 | Process terminated after a fault. |
