Real Time Systems: Notes

Disabling an IRQ Within a Linux Handler

 

Introduction

This explains an approach for conditionally leaving a specific interrupt masked at the PIC (programmable interrupt controller) after the interrupt handler has returned. It seems the Linux kernel was not designed with this in mind. Apparently, one must modify the kernel. The following paragraphs explain how one can modify the kernel to add this capability.

This information was originally written as advice for students working on a project in a course on operating systems, in which they needed to prevent a network interface device (Ethernet controller) from generating interrupts during certain intervals. It may be of use also to students in other courses that require some knowledge of interrupts, as an illustration of exactly what is meant by disabling an interrupt at the programmable interrupt controller (PIC).

I believe the information is essentially correct, but since I did not test it personally one should assume there may be some errors, or some details I didn't think of.

Background

If you examine the code of the Linux kernel functions enable_irq and disable_irq, you will see that they interact directly with the PIC. The interaction is via calls to mask_irq and unmask_irq. The code for these functions is found in .../linux/arch/i386/kernel/irq.c.

static unsigned char cache_21 = 0xff;
static unsigned char cache_A1 = 0xff;
...
static inline void mask_irq(unsigned int irq_nr)
{
        unsigned char mask;
        mask = 1 << (irq_nr & 7);
        if (irq_nr < 8) {
                cache_21 |= mask;
                outb(cache_21,0x21);
        } else {
                cache_A1 |= mask;
                outb(cache_A1,0xA1);
        }
}
static inline void unmask_irq(unsigned int irq_nr)
{
        unsigned char mask;
        mask = ~(1 << (irq_nr & 7));
        if (irq_nr < 8) {
                cache_21 &= mask;
                outb(cache_21,0x21);
        } else {
                cache_A1 &= mask;
                outb(cache_A1,0xA1);
        }
}

The variables cache_21 and cache_A1 are used to hold the values of the interrupt masks of the two PIC's. One PIC is at I/O address 0x21 and the other is at I/O address 0xA1. The "outb" operation updates the mask of the controller, and is used to both mask and unmask interrupts.

There is another place where the interrupt mask of the PIC is updated. That is in the first-level hardware interrupt handler routines, which are declared as macros in .../linux/include/asm-i386/irq.h.:

#define BUILD_IRQ(chip,nr,mask) \
asmlinkage void IRQ_NAME(nr); \
asmlinkage void FAST_IRQ_NAME(nr); \
asmlinkage void BAD_IRQ_NAME(nr); \
__asm__( \
"\n"__ALIGN_STR"\n" \
SYMBOL_NAME_STR(IRQ) #nr "_interrupt:\n\t" \
        "pushl $-"#nr"-2\n\t" \
        SAVE_ALL \
        ENTER_KERNEL \
        ACK_##chip(mask,(nr&7)) \
        "incl "SYMBOL_NAME_STR(intr_count)"\n\t"\
        "sti\n\t" \
        "movl %esp,%ebx\n\t" \
        "pushl %ebx\n\t" \
        "pushl $" #nr "\n\t" \
        "call "SYMBOL_NAME_STR(do_IRQ)"\n\t" \
        "addl $8,%esp\n\t" \
        "cli\n\t" \
        UNBLK_##chip(mask) \
        "decl "SYMBOL_NAME_STR(intr_count)"\n\t" \
        "incl "SYMBOL_NAME_STR(syscall_count)"\n\t" \
        "jmp ret_from_sys_call\n" \
"\n"__ALIGN_STR"\n" \
SYMBOL_NAME_STR(fast_IRQ) #nr "_interrupt:\n\t" \
        SAVE_MOST \
        ENTER_KERNEL \
        ACK_##chip(mask,(nr&7)) \
        "incl "SYMBOL_NAME_STR(intr_count)"\n\t" \
        "pushl $" #nr "\n\t" \
        "call "SYMBOL_NAME_STR(do_fast_IRQ)"\n\t" \
        "addl $4,%esp\n\t" \
        "cli\n\t" \
        UNBLK_##chip(mask) \
        "decl "SYMBOL_NAME_STR(intr_count)"\n\t" \
        LEAVE_KERNEL \
        RESTORE_MOST \
"\n"__ALIGN_STR"\n" \
SYMBOL_NAME_STR(bad_IRQ) #nr "_interrupt:\n\t" \
        SAVE_MOST \
        ENTER_KERNEL \
        ACK_##chip(mask,(nr&7)) \
        LEAVE_KERNEL \
        RESTORE_MOST);

