Interrupt Handling

An interrupt notifies the CPU of some event. Much of the work of an operating system relates to interrupts in one way or another. For our purposes, we classify interrupts into two broad categories:

  1. Internal interrupts, that is, interrupts caused directly by CPU instructions.

    • System calls, attempts at invalid memory access (page faults), and attempts to divide by zero are some activities that cause internal interrupts.

    • Because they are caused by CPU instructions, internal interrupts are synchronous or synchronized with CPU instructions. intr_disable() does not disable internal interrupts.

  2. External interrupts, that is, interrupts originating outside the CPU.

    • These interrupts come from hardware devices such as the system timer, keyboard, serial ports, and disks.

    • External interrupts are asynchronous, meaning that their delivery is not synchronized with instruction execution. Handling of external interrupts can be postponed with intr_disable() and related functions (see section Disabling Interrupts).

The CPU treats both classes of interrupts largely the same way, so Pintos has common infrastructure to handle both classes.

  • The following section describes this common infrastructure.

  • The sections after that give the specifics of external and internal interrupts.

Interrupt Infrastructure

When an interrupt occurs, the CPU saves its most essential state on a stack and jumps to an interrupt handler routine.

  • The 80x86 architecture supports 256 interrupts, numbered 0 through 255, each with an independent handler defined in an array called the interrupt descriptor table or IDT.

The Interrupt Handling Process

  1. In Pintos, intr_init() in threads/interrupt.c sets up the IDT so that each entry points to a unique entry point in threads/intr-stubs.S named intrNN_stub(), where NN is the interrupt number in hexadecimal.

  2. Because the CPU doesn't give us any other way to find out the interrupt number, this entry point pushes the interrupt number on the stack. Then it jumps to intr_entry(), which pushes all the registers that the processor didn't already push for us, and then calls intr_handler(), which brings us back into C in threads/interrupt.c.

  3. The main job of intr_handler() is to call the function registered for handling the particular interrupt. (If no function is registered, it dumps some information to the console and panics.) It also does some extra processing for external interrupts (see section External Interrupt Handling).

  4. When intr_handler() returns, the assembly code in threads/intr-stubs.S restores all the CPU registers saved earlier and directs the CPU to return from the interrupt.

Types and Functions

Types and Functions for Interrupt
  • Type: void intr_handler_func (struct intr_frame *frame)

    • This is how an interrupt handler function must be declared.

    • Its frame argument (see below) allows it to determine the cause of the interrupt and the state of the thread that was interrupted.

  • Type: struct intr_frame

    • The stack frame of an interrupt handler, as saved by the CPU, the interrupt stubs, and intr_entry(). Its most interesting members are described below.

    • uint32_t edi

    • uint32_t esi

    • uint32_t ebp

    • uint32_t esp_dummy

    • uint32_t ebx

    • uint32_t edx

    • uint32_t ecx

    • uint32_t eax

    • uint16_t es

    • uint16_t ds

      • Register values in the interrupted thread, pushed by intr_entry(). The esp_dummy value isn't actually used.

    • uint32_t vec_no

      • The interrupt vector number, ranging from 0 to 255.

    • uint32_t error_code

      • The "error code" pushed on the stack by the CPU for some internal interrupts.

    • void (*eip) (void)

      • The address of the next instruction to be executed by the interrupted thread.

    • void *esp

      • The interrupted thread's stack pointer.

  • Function: const char *intr_name (uint8_t vec)

    • Returns the name of the interrupt numbered vec, or "unknown" if the interrupt has no registered name.

Internal Interrupt Handling

Internal interrupts are caused directly by CPU instructions executed by the running kernel thread or user process (from project 2 onward). An internal interrupt is therefore said to arise in a "process context."

  • In an internal interrupt's handler, it can make sense to examine the struct intr_frame passed to the interrupt handler, or even to modify it. When the interrupt returns, modifications in struct intr_frame become changes to the calling thread or process's state. For example, the Pintos system call handler returns a value to the user program by modifying the saved EAX register.

  • There are no special restrictions on what an internal interrupt handler can or can't do. Generally they should run with interrupts enabled, just like other code, and so they can be preempted by other kernel threads. Thus, they do need to synchronize with other threads on shared data and other resources (see section Synchronization).

  • Internal interrupt handlers can be invoked recursively. For example, the system call handler might cause a page fault while attempting to read user memory. Deep recursion would risk overflowing the limited kernel stack (see section Struct thread), but should be unnecessary.

