英语

锁类型及其规则

简介

内核提供多种锁定原语,可分为三类

  • 睡眠锁

  • CPU 本地锁

  • 自旋锁

本文档从概念上描述了这些锁类型,并提供了它们的嵌套规则,包括 PREEMPT_RT 下使用的规则。

锁的类别

睡眠锁

睡眠锁只能在可抢占任务上下文中获取。

尽管实现允许从其他上下文调用 try_lock(),但也有必要仔细评估 unlock() 以及 try_lock() 的安全性。 此外,还有必要评估这些原语的调试版本。 简而言之,除非没有其他选择,否则不要从其他上下文获取睡眠锁。

睡眠锁类型

  • mutex

  • rt_mutex

  • semaphore

  • rw_semaphore

  • ww_mutex

  • percpu_rw_semaphore

在 PREEMPT_RT 内核上,这些锁类型会转换为睡眠锁

  • local_lock

  • spinlock_t

  • rwlock_t

CPU 本地锁

  • local_lock

在非 PREEMPT_RT 内核上,local_lock 函数是围绕抢占和中断禁用原语的包装器。与其他锁定机制相反,禁用抢占或中断是纯 CPU 本地并发控制机制,不适合于 CPU 间的并发控制。

自旋锁

  • raw_spinlock_t

  • 位自旋锁

在非 PREEMPT_RT 内核上,这些锁类型也是自旋锁

  • spinlock_t

  • rwlock_t

自旋锁隐式地禁用抢占,并且锁/解锁函数可以具有应用进一步保护的后缀

_bh()

禁用/启用下半部(软中断)

_irq()

禁用/启用中断

_irqsave/restore()

保存并禁用/恢复中断禁用状态

所有者语义

除了信号量之外,上述锁类型都具有严格的所有者语义

获取锁的上下文(任务)必须释放它。

rw_semaphores 有一个特殊的接口,允许非所有者为读者释放锁。

rtmutex

RT-mutex 是支持优先级继承 (PI) 的互斥锁。

由于抢占和中断禁用部分,PI 在非 PREEMPT_RT 内核上存在限制。

PI 显然不能抢占代码的禁用抢占或禁用中断区域,即使在 PREEMPT_RT 内核上也是如此。 相反,PREEMPT_RT 内核在可抢占任务上下文中执行大多数此类代码区域,特别是中断处理程序和软中断。 此转换允许通过 RT-mutex 实现 spinlock_t 和 rwlock_t。

semaphore

semaphore 是一种计数信号量实现。

信号量通常用于序列化和等待,但新的用例应改用单独的序列化和等待机制,例如互斥锁和完成量。

semaphores 和 PREEMPT_RT

PREEMPT_RT 不会更改信号量实现,因为计数信号量没有所有者的概念,因此阻止 PREEMPT_RT 为信号量提供优先级继承。 毕竟,无法提升未知所有者。 因此,阻塞信号量可能会导致优先级反转。

rw_semaphore

rw_semaphore 是一种多读者单写者锁定机制。

在非 PREEMPT_RT 内核上,该实现是公平的,因此可以防止写者饥饿。

rw_semaphore 默认情况下符合严格的所有者语义,但存在允许非所有者为读者释放的专用接口。 这些接口独立于内核配置工作。

rw_semaphore 和 PREEMPT_RT

PREEMPT_RT 内核将 rw_semaphore 映射到单独的基于 rt_mutex 的实现,从而改变了公平性

由于 rw_semaphore 写者无法将其优先级授予多个读者,因此被抢占的低优先级读者将继续持有其锁,从而甚至使高优先级写者饥饿。 相比之下,由于读者可以将其优先级授予写者,因此被抢占的低优先级写者的优先级将被提升,直到它释放锁,从而防止该写者使读者饥饿。

local_lock

local_lock 为受禁用抢占或中断保护的关键部分提供了一个命名范围。

在非 PREEMPT_RT 内核上,local_lock 操作映射到抢占和中断禁用和启用原语

local_lock(&llock)

preempt_disable()

local_unlock(&llock)

preempt_enable()

