嵌入式linux中文站在线图书

Previous Page Next Page

Interrupt Handling

Because of the indeterminate nature of I/O, and speed mismatches between I/O devices and the processor, devices request the processor's attention by asserting certain hardware signals asynchronously. These hardware signals are called interrupts. Each interrupting device is assigned an associated identifier called an interrupt request (IRQ) number. When the processor detects that an interrupt has been generated on an IRQ, it abruptly stops what it's doing and invokes an interrupt service routine (ISR) registered for the corresponding IRQ. Interrupt handlers (ISRs) execute in interrupt context.

Interrupt Context

ISRs are critical pieces of code that directly converse with the hardware. They are given the privilege of instant execution in the larger interest of system performance. However, if ISRs are not quick and lightweight, they contradict their own philosophy. VIPs are given preferential treatment, but it's incumbent on them to minimize the resulting inconvenience to the public. To compensate for rudely interrupting the current thread of execution, ISRs have to politely execute in a restricted environment called interrupt context (or atomic context).

Here is a list of do's and don'ts for code executing in interrupt context:

  1. It's a jailable offense if your interrupt context code goes to sleep. Interrupt handlers cannot relinquish the processor by calling sleepy functions such as schedule_timeout(). Before invoking a kernel API from your interrupt handler, penetrate the nested invocation train and ensure that it does not internally trigger a blocking wait. For example, input_register_device() looks harmless from the surface, but tosses a call to kmalloc() under the hood specifying GFP_KERNEL as an argument. As you saw in Chapter 2, "A Peek Inside the Kernel," if your system's free memory dips below a watermark, kmalloc() sleep-waits for memory to get freed up by the swapper, if you invoke it in this manner.

  2. For protecting critical sections inside interrupt handlers, you can't use mutexes because they may go to sleep. Use spinlocks instead, and use them only if you must.

  3. Interrupt handlers cannot directly exchange data with user space because they are not connected to user land via process contexts. This brings us to another reason why interrupt handlers cannot sleep: The scheduler works at the granularity of processes, so if interrupt handlers sleep and are scheduled out, how can they be put back into the run queue?

  4. Interrupt handlers are supposed to get out of the way quickly but are expected to get the job done. To circumvent this Catch-22, interrupt handlers usually split their work into two. The slim top half of the handler flags an acknowledgment claiming that it has serviced the interrupt but, in reality, offloads all the hard work to a fat bottom half. Execution of the bottom half is deferred to a later point in time when all interrupts are enabled. You will learn to develop bottom halves while discussing softirqs and tasklets later.

  5. You need not design interrupt handlers to be reentrant. When an interrupt handler is running, the corresponding IRQ is disabled until the handler returns. So, unlike process context code, different instances of the same handler will not run simultaneously on multiple processors.

  6. Interrupt handlers can be interrupted by handlers associated with IRQs that have higher priority. You can prevent this nested interruption by specifically requesting the kernel to treat your interrupt handler as a fast handler. Fast handlers run with all interrupts disabled on the local processor. Before disabling interrupts or labeling your interrupt handler as fast, be aware that interrupt-off times are bad for system performance. More the interrupt-off times, more is the interrupt latency, or the delay before a generated interrupt is serviced. Interrupt latency is inversely proportional to the real time responsiveness of the system.

A function can check the value returned by in_interrupt() to find out whether it's executing in interrupt context.

Unlike asynchronous interrupts generated by external hardware, there are classes of interrupts that arrive synchronously. Synchronous interrupts are so called because they don't occur unexpectedly—the processor itself generates them by executing an instruction. Both external and synchronous interrupts are handled by the kernel using identical mechanisms.

Examples of synchronous interrupts include the following:

Assigning IRQs

Device drivers have to connect their IRQ number to an interrupt handler. For this, they need to know the IRQ assigned to the device they're driving. IRQ assignments can be straightforward or may require complex probing. In the PC architecture, for example, timer interrupts are assigned IRQ 0, and RTC interrupts answer to IRQ 8. Modern bus technologies such as PCI are sophisticated enough to respond to queries regarding their IRQs (assigned by the BIOS when it walks the bus during boot). PCI drivers can poke into earmarked regions in the device's configuration space and figure out the IRQ. For older devices such as Industries Standard Architecture (ISA)-based cards, the driver might have to leverage hardware-specific knowledge to probe and decipher the IRQ.

