众所周知,C/C++语言本身并不支持垃圾回收机制。人们很痛苦。现代C/C++类库普遍提供智能指针作为内存管理的妥协,如STL的auto_ptr、Boost的Smart_ptr库、QT的QPointer家族,甚至是建立在C语言之上的GTK+也能实现类似的功能。Linux内核是如何解决这个问题的?同样作为C语言的解决方案,Linux内核也采用了引用计数的方法。如果你比较熟悉C++,你可以把它比作Boost的shared_ptr,或者QT的QSharedPointer。在Linux内核中,引用计数是通过structkref结构体实现的。在介绍如何使用kref之前,我们先假设一个场景。如果你开发一个字符设备驱动,当设备插入时,系统自动创建一个设备节点,用户通过文件操作访问设备节点。如上图所示,最左边的绿色框图代表实际设备的插拔动作,中间的黄色框图代表设备对象在内核中的生命周期,蓝色框图右边代表用户程序系统调用的顺序。如果用户程序正在访问设备时突然拔掉设备,驱动中的设备对象是否会立即释放?如果马上释放,用户程序执行的系统调用必然会造成非法访问内存;如果我们想等到用户程序关闭后再释放设备对象,应该如何实现呢?kref的诞生就是为了解决类似的问题。kref的定义非常简单,其结构中只有一个原子变量。structkref{atomic_trefcount;};Linux内核定义了以下三个函数接口来使用kref:我们先通过一段伪代码来了解一下如何使用kref。structmy_obj{intval;structkrefrefcnt;};structmy_obj*obj;voidobj_release(structkref*ref){structmy_obj*obj=container_of(ref,structmy_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不会被释放,obj_release会仅在调用close后执行。看完伪代码,我们就进入实战。为了节省空间,本实现没有创建字符设备,只是通过模块的加载和卸载过程感受kref。#include#includestructmy_obj{intval;structkrefrefcnt;};structmy_obj*obj;voidobj_release(structkref*ref){structmy_obj*obj=container_of(ref,structkrefrefcnt);printk(KERN_INFO"obj_release\n");kfree(obj);}staticint__initkreftest_init(void){printk(KERN_INFO"kreftest_init\n");obj=kmalloc(sizeof(*obj),GFP_KERNEL);kref_init(&obj->refcnt);return0;}staticvoid__exitkreftest_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,然后我们执行以下命令来挂载和卸载模块。sudoinsmod./kref_test.kosudormmodkref_test此时,系统日志会打印如下信息:kreftest_initkreftest_exitobj_release这正是我们所期望的。有了kref引用计数,即使内核驱动写的再复杂,我们对内存管理也应该有信心了!下面主要介绍一下使用kref时的一些注意事项。Linux内核文档kref.txt列出了我们在使用kref时必须遵循的三个规则。规则1:如果你制作一个指针的非临时副本,特别是如果它可以传递给另一个执行线程,你必须在传递它之前用kref_get()增加引用计数;规则2:当你用完一个指针时,你必须调用kref_put();规则三:如果代码试图在没有持有有效指针的情况下获取对krefed结构的引用,则它必须在kref_get()期间不能发生kref_put()的地方序列化访问,并且该结构必须在kref_get期间保持有效().对于规则一,主要是针对多条执行路径的情况(比如开启另外一个线程)。如果是单一执行路径,比如传递一个函数指针,就没有必要使用kref_get。看下面的例子:kref_init(&obj->ref);//dosomethinghere//...kref_get(&obj->ref);call_something(obj);kref_put(&obj->ref);//dosomethinghere//...kref_put(&obj->ref);你觉得call_something前后的一对kref_get和kref_put是多余的吗?obj不是我们无法控制的,所以它们真的是不必要的。但是当遇到多条执行路径时,情况就完全不同了,必须遵守规则一。这是从内核文档中获取的示例:structmy_data{..structkrefrefcount;..};voiddata_release(structkref*ref){structmy_data*data=container_of(ref,structmy_data,refcount);kfree(data);}voidmore_data_handling(void*cb_data){structmy_data*data=cb_data;..dostuffwithdatahere.kref_put(&data->refcount,data_release);}intmy_data_handler(void){intrv=0;structmy_data*data;structtask_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;gotoout;}..dostuffwithdatahere.out:kref_put(&data->refcount,data_release);returnrv;}因为我们不知道线程more_data_handling什么时候结束,所以需要用kref_get来保护我们的数据。注意规则1中的“before”二字,kref_get必须在传递指针之前执行,在这个例子中,kref_get必须在调用kthread_run之前执行,否则怎么保护?关于第二条规则,我们不需要多说。前面我们调用了kref_get,自然要和kref_put配对。第三条规则主要是处理遇到链表的情况。让我们假设一个场景。如果你面前有一个链表,链表中的节点是有引用计数保护的,你要怎么操作呢?首先我们需要得到节点的指针,然后才有可能调用kref_get增加节点的引用计数。根据第三条规则,在这种情况下,我们需要将上述两个动作序列化。一般我们可以使用mutex来实现。请看下面的例子:staticDEFINE_MUTEX(mutex);staticLIST_HEAD(q);structmy_data{structkrefrefcount;structlist_headlink;};staticstructmy_data*get_entry(){structmy_data*entry=NULL;mutex_lock(&mutex);if(!list_empty(&q)){entry=container_of(q.next,structmy_q_entry,link);kref_get(&entry->refcount);}mutex_unlock(&mutex);returnentry;}staticvoidrelease_entry(structkref*ref){structmy_data*entry=container_of(ref,structmy_data,refcount);list_del(&entry->link);kfree(entry);}staticvoidput_entry(structmy_data*entry){mutex_lock(&mutex);kref_put(&entry->refcount,release_entry);mutex_unlock(&mutex);}在这个例子中,mutex用于Protected,如果我们去掉mutex,会发生什么?请记住,我们正在处理的很可能是多线程操作。如果线程A用container_of获取入口指针,在调用kref_get之前,被线程B抢占,而线程B刚好做了kref_put操作,当线程A恢复执行时,肯定会出现内存访问错误。在这种情况下,它必须被序列化。我们在使用kref时,必须严格遵守这三个规则,才能安全有效地管理数据。