TranquilOS
No matching sections — try another keyword.

Project Overview

TranquilOS is an object-capability microkernel operating system for AArch64. Capabilities are the single authorization model: process management, memory management, the IPC namespace, device abstraction, filesystems, the network stack, window composition and application lifecycle are decomposed into clearly bounded userspace services, while the EL1 microkernel focuses on scheduling, IPC, exceptions, interrupts, timers, futexes, base drivers and capability object dispatch.

Inspired by the object-capability model of seL4 and others, the project validates the systematic composition of "microkernel + capability authorization + userspace services" on AArch64: how kernel objects are explicitly authorized, how services discover and communicate with each other, how a graphical desktop runs as ordinary userspace components, and how the build system assembles the pieces into a bootable image.

Full design documents live in the kernel repository's docs/ directory: basic_theory.md (AArch64 architecture, MMU, context switching, drivers, capabilities, IPC, scheduler), microkernel_design.md (microkernel design), idl.md (interface definition and code generation), async_ipc_design.md.

Quick Start

Prerequisites: download a cross toolchain from newos.org/toolchains and extract it into the repo's toolchains/ directory; on macOS run brew install e2fsprogs (needed to assemble the ext2 system.img). env_setup.sh must be sourced in every new shell — its OS_BASE_DIR is hardcoded to ~/kernel, so edit it first if you checked out elsewhere.

# QemuVirt (primary dev platform): build + assemble images + launch QEMU
source env_setup.sh && ./run_qemu_virt.sh

# manual build
source env_setup.sh
gn gen out --args='platform="QemuVirt"'
ninja -C out

# single-target build
ninja -C out Windowmgr

QEMU configuration: 4-core SMP, 2 GB RAM, HVF acceleration on macOS, ramfb display, VirtIO tablet/keyboard/network/block/sound. Other platforms: run_qemu_pi3b.sh (headless serial), run_qemu_pi4b.sh (SD card image), run_board_cm4.sh (real hardware, flashed via rpiboot).

Repository Layout

sys/             system foundation (TCB)
  kernel/        microkernel (EL1)
  virt/          Type-1 hypervisor (EL2)
  boot/          bootloader
  trustee/       trusted execution environment (stub)
  uapps/         bootstrap & core services: base/ init, idle; core/ devmgr, fsmgr
  ulibs/         libc, libcrt, libsyscall, libsystem, libsync, libfdt, libalgorithm
os/              OS userspace
  base/          zygote, netmgr
  framework/     windowmgr, mmimgr, appmgr, audiomgr, fontmgr, statemgr, ...
  apps/          user applications
  libs/          libwindow, libgraphics, libgl, liblvgl, liblwip, ...
  thrid_party/   lvgl, freetype, mbedtls, lwip, musl, ... (upstream sources)
platform/        platform configs: QemuVirt / Pi3b / Pi4b / CM4 (DTBs, linker scripts, build/run scripts)
build/           GN build system & toolchain definitions
images/          ramdisk (cpio) and system image templates
docs/            design documents

Third-party convention: never modify upstream sources directly. Platform adaptation and compatibility wrappers live in os/libs/ and sys/ulibs/; when upgrading a third-party library, replace the whole directory and re-validate the adaptation layer.

Boot Chain & Privilege Model

TranquilOS uses ARMv8-A exception levels as architectural boundaries: EL2 runs an optional Type-1 hypervisor, EL1 is the microkernel, and EL0 carries SystemDaemon, framework services and applications. The bootloader picks the boot path based on the CPU's CurrentEL:

booting from EL2: bootloader → hypervisor → kernel → systemd
booting from EL1: bootloader → kernel → systemd

The hypervisor provides stage-2 translation, VM/VCPU/PCPU lifecycle management, vGIC, vTimer, vPMU and the hypcall interface; VMs and the kernel are packed into boot.img together.

Userspace bring-up order: init (the first userspace process) finds the ramdisk via the device tree, starts devmgr and fsmgr, then runs /root/etc/init.rc; zygote reads init.json, loads service ELFs from ext2 into SHM and calls start_process_from_shm() to start framework services; appmgr manages the app registry from applist.json and launches apps on demand.

Capability System

The capability is the unified management structure for kernel objects and fine-grained rights, and the unified way userspace interacts with the microkernel. Every kernel operation reduces to "invoke a method on a kernel object": OS_{Type}{Method}(ref, args...) — so rights can be fine-grained down to each object and each method.

