Linux 文件系统体系结构-- Linux 文件系统中元数据使用计数的机制_VMware, Unix及操作系统讨论区_Weblogic技术|Tuxedo技术|中间件技术|Oracle论坛|JAVA论坛|Linux/Unix技术|hadoop论坛_联动北方技术论坛  
网站首页 | 关于我们 | 服务中心 | 经验交流 | 公司荣誉 | 成功案例 | 合作伙伴 | 联系我们 |
联动北方-国内领先的云技术服务提供商
»  游客             当前位置:  论坛首页 »  自由讨论区 »  VMware, Unix及操作系统讨论区 »
总帖数
1
每页帖数
101/1页1
返回列表
0
发起投票  发起投票 发新帖子
查看: 3943 | 回复: 0   主题: Linux 文件系统体系结构-- Linux 文件系统中元数据使用计数的机制        下一篇 
cc
注册用户
等级:中校
经验:1900
发帖:195
精华:0
注册:2011-7-25
状态:离线
发送短消息息给cc 加好友    发送短消息息给cc 发消息
发表于: IP:您无权察看 2011-9-9 10:24:48 | [全部帖] [楼主帖] 楼主

概述

元数据是一个文件系统的重要部分。很多书籍和文章都介绍过 dentry 和 inode 在 Linux 中的作用和机制,但却很少有文献涉及到它们的使用计数( usage counter )。使用计数的机制看似很简单:使用了一个元数据就递增,用完了就递减。但在这句简单的描述后面,具体的过程到底是如何进行的呢?这实际上贯穿了整个元数据的操作以及元数据在内存中的管理。了解这部分的机制,是一个很有意思的过程,可以让你看到 Linux 严谨缜密的思路,可以深入认识 Linux 文件系统的运行机制。这部分内容也是构建分布式文件系统所必须的知识。

本文仍然从两方面来介绍使用计数:增加和减少。最后再看一下在分布式环境中有哪些变化。

这里所引用的代码依据的是 Linux 内核 2.6.20 的版本。



使用计数的增加

创建操作

元数据的创建主要可以分为对文件的创建和对目录的创建。不管是文件还是目录,它们都对应同样的元数据结构,在内存中都有 inode 和 dentry 。

下面我们分别看一下主要的两个创建操作:创建文件和创建目录。

(1)创建文件

创建文件是通过系统调用 sys_open() ,并设置 O_CREATE 标志位来实现的。其调用过程如下:

sys_open() > do_sys_open() > do_filp_open() > open_namei()


在 open_namei() 中,会创建出 dentry 和 inode 结构。先看关于 dentry 的路径:

open_namei() > lookup_hash() > __lookup_hash()


这里会分成3种情况:

    在 dcache 中查找: __lookup_hash() > cached_lookup() > d_lookup() > __d_lookup()
    分配新的 dentry: __lookup_hash() > d_alloc() > atomic_set(&dentry -> d_count, 1);
    在具体文件系统中查找: __lookup_hash() > i_op -> lookup()

和查找有关的内容我们在后面介绍,这里只看创建,也就 d_alloc() ,它会分配一个新的 dentry 结构,在分配的过程中,就会把 dentry 的使用计数初始化为1。在 d_alloc() 中,还会通过函数 dget() 递增父目录的使用计数,这是为了防止父目录在该 dentry 删除前被删除。(“/”除外,它没有父目录):

d_alloc() > dget(parent) > atomic_inc(&dentry->d_count);


我们再看关于 inode 的路径:

open_namei() > open_namei_create() > vfs_create() > i_op->create()


最终会调用具体文件系统的 create 函数。这里以 Ext2 为例,其调用过程如下:

ext2_create() > ext2_new_inode() > new_inode() >
alloc_inode() > atomic_set(&inode->i_count, 1);


具体文件系统在分配 inode 结构的时候,会通过初始化把 inode 的 i_count 域置为1。同时还把 inode 的 i_nlink 域置为1,这个域表示 inode 的 hard link 的数目,其值会被写入到具体文件系统的磁盘中。

总结一下,通过创建操作,会在内存中建立起 dentry 和 inode 结构,并且会把它们的使用计数都初始化为1。

