11

深入理解 Gem5 之二

 2 years ago
source link: https://dingfen.github.io/cpp/2022/03/08/gem5-2.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

深入理解 Gem5 之二Permalink

紧接着对 gem5 事件机制的研究,本篇博文我重点研究了 gem5 中对象序列化的操作。总的来说,gem5 在对模拟器中的对象序列化前,需要先将其排水(Drain,由于中文翻译的限制,下文统称为 Drain),将不确定的状态先清除,等到一切安排妥当后,再将对象序列化到磁盘中。

DrainPermalink

DrainStatePermalink

之前的博文详细解释了事件在 gem5 的作用以及其实现机制。本文开始介绍 gem5 中其他比较重要的机制。

当 gem5 正常运行时,模拟器中的对象在一开始时都处于 DrainState::Running 状态,并用事件驱动模拟器的运行,这会导致很多对象在运行时处于似是而非的状态——部分信号正在传递,部分程序正在运行,缓冲区还待处理等。然而,模拟器总要在某些时刻有所停顿——准备快照(snapshot)、准备移交 CPU 等。这时候就需要引入 drain 的概念,将这些中间态清除。drain 指系统清空 SimObject 对象中内部状态的过程。通常,drain 会在序列化、创建检查点、切换 CPU 模型或 timing 模型前使用。对象会调用 drain() 函数将对象转移到 draining 或 drained 状态。然后进入 drained 状态。下面的代码介绍了四种 drain 状态。

enum class DrainState {
    Running,  /**< Running normally */
    Draining, /**< Draining buffers pending serialization/handover */
    Drained,  /**< Buffers drained, ready for serialization/handover */
    Resuming, /**< Transient state while the simulator is resuming */
};

当某一个对象进入 drained 状态(drain::drain() 返回 DrainState::drained 时表示该对象已经 drain 干净了)后,模拟仍会继续,直到所有对象都进入 drained 状态。如果该对象需要更多的时间来处理,那么它返回 DrainState::draining 状态。注意:一个对象的 drain 状态可能会被其他状态干扰,因此模拟器需要不停地重复 drain 来保证所有对象已经进入 DrainState::drained 状态。当系统不再需要所有对象维持在 drained 状态时,会调用 resume() 函数,它将让所有对象调用 drainResume() 返回到正常 DrainState::Running 的状态。注意,在恢复过程中可能会创建新的 Drainable 对象。在这种情况下,新对象将在 Resuming 状态下创建,然后再恢复到正常。

drain 的工作流程Permalink

根据 gem5 的文档以及源文件中的注释,总结一下 drain 工作的主要流程:

  1. 调用 DrainManager::tryDrain() 函数,该函数会让每个对象调用 Drainable::drain() 函数。如果它们全部返回 true,则 drain 已经完成。否则,DrainManager 将跟踪仍在 draining 的对象。
  2. 模拟器会继续仿真。当一个对象完成 drain 时,它会调用 DrainManager::signalDrainDone() 函数,向 DrainManager 报告 drain 已完成。
  3. 检查是否有对象仍然需要 drain(DrainManager::tryDrain()),如果是,重复上面的过程。
  4. 一旦模拟器中的所有对象的内部状态被清空,这些对象就被序列化到磁盘上,或者发生配置更改:切换CPU模型或更改 timing 模型,总之做一些只能在 drained 后做的事情。
  5. 完成后,调用 DrainManager::resume() 函数,该函数会让所有对象调用 Drainable::drainResume(),返回到正常运行的状态。

接下来,我们随着代码逐步分析上面的工作流程:

DrainManagerPermalink

DrainManager 类负责管理全局对象的 drain 工作,显然它必须是个单例。它内部维护了一个包含全局的可 Drainable 的对象数组 _allDrainable,以方便管理所有对象的 drain 工作,并用一个状态变量 _state 指示模拟器的状态。

class DrainManager {
  public:
    /** singleton DrainManager instance */
    static DrainManager &instance() { return _instance; }
  private:
    /** Global simulator drain state */
    DrainState _state;
    /** Lock protecting the set of drainable objects */
    mutable std::mutex globalLock;
    /** Set of all drainable objects */
    std::vector<Drainable *> _allDrainable;
};

从代码上看,第一步中的 tryDrain() 函数实现其实很简单,就是通过 for 循环让每个对象调用 Drainable::drain() 函数,记录并输出 drain 失败的对象(若存在的话),统计其个数,必要时需请求下一轮 drain。

// in DrainManager:  
  public:
    bool tryDrain() {
      // 1. change simulator state to Draining
      _state = DrainState::Draining;
      // 2. let all Drainable objects to drain, call dmDrain()
      for (auto *obj : _allDrainable) {
        DrainState status = obj->dmDrain();
        if (debug::Drain && status != DrainState::Drained) {
            Named *temp = dynamic_cast<Named*>(obj);
            if (temp)
                DPRINTF(Drain, "Failed to drain %s\n", temp->name());
        }
        _count += status == DrainState::Drained ? 0 : 1;
      }
      if (_count == 0) {
        // Drain done.
        _state = DrainState::Drained;
        return true;
      } else {
        DPRINTF(Drain, "Need another drain cycle. %u/%u objects not ready.\n",
                _count, drainableCount());
        return false;
      }
    }

题外话,Named 类为 SimObject 对象提供了名字,所有 SimObject 对象都继承了该类。

/** Interface for things with names. */
class Named {
  private:
    const std::string _name;
  public:
    Named(const std::string &name_) : _name(name_) { }
    virtual ~Named() = default;
    virtual std::string name() const { return _name; }
};

此外,在创建一个可 Drainable 的类对象时,DrainManager 类通过注册机制来管理这些对象:

void DrainManager::registerDrainable(Drainable *obj) {
  std::lock_guard<std::mutex> lock(globalLock);
  _allDrainable.push_back(obj);
}

void DrainManager::unregisterDrainable(Drainable *obj) {
  std::lock_guard<std::mutex> lock(globalLock);
  auto o = std::find(_allDrainable.begin(), _allDrainable.end(), obj);
  _allDrainable.erase(o);
}

DrainablePermalink

至于 Drainable 类,它是 SimObject 类中的一个基类。Drainable 的所有派生类都是可 Drain 的,drain() 函数要求所有派生类都必须实现,dmDrain() 函数是为 DrainManager 类方便调用 drain() 而实现的。Drainable 类包括了一个指示状态的变量以及指向全局 DrainManager 类的指针(引用)。

class Drainable {
  friend class DrainManager;
  protected:
    virtual DrainState drain() = 0;
    virtual void drainResume() {};
  private:
    /** interface for DrainManager */
    DrainState dmDrain();
    void dmDrainResume();
    /** Convenience reference to the global DrainManager */
    DrainManager &_drainManager;
    /**
     * Current drain state of the object. Needs to be mutable since
     * objects need to be able to signal that they have transitioned
     * into a Drained state even if the calling method is const.
     */
    mutable DrainState _drainState;
};

当一个对象完成 drain 后,调用 signalDrainDone() 函数,该函数会通知 DrainManager 其 drain 工作已完成。若 tryDrain() 函数返回值为 false,那么就需要不停地调用 tryDrain() ,此时模拟仍将继续。直到所有对象完成 drain。此时,DrainManager 会退出模拟循环(exitSimLoop()),开始进行第四步中所说的其他操作。

void signalDrainDone() const {
  switch (_drainState) {
    case DrainState::Running:
    case DrainState::Drained:
    case DrainState::Resuming:
      return;
    case DrainState::Draining:
      _drainState = DrainState::Drained;
      _drainManager.signalDrainDone();
   return;
  }
}

void DrainManager::signalDrainDone() {
  assert(_count > 0);
  if (--_count == 0) {
    DPRINTF(Drain, "All %u objects drained..\n", drainableCount());
    exitSimLoop("Finished drain", 0);
  }
}

