Linux中的自旋锁是什么?


32

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

Answers:


34

自旋锁是一种保护共享资源不被两个或多个进程同时修改的方法。尝试修改资源的第一个过程“获取”了锁并继续进行,对资源进行了所需的处理。随后尝试获取锁的任何其他进程都将停止;据说它们“旋转到位”,等待第一个进程释放该锁,因此名称为“旋转锁”。

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可能不会。


@Warren:几个疑问-我想进一步了解这个Big Kernel Lock及其含义。我也明白凭着最后一段“CPU内核的2至4加倍可能会在Linux机器的速度增长近一倍,但16至32可能不会增加一倍”

2
关于:BKL的含义:我认为我在上面已经明确了这一点。在内核中只有一个锁的情况下,每当两个内核尝试执行受BKL保护的操作时,一个内核就会被阻塞,而第一个内核将使用其受保护的资源结束操作。锁定越精细,发生锁定的机会就越小,因此处理器利用率越高。
沃伦·杨

2
回复:加倍:我的意思是,在向计算机添加处理器内核时,存在着收益递减的规律。随着核心数量的增加,其中两个或更多核心将需要访问受特定锁保护的资源的机会也会增加。增加锁的粒度可以减少发生这种冲突的机会,但是增加太多冲突也有开销。您现在可以在超级计算机中轻松地看到这一点,如今这些超级计算机通常具有数千个处理器:大多数工作负载的效率低下,因为它们无法避免由于共享资源争用而使许多处理器空闲。
沃伦·杨

1
尽管这是一个有趣的解释(该解释为+1),但我认为它不能有效地传达自旋锁与其他类型的锁之间的差异。
吉尔斯(Gilles)'所以

2
如果要知道自旋锁和信号灯之间的区别,那就是另外一个问题。另一个很好但切线的问题是,关于Linux内核设计有什么意义,它使自旋锁成为了不错的选择,而不是诸如互斥锁之类的用户区代码中更常见的东西。这个答案照原样大量散播。
沃伦·杨

11

旋转锁是指进程不断轮询要删除的锁。这被认为是不好的,因为该过程不必要地消耗了(通常)周期。它不是特定于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

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

使用inotifywait的同一示例显示了如何在没有自旋锁的情况下完成相同的操作:

#/bin/sh
inotifywait -e delete_self /var/run/example.lock
#do stuff

调度程序在那里扮演什么角色?还是有什么作用?
森2010年

1
在自旋锁方法中,调度程序每隔约1秒钟恢复该进程以执行其任务(只需检查文件的存在)。在inotifywait示例中,调度程序仅在子进程(inotifywait)退出时才恢复该进程。Inotifywait也在睡觉。调度程序仅在inotify事件发生时恢复它。
肖恩·高夫

那么,如何在单个核心处理器系统中处理这种情况?


1
该bash脚本是自旋锁的不良示例。您正在暂停使其进入睡眠状态的过程。自旋锁永远不会睡觉。在单核计算机上,它只是挂起了调度程序并继续(没有繁忙的等待时间)
Martin

7

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

现在,尝试并继续使用的时间少于尝试并阻止所需的时间。暂时说,“尝试并继续”将花费i个时间单位,而“尝试并阻止”将花费100个单位时间。

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

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

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

因此,最后,自旋锁可能非常有效或浪费。当持有锁的“典型”时间比“尝试并阻止”所花费的时间更长时,这是浪费的。当典型的持有锁的时间比“尝试并阻止”的时间短得多时,这是有效的。

附:如果您仍然可以找到关于线程的书,则它是《线程入门》。


螺纹一直旋转到位,直到锁住。您能告诉我这是什么吗?就像它进入wait_queue并由Scheduler执行一样吗?也许我正试图进入低水平,但仍然不能怀疑。

基本上,在循环的3-4条指令之间旋转。
Paul Stelian

5

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

take the lock
use the resource
release the lock

如果已经执行了另一个任务,则不可能进行锁定。(将锁视为一个物理令牌对象。要么该对象位于抽屉中,要么有人拿着它。只有持有该对象的人才能访问该资源。)因此,“拿走锁”的意思是“等到没有人拥有锁,那就把它拿走。”