Each process owns a CNode storing all of its capabilities. The CNode is built on a directory array: array-speed indexing with dynamic-array growth, expanding without copying and preserving linear indexing. A capability reference (cref) is 64 bits: upper 32 bits cnode ID, lower 32 bits slot index; the root CNode has ID 1. The kernel validates object type and a 32-bit rights mask before dispatching any method.

Kernel object types include: VSpace (virtual address space), CNode (capability storage), SContext / XContext (schedule / execute state), IPC Endpoint / Pool, Timer, Futex, IRQ, Console.

Memory Management

The kernel keeps only a boot allocator, serving its own bring-up and SystemDaemon startup; all memory policy lives in userspace. SystemDaemon's memmgr manages physical pages with a buddy allocator, plus process virtual address spaces and SHM.

On the kernel side, the AArch64 MMU is fully configured: MAIR/TCR, block/table descriptors, identity mapping, ASIDs and TLB management. A VSpace object represents a process address space and is operated through capability calls.

IPC Model

Traditional inter-process synchronization (sockets, pipes, shared kernel buffers) responds only when the scheduler runs the server thread — latency is nondeterministic. TranquilOS provides immediate control-flow switching with migrating-thread IPC: when a client issues a synchronous call, the kernel installs the server's address space and a constructed execute_context_s onto the CPU — arguments go into x0–x7 per the AAPCS, the handler address into pc; lr holds a trampoline address, and when the handler returns the trampoline issues a REPLY call to switch back to the client. Scheduling metadata stays under the scheduler's control throughout, so IPC never compromises scheduling fairness.

  • IPC Endpoint: single-threaded — one handler thread per endpoint.
  • Endpoint Pool: load-balanced across pre-allocated endpoints for multi-threaded services; elastic IPC is built on top.
  • Upcall: the reverse, kernel → userspace path, used for syscall forwarding, page faults and similar events.
  • Name service: kernel endpoint cref 0x1. Services register via sys_register_service() / sys_register_service_pool(); clients look them up via sys_get_service() / sys_get_service_pool().

Scheduler

The traditional TCB is split into an execution entity execute_context_s (register state: x0–x30, sp, pc, pstate) and a scheduling entity schedule_context_s (priority, timeslice, queue node, CPU affinity). Per-CPU schedulers only manipulate scheduling metadata; execution state is constructed/restored on real context switches — which also makes migrating-thread IPC much simpler.

Under SMP, each CPU runs an independent local scheduler with cross-core load balancing and preemption. The CFS scheduler class is implemented as a loadable kernel module (sys/kernel/module/), proving the module system on a real use case.

Syscall Paths

The syscall number in x8 selects one of three dispatch paths by flag bit:

PathFlagPurpose
Capability callCAP_CALL_MASKRouted to capability objects: VSpace, CNode, IPC endpoint, Timer, etc. — the capcall is the microkernel's primary syscall
Fast callFAST_CALL_MASKHigh-performance kernel-inline calls (currently a stub)
Compact syscallLinux-compatible table (only getpid so far); everything else is forwarded via upcall to the process-registered userspace handler (dispatched through the libsyscall function table)

Initcall Initialization

Init functions are placed into dedicated ELF sections by macros; the linker script defines start/end symbols per level, and initcall_run(level) executes them in LV0 → LV7 order:

LVMacroPurpose
0early_device_initEarliest device init
1early_device_percpu_initPer-CPU early devices
2key_device_initKey devices
3key_device_percpu_initPer-CPU key devices
4normal_device_initNormal devices
5normal_device_percpu_initPer-CPU normal devices
6module_initKernel modules
7module_percpu_initPer-CPU module init

SystemDaemon

SystemDaemon runs at EL0 as the most privileged userspace process, linked at 0x43080000, issuing capcalls directly (libkernel/capcall.h) instead of going through libsyscall. It takes over the high-level resource management that monolithic kernels keep in-kernel, with each component managed as a singleton to constrain interface exposure:

  • procmgr: process / thread lifecycle, ELF loading (spawn, exit, PID lookup)
  • memmgr: physical page allocation, SHM management
  • ipcmgr: IPC endpoint / pool creation, name service, capability cloning
  • virtmgr: virtual machine management

It exposes 17 IPC methods via systemd_client.c, with data passing over SHM. Core services such as vfs and devmgr are also brought up by it.

Service Skeleton