第五步中,要让系统返回正常运行状态。DrainManager 类要使用 DrainManager::resume() 函数,将 drained 系统返回到正常状态:for 循环中让每个对象调用 dmDrainResume() 函数。dmDrainResume() 就是对 drainResume() 的包装。

/** Resume normal simulation in a Drained system. */
void DrainManager::resume() {
  // New objects (i.e., objects created while resuming) will
  // inherit the Resuming state from the DrainManager.
  _state = DrainState::Resuming;
  do {
    for (auto *obj : _allDrainable) {
      if (obj->drainState() != DrainState::Running) {
        assert(obj->drainState() == DrainState::Drained ||
           obj->drainState() == DrainState::Resuming);
        obj->dmDrainResume();
      }
    }
  } while (!allInState(DrainState::Running));
  _state = DrainState::Running;   
}

SerializationPermalink

Serialization(序列化)指将对象转换成二进制,以方便长期保存、网络传递等。而 deserialization(反序列化)就是将二进制转换成对象。当前文提到的 drain 操作完成后,通常会跟上对象的序列化操作,将对象转换成二进制,用作快照(snapshot)保存或切换模型。在 gem5 中,Serializable 类为 SimObject 类提供序列化支持。Serializable 类通常用于创建检查点(Checkpoints)。所谓检查点其本质上是模拟的快照。当模拟需要非常长的时间时(几乎总是如此),用户可以使用检查点,在自己感兴趣的时间处加上检查点,以便稍后使用 DerivO3CPU 从该检查点恢复。

检查点创建与使用Permalink

通常,检查点会保存在新的文件夹目录 cpt.TICKNUMBER,其中 TICKNUMBER 指 要插入的检查点的 tick 时间值。要创建新的检查点,有以下几种方法:

  • 启动 gem5 模拟器后,执行 m5 的命令插入检查点。
  • 一个伪指令可以用来创建检查点。例如,可以在应用程序中包含这个pseduo指令,以便当应用程序达到某种状态时创建检查点
  • 使用 –take-checkpoints 可以周期性的输出检查点,–checkpoint-at-end 用于在模拟后创建检查点

使用Ruby内存模型创建检查点时,必须使用MOESI锤协议。这是因为检查指向正确的内存状态要求缓存刷新到内存中。这种刷新操作目前仅支持MOESI锤协议。

从检查点恢复通常可以很容易地从命令行完成:

build/<ISA>/gem5.debug configs/example/fs.py -r N
OR
build/<ISA>/gem5.debug configs/example/fs.py --checkpoint-restore=N

整数N表示检查点编号,通常从1开始递增。

CheckPointInPermalink

深入理解序列化的实现离不开对 CheckpointIn 类的剖析。该类主要负责完成检查点的创建与恢复工作。_cptDir 就是检查点保存的目录位置,db 表示 ini 文件。ini 文件就是由很多 section 组成的初始化文件,其中每个 section 包含有若干 key-value 的 entry。通过 ini 文件以及 IniFile 类,检查点将模拟时的对象保存在了磁盘中,这便是序列化。

class CheckpointIn {
  private:
    IniFile db;
    const std::string _cptDir;
    // current directory we're serializing into.
    static std::string currentDirectory;
    // Filename for base checkpoint file within directory.
    static const char *baseFilename;
};

IniFile 类详细实现了 ini 文件的读写,section 的查询、访问等,这不是本博客的主题,不在详述。

SerializablePermalink

任何继承并实现此接口的对象都可以包含在 gem5 的检查点系统中。所有支持序列化的对象都应该继承该类。继承该类对象可大致分为两类:1)真正的 SimObjects(继承了 SimObject 类,SimObject 类继承了 Serializable 类)和 2)未继承 SimObject 类,仅继承 Serializable 类的普通对象。

SimObjects 可通过 SimObject::serializeAll() 函数自动完成序列化,写入到自己的 sections 中。前文提到,SimObjects 也可以包含其他未继承 SimObject 类的可序列化对象。然而,这些“普通”的可序列化成员不会自动序列化,因为它们没有 SimObject::serializeAll() 函数。因此有这些对象的类在实现时需要主动调用其序列化/反序列化函数,以完成序列化。

