《OpenFOAM 源码深度剖析》系列-第 3 章 字典系统、IO 体系与对象注册表

2 阅读9分钟

第 3 章 字典系统、IO 体系与对象注册表

如果说 OpenFOAM 有一个"操作系统内核",那它就是 src/OpenFOAM/db/。这一层把"配置"、"数据"、"依赖管理"、"运行期选择"统一在一个模型下:一切可通过字典配置的东西都是对象,一切对象都注册在一棵注册表树上,一切对象的生命周期由时间(Time)统一管理。

本章聚焦其中三块基石:字典(dictionary)、IO 体系(IOobject/IOstreams)、对象注册表(objectRegistry/regIOobject/Time)。运行期选择(RTS)留到第 4 章专章。

3.1 字典:OpenFOAM 的"文件系统"

3.1.1 类定位

src/OpenFOAM/db/dictionary/dictionary.H 的类头注释(第 30–43 行)定义了它的本质:

Description
    A list of keyword definitions, which is a keyword followed by a number
    of values (eg, words and numbers) or by a sub-dictionary.
    ...
    Dictionary keywords are a plain word or a pattern (regular expression).
    The general order for searching is as follows:
      - exact match
      - pattern match (in reverse order)
      - optional recursion into the enclosing (parent) dictionaries

    The dictionary class is the base class for IOdictionary and also serves
    as a bootstrap dictionary for the objectRegistry data dictionaries.

三个关键事实:

  1. dictionary 继承自 IDLList<entry>(第 135–138 行),即"有序的 entry 双向链表 + 索引"。顺序很重要:字典的写出要保持用户书写的顺序。
  2. 它既是 IOdictionary 的基类,也是 objectRegistry 的"引导字典"(bootstrap dictionary)。
  3. 关键字可以是普通 word,也可以是正则表达式(pattern),查找顺序为:精确匹配 → 正则匹配(逆序) → 递归父字典。

3.1.2 entry 的三种形态

字典的内容是 entry。入口类型只有三种:

类型含义典型内容
primitiveEntry值型条目nu 1e-5;、deltaT 0.001;
dictionaryEntry子字典solvers { p { ... } }
functionEntry指令型条目#include "..."、#calc "..."

primitiveEntry 的注释(第 76–106 行)表明它负责"字典值读取后的过滤、变量展开、函数展开与完整 entry 读取"。核心是 expandVariable——递归展开 ${} 引用。

3.1.3 ${} 变量替换与作用域

dictionary.H 第 45–83 行用一段示例说明了这套"字典内引用"语法:

key1        val1;
key2        $key1;        // 当前作用域 key1
key3        $./key1;      // 当前作用域 key1(显式)

subdict1
{
    key1        val1b;
    key2        $../key1;          // 父作用域 key1
    subdict2
    {
        key2    val2;
        key3    $../../key1;       // 祖父作用域 key1
    }
}

key4        $/subdict1/subdict2/key3;  // 绝对作用域查找

规则很清晰:

  • $key 从当前字典查找;
  • $./key、$../key、$../../key 分别表示当前 / 父 / 祖父作用域(类似相对路径);
  • $/a/b/c 用绝对路径查找(类似 shell 的绝对路径);
  • ${} 形式更清晰,等价于 $。

文档还提到:v1712 之前用点号作用域(.)与 ^/: 锚点,现已弃用(但兼容),推荐斜杠(/)语法。

这个设计使 OpenFOAM 的算例字典可以高度复用——例如在 fvSolution 里引用 ../solver,在 controlDict 里用变量拼出文件路径。

3.1.4 查找 API 的演进

dictionary.H 里能看到两代 API 并存:

  • 现代 API(第 522–587 行):findEntry、findDict、findScoped 等,返回带"上下文"的搜索结果,是内部统一搜索机制的入口。
  • 传统 API(第 1371–1526 行):operator[]、found()、lookupEntryPtr()、subDictPtr()、lookupEntry()、lookup()、readIfPresent()——这些是绝大多数应用代码里使用的接口。

常用接口速查:

接口行为
lookup(key)查找,找不到抛异常
found(key)是否存在
subDict(key, mustRead)取子字典
readEntry(key, val)读取并写入 val
readIfPresent(key, val)存在才读,返回 bool
get<Type>(key)模板化取值(带默认值版本 getOrDefault)
getDict(key)按路径取子字典

3.1.5 词法层:ITstream 与 token

字典的解析建立在一个"记号流"抽象上。src/OpenFOAM/db/IOstreams/Tstreams/ITstream.H(第 31–97 行)定义了 ITstream——把字符流切成 token 序列:

// ITstream 输入 token 流
...
// ITstream::parse_chars 与 peekNoFail:
// 展示输入流如何转换为 token 列表并安全访问 token

token 是 OpenFOAM 自研的"词法记号"类型(能表示 word、string、label、scalar、标点、位置标记等),dictionary 的读写、#include、#calc 指令,全部建立在 ITstream 之上。

3.2 IO 体系:IOobject 与 IOstreams

3.2.1 IOobject:描述"一个可读写对象的位置与策略"

