当前位置: 首页 > 网络应用技术

在对分裂锁的深度分析中,I ++可能导致灾难

时间:2023-03-05 22:19:53 网络应用技术

  Split Lock是CPU支持的内存总线锁,以支持跨缓存线的原子存储器访问。

  某些处理器(例如ARM和RISC-V)不允许不令人满意的内存访问,并且原子能访问交叉缓存线不会产生拆分锁,并且支持X86。

  对于开发人员而言,拆分锁非常方便,因为无需考虑非对齐访问的问题,但这也是一个成本:生成拆分锁的指令将垄断约1,000个时钟周期的内存总线条件下一个添加指令只需要小于10个时钟周期。锁定内存总线会导致其他CPU无法访问内存,并会严重影响系统性能。

  因此,分裂锁的检测和处理非常重要。当前的CPU支持检测功能。可以检测到,如果内核状态直接慌张,它将尝试积极睡眠以减少拆分锁或杀戮用户模式过程产生的频率,然后减轻竞争内存总线的竞争。

  引入虚拟化后,您将尝试处理主机端。KVM通知QEMU的VCPU线程会积极睡眠,以减少拆分锁产生的频率,甚至是杀死虚拟机。上述结论仅是现在的2022/4/19(下面相同)。在过去的两年中,社区对分裂锁定条件的处理有不同的看法。

  我们假设最简单的计算模型,一个CPU(单核,无超线程,没有缓存),一个内存。

  在此处分析教学的语义。需要两个操作,即源操作编号SRC和目标操作编号DEST,实现函数是。CPU需要首先读取i的内容,然后添加1,最后将结果写入我所在的内存地址。总共生成了两个串行内存操作。

  如果计算体系结构复杂,则有2个CPU内核和核心,上述代码必须考虑数据一致性问题:

  1.1.1平行写作问题。如果Corea写在I的内存地址中,该怎么办?

  相同的内存地址实际上非常简单。CPU保证了硬件基本内存操作的原子量。

  具体操作是:

  1.1.2写覆盖问题。如果Corea从内存中读取I,则在我编写I的内存地址之前的时间内,如果CoreB从i i的内存地址编写数据,该怎么办?

  在这种情况下,CoreB编写的数据由Corea编写的数据涵盖,这导致CoreB的书面数据丢失了,Corea不知道书面数据已在阅读后已更新。

  为什么会出现此问题?这是因为添加指令不是原子操作,它将产生两个内存操作。

  如何解决此问题?由于添加指令不是硬件中的原子,因此可以从软件锁定实现原子操作,因此在Corea的内存操作之前,无法执行CoreB的内存操作。

  相应的方法是声明指令前缀,并且汇编代码变为。

  在说明前缀语句之后,执行的指令将成为原子说明。原则是,在执行指令期间,系统总线被锁定,并且其他处理器被禁止执行内存操作,以使其垄断记忆以实现原子质操作。

  让我们以下面的几个示例:

  1.2.1 QEMU中原子中的Qatomic_inc(PTR)函数添加到参数PTR方向的存储器数据中。

  原则是调用GCC的构建-IN -IN -IN __sync_fetch_and_add函数。我们编写一个C程序以查看__sync_fetch_and_add的汇编。

  可以看出,__sync_fetch_and_add的汇编实现是在添加指令前方声明锁定指令前缀。

  1.2.2原子中内核中的原子添加了内核中的Atomic_inc函数,将参数V的存储数据添加到1。

  可以看出,还声明了锁定指令前缀。

  1.2.3 CAS(比较和交换)编程语言中的CAS界面为开发人员提供了原子操作,无法实现锁定机制。

  Golang的Cas

  爪哇的cas

  可以看出,也使用锁定指令前缀实现CAS,那么锁定前缀特定于特定于锁定前缀?

  1.2.4锁定#信号特别是,代码中的说明先前已声明了锁定前缀指令,并且处理器将在指令操作期间生成锁定#信号,因此其他处理器无法通过总线访问内存。

  我们试图从8086 CPU的引脚窥视豹子,以了解锁定#信号的原理。

  8086 CPU具有锁定引脚(图中的引脚29),可在低级别上有效。锁定说明前缀时,将锁定引脚级别拉下以执行sostert操作。目前,其他设备无法获得系统总线的控制。执行锁定指令指令后,将锁定引脚级别拉到脱键。

  因此,整个过程很明确。当非原子指令(例如添加)实施原子操作时,需要在指令之前声明锁定指令前缀。信号使其成为内存总线的独有,而其他处理器无法通过内存总线访问内存,这实现了原子操作。因此,它解决了上述写作覆盖范围的问题。

  看起来不错,但这再次引入:

  1.2.5由公交锁引起的性能下降。现在有越来越多的处理器。如果每个核经常生成锁定#信号以垄断内存总线,以使剩余的核无法访问内存,从而导致性能很好,我该怎么办?

  为了优化由总线锁引起的性能问题,英特尔在P6之后引入了处理器上的缓存锁定机制:通过缓存一致性协议,以确保多个CPU核访问Cross Cache Line的存储器地址的多个访问地址访问多个访问地址的访问多个访问地址,并且一致性,无锁。

  1.3.1 MESI协议首先引入了使用公共MESI.MESI分为四个状态的缓存一致性协议:

  MESI协议状态计算机如下:

  状态机的转换基于两种情况:

  简而言之,通过MESI协议,每个CPU不仅知道其读取和写作的缓存操作,而且还会执行忙碌的嗅探。您可以知道其他CPU的读取和写作操作。它将根据其他CPU的修改为缓存更改缓存的状态。

  1.3.2缓存锁定原理缓存锁定是对缓存一致性协议的内存访问的原子性,因为缓存一致性协议无法通过多个CPU通过多个CPU缓存来修改多个CPU。

  在下面,我们根据示例基于MESI协议分析记忆阅读的原子性。

  我们仍然假设有两个CPU核心,Corea和CoreB可以分析。

  请注意,最后一个操作步骤4,CoreB修改了缓存中的数据,当Corea想要再次修改它时,它将被CoreB嗅探。只有在将CoreB的数据同步到主内存和Corea之后,Corea才会被修改。

  可以看出,由CoreB修改的数据并没有丢失,并且Corea和主内存已同步。上述操作没有锁定内存总线,但是Corea的修改操作已被阻止。与整个内存总线相比,这是可以控制的。

  以上是一个相对简单的情况。两个CPU核心的撰写是序列的。如果Corea和CoreB在操作步骤2之后的同一时间发出了请求,该怎么办?两个缓存的核心会进入M状态吗?

  答案是否定的,MESI协议保证上述情况不会同时发生。根据MESI协议,只有在其缓存为M或E.拥有(RFO)BUS时才能自由执行核心PRWR操作广播,RFO是一项公交交易。如果两个核心同时进行RFO对手的缓存,则总线将是仲裁。核心将失败,其缓存将设置为i状态。因此,我们可以看到在引入缓存层后,原子操作从锁定内存总线更改为总线仲裁。

  如果声明了锁定指令前缀,则相应的缓存地址将由总线锁定。在上面的示例中,其他核心将等到访问之前执行指令。

  然后总结缓存锁定:锁定指令前缀在代码指令之前声明。如果您希望原子访问内存数据,则存储器数据可以是缓慢的缓存一致性协议,总线仲裁机制和缓存锁,则可以防止两个或多个CPU内核访问相同的地址。

  因此,所有总线锁都可以优化到缓存锁中吗?答案是否定的,无法优化的情况是拆分锁。

  由于缓存一致性协议的粒度是缓存线路,因此当原子操作跨缓存线操作的数据时,依赖缓存锁定机制无法确保数据的一致性,并且它将退化为总线锁定以确保一致性。这种情况是拆分锁,分裂,分裂,分裂,拆分也可以理解,缓存被拆分捕获到两行。

  例如,有以下数据结构:

  当它被缓存以使用64个字节大小的缓存线缓存时,跨缓存线的值成员。

  目前,如果要在数据结构中操作值成员,则无法通过缓存锁解决。您只能沿着旧的道路锁定公共汽车以确保数据一致性。

  锁总线将导致严重的性能下降,并且访问的延迟将增加约100倍。如果是内存 - 类型的业务,则性能将降低2级。因此,在现代X86处理器中,我们必须避免编写将生成拆分锁定的代码并具有检测拆分锁定的能力。

  回顾拆分锁的条件:

  由于原子操作是一个相对基本的操作,因此我们通过跨缓存线的数据存储来分析干预点。

  如果数据仅存储在缓存线中,则可以解决该问题。

  我们面前的数据结构对此GCC的特性很有用,表明它不会优化内存对。

  如果未引入,当对内存进行优化时,编译器将填充内存数据。例如,填充填充后的2个字节,以便可以通过4个字节删除值的内存地址以实现对齐。当将其缓存到缓存时,值将不会越过缓存线。

  由于可以优化编译器,因此您可以通过内存避免在缓存线上访问。为什么要引入?

  这是因为它也通过强制性按下数据结构也是好的。例如,基于数据结构的网络通信不需要填充多余的字节。

  在编写代码的过程中,我们需要注意以下几点:

  以下主要基于云环境,并从底部进行分析。

  尝试拆分锁定操作时,将生成对齐检查(#ac)异常。当获得和执行总线锁时,将生成调试(#DB)陷阱。

  硬件是通过拆分锁和总线锁来区分的:

  在概念上,拆分锁是一种总线锁。拆分锁会倾向于跨缓存线访问,并且总线锁的业务倾向于锁定巴士。

  3.2.1当发生拆分锁定和总线锁时,是否发生相关寄存器(MSR),这会生成相应的异常,该异常可以由特定寄存器控制,相关控制寄存器如下。

  以v5.17版本为例。内核的当前版本支持相关的启动参数split_lock_detect。配置项和相应的功能如下:

  Split_lock_detect的实现主要分为3个部分:配置,初始化,处理,让我们一一分析源代码:

  3.3.1配置

  当内核启动时,SLD(拆分锁定检测)首先启动。

  在_split_lock_setup中尝试启用/禁用33H MSR进行验证。最后,没有启用拆分锁#ac例外。相反,只有一个全局变量MSR_TEST_CTRL_CACHE用作此MSR的缓存。

  SLD_STATE_SETUP确实可以分析内核启动参数split_lock_detect的配置(您可以看到默认配置是警告级别)。如果它是比例配置,则内核的比例库将hander阶段的bld_ratelimit全局变量初始化。

  3.3.2初始化

  设置以完成基本验证并获取启动参数配置后,您将尝试执行硬件Enbale操作。

  3.3.2.1拆分锁初始化

  在拆分锁定的初始化中,如果发现配置参数是比例的,则禁用拆分锁的硬件检测。其他非off参数(警告,致命)将启用硬件。

  3.3.2.2公交锁初始

  在总线锁的初始化中,如果CPU不支持总线锁,则将无法检测到启用总线锁的硬件检测。

  因此,CPU有两种类型的总线锁检测:首先,CPU支持总线锁定检测,配置参数被指定为Ratelimt.essence。

  3.3.3处理3.3.3.1拆分锁手柄

  如果是用户模式,则在配置为致命时,Sigbus信号将发送到当前的用户状态进程。用户模式过程将被杀死,而无需手动捕获Sigbus。

  如果是用户模式,则在配置为警告时,它将打印警告日志并输出当前的进程信息。同时,禁用拆分锁检测它,这意味着通过设置当前过程的TIF_SLD位来测试此过程一次。

  当Context_Switch的进程开关的过程切换时,如果上次的标志中的TIF_SLD位不同,则执行拆分锁定检测开关。切换的基础是下一个过程的TIF_SLD位。

  例如,在CPU 0处理后,A触发了拆分锁的警告检测,将禁用CPU 0的拆分锁测试,以避免频繁警告日志。进程A的标志位置TIF_SLD.SWITCH到该过程B在CPU 0上运行。在切换过程中,由于A和B的标志的TIF_SLD位不同,我们将按照根据启用的拆分锁定的标志来检测到启用的拆分锁。该过程B.在那个时候,它将被禁用拆分锁。

  上述机制实现了仅警告一次的每个过程的需求。

  3.3.3.2巴士锁手柄

  总线锁生成的频率被强行降低为配置比率。原理是,如果频率超过设置,则直接直至频率下降直至降低(这里的频率是整个系统的频率)生成总线的频率锁)。

  频率降档之后,通过编译器的跌文生成警告日志中的SLD_WARN情况。

  先前的分析是物理计算机上的内核和用户 - 状态程序(VMX根模式)。在虚拟化环境中,您需要考虑一些问题。例如,如果拆分锁来自客人,房屋如何检测到?如何避免对其他客人的影响?如果您直接在启用公交锁定率上,它可能会影响尚未准备就绪的客人。锁定检测开关接触客人,如何与主机或其他客人打交道等。

  CPU可以支持在VMX模式下通过拆分锁检测到的#AC陷阱,并且稍后由Hyprovisor确定如何通过管理程序来确定它。大多数操纵组织将直接将陷阱直接转发给来宾。如果客人还没有准备好,可能会生成崩溃。先前的VMX版本中存在问题。

  因此,必须正确处理管理程序。

  以下是虚拟化环境的整体处理流程图,用于分开锁定和总线锁:

  尝试客人中的分开锁操作,然后VM出口到KVM。如果硬件不支持拆分锁定检测或遗留#ac,则#AC将注入来宾。如果硬件支持检测,则将根据配置警告,甚至可以尝试。

  客人是总线锁后,VM出口将在KVM中,KVM通知QEMU,VCPU线将积极执行睡眠频率降低。

  让我们从底部进行分析。

  4.2.1拆分锁

  4.2.2巴士锁

  在VMX非根模式下,在检测到CPU后,CPU将生成VM出口,原因为74。

  4.3.1拆分锁

  当客人内部生成拆分锁定操作时,因为它是#ac例外,VM出口将出现。

  首先,#AC异常本身有两种类型:有两种类型:

  KVM最终根据主人和来宾国家产生了两种行为:

  让主机处理:生成警告甚至发送Sigbus。

  4.3.2巴士锁

  VM退出后,根据74索引执行函数handle_bus_lock_vmexit,其中exit_reason的bus_lock_detected位置。

  执行__vmx_handle_exit后,检测到BUS_LOCK_DETECT,然后将EXIT_REASAS和标志返回到用户状态。

  以v6.2.0版本为例:

  Qemu了解到,来宾中有BUS锁定通过KVM返回值,然后输入KVM_RATE_LIMIT_ON_BUS_LOCK。它还可以通过睡眠来达到比例,以降低客人锁定频率的效果。

  主机上的ratelimit由start_lock_detect启动参数控制,访客呢?

  QEMU的访客总线锁的比例是从启动参数总线摇杆 - 埃拉特氏菌获得的。

  Libvirt支持QEMU的BUS锁比例启动参数。

  https://listman.redhat.com/archives/libvir-list/2021- 12月/225755.html

  由于X86硬件的特征,支持交叉缓存线的原子语义,有必要使用拆分锁来维持原子能,但是这需要以成本为代价。开发人员逐渐意识到此功能不是浅的,可以尝试一下不使用它。例如,内核开发人员保证不会产生拆分锁,即使以内核恐慌为代价。用户-State程序将生成警告,然后降低程序或杀戮的执行频率,由内核执行。由于软件堆栈的数量大量,虚拟化环境将被视为主机侧KVM和QEMU,以提醒甚至杀死虚拟机,或者将QEMU通知QEMU以降低频率。

  原始:https://juejin.cn/post/7096082003933528077