OpenFOAM 源码深度剖析 · 第二季-16 工程与工具链:Windows 交叉编译、性能剖析、数据交换与测试

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 里其实有两种完全不同的含义,必须严格区分:

  1. runtime-only(运行期支持) :在 Windows(或 MSYS/MinGW shell)里能运行 Linux 上编出来的东西------这依赖 Windows 的 WSL/POSIX 子系统,源码层面只提供 OSspecific/MSwindows 适配层;
  2. 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 行:当"家族"与"完整编译器名"不同时 (例如 Mingw vs Gcc-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)------标记"在栈上"。
  • endTimer:
    • 第 101--104 行:从 stack_/times_ 各自弹出栈顶(必须成对)。
    • 第 106 行:info->update(clockval.elapsed())------用进入时刻算出 elapsed,交给节点累加。
    • 第 107 行:setActive(false)------标记"离开栈"。
    • 第 109 行:返回该节点指针(供调用方比较)。

这就是计时树的两条原语 :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 { ... }

解读四步法:

  1. 看 totalTime - childTime (自身净时间)排序。占比最高的几项就是真正的瓶颈 ;只看 totalTime 会被"父节点包含子节点"误导。
  2. 看 calls :次数异常多(百万级)而单次极短的项,往往是可被合并/向量化/减少调用的热点(如逐面循环里的某个函数)。
  3. 看调用树(parentId 链) :沿 parentId 回溯,能找到"最外层耗时入口"(如某求解器的 solve),再逐层下钻。
  4. 看 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 位浮点。这是"体积/精度"的显式取舍,也解释了 foamToVTK legacy 分支为何要检查 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 .o 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 的"反向映射拼装",每一处都是用具体数据结构解决具体平台/性能/格式约束的范例。

相关推荐
鶴哥只手遮天1 小时前
Gmsh 源码深度解析-第 3 章 脚本语言与解析器
算法·开源
鶴哥只手遮天1 小时前
Gmsh 源码深度解析-第 5 章 二维曲面网格算法
算法·开源
鶴哥只手遮天1 小时前
Gmsh 源码深度解析-第 1 章 架构总览与数据模型
算法·开源
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-12 `coded` 系列:在字典里写 C++ 并在运行期编译
算法·开源
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-01 多相流与 VOF:`interFoam`、界面压缩与 MULES
算法·开源
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-14 `functionObjects`、求解器控制框架 与 表达式系统
算法·开源
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-13 `fvOptions` / `fvConstraints` / `corrections`
算法·开源
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-03 拉格朗日粒子与喷雾:`Cloud`/`parcel` 模板体系、运动积分与子模型群
算法·开源
hetao17338371 小时前
校内场-提高组 ZYZSC-S-Round 2
c++·算法