- 科研
- 科学计算
- 高性能计算
【免费下载链接】lammps
Public development project of the LAMMPS MD software package
本篇技术指南基于 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),需要附加调试器到运行中的进程。步骤是:
- 找到进程 ID(PID)。在 Linux 上可用
top、htop、ps或pstree命令查询; - 用
gdb -p <PID>把调试器附加到进程,该进程随即停止执行; - 此后所有调试命令与 2.1 节相同:
where获取堆栈、打印内部数据、单步或继续执行; - 退出调试器后,计算会正常恢复运行。
关键收获是where:它能告诉你这个进程此刻正卡在哪一个 timestep 的哪一个函数里——是卡在MPI_Barrier(指向 4.4 的死锁),还是卡在自己写的某个while循环里(指向 4.5 的死循环)。
五、排查流程小结与源码级要点
把全文串起来,一个完整的崩溃/卡死排查路径是:
| 场景 | 首选手段 | 关键命令/证据 |
|---|---|---|
| 段错误,可本地复现 | GDB 内运行 | gdb --args ./lmp -in in.melt→run→where→print |
| 段错误,事后分析 | core dump | coredumpctl -o core dump lmp→gdb lmp core |
| 想提前发现未崩溃的内存错误 / 并行环境 | valgrind | valgrind --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
相关推荐
TDengine Linux 分析调试工具实战指南:gdb、valgrind、bpftrace、perf 与 core dump 定位崩溃
TDengine Linux 分析调试工具实战指南:gdb、valgrind、bpftrace、perf 与 core dump 定位崩溃 TDengine 服
数据库时序数据库物联网大数据实时分析云原生Swoole 崩溃问题排查与 Bug 报告指南:从 Valgrind、ASAN 到 GDB CoreDump 的完整工具箱
Swoole 崩溃问题排查与 Bug 报告指南:从 Valgrind、ASAN 到 GDB CoreDump 的完整工具箱 Swoole 是一款基于协程的 PH
后端网络异步编程并发编程TDengine Linux 调试与故障定位指南:gdb、valgrind、bpftrace、perf 与 Core Dump 实战
TDengine Linux 调试与故障定位指南:gdb、valgrind、bpftrace、perf 与 Core Dump 实战 TDengine 的数据库
数据库时序数据库物联网大数据实时分析云原生
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考