从高级的角度来看,实现锁的主要方式有两种:自旋锁和条件。使用自旋锁时,锁定意味着仅“旋转”(即,不执行任何操作),直到其他人都没有锁定为止。在有条件的情况下,如果一个任务尝试获取该锁但由于另一个任务持有该锁而被阻止,则新手将进入等待队列;释放操作会向任何等待的任务发出信号,表明锁已可用。

(这些解释不足以让您实现锁,因为我没有对原子性说过什么。但是原子性在这里并不重要。)

自旋锁显然是浪费的:正在等待的任务会不断检查是否已锁定。那么为什么以及何时使用它呢?在没有持有锁的情况下,自旋锁通常很便宜。当持有锁的机会很小时,这使其具有吸引力。此外,自旋锁只有在预计不会花费很长时间的情况下才可行。因此,自旋锁通常用于保持时间很短的情况,因此大多数尝试都有望在首次尝试时成功,而那些需要等待的时间不会等待很长时间。

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


什么是实现其他同步原语的好方法?自旋锁,检查是否有人在实现实际的原语,然后使用调度程序或授予访问权限?我们能否将Java中的synced()块视为自旋锁的一种形式,并且在正确实现的原语中仅使用自旋锁?
Paul Stelian

@PaulStelian以这种方式实现“慢速”锁定确实很常见。我不了解足够的Java语言来回答这一部分,但是我怀疑synchronized是否可以通过自旋锁来实现:一个synchronized块可能会运行很长时间。synchronized是一种在某些情况下易于使用锁的语言构造,而不是用于构建较大的同步原语的原语。
吉尔斯(Gilles)'所以

3

自旋锁是一种通过禁用调度程序以及可能在获取该锁的特定内核上可能发生的中断(irqsave变体)进行操作的锁。它与互斥锁的不同之处在于,它禁用了调度功能,因此在持有自旋锁的同时只能运行线程。互斥锁允许在保留它的同时调度其他更高优先级的线程,但不允许它们同时执行受保护的部分。由于自旋锁禁用了多任务,因此您无法进行自旋锁,然后调用其他一些试图获取互斥锁的代码。自旋锁部分中的代码必须永不休眠(当代码遇到互斥锁或空信号量时,通常会休眠)。

互斥锁的另一个区别是,线程通常会排队等待一个互斥锁,因此下面的互斥锁有一个队列。而自旋锁只是确保即使有必要,其他线程也不会运行。因此,当您不确定文件是否会休眠时,在调用文件外部的函数时,切勿持有自旋锁。

当您想与中断共享自旋锁时,必须使用irqsave变体。这不仅会禁用调度程序,还会禁用中断。有道理吧?自旋锁的工作原理是确保其他任何东西都不会运行。如果您不希望运行中断,请禁用该中断,然后安全地进入关键部分。

在多核计算机上,自旋锁实际上将旋转,等待持有该锁的另一个内核释放它。这种旋转仅在多核计算机上发生,因为在单核计算机上它不会发生(您按住自旋锁并继续进行,或者直到释放锁后才运行)。

自旋锁在合理的地方并不浪费。对于非常小的关键部分,与简单地将调度程序挂起完成重要工作所需要的几微秒相比,分配互斥锁任务队列会很浪费。如果您需要休眠或在io操作中保持锁定(可能会休眠),请使用互斥锁。当然,绝对不要锁定自旋锁,然后尝试在中断内释放它。尽管这将起作用,但是就像while(flagnotset)的arduino废话一样;在这种情况下,请使用信号灯。

当您需要对内存事务块进行简单互斥时,请抓住自旋锁。当您希望多个线程在互斥锁锁定之前立即停止时,获取一个互斥锁,然后在互斥锁变为空闲时以及在同一线程中锁定和释放时,选择最高优先级的线程继续运行。当您打算在一个线程或一个中断中发布信号并在另一个线程中接收它时,请抓住它。确保相互排斥的三种方式略有不同,并且它们的使用目的也略有不同。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.