深入理解 Gem5 之二
深入理解 Gem5 之二
紧接着对 gem5 事件机制的研究,本篇博文我重点研究了 gem5 中对象序列化的操作。总的来说,gem5 在对模拟器中的对象序列化前,需要先将其排水(Drain,由于中文翻译的限制,下文统称为 Drain),将不确定的状态先清除,等到一切安排妥当后,再将对象序列化到磁盘中。
Drain
DrainState
之前的博文详细解释了事件在 gem5 的作用以及其实现机制。本文开始介绍 gem5 中其他比较重要的机制。
当 gem5 正常运行时,模拟器中的对象在一开始时都处于 DrainState::Running
状态,并用事件驱动模拟器的运行,这会导致很多对象在运行时处于似是而非的状态——部分信号正在传递,部分程序正在运行,缓冲区还待处理等。然而,模拟器总要在某些时刻有所停顿——准备快照(snapshot)、准备移交 CPU 等。这时候就需要引入 drain 的概念,将这些中间态清除。drain 指系统清空 SimObject 对象中内部状态的过程。通常,drain 会在序列化、创建检查点、切换 CPU 模型或 timing 模型前使用。对象会调用 drain()
函数将对象转移到 draining 或 drained 状态。然后进入 drained 状态。下面的代码介绍了四种 drain 状态。
1 |
|
当某一个对象进入 drained 状态(drain::drain()
返回 DrainState::drained
时表示该对象已经 drain 干净了)后,模拟仍会继续,直到所有对象都进入 drained 状态。如果该对象需要更多的时间来处理,那么它返回 DrainState::draining
状态。注意:一个对象的 drain 状态可能会被其他状态干扰,因此模拟器需要不停地重复 drain 来保证所有对象已经进入 DrainState::drained
状态。当系统不再需要所有对象维持在 drained 状态时,会调用 resume()
函数,它将让所有对象调用 drainResume()
返回到正常 DrainState::Running
的状态。注意,在恢复过程中可能会创建新的 Drainable 对象。在这种情况下,新对象将在 Resuming 状态下创建,然后再恢复到正常。
drain 的工作流程
根据 gem5 的文档以及源文件中的注释,总结一下 drain 工作的主要流程:
- 调用
DrainManager::tryDrain()
函数,该函数会让每个对象调用Drainable::drain()
函数。如果它们全部返回 true,则 drain 已经完成。否则,DrainManager 将跟踪仍在 draining 的对象。 - 模拟器会继续仿真。当一个对象完成 drain 时,它会调用
DrainManager::signalDrainDone()
函数,向 DrainManager 报告 drain 已完成。 - 检查是否有对象仍然需要 drain(
DrainManager::tryDrain()
),如果是,重复上面的过程。 - 一旦模拟器中的所有对象的内部状态被清空,这些对象就被序列化到磁盘上,或者发生配置更改:切换CPU模型或更改 timing 模型,总之做一些只能在 drained 后做的事情。
- 完成后,调用
DrainManager::resume()
函数,该函数会让所有对象调用Drainable::drainResume()
,返回到正常运行的状态。
接下来,我们随着代码逐步分析上面的工作流程:
DrainManager
DrainManager 类负责管理全局对象的 drain 工作,显然它必须是个单例。它内部维护了一个包含全局的可 Drainable 的对象数组 _allDrainable,以方便管理所有对象的 drain 工作,并用一个状态变量 _state 指示模拟器的状态。
1 |
|
从代码上看,第一步中的 tryDrain()
函数实现其实很简单,就是通过 for 循环让每个对象调用 Drainable::drain()
函数,记录并输出 drain 失败的对象(若存在的话),统计其个数,必要时需请求下一轮 drain。
1 |
|
题外话,Named 类为 SimObject 对象提供了名字,所有 SimObject 对象都继承了该类。
1 |
|
此外,在创建一个可 Drainable 的类对象时,DrainManager 类通过注册机制来管理这些对象:
1 |
|
Drainable
至于 Drainable 类,它是 SimObject 类中的一个基类。Drainable 的所有派生类都是可 Drain 的,drain()
函数要求所有派生类都必须实现,dmDrain()
函数是为 DrainManager 类方便调用 drain()
而实现的。Drainable 类包括了一个指示状态的变量以及指向全局 DrainManager 类的指针(引用)。
1 |
|
当一个对象完成 drain 后,调用 signalDrainDone()
函数,该函数会通知 DrainManager 其 drain 工作已完成。若 tryDrain()
函数返回值为 false,那么就需要不停地调用 tryDrain()
,此时模拟仍将继续。直到所有对象完成 drain。此时,DrainManager 会退出模拟循环(exitSimLoop()
),开始进行第四步中所说的其他操作。
1 |
|
第五步中,要让系统返回正常运行状态。DrainManager 类要使用 DrainManager::resume()
函数,将 drained 系统返回到正常状态:for 循环中让每个对象调用 dmDrainResume()
函数。dmDrainResume()
就是对 drainResume()
的包装。
1 |
|
Serialization
Serialization(序列化)指将对象转换成二进制,以方便长期保存、网络传递等。而 deserialization(反序列化)就是将二进制转换成对象。当前文提到的 drain 操作完成后,通常会跟上对象的序列化操作,将对象转换成二进制,用作快照(snapshot)保存或切换模型。在 gem5 中,Serializable 类为 SimObject 类提供序列化支持。Serializable 类通常用于创建检查点(Checkpoints)。所谓检查点其本质上是模拟的快照。当模拟需要非常长的时间时(几乎总是如此),用户可以使用检查点,在自己感兴趣的时间处加上检查点,以便稍后使用 DerivO3CPU 从该检查点恢复。
检查点创建与使用
通常,检查点会保存在新的文件夹目录 cpt.TICKNUMBER,其中 TICKNUMBER 指 要插入的检查点的 tick 时间值。要创建新的检查点,有以下几种方法:
- 启动 gem5 模拟器后,执行 m5 的命令插入检查点。
- 一个伪指令可以用来创建检查点。例如,可以在应用程序中包含这个pseduo指令,以便当应用程序达到某种状态时创建检查点
- 使用 --take-checkpoints 可以周期性的输出检查点,–checkpoint-at-end 用于在模拟后创建检查点
使用Ruby内存模型创建检查点时,必须使用MOESI锤协议。这是因为检查指向正确的内存状态要求缓存刷新到内存中。这种刷新操作目前仅支持MOESI锤协议。
从检查点恢复通常可以很容易地从命令行完成:
1 |
|
整数N表示检查点编号,通常从1开始递增。
CheckPointIn
深入理解序列化的实现离不开对 CheckpointIn 类的剖析。该类主要负责完成检查点的创建与恢复工作。_cptDir 就是检查点保存的目录位置,db 表示 ini 文件。ini 文件就是由很多 section 组成的初始化文件,其中每个 section 包含有若干 key-value 的 entry。通过 ini 文件以及 IniFile 类,检查点将模拟时的对象保存在了磁盘中,这便是序列化。
1 |
|
IniFile 类详细实现了 ini 文件的读写,section 的查询、访问等,这不是本博客的主题,不在详述。
Serializable
任何继承并实现此接口的对象都可以包含在 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 对象调用序列化函数;否则,这些对象将被不必要地序列化多次。
1 |
|
注意到 Serializable 类中仅维护了一个路径堆栈 path,实时记录对象所在的位置。下面代码可以看出,当新的 section 被创建后,其名字会被压栈进入到 path 中。ScopedCheckpointSection 类是为命名 section 时更加方便:section 名字便是该对象所在的系统位置。例如,Section1.Section2.Section3 表示对象从内到外处于Section3 Section2 Section1 中。
1 |
|
1 |
|
下面给出创建检查点的函数。给定了文件目录名后,创建文件夹和 ini 文件,然后输出检查点即可。
1 |
|