英语

锁的教训

教训 1:自旋锁

最基本的锁定原语是自旋锁

static DEFINE_SPINLOCK(xxx_lock);

      unsigned long flags;

      spin_lock_irqsave(&xxx_lock, flags);
      ... critical section here ..
      spin_unlock_irqrestore(&xxx_lock, flags);

以上操作始终是安全的。它会 _局部地_ 禁用中断,但自旋锁本身会保证全局锁,因此它会保证在由该锁保护的区域内只有一个控制线程。即使在 UP 下也能很好地工作,因此代码 _不必_ 担心 UP 与 SMP 的问题:自旋锁在两者下都能正确工作。

注意!自旋锁对内存的影响在以下文档中进一步描述:

Documentation/memory-barriers.txt

  1. 获取(ACQUIRE)操作。

  2. 释放(RELEASE)操作。

以上通常非常简单(对于大多数情况,您通常只需要一个自旋锁 - 使用多个自旋锁会使事情变得更加复杂甚至更慢,通常只有在您 **知道** 需要拆分序列时才值得这样做:如果您不确定,请不惜一切代价避免这样做)。

这确实是关于自旋锁唯一真正困难的部分:一旦您开始使用自旋锁,它们往往会扩展到您之前可能没有注意到的区域,因为您必须确保自旋锁在 **所有** 使用它们的共享数据结构的地方都正确保护了它们。自旋锁最容易添加到完全独立于其他代码的地方(例如,其他任何人都不会触及的内部驱动程序数据结构)。

注意!只有当您 **也** 使用锁本身在 CPU 之间进行锁定时,自旋锁才是安全的,这意味着接触共享变量的 **所有** 内容都必须同意他们想要使用的自旋锁。


教训 2:读者-写者自旋锁。

如果您的数据访问具有非常自然的模式,即您通常倾向于主要从共享变量读取,那么自旋锁的读者-写者锁(rw_lock)版本有时会很有用。它们允许多个读者同时处于同一个临界区,但是如果有人想更改变量,则必须获得独占的写锁。

注意!读者-写者锁比简单的自旋锁需要更多的原子内存操作。除非读者临界区很长,否则最好只使用自旋锁。

这些例程看起来与上面相同

rwlock_t xxx_lock = __RW_LOCK_UNLOCKED(xxx_lock);

     unsigned long flags;

     read_lock_irqsave(&xxx_lock, flags);
     .. critical section that only reads the info ...
     read_unlock_irqrestore(&xxx_lock, flags);

     write_lock_irqsave(&xxx_lock, flags);
     .. read and write exclusive access to the info ...
     write_unlock_irqrestore(&xxx_lock, flags);

以上类型的锁可能对于复杂的的数据结构(如链表)很有用,尤其是在不更改列表本身的情况下搜索条目时。读锁允许多个并发读取器。任何 **更改** 列表的操作都必须获得写锁。

注意!RCU 更适合列表遍历,但需要仔细注意设计细节(请参阅 使用 RCU 保护主要用于读取的链表)。

此外,您不能将读锁“升级”为写锁,因此如果您在 _任何_ 时候需要进行任何更改(即使您不是每次都这样做),您都必须在最开始时获得写锁。

注意!我们正在努力在大多数情况下删除读者-写者自旋锁,因此请在没有达成共识的情况下不要添加新的自旋锁。(相反,请参阅 RCU 概念 以获取完整信息。)


教训 3:重新审视自旋锁。

上面的单个自旋锁原语绝不是唯一的原语。它们是最安全的,也是在所有情况下都能工作的原语,但部分 **因为** 它们是安全的,所以它们也相当慢。它们比它们需要的慢,因为它们必须禁用中断(这在 x86 上只是一条指令,但它是一条代价高昂的指令 - 在其他架构上可能会更糟)。

如果您需要在多个 CPU 上保护数据结构,并且想要使用自旋锁,则可以潜在地使用更便宜的自旋锁版本。如果您知道自旋锁永远不会在中断处理程序中使用,则可以使用非 irq 版本

spin_lock(&lock);
...
spin_unlock(&lock);

(当然还有等效的读写版本)。自旋锁将保证相同的独占访问,并且速度会快得多。如果您知道所讨论的数据仅从“进程上下文”中操作,即不涉及中断,则此操作很有用。

如果您有与自旋锁一起使用的中断,则不能使用这些版本的原因是您可能会陷入死锁

spin_lock(&lock);
...
        <- interrupt comes in:
                spin_lock(&lock);

其中一个中断尝试锁定一个已锁定的变量。如果另一个中断发生在另一个 CPU 上,这是可以的,但如果中断发生在已持有锁的同一 CPU 上,这是 _不可以的_,因为锁显然永远不会被释放(因为中断正在等待锁,并且锁的持有者被中断中断,并且在中断处理完毕之前不会继续)。

(这也是 irq 版本的自旋锁只需要禁用 _本地_ 中断的原因 - 在其他 CPU 上的中断中使用自旋锁是可以的,因为另一个 CPU 上的中断不会中断持有锁的 CPU,因此锁持有者可以继续并最终释放锁)。

Linus


参考信息:

对于动态初始化,请根据需要使用 spin_lock_init() 或 rwlock_init()

spinlock_t xxx_lock;
rwlock_t xxx_rw_lock;

static int __init xxx_init(void)
{
     spin_lock_init(&xxx_lock);
     rwlock_init(&xxx_rw_lock);
     ...
}

module_init(xxx_init);

对于静态初始化,请根据需要使用 DEFINE_SPINLOCK() / DEFINE_RWLOCK() 或 __SPIN_LOCK_UNLOCKED() / __RW_LOCK_UNLOCKED()。