如何用 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 .
相關文章
相關標籤/搜索