authors: Jason Lowe-Power

内存系统

M5 的新内存系统(在第一个 2.0 beta 版本中引入)的设计目标如下:

  1. 在时序模式下统一时序和功能访问。对于旧的内存系统,时序访问没有数据,只是计算操作所需的时间。然后,单独的功能访问实际上使操作对系统可见。这种方法令人困惑,它允许模拟组件意外作弊,并阻止内存系统返回依赖于时序的值,这对于 execute-in-execute CPU 模型是不合理的。
  2. 简化内存系统代码——删除大量的模板和重复代码。
  3. 使更改更容易,特别是允许除共享总线之外的其他内存互连。

有关新一致性协议的详细信息,该协议是在 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.port2B.port2 = A.port1 含义相同。主端口和从端口的概念也存在于 Python 对象中,并且在端口连接在一起时会进行检查。

诸如总线之类的具有潜在无限数量端口的对象使用“向量端口”。对向量端口的赋值将对等方附加到连接列表,而不是覆盖以前的连接。

在 C++ 中,内存端口在所有对象实例化后由 python 代码连接在一起。

请求 (Request)

请求对象封装了 CPU 或 I/O 设备发出的原始请求。此请求的参数在整个事务中是持久的,因此请求对象的字段旨在对于给定的请求最多写入一次。有少数构造函数和更新方法允许在不同时间写入(或根本不写入)对象字段的子集。通过验证正在读取的字段中的数据是否有效的访问器方法提供对所有请求字段的读取访问。

请求对象中的字段通常对真实系统中的设备不可用,因此它们通常只应用于统计或调试,而不作为架构值。

请求对象字段包括:

数据包 (Packet)

Packet 用于封装内存系统中两个对象之间的传输(例如,L1 和 L2 缓存)。这与 Request 形成对比,单个 Request 从请求者一直传输到最终目的地并返回,沿途可能由几个不同的 Packet 传送。

许多数据包字段的读取访问是通过访问器方法提供的,这些方法验证正在读取的字段中的数据是否有效。

数据包包含以下所有内容,这些内容都通过访问器访问以确保数据有效:

访问类型

端口支持三种类型的访问。

  1. Timing - Timing 访问是最详细的访问。它们反映了我们对现实时序的最佳努力,包括排队延迟和资源争用的建模。一旦成功发送时序请求,在将来的某个时间点,发送请求的设备将获得响应,或者如果请求无法完成,将获得 NACK(下文详述)。Timing 和 Atomic 访问不能在内存系统中共存。
  2. Atomic - Atomic 访问是比详细访问更快的访问。它们用于快进和预热缓存,并在没有任何资源争用或排队延迟的情况下返回完成请求的大致时间。发送原子访问时,函数返回时提供响应。Atomic 和 timing 访问不能在内存系统中共存。
  3. Functional - 与原子访问一样,功能访问瞬间发生,但与原子访问不同,它们可以在内存系统中与原子或时序访问共存。功能访问用于加载二进制文件、检查/更改模拟系统中的变量以及允许将远程调试器附加到模拟器等事情。重要的一点是,当设备接收到功能访问时,如果它包含数据包队列,则必须搜索所有数据包以查找功能访问正在影响的请求或响应,并且必须适当地更新它们。Packet::intersect()fixPacket() 方法可以帮助解决这个问题。

数据包分配协议

Packet 对象的分配和释放协议根据访问类型而异。(我们在这里讨论的是低级 C++ new/delete 问题,而不是与一致性协议相关的任何内容。)

时序流控制

时序请求模拟真实的内存系统,因此与功能和原子访问不同,它们的响应不是瞬时的。由于时序请求不是瞬时的,因此需要流控制。当通过 sendTiming() 发送时序数据包时,数据包可能会或可能不会被接受,这是通过返回 true 或 false 来发信号的。如果返回 false,则对象不应尝试发送更多数据包,直到收到 recvRetry() 调用。此时,它应该再次尝试调用 sendTiming();但是,数据包可能会再次被拒绝。注意:原始数据包不需要重新发送,可以发送更高优先级的数据包。一旦 sendTiming() 返回 true,数据包可能仍然无法到达其目的地。对于需要响应的数据包(即 pkt->needsResponse() 为 true),任何内存对象都可以通过将其结果更改为 Nacked 并将其发送回源来拒绝确认该数据包。但是,如果是响应数据包,则不能这样做。true/false 返回旨在用于本地流控制,而 nacking 用于全局流控制。在这两种情况下,响应都不能被 nack。

响应和 Snoop 范围

内存系统中的范围是通过让对地址范围敏感的设备在其从端口对象中提供 getAddrRanges 的实现来处理的。此方法返回它响应的 AddrRangeList。当这些范围发生变化(例如,由于 PCI 配置发生)时,设备应在其从端口上调用 sendRangeChange(),以便新范围传播到整个层次结构。这正是 init() 期间发生的事情;所有内存对象都调用 sendRangeChange(),并且会发生一系列范围更新,直到每个人的范围都传播到系统中的所有总线。