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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343