CS444 Class 6
Last time: Interrupts, specifically receiver interrupts.
Android note (optional material). Android runs mostly on ARM (32-bit), but also on x86 (32-bit). Recall that “32-bit” means 32-bit addresses, thus 4GB address spaces. What about the ARM interrupt system? It’s very similar to the x86—it has a separate interrupt controller, only two interrupt input pins on the CPU itself (vs. one for x86), for ordinary and “fast” interrupts, of higher priority. The CPU has two bits in its processor status (like EFLAGS) register, I and F, for the two kinds of interrupts, instead of one IF in EFLAGS. The instruction set includes the SWI instruction (SWI = software interrupt), the system call instruction, that lifts execution from user mode to supervisor mode (actually there are several privileged modes for the CPU), while saving the old CPU state for later resumption. Thus we can think of an x86 system as a somewhat simplified ARM system, in terms of interrupts.
This time: the trickier case of transmitter interrupts.
Receiver interrupt from start to finish:
1. Char arrives at UART, the “event”
2. UART sends IRQ to PIC
3. PIC signals CPU now or after higher priority interrupts are handled
4. CPU does its interrupt cycle, perhaps after running a while with IF=0, ends up with EIP = interrupt handler address, IF=0, in kernel mode, old CPU state on the stack.
5. Interrupt handler runs, sends EOI to PIC, picks up char by inb from the UART, ends with IRET (causing interrupted code to resume)
6. PIC detects EOI, forgets about this interrupt
7. UART detects inb, forgets about this interrupt
Look at handout $pcex/typewr.c: continued.
Since the interrupt cycle makes IF=0, the interrupt handler starts off execution with IF=0, and since there is no “sti()” or “set_eflags(...)” in either part of the interrupt handler code, the whole interrupt handler (as part and C part) executes with interrupts off. When the final IRET executes and restores the old EFLAGS and EIP, IF returns to 1. This is the normal setup for a simple interrupt handler, i.e., anything we need for this course, plus some real OS interrupt handlers. Because IF=0 already, there is no need to use cli() inside an interrupt handler—it would be a dumb thing to do, though harmless.
Here the software has the char to transmit, the hardware may or may not be ready to transmit it (may be busy with earlier work).
“event” here: transmitter becomes ready to transmit. It sends in an interrupt with this good news, so the software can feed it with the next char to output.
Handling a sequence of chars:
Int handler: output next char
Great system until we run out
of data to give to it. What to do then? Simplest solution is to
shut down transmitter interrupts. (There is another solution for this
UART.)
Quick intro on how to
handle TX interrupts:
Note that the transmitter and
receiver share a single IRQ, for example IRQ3 for COM2. Since the
provided hw1 code is doing receiver interrupts, it already has an irq3inthandc
function implemented. All you have to do is add to it to handle another
case.
How can we tell whether it’s
a transmitter or receiver interrupt?
Answer: the UART’s LSR tells
us via the THRE and DR bits.
If it’s a TX int, dequeue a char and output
it. If the queue is empty, shut down TX ints.
Be careful not to shut down RX ints! They should
be on continuously. It’s only TX ints that go
on and off depending on output availability.
So when do we turn on TX ints? When data shows up to be
output, via calls to ttywrite (or echoes of user
input.)
Output by Polling, AKA
Programmed I/O
Loop over the chars of the
string:
Loop over
checking the transmit-ready bit, THRE, of the LSR register, until it goes on.
Write the
char to the UART’s transmit register.
Output by Interrupts
“Interrupt-driven output”
We have discussed how the
interrupt handler needed to tell whether a particular interrupt came from a
received char or transmitter-ready, and how the interrupt handler needs to shut
down transmit interrupts when it runs out of data to output. That means we need
to start up the interrupts when data shows up again to be output, and we can
initialize the system with transmit interrupts off.
initialization: set up the interrupt gate in IDT, but leave
transmitter interrupts off in the UART. Also, make sure the IRQ can pass
through the PIC, and that the CPU has interrupts enabled at the end of initialization.
transmit interrupt
handler: call dequeue to get the next
char or find out there are none, and outb a real char
to the UART’s transmit register, or shut down interrupts if there are none left
To output a string (say in ttywrite):
In 3., we see a "busy
loop", in fact a double loop, outer loop over chars to output, inner loop
over attempts to enqueue a certain char. It's
unfortunate to have to have a busy loop, but that's the only thing we can do
here. In the future, we'll have a real OS and can block one process and
let another use the CPU. A real OS should never have a busy loop.
Note that step 1. doesn’t affect the UART. It is just groundwork so that
the first run of the interrupt handler will find a char to output.
Note: enabling interrupts in
the UART transmitter (step 2.) causes the UART
to immediately send in an interrupt if it is ready to transmit.
Note: the enqueue
in ttywrite is a “critical region” because both ttywrite and the interrupt handler are manipulating the
queue data structure. That means we need to turn off interrupts during
the enqueue operation. Note that interrupts
need to be on for part of each pass of the loop, to allow the system to
progress. More on this below.
Note that the queue holds
only 6 chars, so we can easily test fullness conditions.
Specific
example: write (TTY1, “123456789”,
9):
1.
“123456” put in queue during setup.
2.
“123” output, one by one, by int. handler, and “789” replace them in queue by
loop of enqueues in ttywrite.
3.
Everything is in queue, so ttywrite returns
4. ttywrite has returned, but 6 chars
“456789” are still in queue: they are output one by one, by int. handler.
kprintf: kprintf(...) is used just
like printf for printing debug output in programs
like testio.c that enable interrupts. It is
equivalent to
int saved_eflags;
saved_eflags = get_eflags(); /* get
caller's IF value */
cli()
printf(...); /* note that
SAPC-library printf uses programmed i/o */
set_eflags(saved_eflags);
It is important to turn off
interrupts before using printf when your program uses
transmit interrupts. Otherwise you would have part of the code trying to
do programmed i/o and another part doing
interrupt-driven i/o all at the same time. The two methods would be
fighting over the transmitter! With kprintf, as
soon as execution reaches it, it locks out the interrupt-driven behavior by
setting IF=0 and takes all the time it needs to output the string by programmed
i/o. This is just what you want for
debugging: intact status messages that tell you what’s
true right at that moment.
Now
add to example above:
In testio.c, suppose we have:
write (TTY1, “123456789”, 9):
kprintf("after write");
for (i=0; i<100000000;
i++) /* busy loop, runs with IF = 1 */
;
We expect to see on the screen:
123after write456789
because as soon as ttywrite returns, kprintf is executed and does its polling output without interrupts happening. So the 123 is output during ttywrite's execution, the "after write" is output during kprintf's execution, and the 456789 is output during the busy loop's execution.
Idea of a Critical Region--enqueue in ttywrite vs. interrupt handler dequeue
(also
dequeue in ttyread vs.
interrupt handler enqueue)
Ref: Tan, pg 120-first two
paragraph of p. 121. As discussed there, this technique of cli/sti only works for uniprocessor
systems. But it can be upgraded to more complex
mechanisms with a little additional work.
When the program level is
executing in ttywrite, we have the situation of two
concurrent activities:
--Program level looping on enqueues
--Interrupt level
intermittently executing doing a dequeue each time
and we’re talking about the same queue here, the output
queue.
We have to have IF=1 for at
least part of each pass of the loop in ttywrite, to
let the interrupts happen.
Luckily, it’s OK to make IF=0
for a while, then turn it back on—the PIC saves the knowledge of an interrupt
and signals the CPU until it pays attention.
And interrupts can occur
between any two machine instructions.
Suppose IF=1 solidly in the
loop of enqueues. Then an interrupt could
happen half way through the enqueue in ttywrite. The interrupt handler would run and do a dequeue on a partially-finished
edit to the queue. This can break the queue data structure.
Scenario:
How can we arrange to avoid
this?
We’re in charge, so we can turn
off interrupts during the execution of enqueue,
and protect it from this.
We say the call to enqueue in ttywrite is a “critical section” or a “critical region”.
But watch out, if you turn
off interrupts too long, the output can't drain at interrupt level.
Interrupts need to go on and off on each pass of the inner loop.
Similarly, in ttyread, we can turn off interrupts during its call to dequeue, another critical region.
Here is how to do it:
cli()
/* disable interrupts in the CPU by setting IF=0 in EFLAGS */
enqueue(...)
/* critical code */
sti()
/* or set_eflags(saved_eflags)
like you see in ttyread: reenable
interrupts */
This is a form of mutual
exclusion, or mutex. It isn’t the only way
to do it—we could manipulate the interrupt mask register in the PIC, for
example. But it’s very simple to do this way, so we’ll use setting IF=0
as our form of kernel mutex.
It is important to reenable interrupts because they need to be on in every
pass of the loop in step 3. of the string-output
algorithm, to allow the interrupts to send chars to the UART.
(You
can ignore this coverage of IIR if you want)
The
UART in the PC is quite a complicated device, and we have only looked at part
of its behavior. By treating it as a simple UART, we are seeing the
bare-bones of device input/output, i.e., pretty general approaches. A
more serious programmer would want to take advantage of its features. One
thing it is able to do is tell us what kind of interrupt it is currently
reporting—a receiver int, a transmitter int, or two other kinds we’re not using. It shows
this information in its IIR. Furthermore, when it detects a read of this
register over the bus, it is ack’d for that
interrupt, and is given permission to generate another IRQ if
appropriate. The UART is acting as its own little interrupt controller,
sending out one interrupt at a time for its four interrupt sources.
So
all we have to do is read the IIR, and use the info to either do the
input interrupt handling or the output interrupt handling. And we can
leave transmitter interrupts on all the time, just like receiver interrupts, because we have successfully ack’d
the device even in the case of no more output.
Note
that the IIR uses a 2-bit field to report the interrupt type, and this field
has mask UART_IIR_ID, and value UART_IIR_RDI in case of receiver int. The
proper code to test for RDI is therefore “test under mask” like this, if iir has the register value:
if ((iir
& UART_IIR_ID) == UART_IIR_RDI) ...
End of coverage on IIR