ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LAMMPS 崩溃与“假死“排查实战:GDB、Core Dump 与 Valgrind 完整工作流

LAMMPS 崩溃与“假死“排查实战:GDB、Core Dump 与 Valgrind 完整工作流
  • 科研
  • 科学计算
  • 高性能计算

【免费下载链接】lammps

Public development project of the LAMMPS MD software package

项目地址:https://gitcode.com/gh_mirrors/la/lammps
点击查看免费下载

本篇技术指南基于 LAMMPS 官方文档 Errors_debug.rst 展开,系统讲解当lmp出现Segmentation fault、bus error或进程看似卡死时的完整排查方法。文中以一个被刻意植入空指针缺陷的lj/cutpair style 为案例,演示如何用 GDB 在调试器中运行 LAMMPS、检查 core dump 文件、用 valgrind 定位内存错误,以及如何在计算"看似卡住"时把调试器附加到正在运行的进程上;并结合 pair_lj_cut.cpp、error.cpp 等源码解释崩溃与"卡住"背后的底层机制。读完后,你应当能够独立定位段错误的来源函数、解释悬垂/空指针问题,并判断一个 LAMMPS 计算究竟是"太慢"还是"真的挂了"。

一、前提条件与可复现的崩溃案例

1.1 编译必须包含调试信息

所有排查手段的第一步都是同一个前提:LAMMPS 可执行文件必须包含调试信息(debug information)。如果编译时没有保留符号表,GDB 就无法把崩溃地址反查回函数名和源码行号,只能得到一串无意义的十六进制地址。构建时如何开启调试选项,参见构建文档 Build.rst 中的调试相关配置。

1.2 用一个补丁制造一个"标准崩溃"

官方文档给出了一个非常精巧的排错教学案例:对 pair_lj_cut.cpp 打一个补丁,向PairLJCut::compute()中注入一个空指针缺陷,再配合官方示例输入 examples/melt/in.melt(一个 4000 原子的 Lennard-Jones 熔化体系)运行。补丁内容如下:

--- a/src/pair_lj_cut.cpp +++ b/src/pair_lj_cut.cpp @@ -81,6 +81,7 @@ void PairLJCut::compute(int eflag, int vflag) int nlocal = atom->nlocal; double *special_lj = force->special_lj; int newton_pair = force->newton_pair; + double comx = 0.0; inum = list->inum; ilist = list->ilist; @@ -134,8 +135,10 @@ void PairLJCut::compute(int eflag, int vflag) evdwl,0.0,fpair,delx,dely,delz); } } - } + comx += atom->rmass[i]*x[i][0]; /* BUG */ + } + printf("comx = %g\n",comx); if (vflag_fdotr) virial_fdotr_compute(); }

对照当前仓库源码可以确认这个缺陷的位置:compute() 中正常使用的成员是atom->x(坐标数组,第 81 行处声明double **x = atom->x;),而补丁里访问的atom->rmass(原子质量数组)在这个 melt 示例中从未被任何 fix 分配和填充——只有需要显式管理动能/质量信息的 fix 才会设置它。因此atom->rmass保持为 NULL,对atom->rmass[i]的解引用就是典型的 NULL 指针访问。

重新编译并运行后,你会得到这样的输出(节选自原文档):

$ ./lmp -in in.melt LAMMPS (19 Mar 2020) using 1 OpenMP thread(s) per MPI task Lattice spacing in x,y,z = 1.6796 1.6796 1.6796 Created orthogonal box = (0 0 0) to (16.796 16.796 16.796) 1 by 1 by 1 MPI processor grid Created 4000 atoms create_atoms CPU = 0.000432253 secs Neighbor list info ... update every 20 steps, delay 0 steps, check no max neighbors/atom: 2000, page size: 100000 master list distance cutoff = 2.8 ghost atom cutoff = 2.8 binsize = 1.4, bins = 12 12 12 1 neighbor lists, perpetual/occasional/extra = 1 0 0 (1) pair lj/cut, perpetual attributes: half, newton on pair build: half/bin/atomonly/newton stencil: half/bin/3d/newton bin: standard Setting up Verlet run ... Unit style : lj Current step : 0 Time step : 0.005 Segmentation fault (core dumped)

