gem5を構成するSimObjectを追加するためのチュートリアルをやってみる。
SimpleMemobj
を使った簡単なブリッジの実装が良く分からなかったので、もう一度よく読みなおしてみることにした。
- gem5記事一覧インデックス
単純なメモリ・オブジェクトの例
本節では、単純なメモリ・オブジェクトを作る。以下の図の通り、CPUからはicacheとdcacheを経由して接続されており、外部にリクエストを転送するためのmem_sideポートが宣言されている。
- mem_side : メモリ・バスにリクエストを送るためのマスタ・ポート
- dcache_port : CPUからのリクエストを受け付けるためのスレーブ・ポート
- icache_port : CPUからのリクエストを受け付けるためのスレーブ・ポート
次の章simplecache-chapterでは、このオブジェクトをキャッシュにするためのロジックを追加するが、今回は単なるブリッジとしての役割を持たせるということにする。
大きな手順は以下のとおりである:
- Python上でSimObjectを宣言する。
- 必要なポートを定義する
- SConscriptファイルを作成し、クラスをビルド対象として登録する
- C++側のSimpleMemobjクラスを定義する
- ヘッダファイルを作成する
- CPU側のポート(スレーブ・ポート)を定義する
- メモリ・バス側のポート(マスタ・ポート)を定義する
getPort()
によるポートのインタフェースを作成する
- slaveとmasterのポートに必要な関数を実装する
getAddrRanges()
recvAtomic()
recvFunctional()
recvRangeChange()
- CPU側からリクエストを受け付ける際のコードを記述する
recvTimingReq()
recvRespRetry()
- これらには、メモリ・バス側に転送できない場合のフロー制御の実装も含まれる
- メモリ・バス側からレスポンスを受け付ける際のコードを記述する
recvTimingResp()
recvReqRetry()
- これらには、CPU側に転送できない場合のフロー制御の実装も含まれる
SimObjectを宣言する
まずはPython上で操作をするためのC++ラッパであるSimpleMemobj
クラスを作成する。これまでと同様に、SimObjectを継承させて作成する。
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
がある。
このオブジェクトのパラメータは3つのポートである。CPUが命令ポートとデータ・ポートに接続するための2つのポートと、メモリー・バスに接続するためのポートだ。これらのポートにはデフォルト値はなく、簡単な説明しかない。
このSimpleMemobjクラスには、3つのメンバ変数が追加されていることに注意する。それぞれが上記で説明したポートに相当している。
- inst_port : CPU側のスレーブ・ポート
- data_port : CPU側のスレーブ・ポート
- mem_side : メモリ側のマスタ・ポート
これらのポートは、SimObject
クラスで継承すべきgetPort()
を実装する際に覚えておく必要がある。
さらに、SimpleMemobj
をビルド対象として追加するためのSConscriptを追加する必要があることも忘れないこと。
C++側でSimpleMemobjクラスを定義する
次に、PythonにラップしてもらうためのC++側でのSimpleMemobj
のヘッダファイルを作成する。
まずはクラスの外観とコンストラクタのみを作成する。SimpleMemobj
クラスは、SimObject
を継承する形で作成する。
#include "mem/port.hh" #include "params/SimpleMemobj.hh" #include "sim/sim_object.hh" class SimpleMemobj : public SimObject { private: public: /** constructor */ SimpleMemobj(SimpleMemobjParams *params); };
slaveポート型を定義する
次に、2種類のポート(CPU側ポートとメモリー側ポート)のクラスを定義する必要がある。他のオブジェクトがこれらのクラスを使うことはないので、SimpleMemobj
クラスの中でこれらのクラスを宣言する。
まず、スレーブポート(CPU側ポート)から始めよう。SlavePortクラスを継承する。以下は、SlavePort
クラスのすべての純粋仮想関数をオーバーライドするために必要なコードである。
まず、スレーブ・ポート(CPU側のポート)から定義する。SlavePortクラスというのがあり、これを継承する形で定義する。SlavePortクラスの全ての純粋仮想関数をオーバ・ライドする必要がある。
// SimpleMemobjクラス内でCPUSidePortを宣言する 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: // これらはSlavePortクラスで定義されている // 純粋仮想関数 Tick recvAtomic(PacketPtr pkt) override { panic("recvAtomic unimpl."); } void recvFunctional(PacketPtr pkt) override; bool recvTimingReq(PacketPtr pkt) override; void recvRespRetry() override; };
このオブジェクトでは、5つの関数を定義する必要がある。
getAddrRanges()
recvAtomic()
recvFunctional()
recvTimingReq()
recvRespRetry()
masterポート型を定義する
次に、マスタ・ポート・タイプを定義する必要がある。これは、CPU側からのリクエストをメモリ・システムの残りの部分に転送するメモリ側のポートになる。
同様に、SimpleMemobjクラスの下に定義し、MasterPortというクラスを継承することで作成する。
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つ用意されている。
recvTimingResp()
recvReqRetry()
recvRangeChange()
SimObjectインタフェースを定義する
ここまでで、2つの新しい型CPUSidePort
とMemSidePort
を定義し、SimpleMemobj
で使用する3つのポートを宣言できるようになった。SimObjectクラスの純粋仮想関数についても定義する必要がある。
これがgetPort()
である。この関数はポートを通じてメモリオブジェクトを接続する初期化フェーズで実行される。
class SimpleMemobj : public SimObject { private: <CPUSidePort クラスの定義> <MemSidePort クラスの定義> // 実際のCPUSidePort/MemSidePortのインスタンス CPUSidePort instPort; CPUSidePort dataPort; MemSidePort memPort; public: SimpleMemobj(SimpleMemobjParams *params); // 新たに実装されるgetPort() // これは必ずオーバライドしなければならない Port &getPort(const std::string &if_name, PortID idx=InvalidPortID) override; };
基本的なSimObject関数の実装
SimpleMemobjのコンストラクタの実装
SimpleMemobj
のコンストラクタでは、単純にSimObjectコンストラクタが呼ばれるだけである。また、すべてのポートを初期化する必要がある。それぞれのポートのコンストラクタは2つの引数を持つ:
- ポートの名前
- その所持者のポインタ(
this
)
これらはヘッダファイルに定義したものを使用する。名前はどのような文字列でも良いが、利便性のために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()
の実装
getPort()関数の目的は、オブジェクトから、ポートの名前を使ってその内部のポートインスタンスを呼び出すためのものである。
実装は極めて単純で、引数のif_name
を比較してmem_side
という名前であるかをチェックする。もしそうであれば、memPort
オブジェクトを返す。名前がinst_port
であれば、instPort
を返し、data_port
であればdataPort
を返す。そうでなければ、新たに上位のgetPort()
を返すことになる。
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); } }
slaveポートとmasterポート関数の実装
上記で放置していた、オーバ・ライドしなければならない関数を実装していく。
getAddrRanges()
- 該当するSimObjectがターゲットとしているメモリアドレスの領域のリストを返す
- Convenience typedef for a collection of address ranges.
recvAtomic()
recvFunctional()
- これは良く分からない。実際のRD/WR操作を行うための関数?
recvTimingReq()
recvRespRetry()
recvTimingResp()
recvReqRetry()
recvRangeChange()
slaveとmasterポートの実装はシンプルである。ほとんどの場合、ポートの関数はメインメモリオブジェクト(SimpleMemobj
)にフォワードするだけである。
2つのシンプルな関数から始める:getAddrRanges()
とrecvFunctional()
は、単純にSimpleMemobj
のものを呼び出すだけである
AddrRangeList SimpleMemobj::CPUSidePort::getAddrRanges() const { // SimpleMemobjのgetAddrRanges()を呼び出す return owner->getAddrRanges(); } void SimpleMemobj::CPUSidePort::recvFunctional(PacketPtr pkt) { // SimpleMemobjのhandleFunctionalを呼び出す return owner->handleFunctional(pkt); }
SimpleMemobj
のgetAddrRanges()
は、メモリ・バス側のgetAddrRanges()
を呼び出す。
また、SimpleMemobj
のhandleFunctional()
は、メモリ・バス側のsendFunctional()
を呼び出す。
つまり、どちらとも単純にスレーブからマスターへパケットをフォワードしているだけである。
void SimpleMemobj::handleFunctional(PacketPtr pkt) { memPort.sendFunctional(pkt); } AddrRangeList SimpleMemobj::getAddrRanges() const { DPRINTF(SimpleMemobj, "Sending new ranges\n"); return memPort.getAddrRanges(); }
MemSidePort
では、recvRangeChangeを実装する必要があり、SimpleMemobj
を通じてスレーブポートに転送を行う。
SimpleMemobj
を通じて、instPort
とdataPort
に対してsendRangeChange()
を呼び出す。
void SimpleMemobj::MemSidePort::recvRangeChange() { owner->sendRangeChange(); }
void SimpleMemobj::sendRangeChange() { instPort.sendRangeChange(); dataPort.sendRangeChange(); }