These macros are used to generate the actual interrupt handler functions. Internally, they use other macros, including ACK_FIRST, ACK_SECOND, UNBLK_FIRST, and UNBLK_SECOND. Note that these macros are in pairs, corresponding to the two PIC's (FIRST and SECOND):

#define ACK_FIRST(mask,nr) \
        "inb $0x21,%al\n\t" \
        "jmp 1f\n" \
        "1:\tjmp 1f\n" \
        "1:\torb $" #mask ","SYMBOL_NAME_STR(cache_21)"\n\t" \
        "movb "SYMBOL_NAME_STR(cache_21)",%al\n\t" \
        "outb %al,$0x21\n\t" \
        "jmp 1f\n" \
        "1:\tjmp 1f\n" \
        "1:\tmovb $0x20,%al\n\t" \
        "outb %al,$0x20\n\t"

#define ACK_SECOND(mask,nr) \
        "inb $0xA1,%al\n\t" \
        "jmp 1f\n" \
        "1:\tjmp 1f\n" \
        "1:\torb $" #mask ","SYMBOL_NAME_STR(cache_A1)"\n\t" \
        "movb "SYMBOL_NAME_STR(cache_A1)",%al\n\t" \
        "outb %al,$0xA1\n\t" \
        "jmp 1f\n" \
        "1:\tjmp 1f\n" \
        "1:\tmovb $0x20,%al\n\t" \
        "outb %al,$0xA0\n\t" \
        "jmp 1f\n" \
        "1:\tjmp 1f\n" \
        "1:\toutb %al,$0x20\n\t"

#define UNBLK_FIRST(mask) \
        "inb $0x21,%al\n\t" \
        "jmp 1f\n" \
        "1:\tjmp 1f\n" \
        "1:\tandb $~(" #mask "),"SYMBOL_NAME_STR(cache_21)"\n\t" \
        "movb "SYMBOL_NAME_STR(cache_21)",%al\n\t" \
        "outb %al,$0x21\n\t"

#define UNBLK_SECOND(mask) \
        "inb $0xA1,%al\n\t" \
        "jmp 1f\n" \
        "1:\tjmp 1f\n" \
        "1:\tandb $~(" #mask "),"SYMBOL_NAME_STR(cache_A1)"\n\t" \
        "movb "SYMBOL_NAME_STR(cache_A1)",%al\n\t" \
        "outb %al,$0xA1\n\t"

The effect of ACK_xxxx is very similar to the function disable_irq, and the effect of UNBLK_xxxx is very similar to the function enable_irq.

If you look closer at one of the interrupt handler "wrapper" functions, you will see that it starts out execution with all the interrupts masked at the CPU level (i.e., the allow-interrupt flag cleared). The specific interrupt that is being handled is then masked at the PIC, by the macro ACK_xxxx, before all interrupts are unmasked at the CPU. The user-specified handler procedure is then called, with only the interrupt that is being handled masked. On return, the process is reverse. All interrupts are masked at the CPU, and the specific interrupt being handled is unmasked at the PIC. The interrupt is finally unmasked after return from the handler, by the return-from-interrupt operation, which reloads the value of the status register containing the old interrupt-enable bit:

        SYMBOL_NAME_STR(IRQ) #nr "_interrupt:\n\t" \
        "pushl $-"#nr"-2\n\t" \
        SAVE_ALL \
        ENTER_KERNEL \
        ACK_##chip(mask,(nr&7))

masks the interrupt at the PIC

        "incl "SYMBOL_NAME_STR(intr_count)"\n\t"\
        "sti\n\t"

unmasks all interrupts at the CPU

        "movl %esp,%ebx\n\t" \
        "pushl %ebx\n\t" \
        "pushl $" #nr "\n\t" \
        "call "SYMBOL_NAME_STR(do_IRQ)"\n\t"