local_lock_irq(&llock)

local_irq_disable()

local_unlock_irq(&llock)

local_irq_enable()

local_lock_irqsave(&llock)

local_irq_save()

local_unlock_irqrestore(&llock)

local_irq_restore()

local_lock 的命名范围比常规原语有两个优点

  • 锁名称允许静态分析,并且也是保护范围的明确文档,而常规原语是无范围且不透明的。

  • 如果启用了 lockdep,则 local_lock 会获得一个锁映射,该锁映射允许验证保护的正确性。 这可以检测到诸如从中断或软中断上下文调用使用 preempt_disable() 作为保护机制的函数的情况。 除此之外,lockdep_assert_held(&llock) 的工作方式与其他任何锁定原语一样。

local_lock 和 PREEMPT_RT

PREEMPT_RT 内核将 local_lock 映射到每个 CPU 的 spinlock_t,从而改变了语义

  • 所有 spinlock_t 更改也适用于 local_lock。

local_lock 用法

在禁用抢占或中断是适当的并发控制形式以在非 PREEMPT_RT 内核上保护每个 CPU 的数据结构的情况下,应使用 local_lock。

由于 PREEMPT_RT 特定的 spinlock_t 语义,local_lock 不适合于在 PREEMPT_RT 内核上防止抢占或中断。

raw_spinlock_t 和 spinlock_t

raw_spinlock_t

raw_spinlock_t 是所有内核(包括 PREEMPT_RT 内核)中的严格自旋锁实现。 仅在真正的关键核心代码、低级中断处理以及需要禁用抢占或中断(例如,安全访问硬件状态)的地方使用 raw_spinlock_t。 当关键部分很小,从而避免 RT-mutex 开销时,有时也可以使用 raw_spinlock_t。

spinlock_t

spinlock_t 的语义随着 PREEMPT_RT 的状态而变化。

在非 PREEMPT_RT 内核上,spinlock_t 映射到 raw_spinlock_t 并且具有完全相同的语义。

spinlock_t 和 PREEMPT_RT

在 PREEMPT_RT 内核上,spinlock_t 映射到基于 rt_mutex 的单独实现,这改变了语义

  • 抢占未禁用。

  • 用于 spin_lock / spin_unlock 操作的硬中断相关后缀(_irq、_irqsave / _irqrestore)不会影响 CPU 的中断禁用状态。

  • 软中断相关后缀 (_bh()) 仍然禁用软中断处理程序。

    非 PREEMPT_RT 内核禁用抢占以获得此效果。

    PREEMPT_RT 内核使用每个 CPU 的锁进行序列化,从而保持启用抢占。 该锁禁用软中断处理程序,并防止由于任务抢占而导致的重入。

PREEMPT_RT 内核保留所有其他 spinlock_t 语义

  • 持有 spinlock_t 的任务不会迁移。 非 PREEMPT_RT 内核通过禁用抢占来避免迁移。 PREEMPT_RT 内核改为禁用迁移,这可确保即使任务被抢占,指向每个 CPU 变量的指针仍然有效。

  • 任务状态在获取自旋锁期间保持不变,确保任务状态规则适用于所有内核配置。 非 PREEMPT_RT 内核保持任务状态不变。 但是,如果任务在获取期间阻塞,PREEMPT_RT 必须更改任务状态。 因此,它会在阻塞之前保存当前任务状态,并且相应的锁唤醒会恢复它,如下所示

    task->state = TASK_INTERRUPTIBLE
     lock()
       block()
         task->saved_state = task->state
         task->state = TASK_UNINTERRUPTIBLE
         schedule()
                                        lock wakeup
                                          task->state = task->saved_state
    

    其他类型的唤醒通常会无条件地将任务状态设置为 RUNNING,但这在这里不起作用,因为任务必须保持阻塞状态,直到锁可用为止。 因此,当非锁唤醒尝试唤醒阻塞等待自旋锁的任务时,它会改为将保存的状态设置为 RUNNING。 然后,当锁获取完成时,锁唤醒会将任务状态设置为保存的状态,在这种情况下将其设置为 RUNNING

    task->state = TASK_INTERRUPTIBLE
     lock()
       block()
         task->saved_state = task->state
         task->state = TASK_UNINTERRUPTIBLE
         schedule()
                                        non lock wakeup
                                          task->saved_state = TASK_RUNNING
    
                                        lock wakeup
                                          task->state = task->saved_state
    

    这可确保不会丢失真正的唤醒。

