searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

编译器的一次沉默,开发者的半天忙活

2025-03-25 05:33:48
3
0

一、问题现象

最初的问题表现是 spdk 的 nvme bdev 断链失败,一直刷下面这条日志,刷日志的速度特别快,而且长时间无法恢复。

二、初步分析

根据经验看,这种刷日志的速度应该不是 socket 事件触发的,可能是某个 poller 每轮 polling 都打印了这条日志。

对应的代码如下:

spdk_nvme_ctrlr_free_io_qpair 函数是用来释放 qpair 的,理论上调用过这个函数之后,就不会再打印日志文件中的那条日志,因此初步定位发生此问题的原因应该是这个函数没有被调用。

三、单步跟踪

第一想法就是 gdb attach 到进程上,在 spdk_nvme_ctrlr_free_io_qpair 函数的起始点上打个断点,验证下猜想。结果和推测一样,这个函数没有调用。

那就更进一步,在这个函数的 caller (bdev_nvme_disconnected_qpair_cb)中打断点,然后单步跟踪为什么没有调用 spdk_nvme_ctrlr_free_io_qpair。

单步跟踪的结果很意外,bdev_nvme_disconnected_qpair_cb 函数传入的 qpair 参数在 group->qpair_list 链表中是存在的,但就是没有调用到 spdk_nvme_ctrlr_free_io_qpair 这个函数。

此时,必须得查看一下汇编指令,看看程序为什么会有如此不合理的表现。汇编代码如下

看了这段汇编代码后,更是觉得不可思议。从汇编来看,bdev_nvme_disconnected_qpair_cb 这个函数就是打印一条 log,之后什么都不做就退出了。

第一反应就是我查看的代码版本和运行的进程不匹配,但是经过多次比对确认是没问题。

四、确定排查方向

排查到这里,对于问题的根因我主要有两个怀疑方向,一是怀疑编译器有 bug,导致编译产生的指令和代码对不上;二是编译器在特定场景下做了某种优化。考虑到前者发生的概率较小且排查难度较后者更大,因为后续的排查方向主要以后者为主。

如果是编译器优化掉了 bdev_nvme_disconnected_qpair_cb 函数的大段代码,最可能的出现问题的地方就是 nvme_poll_group_get_qpair 这个函数了,因为这个函数如果最终返回的 nvme_qpair 指针是 NULL,那么 bdev_nvme_disconnected_qpair_cb 函数的确只剩下一条打印日志的指令了。

既然推测和事实能够吻合,那么就按这个思路进行验证。

五、验证猜想

首先在 FOREACH 循环里面加 log,可以看出对应的调用 spdk_log 函数的指令是编译出来了的

再在其内层的 for 循环里加 log 也可以把调用 spdk_log 的指令编译出来

到此,编译器都是按预期工作。

六、找到问题所在

那么优化掉 bdev_nvme_disconnected_qpair_cb 函数大段指令的原因可能就是编译器认为 nvme_poll_group_get_qpair 函数的返回值总是 NULL,所以 if (nvme_qpair != NULL) 这个条件总是不成立,编译器自然有道理优化掉这个 if 内的所有语句了。这个推理也是有理有据的,接下来我们需要对照代码进行梳理和验证。

果然,沿着这个思路,最终找到了问题所在。

nvme_poll_group_get_qpair 函数中的 break 指令只能跳出一层循环,然而 FOREACH 也是一个封装为循环的宏,在 break 指令跳出内层循环后会继续在 FOREACH 循环中执行,函数并不会结束,直到遍历完整个链表,函数才会返回,这时函数的返回值一定是链表尾节点的 next 指针的值,也就是 NULL,所以这个函数的确在任何时候都只会返回 NULL,编译器并没有出错。

那么按照如下方式进行修复即可。

修复后,查看汇编代码也符合预期了。

七、回顾和思考

这个问题本身是一个很小的问题,可能只是开发这段代码的同学的一处笔误,但是编译器在发现这个问题后并没有产生 warning,而是自作主张完成了优化,虽然本本份份完成了自己的指令编译工作,也没有出现错误,但是还是缺少了一些对程序员友好的提醒。也是印证了标题:“编译器的一次沉默,开发者的半天忙活”。

 

