介绍
内核提供了多种锁定原语,可以将其分为几类:
- 睡眠锁
- CPU本地锁
- 自旋锁
本文档从概念上描述了这些锁类型,并提供了它们的嵌套规则,包括在PREEMPT_RT
下使用的规则。
译者注:PREEMPT_RT是Linux内核的一个实时补丁,能让Linux变成一个实时操作系统。
锁类别
睡眠锁
只能在可抢占的任务上下文中获取睡眠锁。
尽管实现允许在其他上下文中使用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
- bit spinlocks
在非PREEMPT_RT内核上,这些锁类型也是自旋锁:
- spinlock_t
- rwlock_t
自旋锁隐式禁用抢占,并且加锁/解锁功能可以具有后缀,这些后缀可以提供进一步的保护:
后缀 | 作用 |
---|---|
_bh() | 启用/禁止下半部分(软中断) |
_irq() | 启用/禁止中断 |
_irqsave/restore() | 保存并禁用/恢复中断禁用状态 |
译者注:Linux的中断处理分为两个部分,上半部分和下半部分,前者简单快速,执行的时候禁止一些或全部中断,后者稍后执行,并且执行期间可以响应所有中断。这种设计可使系统处于中断屏蔽状态的时间尽可能的短,以此来提高系统的响应能力。
参考:http://unicornx.github.io/2016/04/19/20160419-lk-drv-th-bh/
所有者语义
除信号量外,上述锁类型具有严格的所有者语义:
- 获取锁的上下文(任务)必须释放它。
rw_semaphores具有一个特殊的接口,该接口允许非所有者释放读锁。
rt_mutex
rt_mutex是支持优先级继承 (PI) 的互斥锁。
由于存在抢占和中断禁用部分,因此PI在非PREEMPT_RT内核上具有局限性。
即使在PREEMPT_RT内核上,PI显然也不能抢占已禁用可抢占或中断的代码。 相反,PREEMPT_RT内核在可抢占的任务上下文中执行大多数此类代码区域,尤其是中断处理程序和软中断。 这种转换允许通过rt_mutex实现spinlock_t和rwlock_t。
semaphore
semaphore是一种计数信号量实现。
semaphore通常用于序列化和等待,但是新的用例应该使用单独的序列化和等待机制,例如mutex和completion。
semaphores and PREEMPT_RT
PREEMPT_RT不会更改信号量的实现,因为对信号量进行计数没有所有者的概念,从而阻止了PREEMPT_RT为信号量提供优先级继承。 毕竟,无法提升未知的所有者。 结果,阻塞信号量可能导致优先级倒置。
rw_semaphore
rw_semaphore是一种多reader和单writer锁定机制。
在非PREEMPT_RT内核上,实现是公平的,因此可以防止写入程序饥饿。
默认情况下,rw_semaphore遵循严格的所有者语义,但是存在一些特殊用途的接口,这些接口允许非所有者释放读锁。 这些接口独立于内核配置而工作。
rw_semaphore and PREEMPT_RT
PREEMPT_RT内核将rw_semaphore映射到单独的基于rt_mutex的实现,从而改变了公平性:
由于rw_semaphore writer无法将其优先级授予多个reader,因此被抢占的低优先级reader将继续保持其锁定状态,从而使高优先级writer饥饿。
相比之下,由于reader可以将其优先级授予writer,因此,抢占的低优先级writer将获得更高的优先级,直到释放锁定,从而防止该writer使reader挨饿。
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_save(&llock) | local_irq_save() |
local_lock_restore(&llock) | local_irq_save() |
与常规原语相比,local_lock的命名范围具有两个优点:
- 锁名称允许进行静态分析,并且在常规原语是无作用域且不透明的情况下,也是保护范围的清晰文档。
- 如果启用了lockdep,则local_lock将获得一个lockmap,该图可以验证保护的正确性。 这可以检测例如 从中断或软中断上下文中调用使用
preempt_disable()
作为保护机制的函数。 除了lockdep_assert_held(&llock)
以外,其他任何锁定原语都可以使用。
local_lock and PREEMPT_RT
PREEMPT_RT内核将local_lock映射到每个CPU的spinlock_t,从而更改了语义:
所有spinlock_t更改也适用于local_lock。
local_lock 使用
在禁用抢占或中断是并发控制的适当形式的情况下,应使用local_lock来保护非PREEMPT_RT内核上的每CPU数据结构。
由于PREEMPT_RT特定的spinlock_t语义,local_lock不适合防止PREEMPT_RT内核上的抢占或中断。
raw_spinlock_t and spinlock_t
raw_spinlock_t
raw_spinlock_t是严格的自旋锁实现,而与包括启用PREEMPT_RT的内核在内的内核配置无关。
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 and PREEMPT_RT
在PREEMPT_RT内核上,将spinlock_t映射到基于rt_mutex的单独实现,该实现会更改语义:
- 抢占没有被禁用。
- spin_lock/spin_unlock操作的硬中断相关后缀
_irq/_irqsave/_irqrestore
不会影响CPU的中断禁用状态。 - 与软中断相关的后缀
_bh
仍禁用softirq处理程序。 - 非PREEMPT_RT内核会禁用抢占以获得此效果。
- PREEMPT_RT内核使用每个CPU锁进行序列化,从而使抢占保持禁用状态。 该锁禁用softirq处理程序,并防止由于任务抢占而导致的重新进入。
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是多重reader和单一writer锁定机制。
非PREEMPT_RT内核将rwlock_t实现为自旋锁,并且spinlock_t的后缀规则也适用。 实施是公平的,因此避免了writer的饥饿。
rwlock_t and PREEMPT_RT
PREEMPT_RT内核将rwlock_t映射到单独的基于rt_mutex的实现,从而改变了语义:
所有spinlock_t更改也适用于rwlock_t。
由于rwlock_t writer无法将其优先级授予多个reader,因此被抢占的低优先级reader将继续保持其锁定状态,从而使高优先级writer也饥饿。 相比之下,由于reader可以将其优先级授予writer,因此,抢占的低优先级writer将获得更高的优先级,直到释放锁定,从而防止该writer使reader挨饿。
PREEMPT_RT注意事项
local_lock on RT
在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_lock_irqrestore(&local_lock_1, flags);
}
func2()
{
local_irq_save(flags); -> local_lock_irqsave(&local_lock_2, flags);
func3();
local_irq_restore(flags); -> local_lock_irqrestore(&local_lock_2, flags);
}
func3()
{
lockdep_assert_irqs_disabled();
access_protected_data();
}
在非PREEMPT_RT内核上,它可以正常工作,但在PREEMPT_RT内核上,local_lock_1和local_lock_2是不同的,并且无法序列化func3()
的调用者。 同样,由于spinlock_t在PREEMPT_RT上特定的语义,local_lock_irqsave()
不会禁用中断,因此lockdep断言也会在PREEMPT_RT内核上触发。 正确的替换是:
func1()
{
local_irq_save(flags); -> local_lock_irqsave(&local_lock, flags);
func3();
local_irq_restore(flags); -> local_lock_irqrestore(&local_lock, flags);
}
func2()
{
local_irq_save(flags); -> local_lock_irqsave(&local_lock, flags);
func3();
local_irq_restore(flags); -> local_lock_irqrestore(&local_lock, flags);
}
func3()
{
lockdep_assert_held(&local_lock);
access_protected_data();
}
spinlock_t and 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内核上,这会中断。 特定于PREEMPT_RT的spinlock_t语义更改不允许获取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);
在非PREEMPT_RT内核上,migration_disable()
映射到preempt_disable()
,这使得上述代码完全等效。 在PREEMPT_RT内核上,migration_disable()
确保将任务固定在当前CPU上,从而保证对var1和var2的按CPU的访问保持在同一CPU上。
对于以下情况,migrate_disable()
替换无效:
func()
{
struct foo *p;
migrate_disable();
p = this_cpu_ptr(&var1);
p->val = func2();
虽然在非PREEMPT_RT内核上是正确的,但在PREEMPT_RT上却会中断,因为在此,migrate_disable()
不能防止因抢占任务而重新进入。 这种情况的正确替代方法是:
func()
{
struct foo *p;
local_lock(&foo_lock);
p = this_cpu_ptr(&var1);
p->val = func2();
在非PREEMPT_RT内核上,这可以通过禁用抢占来防止重入。 在PREEMPT_RT内核上,这是通过获取基础的每CPU自旋锁来实现的。
raw_spinlock_t on RT
获取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);
bit spinlocks
PREEMPT_RT无法替代位自旋锁,因为单个位太小而无法容纳 rt_mutex。 因此,位自旋锁的语义保留在PREEMPT_RT内核上,因此raw_spinlock_t警告也适用于位自旋锁。
在使用地点使用有条件的代码(#ifdef),将某些自旋锁替换为PREEMPT_RT的常规spinlock_t。 相反,spinlock_t替换不需要使用位置更改。 相反,头文件中的条件和核心锁定实现使编译器可以透明地进行替换。
锁类型嵌套规则
最基本的规则是:
- 相同锁类别(睡眠,CPU本地,自旋)的锁类型可以任意嵌套,只要它们遵守通用锁排序规则以防止死锁。
- 睡眠锁类型不能嵌套在CPU本地锁和自旋锁类型内。
- CPU本地和自旋锁类型可以嵌套在睡眠锁类型内。
- 自旋锁类型可以嵌套在所有锁类型内
这些约束适用于PREEMPT_RT和其他情况。
PREEMPT_RT将spinlock_t和rwlock_t的锁定类别从自旋更改为休眠,并用每CPU spinlock_t替换local_lock的事实意味着,在保留原始自旋锁的同时无法获取它们。 这导致以下嵌套顺序:
- 睡锁
- spinlock_t,rwlock_t,local_lock
- raw_spinlock_t和位自旋锁
无论是在PREEMPT_RT中还是在其他方面,如果违反这些约束,Lockdep都会报错。