rwlock_t

rwlock_t 是一种多读者单写者锁定机制。

非 PREEMPT_RT 内核将 rwlock_t 实现为自旋锁,并且 spinlock_t 的后缀规则相应地适用。 该实现是公平的,因此可以防止写者饥饿。

rwlock_t 和 PREEMPT_RT

PREEMPT_RT 内核将 rwlock_t 映射到单独的基于 rt_mutex 的实现,从而改变了语义

  • 所有 spinlock_t 更改也适用于 rwlock_t。

  • 由于 rwlock_t 写者无法将其优先级授予多个读者,因此被抢占的低优先级读者将继续持有其锁,从而甚至使高优先级写者饥饿。 相比之下,由于读者可以将其优先级授予写者,因此被抢占的低优先级写者的优先级将被提升,直到它释放锁,从而防止该写者使读者饥饿。

PREEMPT_RT 注意事项

RT 上的 local_lock

local_lock 到 PREEMPT_RT 内核上的 spinlock_t 的映射有一些含义。 例如,在非 PREEMPT_RT 内核上,以下代码序列按预期工作

local_lock_irq(&local_lock);
raw_spin_lock(&lock);

并且与以下代码完全等效

raw_spin_lock_irq(&lock);

在 PREEMPT_RT 内核上,此代码序列会中断,因为 local_lock_irq() 映射到既不禁用中断也不禁用抢占的每个 CPU 的 spinlock_t。 以下代码序列在 PREEMPT_RT 和非 PREEMPT_RT 内核上都能完美地工作

local_lock_irq(&local_lock);
spin_lock(&lock);

local 锁的另一个注意事项是每个 local_lock 都有一个特定的保护范围。 因此,以下替换是错误的

func1()
{
  local_irq_save(flags);    -> local_lock_irqsave(&local_lock_1, flags);
  func3();
  local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock_1, flags);
}

func2()
{
  local_irq_save(flags);    -> local_lock_irqsave(&local_lock_2, flags);
  func3();
  local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock_2, flags);
}

func3()
{
  lockdep_assert_irqs_disabled();
  access_protected_data();
}

在非 PREEMPT_RT 内核上,这可以正常工作,但在 PREEMPT_RT 内核上,local_lock_1 和 local_lock_2 是不同的,并且无法序列化 func3() 的调用者。 此外,lockdep 断言将在 PREEMPT_RT 内核上触发,因为由于 spinlock_t 的 PREEMPT_RT 特定语义,local_lock_irqsave() 不会禁用中断。 正确的替换是

func1()
{
  local_irq_save(flags);    -> local_lock_irqsave(&local_lock, flags);
  func3();
  local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock, flags);
}

func2()
{
  local_irq_save(flags);    -> local_lock_irqsave(&local_lock, flags);
  func3();
  local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock, flags);
}

func3()
{
  lockdep_assert_held(&local_lock);
  access_protected_data();
}

spinlock_t 和 rwlock_t

PREEMPT_RT 内核上 spinlock_t 和 rwlock_t 语义的更改有一些含义。 例如,在非 PREEMPT_RT 内核上,以下代码序列按预期工作

local_irq_disable();
spin_lock(&lock);

并且与以下代码完全等效

spin_lock_irq(&lock);

同样适用于 rwlock_t 和 _irqsave() 后缀变体。

在 PREEMPT_RT 内核上,此代码序列会中断,因为 RT-mutex 需要完全可抢占的上下文。 而是使用 spin_lock_irq() 或 spin_lock_irqsave() 及其解锁对应项。 如果中断禁用和锁定必须保持分离,PREEMPT_RT 会提供一种 local_lock 机制。 获取 local_lock 会将任务固定到 CPU,从而允许获取每个 CPU 的中断禁用锁之类的东西。 但是,仅应在绝对必要时使用此方法。

