如何用 Valgrind 检测使用 LuaJIT FFI 过程中的内存泄漏
什么情况下可能会有内存泄漏
给带 GC 的语言写 C binding 一向是件让人迷糊的事。到底应该在 C 手工释放资源呢,还是依靠 GC 来回收?
还好 LuaJIT FFI 提供了很好用的 ffi.gc
方法。该方法允许给 cdata 对象注册在 gc 时调用的回调,它能让你在 Lua 领域里完成 C 手工释放资源的事。
C++ 提倡用一种叫 RAII 的方式管理你的资源。简单地说,就是创建对象时获取,销毁对象时释放。我们可以在 LuaJIT FFI 里借鉴同样的做法,在调用 resource = ffi.C.xx_create
等申请资源的函数之后,立即补上一行 ffi.gc(resource, ...)
来注册释放资源的函数。尽量避免尝试手动释放资源!即使不考虑 error
对执行路径的影响,在每个出口都补上一模一样的逻辑会够你受的(用 goto
也差不多,只是稍稍好一点)。
有些时候,ffi.C.xx_create
返回的不是具体的 cdata,而是整型的 handle。这会儿需要用 ffi.metatype
把 ffi.gc
包装一下:
local resource_type = ffi.metatype("struct {int handle;}", {
__gc = free_resource
})
local function free_resource(handle)
...
end
resource = ffi.new(resource_type)
resource.handle = ffi.C.xx_create()
回到小标题,如果你没能把申请资源和释放资源的步骤放一起,那么内存泄露多半会在前方等你。写代码的时候切记这一点。
在单元测试中检查内存泄漏
当然要想保障代码里不存在内存泄露,严格按照 RAII 规范编写代码并不够。毕竟圣人千虑,必有一失;何况你我凡胎?显而易见,我们需要一个侦测内存泄漏的工具。在这方面首选 Valgrind。
Valgrind 只能检查程序运行路径上的内存问题。所以要想最大化 Valgrind 检查的覆盖面,最好结合单元测试一起跑。这样单元测试覆盖到的地方,内存检查也能覆盖到。
鉴于 OpenResty 在这方面提供了一套工具集,而且我写这篇文章也是为了解决 OpenResty 应用开发中的一些问题,所以请允许我先以 OpenResty 应用为例,说说如何预防内存泄漏。
TEST_NGINX_USE_VALGRIND=1
OpenResty 官方的测试框架 test-nginx
内置了对 Valgrind 的支持。你所需的,不过是加个 TEST_NGINX_USE_VALGRIND=1
环境变量。测试框架看到该环境变量的存在后,会在启动 Nginx 的时候,前面加上 valgrind --leak-check
等选项。这样 Valgrind 就会去检查 Nginx 内部的内存分配。一旦 FFI 调用中存在内存泄漏,Valgrind 便会报告出来。效果与用 Valgrind 运行一个普通的二进制程序无异。
$opts = "--tool=memcheck --leak-check=full --show-possibly-lost=no";
if (-f 'valgrind.suppress') {
# 如果 valgrind.suppress 存在,用它来消除警告
$cmd = "valgrind --num-callers=100 -q $opts --gen-suppressions=all --suppressions=valgrind.suppress $cmd";
} else {
$cmd = "valgrind --num-callers=100 -q $opts --gen-suppressions=all $cmd";
}
由于 Valgrind 会显著拖慢托管程序的运行速度,你通常还需要另一个环境变量 TEST_NGINX_SLEEP
设置 test-nginx
测试框架的超时时间,以免遭遇各种奇怪的错误。最后完整可用的运行方式如下:
TEST_NGINX_USE_VALGRIND=1 TEST_NGINX_SLEEP=1 prove -r t
实际运行一下,你会发现输出来的“错误”非常多,甚至可能会出现尴尬的内容:
==10898== More than 1000 different errors detected. I'm not reporting any more.
==10898== Final error counts will be inaccurate. Go fix your program!
不用担心!大部分都是 faise positive(假阳性)。你只需弄一个 valgrind.suppress 来消除错误。由于我们只关注内存泄漏问题,这里简单粗暴地关闭其他错误输出:
{
<insert_a_suppression_name_here>
Memcheck:Cond
obj:*
}
...
还有一类 Nginx 或 LuaJIT 相关的内存泄漏报告,我们可以把它们也一并消除掉:
{
<insert_a_suppression_name_here>
Memcheck:Leak
fun:malloc
fun:ngx_alloc
}
...
现在再跑一次测试,如果还有报错,应该就是你的 FFI 代码问题了。背景噪音消除了,问题排查就清晰多了。
注意默认情况下 Valgrind 的检测结果不会影响退出码,所以为了跟 CI 配合,需要 grep 一下具体的报错:
TEST_NGINX_USE_VALGRIND=1 TEST_NGINX_SLEEP=1 prove -r t 2>&1 | grep -B 3 -A 20 "match-leak-kinds: definite"
# 忽略测试失败或 grep 不到东西的场景
test $? -eq 0 && exit 1
# 否则正常退出(一遍我们会跑两次测试,第一次不带 Valgrind。所以第二次测试失败(比如由于超时)不会影响最终的正确性)
exit 0
这样一旦 Valgrind 报告中出现了 "match-leak-kinds: definite" 字眼,测试就会失败。
非 test-nginx 下的内存泄漏检测
如果用的不是 test-nginx
那一套,又该怎么检测内存泄漏呢?
我们可以照搬 test-nginx 的原理,加塞 Valgrind 参数进去。比如,如果测试集只依赖 LuaJIT 本身,你可以这么运行:
opts="--tool=memcheck --leak-check=full --show-possibly-lost=no --error-exitcode=42"
valgrind --num-callers=100 -q $opts --gen-suppressions=all [--suppressions=valgrind.suppress] luajit ...
不像 test-nginx
,这里不再需要 grep 一下。通过指定 --error-exitcode
,一旦 Valgrind 发现了错误,会以指定的错误码退出。
如果测试集基于 resty 命令行工具驱动,可以用 resty 的 --valgrind
选项。
如果测试集基于 busted 测试框架,可以改造下调用方式。
首先,创建一个 test_valgrind.lua
文件,绕过 luajit -e
无法传参的缺陷。
require "busted.runner"({ standalone = false })
然后用 Valgrind 运行 luajit:
valgrind --error-exitcode=42 --tool=memcheck \
--gen-suppressions=all --suppressions=valgrind.suppress \
luajit test_valgrind.lua .
spacewander
make building blocks that people can understand and use easily, and people will work together to ...
make building blocks that people can understand and use easily, and people will work together to ...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。