Types and Functions

Types and Functions for Internal Interrupt Handling
  • Function: void intr_register_int (uint8_t vec, int dpl, enum intr_level level, intr_handler_func *handler, const char *name)

    • Registers handler to be called when internal interrupt numbered vec is triggered. Names the interrupt name for debugging purposes.

    • If level is INTR_ON, external interrupts will be processed normally during the interrupt handler's execution, which is normally desirable.

    • Specifying INTR_OFF will cause the CPU to disable external interrupts when it invokes the interrupt handler. The effect is slightly different from calling intr_disable() inside the handler, because that leaves a window of one or more CPU instructions in which external interrupts are still enabled. This is important for the page fault handler; refer to the comments in userprog/exception.c for details.

    • dpl determines how the interrupt can be invoked.

      • If dpl is 0, then the interrupt can be invoked only by kernel threads. Otherwise dpl should be 3, which allows user processes to invoke the interrupt with an explicit INT instruction.

      • The value of dpl doesn't affect user processes' ability to invoke the interrupt indirectly, e.g. an invalid memory reference will cause a page fault regardless of dpl.

External Interrupt Handling

External interrupts are caused by events outside the CPU. They are asynchronous, so they can be invoked at any time that interrupts have not been disabled. We say that an external interrupt runs in an "interrupt context."

  • In an external interrupt, the struct intr_frame passed to the handler is not very meaningful. It describes the state of the thread or process that was interrupted, but there is no way to predict which one that is. It is possible, although rarely useful, to examine it, but modifying it is a recipe for disaster.

  • Only one external interrupt may be processed at a time. Neither internal nor external interrupt may nest within an external interrupt handler. Thus, an external interrupt's handler must run with interrupts disabled (see section Disabling Interrupts).

  • An external interrupt handler must not sleep or yield, which rules out calling lock_acquire(), thread_yield(), and many other functions. Sleeping in interrupt context would effectively put the interrupted thread to sleep, too, until the interrupt handler was again scheduled and returned. This would be unfair to the unlucky thread, and it would deadlock if the handler were waiting for the sleeping thread to, e.g., release a lock.

  • An external interrupt handler effectively monopolizes the machine and delays all other activities. Therefore, external interrupt handlers should complete as quickly as they can. Anything that require much CPU time should instead run in a kernel thread, possibly one that the interrupt triggers using a synchronization primitive.

  • External interrupts are controlled by a pair of devices outside the CPU called programmable interrupt controllers, PICs for short.

    • When intr_init() sets up the CPU's IDT, it also initializes the PICs for interrupt handling.

    • The PICs also must be "acknowledged" at the end of processing for each external interrupt. intr_handler() takes care of that by calling pic_end_of_interrupt(), which properly signals the PICs.

Types and Functions

The following functions relate to external interrupts.

Types and Functions for External Interrupt Handling
  • Function: void intr_register_ext (uint8_t vec, intr_handler_func *handler, const char *name)

    • Registers handler to be called when external interrupt numbered vec is triggered. Names the interrupt name for debugging purposes.

    • The handler will run with interrupts disabled.

  • Function: bool intr_context (void)

    • Returns true if we are running in an interrupt context, otherwise false.

    • Mainly used in functions that might sleep or that otherwise should not be called from interrupt context, in this form:

      ASSERT (!intr_context ());
  • Function: void intr_yield_on_return (void)

    • When called in an interrupt context, causes thread_yield() to be called just before the interrupt returns.

    • Used in the timer interrupt handler when a thread's time slice expires, to cause a new thread to be scheduled.

Last updated