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
2
3
4
5
Import('*')
# 注册协议名‘MSI’,Scons将假定一个名为MSI.slicc的文件
main.Append(ALL_PROTOCOLS=['MSI'])
# 告诉SCons在当前目录中查找要传递给SLCC编译器的文件。
main.Append(PROTOCOL_DIRS=[Dir('.')])

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
2
3
4
5
machine(MachineType:L1Cache, "MSI cache")
: <parameters>
{
<All state machine code>
}

MachineType:L1Cache将状态机命名为L1Cache,SLICC 将使用该名称为我们生成许多不同的对象。例如,一旦编译了这个文件,就会有一个新的 SimObject: L1Cache_Controller 作为缓存控制器。这个声明中还包括对这个状态机的描述: “ MSI cache”。

2.2 状态机参数声明

状态机参数的声明在冒号(:)之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
machine(MachineType:L1Cache, "MSI cache")
: Sequencer *sequencer;
CacheMemory *cacheMemory;
bool send_evictions;
# “To” buffer
MessageBuffer * requestToDir, network="To", virtual_network="0", vnet_type="request";
MessageBuffer * responseToDirOrSibling, network="To", virtual_network="2", vnet_type="response";
# “From” buffer
MessageBuffer * forwardFromDir, network="From", virtual_network="1", vnet_type="forward";
MessageBuffer * responseFromDirOrSibling, network="From", virtual_network="2", vnet_type="response";
# “To” buffer
MessageBuffer * mandatoryQueue;
{

}
  • 排序器(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from m5.params import *
from m5.SimObject import SimObject
from Controller import RubyController

class L1Cache_Controller(RubyController):
type = 'L1Cache_Controller'
cxx_header = 'mem/protocol/L1Cache_Controller.hh'
sequencer = Param.RubySequencer("")
cacheMemory = Param.RubyCache("")
send_evictions = Param.Bool("")
requestToDir = Param.MessageBuffer("")
responseToDirOrSibling = Param.MessageBuffer("")
forwardFromDir = Param.MessageBuffer("")
responseFromDirOrSibling = Param.MessageBuffer("")
mandatoryQueue = Param.MessageBuffer("")

对这个文件,不能做任何修改!

2.3 状态声明

通过state_declaration声明状态机的所有稳定和瞬态。对瞬态的命名遵循Sorin等人的命名约定。例如,瞬态“IM_AD”对应于在等待确认(A)和数据(D)时从无效(I)移动到已修改(M)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
state_declaration(State, desc="Cache states") {
I, AccessPermission:Invalid,
desc="Not present/Invalid";

// States moving out of I
IS_D, AccessPermission:Invalid,
desc="Invalid, moving to S, waiting for data";
IM_AD, AccessPermission:Invalid,
desc="Invalid, moving to M, waiting for acks and data";
IM_A, AccessPermission:Busy,
desc="Invalid, moving to M, waiting for acks";

S, AccessPermission:Read_Only,
desc="Shared. Read-only, other caches may have the block";

// States moving out of S
SM_AD, AccessPermission:Read_Only,
desc="Shared, moving to M, waiting for acks and 'data'";
SM_A, AccessPermission:Read_Only,
desc="Shared, moving to M, waiting for acks";

M, AccessPermission:Read_Write,
desc="Modified. Read & write permissions. Owner of block";

// States moving to Invalid
MI_A, AccessPermission:Busy,
desc="Was modified, moving to I, waiting for put ack";
SI_A, AccessPermission:Busy,
desc="Was shared, moving to I, waiting for put ack";
II_A, AccessPermission:Invalid,
desc="Sent valid data before receiving put ack. "Waiting for put ack.";
}

每个状态都有一个相关的访问权限AccessPermission,包括无效(Invalid)、不存在(NotPresent)、忙(Busy)、只读(Read_Only)、读写(Read_Write),访问权限用于对缓存进行功能访问functional accesses。对于功能访问,将检查所有缓存,看它们是否有具有匹配地址的相应块。

2.4 事件声明

声明由该缓存控制器的传入消息触发的所有事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
enumeration(Event, desc="Cache events") {
// From the processor/sequencer/mandatory queue
Load, desc="Load from processor";
Store, desc="Store from processor";

// Internal event (only triggered from processor requests)
Replacement, desc="Triggered when block is chosen as victim";

// Forwarded request from other cache via dir on the forward network
FwdGetS, desc="Directory sent us a request to satisfy GetS. We must have the block in M to respond to this.";
FwdGetM, desc="Directory sent us a request to satisfy GetM. We must have the block in M to respond to this.";
Inv, desc="Invalidate from the directory.";
PutAck, desc="Response from directory after we issue a put. This must be on the fwd network to avoid deadlock.";

// Responses from directory
DataDirNoAcks, desc="Data from directory (acks = 0)";
DataDirAcks, desc="Data from directory (acks > 0)";

// Responses from other caches
DataOwner, desc="Data from owner";
InvAck, desc="Invalidation ack from other cache after Inv";

// Special event to simplify implementation
LastInvAck, desc="Triggered after the last ack is received";
}

状态和事件可以参考表8.3的缓存控制器转换表

image-20231204190803198

2.5 用户定义结构体

下面定义的是在这个控制器的其他地方用到的结构体

Entry

这是存储在CacheMemory中的结构。它只需要包含数据和状态

1
2
3
4
structure(Entry, desc="Cache entry", interface="AbstractCacheEntry") {
State CacheState, desc="cache state";
DataBlock DataBlk, desc="Data in the block";
}

TBE

TBE是“事务缓冲区条目”。这存储了瞬态期间所需的信息。这就像MSHR。它在该协议中起MSHR的作用,但该条目也被分配用于其他用途。在该协议中,它将存储状态(通常需要)、数据(通常也需要)以及该块当前正在等待的ack数。

1
2
3
4
5
structure(TBE, desc="Entry for transient requests") {
State TBEState, desc="State of block";
DataBlock DataBlk, desc="Data for the block. Needed for MI_A";
int AcksOutstanding, default=0, desc="Number of acks left to receive.";
}

TBETable

还需要一个存放所有TBE的地方。其中external="yes"表明这是一个外部定义的类;它是在SLICC之外的C++中定义的。因此,我们需要声明我们将要使用它,并声明我们将对其调用的任何函数。您可以在src/mem/ruby/structures/TBETable.hh中找到TBETable的代码。

1
2
3
4
5
6
structure(TBETable, external="yes") {
TBE lookup(Addr);
void allocate(Addr);
void deallocate(Addr);
bool isPresent(Addr);
}

外部函数声明

如果我们要在文件的其余部分中使用AbstractController中的任何函数,都需要声明它们。

1
2
3
4
5
Tick clockEdge();
void set_cache_entry(AbstractCacheEntry a);
void unset_cache_entry();
void set_tbe(TBE b);
void unset_tbe();

样板代码

下面这一组样板工具代码很少在不同的协议之间发生变化。在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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
State getState(TBE tbe, Entry cache_entry, Addr addr) {
// The TBE state will override the state in cache memory, if valid
if (is_valid(tbe)) { return tbe.TBEState; }
// Next, if the cache entry is valid, it holds the state
else if (is_valid(cache_entry)) { return cache_entry.CacheState; }
// If the block isn't present, then it's state must be I.
else { return State:I; }
}

void setState(TBE tbe, Entry cache_entry, Addr addr, State state) {
if (is_valid(tbe)) { tbe.TBEState := state; }
if (is_valid(cache_entry)) { cache_entry.CacheState := state; }
}

AccessPermission getAccessPermission(Addr addr) {
TBE tbe := TBEs[addr];
if(is_valid(tbe)) {
return L1Cache_State_to_permission(tbe.TBEState);
}
Entry cache_entry := getCacheEntry(addr);
if(is_valid(cache_entry)) {
return L1Cache_State_to_permission(cache_entry.CacheState);
}
return AccessPermission:NotPresent;
}

void setAccessPermission(Entry cache_entry, Addr addr, State state) {
if (is_valid(cache_entry)) {
cache_entry.changePermission(L1Cache_State_to_permission(state));
}
}

void functionalRead(Addr addr, Packet *pkt) {
TBE tbe := TBEs[addr];
if(is_valid(tbe)) {
testAndRead(addr, tbe.DataBlk, pkt);
} else {
testAndRead(addr, getCacheEntry(addr).DataBlk, pkt);
}
}

int functionalWrite(Addr addr, Packet *pkt) {
int num_functional_writes := 0;
TBE tbe := TBEs[addr];
if(is_valid(tbe)) {
num_functional_writes := num_functional_writes +
testAndWrite(addr, tbe.DataBlk, pkt);
return num_functional_writes;
}

以下是对这些状态和转换的高层次描述:

img

2.6 输入输出端口

参考链接🔗

gem5模拟器入门(四) - 知乎 (zhihu.com)

EM5官方教程全流程: part 3 RUBY-CSDN博客