深入理解 Gem5 之四
Gem5 中的内存系统
本文部分内容参考官方文档中关于内存系统的相关说明,内存系统中创建 SimObjects
MemObjects
之前的 gem5 版本中,所有连接到内存系统的对象都派生于 MemObject 类。然而,在最新版本(v21.0.1.0)中,该类被删去了。
那么现在用什么类呢?猜测是 SimObject
Ports
在深入研究内存系统之前,我们应该首先理解 gem5 中的端口类 Port。因为所有在内存系统内的对象都要通过端口来建立连接,因而它们总是成对出现,这使得 gem5 的设计更加模块化。
memory system modes
Port 类实现了三种不同的内存系统模式:时序(timing)、原子(Atomic)和功能(functional),最重要的模式是时序模式。时序模式是产生正确仿真结果的唯一模式。其他模式仅在特殊情况下使用:
- Atomic mode 原子模式常用于快进到感兴趣的模拟区域,以及预热缓存,这种模式假设在内存系统中不会产生任何事件。相反,所有的内存请求都通过一个长调用链执行。除非它将在快进或模拟器预热期间使用,否则不需要实现对内存对象的原子访问。
- Functional mode 功能模式更适合描述为调试模式。功能模式用于从 host 读取数据到模拟器内存等操作。它在 Syscall Emulation(SE) 模式中被大量使用。例如,函数模式使用
process.cmd
从 host 中加载二进制文件,这样模拟系统就可以访问它。不论数据在何处,函数的读操作总能返回最新的数据,而其写操作中需要更新所有可能的有效数据(比如多个有效的缓存块中)。
Port
Port 类(端口)是 SimObject 之间的交互接口。在 gem5 中,Port 类是所有交互接口类(包括网络连接以及硬件模块端口连接等)的父类,其地位可见一斑。
1 |
|
portName 是端口的描述名,id 类型为 typename int16_t PortID
,用于在 vector 中区分并识别端口,当 id 为负数时,指示端口不在 vector 中。_peer 指向与该端口相连的端口,_connected 表示端口是否有一个端口与之相连。此外,Port 类中还定义了一个空类 UnboundPortException,用于在程序发现未绑定端口时 throw 出特定的错误。
成对的两个端口如何进行绑定与解绑呢?很简单,只需要改变 _peer 指针就行:
1 |
|
takeOverFrom()
函数也提供了快速交换两个端口之间连接的方法。它将原本与 old 绑定的端口绑定。
1 |
|
Master-vs-Slave ? Request-vs-Response !
Port 类分别派生出了两种不同的子类,RequestPort 类和 ResponsePort 类。其中,RequsetPort 类是请求端口,用于发送接受请求,RespondPort 则用于发送应答。在之前的版本中,RequestPort 被称为 MasterPort,而 RespondPort 被称为 SlavePort,且官方文档中仍旧使用这些名称,为方便统一,下文使用 RequestPort 和 ResponsePort。一个主模块,如 CPU,通常有一个或多个 RequestPort 实例,从 Cache 中请求想要的数据。从模块,例如内存,具有一个或多个 ResponsePort,响应请求发回对应的数据。一个互连组件,例如缓存、网桥或总线,通常同时具有 RequestPort 和 ResponsePort 实例。
1 |
|
以 RequestPort 类为例,RequestPort 类中包含了指向应答端口的指针 _responsePort 和拥有该请求端口的 SimObject。它还继承了三个不同级别的传输协议类:AtomicRequestProtocol、TimingRequestProtocol 和 FunctionalRequestProtocol。除了发送数据包的基本功能外,它还可以更改接收范围和侦听(snoop)端口的功能。
时序传输数据流程
时序模式下传输数据在 gem5 中非常常见,因此我们先来了解一下时序模式下的数据传输实现。gem5 中的数据传输,都是靠 Packet 类来完成。因此不论是 send 还是 recv 函数,都需要传递 Packet 类指针:PacketPtr。
当主模块需要下游传来数据时,会通过 RequestPort 调用 sendTimingReq(pkt)
发送请求, pkt 是 Packet 的指针,内含有请求数据、应执行的指令、状态等。然而实际上 sendTimingReq(pkt)
的实现就是调用 peer->recvTimingReq(pkt)
并返回该函数的返回值:
1 |
|
于是,PacketPtr 通过函数参数的方式传给了 RespondPort,而 RespondPort 事实上是处于从模块中,所以现在数据就移动到了下游从模块。注意,recvTimingReq()
的返回值给最终会 return 到 sendTimingReq
函数中。因此主模块可以知晓请求是否被从模块接收,true 表示该数据包已被收到。false 意味着从模块目前无法接收请求,必须在未来的某个时刻重试。
若 RespondPort 成功接收了 PacketPtr,此时主模块会继续自己的运行,从模块则会处理 Packet,双方都不会被阻塞。当从模块完成处理后,需向主模块发送响应:调用 sendTimingResp(pkt)
(此时 pkt 是与请求相同的指针,但它现在指向一个响应包)。类似的是,sendTimingResp(pkt)
内部实现还是直接调用 peer->recvTimingResp(pkt)
并返回该函数的返回值。若 master 的 recvTimingResp()
函数返回 true,表明 master 已经收到应答,如此一来,该请求的交互就完成了(见下图)。
1 |
|
之前说的都是非常顺利的情况,若出现从模块因某些原因暂无法接收。那么 recvTimingReq()
的返回值为 false,于是主模块会得知从模块正在忙碌,当从模块可以接受数据后,会调用 sendReqRetry()
函数来通知主模块再次发送请求。而主模块也只有在等到 recvReqRetry()
执行后,才能再次调用 sendTimingReq()
函数来发送请求。当然,第二次发送请求失败也是有可能的,因此上述过程可能会发生很多次。注意:主模块负责保存失败的 PacketPtr,而不是从模块,从模块不保留失败的 PacketPtr。
当然,也可能出现主模块因忙碌而无法接收应答的情况,从模块通过 sendTimingResp
的返回值可知应答未被主模块接收,那么从模块需要等待主模块调用 sendRespRetry()
函数,然后才能再次发送应答。
最后,补上官方文档中关于时序数据流控制的说明:
当
sendTiming()
函数返回 false 时,相同的 Packet 就不应当再次发送,直到recvRetry()
函数被调用时,才可以再次调用sendTiming()
函数,然而此时也不必一定要重发之前的 Packet,可以发送一个优先级更高的 Packet。Once sendTiming() returns true, the packet may still not be able to make it to its destination. For packets that require a response (i.e. pkt->needsResponse() is true), any memory object can refuse to acknowledge the packet by changing its result to Nacked and sending it back to its source. However, if it is a response packet, this can not be done. The true/false return is intended to be used for local flow control, while nacking is for global flow control. In both cases a response can not be nacked.
Packet
简介
如上文所述,Packet 类通常表示内存对象之间传输的数据。因此,单个 Request 从请求者一直传输到最终目的地,然后再返回,可能是由几个不同 Packet 在这个过程中传输的。
各种类中的 accessor 函数可以访问并使用 Packet 类中的信息,并进而验证读入的数据是否有效。比如,在 SimpleCache 的例子中,accessFunctional()
函数内使用了 Packet 类的地址、块大小、读/写操作等信息:
1 |
|
Packet 类
1 |
|
其中 Printable 类是为更方便调试、打印信息而存在的抽象类。
而 Flags 类描述了 Packet 对象内具体状态信息,包括侦听、拷贝、应答、共享、有效位等:
符号 | 描述 |
---|---|
COPY_FLAGS | Flags to transfer across when copying a packet |
RESPONDER_FLAGS | used to create reponse packets |
HAS_SHARERS | packet have sharers (which means it should not be considered writable) or not. |
EXPRESS_SNOOP | Special timing-mode atomic snoop for multi-level coherence. |
RESPONDER_HAD_WRITABLE | Allow a responding cache to inform the cache hierarchy that it had a writable copy before responding. |
CACHE_RESPONDING | Snoop co-ordination flag to indicate that a cache is responding to a snoop. |
WRITE_THROUGH | The writeback/writeclean |
SATISFIED | Response co-ordination flag for cache maintenance |
FAILS_TRANSACTION | Indicates that this packet/request has returned from the cache hierarchy in a failed transaction. |
FROM_TRANSACTION | Indicates that this packet/request originates in the CPU executing in transactional mode |
VALID_ADDR | addr valid fields |
VALID_SIZE | size valid fields |
STATIC_DATA | The data pointers to a value that shouldn’t be freed when the packet is destroyed. |
DYNAMIC_DATA | The data pointers to a value that should be freed when the packet is destroyed. |
SUPPRESS_FUNC_ERROR | suppress the error if this packet encounters a functional access failure. |
BLOCK_CACHED | Signal block present to squash prefetch and cache evict packets through express snoop flag |
Memcmd
MemCmd 类定义了与命令相关的属性和其他数据。MemCmd 类中有所有关于cache/memory 的操作和属性。关于cache的命令操作,可分为以下几大类:
- 无效
- 读取
- 预取
- 写入
- 清除
- 升级
- 同步
这些命令操作也会配上数据包的属性,且命令与数据通常有固定搭配,不完全举例如下:
命令 | 属性字符 | 应答命令 | 描述 |
---|---|---|---|
InvalidCmd | - | InvalidCmd(即不应答) | 无效命令 |
ReadReq | IsRead, IsRequest, NeedsResponse | ReadResp | 由非缓存代理(例如 CPU 或设备)发出的读取,对对齐没有限制 |
ReadResp | IsRead, IsResponse, HasData | InvalidCmd | 从 requester 到 responder 的数据流 |
ReadRespWithInvalidate | IsRead, IsResponse, HasData, IsInvalidate | InvalidCmd | 是否是要升级的数据 |
WriteReq | IsWrite, NeedsWritable, IsRequest, NeedsResponse, HasData | WriteResp | |
WriteResp | IsWrite, IsResponse | InvalidCmd | |
WriteCompleteResp | IsWrite, IsResponse | InvalidCmd | |
WritebackDirty | IsWrite, IsRequest, IsEviction, HasData, FromCache | InvalidCmd | |
WritebackClean | IsWrite, IsRequest, IsEviction, HasData, FromCache | InvalidCmd | |
WriteClean | IsWrite, IsRequest, HasData, FromCache | InvalidCmd | |
CleanEvict | IsRequest, IsEviction, FromCache | InvalidCmd | |
SoftPFReq | IsRead, IsRequest, IsSWPrefetch, NeedsResponse | SoftPFResp | |
SoftPFExReq | IsRead, NeedsWritable, IsInvalidate, IsRequest, IsSWPrefetch, NeedsResponse | SoftPFResp | |
HardPFReq | IsRead, IsRequest, IsHWPrefetch, NeedsResponse, FromCache | HardPFResp | |
SoftPFResp | IsRead, IsResponse, IsHWPrefetch, HasData | InvalidCmd | |
HardPFResp | IsRead, IsResponse, IsHWPrefetch, HasData | InvalidCmd | |
WriteLineReq | IsWrite, NeedsWritable, IsRequest, NeedsResponse, HasData | WriteResp | |
UpgradeReq | IsInvalidate, NeedsWritable, IsUpgrade, IsRequest, NeedsResponse, FromCache | UpgradeResp | |
SCUpgradeReq | IsInvalidate, NeedsWritable, IsUpgrade, IsLlsc, IsRequest, NeedsResponse, FromCache | UpgradeResp | IsUpgrade, IsResponse |
SCUpgradeFailReq | sRead, NeedsWritable, IsInvalidate, IsLlsc, IsRequest, NeedsResponse, FromCache | UpgradeFailResp | |
UpgradeFailResp | IsRead, IsResponse, HasData | InvalidCmd | |
ReadExReq | IsRead, NeedsWritable, IsInvalidate, IsRequest, NeedsResponse, FromCache | ReadExResp | |
ReadExResp | IsRead, IsResponse, HasData | InvalidCmd | |
ReadCleanReq | IsRead, IsRequest, NeedsResponse, FromCache | ReadResp | |
ReadSharedReq | IsRead, IsRequest, NeedsResponse, FromCache | ReadResp | |
LoadLockedReq | IsRead, IsLlsc, IsRequest, NeedsResponse | ||
StoreCondReq | sWrite, NeedsWritable, IsLlsc, IsRequest, NeedsResponse, HasData | ||
StoreCondFailReq | IsWrite, NeedsWritable, IsLlsc, IsRequest, NeedsResponse, HasData | ||
StoreCondResp | IsWrite, IsLlsc, IsResponse | ||
SwapReq | IsRead, IsWrite, NeedsWritable, IsRequest, HasData, NeedsResponse | ||
SwapResp | IsRead, IsWrite, IsResponse, HasData | ||
MemFenceReq | |||
MemSyncReq | |||
MemSyncResp | |||
MemFenceResp | |||
CleanSharedReq | |||
CleanSharedResp | |||
CleanInvalidReq | |||
CleanInvalidResp |
gem5 中为方便两者相连,使用了很有技巧的编程手段。定义 MemCmd::CommandInfo 结构体如下,位域中通过位图表示的多个属性,在初始化时命令和属性就会被捆绑起来,保存在静态变量数组 commandInfo[]
中。
1 |
|
使用 testCmdAttrib
函数和静态类数组 commandInfo
(包含了所有命令),就可以简洁地实现测试标志位的函数,如 isRead()
:
1 |
|
SenderState
类描述 Packet 的发送者状态。对于看到该 Packet
的 SimObject
对象而言,SenderState
类可以用于保存与 Packet 相关的状态(例如,MSHR)。
MSHR(Miss Status and handling Register) 保存并处理缓存丢失所需的所有信息,包括要请求的目标列表。
指向 SenderState
类的指针会在应答 Packet
的函数中被返回,如此一来,SimObject 对象可以迅速查看 Packet
中的状态位,并进行相应的处理(见 findNextSenderState()
函数)。 SenderState
类以链表的形式相串起来:
1 |
|
在响应该 Packet 时,会返回一个 SenderState* 类型的指针,以便 SimObject 对象可以快速查找处理它所需的状态。要遍历发送者组成的链表,返回第一个符合类型T的实例,需使用 findNextSenderState()
函数。
1 |
|
有时,为处理特殊发送设备的状态,程序员也可以从该类中派生出相对应的子类。由于多个 SimObject
对象都可以从自己的视角出发来添加新的 SenderState
,只要在响应返回时,能恢复之前的 SenderState
对象即可。因此,在修改 Packet
类中的 SenderState
字段之前,应该始终维护 SenderState
链表。
Packet 功能
通常来说,Packet
类中包含了以下内容,可被函数使用:
- 地址。 通过
getAddr()
函数获得。该地址将用于将 Packet 路由到其目的地(若未明确设置目的地)并在目标处处理 Packet 的地址。通常,它是发起请求对象的物理地址,某些情况下也可能是虚拟地址:在执行地址转换之前访问虚拟 Cache。但有时也可能是需要获取的数据地址:例如,在 Cache 未命中时,Packet 地址可能是要获取的块的地址,而非请求地址。 - 请求或包的大小。 通过
getSize()
获得。请求所占的空间大小 - 指向 Packet 中数据的指针。在不同层次结构中,数据可能是不同的,因此在设计上它位于 Packet 对象,而不是 request。
- 用
dataStatic()
dataDynamic()
函数设置的数据,在 Packet 对象被 free 时,其内的数据分别应:不被 free、不使用 delete [] 进行 free。 - 用
allocate()
函数分配空间时,数据会在 Packet 被释放时 free - 通过
getPtr()
获得指针 - 使用
get()
函数获取,set()
函数设置
- 用
- 状态 包括以下几种:Success, BadAddress, Not Acknowleged, and Unknown.
- List of command attributes 需要对 Packet 施加的命令和属性,由 MemCmd 维护。注意:状态字段和命令属性中的数据有一些重叠。这在很大程度上是为了使包在打包时可以很容易地重新初始化,或者在原子访问或函数访问时很容易重用。
- Pointer to SenderState 携带特定的发送设备的状态。在包的响应中返回一个指向该状态的指针,以便发送方可以快速查找处理它所需的状态。
- Pointer to CoherenceState 用于保存 Coherence 一致性相关的状态。
- Pointer to request 指向请求的指针
Request
Request
对象封装了 CPU 或 I/O 设备发出的原始请求。Request
的参数在整个事务中是持久的。因此对于一给定的 Request
,其字段最多只需写入一次。但也有一些构造函数和 update 方法允许在不同时间(或根本不)写入对象的某些字段。用户可通过 accessor()
函数获取 Request 字段的读取权限,同时也可验证正在读取的字段中的数据是否有效。注意,Request 中的字段通常不适用于真实系统中的设备,通常用于统计或调试,不能作为真实的系统架构。
Request
对象的字段包括了:
- Virtual Address 虚拟地址。当该请求直接表示为物理地址时该字段无效(如 DMA I/O 设备发出的请求)
- Physical Address 物理地址
- Data Size 数据大小
- Time the request was created 创建时间
- The ID of the CPU/thread that caused this request. 创建该请求的 CPU 或线程 ID
- The PC that caused this request 产生该请求的指令 PC 值。若不是由 CPU 发送的,那么该字段无效
Request 类
1 |
|