spinlockmutex

Spinlocklinux内核中常用的一种互斥锁机制,和mutex不同,当无法持锁进入临界区的时候,当前执行线索不会阻塞,而是不断的自旋等待该锁释放。正因为如此,自旋锁也是可以用在中断上下文的。也正是因为自旋,临界区的代码要求尽量的精简,否则在高竞争场景下会浪费宝贵的CPU资源。

mutex

Mutex(互斥锁)是一种在多线程编程中常用的同步机制,用于控制多个线程对共享资源的访问,以避免并发冲突。Mutex 简称互斥量,其目的是确保在任何给定时刻只有一个线程可以访问共享资源,从而防止数据竞争和不确定性行为。

临界区

在并发编程中,临界区(Critical Section)是指一段代码或代码块,其中包含了对共享资源的访问或修改操作。这个临界区被保护,以确保在任何给定时刻只有一个线程可以访问它,以避免并发冲突和数据竞争。

这里临界区指的是那些被自旋锁保护的代码端。当一个现场获得了自旋锁并进入临界区时,其它线程如果也尝试获得同一个自旋锁,他们将会自旋等待该锁的释放,而不会阻塞。这意味着他们会反复检查锁是否可用,而不会被挂起,因此称为自旋。

自旋锁通常在临界区的代码尽可能保持短小和高效,以减少自旋等待的时间。如果临界区的代码过于复杂或执行时间过长,那么在高竞争情况下,自旋锁会导致线程不断自旋,浪费宝贵的 CPU 资源,因此要谨慎设计临界区的代码。

临界区的目标是确保在同一时间只有一个线程可以执行其中的代码,以保护共享资源免受并发访问的影响。自旋锁是一种用于实现这种同步的机制,它在一些情况下比传统的互斥锁(如 Mutex)更适用,尤其是在内核代码中和中断上下文中,因为它不会引发线程的阻塞。但是,自旋锁在高竞争情况下可能效率较低,因此在选择锁的类型时需要权衡不同的因素。

代码结构

上层

最上层的是通用自旋锁的代码(体系结构无关,平台无关),这一层的代码提供了两种接口:spinlock 接口和raw spinlock 接口。在没有配置PREEMPT_RT情况下,spinlock接口和raw spinlock接口一模一样,但是如果配置了PREEMPT_RT,spinlock接口走 rt spinlock,底层是基于rtmutex的。

"PREEMPT_RT" 是一种 Linux 内核的实时扩展,旨在提供更可预测和响应性能的内核。PREEMPT_RT 的全称是 "PREEMPT-Real Time",它通过对 Linux 内核的修改,使其更适合实时应用,以满足更严格的实时要求。

PREEMPT_RT 的主要特性包括:

  1. 抢占性内核(Kernel Preemption):PREEMPT_RT 引入了更多的内核抢占点,这意味着内核能够在更多的情况下响应实时任务的请求,而不会被普通内核任务阻塞。
  2. 减少中断禁用时间:PREEMPT_RT 通过减少中断禁用时间来改进内核响应性,这有助于减小实时任务受阻的机会。
  3. 更可预测的延迟:PREEMPT_RT 旨在减小任务切换和响应中断的时间,从而提供更可预测的性能,对于实时应用程序而言非常重要。
  4. 更好的时间管理:PREEMPT_RT 提供了更准确的时间管理机制,允许实时应用程序更精确地控制和协调时间敏感任务。
  5. 支持硬实时和软实时需求:PREEMPT_RT 可以根据应用程序的需求配置,以适应硬实时或软实时的要求。

这个扩展使 Linux 内核能够更好地胜任实时应用,如工业自动化、机器人控制、音视频处理等领域,其中时间敏感性和可预测性至关重要。

也就是说,这时候的spinlock不再禁止抢占,不在自旋等待,而是使用了支持 PI的睡眠锁来实现,因此有了更好的实时性。

"PI" 指的是 "Priority Inheritance"(优先级继承)。优先级继承是一种实时系统中用于解决资源竞争和优先级反转问题的技术。在实时系统中,任务通常有不同的优先级,而共享资源(如锁)的争用可能导致优先级反转问题,即低优先级任务持有了高优先级任务所需的资源,从而导致高优先级任务无法继续执行。

为了解决这个问题,引入了优先级继承机制,其中:

  1. 当一个高优先级任务等待一个由低优先级任务持有的资源时,低优先级任务的优先级会被提升到高优先级任务的优先级。这样,资源持有者的优先级被临时提高,以确保高优先级任务能够及时获得资源。
  2. 一旦高优先级任务获得了资源,低优先级任务的优先级会恢复到原来的状态。

这种优先级继承机制有助于防止优先级反转问题,确保高优先级任务能够按时执行。在实时系统中,特别是在 PREEMPT_RT 扩展的 Linux 内核中,使用支持优先级继承的睡眠锁来实现,可以提高实时性和可预测性,以确保实时任务能够按时响应。

