Linux 核心裡的“智慧指標”
眾所周知,C/C++語言本身並不支援垃圾回收機制,雖然語言本身具有極高的靈活性,但是當遇到大型的專案時,繁瑣的記憶體管理往往讓人痛苦異常。現代的C/C++類庫一般會提供智慧指標來作為記憶體管理的折中方案,比如STL的auto_ptr,Boost的Smart_ptr庫,QT的QPointer家族,甚至是基於C語言構建的GTK+也通過引用計數來實現類似的功能。Linux核心是如何解決這個問題呢?同樣作為C語言的解決方案,Linux核心採用的也是引用計數的方式。如果您更熟悉C++,可以把它類比為Boost的shared_ptr,或者是QT的QSharedPointer。
在Linux核心裡,引用計數是通過 struct kref 結構來實現的。在介紹如何使用 kref 之前,我們先來假設一個情景。假如您開發的是一個字元裝置驅動,當裝置插上時,系統自動建立一個裝置節點,使用者通過檔案操作來訪問裝置節點。
如上圖所示,最左邊的綠色框圖表示實際裝置的插拔動作,中間黃色的框圖表示核心中裝置物件的生存週期,右邊藍色的框圖表示使用者程式系統呼叫的順序。如果使用者程式正在訪問的時候裝置突然被拔掉,驅動程式裡的裝置物件是否立刻釋放呢?如果立刻釋放,使用者程式執行的系統呼叫一定會發生記憶體非法訪問;如果要等到使用者程式close之後再釋放裝置物件,我們應該怎麼來實現?kref就是為了解決類似的問題而生的。
kref的定義非常簡單,其結構體裡只有一個原子變數。
struct kref { atomic_t refcount; };
Linux核心定義了下面三個函式介面來使用kref:
void kref_init(struct kref *kref); void kref_get(struct kref *kref); int kref_put(struct kref *kref, void (*release) (struct kref *kref));
我們先通過一段虛擬碼來了解一下如何使用kref。
struct my_obj { int val; struct kref refcnt; }; struct my_obj *obj; void obj_release(struct kref *ref) { struct my_obj *obj = container_of(ref, struct my_obj, refcnt); kfree(obj); } device_probe() { obj = kmalloc(sizeof(*obj), GFP_KERNEL); kref_init(&obj->refcnt); } device_disconnect() { kref_put(&obj->refcnt, obj_release); } .open() { kref_get(&obj->refcnt); } .close() { kref_put(&obj->refcnt, obj_release); }
在這段程式碼裡,我們定義了obj_release來作為釋放裝置物件的函式,當引用計數為0時,這個函式會被立刻呼叫來執行真正的釋放動作。我們先在device_probe裡把引用計數初始化為1,當使用者程式呼叫open時,引用計數又會被加1,之後如果裝置被拔掉,device_disconnect會減掉一個計數,但此時refcnt還不是0,裝置物件obj並不會被釋放,只有當close被呼叫之後,obj_release才會執行。
看完虛擬碼之後,我們再來實戰一下。為了節省篇幅,這個實作並沒有建立一個字元裝置,只是通過模組的載入和解除安裝過程來對感受一下kref。
#include <linux/kernel.h> #include <linux/module.h> struct my_obj { int val; struct kref refcnt; }; struct my_obj *obj; void obj_release(struct kref *ref) { struct my_obj *obj = container_of(ref, struct my_obj, refcnt); printk(KERN_INFO "obj_release\n"); kfree(obj); } static int __init kreftest_init(void) { printk(KERN_INFO "kreftest_init\n"); obj = kmalloc(sizeof(*obj), GFP_KERNEL); kref_init(&obj->refcnt); return 0; } static void __exit kreftest_exit(void) { printk(KERN_INFO "kreftest_exit\n"); kref_put(&obj->refcnt, obj_release); return; } module_init(kreftest_init); module_exit(kreftest_exit); MODULE_LICENSE("GPL");
通過kbuild編譯之後我們得到kref_test.ko,然後我們順序執行以下命令來掛載和解除安裝模組。
sudo insmod ./kref_test.ko
sudo rmmod kref_test
此時,系統日誌會列印出如下訊息:
kreftest_init
kreftest_exit
obj_release
這正是我們預期的結果。
有了kref引用計數,即使核心驅動寫的再複雜,我們對記憶體管理也應該有信心了吧。
接下來主要介紹幾點使用kref時的注意事項。
Linux核心文件kref.txt羅列了三條規則,我們在使用kref時必須遵守。
規則一:
If you make a non-temporary copy of a pointer, especially if it can be passed to another thread of execution, you must increment the refcount with kref_get() before passing it off;
規則二:
When you are done with a pointer, you must call kref_put();
規則三:
If the code attempts to gain a reference to a kref-ed structure without already holding a valid pointer, it must serialize access where a kref_put() cannot occur during the kref_get(), and the structure must remain valid during the kref_get().
對於規則一,其實主要是針對多條執行路徑(比如另起一個執行緒)的情況。如果是在單一的執行路徑裡,比如把指標傳遞給一個函式,是不需要使用kref_get的。看下面這個例子:
kref_init(&obj->ref); // do something here // ... kref_get(&obj->ref); call_something(obj); kref_put(&obj->ref); // do something here // ... kref_put(&obj->ref);
您是不是覺得call_something前後的一對kref_get和kref_put很多餘呢?obj並沒有逃出我們的掌控,所以它們確實是沒有必要的。
但是當遇到多條執行路徑的情況就完全不一樣了,我們必須遵守規則一。下面是摘自核心文件裡的一個例子:
struct my_data { . . struct kref refcount; . . }; void data_release(struct kref *ref) { struct my_data *data = container_of(ref, struct my_data, refcount); kfree(data); } void more_data_handling(void *cb_data) { struct my_data *data = cb_data; . . do stuff with data here . kref_put(&data->refcount, data_release); } int my_data_handler(void) { int rv = 0; struct my_data *data; struct task_struct *task; data = kmalloc(sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; kref_init(&data->refcount); kref_get(&data->refcount); task = kthread_run(more_data_handling, data, "more_data_handling"); if (task == ERR_PTR(-ENOMEM)) { rv = -ENOMEM; goto out; } . . do stuff with data here . out: kref_put(&data->refcount, data_release); return rv; }
因為我們並不知道執行緒more_data_handling何時結束,所以要用kref_get來保護我們的資料。
注意規則一里的那個單詞“before”,kref_get必須是在傳遞指標之前進行,在本例裡就是在呼叫kthread_run之前就要執行kref_get,否則,何談保護呢?
對於規則二我們就不必多說了,前面呼叫了kref_get,自然要配對使用kref_put。
規則三主要是處理遇到連結串列的情況。我們假設一個情景,如果有一個連結串列擺在你的面前,連結串列裡的節點是用引用計數保護的,那你如何操作呢?首先我們需要獲得節點的指標,然後才可能呼叫kref_get來增加該節點的引用計數。根據規則三,這種情況下我們要對上述的兩個動作序列化處理,一般我們可以用mutex來實現。請看下面這個例子:
static DEFINE_MUTEX(mutex); static LIST_HEAD(q); struct my_data { struct kref refcount; struct list_head link; }; static struct my_data *get_entry() { struct my_data *entry = NULL; mutex_lock(&mutex); if (!list_empty(&q)) { entry = container_of(q.next, struct my_q_entry, link); kref_get(&entry->refcount); } mutex_unlock(&mutex); return entry; } static void release_entry(struct kref *ref) { struct my_data *entry = container_of(ref, struct my_data, refcount); list_del(&entry->link); kfree(entry); } static void put_entry(struct my_data *entry) { mutex_lock(&mutex); kref_put(&entry->refcount, release_entry); mutex_unlock(&mutex); }
這個例子裡已經用mutex來進行保護了,假如我們把mutex拿掉,會出現什麼情況?記住,我們遇到的很可能是多執行緒操作。如果執行緒A在用container_of取得entry指標之後、呼叫kref_get之前,被執行緒B搶先執行,而執行緒B碰巧又做的是kref_put的操作,當執行緒A恢復執行時一定會出現記憶體訪問的錯誤,所以,遇到這種情況一定要序列化處理。
我們在使用kref的時候要嚴格遵循這三條規則,才能安全有效的管理資料。
相關文章
- 智慧指標指標
- 智慧網聯建設核心評價指標探討指標
- perl 裡邊的 函式指標函式指標
- [CPP] 智慧指標指標
- 什麼是智慧指標?為什麼要用智慧指標?指標
- openfoam 智慧指標探索指標
- vtk智慧指標指標
- 智慧指標學習指標
- 【c++】智慧指標C++指標
- C++智慧指標C++指標
- 網路營銷效果衡量的核心指標指標
- 智慧指標之手撕共享指標shared_ptr指標
- UE4 智慧指標指標
- 批註:智慧指標分析指標
- C++11 智慧指標C++指標
- 「C++」理解智慧指標C++指標
- auto_ptr 智慧指標指標
- SMART POINTER(智慧指標) (轉)指標
- 智慧指標用法學習指標
- STL容器裡存放物件還是指標物件指標
- 指標+AI:邁向智慧化,讓指標應用更高效指標AI
- C++ 用智慧指標這樣包裝 this 指標是否可行C++指標
- C++進階(智慧指標)C++指標
- C++11智慧指標用法C++指標
- C++ 智慧指標詳解C++指標
- [CareerCup] 13.8 Smart Pointer 智慧指標指標
- 技術初創公司的五個核心指標 - James指標
- NULL 指標、零指標、野指標Null指標
- 這裡是值引用還是指標引用?指標
- C++標準庫有四種智慧指標C++指標
- C++筆記(11) 智慧指標C++筆記指標
- 話說智慧指標發展之路指標
- c++ 智慧指標用法詳解C++指標
- C++智慧指標簡單剖析C++指標
- c++ auto_ptr 智慧指標C++指標
- 【C++智慧指標 auto_ptr】C++指標
- Spear Parser(一):智慧指標類薦指標
- 資料統計工具與常用的核心資料指標指標