May 30, 2013 — My current understanding of spin locks and interrupt handlers in the linux kernel

May 30, 2013 — What I’ve learned about spinlocks and interrupts in the linux kernel lately.

Caveat: My understanding is probably still incomplete and this may contain mistakes or wrong information. But this is my current understanding.

TL;DR: It’s ok to use spin_lock() in interrupt handlers so long as you never call your interrupt handler from anywhere in your driver since linux guarantees not to call interrupts re-entrantly on the same cpu. See also the table at the bottom of this page which tells you how to decide which of spin_lock(), spin_lock_irq() or spin_lock_irqsave() you may safely use under what circumstances.

Documentation/spinlocks.txt says:

  8<------8<-------8< ------8<-------8< ------8<-------
  [...] IFF you know that the spinlocks are never used in interrupt
  handlers, you can use the non-irq versions:
  
      spin_lock(&lock);
      ...
      spin_unlock(&lock);
  
  [...]
  
  The reasons you mustn't use these versions [non-irq versions] if
  you have interrupts that play with the spinlock is that you can
  get deadlocks:
  
      spin_lock(&lock);
      ...
              <- interrupt comes in:
                      spin_lock(&lock);
  
  where an interrupt tries to lock an already locked variable. This is ok if
  the other interrupt happens on another CPU, but it is _not_ ok if the
  interrupt happens on the same CPU that already holds the lock, because the
  lock will obviously never be released (because the interrupt is waiting
  for the lock, and the lock-holder is interrupted by the interrupt and will
  not continue until the interrupt has been processed).
  8<------8<-------8< ------8<-------8< ------8<-------

Note the “IFF” in the first line above. I take that to mean “if and only if”, in the mathematical sense, which means the implication goes both ways, and which would imply that you cannot ever use spin_lock() in an interrupt handler, but must always use spin_lock_irqsave() in interrupt handlers if you need a spin lock in an interrupt handler.

However, if you look at some driver code, you will find examples of interrupt handlers using spin_lock(). So, what gives?

One thing which is left unstated in the above example is that the first spin_lock(&lock) in the above example is assumed to be in process context and not in the interrupt handler. It is also assumed that you know that an interrupt handler won’t be re-entered by the same interrupt again while in progress.

If you don’t know that, and don’t realize the first spin_lock() is not in the interrupt handler, it’s easy to read the above example as an interrupt handler deadlocking with itself, and there’s nothing I can see in spinlocks.txt that will lead you away from this mis-reading once you’ve made that mistake.

So anyway, I wrote Matthew Wilcox (nvme driver author) and asked him about this since nvme uses spin_lock() inside the interrupt handler (as do several other drivers), and he informed me that linux won’t re-enter an interrupt handler on the same interrupt, and pointed out the code that makes this so:

  > kernel/irq/handle.c:
  > 
  > irqreturn_t handle_irq_event(struct irq_desc *desc)
  > {
  >        	irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);
  > ... 
  > 
  > and in handle_*_irq:
  > 
  >     	if (unlikely(irqd_irq_inprogress(&desc->irq_data)))
  >                 if (!irq_check_poll(desc))
  >                         goto out; 
  > 

Which matched my empirical results when I had tried experimentally to get an interrupt to re-enter itself, but it was good to have the code that guarantees that this will work and isn’t just dumb luck pointed out.

So I think a safe set of rules that can be used to determine which kind of spin lock you can use at any given point in the code are as follows:

Which variant of spin_(un)lock you may use in a particular bit of code depends on two things:

  1. In what contexts are the data you are protecting with the lock accessed, considering all access points in the code?

    Possible answers: Data is accessed from

    • A: only from interrupt context,
    • B: only from process context,
    • C: both process and interrupt contexts.
  2. 2. In what context is this particular bit of code that is taking or releasing the lock executing?

    Possible answers are:

    • X. interrupt context
    • Y. process context
    • Z. sometimes interrupt context, sometimes process context, and you do not know which ahead of time.

Considering the above, for any particular bit of code that is taking a lock the weakest spin lock variant you may safely get away with is:

--------- X --------------- Y -------------- Z -------------
A    |    spin_lock    |    n/a           |  n/a
B    |    n/a          |    spin_lock     |  n/a
C    |    spin_lock    |    spin_lock_irq |  spin_lock_irqsave
--------- X --------------- Y -------------- Z -------------

Also, it may be worth while to note that a pattern in which the interrupt handler is polled from process context code can often be refactored to avoid requiring use of spin_lock_irqsave().

If you have something like this:

  void some_process_context_function()
  {
          blah();
          interrupt_handler();
          blah();
  }
  
  void interrupt_handler() 
  {
          spin_lock_irqsave();
          do_some_stuff();
          do_some_other_stuff();
          spin_unlock_irqrestore();
  }

You can sometimes re-arrange it like this:

  void interrupt_handler_guts()
  {
          do_some_stuff();
          do_some_other_stuff();
  }
  
  void some_process_context_function()
  {
          blah();
          spin_lock_irq();
          interrupt_handler_guts();
          spin_unlock_irq();
          blah();
  }
  
  void interrupt_handler()
  {
          spin_lock();
          interrupt_handler_guts();
          spin_unlock();
  }

Note that there are still some subtleties to be aware of. For instance, if your hardware has multiple MSI-X vectors targeting the same interrupt handler, and you have a single common lock all those vectors use (that is, not per-vector locks) for that sort of case I’d think you would need spin_lock_irqsave() in the interrupt handler. as in that case, your interrupt handler could deadlock itself on a single cpu with different vectors calling concurrently into the same handler trying to obtain the same lock. Generally though, the point of having the multiple vectors would be to get more parallelism, and so you’d probably have per-vector locks.

~ by scaryreasoner on May 30, 2013.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: