如何用 Valgrind 检测使用 LuaJIT FFI 过程当中的内存泄漏

什么状况下可能会有内存泄漏

给带 GC 的语言写 C binding 一贯是件让人迷糊的事。到底应该在 C 手工释放资源呢,仍是依靠 GC 来回收?
还好 LuaJIT FFI 提供了很好用的 ffi.gc 方法。该方法容许给 cdata 对象注册在 gc 时调用的回调,它能让你在 Lua 领域里完成 C 手工释放资源的事。nginx

C++ 提倡用一种叫 RAII 的方式管理你的资源。简单地说,就是建立对象时获取,销毁对象时释放。咱们能够在 LuaJIT FFI 里借鉴一样的作法,在调用 resource = ffi.C.xx_create 等申请资源的函数以后,当即补上一行 ffi.gc(resource, ...) 来注册释放资源的函数。尽可能避免尝试手动释放资源!即便不考虑 error 对执行路径的影响,在每一个出口都补上如出一辙的逻辑会够你受的(用 goto 也差很少,只是稍稍好一点)。框架

有些时候,ffi.C.xx_create 返回的不是具体的 cdata,而是整型的 handle。这会儿须要用 ffi.metatypeffi.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 应用为例,说说如何预防内存泄漏。ui

TEST_NGINX_USE_VALGRIND=1

OpenResty 官方的测试框架 test-nginx 内置了对 Valgrind 的支持。你所需的,不过是加个 TEST_NGINX_USE_VALGRIND=1 环境变量。测试框架看到该环境变量的存在后,会在启动 Nginx 的时候,前面加上 valgrind --leak-check 等选项。这样 Valgrind 就会去检查 Nginx 内部的内存分配。一旦 FFI 调用中存在内存泄漏,Valgrind 便会报告出来。效果与用 Valgrind 运行一个普通的二进制程序无异。lua

$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

实际运行一下,你会发现输出来的“错误”很是多,甚至可能会出现尴尬的内容:rest

==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 .
相关文章
相关标签/搜索