值得注意的细节:崩溃发生在Current step : 0的 Verlet 设置阶段,也就是第一个 timestep 构建邻居列表之后、真正步进之前。这一点与 in.melt 的脚本结构吻合——thermo 50和run 250意味着此时还没有任何步进输出,程序在run命令执行到 force/energy 首次计算时就死掉了。这类"还没开始跑就崩"的场景,是 pair style 空指针、参数缺失等 bug 最常见的暴露时机。

二、方法一:用 GDB 调试器获取堆栈跟踪

GDB 是排查段错误的核心工具。原文档指出 GDB 功能远不止于此(官方在线文档有大量选项),下面完整保留文档中的两条路径:在调试器里直接启动 LAMMPS与事后检查 core dump 文件。

2.1 在调试器中运行 LAMMPS

注意一个重要限制:在 GDB 中直接启动 LAMMPS 只对单 MPI rank 有效。调试多进程并行程序通常需要并行调试器。最简单的启动方式是在命令行前加gdb --args,然后在 GDB 提示符下输入run:

$ gdb --args ./lmp -in in.melt (gdb) run [...] Setting up Verlet run ... Unit style : lj Current step : 0 Time step : 0.005 Program received signal SIGSEGV, Segmentation fault. 0x00000000006653ab in LAMMPS_NS::PairLJCut::compute (this=0x829740, eflag=1, vflag=<optimized out>) at /home/akohlmey/compile/lammps/src/pair_lj_cut.cpp:139 139 comx += atom->rmass[i]*x[i][0]; /* BUG */ (gdb)

GDB 在触发 SIGSEGV 的瞬间停下,直接打印出当前代码行:pair_lj_cut.cpp:139的缺陷语句,函数名PairLJCut::compute、参数eflag=1全部可见——这正是调试信息的作用所在。

接下来输入where命令,可以看到从崩溃点一路回溯到main()的完整调用栈:

(gdb) where #0 0x00000000006653ab in LAMMPS_NS::PairLJCut::compute (this=0x829740, eflag=1, vflag=<optimized out>) at /home/akohlmey/compile/lammps/src/pair_lj_cut.cpp:139 #1 0x00000000004cf0a2 in LAMMPS_NS::Verlet::setup (this=0x7e6c90, flag=1) at /home/akohlmey/compile/lammps/src/verlet.cpp:131 #2 0x000000000049db42 in LAMMPS_NS::Run::command (this=this@entry=0x7fffffffcca0, narg=narg@entry=1, arg=arg@entry=0x7e8750) at /home/akohlmey/compile/lammps/src/run.cpp:177 #3 0x000000000041258a in LAMMPS_NS::Input::command_creator<LAMMPS_NS::Run> (lmp=<optimized out>, narg=1, arg=0x7e8750) at /home/akohlmey/compile/lammps/src/input.cpp:878 #4 0x0000000000410ad3 in LAMMPS_NS::Input::execute_command (this=0x7d1410) at /home/akohlmey/compile/lammps/src/input.cpp:864 #5 0x00000000004111fb in LAMMPS_NS::Input::file (this=0x7d1410) at /home/akohlmey/compile/lammps/src/input.cpp:229 #6 0x000000000040933a in main (argc=<optimized out>, argv=<optimized out>) at /home/akohlmey/compile/lammps/src/main.cpp:65 (gdb)

这条调用链本身就是一份"崩溃事故报告":main()读取输入文件 →Input::execute_command()逐行解释命令 → 执行到run命令时交给Run::command()→Verlet::setup()构建力计算环境 → 最终落入PairLJCut::compute()崩溃。对照当前仓库源码可逐帧验证:入口在 main.cpp 中lammps->input->file()驱动输入脚本,而 pair 计算入口就是 PairLJCut::compute(第 71 行起),补丁注入的第 139 行正位于其邻居循环之内。

