3 在memory system裡建立SimObject
在本章節,我們建立一個簡單對象在CPU和記憶體總線之間。下一個章節添加邏輯讓它成為一個簡單的阻塞式單處理器緩存。
3.1 Gem5主端口和從端口
深入記憶體對象之前,應首先了解gem5的主從端口。因為所有的memory對象都是通過這些端口連接配接一起的。
這些端口實作了3種不同的memory system mode:timing,atomic,functional。其中最重要的mode是timing mode。因為timing mode是唯一生成正确模拟結果的模式。其它模式一般用在特殊的情形。
atomic mode對于轉發(fastforwarding)模拟和加速模拟器來講比較有用。這個模式假設在memory system中沒有事件(event)會發生。相反地,所有記憶體請求都是通過一個長長的調用鍊(callchain)執行。
functional mode更好的一個說法是debug mode。
3.2 Packets
Gem5中,packets通過接口發送。一個Packet由記憶體請求對象
MemReq
組成。
MemReq
儲存一些原始請求資訊諸如請求者、位址、請求類型(read or write)。
Packet也有一個
MemCmd
,這是資料包的目前指令。該指令可以在資料包的整個生命周期中變化(比如當存儲器指令滿足了,請求變成相應)。常見的
MemCmd
包括
ReadReq
讀取請求,
ReadResp
讀取相應,
WriteReq
寫入請求,
WriteResp
寫入響應。還有緩存和許多其它指令類型的寫回請求(
WritebackDirty
,
WritebackClean
)。
Packet還保留請求的資料或指向資料的指針。
最後資料包被用來在classic cache中作為跟蹤一緻性的單元。是以大多數packet代碼都是classic cache一緻性協定所特有的。然而,packet用于gem5中記憶體對象之間的所有通信,即使他們不涉及一緻性(比如DRAM控制器和CPU型号)。
所有端口的接口函數都接受一個Packet指針作為參數。
3.3 Port interface
Gem5中有兩種類型的端口:主端口和從端口。每當你實作一個記憶體對象的時候,你要至少實作這些類型的端口之一(主or從)。要這麼做,需要建立一個新的類繼承自
MasterPort
或
SlavePort
。主接口發送請求,從接口接收請求。
下圖是一個簡單的master-slave接口:

