Linux内核对象引用计数和生命周期控制

Linux内核中没有垃圾收集机制,为了控制对象的生命周期,对于单线程环境之外具有可见性的数据结构应该使用引用计数.

kobject

  • kobject_init:初始化kobj各个字段的值,指定kobj的ktype,将引用计数初始化为1.
  • kobject_add:设置name和parent,增加parent的引用计数,调用kobj_kset_join,在sysfs创建目录.
  • kobj_set_join:如果kobj->set不为空,将kobj添加到kobj->set的list.
  • kobject_uevent:用于通知用户空间。kset_register会调用kobject_uevent来上报KOBJ_ADD事件.
  • kobject_get/kobject_put:增加/递减引用计数.

引用计数初始化kref_init:

kobject_init->kobject_init_internal->kref_init(&kobj->kref);->refcount_set(&kref->refcount, 1);

get

 kobject_get(kobj);->kref_get(&kobj->kref);->refcount_inc(&kref->refcount);

put

kobject_put->kref_put->refcount_dec_and_test(&kref->refcount) release();

struct kobject 和Linux内核驱动模型捆绑紧密,但是并不是所有的数据结构都需要在sysfs中暴露出来,Linux内核犹如磕药的蜘蛛织的网, 使用仅用于引用计数的struct kobject是对内存资源的严重浪费,一个简单的KREF机制足够了,不需要struct kobject 那样复杂的机制。

dma_fence生命期控制

dma_fence引用计数初始化

dma_fence_init->kref_init(&fence->refcount);->refcount_set(&kref->refcount, 1);

get:

dma_fence_get->kref_get(&fence->refcount);->refcount_inc(&kref->refcount);

put:

dma_fence_put-> kref_put(&fence->refcount, dma_fence_release);->refcount_dec_and_test(&kref->refcount) release(kref);

Infinite Band refcount

infinite bind模块使用了引用计数管理内存对象,其原理和kref类似,区别是使用了裸函数操作计数的增减,管理主干抽取出来如下面代码所示:

struct file

struct file分配,引用计数初始化为1

__alloc_file->atomic_long_set(&f->f_count, 1);

get

struct file *get_file(struct file *f)->atomic_long_inc(&f->f_count);

__fget->__fget_files_rcu->get_file_rcu_many-> atomic_long_add_unless(&(x)->f_count, (cnt), 0)

get_file_rcu->get_file_rcu_many((x), 1)->atomic_long_add_unless(&(x)->f_count, (cnt), 0)

put

void fput(struct file *file)->fput_many(file, 1);->atomic_long_sub_and_test(refs, &file->f_count);add callback ____fput to current return to userspace.

fput是异步释放,同步释放可以调用__fput_sync接口

__fput_sync->atomic_long_dec_and_test(&file->f_count);__fput(file);->file->f_op->release(inode, file);

当struct file对象的f_count引用为0时,struct file->fops->release资源才会释放,下图是一个例子,图中的流程,struct file file1的release函数会始终下不来。

dup/dup2/dup3

dup操作针对一个已经存在的struct file对象,增加其引用技数后,从fdtable中重新分配一个slot,记录这个struct file对象的指针,所以,dup操作后目标和源共享同一个struct file.

用例中连续对一个已经打开的文件调用两次dup2,创建出6和10两个新的fd,引用计数递增,并且指向相同的struct file文件。

struct files_struct

初始化引用计数为1

 atomic_set(&newf->count, 1);

get

get_files_struct->atomic_inc(&files->count);

put

put_files_struct->atomic_dec_and_test(&files->count)

struct file_struct fdtable结构持有struct file的一个引用计数,这个引用计数在如下调用链中发挥作用:

put_files_struct
   if (atomic_dec_and_test(&files->count)) {
      close_files(files);
        filp_close(file, files);
           fput(filp);
             fput_many(file, 1);
               if (atomic_long_sub_and_test(refs, &file->f_count)) {
                  init_task_work(&file->f_u.fu_rcuhead, ____fput);
                  task_work_add(task, &file->f_u.fu_rcuhead, true);
               } 
   }

内核中利用引用计数提供的信息对一些函数进行更加优化的实现,以__fget_light函数为例,观察其实现,当files->count为1的时候,其获取struct file对象引用的方式仅仅是从fdtable中 "checkout"出对应的struct file结构来,并不会增加struct file->f_count计数,而files->count>1的时候,则通过__fget增加对struct file->f_count的引用计数。

这样做的原因是因为,struct file_struct->count反映了进程中共享fd table的线程数量,包含主线程,所以struct file_struct->count实际上反映了进程内线程的数量,同时也反映了进程内针对struct file_struct对象可能存在的并发执行的数量。所以,当struct file_struct->count为1的时候,说明进程只有一个主线程在运行,在__fget_light和__fput_light中间,不存在并发的执行流访问struct file->f_count出发对文件的释放操作,所以__fget_light可以仅仅checkout struct file对象而不必增加struct file->f_count延长struct file生命期以表名对struct file的占有。可以仔细揣摩__fget_light的注释理解代码作者表达的意思。

