Linux软件锁使用规则,【翻译】Linux 锁的种类和规则

介绍

内核提供了多种锁定原语,可以将其分为几类:

睡眠锁

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的中断处理分为两个部分,上半部分和下半部分,前者简单快速,执行的时候禁止一些或全部中断,后者稍后执行,并且执行期间可以响应所有中断。这种设计可使系统处于中断屏蔽状态的时间尽可能的短,以此来提高系统的响应能力。

所有者语义

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

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

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都会报错。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值