FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

Gem5のチュートリアル "Learning Gem5"をやってみる(14. SimpleMemobjの実装を噛み砕く2)

gem5を構成するSimObjectを追加するためのチュートリアルをやってみる。

www.gem5.org

SimpleMemobjを使った簡単なブリッジの実装が良く分からなかったので、もう一度よく読みなおしてみることにした。

  • gem5記事一覧インデックス

msyksphinz.hatenablog.com


リクエスト受信の実装 (recvTimingReq())

recvTimingReq()は、CPU側のポートからリクエストを受け付けるための関数である。これのために必要な要件は以下のようになる:

  • SimpleMemobjがリクエストを受信できるかどうかをチェックする
    • SimpleMemobjは非常にシンプルなブロック構造をしている
    • 1つのリクエストのみ同時に受け付けることができる
    • あるリクエストを処理中に別のリクエストが到達すると、2番目のリクエストはブロックされる。
  • CPUSidePortはポートインタフェースにすべてのフローコントロール情報を格納する。
    • CPUSidePortに別のメンバ変数としてneedRetryを追加する
      • boolean変数であり、SimpleMemobjがフリーになった場合にリトライが必要であることを意味する。
      • SimpleMemobjはリクエストがブロックされ、将来的にリトライする実行する必要がある

以下がその実装となる

// CPU側からリクエストを受け付ける実装
bool
SimpleMemobj::CPUSidePort::recvTimingReq(PacketPtr pkt)
{
    if (!owner->handleRequest(pkt)) {
                // handleRequest()を実行して答えがFalseだと、リクエストを処理中である
                // ポート自体はfalseを返してリクエストを拒否し、retryが必要であることを覚える
        needRetry = true;
        return false;
    } else {
                // handleRequest()を実行して答えがTrueだと、そのままリクエストを受け付ける
        return true;
    }
}

次にhandleRequest()の実装は以下のようになる:

  1. SimpleMemobjが他のリクエストへの応答を待って既にブロックされているかどうかをチェックする (handleRequest())
  2. ブロックされている場合
    1. falseを返す(今リクエストを 受け付けられないことを通知する)
  3. ブロックされていない場合
    1. ポートをブロックされたものとしてマークする (block = true)
    2. パケットをメモリポートから送信する (memPort.sendPacket())
// CPUからのリクエストを処理するサポート関数
bool
SimpleMemobj::handleRequest(PacketPtr pkt)
{
        // 現在他のリクエストを処理しているためブロックされていることを意味する
    if (blocked) {
        return false;
    }
    DPRINTF(SimpleMemobj, "Got request for addr %#x\n", pkt->getAddr());
        // 他のリクエストがないため処理を行う。
        // blocked = trueとしてブロック中にする
        // メモリ・バスにパケットを送信する
    blocked = true;
    memPort.sendPacket(pkt);
    return true;
}

次に、MemSidePortsendPacket関数を実装する:

  • 相手のスレーブ・ポートがリクエストを受け付けられない場合のフロー制御を行う。
  • パケットがブロックされた場合に備えてパケットを保存するためのメンバを追加する(blockedPacket)
    • simpleMemobj送信先がリクエスト(またはレスポンス)を受信できない場合にパケットを保存するのは、送信側(simpleMemobj)の責任である。
    • 送信に失敗した場合、このオブジェクトはパケットをblockedPacketメンバ関数に格納
      • 後で(recvReqRetryを受信したときに)パケットを送信できるようにする。
      • この関数には、バグがないことを確認するための防御コードも含まれており、blockedPacket変数を間違って上書きしようとすることはない。
        • panic_ifの部分のこと
void
SimpleMemobj::MemSidePort::sendPacket(PacketPtr pkt)
{
    panic_if(blockedPacket != nullptr, "Should never try to send if blocked!");
        // パケットをメモリ・バス側に送信しようとして拒否された場合
        // そのパケットを保存しておく
        // パケットを送信するときに、保存用のblockedPacketは必ずemptyの状態である必要がある
    if (!sendTimingReq(pkt)) {
        blockedPacket = pkt;
    }
}

パケットを再送するコードを実装する必要がある:

上で書いたsendPacket関数を呼び出して、パケットの再送を試みる。

void
SimpleMemobj::MemSidePort::recvReqRetry()
{
    assert(blockedPacket != nullptr);

    PacketPtr pkt = blockedPacket;
    blockedPacket = nullptr;

    sendPacket(pkt);
}

