Gem5模拟器学习(八)————通过Ruby实现缓存一致性协议
Ruby
Ruby是一个详细的内存子系统的模拟模型。可以通过各种替换策略、一致性协议实现、互连网络、DMA和内存控制器、发起内存请求和处理响应的各种定序器来建模包含(inclusive)/独占(exclusive)缓存层次结构。这些模型是模块化的、灵活的和高度可配置的。
Ruby有以下三个特点:
- 关注点分离。将内存系统的各个模型模块化,例如,一致性协议规范与替换策略和缓存索引映射是分开的,网络拓扑结构与实现也是分开指定的。
- 丰富的可配置性。几乎所有影响内存层次结构的功能和时序都可以控制。
- 快速的原型设计。使用一种高级规范语言SLICC来指定各种控制器的功能。
SLICC + 一致性协议
SLICC:Specification Language for Implementing Cache Coherence,是一种特定领域的语言,用于指定缓存一致性协议。
缓存一致性协议以状态机的方式工作,而SLICC用于指定状态机的行为。SLICC文件以“.sm”结尾,它们是状态机文件。每个文件描述状态、某些事件从开始状态到结束状态的转换,以及在转换过程中要采取的操作。
每个一致性协议都由多个SLICC状态机文件组成。这些文件是用SLCC编译器编译的,该编译器是用Python编写的,也是gem5源代码的一部分。SLIC编译器获取状态机文件并输出一组C++文件,这些文件与gem5的所有其他文件一起编译。这些文件包括SimObject声明文件以及SimObjects和其他C++对象的实现文件。
实现一致性协议的步骤
以下是对官网教程gem5: Introduction to Ruby的内容整理,教程的目标是实现MSI协议,协议的具体内容可以在《A Primer on Memory Consistency and Cache Coherence》书的第8.2节找到 (pages 141-149)
1. 注册状态机
MSI协议是通过SLICC语言编写的状态机文件实现的,这些状态机文件是用SLCC编译器编译的,会通过scons与gem5其他文件一起编译,因此需要为SCons创建一个文件,以便知道要编译什么。这里我们创建一个Sconsopts
文件,而不是Sconscript
文件,这是因为Sconsopts
会在Sconscript
之前执行,而我们也需要在编译gem5之前编译状态机文件。
1 | Import('*') |
2.编写状态机文件
编写状态机文件是实现一致性协议的最主要工作,状态机文件通常包含以下几个部分:
Parameters
These are the parameters for the SimObject that will be generated from the SLICC code.
Declaring required structures and functions
This section declares the states, events, and many other required structures for the state machine.
In port code blocks
Contain code that looks at incoming messages from the (
in_port
) message buffers and determines what events to trigger.Actions
These are simple one-effect code blocks (e.g., send a message) that are executed when going through a transition.
Transitions
Specify actions to execute given a starting state and an event and the final state. This is the meat of the state machine definition.
Over the next few sections we will go over how to write each of these components of the protocol.
2.1 状态机声明
创建一个名为MSI-call.sm
的文件,并按以下格式声明状态机
1 | machine(MachineType:L1Cache, "MSI cache") |
MachineType:L1Cache
将状态机命名为L1Cache
,SLICC 将使用该名称为我们生成许多不同的对象。例如,一旦编译了这个文件,就会有一个新的 SimObject: L1Cache_Controller
作为缓存控制器。这个声明中还包括对这个状态机的描述: “ MSI cache”。
2.2 状态机参数声明
状态机参数的声明在冒号(:)之后
1 | machine(MachineType:L1Cache, "MSI cache") |
排序器(Sequencer)
Sequencer是一个带有从端口的gem5 MemObject,因此它可以接受来自其他对象的内存请求。Swquencer接受来自CPU(或其他主端口)的请求,并将gem5数据包转换为RubyRequest。最后,RubyRequest被推送到状态机的强制队列(commandoryQueue)中。
缓存数据(Cache Memory)
用于保存缓存数据(即缓存条目)的内容。
消息缓冲区(MessageBuffer)
消息缓冲区是状态机和Ruby网络之间的接口。通过消息缓冲区发送和接收消息。因此,对于我们协议中的每个虚拟通道,我们都需要一个单独的消息缓冲区。
虚拟网络的作用是防止死锁。MSI协议需要三个不同的虚拟网络。在该协议中,最高优先级是响应Response(虚拟网络2),其次是转发的请求Forwarded Requests(虚拟网络1),然后请求Requests具有最低优先级(虚拟网络0)。
代码中有两个”To” buffer,两个”From” buffer和一个Special buffer。其中,”To” buffer类似于gem5中的主端口,是此控制器用来向系统中的其他控制器发送消息的消息缓冲区;”From” buffer类似于gem5中的从端口,是此控制器用来接收系统中其他控制器发送的消息的消息缓冲区;对于Special buffer,Sequencer使用此消息缓冲区将gem5数据包转换为Ruby请求。与其他消息缓冲区不同,commandoryQueue不连接到Ruby网络,并且,此消息缓冲区的名称是硬编码的,必须为“commandoryQueue”
两个”To” buffer一个用于低优先级请求,另一个用于高优先级响应。优先级基于其他控制器查看消息缓冲区的顺序。类似地,两个”From” buffer使该缓存可以通过两种不同的方式接收消息,要么作为来自目录的转发请求,要么作为对该控制器发出的请求的响应。响应的优先级高于转发的请求。
如前所述,状态机文件在经过SLICC编译后会成为Simobject文件,以上代码编译后产生的文件为L1Cache_Controller.py
,内容如下:
1 | from m5.params import * |
对这个文件,不能做任何修改!
2.3 状态声明
通过state_declaration
声明状态机的所有稳定和瞬态。对瞬态的命名遵循Sorin等人的命名约定。例如,瞬态“IM_AD”对应于在等待确认(A)和数据(D)时从无效(I)移动到已修改(M)。
1 | state_declaration(State, desc="Cache states") { |
每个状态都有一个相关的访问权限AccessPermission
,包括无效(Invalid)、不存在(NotPresent)、忙(Busy)、只读(Read_Only)、读写(Read_Write),访问权限用于对缓存进行功能访问functional accesses。对于功能访问,将检查所有缓存,看它们是否有具有匹配地址的相应块。
2.4 事件声明
声明由该缓存控制器的传入消息触发的所有事件
1 | enumeration(Event, desc="Cache events") { |
状态和事件可以参考表8.3的缓存控制器转换表
2.5 用户定义结构体
下面定义的是在这个控制器的其他地方用到的结构体
Entry
这是存储在CacheMemory中的结构。它只需要包含数据和状态
1 | structure(Entry, desc="Cache entry", interface="AbstractCacheEntry") { |
TBE
TBE是“事务缓冲区条目”。这存储了瞬态期间所需的信息。这就像MSHR。它在该协议中起MSHR的作用,但该条目也被分配用于其他用途。在该协议中,它将存储状态(通常需要)、数据(通常也需要)以及该块当前正在等待的ack数。
1 | structure(TBE, desc="Entry for transient requests") { |
TBETable
还需要一个存放所有TBE的地方。其中external="yes"
表明这是一个外部定义的类;它是在SLICC之外的C++中定义的。因此,我们需要声明我们将要使用它,并声明我们将对其调用的任何函数。您可以在src/mem/ruby/structures/TBETable.hh中找到TBETable的代码。
1 | structure(TBETable, external="yes") { |
外部函数声明
如果我们要在文件的其余部分中使用AbstractController中的任何函数,都需要声明它们。
1 | Tick clockEdge(); |
样板代码
下面这一组样板工具代码很少在不同的协议之间发生变化。在AbstractController中,我们必须实现一组纯虚拟的函数。
- getState
Given a TBE, cache entry, and address return the state of the block. This is called on the block to decide which transition to execute when an event is triggered. Usually, you return the state in the TBE or cache entry, whichever is valid.
给定IBE,entry和地址,返回地址所在缓存块的状态
- setState
Given a TBE, cache entry, and address make sure the state is set correctly on the block. This is called at the end of the transition to set the final state on the block.
- getAccessPermission
Get the access permission of a block. This is used during functional access to decide whether or not to functionally access the block. It is similar to getState
, get the information from the TBE if valid, cache entry, if valid, or the block is not present.
- setAccessPermission
Like getAccessPermission
, but sets the permission.
- functionalRead
Functionally read the data. It is possible the TBE has more up-to-date information, so check that first. Note: testAndRead/Write defined in src/mem/ruby/slicc_interface/Util.hh
- functionalWrite
Functionally write the data. Similarly, you may need to update the data in both the TBE and the cache entry.
1 | State getState(TBE tbe, Entry cache_entry, Addr addr) { |
以下是对这些状态和转换的高层次描述:
2.6 输入输出端口
参考链接🔗