锁类型及其规则¶
简介¶
内核提供多种锁定原语,可分为三类
睡眠锁
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 意味着它们不能在持有原始自旋锁时获取。 这导致以下嵌套顺序
睡眠锁
spinlock_t、rwlock_t、local_lock
raw_spinlock_t 和位自旋锁
如果违反这些约束,Lockdep 将会发出抱怨,无论是 PREEMPT_RT 还是其他情况。