PV qspinlock原理

PV qspinlock是一种针对虚拟化环境优化的自旋锁,解决Lock Holder Preemption(LHP)和 Lock Waiter Preemption(LWP)问题。文章详细介绍了自旋锁的发展,从最初的spinlock到ticket spinlock、MCS spinlock,再到qspinlock的优化,尤其是qspinlock如何通过数据结构压缩和避免cache颠簸提高性能。在PV qspinlock中,增加了pv_wait和pv_kick操作,通过halt vcpu方式减少CPU资源浪费,同时利用pv_lock_hash保存qspinlock与pv_node的关系,优化了加锁和解锁流程。
摘要由CSDN通过智能技术生成

1 前言

自旋锁(spinlock)是用来在多处理器环境中工作的一种锁。如果内核控制路径发现spinlock是unlock,就获取锁并继续执行;相反,如果内核控制路径发现锁由运行在另一个CPU上的内核控制路径lock,就在周围“旋转”,反复执行一条紧凑的循环指令,直到锁被释放。spinlock的循环指令表示“忙等”:即使等待的内核控制路径无事可做(除了浪费时间),它也在CPU上保持运行。
spinlock的实现依赖这样一个假设:锁的持有线程和等待线程都不能被抢占。但是在虚拟化场景下,vCPU可能在任意时刻被hypervisor调度,导致其他vCPU上的锁等待线程忙等浪费CPU。这会导致已知的Lock Holder Preemption(LHP)和 Lock Waiter Preemption(LWP)问题。

  • LHP:虚拟机中的锁持有线程被抢占,导致锁等待线程忙等,直到锁持有者线程再次被调度并释放锁后,锁等待线程才能获取到锁。从锁持有线程被抢占到其再次被调度运行这段时间,其余锁等待线程的忙等其实是在浪费CPU算力。
  • LWP:虚拟机中的下一个锁等待线程被抢占,直到其下一次再次被调度并获取锁后,其余锁等待线程的忙等其实锁在浪费CPU算力。

本文首先介绍了最初的spinlock实现,然后介绍了ticket spinlock和mcs lock,最后介绍了qspinlock和pv(paravirt) spinlock。

2 最初的spinlock

linux kernel 2.6.24及之前版本的spinlock就是一个整数。其实现基于原子操作。spinlock初始化为1,加锁时,将spinlock减1(原子操作),然后判断其值是否为0,如果为0,则成功获取到锁;如果为负数,则需要忙等,直到spinlock的值变为正数,再次尝试加锁操作。解锁时,将spinlock的值置1即可。

3 ticket spinlock

最初的spinlock存在一个重要的缺陷:公平性;其实现机制无法确保等待时间最长的竞争者优先获取到锁。
为了解决这种无序竞争带来的不公平的问题,ticket spinlock被提出了。ticket spinlock的实现类似叫号系统,每个锁的竞争者领取一个号码,锁持有者释放锁的时候递增号码,这样下一个竞争者就能获取到锁。

4 MCS spinlock

ticket spinlock存在一个性能问题:cache颠簸;在MP系统中,每次获取锁的值的时候都会刷新所有竞争CPU上的cache,这在竞争激烈的情况下会损耗大量的系统性能。
为了解决ticket spinlock带来的cache颠簸的问题,MCS spinlock被提出了。MCS其实就是这两位作者的简称,John M. Mellor-Crummey and Michael L. Scott,所以MCS并不是一个算法名称,也和自旋锁本身没啥关系。
内核开发者Tim Chen在内核中引入了MCS spinlock,通过让每个CPU在自己的自旋锁结构体变量上自旋,能够避免绝大部分的cache颠簸。
mcs_spinlock包含一个next指针和一个整形变量用于表示当前锁的状态。3.15版本的内核中mcs_spinlock定义如下:

struct mcs_spinlock {

    struct mcs_spinlock *next;

    unsigned int locked; /* 1 if lock acquired */

};

假定多个CPU需要竞争的锁为MCS Lock,MCS Lock初始时next为NULL,locked为0。MCS Lock初始状态如下图所示:

当CPU 0需要获取锁时,会使用CPU 0的锁结构,使用原子指令将自己的锁结构的地址与MCS Lock的next指针交换,并获取MCS Lock的next指针的旧值,如果该旧值为NULL,则CPU 0成功获取到锁。注意:CPU 0是第一个获取锁的,其自己的锁结构的locked的值是不需要设置为1的。CPU 0获取到锁后的状态如下图所示:

当CPU 0持锁时,CPU 1来竞争锁,CPU 1使用原子指令将自己的锁结构的地址与MCS Lock的next指针交换,并获取MCS Lock的next指针的旧值,此时该旧值为CPU 0的锁结构的地址,此时CPU 1不能获取到锁,CPU 1需要将CPU 0的锁结构的next指针指向CPU 1的锁结构地址,这样CPU 0在释放锁的时候就知道下一个排队的是CPU 1;设置好CPU 0的锁结构的next指针之后,CPU 1就需要在自己的锁的locked值上自旋直到其值变为1。CPU 0获取到锁后CPU 1来获取锁时的状态如下图所示:

CPU 0释放锁时,使用原子条件交换指令,如果MCS Lock的next指针指向CPU 0的锁结构,则将MCS Lock的next指针设为NULL,此时没有其他等待者,释放锁的流程结束;如果MCS Lock的next指针不指向CPU 0的锁结构,说明此时还有其他等待者,通过CPU 0的锁结构的next指针可以获取到下一个等待的CPU的锁结构,将下一个等待的CPU的锁结构的locked值置为1,CPU 0的解锁流程结束。注意:CPU 0解锁时如果其锁结构的next指针为NULL,但是MCS Lock的next指针不为NULL,CPU 0需要自旋一段时间以等待CPU 1设置CPU 0的锁结构的next指针。解锁过程如下图所示:

CPU 1一直在自己的锁结构的locked值上自旋,直到其值变为1之后,CPU 1获取到锁,之后就可以开始执行CPU 1临界区的代码。CPU 1获取到锁后的状态如下图所示:

MCS Lock加锁解锁的过程中有两个地方需要注意:

  1. 因为每个CPU来获取锁的时候都是使用原子指令将自己的锁结构的地址与MCS Lock的next指针交换,因此MCS Lock的next指针始终指向等待锁队列的最后一个,下一个来获取锁的CPU能将其自己的锁结构的地址添加到等待队列的队尾;该交换指令是原子操作,因此只有一个CPU能获取到当前队尾的CPU的锁结构地址,因此一个MCS Lock只可能存在一个排队队列。
  2. 每个CPU都在自己的锁结构的locked值上自旋,可以避免绝大部分的cache颠簸。

5 qspinlock

MCS Lock并没有完全替代Ticket spinlock,其中一个原因是MCS Lock的数据结构大于32bit,内核中很多重要的结构体中都内嵌了spinlock,其中一些(典型的如struct page)的体积是不允许变大的。后来Waiman Long提出了qspinlock,并由Peter Zijlstra进行了改进,形成了现在的qspinlock。qspinlock是在kernel 4.16成为默认spinlock的。
qspinlock基于MCS Lock进行优化,既解决了MCS Lock的数据结构大于32bit的问题,又避免了cache颠簸的问题。后续对qspinlock的分析都是基于linux kernel 4.19。
qspinlock结合使用32bit的qspinlock结构体和的per-CPU mcs_spinlock,设计有几个要点:

  • 数据结构压缩:qspinlock结构体的大小是32bit,使用位来表示锁状态(8bit)和队尾的CPU。
  • 避免cache颠簸:qspinlock的设计使用了MCS spinlock队列对等待的CPU进行排队,每个排队中的CPU都在自己的per-CPU MCS spinlock上自旋,因此可以避免cache颠簸。

5.1 qspinlock对数据结构的压缩

qspinlock的数据结构大小为32bit,其定义如下(include/asm-generic/qspinlock_types.h):

typedef struct qspinlock {

    union {

        atomic_t val;

        /*

         * By using the whole 2nd least significant byte for the

         * pending bit, we can allow better optimization of the lock

         * acquisition for the pending bit holder.

         */

#ifdef __LITTLE_ENDIAN

        struct {

            u8  locked;

            u8  pending;

        };

        struct {

            u16 locked_pending;

            u16 tail;

        };

#else

        struct {

            u16 tail;

            u16 locked_pending;

        };

        struct {

            u8  reserved[2];

            u8  pending;

            u8  locked;

        };

#endif

    };

} arch_spinlock_t;

qspinlock结构体由一个union组成,可以看到一个32bit的qspinlock被分成了4部分,如下图所示:

其中各部分说明如下:

  • locked:用于指示qspinlock是否加锁,0表示未加锁,其余值表示已加锁(通常情况下_Q_LOCKED_VAL == 1表示加锁,在PV场景下可能会使用_Q_SLOW_VAL == 3)。
  • pending:第一个等待锁的CPU需要先设置pending位,后续等待锁的CPU则全部进入MCS spinlock队列自旋等待。最初Waiman Long的patch并未包含该位,引入该pending位后,第一个等待者可以避免与访问自己的MCS spinlock数组相关的缓存未命中惩罚。
  • tail index:2个bit位,每个CPU在同一时刻可能存在4种不同的上下文:Normal, Software interrupt, Hardware interrupt, Non-maskable interrupt,因此每个CPU的per-CPU MCS spinlock需要包含一组共4个MCS spinlock,每个MCS spinlock对应一个上下文场景。
  • tail cpu:队尾的CPU编号(+1),将编号+1是为了和没有CPU排队的情况区分开来。

5.2 qspinlock对MCS spinlock的使用

前面提到,qspinlock的设计使用了MCS spinlock队列对等待的CPU进行排队,每个排队中的CPU都在自己的per-CPU MCS spinlock上自旋,per-cpu MCS spinlock定义如下:

/*

 * Per-CPU queue node structures; we can never have more than 4 nested

 * contexts: task, softirq, hardirq, nmi.

 *

 * Exactly fits one 64-byte cacheline on a 64-bit architecture.

 *

 * PV doubles the storage and uses the second cacheline for PV state.

 * CNA also doubles the storage and uses the second cacheline for

 * CNA-specific state.

 */

