Skip to content

Core Thread Scheduling

opencode-agent[bot] edited this page May 10, 2026 · 2 revisions

Core - Thread Scheduling

JNode's thread scheduler uses cooperative yieldpoints inserted by the JIT compiler and preemptive timer interrupts to switch between runnable threads.

Overview

JNode implements a hybrid scheduling model on x86 that combines efficient cooperative scheduling with fair preemptive time-slicing. The scheduler is built around three core classes: VmThread (the schedulable unit), VmProcessor (the CPU core abstraction), and VmScheduler (the queue manager). The system uses a thread-switch indicator (TSI) pattern to control when switches can occur, and the @Uninterruptible annotation to protect critical sections from being interrupted.

The scheduler lives entirely in core/src/core/org/jnode/vm/scheduler/. There is one VmProcessor per physical CPU, each holding a reference to the current running thread and the next thread to run. The assembly layer (vm-ints.asm) handles hardware timer interrupts and yieldpoint traps (INT 0x30).

Key Components

Class / File Role
core/src/core/org/jnode/vm/scheduler/VmThread.java Schedulable unit with thread state, priority, queue entries
core/src/core/org/jnode/vm/scheduler/VmProcessor.java CPU core abstraction, manages currentThread/nextThread, TSI flags
core/src/core/org/jnode/vm/scheduler/VmScheduler.java Manages readyQueue, sleepQueue, allThreadsQueue; controls scheduling policy
core/src/core/org/jnode/vm/scheduler/VmThreadQueue.java Priority-sorted run queue; also used for wait/sleep queues
core/src/core/org/jnode/vm/scheduler/VmThreadQueueEntry.java Linked list entry embedded in VmThread for queue placement
core/src/core/org/jnode/vm/scheduler/IdleThread.java Idle loop thread, runs when no other thread is runnable
core/src/core/org/jnode/vm/scheduler/Monitor.java Object monitor with enter/exit; interacts with @Uninterruptible
core/src/core/org/jnode/vm/scheduler/IRQManager.java IRQ dispatch; timer interrupt drives preemptive scheduling
core/src/core/org/jnode/vm/scheduler/IRQThread.java Per-IRQ thread at MAX_PRIORITY that calls scheduler on interrupt
core/src/native/x86/vm-ints.asm Timer interrupt handler, yieldpoint INT 0x30 handler
core/src/vmmagic/org/vmmagic/pragma/Uninterruptible.java Interface marker that suppresses yieldpoint injection
core/src/vmmagic/org/vmmagic/pragma/UninterruptiblePragma.java Exception class; methods with this in throws also skip yieldpoints

How It Works

Thread States

VmThread manages these states (defined as int constants):

  • CREATED (0) — constructed but not yet started
  • RUNNING (1) — currently executing on a processor
  • SUSPENDED (2) — voluntarily paused by suspend()
  • ASLEEP (3) — waiting on a timer via sleep(nanoseconds)
  • STOPPED (4) — terminated by stop()
  • DESTROYED (5) — fully cleaned up
  • WAITING_ENTER (6) — blocked on a monitor
  • WAITING_NOTIFY (7) — in wait() without a timeout
  • WAITING_NOTIFY_TIMEOUT (8) — in wait(timeout)
  • YIELDING (9) — voluntarily yielded, waiting to be rescheduled

Thread Switch Indicator (TSI)

VmProcessor uses a 4-bit TSI word to coordinate scheduling state:

static final int TSI_SWITCH_NEEDED = 0x0001;   // Set by timer interrupt / yieldpoint
static final int TSI_SYSTEM_READY   = 0x0002;   // Set after VM init completes
static final int TSI_SWITCH_ACTIVE  = 0x0004;   // Context switch in progress
static final int TSI_BLOCK_SWITCH   = 0x0008;   // Lock held; yieldpoints suppressed

TSI_SWITCH_REQUESTED is defined as TSI_SWITCH_NEEDED | TSI_SYSTEM_READY. A context switch only proceeds when threadSwitchIndicator & TSI_SWITCH_REQUESTED == TSI_SWITCH_REQUESTED && !isSwitchActive().

TSI_BLOCK_SWITCH is set whenever VmProcessor.lockCount > 0, which happens around critical sections like monitor acquisition. This forces cooperative yielding to wait.

Cooperative Scheduling — Yieldpoints