其中,首选方法是使用 serializeSection() 函数,这会将序列化对象放入当前 section(此section 就是 ini 文件中的section) 中的新 subsection。另一种选择是直接调用 serialize() ,它将对象序列化到当前 section,但不推荐使用后者,因为这会导致可能存在的命名冲突。下面代码给出了 Serializable 类中最重要的函数。serialize()unserializa() 函数都需要子类实现。serializeAll() 函数,从后往前(why?)遍历所有的对象,调用 serializeSection() 函数将它们序列化。因此,不应在其他任何地方对 SimObject 对象调用序列化函数;否则,这些对象将被不必要地序列化多次。

/* In class SimObject
* Create a checkpoint by serializing all SimObjects in the system.*/
static void serializeAll(const std::string &cpt_dir) {
  std::ofstream cp;
  Serializable::generateCheckpointOut(cpt_dir, cp);
  SimObjectList::reverse_iterator ri = simObjectList.rbegin();
  SimObjectList::reverse_iterator rend = simObjectList.rend();
  for (; ri != rend; ++ri) {
    SimObject *obj = *ri;
    // This works despite name() returning a fully qualified name
    // since we are at the top level.
    obj->serializeSection(cp, obj->name());
   }
}

class Serializable {
  /* Serialize an object
   * Output an object's state into the current checkpoint section. */
  virtual void serialize(CheckpointOut &cp) const = 0;
    
  /* Unserialize an object
   * Read an object's state from the current checkpoint section. */
  virtual void unserialize(CheckpointIn &cp) = 0;
  
  /* Serialize an object into a new section in a checkpoint 
   * and calls serialize() to serialize the current object into
   * the new section. */
  void serializeSection(CheckpointOut &cp, const char *name) const;
  private:
    static std::stack<std::string> path;
}

注意到 Serializable 类中仅维护了一个路径堆栈 path,实时记录对象所在的位置。下面代码可以看出,当新的 section 被创建后,其名字会被压栈进入到 path 中。ScopedCheckpointSection 类是为命名 section 时更加方便:section 名字便是该对象所在的系统位置。例如,Section1.Section2.Section3 表示对象从内到外处于Section3 Section2 Section1 中。

Serializable::ScopedCheckpointSection(CP &cp, const char *name) {
  pushName(name);
  nameOut(cp);
}

void Serializable::ScopedCheckpointSection::pushName(const char *obj_name) {
  if (path.empty()) {
    path.push(obj_name);
  } else {
    path.push(csprintf("%s.%s", path.top(), obj_name));
  }
  DPRINTF(Checkpoint, "ScopedCheckpointSection::pushName: %s\n", obj_name);
}


void Serializable::ScopedCheckpointSection::nameOut(CheckpointOut &cp) {
  DPRINTF(Checkpoint, "ScopedCheckpointSection::nameOut: %s\n",
          Serializable::currentSection());
  cp << "\n[" << Serializable::currentSection() << "]\n";
}
Serializable::ScopedCheckpointSection::~ScopedCheckpointSection() {
    DPRINTF(Checkpoint, "Popping: %s\n", path.top());
    path.pop();
}

下面给出创建检查点的函数。给定了文件目录名后,创建文件夹和 ini 文件,然后输出检查点即可。

void Serializable::generateCheckpointOut(const std::string &cpt_dir,
        std::ofstream &outstream) {
  std::string dir = CheckpointIn::setDir(cpt_dir);
  if (mkdir(dir.c_str(), 0775) == -1 && errno != EEXIST)
    fatal("couldn't mkdir %s\n", dir);

  std::string cpt_file = dir + CheckpointIn::baseFilename;
  outstream = std::ofstream(cpt_file.c_str());
  time_t t = time(NULL);
  if (!outstream)
    fatal("Unable to open file %s for writing\n", cpt_file.c_str());
  outstream << "## checkpoint generated: " << ctime(&t);
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK