LWN:引用計數(shù)的調(diào)試機制!
關(guān)注了就能看到更多這么棒的文章哦~
A reference-count tracking infrastructure
By Jonathan Corbet
December 6, 2021
DeepL assisted translation
https://lwn.net/Articles/877603/
引用計數(shù)(reference count)是一種非常常用的機制,用來跟蹤(track)當前的計算系統(tǒng)中某個對象的生命周期(life cycle)。只要這個對象的每個使用者都能做到準確加、減引用計數(shù)來正確維護這個引用,那么該對象只要還有用戶需要的情況下就能確保一直存在,并在最后一個使用者完成使用后被正常銷毀。不過,這句話中提到的 "正確" 是非常重要的,如果出現(xiàn)引用計數(shù)使用錯誤的情況,那么事情就不那么理想了。網(wǎng)絡(luò)領(lǐng)域的開發(fā)人員 Eric Dumazet 正在開發(fā)一個引用計數(shù)的 tracking 系統(tǒng),可以用來發(fā)現(xiàn)在網(wǎng)絡(luò)子系統(tǒng)中出現(xiàn)的這類 bug,并且希望有一天也能用來幫助整個內(nèi)核檢測這種問題。
由于引用計數(shù)本身是匿名的,所以引用技術(shù)的操作中的錯誤就很難被發(fā)現(xiàn)。比如說,雖然可以比較容易地確認出這個對象的某個使用者在不再需要該對象的時候忘記了釋放對它的引用(也就是沒有遞減引用技術(shù)),但是通常沒有什么簡單的辦法知道具體是哪個使用者。因此,內(nèi)核最終會有一個無法釋放的、未被使用的對象,但是沒有辦法知道引用計數(shù)機制是因為什么而失效了,甚至沒有辦法知道具體是哪個 reference 被丟失了。如果有一種方法可以確定一個對象的幾十個引用中的哪一個被遺忘了導(dǎo)致的泄露,那么就會很容易來找到這個存在 bug 的引用釋放部分的代碼。
Dumazet 的 patch set 通過創(chuàng)建一個跟蹤(tracking)機制來解決這個問題,該機制基本上是把使用引用計數(shù)的地方包起來。使用這個引用計數(shù)跟蹤機制的第一步,就是添加這個 tracker 本身,這是使用下面這個 ref_tracker_dir 結(jié)構(gòu)來完成的:
struct ref_tracker_dir {
#ifdef CONFIG_REF_TRACKER
spinlock_t lock;
unsigned int quarantine_avail;
refcount_t untracked;
struct list_head list; /* List of active trackers */
struct list_head quarantine; /* List of dead trackers */
#endif
};
這個結(jié)構(gòu)不包含引用計數(shù)本身,相反,它要被添加到包含了我們感興趣的那個引用計數(shù)的對象之中。正如人們可以預(yù)料到的那樣,它的第一個使用場景就是在到龐大的 net_device 結(jié)構(gòu)中去,這個結(jié)構(gòu)是用來管理網(wǎng)絡(luò)設(shè)備的。這些結(jié)構(gòu)在網(wǎng)絡(luò)子系統(tǒng)的許多地方都被引用到,很容易出現(xiàn)引用計數(shù)錯誤。引用的獲取和釋放非常頻繁,以至于都專門定制了一種 per-CPU 的引用計數(shù)機制。萬一發(fā)生了一個引用泄露,就會導(dǎo)致系統(tǒng)無法移除一個網(wǎng)絡(luò)設(shè)備,這進而又會阻礙 container 或虛擬機的清理工作。為了減少查出這些錯誤耗費的力氣,Dumazet 的 patch set 首先在 net_device 結(jié)構(gòu)中加入了一個 ref_tracker_dir 結(jié)構(gòu)。這個結(jié)構(gòu)被初始化(通過調(diào)用 ref_tracker_dir_init() 來初始化)時 list 是空的,quarantine_avail(會在后面再介紹)設(shè)置為調(diào)用者所提供的值。
要想使引用計數(shù)跟蹤機制正常工作,每一個獲取或釋放引用的代碼都必須要告訴 tracker 機制這一點。當引用計數(shù)操作已經(jīng)封裝在其他一些函數(shù)中的時候,這一點相對容易做到。否則可能需要對代碼進行相當多的改動。每當代碼需要獲取這個對象的引用時,它應(yīng)該調(diào)用:
int ref_tracker_alloc(struct ref_tracker_dir *dir, struct ref_tracker **trackerp,
gfp_t gfp_flags);
dir 參數(shù)是指向添加到引用計數(shù)對象中的那個 ref_tracker_dir 結(jié)構(gòu)。這個函數(shù)會分配一個 ref_tracker 結(jié)構(gòu)來跟蹤這個特定的引用,使用這里提供的 gfp_flags,并將其地址存儲在 *trackerp 中。返回值是 0 或錯誤代碼(如果無法分配成功 ref_tracker 結(jié)構(gòu)的話很可能是返回-ENOMEM)。
ref_tracker_alloc() 將把這個新的結(jié)構(gòu)添加到 ref_tracker_dir 結(jié)構(gòu)的 list 中,從而意味著已經(jīng)獲取了一個引用。不過,要想真正獲取有價值的信息的話,這個跟蹤機制必須要以某種方式來記錄下來這個特定的引用是在哪里被獲取的。獲取引用的函數(shù)的名字雖然也會有用,但其實真正出錯的地方是在函數(shù)調(diào)用棧往上走基層的位置,所以還需要更多的信息來確定問題的真正來源。這個追蹤機制中使用了一個名為 "stackdepot "的內(nèi)核調(diào)試功能,它能夠生成并保存完整的 stack trace,stackdepot 嚴格來說是沒有文檔的,可以查看 lib/stackdepot.c 的源代碼來了解它。通過存儲完整的 stack trace,這個引用技術(shù)追蹤機制才有能力真正揭示引用計數(shù)問題真正來自哪里。
當一個引用被釋放時,必須要調(diào)用:
int ref_tracker_free(struct ref_tracker_dir *dir, struct ref_tracker **trackerp);
這個函數(shù)會做不少事情。首先將 trackerp 指向(間接指向)的 tracker 跟蹤器從 list 中移除。跟蹤器內(nèi)部會標記為已被釋放,但該結(jié)構(gòu)本身不會被立即釋放,而是添加到 quarantine list (隔離列表)中,并將當前的 stack trace 存儲下來。這樣做是為了能捕捉那些 double-free 類型的 bug。如果 ref_tracker_free() 再次調(diào)用的時候是使用了一個被標記為已經(jīng)被釋放的 ref_tracker 結(jié)構(gòu),那么會立即生成一份報告,顯示與分配事件和兩個釋放事件等操作相關(guān)的 stack trace 信息。
對于那些很繁忙的對象,quarantine list 很容易增長的太大了,所以上面提到的 quarantine_avail 數(shù)值就是被用來限制這個 list 的長度的。每當一個跟蹤器被添加到 quarantine 區(qū)域時,就會檢查這個數(shù)字,如果 quarantine_avail 為零,quarantine list 中存放最長時間的那個 track 就會被釋放;不為零的話則對這個數(shù)字減一。quarantine_avail 的初始值是在 ref_tracker_dir 初始化時指定的,對于網(wǎng)絡(luò)設(shè)備來說可以初始設(shè)置是允許存放 128 條。
double-free 的 bug 可以被立即捕獲出來,但引用泄露的情況則只有在釋放引用計數(shù)對象的時候才能被檢測到。當內(nèi)核被要求移除一個網(wǎng)絡(luò)設(shè)備時,它會一直等待到該設(shè)備上的引用計數(shù)達到零。而如果有引用泄漏的話,就不會達成這個條件,,直到系統(tǒng)被重新啟動才能完成釋放。當然,到了那個時候,之前保存的關(guān)于哪個引用被泄露的信息早已不復(fù)存在,這是在引用計數(shù)跟蹤機制出現(xiàn)之前的現(xiàn)實情況。任何對對象的引用,如果沒有被釋放的話,仍然會存在對應(yīng)的 active ref_tracker 結(jié)構(gòu)。直接調(diào)用一下 ref_tracker_dir_print() 就可以把所有跟泄露的引用相關(guān)的 stack trace 都打印到系統(tǒng)日志里。
這種機制的優(yōu)勢很明顯:它應(yīng)該能夠找出引用計數(shù)的錯誤,簡化了調(diào)試這些錯誤的步驟。但是另一方面來說,這是一個相當耗費資源的機制,不適合在生產(chǎn)系統(tǒng)中使用。這個實現(xiàn)還需要將引用計數(shù) tracking 機制要專門針對每個想要使用它的子系統(tǒng)中都進行修改,而且代碼變動可能還不小;在 Dumazet 的 23 個 patch 之中,有 21 個是專門用于對 net_device 結(jié)構(gòu)進行檢測的。如果能直接、透明地基于 refcount_t 類型來開發(fā)出一個調(diào)試機制的話,那會更加可行、更少干擾,但它無法配合與網(wǎng)絡(luò)設(shè)備中使用的 one-off (一次性)機制共同使用。
因此,這項工作還不是一個通用的引用計數(shù)調(diào)試工具,但它是朝著這個方向邁出的重要一步。這些拼圖就在那里等待著有足夠動力的開發(fā)者來把它們拼成更加通用的功能。同時,現(xiàn)在的代碼應(yīng)該已經(jīng)可以幫助減少網(wǎng)絡(luò)子系統(tǒng)的代碼中的引用計數(shù) bug 數(shù)量了,這是一個很好的開始。
全文完
LWN 文章遵循 CC BY-SA 4.0 許可協(xié)議。
長按下面二維碼關(guān)注,關(guān)注 LWN 深度文章以及開源社區(qū)的各種新近言論~