(2)创建目录

创建目录和创建文件是类似的,这里我们简单看一下调用的路径就清楚了。

创建目录是通过系统调用 sys_mkdir() 来实现的。关于 dentry 的路径如下:

sys_mkdir() > sys_mkdirat() > lookup_create() > lookup_hash() > __lookup_hash()


可以看出,这与前面“创建文件”中介绍的是一样的。

关于 inode 的路径如下:

sys_mkdir() > sys_mkdirat() > vfs_mkdir() > i_op->mkdir()


最终会调用具体文件系统的 mkdir 函数。这里以 Ext2 为例,其调用过程如下:

ext2_mkdir() > ext2_new_inode() > new_inode() > alloc_inode() > atomic_set()


可以看出,这与前面“创建文件”中介绍的也是一样的。

由此也可以看出,从内存中的元数据结构来看,Linux对文件和目录的管理是一样的。

查找操作

创建了一个对象(文件或目录)后,要使用这个对象,就必须先进行查找。查找操作是元数据使用的关键操作,基本上所有元数据操作都会以查找操作为起始,因为只有找到了元数据才能进一步对其进行操作。即使对于创建操作,一开始也要进行查找,只不过因为要创建的对象还不存在,所以会查找失败,然后才进行创建。

查找操作的入口函数是 __link_path_walk() ,其调用过程如下:

__link_path_walk() > do_lookup()


到了这里,要做的事情主要是在内存中查找相应文件所对应的 dentry 结构。这会分为两种情况:

(1)该 dentry 结构在内存中

此时,通过哈希就可以获取该 dentry 结构,并将其使用计数递增。

do_lookup() > __d_lookup() > atomic_inc(&dentry->d_count)


(2)该 dentry 结构不在内存中

此时,该 dentry 结构可能从来就没在内存中建立起来,或者在内存中存在过,但已经从 LRU 队列 dentry_unused 中被换出内存。无论如何,都需要从磁盘读取元数据,在内存中建立起 dentry 和 inode 结构。这时所进行的步骤是:

首先在内存中分配一个dentry结构:

do_lookup() > real_lookup() > d_alloc() > atomic_set(&dentry->d_count, 1);


这里的 d_alloc() 和前面“创建操作”介绍的一样,会把 dentry 的使用计数初始化为1,并将其父目录的使用计数通过 dget() 递增。

分配了 dentry 结构后,就要从磁盘找出对应的元数据。这个过程因文件系统而异,所以通过父节点的 inode -> i_op 里的函数来进行。

real_lookup() > i_op->lookup()


这里以 Ext2 为例,调用的是 ext2_create() ,过程如下:

(1) ext2_lookup() > iget(dir->i_sb, ino);

(2) ext2_lookup() > d_splice_alias() > __d_find_alias() > __dget_locked() >

atomic_inc(&dentry->d_count);


前者调用 iget() ,首先通过 ino 在 inode cache 中查找 inode ,如果找到就返回并增加其引用计数;如果没有找到,就分配一个新的(调用 alloc_inode() ,会把使用计数初始化为1,参照前面“创建操作”),并从磁盘读入相应索引节点,在内存中建立起 inode 结构。

后者则把 dentry 与 inode 结构绑定,并递增了 dentry 的使用计数。

总结一下,查找操作的主要过程就是在内存中查找 dentry 结构,如果找到就递增其使用计数;如果找不到就到磁盘中去取,并在内存建立 dentry 和 inode 结构,同时将它们的使用计数初始化为1。因此查找操作都会增加 dentry 的使用计数,或者递增,或者初始化为1。

元数据操作对使用计数的运用

这里我们举例说明元数据操作对 dentry 使用计数的运用,让大家对其有个比较具体的认识和感觉。

元数据操作的实质就是对元数据进行使用。那么,要使用某个元数据时,必须在内存中为其建立相应的结构,即 inode 和 dentry 。但并不是所有的元数据每时每刻都会有对应的结构在内存中,只有需要时才会建立这些结构,并且在特定的时候又会被换出内存。那么如何管理内存元数据结构的使用,从而决定其何时在内存中,何时被换出,这就是通过 dentry 的使用计数来实现的。

