锁类型及其规则¶
简介¶
内核提供了多种锁原语,可分为三类
睡眠锁
CPU 本地锁
自旋锁
本文档在概念上描述了这些锁类型,并提供了其嵌套规则,包括在 PREEMPT_RT 下使用的规则。
锁类别¶
睡眠锁¶
睡眠锁只能在可抢占的任务上下文中获取。
尽管实现允许从其他上下文中调用 try_lock(),但有必要仔细评估 unlock() 以及 try_lock() 的安全性。此外,还有必要评估这些原语的调试版本。简而言之,除非没有其他选择,否则不要从其他上下文中获取睡眠锁。
睡眠锁类型
mutex
rt_mutex
信号量
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_semaphore 具有一个特殊的接口,允许读者进行非所有者释放。
rtmutex¶
RT-mutex 是支持优先级继承(PI)的互斥锁。
由于抢占和中断禁用区段的存在,PI 在非 PREEMPT_RT 内核上有局限性。
显然,即使在 PREEMPT_RT 内核上,PI 也无法抢占禁用抢占或禁用中断的代码区域。相反,PREEMPT_RT 内核在可抢占的任务上下文中执行绝大多数此类代码区域,尤其是中断处理程序和软中断。这种转换使得 spinlock_t 和 rwlock_t 可以通过 RT-mutex 来实现。
信号量¶
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 将获得一个锁映射(lockmap),从而可以验证保护的正确性。这可以检测出例如使用
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 内核上防范抢占或中断。
CPU 本地作用域与底半部¶
仅在软中断上下文中访问的每 CPU 变量不应依赖于以下假设:即该上下文由于不可抢占而受到隐式保护。在 PREEMPT_RT 内核中,软中断上下文是可抢占的,通过隐式上下文同步每个底半部禁用的区段会导致隐式的每 CPU “大内核锁”。
local_lock_t 连同用于加锁操作的 local_lock_nested_bh() 和 local_unlock_nested_bh() 有助于识别加锁作用域。
当启用 lockdep 时,这些函数会验证数据结构访问是否发生在软中断上下文中。与 local_lock() 不同,local_unlock_nested_bh() 不会禁用抢占,并且在不使用 lockdep 时不会增加开销。
在 PREEMPT_RT 内核上,local_lock_t 表现为真正的锁,并且 local_unlock_nested_bh() 对数据结构的访问进行串行化,这允许移除通过 local_bh_disable() 进行的串行化。
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 的独立实现,这会改变其语义
抢占不会被禁用。
用于 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¶
在 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_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_obj(*p, GFP_ATOMIC);
但此代码在 PREEMPT_RT 内核上会失败,因为内存分配器是完全可抢占的,因此不能从真正的原子上下文中调用。然而,在持有普通非 raw 自旋锁的同时调用内存分配器是完全没问题的,因为它们在 PREEMPT_RT 内核上不会禁用抢占
spin_lock(&lock);
p = kmalloc_obj(*p, GFP_ATOMIC);
位自旋锁¶
PREEMPT_RT 无法替代位自旋锁,因为单个位太小,无法容纳 RT-mutex。因此,位自旋锁的语义在 PREEMPT_RT 内核上得以保留,从而 raw_spinlock_t 的注意事项同样适用于位自旋锁。
为了 PREEMPT_RT,某些位自旋锁在用法处通过条件编译(#ifdef 保护)的代码更改被替换为常规 spinlock_t。相比之下,spinlock_t 的替换不需要在用法处进行更改。相反,头文件和核心锁实现中的条件编译使编译器能够透明地进行替换。
锁类型嵌套规则¶
最基本的规则是
只要它们遵守通用的锁排序规则以防止死锁,相同锁类别(睡眠、CPU 本地、自旋)的锁类型可以任意嵌套。
睡眠锁类型不能嵌套在 CPU 本地锁和自旋锁类型内部。
CPU 本地锁和自旋锁类型可以嵌套在睡眠锁类型内部。
自旋锁类型可以嵌套在所有锁类型内部
这些约束在 PREEMPT_RT 及其他情况下都适用。
PREEMPT_RT 将 spinlock_t 和 rwlock_t 的锁类别从自旋改为睡眠,并用每 CPU 的 spinlock_t 代替 local_lock,这意味着在持有 raw 自旋锁时无法获取它们。这导致了以下嵌套顺序
睡眠锁
spinlock_t, rwlock_t, local_lock
raw_spinlock_t 和位自旋锁
如果违反了这些约束,无论是在 PREEMPT_RT 还是其他情况下,Lockdep 都会报错。