CLOSE

In computer systems, interrupts are signals that temporarily halt the CPU's current operations to attend to a specific task. Interrupts can be broadly categorized into several types based on their sources and purposes. Understanding these different types of interrupts is crucial for effective system and application programming.

1 Types of Interrupts

1.1  Hardware Interrupts

a. Maskable Interrupts (IRQs)

Maskable interrupts are hardware interrupts that can be ignored or "masked" by setting a specific bit in a mask register. These interrupts are usually generated by peripheral devices such as keyboards, mice, and network cards. The Programmable Interrupt Controller (PIC) or Advanced Programmable Interrupt Controller (APIC) manages these interrupts.

Examples:

  • Keyboard interrupt
  • Mouse interrupt
  • Network card interrupt

b. Non-Maskable Interrupts (NMIs)

Non-maskable interrupts are high-priority hardware interrupts that cannot be ignored by the CPU. NMIs are typically used for critical events that require immediate attention, such as hardware failures.

Examples:

  • Memory parity error
  • Power failure

1.2 Software Interrupts

Software interrupts are triggered by software instructions rather than hardware signals. They are used for various purposes, including system calls and debugging.

a. INT Instructions

The INT instruction is used to generate a software interrupt. This instruction is commonly used to invoke system calls in operating systems.

Example:

  • INT 0x80 in Linux for system calls

b. Trap Instructions

Trap instructions are used to create a controlled transition to an interrupt handler. They are often used for debugging purposes.

Examples:

  • INT3 for breakpoints in debugging

1.3 Processor Exceptions

Processor exceptions are interrupts generated by the CPU in response to errors or specific conditions during program execution. These exceptions indicate various error states or special conditions that the CPU encounters.

a. Faults

Faults are exceptions that occur during instruction execution and usually leave the program counter pointing to the instruction that caused the fault. The instruction can be retried after handling the fault.

Examples:

  • Page fault (when a program accesses a non-resident page in memory)
  • Divide-by-zero fault

b. Traps

Traps are exceptions that occur after the execution of the instruction that caused them. They are used to signal conditions that do not require the instruction to be re-executed.

Examples:

  • System calls
  • Breakpoint traps

c. Aborts

Aborts are serious exceptions that indicate critical errors, typically related to hardware failures. Aborts do not allow the program to continue execution.

Examples:

  • Double fault (when a second exception occurs while trying to handle the first exception)
  • Machine check

1.4 Inter-Processor Interrupts (IPIs)

Inter-Processor Interrupts are used in multiprocessor systems to send interrupts from one processor to another. They are commonly used for tasks such as signaling the need for task scheduling, updating system state, or handling certain types of system calls.

Examples:

  • Task rescheduling signals
  • TLB (Translation Lookaside Buffer) shootdown requests

2 Steps to Implement the Interrupts

  1. Define the IDT and IDT pointer Structure
  2. Initialize IDT entries
  3. Write ISR
  4. Register the ISR with the IDT
  5. Load the IDT
  6. Enable Interrupts
  7. Configure the PIC (for handling hardware interrupts)

3 Step1 - Define the IDT and IDT Pointer Structure

First, define the IDT entry structure and the IDT pointer. This structure should include the necessary fields to store the ISR address and associated metadata.

3.1 What is the IDT in x86_64?

The IDT in x86_64 is a table that the CPU uses to determine how to handle interrupts and exceptions. It contains 256 entries, each corresponding to a different interrupt vector. Each entry in the IDT points to an interrupt service routine (ISR) or an exception handler.

3.1 Structure of an IDT Entry

Each entry in the IDT is 16 bytes long and has the following structure in x86_64:

  • Offset (Bits 0-15): Lower part of the ISR address.
  • Selector (Bits 16-31): Segment selector.
  • IST (Bits 32-34): Interrupt Stack Table offset (not commonly used).
  • Type and Attributes (Bits 35-47): Type, attributes, and privilege level.
  • Offset (Bits 48-63): Middle part of the ISR address.
  • Offset (Bits 64-127): Higher part of the ISR address.
  • Zero (Bits 128-159): Reserved, must be zero.

IDT Entry Layout

  Bits 0-15   : Offset bits 0-15
  Bits 16-31  : Segment Selector
  Bits 32-34  : IST (Interrupt Stack Table)
  Bits 35-39  : Type and Attributes
  Bits 40-47  : Type and Attributes continued
  Bits 48-63  : Offset bits 16-31
  Bits 64-95  : Offset bits 32-63
  Bits 96-127 : Reserved, must be zero

IDT Structure Definition:

// IDT entry structure for x86_64
struct idt_entry {
    uint16_t offset_low;    // Offset bits 0-15
    uint16_t selector;      // Selector
    uint8_t  ist;           // Interrupt Stack Table offset
    uint8_t  type_attr;     // Type and attributes
    uint16_t offset_mid;    // Offset bits 16-31
    uint32_t offset_high;   // Offset bits 32-63
    uint32_t zero;          // Reserved, set to zero
} __attribute__((packed));

IDT Pointer Definition:

// IDT pointer structure

struct idt_ptr {
    uint16_t limit;         // Size of the IDT - 1
    uint64_t base;          // Base address of the IDT
} __attribute__((packed));
// Define an IDT with 256 entries

#define IDT_ENTRIES 256
struct idt_entry idt[IDT_ENTRIES];
struct idt_ptr idtp;

4 Step 2 - Initialize IDT Entries

Initialize each entry in the IDT with the address of the ISR, the segment selector, and the appropriate attributes. For this, create a function to set up each entry in the IDT with the ISR address, segment selector, and attributes.

void set_idt_entry(int num, uint64_t base, uint16_t selector, uint8_t flags) {
    idt[num].offset_low = base & 0xFFFF;
    idt[num].selector = selector;
    idt[num].ist = 0;
    idt[num].type_attr = flags;
    idt[num].offset_mid = (base >> 16) & 0xFFFF;
    idt[num].offset_high = (base >> 32) & 0xFFFFFFFF;
    idt[num].zero = 0;
}

5 Step 3: Write ISRs

ISRs must be written for each interrupt and exception. Here's an example of a simple ISR for a keyboard interrupt in x86_64 assembly.

global isr_keyboard
section .text

isr_keyboard:
    cli                     ; Clear interrupts
    push rax                ; Save registers
    push rbx
    push rcx
    push rdx

    ; ISR code to handle keyboard interrupt

    pop rdx                 ; Restore registers
    pop rcx
    pop rbx
    pop rax
    sti                     ; Set interrupts
    iretq                   ; Return from interrupt

6 Step 4: Register the ISR with the IDT

Register the ISR with the IDT using the set_idt_entry function.

extern void isr_keyboard();  // Declare the ISR function

void init_idt() {
    set_idt_entry(33, (uint64_t)isr_keyboard, 0x08, 0x8E);  // Keyboard interrupt vector is 33
    load_idt();
}

7 Step 5: Load the IDT

Load the IDT using the lidt instruction.

void load_idt() {
    idtp.limit = (sizeof(struct idt_entry) * 256) - 1;
    idtp.base = (uint64_t)&idt;
    __asm__ __volatile__("lidt %0" : : "m" (idtp));
}

8 Step 6: Enable Interrupts

Finally, enable interrupts so that the CPU can start handling them.

sti  ; Set the interrupt flag