下面我们以两个常见的元数据操作为例,来看 Linux 如何管理内存元数据结构的使用。

(1) getattr 操作

Linux 内核中有很多操作都会调用到 getattr ,我们举其中的一个来说明:sys_stat() > vfs_stat_fd() 。

函数 vfs_stat_fd() 比较短,我们将其内容都列出来:

int vfs_stat_fd(int dfd, char __user *name, struct kstat *stat)
{
      struct nameidata nd;
      int error;
      error = __user_walk_fd(dfd, name, LOOKUP_FOLLOW, &nd);
      if (!error) {
            error = vfs_getattr(nd.mnt, nd.dentry, stat);
            path_release(&nd);
      }
      return error;
}


这里先调用了 __user_walk_fd() ,这个函数继续走下去的路径是:

__user_walk_fd() > do_path_lookup() > link_path_walk() > __link_path_walk()


可以看出, __link_path_walk() 就是前面介绍过的查找操作。如果成功返回,就会增加 dentry 的使用计数,否则就不增加。而如果查找成功,就进行具体的 getattr 的工作,调用的是 vfs_stat_fd() 的主体函数 vfs_getattr() 。这之后,会调用 path_release() ,这个函数的路径是:

path_release(&nd) > dput(nd->dentry)


函数 dput() 会将 dentry 的使用计数减少,这个函数我们将在后面详细介绍。

总结一下, getattr 操作首先要查找元数据,找到后,就增加 dentry 的使用计数,只要 dentry 的使用计数不为0,它就会存在于 dcache 中,而不会被换出内存。当 getattr 的主要操作步骤完成后,就会减少 dentry 的使用计数,表明 getattr 操作已经完成,不再需要使用这个 dentry 了。

(2) link 操作

下面再看一个操作。 Link 操作用于创建一个对象链接。其调用路径为:

sys_link() > sys_linkat()


接下来可以分为七个部分:

(1) error = __user_walk_fd(olddfd, oldname,

flags & AT_SYMLINK_FOLLOW ? LOOKUP_FOLLOW : 0,
&old_nd);
(2) error = do_path_lookup(newdfd, to, LOOKUP_PARENT, &nd);
(3) new_dentry = lookup_create(&nd, 0);
(4) error = vfs_link(old_nd.dentry, nd.dentry->d_inode, new_dentry);
(5) dput(new_dentry);
(6) path_release(&nd);
(7) path_release(&old_nd);


第1步中, __user_walk_fd() 会查找要被链接的文件,这和前面 getattr 中的函数一样,会把这个文件对应的 dentry 的使用计数进行递增。它和第7步中的 path_release() 对应。

第2步中, do_path_lookup() 会查找要创建的链接的父目录,它同样会进行查找操作,递增 dentry 的使用计数。它和第6步中的 path_release() 对应。

第3步中, lookup_create() 会创建链接对象的 dentry 结构,这和前面“创建目录”中介绍的函数一样。它和第5步中的 dput() 对应。

这里我们再次看到,一个元数据操作中都会先查找涉及到的元数据,并增加其 dentry 的使用计数,然后在该操作结束的时候递减这些使用计数。

对于 link 操作,我们还要讲讲它的主体函数,也就是 vfs_link() ,其路径为:

vfs_link() > i_op->link()


以 Ext2 为例,调用的是 ext2_link() ,过程如下:

(1) ext2_link() > inode_inc_link_count() > inc_nlink()

(2) ext2_link() > atomic_inc(&inode->i_count);

先看前者,这里的 inode 结构对应的是被链接的文件, ext2_link() 会递增该 inode 的 i_nlink ,前面说过,这个域表示 inode 的 hard link 的数目,因为我们对这个文件建立了一个新的链接,所以会对这个域进行递增。

后者的 inode 结构依然对应的是被链接的文件, ext2_link() 会递增 inode 的使用计数。到目前位置,我们看到,各个操作对元数据使用计数的运用主要都是针对 dentry ,而很少针对 inode 。这是因为 dentry 主要用于内存中目录树结构的表示,而查找操作主要就是针对目录树结构来进行的,因此它频繁地对 dentry 的使用计数进行操作。对于 inode 的使用计数,它主要表示这个元数据的存在,因此一般只在创建这个 inode 的时候以及删除的时候才会用到。我们知道一个 inode 可以对应多个 dentry ,这是因为一个文件可以有多个链接,所以当多了一个链接时,就要递增 inode 的使用计数。