Take a look at /proc/interrupts for a list of active IRQs on your system.

Device Example: Roller Wheel

Now that you have learned the basics of interrupt handling, let's implement an interrupt handler for an example roller wheel device. Roller wheels can be found on some phones and PDAs for easy menu navigation and are capable of three movements: clockwise rotation, anticlockwise rotation, and key-press. Our imaginary roller wheel is wired such that any of these movements interrupt the processor on IRQ 7. Three low order bits of General Purpose I/O (GPIO) Port D of the processor are connected to the roller device. The waveforms generated on these pins corresponding to different wheel movements are shown in Figure 4.3. The job of the interrupt handler is to decipher the wheel movements by looking at the Port D GPIO data register.

Figure 4.3. Sample wave forms generated by the roller wheel.


The driver has to first request the IRQ and associate an interrupt handler with it:

#define ROLLER_IRQ  7
static irqreturn_t roller_interrupt(int irq, void *dev_id);


if (request_irq(ROLLER_IRQ, roller_interrupt, IRQF_DISABLED |
                IRQF_TRIGGER_RISING, "roll", NULL)) {
  printk(KERN_ERR "Roll: Can't register IRQ %d\n", ROLLER_IRQ);
  return -EIO;
}

Let's look at the arguments passed to request_irq(). The IRQ number is not queried or probed but hard-coded to ROLLER_IRQ in this simple case as per the hardware connection. The second argument, roller_interrupt(), is the interrupt handler routine. Its prototype specifies a return type of irqreturn_t, which can be IRQ_HANDLED if the interrupt is handled successfully or IRQ_NONE if it isn't. The return value assumes more significance for I/O technologies such as PCI, where multiple devices can share the same IRQ.

The IRQF_DISABLED flag specifies that this interrupt handler has to be treated as a fast handler, so the kernel has to disable interrupts while invoking the handler. IRQF_TRIGGER_RISING announces that the roller wheel generates a rising edge on the interrupt line when it wants to signal an interrupt. In other words, the roller wheel is an edge-sensitive device. Some devices are instead level-sensitive and keep the interrupt line asserted until the CPU services it. To flag an interrupt as level-sensitive, use the IRQF_TRIGGER_HIGH flag. Other possible values for this argument include IRQF_SAMPLE_RANDOM (used in the section, "Pseudo Char Drivers" in Chapter 5, "Character Drivers") and IRQF_SHARED (used to specify that this IRQ is shared among multiple devices).

The next argument, "roll", is used to identify this device in data generated by files such as /proc/interrupts. The final parameter, set to NULL in this case, is relevant only for shared interrupt handlers and is used to identify each device sharing the IRQ line.

Starting with the 2.6.19 kernel, there have been some changes to the interrupt handler interface. Interrupt handlers used to take a third argument (struct pt_regs *) that contained a pointer to CPU registers, but this has been removed starting with the 2.6.19 kernel. Also, the IRQF_xxx family of interrupt flags replaced the SA_xxx family. For example, with earlier kernels, you had to use SA_INTERRUPT rather than IRQF_DISABLED to mark an interrupt handler as fast.


Driver initialization is not a good place for requesting an IRQ because that can hog that valuable resource even when the device is not in use. So, device drivers usually request the IRQ when the device is opened by an application. Similarly, the IRQ is freed when the application closes the device and not while exiting the driver module. Freeing an IRQ is done as follows:

free_irq(int irq, void *dev_id);

Listing 4.1 shows the implementation of the roller interrupt handler. roller_interrupt() takes two arguments: the IRQ and the device identifier passed as the final argument to the associated request_irq(). Look at Figure 4.3 side by side with this listing.

Listing 4.1. The Roller Interrupt Handler

spinlock_t roller_lock = SPIN_LOCK_UNLOCKED;
static DECLARE_WAIT_QUEUE_HEAD(roller_poll);

