Interrupt Handling
Last updated
Last updated
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:
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.
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 ).
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.
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.
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.
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
.
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 ).
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.
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.
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.
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, or 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.
The following functions relate to external interrupts.
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 ).
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 ), but should be unnecessary.
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 ).