7

Linux的VFS实现 - 番外[一] - dcache

 3 years ago
source link: https://zhuanlan.zhihu.com/p/261669249
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Linux中的VFS实现 [二]

dcache的构成

dcache中每一项的内容是一个路径到inode的映射关系,其数量众多,通过hash表的形式来管理是很自然的。既然是hash表,那就得有作为"key"的元素,在dentry的数据结构中,是通过类型为"qstr"的"name来充当key值,进而计算出hash表的索引(即"value")。

struct qstr d_name;

d_hash(dentry->d_name.hash);

static inline struct hlist_bl_head *d_hash(unsigned int hash)
{
     return dentry_hashtable + (hash >> d_hash_shift);
}

如果一个dentry的引用计数(d_lockref.count)变为0,表明其不再被使用,但此时与之关联的inode并不会被立即释放。根据局部性原理,该inode之后还可能会用到,所以这些处于"unused"的状态dentries会被放入一个LRU链表,其所占据的内存在shrink的时候才被回收。

static void d_lru_add(struct dentry *dentry)
{
    dentry->d_flags |= DCACHE_LRU_LIST;
    this_cpu_inc(nr_dentry_unused);
    if (d_is_negative(dentry))
        this_cpu_inc(nr_dentry_negative);
    WARN_ON_ONCE(!list_lru_add(&dentry->d_sb->s_dentry_lru, &dentry->d_lru));
}

只要一个dentry是"cached"状态,那么它指向的inode也会被"cached",这就构成了inode cache(简称 icache ),icache的每一项内容都是一个已挂载的文件系统中的文件inode。

即便是对应的inode已经被删除,之前的dentry也会以"negative entry"的形式记录在dcache里,这样下次在试图访问这个不存在的路径时,可以立即返回错误,不用再去磁盘瞎折腾一番。

nIbqaeM.jpg!mobile

【状态转换】

当创建一个新的dentry时,d_alloc()会为其申请内存空间,而后通过d_add()将这个新的dentry和对应的inode关联起来,并放入dcache的hash表中。

对dentry的使用以dget()和dput()来实现「引用计数」的加减。d_drop()直接将一个dentry从hash表中移除,而d_delete()的目标则是归还inode,并将一个dentry置于"negative"状态。

JRjUFzf.jpg!mobile

dcache的查找

当然,在dcache的所有操作里,“查找”是最频繁也最重要的。先来看下有了dcache后,路径查找的过程是怎样的吧。假设现在要打开一个路径为"/a/b/c"的文件,那么首先查看"/a/b/c"是否已经存在于dcache中,没有的话再尝试它的上级目录(即"parent")是否在dcache中……以此类推(很像 paging structure caches )。

6biaMjE.jpg!mobile

查找并不涉及对dentry的修改,本质上是一个“读”的过程,但考虑到并行的需要,需要对途径的dentry的引用计数加1,而后再减1,由于涉及到 reference count 值的更新,所以这种方式被称为 "ref-walk"

频繁地加减reference count会带来什么问题呢?由于这是一个“写”操作,会刷新cache line,造成cacheline bouncing。此外,这种"ref-walk"的方式还需要持有所操作dentry的 spinlock (即"d_lock"),开销较大。

后来,内核开发者们借鉴RCU的思路,实现了在SMP系统中扩展性更强的 "rcu-walk " 。关于这两种模式在实现上的区别,还是回到代码上来,看看实现"ref-walk"的核心函数 __d_lookup() 和实现"rcu-walk"的 __d_lookup_rcu() 有何差异。

  • 首先都是获取hash key和计算hash value,两者采用的方式略有不同,不过这并不是两者的关键差异。
qQZFBra.jpg!mobile
  • hash value准备好了,接下来就是遍历hash表的各个slot。这里,"rcu-walk"使用的seqlock,至于它的作用,将在稍后阐释。
iAf2aun.jpg!mobile

一个运行的系统是动态的,在查找目标dentry的这段时间里,其"parent"可能发生了变化,所以需要再次校验。两者在这部分的代码相似度很高,但没有将相同的部分抽取出来聚合成函数,跟据代码中comment的说法,是为了防止"single threaded performance regressions"。

  • 再往下走,是hash比对的环节,"ref-walk"不出意料地更新了ref count,而"rcu-walk"进行的则是seqlock中sequence的判断。采用seqlock的逻辑是:在开始RCU walk查找后,相关的目录可能被更改,这种场景下"rcu-walk"往往没法正确地处理,而判断“被更改”的依据就是sequence的变化。
juyEZ3Q.jpg!mobile

没法处理可怎么办?"rcu-walk"本来就是以“快”取胜,有得必有失,处理不了也不强求,fall back到"ref-walk"的方式继续查找就是。那"ref-walk"也失败呢?说明要找的dentry不在dcache中。这时就只能调用inode的lookup,老老实实地顺着文件系统的directory tree去查找。

nyuUvi6.jpg!mobile

其实啊,在当前"ref-walk"的实现中,也能看到RCU的身影,它同样使用了rcu_read_lock()来防止正在使用的dentry被意外释放。而且,在某些context下,"ref-walk"也会用到seqlock,不过目的不同。

试想一下,一个CPU正在查找"/a/b",然后该路径被其他CPU重命名成了"/a/c/b",你没法防止这种情况的发生,只能通过seqlock检测出来,放弃之前的查找,再次尝试。因为这个seqlock主要用来处理“重命名”的问题,所以在代码中被命名为"rename_lock"。

从"ref-walk"到"rcu-walk"这一改动涉及到了VFS核心层的变化,因而Linus最初在考虑是否它merge进来的时候也是比较谨慎的。不过他随后做了一个实验,在home目录执行"find . -size"命令(该命令可以查找该目录下所有文件的stat信息),结果显示,改动后的设计将速度提升了35%。面对这么亮眼的数据,谁又能对它说“不”呢(参考这篇文章)。

参考:

原创文章,转载请注明出处。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK