Linux內(nèi)核里的“智能指針”
眾所周知,C/C++語言本身并不支持垃圾回收機(jī)制,雖然語言本身具有極高的靈活性,但是當(dāng)遇到大型的項(xiàng)目時(shí),繁瑣的內(nèi)存管理往往讓人痛苦異?!,F(xiàn)代的C/C++類庫一般會(huì)提供智能指針來作為內(nèi)存管理的折衷方案,比如STL的auto_ptr,Boost的Smart_ptr庫,QT的QPointer家族,甚至是基于C語言構(gòu)建的GTK+也通過引用計(jì)數(shù)來實(shí)現(xiàn)類似的功能。Linux內(nèi)核是如何解決這個(gè)問題呢?同樣作為C語言的解決方案,Linux內(nèi)核采用的也是引用計(jì)數(shù)的方式。如果您更熟悉C++,可以把它類比為Boost的shared_ptr,或者是QT的QSharedPointer。
在Linux內(nèi)核里,引用計(jì)數(shù)是通過 struct kref 結(jié)構(gòu)來實(shí)現(xiàn)的。在介紹如何使用 kref 之前,我們先來假設(shè)一個(gè)情景。假如您開發(fā)的是一個(gè)字符設(shè)備驅(qū)動(dòng),當(dāng)設(shè)備插上時(shí),系統(tǒng)自動(dòng)建立一個(gè)設(shè)備節(jié)點(diǎn),用戶通過文件操作來訪問設(shè)備節(jié)點(diǎn)。
如上圖所示,最左邊的綠色框圖表示實(shí)際設(shè)備的插拔動(dòng)作,中間黃色的框圖表示內(nèi)核中設(shè)備對(duì)象的生存周期,右邊藍(lán)色的框圖表示用戶程序系統(tǒng)調(diào)用的順序。如果用戶程序正在訪問的時(shí)候設(shè)備突然被拔掉,驅(qū)動(dòng)程序里的設(shè)備對(duì)象是否立刻釋放呢?如果立刻釋放,用戶程序執(zhí)行的系統(tǒng)調(diào)用一定會(huì)發(fā)生內(nèi)存非法訪問;如果要等到用戶 程序close之后再釋放設(shè)備對(duì)象,我們應(yīng)該怎么來實(shí)現(xiàn)?kref就是為了解決類似的問題而生的。
kref的定義非常簡(jiǎn)單,其結(jié)構(gòu)體里只有一個(gè)原子變量。
- struct kref {
 - atomic_t refcount;
 - };
 
Linux內(nèi)核定義了下面三個(gè)函數(shù)接口來使用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來作為釋放設(shè)備對(duì)象的函數(shù),當(dāng)引用計(jì)數(shù)為0時(shí),這個(gè)函數(shù)會(huì)被立刻調(diào)用來執(zhí)行真正的釋放動(dòng)作。我們先在 device_probe里把引用計(jì)數(shù)初始化為1,當(dāng)用戶程序調(diào)用open時(shí),引用計(jì)數(shù)又會(huì)被加1,之后如果設(shè)備被拔 掉,device_disconnect會(huì)減掉一個(gè)計(jì)數(shù),但此時(shí)refcnt還不是0,設(shè)備對(duì)象obj并不會(huì)被釋放,只有當(dāng)close被調(diào)用之 后,obj_release才會(huì)執(zhí)行。
看完偽代碼之后,我們?cè)賮韺?shí)戰(zhàn)一下。為了節(jié)省篇幅,這個(gè)實(shí)作并沒有建立一個(gè)字符設(shè)備,只是通過模塊的加載和卸載過程來對(duì)感受一下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");
 
#p#
通過kbuild編譯之后我們得到kref_test.ko,然后我們順序執(zhí)行以下命令來掛載和卸載模塊。
sudo insmod ./kref_test.ko
sudo rmmod kref_test
此時(shí),系統(tǒng)日志會(huì)打印出如下消息:
kreftest_init
kreftest_exit
obj_release
這正是我們預(yù)期的結(jié)果。
有了kref引用計(jì)數(shù),即使內(nèi)核驅(qū)動(dòng)寫的再復(fù)雜,我們對(duì)內(nèi)存管理也應(yīng)該有信心了吧!
接下來主要介紹幾點(diǎn)使用kref時(shí)的注意事項(xiàng)。
Linux內(nèi)核文檔kref.txt羅列了三條規(guī)則,我們?cè)谑褂胟ref時(shí)必須遵守。
規(guī)則一:
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;
規(guī)則二:
When you are done with a pointer, you must call kref_put();
規(guī)則三:
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().
對(duì)于規(guī)則一,其實(shí)主要是針對(duì)多條執(zhí)行路徑(比如另起一個(gè)線程)的情況。如果是在單一的執(zhí)行路徑里,比如把指針傳遞給一個(gè)函數(shù),是不需要使用kref_get的??聪旅孢@個(gè)例子:
- 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前后的一對(duì)kref_get和kref_put很多余呢?obj并沒有逃出我們的掌控,所以它們確實(shí)是沒有必要的。
但是當(dāng)遇到多條執(zhí)行路徑的情況就完全不一樣了,我們必須遵守規(guī)則一。下面是摘自內(nèi)核文檔里的一個(gè)例子:
- 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;
 - }
 
因?yàn)槲覀儾⒉恢谰€程more_data_handling何時(shí)結(jié)束,所以要用kref_get來保護(hù)我們的數(shù)據(jù)。
注意規(guī)則一里的那個(gè)單詞“before”,kref_get必須是在傳遞指針之前進(jìn)行,在本例里就是在調(diào)用kthread_run之前就要執(zhí)行kref_get,否則,何談保護(hù)呢?
對(duì)于規(guī)則二我們就不必多說了,前面調(diào)用了kref_get,自然要配對(duì)使用kref_put。
規(guī)則三主要是處理遇到鏈表的情況。我們假設(shè)一個(gè)情景,如果有一個(gè)鏈表擺在你的面前,鏈表里的節(jié)點(diǎn)是用引用計(jì)數(shù)保護(hù)的,那你如何操作呢?首先我們需要獲得節(jié)點(diǎn)的指針,然后才可能調(diào)用kref_get來增加該節(jié)點(diǎn)的引用計(jì)數(shù)。根據(jù)規(guī)則三,這種情況下我們要對(duì)上述的兩個(gè)動(dòng)作串行化處理,一般我們可以用mutex來實(shí)現(xiàn)。請(qǐng)看下面這個(gè)例子:
- 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);
 - }
 
這個(gè)例子里已經(jīng)用mutex來進(jìn)行保護(hù)了,假如我們把mutex拿掉,會(huì)出現(xiàn)什么情況?記住,我們遇到的很可能是多線程操作。如果線程A在用 container_of取得entry指針之后、調(diào)用kref_get之前,被線程B搶先執(zhí)行,而線程B碰巧又做的是kref_put的操作,當(dāng)線程A恢復(fù)執(zhí)行時(shí)一定會(huì)出現(xiàn)內(nèi)存訪問的錯(cuò)誤,所以,遇到這種情況一定要串行化處理。
我們?cè)谑褂胟ref的時(shí)候要嚴(yán)格遵循這三條規(guī)則,才能安全有效的管理數(shù)據(jù)。
















 
 
 









 
 
 
 