美文网首页
【翻译】Linux 锁的种类和规则

【翻译】Linux 锁的种类和规则

作者: WqyJh | 来源:发表于2020-08-14 22:12 被阅读0次

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

    参考: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的事实意味着,在保留原始自旋锁的同时无法获取它们。 这导致以下嵌套顺序:

    1. 睡锁
    2. spinlock_t,rwlock_t,local_lock
    3. raw_spinlock_t和位自旋锁

    无论是在PREEMPT_RT中还是在其他方面,如果违反这些约束,Lockdep都会报错。

    相关文章

      网友评论

          本文标题:【翻译】Linux 锁的种类和规则

          本文链接:https://www.haomeiwen.com/subject/jcondktx.html