calls a user-specified handler procedure

        "addl $8,%esp\n\t"\
        "cli\n\t"

masks all interrupts at the CPU

UNBLK_##chip(mask)

unmasks the interrupt at the PIC

        "decl "SYMBOL_NAME_STR(intr_count)"\n\t" \
        "incl "SYMBOL_NAME_STR(syscall_count)"\n\t" \
        "jmp ret_from_sys_call\n" \
        "\n"__ALIGN_STR"\n"

The Problem

The problem above is that UNBLK_xxxx will always unmask the interrupt at the PCI, regardless of what the user handler (part of the device driver) does. In particular, if the device driver's interrupt handler calls disable_irq the effect will be undone later by the implicit call to UNBLCK_xxxx that occurs on the way out of the interrupt handler wrapper.

Changes to Solve the Problem

A way to work around this is to modify the code of the interrupt handler wrapper, to optionally skip unmasking the interrupt at the PIC. One way this might be done is shown below:

Add the following new variables to irq.c, right after the declarations of cache_21 and cache_A1:

static unsigned char hack_21 = 0xff;
static unsigned char hack_A1 = 0xff;

/* The Ith bit of hack_xxx is 1 if interrupt I should not
   be unmasked by the PIC on return from the handler;
   it is 0 otherwise.
 */

We then modify the code of macro UNBLOCK_FIRST as follows:

#define UNBLK_FIRST(mask) \
        "inb $0x21,%al\n\t" \
        "jmp 1f\n" \
        "1:\tjmp 1f\n" \
        "1:\tmov $~(" #mask "),%a1\n\t" \
        "\tor "SYMBOL_NAME_STR(hack_21)",%a1\n\t" \
         "\tandb %a1,"SYMBOL_NAME_STR(cache_21)"\n\t" \
        "movb "SYMBOL_NAME_STR(cache_21)",%al\n\t" \
        "outb %al,$0x21\n\t"

The change is intended to nullify the effect of the mask if the corresponding bit is set in variable hack_21. The parameter mask is complemented and put into register A1. This value is then OR'd with the variable hack_21, and the result is AND'd into the variable cache_21. The intended effect is similar to

cache_21 = cache_21 & (hack_21 | ~mask);

Here, mask is a bit-vector that is all 1's except for a single 1-bit;
~mask is a bit-vector with all 1's except for a single 0-bit;
hack_21 | ~mask is either the same as ~mask, or its is all 1's; thus
cache_21 & (hack_21 | ~mask) is either no different from cache_21 or has just one bit changed from 0 to 1.

We should do similarly for UNBLK_SECOND. Then, we add the following functions to irq.h (prototypes) and irg.c (bodies).

/* The following functions set and unset, respectively,
   the bit for a specific IRQ in the mask variables
   hack_21 and hack_A1.  The effect of having the bit set
   is that the IRQ will not be unblocked (enabled) on
   return from the interrupt handler.

static inline void hack_irq(unsigned int irq_nr)
{
        unsigned char mask;
        mask = 1 << (irq_nr & 7);
        if (irq_nr < 8) {
                hack_21 |= mask;
        } else {
                hack_A1 |= mask;
        }
}

static inline void unhack_irq(unsigned int irq_nr)
{
        unsigned char mask;
        mask = ~(1 << (irq_nr & 7));
        if (irq_nr < 8) {
                hack_21 &= mask;
                outb(cache_21,0x21);
        } else {
                hack_A1 &= mask;
                 outb(cache_A1,0xA1);
        }
}

Note that hack_irq does not call outb to reset the mask in the PIC. This function would be called inside an interrupt handler, to keep the interrupt masked in the IRQ. The effect occurs later, when the value in hack_21 or hack_A1 prevents unmasking of the interrupt in UNBLCK_xxxx.

On the other hand, unhack_irq does call outb to reset the mask in the PIC. This function would be called inside a timer handler, at the time when we decide it is OK to again allow interrupts from some (other) source. In that case we want the effect to be immediate.

© 1998, 2003 T. P. Baker. No part of this publication may be reproduced, stored in a retrieval system, or transmitted in any form or by any means without written permission.
$Id: irqs.html,v 1.1 2003/10/17 12:34:01 baker Exp baker $