Linux 中的自旋锁是什么?

Sen*_*Sen 38 linux spinlock lock

我想详细了解 Linux 自旋锁;有人可以向我解释一下吗?

War*_*ung 40

自旋锁是一种保护共享资源不被两个或多个进程同时修改的方法。第一个尝试修改资源的进程“获取”锁并继续前进,对资源做它需要做的事情。随后尝试获取锁的任何其他进程都会停止;据说它们“就地旋转”,等待第一个进程释放锁,因此称为自旋锁。

Linux 内核在许多事情上使用自旋锁,例如在向特定外围设备发送数据时。大多数硬件外围设备并非旨在处理多个同时状态更新。如果必须发生两种不同的修改,一个必须严格遵循另一个,它们不能重叠。自旋锁提供必要的保护,确保一次修改一次。

自旋锁是一个问题,因为自旋会阻止该线程的 CPU 内核执行任何其他工作。尽管 Linux 内核确实为在其下运行的用户空间程序提供了多任务处理服务,但该通用多任务处理功能并未扩展到内核代码。

这种情况正在改变,并且在 Linux 存在的大部分时间里都是如此。在 Linux 2.0 之前,内核几乎完全是一个单任务程序:每当 CPU 运行内核代码时,只使用一个 CPU 内核,因为有一个自旋锁保护所有共享资源,称为大内核锁(BKL) )。从 Linux 2.2 开始,BKL 逐渐被分解为许多独立的锁,每个锁保护更集中的资源类别。今天,在内核 2.6 中,BKL 仍然存在,但它只被真正旧的代码使用,这些代码不能很容易地移动到一些更细粒度的锁。现在,多核机器很可能让每个 CPU 都运行有用的内核代码。

由于 Linux 内核缺乏通用的多任务处理,因此分解 BKL 的实用性是有限的。如果 CPU 内核在内核自旋锁上被阻塞,则无法重新分配任务,直到释放锁才能执行其他操作。它只是坐着旋转,直到锁被释放。

自旋锁可以有效地将一个巨大的 16 核盒子变成一个单核盒子,如果工作负载是这样的,每个核心总是在等待一个自旋锁。这是 Linux 内核可扩展性的主要限制:将 CPU 内核从 2 个增加到 4 个可能会使 Linux 机器的速度几乎翻倍,但对于大多数工作负载而言,从 16 个增加到 32 个可能不会。

  • 回复:BKL 的含义:我想我在上面已经说清楚了。内核中只有一个锁,任何时候两个内核尝试做一些受 BKL 保护的事情,一个内核被阻塞,而第一个内核完成使用其受保护的资源。锁定的粒度越细,发生这种情况的可能性就越小,因此处理器利用率就越高。 (2认同)
  • 回复:加倍:我的意思是在向计算机添加处理器内核时存在收益递减规律。随着内核数量的增加,其中两个或更多需要访问受特定锁保护的资源的可能性也会增加。增加锁粒度会减少发生此类冲突的机会,但添加太多也会带来开销。您可以很容易地在超级计算机中看到这一点,如今它们通常有数千个处理器:大多数工作负载在它们上面效率低下,因为它们无法避免由于共享资源争用而使许多处理器空闲。 (2认同)
  • 如果你想知道自旋锁和信号量之间的区别,那是一个不同的问题。另一个很好但切中要害的问题是,Linux 内核设计是什么使自旋锁成为不错的选择,而不是用户态代码中更常见的东西,例如互斥锁。这个答案按原样分散了很多。 (2认同)

Sha*_*off 14

自旋锁是指进程不断轮询要删除的锁。它被认为是糟糕的,因为该过程正在不必要地消耗周期(通常)。它不是特定于 Linux 的,而是一种通用的编程模式。虽然它通常被认为是一种不好的做法,但实际上它是正确的解决方案;在某些情况下,使用调度程序的成本(就 CPU 周期而言)高于自旋锁预期持续的几个周期的成本。

自旋锁示例:

#!/bin/sh
#wait for some program to clear a lock before doing stuff
while [ -f /var/run/example.lock ]; do
  sleep 1