static irqreturn_t
roller_interrupt(int irq, void *dev_id)
{
  int i, PA_t, PA_delta_t, movement = 0;

  /* Get the waveforms from bits 0, 1 and 2
     of Port D as shown in Figure 4.3 */
  PA_t = PORTD & 0x07;

  /* Wait until the state of the pins change.
     (Add some timeout to the loop) */
  for (i=0; (PA_t==PA_delta_t); i++){
    PA_delta_t = PORTD & 0x07;
  }

  movement = determine_movement(PA_t, PA_delta_t); /* See below */

  spin_lock(&roller_lock);

  /* Store the wheel movement in a buffer for
     later access by the read()/poll() entry points */
  store_movements(movement);

  spin_unlock(&roller_lock);

  /* Wake up the poll entry point that might have
     gone to sleep, waiting for a wheel movement */
  wake_up_interruptible(&roller_poll);

  return IRQ_HANDLED;
}
int
determine_movement(int PA_t, int PA_delta_t)
{
  switch (PA_t){
    case 0:
      switch (PA_delta_t){
      case 1:
        movement = ANTICLOCKWISE;
        break;
      case 2:
        movement = CLOCKWISE;
        break;
      case 4:
        movement = KEYPRESSED;
        break;
      }
      break;
    case 1:
      switch (PA_delta_t){
      case 3:
        movement = ANTICLOCKWISE;
        break;
      case 0:
        movement = CLOCKWISE;
        break;
      }
      break;
    case 2:
      switch (PA_delta_t){
      case 0:
        movement = ANTICLOCKWISE;
        break;
      case 3:
        movement = CLOCKWISE;
        break;
      }
      break;
    case 3:
      switch (PA_delta_t){
      case 2:
        movement = ANTICLOCKWISE;
        break;
      case 1:
        movement = CLOCKWISE;
        break;
      }
    case 4:
      movement = KEYPRESSED;
      break;
  }
}

					  

 

Driver entry points such as read() and poll() operate in tandem with roller_interrupt(). For example, when the handler deciphers wheel movement, it wakes up any waiting poll() threads that may have gone to sleep in response to a select() system call issued by an application such as X Windows. Revisit Listing 4.1 and implement the complete roller driver after learning the internals of character drivers in Chapter 5.

Listing 7.3 in Chapter 7, "Input Drivers," takes advantage of the kernel's input interface to convert this roller wheel into a roller mouse.

Let's end this section by introducing some functions that enable and disable interrupts on a particular IRQ. enable_irq(ROLLER_IRQ) enables interrupt generation when the roller wheel moves, while disable_irq(ROLLER_IRQ) does the reverse. disable_irq_nosync(ROLLER_IRQ) disables roller interrupts but does not wait for any currently executing instance of roller_interrupt() to return. This nosync flavor of disable_irq() is faster but can potentially cause race conditions. Use this only when you know that there can be no races. An example user of disable_irq_nosync() is drivers/ide/ide-io.c, which blocks interrupts during initialization, because some systems have trouble with that.

Softirqs and Tasklets

As discussed previously, interrupt handlers have two conflicting requirements: They are responsible for the bulk of device data processing, but they have to exit as fast as possible. To bail out of this situation, interrupt handlers are designed in two parts: a hurried and harried top half that interacts with the hardware, and a relaxed bottom half that does most of the processing with all interrupts enabled. Unlike interrupts, bottom halves are synchronous because the kernel decides when to execute them. The following mechanisms are available in the kernel to defer work to a bottom half: softirqs, tasklets, and work queues.

Softirqs are the basic bottom half mechanism and have strong locking requirements. They are used only by a few performance-sensitive subsystems such as the networking layer, SCSI layer, and kernel timers. Tasklets are built on top of softirqs and are easier to use. It's recommended to use tasklets unless you have crucial scalability or speed requirements. A primary difference between a softirq and a tasklet is that the former is reentrant whereas the latter isn't. Different instances of a softirq can run simultaneously on different processors, but that is not the case with tasklets.

To illustrate the usage of softirqs and tasklets, assume that the roller wheel in the previous example has inherent hardware problems due to the presence of moving parts (say, the wheel gets stuck occasionally) resulting in the generation of out-of-spec waveforms. A stuck wheel can continuously generate spurious interrupts and potentially freeze the system. To get around this problem, capture the wave stream, run some analysis on it, and dynamically switch from interrupt mode to a polled mode if the wheel looks stuck, and vice versa if it's unstuck. Capture the wave stream from the interrupt handler and perform the analysis from a bottom half. Listing 4.2 implements this using softirqs, and Listing 4.3 uses tasklets. Both are simplified variants of Listing 4.1. This reduces the handler to two functions: roller_capture() that obtains a wave snippet from GPIO Port D, and roller_analyze() that runs an algorithmic analysis on the wave and switches to polled mode if required.

