gem5学习(15):Memory system

目录

一、MemObjects

二、Ports

三、Connections

四、Request

五、Packet

六、Access Types

七、Packet allocation protocol

八、Timing Flow control

九、Response and Snoop ranges


官网教程:gem5: Memory system

M5的新内存系统的设计目标如下:

  1. 在旧的内存系统中,有两种类型的访问:计时访问和功能访问(Unify timing and functional accesses)。计时访问仅用于计算操作所需的时间,不包含实际的数据,并且对系统来说是不可见的。而功能访问则是用来执行实际的操作,并使其对系统可见。因为模拟组件可以意外地通过计时访问来绕过实际的操作,这不符合执行-执行的CPU模型的要求。此外,这种设计也阻止了内存系统返回与计时相关的值,这同样不合理。因此,在新的内存系统中,我们统一了计时访问和功能访问,在计时模式下两者是一致的,这样能更加清晰和合理地模拟CPU的执行过程。
  2. 简化内存系统代码,删除大量的模板和重复代码。
  3. 新的内存系统旨在使对系统的更改更加容易,特别是在处理内存互连时。它不再局限于使用共享总线这一种内存互连方式,而是提供了更灵活的选择。这意味着我们可以使用除了共享总线之外的其他内存互连方式,以适应不同的需求和配置。这样的改进使得在系统中进行各种内存互连的调整和修改更加简单和方便。

一、MemObjects

所有连接到内存系统的对象都继承自MemObject类。这个类添加了纯虚函数getMasterPort(const std::string &name, PortID idx)和getSlavePort(const std::string &name, PortID idx),它们返回与给定名称和索引相对应的端口。这个接口用于在结构上将MemObjects连接在一起。

二、Ports

内存系统的下一个重要部分是端口(ports)。端口用于连接内存对象之间的接口。它们总是成对出现,有一个MasterPort和一个SlavePort,我们将另一个端口对象称为对等端口(peer)。这样设计的目的是使系统更具模块化。使用端口,不需要为每种类型的对象创建特定的接口。每个内存对象至少需要一个端口才能发挥作用。主模块(如CPU)有一个或多个MasterPort实例。从模块(如内存控制器)有一个或多个SlavePort。互连组件(如缓存、桥接器或总线)则同时具有MasterPort和SlavePort实例。

端口对象分为两组函数。send*函数由拥有该端口的对象调用。例如,在内存系统中发送数据包时,CPU会调用myPort->sendTimingReq(pkt)。每个send函数都有相应的recv函数,在对等端口上调用。因此,上述sendTimingReq()调用的实现只需在从端口上调用peer->recvTimingReq(pkt)。使用这种方法,我们只有一个虚函数调用开销,但可以保持通用的端口,可以连接任何内存系统对象。

Master端口可以发送请求和接收响应,而Slave端口接收请求并发送响应。由于一致性协议的原因,Slave端口还可以发送嗅探请求并接收嗅探响应,而Master端口则具有相同的接口。

注意:在gem5学习(7)中有对主从端口的详细介绍。

gem5学习(7):内存系统中创建 SimObjects--Creating SimObjects in the memory system-CSDN博客

三、Connections

在Python中,端口(Ports)是仿真对象的一级属性,就像参数(Params)一样。两个对象可以使用赋值运算符指定它们的端口应该连接在一起。与普通变量或参数赋值不同,端口连接是对称的:A.port1 = B.port2B.port2 = A.port1 具有相同的意义。主端口和从端口的概念也存在于Python对象中,在连接端口时会进行检查。

具有潜在无限数量端口的总线等对象使用“向量端口”(vector ports)。对向量端口的赋值会将对等端口追加到连接列表中,而不是覆盖先前的连接。

在C++中,内存端口通过Python代码(就是最后config目录中的运行文件)在实例化所有对象之后连接在一起。

gem5学习(8)中有对向量端口的说。

gem5学习(8):创建一个简单的缓存对象--Creating a simple cache object-CSDN博客

四、Request

请求对象(Request object)封装了由CPU或I/O设备发出的原始请求。该请求的参数在整个事务过程中是持久的,请求对象的字段通常只被写入一次,用于特定的请求。有一些构造函数和更新方法允许在不同的时间(或根本不写入)写入对象的字段进行部分更新。通过访问器方法,可以读取请求对象的所有字段,并且这些方法会验证正在读取的字段中的数据是否有效【简而言之,请求对象是用来封装请求信息并提供对请求字段进行读取和更新操作的工具】。

在实际的系统中,设备通常无法直接访问请求对象中的字段。因此,这些字段通常只用于统计信息或调试目的,而不作为设备进行处理的重要数值。它们主要用于记录和分析系统的运行情况,或者在进行故障排查和调试时提供额外的信息。对于设备的正常操作和功能而言,这些字段通常没有直接的影响或用途。

请求对象的字段包括:

  • 虚拟地址(Virtual address)。如果请求是直接在物理地址上发出的(例如由DMA I/O设备发出),则该字段可能无效。
  • 物理地址(Physical address)。
  • 数据大小。
  • 请求创建的时间。
  • 引起该请求的CPU/线程的ID。如果请求不是由CPU发出的(例如设备访问或缓存写回),则可能无效。
  • 引起该请求的PC(程序计数器)。如果请求不是由CPU发出的,也可能无效。

五、Packet

Packet是用来封装内存系统中两个对象之间的传输的工具。Packet用于封装内存系统中两个对象之间的传输(例如L1和L2缓存)。与请求(Request)的区别在于,一个请求从发送方一直传输到最终的目的地,可能会经过多个不同的Packet来完成传输。

访问器方法是用于读取Packet中许多字段的工具,通过这些方法,可以获取字段中存储的数据。而且,访问器方法还会验证正在读取的字段中的数据是否有效。

  • 通俗地说,想象有一个包裹(Packet),里面装着一些物品(字段)。为了查看这些物品,需要使用访问器方法(一种工具)。可以使用这些访问器方法来打开包裹,检查里面的物品,并获取它们的信息。
  • 但是,这些访问器方法不仅仅是获取数据的工具,它们还会进行额外的验证。比如,当你使用访问器方法读取字段中的数据时,它们会检查这些数据是否有效和合法。这样可以确保你获取到的数据是准确的,并且可以信任和使用。

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

  • 地址(Address)。地址字段用于确定Packet的目标位置,并在该位置进行处理。它通常是从请求对象的物理地址派生而来,但在某些情况下可能从虚拟地址派生(例如,在访问一个完全虚拟缓存时,在执行地址转换之前,地址可能是虚拟地址)。它可能与原始请求地址不完全相同:例如,当缓存未命中时,Packet的地址可能是要获取的数据块的地址,而不是请求的地址。
  • 大小(Size)。同样,这个大小可能与原始请求的大小不同,比如在缓存未命中的情况下。
  • 指向被操作数据的指针。
    • 通过dataStatic()、dataDynamic()和dataDynamicArray()进行设置,这些方法控制与Packet关联的数据在Packet销毁时是否被释放,使用delete、delete[]等方式。
    • 如果没有通过上述方法设置,数据在Packet销毁时会被释放。(始终可以安全调用)【通俗解释就是,想象一下Packet就像一个容器,里面可以装载一些数据。通过使用dataStatic()dataDynamic()dataDynamicArray()这些方法,我们可以将数据放入Packet中,并指定数据在Packet销毁时是否需要手动释放。如果没有使用这些方法,那么当Packet被销毁时,数据会自动被释放。】
    • 可以通过调用getPtr()获取指针。
    • 可以使用get()和set()来操作Packet中的数据。get()方法用于将数据从宿主端序(大端序或小端序)转换为虚拟机端序(通常是特定硬件或软件环境定义的端序),而set()方法用于将数据从虚拟机端序转换为宿主端序。
  • 表示成功(Success)、地址错误(BadAddress)、未确认(Not Acknowledged)和未知(Unknown)的状态。
  • 与Packet相关的一系列命令属性列表。
    • 注意:状态字段和命令属性中的数据存在一些重叠。这主要是为了在被拒绝时可以轻松重新初始化Packet,或者在原子或功能性访问中可以轻松重用Packet。
  • SenderState指针是一个不透明结构,它是一个虚拟基类,用于保存与Packet相关的发送设备(例如MSHR,Memory Sideband Request Handler)特定的状态信息。在Packet的响应中,会返回这个状态的指针,这样发送方就可以快速查找和获取处理所需的状态信息。可以通过派生特定的子类,来扩展并携带特定于特定发送设备的状态。
  • CoherenceState指针是一个虚拟基类的不透明结构,用于保存与一致性相关的状态。可以从该类别中派生出特定的子类,以携带特定于特定一致性协议的状态。
  • 一个指向请求的指针。

六、Access Types

有三种类型的端口访问方式。

  • Timing(时序)- Timing访问是最详细的访问方式。反映了对实际时序的最佳模拟,包括排队延迟和资源争用的建模。一旦在未来的某个时刻成功发送了一个Timing请求,发送请求的设备要么会收到响应,要么会收到无法完成请求的NACK(后面会详细解释)。Timing和Atomic访问不能同时存在于内存系统中。
  • Atomic(原子)- Atomic访问是比详细访问更快的访问方式。它们用于快速转发和缓存预热,并返回一个近似的完成请求所需的时间,而不考虑任何资源争用或排队延迟。当发送一个Atomic访问时,响应会在函数返回时提供。Atomic和Timing访问不能同时存在于内存系统中。
  • Functional(功能)- 与Atomic访问一样,Functional访问是瞬间发生的,但与Atomic访问不同的是,它们可以与Atomic或Timing访问同时存在于内存系统中。Functional访问用于诸如加载二进制文件、检查/更改模拟系统中的变量以及允许连接到模拟器的远程调试器等任务。重要的一点是,当一个设备接收到Functional访问时,如果它包含一个包队列,那么必须搜索所有的包,以查找Functional访问所影响的请求或响应,并进行相应的更新。Packet::intersect()和fixPacket()方法可以实现这一点。