使用计数的减少

dput 函数

前面我们提到过,在元数据操作中,通过查找操作增加了 dentry 的使用计数,会在结尾处通过 dput() 进行递减。这里我们就来看看 dput() 的机制。

在 dput() 中,处理的步骤如下:

    对 dentry -> d_count 递减,如果不为0,就直接返回。
    判断具体文件系统是否定义了 d_op -> d_delete 这个接口函数。本地文件系统 Ext2, Ext3 都没有定义这个函数, NFS 定义了这个函数。对于该函数的具体作用我们在后面介绍。既然 Ext2 没有定义,我们就继续往下看。
    判断 dentry 是否从 dcache 的哈希链上移除了。(1)如果是,表示该元数据对应的对象已经被删除了,此时可以释放该元数据;(2)如果不是,表示该元数据对应的对象没有被删除,这时把 dentry 挂到 LRU 队列 dentry_unused 上,然后返回。
    如果进入释放元数据的那条路径,会释放 dentry 结构和 inode 结构。

其中,释放 inode 结构的调用路径是:

dput() > dentry_iput() > iput() > iput_final()


在 iput() 中,会递减 inode 的使用计数,如果递减完后为0,就进一步调用 iput_final()

iput_final() > generic_drop_inode()


在 generic_drop_inode() 中,会判断 inode -> i_nlink 的值。我们在前面说过, i_nlink 这个域表示 inode 的 hard link 数目,这里就是通过这个域来判断该 inode 能否被删除。

若 inode -> i_nlink 为0,说明没有 hard link 指向该 inode ,可以将其删除,路径为:

generic_drop_inode() > generic_delete_inode() > s_op->delete_inode()


若 inode -> i_nlink 不为0,说明仍有 hard link 指向该 inode ,不能将其删除,路径为:

generic_drop_inode() > generic_forget_inode()


释放 inode 结构的步骤就是这些。释放 dentry 的主要做的事情就是将 dentry与inode 脱离,然后释放 dentry 结构。要注意的是,每当释放了一个 dentry ,都要获取其原来父目录的 dentry ,然后又跳转到 dput() 的开头,继续对父目录的 dentry 进行释放操作。这是因为,前面也提过,每次创建一个 dentry 结构,除了增加自身的使用计数外,还会增加其父目录 dentry 的使用计数。所以当释放了一个 dentry 后也要将其父目录 dentry 使用计数递减,才能保证父目录为空时能够被释放。

unlink 函数

在 dput() 中我们提到过要判断 dentry 是否从 dcache 的哈希链上移除。在本地文件系统中,当删除一个文件时,就会将其对应的 dentry 从 dcache 的哈希链上移除,这是通过 unlink() 来实现。unlink() 的流程如下:

sys_unlink() > do_unlinkat() >


在do_unlinkat()中,主要可以分为五个部分:

(1) dentry = lookup_hash(&nd);

(2) atomic_inc(&inode->i_count);

(3) vfs_unlink(nd.dentry->d_inode, dentry);

(4) dput(dentry);

(5) iput(inode);      

可以看出,主体函数 vfs_unlink() 前后,有配对的操作对 dentry 和 inode 进行增减。

我们先看 vfs_unlink() ,其路径为:

vfs_unlink() > d_delete()


在 d_delete() 中,如果 dentry 的使用计数为1,说明此时没有其他人引用该 dentry ,那么就尝试把该 dentry 的 inode 删除,这里调用的是 dentry_iput() ,这个函数已经在 dput() 那部分介绍过了。这个过程的实质就是把 dentry 转为 negative 状态,然后返回。转为 negative 的 dentry 依然在 dcache 的哈希链中,但删除操作已经完成,对应的代码为:

if (atomic_read(&dentry->d_count) == 1) {
      dentry_iput(dentry);
      ...
      return;
}


