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
2
3
4
5
AddrRangeList
SimpleMemobj::CPUSidePort::getAddrRanges() const
{
return owner->getAddrRanges();
}

2. SimpleMemobj::getAddrRanges

SimpleMemobj同样直接将请求传递给其子对象MemSidePort

1
2
3
4
5
6
7
AddrRangeList
SimpleMemobj::getAddrRanges() const
{
DPRINTF(SimpleMemobj, "Sending new ranges\n");
// Just use the same ranges as whatever is on the memory side.
return memPort.getAddrRanges();
}

MemSidePort.getAddrRanges()函数已经在MemSidePort的父类RequestPort中被实现了,返回地址范围,可直接使用。

$\textcolor[RGB]{100,106,255}{(2)通知内存模型的地址范围发生更改(Mem bus –> CPU)} $

Mem bus发送请求,向CPU通知内存模型的地址范围发生更改,同样此通知也不会阻塞

1. SimpleMemobj::MemSidePort::recvRangeChange

MemSidePort 直接将请求传递给其父对象 SimpleMemobj

1
2
3
4
5
void
SimpleMemobj::MemSidePort::recvRangeChange()
{
owner->sendRangeChange();
}

2. SimpleMemobj::sendRangeChange

SimpleMemobj同样直接将请求传递给其子对象CPUSidePort

1
2
3
4
5
6
void
SimpleMemobj::sendRangeChange()
{
instPort.sendRangeChange();
dataPort.sendRangeChange();
}

CPUSidePort.sendRangeChange函数同样已经在CPUSidePort的父类ResponsePort中被实现了,可直接使用。

$\textcolor[RGB]{250,250,100}{(3)功能请求与响应(CPU –> Mem bus)} $

功能请求是指不改变系统状态的请求,通常用于读取数据或检查系统状态。同样这种请求也不会发生阻塞

这个过程的调用链与获取地址范围大致相同,只不过需要传递数据包指针。

1. SimpleMemobj::CPUSidePort::recvFunctional

CPUSidePort直接将请求传递给其父对象 SimpleMemobj

1
2
3
4
5
6
void
SimpleMemobj::CPUSidePort::recvFunctional(PacketPtr pkt)
{
// Just forward to the memobj.
return owner->handleFunctional(pkt);
}

2. SimpleMemobj::handleFunctional

SimpleMemobj同样直接将请求传递给其子对象MemSidePort

1
2
3
4
5
6
void
SimpleMemobj::handleFunctional(PacketPtr pkt)
{
// Just pass this on to the memory side to handle for now.
memPort.sendFunctional(pkt);
}

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
2
3
4
5
6
7
8
9
10
11
bool
SimpleMemobj::CPUSidePort::recvTimingReq(PacketPtr pkt)
{
// Just forward to the memobj.
if (!owner->handleRequest(pkt)) {
needRetry = true;
return false;
} else {
return true;
}
}

2. SimpleMemobj::handleRequest

来到SimpleMemobj的handleRequest函数。首先检查目前没有在等待响应(被阻塞)

如果没有被阻塞,则将blocked置为true,即进入阻塞状态,并通过子对象MemSidePort的sendPacket函数发送数据包,并返回true;

反之如果被阻塞,直接返回false,拒绝此请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bool
SimpleMemobj::handleRequest(PacketPtr pkt)
{
if (blocked) {
// There is currently an outstanding request. Stall.
return false;
}
DPRINTF(SimpleMemobj, "Got request for addr %#x\n", pkt->getAddr());

// This memobj is now blocked waiting for the response to this packet.
blocked = true;

// Simply forward to the memory port
memPort.sendPacket(pkt);
return true;
}

3. SimpleMemobj::MemSidePort::sendPacket

MemSidePort的sendPacket会调用sendTimingReq函数(在其父类中定义,可直接使用)发送请求数据包给内存总线。如果发送不成功,则将要发送的数据包指针保存下来,准备重发该数据包。

1
2
3
4
5
6
7
8
9
10
11
void
SimpleMemobj::MemSidePort::sendPacket(PacketPtr pkt)
{
// Note: This flow control is very simple since the memobj is blocking.
panic_if(blockedPacket != nullptr, "Should never try to send if blocked!");

// If we can't send the packet across the port, store it for later.
if (!sendTimingReq(pkt)) {
blockedPacket = pkt;
}
}

4. SimpleMemobj::MemSidePort::recvReqRetry

内存总线结束忙态后,会调用MemSidePort类的recvReqRetry邀请MemSidePort重发之前被阻塞的请求数据包,即重发blockedPacket数据包。注意:在重发前,应该一定会有被阻塞的数据包,否则报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
void
SimpleMemobj::MemSidePort::recvReqRetry()
{
// We should have a blocked packet if this function is called.
assert(blockedPacket != nullptr);

// Grab the blocked packet.
PacketPtr pkt = blockedPacket;
blockedPacket = nullptr;

// Try to resend it. It's possible that it fails again.
sendPacket(pkt);
}

