从横向和纵向了解 CPU Cache 缓存一致性

2023年 9月 2日 114.1k 0

📣 大家好,我是Zhan,一名个人练习时长两年的大三后台练习生🏀

📣  这篇文章是 操作系统 第三篇笔记📙

📣 如果有不对的地方,欢迎各位指正🙏🏼

📣 Just do it! 🫵🏼🫵🏼🫵🏼

🔔 引言

在上篇文章程序员应该需要了解的CPU Cache知识和使用技巧 - 操作系统(二)中,我们了解到了 CPU 如何根据数据的地址进行映射,查找 Cache 中是否存在该数据,也就是 CPU Cache 的读取,而本篇我们将要介绍 Cache 的写入,对于缓存来说,写入的方式是我们需要重点去要了解的,就像 Redis 的缓存,我们如何保证缓存的一致性,是本文需要解决的问题。

1️⃣ 数据的写入方式

CPU Cache的数据写入通常有两种方式,分别是写回(Write-Back)和写直通(Write-Through)。这两种方式在处理缓存和主内存之间的数据写入时有不同的策略。

🚌 写直通

在这种方式下,当 CPU 需要写入数据要缓存的时候,数据会被同时写入缓存和内存中。 这样的话,确实可以保证缓存和内存的数据一致性,但是会增加主内存的写入频率,且花费大量的时间,影响性能。

🚈 写回

当发生写操作的时候,新的数据仅仅被写入 Cache Block 里,只有当修改过的 Cache Line 被替换的时候才需要写到内存中,听起来有些抽象,看看下面的流程图:

