Skip to content

Interrupt Handling

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

Core - Interrupt-Handling

JNode routes hardware interrupts from x86 CPU exceptions and external devices through assembly handlers into Java-level IRQ threads managed by the scheduler.

Overview

JNode handles two distinct categories of interrupts: CPU exceptions (traps and faults from the processor) and hardware IRQs (external devices like timers, keyboards, and disk controllers). Both travel through the x86 Interrupt Descriptor Table (IDT), but they take different paths to reach Java code.

The IDT is set up in ints.asm using the intport macro. Hardware IRQs are routed through the 8259A PIC (or Local APIC on newer systems) and dispatched by IRQManager to registered Java IRQHandler implementations. CPU exceptions (division by zero, page faults, etc.) are caught in ints.asm and converted to Java exceptions via VmThread exception constants.

Key Components

Class / File Role
core/src/native/x86/ints.asm IDT construction, CPU exception handlers, PIC initialization
core/src/native/x86/vm-ints.asm IRQ handlers, yieldpoint/trap handlers, FPU not-available handler
core/src/core/org/jnode/vm/x86/X86IRQManager.java x86-specific IRQ dispatch with PIC/APIC EOI support
core/src/core/org/jnode/vm/scheduler/IRQManager.java IRQ claim/release, interrupt dispatch to IRQ threads
core/src/core/org/jnode/vm/scheduler/IRQThread.java Per-IRQ high-priority thread that runs IRQHandler.handleInterrupt()
core/src/core/org/jnode/system/resource/IRQHandler.java Interface implemented by device drivers to handle IRQs
core/src/core/org/jnode/vm/x86/PIC8259A.java 8259A PIC wrapper for legacy IRQ routing and EOI
core/src/core/org/jnode/vm/x86/LocalAPIC.java Local APIC for SMP and modern IRQ routing

How It Works

IDT Setup

The IDT is constructed in ints.asm via the Lsetup_idt function. Each entry is created using the intport macro, which defines the gate type (trap vs interrupt) and privilege level. The key entries are:

; CPU exceptions
int_noerror int_div, 0          ; Division by zero
int_error   int_pf, 14          ; Page fault
; Hardware IRQs (remapped to 0x20-0x2F)
int_irq 0, timer_handler        ; IRQ0 = timer
int_irq 1, def_irq_handler     ; keyboard
; ...
; Software interrupts
intport 0x30, yieldPointHandler, 3 ; Yieldpoint (INT 0x30)
intport 0x31, int_stack_overflow, 3
intport 0x33, timesliceHandler, 3  ; Timeslice (INT 0x33)

The PIC is reprogrammed to remap IRQs 0-15 to vectors 0x20-0x2F, so they don't conflict with CPU exceptions (vectors 0-31).

CPU Exception Handling

When a CPU exception occurs in user mode, the handler checks the CS register to distinguish kernel vs user mode. In user mode, it uses the SYSTEM_EXCEPTION macro to save the exception state and redirect execution to doSystemException, which calls SoftByteCodes.systemException to throw a Java exception:

int_div:
    cmp GET_OLD_CS, USER_CS
    jne int_die
    SYSTEM_EXCEPTION VmThread_EX_DIV0, GET_OLD_EIP
    ret

Kernel-mode exceptions and unimplemented exceptions jump to int_die, which prints a message and halts.

Hardware IRQ Flow

The IRQ handling chain is:

Hardware interrupt (PIC/APIC) 
  → IDT gate → irq0-15 handler in ints.asm
  → def_irq_handler / timer_handler in vm-ints.asm
  → Set TSI_SWITCH_NEEDED, increment IRQ counter
  → Return to Java context
  
Later (on yieldpoint/trap):
  → VmProcessor.reschedule() is called
  → VmScheduler calls IRQManager.dispatchInterrupts()
  → For each IRQ with a registered handler:
       → IRQThread.signalIRQ() is called
       → IRQThread wakes up at MAX_PRIORITY
       → IRQThread.run() calls IRQHandler.handleInterrupt(irq)
       → EOI is sent to PIC/APIC

IRQThread and Handler Dispatch

