Linux 中的自旋锁是什么?

Linux 中的自旋锁是什么?

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

答案1

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

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 个可能不会增加一倍。

答案2

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

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

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

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

答案3

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

现在,尝试并继续比尝试并阻止所用的时间要少得多。暂时假设“尝试并继续”需要一个时间单位,“尝试并阻止”需要一百个时间单位。

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

一项附加的安全措施是限制循环运行的次数。例如,您进行一次 for 循环运行,比如说六次,如果失败,那么您“尝试并阻止”。

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

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

Ps:关于线程要读的书是《A Thread Primer》,如果你还能找到的话。

答案4

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

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

当您想与中断共享自旋锁时,您必须使用 irqsave 变体。这不仅会禁用调度程序,还会禁用中断。这有道理吗?自旋锁的工作原理是确保没有其他东西会运行。如果您不想运行中断,则可以禁用它并安全地进入临界区。

在多核机器上,自旋锁实际上会旋转等待另一个持有锁的核心来释放它。这种旋转仅发生在多核机器上,因为在单核机器上它不会发生(您要么持有自旋锁并继续,要么在锁释放之前永远不会运行)。

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

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

相关内容