Monday, December 1, 2008

OS/2 Interrupt Handling

Written December 1, 2008.  I recently received an inquiry regarding how OS/2 interrupts are handled and what is the correct action of a device driver upon being called by the OS/2 kernel. My first response was, you have got to be kidding me, the operating system has been dead for 10 years. The second response was to tell them the answer and now I write this blog so other folks might find it useful.

The failing scenario was a UNIX based sound API ported to OS/2 and the fact that it was dependent on "time" increasing during interrupt processing. The viewed behavior was that time would not increment so long as the device driver was doing work and this caused the sound library to come unglued.

The solution was easy - TIME should increase while the device driver is processing an interrupt. You as the device driver writer should not prevent other device drivers from doing their work, especially an important device driver like the one inside the OS/2 kernel that keeps track of time.

The foundation of the problem was that the sound device driver in question was running its interrupt handler and that interrupt handler was preventing the dispatch of other interrupts. The solution: In the device driver interrupt handler, you should VERY EARLY enable further interrupts. This sounds like something you shouldn't do, but you should. Example code describes better than words.

// HARDWARE INTERRUPT HANDLER
// Called by OS/2 kernel (interrupt dispatcher)
// On entry:
// DS is already set
// Interrupts are disabled
//
// On exit:
// We do not have to preserve the general purpose registers.
// We must clear the carry flag to tell the kernel that it
// was our IRQ.

void _interrupt IRQHandler (void)
{
   BYTE irqFlag;

   // Determine why the device generated the IRQ
   irqFlag = codecRead (...);

   if (it wasn't us)
   {
       // Set carry flag to tell the OS/2 kernel that it 
       // wasn't ours and return to the kernel (iret).
       // Side note: An interrupt that is dispatched, but that
       // has zero device drivers claim responsibility will be
       // masked off by the OS/2 kernel before interrupt
       // processing is completed.
       Code omitted;
   }

   // Acknowledge the device interrupt
   // In a level triggered world (PCI), the device stops pulling
   // on the interrupt line. Other devices can still be pulling.
   codecWrite (...);

   // Enable higher priority interrupts
   // Omitting this was the bug in the inquiry I received.
   // Enabling interrupts at the CPU does not mean that you will
   // be reentered. Quite the opposite, you WON'T be reentered
   // until you tell the 8259 interrupt controller that you have
   // completed processing this interrupt level.
   // By enabling interrupts at the CPU, what you are doing is
   // enabling the dispatch of "higher priority" interrupts where
   // priority is determined by the PIC.
   // In this example case, IRQ-8 (Timer) is higher priority
   // than IRQ-A (PCI). 
   // With the addition of the enable interrupts at the CPU, 
   // the timer was able to fire and "time" advances.
   sti();

   // Do heavy lifting of moving data and otherwise doing the
   // work of pulling data from the device.
   // Depending on the architecture of the device, it may be
   // necessary to pull/push the data before acking the interrupt
   // at the device.
   Code omitted;

   // Time to return to the kernel
   // Prevent nesting by disabling interrupts at the CPU.
   // Kernel dispatcher will reenable interrupts when we return
   // and it is possible that it will again immediately call us.
   // This is okay because the stack will unwind before next call

   // Prevent interrupts by blocking all of them at the CPU
   cli();

   // Finally - ack the interrupt at the 8259 PIC.
   // The PIC will send interrupts to the CPU, but it won't see
   // them, yet.
   DevHelp_EOI (codec_int);

   // Tell the kernel that we handled this IRQ so that it will
   // skip calling any other device drivers that may be
   // registered for this interrupt.
   clc();

   // Observe that interrupt flag is still clear, this is
   // critical to prevent reentry once the EOI was commanded.
   // iret will be generated by the compiler as part of the
   // function return statement 
   // (dictated by the _interrupt prefix).
   // Kernel will re-enable interrupts (sti) soon after 
   // our return.
}