典型的场景是线程上下文中每个 CPU 变量的保护

struct foo *p = get_cpu_ptr(&var1);

spin_lock(&p->lock);
p->count += this_cpu_read(var2);

这是非 PREEMPT_RT 内核上的正确代码,但在 PREEMPT_RT 内核上,这会中断。spinlock_t 语义的 PREEMPT_RT 特定更改不允许获取 p->lock,因为 get_cpu_ptr() 隐式禁用抢占。 以下替换适用于两个内核

struct foo *p;

migrate_disable();
p = this_cpu_ptr(&var1);
spin_lock(&p->lock);
p->count += this_cpu_read(var2);

migrate_disable() 确保任务固定在当前 CPU 上,这反过来保证了对 var1 和 var2 的每个 CPU 的访问在任务保持可抢占的同时停留在同一个 CPU 上。

migrate_disable() 替换对于以下场景无效

func()
{
  struct foo *p;

  migrate_disable();
  p = this_cpu_ptr(&var1);
  p->val = func2();

这会中断,因为 migrate_disable() 不能防止来自抢占任务的重入。 这种情况的正确替换是

func()
{
  struct foo *p;

  local_lock(&foo_lock);
  p = this_cpu_ptr(&var1);
  p->val = func2();

在非 PREEMPT_RT 内核上,这通过禁用抢占来防止重入。 在 PREEMPT_RT 内核上,这通过获取底层每个 CPU 的自旋锁来实现。

RT 上的 raw_spinlock_t

获取 raw_spinlock_t 会禁用抢占,并且可能还会禁用中断,因此关键部分必须避免获取常规 spinlock_t 或 rwlock_t,例如,关键部分必须避免分配内存。 因此,在非 PREEMPT_RT 内核上,以下代码可以完美地工作

raw_spin_lock(&lock);
p = kmalloc(sizeof(*p), GFP_ATOMIC);

但是此代码在 PREEMPT_RT 内核上会失败,因为内存分配器是完全可抢占的,因此无法从真正的原子上下文中调用。 但是,在保持正常的非原始自旋锁时调用内存分配器是完全可以的,因为它们不会在 PREEMPT_RT 内核上禁用抢占

spin_lock(&lock);
p = kmalloc(sizeof(*p), GFP_ATOMIC);

位自旋锁

PREEMPT_RT 无法替换位自旋锁,因为单个位太小,无法容纳 RT-mutex。 因此,位自旋锁的语义在 PREEMPT_RT 内核上得以保留,因此 raw_spinlock_t 的注意事项也适用于位自旋锁。

某些位自旋锁已使用条件 (#ifdef'ed) 代码更改在用法站点替换为常规 spinlock_t 以用于 PREEMPT_RT。 相比之下,spinlock_t 替换不需要用法站点更改。 而是,头文件和核心锁定实现中的条件允许编译器透明地进行替换。

锁类型嵌套规则

最基本的规则是

  • 相同锁类别(睡眠、CPU 本地、自旋)的锁类型可以任意嵌套,只要它们遵守常规锁排序规则以防止死锁即可。

  • 睡眠锁类型不能嵌套在 CPU 本地和自旋锁类型中。

  • CPU 本地和自旋锁类型可以嵌套在睡眠锁类型中。

  • 自旋锁类型可以嵌套在所有锁类型中

这些约束既适用于 PREEMPT_RT,也适用于其他情况。

PREEMPT_RT 将 spinlock_t 和 rwlock_t 的锁类别从自旋更改为睡眠,并将 local_lock 替换为每个 CPU 的 spinlock_t 意味着它们不能在持有原始自旋锁时获取。 这导致以下嵌套顺序

  1. 睡眠锁

  2. spinlock_t、rwlock_t、local_lock

  3. raw_spinlock_t 和位自旋锁

如果违反这些约束,Lockdep 将会发出抱怨,无论是 PREEMPT_RT 还是其他情况。