レスポンス受信(recvTimingResp())の実装

recvTimingResp()は、MemSidePort側が、リクエストに対するレスポンスを受信したときに駆動する関数である。

MemSidePortが応答を取得すると、SimpleMemobjを通して適切なCPUSidePortに応答を転送する。

bool
SimpleMemobj::MemSidePort::recvTimingResp(PacketPtr pkt)
{
    return owner->handleResponse(pkt);
}

handleResponse()の実装を行う。ここでは、すでにCPU側の別のパケットがブロックしていた際の挙動も含まれている:

  1. 最初のアサーションはCPUからリクエストを受け付けたため、レスポンスが戻った際にはblocked=trueになっていることを確認するための実装である。
  2. blockedを解除する
    1. これは、sendTimingRespを呼び出す前に行わなければならない。
    2. そうしないと、マスター・ポートが応答を受信してから別のリクエストを送信するまでの間に1つのコールチェーンを持っている可能性があるため、無限ループにはまる可能性がある。
  3. パケットが命令パケットかデータパケットかをチェックし、適切なポートに送り返す。
  4. オブジェクトのブロックが解除されたので、CPU側のポートに、失敗したリクエストの再試行が可能になったことを通知する必要があるかもしれない。
bool
SimpleMemobj::handleResponse(PacketPtr pkt)
{
        // CPUからリクエストを受け付けたため、レスポンスが戻った際には
        // blocked=trueになっていることを想定
    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;
}

CPUSidePort側でこれらに関して新たに実装しなければならない関数は以下である:

  • CPUSidePort::sendPacket()
    • CPU側にレスポンスパケットを送信する
  • CPUSidePort::recvRespRetry()
    • 純粋仮想関数でオーバライドする必要がある。ブロックされているパケットがある場合に再送信を行うための関数
  • CPUSidePort::trySendRetry()
    • CPU側でブロックされたパケットを、新たに送信するための要求を行う。

まずはsendPacket()の実装:

  1. sendTimingRespを呼び出し、その結果、相手マスター・ポートのrecvTimingRespが呼び出される。
  2. 呼び出しが失敗した場合
    1. CPU側のポートが現在ブロックされていることを意味する。
    2. あとで送信するためにパケットを保存する (blockedPacket)
void
SimpleMemobj::CPUSidePort::sendPacket(PacketPtr pkt)
{
    panic_if(blockedPacket != nullptr, "Should never try to send if blocked!");

    if (!sendTimingResp(pkt)) {
        blockedPacket = pkt;
    }
}

次にrecvRespRetry()の実装である:

メモリ・バス側のrecvReqRetry()とまったく同じで、単にパケットを再送しようとするだけである。

void
SimpleMemobj::CPUSidePort::recvRespRetry()
{
    assert(blockedPacket != nullptr);

    PacketPtr pkt = blockedPacket;
    blockedPacket = nullptr;

    sendPacket(pkt);
}

次に、trySendRetry()の実装:

SimpleMemobjがCPU側の新たなリクエストをrecvTimingReq()でブロックした場合、ブロックを解除するためにリトライが必要かどうかをチェックする。

リトライが必要な場合、この関数はsendRetryReqを呼び出し、次に相手マスターポート(この場合はCPU)のrecvReqRetryを呼び出す。

void
SimpleMemobj::CPUSidePort::trySendRetry()
{
    if (needRetry && blockedPacket == nullptr) {
        needRetry = false;
        DPRINTF(SimpleMemobj, "Sending retry req for %d\n", id);
        sendRetryReq();
    }
}

この関数に加えて、ファイルを完成させるためにSimpleMemobjcreate関数を追加する。

SimpleMemobj*
SimpleMemobjParams::create()
{
    return new SimpleMemobj(this);
}

以下の図は、CPUSidePortMemSidePortSimpleMemobj の関係を示している。この図は、ピアポートがSimpleMemobjの実装とどのように相互作用するかを示している。太字の関数はそれぞれ実装しなければならないもので、太字でない関数はピアポートへのポートインターフェースである。色は、オブジェクトを通るAPIパスの1つ(例えば、リクエストの受信やメモリ範囲の更新)をハイライトしている。

この単純なメモリー・オブジェクトでは、パケットはCPU側からメモリー側に転送されるだけだ。しかし、handleRequesthandleResponseを変更することで、次の章のキャッシュのようなリッチな機能を持つオブジェクトを作ることができる。