ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

VCS仿真Hang死排查实战:loopdetect与pstack组合定位死锁与死循环

VCS仿真Hang死排查实战:loopdetect与pstack组合定位死锁与死循环 1. 仿真跑着跑着不动了先搞清楚卡住到底卡在哪做数字前端验证的人几乎都遇到过这种场景晚上挂上回归早上到工位一看仿真进程还在但log已经几个小时没动静了CPU占用率要么是0要么是100%单核死转。这就是典型的仿真Hang死。它和仿真报错崩溃完全是两码事——报错至少告诉你哪一行出了问题Hang死是活着但不动了像极了电脑蓝屏前的假死状态最让人抓狂。我做了十多年验证踩过的Hang死坑没有一百也有八十。早期遇到这种情况我的做法非常原始看log最后打印到哪然后去代码里翻靠肉眼找死循环。这个方法在代码量小的时候还能凑合一旦设计规模上到几百万门、验证环境里几十个component互相通信肉眼排查基本等于大海捞针。更麻烦的是有些Hang死不是死循环而是进程间通信死锁、事件等待永远等不到、fork/join结构里的某个分支卡住这些情况log上往往看不出任何异常。后来我逐渐总结出一套组合拳先用VCS自带的loopdetect机制做第一轮筛查再用pstack抓取进程调用栈做精确定位。这套方法的好处是不需要你提前埋任何探针不需要改代码重新编译直接在已经卡住的仿真进程上操作就能拿到关键线索。这篇文章就把这套流程完整拆开讲清楚包括loopdetect的编译选项怎么加、pstack怎么用、抓到的栈信息怎么读、以及几种典型Hang死场景的排查思路。适合读这篇内容的人正在用VCS做仿真验证的工程师、被Hang死问题折磨过的验证同行、以及想提前储备排查手段的初学者。哪怕你之前没用过pstack跟着走一遍也能上手。2. loopdetect不是万能药但它是性价比最高的第一道防线2.1 loopdetect到底在检测什么VCS的loopdetect机制本质上是在仿真运行时对进程的执行状态做周期性采样判断某个进程是否长时间停留在同一位置没有推进。你可以把它理解成给仿真进程装了一个心跳监测器——正常情况下每个进程的心跳应该随着仿真时间推进而不断变化如果某个进程的心跳长时间不变loopdetect就会怀疑它陷入了循环或死等。这里有个关键点需要澄清loopdetect检测的不是严格意义上的无限循环而是执行停滞。一个进程可能在一个while(1)里死转也可能卡在wait(some_event)上永远等不到事件还可能卡在某个mailbox的get()上因为对方永远不put()。这三种情况在loopdetect眼里都表现为不推进所以它都能覆盖到。我见过不少同行对loopdetect有误解以为加了编译选项就万事大吉。实际上它只是一个预警机制告诉你哪个进程可能有问题但不会直接告诉你为什么有问题。真正的根因定位还得靠后面的pstack。2.2 编译选项怎么加加在哪一层loopdetect的启用方式是在VCS编译阶段加入对应的编译选项。具体来说需要在编译命令里加上vcsloopdetect这个选项。注意这是编译期选项不是运行期选项也就是说你必须重新编译一次仿真程序才能生效。这一点很关键很多人卡在这里——仿真已经Hang住了才想起来要加loopdetect结果发现必须重新编译而重新编译又得等半天。所以我的建议是在项目搭建初期就把loopdetect编译选项加进Makefile或编译脚本里。它带来的编译开销和运行时开销都很小但关键时刻能救命。不要等到出了问题才临时加那时候你连复现都未必能复现。具体操作上如果你用的是Makefile大概是这样VCS_OPTS vcsloopdetect VCS_OPTS vcsloopreport其中vcsloopreport是让VCS在检测到疑似循环时生成报告文件。这两个选项配合使用效果最好。如果你用的是VCS的图形界面或者脚本封装找到编译选项那一栏把这两个加进去即可。加完之后重新编译仿真程序就带上了loopdetect能力。2.3 检测到之后报告长什么样当loopdetect检测到疑似停滞时VCS会在仿真目录下生成一个报告文件通常命名为类似loopdetect.report或者带进程ID的日志文件。报告里会包含以下信息疑似停滞的进程名称比如某个initial块、某个always块、或者某个class的method停滞的位置通常是文件名加行号停滞的持续时间仿真时间推进了多少但该进程没有动调用关系该进程被谁调用、又调用了谁我实测下来这份报告最有价值的是进程名称和停滞位置这两项。它直接把排查范围从整个验证环境缩小到某几个进程效率提升非常明显。但要注意loopdetect会有误报。有些进程本身设计上就是长时间等待的比如一个只在特定条件下才触发的monitor它可能大部分时间都挂在wait上。这种情况下loopdetect也会报但它不是bug。所以拿到报告后第一步是判断这个停滞是否合理而不是直接当成bug去改。2.4 一个真实的误报案例我之前做过一个项目loopdetect报告说某个scoreboard进程停滞了。我一开始很紧张以为scoreboard逻辑写错了。结果仔细一看那个scoreboard在等待DUT输出比对数据而DUT在那个时间段确实没有输出所以它就是在正常等待。这种情况下loopdetect的报警是正确但无用的。后来我的做法是在报告里先看停滞位置如果停滞位置在wait、、fork/join这类等待语句上先判断等待条件是否应该被满足。如果条件本身就不该满足那就是正常等待如果条件应该满足但没满足那才是真问题。这个判断过程需要你对验证环境的结构比较熟悉。如果是别人写的环境可能需要花点时间理清进程间的依赖关系。3. pstack抓栈让卡住的进程自己开口说话3.1 pstack是什么为什么它能定位Hang死pstack是Linux系统下的一个命令行工具作用是打印指定进程的调用栈。所谓调用栈就是当前进程执行到哪一行、这一行是被谁调用的、上一层又是被谁调用的一直追溯到进程入口。对于C/C程序来说pstack能把整个函数调用链完整展示出来。VCS仿真器本身是用C/C写的你的SystemVerilog代码在仿真时会被编译成C/C层面的执行逻辑。所以当仿真Hang住时用pstack去抓VCS进程的栈就能看到仿真器当前卡在哪个函数里。如果卡在用户代码对应的函数里栈信息里通常能看到你的模块名、类名、方法名如果卡在仿真器内部函数里也能看出是卡在调度、等待还是循环检测上。这比看log强太多了。log只能告诉你最后打印了什么而pstack告诉你现在正在执行什么。一个是过去时一个是现在进行时。3.2 操作步骤从找到进程到抓到栈整个操作流程分三步第一步找到VCS仿真进程的PID。ps -ef | grep simv或者如果你的仿真程序叫别的名字ps -ef | grep vcs输出里会显示进程ID记下这个PID。如果有多个仿真进程在跑根据启动时间、命令行参数或者工作目录来区分。第二步用pstack抓栈。pstack PID比如PID是12345pstack 12345如果提示权限不够加sudosudo pstack 12345第三步把输出保存下来分析。pstack 12345 stack_$(date %s).txt建议间隔几秒抓多次比如抓三次每次间隔5秒。因为单次抓栈可能刚好抓到某个瞬时状态多次抓取能看出进程是否真的卡在同一个位置。如果三次抓到的栈顶完全一样那基本可以确定就是卡在那里了。3.3 栈信息怎么读从栈顶往下看pstack的输出格式大致是这样的#0 0x00007f8a1c2b3d4e in wait_for_event () #1 0x00007f8a1c2b5f6a in process_execute () #2 0x00007f8a1c2b7a8c in scheduler_run () #3 0x00007f8a1c2b9c0e in simv_main () ...栈顶#0是当前正在执行的函数往下依次是调用者。读栈的关键是看栈顶几层里有没有你认识的函数名。如果栈顶是wait_for_event、wait_for_signal这类说明进程在等待某个事件或信号。这时候你要问这个事件应该被触发吗谁负责触发它触发条件是什么如果栈顶是某个循环相关的函数比如loop_execute、while_loop那可能是死循环。如果栈顶是mailbox_get、semaphore_wait、fork_join这类说明卡在进程间同步上。这类问题最常见也最难查因为涉及多个进程的交互。我一般会把栈信息复制到文本编辑器里从#0开始往下逐层看直到看到第一个能对应到我的验证环境里的模块名或类名。那个位置就是案发现场。3.4 抓栈的时机很关键有个细节很多人忽略pstack要在仿真Hang住但进程还活着的时候抓。如果进程已经退出了pstack就抓不到了。如果进程还在跑但只是慢抓到的栈可能一直在变那说明不是Hang死只是性能问题。所以我的习惯是发现仿真长时间没输出后先ps确认进程还在然后立刻抓栈。如果抓到的栈顶在几次抓取中保持不变就基本锁定问题了。另外如果仿真环境里用了多线程或者多进程pstack默认只抓主线程。有些Hang死发生在子线程里这时候需要用pstack的扩展用法或者gdb的thread apply all bt来抓所有线程的栈。不过VCS仿真大部分情况下是单线程调度主线程栈通常就够了。4. 三类典型Hang死场景的排查链路4.1 场景一死循环——loopdetect直接命中这是最容易排查的一类。loopdetect报告会直接指出某个进程停滞pstack抓到的栈顶通常是循环相关的函数。排查链路loopdetect报告指出进程A停滞在file.sv:123pstack确认栈顶在循环执行函数打开file.sv第123行检查循环条件常见原因循环变量没有递增、循环退出条件永远为假、循环体内有continue导致变量更新被跳过我遇到过一个经典案例一个for循环里循环变量在if分支里才递增但某个条件下if分支永远不进入导致循环变量永远不变。这种bug肉眼很难发现但loopdetect一报一个准。修复方式把循环变量递增移到循环体开头或者改用while加明确的退出条件。4.2 场景二事件等待死锁——pstack是主力这类问题loopdetect可能报也可能不报取决于等待时间是否超过阈值。但pstack一定能抓到线索。排查链路pstack抓到栈顶在wait_for_event或类似函数从栈往下找到对应的进程名分析该进程在等什么事件找到负责触发该事件的进程检查它为什么没触发常见原因触发进程本身也在等待、触发条件写错、事件被提前消费我印象最深的一次是一个验证环境里两个component互相等对方发数据。A等B的putB等A的get结果两个都卡住。pstack抓出来两个进程的栈一看就明白了。这种问题如果只看log可能log上两边都只打印了waiting...根本看不出是互相等。修复方式调整握手顺序或者引入超时机制。我通常会在关键的wait上加timeout超时后打印错误信息并退出避免整个仿真卡死。4.3 场景三fork/join结构卡住——最隐蔽的一类fork/join是SystemVerilog里常用的并行结构。join会等所有分支完成join_any等任一分支完成join_none不等。如果用了join但某个分支永远不结束整个fork块就卡住了。排查链路pstack栈顶可能在fork_join相关函数需要结合代码分析fork块里有哪些分支逐个检查每个分支的退出条件常见原因某个分支里的循环没有退出、某个分支在等一个永远不会来的事件这类问题loopdetect不一定报因为fork块本身可能在正常等待只是某个分支卡住了。pstack能告诉你卡在fork_join上但具体是哪个分支还得回去看代码。我的经验在fork/join块里给每个分支加$display打印标明分支开始和结束。这样一旦卡住log上能看出哪个分支没打印结束信息。虽然土但有效。5. 把loopdetect和pstack串起来用一套完整的排查流程5.1 标准操作流程把前面讲的内容串起来一套完整的Hang死排查流程是这样的第一步确认Hang死。仿真长时间无输出ps确认进程还在CPU占用率异常0%或100%单核。第二步抓pstack。间隔5秒抓三次保存下来。如果三次栈顶一致确认卡住。第三步看loopdetect报告。如果编译时加了loopdetect去仿真目录找报告文件看有没有指向具体进程。第四步交叉比对。把pstack栈顶的函数名和loopdetect报告的进程名对照锁定问题进程。第五步回代码分析。根据进程名和停滞位置回到SystemVerilog代码里找对应的逻辑分析等待条件或循环条件。第六步修复并验证。改完代码重新编译仿真确认问题消失。这套流程我用了很多年大部分Hang死问题都能在半小时内定位到根因。比起盲目翻代码效率高太多了。5.2 几个容易踩的坑坑一编译时没加loopdetect出问题了才想加。这时候要么重新编译耗时要么只能靠pstack硬扛。所以再次强调项目初期就加上。坑二pstack抓到的栈全是仿真器内部函数看不到用户代码。这是因为编译时没有加调试信息。解决办法是在VCS编译选项里加-debug_accessall或者至少-debug_accesspp让仿真器保留符号信息。这样pstack才能显示出你的模块名和类名。坑三仿真进程有多个抓错了。有些环境会启动多个simv进程比如co-simulation场景。这时候要先用ps看清楚每个进程的命令行参数找到真正在跑仿真的那个。坑四pstack输出太长看花眼。栈可能有几十层不用全看。从#0往下看找到第一个你认识的函数名就停那通常就是关键位置。5.3 一个提高效率的小技巧我习惯在验证环境的顶层加一个看门狗进程用fork启动一个定时器如果仿真超过预设时间还没有正常结束就自动打印当前所有关键进程的状态并退出。这样即使Hang死了也能拿到一些线索不用干等。看门狗的大致写法initial begin fork begin #1_000_000; // 超时时间 $display([WATCHDOG] Simulation timeout, dumping state...); // 这里可以调用一些dump函数 $finish; end begin // 正常仿真流程 run_test(); end join_any end这个看门狗配合loopdetect和pstack基本能覆盖绝大多数Hang死场景。6. 从根因上减少Hang死几个编码习惯排查手段再好也不如一开始就不写出会Hang死的代码。分享几个我多年总结的编码习惯第一所有wait都加超时。SystemVerilog的wait语句可以配合fork/join_any加超时或者用wait(event) or #timeout的形式。这样即使事件永远不来也能在超时后报错退出而不是无限等待。第二fork/join块里避免无限循环。如果某个分支需要长时间运行用join_none而不是join或者给分支加明确的退出条件。第三mailbox和semaphore操作要成对。get和put、wait和post必须配对出现。我见过太多因为漏了一个put导致对方永远get不到的情况。第四关键路径加打印。在进程开始、结束、等待、唤醒这些关键节点加$displaylog上能看出执行轨迹。虽然会增加log量但排查时非常有用。第五定期跑回归时开启loopdetect。不要等到出了问题才开平时就开着这样问题一出现就能第一时间发现。这些习惯看起来简单但坚持下来能省掉大量排查时间。我现在带新人第一件事就是让他们把这些习惯刻进肌肉记忆里。7. 关于工具版本和环境的几点补充不同版本的VCS在loopdetect和pstack的支持上可能有细微差异。我用的比较多的几个版本里loopdetect的编译选项基本一致但报告文件的命名和格式略有不同。建议你第一次用的时候先在一个小demo上跑一遍确认报告文件生成在哪里、长什么样。pstack本身是Linux系统工具和VCS版本无关。但pstack能否显示出有意义的函数名取决于VCS编译时是否保留了调试符号。所以-debug_access系列选项一定要加。另外如果仿真环境跑在容器里pstack可能需要在宿主机上执行或者容器里要安装对应的工具。这个根据实际环境调整。还有一点如果仿真进程的栈信息里出现了大量??或者地址而不是函数名说明符号信息缺失。这时候要么重新编译加调试选项要么用addr2line手动把地址转成函数名。不过后者比较麻烦不如重新编译来得直接。最后说一个我自己的体会Hang死问题最怕的不是难查而是没有线索。loopdetect和pstack的组合本质上就是给没有线索的场景强行制造线索。只要你能拿到栈信息哪怕看不懂把栈信息贴到团队群里有经验的人往往一眼就能看出问题。所以遇到Hang死第一反应不应该是慌而是先抓栈再分析。这个习惯养成之后你会发现Hang死问题其实没那么可怕。
返回列表