第 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 行)定义了它的本质:
cpp
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.
三个关键事实:
dictionary继承自IDLList<entry>(第 135--138 行),即"有序的 entry 双向链表 + 索引"。顺序很重要:字典的写出要保持用户书写的顺序。- 它既是
IOdictionary的基类,也是objectRegistry的"引导字典"(bootstrap dictionary)。 - 关键字可以是普通 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 行用一段示例说明了这套"字典内引用"语法:
cpp
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 序列:
cpp
// 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 行为的钥匙:
cpp
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 的派生类,增加了一个关键职责------自动注册到对象注册表:
cpp
Class
Foam::regIOobject
Description
regIOobject is an abstract class derived from IOobject to handle
automatic object registration with the objectRegistry.
其私有数据(第 90--114 行)暴露了"依赖管理"的实现细节:
cpp
//- 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_;
三个概念值得展开:
registered_/ownedByRegistry_:对象是否挂在注册表上、注册表是否有权删除它。REGISTER与NO_REGISTER的差别就在这里------大量临时场用NO_REGISTER避免污染注册表、每步都写盘。eventNo_:事件号。每当注册表发生"写事件",全局event_递增;依赖它判断"是否需要重新读取"。这是 OpenFOAM 处理"运行中物性/字典被修改后自动重读"的机制。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 行)的继承与数据极具启发性:
cpp
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_;
...
};
要点:
- 它既是
regIOobject(因此自己也注册),又是HashTable<regIOobject*>(因此能装别人)。 这是一种"自相似"的树形结构:注册表本身也是对象,可以被注册到父注册表。 time_与parent_:每个注册表都持有"主时间注册表"的引用与"父注册表"的引用。查找时若在本级找不到,会沿 parent_ 链向上递归 ,直到parentNotTime()(第 91--93 行)判定已到根(时间注册表),停止。dbDir_:本注册表相对于时间的目录路径。这使注册表可以映射到 case 的目录结构------0/下有U、p等,constant/下有transportProperties等。OpenFOAM 的"目录树"与"注册表树"是同构的,这是它能自动读写、自动定位文件的关键。cacheTemporaryObjects_:临时对象缓存机制。某些"临时对象"(如湍流模型中间量)可在controlDict中用cacheTemporaryObjects指定,读入后缓存复用,避免重复计算。<Pair<bool>>记录每个临时对象的缓存状态。
3.4.2 注册表树的全貌
带注释的代码显示 Time 继承自 objectRegistry(Time.H 第 73--80 行)。于是整个注册表树是:
scss
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 运行的中枢。它的两个关键点:
- 继承
objectRegistry(第 73--80 行),因此根注册表就是它。所有对象最终都属于Time的树。 - 持有
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 三块基石如何协同:一次"读取并求解"的旅程
把本章三块拼起来,看一个典型的启动流程:
- 启动 :
Time从system/controlDict构造,创建 case 路径。 - 读网格 :
fvMesh作为regIOobject注册到Time;它读constant/polyMesh/。 - 读场 :求解器创建
volVectorField U(IOobject("U", runTime.timeName(), mesh, IOobject::MUST_READ, IOobject::AUTO_WRITE), mesh)。构造子把U注册到mesh注册表,并因MUST_READ去0/U读数据。 - 运行时 :每一步
Time++;runTime.write()遍历注册表,把AUTO_WRITE的场写到当前时间目录;runTimeModifiable检查fvSolution是否改动。 - 查找与交互 :函数对象用
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 运行期的"名字服务器",让求解器、模型、函数对象彼此解耦地互相发现。