GEM5教程--修改和拓展gem5(四)
六、在内存系统中创建SimObjects
在这个部分中,我们将创建一个位于CPU和内存总线之间的简单内存对象。在下面的部分中中,我们将使用这个简单的内存对象,并为其添加一些逻辑,使其成为一个非常简单的阻塞单处理器缓存。
1、gem5主、从端口
在深入研究内存对象的实现之前,我们应该首先了解gem5的主端口和从端口接口。正如前面在 创建简单配置脚本
中所讨论的,所有内存对象都通过端口连接在一起。这些端口在这些内存对象之间提供了一个刚性接口。
这些端口实现三种不同的存储系统模式:定时、原子和功能。最重要的模式是定时模式。定时模式是产生正确模拟结果的唯一模式。其他模式仅在特殊情况下使用。
原子模式
有助于将模拟快速转发到感兴趣的区域并预热模拟器。此模式假定内存系统中不会生成任何事件。相反,所有的内存请求都通过一个长的调用链执行。不需要为内存对象实现原子访问,除非它将在快速转发期间或模拟器预热期间使用。
功能模式
更好地描述为调试模式。功能模式用于将数据从主机读取到模拟器内存中。它在系统调用模拟模式中使用得很频繁。例如,函数模式用于将process.cmd中的二进制文件从主机加载到模拟系统的内存中,以便模拟系统可以访问它。无论数据在哪里,功能访问都应在读时返回最新的数据,并应在写时更新所有可能的有效数据(例如,在具有缓存的系统中,可能有多个具有相同地址的有效缓存块)。
2、数据包
在gem5中,数据包
通过端口发送。包由MemReq组成,MemReq是内存请求对象。MemReq保存初始化包的原始请求的信息,例如请求者、地址和请求类型(读、写等)。
数据包还有一个MemCmd,它是数据包的当前命令。此命令可以在数据包的整个生命周期中改变(例如,一旦满足内存命令,请求就变成响应)。最常见的MemCmd是ReadReq(读请求)、ReadResp(读响应)、WriteReq(写请求)、WriteResp(写响应)。还有缓存和许多其他命令类型的写回请求(WritebackDirty、WritebackClean)
数据包也可以保留请求的数据,或指向数据的指针。创建数据包时,可以选择数据是动态的(显式分配和释放)还是静态的(由数据包对象分配和释放)。
最后,在经典缓存中使用包作为跟踪一致性的单元。因此,许多包代码都是针对经典的缓存一致性协议的。然而,包用于gem5中的所有内存对象之间的通信,即使它们不直接涉及一致性(例如DRAM控制器和CPU模型)。
所有端口接口函数都接受数据包指针作为参数。由于这个指针很常见,gem5包含一个typedef:PacketPtr。
3、端口接口
gem5中有两种端口:主端口和从端口。无论何时实现内存对象,都将至少实现其中一种类型的端口。为此,您将创建一个新类,该类分别从主端口或从端口的SlavePort继承主端口和从端口。主端口发送请求(和接收响应),从端口接收请求(和发送响应)。
(1)简单的主从交互,当两者都能接受请求和响应时。 概述主端口和从端口之间最简单的交互。
此图显示计时模式下的交互`。其他模式要简单得多,在主模式和从模式之间使用一个简单的调用链。
如上所述,所有端口接口都需要一个PacketPtr作为参数。这些函数(sendTimingReq、recvTimingReq等)都接受一个参数PacketPtr。此数据包是发送或接收的请求或响应。
要发送请求包,主机调用sendTimingReq。反过来,(在同一个调用链中),函数recvTimingReq被调用到具有相同PacketPtr作为其唯一参数的从机上。
recvTimingReq的返回类型为bool。这个布尔返回值直接返回给调用主控。返回值true表示数据包已被从属服务器接受。另一方面,返回值false意味着从机无法接受请求,必须在将来某个时间重试该请求。
在简单的主从交互中,当两者都可以接受请求和响应时,首先,主机通过调用sendTimingReq来发送定时请求,而sendTimingReq又调用recvtimingreps。从机,从recvtimingreps返回true,这是从对sendTimingReq的调用返回的。主服务器继续执行,从服务器执行完成请求所需的任何操作(例如,如果它是一个缓存,它会查找标记以查看是否与请求中的地址匹配)。
一旦从机完成请求,它就可以向主机发送响应。从机使用响应包调用sendTimingResp(这应该是与请求相同的PacketPtr,但现在应该是响应包)。然后,调用主函数recvTimingResp。主机的recvTimingResp函数返回true,这是从机中sendTimingResp的返回值。因此,该请求的交互已完成。
在后面的简单内存对象示例
中,我们将展示这些函数的示例代码。
(2)当主服务器或从服务器接收到请求或响应时,它们可能正忙。从机忙时的简单主从交互
显示了原始请求发送时从机忙的情况。
在这种情况下,从机从recvTimingReq函数返回false。当主机在调用sendTimingReq后收到false时,它必须等待它的函数recvReqRetry被执行。只有在调用此函数时,才允许主机重试调用sendTimingRequest。上图显示计时请求失败一次,但可能失败任意次数。注意:由主机来跟踪失败的包,而不是从机。从机不保留指向失败数据包的指针。
(3)
类似地,当主服务器忙时的简单主从交互
显示了当主服务器忙时,从服务器试图发送响应的情况。在这种情况下,从机在收到recvresprety之前不能调用sendTimingResp。
重要的是,在这两种情况下,重试代码路径可以是单个调用堆栈。例如,当主机调用sendRespRetry时,也可以在同一个调用堆栈中调用recvTimingReq。因此,很容易错误地创建无限递归错误或其他错误。重要的是,在内存对象发送重试之前,它在该时刻准备好接受另一个数据包。
4、简单内存对象示例
在这个部分中,我们将构建一个简单的内存对象。最初,它只是将请求从CPU端(一个简单的CPU)传递到内存端(一个简单的内存总线)。请参见下面的图,它有一个位于CPU和内存总线之间的简单内存对象。它将有一个主端口,用于向内存总线发送请求,以及两个cpu侧端口,用于cpu的指令和数据缓存端口。在后面的部分中,我们将添加使此对象成为缓存的逻辑。
5、声明SimObject
就像我们在创建非常简单的SimObject时创建SimObject一样,第一步是创建SimObject Python文件。我们将这个简单内存对象称为simple memobj,并在src/learning-gem5/simple-memobj中创建SimObject Python文件。
from m5.params import *
from m5.proxy import *
from MemObject import MemObject
class SimpleMemobj(MemObject):
type = 'SimpleMemobj'
cxx_header = "learning_gem5/simple_memobj/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")
对于这个对象,我们从MemObject
继承,而不是SimObject,因为我们正在创建一个与内存系统交互的对象。MemObject类有两个纯虚函数,我们将在C++实现、getMasterPort
和getSlavePort
中定义它们。
这个对象的参数是三个端口。两个CPU端口用于连接指令和数据端口,一个端口用于连接内存总线。这些端口没有默认值,它们有一个简单的描述。 记住这些端口的名称很重要。在实现SimpleMemobj和定义getMasterPort和getSlavePort函数时,我们将显式地使用这些名称。
6、定义SimpleMemobj类
现在,我们为simplemmobj创建一个头文件。
class SimpleMemobj : public MemObject
{
private:
public:
/** constructor
*/
SimpleMemobj(SimpleMemobjParams *params);
};
7、定义从端口类型
现在,我们需要为两种端口定义类:CPU端和内存端端口。为此,我们将在simplemmobj类中声明这些类,因为没有其他对象将使用这些类。
让我们从从端口开始,或者从CPU端端口开始。我们将从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;
};
此对象需要定义五个函数。:
(1)AddrRangeList getAddrRanges():此函数返回所有者负责的非重叠地址范围的列表。所有从端口都必须重写此函数并返回包含至少一个项的填充列表。crossbar对象使用它来知道向哪个端口发送请求。大多数内存对象要么返回所有内存,要么返回其对等端响应的任何地址范围。
(2)Tick recvAtomic(PacketPtr pkt):这是每当CPU试图进行原子内存访问时调用的函数。我们暂时不打算实现这个功能。相反,如果调用此函数,我们将“惊慌失措”。panic退出模拟并打印出消息。
(3)void recvFunctional(PacketPtr pkt):当CPU进行功能访问时调用。如上所述,这在syscall仿真模式中用于从主机文件系统加载文件。
(4)bool recvTimingReq(PacketPtr pkt):此端口的对等方调用sendTimingReq时调用此函数。它接受单个参数,该参数是请求的数据包指针。如果数据包被接受,则此函数返回true。如果此函数返回false,则在将来的某个时间点,此对象必须调用sendReqRetry,以便通知对等端口它能够接受被拒绝的请求。
(5)void recvRespRetry():对等端口调用sendRespRetry时调用此函数。执行此函数时,此端口应再次调用sendTimingResp以重试将响应发送到其对等主端口。
这个对象还有一个成员变量,它的所有者,所以它可以调用该对象上的函数。
8、定义主端口类型
接下来,我们需要定义一个主端口类型。这将是内存侧端口,它将请求从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;
};
这个类只有三个我们必须重写的纯虚拟函数。
(1)
bool recvTimingResp(PacketPtr pkt):当此端口的从属对等方调用sendTimingResp时调用此函数。如果此对象可以接受响应,则此函数返回true。否则,在将来的某个时候,此对象必须调用sendRespRetry来通知其对等方它现在能够接收响应。
(2)
void recvReqRetry():当对等端口调用sendReqRetry时调用此函数,表示此对象应尝试重新发送以前失败的数据包。
(3)
void recvRangeChange():与上面的sendRangeChange类似,每当对等端口希望通知此对象它接受的地址范围正在更改时,都会调用此函数。此函数通常只在内存系统初始化时调用,而不是在模拟执行时调用。
9、定义MemObject接口
现在我们已经定义了这两种新类型CPUSidePort和MemSidePort,我们可以将这三个端口声明为SimpleMemobj的一部分。我们还需要声明MemObject类中的两个纯虚拟函数getMasterPort和getSlavePort。在初始化阶段,gem5使用这两个函数通过端口将内存对象连接在一起。
class SimpleMemobj : public MemObject
{
private:
<CPUSidePort declaration>
<MemSidePort declaration>
CPUSidePort instPort;
CPUSidePort dataPort;
MemSidePort memPort;
public:
SimpleMemobj(SimpleMemobjParams *params);
BaseMasterPort& getMasterPort(const std::string& if_name,
PortID idx = InvalidPortID) override;
BaseSlavePort& getSlavePort(const std::string& if_name,
PortID idx = InvalidPortID) override;
};
10、实现基本的MemObject函数
(1)
对于SimpleMemobj的构造函数,我们只需调用MemObject构造函数。我们还需要初始化所有端口。每个端口的构造函数都有两个参数:名称和指向其所有者的指针,正如我们在头文件中定义的那样。名称可以是任何字符串,但根据约定,它与Python SimObject文件中的名称相同。
SimpleMemobj::SimpleMemobj(SimpleMemobjParams *params) :
MemObject(params),
instPort(params->name + ".inst_port", this),
dataPort(params->name + ".data_port", this),
memPort(params->name + ".mem_side", this)
{
}
(2)
接下来,我们需要实现接口来获取端口。这个接口由两个函数getMasterPort和getSlavePort组成。这些函数有两个参数。if_name是此对象接口的Python变量名。在主端口的情况下,它将是mem_side,因为这是我们在Python SimObject文件中声明的主端口。
BaseMasterPort &getMasterPort(const std::string &if_name, PortID idx)
:尝试将从端口连接到此对象时调用此函数。if_name是此对象接口的Python变量名。idx是使用矢量端口时的端口号,默认情况下无效。此函数返回对主端口对象的引用。
BaseSlavePort &getSlavePort(const std::string &if_name, PortID idx)
:尝试将主端口连接到此对象时调用此函数。if_name是此对象接口的Python变量名。idx是使用矢量端口时的端口号,默认情况下无效。此函数返回对从端口对象的引用。
为了实现getMasterPort,我们比较if_name并检查它是否是Python SimObject文件中指定的mem_side。如果是,则返回memPort对象。如果没有,则将请求名称传递给父级。但是,如果我们尝试将一个从端口连接到任何其他命名端口,这将是一个错误,因为父类没有定义端口。
BaseMasterPort&
SimpleMemobj::getMasterPort(const std::string& if_name, PortID idx)
{
if (if_name == "mem_side") {
return memPort;
} else {
return MemObject::getMasterPort(if_name, idx);
}
}
为了实现getSlavePort,我们同样检查if_name是否与我们在Python SimObject文件中为从端口定义的任何一个名称匹配。如果名称是“inst_port”,则返回instPort;如果名称是data_port,则返回data port。
BaseSlavePort&
SimpleMemobj::getSlavePort(const std::string& if_name, PortID idx)
{
if (if_name == "inst_port") {
return instPort;
} else if (if_name == "data_port") {
return dataPort;
} else {
return MemObject::getSlavePort(if_name, idx);
}
}
11、实现从端口和主端口功能
从端口和主端口的实现都相对简单。在大多数情况下,每个端口函数只是将信息转发给主内存对象(SimpleMemobj)。
(1)
从两个简单的函数开始,getAddrRanges和recvFunctional只需调用SimpleMemobj。
AddrRangeList
SimpleMemobj::CPUSidePort::getAddrRanges() const
{
return owner->getAddrRanges();
}
void
SimpleMemobj::CPUSidePort::recvFunctional(PacketPtr pkt)
{
return owner->handleFunctional(pkt);
}
(2)
这些函数在simplemmobj中的实现同样简单。这些实现只是将请求传递到内存端。我们可以在这里使用DPRINTF调用来跟踪出于调试目的发生的事情。
void
SimpleMemobj::handleFunctional(PacketPtr pkt)
{
memPort.sendFunctional(pkt);
}
AddrRangeList
SimpleMemobj::getAddrRanges() const
{
DPRINTF(SimpleMemobj, "Sending new ranges\n");
return memPort.getAddrRanges();
}
(3)
同样对于MemSidePort,我们需要实现recvRangeChange并通过SimpleMemobj将请求转发到从端口。
void
SimpleMemobj::MemSidePort::recvRangeChange()
{
owner->sendRangeChange();
}
void
SimpleMemobj::sendRangeChange()
{
instPort.sendRangeChange();
dataPort.sendRangeChange();
}
12、实现接收请求
(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;
}
}
(2)
为了处理对SimpleMemobj的请求,我们首先检查SimpleMemobj是否已经被阻止,等待对另一个请求的响应。如果它被阻塞了,那么我们返回false来向主叫端口发送我们现在不能接受请求的信号。否则,我们将端口标记为阻塞,并将数据包从内存端口发送出去。为此,我们可以在MemSidePort对象中定义一个helper函数,以对SimpleMemobj实现隐藏流控件。我们假设memPort处理所有的流控制,并且总是从handleRequest返回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)
接下来,我们需要在MemSidePort中实现sendPacket函数。此函数将处理流控制,以防其对等从端口无法接受请求。为此,我们需要在MemSidePort中添加一个成员来存储数据包,以防它被阻塞。如果接收者无法接收请求(或响应),发送者有责任存储数据包。
这个函数只是通过调用sendTimingReq函数来发送数据包。如果发送失败,则此对象将数据包存储在blockedPacket成员函数中,以便稍后(在接收到recvReqRetry时)发送数据包。此函数还包含一些防御代码,以确保不存在错误,并且我们从不试图错误地覆盖blockeddepacket变量。
void
SimpleMemobj::MemSidePort::sendPacket(PacketPtr pkt)
{
panic_if(blockedPacket != nullptr, "Should never try to send if blocked!");
if (!sendTimingReq(pkt)) {
blockedPacket = pkt;
}
}
(4)
接下来,我们需要实现代码来重新发送数据包。在这个函数中,我们试图通过调用上面编写的sendPacket函数来重新发送数据包。
void
SimpleMemobj::MemSidePort::recvReqRetry()
{
assert(blockedPacket != nullptr);
PacketPtr pkt = blockedPacket;
blockedPacket = nullptr;
sendPacket(pkt);
}
13、执行接收响应
(1)
响应码路径类似于接收码路径。当MemSidePort
得到响应时,我们通过SimpleMemobj
将响应转发到相应的 CPUSidePort
。
bool
SimpleMemobj::MemSidePort::recvTimingResp(PacketPtr pkt)
{
return owner->handleResponse(pkt);
}
(2)
在SimpleMemobj中,首先,当我们收到响应时,它应该总是被阻塞,因为对象是阻塞的。在将数据包发送回CPU端之前,我们需要标记该对象不再被阻止。这必须在调用“sendTimingResp”之前完成。否则,可能会陷入无限循环,因为主端口在接收响应和发送另一个请求之间可能只有一个调用链。
在解除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;
}
(3)
类似于我们如何在MemSidePort中实现发送数据包的便利函数,我们可以在CPUSidePort中实现发送数据包函数,将响应发送到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;
}
}
(4)
稍后我们将在接收到recvRespRetry
时发送此被阻止的数据包。此函数与上面的recvReqRetry
完全相同,只是尝试重新发送数据包,数据包可能会再次被阻塞。
void
SimpleMemobj::CPUSidePort::recvRespRetry()
{
assert(blockedPacket != nullptr);
PacketPtr pkt = blockedPacket;
blockedPacket = nullptr;
sendPacket(pkt);
}
(5)
最后,我们需要为CPUSidePort
实现额外的函数trySendRetry
。只要SimpleMemobj
可以被解除阻塞,SimpleMemobj
就会调用此函数。trySendRetry
检查是否需要重试,只要SimpleMemobj
在新请求上被阻止,我们就会在recvTimingReq
中标记重试。然后,如果需要重试,则此函数调用sendRetryReq
,后者反过来调用对等主端口(本例中为CPU)上的recvReqRetry
。
void
SimpleMemobj::CPUSidePort::trySendRetry()
{
if (needRetry && blockedPacket == nullptr) {
needRetry = false;
DPRINTF(SimpleMemobj, "Sending retry req for %d\n", id);
sendRetryReq();
}
}
(6)
下图显示了CPUSidePort
、MemSidePort
和SimpleMemobj
之间的关系。此图显示对等端口如何与SimpleMemobj
的实现交互。每个粗体函数都是我们必须实现的函数,而非粗体函数是对等端口的端口接口。颜色突出显示通过对象的一个API路径(例如,接收请求或更新内存范围)。
对于这个简单的内存对象,数据包只是从CPU端转发到内存端。但是,通过修改handleRequest和handleResponse,我们可以创建丰富的特性对象,就像下一部分中的缓存一样。
14、创建一个配置文件
这是实现一个简单内存对象所需的全部代码!在下一部分中,我们将采用这个框架并添加一些缓存逻辑,使这个内存对象成为一个简单的缓存。不过,在这之前,让我们看看配置文件,将SimpleMemobj添加到您的系统中。
此配置文件是在创建简单配置脚本
时基于简单配置文件生成的。但是,我们将实例化一个SimpleMemobj,并将其放置在CPU和内存总线之间,而不是将CPU直接连接到内存总线。
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 = TimingSimpleCPU()
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 Simulator System. http://gem5.org
gem5 is copyrighted software; use the --copyright option for details.
gem5 compiled Jan 5 2017 13:40:18
gem5 started Jan 9 2017 10:17:17
gem5 executing on chinook, pid 5138
command line: build/X86/gem5.opt configs/learning_gem5/part2/simple_memobj.py
Global frequency set at 1000000000000 ticks per second
warn: DRAM device capacity (8192 Mbytes) does not match the address range assigned (512 Mbytes)
0: system.remote_gdb.listener: listening for remote gdb #0 on port 7000
warn: CoherentXBar system.membus has no snooping ports attached!
warn: ClockedObject: More than one power state change request encountered within the same simulation tick
Beginning simulation!
info: Entering event queue @ 0. Starting simulation...
Hello world!
Exiting @ tick 507841000 because target called exit()
如果使用SimpleMemobj调试标志运行,则可以看到来自CPU和来自CPU的所有内存请求和响应。
gem5 Simulator System. http://gem5.org
gem5 is copyrighted software; use the --copyright option for details.
gem5 compiled Jan 5 2017 13:40:18
gem5 started Jan 9 2017 10:18:51
gem5 executing on chinook, pid 5157
command line: build/X86/gem5.opt --debug-flags=SimpleMemobj configs/learning_gem5/part2/simple_memobj.py
Global frequency set at 1000000000000 ticks per second
Beginning simulation!
info: Entering event queue @ 0. Starting simulation...
0: system.memobj: Got request for addr 0x190
77000: system.memobj: Got response for addr 0x190
77000: system.memobj: Got request for addr 0x190
132000: system.memobj: Got response for addr 0x190
132000: system.memobj: Got request for addr 0x190
187000: system.memobj: Got response for addr 0x190
187000: system.memobj: Got request for addr 0x94e30
250000: system.memobj: Got response for addr 0x94e30
250000: system.memobj: Got request for addr 0x190
...