锁机制教程

第 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);

上述类型的锁对于链表等复杂数据结构可能会很有用,特别是用于在不改变链表本身的情况下搜索条目。读锁允许多个并发读者。任何会改变链表的操作都必须获取写锁。

NOTE! RCU is better for list traversal, but requires careful attention to design detail (see Using RCU to Protect Read-Mostly Linked Lists).

此外,你不能将读锁“升级”为写锁,因此如果你在_任何_时候需要进行任何修改(即使并非每次都这样做),你都必须在一开始就获取写锁。

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


第 3 课:再探自旋锁。

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

如果你遇到需要在多个 CPU 之间保护数据结构且想使用自旋锁的情况,你潜在地可以使用开销更小的自旋锁版本。前提是(IFF)你知道自旋锁从不在中断处理程序中使用,你可以使用非 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()