struct mm_struct

struct mm_struct结构体内定义了两个引用计数字段,分别是atomic_t mm_users和atomic_t mm_count,它们的目的各不相同:

内核通过struct task_struct->mm->mm_users统计共享同一个用户地址空间的线程数量,包含主线程,所以其计数的持有者是线程,所以这个计数值应该等于进程中的线程数。mm_users管理的是进程的VMA资源。

内核通过struct task_struct->mm->mm_count统计针对struct mm_struct对象的引用计数。管理的是struct mm_struct本身的生命期,由注释可以看出,mm_user本身对其进行了一次引用。所以,struct mm_struct的生命期不会短于vma的生命期。

如果存在额外的对mm->mm_user的引用,将会导致VMA无法释放,vma对应的文件release下不来。参考如下博客的分析:

【精选】Linux内核进程,线程,进程组,会话组织模型以及进程管理_papaofdoudou的博客-CSDN博客

mm_user管理接口mmget()/mmget_not_zero()/mmput()

struct mm_struct管理接口mmgrab/mmdrop

进程的vm_area_struct生命期由mm_user控制,前面讲到,当mm_user减为0时,vma struct 的生命期结束,vm_area_struct被释放,具体可以参考mmput的调用路径,但是这个时候mm_struct还存在,并且,通过mm_struct查找vma的链表和红黑树并没有在mmput的执行路径中被删除,所以,如果有地方mmgrab了mm_struct,仍然能够通过链表或者红黑树找到这些被释放的VMA的描述,并且,很可能这些VMA SLAB已经分配给别的进程了。

具体参考seqfile的#140测试case.

struct mm_struct生命期的管理是通过mm_count成员控制的,操作函数是mmgrab/mmdrop.

只要mm_users不为0,就会持有mm_count 1个引用计数:

所以,mm_init中对mm_count初始化为1,对应着的释放应用计数的地方在mmput中当mm_users减为0时,调用__mmput中对应的mmdrop。

正常运行过程中,mm_count的操作主要集中在调度器中,当调度目标任务为内核线程时,如果源任务为用户任务,将会“借用”源任务的mm_struct,所以必须调用mmgrab.直到结束内任务之间的互相调度,发生一次从内核到用户的任务调度的时候(在内核任务调度期间,会将这个mm_struct传递给下一个要执行的内核任务,直到发生这次kernel->user),才会在切换新的MMSTRUCT 为新用户任务的mm_struct,调度完成之后,调用finish_task_switch释放之前一直传递的首个用户任务的mm_struct.

最后一次对应关系发生在任务退出的时候,当任务执行exit_mm时,也会调用mmgrab增加对mm_struct的引用计数。这里有一个很重要的细节,由于任务退出后,不会再返回用户态,也不会在引用任务自己的虚拟地址空间(进程中的其他线程可以),所以任务会将current->mm设为NULL,这样当这个任务完成生命期的最后一次调度时,如果调度目标是内核线程,调度器context_switch实现中就会将退出的任务判断为内核线程而不会执行mmgrab,这样exit_mm中的mmgrab就相当于“替context_switch"完成一次mmgrab,以借用给接下来的向内核线程的调度,当最终完成一次从KERNEL到USER的调度时,对应finish_task_switch会调用mmdrop完成对退出任务mm_struct的释放。

而如果退出线程的调度目标不是内核线程,由于此时源任务的current->mm已经为NULL,效果上就相当于发生一次从KERNEL到USER的调度,rq->prev_mm = prev->active_mm会记录退出任务的MM,这样,调度完成后,对应的finish_task_switch会立刻调用mmdrop释放MM。

所以,mm_struct引用计数管理总的对应关系如下图,和finish_task_switch中的mmdrop对应的有两个位置,分别是context_switch中的mmgrab以及exit_mm中的mmgrab.这也是为何在finsh_task_switch中需要传入源任务的指针的缘故了。

LINUX页表切换在不同架构上的实现_linux 切换页表操作-CSDN博客

struct task_struct生命期管理

struct task_struct引用计数定义:

初始化为1

增加引用get

get_task_struct->refcount_inc(&t->usage);

递减引用计数

 put_task_struct(struct task_struct *t)->if (refcount_dec_and_test(&t->usage)) __put_task_struct(t);

task_struct的生命期间结束到struct task_struct->rcu_users的控制,具体参考delayed_put_task_struct函数,它和ZOMBIE僵尸进程的回收有关。

struct dentry的引用计数

struct dentry的生命期由引用计数控制,引用计数在分配到时候初始化为1:

通过dget/dput增加/减少引用。

内核对struct dentry的管理符合如下规律:

1.struct dentry 有被引用(引用计数大于0,就不会被销毁).

2.struct dentry引用计数被初始化为1,代表当前目录"."对其的引用。

3.每一个子目录都会递增父目录的引用计数。