所有的端口接口都需要一個
PacketPtr
作為一個參數。這些函數(
sendTimingReq
,
recvTimingReq
, etc.)接收一個參數
PacketPtr
。
為了發射一個request的packet,master調用
sendTimingReq
。相對應的,函數
recvTimingReq
在slave中被調用并且參數是同一個
PacketPtr
。
recvTimingReq
有一個布爾傳回值,這個傳回值直接發回到調用的master。
true
代表packet被slave接收,
false
代表不能被接收并且請求需要過一段時間再次發送。
master或slave在接收到請求或響應時可能處于忙碌狀态。下圖展示了當原始請求發送時,slave繁忙的情形:
上圖這種情況,slave傳回了false。當master調用了
sendTimingReq
後接收到false,它必須一直等到它的函數
recvReqRetry
被執行。僅僅當這個函數被master調用,才允許重新調用
sendTimingReq
。上圖僅顯示了失敗了一次的情況,其實實際上可能會發生數次。注意:必須由master跟蹤失敗的packet而不是slave(slave不儲存失敗的packet的對應指針)。
同樣,master忙碌時,下圖顯示了這種情況,slave直到接收到
recvRespRetry
後才能調用
sendTimingResp
:
3.4 簡單的記憶體對象例子
這個簡單的對象僅僅在CPU端和記憶體端之間實作請求的傳送。下一章節添加邏輯讓它成為一個cache。
下圖就是這樣的一個簡單的memory object所在的系統結構圖:
1.聲明SimObject
就像我們之前建立一個簡單的SimObject一樣。第一步就是建立一個SimObject的Python檔案。我們将會調用這個記憶體對象并且建立這樣的python檔案
SimpleMemobj.py
在
src/learning_gem5/simple_memobj
下。
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
類有兩個純粹的虛函數
getMasterPort
and
getSlavePort
。
這個對象有3個端口,兩個端口是和CPU的指令和資料端口連接配接,還有一個是和memory bus連接配接。這些端口都沒有預設值。
當然别忘了建立SConscript檔案并聲明這個python檔案。
2.定義SimpleMemobj類
現在我們建立一個頭檔案
SimpleMemobj
.
class SimpleMemobj : public MemObject
{
private:
public:
/** constructor
*/
SimpleMemobj(SimpleMemobjParams *params);
};
3.定義一個slave port類型
現在需要為兩種端口定義類:CPU端口和memory端口。為此我們要在
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個函數:
AddrRangeList getAddrRanges()
這個函數傳回所有者負責的非重疊位址範圍的清單。
Tick recvAtomic(PacketPtr pkt)
這是當CPU嘗試進行原子記憶體通路時調用的函數。
void recvFunctional(PacketPtr pkt)
當CPU進行功能通路時調用。
bool recvTimingReq(PacketPtr pkt)
當這個端口對應的一端調用
sendTimingReq
時,這個函數被調用。
void recvRespRetry()
當這個端口對應的一端調用
sendRespRetry
時,這個函數被調用。
4 定義一個master port類型
下面定義master port的類型。這将會是記憶體的那一邊端口,負責從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;
};
以上這個類僅僅有3個我們需要重寫的虛函數:
bool recvTimingResp(PacketPtr pkt)¶
當對應的從端口調用
sendTimingResp
時,調用此函數。
void recvReqRetry()
上面類似
void recvRangeChange()
上面類似
5 定義MemObject接口
現在我們定義了兩個類型了:
CPUSidePort
and
MemSidePort
,我們可以聲明3個接口作為
SimpleMemobj
的一部分。我們還需要在
MemObject
類裡聲明兩個純虛函數:
getMasterPort
and
getSlavePort
。
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;
};
6 實作基本的MemObject功能
對于
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)
{
}
然後實作兩個函數以擷取端口:
getMasterPort
and
getSlavePort
。這些函數有兩個參數,
if_name
是此對象的接口的Python變量名稱。當主端口時,它将成為
mem_side
,因為我們在Python SimObject檔案聲明它作為
MasterPort
。
BaseMasterPort&getMasterPort(const std :: string&if_name,PortID idx )
嘗試将從端口連接配接到此對象時會調用此函數。if_name是此對象的接口的Python變量名稱。idx是使用向量端口時的端口号,并且是InvalidPortID預設設定。該函數傳回對主端口對象的引用。
BaseSlavePort&getSlavePort(const std :: string&if_name,PortID idx )
和上面相似。
為了實作
getMasterPort
,我們比較
if_name
并檢查它是否是Python SimObject檔案中指定的
mem_side
。如果是,那麼我們傳回
memPort
對象。如果不是,那麼我們将請求名稱傳遞給parent。但是,如果我們嘗試将從端口連接配接到任何其他指定過的端口時,将會報錯,因為父類沒有定義端口。
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
傳回的資料端口。
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);
}
}
7 實作主從端口的功能
實作主or從端口非常相似。大部分情形,每一個端口函數僅轉發資訊到main memory對象(SimpleMemobj)。
從兩個簡單的函數開始,
getAddrRanges
和
recvFunctional
簡單地調用
SimpleMemobj
。
AddrRangeList
SimpleMemobj::CPUSidePort::getAddrRanges() const
{
return owner->getAddrRanges();
}
void
SimpleMemobj::CPUSidePort::recvFunctional(PacketPtr pkt)
{
return owner->handleFunctional(pkt);
}
這些函數在
SimpleMemobj
的實作很相似,僅僅把request傳到memory side。
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();
}
8 實作receiving request
對于接收這部分稍微複雜一點,因為要檢查
SimpleMemobj
是否可以接收這個request。
SimpleMemobj
是一個非常簡單的阻塞結構,一次我們隻允許一個單一的request發生。是以request在發生時接收到了一個request,隻能阻塞第二個request。
為了簡化實作,
CPUSidePort
存儲port interface的所有控制流資訊。是以,我們添加另一個成員變量
needRetry
到
CPUSidePort
裡。這個變量是布爾值,存儲是否當
SimpleMemobj
空閑時需要發送retry。
bool
SimpleMemobj::CPUSidePort::recvTimingReq(PacketPtr pkt)
{
if (!owner->handleRequest(pkt)) {
needRetry = true;
return false;
} else {
return true;
}
}
在處理request
SimpleMemobj
時,先檢查是否
SimpleMemobj
因為等待另一個請求的響應時被阻塞,如果被阻塞,那麼我們傳回false通知master port。否則,将端口标記為blocked,并将packet發送到memory side。
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
功能。當其對等的那個從端口無法接收request時,這個函數負責處理控制流。添加一個成員到
MemSidePort
來存儲packet以防止被阻塞。如果接收方無法接收request or response,發送方負責存儲packet。
void
SimpleMemobj::MemSidePort::sendPacket(PacketPtr pkt)
{
panic_if(blockedPacket != nullptr, "Should never try to send if blocked!");
if (!sendTimingReq(pkt)) {
blockedPacket = pkt;
}
}
下面是重新發送packet的代碼:
void
SimpleMemobj::MemSidePort::recvReqRetry()
{
assert(blockedPacket != nullptr);
PacketPtr pkt = blockedPacket;
blockedPacket = nullptr;
sendPacket(pkt);
}
9 實作receiving response
相應和接收請求類似,如果
MemSidePort
擷取了一個response,轉發這個response通過
SimpleMemobj
到
CPUSidePort
。
bool
SimpleMemobj::MemSidePort::recvTimingResp(PacketPtr pkt)
{
return owner->handleResponse(pkt);
}
在
SimpleMemobj
裡,首先,當它接收到了一個response後就必須一直阻塞。發送packet到CPU side之前,應該标記object不再被阻塞。 否則可能導緻後面調用了
sendTimingResp
後進入無限循環。
在解除
SimpleMemobj
的阻塞前,我們檢查是否packet是一個指令or一條資料,然後發到合适的端口。最後通知CPU端的端口他們可以發送failed掉的request了。
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;
}
類似于我們在
MemSidePort
實作發送資料包的功能,我們可以實作一個
sendPacket
功能來将響應發送到CPU端。這個函數調用
sendTimingResp
。如果此呼叫失敗并且對等端口目前被阻止,那麼我們将存儲稍後要發送的資料包。
void
SimpleMemobj::CPUSidePort::sendPacket(PacketPtr pkt)
{
panic_if(blockedPacket != nullptr, "Should never try to send if blocked!");
if (!sendTimingResp(pkt)) {
blockedPacket = pkt;
}
}
當我們收到
recvRespRetry
時,應稍後發送阻塞的packet。該功能與
recvReqRetry
完全相同,隻是試圖重新發送資料包,可能會再次阻止該資料包。
void
SimpleMemobj::CPUSidePort::recvRespRetry()
{
assert(blockedPacket != nullptr);
PacketPtr pkt = blockedPacket;
blockedPacket = nullptr;
sendPacket(pkt);
}
最後,我們需要實作額外的功能
trySendRetry
:
void
SimpleMemobj::CPUSidePort::trySendRetry()
{
if (needRetry && blockedPacket == nullptr) {
needRetry = false;
DPRINTF(SimpleMemobj, "Sending retry req for %d\n", id);
sendRetryReq();
}
}
下圖展示了
CPUSidePort
,
MemSidePort
, and
SimpleMemobj
三者的關系和互動通道:
10 建立一個Config檔案
我們執行個體化一個
SimpleMemobj
将它放在CPU和Memory bus之間:
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[].pio = system.membus.master
system.cpu.interrupts[].int_master = system.membus.slave
system.cpu.interrupts[].int_slave = system.membus.master
system.mem_ctrl = DDR3_1600_8x8()
system.mem_ctrl.range = system.mem_ranges[]
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())