Gem5模拟器学习(四)
本文是官网教程gem5: Creating SimObjects in the memory system的学习笔记。
本节教程是创建一个位于CPU和内存总线之间的阻塞式简单内存对象,主要实现了基本请求的传递。在下一节教程中,会在本节创建的简单内存对象之上增加部分逻辑,使其成为一个非常简单的阻塞单处理器缓存。下图是整个系统的示意图,其中,我们创建的内存对象有两个CPU侧的从端口(slave port)和一个内存总线侧的主端口(master port)。它将实现将请求从CPU传递到内存总线,并将响应从内存总线传递到CPU。
一、主从端口
主端口(master port)和从端口(slave port)是模拟器中创造的概念,用于描述计算机系统中不同组件之间的数据传输关系。在模拟器中用于连接计算机系统中的各种组件。其中,主端口负责发送请求(send req)、接收响应(recv resp);从端口负责接受请求(recv req)、发送响应(send resp),因此,主从端口必须配对使用。
以本模拟系统为例,memory object有两个CPU侧的从端口,用于接收CPU的请求,并向其返回响应;同时有一个mem bus侧的主接口,用于向mem bus发送请求,并接收其响应。
这些端口实现三种不同的存储系统模式:
- 定时模式(timing mode)。唯一的产生正确模拟结果的模式,最常用。
- 原子模式(atomic mode)。
- 功能模式(functional mode)。
其他模式暂时不懂。。。
三种访存模式介绍【Gem5】gem5模拟器中三种访存模式Atomic、Timing、Functional的总结对比_空空7的博客-CSDN博客
二、数据包
在gem5中,端口通过发送数据包(packet)实现交互。数据包由MemReq组成,MemReq是内存请求对象。MemReq保存初始化包的原始请求的信息,例如请求者、地址和请求类型(读、写等)。数据包还有一个MemCmd,它是数据包的当前命令。此命令可以在数据包的整个生命周期中改变(例如,一旦满足内存命令,请求就变成响应)。最常见的MemCmd是ReadReq(读请求)、ReadResp(读响应)、WriteReq(写请求)、WriteResp(写响应)。还有缓存和许多其他命令类型的写回请求(WritebackDirty、WritebackClean)
三、主从交互
在定时模式下,主从端口的交互有以下三种情况,需要理清其函数调用链
(1)正常情况下的主从交互
正常情况下,主机通过调用sendTimingReq函数发送请求,从机的recvTimingReq函数也随之被调用,如果从机目前可以接受此请求,则返回true,表示从机已经接受此次请求。从机接受请求后随即开始处理此请求。
从机处理完请求后,通过调用sendTimingResp函数发送此次请求的响应,类似地,主机的recvTimingResp函数随之被调用,如果主机目前可以接受此响应,则返回true,表示主机已经接受了此次响应。交互结束。
(2)从机忙时的主从交互
以上情况是主从都顺利接收的理想情况,但当从机接受请求或主机接受响应时,它们可能正忙。
下面就是从机忙时主从交互的过程。
从机忙时,从机无法接受主机发送的请求,因此recvTimingReq函数返回false,拒绝接受此次请求。但当从机结束忙态后,会通过调用sendReqRetry函数通知主机,“邀请”主机再次重试发送请求,主机通过recvReqRetry函数接收重试通知后,随机再次发起新的请求。当然,新请求也可能再次因为从机忙而被拒绝。
(3)主机忙时的主从交互
类似地,在主机忙时,主机无法接收从机发送的响应,因此recvTimingResp函数返回false,拒绝接收此次响应。但当主机结束忙态后,会通过调用sendRespRetry函数通知从机,“邀请”从机再次重试发送响应,从机通过recvRespRetry函数接收重试通知后,随机再次发起新的响应。
四、SimpleMemobj主从端口函数实现
在本节的简单内存对象(SimpleMemobj)下定义了两个嵌套类CPUSidePort和MemSidePort,它们分别继承自ResponsePort/SlavePort和RequstPort/MasterPort,即从端口和主端口。
- SimpleMemobj类的成员变量
- CPU侧从端口 CPUSidePort instPort; CPUSidePort dataPort;
- 内存总线侧主端口 MemSidePort memPort;
- 阻塞标志 目前是否正在阻塞等待一个响应 bool blocked;
- CPUSidePort类的成员变量
- 父对象指针(即SimpleMemobj对象) SimpleMemobj *owner;
- 是否需要重发 CPU试图发送请求给端口,但被拒绝的情况下,需要记录一下存在这种情况,端口在结束忙态后会通知CPU重发。bool needRetry;
- 被阻塞的数据包指针 该端口试图给CPU发送响应,但被CPU拒绝,需要暂存这个数据包。PacketPtr blockedPacket;
- MemSidePort类的成员变量
- 父对象指针(即SimpleMemobj对象) SimpleMemobj *owner;
- 被阻塞的数据包指针 该端口试图给主存发送请求,但被主存拒绝,需要暂存这个数据包。PacketPtr blockedPacket;
各类的成员函数如下图,其中,加粗函数为必须实现的函数,未加粗的函数为在父类中已经实现的函数。
下面按上图顺序分别对五个函数调用链进行梳理
$\textcolor[RGB]{250,106,106}{(1)获取内存模型的地址范围 (CPU –> Mem bus)}$
CPU 发送请求,查询内存模型的地址范围,并返回一个 AddrRangeList 类型的值。这种查询请求不存在阻塞情况。
1. SimpleMemobj::CPUSidePort::getAddrRanges
CPUSidePort直接将请求传递给其父对象SimpleMemobj
1 | AddrRangeList |
2. SimpleMemobj::getAddrRanges
SimpleMemobj同样直接将请求传递给其子对象MemSidePort
1 | AddrRangeList |
MemSidePort.getAddrRanges()函数已经在MemSidePort的父类RequestPort中被实现了,返回地址范围,可直接使用。
$\textcolor[RGB]{100,106,255}{(2)通知内存模型的地址范围发生更改(Mem bus –> CPU)} $
Mem bus发送请求,向CPU通知内存模型的地址范围发生更改,同样此通知也不会阻塞。
1. SimpleMemobj::MemSidePort::recvRangeChange
MemSidePort 直接将请求传递给其父对象 SimpleMemobj
1 | void |
2. SimpleMemobj::sendRangeChange
SimpleMemobj同样直接将请求传递给其子对象CPUSidePort
1 | void |
CPUSidePort.sendRangeChange函数同样已经在CPUSidePort的父类ResponsePort中被实现了,可直接使用。
$\textcolor[RGB]{250,250,100}{(3)功能请求与响应(CPU –> Mem bus)} $
功能请求是指不改变系统状态的请求,通常用于读取数据或检查系统状态。同样这种请求也不会发生阻塞。
这个过程的调用链与获取地址范围大致相同,只不过需要传递数据包指针。
1. SimpleMemobj::CPUSidePort::recvFunctional
CPUSidePort直接将请求传递给其父对象 SimpleMemobj
1 | void |
2. SimpleMemobj::handleFunctional
SimpleMemobj同样直接将请求传递给其子对象MemSidePort
1 | void |
MemSidePort.sendFunctional()函数已经在MemSidePort的父类RequestPort中被实现了,可直接使用。
$\textcolor[RGB]{250,100,250}{(4)发送定时请求(CPU –> Mem bus)} $
定时请求是指需要等待一段时间后才能完成的请求,通常用于写入数据或执行耗时操作。由于CPU发送请求时,mem bus可能尚未处理完上一次请求,处于忙态,无法接收此次请求,因此这个过程可能会发生阻塞。
1. SimpleMemobj::CPUSidePort::recvTimingReq
CPUSidePort尝试通过父对象 SimpleMemobj的handleRequest函数发送定时请求
如果成功,返回true;
如果失败,将needRetry置为true并返回false,该请求被阻止,CPU在将来某个时候需要发送一个重试(见SimpleMemobj::CPUSidePort::trySendRetry()函数)。
1 | bool |
2. SimpleMemobj::handleRequest
来到SimpleMemobj的handleRequest函数。首先检查目前没有在等待响应(被阻塞)
如果没有被阻塞,则将blocked置为true,即进入阻塞状态,并通过子对象MemSidePort的sendPacket函数发送数据包,并返回true;
反之如果被阻塞,直接返回false,拒绝此请求。
1 | bool |
3. SimpleMemobj::MemSidePort::sendPacket
MemSidePort的sendPacket会调用sendTimingReq函数(在其父类中定义,可直接使用)发送请求数据包给内存总线。如果发送不成功,则将要发送的数据包指针保存下来,准备重发该数据包。
1 | void |
4. SimpleMemobj::MemSidePort::recvReqRetry
内存总线结束忙态后,会调用MemSidePort类的recvReqRetry邀请MemSidePort重发之前被阻塞的请求数据包,即重发blockedPacket数据包。注意:在重发前,应该一定会有被阻塞的数据包,否则报错。
1 | void |
$\textcolor[RGB]{100,250,250}{(5)接收定时请求的响应(Mem bus –> CPU)} $
内存总线处理完定时请求后,会向CPU发送该定时请求的响应。类似地,CPU也可能会因为处于忙态而拒绝接收响应,也可能会存在阻塞
1. SimpleMemobj::MemSidePort::recvTimingResp
MemSidePort直接将请求传递给其父对象SimpleMemobj
1 | bool |
2. SimpleMemobj::handleResponse
SimpleMemobj处理响应时,首先因为收到了响应所以消除阻塞状态,然后根据数据包的属性确定是发送给指令端口还是数据端口。
结束了阻塞状态以后,会尝试通过trySendRetry()函数让CPU重发未能成功发送的请求(如果有的话)。
注意:在接收响应时,SimpleMemobj应该一定处于阻塞状态,否则报错。
1 | bool |
3. SimpleMemobj::CPUSidePort::sendPacket
CPUSidePort的sendPacket会调用sendTimingReq函数(在其父类中定义,可直接使用)发送响应数据包给CPU。如果发送不成功,则将要发送的数据包指针保存下来,准备重发该数据包。
1 | void |
4. SimpleMemobj::CPUSidePort::trySendRetry
尝试让CPU重发未能发送的请求
如果needRetry为True,则说明之前CPU有未能发送的请求;如果blockedPacket指针为空,说明SimpleMemobj未处于阻塞状态,则可以通过sendRetryReq函数(父类中已经实现,可直接使用)让CPU重发请求。
1 | void |
5. SimpleMemobj::CPUSidePort::recvRespRetry
CPU忙态结束以后,会通过调用CPUSidePort类的recvRespRetry函数邀请CPUSidePort重新发送之前被阻塞的响应数据包,即重发blockedPacket数据包。注意:在重发前,应该一定会有被阻塞的数据包,否则报错。
1 | void |
五、配置脚本
实例化SimpleMemobj对象,并运行hello world负载的配置脚本如下:
1 | import m5 |
- 在命令行执行以下命令,可以运行模拟系统
1 | build/X86/gem5.opt configs/learning_gem5/part2/simple_memobj.py |
输出内容出现Hello World则模拟成功。
- 在命令行执行以下命令,可以在debug模式下运行模拟系统,由于输出较多,只输出前五十行。
1 | build/X86/gem5.opt --debug-flags=SimpleMemobj configs/learning_gem5/part2/simple_memobj.py | head -n 50 |
输出如下:
1 | Beginning simulation! |