计算机组成原理:如何让多核CPU的高速缓存保持一致

本文探讨了多核CPU中的缓存一致性问题,重点介绍了MESI协议如何解决这个问题。MESI协议通过写失效机制确保写传播,并通过状态机管理(Modified, Exclusive, Shared, Invalidated)实现事务的串行化,从而保证数据的一致性。缓存一致性对于多核系统和数据库事务处理至关重要。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

多核CPU有很多好处,其中最重要的一个就是,它使得我们在不断提升CPU的主频之后,找到了另一种提供CPU吞吐率的方法。

多核CPU里的每一个CPU核,都有独立的属于自己的L1 Cache和L2 Cache。多个CPU之间,只是共用L3 Cache和主内存。

在这里插入图片描述

但是因为CPU的每个核都有各自的缓存,互相之间的操作又是各自独立的,就会带来缓存一致性问题。

在这里插入图片描述

ps:语言层面解决缓存不一致,可以使用volatile,强制CPU读写都直接访问主内存。(volatile的本意是告诉编译器,此变量的值是易变的,每次读写该变量的值时务必从该变量的内存地址中读取写入,不能为了效率使用对一个“临时”变量的读写来代替对该变量的直接读写)

为了解决这个缓存不一致的问题,我们就需要有一种机制,来同步两个不同核心里面的缓存数据。而这样的机制必须满足如下条件:

  • 写传播:就是说,在一个CPU核心里,我们的Cache数据更新,必须能够传播到其他的对应的节点的Cache Line中
  • 事务的串行化:意思是,在一个CPU核心里面的读取和写入,在其他节点看起来,顺序是一样的

对于第二点,事务串行化,举个例子:

在这里插入图片描述

  • 我们找一个有 4 个核心的 CPU。1 号核心,先把 iPhone 的价格改成了 5000 块。差不多在同一个时间,2 号核心把 iPhone的价格改成了 6000 块。这里两个修改,都会传播到 3 号核心和 4 号核心。
  • 然而这里有个问题,3 号核心先收到了 2 号核心的写传播,再收到 1 号核心的写传播。所以 3 号核心看到的 iPhone 价格是先变成了 6000 块,再变成了 5000 块。而 4 号核心呢,是反过来的,先看到变成了 5000 块,再变成 6000 块。虽然写传播是做到了,但是各个 Cache 里面的数据,是不一致的。
  • 事实上,我们需要的是,从 1 号到 4 号核心,都能看到相同顺序的数据变化。比如说,都是先变成了 5000 块,再变成了 6000 块。这样,我们才能称之为实现了事务的串行化。

事务的串行化,不仅仅是缓存一致性中所必须的。比如,我们平时所用到的系统中,最需要保障事务串行化的就是数据库。多个不同的连接去访问数据库的时候,我们必须保证事务的串行化,做不到事务的串行化的数据库,根本没法作为可靠的商业数据库来使用。

而在CPU Cache里做到事务串行化,需要做到两点,第一点是一个CPU核心对于数据的操作,需要同步通信到其他CPU核心。第二点是,如果两个CPU核心里同一个数据的Cache,那么对于这个Cache数据的更新,需要有一个“锁”的概念,只有拿到了对应的Cache Block的“锁”之后,才能进行对应的数据更新。 而MESI协议实现了这两个机制

总线嗅探机制和MESI协议

要解决缓存一致性问题,首先要解决的是多个CPU核心之间的数据传输问题。最常见的一种解决方案,叫做总线嗅探

  • 这个策略,本质上是把所有的读写请求都通弄过总线广播给所有的CPU核心。然后让各个核心去“嗅探”这些请求,在根据本地的情况进行响应。
  • 总线本身就是一个特别适合广播进行数据传输的机制,所以总线嗅探这个办法也是我们日常使用的 Intel CPU 进行缓存一致性处理的解决方案。
  • 基于总线嗅探机制,其实还可以分成很多种不同的缓存一致性协议。不过其中最常用的,就是今天MESI 协议。