IOobject 聚合了"一个对象在磁盘上的身份":

  • name(对象名)
  • location(相对 case 的目录,如 0/、constant/)
  • instance/时间(如 uniform 或某个时间名)
  • objectRegistry(注册到哪张注册表)
  • readOption / writeOption / registerOption

这些"策略位"被抽到独立的 IOobjectOption 类里(src/OpenFOAM/db/options/IOobjectOption.H),其枚举(第 61–106 行)是理解 OpenFOAM IO 行为的钥匙:

enum readOption : unsigned char
{
    NO_READ = 0,                                // 不读
    MUST_READ = 0x1,                            // 必须读(不存在则报错)
    READ_MODIFIED = 0x3,                        // 必须读,且监视运行时修改
    MUST_READ_IF_MODIFIED = 0x3,                // 同上(别名)
    LAZY_READ = 0x4,                            // 可选读(不存在则跳过)
    READ_IF_PRESENT = 0x4                       // 同上(别名)
};

enum writeOption : unsigned char
{
    NO_WRITE = 0,                               // 不自动写
    AUTO_WRITE = 0x10                           // 随 registry 自动写出
};

enum registerOption : unsigned char
{
    NO_REGISTER = 0,
    REGISTER = 1,
    LEGACY_REGISTER = 2
};

请在脑子里建立这张位掩码表,因为它解释了 OpenFOAM 无数"为什么这个场丢了/写了/没写"的行为:

  • MUST_READ + AUTO_WRITE 是读入并写出的场(典型 CFD 解场)。
  • NO_READ + AUTO_WRITE 是纯粹由程序生成、需要写出的场(如 phi、U 的初始猜测)。
  • READ_IF_PRESENT 是可选读(如某些可选的 fvOptions 文件)。

3.2.2 IOstreams:字节流与格式

src/OpenFOAM/db/IOstreams/ 是"字节流到对象"的桥梁:

  • Istream / Ostream:抽象基类;
  • ISstream / OSstream(字符串流)、IFstream / OFstream(文件流);
  • IOstream:读写双向;
  • compressionStreams:压缩读写;
  • Tstreams/:token 流(ITstream/OTstream);
  • 数据格式由 IOstreamOption 控制:ASCII / BINARY,并可指定压缩。

字典里的 #include、#inputMode(merge / overwrite / protect)、#calc 等"指令",全部由 db/dictionary/functionEntries/ 下的一批 functionEntry 实现。

3.3 regIOobject:自动注册的对象

regIOobject(src/OpenFOAM/db/regIOobject/regIOobject.H,第 27–39 行)是 IOobject 的派生类,增加了一个关键职责——自动注册到对象注册表:

Class
    Foam::regIOobject

Description
    regIOobject is an abstract class derived from IOobject to handle
    automatic object registration with the objectRegistry.

其私有数据(第 90–114 行)暴露了"依赖管理"的实现细节:

//- Is this object registered with the registry
bool registered_;
//- Is this object owned by the registry
bool ownedByRegistry_;
//- The eventNo of last update
label eventNo_;
//- List of additional files to watch
mutable DynamicList<fileName> watchFiles_;
//- List of modification watch indices
mutable DynamicList<label> watchIndices_;
//- Dictionary for any meta-data
autoPtr<dictionary> metaDataPtr_;
//- Istream for reading
autoPtr<ISstream> isPtr_;

三个概念值得展开:

  1. registered_ / ownedByRegistry_:对象是否挂在注册表上、注册表是否有权删除它。REGISTER 与 NO_REGISTER 的差别就在这里——大量临时场用 NO_REGISTER 避免污染注册表、每步都写盘。
  2. eventNo_:事件号。每当注册表发生"写事件",全局 event_ 递增;依赖它判断"是否需要重新读取"。这是 OpenFOAM 处理"运行中物性/字典被修改后自动重读"的机制。
  3. watchFiles_ / watchIndices_:需要监视"运行时修改"的文件列表(时间戳)。这解释了 MUST_READ_IF_MODIFIED/READ_MODIFIED 的语义:对应文件被改动后,Time::runTimeModifiable 会在下一步重读。

3.4 objectRegistry:一切对象的注册树

3.4.1 类结构

objectRegistry(src/OpenFOAM/db/objectRegistry/objectRegistry.H,第 58–86 行)的继承与数据极具启发性:

class objectRegistry
:
    public regIOobject,
    public HashTable<regIOobject*>
{
    // Private Data
        //- Master time objectRegistry
        const Time& time_;
        //- Parent objectRegistry
        const objectRegistry& parent_;
        //- Local directory path of this objectRegistry relative to time
        fileName dbDir_;
        //- Current event
        mutable label event_;
        //- State of cacheTemporaryObjects_, set true after reading
        mutable bool cacheTemporaryObjectsActive_;
        //- Names of temporary object with current state
        mutable HashTable<Pair<bool>> cacheTemporaryObjects_;
        ...
};