The JIT compilers (L1 and L2) inject yieldpoints at method prologues and conditional branches. When a yieldpoint fires, the sequence is:

  1. The compiled code executes INT 0x30 (the yieldpoint interrupt).
  2. Assembly handler yieldPointHandler in vm-ints.asm saves thread state (registers, FPU, segment registers, model-specific registers).
  3. It sets TSI_SWITCH_ACTIVE and calls VmProcessor.reschedule().
  4. Java code in VmProcessor.reschedule() dequeues the next ready thread from the VmScheduler's priority queue.
  5. The handler restores the new thread's state and returns via IRET.

Methods marked with @Uninterruptible (or declaring UninterruptiblePragma in their throws clause) have yieldpoints suppressed by the compiler. This means they run to completion atomically without yielding.

Preemptive Scheduling — Timer Interrupts

The hardware timer interrupt fires roughly every 8ms and is handled by timer_handler in vm-ints.asm:

  1. The handler sets TSI_SWITCH_NEEDED on all processors via broadcastTimeSliceInterrupt().
  2. IRQThread dispatches the timer IRQ, waking any sleeping threads whose wakeupTime has passed.
  3. On the next yieldpoint or timer interrupt, reschedule() will switch to a different thread.

A deadlock detector increments a counter on each timer tick and panics the system at 0x4000 iterations without a switch — an indication that a thread has been holding a lock too long without yielding.

Reschedule Flow

reschedule()
  1. Mark current thread (if runnable) as YIELDING, push to readyQueue
  2. Wake sleeping threads: pop all from sleepQueue whose wakeupTime <= now
  3. Pop the highest-priority thread from readyQueue as nextThread
  4. Call wakeUpByScheduler() on nextThread → sets state to RUNNING
  5. Swap context: currentThread = nextThread
  6. Assembly layer restores next thread's registers and executes IRET

Priority Queue

The VmThreadQueue is a priority-sorted run queue. Threads are ordered first by priority (lower number = higher priority), then by insertion order (FIFO within the same priority). The scheduler uses insertByPriority() and popFirst() as its core operations.

@Uninterruptible in Scheduling

The @Uninterruptible interface (from org.vmmagic.pragma) is the primary mechanism to opt out of scheduling:

  • Compiler effect: Both L1 and L2 JIT compilers check for this interface. Methods implementing it (or declaring UninterruptiblePragma in throws) receive no yieldpoint injections.
  • Runtime effect: The disableReschedule() / enableReschedule() pair increments/decrements VmProcessor.lockCount. When lockCount > 0, TSI_BLOCK_SWITCH is set, causing yieldpoints to be deferred.
  • Purpose: Critical sections — particularly monitor enter/exit, SpinLock, and ProcessorLock operations — must complete atomically. If a thread could yield while holding a lock, deadlock or data corruption could result.
  • LogicallyUninterruptiblePragma: Used in rare cases where uninterruptibility is guaranteed by external means (e.g., interrupts are disabled). Marks intent without generating code.

Gotchas & Non-Obvious Behavior

  • Priority 0 is MAX: JNode uses lower numbers for higher priority. Priority 0 (Thread.MAX_PRIORITY) is the highest. Priority 10 is the lowest. The idle thread runs at priority 10.
  • Idle thread starvation: The idle thread only runs when the ready queue is empty. On a single-processor system, this means the idle thread loops when no other thread is runnable.
  • Yieldpoints are everywhere: In normal compiled code, yieldpoints are injected at almost every branch. This makes the system highly cooperative — a busy-waiting loop will yield frequently. This is intentional and efficient for I/O-bound workloads.
  • Timer interrupt is broadcast: broadcastTimeSliceInterrupt() sets TSI_SWITCH_NEEDED on every VmProcessor, not just the current one. This ensures all CPUs participate in preemption.
  • @Uninterruptible is an interface, not an annotation: The org.vmmagic.pragma.Uninterruptible is a marker interface. The compiler recognizes it and skips yieldpoint injection. This is distinct from org.jnode.annotation.Uninterruptible, which is a source-level annotation for documentation.
  • Thread state is not a lock: Setting a thread to YIELDING or SUSPENDED does not prevent a timer interrupt from setting TSI_SWITCH_NEEDED. The actual switch is deferred until reschedule() is called.
  • yieldPoint() is not a method call: The assembly code INT 0x30 is a software interrupt. The handler runs in kernel context before returning to Java. Unsafe.yieldPoint() is a native stub that triggers this interrupt.

Related Pages

  • Architecture — High-level subsystem overview
  • TSI — Thread Switch Indicator: atomic flags, assembly manipulation, state transitions
  • VM-Magic — Unboxed types, @Uninterruptible pragma, Unsafe
  • Boot-Sequence — Where the scheduler is initialized at boot
  • Memory-Management — Stack allocation for new threads
  • Glossary — VmProcessor, VmThread definitions

Clone this wiki locally