进一步地,可以用print打印变量值来找出真正的异常点。段错误最常见的原因之一就是指针变量未被赋值、仍为 NULL——本例正是如此:

(gdb) print x $1 = (double **) 0x7ffff7ca1010 (gdb) print i $2 = 0 (gdb) print x[0] $3 = (double *) 0x7ffff6d80010 (gdb) print x[0][0] $4 = 0 (gdb) print x[1][0] $5 = 0.83979809569125363 (gdb) print atom->rmass $6 = (double *) 0x0 (gdb)

坐标指针x、下标i都正常,唯独atom->rmass是0x0——空指针实锤。排错的技巧性在于:逐层print把"哪一行"的问题细化成"哪个指针没分配"的问题。

2.2 用调试器检查 core dump 文件

当可执行文件崩溃并提示 "core dumped" 时,系统会生成core或core.<PID#>文件,保存进程当时的完整内存与寄存器状态。这个文件可能位于你运行 LAMMPS 的目录,也可能被 systemd 守护进程收进隐藏目录。后一种情况需要用coredumpctl工具把 core 文件抽取到当前目录,例如:

coredumpctl -o core dump lmp

然后直接用 GDB 同时加载可执行文件、调试信息和 core 文件:

$ gdb lmp core Reading symbols from lmp... [New LWP 1928535] [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib64/libthread_db.so.1". Core was generated by `./lmp -in in.melt'. Program terminated with signal SIGSEGV, Segmentation fault. #0 0x00000000006653ab in LAMMPS_NS::PairLJCut::compute (this=0x1b10740, eflag=1, vflag=<optimized out>) at /home/akohlmey/compile/lammps/src/pair_lj_cut.cpp:139 139 comx += atom->rmass[i]*x[i][0]; /* BUG */ (gdb)

注意它直接落在与在线调试完全相同的崩溃行。从这里开始,where、print等命令的用法与前文完全一致。两种 GDB 路径的适用场景可以这样区分:

  • 在调试器中运行:适合能在本地稳定复现的崩溃,可以随时打断点、单步;
  • 检查 core dump:适合生产环境中崩溃后无法立即复现、或希望事后从容分析的情况,尤其适合配合集群作业脚本使用。

三、方法二:用 valgrind 获取内存错误报告

valgrind 工具套件通过模拟 CPU 并对程序进行插桩来"近距离"检查编译后程序的行为。它带来的执行速度下降非常显著,但优势在于能报告那些尚未导致崩溃的内存问题(越界读、未初始化内存、内存泄漏等)。默认的 valgrind 工具是内存检查器(Memcheck),使用前只需在正常命令行前加valgrind。

与 GDB 不同,valgrind 也适用于并行执行。官方文档建议把 valgrind 的输出重定向到文件,例如用--log-file=crash-%p.txt(其中%p会被替换为进程 ID),以避免多个 valgrind 实例的控制台输出混杂在一起。完整输出示例:

$ valgrind ./lmp -in in.melt ==1933642== Memcheck, a memory error detector ==1933642== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. ==1933642== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info ==1933642== Command: ./lmp -in in.melt ==1933642== LAMMPS (19 Mar 2020) OMP_NUM_THREADS environment is not set. Defaulting to 1 thread. (src/comm.cpp:94) using 1 OpenMP thread(s) per MPI task Lattice spacing in x,y,z = 1.6796 1.6796 1.6796 Created orthogonal box = (0 0 0) to (16.796 16.796 16.796) 1 by 1 by 1 MPI processor grid Created 4000 atoms create_atoms CPU = 0.032964 secs Neighbor list info ... update every 20 steps, delay 0 steps, check no max neighbors/atom: 2000, page size: 100000 master list distance cutoff = 2.8 ghost atom cutoff = 2.8 binsize = 1.4, bins = 12 12 12 1 neighbor lists, perpetual/occasional/extra = 1 0 0 (1) pair lj/cut, perpetual attributes: half, newton on pair build: half/bin/atomonly/newton stencil: half/bin/3d/newton bin: standard Setting up Verlet run ... Unit style : lj Current step : 0 Time step : 0.005 ==1933642== Invalid read of size 8 ==1933642== at 0x6653AB: LAMMPS_NS::PairLJCut::compute(int, int) (pair_lj_cut.cpp:139) ==1933642== by 0x4CF0A1: LAMMPS_NS::Verlet::setup(int) (verlet.cpp:131) ==1933642== by 0x49DB41: LAMMPS_NS::Run::command(int, char**) (run.cpp:177) ==1933642== by 0x412589: void LAMMPS_NS::Input::command_creator<LAMMPS_NS::Run>(LAMMPS_NS::LAMMPS*, int, char**) (input.cpp:881) ==1933642== by 0x410AD2: LAMMPS_NS::Input::execute_command() (input.cpp:864) ==1933642== by 0x4111FA: LAMMPS_NS::Input::file() (input.cpp:229) ==1933642== by 0x409339: main (main.cpp:65) ==1933642== Address 0x0 is not stack'd, malloc'd or (recently) free'd ==1933642==

可以看到:

  • 堆栈信息与 GDB 得到的基本一致(同样落在pair_lj_cut.cpp:139,同样经由Verlet::setup→Run::command→Input::file→main);
  • 额外多了一条更具体的提示:Invalid read of size 8加上Address 0x0 is not stack'd, malloc'd or (recently) free'd,即这是一次对 NULL 地址的 8 字节读取,直接点明段错误的本质是空指针解引用;
  • 但 valgrind 不会告诉你是哪一个指针为 NULL——这一步仍需回到 GDB 用print逐个检查。

因此,GDB(精确定位指针)与 valgrind(提前发现未崩溃的内存错误、支持并行)是互补关系,官方建议两者配合使用。

四、当 LAMMPS 看似"卡死"时的排查

另一种常见状况不是崩溃,而是计算看似卡住:进程仍然活跃,但没有任何可见进展。原文档归纳了五类原因,这里完整保留并结合源码逐一说明:

4.1 风格本身很慢,体系又很大

所选 pair/fix 等 style 计算慢、体系大,需要大量 CPU 时间。从小体系外推预期速度时要意识到:并非所有模型都随体系规模线性扩展,典型如ewald、pppm等 kspace 风格(参见 kspace 风格文档 kspace_style.rst 或 KSPACE 目录下的实现)。这种情况下能做的确实有限——只能换模型、缩小体系或加机器。

4.2 输出间隔未设置或设得太大

thermo命令控制热力学输出频率。若从未设置thermo,则只在第一步和最后一步有输出,中间看起来就像"没在动"。in.melt 中使用的是thermo 50,即每 50 步打印一次。

4.3 输出被块缓冲(block-buffered)

标准输出可能是块缓冲而非行缓冲:要累积 4096 或 8192 字符后才真正写到屏幕上。这种情况最常发生在输出重定向到文件时,也常见于MPI 并行可执行文件向屏幕输出——因为屏幕输出由 MPI 库统一接管以便合并显示所有进程的输出。解决办法是使用-nonblock或-nb命令行标志关闭屏幕与 log 文件的缓冲。对照当前仓库源码 lammps.cpp,该开关的实现为-nonbuf/-nb两种拼写都设置nonbufflag = 1(第 325–328 行),帮助信息中的说明是-nonbuf : disable screen/logfile buffering (-nb)。也就是说,当前版本源码实际接受的长形式是-nonbuf,短形式-nb在文档与源码中一致有效;如果你使用的构建版本只识别-nonblock,请以lmp -h的实际输出为准。

4.4 集合通信与点对点通信的冲突

MPI 并行计算中有一类典型死锁 bug:在未完成的点对点通信之前调用了集合通信函数(如MPI_Barrier()、MPI_Bcast()、MPI_Allreduce()),或者集合函数只被部分 MPI 进程调用。这类问题同样适用于 LAMMPS 内部函数:Error::all()内部使用了MPI_Barrier()(见 error.cpp 中all()的MPI_Barrier(universe->uworld)调用,第 60 行附近),因此如果某个错误条件并不在所有 MPI 进程上同时成立,必须改用Error::one(),而不是Error::all()。从 Error::one() 的实现看,它只报告单进程错误并抛出LAMMPSAbortException,不引入全局屏障,正是这类场景的正确选择——这一点也是阅读 LAMMPS 源码、开发自定义命令/风格时必须遵守的约定。

4.5 死循环:循环退出条件永不满足

某段代码的for/while循环不触发退出条件而无限循环。典型诱因包括增错了变量,或比较中的某个值因溢出变成了NaN——与NaN的任何大小比较都为假,while条件将永远"看起来成立"。这类 bug 与 4.4 一样,只能通过把调试器附加到运行中的进程来定位。

4.6 把 GDB 附加到正在运行的进程

对后两种情况(4.4、4.5),需要附加调试器到运行中的进程。步骤是:

  1. 找到进程 ID(PID)。在 Linux 上可用top、htop、ps或pstree命令查询;
  2. 用gdb -p <PID>把调试器附加到进程,该进程随即停止执行;
  3. 此后所有调试命令与 2.1 节相同:where获取堆栈、打印内部数据、单步或继续执行;
  4. 退出调试器后,计算会正常恢复运行。

关键收获是where:它能告诉你这个进程此刻正卡在哪一个 timestep 的哪一个函数里——是卡在MPI_Barrier(指向 4.4 的死锁),还是卡在自己写的某个while循环里(指向 4.5 的死循环)。

五、排查流程小结与源码级要点

把全文串起来,一个完整的崩溃/卡死排查路径是:

场景首选手段关键命令/证据
段错误,可本地复现GDB 内运行gdb --args ./lmp -in in.melt→run→where→print
段错误,事后分析core dumpcoredumpctl -o core dump lmp→gdb lmp core
想提前发现未崩溃的内存错误 / 并行环境valgrindvalgrind --log-file=crash-%p.txt ./lmp -in in.melt
进程"假死"先查thermo/缓冲,再附加调试器gdb -p <PID>→where

几个源码级的要点值得记住:

  • 崩溃点必须能反查到函数名:整个流程依赖带调试信息的构建(见 Build.rst);本例中PairLJCut::compute在 pair_lj_cut.cpp 第 71 行 开始,atom->x/atom->f等成员指针从atom对象直接取用(第 81–86 行),而未初始化成员(如rmass)保持 NULL 是段错误的直接来源;
  • 调用链可验证:main→Input::file()→execute_command()→Run::command()→Verlet::setup()→ pair 的compute(),与 GDB/valgrind 打出的堆栈逐帧对应,入口代码见 main.cpp;
  • 单进程错误用Error::one():Error::all()含MPI_Barrier(),在"错误只在部分 rank 出现"时会造成全体阻塞(error.cpp);
  • 输出"消失"先看缓冲:文件输出与 MPI 屏幕输出的块缓冲是"假死"假象的高频来源,用-nb关闭缓冲即可让进度实时可见。

掌握这套方法后,无论是排查自己的输入脚本问题,还是向 LAMMPS 开发者报告 bug(附上where堆栈与变量值),你都能提供可定位、可复现的完整信息,从而让修复——无论是改输入脚本还是改源码——快速落地。

  • 科研
  • 科学计算
  • 高性能计算

【免费下载链接】lammps

Public development project of the LAMMPS MD software package

项目地址:https://gitcode.com/gh_mirrors/la/lammps
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进