static DEFINE_PER_CPU_ALIGNED(struct qnode, qnodes[MAX_NODES]);

MAX_NODES的值为4,可以看到每个CPU有4个struct qnode结构类型的per-cpu变量,per-cpu类型定义如下:

/*

 * On 64-bit architectures, the mcs_spinlock structure will be 16 bytes in

 * size and four of them will fit nicely in one 64-byte cacheline. For

 * pvqspinlock, however, we need more space for extra data. The same also

 * applies for the NUMA-aware variant of spinlocks (CNA). To accommodate

 * that, we insert two more long words to pad it up to 32 bytes. IOW, only

 * two of them can fit in a cacheline in this case. That is OK as it is rare

 * to have more than 2 levels of slowpath nesting in actual use. We don't

 * want to penalize pvqspinlocks to optimize for a rare case in native

 * qspinlocks.

 */

struct qnode {

    struct mcs_spinlock mcs;

#if defined(CONFIG_PARAVIRT_SPINLOCKS) || defined(CONFIG_NUMA_AWARE_SPINLOCKS)

    long reserved[2];

#endif

};

stuct qnode包含了struct mcs_spinlock,在PV场景下(内核编译时指定了CONFIG_PARAVIRT_SPINLOCKS),(暂时忽略CONFIG_NUMA_AWARE_SPINLOCKS)。多CPU竞争qspinlock时的示意图如下:

  1. 第一个锁等待者在设置好pending位之后,就在qspinlock结构的locked上自旋,直到锁持有者释放锁(将qspinlock结构的locked值设置为0)。
  2. 第二个锁等待者需要将自己放入mcs_spinlock队列尾部,因为其是mcs_spinlock队列的头,其在qspinlock结构的pending | locked上自旋,直到qspinlock结构的pending和locked均变为0。
  3. 第三个及以后的锁等待者将自己放入mcs_spinlock队列尾部,并在自己的per-cpu MCS spinlock上自旋,直到到达队列头部,然后在qspinlock结构的pending | locked上自旋,直到qspinlock结构的pending和locked均变为0。

5.3 qspinlock加锁流程

如前所述,qspinlock加锁分三个阶段:

  1. 在qspinlock结构上竞争。
  2. 在mcs_spinlock队列上排队。
  3. 获取锁。

“在qspinlock结构上竞争”时,又分以下几种情况:

  • 情况一:锁当前是unlock状态,直接加锁。
  • 情况二:临时状态,此时(tail, pending, lock)的值为(0, 1, 0)。
  • 情况三:tail != 0 || pending != 0,进入mcs_spinlock队列排队。
  • 情况四:设置pending位成功,在qspinlock的locked值上自旋。
  • 情况五:竞争pending位失败,进入mcs_spinlock队列排队。

“在mcs_spinlock队列上排队”时,也分两种情况:

  • 排队情况一:已经排到队列头,在qspinlock的pending | locked上自旋。等待(tail, pending, lock)从(*, x, y)变为(*, 0, 0)。
  • 排队情况二:在mcs_spinlock队列中,在CPU的per-cpu MCS spinlock的locked上自旋,直到到达队列头部。

当锁的持有者释放锁之后,mcs_spinlock队列头的CPU可以进入“获取锁”阶段,也分两种情况:

  • 获取锁情况一:没有其他排队的竞争者,加锁并将qspinlock的tail清空。
  • 获取锁情况二:有其他排队的竞争者,加锁并让mcs_spinlock队列中的下一个CPU成为队列头。

qspinlock加锁流程如下图所示:

下面的描述中将省略(tail, pending, lock),出现形如(x, y, z)均表示qspinlock结构中的tail == x && pending == y && lock == z。

5.3.1 在qspinlock上竞争情况一

锁当前是unlock状态,即(0, 0, 0),直接加锁,然后进入临界区。这是最快的情况,对应的代码在queued_spin_lock函数中,具体可以参看代码注释:

  • 6
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
PV操作是进程管理中的一种同步机制,用于实现进程间的互斥和同步。在PV操作中,P操作表示申请资源或进入临界区,V操作表示释放资源或离开临界区。 P操作的原理是通过将信号量(也称为计数器)减1,若结果小于0,则该进程暂停执行,并进入等待队列。这是为了确保资源的独占性,同一时间只有一个进程能够进入临界区或获取资源。 V操作的原理是通过将信号量加1,若结果小于或等于0,则唤醒等待队列中的一个进程。这是为了释放资源或离开临界区,让其他进程有机会获取资源。 通过使用PV操作,可以实现多个进程之间的协调和合作,确保资源的正确使用和互斥访问。它是解决生产者消费者问题、避免死锁等并发编程中常用的同步机制。 总结起来,PV操作原理就是通过P操作申请资源或进入临界区,V操作释放资源或离开临界区,以实现进程间的同步和互斥。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [进程同步互斥中的的PV操作](https://blog.csdn.net/m0_46894211/article/details/105921470)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [进程管理PV信号量购书店题详解](https://blog.csdn.net/lqb3732842/article/details/126741147)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值