MESI协议,是一种叫做写失效的协议:

  • 在写失效协议里面,只有一个CPU核心负责写入数据,其他的核心,只是同步读取到这个写入
  • 在这个CPU核心写入Cache之后,它会去广播一个“失效”请求告诉所有其他的CPU核心。
  • 其他的CPU核心,只是去判断自己是否也有一个“失效”版本的Cache Block,然后把这个也标记成失效的就好了。
    在这里插入图片描述

相对于写失效协议,还有一种叫作写广播(Write Broadcast)的协议。

  • 一个写入请求广播到所有的 CPU 核心,同时更新各个核心里的 Cache。
  • 写广播在实现上自然很简单,但是写广播需要占用更多的总线带宽。
    • 写失效只需要告诉其他的 CPU 核心,哪一个内存地址的缓存失效了
    • 但是写广播还需要把对应的数据传输给其他CPU 核心

在这里插入图片描述

MESI 协议的由来呢,来自于我们对 Cache Line 的四个不同的标记,分别是:

  • M:代表已修改(Modified)
  • E:代表独占(Exclusive)
  • S:代表共享(Shared)
  • I:代表已失效(Invalidated)

所谓的“已修改”,就是“脏”的 Cache Block。Cache Block 里面的内容我们已经更新过了,但是还没有写回到主内存里面。

所谓的“已失效“,自然是这个 Cache Block 里面的数据已经失效了,我们不可以相信这个 Cache Block 里面的数据。

“独占”和“共享”这两个状态是 MESI 协议的精华所在了。

  • 无论是独占状态还是共享状态,缓存里面的数据都是“干净”的。
  • 这个“干净”,自然对应的是前面所说的“脏”的,也就是说,这个时候,Cache Block 里面的数据和主内存里面的数据是一致的。

那么“独占”和“共享”这两个状态的差别在哪里呢?

  • 独占状态下:
    • 对应的Cache Line只加载了当前CPU核所拥有的Cache里。其他的CPU核,并没有加载对应的数据到自己的Cache里。
    • 这个时候,如果要向独占的Cache Block写入数据,我们可以自由的写入数据,而不需要告知其他的CPU核。
    • 在独占状态下的数据,如果收到了一个来自于总线的读取对应缓存的请求,它就会变成共享的。这个共享状态是因为,这个时候,另外一个CPU核心,也把对应的Cache Block,从内存里面加载到了自己的Cache里来
  • 共享状态下:
    • 因为同样的数据在多个 CPU 核心的 Cache 里都有。
    • 所以,当我们想要更新 Cache 里面的数据的时候,不能直接修改,而是要先向所有的其他 CPU 核心广播一个请求,要求先把其他 CPU 核心里面的 Cache,都变成无效的状态,然后再更新当前Cache 里面的数据。
    • 这个广播操作,一般叫作 RFO(Request For Ownership),也就是获取当前对应 Cache Block 数据的所有权。

(这个操作有点儿像我们在多线程里面用到的读写锁。在共享状态下,大家都可以行去读对应的数据。但是如果要写,我们就需要通过一个锁,获取当前写入位置的所有权。)

整个 MESI 的状态,可以用一个有限状态机来表示它的状态流转。需要注意的是,对于不同状态触发的事件操作,可能来自于当前 CPU 核心,也可能来自总线里其他 CPU 核心广播出来的信号。
在这里插入图片描述

总结

想要实现缓存一致性,关键要满足两点:

  • 第一个是写传播,也就是在一个CPU核心写入的内容,需要传播到其他CPU核心里面
  • 更重要的是第二点,保障事务的串行化,才能保障我们的数据是真正一致的,我们的程序在各个不同的核心上运行的结果也是一致的。这个特性不仅在 CPU 的缓存层面很重要,在数据库层面更加重要。

MESI是基于总线嗅探机制的,是一种基于写失效的缓存一致性协议。写失效协议的好处是,我们不需要在总线上传输数据内容,而只要传输操作信号和地址信号就好了,不会那么占总线带宽。

MESI 协议,是已修改、独占、共享以及已失效这四个缩写的合称。独占和共享状态,就好像我们在多线程应用开发里面的读写锁机制,确保了我们的缓存一致性。而整个MESI的状态变更,则是根据来自自己的CPU核心的请求,以及来自其他CPU核心通过总线传输过来的操作信号和地址信息,进行状态流转的一个有限状态机。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值