Listing 4.2. Using Softirqs to Offload Work from Interrupt Handlers

void __init
roller_init()
{
  /* ... */

  /* Open the softirq. Add an entry for ROLLER_SOFT_IRQ in
     the enum list in include/linux/interrupt.h */
  open_softirq(ROLLER_SOFT_IRQ, roller_analyze, NULL);
}


/* The bottom half */
void
roller_analyze()
{
  /* Analyze the waveforms and switch to polled mode if required */
}
/* The interrupt handler */
static irqreturn_t
roller_interrupt(int irq, void *dev_id)
{
  /* Capture the wave stream */
  roller_capture();

  /* Mark softirq as pending */
  raise_softirq(ROLLER_SOFT_IRQ);

  return IRQ_HANDLED;
}

					  

To define a softirq, you have to statically add an entry to include/linux/interrupt.h. You can't define one dynamically. raise_softirq() announces that the corresponding softirq is pending execution. The kernel will execute it at the next available opportunity. This can be during exit from an interrupt handler or via the ksoftirqd kernel thread.

Listing 4.3. Using Tasklets to Offload Work from Interrupt Handlers

struct roller_device_struct { /* Device-specific structure */
  /* ... */
  struct tasklet_struct tsklt;
  /* ... */
}

void __init roller_init()
{
  struct roller_device_struct *dev_struct;
  /* ... */

  /* Initialize tasklet */
  tasklet_init(&dev_struct->tsklt, roller_analyze, dev);
}


/* The bottom half */
void
roller_analyze()
{
/* Analyze the waveforms and switch to
   polled mode if required */
}
/* The interrupt handler */
static irqreturn_t
roller_interrupt(int irq, void *dev_id)
{
  struct roller_device_struct *dev_struct;

  /* Capture the wave stream */
  roller_capture();

  /* Mark tasklet as pending */
  tasklet_schedule(&dev_struct->tsklt);

  return IRQ_HANDLED;
}

					  

tasklet_init() dynamically initializes a tasklet. The function does not allocate memory for a tasklet_struct, rather you have to pass the address of an allocated one. tasklet_schedule() announces that the corresponding tasklet is pending execution. Like for interrupts, the kernel offers a bunch of functions to control the execution state of tasklets on systems having multiple processors:

You have seen the differences between interrupt handlers and bottom halves, but there are a few similarities, too. Interrupt handlers and tasklets are both not reentrant. And neither of them can go to sleep. Also, interrupt handlers, tasklets, and softirqs cannot be preempted.

Work queues are a third way to defer work from interrupt handlers. They execute in process context and are allowed to sleep, so they can use drowsy functions such as mutexes. We discussed work queues in the preceding chapter when we looked at various kernel helper facilities. Table 4.1 compares softirqs, tasklets, and work queues.

Table 4.1. Comparing Softirqs, Tasklets, and Work Queues
 SoftirqsTaskletsWork Queues
Execution contextDeferred work runs in interrupt context.Deferred work runs in interrupt context.Deferred work runs in process context.
ReentrancyCan run simultaneously on different CPUs.Cannot run simultaneously on different CPUs. Different CPUs can run different tasklets, however.Can run simultaneously on different CPUs.
Sleep semanticsCannot go to sleep.Cannot go to sleep.May go to sleep.
PreemptionCannot be preempted/scheduled.Cannot be preempted/scheduled.May be preempted/scheduled.
Ease of useNot easy to use.Easy to use.Easy to use.
When to useIf deferred work will not go to sleep and if you have crucial scalability or speed requirements.If deferred work will not go to sleep.If deferred work may go to sleep.


There is an ongoing debate in LKML on the feasibility of getting rid of the tasklet interface. Tasklets enjoy more priority than process context code, so they present latency problems. Moreover, as you learned, they are constrained not to sleep and to execute on the same CPU. It's being suggested that all existing tasklets be converted to softirqs or work queues on a case-by-case basis.

The –rt patch-set alluded to in Chapter 2 moves interrupt handling to kernel threads to achieve wider preemption coverage.


Previous Page Next Page