Let me tell you a story. Well, in fact, let me tell you several
stories/threads illustrating how the MIPS hardware provides the
low-level
features that prop up the
Linux kernel -
Story/thread #1
-The life and times of an interrupt: What happens when some piece of
hardware signals the CPU that it needs attention? Interrupts can cause
chaos when an interrupt-scheduled piece of code interrupts another code
halfway through some larger operation, so that leads on to a section on
threads, critical regions, and atomicity.
Story/thread #2
- What happens on a systemcall: After a userland application program
calls a kernel subroutine, what happens and how is that kept secure?
Story/thread #3
- How addresses get translated in Linux/MIPS: A fairly long story of
virtual memory and how the MIPS hardware serves the Linux memory map.
(The first two threads are covered
in this
part in this series and the third covered next in Part 4. Part 1, if
you remember, was a high
level view of GNU/Linux and how it operates on the MIPS 32k/64k
architecture.)
Thread #1: The Life and Times of an
Interrupt
It all starts when something happens in the real world: Maybe you
tapped a key on the keyboard. The device controller hardware picks up
the data and activates an interrupt signal.
That wends its way - probably via various pieces of hardware outside
the CPU, which are all different and not interesting right now - until
it shows up as the activation of one of the CPU's interrupt signals,
most often one of Int0-5*.
The CPU hardware polls those inputs on every clock cycle. The Linux
kernel sometimes disables interrupts, but not often; most often, the
CPU is receptive. The CPU responds by taking an interrupt exception the
next time it's ready to fetch an instruction.
It's sometimes difficult to get across just how fast this happens. A
500-MHz MIPS CPU presented with an interrupt will fetch the first
instruction of the exception handler (if in cache) within 3 to 4
clocks: That's 8 nanoseconds.
Seasoned low-level programmers know that you can't depend on an 8-ns
interrupt latency in any system, so we'll discuss below what gets in
the way. As the CPU fetches instructions from the exception entry
point, it also flips on the exception state bit SR(EXL), which will
make it insensitive to further interrupts and puts it in
kernel-privilege mode. It will go to the general exception entry point,
at 0x8000.0180.1
(Of course, it can't be that
simple; some MIPS CPUs nowprovide a dedicated interrupt-exception entry
point, and some will even go to a different entry point according to
which interrupt signal was asserted. But we'll keep it simple - many
OSs still do.)
The general exception handler inspects the Cause register and in
particular the Cause(ExcCode) field: That has a zero value, showing
that this was an interrupt exception. The Cause(IP7-2) field shows
which interrupt input lines are active, and SR(IM7-2) shows which of
these are currently sensitized.
There ought to be at least one active, enabled interrupt; the
software calculates a number corresponding to the number of the active
interrupt input that will become Linux's irq number.
(In many cases, the exception
handler may consult system-dependent external registers to refine the
information about the interrupt number.)
Before calling out into something more like generic code, the main
exception handler saves all the integer register values - they belong
to the thread that was just interrupted and they will need to be
restored before that thread is allowed to continue, somewhere in the
system's future.
(Some MIPS CPUs can avoid this
overhead for very low level interrupt handlers using shadow Registers).
The values are saved on the kernel stack of the thread that was
running when the interrupt happened, and the stack pointer is set
accordingly. Some key CP0 register values are saved too; those include
SR, EPC, and Cause.
With the SR(EXL) bit set, we're not really ready to call routines in
the main kernel. In this state we can't take another exception, so we
have to be careful to steer clear of any mapped addresses, for example.
(This is not quite inevitable. The
MIPS architecture has some carefully designed corners that make it
possible to nest exceptions in carefully controlled conditions. But
Linux doesn't use them.)
Now that we have saved registers and established a stack we can
change SR, clearing SR(EXL) but also clearing SR(IE) to avoid taking a
second interrupt - after all, the interrupt signal we responded to is
still active.
Now we're ready to call out to do IRQ(). It's a machine-dependent
routine but written in C, with a fairly well standardized flow. do
IRQ() is passed a structure full of register values as a parameter. Its
job is to selectively disable this interrupt in particular and
acknowledge any interrupt management hardware that needs a confirmation
that the interrupt has been serviced.
Assuming there is a device ISR (interrupt service routine)
registered on this irq number, do IRQ() calls on to handle IRQ event().
It's undesirable to run for long with all interrupts disabled; some
other device out there may need fast response to its interrupt.