
文档简介:
解决多线程内存问题的实践总结
2013/2/4
杨志峰yangzhifeng83 @
关键字多线程,内存越界,valgrind,电子栅栏,mprotect,libsigsegv,glibc
最近,在多线程服务器程序(OceanBase MergeServer)中,一个线程非法篡改了另一个线程的内存,并导致程序核心掉线. 找到整个问题花了整整一周的时间. 在此期间,我尝试了各种内存调试方法. 通常感觉天空的意志会变得清晰,但是他们发现自己已经进入了另一个死胡同. 最后,使用功能强大的工具(例如mprotect + backtrace + libsigsegv)成功找到了问题所在. 在整个定位过程中遇到的问题和解决方案通常是多线程内存超出范围的问题. 我将简要总结并与您分享. 对于只对最终组合秘诀感兴趣的学生,请直接阅读最后一节. 其他各章也写在这里供大众科学使用.
现象

核心是在系统集成测试期间发现的. 服务器程序MergeServer具有一个由50个工作线程组成的线程池. 当使用8个线程的测试程序通过MergeServer读取数据时,后者有时会成为核心. 使用gdb检查核心文件,发现该核心的原因是指针的地址非法. 当进程访问指针指向的地址时,将导致段错误. 见下文.
在ame_的对象中,该对象是动态数组field_columns_的第十个元素的成员. 如下所示.
重现问题
此后,花了2天的时间终于找到了重现该问题的方法. 重复多次,可以观察到以下现象:
随着并发客户端数量的增加(从8个线程增加到16个线程),核心的可能性也随之增加;
将服务器端线程池中的线程数减少(从50个减少到2个),并且无法复制内核.
被篡改的指针总是将一半(高4个字节)更改为0,而另一半似乎是正确的.

请参阅上一节. 它重复了很多次,并且每次输出内核时,都是因为field_columns_动态数组的第十个元素data_ [9]的cname_成员的ptr_成员已被篡改. 这是一个很难解释的奇怪现象.
在代码中插入一个检查点,从最初生成的field_columns_内容到导致序列超出界限的代码序列中的“埋入点”,即ame_的代码位置. 事实证明,该程序有时核心位于检查点之前,有时核心位于检查点之后.
基于上述现象,初步判断这是一个多线程程序中的内存越界问题.
使用glibc的MALLOC_CHECK _
由于这是内存问题,请考虑使用一些内存调试工具来查找问题. 由于OB具有自己的存储块缓存,因此需要消除其影响. 修改OB内存分配器,使其每次都直接调用malloc并释放c库,而无需进行缓存. 然后,您可以使用glibc的内置内存块完整性检查功能.
使用此功能,无需重新编译程序,只需在运行时设置环境变量MALLOC_CHECK_(请注意下划线). 在程序运行过程中,只要给glibc释放了可用内存,glibc都会检查其隐藏元数据的完整性,并在发现错误时立即终止.
使用类似于以下内容的命令行启动服务器程序:
导出MALLOC_CHECK_ = 2
bin / mergeserver -z 45447 -r 10.232.36.183:45401 -p45441
使用MALLOC_CHECK_之后,程序核心转到了另一个位置. 调用free时,glibc会检查内存块前面的check header错误并中止它. 如下所示.
但是这个核心带给我们的信息很少. 我们只是找到了另一种稍微更有效地重现该问题的方法. 也许我最初看到的核心现象是延迟表现,但记忆在“较早”的时刻就被破坏了.
valgrind
glibc提供的MALLOC_CHECK_函数太简单了. 是否有更高级的工具不仅可以报告错误,还可以分析问题的原因?我们自然想到了著名的瓦尔格朗德. 使用valgrind来检查内存问题,不需要重新编译程序,只需使用valgrind来启动:
nohup valgrind --error-limit = no --suppressions =禁止bin / mergeserver -z 45447 -r 10.232.36.183:45401 -p45441> nohup.out&

默认情况下,当valgrind发现1000个不同的错误或错误总数超过1000万时,它将停止报告错误. 添加--error-limit = no后,可以禁用此功能. --suppressions用于屏蔽一些无关紧要的误报.
经过反复折腾,valgrind无法重现核心问题. valgrind报告的错误也是与问题无关的误报. 可能是因为运行该程序的valgrind将使该程序的性能降低10倍以上,这将影响多线程程序的运行时间,从而导致无法重现内核. 这条路无处.
幻数
由于MALLOC_CHECK_可以检测程序的内存问题,因此我们实际上想知道是谁(哪段代码)越过了线. 这时,我们想到了使用幻数填充来指示数据结构. 如果我们在越界内存中看到某个幻数,就知道是哪段代码.
首先,修改malloc的封装函数,用特殊值(此处为0xEF)填充返回给用户的内存块,并在开头和结尾处申请额,就可以确定哪个线程超出范围. 代码示例如下.
然后,当用户程序通过我们的自由条目释放内存时,请检查我们填充到边界的幻数. 同时,调用mprobe强制glibc对内存块执行完整性检查.
最后,将幻数添加到程序中所有可疑的关键数据结构中,以便在调试器中检查内存时可以识别出它们. 例如
好的,一切都添加了. 使用MALLOC_CHECK_重新运行. 程序核心再次下降,检查内存在越界位置:
如上图所示,红色部分是我们自己装满的越界检查头,我们可以看到它没有损坏. 确定存储在第二行中的线程号确实等于我们当前线程的线程号. 蓝色部分是前一个动态内存分配的结尾,该分配也已完成(24字节0xdc). 两行0x44afb60和0x44afb68中显示的内存是glibc malloc存储其自己的元数据的地方. 程序核心掉线的原因是,在检查这两行的完整性时发现了错误. 由此推断,非法篡改的内容小于16个字节. 仔细观察这16个字节的内容,我们看不到熟悉的幻数,因此无法通过错误推断代码. 我们最初发现的核心现象相互证实了这一点. 非法修改的内容可能只有4个字节(int32_t大小).
本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/shoujiruanjian/article-294464-1.html
说得好
有人需要便宜的
基本不可能从卵变成蛆
议论政府的权利
现在呢