Note: must describe the interaction of timer interrupts, signal handlers,
and the two-level timer.
When Larceny is first started, timer interrupts are turned off, and the
countdown timer is set to its maximum value. It is not good to leave
interrupts off permanently, since a timer interrupt in a critical
section will cause the timer to be reset to some small value in order to
allow the computation to make progress. However, since the value is
small, there will be another timer interrupt soon, so overall
performance may be worse than usual from the interrupt handling
overhead. This is not a problem if interrupts will be enabled in the
near future and handled properly, but it may cause performance problems
if interrupts are permanently disabled.
Therefore, in an interactive system, the Larceny read-eval-print loop
turns interrupts on, and changes the handler to one that resets the
timer to the maximum value every time it expires.
Timer interrupts are turned off with the disable-interrupts
procedure:
The procedure interrupt-handler can be used to get and
set the currently installed interrupt handler:
Finally, a predefined procedure call-without-interrupts runs
a procedure with interrupts turned off, i.e., in a critical section:
Notice how the new interrupt handler calls the old interrupt handler if
the interrupt was not a timer interrupt; this is so that the new handler
does not disable other system functionality (notably handling of
keyboard interrups, which are also handled by the common interrupt
handler).
If the service routine needs to access data that is used by the procedures
in main thread of computation, then the latter procedures can use critical
sections to guard against race conditions. Consider the following program,
which maintains a list of data, and a service routine that occasionally
computes the length of the list.
1. Overview
Larceny makes a primitive preemption facility in the form of timer
interrupts available to the programmer. These interrupts are currently
based on a count-down software timer which is decremented and checked on
every procedure call and backward branch. When the timer reaches 0,
a user-installable handler procedure is called.
2. Operations
Timer interrupts
are turned on, and the timer value (the number of "ticks") is set,
with the procedure
enable-interrupts:
(enable-interrupts timer-value) => unspecified
where timer-value must be a positive fixnum.
Enable-interrupts is an integrable procedure and this operation is
inexpensive in code compiled for speed.
(disable-interrupts) => ticks-remaining
where ticks-remaining is the number of ticks remaining on the
clock before the next interrupt, or #f if interrupts were
already disabled. Disable-interrupts is also an integrable
procedure.
(interrupt-handler) => handler
(interrupt-handler handler) => old-handler
A handler is a procedure of one argument: a symbol that
denotes the type of interrupt. The symbol for the timer interrupt
is timer.
(call-without-interrupts thunk) => object
where object is the value returned by thunk. Calls
to call-without-interrupts can be nested; only when the outermost
call returns will interrupts be re-enabled.
3. Examples
The following code sets up a service routine that will be run atomically
every k ticks:
(define (setup-service-routine thunk k)
(let ((old-handler (interrupt-handler)))
(interrupt-handler (lambda (type)
(if (eq? type 'timer)
(begin (thunk)
(enable-interrupts k))
(old-handler type))))
(enable-interrupts k)))
Atomicity is guaranteed since interrupts are disabled when the handler is
run, and are not enabled until the service routine has returned.
(define l '()) ; shared global
(define n '()) ; used by service routine only
(define (main-thread input)
(setup-service-routine
(lambda ()
(set! n (cons (length l) n)))
10000)
(loop input))
(define (loop input)
(do ((x (read input) (read input)))
((= x 0))
(if (> x 0)
(call-without-interrupts
(lambda ()
(set! l (cons x l))))
(call-without-interrupts
(lambda ()
(set! l (remove! x l)))))))
4. Some things to watch out for
In the current system, timer interrupts are not well-integrated with the
rest of the library. In particular, those parts of the system that
side-effect data structures or global variables (the I/O system; the
symbol table) are not protected by critical sections. Furthermore, the
I/O system calls are blocking. These problems will be fixed over time,
but are not on the critical path.
5. Implementation
At each procedure call (MAL instruction "Invoke") or backward branch
(MAL instructions "Branchf" or "Branch", but not "Skip"), code will be
generated that decrements the TIMER register and jumps to a millicode
routine if the the register is 0. That millicode routine is
m_timer_exception in Rts/Sparc/mcode.s. If interrupts are disabled, the
TIMER is given a little to run on (currently 5 ticks), and the handler
returns. Otherwise, the timer is disabled, the TIMER register is set to
a large value (currently 100,000) and a standard exception with type
EX_TIMER is triggered. That exception eventually ends up in the Scheme
exception handler, which calls the interrupt handler for this type of
exception (bypassing the user-installed error handler, if any). The
error handler can then do whatever it likes; the current continuation
will return to the instruction following the timer exception.
$Id: note1-timer.html 87 1998-11-25 14:38:41Z lth $
larceny@ccs.neu.edu