IRQManager.claimIRQ() registers a handler and starts an IRQThread:

IRQThread newThread = new IRQThread(this, irq, owner, handler, shared, defaultIrqProcessor);
handlers[irq] = newThread;
newThread.start();

Each IRQThread runs at Thread.MAX_PRIORITY and waits until signalIRQ() is called. The dispatch cycle:

  1. IRQManager.dispatchInterrupts() is called by the scheduler (at yieldpoint)
  2. It iterates all IRQs, calling IRQThread.signalIRQ(irqCount[irq], current)
  3. signalIRQ() increments irqCount and calls vmThread.unsecureResume() if needed
  4. The IRQThread wakes up and calls handleInterrupt(irq) for all registered handlers
  5. After handling, IRQManager.eoi(irq) sends End-Of-Interrupt to the PIC/APIC

PIC and APIC EOI

The 8259A PIC requires an EOI command after each IRQ:

// Master IRQ
io8259_A.outPortByte(0x20, 0x60 + irq);
// Slave IRQ (8-15)
io8259_B.outPortByte(0xA0, 0x60 + irq - 8);
io8259_A.outPortByte(0x20, 0x60 + 2); // Cascade EOI

Local APIC uses a memory-mapped write:

mem.setInt(REG_EOI, 0);

Yieldpoint and Timeslice Interrupts

The INT 0x30 and INT 0x33 software interrupts are used for scheduling:

  • INT 0x30 (yieldpoint): Injected by JIT at method prologues and branches. Handler saves thread state and calls VmProcessor.reschedule().
  • INT 0x33 (timeslice): Broadcast to all CPUs via Local APIC IPI. Sets TSI_SWITCH_NEEDED to trigger preemption.

Both are defined in ints.asm:

intport 0x30, yieldPointHandler, 3
intport 0x33, timesliceHandler, 3

FPU Not-Available (INT 7)

The int_dev_na handler in vm-ints.asm handles the "Device Not Available" exception (CR0.TS set). It restores FPU/XMM state for the current thread and clears CR0.TS, enabling lazy FPU context switching.

Gotchas & Non-Obvious Behavior

  • IRQ handlers run with interrupts disabled: IRQHandler.handleInterrupt() is called from the kernel with interrupts disabled. Keep the handler short — no locking, no blocking. All heavy work is deferred to the IRQThread which runs normally.
  • Lazy FPU saving: CR0.TS is set on thread switch. The FPU state is only restored when the thread actually uses an FPU/SSE instruction, minimizing save/restore overhead.
  • PIC remapping: The PIC is reprogrammed at boot to map IRQs 0-15 to vectors 0x20-0x2F. This is non-negotiable — CPU exceptions occupy 0x00-0x1F and must not overlap.
  • IRQ threads vs handler: The IRQHandler interface is just the dispatch hook. The actual work runs in IRQThread.run() at MAX_PRIORITY after the scheduler signals the IRQ.
  • No shared IRQs by default: claimIRQ() throws ResourceNotFreeException if the IRQ is already claimed unless shared=true is passed. Shared IRQs chain multiple handlers via IRQAction.
  • EOI order for cascaded IRQs: When handling IRQ 8-15 (slave PIC), EOI must be sent to both the slave and the master cascade line (IRQ 2).
  • Timeslice broadcast: broadcastTimeSliceInterrupt() sends an IPI to all CPUs except self using ICR_DESTINATION_SHORTHAND_ALL_EX_SELF. This ensures SMP preemption fairness.
  • No nested interrupts: JNode runs with interrupts disabled during critical sections. Nested hardware interrupts are not supported — the IDT gates have appropriate privilege levels to prevent re-entry.

Related Pages

  • Core-Thread-Scheduling — How timer interrupts and yieldpoints drive scheduling
  • TSI — Thread Switch Indicator atomic flags that are set/modified by interrupt handlers
  • Assembly-Files — The role of ints.asm and vm-ints.asm in the assembly layer
  • Architecture — System layers and where interrupt handling fits
  • Driver-Framework — How drivers register IRQ handlers via the device manager
  • Boot-Sequence — Where IDT setup happens during boot

Clone this wiki locally