我正在尝试学习有关如何检测Perl中的内存泄漏的更多信息。 我有这个程序:
p.pl :
#! /usr/bin/env perl
use Devel::LeakTrace;
my $foo;
$foo = \$foo;
输出:
leaked SV(0xac2df8e0) from ./p.pl line 5
leaked SV(0xac2df288) from ./p.pl line 5
为什么这会泄漏两个标量(而不仅仅是一个)?
然后我通过valgrind
运行它。首先,我创建了perl
的调试版本:
$ perlbrew install perl-5.30.0 --as=5.30.0-D3L -DDEBUGGING \
-Doptimize=-g3 -Accflags="-DDEBUG_LEAKING_SCALARS"
$ perlbrew use 5.30.0-D3L
$ cpanm Devel::LeakTrace
然后我按照perlhacktips
中的建议运行valgrind
设置PERL_DESTRUCT_LEVEL=2
:
$ PERL_DESTRUCT_LEVEL=2 valgrind --leak-check=yes perl p.pl
==12479== Memcheck, a memory error detector
==12479== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==12479== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info
==12479== Command: perl p.pl
==12479==
leaked SV(0x4c27320) from p.pl line 5
leaked SV(0x4c26cc8) from p.pl line 5
==12479==
==12479== HEAP SUMMARY:
==12479== in use at exit: 105,396 bytes in 26 blocks
==12479== total heap usage: 14,005 allocs, 13,979 frees, 3,011,508 bytes allocated
==12479==
==12479== 16 bytes in 1 blocks are definitely lost in loss record 5 of 21
==12479== at 0x483874F: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==12479== by 0x484851A: note_changes (LeakTrace.xs:80)
==12479== by 0x48488E3: XS_Devel__LeakTrace_hook_runops (LeakTrace.xs:126)
==12479== by 0x32F0A2: Perl_pp_entersub (pp_hot.c:5237)
==12479== by 0x2C0C50: Perl_runops_debug (dump.c:2537)
==12479== by 0x1A2FD9: Perl_call_sv (perl.c:3043)
==12479== by 0x1ACEE3: Perl_call_list (perl.c:5084)
==12479== by 0x181233: S_process_special_blocks (op.c:10471)
==12479== by 0x180989: Perl_newATTRSUB_x (op.c:10397)
==12479== by 0x220D6C: Perl_yyparse (perly.y:295)
==12479== by 0x3EE46B: S_doeval_compile (pp_ctl.c:3502)
==12479== by 0x3F4F87: S_require_file (pp_ctl.c:4322)
==12479==
==12479== LEAK SUMMARY:
==12479== definitely lost: 16 bytes in 1 blocks
==12479== indirectly lost: 0 bytes in 0 blocks
==12479== possibly lost: 0 bytes in 0 blocks
==12479== still reachable: 105,380 bytes in 25 blocks
==12479== suppressed: 0 bytes in 0 blocks
==12479== Reachable blocks (those to which a pointer was found) are not shown.
==12479== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==12479==
==12479== For counts of detected and suppressed errors, rerun with: -v
==12479== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
因此丢失了16个字节。但是,如果我注释掉use Devel::LeakTrace
中的行p.pl
并再次运行valgrind
,则输出为:
==12880== Memcheck, a memory error detector
==12880== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==12880== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info
==12880== Command: perl p.pl
==12880==
==12880==
==12880== HEAP SUMMARY:
==12880== in use at exit: 0 bytes in 0 blocks
==12880== total heap usage: 1,770 allocs, 1,770 frees, 244,188 bytes allocated
==12880==
==12880== All heap blocks were freed -- no leaks are possible
==12880==
==12880== For counts of detected and suppressed errors, rerun with: -v
==12880== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
问题是:为什么Devel::LeakTrace
导致内存泄漏?
答案 0 :(得分:2)
似乎泄漏的内存甚至超过报告的valgrind
。
每次创建新的SV时,Devel::LeakTrace
都会在16 bytes structure called when
中记录当前文件名和行号:
typedef struct {
char *file;
int line;
} when;
这些块与malloc()
一起分配在line #80处,但似乎永远不会释放这些块。因此,创建的标量越多,泄漏的内存就越多。
模块尝试从END{}
phaser确定泄漏的SV。此时,所有已分配的SV应该已经超出了主程序的范围,并具有reference count decreased to zero,应该将其销毁。但是,如果由于某种原因引用计数未减少为零,则标量将不会被破坏和释放
从perl的内部内存管理池中。在这种情况下,标量被认为是模块泄漏的。
请注意,这与从操作中看到的内存泄漏不同 由例如malloc()。当perl退出时,它仍然会 将所有泄漏的标量(从其内部内存池中)释放回系统内存池。
这意味着该模块不是要检测泄漏的系统内存。为此,我们可以使用valgrind
。
该模块挂接到perl runops loop中,对于类型为OP_NEXTSTATE
的每个OP,它将扫描所有arenas和其中的所有SV,以查找新的SV(即:从上一个OP_NEXTSTATE
开始引入。
对于这个示例程序p.pl
,我计算了31个竞技场,每个竞技场都包含71个SV的空间。这些SV几乎全部在运行时使用(其中大约2150个)。模块将每个SV保留在哈希值used
中,其密钥等于SV的地址,其值等于分配了标量的when
块(请参见上文)。然后,对于每个OP_NEXTSTATE
,它可以扫描所有SV,并检查used
哈希中是否不存在某些SV。
used
哈希不是perl哈希(我想这是为了避免与
(模块尝试跟踪的已分配SV),而模块使用GLib
hash tables。
为了跟踪分配的when
块,我使用了一个名为when_hash
的新glib哈希。然后,在模块打印泄漏的标量之后,可以通过在when
中查找所有键来释放when_hash
块。
我还发现该模块没有释放used
哈希。据我所知,它应该调用glib g_hash_table_destroy()
从END{}
块中释放它。这是补丁:
LeakTrace.xs (已修补):
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#include <glib.h>
typedef struct {
char *file;
int line;
} when;
/* a few globals, never mind the mess for now */
GHashTable *used = NULL;
GHashTable *new_used = NULL;
/* cargo from Devel::Leak - wander the arena, see what SVs live */
typedef long used_proc _((void *,SV *,long));
/* PATCH: fix memory leaks */
/***************************/
GHashTable *when_hash = NULL; /* store the allocated when blocks here */
static int have_run_end_hook = 0; /* indicator to runops that we are done */
static runops_proc_t save_orig_run_ops; /* original runops function */
/* Called from END{}, i.e. from show_used() after having printed the leaks.
* Free memory allocated for the when blocks */
static
void
free_when_block(gpointer key, gpointer value, gpointer user_data) {
free(key);
}
static
void
do_cleanup() {
/* this line was missing from the original show_used() */
if (used) g_hash_table_destroy( used );
if (when_hash) g_hash_table_foreach( when_hash, free_when_block, NULL );
g_hash_table_destroy( when_hash );
PL_runops = save_orig_run_ops;
have_run_end_hook = 1;
}
/* END PATCH: fix memory leaks */
/*******************************/
static
long int
sv_apply_to_used(void *p, used_proc *proc, long n) {
SV *sva;
for (sva = PL_sv_arenaroot; sva; sva = (SV *) SvANY(sva)) {
SV *sv = sva + 1;
SV *svend = &sva[SvREFCNT(sva)];
while (sv < svend) {
if (SvTYPE(sv) != SVTYPEMASK) {
n = (*proc) (p, sv, n);
}
++sv;
}
}
return n;
}
/* end Devel::Leak cargo */
static
long
note_used(void *p, SV* sv, long n) {
when *old = NULL;
if (used && (old = g_hash_table_lookup( used, sv ))) {
g_hash_table_insert(new_used, sv, old);
return n;
}
g_hash_table_insert(new_used, sv, p);
return 1;
}
static
void
print_me(gpointer key, gpointer value, gpointer user_data) {
when *w = value;
char *type;
switch SvTYPE((SV*)key) {
case SVt_PVAV: type = "AV"; break;
case SVt_PVHV: type = "HV"; break;
case SVt_PVCV: type = "CV"; break;
case SVt_RV: type = "RV"; break;
case SVt_PVGV: type = "GV"; break;
default: type = "SV";
}
if (w->file) {
fprintf(stderr, "leaked %s(0x%x) from %s line %d\n",
type, key, w->file, w->line);
}
}
static
int
note_changes( char *file, int line ) {
static when *w = NULL;
int ret;
/* PATCH */
if (have_run_end_hook) return 0; /* do not enter after clean up is complete */
/* if (!w) w = malloc(sizeof(when)); */
if (!w) {
w = malloc(sizeof(when));
if (!when_hash) {
/* store pointer to allocated blocks here */
when_hash = g_hash_table_new( NULL, NULL );
}
g_hash_table_insert(when_hash, w, NULL); /* store address to w */
}
/* END PATCH */
w->line = line;
w->file = file;
new_used = g_hash_table_new( NULL, NULL );
if (sv_apply_to_used( w, note_used, 0 )) w = NULL;
if (used) g_hash_table_destroy( used );
used = new_used;
return ret;
}
/* Now this bit of cargo is a derived from Devel::Caller */
static
int
runops_leakcheck(pTHX) {
char *lastfile = 0;
int lastline = 0;
IV last_count = 0;
while ((PL_op = CALL_FPTR(PL_op->op_ppaddr)(aTHX))) {
PERL_ASYNC_CHECK();
if (PL_op->op_type == OP_NEXTSTATE) {
if (PL_sv_count != last_count) {
note_changes( lastfile, lastline );
last_count = PL_sv_count;
}
lastfile = CopFILE(cCOP);
lastline = CopLINE(cCOP);
}
}
note_changes( lastfile, lastline );
TAINT_NOT;
return 0;
}
MODULE = Devel::LeakTrace PACKAGE = Devel::LeakTrace
PROTOTYPES: ENABLE
void
hook_runops()
PPCODE:
{
note_changes(NULL, 0);
PL_runops = runops_leakcheck;
}
void
reset_counters()
PPCODE:
{
if (used) g_hash_table_destroy( used );
used = NULL;
note_changes(NULL, 0);
}
void
show_used()
CODE:
{
if (used) g_hash_table_foreach( used, print_me, NULL );
/* PATCH */
do_cleanup(); /* released allocated memory, restore original runops */
/* END PATCH */
}
$ wget https://www.cpan.org/modules/by-module/Devel/Devel-LeakTrace-0.06.tar.gz
$ tar zxvf Devel-LeakTrace-0.06.tar.gz
$ cd Devel-LeakTrace-0.06
$ perlbrew use 5.30.0-D3L
# replace lib/Devel/LeakTrace.xs with my patch
$ perl Makefile.PL
$ make
$ make install # <- installs the patch
# cd to test folder, then
$ PERL_DESTRUCT_LEVEL=2 valgrind --leak-check=yes perl p.pl
==25019== Memcheck, a memory error detector
==25019== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==25019== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info
==25019== Command: perl p.pl
==25019==
leaked SV(0x4c26cd8) from p.pl line 5
leaked SV(0x4c27330) from p.pl line 5
==25019==
==25019== HEAP SUMMARY:
==25019== in use at exit: 23,324 bytes in 18 blocks
==25019== total heap usage: 13,968 allocs, 13,950 frees, 2,847,004 bytes allocated
==25019==
==25019== LEAK SUMMARY:
==25019== definitely lost: 0 bytes in 0 blocks
==25019== indirectly lost: 0 bytes in 0 blocks
==25019== possibly lost: 0 bytes in 0 blocks
==25019== still reachable: 23,324 bytes in 18 blocks
==25019== suppressed: 0 bytes in 0 blocks
==25019== Reachable blocks (those to which a pointer was found) are not shown.
==25019== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==25019==
==25019== For counts of detected and suppressed errors, rerun with: -v
==25019== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
答案 1 :(得分:1)
首先,valgrind
在一个脚本中报告16个字节的泄漏内存,该脚本最多包含use Devel::LeakTrace
。可能的泄漏与第四和第五行无关。在您的链接中,
NOTE 3: There are known memory leaks when there are compile-time errors
within eval or require, seeing S_doeval in the call stack is a good sign
of these. Fixing these leaks is non-trivial, unfortunately, but they must be fixed
eventually.
由于我看到了行by 0x3F18E5: S_doeval_compile (pp_ctl.c:3502)
,并且在您的示例中也看到了类似的行,因此我想说,这就是Devel::LeakTrace
导致明显的内存泄漏的原因。
其次,对于原始脚本,Devel::LeakTrace
仅报告(至少)第五行的循环引用引起的泄漏。您可以使用weaken
中的Scalar::Util
来查看此内容:
#! /usr/bin/env perl
use Devel::LeakTrace;
use Scalar::Util;
my $foo;
$foo = \$foo;
Scalar::Util::weaken($foo);
然后,perl p.pl
将不会报告任何泄漏。我的猜测是,第一个脚本报告了两次泄漏,因为除了创建循环引用之外,perl还在$foo = \$foo
处丢失了一个指针。当您削弱$foo
显然可以解决两个问题时,会发生一些我无法理解的魔术。您可以通过调整原始脚本来查看此内容:
#! /usr/bin/env perl
use Devel::LeakTrace;
my $foo;
my $bar = \$foo;
$foo = $bar;
结果$foo
应该相同,我们刚刚创建了$bar
来保存引用。但是,在这种情况下,脚本仅报告一次泄漏。
因此,总而言之,我想说:1)Devel::LeakTrace
有一个错误,独立于代码,显示为valgrind
中的内存泄漏; 2)perl在原始脚本中创建一个循环引用并丢失了一个指针,这就是Devel::LeakTrace
报告两次泄漏的原因。