Larceny Note #1: Timer Interrupts

June 27, 1997 / Lars T Hansen
1. Overview
2. Operations
3. Examples
4. Things to watch out for
5. Implementation

Note: must describe the interaction of timer interrupts, signal handlers, and the two-level timer.

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.

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.

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.

Timer interrupts are turned off with the disable-interrupts procedure:

    (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.

The procedure interrupt-handler can be used to get and set the currently installed interrupt handler:

    (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.

Finally, a predefined procedure call-without-interrupts runs a procedure with interrupts turned off, i.e., in a critical section:

    (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.

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.

   (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