500

  • 判断目前需要存储的数据是否存在于 Cache 中,对于不存在的数据,我们直接写入 Cache Line 中,同时标记该数据为脏数据(即内存和缓存不一致),不写入内存中,要注意写回这种方式,只有数据为脏数据且要被替换的时候才会写入内存中
  • 如果通过映射得到的地址中有其他的缓存的数据的话,就需要检测该数据是否为脏数据:
  • 如果是脏数据,现在需要取代它的位置,就要把这个脏数据写入内存,同时为了防止原本的数据在这个期间被 CPU 的其他核心修改,我们需要再次从内存中获取这个地址的值进行写入到 Cache LIne 中(即不是在同步脏数据后直接把原数据写入)
  • 如果不是脏数据,就只需要从内存中再次读值然后写入 Cache LIne 中
  • 综上,其实我们可以发现,CPU 直接操作的是 Cache,而只有在脏数据被取代的时候才会操作内存,不像写直达,每次都要操作内存,影响性能

    2️⃣ 缓存数据一致性

    🚩 写回策略的数据不一致性

    尽管写回策略看上去比写直达策略更好,能够有更好的性能,更快的响应时间,不过写回策略无法保证数据的一致性,如果在缓存中的脏数据尚未同步到内存中的时候,系统崩溃或者断电,就可能会导致数据的不一致性。

    而为了保证数据一致性,通常会在写回策略中引入额外的机制:

  • 写缓冲区:在处理器和缓存控制器之间引入写缓冲区,用于临时存储已经修改的数据。这样,即使数据尚未写回到内存,处理器也可以继续执行操作,从而提高性能。有点类似于一个异步线程
  • 日志记录:在写回策略中,可以用日志记录下缓存和内存之间的数据操作,这样有利于在发生故障的时候进行数据一致性的恢复。
  • 🏴 多核导致的数据不一致性

    如果两个 CPU 核心都要修改一个变量 VAR,它在内存中的值为 10,而第一个核心让该变量进行自增操作,也就是变为 11,但是这个修改,按照写回的策略,仅仅是在缓存中,没有同步到内存中,那么另外一个 CPU 核心让这个变量翻倍,VAR 的值变为 20,在进行脏数据同步到内存的时候就会发生矛盾

    其实这个问题让我想到了 volatile 这个轻量锁,用它修饰的变量可以让多线程可见,对呀,如果单个 CPU 在修改这个变量的时候,能把修改对其他的 CPU 可见

    当然,除了要保证对其他的 CPU 可见,还要保证修改这个值的时候要加锁,保证事务的串行化

    如果都是对变量做更改,需要保证其他核心对于更改的顺序是一致的,否则就会出现,其他的核心看到的变量的值并不一样,要做到这一步,那就不得不加锁,只有拿到了锁才能对数据对更改

    3️⃣基于总线嗅探机制的 MESI 协议

    对于 Java 这门高级语言来说,我们可以简单的用 volatile 实现可见性,但是对于 CPU 这个硬件来说,写传播(更改让其他 CPU 核心可见) 和 事务串行化(加锁)的具体实现就不一样了:

    🔗 总线嗅探

    CPU多个核心之间有一条总线,这是它们可以相互通信的方法,那么 CPU 每时每刻监听总线上的一切活动,不管其他的核心是否缓存了这个数据,都会发出一个广播表明数据的修改,这样其他的 CPU 核心在监听到后,判断自己的 L1 Cache 和 L2 Cache 中有没有该数据,有则修改。

    但是,第一,总线嗅探只能保证其他CPU核心能够意识到它的数据的变更,但是还是无法保证事务的串行化,第二,不管其他的CPU核心有没有这个数据都会发出广播,总线的带宽压力很大

    📜 MESI 协议

    MESI 协议解决了上面的问题,实现了事务串行化的同时,使用状态机降低了总线带宽的压力,至此基于总线嗅探机制的 MESI 协议就实现了 CPU 缓存一致性,那么下面就会讲解 MESI 协议是怎么做的:

    MESI 协议其实是 4 个状态的开头字母缩写:Modified-已修改、Exclusive-独占,Shared-共享、Invalidated-已失效,依赖这四个标志,成功的实现了 CPU 缓存一致性:

    • 【已修改】:就是上面说到的脏标志,代表这个 Cache Line 的数据是被修改过的
    • 【已失效】:该 Cache Line 中的数据已经失效了,不能够被读取
    • 【独占】:代表 Cache Line 中的数据只是当前 CPU 独有的,即其他的核心中并不缓存这个数据,就可以自由的读写,而不广播到其他的核心
    • 【共享】:与【独占】相反,它代表数据是被多个 CPU 核心所共享的,因此在我们需要进行更新数据的时候,应该先要向其他的核心广播,然后其他的核心把该 Cache Line 设置为【已失效】,然后更新当前 Cache 中的数据,并把数据的状态设置为【已修改】

    注意:我们在把其他的核心设置为【已失效】后,是不会恢复它的状态的,因此对于【已修改】和【独占】的数据,可以直接修改更新数据而不需要广播给其他的 CPU 核心

    这里还有一个 MESI 协议可视化网站,大家可以根据本文所学知识进行调试,与自己形成的知识体系做对比,看看是否能够得到验证

    💬 总结

    其实说到缓存,一致性这个问题就是我们躲不开的,本文主要从两个维度来解决缓存一致性的问题:

    • 纵向:写回和写直达,讨论了如何让 Cache 和 内存 的数据一致性
    • 横向:基于总线嗅探机制的 MESI 协议,讨论了 CPU 多核 之间的数据一致性

    对于纵向的缓存一致性,我们讨论了写回和写直达两种方式:

    • 写直达:在写入 Cache 的时候 同时写入主存中,这种方式对于性能的影响很大
    • 写回:直接操作 Cache,而只有在脏数据被取代的时候才会操作内存,不像写直达,每次都要操作内存,影响性能,但是 断电、系统故障 也可能导致脏数据没有及时同步到内存,现代的 CPU 会使用 写缓冲区、日志记录 的方式尽力的去挽救

    对于横向的缓存一致性,我们讨论了需要解决的两个问题:

    • 写传播:这点我们可以通过总线进行多核 CPU 之间的传播,需要每时每刻监听总线上的信息
    • 事务串行化:这点我们使用了 MESI 协议,通过对数据进行状态的标记,这样不仅能实现事务串行化,还有效的降低了主线监听的频率

    🍁 友链

    • 无论你是科技爱好者还是程序猿,冯诺依曼体系结构你得知道! - 操作系统(一)
    • 程序员应该需要了解的CPU Cache知识和使用技巧 - 操作系统(二)
    • 计算机系统 #10 12 张图看懂 CPU 缓存一致性与 MESI 协议,真的一致吗?
    • MESI 协议可视化网站
    • 小林coding (xiaolincoding.com)

    ✒写在最后

    都看到这里啦~,给个点赞再走呗~,也欢迎各位大佬指正以及补充,在评论区一起交流,共同进步!也欢迎加微信一起交流:Goldfish7710。咱们明天见~

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论