目录
一、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 port)和从端口(slave port)。所有的内存对象都通过端口连接在一起,这些端口在内存对象之间提供一个严格的接口。
端口可以实现三种不同的内存系统模式:时序(timing), 原子(atomic)和功能(functional)。
原子模式(Atomic mode):在原子模式下,内存系统操作按照顺序依次执行,没有并发事件发生。使用该模式的主要目的是加快仿真速度并预热仿真器。通过将所有内存请求串行执行,避免事件的并发处理和同步开销。
功能模式(Functional mode):也可以被描述为调试模式,是用于从主机上读取数据到模拟器内存中。
例如:功能模式用于将主机中的process.cmd中的二进制文件加载到模拟系统的内存中,以便模拟系统可以访问它。在读取时,功能访问应返回最新的数据,无论数据位于何处,并且在写入时应更新所有可能的有效数据(例如,在具有缓存的系统中,可能存在多个具有相同地址的有效缓存块)
在gem5中,数据包(Packet)通过端口进行传输。一个数据包由一个内存请求对象(MemReq)组成。内存请求对象(MemReq)保存了关于发起该数据包(Packet)的原始请求的信息,例如请求者、地址和请求类型(读取、写入等)。
数据包还有一个内存命令(MemCmd),它表示数据包的当前命令。该命令在数据包的生命周期中可以发生变化(例如,一旦内存命令满足,请求就会转变为响应)。最常见的内存命令包括ReadReq(读取请求)、ReadResp(读取响应)、WriteReq(写入请求)、WriteResp(写入响应)。还有用于缓存的写回请求(WritebackDirty、WritebackClean)和许多其他命令类型。
内存命令详细解释。
ReadReq和ReadResp用于在CPU和内存之间进行读取数据的请求和响应;WriteReq和WriteResp用于在CPU和内存之间进行写入数据的请求和响应。读取请求和写入请求是CPU向内存发送的命令,而读取响应和写入响应是内存对象向CPU发送的命令,用于确认请求的执行和数据的传输。
数据包可以保存请求的数据(写操作),或者保存指向数据的指针(读操作)。在创建数据包时,可以选择数据是动态的(显式分配和释放)还是静态的(由数据包对象分配和释放)。
最后,数据包在经典缓存中被用作跟踪一致性的单位。因此,数据包代码的大部分是针对经典缓存一致性协议的。然而,在gem5中,数据包用于所有内存对象之间的通信,即使它们与一致性没有直接关系(例如DRAM控制器和CPU模型)。
所有端口接口函数都接受一个数据包(Packet)指针作为参数。由于该指针非常常见,gem5中包含了一个typedef:PacketPtr。【用来作为一次请求是否完成的根据】
在Gem5中,有两种类型的端口:主端口(Master Port)和从端口(Slave Port)。要实现一个内存对象,都需要实现至少一种类型的端口。主端口用于向内存对象发送读取和写入请求,从端口用于接收来自其他组件(例如CPU)的读取和写入请求。
为此,可以创建一个新的类,继承自MasterPort或SlavePort,用于主端口和从端口。
下面的图示展示了主端口和从端口之间最简单的交互方式。该图展示了时序模式下的交互。其他模式则更简单,并且在主端口和从端口之间使用简单的调用链。
所有的端口接口都要求以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。因此,该请求的交互过程完成了。
在这种情况下,从设备在 recvTimingReq 函数中返回 false【说明此时从设备在忙,没能及时相应主设备的请求】。当主设备在调用 sendTimingReq 后收到 false 时,它必须等待直到执行 recvReqRetry 函数【主设备需要等待从设备发出的信号,而不是持续等待,而从设备会主动发送sendReqRetry信号,通知主设备可以重新尝试发送请求】。只有在调用该函数之后,主设备才能重新尝试调用 sendTimingRequest【相当于第一次请求没响应,再请求一次】。上述图示展示了定时请求失败一次的情况,但它可能会失败任意次数。
注意:跟踪失败的数据包是主设备要完成的事情,而不是从设备的任务。从设备不会保留失败的数据包指针。【也就是说从设备没有记忆,不会记录失败的数据包】
类似于上述,当主设备在从设备尝试发送响应时忙于其他任务的情况【主设备忙】。在这种情况下,从设备在接收到 recvRespRetry 之前无法调用 sendTimingResp。
在这两种情况下,重试代码路径可以是一个单一的调用堆栈。例如,当主设备调用sendRespRetry时,recvTimingReq也可以在同一个调用堆栈中被调用。因此,很容易错误地创建无限递归错误或其他错误。重要的是,在内存对象发送重试之前,它在那一刻准备好接受另一个数据包。
通俗理解:一些函数的调用会在同一个调用栈中完成,由于代码路径的连续性,可能创建无限递归或其他错误,导致程序陷入无限循环,或者产生其他不正确的行为。所以,从设备在发送sendReqRetry信号之前,就要做好处理下一个请求的准备。
在本节中,将构建一个简单的内存对象。
它将仅仅将请求从 CPU 端(a simple CPU)传递到内存端(a simple memory bus)。它具有一个主设备端口(master port),用于向内存总线(the memory bus)发送请求,并具有两个 CPU 端口(two cpu-side ports),用于 CPU 的指令(instruction port)和数据缓存端口(data cache port)。
创建一个 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 函数时,需要使用这些名称。
构造文件中声明SimObject 的 Python 文件。名称:SConscript
Import('*')
SimObject('SimpleMemobj.py')
Source('simple_memobj.cc')
DebugFlag('SimpleMemobj', "For Learning gem5 Part 2.")
为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文件,但因为指定了路径,编译过程会成功,并且引用的头文件将在构建后的可执行文件中正确被解析。【可能有些绕,多读几遍】
这部分是在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;
};
这个对象需要定义五个函数。
此对象还有一个成员变量,即它的所有者,因此它可以在该对象上调用函数。
这部分也是在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;
};
这个类只有三个纯虚函数。
上面已经定义了两种新类型CPUSidePort 和 MemSidePort,将它们作为 SimpleMemobj 的一部分来声明三个端口。还需要在 SimObject 类中声明纯虚函数 getPort。在初始化阶段,gem5 使用这个函数通过端口将内存对象连接在一起。
class SimpleMemobj : public SimObject
{
private:
CPUSidePort instPort;
CPUSidePort dataPort;
MemSidePort memPort;
public:
SimpleMemobj(SimpleMemobjParams *params);
Port &getPort(const std::string &if_name,
PortID idx=InvalidPortID) override;
};
名称: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);
}
}
主从端口的实现都比较简单,大多数情况下,每个端口函数都是将信息转发给主内存对象(SimpleMemobj)。
它们只是调用 SimpleMemobj 的相应函数。
AddrRangeList
SimpleMemobj::CPUSidePort::getAddrRanges() const
{
return owner->getAddrRanges();
}
void
SimpleMemobj::CPUSidePort::recvFunctional(PacketPtr pkt)
{
return owner->handleFunctional(pkt);
}
将请求传递到内存端,使用 DPRINTF 调用来跟踪调试目的的操作情况。
void
SimpleMemobj::handleFunctional(PacketPtr pkt)
{
memPort.sendFunctional(pkt);
}
AddrRangeList
SimpleMemobj::getAddrRanges() const
{
DPRINTF(SimpleMemobj, "Sending new ranges\n");
return memPort.getAddrRanges();
}
对于 MemSidePort,需要实现 recvRangeChange 并通过 SimpleMemobj 将请求转发到从设备端口。
void
SimpleMemobj::MemSidePort::recvRangeChange()
{
owner->sendRangeChange();
}
void
SimpleMemobj::sendRangeChange()
{
instPort.sendRangeChange();
dataPort.sendRangeChange();
}
需要检查 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,因为我们成功消耗了请求。
blocked
为true,表示SimpleMemobj当前被阻塞,有其他请求正在处理中,那么函数会返回false,表示无法接受新的请求。blocked
标记为true,表示SimpleMemobj被阻塞,然后通过memPort
发送数据包(pkt
)。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;
}
需要在 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;
}
}
实现重新发送数据包。在这个函数里,可以直接调用上述的sendPacket 函数重新发送数据包。
void
SimpleMemobj::MemSidePort::recvReqRetry()
{
assert(blockedPacket != nullptr);
PacketPtr pkt = blockedPacket;
blockedPacket = nullptr;
sendPacket(pkt);
}
响应请求部分和接受请求类似。
当 MemSidePort 收到响应时,通过 SimpleMemobj 将响应转发到相应的 CPUSidePort。
bool
SimpleMemobj::MemSidePort::recvTimingResp(PacketPtr pkt)
{
return owner->handleResponse(pkt);
}
在 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;
}
}
接收到 recvRespRetry 时,将重新发送这个被阻塞的数据包。这个函数与上面的 recvReqRetry 完全相同,只是简单地尝试重新发送数据包,它可能再次被阻塞。
void
SimpleMemobj::CPUSidePort::recvRespRetry()
{
assert(blockedPacket != nullptr);
PacketPtr pkt = blockedPacket;
blockedPacket = nullptr;
sendPacket(pkt);
}
最后,需要在 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();
}
}
上述的函数都是在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
文件名: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
变量中。
最后,打印仿真结束的信息,包括仿真结束时的时钟周期数和结束的原因。
build/X86/gem5.opt configs/learning_gem5/part2/simple_cache.py
build/X86/gem5.opt --debug-flags=SimpleMemobj configs/learning_gem5/part2/simple_memobj.py
使用乱序 CPU,可能会看到不同的地址流,因为它允许同时存在多个内存请求。
在使用乱序 CPU 时,由于 SimpleMemobj 是阻塞的,可能会出现很多停顿。
最后说明:
上述是个人比较肤浅的学习总结,大多是直接翻译英文教程,其中有一些英文逻辑理解不太清晰的我结合自己个人理解又加了一些内容,欢迎大家提出问题,共同探讨。