否则,即 dentry 的使用计数大于1(这里不可能小于1,因为 do_unlinkat() 中调用 lookup_hash() 时已经对 dentry 的使用计数进行了增加),说明有其他人引用该 dentry ,此时不能把这个 dentry 转为 negative ,那么就把这个 dentry 从 dcache 的哈希链中脱离,对应的代码为:

if (!d_unhashed(dentry))
__d_drop(dentry);


那么什么时候删除这个 dentry 所对应的 inode 呢?大家可以回过头看一下 dput() 介绍过的内容。在 dput() 中,当 dentry 已经从 dcache 的哈希链上移除后,就会继续进行释放元数据的操作。所以只要当最后一个使用 dentry 的操作结束时调用 dput() ,就会调用 dentry_iput() 。

总结一下,删除 inode 必须由 unlink 的 d_delete 发起,如果可能就在 d_delete 中完成删除;否则就 unhash ,然后由最后一个使用者调用 dput() 删除。但只有在 d_delete 完成 unhash 之后, dput 才有可能删除 inode 。

看完主体函数 vfs_unlink() 后,我们关注一下对 inode 使用计数的增减。

在 do_unlinkat() 中,调用 vfs_unlink() 之前,递增了 inode -> i_count ,因此,如果刚进入 do_unlinkat() 时 dentry 的使用计数为1,真正的删除操作并不是在 vfs_unlink() 的 d_delete() 进行,而是在 vfs_unlink() 之后的 iput() 进行(源码里也在 iput() 旁边进行了注释)。

大家也许要问, vfs_unlink() 之后,在 iput() 之前不是还有一个 dput() 吗?那么会不会在这里就删除了 inode 呢?其实不会,我们分三种情况来讨论:

    如果 dentry 的使用计数为1,说明没有其他人在使用,则此前 vfs_unlink() 的 d_delete() 必然已经将 dentry 转为 negative ,但没有脱离 dcache 的哈希链,因此不会进行删除(参看 dput() 流程)。删除 inode 的时机是 do_unlinkat() 的 iput() 。
    如果 dentry 的使用计数大于1,说明有其他人在使用。若在 do_unlinkat() 调用 dput() 之前,其他使用者都调用了自己的 dput() ,则此时 dentry 的使用计数又变为了1。由于之前 vfs_unlink() 的 d_delete() 已经将 dentry 从哈希链上移除(但也因此没有走到 dentry_iput() 那步,没有递减 inode 的使用计数),因此在 do_unlinkat() 的 dput() 中会走到 dentry_iput() ,但由于 do_unlinkat() 一开始递增了 inode 的使用计数,所以这个 dput() 也不能删除 inode 。删除 inode 的时机仍然是 do_unlinkat() 的 iput() 。
    如果 dentry 的使用计数大于1,并且在 do_unlinkat() 调用 dput() 之前,其他使用者没有调用自己的 dput() ,则 do_unlinkat() 调用 dput() 递减了 dentry 的使用计数之后就直接返回了。删除 inode 的时机是其他使用者的 dput() 。

总结一下,这部分的操作比较繁杂,需要结合 dput() 和 unlink 操作一起来看,但理清思路后,也会发现其逻辑还是很清晰的,而这种机制也与使用计数的增加结合得非常好,共同构成了 Linux 文件系统管理内存元数据结构的机制。



分布式文件系统对使用计数机制的扩展

分布式文件系统和本地文件系统有所差别,典型的结构由三部分构成:客户端、元数据服务器、数据服务器。它的一个特点就是允许多客户端的模式,因此有些场景跟单机的模式不大一样。

查找后的 revalidate

前面提到过,在查找操作中,会先到内存去找,如果找不到还要到具体文件系统的磁盘上去取。在分布式环境中,当一个客户端更新了一个文件,从而相应地修改了其元数据,其他客户端并不能马上知道这个更新,于是当查找操作在内存中找到所要的元数据后,会判断一下文件系统是否实现了 d_op -> d_revalidate 这个接口函数,如果实现了,就会再到元数据服务器取一份最新的元数据,从而保证客户端缓存中数据的有效性,这也是这个操作名称叫做 revalidate 的原因。

本地文件系统如 Ext2, Ext3 都没有实现这个接口函数,也没有必要。 NFS 中是有实现的。