要点:

  1. 它既是 regIOobject(因此自己也注册),又是 HashTable<regIOobject*>(因此能装别人)。 这是一种"自相似"的树形结构:注册表本身也是对象,可以被注册到父注册表。
  2. time_ 与 parent_:每个注册表都持有"主时间注册表"的引用与"父注册表"的引用。查找时若在本级找不到,会沿 parent_ 链向上递归,直到 parentNotTime()(第 91–93 行)判定已到根(时间注册表),停止。
  3. dbDir_:本注册表相对于时间的目录路径。这使注册表可以映射到 case 的目录结构——0/ 下有 U、p 等,constant/ 下有 transportProperties 等。OpenFOAM 的"目录树"与"注册表树"是同构的,这是它能自动读写、自动定位文件的关键。
  4. cacheTemporaryObjects_:临时对象缓存机制。某些"临时对象"(如湍流模型中间量)可在 controlDict 中用 cacheTemporaryObjects 指定,读入后缓存复用,避免重复计算。<Pair<bool>> 记录每个临时对象的缓存状态。

3.4.2 注册表树的全貌

带注释的代码显示 Time 继承自 objectRegistry(Time.H 第 73–80 行)。于是整个注册表树是:

Time (根注册表, 也是 regIOobject)
 ├── (默认) mesh 注册表 → fvMesh
 │        ├── U, p, k, epsilon, phi, ...
 │        └── (子注册表, 如 region)
 ├── constant/ 相关注册表
 └── ...

查找一个场 U 时,代码写 mesh.lookupObject<volVectorField>("U"),实际是:在 mesh 所在树里按名字查找 regIOobject*。函数对象、求解器、边界条件都通过这种方式互相找到对方。

3.4.3 writeObject 与"谁在什么时候写盘"

注册表最重要的行为是批量写出:objectRegistry::writeObject() 会遍历表内所有 AUTO_WRITE 的对象,逐个调用其 write()。当 Time 到达 writeInterval 时,它调用根注册表的写出,从而级联写出所有需要写的场。

这也解释了 AUTO_WRITE / NO_WRITE 的差别:NO_WRITE 的场(如 phi)即使在注册表里也不会被自动写。

3.5 Time:时间、控制与运行期修改

Time(src/OpenFOAM/db/Time/Time.H)是 case 运行的中枢。它的两个关键点:

  1. 继承 objectRegistry(第 73–80 行),因此根注册表就是它。所有对象最终都属于 Time 的树。
  2. 持有 controlDict_ 与动态库表(第 121–133 行),并提供 adjustDeltaT() 与 setControls()(第 182–189 行):时间步可自适应调整,控制参数可运行期重读。

时间管理的几个面向:

  • 时间推进:deltaT、startTime、endTime、writeInterval、purgeWrite、timeFormat/timePrecision。
  • 运行期修改:runTimeModifiable 为真时,Time 会在每一步检查被监视的文件(含 controlDict 与标记了 READ_MODIFIED 的对象)是否变化,变了就重读。这就是"不重启程序也能改 fvSolution 的松弛因子"背后的机制。
  • TimePaths:负责构造/解析 case 的路径(0/、constant/、system/、processorN/)。
  • TimeState 与 subCycle:支持子循环(sub-cycling)与多时间层。

3.6 三块基石如何协同:一次"读取并求解"的旅程

把本章三块拼起来,看一个典型的启动流程:

  1. 启动:Time 从 system/controlDict 构造,创建 case 路径。
  2. 读网格:fvMesh 作为 regIOobject 注册到 Time;它读 constant/polyMesh/。
  3. 读场:求解器创建 volVectorField U(IOobject("U", runTime.timeName(), mesh, IOobject::MUST_READ, IOobject::AUTO_WRITE), mesh)。构造子把 U 注册到 mesh 注册表,并因 MUST_READ 去 0/U 读数据。
  4. 运行时:每一步 Time++;runTime.write() 遍历注册表,把 AUTO_WRITE 的场写到当前时间目录;runTimeModifiable 检查 fvSolution 是否改动。
  5. 查找与交互:函数对象用 mesh.lookupObject<...>("U") 拿到 U,湍流模型用注册表拿 nut、k、epsilon。注册表就是运行期对象总线的"名字服务器"。

理解了这一层,第 4 章的运行期选择机制就水到渠成——因为 RTS 的注册表也常常"寄生"在对象注册表之上,且其对象往往由字典驱动构造。


本章要点回顾

  • dictionary = 有序 entry 列表 + 正则匹配 + ${} 变量替换 + 父字典递归查找;entry 分 primitiveEntry / dictionaryEntry / functionEntry。
  • IOobjectOption 的位掩码(MUST_READ/READ_IF_PRESENT/MUST_READ_IF_MODIFIED + AUTO_WRITE + REGISTER)决定对象的读写与注册行为。
  • regIOobject 在 IOobject 之上增加了自动注册 + eventNo_ 依赖管理 + 文件监视。
  • objectRegistry 是自相似的树(既是 regIOobject 又是 HashTable<regIOobject*>),目录树与注册表树同构;查找沿 parent_ 递归。
  • Time 是根注册表,掌管时间推进、写出、运行期重读(runTimeModifiable)。
  • 注册表是 OpenFOAM 运行期的"名字服务器",让求解器、模型、函数对象彼此解耦地互相发现。