目录
一、gem5 master and slave ports
二、Packets
三、Port interface
1、主设备发送请求时从设备忙
2、从设备发送响应时主设备忙
四、Simple memory object example
1、Declare the SimObject
2、Define the SimpleMemobj class
3、Define the SimpleMemobj class
4、Define a slave port type
5、Define a master port type
6、Defining the SimObject interface
7、Implementing basic SimObject functions
8、Implementing slave and master port functions
(1)两个简单的函数:getAddrRanges 和 recvFunctional
(2)handleFunctional
(3)recvRangeChange
9、Implementing receiving requests
(1)recvTimingReq
(2)handleRequest(辅助函数)
(3)sendPacket
(4)recvReqRetry
10、Implementing receiving responses
(1)handleResponse
(3)recvRespRetry
(4)trySendRetry
11、create
五、函数之间的调用关系
六、Create a config file
七、测试程序
1、不使用debug flag
2、使用debug flag
3、将 CPU 模型更改为乱序模型(X86O3CPU)【可选项】
前阵子忙于期末大论文和专利的撰写,没有继续学习剩余教程。
今天接着之前的博客总结一下教程学习过程中的心得。
官网教程:gem5: Creating SimObjects in the memory system
这部分教程主要是为了创建一个简单的位于CPU和内存之间的缓存类(下一个教程是在这个缓存类的基础上增加部分逻辑,完成一个简单的拥塞式单处理器缓存)。
一、gem5 master and slave ports
gem5中有两种端口:主端口(master port)和从端口(slave port)。所有的内存对象都通过端口连接在一起,这些端口在内存对象之间提供一个严格的接口。
端口可以实现三种不同的内存系统模式:时序(timing), 原子(atomic)和功能(functional)。
原子模式(Atomic mode):在原子模式下,内存系统操作按照顺序依次执行,没有并发事件发生。使用该模式的主要目的是加快仿真速度并预热仿真器。通过将所有内存请求串行执行,避免事件的并发处理和同步开销。
功能模式(Functional mode):也可以被描述为调试模式,是用于从主机上读取数据到模拟器内存中。
例如:功能模式用于将主机中的process.cmd中的二进制文件加载到模拟系统的内存中,以便模拟系统可以访问它。在读取时,功能访问应返回最新的数据,无论数据位于何处,并且在写入时应更新所有可能的有效数据(例如,在具有缓存的系统中,可能存在多个具有相同地址的有效缓存块)
二、Packets
在gem5中,数据包(Packet)通过端口进行传输。一个数据包由一个内存请求对象(MemReq)组成。内存请求对象(MemReq)保存了关于发起该数据包(Packet)的原始请求的信息,例如请求者、地址和请求类型(读取、写入等)。
数据包还有一个内存命令(MemCmd),它表示数据包的当前命令。该命令在数据包的生命周期中可以发生变化(例如,一旦内存命令满足,请求就会转变为响应)。最常见的内存命令包括ReadReq(读取请求)、ReadResp(读取响应)、WriteReq(写入请求)、WriteResp(写入响应)。还有用于缓存的写回请求(WritebackDirty、WritebackClean)和许多其他命令类型。
内存命令详细解释。
- ReadReq(读取请求):ReadReq是一个读取请求命令,用于请求从内存中读取数据。当CPU需要读取特定内存地址中的数据时,它将发送一个ReadReq命令给内存对象,以请求数据传输。
- ReadResp(读取响应):ReadResp是一个读取响应命令,用于回复读取请求并返回请求的数据。当内存对象接收到ReadReq命令后,在从内存中读取到相应数据后,会通过发送ReadResp命令将数据返回给CPU。
- WriteReq(写入请求):WriteReq是一个写入请求命令,用于请求将数据写入到内存中的特定地址。当CPU需要将数据写入到特定内存地址时,它将发送一个WriteReq命令给内存对象,以请求数据的写入操作。
- WriteResp(写入响应):WriteResp是一个写入响应命令,用于回复写入请求并确认数据已成功写入内存。当内存对象接收到WriteReq命令后,它会执行写入操作,并通过发送WriteResp命令来确认数据已经成功写入内存。
ReadReq和ReadResp用于在CPU和内存之间进行读取数据的请求和响应;WriteReq和WriteResp用于在CPU和内存之间进行写入数据的请求和响应。读取请求和写入请求是CPU向内存发送的命令,而读取响应和写入响应是内存对象向CPU发送的命令,用于确认请求的执行和数据的传输。
数据包可以保存请求的数据(写操作),或者保存指向数据的指针(读操作)。在创建数据包时,可以选择数据是动态的(显式分配和释放)还是静态的(由数据包对象分配和释放)。
最后,数据包在经典缓存中被用作跟踪一致性的单位。因此,数据包代码的大部分是针对经典缓存一致性协议的。然而,在gem5中,数据包用于所有内存对象之间的通信,即使它们与一致性没有直接关系(例如DRAM控制器和CPU模型)。
所有端口接口函数都接受一个数据包(Packet)指针作为参数。由于该指针非常常见,gem5中包含了一个typedef:PacketPtr。【用来作为一次请求是否完成的根据】
三、Port interface
在Gem5中,有两种类型的端口:主端口(Master Port)和从端口(Slave Port)。要实现一个内存对象,都需要实现至少一种类型的端口。主端口用于向内存对象发送读取和写入请求,从端口用于接收来自其他组件(例如CPU)的读取和写入请求。
为此,可以创建一个新的类,继承自MasterPort或SlavePort,用于主端口和从端口。
- 主端口(Master Port)用于向内存对象发送读取和写入请求。它由主要的控制单元(通常是CPU)使用,用于主动发起对内存对象的读取和写入操作。主端口负责管理请求的生成、发送和处理,以及与内存对象的通信。CPU或其他主要组件使用主端口来访问内存对象,主动控制读取和写入的时机和方式。
- 从端口(Slave Port)用于接收来自其他组件(例如CPU)的读取和写入请求。它作为内存对象的接口,用于接收来自其他组件的读取和写入请求,并将这些请求传递给内部的处理逻辑。从端口负责处理传入请求,并根据请求类型执行相应的读取和写入操作。
- 主端口和从端口的主要区别在于使用方式和功能角色。主端口由主要控制单元使用,主动发起对内存对象的读取和写入请求;而从端口作为内存对象的接口,被动地接收和处理来自其他组件的读取和写入请求。
下面的图示展示了主端口和从端口之间最简单的交互方式。该图展示了时序模式下的交互。其他模式则更简单,并且在主端口和从端口之间使用简单的调用链。
所有的端口接口都要求以PacketPtr作为参数。每个函数(sendTimingReq、recvTimingReq等)都接受一个参数,即PacketPtr。这个PacketPtr【数据包指针】代表要发送或接收的请求或响应数据包。
要发送一个请求数据包,主设备(发送请求的设备)调用sendTimingReq函数。在同一个调用链中,从设备上的recvTimingReq函数被调用,它的唯一参数也是PacketPtr,与sendTimingReq函数使用的是同一个PacketPtr。
recvTimingReq函数的返回类型是bool。这个布尔返回值直接返回给调用的主设备。返回true表示从设备已经接受了数据包。而返回false则表示从设备无法接受数据包,请求必须在将来的某个时间重试。
在上面的示例中,首先,主设备通过调用sendTimingReq发送一个定时请求,该函数接着调用recvTimingResp。从设备从recvTimingReq函数中返回true,这个返回值从sendTimingReq函数中返回。主设备继续执行,而从设备则完成必要的操作来处理请求(例如,如果它是一个缓存,它会查找标签以查看请求中的地址是否匹配)。
一旦从设备完成请求处理,它可以向主设备发送响应。从设备调用 sendTimingResp 函数并传递响应数据包(这应该是与请求相同的 PacketPtr,但现在应该是一个响应数据包)。接着,主设备的 recvTimingResp 函数将被调用。主设备的 recvTimingResp 函数返回 true,而这个返回值将传递给从设备的 sendTimingResp。因此,该请求的交互过程完成了。
1、主设备发送请求时从设备忙
在这种情况下,从设备在 recvTimingReq 函数中返回 false【说明此时从设备在忙,没能及时相应主设备的请求】。当主设备在调用 sendTimingReq 后收到 false 时,它必须等待直到执行 recvReqRetry 函数【主设备需要等待从设备发出的信号,而不是持续等待,而从设备会主动发送sendReqRetry信号,通知主设备可以重新尝试发送请求】。只有在调用该函数之后,主设备才能重新尝试调用 sendTimingRequest【相当于第一次请求没响应,再请求一次】。上述图示展示了定时请求失败一次的情况,但它可能会失败任意次数。
注意:跟踪失败的数据包是主设备要完成的事情,而不是从设备的任务。从设备不会保留失败的数据包指针。【也就是说从设备没有记忆,不会记录失败的数据包】
2、从设备发送响应时主设备忙
类似于上述,当主设备在从设备尝试发送响应时忙于其他任务的情况【主设备忙】。在这种情况下,从设备在接收到 recvRespRetry 之前无法调用 sendTimingResp。
在这两种情况下,重试代码路径可以是一个单一的调用堆栈。例如,当主设备调用sendRespRetry时,recvTimingReq也可以在同一个调用堆栈中被调用。因此,很容易错误地创建无限递归错误或其他错误。重要的是,在内存对象发送重试之前,它在那一刻准备好接受另一个数据包。
通俗理解:一些函数的调用会在同一个调用栈中完成,由于代码路径的连续性,可能创建无限递归或其他错误,导致程序陷入无限循环,或者产生其他不正确的行为。所以,从设备在发送sendReqRetry信号之前,就要做好处理下一个请求的准备。
四、Simple memory object example
在本节中,将构建一个简单的内存对象。
它将仅仅将请求从 CPU 端(a simple CPU)传递到内存端(a simple memory bus)。它具有一个主设备端口(master port),用于向内存总线(the memory bus)发送请求,并具有两个 CPU 端口(two cpu-side ports),用于 CPU 的指令(instruction port)和数据缓存端口(data cache port)。
1、Declare the SimObject
创建一个 SimObject 的 Python 文件。名称:SimpleMemobj.py
将这个简单的内存对象命名为 SimpleMemobj,并在 src/learning_gem5/part2/simple_memobj 中创建 SimObject 的 Python 文件。
from m5.params import *
from m5.proxy import *
from m5.SimObject import SimObject
class SimpleMemobj(SimObject):
type = 'SimpleMemobj'
cxx_header = "learning_gem5/part2/simple_memobj.hh"
inst_port = SlavePort("CPU side port, receives requests")
data_port = SlavePort("CPU side port, receives requests")
mem_side = MasterPort("Memory side port, sends requests")
这个对象是从 SimObject 继承的。SimObject 类有一个纯虚函数【就是只定义了这个函数,但是没有任何实现。如果继承了这个类,就需要在类中实现这个函数】,需要在 C++ 实现中定义它,即 getPort。
这个对象的参数是三个端口。两个端口用于连接 CPU 的指令和数据端口,另一个端口用于连接内存总线。这些端口没有默认值,并且有一个简单的描述【可以没有参数,如果有的话就必须是描述】。在实现 SimpleMemobj 并定义 getPort 函数时,需要使用这些名称。
2、Define the SimpleMemobj class
构造文件中声明SimObject 的 Python 文件。名称:SConscript
Import('*')
SimObject('SimpleMemobj.py')
Source('simple_memobj.cc')
DebugFlag('SimpleMemobj', "For Learning gem5 Part 2.")
3、Define the SimpleMemobj class
为SimpleMemobj类创建一个头文件。名称:SimpleMemobj.hh
#include "mem/port.hh"
#include "params/SimpleMemobj.hh"
#include "sim/sim_object.hh"
class SimpleMemobj : public SimObject
{
private:
public:
/** constructor
*/
SimpleMemobj(SimpleMemobjParams *params);
};
前阵子师弟问了我一个问题,就是在引入头文件的时候,没有"params/SimpleMemobj.hh",为什么在引入后不会提示有错。
答案:这个文件夹里的都是.hh头文件,准确来说/build/X86/params文件夹中的.hh文件是由gem5的构建过程生成的,这个构建过程包括了三个阶段:配置(SCons配置gem5的构建环境),编译(编译源代码并生成可执行文件)、生成(把可执行文件和其他文件复制到指定的目标位置)。这个过程中就会自动生成一些文件,其中就包括这个.hh文件(这个文件是构建过程中生成的中间文件)。在scons的时候,哪怕这个编译过程没有完成,但是前面这些为了生成可执行文件而配置的环境和中间文件,就自动生成了。构建系统会根据构建规则生成编译所需的中间文件和目标文件,并将它们放置在指定的构建目录中(比如gem5/build/X86/params文件夹)。所以即使在构建过程中尚未生成ZylObject.hh文件,但因为指定了路径,编译过程会成功,并且引用的头文件将在构建后的可执行文件中正确被解析。【可能有些绕,多读几遍】
4、Define a slave port type
这部分是在SimpleMemobj.hh中定义SimpleMemobj类时的内部类定义。
从设备端口(CPU 端口)
这两个类型的端口可以直接在SimpleMemobj 类内部声明,因为其他对象不会使用这些类。
从 SlavePort 类继承,并实现SlavePort 类中所有纯虚函数。
class CPUSidePort : public SlavePort
{
private:
SimpleMemobj *owner;
public:
CPUSidePort(const std::string& name, SimpleMemobj *owner) :
SlavePort(name, owner), owner(owner)
{ }
AddrRangeList getAddrRanges() const override;
protected:
Tick recvAtomic(PacketPtr pkt) override { panic("recvAtomic unimpl."); }
void recvFunctional(PacketPtr pkt) override;
bool recvTimingReq(PacketPtr pkt) override;
void recvRespRetry() override;
};
这个对象需要定义五个函数。
此对象还有一个成员变量,即它的所有者,因此它可以在该对象上调用函数。
5、Define a master port type
这部分也是在SimpleMemobj.hh中定义SimpleMemobj类时的内部类定义。
定义一个主设备端口类型。这将是内存端口,它将把来自 CPU 端的请求转发到其他内存系统中。
class MemSidePort : public MasterPort
{
private:
SimpleMemobj *owner;
public:
MemSidePort(const std::string& name, SimpleMemobj *owner) :
MasterPort(name, owner), owner(owner)
{ }
protected:
bool recvTimingResp(PacketPtr pkt) override;
void recvReqRetry() override;
void recvRangeChange() override;
};
这个类只有三个纯虚函数。
6、Defining the SimObject interface
上面已经定义了两种新类型CPUSidePort 和 MemSidePort,将它们作为 SimpleMemobj 的一部分来声明三个端口。还需要在 SimObject 类中声明纯虚函数 getPort。在初始化阶段,gem5 使用这个函数通过端口将内存对象连接在一起。
- 纯虚函数(Pure Virtual Function)是在基类中声明的虚函数,但没有提供具体的实现。
- 纯虚函数在基类中用于定义接口和行为规范,它要求派生类必须实现该函数。由于纯虚函数没有具体的实现,所以基类本身不能被实例化,而只能作为一个抽象类使用【抽象类(Abstract Class)是指包含纯虚函数(pure virtual function)的类。抽象类不能被实例化,只能作为基类用于派生其他类】,派生类必须提供对纯虚函数的具体实现才能被实例化。
- 派生类必须在其定义中提供对纯虚函数的实现,否则派生类也将成为抽象类。如果派生类没有提供对所有纯虚函数的实现,则该派生类仍然是抽象类,无法被实例化。
class SimpleMemobj : public SimObject
{
private:
<CPUSidePort declaration>
<MemSidePort declaration>
CPUSidePort instPort;
CPUSidePort dataPort;
MemSidePort memPort;
public:
SimpleMemobj(SimpleMemobjParams *params);
Port &getPort(const std::string &if_name,
PortID idx=InvalidPortID) override;
};
7、Implementing basic SimObject functions
名称:SimpleMemobj.cc
对于 SimpleMemobj 的构造函数,将简单地调用 SimObject 的构造函数。还需要初始化所有的端口。每个端口的构造函数有两个参数:名称和指向其所有者的指针。名称可以是任何字符串,但按照惯例,它与 Python SimObject 文件中的名称相同。同时将 blocked 初始化为 false。
#include "learning_gem5/part2/simple_memobj.hh"
#include "debug/SimpleMemobj.hh"
SimpleMemobj::SimpleMemobj(SimpleMemobjParams *params) :
SimObject(params),
instPort(params->name + ".inst_port", this),
dataPort(params->name + ".data_port", this),
memPort(params->name + ".mem_side", this), blocked(false)
{
}
接下来,我们需要实现获取端口的接口。这个接口是由函数 getPort
组成。该函数有两个参数。if_name
是该对象的接口的 Python 变量名。
为了实现 getPort
,我们将比较 if_name
并检查它是否与我们的 Python SimObject 文件【SimpleMemobj.py】中指定的 mem_side
相匹配。如果匹配,则返回 memPort
对象。如果名称是 "inst_port",则返回 instPort
,如果名称是 "data_port",则返回data_Port
。如果不是,我们将请求名称传递给父类。【这个过程就是判断端口属性的】
Port &
SimpleMemobj::getPort(const std::string &if_name, PortID idx)
{
panic_if(idx != InvalidPortID, "This object doesn't support vector ports");
// This is the name from the Python SimObject declaration (SimpleMemobj.py)
if (if_name == "mem_side") {
return memPort;
} else if (if_name == "inst_port") {
return instPort;
} else if (if_name == "data_port") {
return dataPort;
} else {
// pass it along to our super class
return SimObject::getPort(if_name, idx);
}
}
8、Implementing slave and master port functions
主从端口的实现都比较简单,大多数情况下,每个端口函数都是将信息转发给主内存对象(SimpleMemobj)。
(1)两个简单的函数:getAddrRanges 和 recvFunctional
它们只是调用 SimpleMemobj 的相应函数。
AddrRangeList
SimpleMemobj::CPUSidePort::getAddrRanges() const
{
return owner->getAddrRanges();
}
void
SimpleMemobj::CPUSidePort::recvFunctional(PacketPtr pkt)
{
return owner->handleFunctional(pkt);
}
(2)handleFunctional
将请求传递到内存端,使用 DPRINTF 调用来跟踪调试目的的操作情况。
void
SimpleMemobj::handleFunctional(PacketPtr pkt)
{
memPort.sendFunctional(pkt);
}
AddrRangeList
SimpleMemobj::getAddrRanges() const
{
DPRINTF(SimpleMemobj, "Sending new ranges\n");
return memPort.getAddrRanges();
}
(3)recvRangeChange
对于 MemSidePort,需要实现 recvRangeChange 并通过 SimpleMemobj 将请求转发到从设备端口。
void
SimpleMemobj::MemSidePort::recvRangeChange()
{
owner->sendRangeChange();
}
void
SimpleMemobj::sendRangeChange()
{
instPort.sendRangeChange();
dataPort.sendRangeChange();
}
9、Implementing receiving requests
(1)recvTimingReq
需要检查 SimpleMemobj 是否可以接受该请求。SimpleMemobj 是一个非常简单的阻塞结构;一次只允许一个请求。因此,如果在一个请求正在处理时收到另一个请求,SimpleMemobj 将阻塞第二个请求。
为了简化实现,CPUSidePort 存储了端口的所有流控信息。因此,我们需要向 CPUSidePort 添加一个额外的成员变量 needRetry,一个布尔值,用于存储当 SimpleMemobj 变得空闲时是否需要发送重试。因此,如果 SimpleMemobj 在处理请求时被阻塞,我们将设置在将来某个时间需要发送重试。
bool
SimpleMemobj::CPUSidePort::recvTimingReq(PacketPtr pkt)
{
if (!owner->handleRequest(pkt)) {
needRetry = true;
return false;
} else {
return true;
}
}
为了处理 SimpleMemobj 的请求,首先检查 SimpleMemobj 是否已经被阻塞,等待另一个请求的响应。如果被阻塞(有请求未处理),将返回 false,向调用的主设备端口表示从设备目前无法接受该请求。否则,将标记该端口为被阻塞状态,并通过内存端口发送数据包。
为此,可以在 MemSidePort 对象中定义一个辅助函数,将流控隐藏在 SimpleMemobj 实现之后。我们假设 memPort 处理所有的流控,并且始终从 handleRequest 中返回 true,因为我们成功消耗了请求。
(2)handleRequest(辅助函数)
blocked
为true,表示SimpleMemobj当前被阻塞,有其他请求正在处理中,那么函数会返回false,表示无法接受新的请求。- SimpleMemobj没有被阻塞,那么会输出一条日志信息,表示接收到了请求的地址。接着,将
blocked
标记为true,表示SimpleMemobj被阻塞,然后通过memPort
发送数据包(pkt
)。 - 最后,函数返回true,表示成功处理了请求,可以继续处理后续的请求。
bool
SimpleMemobj::handleRequest(PacketPtr pkt)
{
if (blocked) {
return false;
}
DPRINTF(SimpleMemobj, "Got request for addr %#x\n", pkt->getAddr());
blocked = true;
memPort.sendPacket(pkt);
return true;
}
(3)sendPacket
需要在 MemSidePort 中实现 sendPacket
函数。这个函数将处理流控,以防其对等的从设备端口无法接受请求。为此,我们需要在 MemSidePort 中添加一个成员变量来存储在被阻塞时的数据包。如果接收方无法接收请求(或响应),发送方负责存储数据包。
这个函数简单地调用 sendTimingReq
函数来发送数据包。如果发送失败,那么该对象将数据包存储在 blockedPacket
成员变量中,以便在以后的时间发送数据包(当它接收到 recvReqRetry
时)。这个函数还包含了一些防御性的代码提示,如果blockedPacket
不为空(即已经有一个被阻塞的数据包),就会抛出错误(panic)。提示“如果存在被阻塞的数据包,就不应该尝试发送新的数据包”。【防止出现不一致状态】
void
SimpleMemobj::MemSidePort::sendPacket(PacketPtr pkt)
{
panic_if(blockedPacket != nullptr, "Should never try to send if blocked!");
if (!sendTimingReq(pkt)) {
blockedPacket = pkt;
}
}
(4)recvReqRetry
实现重新发送数据包。在这个函数里,可以直接调用上述的sendPacket 函数重新发送数据包。
void
SimpleMemobj::MemSidePort::recvReqRetry()
{
assert(blockedPacket != nullptr);
PacketPtr pkt = blockedPacket;
blockedPacket = nullptr;
sendPacket(pkt);
}
10、Implementing receiving responses
响应请求部分和接受请求类似。
当 MemSidePort 收到响应时,通过 SimpleMemobj 将响应转发到相应的 CPUSidePort。
bool
SimpleMemobj::MemSidePort::recvTimingResp(PacketPtr pkt)
{
return owner->handleResponse(pkt);
}
(1)handleResponse
在 SimpleMemobj 中,当收到响应时,首先,对象应该始终处于阻塞状态,因为它是一个阻塞对象(阻塞状态表示该对象当前正在处理某个请求,还未完成)。在将数据包发送回 CPU 端口之前,需要将对象标记为非阻塞状态。这一步骤必须在调用sendTimingResp
函数之前完成。如果在发送响应之前没有解除阻塞状态,可能会导致无限循环。这是因为在接收到响应并发送另一个请求之间,主设备端口可能只有一个调用链(调用路径),而没有其他机制来检测和处理阻塞状态。
在解除 SimpleMemobj 的阻塞后,检查数据包是指令包还是数据包,并将其通过适当的端口发送回去。最后,由于SimpleMemobj 对象现在不再阻塞,需要通知 CPU 端口可以重新尝试之前失败的请求(其中包括指令请求和数据请求)。
bool
SimpleMemobj::handleResponse(PacketPtr pkt)
{
assert(blocked);
DPRINTF(SimpleMemobj, "Got response for addr %#x\n", pkt->getAddr());
blocked = false;
// Simply forward to the memory port
if (pkt->req->isInstFetch()) {
instPort.sendPacket(pkt);
} else {
dataPort.sendPacket(pkt);
}
instPort.trySendRetry();
dataPort.trySendRetry();
return true;
}
(2)sendPacket
类似于在 MemSidePort 中实现的发送数据包函数,可以在 CPUSidePort 中实现一个 sendPacket 函数,用于向 CPU 端发送响应。这个函数调用 sendTimingResp,然后调用对等主设备端口的 recvTimingResp。
如果调用失败,并且对等端口当前被阻塞,那么将存储要稍后发送的数据包。
void
SimpleMemobj::CPUSidePort::sendPacket(PacketPtr pkt)
{
panic_if(blockedPacket != nullptr, "Should never try to send if blocked!");
if (!sendTimingResp(pkt)) {
blockedPacket = pkt;
}
}
(3)recvRespRetry
接收到 recvRespRetry 时,将重新发送这个被阻塞的数据包。这个函数与上面的 recvReqRetry 完全相同,只是简单地尝试重新发送数据包,它可能再次被阻塞。
void
SimpleMemobj::CPUSidePort::recvRespRetry()
{
assert(blockedPacket != nullptr);
PacketPtr pkt = blockedPacket;
blockedPacket = nullptr;
sendPacket(pkt);
}
(4)trySendRetry
最后,需要在 CPUSidePort 中实现额外的函数 trySendRetry。每当 SimpleMemobj 可能解除阻塞时, SimpleMemobj 调用该函数。trySendRetry
函数的作用是检查是否需要进行重试。在SimpleMemobj的recvTimingReq
函数中,当SimpleMemobj在新的请求上被阻塞时,会进行标记。这个标记的目的是指示当前的请求无法立即执行,需要进行重试。因此,在trySendRetry
函数中,会检查是否存在需要重试的情况。如果需要重试,该函数会调用sendRetryReq
函数,而sendRetryReq
函数会调用对等主设备端口(在这个例子中是CPU)的recvReqRetry
函数。
void
SimpleMemobj::CPUSidePort::trySendRetry()
{
if (needRetry && blockedPacket == nullptr) {
needRetry = false;
DPRINTF(SimpleMemobj, "Sending retry req for %d\n", id);
sendRetryReq();
}
}
11、create
上述的函数都是在SimpleMemobj.cc中实现的,但是下面create函数还没找到在哪实现(存疑)。
除了这个函数之外,为了完成文件,我们还需要添加 SimpleMemobj 的 create 函数
SimpleMemobj*
SimpleMemobjParams::create()
{
return new SimpleMemobj(this);
}
五、函数之间的调用关系
下图显示了 CPUSidePort、MemSidePort 和 SimpleMemobj 之间的关系。该图示展示了对等端口与 SimpleMemobj 实现之间的交互方式。每个加粗的函数都是必须实现的函数,而非加粗的函数则是与对等端口的接口函数。颜色突出显示了对象中的一个 API 路径(例如,接收请求或更新内存范围)。
对于这个简单的内存对象,数据包只是从 CPU 端转发到内存端。然而,通过修改 handleRequest 和 handleResponse,我们可以创建功能丰富的对象,比如在下一章中介绍的缓存对象。gem5: Creating a simple cache object
六、Create a config file
文件名:simple_cache.py【执行文件】
将 SimpleMemobj 添加到系统的配置文件中。
import m5
from m5.objects import *
system = System()
system.clk_domain = SrcClockDomain()
system.clk_domain.clock = '1GHz'
system.clk_domain.voltage_domain = VoltageDomain()
system.mem_mode = 'timing'
system.mem_ranges = [AddrRange('512MB')]
system.cpu = X86TimingSimpleCPU()
system.memobj = SimpleMemobj()
system.cpu.icache_port = system.memobj.inst_port
system.cpu.dcache_port = system.memobj.data_port
system.membus = SystemXBar()
system.memobj.mem_side = system.membus.slave
system.cpu.createInterruptController()
system.cpu.interrupts[0].pio = system.membus.master
system.cpu.interrupts[0].int_master = system.membus.slave
system.cpu.interrupts[0].int_slave = system.membus.master
system.mem_ctrl = DDR3_1600_8x8()
system.mem_ctrl.range = system.mem_ranges[0]
system.mem_ctrl.port = system.membus.master
system.system_port = system.membus.slave
process = Process()
process.cmd = ['tests/test-progs/hello/bin/x86/linux/hello']
system.cpu.workload = process
system.cpu.createThreads()
root = Root(full_system = False, system = system)
m5.instantiate()
print ("Beginning simulation!")
exit_event = m5.simulate()
print('Exiting @ tick %i because %s' % (m5.curTick(), exit_event.getCause()))
这段代码是使用gem5模拟器配置系统并运行一个简单的hello程序。
-
首先,通过
import m5
和from m5.objects import *
导入gem5的相关模块和对象。 -
然后,创建一个
System
对象来表示系统。设置系统的时钟域(clk_domain
)为1GHz,并指定电压域(voltage_domain
)。将系统的内存模式(mem_mode
)设置为'timing',表示使用时序模型。并指定系统的内存范围(mem_ranges
)为512MB。 -
接下来,创建一个X86架构的简单时序CPU(
X86TimingSimpleCPU
)作为系统的CPU。 -
创建一个
SimpleMemobj
对象作为系统中的内存对象。 -
将CPU的指令端口(
icache_port
)和数据端口(dcache_port
)连接到SimpleMemobj
的端口。 -
创建一个
SystemXBar
对象作为系统的内存总线。 -
将
SimpleMemobj
的内存端口(mem_side
)连接到系统的内存总线上。 -
为CPU创建一个中断控制器,并将其与系统的总线连接起来,用于处理中断信号。
-
创建一个DDR3内存控制器(
DDR3_1600_8x8
),并将其范围设置为系统的内存范围,将其端口连接到系统的总线上。 -
将系统总线的从端口(
slave
)连接到系统的系统端口(system_port
)。 -
创建一个
Process
对象,设置其cmd
属性为要运行的hello程序的路径【在这段代码中,process.cmd
是一个字符串列表,指定要运行的可执行程序的路径和命令行参数。在这里,process.cmd
被设置为['tests/test-progs/hello/bin/x86/linux/hello']
,表示要运行的可执行程序是hello
。这个程序将在gem5模拟的系统中作为工作负载被执行】。 -
将这个进程作为工作负载(
workload
)分配给CPU。 -
创建CPU的线程。
-
创建一个
Root
对象来表示系统的根节点,将其full_system
属性设置为False,表示运行的是一个部分系统模拟。将系统对象指定为系统根节点的属性。 -
通过
m5.instantiate()
实例化系统对象。 -
开始仿真过程,调用
m5.simulate()
函数,并将返回的exit_event
保存在exit_event
变量中。最后,打印仿真结束的信息,包括仿真结束时的时钟周期数和结束的原因。
七、测试程序
1、不使用debug flag
build/X86/gem5.opt configs/learning_gem5/part2/simple_cache.py
2、使用debug flag
build/X86/gem5.opt --debug-flags=SimpleMemobj configs/learning_gem5/part2/simple_memobj.py
3、将 CPU 模型更改为乱序模型(X86O3CPU)【可选项】
使用乱序 CPU,可能会看到不同的地址流,因为它允许同时存在多个内存请求。
在使用乱序 CPU 时,由于 SimpleMemobj 是阻塞的,可能会出现很多停顿。
最后说明:
上述是个人比较肤浅的学习总结,大多是直接翻译英文教程,其中有一些英文逻辑理解不太清晰的我结合自己个人理解又加了一些内容,欢迎大家提出问题,共同探讨。