Upstart启动程序存在内存泄漏问题,如何进行调试?

7
我在Upstart init进程(pid 1)中遇到了内存泄漏问题,有哪些调试选项可以采用?
编辑:请推荐一些实际的工具,手动打印printf或手动计算内存分配是不够的。此外,转储init核心并在其中探索并不是一个十分可行的选择。
更新1:valgrind无法工作。通过将内核命令行上的/sbin/init替换为适当的valgrind + init命令似乎也不可行,因为它尝试访问/proc以获取smaps的自身信息,但在运行init之前这些信息还不可用。
更新2:dmalloc也无法工作(在ARM上无法编译)。

当然是Valgrind啦。:-P - C. K. Young
如果这不是幽默,请用指示将其制作成适当的答案。好的。 - Tuminoid
请不要简短地提问。您有足够的空间来提出问题。 - camh
你还需要什么信息?由于其特殊性质,它泄漏了,我已经没有调试选项了。 - Tuminoid
关于Valgrind不能正常工作的问题,或许你可以编写一个简单的程序,挂载/proc和Valgrind所需要的其他设置,然后执行Valgrind。 - ninjalj
5个回答

8
一个简便的解决方案是记录每次调用mallocfree,然后检查日志以寻找模式。 ld提供了一个非常棒的功能可以在这里发挥作用。

--wrap=symbol

Use a wrapper function for symbol. Any undefined reference to symbol will be resolved to "__wrap_symbol". Any undefined reference to "__real_symbol" will be resolved to symbol.

This can be used to provide a wrapper for a system function. The wrapper function should be called "__wrap_symbol". If it wishes to call the system function, it should call "__real_symbol".

Here is a trivial example:

void *
__wrap_malloc (size_t c)
{
   printf ("malloc called with %zu\n", c);
   return __real_malloc (c);
}

If you link other code with this file using --wrap malloc, then all calls to "malloc" will call the function "__wrap_malloc" instead. The call to "__real_malloc" in "__wrap_malloc" will call the real "malloc" function.

You may wish to provide a "__real_malloc" function as well, so that links without the --wrap option will succeed. If you do this, you should not put the definition of "__real_malloc" in the same file as "__wrap_malloc"; if you do, the assembler may resolve the call before the linker has a chance to wrap it to "malloc".


更新

这里需要明确一下这个功能的用处。

  • 将自定义文件添加到Upstart的构建中。

操作步骤如下:

void*__wrap_malloc( size_t c )
{
   void *malloced = __real_malloc(c);
   /* log malloced with its associated backtrace*/
   /* something like: <malloced>: <bt-symbol-1>, <bt-symbol-2>, .. */
   return malloced
}

void __wrap_free( void* addr )
{
   /* log addr with its associated backtrace*/
   /* something like: <addr>: <bt-symbol-1>, <bt-symbol-2>, .. */
   __real_free(addr);
}

重新编译upstart并使用调试符号(-g),这样您就可以获得一些漂亮的回溯。如果您愿意,仍然可以优化(-O2/-O3)代码。
将Upstart与额外的LD_FLAGS --wrap=malloc, --wrap=free链接。现在,无论何时Upstart调用malloc,该符号都会神奇地解析为您的新符号__wrap_malloc。美妙的是,由于它发生在链接时,因此对编译代码来说完全透明。这就像shimminginstrumenting,没有任何混乱。
像往常一样运行重新编译的Upstart,直到确认出现泄漏。
查看日志以查找不匹配的mallocedaddr
一些注意事项: --wrap=symbol功能不适用于实际上是宏的函数名。所以要小心#define malloc nih_malloc。如果这是libnih所做的,您需要使用--wrap=nih_malloc__wrap_nih_malloc
使用gcc的内置回溯功能。
所有这些更改仅影响重新编译的Upstart可执行文件。
您可以将日志转储到sqlite DB中,这可能会使查找不匹配的malloc和free更容易。
您可以使日志格式成为SQL插入语句,然后在事后将其插入数据库以进行进一步分析。

2
请注意,如果您不想在此处使用printf,则可以分配一个缓冲区并将日志信息放入其中,然后在init完成后将其转储出来(或者只需在调试器中poke它)。 我期望的理想策略是确认结果在每次运行时都是一致的,找出日志中包含未匹配的malloc的行,然后设置__wrap_malloc以在该行上进行陷阱 - 在这一点上,您可以在调试器中查看调用堆栈并找到有问题的调用。 - Brooks Moses
Upstart使用libnih来处理所有的结构和内存,一些其他组件也是如此。将一些日志记录挂钩到nih的malloc/free并不能解决问题。封装所有libnih的API在Upstart中会带来很多麻烦,但我想我真的没有其他选择了。 - Tuminoid
--wrap=symbol 只能用于修改初始化符号。它不会(也不能)影响库的其他用户。此外,使用 --wrap=symbol 将 libnih 包装在 init 中尽可能地无痛。这就是 --wrap=symbol 的发明目的。有关更多详细信息,请参见我的更新。 - deft_code
我的观点是,如果我在init中包装nih,我需要包装100个函数,因为nihlib类似于glib,有很多分配内存的实用函数。这不像简单的包装malloc/free对。无论如何,我会给你最好尝试的赏金。 - Tuminoid
我怀疑所有的内存分配函数最终都会调用malloc/realloc。即使是libstdc++在实现“new”时也会调用malloc。如果og实用程序函数创建了内存池,你可能需要多做一些工作,大约需要3-4个额外的函数。 - deft_code
显示剩余2条评论

2

您可以通过挂钩malloc/free调用并计算每次分配和释放的字节数,来自行检测内存分配情况。


2
您也可以使用未更改的init,但创建一个包装器,将MALLOC_CHECK环境变量设置为1或更高。这样可以让您看到一些内存分配诊断信息。
一种变化是稍微更改init源代码,以在开始使用malloc之前自行设置该环境变量。
您还可以像AmineK建议的那样,在init源代码本身中添加调试代码。

0

你可以尝试将你的upstart版本与Google's TCMalloc链接起来。它自带一个堆检查器

堆检查器有两种启用方式:

  • 将环境变量HEAPCHECK设置为{ normal | strict | draconian }中的一种。
  • HEAPCHECK设置为local,并使用HeapProfileLeakChecker对象手动检查代码。

但我不知道如何为init设置环境变量。


TCMalloc 在 ARM 上也不起作用 :/ 虽然感谢建议。 - Tuminoid

0

你可以尝试在进程上运行pmap并检查哪些内存段正在增长。这可能会让你对是什么在消耗内存有一些想法。通过一些脚本编写,这个过程几乎可以自动化。

在以前的工作中,我实际上编写了一个脚本,它会在一组运行进程上每隔t秒拍摄n个pmap快照。该输出被馈送到一个perl脚本中,该脚本识别更改大小的段。我用它来定位商业代码中的几个内存泄漏。[我想分享这些脚本,但它们受到以前雇主的知识产权(版权)保护。]

  • 约翰

这种方法没有用,它只是增加了堆的大小。 - Tuminoid

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接