0条评论
0 / 1000
l****n
5文章数
0粉丝数
l****n
5 文章 | 0 粉丝
原创

编译器的一次沉默,开发者的半天忙活

2025-03-25 05:33:48
3
0

一、问题现象

最初的问题表现是 spdk 的 nvme bdev 断链失败,一直刷下面这条日志,刷日志的速度特别快,而且长时间无法恢复。

二、初步分析

根据经验看,这种刷日志的速度应该不是 socket 事件触发的,可能是某个 poller 每轮 polling 都打印了这条日志。

对应的代码如下:

spdk_nvme_ctrlr_free_io_qpair 函数是用来释放 qpair 的,理论上调用过这个函数之后,就不会再打印日志文件中的那条日志,因此初步定位发生此问题的原因应该是这个函数没有被调用。

三、单步跟踪

第一想法就是 gdb attach 到进程上,在 spdk_nvme_ctrlr_free_io_qpair 函数的起始点上打个断点,验证下猜想。结果和推测一样,这个函数没有调用。

那就更进一步,在这个函数的 caller (bdev_nvme_disconnected_qpair_cb)中打断点,然后单步跟踪为什么没有调用 spdk_nvme_ctrlr_free_io_qpair。

单步跟踪的结果很意外,bdev_nvme_disconnected_qpair_cb 函数传入的 qpair 参数在 group->qpair_list 链表中是存在的,但就是没有调用到 spdk_nvme_ctrlr_free_io_qpair 这个函数。

此时,必须得查看一下汇编指令,看看程序为什么会有如此不合理的表现。汇编代码如下

看了这段汇编代码后,更是觉得不可思议。从汇编来看,bdev_nvme_disconnected_qpair_cb 这个函数就是打印一条 log,之后什么都不做就退出了。

第一反应就是我查看的代码版本和运行的进程不匹配,但是经过多次比对确认是没问题。

四、确定排查方向

排查到这里,对于问题的根因我主要有两个怀疑方向,一是怀疑编译器有 bug,导致编译产生的指令和代码对不上;二是编译器在特定场景下做了某种优化。考虑到前者发生的概率较小且排查难度较后者更大,因为后续的排查方向主要以后者为主。

如果是编译器优化掉了 bdev_nvme_disconnected_qpair_cb 函数的大段代码,最可能的出现问题的地方就是 nvme_poll_group_get_qpair 这个函数了,因为这个函数如果最终返回的 nvme_qpair 指针是 NULL,那么 bdev_nvme_disconnected_qpair_cb 函数的确只剩下一条打印日志的指令了。

既然推测和事实能够吻合,那么就按这个思路进行验证。

五、验证猜想

首先在 FOREACH 循环里面加 log,可以看出对应的调用 spdk_log 函数的指令是编译出来了的

再在其内层的 for 循环里加 log 也可以把调用 spdk_log 的指令编译出来

到此,编译器都是按预期工作。

六、找到问题所在

那么优化掉 bdev_nvme_disconnected_qpair_cb 函数大段指令的原因可能就是编译器认为 nvme_poll_group_get_qpair 函数的返回值总是 NULL,所以 if (nvme_qpair != NULL) 这个条件总是不成立,编译器自然有道理优化掉这个 if 内的所有语句了。这个推理也是有理有据的,接下来我们需要对照代码进行梳理和验证。

果然,沿着这个思路,最终找到了问题所在。

nvme_poll_group_get_qpair 函数中的 break 指令只能跳出一层循环,然而 FOREACH 也是一个封装为循环的宏,在 break 指令跳出内层循环后会继续在 FOREACH 循环中执行,函数并不会结束,直到遍历完整个链表,函数才会返回,这时函数的返回值一定是链表尾节点的 next 指针的值,也就是 NULL,所以这个函数的确在任何时候都只会返回 NULL,编译器并没有出错。

那么按照如下方式进行修复即可。

修复后,查看汇编代码也符合预期了。

七、回顾和思考

这个问题本身是一个很小的问题,可能只是开发这段代码的同学的一处笔误,但是编译器在发现这个问题后并没有产生 warning,而是自作主张完成了优化,虽然本本份份完成了自己的指令编译工作,也没有出现错误,但是还是缺少了一些对程序员友好的提醒。也是印证了标题:“编译器的一次沉默,开发者的半天忙活”。

 

文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0