天天看點

【Gem5】在memory system裡建立SimObject

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接口:

【Gem5】在memory system裡建立SimObject

所有的端口接口都需要一個

PacketPtr

作為一個參數。這些函數(

sendTimingReq

,

recvTimingReq

, etc.)接收一個參數

PacketPtr

為了發射一個request的packet,master調用

sendTimingReq

。相對應的,函數

recvTimingReq

在slave中被調用并且參數是同一個

PacketPtr

recvTimingReq

有一個布爾傳回值,這個傳回值直接發回到調用的master。

true

代表packet被slave接收,

false

代表不能被接收并且請求需要過一段時間再次發送。

master或slave在接收到請求或響應時可能處于忙碌狀态。下圖展示了當原始請求發送時,slave繁忙的情形:

【Gem5】在memory system裡建立SimObject

上圖這種情況,slave傳回了false。當master調用了

sendTimingReq

後接收到false,它必須一直等到它的函數

recvReqRetry

被執行。僅僅當這個函數被master調用,才允許重新調用

sendTimingReq

。上圖僅顯示了失敗了一次的情況,其實實際上可能會發生數次。注意:必須由master跟蹤失敗的packet而不是slave(slave不儲存失敗的packet的對應指針)。

同樣,master忙碌時,下圖顯示了這種情況,slave直到接收到

recvRespRetry

後才能調用

sendTimingResp

【Gem5】在memory system裡建立SimObject

3.4 簡單的記憶體對象例子

這個簡單的對象僅僅在CPU端和記憶體端之間實作請求的傳送。下一章節添加邏輯讓它成為一個cache。

下圖就是這樣的一個簡單的memory object所在的系統結構圖:

【Gem5】在memory system裡建立SimObject

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

三者的關系和互動通道:

【Gem5】在memory system裡建立SimObject

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())
           

繼續閱讀