七、Packet allocation protocol

根据访问类型的不同,Packet对象的分配和释放协议也有所不同(这里讨论的是低级C++的new/delete问题,与一致性协议无关)。

  • Atomic和Functional访问:Packet对象由请求者拥有。响应者必须用响应覆盖请求包(通常使用Packet::makeResponse()方法)。一个请求只能有一个响应者。由于响应总是在sendAtomic()或sendFunctional()返回之前生成,请求者可以静态地或者在堆栈上分配Packet对象。
  • Timing访问:是由一个请求和一个响应组成的定时事务。在这两种情况下,Packet对象必须由发送方进行动态分配内存。释放这些对象的责任则落在接收方(通常是目标设备,如内存)身上。当接收方生成响应时,它可以选择重用请求包作为响应,以避免频繁地调用delete和new,从而减少内存开销(并方便使用makeResponse()方法)。然而,这种优化是可选的,请求方不能依赖于接收到相同的Packet对象作为响应返回。需要注意的是,当响应者不是目标设备(例如高速缓存到高速缓存的传输)时,目标设备仍然会释放请求包,因此响应方必须为其响应分配一个新的Packet对象。这是因为目标设备可能在传递请求包后立即删除它,因此无法保证传递的包指针保持有效。此外,由于在传递包后目标设备可能立即删除包,因此任何希望在传递包后引用广播包的其他内存设备都必须复制该包。这是因为无法保证传递的包指针在传递后仍然有效。

八、Timing Flow control

定时请求是为了模拟真实的内存系统,与功能性访问和原子访问不同,定时请求的响应不是立即返回的。因为定时请求不是瞬时的,所以需要进行流量控制来确保系统的稳定性和可靠性。

当使用sendTiming()发送定时请求的Packet时,这个Packet可能会被接受或者被拒绝,这通过sendTiming()方法的返回值来表示。如果返回值是false,表示该Packet在接收到recvRetry()调用之前,对象不应该再尝试发送任何Packet。在这种情况下,对象应该等待并再次调用sendTiming()方法,但是仍然有可能再次被拒绝。需要注意的是,并不需要重新发送原始的Packet,而是可以发送一个优先级更高的Packet来尝试。

即使sendTiming()方法返回true,表示Packet已经被接受,但这并不意味着该Packet一定能够成功到达目的地。对于需要得到响应的Packet(即pkt->needsResponse()为true),任何内存对象都有权利拒绝确认该Packet,将其结果更改为"Nacked"并发送回源头。然而,如果这是一个响应Packet,就无法进行拒绝确认。因此,返回的true/false值用于进行局部的流量控制,而"Nacked"用于进行全局的流量控制。无论哪种情况,响应Packet都不能被拒绝确认。

总结来说,定时请求模拟了真实的内存系统,定时请求的响应不是立即返回的,需要进行流量控制来确保系统的稳定性。通过sendTiming()方法发送定时请求的Packet时,返回值表示Packet是否被接受。如果返回false,需要等待recvRetry()调用后再次尝试发送。即使返回true,Packet也可能无法成功到达目的地。对于需要响应的Packet,内存对象可以拒绝确认并将结果设置为"Nacked",但对于响应Packet则无法进行拒绝确认。返回的true/false用于局部流量控制,而"Nacked"用于全局流量控制。

九、Response and Snoop ranges

在内存系统中,通过对敏感于地址范围的设备实现其从端口对象中的getAddrRanges方法来定义地址范围。getAddrRanges方法返回一个AddrRangeList,表示设备所响应的地址范围。设备可以根据自身的需求定义多个地址范围。

当地址范围发生变化时,例如进行PCI配置或其他操作,设备应该在其从端口上调用sendRangeChange()方法。这样可以将新的地址范围传播到整个层次结构中。通常在系统初始化(init())期间发生这种情况。在初始化期间,所有的内存对象都会调用sendRangeChange()方法,一系列的范围更新将会发生,直到每个设备的范围都传播到系统中的所有总线。

这种机制确保了内存系统中的各个设备在处理地址范围时保持同步,并能够根据系统配置的变化及时更新其响应的地址范围。通过使用getAddrRanges和sendRangeChange方法,设备可以灵活地管理其所响应的地址范围,并与其他设备进行通信和协调。

你可能感兴趣的:(gem5学习,学习)