Linux内核阅读--文件路径查找(二)

来源:互联网 发布:温职院网络课程 编辑:程序博客网 时间:2024/04/20 04:18

Linux文件路径查找的基本策略,是从查找根(一般是根目录或当前目录)开始,逐级向下查找。具体到代码中,每个查找的节点被表示为path,path的定义如下。

struct path {       struct vfsmount *mnt;       struct dentry *dentry;};
我们可以看到,linux用挂载点和目录项来唯一表示一个路径节点。目录查找,就是不断的从父路径节点找到子目录节点的过程。在路径的查找中,可能会遇到一些跳转,比如遇到挂载点,遇到符号链接,遇到"..",就需要跳转到相应的位置继续查询。

如果没有遇到需要跳转的情况,则主要依赖dentry中保存的信息进行路径查找。dentry中的信息,一个是inode、superblock等文件系统相关的信息,这些信息可以用来从磁盘中查询子目录。除了文件系统信息,dentry也维护了一个Cache,用来存储之前查询过的目录项。

一般来说,dentry有很大的机率被同时访问。并发访问的时候,为了维护dentry结构的内部一致性,每次查找子目录项,都需要对父目录加锁。在高并发的情形下,因为'/'之类的目录很容易被访问文,锁冲突的概率比较大,路径查找的性能就会降低。

linux的解决方案是采用RCU算法,dentry的数据结构被设计为读安全的,即在不加锁的情况下读,可能读到老数据,但不会造成飞指针指之类的恶性事故。linux在每个dentry记录一个版本号,在查询之前会将这个版本号记录下来,等查询操作完成之后,再对比一个版本号有无变化,没有变化,说明这次读操作访问的数据结构是一致的,结果有效。

因为每次路径查找,往往都是对最后一个节点进行修改,最容易冲突的dentry节点往往是最不常被修改的,因此这种算法可以比较有效的解决锁冲突问题。不过这里存在一个问题,如果读操作失败该如何处理?难道要接着重试么?这种重试会不会造成死循环?

linux采用的策略是,如果某次读dentry失败,则放弃RCU策略,转为层层加锁策略。另外,假如查询需要下放到文件系统层,linux也会放弃RCU策略,转入加锁、引用计数策略。下面贴一下路径查找的核心代码,具体看一下流程。

static inline int walk_component(struct nameidata *nd, struct path *path,                int follow){        struct inode *inode;        int err;        /*         * "." and ".." are special - ".." especially so because it has         * to be able to know about the current root directory and         * parent relationships.         */        if (unlikely(nd->last_type != LAST_NORM))                return handle_dots(nd, nd->last_type);        err = lookup_fast(nd, path, &inode);        if (unlikely(err)) {                if (err < 0)                        goto out_err;                err = lookup_slow(nd, path);                if (err < 0)                        goto out_err;                inode = path->dentry->d_inode;        }        err = -ENOENT;        if (!inode)                goto out_path_put;        if (should_follow_link(inode, follow)) {                if (nd->flags & LOOKUP_RCU) {                        if (unlikely(unlazy_walk(nd, path->dentry))) {                                err = -ECHILD;                                goto out_err;                        }                }                BUG_ON(inode != path->dentry->d_inode);                return 1;        }        path_to_nameidata(path, nd);        nd->inode = inode;        return 0;out_path_put:        path_to_nameidata(path, nd);out_err:        terminate_walk(nd);        return err;}
walk_compoment完成正是从父目录查询子目录项的功能。我们可以看到,每次内核都会都会尝试用lookup_fast查询dentry中的缓存,看一下是否命中,如果没有命中,则会用lookup_slow下降到文件系统层进行路径查找。之后我们还可以注意到一个细节,就是当遇到符号链接的时候,内核也会调用unlazy_walk函数来终止RCU查找模式。

下面我从lookup_fast中截取一段核心代码 。

        if (nd->flags & LOOKUP_RCU) {                                                               unsigned seq;                                                                       dentry = __d_lookup_rcu(parent, name, &seq, nd->inode);                             if (!dentry)                                                                                goto unlazy;                                                                                                                                                    /*                                                                                   * This sequence count validates that the inode matches                          * the dentry name information from lookup.                                      */                                                                             *inode = dentry->d_inode;                                                       if (read_seqcount_retry(&dentry->d_seq, seq))                                           return -ECHILD;                                                                                                                                         /*                                                                               * This sequence count validates that the parent had no                          * changes while we did the lookup of the dentry above.                          *                                                                               * The memory barrier in read_seqcount_begin of child is                         *  enough, we can use __read_seqcount_retry here.                               */                                                                             if (__read_seqcount_retry(&parent->d_seq, nd->seq))                                     return -ECHILD;                                                         nd->seq = seq;                                                                                                                                                  if (unlikely(d_need_lookup(dentry))) {                                                   goto unlazy;                                                                      }                                                                               path->mnt = mnt;                                                                path->dentry = dentry;                                                          if (unlikely(!__follow_mount_rcu(nd, path, inode)))                                     goto unlazy;                                                            if (unlikely(path->dentry->d_flags & DCACHE_NEED_AUTOMOUNT))                            goto unlazy;                                                            return 0;                                                       unlazy:                                                                                         if (unlazy_walk(nd, dentry))                                                            return -ECHILD;                                                 } else {                                                                                dentry = __d_lookup(parent, name);                                      }   
这里我们能够更清晰的看到,内核在RCU模式下会不加锁查询dentry 缓存,在非RCU模式下,则会用加锁的方式。当RCU读不成功,则读取结果不靠谱,终止当前查询流程,然后用非RCU的方式从当前进度继续查询。如果查询dentry 缓存找不到所需目录项,则会调用unlazy_walk,在当前查询流程里直接转入非RCU模式。