内存系统
M5 的新内存系统(在第一个 2.0 beta 版本中引入)的设计目标如下:
- 在时序模式下统一时序和功能访问。对于旧的内存系统,时序访问没有数据,只是计算操作所需的时间。然后,单独的功能访问实际上使操作对系统可见。这种方法令人困惑,它允许模拟组件意外作弊,并阻止内存系统返回依赖于时序的值,这对于 execute-in-execute CPU 模型是不合理的。
- 简化内存系统代码——删除大量的模板和重复代码。
- 使更改更容易,特别是允许除共享总线之外的其他内存互连。
有关新一致性协议的详细信息,该协议是在 2.0b4 中引入的(连同大量的缓存模型重写),请参阅 一致性协议。
MemObjects
连接到内存系统的所有对象都继承自 MemObject。
此类添加了纯虚函数 getMasterPort(const std::string &name, PortID idx) 和 getSlavePort(const std::string &name, PortID idx),它们返回对应于给定名称和索引的端口。此接口用于在结构上将 MemObjects 连接在一起。
端口
内存系统的下一个主要部分是端口的概念。端口用于将内存对象相互接口。它们总是成对出现,有一个 MasterPort 和一个 SlavePort,我们将另一个端口对象称为对等端口。这些用于使设计更加模块化。使用端口,不必在每种类型的对象之间创建特定接口。每个内存对象必须至少有一个端口才能有用。主模块(如 CPU)具有一个或多个 MasterPort 实例。从模块(如内存控制器)具有一个或多个 SlavePort。互连组件(如缓存、桥接器或总线)具有 MasterPort 和 SlavePort 实例。
端口对象中有两组函数。send* 函数由拥有该端口的对象在端口上调用。例如,要在内存系统中发送数据包,CPU 将调用 myPort->sendTimingReq(pkt)。每个发送函数都有一个相应的 recv 函数,该函数在端口对等方上调用。因此,上述 sendTimingReq() 调用的实现将仅仅是 slave 端口上的 peer->recvTimingReq(pkt)。使用这种方法,我们只有一个虚函数调用惩罚,但保留了可以将任何内存系统对象连接在一起的通用端口。
主端口可以发送请求和接收响应,而从端口接收请求并发送响应。由于一致性协议,从端口还可以发送 snoop 请求并接收 snoop 响应,主端口具有镜像接口。
连接
在 Python 中,端口是模拟对象的一流属性,很像 Params。两个对象可以使用赋值运算符指定它们的端口应该连接。与普通变量或参数赋值不同,端口连接是对称的:A.port1 = B.port2 与 B.port2 = A.port1 含义相同。主端口和从端口的概念也存在于 Python 对象中,并且在端口连接在一起时会进行检查。
诸如总线之类的具有潜在无限数量端口的对象使用“向量端口”。对向量端口的赋值将对等方附加到连接列表,而不是覆盖以前的连接。
在 C++ 中,内存端口在所有对象实例化后由 python 代码连接在一起。
请求 (Request)
请求对象封装了 CPU 或 I/O 设备发出的原始请求。此请求的参数在整个事务中是持久的,因此请求对象的字段旨在对于给定的请求最多写入一次。有少数构造函数和更新方法允许在不同时间写入(或根本不写入)对象字段的子集。通过验证正在读取的字段中的数据是否有效的访问器方法提供对所有请求字段的读取访问。
请求对象中的字段通常对真实系统中的设备不可用,因此它们通常只应用于统计或调试,而不作为架构值。
请求对象字段包括:
- 虚拟地址。如果请求是直接在物理地址上发出的(例如,由 DMA I/O 设备),则该字段可能无效。
- 物理地址。
- 数据大小。
- 请求创建的时间。
- 导致此请求的 CPU/线程的 ID。如果请求不是由 CPU 发出的(例如,设备访问或缓存写回),则可能无效。
- 导致此请求的 PC。如果请求不是由 CPU 发出的,也可能无效。
数据包 (Packet)
Packet 用于封装内存系统中两个对象之间的传输(例如,L1 和 L2 缓存)。这与 Request 形成对比,单个 Request 从请求者一直传输到最终目的地并返回,沿途可能由几个不同的 Packet 传送。
许多数据包字段的读取访问是通过访问器方法提供的,这些方法验证正在读取的字段中的数据是否有效。
数据包包含以下所有内容,这些内容都通过访问器访问以确保数据有效:
- 地址。这是将用于将数据包路由到其目标(如果未显式设置目的地)并在目标处处理数据包的地址。它通常源自请求对象的物理地址,但在某些情况下可能源自虚拟地址(例如,在执行地址转换之前访问完全虚拟缓存)。它可能与原始请求地址不同:例如,在缓存未命中时,数据包地址可能是要获取的块的地址,而不是请求地址。
- 大小。同样,这个大小可能与原始请求的大小不同,如在缓存未命中场景中。
- 指向正在操作的数据的指针。
- 由
dataStatic(),dataDynamic(), 和dataDynamicArray()设置,它们分别控制与数据包关联的数据是在数据包销毁时释放、不释放、使用delete释放,还是使用delete []释放。 - 如果未通过上述方法之一设置,则通过
allocate()分配,并且数据在数据包被销毁时释放。(总是可以安全调用)。 - 可以通过调用
getPtr()获取指针 get()和set()可用于操作数据包中的数据。get() 方法执行 guest-to-host 字节序转换,set 方法执行 host-to-guest 字节序转换。
- 由
- 指示 Success, BadAddress, Not Acknowleged, 和 Unknown 的状态。
- 与数据包关联的命令属性列表
- 注意:状态字段中的数据与命令属性有一些重叠。这主要是为了使数据包在被 nack 时可以轻松地重新初始化,或者轻松地重用于原子或功能访问。
- 一个
SenderState指针,这是一个虚拟基类不透明结构,用于保存与数据包关联但特定于发送设备(例如 MSHR)的状态。指向此状态的指针在数据包的响应中返回,以便发送者可以快速查找处理它所需的状态。将从中派生特定的子类以携带特定于特定发送设备的状态。 - 一个
CoherenceState指针,这是一个虚拟基类不透明结构,用于保存一致性相关状态。将从中派生特定的子类以携带特定于特定一致性协议的状态。 - 指向请求的指针。
访问类型
端口支持三种类型的访问。
- Timing - Timing 访问是最详细的访问。它们反映了我们对现实时序的最佳努力,包括排队延迟和资源争用的建模。一旦成功发送时序请求,在将来的某个时间点,发送请求的设备将获得响应,或者如果请求无法完成,将获得 NACK(下文详述)。Timing 和 Atomic 访问不能在内存系统中共存。
- Atomic - Atomic 访问是比详细访问更快的访问。它们用于快进和预热缓存,并在没有任何资源争用或排队延迟的情况下返回完成请求的大致时间。发送原子访问时,函数返回时提供响应。Atomic 和 timing 访问不能在内存系统中共存。
- Functional - 与原子访问一样,功能访问瞬间发生,但与原子访问不同,它们可以在内存系统中与原子或时序访问共存。功能访问用于加载二进制文件、检查/更改模拟系统中的变量以及允许将远程调试器附加到模拟器等事情。重要的一点是,当设备接收到功能访问时,如果它包含数据包队列,则必须搜索所有数据包以查找功能访问正在影响的请求或响应,并且必须适当地更新它们。
Packet::intersect()和fixPacket()方法可以帮助解决这个问题。
数据包分配协议
Packet 对象的分配和释放协议根据访问类型而异。(我们在这里讨论的是低级 C++ new/delete 问题,而不是与一致性协议相关的任何内容。)
- Atomic 和 Functional : Packet 对象归请求者所有。响应者必须用响应覆盖请求数据包(通常使用
Packet::makeResponse()方法)。没有为单个请求提供多个响应者的规定。由于响应总是在sendAtomic()或sendFunctional()返回之前生成,因此请求者可以静态地或在堆栈上分配 Packet 对象。 - Timing : Timing 事务由两个单向消息组成,请求和响应。在这两种情况下,Packet 对象必须由发送者动态分配。释放是接收者(或对于广播一致性数据包,是目标设备,通常是内存)的责任。在接收请求者生成响应的情况下,它 可以 选择重用请求数据包作为其响应,以节省调用
delete然后调用new的开销(并获得使用makeResponse()的便利)。但是,此优化是可选的,请求者不得依赖于收到相同的 Packet 对象作为请求的响应。请注意,当响应者不是目标设备(如在缓存到缓存传输中)时,目标设备仍将删除请求数据包,因此响应缓存必须为其响应分配一个新的 Packet 对象。此外,因为目标设备可能会在交付时立即删除请求数据包,所以任何希望在数据包交付点之后引用广播数据包的其他内存设备必须制作该数据包的副本,因为交付的数据包的指针不能依赖于保持有效。
时序流控制
时序请求模拟真实的内存系统,因此与功能和原子访问不同,它们的响应不是瞬时的。由于时序请求不是瞬时的,因此需要流控制。当通过 sendTiming() 发送时序数据包时,数据包可能会或可能不会被接受,这是通过返回 true 或 false 来发信号的。如果返回 false,则对象不应尝试发送更多数据包,直到收到 recvRetry() 调用。此时,它应该再次尝试调用 sendTiming();但是,数据包可能会再次被拒绝。注意:原始数据包不需要重新发送,可以发送更高优先级的数据包。一旦 sendTiming() 返回 true,数据包可能仍然无法到达其目的地。对于需要响应的数据包(即 pkt->needsResponse() 为 true),任何内存对象都可以通过将其结果更改为 Nacked 并将其发送回源来拒绝确认该数据包。但是,如果是响应数据包,则不能这样做。true/false 返回旨在用于本地流控制,而 nacking 用于全局流控制。在这两种情况下,响应都不能被 nack。
响应和 Snoop 范围
内存系统中的范围是通过让对地址范围敏感的设备在其从端口对象中提供 getAddrRanges 的实现来处理的。此方法返回它响应的 AddrRangeList。当这些范围发生变化(例如,由于 PCI 配置发生)时,设备应在其从端口上调用 sendRangeChange(),以便新范围传播到整个层次结构。这正是 init() 期间发生的事情;所有内存对象都调用 sendRangeChange(),并且会发生一系列范围更新,直到每个人的范围都传播到系统中的所有总线。