前面说过,查找操作会增加 dentry 的使用计数,也就是说不管走了什么路径,只要找到了所需的元数据,那么通过查找操作所返回的 dentry 结构,其使用计数是增加过的。所以具体文件系统的 d_revalidate 函数也要对 dentry 的使用计数进行增加,这样才能和 VFS 层正确衔接起来。

dput 函数的变动

大家是否还记得,前面介绍 dput() 函数时,曾经提到其处理步骤可分为五步,第2步中要判断具体文件系统是否定义了 d_op -> d_delete 这个接口函数。由于 Ext2, Ext3 都没有定义,我们也就跳过了这个判断。然而,在 NFS 中,是有定义这个接口函数的,但没有什么实质内容,基本就是直接返回。其实,是否定义这个函数是一个路口的转向。如果定义了, dput() 的流程就会直接跳转到函数 __d_drop() ,其作用是把 dentry 从哈希链上移除;再接下来就进入 dentry_iput() 尝试删除 inode 。

大家也许会奇怪,前面介绍了 dput() 和 unlink 操作之间那么多复杂的关系,怎么这里直接就都跳过了。这其实是分布式环境的一种解决方法。在分布式环境中,由于有多个客户端,只要有一个客户端进行了删除操作,相应的元数据就应该可以被删除。如果按照本地文件系统的那种机制,此时只有进行了删除操作的客户端才可以通过 dput() 最终走到函数 s_op -> delete_inode() ,从而在元数据服务器端释放元数据;其他客户端由于没有进行删除操作,它们的 dput() 都无法走到 dentry_iput() 那一步,更别说 s_op -> delete_inode() 了。所以在分布式环境中,就采用另一种思路:只要 dentry 的使用计数为1,此时调用 dput() 都可以走到 dentry_iput() ,如果此时 inode 的使用计数也为1,就能继续一直走到 generic_drop_inode() , generic_drop_inode() 可以继续走到 s_op -> delete_inode() 。这样,元数据服务器就可以自己记录有哪些客户端使用了这个 dentry ,而各个客户端在递减 dentry 的使用计数为0时,都会通过 dput() 与元数据服务器通信。元数据服务器就可以知道某个时刻是否还有客户端在使用这个 dentry ,从而当元数据被删除时决定何时可以进行释放。

这里还有一个问题。前面介绍过, generic_drop_inode() 会根据 inode -> i_nlink 是否为0来判断要调用哪个函数:

若 inode -> i_nlink为0,说明没有 hard link 指向该 inode ,可以将其删除,路径为:

generic_drop_inode() > generic_delete_inode() > s_op->delete_inode()


若 inode->i_nlink 不为0,说明仍有 hard link 指向该 inode ,不能将其删除,路径为:

generic_drop_inode() > generic_forget_inode()


也就是说 generic_drop_inode() 不一定能走到 s_op -> delete_inode() 。而且 inode -> i_nlink 会记录在具体文件系统的磁盘上,它的值是一个全局的值,而不是针对某个客户端的。所以一种解决办法就是不以 s_op -> delete_inode() 作为客户端与元数据服务器的通信接口,而以 s_op -> clear_inode() 作为通信接口。对于 s_op -> clear_inode() ,无论 inode -> i_nlink 的值为多少,都会被调用:

(1) generic_drop_inode() > generic_delete_inode() > s_op->clear_inode()

(2) generic_drop_inode() > generic_forget_inode() > s_op->clear_inode()



总结

通过上面的介绍,我们看到了 Linux 如何对 dentry 和 inode 的使用计数进行操作:何时增加,何时减少。并且了解了元数据操作是如何通过这些使用计数来表明自己正在使用这些元数据的。我们也看到了当删除一个元数据后,如何等到所有使用者都把该元数据的使用计数递减为0之后,才真正对元数据进行释放。最后我们通过分布式文件系统的一些机制了解了在分布式环境下对使用计数的操作要做哪些相应的改变




赞(0)    操作        顶端 
总帖数
1
每页帖数
101/1页1
返回列表
发新帖子
请输入验证码: 点击刷新验证码
您需要登录后才可以回帖 登录 | 注册
技术讨论