Answers:
自旋锁是一种保护共享资源不被两个或多个进程同时修改的方法。尝试修改资源的第一个过程“获取”了锁并继续进行,对资源进行了所需的处理。随后尝试获取锁的任何其他进程都将停止;据说它们“旋转到位”,等待第一个进程释放该锁,因此名称为“旋转锁”。
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可能不会。
旋转锁是指进程不断轮询要删除的锁。这被认为是不好的,因为该过程不必要地消耗了(通常)周期。它不是特定于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
当线程尝试获取锁时,如果失败,可能会发生三件事,可以尝试阻止,可以尝试继续,然后可以尝试进入睡眠状态,告诉操作系统在某些事件发生时将其唤醒。
现在,尝试并继续使用的时间少于尝试并阻止所需的时间。暂时说,“尝试并继续”将花费i个时间单位,而“尝试并阻止”将花费100个单位时间。
现在让我们暂时假设一个线程平均持有该锁将花费4个时间。等待100个单位很浪费。因此,您编写了一个“尝试并继续”的循环。在第四次尝试中,您通常会获得锁。这是一个自旋锁。之所以这样称呼,是因为线程一直在旋转,直到获得锁为止。
增加的安全措施是限制循环运行的次数。因此,在示例中,您进行了一次for循环运行,例如六次,如果失败,则“尝试并阻止”。
如果您知道某个线程将始终持有该锁200个单位,那么您在每次尝试并继续时都在浪费计算机时间。
因此,最后,自旋锁可能非常有效或浪费。当持有锁的“典型”时间比“尝试并阻止”所花费的时间更长时,这是浪费的。当典型的持有锁的时间比“尝试并阻止”的时间短得多时,这是有效的。
附:如果您仍然可以找到关于线程的书,则它是《线程入门》。
甲锁定为两个或更多个任务(进程,线程)来同步的一种方式。具体来说,当两个任务都需要间歇性访问一次只能由一个任务使用的资源时,这是任务安排不同时使用该资源的一种方式。为了访问资源,任务必须执行以下步骤:
take the lock
use the resource
release the lock
如果已经执行了另一个任务,则不可能进行锁定。(将锁视为一个物理令牌对象。要么该对象位于抽屉中,要么有人拿着它。只有持有该对象的人才能访问该资源。)因此,“拿走锁”的意思是“等到没有人拥有锁,那就把它拿走。”
从高级的角度来看,实现锁的主要方式有两种:自旋锁和条件。使用自旋锁时,锁定意味着仅“旋转”(即,不执行任何操作),直到其他人都没有锁定为止。在有条件的情况下,如果一个任务尝试获取该锁但由于另一个任务持有该锁而被阻止,则新手将进入等待队列;释放操作会向任何等待的任务发出信号,表明锁已可用。
(这些解释不足以让您实现锁,因为我没有对原子性说过什么。但是原子性在这里并不重要。)
自旋锁显然是浪费的:正在等待的任务会不断检查是否已锁定。那么为什么以及何时使用它呢?在没有持有锁的情况下,自旋锁通常很便宜。当持有锁的机会很小时,这使其具有吸引力。此外,自旋锁只有在预计不会花费很长时间的情况下才可行。因此,自旋锁通常用于保持时间很短的情况,因此大多数尝试都有望在首次尝试时成功,而那些需要等待的时间不会等待很长时间。
Linux设备驱动程序第5章对Linux内核的自旋锁和其他并发机制进行了很好的解释。
synchronized
是否可以通过自旋锁来实现:一个synchronized
块可能会运行很长时间。synchronized
是一种在某些情况下易于使用锁的语言构造,而不是用于构建较大的同步原语的原语。
自旋锁是一种通过禁用调度程序以及可能在获取该锁的特定内核上可能发生的中断(irqsave变体)进行操作的锁。它与互斥锁的不同之处在于,它禁用了调度功能,因此在持有自旋锁的同时只能运行线程。互斥锁允许在保留它的同时调度其他更高优先级的线程,但不允许它们同时执行受保护的部分。由于自旋锁禁用了多任务,因此您无法进行自旋锁,然后调用其他一些试图获取互斥锁的代码。自旋锁部分中的代码必须永不休眠(当代码遇到互斥锁或空信号量时,通常会休眠)。
互斥锁的另一个区别是,线程通常会排队等待一个互斥锁,因此下面的互斥锁有一个队列。而自旋锁只是确保即使有必要,其他线程也不会运行。因此,当您不确定文件是否会休眠时,在调用文件外部的函数时,切勿持有自旋锁。
当您想与中断共享自旋锁时,必须使用irqsave变体。这不仅会禁用调度程序,还会禁用中断。有道理吧?自旋锁的工作原理是确保其他任何东西都不会运行。如果您不希望运行中断,请禁用该中断,然后安全地进入关键部分。
在多核计算机上,自旋锁实际上将旋转,等待持有该锁的另一个内核释放它。这种旋转仅在多核计算机上发生,因为在单核计算机上它不会发生(您按住自旋锁并继续进行,或者直到释放锁后才运行)。
自旋锁在合理的地方并不浪费。对于非常小的关键部分,与简单地将调度程序挂起完成重要工作所需要的几微秒相比,分配互斥锁任务队列会很浪费。如果您需要休眠或在io操作中保持锁定(可能会休眠),请使用互斥锁。当然,绝对不要锁定自旋锁,然后尝试在中断内释放它。尽管这将起作用,但是就像while(flagnotset)的arduino废话一样;在这种情况下,请使用信号灯。
当您需要对内存事务块进行简单互斥时,请抓住自旋锁。当您希望多个线程在互斥锁锁定之前立即停止时,获取一个互斥锁,然后在互斥锁变为空闲时以及在同一线程中锁定和释放时,选择最高优先级的线程继续运行。当您打算在一个线程或一个中断中发布信号并在另一个线程中接收它时,请抓住它。确保相互排斥的三种方式略有不同,并且它们的使用目的也略有不同。