done
#do stuff
Run Code Online (Sandbox Code Playgroud)

通常有一种方法可以避免自旋锁。对于这个特定示例,有一个名为inotifywait的 Linux 工具(默认情况下通常不安装)。如果它是用 C 编写的,您只需使用Linux 提供的inotify API。

同样的例子,使用 inotifywait 展示了如何在没有自旋锁的情况下完成同样的事情:

#/bin/sh
inotifywait -e delete_self /var/run/example.lock
#do stuff
Run Code Online (Sandbox Code Playgroud)

  • 在自旋锁方法中,调度程序每约 1 秒恢复进程以执行其任务(这只是检查文件是否存在)。在 inotifywait 示例中,调度程序仅在子进程 (inotifywait) 退出时恢复进程。Inotifywait 也在休眠;调度程序仅在 inotify 事件发生时恢复它。 (2认同)

Han*_*ndy 8

当一个线程尝试获取锁时,如果失败,可能会发生三件事 - 它可以尝试并阻塞,可以尝试并继续,可以尝试然后进入睡眠状态,告诉操作系统在发生某些事件时将其唤醒。

现在,尝试并继续使用的时间比尝试并阻止要少得多。让我们暂时假设“尝试并继续”将花费一个时间单位,而“尝试并阻止”将花费一百个时间。

现在让我们暂时假设一个线程平均需要 4 个单位的时间来持有锁。等待100个单位是浪费。因此,您可以编写一个“尝试并继续”的循环。在第四次尝试时,您通常会获得锁。这是一个自旋锁。之所以这样称呼是因为线程在获得锁之前一直在原地旋转。

附加的安全措施是限制循环运行的次数。因此,例如,您进行 for 循环运行,例如六次,如果失败,则您“尝试并阻止”。

如果您知道一个线程将始终持有锁,例如 200 个单位,那么您每次尝试和继续都在浪费计算机时间。

所以最终,自旋锁可能非常有效,也可能很浪费。当持有锁的“典型”时间高于“尝试并阻塞”所需的时间时,这是一种浪费。当持有锁的典型时间远低于“尝试和阻塞”的时间时,它是有效的。

Ps:关于线程的书是“A Thread Primer”,如果你还能找到的话。


Gil*_*il' 6

锁定为两个或更多个任务(进程,线程)来同步的一种方式。具体来说,当两个任务都需要间歇性地访问一次只能由一个任务使用的资源时,这是安排任务不同时使用资源的一种方式。为了访问资源,任务必须执行以下步骤:

take the lock
use the resource
release the lock
Run Code Online (Sandbox Code Playgroud)

如果另一个任务已经取得锁,则不可能取得锁。(把锁想象成一个物理令牌对象。对象要么在抽屉里,要么有人手里拿着它。只有持有对象的人才能访问资源。)所以“拿锁”的真正意思是“等到没有其他人拥有锁,然后拿走它”。

从高层的角度来看,实现锁的主要方式有两种:自旋锁和条件。使用自旋锁,获取锁意味着只是“旋转”(即在循环中什么都不做),直到没有其他人拥有锁。有条件,如果一个任务试图获取锁但因为另一个任务持有它而被阻塞,则新来者进入等待队列;释放操作向任何等待的任务发出信号,表明锁现在可用。

(这些解释不足以让你实现一个锁,因为我没有说过原子性。但原子性在这里并不重要。)

自旋锁显然是浪费的:等待任务一直在检查是否获得了锁。那么为什么以及何时使用它呢?在没有持有锁的情况下,获得自旋锁通常非常便宜。当锁被持有的机会很小时,这使得它很有吸引力。此外,只有在预计不会花费很长时间才能获得锁的情况下,自旋锁才是可行的。所以自旋锁倾向于在它们将保持很短时间的情况下使用,以便大多数尝试在第一次尝试时会成功,而那些需要等待的不会等待很长时间。

Linux 设备驱动程序第 5 章中对 Linux 内核的自旋锁和其他并发机制有很好的解释。