锁类型及其规则¶
简介¶
内核提供了多种锁定原语,可以分为三类
睡眠锁
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 内核上存在限制。
即使在 PREEMPT_RT 内核上,PI 显然也不能抢占禁用抢占或禁用中断的代码区域。相反,PREEMPT_RT 内核在可抢占的任务上下文中执行大多数此类代码区域,特别是中断处理程序和软中断。这种转换允许通过 RT-mutex 实现 spinlock_t 和 rwlock_t。
semaphore¶
semaphore 是一个计数信号量实现。
信号量通常用于序列化和等待,但新的用例应使用单独的序列化和等待机制,例如互斥锁和完成。
信号量和 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。当临界区很小时,有时也可以使用 raw_spinlock_t,从而避免 RT-mutex 开销。
spinlock_t¶
spinlock_t 的语义随 PREEMPT_RT 的状态而变化。
在非 PREEMPT_RT 内核上,spinlock_t 映射到 raw_spinlock_t 并具有完全相同的语义。
spinlock_t 和 PREEMPT_RT¶
在 PREEMPT_RT 内核上,spinlock_t 映射到基于 rt_mutex 的单独实现,这会改变语义
抢占未禁用。
自旋锁/自旋解锁操作(_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¶
在 PREEMPT_RT 内核上将 local_lock 映射到 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() 的调用者。此外,在 PREEMPT_RT 内核上会触发 lockdep 断言,因为 local_lock_irqsave() 由于 spinlock_t 的 PREEMPT_RT 特定语义而不会禁用中断。正确的替换是
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,这意味着在持有原始自旋锁时无法获取它们。这导致以下嵌套顺序
睡眠锁
spinlock_t、rwlock_t、local_lock
raw_spinlock_t 和位自旋锁
如果违反这些约束,Lockdep 会发出抱怨,无论是在 PREEMPT_RT 还是其他情况下。