Every OS service (os/framework/* and sys/uapps/core/*) follows the same structure:

main.c            init, register the service over IPC, event loop
service/service.c IPC handler dispatch table indexed by method number
client/           thin IPC client wrapper using SHM for data passing
include/          internal service headers
client/include/   public client API headers

The dispatch pattern is identical across services: a handler function-pointer table indexed by method number; service_entry validates the method number, calls the handler and replies with OSIpcEndPointPoolReply(). At startup the service registers with sys_register_service_pool(IPC_SERVICE_ID_*, &service_entry).

SHM Data Passing

All data transfer between processes uses shared memory — it is the only data channel; there is no message-copying IPC. The universal flow:

  • The client allocates SHM via systemd->ops.alloc_shm()
  • Maps it and copies data in
  • Issues the IPC call, passing the SHM handle as an argument
  • The service maps the same SHM via systemd->ops.get_shm()
  • Processes the data in place (zero copy)
  • Replies; the client frees the SHM

Service Directory

ServiceResponsibility
appmgrApplication lifecycle: registry, on-demand launch
windowmgrWindow compositor & surface management, input dispatch
statemgrSystem-wide key-value state store
devmgrDevice driver framework & peripheral management
fsmgrFilesystem abstraction: rootfs, ext2, FAT32, procfs, sysfs
netmgrlwIP TCP/IP network stack
mmimgrMulti-modal input management
audiomgrAudio service
fontmgrFreeType font rendering
agentmgrAI agent framework
timemgrTimekeeping, RTC alarms, NTP sync
imemgrInput method editor

IDL & Code Generation

Services describe their synchronous IPC interfaces in .idl files. The generator (tools/idl_gen.py, usually run automatically by a GN build target) emits five files: <service>_types.h, <service>_client.h/.c and <service>_service.h/.c.

service statemgr_ipc {
    id = 0x9;

    struct query  { version = 1; char key[64]; };
    struct result { version = 1; uint32_t found; };

    uint64_t get(in query *request,
                 out result *response) id = 0x1;
};

Parameter direction controls copying in the generated code: in copies caller data into SHM before the IPC; out zeroes the SHM before and copies results back after; inout does both (the default for unannotated struct pointers). Scalars travel directly in IPC argument slots — at most three parameters per method.

Server receive buffers: struct pointers use fixed buffers owned by the server. The first call establishes SHM authorization per caller, method and parameter slot; the client keeps the connection-level CRef and mapped address for later calls. A per-method lock covers input copying, IPC and output copying. Variable-length or caller-owned bulk data still passes an explicit SHM handle and length.

Transport & mapping: by default, IPC goes through an endpoint pool and SHM is mapped via SystemDaemon; service attributes can switch this: transport = endpoint, mapping = direct.

ABI rules: transport structs must declare version > 0; the generator adds abi_size / abi_version fields that the server validates before invoking the handler. Transport structs may not contain pointer fields (pointers are only valid in their own address space). Full grammar: tools/idl_ref/tranquil_idl.ebnf.

GN Build Targets

Target typeUse
executable()ELFs: kernel, SystemDaemon, framework services, apps
source_set()Libraries compiled into each consumer (no standalone .a)
group()Meta-target bundling dependencies (e.g. uapp_runtime)
config()Compiler/linker flag bundles applied via configs +=

Every executable links a platform-specific linker script (platform/$PLATFORM/linker/*.lds); userspace executables use libcrt/uapp.lds and must list libcrt/start.S (which provides _start) as their first source. Common dependency groups: //sys/ulibs:uapp_runtime, uapp_runtime_with_syscall, //sys/ulibs/libsystem:systemd_client.

Platforms & Images

PlatformRoleNotes
QemuVirtPrimary dev platformMost complete device set (ramfb, full VirtIO); validate kernel changes here first
Pi3B (QEMU)Platform validation-nographic serial, headless
Pi4B (QEMU)Platform validationMBR-partitioned SD card image (mkpiimg.sh)
CM4Real hardwareFlashed over USB via rpiboot; requires a carrier board
boot.img (64MB)
  offset 0      Bootloader    8MB     system.img (128MB, ext2)
  offset 2048   DTB           8MB       bin/   framework services
  offset 4096   Hypervisor   16MB       apps/  user applications
  offset 8192   Kernel       16MB       etc/   fonts · icons · init config
  offset 12288  SystemDaemon  8MB
  offset 14336  Ramdisk cpio  8MB