4.如果应用将目录作为当前工作目录,会递增目录的引用计数,离开目录后(无论去上级还是下一级目录),引用计数将会减1。

5.引用计数只会被子目录递增(包括表示当前目录的.),不会被孙目录隔代递增。

以如下的目录等级为例:

在没有控制台将此目录树中的目录作为工作目录时,dir1引用计数为2,dir2引用计数为5。

当删除dir2下所有子目录,dir1不变,dir2的引用计数变为1:

和struct file与struct file_struct的关系不同,在struct dentry生命周期管理中,子目录握有父目录的引用,所以子目录消失之前,父目录不能消失,而struct file_struct的消失可能早于struct file的消失。这可能就是我们无法用rmdir删除一个存在子目录的父目录的原因:

struct kobject在 sysfs中可以看作一个目录,所以也符合同样的规律,很难直接删除一个具有子目录的sysfs父目录,除非从子目录开始减少对父目录的引用计数,具体可以参考kobject_cleanup/kobjet_del两个函数的实现,它们都会减少对父目录的引用计数:

生命期控制

对象的生命期受到client和对象之间连线的控制,而并非client本身,对于目录来说,删除目录的行为和父目录引用计数减1没有必然联系,仅仅是因为删除子目录会调用put切断对父目录引用计数的连线导致的。所以反映在目录上则为子目录的生命期小于父目录,而对其其他的场景,比如驱动中的应用,调用PUT的CLIENT方可能是驱动中的某个子模块,这个模块递减引用计数,并不会导致这个CLIENT本身的消失。

AMDKFD GPU驱动的例子

在6.3.X的KFD的实现中,每次OPEN对kfd_process的的引用计数都会增加,首次OPEN的时候引用计数从无到有增2,因为创建kref_init初始化为1,返回kfd_open再kref_get增加一次,所以一共2次。而其他的OPEN只增加1次(注意find_process第二个参数为FALSE不递增PROCESS对象的引用计数)。后者可以由kfd_release时的引用技术递减一次平衡调,但是谁来平衡首次open调用create_process初始化的引用计数1呢?答案是如图虚线指向的路径。下图中,相同颜色的引用计数操作代表互相平衡的操作。

对比5.4.X KFD的实现,可以发现更加简单,在5.4.X KFD中,没有定义kfd fops的release(kfd_realease)接口,所以对应的也不会有上途中第二次的kref_get操作。find_process也不会递增引用计数,只有首次创建process时有初始化引用计数为1,后续在由虚线的路径平衡即可。相对简单很多,如下图:

循环引用的处理

以KOBJECT为例,下图的场景,除了kobject默认的引用关系之外,还存在其嵌入的父对象之间的互相引用,所以,一开始父对象和子对象的引用计数为3和2。伴随着调用的一步步发生,两个对象被安全销毁。

这里需要注意的是kobject_del的调用,kobject_del函数应用在子对象上,会切断kobject对象内部对父对象的引用,并且对父对象的引用计数减1,但是子对象并没有被销毁,引用计数不变。通过这种操作,相当于切断了内核内部两个对象之间的互相引用,但是嵌入外部的父对象之间的引用还在,不过没有关系,外部是我们可以控制的,这不下一步就是child->priv = NULL;切断外部子对象对父对象的引用,此时父对象的引用计数都是1,各自保留嵌入父对象之间的引用,然后此时再次对父对象调用kobject_put(),触发销毁父对象,引发对父对象的release回调,此时父对象的PRIV仍然指向子对象,副对象在release中进而触发对子对象的kobject_put,导致子对象的release回调被调用,由于事先我们已经切断了child->priv的引用,所以在子对象的回调中不会再次触发父对象的kobject_put导致错误。从而安全销毁两个对象。

如果不掉用kobject_del事先切断这种KOBJECT内部的引用,而是直接调用kobject_put销毁对象,无论先销毁谁,由于内部的子对象的链接仍然存在,会出现重复删除的情况。

总结

个人理解,从设计模式的角度来说,引用计数主要用于两个对象或者一个对象对环境采用聚合形式的结构上面,如果是组合结构,对象的之间或者对象和环境之间的生命期是一致的,就不需要引用计数进行管理。当局部的生生命期和引用方之间的生命期不一致时,才会使用引用计数,所以,不是所有的对象都需要用引用计数做生命期管理,如果一个对象是另一个对象的内嵌对象,而被嵌入的父对象已经有了引用计数管理,则此对象就不需要再使用引用计数。

【精选】深入浅出理解桥接模式-CSDN博客

参考资料

linux kref详解-CSDN博客

https://github.com/shornado/mybook/blob/master/Reprint-Kroah-Hartman-OLS2004.pdf

http://www.kroah.com/linux/talks/ols_2004_kref_paper/Reprint-Kroah-Hartman-OLS2004.pdf

linux-5.15.146/Documentation/core-api/kref.rst

linux-5.15.146/Documentation/core-api/kobject.rst

https://gitee.com/tugouxp/kref.git


End

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

papaofdoudou

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值