而raw spinlock接口即便在配置了PREEMPT_RT下仍然保持传统自旋锁特性。

中间层

中间一层是区分SMP和UP的,在SMP和UP上,自旋锁的实现是不一样的。对于UP,自旋没有意义,因此spinlock的上锁和放锁操作退化为preempt disable(抢占禁用)和 enable(抢占启用)。而不是进行无效的自旋操作。这样可以确保 UP 系统中的任务不会浪费时间在自旋上,而是能够有效地等待锁的释放。

在单 CPU 系统中,通常可以使用更简单的同步机制,如互斥锁或信号量,来确保任务之间的互斥和同步。这些同步机制会在任务需要访问共享资源时将任务置于休眠状态,等待资源可用,而不是在一个循环中自旋等待资源的释放。

SMP平台上,除了抢占操作之外还有正常自旋锁的逻辑,具体如何实现自旋锁逻辑是和底层的CPU architecture相关的,后面我们会详细描述。

下层

最底层的代码是体系结构相关的代码,ARM64上,目前采用是qspinlock。和体系结构无关的Qspinlock代码抽象在qspinlock.c文件中,也就是本文重点要描述的内容。

自旋锁的演进

TAS

最早的自旋锁是TAS (test and set) 自旋锁,即通过原子指令来修改自旋锁的状态(locked、unlocked)。这种锁存在不公平的现象,具体原因如下:

如果thread4当前持锁,同一个cluster中的cpu7的thread7和另外一个cluster中的thread0都在自旋等待锁的释放。当thread4释放锁的时候,由于cpu7和cpu4的拓扑距离更近,thread7会有更高概率可以抢到自旋锁,从而产生了不公平现象。

为什么不同的cluster拓扑距离更近的cpu有更高概率抢到自旋锁

在多核系统中,不同的CPU核心可以分布在不同的物理或逻辑集群中,而每个集群可能具有不同的内部连接拓扑结构。集群内的核心通常会更容易快速访问彼此之间的共享资源(如内存),因此在争夺自旋锁时,拓扑距离更近的核心更有可能抢到锁。

这是因为集群内的核心通常会通过更快速的内部总线或互连结构进行通信,从而更容易快速获取锁和访问共享资源。与集群内的核心相比,与其他集群的核心进行通信可能需要更多的时间和额外的延迟。

在多核系统中,CPU核心之间的争夺自旋锁通常涉及到在多个核心之间共享数据结构,如锁本身或被锁保护的共享资源。因此,抢夺锁可能需要访问共享数据结构,而这个过程可能会受到拓扑距离的影响。

拓扑距离更近的核心更容易访问共享资源,因此在自旋等待期间,它们更有可能更快地获取锁。这导致了拓扑距离更近的核心在争夺锁时有更高的成功率。

这就是为什么在考虑锁竞争的情况下,多核系统的拓扑结构和拓扑距离通常被纳入考虑,以帮助最大程度地减少锁争用和提高性能。在某些情况下,可以通过合理地调度任务和考虑拓扑结构来降低锁竞争,从而提高多核系统的效率。

tiket based

为了解决这个问题,内核工程师又开发了ticket base的自旋锁,但是这种自旋锁在持锁失败的时候会对自旋锁状态数据next成员进行++操作。

qspinlock

当CPU数据巨大并竞争激烈的时候,自旋锁状态数据对应的cacheline会在不同cpu上跳来跳去,从而对性能产生影响,为了解决这个问题,qspinlock产生了。

什么是cacheline?

"cacheline" 是指缓存行,是计算机系统中用于缓存数据的最小单位。缓存行是一块固定大小的内存块,通常包含多个字节或字的数据。当 CPU 从主内存中加载数据时,它通常会将数据存储在缓存行中,以便更快地访问数据。

多个 CPU 核心可以共享相同的物理内存,但每个 CPU 核心通常都有自己的缓存。这意味着当多个 CPU 核心同时访问相同的数据时,数据可能会被存储在不同的 CPU 缓存中的不同缓存行上。这种情况下,不同 CPU 核心之间可能需要协调缓存行的数据一致性,以确保数据的正确性。

在多线程编程中,当多个线程同时访问共享数据时,可能会引发缓存一致性问题。这是因为一个线程在一个 CPU 核心上修改了共享数据的缓存行,但其他线程在不同 CPU 核心上可能仍然使用旧的缓存数据。为了解决这个问题,需要使用内存栅栏或原子操作来确保数据的一致性。

因此,在多核 CPU 系统中,缓存行的管理和缓存一致性是性能优化和多线程编程中需要考虑的重要因素。qspinlock 的设计考虑了缓存一致性的问题,以减少锁状态数据的频繁访问和缓存争用,从而提高性能。

具体qspinlock是如何减少内存访问的,请看下回分解。

参考连接

一文带你自旋锁探秘 - 知乎 (zhihu.com)