【问题标题】:Is calling spin_lock_irqsave, instead of local_irq_disable followed by spin_lock, the same for a per-prorcessor struct?调用 spin_lock_irqsave,而不是 local_irq_disable 后跟 spin_lock,对于每个处理器的结构是否相同?
【发布时间】:2021-05-06 23:59:24
【问题描述】:

考虑以下内核代码

local_irq_disable();
__update_rq_clock(rq);
spin_lock(&rq->lock);

rq 是指向每个处理器 struct 的指针(即不受 SMP 并发约束)。由于在调用 local_irq_disable 后,rq 将永远不会在另一个地方被访问(因为 rq 仅由单个处理器使用,并且禁用本地中断意味着不会在该 CPU 上运行中断处理程序),那么有什么意义在前面的函数之间嵌入__update_rq_clock?换句话说,考虑到rq__update_rq_clock 内部的两种情况下都是安全的,无论是否被锁定,它与以下内容有何不同?

spin_lock_irqsave(&rq->lock, flags);
__update_rq_clock(rq);

【问题讨论】:

  • 第一个代码在获取锁之前更新,所以锁没有效果。第二个代码保存了之前的 irq 状态,想必以后会恢复。所以它们是非常不同的。
  • @stark 没关系,调用local_irq_disablespin_lock_irqsave 的结果是一样的:它们都会禁用中断。中断级别的唯一区别是spin_lock_irqsave 会记住启用了哪些中断,禁用了哪些中断,以便我们以后可以将它们恢复为以前的状态,而local_irq_enable 只会启用所有内容。但我不关心这部分,我的问题不是关于启用中断,而是关于锁定rq 的含义、__update_rq_clock 内部的安全性以及调用这些函数的顺序。
  • 不同之处在于关键部分(受锁保护)在他们的情况下比在你的情况下更小。如果某些功能不需要锁保护,为什么要在锁下调用它?
  • @Tsyvarev 但是,这没关系!是一样的,rq 在这两种情况下都受到了根本的保护,rq 不受 SMP 并发的影响,因为它是一个 per-processor 结构,这意味着唯一的并发风险来自中断处理程序,但中断处理程序被 @ 禁用987654339@,这意味着rq 没有被其他地方访问的风险(既不是处理器也不是中断处理程序!),因此将spin_lock(&rq->lock) 延迟到__update_rq_clock(rq) 之后不会导致更小的临界区,因为实际上没有关键部分!
  • 这意味着我们实际上没有从延迟spin_lock中得到任何东西,所以我们可以直接使用spin_lock_irqsave,它绝对会导致同样的事情!

标签: c concurrency linux-kernel locking spinlock


【解决方案1】:

首先:您展示的两个示例具有不同的语义:local_irq_disable 不保存 IRQ 的旧状态。换句话说,当相应的local_irq_enable 函数被调用时,它将强制重新启用 IRQ(无论它们是否已经被禁用)。另一方面,spin_lock_irqsave 确实保存了旧的 IRQ 状态,因此以后可以通过spin_unlock_irqrestore 恢复它。因此,您展示的两段代码非常不同,比较它们没有多大意义。

现在,进入真正的问题:

因为在调用 local_irq_disable 后,rq 将永远不会在另一个地方被访问(因为 rq 仅由单个处理器使用,并且禁用本地中断意味着不会在该 CPU 上运行任何中断处理程序)

这并不总是正确的。没有“魔法屏障”可以阻止 CPU 访问另一个 CPU 的每个 CPU 数据。这仍然是可能的,在这种情况下,必须通过适当的锁定机制格外小心。

虽然 per-CPU 变量通常旨在为单个 CPU 提供对对象的快速访问,因此 可以 具有不需要锁定的优势,但除了保持处理器的约定之外,别无其他从挖掘其他处理器的每个 CPU 数据 (quote)。

运行队列就是一个很好的例子:由于调度程序经常需要将任务从一个运行队列迁移到另一个运行队列,它肯定需要在某个时候同时访问两个运行队列。确实,这可能是struct rq.lock 字段的原因之一。

事实上,在不持有rq->lock 的情况下进行rq 时钟更新似乎被认为是最近内核代码中的一个错误,正如您在update_rq_clock() 中的this lockdep assertion 中看到的那样:

void update_rq_clock(struct rq *rq)
{
    s64 delta;

    lockdep_assert_held(&rq->lock);

    // ...

感觉您在第一个代码 sn-p 中显示的语句应该重新排序以先锁定然后更新,但是代码很旧(v2.6.25),并且对 __update_rq_clock() 的调用似乎在获取锁之前故意进行。很难说为什么,但也许旧的运行​​队列语义不需要锁定来更新.lock/.prev_clock_raw,因此之后进行锁定只是为了最小化临界区的大小。

【讨论】:

  • 结构中有lock 字段并不意味着访问此结构中的所有 字段都需要被锁保护:可能是锁仅在保护结构的某些字段时才需要,但对其他字段的访问不需要锁。至于lockdep_assert_heldupdate_rq_clock 内部调用,它实际上需要在调用update_rq_clock 的同时获取锁。但是..问题帖子是关于__update_rq_clock 函数,而不是关于update_rq_clock 一个。 (虽然我没有找到具有__update_rq_clock 的 Linux 内核版本)。
  • @Tsyvarev 关于锁的好点,肯定可能需要别的东西,我会从答案中去掉那部分。根据__update_rq_clock 函数,它似乎很旧,我只能从 2.6.23 到 2.6.25 找到它,而且看起来旧代码仍然假定锁仍然存在(认为不存在 lockdep 断言)。
  • @KarimManaouil 是的。在一个完美的世界中,每个 CPU 变量实际上只由其 CPU 使用,您根本不需要锁定,只需在需要时禁用本地 irq。尽管即使在这种情况下,适当的锁定量仍会提高性能,因为持续禁用 IRQ 仅访问变量是相当过分的,并且会阻止所有线程被调度,即使它们不需要与变量交互。
  • @KarimManaouil 如果锁定的语义是以某种方式定义的,那么推迟锁定直到真正需要时才有意义:例如如果只需要锁来操作其他字段,而不是时钟。在我最后的声明中,我说我假设锁定是在 update_rq_clock 之后完成的,因为更新时钟不需要锁定旧内核。是的,您肯定是正确的,它不是 一个错误。我只是说“要么是这种方式,要么是一个错误”(因此,它必须是这种方式)。
  • @MarcoBonelli 事情现在清楚多了!非常感谢您的回答,祝您度过愉快的一天,我的地中海 amico
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-10-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-08-16
  • 2011-07-02
相关资源
最近更新 更多