Unikraft guests on Hyperlight run on a single vCPU with a cooperative scheduler. This page covers what that means for threads and multiprocess.
┌─────────────────────────────────┐
│ Single vCPU │
│ │
│ Thread A ──yield──→ Thread B │
│ Thread B ──yield──→ Thread A │
│ │
│ (no preemption, one core) │
└─────────────────────────────────┘
- One vCPU. The Hyperlight platform boots a single logical CPU. There is no multi-core execution.
- Cooperative scheduling. The kernel uses Unikraft's
ukschedcoop— a non-preemptive round-robin scheduler. A thread runs until it explicitly yields, blocks on I/O, or exits. It is never interrupted by a timer tick. Hyperlight does support hardware interrupt injection (a paravirtualised timer that delivers vector 0x20 at a configurable rate), which could enable preemptive scheduling in the future. - Single address space. All threads share one address space. There is no process isolation in the traditional sense.
For a full description of Unikraft's process model and the syscalls available at each feature level, see the upstream posix-process README.
Between host entries the guest is never running: when every guest thread is blocked the kernel hands the vCPU back to the host with the time until its next timer, and the host parks (VM halted, in poll(2) on the guest's host sockets) until the guest could make progress. An idle guest costs no CPU; a blocking accept() or sleep() in the guest is served by the host waking it. Named calls (Exec) run on the runtime driver's own thread, and a guest whose entry point is a plain program is driven with [AppSandbox::join] instead. How the host drives a guest, the kinds of guest, run/submit/step/join, snapshots and restore are in execution.md; what a restore does to the clocks and the random source is in clock.md and random.md.
Threading works — create, join, locks, events, and thread pools all function correctly. A call returns when its main code returns; a thread it started, daemon or not, is left behind and keeps making progress on the steps that follow. asyncio works too: its event loop's self-pipe is an AF_UNIX socketpair(), served by the kernel's posix-unixsocket (see examples/python/asyncio_demo.py).
A CPU-bound thread that never yields will starve all other threads because there is no preemption.
Unikraft supports vfork()+execve(), which runtimes use to spawn child processes. Python 3.12's subprocess.run() uses this path internally via posix_spawn().
vfork()— creates a child sharing the parent's address space. The parent suspends until the child callsexecve()or_exit().execve()— loads a new ELF binary (must be PIE) into the address space via the elfloader's binfmt handler. The child gets a new stack and begins executing the new program.- The parent resumes and can read the child's stdout/stderr.
This is evidenced by the examples/python/subprocess_demo.py example, which spawns Python child processes for computation, stdin/stdout piping, module execution, and error handling.
os.fork()— returnsENOTSUP.clone()withoutCLONE_VMis rejected. This means:multiprocessing.Process(usesforkstart method on Linux)os.popen()- Any library that calls
fork()directly
- Parallel children —
vfork()suspends the parent, so children run sequentially.
examples/python/threading_demo.py— threads with locks and shared stateexamples/python/threaded_select.py— threaded server withselect()examples/python/subprocess_demo.py— spawning child processes viasubprocess.run()examples/python/tcp_echo.py— TCP echo server/client