目录
官网教程:gem5: Memory system
M5的新内存系统的设计目标如下:
所有连接到内存系统的对象都继承自MemObject类。这个类添加了纯虚函数getMasterPort(const std::string &name, PortID idx)和getSlavePort(const std::string &name, PortID idx),它们返回与给定名称和索引相对应的端口。这个接口用于在结构上将MemObjects连接在一起。
内存系统的下一个重要部分是端口(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博客
在Python中,端口(Ports)是仿真对象的一级属性,就像参数(Params)一样。两个对象可以使用赋值运算符指定它们的端口应该连接在一起。与普通变量或参数赋值不同,端口连接是对称的:A.port1 = B.port2 与 B.port2 = A.port1 具有相同的意义。主端口和从端口的概念也存在于Python对象中,在连接端口时会进行检查。
具有潜在无限数量端口的总线等对象使用“向量端口”(vector ports)。对向量端口的赋值会将对等端口追加到连接列表中,而不是覆盖先前的连接。
在C++中,内存端口通过Python代码(就是最后config目录中的运行文件)在实例化所有对象之后连接在一起。
gem5学习(8)中有对向量端口的说。
gem5学习(8):创建一个简单的缓存对象--Creating a simple cache object-CSDN博客
请求对象(Request object)封装了由CPU或I/O设备发出的原始请求。该请求的参数在整个事务过程中是持久的,请求对象的字段通常只被写入一次,用于特定的请求。有一些构造函数和更新方法允许在不同的时间(或根本不写入)写入对象的字段进行部分更新。通过访问器方法,可以读取请求对象的所有字段,并且这些方法会验证正在读取的字段中的数据是否有效【简而言之,请求对象是用来封装请求信息并提供对请求字段进行读取和更新操作的工具】。
在实际的系统中,设备通常无法直接访问请求对象中的字段。因此,这些字段通常只用于统计信息或调试目的,而不作为设备进行处理的重要数值。它们主要用于记录和分析系统的运行情况,或者在进行故障排查和调试时提供额外的信息。对于设备的正常操作和功能而言,这些字段通常没有直接的影响或用途。
请求对象的字段包括:
Packet是用来封装内存系统中两个对象之间的传输的工具。Packet用于封装内存系统中两个对象之间的传输(例如L1和L2缓存)。与请求(Request)的区别在于,一个请求从发送方一直传输到最终的目的地,可能会经过多个不同的Packet来完成传输。
访问器方法是用于读取Packet中许多字段的工具,通过这些方法,可以获取字段中存储的数据。而且,访问器方法还会验证正在读取的字段中的数据是否有效。
Packet包含以下内容,所有这些内容都通过访问器进行访问,以确保数据的有效性:
dataStatic()
、dataDynamic()
和dataDynamicArray()
这些方法,我们可以将数据放入Packet中,并指定数据在Packet销毁时是否需要手动释放。如果没有使用这些方法,那么当Packet被销毁时,数据会自动被释放。】get()
方法用于将数据从宿主端序(大端序或小端序)转换为虚拟机端序(通常是特定硬件或软件环境定义的端序),而set()
方法用于将数据从虚拟机端序转换为宿主端序。有三种类型的端口访问方式。
根据访问类型的不同,Packet对象的分配和释放协议也有所不同(这里讨论的是低级C++的new/delete问题,与一致性协议无关)。
定时请求是为了模拟真实的内存系统,与功能性访问和原子访问不同,定时请求的响应不是立即返回的。因为定时请求不是瞬时的,所以需要进行流量控制来确保系统的稳定性和可靠性。
当使用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"用于全局流量控制。
在内存系统中,通过对敏感于地址范围的设备实现其从端口对象中的getAddrRanges方法来定义地址范围。getAddrRanges方法返回一个AddrRangeList,表示设备所响应的地址范围。设备可以根据自身的需求定义多个地址范围。
当地址范围发生变化时,例如进行PCI配置或其他操作,设备应该在其从端口上调用sendRangeChange()方法。这样可以将新的地址范围传播到整个层次结构中。通常在系统初始化(init())期间发生这种情况。在初始化期间,所有的内存对象都会调用sendRangeChange()方法,一系列的范围更新将会发生,直到每个设备的范围都传播到系统中的所有总线。
这种机制确保了内存系统中的各个设备在处理地址范围时保持同步,并能够根据系统配置的变化及时更新其响应的地址范围。通过使用getAddrRanges和sendRangeChange方法,设备可以灵活地管理其所响应的地址范围,并与其他设备进行通信和协调。