給帶 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.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 應用爲例,說說如何預防內存泄漏。ui
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 的原理,加塞 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 .