$\textcolor[RGB]{100,250,250}{(5)接收定时请求的响应(Mem bus –> CPU)} $

内存总线处理完定时请求后,会向CPU发送该定时请求的响应。类似地,CPU也可能会因为处于忙态而拒绝接收响应,也可能会存在阻塞

1. SimpleMemobj::MemSidePort::recvTimingResp

MemSidePort直接将请求传递给其父对象SimpleMemobj

1
2
3
4
5
6
bool
SimpleMemobj::MemSidePort::recvTimingResp(PacketPtr pkt)
{
// Just forward to the memobj.
return owner->handleResponse(pkt);
}

2. SimpleMemobj::handleResponse

SimpleMemobj处理响应时,首先因为收到了响应所以消除阻塞状态,然后根据数据包的属性确定是发送给指令端口还是数据端口。

结束了阻塞状态以后,会尝试通过trySendRetry()函数让CPU重发未能成功发送的请求(如果有的话)。

注意:在接收响应时,SimpleMemobj应该一定处于阻塞状态,否则报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
bool
SimpleMemobj::handleResponse(PacketPtr pkt)
{
assert(blocked);
DPRINTF(SimpleMemobj, "Got response for addr %#x\n", pkt->getAddr());

// The packet is now done. We're about to put it in the port, no need for
// this object to continue to stall.
// We need to free the resource before sending the packet in case the CPU
// tries to send another request immediately (e.g., in the same callchain).
blocked = false;

// Simply forward to the memory port
if (pkt->req->isInstFetch()) {
instPort.sendPacket(pkt);
} else {
dataPort.sendPacket(pkt);
}

// For each of the cpu ports, if it needs to send a retry, it should do it
// now since this memory object may be unblocked now.
instPort.trySendRetry();
dataPort.trySendRetry();

return true;
}

3. SimpleMemobj::CPUSidePort::sendPacket

CPUSidePort的sendPacket会调用sendTimingReq函数(在其父类中定义,可直接使用)发送响应数据包给CPU。如果发送不成功,则将要发送的数据包指针保存下来,准备重发该数据包。

1
2
3
4
5
6
7
8
9
10
11
12
void
SimpleMemobj::CPUSidePort::sendPacket(PacketPtr pkt)
{
// Note: This flow control is very simple since the memobj is blocking.

panic_if(blockedPacket != nullptr, "Should never try to send if blocked!");

// If we can't send the packet across the port, store it for later.
if (!sendTimingResp(pkt)) {
blockedPacket = pkt;
}
}

4. SimpleMemobj::CPUSidePort::trySendRetry

尝试让CPU重发未能发送的请求

如果needRetry为True,则说明之前CPU有未能发送的请求;如果blockedPacket指针为空,说明SimpleMemobj未处于阻塞状态,则可以通过sendRetryReq函数(父类中已经实现,可直接使用)让CPU重发请求。

1
2
3
4
5
6
7
8
9
10
void
SimpleMemobj::CPUSidePort::trySendRetry()
{
if (needRetry && blockedPacket == nullptr) {
// Only send a retry if the port is now completely free
needRetry = false;
DPRINTF(SimpleMemobj, "Sending retry req for %d\n", id);
sendRetryReq();
}
}

5. SimpleMemobj::CPUSidePort::recvRespRetry

CPU忙态结束以后,会通过调用CPUSidePort类的recvRespRetry函数邀请CPUSidePort重新发送之前被阻塞的响应数据包,即重发blockedPacket数据包。注意:在重发前,应该一定会有被阻塞的数据包,否则报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
void
SimpleMemobj::CPUSidePort::recvRespRetry()
{
// We should have a blocked packet if this function is called.
assert(blockedPacket != nullptr);

// Grab the blocked packet.
PacketPtr pkt = blockedPacket;
blockedPacket = nullptr;

// Try to resend it. It's possible that it fails again.
sendPacket(pkt);
}

五、配置脚本

实例化SimpleMemobj对象,并运行hello world负载的配置脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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())
  1. 在命令行执行以下命令,可以运行模拟系统
1
build/X86/gem5.opt configs/learning_gem5/part2/simple_memobj.py

输出内容出现Hello World则模拟成功。

  1. 在命令行执行以下命令,可以在debug模式下运行模拟系统,由于输出较多,只输出前五十行。
1
build/X86/gem5.opt --debug-flags=SimpleMemobj configs/learning_gem5/part2/simple_memobj.py | head -n 50

输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Beginning 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
126000: system.memobj: Got response for addr 0x190
126000: system.memobj: Got request for addr 0x190
175000: system.memobj: Got response for addr 0x190
175000: system.memobj: Got request for addr 0x94db0
238000: system.memobj: Got response for addr 0x94db0
238000: system.memobj: Got request for addr 0x190
287000: system.memobj: Got response for addr 0x190
287000: system.memobj: Got request for addr 0x198
336000: system.memobj: Got response for addr 0x198
336000: system.memobj: Got request for addr 0x198
385000: system.memobj: Got response for addr 0x198
...