OpenFOAM 源码深度剖析 · 第二季-16 工程与工具链:Windows 交叉编译、性能剖析、数据交换与测试
第二季收官章。前面十五节讲的都是"物理、数值、网格、框架"------它们决定算得对不对。本节讲的是"工程面":跨平台构建(尤其是 Windows) 、性能剖析与优化 、数据交换与可视化 、模块扩展 、测试与回归。这些内容不改变控制方程,却决定了一份 OpenFOAM 源码能不能在你的机器上编译出来、跑得快、能被 ParaView/EnSight 读懂、能不能被自动化验证。换言之,本章是"从源码到可用工具"的最后一公里。
本章所有路径、行号与代码摘录均来自本仓库(OpenFOAM master,
WM_PROJECT_VERSION=2606,C++17)的真实文件,已逐个 Read 核对。
1. 导言与全景:OpenFOAM 的工程工具链地图
1.1 一条完整的工程闭环
一个 OpenFOAM 开发者的日常工作,可以抽象成一条闭环:
bash
写代码 ──▶ wmake(S2-15、第一季第 2 章)
调试 ──▶ Info / FatalError / DebugInfo + applications/test
性能 ──▶ profiling(controlDict)→ 定位瓶颈 → OpenMP / SPDP / 算法改造
并行 ──▶ decomposePar → 运行 → reconstructPar
后处理 ──▶ functionObjects / foamToVTK / foamToEnsight / postProcess
跨平台 ──▶ MinGW 交叉编译(.def / libo / LINKEXE_STUB)
扩展 ──▶ libs(...) / modules(adios、visualization、external-solver)/ plugins
验证 ──▶ applications/test 单元自检 + tutorials 端到端回归 + META-INFO 版本校验
本节逐环拆解,每一环都落到具体文件、具体行。
1.2 三条主线与三类"看不见的抽象"
本章覆盖三条主线,每条都对应一套"看不见的抽象层":
| 主线 | 目标 | 关键抽象层 | 代表文件 |
|---|---|---|---|
| 跨平台构建 | Linux 上编出能跑在 Windows 的 exe/dll | 规则链(rules)+ 符号导出层(.def) | src/Allwmake、wmake/makefiles/general、wmake/rules/*Mingw |
| 性能剖析 | 找到"时间花在哪" | 计时树(profiling) | src/OpenFOAM/global/profiling/* |
| 数据交换 | 与外部工具/后处理器互通 | 格式层(fileFormats)+ 转换层(conversion) | src/fileFormats/*、src/conversion/*、applications/utilities/postProcessing/* |
补充两条辅线:modules 扩展 (modules/)与测试体系 (applications/test/、tutorials/、META-INFO/)。
1.3 为什么"工程面"值得单独一章
前十五节反复出现的三层抽象(运行期选择 RTS、字典驱动、模板 + 零拷贝 )解决的是"程序内部如何可配置、可扩展"。而工程面解决的是"程序外部如何被构建、被测量、被交换、被验证"。二者合起来,才构成一个完整的 OpenFOAM:
- 没有跨平台构建,源码只能活在 Linux;
- 没有 profiling,优化只能靠猜;
- 没有数据交换,结果只活在 OpenFOAM 自己的目录里;
- 没有测试,改动就没有安全网。
2. Windows / MinGW 交叉编译
OpenFOAM 原生面向 Linux/Unix。所谓"Windows 支持",在 OpenFOAM 里其实有两种完全不同的含义,必须严格区分:
- runtime-only(运行期支持) :在 Windows(或 MSYS/MinGW shell)里能运行 Linux 上编出来的东西------这依赖 Windows 的 WSL/POSIX 子系统,源码层面只提供
OSspecific/MSwindows适配层; - cross-compilation(交叉编译) :在 Linux 主机 上用 mingw-w64 工具链编出原生 Windows 的
.exe/.dll------这是真正的"跨平台构建",也是本节重点。
etc/config.sh/settings 对这两种情况做了清晰的编码。
2.1 平台识别:etc/config.sh/settings 的 MINGW 分支
etc/config.sh/settings(第 92--108 行):
sh
# With mingw cross-compiled : presume x86_64 (unless aarch64)
MINGW* | MSYS*)
export WM_COMPILER_LIB_ARCH=64 # Use lib64 (consistent with build platform)
WM_ARCH=win64
case "$(uname -m)" in
(aarch64 | arm64)
WM_ARCH=winARM64
;;
esac
# Default compiler should be 'Mingw'
case "$WM_COMPILER" in
(Clang* | Gcc*)
WM_COMPILER=Mingw
;;
esac
echo "openfoam: windows support ($WM_ARCH) is runtime only" 1>&2
;;
逐行解读:
- 第 93 行
MINGW* | MSYS*:当uname -s返回以MINGW或MSYS开头的系统名时进入(典型是 MSYS2 的 bash)。这是一条 OS 名称模式匹配 ,WM_ARCH初值在第 23 行由uname -s得到。 - 第 94 行:强制
WM_COMPILER_LIB_ARCH=64,让库目录后缀与构建平台一致(lib64)。 - 第 95 行:把架构名改成
win64------注意这里WM_ARCH=win64而不是linux64,字符串里已经写明"目标是 Windows"。 - 第 96--100 行:若
uname -m为aarch64/arm64,则改成winARM64,为 ARM 版 Windows 做准备。 - 第 102--106 行:把
Clang*/Gcc*统一改写为Mingw,因为在这个 shell 里默认编译器族应当是 MinGW。 - 第 107 行:打印
windows support ($WM_ARCH) is runtime only------这行提示是本分支存在的核心原因 :此分支本身描述的是"运行期环境",真正的交叉编译由etc-mingw/另行配置。
2.2 WM_OPTIONS:平台字符串如何编码"为谁构建"
etc/config.sh/settings(第 127--134 行):
sh
# wmake configuration
export WM_DIR="$WM_PROJECT_DIR/wmake"
export WM_LABEL_OPTION="Int$WM_LABEL_SIZE"
export WM_OPTIONS="$WM_ARCH$WM_COMPILER$WM_PRECISION_OPTION$WM_LABEL_OPTION$WM_COMPILE_OPTION"
# Base executables/libraries
export FOAM_APPBIN="$WM_PROJECT_DIR/platforms/$WM_OPTIONS/bin"
export FOAM_LIBBIN="$WM_PROJECT_DIR/platforms/$WM_OPTIONS/lib"
- 第 129 行:
WM_LABEL_OPTION=Int32或Int64(取决于WM_LABEL_SIZE)。 - 第 130 行:
WM_OPTIONS由五段拼接:{ARCH}{COMPILER}{PRECISION}{LABEL}{OPTION}。交叉编译时典型值为linux64MingwDPInt32Opt------注意第一个字段是linux64而非win64,因为宿主(编译发生地)是 Linux ,WM_ARCH仍是linux64;目标平台由WM_COMPILER=Mingw这段编码。 - 第 133--134 行:所有产物落到
platforms/$WM_OPTIONS/{bin,lib}。这解释了为什么"同一份源码可以同时存在 linux64GccDPInt32Opt 与 linux64MingwDPInt32Opt 两套产物"------平台目录天然隔离不同工具链的产物。
关键结论:
WM_OPTIONS是一串自描述的平台标识 。看到linux64Mingw...就知道:宿主 Linux、目标 Windows、编译器 MinGW。这个字符串贯穿 wmake 的规则选择、目录命名与链接参数。
2.3 WM_OSTYPE=MSwindows:把 OS 类型也交给编译器决定
etc/config.sh/settings(第 221--226 行):
sh
# Non-POSIX systems and cross-compiling
case "$WM_COMPILER" in
(Mingw*)
export WM_OSTYPE=MSwindows
;;
esac
- 第 222--226 行:只要编译器是
Mingw*,就把WM_OSTYPE设为MSwindows。这是"OS 差异"被编译工具链决定而非被uname决定的证据。 WM_OSTYPE随后被src/Allwmake用来选择OSspecific/MSwindows或OSspecific/POSIX目录,也被wmake/makefiles/general(第 231 行findstring windows,$(WM_OSTYPE))用来切换libso的链接分支。
2.4 etc-mingw/:交叉编译专用配置层
etc-mingw/ 目录注入的是"用于交叉编译的偏好"。etc-mingw/prefs.sh(第 1--27 行)全文很短,但信息密度很高:
sh
# Preferences for mingw cross-compilation
export FOAM_CONFIG_ETC="etc-mingw"
export WM_COMPILER=Mingw
export WM_MPLIB=msmpi-10.0
unset WM_COMPILE_CONTROL
# No zlib available:
# export WM_COMPILE_CONTROL="~libz"
# No mpi available:
# WM_MPLIB=none
# [mingw] generally wish to avoid debug symbols from bloating the sizes
case "$FOAM_EXTRA_CXXFLAGS" in
(*-g*)
FOAM_EXTRA_CXXFLAGS="$(echo "$FOAM_EXTRA_CXXFLAGS" | sed 's/-g[0-9]*//')"
echo "Removed -g from FOAM_EXTRA_CXXFLAGS ($WM_COMPILER)" 1>&2
;;
esac
逐行/逐段解读:
- 第 4 行
FOAM_CONFIG_ETC="etc-mingw":把配置搜索目录从默认etc切到etc-mingw。于是同名的config.sh/*文件(如FFTW、CGAL、scotch)会被etc-mingw/config.sh/下的版本覆盖。 - 第 6 行:显式声明
WM_COMPILER=Mingw。 - 第 7 行:
WM_MPLIB=msmpi-10.0------Windows 下用微软的 MS-MPI,而不是 OpenMPI。对应规则wmake/rules/linux64Mingw/mplibMSMPI。 - 第 9 行:
unset WM_COMPILE_CONTROL------清空编译控制串,避免宿主环境的+openmp/~float等泄漏到交叉编译。 - 第 20--25 行:如果
FOAM_EXTRA_CXXFLAGS里含-g(调试符号),用sed 's/-g[0-9]*//'剥掉,并打一条提示。Windows 上 DLL/EXE 带调试符号会显著膨胀体积,所以交叉编译默认去调试符号。这是很具体的工程取舍。
etc-mingw/config.sh/ 下有 3 个覆盖文件:
FFTW.system(第 22--23 行) :fftw_version=fftw-system;export FFTW_ARCH_PATH=/usr/x86_64-w64-mingw32/sys-root/mingw------用宿主 mingw 提供的 Windows 版 FFTW。CGAL.none(第 22--26 行) :boost_version=boost-none、cgal_version=CGAL-none,路径设成/$boost_version、/$cgal_version这样故意不存在的路径,从机制上强制禁用 Boost/CGAL。scotch(第 35--36 行) :SCOTCH_VERSION=scotch_6.1.0;SCOTCH_ARCH_PATH拼进$WM_ARCH$WM_COMPILER$WM_PRECISION_OPTION$WM_LABEL_OPTION前缀(与WM_OPTIONS同构),定位交叉编译对应的 ThirdParty 平台目录。
小结其余覆盖:FFTW 用 FFTW.system 指向宿主的 /usr/x86_64-w64-mingw32/sys-root/mingw;CGAL/Boost 用 CGAL.none 把路径指向不存在处以机制性禁用;SCOTCH 用 6.1.0 版本且路径含 Mingw 前缀以分区;MPI 切到 WM_MPLIB=msmpi-10.0;unset WM_COMPILE_CONTROL 避免宿主开关泄漏;-g 被 sed 剥离以减小 DLL/EXE 体积。
2.5 OSspecific/MSwindows:OS 差异适配层
src/OSspecific/ 下有两套并列实现:POSIX 与 MSwindows。src/Allwmake(第 25--31 行)在构建时选择其一:
sh
wmakeLnInclude -u OpenFOAM
wmakeLnInclude -u OSspecific/"${WM_OSTYPE:-POSIX}"
# Update version info (as required)
OpenFOAM/Alltouch -check 2>/dev/null
OSspecific/"${WM_OSTYPE:-POSIX}"/Allwmake $targetType $*
- 第 26 行:
OSspecific/MSwindows或OSspecific/POSIX------上层代码只认Foam::接口,平台差异由这一薄层消化。 - 第 31 行:调用该层自己的
Allwmake。
MSwindows 的组成见 src/OSspecific/MSwindows/Make/files:MSwindows.C;cpuInfo/cpuInfo.C、memInfo/memInfo.C;memory/MemoryPool.cxx;signals/{sigFpe,sigInt,sigQuit,sigSegv,sigStopAtWriteNow,sigWriteNow,timer}.cxx;fileStat/fileStat.C;fileMonitor/fileMonitor.C(注释 /* Without inotify */);printStack/dummyPrintStack.C;LIB = $(FOAM_LIBBIN)/libOSspecific。
MSwindows 与 POSIX 功能语义一致、实现不同 :CPU 信息由 cpuInfo.C(populate() 为空,核数退化为 std::thread::hardware_concurrency());内存信息 memInfo.C(Windows API);MemoryPool.cxx 平台版;信号为同名 .cxx(Windows 异常/信号模型);文件状态 fileStat.C(替代 POSIX 时间戳);文件监视 fileMonitor.C(#ifdef _WIN32 强制 #undef FOAM_USE_INOTIFY);栈回溯用 dummyPrintStack.C 空实现 (Windows 上没有真实栈回溯);计时 cpuTime.H 平台版;链接由 Make/options 的 EXE_INC=$(COMP_FLAGS)/LIBO_LIBS=$(LINK_FLAGS) 注入 -DFOAM_USE_WINDOWS_PSAPI 与 -lpsapi。
MSwindows/Allwmake(第 1--29 行) 是这一层的构建入口:
sh
targetType=libo # Preferred library type
. ${WM_PROJECT_DIR:?}/wmake/scripts/AllwmakeParseArguments $*
if [ -f /usr/include/FlexLexer.h ] && [ ! -f FlexLexer.h ]
then
ln -sf /usr/include/FlexLexer.h FlexLexer.h # 交叉编译就地补链接
if [ -d lnInclude ]; then (cd lnInclude && ln -sf ../FlexLexer.h .); fi
fi
COMP_FLAGS="-DFOAM_USE_WINDOWS_PSAPI"
LINK_FLAGS="-lpsapi"
wmake -no-openmp $targetType
- 第 3 行
targetType=libo:OSspecific 层默认编成单个.o,避免 DLL 导出复杂性与不必要的 OpenMP。 - 第 8--18 行:交叉编译时把宿主
/usr/include/FlexLexer.h软链 进当前目录(并在lnInclude/再链一次)------"交叉编译环境差异补丁"的典型手法。 - 第 22--23 行:
-DFOAM_USE_WINDOWS_PSAPI+-lpsapi(Windows 进程状态 API,供memInfo读内存)。 - 第 27 行:
wmake -no-openmp libo。
2.6 难题一:Pstream 与 OpenFOAM 的循环依赖 → libo
src/Allwmake(第 33--80 行)是整章最关键的脚本,它把"跨平台难题"写成了工程方案:
sh
case "$WM_ARCH/$WM_COMPILER" in
(*ARM64/Mingw*)
# Handle Pstream/OpenFOAM cyclic dependency
# - compile Pstream as single .o object
# - compile OpenFOAM and link against libPstream.o
Pstream/Allwmake libo #<- dummy and mpi
FOAM_LINK_DUMMY_PSTREAM=libo wmake $targetType OpenFOAM
# Extra handling to ensure that export symbols are not pruned
special_def_file="${FOAM_LIBBIN:?}/dummy/libPstream.o.def"
if [ -f "$special_def_file" ]
then
# Relink [dummy] libPstream as dll with full export
FOAM_EXTRA_CXXFLAGS="$FOAM_EXTRA_CXXFLAGS -Wl,$special_def_file" \
WM_MPLIB=dummy Pstream/Allwmake $targetType $*
fi
# --- 对 MPI 版 libPstream 做同样处理(.def 优先取 FOAM_MPI_LIBBIN)---
# ... Pstream/Allwmake-mpi $targetType $*
# Force relink of libOpenFOAM against libPstream.{dll,so}
OpenFOAM/Alltouch 2>/dev/null
;;
(*/Mingw*)
# Handle Pstream/OpenFOAM cyclic dependency
WM_MPLIB=dummy Pstream/Allwmake libo
FOAM_LINK_DUMMY_PSTREAM=libo wmake $targetType OpenFOAM
OpenFOAM/Alltouch 2>/dev/null
;;
esac
逐行解读:
- 第 33 行
case "$WM_ARCH/$WM_COMPILER":以"架构/编译器"二元组分支------ARM64+Mingw 走第一支,普通 x86_64 Mingw 走第二支。 - 第 35--37 行注释把问题说透:Handle Pstream/OpenFOAM cyclic dependency 。
libOpenFOAM依赖libPstream的通信符号;libPstream又用OpenFOAM的类型/工具。ELF 上可用--start-group或"默认导出全部符号"糊过去,但 Windows DLL 下这条链会成环且符号被裁。 - 第 38 行
Pstream/Allwmake libo:把 Pstream 编成单个.o(libo目标) ,.o是无条件可链接的对象,绕开"共享库导入/导出"这层。 - 第 39 行:
FOAM_LINK_DUMMY_PSTREAM=libo wmake ... OpenFOAM------用环境变量告知"用 dummy Pstream 的 libo"。libo一词同时传递"目标类型"与"链接来源"。 - 第 42--48 行:定位
${FOAM_LIBBIN}/dummy/libPstream.o.def,若存在则用-Wl,<def>把它传给链接器,重链 dummy 版 Pstream 为全导出 dll (注释Relink [dummy] libPstream as dll with full export)。 - 第 50--65 行:对 MPI 版 Pstream 做同样处理(
.def优先取${FOAM_MPI_LIBBIN},退化取${FOAM_LIBBIN}/$FOAM_MPI)。 - 第 67--68 / 77--78 行
OpenFOAM/Alltouch:强制 libOpenFOAM 重新链接(touch 源文件使 wmake 认定需重建),确保最终链接到重链后的 Pstream。
2.7 难题二:Windows DLL 必须显式导出符号 → .def
Linux 上 g++ -shared 默认导出所有非隐藏符号;Windows 的 DLL 默认只导出被显式标注的符号 。因此 MinGW 交链必须生成 .def(模块定义文件),里面列出要导出的 mangled 符号名。
先生成 .def 的是链接器本身。wmake/rules/linux64Mingw/c++(第 34--47 行)里的 LINKLIBSO:
sh
LINKLIBSO = $(CC) $(c++FLAGS) -shared \
-Wl,--output-def=$(LIB).def \
-Wl,--out-implib=$(LIB).dll.a \
-Wl,--no-undefined \
-Wl,--enable-runtime-pseudo-reloc \
-Wl,--enable-auto-import \
-Wl,--enable-auto-image-base \
-Wl,--strip-all
LINKEXE = $(CC) $(c++FLAGS) \
-static-libgcc -static-libstdc++ \
-Wl,--enable-auto-import \
-Wl,--strip-all \
-Wl,--force-exe-suffix
逐行解读:
- 第 34--35 行:
-shared生成共享库,--output-def=$(LIB).def让链接器顺带写出一个.def;--out-implib=$(LIB).dll.a同时产出导入库。 - 第 38 行:
--no-undefined要求链接期无未定义符号;注释(第 31--32 行)说明它与--enable-runtime-pseudo-reloc不能 一起"宽容"未定义符号------必须把所有符号凑齐,这与".def显式导出"是配套的严格策略。 - 第 40--41 行:
--enable-auto-import/--enable-auto-image-base是 MinGW 兼容开关;--strip-all去符号表。 - 第 43--47 行:可执行文件额外
-static-libgcc -static-libstdc++(静态链接 libgcc/libstdc++,免于在 Windows 上再分发这两个 DLL ),--force-exe-suffix强制.exe。
但链接器自动写出的 .def 未必包含所有需要导出的 C++ mangled 符号 (模板实例、被优化掉的引用等)。于是 OpenFOAM 主动用 llvm-nm 抽取符号,补一份 .def。这就是 wmake/makefiles/general(第 113--125 行)的宏:
make
# For llvm-mingw, export symbols are needed in a few places.
# Filters the output of 'nm' xxx [BDRT] _ZFoam...
#
# Called with (input-file, output-file)
define LLVM_MINGW_EXPORT_DEF
@if test -f $(1); then \
echo "symbols: $(2)"; \
echo 'EXPORTS' >| $(2); \
llvm-nm --defined-only --extern-only $(1) \
| awk '$$2 ~ /[BDRT]/ && $$3 ~ /^_Z/ {print " " $$3}' \
| sort -u >> $(2); \
fi
endef
逐行解读:
- 第 117--118 行:定义 make 宏
LLVM_MINGW_EXPORT_DEF,入参是"输入文件"${1}、输出.def文件${2}。 - 第 119 行
@if test -f $(1):输入文件存在才做。 - 第 121 行
echo 'EXPORTS' >| $(2):>|是 make 转义后的>(强制覆盖写),第一行写入 DEF 文件必需的EXPORTS关键字。 - 第 122--123 行:
llvm-nm --defined-only --extern-only $(1)列出该.o中已定义且外部的符号 ;awk '$$2 ~ /[BDRT]/ && $$3 ~ /^_Z/ {print " " $$3}':$$2是 nm 输出的符号段/类型列 ,要求匹配[BDRT](B =BSS、D =数据、R =只读数据、T =代码文本段,即"需要导出"的几类);$$3是符号名列,要求以_Z开头------这是 Itanium C++ ABI 的 mangled 名前缀 (_Z后跟函数/变量编码)。满足两条件者,前面缩进四个空格写入.def。 - 第 124 行:
sort -u去重排序。 - 第 125 行
>> $(2):追加到.def。
这就是"
.def导出符号的生成流程" :llvm-nm抽_Z外部定义符号 →awk按段类型过滤 →sort -u→ 写进EXPORTS块。它把"人不愿手列几百个 mangled 名"这件事自动化了。
机制链 A(完整推导):MinGW 下 .def 从 .o 到 lld-link
makefile
源文件 .C/.cxx ──(x86_64-w64-mingw32-g++ -c, -Wa,-mbig-obj)──▶ .o(COFF,含 _Z 符号)
│
├─(wmake libo 目标)──▶ libX.o ──(LLVM_MINGW_EXPORT_DEF)──▶ libX.o.def(EXPORTS 块)
│
└─(wmake libso 目标, LINKLIBSO)──▶
x86_64-w64-mingw32-g++ -shared
-Wl,--output-def=libX.def ← 链接器自写一份 .def
-Wl,--out-implib=libX.dll.a ← 导入库
-Wl,--no-undefined ... ← 严格检查
──▶ libX.dll (+ libX.def + libX.dll.a)
重链(当需要"全导出"时,如 Pstream):
FOAM_EXTRA_CXXFLAGS="-Wl,libX.o.def"
──▶ 链接器按 .def 的 EXPORTS 强制导出全部列名符号
ARM64 交叉编译的 libo 特有分支(wmake/makefiles/general 第 273--278 行):
ifneq (,$(findstring linuxARM64Mingw,$(WM_OPTIONS)))
$E lld-link -lib -out:$(LIB).o $(OBJECTS) $(LIBO_LIBS)
$(call LLVM_MINGW_EXPORT_DEF,$(LIB).o,$(LIB).o.def)
else
$E $(LD) -r -o $(LIB).o $(OBJECTS) $(LIBO_LIBS)
endif
2.8 libo 目标:单对象库如何打破循环依赖
wmake/makefiles/general 对三种库目标做了对照实现。下面把 libso/lib/libo 三段并排给出(第 226--278 行),再逐段解释。
(a)libso:共享库(第 228--243 行)。分 Windows / 非 Windows 两支:
make
ifneq (,$(findstring windows,$(WM_OSTYPE)))
$(LIB)$(EXT_SO): $(OBJECTS)
$E $(LINKLIBSO) $(FOAM_EXTRA_LDFLAGS) $(OBJECTS) -L$(LIB_PLATFORMS) \
$(PROJECT_LIBS) $(LIB_LIBS) $(GLIB_LIBS) -o $(LIB)$(EXT_SO)
else
$(LIB)$(EXT_SO): $(OBJECTS)
$E $(LINKLIBSO) $(FOAM_EXTRA_LDFLAGS) $(OBJECTS) -L$(LIB_PLATFORMS) \
$(LIB_LIBS) $(GLIB_LIBS) -o $(LIB)$(EXT_SO)
endif
- 第 231 行:
findstring windows,$(WM_OSTYPE)------仅 Windows 下 才在链接命令加入$(PROJECT_LIBS)(-lOpenFOAM -ldl)。Linux 分支不加,因为 ELF 允许未解析符号延后到最终可执行文件;Windows DLL 必须链接期解析,所以要显式带上。这是"同一 Makefile 按 OS 分叉链接命令"的直接证据。
(b)lib:静态归档 (第 245--257 行):用 $(AR) $(ARFLAGS) $(LIB).a $(OBJECTS) 打包,再 $(RANLIB) 建索引。传统 Unix 静态链接,Windows 交叉编译一般不用。
(c)libo:单对象库(第 259--278 行)
make
# libo : single object lib
# Note: llvm-mingw expects lld-link with '-lib', '-out:...' flags.
# It is useful to create an .def export file from the output object
# (currently done manually)
.PHONY: libo
libo: $(LIB).o | silent
$(LIB).o: $(OBJECTS)
@$(WM_SCRIPTS)/makeTargetDir $(LIB)
@rm -f $(LIB).o
$(call QUIET_MESSAGE,ld,$(LIB).o)
ifneq (,$(findstring linuxARM64Mingw,$(WM_OPTIONS)))
$E lld-link -lib -out:$(LIB).o $(OBJECTS) $(LIBO_LIBS)
$(call LLVM_MINGW_EXPORT_DEF,$(LIB).o,$(LIB).o.def)
else
$E $(LD) -r -o $(LIB).o $(OBJECTS) $(LIBO_LIBS)
endif
- 第 260--264 行注释点明了两件事:llvm-mingw 需要
lld-link -lib -out:...参数形态;并且从输出对象生成.def是"currently done manually"(当前靠上述宏半自动完成)。 - 第 269 行目标
$(LIB).o:由所有.o合成一个大.o。 - 第 273 行:
linuxARM64Mingw走lld-link -lib -out:$(LIB).o(LLVM 工具链的"静默合并对象"模式),随后立刻调LLVM_MINGW_EXPORT_DEF产出.def。 - 第 277 行:其它平台走
$(LD) -r -o $(LIB).o------即ld -r(relocatable 链接 ,把多个.o合并成一个可再链接的.o,符号仍保留、无重定位丢失)。这一步是"打破循环依赖"的技术核心:合并成一个.o后,链接它时不再需要它反过来解析共享库符号。
机制链 B(完整推导):libo 如何打破 Pstream/OpenFOAM 循环依赖
python
问题:libOpenFOAM 需要 libPstream 的通信符号
libPstream 又需要 libOpenFOAM 的类型/工具
→ 在 DLL 模型下,二者互为"必须链接期解析"的依赖,成环。
解法(src/Allwmake 第 38--39 / 74--75 行):
1) Pstream/Allwmake libo
→ 不产 .dll,而产 libPstream.o(单对象,ld -r / lld-link -lib 合并)
2) FOAM_LINK_DUMMY_PSTREAM=libo wmake OpenFOAM
→ libOpenFOAM 链接时把 libPstream.o 直接吃进去
→ .o 里符号已"定义",不再触发"反过来需要 dll 导入"的成环解析
3) (ARM64 分支) 生成 libPstream.o.def → 用 -Wl,<def> 重链出全导出 dll
4) OpenFOAM/Alltouch → 强制 libOpenFOAM 重链到最终的 Pstream 产物
一句话 :ELF 上"循环依赖"可以靠
--start-group或"默认导出全部符号"糊过去;Windows DLL 上这两条都不成立 ,只能"把一侧压成单对象.o直接吞进去 + 用.def显式导出"。这就是 OpenFOAM 跨平台构建比"换个编译器"复杂的根本原因。
2.9 难题三:纯串行构建的符号 stub → LINKEXE_STUB
第三种"符号供应"问题是纯串行构建 :不需要任何 MPI,但代码仍引用 Pstream 符号。OpenFOAM 用"空实现 stub 库"解决。wmake/rules/General/Gcc/link-c++(第 1--34 行)关键段:
make
LINKEXE = $(CC) $(c++FLAGS)
undefine LINKEXE_STUB
# Use gold linker
ifneq (,$(findstring +gold,$(WM_COMPILE_CONTROL)))
LINKEXE += -fuse-ld=gold
LINKEXE_STUB := true
else
LINKLIBSO += -Xlinker --add-needed -Xlinker --no-as-needed
LINKEXE += -Xlinker --add-needed -Xlinker --no-as-needed
endif
# Link dummy stub to resolve libPstream symbols used by libOpenFOAM
ifneq (,$(LINKEXE_STUB))
ifneq (,$(findstring -lOpenFOAM,$(PROJECT_LIBS)))
LINKEXE += -L$(FOAM_LIBBIN)/dummy -lPstream
endif
endif
逐行解读:
- 第 8 行
undefine LINKEXE_STUB:默认未定义该开关(先清空,避免继承)。 - 第 13--18 行:若编译控制含
+gold,用 gold 链接器并打开LINKEXE_STUB := true。 - 第 20--24 行:否则用默认
ld,加-Xlinker --add-needed --no-as-needed------保证"看起来没被引用"的库也进 DT_NEEDED(防止符号被裁 );此分支不设LINKEXE_STUB。 - 第 29--33 行:当
LINKEXE_STUB打开且PROJECT_LIBS含-lOpenFOAM时 ,给可执行文件链接追加-L$(FOAM_LIBBIN)/dummy -lPstream,即链接 dummy stub 来"喂饱"libOpenFOAM用到的 Pstream 符号。
三种"符号供应"问题归纳(对比表):
| 场景 | 症状 | OpenFOAM 解法 | 关键文件/行 |
|---|---|---|---|
| ARM64 Mingw:Pstream↔OpenFOAM 成环 | DLL 导入导出互锁 | libo 单对象 + .def 全导出重链 |
src/Allwmake 33--69 |
| x86_64 Mingw:同上 | 同上 | libo 单对象 + 重链 |
src/Allwmake 70--79 |
| 纯串行构建缺 Pstream 符号 | 链接期 undefined | dummy stub(libPstream) |
wmake/rules/General/Gcc/link-c++ 28--33 |
| 需要全导出的 MinGW dll | 符号被裁 | LLVM_MINGW_EXPORT_DEF 生成 .def |
wmake/makefiles/general 113--125 |
3. MinGW 的两条规则链与 wmake 规则体系
3.1 规则目录的两级结构
wmake/rules/ 采用两级命名:
- 架构级 :
rules/<arch><compiler>/,例如linux64Mingw/、linuxARM64Mingw/、linux64Gcc/; - 编译器族级 :
rules/General/<family>/,例如rules/General/Gcc/、rules/General/Nvidia/; - 公共级 :
rules/General/与rules/General/common/。
选择逻辑在 wmake/rules/General/general(第 17--21、52--57 行):
make
ARCHITECTURE_RULES = $(WM_DIR)/rules/$(WM_ARCH)
COMPILER_FAMILY = $(shell echo "$(WM_COMPILER)" | sed -e 's/[-+.0-9~].*//')
DEFAULT_RULES = $(ARCHITECTURE_RULES)$(COMPILER_FAMILY)
RULES = $(ARCHITECTURE_RULES)$(WM_COMPILER)
make
include $(DEFAULT_RULES)/general
sinclude $(DEFAULT_RULES)/override
ifneq ("$(COMPILER_FAMILY)","$(WM_COMPILER)")
sinclude $(RULES)/general
sinclude $(RULES)/c++
endif
逐行解读:
- 第 17 行:
ARCHITECTURE_RULES = wmake/rules/$(WM_ARCH)。注意此处用${WM_ARCH}(如linux64),不含编译器。 - 第 18 行:
COMPILER_FAMILY用sed -e 's/[-+.0-9~].*//'把WM_COMPILER里从第一个-/+/./数字/~起的后缀全部截掉 。于是Mingw→Mingw,Clang-14→Clang,Gcc-11→Gcc。这就是"编译家族"的提取规则。 - 第 19 行:
DEFAULT_RULES = rules/<arch><family>,如rules/linux64Mingw。 - 第 20 行:
RULES = rules/<arch><compiler>,带完整编译器名(含版本后缀)。 - 第 52 行:先
include $(DEFAULT_RULES)/general------必定存在,提供编译器族通用设置。 - 第 53 行:
sinclude .../override------可选覆盖文件。 - 第 54--57 行:当"家族"与"完整编译器名"不同时 (例如
MingwvsGcc-11这种带版本后缀的情形),再额外sinclude $(RULES)/general与$(RULES)/c++,作为编译器版本专属的覆盖。
这一条链解释了 wmake/rules/linux64Mingw/ 下为何既有一批文件、又要 include $(GENERAL_RULES)/Gcc/c++。
3.2 wmake/rules/linux64Mingw/ 目录内容逐项
先列出该目录下实际存在的文件(本仓库真实内容):
| 文件 | 作用 |
|---|---|
general |
交叉工具链三件套:CPP/LD/AR/RANLIB + PROJECT_LIBS + 后缀 |
c / c++ |
C/C++ 编译命令与架构标志 |
cOpt/cDebug/cProf |
C 的 Opt/Debug/Prof 优化项 |
c++Opt/c++Debug/c++Prof |
C++ 的 Opt/Debug/Prof 优化项 |
mplibMSMPI |
MS-MPI 的 include/lib 设置 |
逐文件读。
wmake/rules/linux64Mingw/general(第 1--19 行,全文)
sh
# Need -D_WIN32 -D__MINGW32__ if host cpp is used
#CPP = /lib/cpp -traditional-cpp -D_WIN32 -D__MINGW32__ $(GFLAGS)
CPP = x86_64-w64-mingw32-cpp -traditional-cpp $(GFLAGS)
LD = x86_64-w64-mingw32-ld
AR = x86_64-w64-mingw32-ar
RANLIB = x86_64-w64-mingw32-ranlib
PROJECT_LIBS = -l$(WM_PROJECT) \
-L$(FOAM_LIBBIN)/dummy -lPstream
include $(GENERAL_RULES)/standard
include $(DEFAULT_RULES)/c
include $(DEFAULT_RULES)/c++
EXT_SO = .dll
EXT_EXE = .exe
逐行解读:
- 第 2--4 行:若用宿主 cpp 需补
-D_WIN32 -D__MINGW32__,但最终采用第 5 行 mingw 自带cpp,其预定义宏天然是目标平台的。 - 第 5 行:
x86_64-w64-mingw32-cpp -traditional-cpp-------traditional-cpp让预处理更像 K&R 预处理器(wmake 依赖生成依赖此行为)。 - 第 6--8 行:
LD/AR/RANLIB全换成x86_64-w64-mingw32-*------"交叉"的字面体现:宿主 ld/ar 不能处理 Windows COFF 目标。 - 第 9--10 行:
PROJECT_LIBS默认就把 dummy Pstream 加进来 ,与LINKEXE_STUB同目的(Windows 下倾向无条件带上)。 - 第 17--18 行:
EXT_SO=.dll、EXT_EXE=.exe------输出后缀改 Windows 约定。
wmake/rules/linux64Mingw/c++(第 6--22 行关键段)
sh
include $(GENERAL_RULES)/Gcc/c++
CC := x86_64-w64-mingw32-g++ -std=gnu++17
c++ARCH := -m64 -pthread
ifneq (,$(strip $(WM_COMPILE_OPTION)))
sinclude $(DEFAULT_RULES)/c++$(WM_COMPILE_OPTION)
endif
c++FLAGS = \
$(c++ARCH) $(GFLAGS) $(c++WARN) $(c++OPT) $(c++DBUG) $(ptFLAGS) \
$(FOAM_EXTRA_CXXFLAGS) $(LIB_HEADER_DIRS)
c++FLAGS += -Wa,-mbig-obj
逐行解读:
- 第 6 行:
include $(GENERAL_RULES)/Gcc/c++------复用 Gcc 家族设置(见 3.3)。 - 第 8 行:
x86_64-w64-mingw32-g++ -std=gnu++17。用gnu++17而非c++17(第 4 行注释),因为c++17会定义__STRICT_ANSI__,屏蔽 GCC 扩展与某些 MinGW 头行为。 - 第 9 行:
-m64 -pthread(64 位 + posix 线程模型)。 - 第 11--13 行:按编译选项
sinclude对应优化文件 (Opt→linux64Mingw/c++Opt)。 - 第 22 行:
-Wa,-mbig-obj------注释(第 19--21 行):某些文件(如rhoReactionThermos)超过传统 16 位 COFF section 编号上限 ,须用大对象格式。这是 x86_64-w64-mingw32 特有的"补丁式编译标志"。
各优化档位文件(全文极短) :cOpt(cDBUG=-DNDEBUG; cOPT=-O3)、c++Opt(c++DBUG=-DNDEBUG; c++OPT=-O3; ROUNDING_MATH=-frounding-math)、cDebug(-gstabs3 -DFULLDEBUG; -O1 -finline-functions)、c++Debug(-gstabs3 -DFULLDEBUG; -O0 -fdefault-inline)、cProf/c++Prof(-gstabs3 -pg; -O2)。
注意
-gstabs3:目标是 Windows,调试信息用 stabs (MinGW gas 支持)而非 DWARF;-pg是gprof插桩。二者都是"交叉编译特有的调试/剖析符号格式"。
wmake/rules/linux64Mingw/mplibMSMPI(第 1--3 行) :PFLAGS=-DMPICH_SKIP_MPICXX -DOMPI_SKIP_MPICXX(屏蔽第三方 MPI 的 C++ 接口)、PINC=-I$(MPI_ARCH_PATH)/include、PLIBS=-L$(MPI_ARCH_PATH)/lib -L$(MPI_ARCH_PATH)/lib/x64 -lmsmpi(MS-MPI 的 lib/lib/x64 布局,链接 -lmsmpi)。
3.3 Gcc 家族与 common 规则:跨平台共用的那一半
wmake/rules/General/Gcc/c++(第 1--29 行)关键段:
sh
SUFFIXES += .C .cc .cpp .cxx
CC := g++$(COMPILER_VERSION) -std=c++17
c++OPT := -O2
ptFLAGS := -DNoRepository -ftemplate-depth=1000
c++WARN = \
-Wall -Wextra -Wold-style-cast \
-Wnon-virtual-dtor -Wno-unused-parameter -Wno-invalid-offsetof \
-Wno-attributes -Wno-unknown-pragmas
c++LESSWARN = -Wno-old-style-cast -Wno-unused-local-typedefs -fpermissive
sinclude $(GENERAL_RULES)/common/c++$(WM_COMPILE_OPTION)
- 第 4 行
SUFFIXES += .C .cc .cpp .cxx:make 后缀规则;.C大写是 OpenFOAM 传统 C++ 后缀。 - 第 6 行:
g++$(COMPILER_VERSION) -std=c++17。$(COMPILER_VERSION)由rules/General/general第 29--33 行从WM_COMPILE_CONTROL的version=提取(如-11)。 - 第 10--11 行:
-DNoRepository关闭模板仓库链接(OpenFOAM 用 lnInclude 实例化);-ftemplate-depth=1000放宽模板递归深度。 - 第 13--19 行:默认警告集 ;
-Wno-*是有意放行的(OpenFOAM 代码风格需要)。 - 第 21--25 行:
c++LESSWARN用于 flex++/CGAL 等生成或第三方代码。 - 第 28 行:
sinclude .../common/c++$(WM_COMPILE_OPTION)------引入公共优化档。
wmake/rules/General/common/ 的档位文件 :common/c++Opt(c++DBUG:=、c++OPT:=-O3)、common/c++Debug(-g -DFULLDEBUG; -O0)、common/cDebug(同)。
覆盖顺序:
linux64Mingw/c++Opt(带ROUNDING_MATH)与General/common/c++Opt同时被sinclude,Mingw 目录那份更具体、优先------体现 OpenFOAM 规则体系"越具体越优先"的分层思想。
3.4 MinGW 规则链对比表
| 层级 | POSIX/Linux(linux64Gcc) |
MinGW 交叉(linux64Mingw) |
|---|---|---|
| 预处理 / 编译 | 系统 cpp;g++ -std=c++17 |
x86_64-w64-mingw32-cpp -traditional-cpp;x86_64-w64-mingw32-g++ -std=gnu++17 |
| 架构 / 对象 | 无 | -m64 -pthread;-Wa,-mbig-obj |
| 链接器 / 后缀 | ld;.so / 空 |
x86_64-w64-mingw32-ld(ARM64 用 lld-link);.dll / .exe |
| 链接命令 | -shared 默认全导出 |
libso -shared --output-def/--out-implib --no-undefined ...;exe 加 -static-libgcc -static-libstdc++ --force-exe-suffix |
| 符号导出 | 自动 | .def(链接器 + LLVM_MINGW_EXPORT_DEF) |
| 循环依赖 | --start-group 可解 |
libo 单对象 |
| 调试 / 剖析 | DWARF;-pg |
-gstabs3;-gstabs3 -pg(Prof 档) |
| MPI / 优化 / 链接库 | OpenMPI;-O3;-lOpenFOAM -ldl |
MS-MPI(-lmsmpi);-O3 + -frounding-math;-lOpenFOAM -ldl + dummy/-lPstream |
4. 三个机制链汇总(交叉编译部分)
为便于回顾,交叉编译的三条机制链概述:A(.def 导出) = 从含 _Z 符号的 .o,经 --output-def 自写、LLVM_MINGW_EXPORT_DEF(llvm-nm+awk [BDRT]+awk ^_Z+sort -u)、-Wl,<def> 重链,得到全导出 .dll;B(libo 破环) = Pstream 源文件经 wmake libo(ld -r / lld-link -lib 合并为单 .o)后由 OpenFOAM 直接吞入,消除循环依赖;C(stub 供符号) = 纯串行构建下经 LINKEXE_STUB=true(gold 时)或 PROJECT_LIBS 默认带的 -L$(FOAM_LIBBIN)/dummy -lPstream 供符号。
本节核心结论:MinGW 交叉编译的 90% 工作量不在"换编译器",而在"换符号模型"------Windows DLL 的显式导出(.def)与链接期严格解析(--no-undefined),迫使 OpenFOAM 用"单对象 libo + .def 全导出 + dummy stub"三件套重写链接流程。
补充一条交叉编译环境搭建清单(模板 T6,可直接照做):
bash
# 0) 在 Linux 宿主安装 mingw-w64 交叉工具链(Debian/Ubuntu 示例)
sudo apt install mingw-w64 # 提供 x86_64-w64-mingw32-{gcc,g++,cpp,ld,ar,ranlib}
# 1) 使用 etc-mingw 下的偏好(FOAM_CONFIG_ETC=etc-mingw)
export FOAM_CONFIG_ETC=etc-mingw
# 2) 指定编译器族为 Mingw、MPI 为 MS-MPI(prefs.sh 已设默认)
export WM_COMPILER=Mingw
export WM_MPLIB=msmpi-10.0
# 3) 确认 WM_OSTYPE 变为 MSwindows(由 settings 依据 WM_COMPILER 自动设置)
source $WM_PROJECT_DIR/etc/bashrc # 或对应 shell 的启动脚本
echo "WM_OPTIONS=$WM_OPTIONS" # 期望形如 linux64MingwDPInt32Opt
echo "WM_OSTYPE=$WM_OSTYPE" # 期望 MSwindows
# 4) 构建(顶层 Allwmake 会自动走到 src/Allwmake 的 MinGW case)
./Allwmake # 或:./Allwmake -j -s
# 5) 检查产物后缀:应为 .exe/.dll
ls $FOAM_LIBBIN | head # 期望 libOpenFOAM.dll、dummy/libPstream.o(.def)
ls $FOAM_APPBIN | head # 期望 *.exe
逐项说明表:
| 步骤 | 命令要点 | 校验点 | 常见失败 |
|---|---|---|---|
| 0 | 装 mingw-w64 |
which x86_64-w64-mingw32-g++ 有输出 |
未装工具链 → wmake 找不到 CC |
| 1 | FOAM_CONFIG_ETC=etc-mingw |
配置取自 etc-mingw/config.sh/* |
忘设 → 用到宿主 FFTW/CGAL |
| 2 | WM_COMPILER=Mingw |
WM_COMPILER 为 Mingw |
设成 Gcc → 走错规则目录 |
| 3 | source etc/bashrc |
WM_OPTIONS 含 Mingw、WM_OSTYPE=MSwindows |
环境未生效 |
| 4 | ./Allwmake |
进入 src/Allwmake 的 */Mingw* 分支 |
循环依赖未解 → 链接 undefined |
| 5 | 看产物 | 后缀 .exe/.dll、存在 .def |
符号被裁 → 运行期 DLL 加载失败 |
5. 性能剖析 profiling:开启方式与数据结构
5.1 一句话开启:controlDict 里的 profiling {}
src/OpenFOAM/global/profiling/profiling.H 的类注释(第 30--48 行)就是使用说明:
csharp
Description
Code profiling.
This is typically activated from within system/controlDict as follows
(defaults shown):
\code
profiling
{
active true;
cpuInfo false;
memInfo false;
sysInfo false;
}
\endcode
or simply using all defaults:
\code
profiling
{}
\endcode
逐行解读:
- 第 33 行:明确"activate from within system/controlDict"------入口就是字典 ,无需重编译、无需
#ifdef。 - 第 35--42 行:给出带默认值的完整写法:
active=true、cpuInfo=false、memInfo=false、sysInfo=false。注意默认只开 active,不开 cpuInfo/memInfo/sysInfo(后三者开销更大或信息更"静态")。 - 第 44--48 行:
profiling {}即"全用默认"。
模板 T1:system/controlDict 的 profiling 片段(可直接粘贴)
ruby
profiling
{
active true; // 总开关:profiling::initialize() 是否构造单例
cpuInfo true; // 输出 CPU 型号/核数(cpuInfo 对象)
memInfo true; // 输出内存 hwm/rss/size/free(memInfo 对象)
sysInfo true; // 输出系统信息(profilingSysInfo)
}
开关项对比表(C4):
| 开关 | 默认 | 含义(含输出位置) |
|---|---|---|
active |
true |
总开关;为 false 时 setMonitoring 走"跳过"分支(决定是否打印整个 profiling {} 块) |
cpuInfo |
false |
是否实例化 cpuInfo(厂商/型号/核数),一次性探测,块尾写 cpuInfo {} |
memInfo |
false |
是否实例化 memInfo 并在每次 New 时更新峰值(每计时点一次 ),块尾写 memInfo {} |
sysInfo |
false |
是否实例化 profilingSysInfo(主机名等),块尾写 sysInfo {};关时仍写 host <名字> 保底 |
5.2 Time 如何初始化 profiling:setMonitoring
入口在 src/OpenFOAM/db/Time/Time.C 的 Time::setMonitoring(第 350--397 行):
cpp
void Foam::Time::setMonitoring(const bool forceProfiling)
{
const dictionary* profilingDict = controlDict_.findDict("profiling");
if (!profilingDict)
{
// ... or from etc/controlDict
profilingDict = debug::controlDict().findDict("profiling");
}
if (forceProfiling)
{
profiling::initialize(IOobject("profiling", timeName(), "uniform", *this,
IOobjectOption::NO_READ, IOobjectOption::AUTO_WRITE,
IOobjectOption::REGISTER), *this);
}
else if
(
profilingDict
&& profilingDict->getOrDefault("active", true)
)
{
profiling::initialize(*profilingDict,
IOobject("profiling", timeName(), "uniform", *this,
IOobjectOption::NO_READ, IOobjectOption::AUTO_WRITE,
IOobjectOption::REGISTER), *this);
}
}
逐行解读:
- 第 352 行:先在本算例的
controlDict_里找profiling子字典。 - 第 353--357 行:找不到时回退到
etc/controlDict(全局debug::controlDict())------故可"全局开启"或"按算例开启"。 - 第 362--378 行:
forceProfiling分支 (来自命令行-profiling),走"全开"重载(cpuInfo/memInfo/sysInfo 全开)。 - 第 379--397 行:否则检查
profilingDict存在且active不为 false,才调用带字典的重载------逐项读取开关。 - IOobject 名为
"profiling"、instance 为"uniform":报告写到<time>/uniform/profiling(与时间步无关)。NO_READ/AUTO_WRITE/REGISTER说明它是"只写不读、自动落盘、登记到注册表"。
命令行侧在 Time.C 第 560--561 行:setMonitoring(args.found("profiling"));(-profiling = force profiling, ignore controlDict entry),以及第 692 行 setMonitoring(); // for profiling etc。
开启 profiling 有两条路 :①
controlDict(或etc/controlDict)加profiling {};② 命令行-profiling强制全开。二者最终都落到profiling::initialize(...)。
5.3 profiling 类的静态单例与核心数据成员
profiling.H(第 82--137 行)的类骨架(只列数据成员):
cpp
class profiling : public IOdictionary
{
public:
typedef profilingInformation Information;
typedef profilingTrigger Trigger;
static int allowed; // 是否允许 profiling
private:
static std::unique_ptr<profiling> singleton_; // 全进程唯一实例
const Time& owner_; // 持有者
PtrDynList<Information> pool_; // 计时器池(所有权)
DynamicList<DynamicList<Information*>> children_; // 父子关系表
DynamicList<Information*> stack_; // LIFO 活动栈
DynamicList<clockValue> times_; // 平行时钟栈
std::unique_ptr<sysInfo> sysInfo_; // 可选
std::unique_ptr<cpuInfo> cpuInfo_; // 可选
std::unique_ptr<memInfo> memInfo_; // 可选
};
逐个数据成员解读("计时树数据结构"的核心):
- 第 84 行:继承
IOdictionary------所以能write()落盘为字典 (报告就是<time>/uniform/profiling),这正是profilingSummary能用IOdictionary::readContents读它的原因。 - 第 97 行
static int allowed:由debug::infoSwitch("allowProfiling", 1)初始化------即使代码里到处addProfiling,只要allowed==0就全部空转。 - 第 109 行
singleton_:全进程唯一实例(注释 "Only one global object is possible")。 - 第 115 行
owner_:持有者Time,用于stop时判断是否同一个 Time。 - 第 118 行
pool_:所有计时节点new后入池,统一内存管理。对象所有权层。 - 第 121 行
children_:children_[i]是"id 为 i 的计时器"的所有子指针列表。负责调用树拓扑(复用判断、写出结构都靠它)。 - 第 124 行
stack_:LIFO 活动栈,进入计时push_back、退出pop_back。 - 第 127 行
times_:与stack_平行的时钟栈。两者必须严格等长、同进同出 ,这是writeData能正确算活动项耗时的前提。 - 第 130/133/136 行:三个可选信息对象,由
controlDict的sysInfo/cpuInfo/memInfo决定是否new。
关系示意 :pool_ 是全部节点(所有权);children_ 是父子边;stack_/times_ 是当前活动链及其时刻。profiling.H 第 190--223 行还声明受保护的 beginTimer/endTimer 原语、initialize 两个重载、stop(owner)、New(descr)(对外核心入口 )与 unstack(info)(结束已知节点计时并校验栈顶一致)。
5.4 allowed 与单例的生命周期
profiling.C(第 39--41 行):
cpp
int Foam::profiling::allowed(Foam::debug::infoSwitch("allowProfiling", 1));
std::unique_ptr<Foam::profiling> Foam::profiling::singleton_(nullptr);
- 第 39 行:
allowed用debug::infoSwitch("allowProfiling", 1)初始化------即可以被运行期InfoSwitches{ allowProfiling 0; }或环境变量关闭。默认 1(允许)。 - 第 41 行:
singleton_初值为nullptr(未初始化)。
active()/disable()/initialize/stop(第 115--182 行):
cpp
bool Foam::profiling::active() noexcept { return allowed && singleton_; }
void Foam::profiling::disable() noexcept { allowed = 0; }
void Foam::profiling::initialize(const IOobject& ioObj, const Time& owner)
{
if (allowed && !singleton_) { singleton_.reset(new profiling(ioObj, owner)); }
}
void Foam::profiling::stop(const Time& owner)
{
if (singleton_ && &owner == &(singleton_->owner_)) { singleton_.reset(nullptr); }
}
- 第 115--118 行:
active()同时要求allowed真且 单例存在------所有计时原始语第一道门。 - 第 121--124 行:
disable()直接allowed=0(profilingSummary开篇就调它)。 - 第 149--159 行:
initialize条件allowed && !singleton_------已存在单例不再重建(单例语义)。 - 第 176--182 行:
stop(owner)只在"当前单例的 owner 就是传入 owner"时才销毁,避免误关别的Time。另有两个转发接口print(os)→writeData、writeNow()→regIOobject::write()(主动落盘)。
生命周期 :
Time构造 →setMonitoring→profiling::initialize(建单例)→ 各处New(...)压栈出栈 → 单例销毁,但报告已通过AUTO_WRITE落盘到<time>/uniform/profiling。
6. profiling 计时树:机制链与报告解读
6.1 顶层重置与子节点查找/新建:create()
profiling.C(第 46--88 行)。第一个重载是"顶层重置 ":pool_/children_/stack_/times_ 全部 clear(),再 new Information(默认构造把 description_="application::main"、parent_=this、id_=0,根节点描述固定为 application::main、父指针自指表示顶层 ),入池并把 children_ 扩到同长。第二个重载是"按 (父, 描述) 找或建":
cpp
Foam::profilingInformation* Foam::profiling::create
(
profilingInformation *parent,
const std::string& descr
)
{
const label parentId = parent->id();
for (Information* child : children_[parentId])
{
if (descr == child->description())
{
return child; // Found existing
}
}
Information* info = new Information(parent, descr, pool_.size());
pool_.push_back(info);
children_.resize(pool_.size());
children_.back().clear(); // safety
children_[parentId].push_back(info);
return info;
}
逐行:
- 第 70 行:取父节点 id。
- 第 72--78 行:线性扫描
children_[parentId],已有同description的子节点则直接复用 (第 76 行Found existing)。这是"同一代码位置反复addProfiling会复用同一计时器、累加次数"的关键。 - 第 80 行:新节点 id 取当前
pool_.size()(自增唯一 id)。 - 第 82--85 行:入池、扩
children_、挂到父的 children 列表。
6.2 压栈与出栈:beginTimer/endTimer
profiling.C(第 91--110 行):
cpp
void Foam::profiling::beginTimer(profilingInformation *info)
{
stack_.push_back(info);
times_.push_back(clockValue::now());
info->setActive(true); // Mark as on stack
}
Foam::profilingInformation* Foam::profiling::endTimer()
{
Information *info = stack_.back();
clockValue clockval = times_.back();
stack_.pop_back();
times_.pop_back();
info->update(clockval.elapsed()); // Update elapsed time
info->setActive(false); // Mark as off stack
return info;
}
逐行解读:
beginTimer:- 第 93 行:
stack_.push_back(info)------把节点压入活动栈。 - 第 94 行:
times_.push_back(clockValue::now())------记录进入时刻 。clockValue是一个"可相减得到 elapsed 的时钟值"类型。 - 第 95 行:
info->setActive(true)------标记"在栈上"。
- 第 93 行:
endTimer:- 第 101--104 行:从
stack_/times_各自弹出栈顶(必须成对)。 - 第 106 行:
info->update(clockval.elapsed())------用进入时刻算出 elapsed,交给节点累加。 - 第 107 行:
setActive(false)------标记"离开栈"。 - 第 109 行:返回该节点指针(供调用方比较)。
- 第 101--104 行:从
这就是计时树的两条原语 :
beginTimer= 压栈 + 记时 + 置 active;endTimer= 出栈 + 算差 + 置 inactive。profiling::New与unstack都建立在它们之上。
6.3 对外入口 New 与 unstack
profiling.C(第 185--230 行):
cpp
Foam::profilingInformation* Foam::profiling::New(const std::string& descr)
{
Information *info = nullptr;
if (active())
{
Information *parent = singleton_->stack_.back();
info = singleton_->create(parent, descr);
singleton_->beginTimer(info);
if (singleton_->memInfo_)
{
singleton_->memInfo_->update();
info->maxMem_ = Foam::max(info->maxMem_, singleton_->memInfo_->size());
}
}
return info;
}
void Foam::profiling::unstack(const profilingInformation *info)
{
if (active() && info)
{
Information *top = singleton_->endTimer();
if (info->id() != top->id())
{
FatalErrorInFunction << "Profiling information to unstack has different id than"
<< " the top of the profiling stack" << nl << ... << abort(FatalError);
}
}
}
逐行解读:
New第 189 行:if (active())------门口检查 ,未启用直接返回nullptr。- 第 191 行:
parent = stack_.back()------当前栈顶就是新节点的父。这就是"调用树"的建立方式:计时纵深即代码调用纵深。 - 第 193--194 行:
create(parent, descr)找或建 →beginTimer(info)压栈开始计时。 - 第 196--205 行:若
memInfo_在,刷新内存并更新该节点maxMem_。每次New都更新一次内存峰值 ,成本与计时点数成正比------故memInfo默认关闭。 unstack第 216 行:endTimer()弹出栈顶。- 第 218--228 行:若传入
info与栈顶 id 不一致,直接FatalError中止 并打印两者 id 与描述。这保证"每个addProfiling的 RAII 对象析构时弹出的正是它自己压的那个节点"------不匹配就是漏配对了addProfiling/endProfiling。
6.4 节点数据模型:profilingInformation
profilingInformation.H(第 61--85 行)私有数据:
cpp
//- Unique id to identify it
const label id_;
//- What this timer does
const std::string description_;
//- Pointer to the parent object (or self for top-level)
profilingInformation* parent_;
//- Number of times this was called
long calls_;
//- Total time spent
scalar totalTime_;
//- Time spent in children
scalar childTime_;
//- Max memory usage on call.
mutable int maxMem_;
//- Is this information active or passive (ie, on the stack)?
mutable bool active_;
逐项:
id_(const):唯一标识,等于入池时的pool_.size()。description_(const std::string):计时段描述,如"application::main"或用户addProfiling给的串。parent_:父节点指针;顶层自指。calls_:被调次数。totalTime_:累计总时间。childTime_:子节点花费的时间(用于"自身净时间 = totalTime - childTime")。maxMem_:调用期间内存峰值(memInfo开时有效)。active_:是否在栈上。
累加逻辑在 profilingInformation.C(第 68--77 行):
cpp
void Foam::profilingInformation::update(const scalar elapsed)
{
++calls_;
totalTime_ += elapsed;
if (id_ != parent_->id())
{
parent_->childTime_ += elapsed;
}
}
- 第 70 行:
++calls_------次数 +1。 - 第 71 行:
totalTime_ += elapsed------总时间累加。 - 第 73--76 行:若不是顶层(
id_ != parent_->id()) ,把 elapsed 同时累加到父的childTime_。这正是"父的 childTime 汇总所有子耗时"的落点;顶层自指被排除,避免自我累加。
机制链 C(完整推导):一次 addProfiling 在计时树里发生了什么
scss
addProfiling(Name, "描述") 展开为:
::Foam::profilingTrigger profilingTriggerFor##Name(__VA_ARGS__)
│
├─ profilingTrigger 构造:possible()? 若 profiling::active() 则 enter(name)
│ │
│ └─ enter(name) → profiling::New(name)
│ ├─ 若 active():
│ │ parent = stack_.back() ← 父=当前栈顶
│ │ info = create(parent, name) ← 池中找/建
│ │ └─ 命中 children_[parent] 则复用;否则 new 并入池
│ │ beginTimer(info) ← stack_.push_back + times_.push_back(now)
│ └─ 若 memInfo_ 开:刷新并更新 info->maxMem_
│
└─ 作用域结束时 ~profilingTrigger() → stop() → profiling::unstack(ptr_)
├─ 若 active() && ptr_:
│ top = endTimer() ← pop stack_/times_、info->update(elapsed)
│ if (ptr_->id() != top->id()) → FatalError ← 配对校验
└─ 结束
对应宏与类在 profilingTrigger.H:
- 第 157--158 行:
#define addProfiling(Name, ...) ::Foam::profilingTrigger profilingTriggerFor##Name(__VA_ARGS__)------用##把Name拼进变量名 (profilingTriggerForName),于是同一作用域内每个Name唯一。RAII:作用域结束自动析构。 - 第 162 行:
#define endProfiling(Name) profilingTriggerFor##Name.stop()------可以手动提前结束。 - 第 167--173 行:
addProfilingInFunction(Name),GCC 用__PRETTY_FUNCTION__,其它用__func__------描述自动取函数名。
profilingTrigger 的模板构造(第 119--129 行)与 trigger(第 88--94 行):
cpp
template<class... StringArgs>
explicit profilingTrigger(StringArgs&&... description) : ptr_(nullptr)
{
if (possible())
{
// Delay string concatenation until actually needed
trigger(std::forward<StringArgs>(description)...);
}
}
...
template<class... StringArgs>
void trigger(std::string name, StringArgs&&... args)
{
string_concat(name, std::forward<StringArgs>(args)...);
enter(name);
}
- 第 124 行:
if (possible())------先判断 profiling 是否可用,再拼字符串。注释 "Delay string concatenation until actually needed" 直接说明:profiling 关时不付字符串拼接代价。这是"零开销"设计的体现。 trigger把可分片描述拼成完整串后交给enter。
6.5 报告结构:writeData() 如何打印计时树
profiling.C(第 308--378 行)。先用函数内静态缓冲 elapsed 记录"当前仍在栈上的各项从进入到现在的耗时":
cpp
bool Foam::profiling::writeData(Ostream& os) const
{
static DynamicList<scalar> elapsed;
const clockValue now(clockValue::now());
const label nstack = stack_.size();
elapsed.resize(nstack+1); // extend for last entry, which has no child.
for (label stacki=0; stacki < nstack; ++stacki)
{
elapsed[stacki] = (now - times_[stacki]);
}
elapsed.back() = 0;
os.beginBlock("profiling");
// Active items
for (label stacki=0; stacki < nstack; ++stacki)
{
if (stacki) os << nl; // Extra line between entries
stack_[stacki]->write(os, true, elapsed[stacki], elapsed[stacki+1]);
}
// Non-active items
for (const Information& info : pool_)
{
if (!info.active()) { os << nl; info.write(os); }
}
os.endBlock();
if (sysInfo_) { os << nl; sysInfo_->writeEntry("sysInfo", os); }
else { os << nl; os.writeEntry("host", Foam::hostName()); }
if (cpuInfo_) { os << nl; cpuInfo_->writeEntry("cpuInfo", os); }
if (memInfo_) { os << nl; memInfo_->update(); os << nl; memInfo_->writeEntry("memInfo", os); }
return os.good();
}
逐行:
- 第 310 行:
static DynamicList<scalar> elapsed------函数内静态缓冲,避免每次分配。 - 第 314 行:
nstack = stack_.size()------此刻还嵌套在多少层计时段里。 - 第 318--322 行:对每个在栈节点算
elapsed[i] = now - times_[i];最后一格置 0。 - 第 326--338 行:先写"活动项" (仍在栈上),
info.write(os, true, elapsed[i], elapsed[i+1]):offset=true表示"次数补 1",并把elapsed[i]当额外 elapsed、elapsed[i+1](子项耗时)当 childTimes。 - 第 340--348 行:再写"非活动项"(已出栈、已累计完的)。
- 第 352--375 行:按开关写
sysInfo{}(关时至少写host <名字>保底)、cpuInfo{}、memInfo{}(落盘前update()一次)。
节点自身写出在 profilingInformation.C(第 88--110 行):块名 trigger<id>,依次写 id、parentId(仅非顶层)、description、calls(活动项 +1)、totalTime、childTime、maxMem(仅非 0)、active(yes/no)。
6.6 报告怎么读
报告是一份 OpenFOAM 字典,形如:
ini
profiling
{
trigger0 { id 0; description application::main; calls 1; totalTime 12.34; childTime 12.30; active no; }
trigger1 { id 1; parentId 0; description lduMatrix::solve; calls 567;
totalTime 8.21; childTime 0.05; maxMem 123456; active no; }
...
}
host myhost
cpuInfo { ... }
memInfo { ... }
解读四步法:
- 看
totalTime - childTime(自身净时间)排序。占比最高的几项就是真正的瓶颈 ;只看totalTime会被"父节点包含子节点"误导。 - 看
calls:次数异常多(百万级)而单次极短的项,往往是可被合并/向量化/减少调用的热点(如逐面循环里的某个函数)。 - 看调用树(
parentId链) :沿parentId回溯,能找到"最外层耗时入口"(如某求解器的solve),再逐层下钻。 - 看
maxMem:内存峰值高的节点提示"这里一次性分配了大对象",可能是tmp/Field未复用之处。
常见瓶颈排序(经验):线性求解器(PCG/GAMG/PBiCGStab)> 梯度/插值(grad/interpolate)> I/O(读写场/网格)> 边界条件与 correctBoundaryConditions。
注意 :profiling 本身有开销(每次 New 至少两次 DynamicList 的 push_back,memInfo 开时还有一次系统调用);别在生产运行里一直开------先用它定位,再关掉跑正式算例。
7. profiling 进阶:profilingSummary 多进程汇总与优化清单
7.1 并行算例的 profiling 分散在各 processor 目录
并行运行时,每个 processorN/ 各自写自己的 <time>/uniform/profiling。要得到"全局视角",OpenFOAM 提供 profilingSummary,位于 applications/utilities/postProcessing/miscellaneous/profilingSummary/。
profilingSummary.cxx(第 62--66 行)定义了要汇总的字段:
cpp
const HashTable<wordList> processing
{
{ "memInfo", { "hwm", "rss", "size", "free" } },
{ "profiling", { "calls", "totalTime", "childTime", "maxMem" } },
};
- 第 63 行:
memInfo汇总hwm/rss/size/free;第 64 行:profiling汇总calls/totalTime/childTime/maxMem。不含description/id/active(它们是标识而非数值)。
第 83--86 行先主动关闭 profiling :profiling::disable();(→ allowed=0),虽本工具不触发计时,但显式关掉以免混淆信息。
核心汇总逻辑(第 356--378 行):
cpp
for (const word& tag : tags)
{
const auto& values = stats(tag);
if (values.size() == nProcs)
{
MinMax<scalar> limits(values);
scalar avg = Foam::sum(values) / nProcs;
if (limits.min() < limits.max())
{
outSubDict.set(tag,
FixedList<scalar, 3>{ limits.max(), avg, limits.min() });
}
}
}
- 第 361 行:只有凑齐
nProcs个值才汇总 (缺一个就continue,见第 321--324 行)。 - 第 363--364 行:
MinMax<scalar>求极值、avg = sum / nProcs。 - 第 366--376 行:仅当 min<max(进程间不一致)时才写
(max avg min)三元组;全同则保留单值(注释:"If the values are identical for all processes, only a single value is written.")。
第 382--427 行还做按主机的内存高水位汇总 :优先读 memInfo.hwm(退化 peak),主机名优先从顶层 host 取(退化 sysInfo.host),按 host 归并 mem_hwm 并求和,写进 memInfo.hwm_total.<host>------因为同一主机上多个 rank 共享物理内存,按主机求和才反映真实占用。
7.2 性能优化的六类手段(结合两季)
结合两季,OpenFOAM 的主要优化手段有:tmp 的 reuse (第一季第 6 章,跨算子零拷贝、复用同一块内存)、表达式模板 + fused (第一季第 6 章,编译期把 a+b*c 折叠成一个循环、零临时、FMA 融合)、LDU 的 CSR 变体 (第一季第 11 章,Amul 由按面循环改为按单元循环以改善缓存)、OpenMP (WM_COMPILE_CONTROL=+openmp + #pragma omp,跨核并行)、PrecisionAdaptor/SPDP (第一季第 11 章 solveScalar,存储单精度、求解双精度以省内存带宽)、连续内存的 Field (第一季第 5 章)与 GAMG 缓存聚合 (S2-07 cacheAgglomeration_,复用粗化结果)。
并行(MPI)与共享内存(OpenMP)是两套独立加速 :前者跨节点、后者跨核。大算例通常是"MPI 分解 + 每进程 OpenMP"。OpenMP 开关在 wmake/makefiles/general(第 72--79 行,+openmp 时把 LINK_OPENMP 加进 SYS_LIBS)与 rules/General/general(第 59--66 行);~openmp(禁用)优先于 +openmp(注释 "disable has higher precedence")。
7.3 -profiling 与 addProfiling 的组合用法
cpp
#include "profiling.H"
void myAlgorithm(...)
{
{ addProfiling(myAlgo, "myAlgorithm"); /* ... */ } // 作用域自动结束(推荐)
addProfiling(loopBody, "myAlgorithm::loopBody"); // 或手动结束
for (...) { /* ... */ }
endProfiling(loopBody);
}
addProfiling(Name, "...") 的 RAII 对象在作用域结束时析构并 unstack;endProfiling(Name) 可提前结束(变量名必须是 profilingTriggerFor<Name>);addProfilingInFunction(Name) 让描述=函数名(GCC 得完整签名);命令行 -profiling 忽略 active 强制全开(cpuInfo/memInfo/sysInfo 也开,开销更大)。
7.4 profiling 排障要点
- 报告没生成 :
profiling.active为 false,或全局allowProfiling 0关了allowed;用-profiling可快速判定。 - 报告只有
trigger0:用户代码没有addProfiling,只有内建 Stage------不是错误。 - 并行下只在 processor0 有 :属正常,各进程各写一份;要看全局用
profilingSummary。
8. 数据交换:fileFormats 全景
8.1 库的组成:src/fileFormats/Make/files
src/fileFormats/Make/files(第 1--83 行)是一份"支持格式总清单"。按目录归类如下:
| 行段 | 目录 | 内容/作用 |
|--------|------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|
| 1--2 | common/ | 库级公共设施(fileFormats.C、manifoldCellsMeshObject) |
| 4--6 | colours/ | colourTable(s)、colourTools:可视化配色 |
| 8--31 | ensight/ | EnSight 读写全套:file/{ensightCase,ensightCaseOptions,ensightFile,ensightGeoFile,ensightWriterCaching}、mesh/ensightMesh(+Options)、output/ensightOutput、part/(第 19 行用 make 变量 part = ensight/part)、read/ensightReadFile、type/ensightPTraits |
| 32--35 | --- | abaqus/ABAQUSCore、nastran/NASCore、obj/OBJstream、fire/FIRECore |
| 37--43 | gltf/ | glTF 2.0 全套:Object/Accessor/BufferView/Mesh/Scene/SceneWriter/Animation |
| 45 | json/ | JSONformatter |
| 47--52 | starcd/+stl/ | STAR-CD 与 STL(三种 ASCII 解析器,见第 10 节) |
| 54--57 | `vtk/base | file` | foamVtkCore、类型 traits、foamVtkFileWriter、foamVtkSeriesWriter、foamVtkVtmWriter |
| 60--67 | vtk/format/ | formatter 家族:Ascii/Base64/AppendBase64/AppendRaw/Base64Layer/LegacyAscii/LegacyRaw |
| 69--70 | vtk/output/ | foamVtkOutput、foamVtkOutputOptions |
| 72--74 | vtk/part/ | foamVtkMeshMaps、foamVtuCells、foamVtuSizing |
| 76--80 | `vtk/read | write/` | vtkUnstructuredReader + line/poly/vertex/surface writer |
| 83 | --- | LIB = $(FOAM_LIBBIN)/libfileFormats |
src/fileFormats/Make/options(第 1--4 行)极简:
make
EXE_INC =
LIB_LIBS = \
-lOpenFOAM
- 说明
libfileFormats只依赖libOpenFOAM,不依赖 finiteVolume------这正是它能被foamToVTK/foamToEnsight(依赖 finiteVolume)复用的原因:格式层与物理层解耦。
8.2 格式对比表(C1)
| 维度 | EnSight | VTK(XML/vtu) | glTF | STL | JSON |
|---|---|---|---|---|---|
| 读/写 | 读写 | 读写 | 写 | 读写 | 写 |
| 二进制 | 有(C Binary) | 有(base64/raw append) | 有(bufferView) | 有(binary) | 否 |
| 时间序列 | .case 归档 |
.vtm.series |
动画(Animation) | 无 | 无 |
| 并行分区 | 每 patch/zone 一个 part | 每 rank 一个 piece | 单场景 | 无 | 无 |
| 顶点/单元布局 | 单元+顶点 | Points/Cells/Offsets | Accessor/BufferView | 三角面 | 键值 |
| 典型消费端 | EnSight/CFD-Post | ParaView | 浏览器/three.js | 切片/3D 打印 | 后处理脚本 |
9. VTK 家族与 base64/append 数据布局
9.1 formatter 家族:把"格式"做成策略对象
VTK 的 formatType 由多个 formatter 类实现,wmake/rules... 不,由 src/fileFormats/Make/files 第 60--67 行可见其家族:
foamVtkAsciiFormatter:XML + 内联 ASCII。foamVtkBase64Formatter:XML + 内联 base64。foamVtkAppendBase64Formatter:XML + append 模式 ,数据追加到AppendedData,base64 编码。foamVtkAppendRawFormatter:XML + append,raw 二进制。foamVtkLegacyAsciiFormatter/foamVtkLegacyRawFormatter:旧版 VTK(legacy)。foamVtkBase64Layer:base64 编码层(被上面几个 formatter 复用)。
src/fileFormats/vtk/output/foamVtkOutputOptions.H 把这些格式的开关语义写得很清楚(第 131--150 行):
cpp
//- Toggle ASCII mode on/off.
// In XML append mode, this switches between base64 and raw binary.
// In XML inline mode, this switches between ASCII and base64.
// In legacy mode, this switches between ASCII and binary.
// \return outputOptions for chaining
outputOptions& ascii(bool on) noexcept;
//- Toggle append mode on/off.
outputOptions& append(bool on) noexcept;
//- Toggle legacy mode on/off.
outputOptions& legacy(bool on) noexcept;
逐行解读:
- 第 131--135 行:
ascii(bool)的语义随上下文变化 ------XML+append 时是 base64↔raw;XML+inline 时是 ASCII↔base64;legacy 时是 ASCII↔binary。同一个布尔开关映射到三套格式分支。 - 第 140--142 行:
append(bool)切换"数据放哪里"------内联(inline)还是追加(append)。 - 第 144--146 行:
legacy(bool)切换新旧 VTK 格式。
foamToVTK 的 -ascii/-legacy 命令行选项正是直接驱动这些开关(见第 12 节)。VTK 输出格式对比表(C7):
| 模式 | legacy |
ascii |
结果 |
|---|---|---|---|
| XML inline ASCII | false | true | 纯文本 XML,人可读、体积大 |
| XML inline base64 | false | false | XML + 内联 base64 |
| XML append base64 | false | true | AppendedData 段 base64 |
| XML append raw | false | false | AppendedData 段 raw 二进制 |
| Legacy ASCII | true | true | 旧版 .vtk 文本 |
| Legacy binary | true | false | 旧版 .vtk 二进制 |
9.2 base64 编码层:3 字节 → 4 字符
base64Layer.H(第 105--127 行)定义编码长度与接口:encodedLength(len) = 4*((len+2)/3)(第 111--114 行)------每 3 字节出 4 字符 、向上取整,是 RFC 4648 标准长度公式的整数写法;write(s,n) 编码输出、reset() 重开一段、close() 补 = 收尾。
实现细节在 base64Layer.C。字母表(第 34--44 行)用 64 个字符 A-Za-z0-9+/。四个编码函数的位切分(第 50--72 行)------encode0/1/2/3 分别取三字节的:
encode0:group_[0]的高 6 位 ((group_[0] & 0xFC) >> 2)。encode1:group_[0]的低 2 位 (&0x03 <<4)作高、group_[1]的高 4 位 (&0xF0 >>4)作低,拼成第二个索引。encode2:group_[1]的低 4 位 (&0x0F <<2)与group_[2]的高 2 位 (&0xC0 >>6)拼成第三个。encode3:group_[2]的低 6 位 (&0x3F)作第四个。
add_char(第 75--91 行)把字符塞进 group_[groupLen_++];攒满 3 字节就编成 4 字符、一次写 4 字节、计数清零。close(第 136--172 行)处理尾部:剩 1 字节补 =表(X X = =),剩 2 字节补一个 =(X X X =),否则不写。
这正是 VTK base64 输出的字节级实现 :foamVtkBase64Layer 只是把它包成 VTK 语义。
9.3 foamVtkBase64Layer:加一层 VTK 语义
src/fileFormats/vtk/format/foamVtkBase64Layer.cxx(第 73--118 行)里,writeSize 先把 uint64_t numbytes 以 sizeof(uint64_t) 裸字节写入(VTK append 模式要求每段数据带一个 8 字节长度前缀 ,这就是"append 数据布局"的第一步);write(uint8_t) 直接进 base64 层;write(label)/write(float) 按各自 sizeof 原样写入。唯一特殊的是 write(double)(第 100--118 行):
cpp
void Foam::vtk::foamVtkBase64Layer::write(const double val)
{
// Limit range of double to float conversion
if (val >= std::numeric_limits<float>::max())
{
write(std::numeric_limits<float>::max());
}
else if (val <= std::numeric_limits<float>::lowest())
{
write(std::numeric_limits<float>::lowest());
}
else
{
float copy(val);
write(copy);
}
}
逐行:
- 第 105--108 行:值超过
float最大 → 写float最大值(钳位)。 - 第 109--112 行:低于
float最小 → 写最小值。 - 第 114--117 行:否则
float copy(val)把 double 压成 float。 - 结论 :VTK 输出统一用 32 位浮点。这是"体积/精度"的显式取舍,也解释了
foamToVTKlegacy 分支为何要检查sizeof(float)!=4。
9.4 append 模式的偏移累加:offset()
foamVtkAppendBase64Formatter.cxx(第 74--83 行):
cpp
uint64_t Foam::vtk::appendBase64Formatter::offset(const uint64_t numbytes)
{
uint64_t prev = offset_;
if (formatter::npos != numbytes)
{
offset_ += this->encodedLength(sizeof(uint64_t) + numbytes);
}
return prev;
}
逐行:
- 第 76 行:记住当前偏移。
- 第 78--81 行:若
numbytes非npos(即"要追加一段数据"),则把偏移前进encodedLength(8 + numbytes)------8 是长度前缀uint64的字节数,encodedLength是 base64 展开后的字符数 。这就是"每个<DataArray>在AppendedData里的偏移量"如何被准确算出来。 - 第 82 行:返回追加前的偏移(VTK 头里
<DataArray offset="N">用的就是这个值)。
机制链 D(完整推导):VTK append-base64 数据布局
sql
写入一个 DataArray(如某个 scalar 场的 32 位浮点数组):
1) formatter::writeHeader → 记 <DataArray ... offset="prev"> ← 取 offset() 返回值
2) 数据体写入 AppendedData 流:
writeSize(uint64 numbytes) ← 8 字节长度前缀(base64 前是裸字节)
write(float/label/double→float) ... ← 元素逐个进 base64Layer::add
base64Layer::add_char:每攒 3 字节出 4 字符
close():尾部不足补 '='
3) offset_ += encodedLength(8 + numbytes) ← 为下一个 DataArray 预留偏移
4) 文件尾写入 AppendedData 整段(一次 base64 大块)
ParaView 读取时:按 <DataArray offset> 定位到 AppendedData 内的字节位置,
先读 8 字节长度,再读 numbytes 数据,base64 解码还原 float。
9.5 VTK formatter 的 XML 标签栈
foamVtkFormatter.cxx 维护一个"当前打开的 XML 标签栈"来生成合法 XML(第 76--155 行)。openTagImpl:若已在标签内再开标签 → 警告并拒绝(inTag_ 状态机),否则缩进、写 <tag、push_back 入栈、置 inTag_=true。closeTag(isEmpty):isEmpty=true 写自闭合 <tag />(弹栈),否则写 >。endTag(tagName):
cpp
Foam::vtk::formatter& Foam::vtk::formatter::endTag(const word& tagName)
{
const word curr(std::move(xmlTags_.back()));
xmlTags_.pop_back();
indent();
if (inTag_) { WarningInFunction << "adding xml endTag '" << curr << "' but already in another tag!" << endl; }
if (!tagName.empty() && tagName != curr)
{
WarningInFunction << "expecting to end xml tag '" << tagName
<< "' but found '" << curr << "' instead" << endl;
}
os_ << "</" << curr << '>' << nl;
inTag_ = false;
return *this;
}
- 第 127--128 行:弹栈取栈顶标签名并缩进。
- 第 131--136 行:已在另一标签内则警告。
- 第 142--148 行:校验传入标签名与栈顶是否一致------不符则警告(挑出配错的开闭标签)。
- 第 150--152 行:写
</tag>、复位状态。
设计要点 :用运行时标签栈保证 XML 合法性。所有 VTK 写出(Points/Cells/DataArray/Piece)都经过这几个原语,任何不匹配都会在 stderr 报警,把"手写 XML 易错"转化为"栈状态检查"。
9.6 polyMesh → VTK cell 的映射与并行偏移
src/fileFormats/vtk/part/foamVtuCells.cxx 顶部有个匿名命名空间工具 parCoordinatedOffsets(第 47--87 行),专解"并行下 VTK 的 begin/end offsets 拼接":并行走特殊逻辑(第 55 行);len = size-1(第 57 行);find_first(len > 0) 找到第一个有非空数据的 rank 作 leader(第 60--64 行);leader 返回完整 offsets (含起始 0 offset,第 65--69 行);非 leader 但有数据 → 只返回 slice(1)(丢掉起始 offset,因为它就是 leader 的末端,重复写会让 offsets 出现重复值,第 70--74 行);无数据的 rank → 返回空(不贡献 offset,第 75--79 行);串行直接复制(第 82--86 行)。
注释(第 40--46 行)把动机写透:"From the first viable rank: use the begin/end offsets; Other ranks: use their end offsets only"。VTK 的 <DataArray> 用 offsets 数组描述变长 cell;并行时各 rank 各写一段,若不协调就会有重复的边界 offset。 同类文件里 vtuCells 的构造(第 94--137 行)还按输出类型(LEGACY/HDF/XML,由 outputOptions 的 legacy()/is_hdf() 决定)选分支------同一个 polyMesh,按输出格式选不同 VTK cell 编码。
机制链 E(VTK 侧) :polyMesh(任意多面体)→ foamVtuSizing/foamVtuCells 把 cell 映射成 VTK 类型(六面体→HEXAHEDRON、四面体→TETRA、棱柱/棱锥→WEDGE/PYRAMID、多面体→分解为 tets/pyramids 或 POLYHEDRON),生成 Points/connectivity/offsets → 并行用 parCoordinatedOffsets 协调 → formatter 按 formatType 编码(inline 写在 <DataArray> 内,append 写长度前缀+数据到 AppendedData 并累加 offset_)→ foamVtkFileWriter/foamVtkSeriesWriter/foamVtmWriter 落盘为 .vtu/.vtk 或 .vtm+.vtm.series。
10. STL 三解析器与其它格式
10.1 STL:同一格式的三个词法分析器
src/fileFormats/stl/STLReader.C 顶部有个优化开关决定用哪个解析器(第 39--42 行):
cpp
int Foam::fileFormats::STLReader::parserType
(
Foam::debug::optimisationSwitch("fileFormats::stl", 0)
);
- 用一个
optimisationSwitch("fileFormats::stl", 0)决定 parser 类型(默认 0)。
选择逻辑在 readASCII(第 65--81 行):
cpp
bool Foam::fileFormats::STLReader::readASCII(const fileName& filename)
{
// No runtime selection of parser (only via optimisationSwitch)
// this is something that is infrequently changed.
if (parserType == 1)
{
return readAsciiRagel(filename);
}
else if (parserType == 2)
{
return readAsciiManual(filename);
}
return readAsciiFlex(filename);
}
逐行:
- 第 72--75 行:
parserType == 1→ Ragel (STLAsciiParseRagel.cc,由.rl生成)。 - 第 76--79 行:
parserType == 2→ 手写 (STLAsciiParseManual.C)。 - 第 80 行:默认 → Flex (
STLAsciiParseFlex.L,由 flex 生成)。 - 注释(第 70--71 行):"No runtime selection of parser"------只在
optimisationSwitch里调,属"不常改"的东西。
STLAsciiParseFlex.L(第 32--33 行)用 %option 声明 flex 类:
ini
%option prefix="yySTL"
%option yyclass="yySTLFlexLexer"
- 第 32 行:把生成符号前缀设为
yySTL(避免与其它 flex 文件冲突)。 - 第 33 行:把扫描器类设为
yySTLFlexLexer(C++ 类封装)。
该文件还有针对 C++17 的兼容补丁(第 45--49 行):
arduino
// Flex may use register, which is deprecated and incompatible with C++17
#pragma clang diagnostic ignored "-Wdeprecated-register"
// A 'nothing' define to effectively remove from code as well (issue #3337)
#undef register
#define register
- 第 46 行:屏蔽 clang 的
-Wdeprecated-register。 - 第 48--49 行:把
register关键字宏定义为空 ,从机制上消除 C++17 里已弃用的register。注释注明 issue #3337。
STL 三解析器对比表(C6):
| 解析器 | 文件 | 生成方式 | 前缀/类 | 取舍 |
|---|---|---|---|---|
| Flex | stl/STLAsciiParseFlex.L |
flex 生成 C++ 扫描器 | yySTL / yySTLFlexLexer |
默认;规则式,可维护 |
| Ragel | stl/STLAsciiParseRagel.cc |
由 .rl 生成 |
--- | parserType=1;状态机 |
| 手写 | stl/STLAsciiParseManual.C |
人工编码 | --- | parserType=2;零依赖、可读 |
设计意图:OpenFOAM 保留三种实现做 A/B------既是历史遗留(从手工到 Ragel 再到 Flex),也是"同一问题不同技术的活教材"。
10.2 EnSight:.case 归档与分区组织
ensightCase.H(第 76--117 行)里能看到 EnSight 的组织方式:两个固定名 dataDirName="data"、geometryName="geometry"(EnSight 的"case 文件 + data 目录 + geometry 文件"三段式 );os_ 只在 master 上打开(注释 "Output stream (master only)",并行时 case 文件只由主进程写 );caseName_ 带 .case 结尾;timeIndex_/timeValue_ 属 timeset 1;timesUsed_ 记录用过的 index/value;geomTimes_ 记录几何写出的时间索引,特殊值 -1 表示静态几何(注释 "The special index '-1' is used for static geometry")。
foamToEnsight 的 -index <start> 选项(见第 13 节)正是改写 data/######## 的编号起点。
10.3 其余格式
glTF (gltf/)是一整套 glTF 2.0 对象(foamGltfObject 基类、Accessor、BufferView、Mesh、Scene、SceneWriter、Animation 时间序列),与规范的"属性→缓冲"层级对应;JSON (json/JSONformatter)供脚本消费;OBJ (obj/OBJstream)主要写出几何;colours (colours/colourTable(s)/colourTools)用于可视化配色;ABAQUS (abaqus/ABAQUSCore)、NASTRAN (nastran/NASCore)是结构分析格式(主要导出网格);FIRE (fire/FIRECore)、STAR-CD (starcd/STARCDCore)是其它 CFD 网格格式。src/conversion/Make/files 里同时出现这些 Core 的读写器,说明格式核心在 fileFormats、转换算法在 conversion。
11. conversion 库与转换工具
11.1 src/conversion/ 的层次
src/conversion/Allwmake(第 1--11 行):
sh
#!/bin/sh
cd "${0%/*}" || exit # Run from this directory
targetType=libso # Preferred library type
. ${WM_PROJECT_DIR:?}/wmake/scripts/AllwmakeParseArguments
wmake $targetType
ccm/Allwmake
#------------------------------------------------------------------------------
逐行:
- 第 3 行:默认目标
libso。 - 第 8 行:
wmake libso编本目录libconversion。 - 第 10 行:再进入
ccm/(AVL FIRE/CCM 需要额外的第三方库,单独构建)。
src/conversion/ 下的子目录与作用:common/(读/写网格的公共骨架 :reader/meshReader.* 的 createPolyCells/createPolyBoundary/calcPointCells、writer/meshWriter.*、tables/{cellTable,boundaryRegion},读入抽象网格后统一生成 polyMesh);ccm/(reader/ccmReader*、writer/ccmWriter*、common/ccmBase,AVL FIRE/CCM 读写);ensight/(output/ensightOutputVolField.txx 等,把场转 EnSight);fire/(FIREMeshReader/Writer、checkFireEdges);starcd/(STARCDMeshReader/Writer);polyDualMesh/(生成对偶网格 :cell 重心连成新 cell、顶点变新 cell);vtk/(output/foamVtkInternalWriter、foamVtkPatchWriter、foamVtkSurfaceMeshWriter 等 + adaptor/foamVtkVtuAdaptor,foamToVTK 的核心写出器)。
关键点:common/reader/meshReader 是"外部网格 → polyMesh"的统一抽象层。 每种外部格式(FIRE/STAR-CD/CCM)实现自己的 reader,最终都调用 createPolyCells/createPolyBoundary 生成 polyMesh------这解释了 *ToFoam 系列为何能统一。
11.2 上层转换工具(applications/utilities)
src/conversion/Allwmake 编完后,上层工具(applications/utilities)才有依赖可用的库:网格转换 (mesh/conversion/)含 gmshToFoam、fluent3DMeshToFoam、star4ToFoam、cfx4ToFoam、netgenNeutralToFoam、ideasUnvToFoam、ensightToFoam、kivaToFoam、plot3dToFoam、fireToFoam、tetgenToFoam、ccm/ccmToFoam、foamToStarMesh、foamToFireMesh、foamToSurface、foamToCcm、vtkUnstructuredToFoam 等;数据转换 (postProcessing/dataConversion/)含 foamToVTK、foamToEnsight、foamToGMV、foamDataToFluent、foamToTetDualMesh、smapToFoam、objToVTK 等;并行后处理 (parallelProcessing/)含 reconstructPar、reconstructParMesh、redistributePar、decomposePar。
依赖关系上:
foamToVTK/foamToEnsight都-lfileFormats、-lconversion、-lfiniteVolume------格式库 + 转换库 + 物理库三者组合,这正是"数据交换"的组件拼图。
12. foamToVTK 逐段解读
12.1 Make/options 与 Make/files
foamToVTK/Make/options(第 1--16 行):include 覆盖 finiteArea/finiteVolume/fileFormats/meshTools/conversion/lagrangian/basic/lagrangian/intermediate 的 lnInclude;EXE_LIBS 链接 -lfiniteArea -lfiniteVolume -lfileFormats -lmeshTools -lconversion -llagrangianIntermediate -lgenericPatchFields------说明该工具能处理体积场、面积场、拉格朗日粒子、子集网格。
foamToVTK/Make/files(第 1--3 行):唯一源文件 foamToVTK.cxx,EXE = $(FOAM_APPBIN)/foamToVTK(装到项目 appbin)。
12.2 输出选项解析:getOutputOptions
foamToVTK.cxx(第 227--259 行):
cpp
vtk::outputOptions getOutputOptions(const argList& args)
{
vtk::outputOptions opts; // 默认:内联 ASCII XML
if (args.found("legacy"))
{
opts.legacy(true);
if (!args.found("ascii"))
{
if constexpr (sizeof(float) != 4 || sizeof(label) != 4)
{
opts.ascii(true);
WarningInFunction << "Using ASCII rather than legacy binary VTK ..." << endl;
}
else { opts.ascii(false); }
}
}
else { opts.ascii(args.found("ascii")); }
return opts;
}
逐行解读:
- 第 230 行:默认就是"内联 ASCII XML"(注释)。
- 第 232--234 行:
-legacy→opts.legacy(true)。 - 第 236 行:若没同时给
-ascii(想要 legacy binary),先做编译期尺寸检查if constexpr (sizeof(float)!=4 || sizeof(label)!=4)。 - 第 240--245 行:尺寸不符(如
label是 64 位)时强制退回 ASCII 并警告------legacy binary VTK 硬编码 4 字节 float/label,不符会写出坏文件。这是"用if constexpr做编译期防错"的漂亮用法。 - 第 252--257 行:非 legacy 时
ascii由-ascii决定(XML 下 ASCII↔base64)。
12.3 主流程:时间循环 + 多区 + 网格代理
主函数关键段落(第 510--520 行配置、第 728--760 行目录管理、第 777--945 行时间循环):
cpp
const bool decomposePoly = args.found("poly-decomp");
const bool doBoundary = !args.found("no-boundary");
const bool doInternal = !args.found("no-internal");
const bool doLagrangian = !args.found("no-lagrangian");
const bool doFiniteArea = !args.found("no-finite-area");
const bool doEdgeFields = args.found("edgeFields");
const bool doSurfaceFields = args.found("surfaceFields");
const bool oneBoundary = args.found("one-boundary") && doBoundary;
const bool nearCellValue = args.found("nearCellValue") && doBoundary;
const vtk::outputOptions writeOpts = getOutputOptions(args);
- 第 510--517 行:把一堆布尔选项解包,统一采用
!args.found("...")的"否定命名" (no-boundary→doBoundary)。 - 第 517 行:
oneBoundary、nearCellValue需同时doBoundary才有意义(短路逻辑)。 - 第 520 行:调
getOutputOptions。
输出目录管理(第 728--760 行):默认 <globalPath>/VTK;-name 可改目录名(绝对或相对),outputDir.clean() 去掉多余的 ..;只有 master 建目录 ,逐 region 建 VTK/<regionName>/,-overwrite 时先 rmDir。并行时非 master 不碰文件系统。
时间循环(第 777--945 行,节选关键):
cpp
forAll(timeDirs, timei)
{
runTime.setTime(timeDirs[timei], timei);
const word timeDesc = "_" + Foam::name(runTime.timeIndex());
const scalar timeValue = runTime.value();
vtk::vtmWriter vtmMultiRegion; // 多区 VTM 累加
forAll(regionNames, regioni)
{
auto& meshProxy = meshProxies[regioni];
auto& vtuMeshCells = vtuMappings[regioni];
polyMesh::readUpdateState meshState = meshProxy.readUpdate();
if (meshState == polyMesh::TOPO_CHANGE || meshState == polyMesh::TOPO_PATCH_CHANGE)
{ vtuMeshCells.clear(); } // 触发 vtk cell 重映射
...
}
if (UPstream::master() && regionNames.size() > 1)
{
fileName outputName(outputDir/outputBaseName + "-regions" + timeDesc + ".vtm");
vtmMultiRegion.setTime(timeValue); vtmMultiRegion.write(outputName);
fileName seriesName(vtk::seriesWriter::base(outputName));
vtk::seriesWriter& series = vtkSeries(seriesName);
if (series.empty()) { series.load(seriesName, true, timeValue); }
series.append(timeValue, outputName); series.write(seriesName);
}
}
- 第 781--782 行:
timeDesc = "_<timeIndex>"、timeValue。用timeIndex而非timeName作后缀------与 VTM/系列命名一致。 - 第 788 行:多区用
vtk::vtmWriter汇总。 - 第 807--819 行:
meshProxy.readUpdate()检查网格变化;拓扑或 patch 变化时vtuMeshCells.clear(),强制重生成 VTK cell 映射(注释 "Trigger change for vtk cells too")。 - 第 916--940 行:多区时 master 写一个
*.vtm(多块索引)并维护*.vtm.series。第 933--937 行if (series.empty()) series.load(...)------内存里没有就先从文件载入,再append、write。这保证多次调用foamToVTK追加时间步而不是覆盖。
各字段写出由若干 #include 的小文件完成(convertVolumeFields.H/convertSurfaceFields.H/convertAreaFields.H/convertLagrangian.H/convertTopoSet.H/convertProcessorPatches.H)。
12.4 foamToVTK 命令清单(模板 T3)
bash
foamToVTK # 基本:全部时间步 → VTK/(默认 XML inline ASCII)
foamToVTK -fields '(p U "alpha.*")' # 只转指定字段(正则)
foamToVTK -no-boundary # 只要内部体网格
foamToVTK -surfaceFields -edgeFields # 同时写面/边场(如 phi)
foamToVTK -cellZone water # 只转子集 cellZone
foamToVTK -with-ids -with-point-ids # 附带 mesh id 字段
foamToVTK -poly-decomp # 多面体分解为 tet/pyramid
foamToVTK -name vtk_out -base-name run1 # 自选目录/前缀
foamToVTK -legacy # 旧版 VTK
foamToVTK -time 100:200 # 时间范围
逐项说明:默认输出 VTK/<region>/<case>_<index>.vtu + .vtm.series;-fields 用 wordRes 正则白名单;-no-boundary 不写 patch 面;-surfaceFields/-edgeFields 额外写面/边场;-cellZone 用 fvMeshSubsetProxy 取子集;-with-ids/-with-point-ids 追加 cellID/procID/patchID/pointID(体积略增);-poly-decomp 分解多面体(cell 数变多);-name/-base-name 改目录/前缀;-legacy 旧版格式(64 位 label 时自动退 ASCII);-time 走 timeSelector。
13. foamToEnsight 逐段解读
13.1 Make/files 与选项
applications/utilities/postProcessing/dataConversion/foamToEnsight/Make/files(第 1--4 行):
make
readFields.cxx
foamToEnsight.cxx
EXE = $(FOAM_APPBIN)/foamToEnsight
主函数 foamToEnsight.cxx 把选项映射成 ensightCase::options 与 ensightMesh::options(第 344--417 行):
cpp
// Default to binary output, unless otherwise specified
const IOstreamOption::streamFormat format =
(
args.found("ascii") ? IOstreamOption::ASCII : IOstreamOption::BINARY
);
const bool doBoundary = !args.found("no-boundary");
const bool doInternal = !args.found("no-internal");
const bool doCellZones = !args.found("no-cellZones");
const bool nearCellValue = args.found("nearCellValue") && doBoundary;
label indexingNumber(0);
const bool doConsecutive = args.readIfPresent("index", indexingNumber);
bool doGeometry = !args.found("no-mesh");
ensightCase::options caseOpts(format);
caseOpts.nodeValues(doPointValues && args.found("nodeValues"));
caseOpts.width(args.getOrDefault<label>("width", 8));
caseOpts.overwrite(!args.found("no-overwrite"));
ensightMesh::options writeOpts;
writeOpts.useBoundaryMesh(doBoundary);
writeOpts.useInternalMesh(doInternal);
writeOpts.useCellZones(doCellZones);
- 第 344--349 行:默认二进制 (
IOstreamOption::BINARY),-ascii才改 ASCII。与 VTK 默认 ASCII 相反------EnSight 的 "C Binary" 是其常用格式。 - 第 352--358 行:一批
do*布尔(同foamToVTK的否定命名风格)。 - 第 361--362 行:
-index <start>→indexingNumber;doConsecutive表示"用连续编号而非时间索引"。 - 第 365--366 行:
-no-mesh→ 不写几何(可只转结果)。 - 第 382 行:
ensightCase::options caseOpts(format)。第 385 行nodeValues(...)------强制插值到节点 ;第 386 行width默认 8(data/########编号位宽);第 387 行默认覆盖旧 EnSight 目录。 - 第 392--414 行:把 patch/cellZone/faceZone 选择直接设进
ensightMesh::options(patchSelection/patchExclude/faceZoneSelection/cellZoneSelection)。
13.2 时间循环与几何写出
主循环关键段(第 499--603 行):
cpp
if (UPstream::master())
{ Info<< "Converting " << timeDirs.size() << " time steps" << nl; }
#include "checkMeshMoving.H"
if (hasMovingMesh && !doGeometry)
{ Info<< "has moving mesh: ignoring '-no-mesh' option" << endl; doGeometry = true; }
#include "findCloudFields.H"
#include "checkFieldAvailability.H"
...
forAll(timeDirs, timei)
{
runTime.setTime(timeDirs[timei], timei);
#include "getTimeIndex.H"
Info<< "Time [" << timeIndex << "] = " << runTime.timeName() << nl;
forAll(regionNames, regioni)
{
auto& mesh = meshes[regioni];
polyMesh::readUpdateState meshState = mesh.readUpdate();
const bool moving = (meshState != polyMesh::UNCHANGED);
auto& ensCase = ensightCases[regioni];
auto& ensMesh = ensightMeshes[regioni];
ensCase.setTime(timeDirs[timei], timeIndex);
if (moving) { ensMesh.expire(); ensMesh.correct(); }
if ((timei == 0 || moving) && doGeometry)
{
autoPtr<ensightGeoFile> os = ensCase.newGeometry(hasMovingMesh);
ensMesh.write(os.ref());
}
...
}
}
- 第 506--511 行:
checkMeshMoving判断动网格;有动网格时-no-mesh被忽略(必须每次写几何)并提示。 - 第 514--517 行:找 cloud(拉格朗日)字段与检查字段可用性。
- 第 534 行:
getTimeIndex.H计算 EnSight 时间索引(-index时用连续编号)。 - 第 550--557 行:读网格更新状态(
moving = state != UNCHANGED),ensCase.setTime(...)------把时间写进.case。 - 第 570--580 行:动网格时
ensMesh.expire()+correct()------令缓存几何失效并重算。 - 第 582--590 行:仅第一个时间步或网格移动时写几何 (
timei==0 || moving且doGeometry),用ensCase.newGeometry(hasMovingMesh)得几何文件再ensMesh.write。
收尾(第 657--668 行):循环结束后统一 write() 各区的 .case 文件 。数据文件在循环里写、case 文件在末尾写------这是 EnSight 的组织顺序。
13.4 foamToEnsight 命令清单(并入模板 T3)
bash
foamToEnsight # 基本:默认 C Binary → EnSight/
foamToEnsight -ascii # ASCII 版
foamToEnsight -no-mesh # 只写结果、不写(静态)几何
foamToEnsight -fields '(p U T)' # 指定字段
foamToEnsight -no-boundary -cellZones water # 只要内部网格 + 指定 cellZone
foamToEnsight -index 1000 # 连续编号(base 从 1000 起)
foamToEnsight -nodeValues # 强制节点插值
foamToEnsight -time 500:1000 -name EnSight_run1 # 时间范围 + 目录改名
EnSight vs VTK 用法差异对比表:
| 维度 | foamToVTK |
foamToEnsight |
|---|---|---|
| 默认格式 | XML inline ASCII | C Binary |
| 开关 | -ascii/-legacy/-poly-decomp |
-ascii/-nodeValues/-index/-width |
| 输出目录 | VTK/(-name 改) |
EnSight/(-name 改) |
| 多区 | .vtm + .vtm.series |
每区一个 .case |
| 编号 | 用 timeIndex | -index 连续编号 / -width 位宽 |
| 子集 | -cellSet/cellZone/faceSet/pointSet |
-cellZones/-faceZones/-patches |
14. reconstructPar 与并行拼装算法
14.1 主流程:从 processorN/ 到整域
reconstructPar.C 的入口先确定进程数与建立各进程 Time(第 217--266 行):
cpp
label nProcs{0};
if (regionNames.empty())
{ FatalErrorInFunction << "No regions specified or detected." << exit(FatalError); }
else if (regionNames[0] == polyMesh::defaultRegion)
{ nProcs = fileHandler().nProcs(args.path()); }
else
{ nProcs = fileHandler().nProcs(args.path(), regionNames[0]); }
if (!nProcs)
{ FatalErrorInFunction << "No processor* directories found" << exit(FatalError); }
// Warn fileHandler of number of processors
fileHandler().constCast().nProcs(nProcs);
// Create the processor databases
PtrList<Time> databases(nProcs);
forAll(databases, proci)
{
databases.set(proci, new Time(Time::controlDictName, args.rootPath(),
args.caseName()/("processor" + Foam::name(proci)),
args.allowFunctionObjects(), args.allowLibs()));
}
- 第 219--237 行:通过
fileHandler().nProcs()自动探测进程数 (非用户传参);region 为默认polyMesh时查根路径,否则查指定 region 子目录。 - 第 239--244 行:
nProcs == 0→ "No processor* directories found"。并行拼装第一道门:没有 processorN 目录就无法重建。 - 第 247 行:
fileHandler().constCast().nProcs(nProcs)------告知 fileHandler 进程数 (后续filePath解析依赖它)。 - 第 250--266 行:为每个
processor{proci}建独立Time。"每个进程是独立算例"的体现 ------拼装时分头读它们。第 303--307 行再把所有进程Time对齐到重建网格时间。
14.2 场重建的核心:fvFieldReconstructor
fvFieldReconstructor.H(第 57--86 行)的数据成员:
cpp
class fvFieldReconstructor
{
//- Reconstructed mesh reference
fvMesh& mesh_;
//- List of processor meshes
const UPtrList<fvMesh>& procMeshes_;
//- List of processor face addressing lists
const UPtrList<labelIOList>& faceProcAddressing_;
//- List of processor cell addressing lists
const UPtrList<labelIOList>& cellProcAddressing_;
//- List of processor boundary addressing lists
const UPtrList<labelIOList>& boundaryProcAddressing_;
//- Number of fields reconstructed
label nReconstructed_;
};
mesh_:重建后的整域网格(引用)。procMeshes_:各processorN的网格列表。faceProcAddressing_/cellProcAddressing_/boundaryProcAddressing_:三套映射表------这是"并行拼装算法"的核心数据 。它们由decomposePar时生成,存于processorN/constant/polyMesh/{cellProcAddressing,faceProcAddressing,boundaryProcAddressing},记录"进程局部 index → 全局 index"。nReconstructed_:计数(用于统计"没有场"的情况)。
体积场的重建算法在 fvFieldReconstructor.txx(第 37--71 行):
cpp
template<class Type>
Foam::tmp<Foam::DimensionedField<Type, Foam::volMesh>>
Foam::fvFieldReconstructor::reconstructField
(
const IOobject& fieldObject,
const UPtrList<DimensionedField<Type, volMesh>>& procFields
) const
{
// Create the internalField
Field<Type> internalField(mesh_.nCells());
forAll(procMeshes_, proci)
{
const auto& procField = procFields[proci];
internalField.rmap(procField.field(), cellProcAddressing_[proci]);
}
auto tfield = tmp<DimensionedField<Type, volMesh>>::New
(
fieldObject, mesh_, procFields[0].dimensions(), internalField
);
tfield.ref().oriented() = procFields[0].oriented();
return tfield;
}
逐行解读(并行拼装最核心的一段):
- 第 46 行:
internalField(mesh_.nCells())------按全局网格单元数分配(不是按某个 processor 的单元数)。 - 第 53--57 行:遍历所有进程,调用
internalField.rmap(procField.field(), cellProcAddressing_[proci])。rmap是"反向映射" :dst.rmap(src, addr)表示"对每个 i,把src[addr[i]]写到dst[i]"。addr即该进程的"局部 i → 全局 g",于是把进程局部值散播到全局场的对应单元 。多个proci顺序调用,天然完成拼装------每个全局单元只被一个进程写一次。 - 第 59--68 行:组装成以
mesh_为基础的新场(维度取自procFields[0]),oriented()亦继承。
边界场更复杂(同文件第 74--165 行):
cpp
forAll(boundaryProcAddressing_[proci], patchi)
{
const label curBPatch = boundaryProcAddressing_[proci][patchi];
const labelList::subList cp =
procField.mesh().boundary()[patchi].patchSlice(faceProcAddressing_[proci]);
if (curBPatch >= 0)
{
const label curPatchStart = mesh_.boundaryMesh()[curBPatch].start();
labelList reverseAddressing(cp.size());
forAll(cp, facei)
{
if (cp[facei] <= 0)
{
FatalErrorInFunction << "Processor " << proci << " patch "
<< procField.mesh().boundary()[patchi].name()
<< " face " << facei << " originates from reversed face since "
<< cp[facei] << exit(FatalError);
}
reverseAddressing[facei] = cp[facei] - 1 - curPatchStart;
}
patchFields[curBPatch].rmap(procField.boundaryField()[patchi], reverseAddressing);
}
}
逐行解读:
- 第 100--110 行:为每个进程 patch 取
curBPatch = boundaryProcAddressing_[proci][patchi](该进程 patch 对应全局哪个 patch ),并用patchSlice(faceProcAddressing_[proci])取该 patch 的面地址子列表cp。 - 第 113 行:
curBPatch >= 0→ 普通物理 patch。 - 第 135--136 行:
curPatchStart是全局 patch 在全局面数组里的起点。 - 第 140--153 行:
cp[facei] <= 0直接 FatalError ------注意faceProcAddressing用正负号编码面方向,≤0 说明数据损坏。 - 第 157 行:
reverseAddressing[facei] = cp[facei] - 1 - curPatchStart。-1与-curPatchStart抵消全局 faceProcAddressing 存的"1-based + patch 偏移" ,还原成该 patch 内的 0-based 索引(OpenFOAM 约定全局面索引存为±(globalFace+1))。 - 第 161--165 行:
patchFields[curBPatch].rmap(...)用反向地址把进程边界值写进全局 patch。
机制链 F(完整推导):reconstructPar 的并行拼装算法
less
decomposePar 阶段(前置):
为每个进程生成三张 addressing 表:
cellProcAddressing : 局部 cell i → 全局 cell g
faceProcAddressing : 局部 face f → ±(全局 face + 1) (符号编码方向)
boundaryProcAddressing : 局部 patch p → 全局 patch P(或 -1/-表示 processor 侧)
存到 processorN/constant/polyMesh/
reconstructPar 阶段(本节):
1) fileHandler().nProcs() 探测进程数 → 建 nProcs 个 Time(各读 processorN)
2) timeSelector 选时间 → 各进程 Time 对齐
3) 读整域网格 fvMesh(从 constant/polyMesh)
4) processorMeshes procMeshes(databases, region) ← 装载各进程网格 + 三张 addressing
5) 每个时间步:
a) mesh.readUpdate() / procMeshes.readUpdate() ← 处理动网格
- POINTS_MOVED → procMeshes.reconstructPoints(mesh)
b) 扫描 processor0 的 IOobjectList → 得到要重建的场清单
c) 体积场:fvFieldReconstructor
internalField(全局 nCells)
for proci: internalField.rmap(procField, cellProcAddressing[proci])
d) 边界场:对每个进程 patch
curBPatch = boundaryProcAddressing[proci][patchi]
cp = patchSlice(faceProcAddressing[proci])
reverseAddressing = cp - 1 - patchStart (还原 0-based,符号即方向)
patchFields[curBPatch].rmap(...)
e) 点场:pointFieldReconstructor
f) 拉格朗日:lagrangianReconstructor(两趟:先收云名,再重建)
g) 面积场:faFieldReconstructor
h) sets:cellSet/faceSet/pointSet 用 cellMap/faceMap/pointMap 反查
i) hexRef8Data(AMR 细化数据)、uniform/ 目录从 master 复制
14.3 sets 与 uniform 的重建
sets 重建(第 631--812 行,节选):cellSet 用 cellMap[celli]、pointSet 用 pointMap[pointi] 把进程局部索引映射到全局;faceSet 用 fSet.insert(mag(faceMap[facei])-1)------**先取绝对值(去方向符号)再减 1(去 1-based 偏移)**得到全局 face 索引。uniform 目录复制(第 959--992 行):把 processor0 的 uniform/ 目录 (time 文件、profiling 报告等)拷到重建的时间目录------因为 uniform 里是"全域一致"的元数据,直接取 master 的即可 ;多区时第 0 区再额外拷一次顶层 uniform/。
14.4 reconstructPar 命令清单
bash
# 1) 基本:重建所有时间、所有场(跳过已有)
reconstructPar
# 2) 只重建新时间(增量,避免覆盖)
reconstructPar -newTimes
# 3) 只重建指定字段
reconstructPar -fields '(p U)'
# 4) 不重建拉格朗日/集合
reconstructPar -no-lagrangian -no-sets
# 5) 只重建网格(等价于 reconstructParMesh)
reconstructPar -no-fields
# 6) 指定时间范围
reconstructPar -time 1000:5000
# 7) 多区
reconstructPar -allRegions
| 选项 | 语义 | 典型场景 |
|---|---|---|
-newTimes |
只重建 master 尚无的时间 | 长时间运行中增量收集 |
-fields |
白名单 | 只关心几个场 |
-no-lagrangian |
跳过粒子 | 无拉格朗日 |
-no-sets |
跳过 sets | 不需要 cellSet 等 |
-no-fields |
只重建网格 | 配合 -constant |
-time |
时间选择 | timeSelector 语法 |
15. 并行 I/O 模式与结果重建总览
15.1 两种并行 I/O 模式
| 模式 | 产物形态 | 读取方式 | 适用 | 控制项 |
|---|---|---|---|---|
| uncollated(分散写) | 各进程各写 processorN/<time>/... |
必须 reconstructPar 或按进程读 |
大多数并行算例;可用 reconstructPar 灵活拼装 |
默认 |
| collated(合并写) | 单套文件(配合 fileHandler),文件数少 |
可单文件直接读(ParaView/postProcess) |
大规模算例、单文件读取友好 | fileHandler / writeFormat |
controlDict 的 writeFormat 控制 ASCII/binary;fileHandler 控制 collated/uncollated 与各类 fileHandler 实现(如 collated、uncollated、adios)。大规模算例常用 collated + 并行 I/O 库(如 modules/adios 的 ADIOS2)以提升写盘带宽。
15.2 并行后处理命令清单(模板 T3 的第三部分)
bash
# 分解 → 运行 → 重建的完整链路
decomposePar # 生成 processorN/
mpirun -np 8 solver -parallel
reconstructPar # 拼回整域时间目录
# 只看网格结构
reconstructParMesh -constant
# 重分布(改进程数后重新分区)
redistributePar -parallel
# 转换并行结果(foamToVTK 支持并行运行,只由 master 写盘)
mpirun -np 8 foamToVTK -parallel
15.3 数据交换全链路
一句话概括:外部网格格式经 *ToFoam(+ src/conversion/common/reader)转成 polyMesh → 求解产生结果(collated 或 processorN/)→ 后处理分四路:reconstructPar(拼回整域)、foamToVTK(VTK/.vtm.series)、foamToEnsight(EnSight/.case)、postProcess(functionObjects)。
16. modules/:官方可选扩展
16.1 modules 与 plugins 的区别
modules/README.md(第 10--13 行)说清了二者的分野:"modules are mainly maintained and released by OpenFOAM developers, whereas plugins are an open and welcoming area for add-ons, predominantly maintained and driven by OpenFOAM community members and groups."------modules 是官方维护、随主仓库发布;plugins 是社区贡献的开放区。
16.2 submodule 机制
modules/README.md(第 21--25 行)给出首次使用步骤:
bash
cd $WM_PROJECT_DIR
git submodule update --init --recursive
- 一条命令克隆全部(含嵌套)子模块;可只初始化指定项:
git submodule update --init --recursive modules/turbulence-community。git submodule status --recursive看状态,cat .gitmodules看清单,git submodule deinit <name>清空某项。
本仓库 .gitmodules 里的 modules 条目:
| 子模块 | 路径 | 仓库 |
|---|---|---|
| adios | modules/adios |
gitlab.com/openfoam/modules/adiosfoam.git |
| OpenQBMM | modules/OpenQBMM |
github.com/OpenQBMM/OpenQBMM.git(branch openfoam.com) |
| visualization | modules/visualization |
gitlab.com/openfoam/modules/visualization.git |
| external-solver | modules/external-solver |
gitlab.com/openfoam/modules/external-solver.git |
(另有 plugins/ 下的 cfmesh、avalanche、bindings/pybFoam、pyOFTools、vtk-hdf、turbulence-community、data-community、research。)
16.3 构建入口:modules/list-modules 与 modules/Allwmake
modules/list-modules(第 68--82 行):
sh
# Each first-level directory with an Allwmake file
for moduleName in *
do
if [ -f "$moduleName/Allwmake" ]
then
case "$moduleName" in
(build | doc | platform*) # Skip some directory names
;;
(*)
echo "$moduleName"
;;
esac
fi
done
- 判定标准:第一级目录且含
Allwmake文件 ;跳过build/doc/platform*。
modules/Allwmake(第 1--39 行)关键段:
sh
targetType=libso
[ -n "$FOAM_MODULE_PREFIX" ] || FOAM_MODULE_PREFIX="${FOAM_LIBBIN%/*}"
export FOAM_MODULE_PREFIX
case "$FOAM_MODULE_PREFIX" in
(false | none) # 允许通过 prefix 禁用整个 modules 构建
echo "OpenFOAM modules disabled (prefix=${FOAM_MODULE_PREFIX})"
exit 0
esac
set +e
export WM_CONTINUE_ON_ERROR=true
for moduleName in $(./list-modules)
do
if [ -d "$moduleName" ]
then
( cd "$moduleName" && wmake -all $targetType ) # -all 允许 Allwmake.override
fi
done
- 第 11 行:
FOAM_MODULE_PREFIX默认取${FOAM_LIBBIN%/*}------即默认装到 OpenFOAM 项目位置。 - 第 15--20 行:前缀为
false/none时整体禁用 modules并干净退出。 - 第 27--28 行:
set +e+WM_CONTINUE_ON_ERROR=true------模块构建出错不中止(模块是可选组件)。 - 第 32 行:
list-modules列出各项,cd进去wmake -all------注释点明用wmake -all而非Allwmake以允许Allwmake.override覆盖。
16.4 构建位置与 -prefix
modules/README.md(第 86--91、106--135 行)给出 -prefix 对照表与模块代码要求:
| 命令 | 安装位置 |
|---|---|
./Allwmake -prefix=user |
$FOAM_USER_APPBIN、$FOAM_USER_LIBBIN |
./Allwmake -prefix=group |
$FOAM_SITE_APPBIN、$FOAM_SITE_LIBBIN |
./Allwmake -prefix=openfoam |
$FOAM_APPBIN、$FOAM_LIBBIN |
./Allwmake -prefix=/some/pathname |
/some/pathname/bin、/some/pathname/lib |
模块要支持各位置,Make/files 应写成 EXE = $(FOAM_MODULE_APPBIN)/...、LIB = $(FOAM_MODULE_LIBBIN)/...;Make/options 里 include $(GENERAL_RULES)/module-path-user(或旧版兼容写法:sinclude .../module-path-user 再 ifeq (,$(FOAM_MODULE_APPBIN)) FOAM_MODULE_APPBIN=$(FOAM_USER_APPBIN) endif)。
顶层
Allwmake(第 75--97 行)也处理 modules:FOAM_MODULE_PREFIX=false/none时打印"modules disabled、可用./Allwmake-modules -prefix=...单独构建",否则cd modules && wmake -all。
modules 模块对比表:
| 模块 | 作用 | 依赖 |
|---|---|---|
adios/ |
ADIOS2 高性能并行 I/O(大规模写盘) | ADIOS2 库 |
visualization/ |
ParaView Catalyst 原位可视化(免写盘出图) | Catalyst |
external-solver/ |
外部线性求解器(PETSc/Hypre 等)集成 | PETSc/Hypre |
OpenQBMM/ |
矩方法/群体平衡(成核、聚并) | --- |
17. applications/test/:单元测试体系
17.1 规模
applications/test/ 下有 309 个一级目录 (本仓库实测 Get-ChildItem -Directory 计数),一个目录一个测试程序 (多数命名为 Test-<thing>.C/.cxx,Make/ 内两个文件)。覆盖面从基础类型到并行通信、从容器到 AMR。
从命名可看覆盖面(部分):基础类型(argList、autoPtr、dictionary、tmp、refPtr);场与张量(Field/volField、tensor/tensorFields);表达式(exprEntry/exprValue1/2/exprTraits);容器(hashTable1..4/HashSet/HashPtrTable、List/List2);网格与求解(hexRef8、GAMMAgglomeration、dynamicIndexedOctree);并行 (parallel-comm0..3b、parallel-nonBlocking、parallel-scan 等);coded(codeStream、dynamicLibrary);IO/环境(ied、IOobject、etcFiles、foamVersion、wmake1)。
17.2 逐段:wmake1/Test-wmake1.C
applications/test/wmake1/Test-wmake1.C(第 48--61 行)演示"如何测试 wmake 的条件编译":
cpp
int main(int argc, char *argv[])
{
printTest();
#ifdef FULLDEBUG
Info<< nl << "Compiled with FULLDEBUG" << nl;
#else
Info<< nl << "Regular compilation" << nl;
#endif
Info<< "\nEnd\n" << nl;
return 0;
}
- 调用
printTest()(在newStub.C/oldStub.C之一里定义,用于测试Make/files的源文件切换)。 #ifdef FULLDEBUG分支打印-------DFULLDEBUG由 Debug 档位的c++DBUG提供(见 3.2 的cDebug/c++Debug)。用同一个源文件验证"Opt/Debug 编译路径是否真的注入/未注入宏"。
17.3 逐段:parallel-comm0(通信器基础)
applications/test/parallel-comm0/Test-parallel-comm0.cxx(第 47--131 行)节选:
cpp
void printInfo(const label comm)
{
Info<< "comm:" << comm << " nprocs:" << UPstream::nProcs(comm)
<< " all:" << UPstream::allProcs(comm) << " sub:" << UPstream::subProcs(comm) << nl;
}
int main(int argc, char *argv[])
{
argList::noBanner();
argList::noCheckProcessorDirectories();
...
label worldVal = returnReduce(val, sumOp<label>(), UPstream::msgType(), UPstream::commWorld());
label selfVal = returnReduce(val, sumOp<label>(), UPstream::msgType(), UPstream::commSelf());
bitSet localUsed(nProcs);
localUsed.set(myRank, ((myRank % 4) == 0));
localUsed.reduceOr(UPstream::worldComm, false);
bitSet procUsed(bitSet::allGather((myRank % 4) == 0));
PackedList<2> uniformity(10); // 0:empty 1:uniform 2:nonuniform 3:mixed
...
Foam::reduce(uniformity.data(), uniformity.num_blocks(), bitOrOp<unsigned int>(),
UPstream::msgType(), UPstream::worldComm);
}
- 第 48--61 行:打印世界/自身通信器的
nProcs/allProcs/subProcs。 - 第 92--98 行:
returnReduce在commWorld与commSelf上求sumOp<label>------验证"同一归约在不同通信器上结果不同"。 - 第 113--120 行:
bitSet::reduceOr与bitSet::allGather------测试位集归约/全收集。 - 第 133--173 行:
PackedList<2>(每位 2 bit,编码 4 种均匀性)+Foam::reduce+bitOrOp------测试对"打包位集"的按位或归约(注释给出 0:empty/1:uniform/2:nonuniform/3:mixed 编码)。
17.4 逐段:foamVersion
applications/test/foamVersion/Test-foamVersion.cxx(第 43--112 行)节选:
cpp
int main()
{
foamVersion::printBuildInfo(Info.stdStream()); // 打印构建信息
Info<< "version " << Foam::FOAMversion << " build " << Foam::FOAMbuild
<< " buildArch " << Foam::FOAMbuildArch << nl; // 宏
Info<< "api " << foamVersion::api << " patch " << foamVersion::patch
<< " version " << foamVersion::version << nl; // 命名空间
for (const std::string& str : { "MSB;label=32;scalar=64", "LSB;label=64;scalar=32",
"LSB;label=;scalar=junk", "LSB label=32 scalar=64" })
{ testExtraction(str); } // 尺寸串解析健壮性
}
- 第 55 行:
foamVersion::printBuildInfo------打印构建信息。 - 第 58--61 行:宏
Foam::FOAMversion/FOAMbuild/FOAMbuildArch。 - 第 66--70 行:命名空间
foamVersion::api/patch/version/build/buildArch。 - 第 43--49 行 + 第 90--106 行:用一组"畸形/正常"尺寸串测试
labelByteSize/scalarByteSize的解析健壮性------这正是"版本/精度元数据"的单元测试。
17.5 用法清单(模板 T5)
bash
# 编译并运行单个测试
cd $WM_PROJECT_DIR/applications/test/hashTable1
wmake # 生成 Test-hashTable1
./Test-hashTable1
# 有算例的测试(自带头部算例目录)
cd ../volField
wmake && ./Test-volField -case cavity
# 批量编译全部测试(applications/Allwmake 会遍历)
cd $WM_PROJECT_DIR/applications && ./Allwmake -test
| 用法 | 命令 | 说明 |
|---|---|---|
| 单测编译 | wmake |
生成 Test-<name> 二进制 |
| 运行 | ./Test-<name> |
打印 Info/断言结果 |
| 带算例 | -case <dir> |
部分测试自带 case1/、cavity/ 等 |
| 选学对象 | 直接读 Test-*.C |
每个测试是"可运行的文档" |
18. tutorials/ 端到端回归与版本元数据
18.1 tutorials/:端到端回归集
tutorials/ 下每个求解器都有对应教程算例,配合 Allrun/Allclean 脚本可批量"跑一遍看是否正常"。这是新版本发布前的主要验证手段:既验证求解器数值正确,也验证 I/O、后处理脚本在真实算例上可用。
18.2 版本元数据与一致性校验
META-INFO/:含api-info与README.md,记录 API/补丁级别。顶层Allwmake末尾会sed打印META-INFO/api-info(第 117 行)。wmake -build-info:查询{api, branch, build}。foamEtcFile -show-api/-show-patch:脚本友好的一行输出。foamVersion(applications/test/foamVersion):打印全部版本信息。
顶层 Allwmake(第 20--43 行)在编译前还会报告环境:list_tools(或 mpirun 位置)、wmake -show-path-cxx 得到的编译器路径与 --version 首行、wmake -show-cxxflags,并打印 WM_PROJECT_DIR、WM_COMPILER [WM_COMPILE_CONTROL]、WM_OPTIONS, with WM_MPLIB FOAM_MPI------构建日志本身就是一份可复现的环境快照。
19. 常见报错与排障(27 条)
下表按"现象 → 成因 → 排查 → 解决"组织,覆盖本节的五个主题面(第 1--24 条为 MinGW/profiling/VTK/Ensight/reconstructPar 相关,第 25--27 条为环境/元数据相关)。
| # | 现象 | 成因 | 排查 | 解决 |
|---|---|---|---|---|
| 1 | MinGW 链接 undefined reference to ..._Z... |
Windows DLL 中该 C++ mangled 符号未被导出 | `llvm-nm --defined-only --extern-only |
awk '2~/[BDRT]/ && 3~/^_Z/'` |
| 2 | 运行 exe 报"找不到 XXX.dll" | DLL 未随 exe 部署,或 PATH 无 FOAM_LIBBIN |
看依赖 `x86_64-w64-mingw32-objdump -p X.exe | grep DLL` |
| 3 | 循环依赖:OpenFOAM 编译期找不到 Pstream 符号 | Pstream↔OpenFOAM 成环,DLL 模型无法 --start-group |
看 src/Allwmake 是否进入 */Mingw* 分支 |
确认 Pstream/Allwmake libo + FOAM_LINK_DUMMY_PSTREAM=libo 已执行 |
| 4 | .def 文件缺失 |
libo 未生成 .def(非 ARM64 分支不自动生成) |
ls $FOAM_LIBBIN/dummy/*.def |
ARM64 走 LLVM_MINGW_EXPORT_DEF;x86_64 可用 nm 手工生成 |
| 5 | wmake 报找不到 x86_64-w64-mingw32-g++ |
宿主未装 mingw-w64 工具链 | which x86_64-w64-mingw32-g++ |
apt install mingw-w64 |
| 6 | 交叉编译编译到宿主的 .so 而非 .dll |
未设 FOAM_CONFIG_ETC=etc-mingw,规则目录错 |
echo $WM_OPTIONS 是否含 Mingw |
设 FOAM_CONFIG_ETC=etc-mingw 并重新 source etc/bashrc |
| 7 | WM_OSTYPE 不是 MSwindows |
WM_COMPILER 未设为 Mingw* |
echo $WM_COMPILER |
export WM_COMPILER=Mingw |
| 8 | 编译报 too many sections |
大文件超 16 位 COFF section 上限 | 看是否缺 -mbig-obj |
规则已加 c++FLAGS += -Wa,-mbig-obj(第 22 行) |
| 9 | 调试信息无法被 Windows 调试器识别 | 用了 DWARF 而非 stabs | 看 c++Debug 是否 -gstabs3 |
用 Debug 档位(-gstabs3) |
| 10 | 链接期 -lPstream 找不到 |
dummy stub 未建或路径错 | ls $FOAM_LIBBIN/dummy |
设 WM_MPLIB=dummy 或依赖 PROJECT_LIBS 默认 dummy 目录 |
| 11 | profiling 报告没生成 | active 被设 false,或全局 allowProfiling 0 |
grep -i 'allowProfiling' 于 etc/controlDict |
controlDict 加 profiling{},或命令行 -profiling 强制 |
| 12 | profiling 报告只有 trigger0 |
用户代码没插 addProfiling |
查报告 description 仅 application::main |
在关键段加 addProfiling(Name, "...") |
| 13 | profiling 运行变慢 | memInfo 每次计时点都 update()(系统调用) |
看 controlDict 是否开了 memInfo |
关掉 memInfo(默认 false) |
| 14 | 并行 profiling 只在 processor0 有 | 各进程各写一份 | ls processor*/<time>/uniform/profiling |
用 profilingSummary 汇总 |
| 15 | profilingSummary 报找不到文件 |
某些 rank 缺 uniform/profiling |
看输出 "Found k/n profiling files. Skipping" | 确保所有 rank 都开了 profiling 并写了报告 |
| 16 | addProfiling 后 FatalError "different id than the top" |
addProfiling/endProfiling 配错对 |
看报错打印的 info/top id 与描述 | 保证同一作用域/同名变量配对 |
| 17 | foamToVTK 输出 VTK 打开报 XML 错 |
开闭标签不匹配(formatter 状态机触发警告) | 看 stderr 的 WarningInFunction: ... xml ... |
修输出(一般不会,属内部问题);确认版本 |
| 18 | foamToVTK 缺字段 |
字段被 -fields/-no-fields/类别剪枝过滤 |
看 Info 的 "Including/Excluding fields" | 显式 -fields,或去掉 -no-point-data/-no-surface 类限制 |
| 19 | legacy binary VTK 尺寸不符警告 | sizeof(float)!=4 或 sizeof(label)!=4 |
看警告 "Using ASCII rather than legacy binary" | 该情形自动退 ASCII,属预期;改用 XML 格式 |
| 20 | foamToEnsight 数据编号跳号 |
用时间索引而非连续编号 | 看 data/######## 文件名 |
用 -index <start> 连续编号 |
| 21 | foamToEnsight 动网格 -no-mesh 无效 |
有动网格时必须每步写几何 | 看 Info "has moving mesh: ignoring '-no-mesh'" | 属预期;如需静态几何先冻结网格 |
| 22 | reconstructPar 报 "No processor* directories found" |
未 decomposePar,或路径/大小写错 |
查 ls processor* |
先 decomposePar;确认在算例根目录运行 |
| 23 | reconstructPar 边界场面 originates from reversed face |
faceProcAddressing 符号 ≤0,说明数据损坏 |
看报错 patch/face 索引 | 重新 decomposePar(勿手工改 addressing 文件) |
| 24 | 多次 foamToVTK 后时间序列乱 |
.vtm.series 未增量更新或被截断 |
看 .vtm.series 内容 |
依赖内置 series.load→append→write;勿手工删 series |
补充三条与环境/元数据相关的排障:
| # | 现象 | 成因 | 排查 | 解决 |
|---|---|---|---|---|
| 25 | wmake 用了错误规则目录 |
WM_COMPILER 家族识别错(版本后缀未剥离) |
看 COMPILER_FAMILY(sed 's/[-+.0-9~].*//') |
检查 WM_COMPILER 值(如 Gcc/Mingw) |
| 26 | modules 未构建 | FOAM_MODULE_PREFIX=false/none |
看顶层 Allwmake 的 "modules disabled" | ./Allwmake-modules -prefix=openfoam |
| 27 | 某 module 编译失败但整体不报错 | WM_CONTINUE_ON_ERROR=true 有意放行 |
看 modules/<name> 的 wmake 输出 |
单独 cd modules/<name> && wmake -all libso 排查 |
20. 工程工具链收官
把 S2-15 与本节合起来,一个 OpenFOAM 开发者需要掌握的工具链闭环是:
bash
写代码 → wmake(S2-15、第一季第 2 章)
调试 → Info/FatalError/DebugInfo + applications/test
性能 → profiling(controlDict)→ 定位瓶颈 → OpenMP/SPDP/算法
并行 → decomposePar → 运行 → reconstructPar
后处理 → functionObjects / foamToVTK / foamToEnSight / postProcess
跨平台 → MinGW 交叉编译(.def / libo / stub)
扩展 → libs(...) / modules(adios/visualization/external-solver)/ plugins
验证 → applications/test + tutorials + META-INFO
五条主线回顾:
| 主线 | 核心机制 | 关键文件 |
|---|---|---|
| 跨平台构建 | .def 显式导出 + libo 单对象破环 + dummy stub |
src/Allwmake、wmake/makefiles/general、wmake/rules/*Mingw |
| 性能剖析 | 计时树(pool_/children_/stack_/times_)+ 报告字典 |
src/OpenFOAM/global/profiling/* |
| 数据交换 | formatter 家族 + base64/append 布局 + meshReader 抽象 |
src/fileFormats/*、src/conversion/* |
| 结果重建 | cellProcAddressing.rmap + face 反向寻址 |
fvFieldReconstructor.txx、reconstructPar |
| 扩展与验证 | submodule modules + applications/test 自检 |
modules/*、applications/test/* |
贯穿两季的主题 :OpenFOAM 用三层抽象 (运行期选择 RTS、字典驱动、模板 + 零拷贝)把"配置、物理、数值、并行、网格"统一在一个可编程框架里;而工程工具链则把这份框架"能构建、可测量、可交换、可验证"地交付给使用者。理解这两面,就能在任意一层(库/应用/配置/构建)扩展 OpenFOAM。
本节核心结论 :工程面看似"外围",实则决定了源码能否落地。MinGW 的"符号模型差异"、profiling 的"计时树",到 VTK 的"base64/append 布局"与 reconstructPar 的"反向映射拼装",每一处都是用具体数据结构解决具体平台/性能/格式约束的范例。