gVisor - Scheduler

Read more: Linux - Scheduler.

gVisor implements its own internal scheduler to manage the application processes running inside its sandbox.

However, it is a two-tier scheduling system. To understand how gVisor handles scheduling, you have to look at the relationship between the sandboxed application, the gVisor "Sentry," and the host Linux kernel.

The Two-Tier Architecture

Because gVisor is a "user-space kernel," it sits between the application and the actual host operating system.

  • Tier 1: The Host Scheduler (Linux): The host Linux kernel sees gVisor (the Sentry process) as a standard multi-threaded application. The host scheduler (CFS/EEVDF) decides when the Sentry's threads get to run on the physical CPUs.
  • Tier 2: The gVisor Scheduler (Internal): Inside the sandbox, the Sentry must decide which application thread (the "Guest" threads) should run on the available "Host" threads.

How gVisor’s Internal Task Model Works

gVisor is written in Go, and its internal concurrency architecture leverages both the Go Runtime Scheduler and host thread pools:

  • Tasks Driven by Goroutines: In the Sentry (pkg/sentry/kernel), every thread in the sandboxed application is tracked as a kernel.Task. Each Task runs a dedicated Go goroutine executing its lifecycle loop (Task.run()).
  • Executing Guest Code via LockOSThread(): An application thread cannot execute arbitrary external machine code directly as a standard goroutine. When a Task is ready to run user-space code, the Sentry locks the current goroutine to a host OS thread (runtime.LockOSThread()) and uses the Platform engine (Systrap or KVM) to switch CPU context into the guest.
  • Handling Blocking Syscalls & Sleeping: When the application traps out to perform a system call or blocks on a guest resource (such as an emulated futex, timer, pipe, or network socket), control returns to the Sentry goroutine. The Sentry can park the goroutine using internal wait queues (waiter.Queue) and Go runtime channels, freeing the underlying OS thread for other tasks.

Why does gVisor need its own scheduler?

If the host Linux kernel already has a great scheduler, why does gVisor need its own task abstraction?

  1. Isolation (The Primary Goal): gVisor’s purpose is to provide a security boundary. Application threads never directly interact with host scheduling queues or host kernel thread descriptors.
  2. Platform Abstraction: gVisor abstracts hardware and kernel virtualization across different platforms (Systrap and KVM). By managing task states internally, the Sentry decides when to switch contexts into guest execution.
  3. Linux Emulation: gVisor must emulate Linux signal delivery, thread groups, process relationships, and futexes (fast user-space mutexes). To ensure identical semantics to native Linux, gVisor controls task state transitions, runnable queues, and signal masks internally.

The Performance Trade-off

This internal scheduling creates a trade-off compared to standard containers (like Docker/runc):

  • The "Double Scheduling" Penalty: Sometimes the gVisor scheduler is ready to run a task, but the host Linux scheduler has de-scheduled the host OS thread that the Sentry is using, causing slight scheduling latency.
  • Context Transition Overhead: Every time a sandboxed application makes an unhandled system call, the CPU must switch between guest execution and Sentry user-space Go code before returning.

Summary

In gVisor, each sandboxed application thread (kernel.Task) is managed by a Go goroutine for state transitions and syscall handling. To run guest machine code, it binds to a host thread and switches CPU context via the Platform engine (Systrap or KVM). When blocking on guest events, it parks in user space, cleanly separating guest execution from host kernel scheduling.

Loading...