VFS directory entry cache(dcache)

808 阅读13分钟

基于 linux 2.6.24


dentry缓存的作用

dentry,即directory entry,目录项缓存。

dentry要解决的是路径查找问题。 我们通过文件路径去访问一个文件,进行文件数据或元数据的读写操作,VFS首先要通过路径解析,找到文件对应的inode。如果没有dentry缓存,那每次路径解析都要根据去读取父目录inode在磁盘上的数据块,拿到父目录的内容,从中找到子目录(文件)的inode,然后依次重复这个过程,最后拿到目标文件的inode进行操作。

本质上,dentry缓存提供了根据name(path)快速找到inode的途径。

struct dentry

struct dentry {
    atomic_t d_count;
    unsigned int d_flags;        /* protected by d_lock */
    spinlock_t d_lock;        /* per dentry lock */
    struct inode *d_inode;        /* Where the name belongs to - NULL is negative */
    /*
     * The next three fields are touched by __d_lookup.  Place them here
     * so they all fit in a cache line.
     */
    struct hlist_node d_hash;    /* lookup hash list */
    struct dentry *d_parent;    /* parent directory */
    struct qstr d_name;

    struct list_head d_lru;        /* LRU list */
    /*
     * d_child and d_rcu can share memory
     */
    union {
        struct list_head d_child;    /* child of parent list */
         struct rcu_head d_rcu;
    } d_u;
    struct list_head d_subdirs;    /* our children */
    struct list_head d_alias;    /* inode alias list */
    unsigned long d_time;        /* used by d_revalidate */
    struct dentry_operations *d_op;
    struct super_block *d_sb;    /* The root of the dentry tree */
    void *d_fsdata;            /* fs-specific data */
#ifdef CONFIG_PROFILING
    struct dcookie_struct *d_cookie; /* cookie, if any */
#endif
    int d_mounted;
    unsigned char d_iname[DNAME_INLINE_LEN_MIN];    /* small names */
};
  • d_count,dentry引用计数,初始为1,通过d_get和d_put来操作
  • d_flags,dentry flags,由d_lock保护
  • d_lock,保护dentry中的各个成员
  • d_inode,指向dentry所属的inode,必定非空
  • d_hash,通过这个字段挂到全局dentry hash 表中,用于dentry的查找(TODO)
  • d_parent,指向父dentry,如果当前是root dentry,则此字段为空
  • d_name,dentry name,qstr类型,里面包含name,len,hash
  • d_lru,通过这个字段挂到LRU链表中,用于dentry的淘汰
  • d_child,子目录/文件通过这个字段挂到父目录dentry项的d_subdirs字段上,dcache_readdir会遍历它
  • d_rcu
  • d_subdirs,子目录/文件链表
  • d_alias,在d_instantiate中,dentry通过这个字段挂到关联的inode->dentry链表中
  • d_time
  • d_op,dentry操作集
  • d_sb,当前文件系统的super blob指针
  • d_fsdata,具体文件系统的私有数据,如autofs会将autofs_info结构的私有数据挂到此字段上
  • d_cookie
  • d_mounted,如果当前目录是其他文件系统的挂载点,此字段记录了挂载的数量(TODO)
  • d_iname,如果dentry name长度不超过DNAME_INLINE_LEN-1,则dentry->d_name.name字段直接使用d_iname字段的内存,否则通过kmalloc重新分配

qstr是quick string的缩写。qstr里的name只存储路径的最后一个分量,即basename,比如 /var/log/messages,只会存放messages。如果路径名比较短,就存放在d_iname中。

内核为了根据name快速查找到dentry,提供了hash表,d_hash会将dentry放置在hash表中的某个头结点所在的链表。__d_lookup提供了dentry hash表的查找。

但是hash值并不是简单地根据目录/文件的basename来计算,否则会有大量的冲突,系统内相同名字的文件就会产生hash冲突。因此,计算hash的时候,将父目录的dentry地址也加入了hash计算当中,影响hash计算结果,这大大降低了碰撞几率。

也就是说,一个dentry的hash值,取决于两个值:父目录dentry的地址和该dentry路径的basename。

static inline struct hlist_head *d_hash(struct dentry *parent,
                    unsigned long hash)
{
    hash += ((unsigned long) parent ^ GOLDEN_RATIO_PRIME) / L1_CACHE_BYTES;
    hash = hash ^ ((hash ^ GOLDEN_RATIO_PRIME) >> D_HASHBITS);
    return dentry_hashtable + (hash & D_HASHMASK);
}

为了避免重复计算hash值带来的开销,qstr结构中有一个hash字段,保存了dentry的hash值,通过稍微牺牲点空间来换取时间性能。

/*
* "quick string" -- eases parameter passing, but more importantly
* saves "metadata" about the string (ie length and the hash).
*
* hash comes first so it snuggles against d_parent in the
* dentry.
*/
struct qstr {
    unsigned int hash;
    unsigned int len;
    const unsigned char *name;
};

dentry_operations

fs/dcache.c中定义了各种各样的dentry操作函数,但为什么struct dentry中还提供了dentry_oprations结构字段呢?原因在于,具体的文件系统会扩展dentry结构,比如利用dentry.d_fsdata字段挂一些私有信息,这些私有信息在struct dentry释放时也需要被释放;另外,dentry的某些行为需要定制,因此需要额外提供dentry_operations结构。

dentry_operation结构描述了一个具体文件系统对于标准dentry operation的重载实现。dentry和dcache是VFS和各个文件系统实现的域。设备驱动与此无关。这些方法可以被设置为NULL,因为它们要么是可选的,要么VFS使用默认实现。从内核2.6.22开始,dentry_oprations结构定义了如下成员:

struct dentry_operations {
    int (*d_revalidate)(struct dentry *, struct nameidata *);
    int (*d_hash) (struct dentry *, struct qstr *);
    int (*d_compare) (struct dentry *, struct qstr *, struct qstr *);
    int (*d_delete)(struct dentry *);
    void (*d_release)(struct dentry *);
    void (*d_iput)(struct dentry *, struct inode *);
    char *(*d_dname)(struct dentry *, char *, int);
};
  • d_revalidate:

当VFS需要revalidate一个dentry(使dentry重新生效)时被调用。每当lookup流程在dcache中找到dentry时,就会调用这个接口。大多数文件系统将其保留为NULL,因为它们在dcache中的所有dentry都是有效的。

  • d_hash

当VFS向dentry hash table中添加一个dentry时调用。

  • d_compare:

两个dentry互相比较时调用。

d_compare比较的是name。在d_lookup时,如果在dentry hash表中找到对应的dentry,则通过d_compare来比较dentry.qstr.name和传入的qstr.name,比如某些文件系统不区分大小写,则可以在d_compare中制定特殊的比较规则,否则默认的compare是case sensitive的memcmp。如果返回0,说明compare一致。

  • d_delete:
  • 删除dentry的最后一个引用时(dput中)调用。这意味着没有人在使用dentry,但dentry仍然是有效的,并且在dcache中。

如果d_delete返回1,表明dentry为unhashed。

  • d_release:

当dentry真正被释放时调用。

对应d_free。

  • d_iput:

释放dentry关联的inode时被调用。当这个字段为NULL时,VFS会调用iput()。如果某个文件系统定义了d_iput,则文件系统必须自己调用iput()。

对应dentry_iput。

  • d_dname:

当应该生成dentry的路径名时调用(d_path)。对于一些伪文件系统(sockfs,pipefs,...)来说很有用(这些文件系统从来不会被挂载,也不支持lookup,这种文件系统的d_path无法通过向上回溯到根目录的方式来填充full path),可以延迟路径名的生成(不是在创建dentry时生成,而只在需要路径名时生成)。真正的文件系统可能不希望使用它,因为它们的dentry存在于全局的dcache哈希中,所以它们的hash值应该是一个不变量。由于没有持有锁,d_name()不应该尝试修改dentry本身,除非使用了适当的SMP安全机制。

注意:d_path()逻辑相当复杂,例如,返回"Hello"的正确方法是将它放在缓冲区的末尾,并返回指向第一个字符的指针。dnamic_dname()函数就是用来帮助解决这个问题。举例来说:

static char *pipefs_dname(struct dentry *dent, char *buffer, int buflen)
{
    return dynamic_dname(dentry, buffer, buflen, "pipe:[%lu]", dentry->d_inode->i_ino);
}

每个dentry都有一个指向父dentry的指针,以及一个子dentry的hash list。子dentry基本上就像目录中的文件。


dcache相关接口

d_alloc

struct dentry *d_alloc(struct dentry * parent, const struct qstr *name);

创建一个dentry。

  • 通过kmem_cache_alloc,从dentry_cache(kmem_cache*)中分配dentry结构体

  • 引用计数d_count字段设置为1

  • d_flags字段设置为DCACHE_UNHASHED

  • d_parent字段设置为入参parent(如果parent非空的话),同时增加parent的引用计数。对于root dentry,parent为空

  • d_sb字段设置为parent->d_sb

  • 如果parent非空,将dentry通过d_child字段挂到parent->d_subdirs

d_alloc_root

struct dentry * d_alloc_root(struct inode * root_inode);

为一个文件系统分配一个root dentry。

d_alloc_root只是对d_alloc的封装,一般是在文件系统mount流程中被调用(get_sb -> xx_fill_super -> d_alloc_root)。

d_free

static void d_free(struct dentry *dentry);

将dcache中不使用的dentry对象释放回dentry_cache slab分配器缓存。

d_free()首先调用dentry对象操作方法中的d_release()函数(如果定义了的话),通常在d_release()函数中释放dentry->fsdata数据。然后,用dname_external()函数判断是否已经为目录项名字d_name分配了内存,如果是,则调用kfree()函数释放d_name所占用的内存。接下来,调用 kmem_cache_free()函数释放这个dentry对象。

d_path

char * d_path(struct dentry *dentry, struct vfsmount *vfsmnt, char *buf, int buflen)

获取dentry对应的full path,写入到buf中。某些文件系统从来不会被挂载,这些文件系统不支持lookup操作,也不需要dentry name。因此对于这些文件系统,需要它们自己提供自己的d_name来实现d_path功能。对于普通文件系统,则通过内部的__d_path接口,从当前dentry,往上回溯直到root dentry,来获取full path。


dcache

我们所说的dcache,指的是目录项高速缓存,主要是用于高效处理路径解析查找,它实际上由两部分组成:dentry_hashtable哈希表和dentry_unused链表。

dentry_hashtable

dentry_hashtable是dcache的全局哈希表,用于dentry的快速lookup。dentry对象通过d_hash域链到dentry_hashtable中。

  1. dentry_hashtable的定义

dentry全局hash表定义在dcache.c中:

static struct hlist_head *dentry_hashtable __read_mostly;

__read_mostly宏表示此处会经常被读取,内核加载时将其存放在cache中。

关于__read_mostly,其原型在include/asm/cache.h中:

#define __read_mostly __attribute__((__section__(".data.read_mostly")))

__read_mostly修饰的变量均放在.data.read_mostly段中。

  1. dentry_hashtable的初始化

dentry_hashtable的初始化在dcache_init()和dcache_init_early()中:

static void __init dcache_init_early(void)
{
    int loop;

    /* If hashes are distributed across NUMA nodes, defer
     * hash allocation until vmalloc space is available.
     */
    if (hashdist)
        return;

    dentry_hashtable =
        alloc_large_system_hash("Dentry cache",
                    sizeof(struct hlist_head),
                    dhash_entries,
                    13,
                    HASH_EARLY,
                    &d_hash_shift,
                    &d_hash_mask,
                    0);


    for (loop = 0; loop < (1 << d_hash_shift); loop++)
        INIT_HLIST_HEAD(&dentry_hashtable[loop]);
}

static void __init dcache_init(void)
{
    int loop;

    /*
     * A constructor could be added for stable state like the lists,
     * but it is probably not worth it because of the cache nature
     * of the dcache.
     */
    dentry_cache = KMEM_CACHE(dentry,
        SLAB_RECLAIM_ACCOUNT|SLAB_PANIC|SLAB_MEM_SPREAD);
    
    register_shrinker(&dcache_shrinker);

    /* Hash may have been set up in dcache_init_early */
    if (!hashdist)
        return;

    dentry_hashtable =
        alloc_large_system_hash("Dentry cache",
                    sizeof(struct hlist_head),
                    dhash_entries,
                    13,
                    0,
                    &d_hash_shift,
                    &d_hash_mask,
                    0);

    for (loop = 0; loop < (1 << d_hash_shift); loop++)
        INIT_HLIST_HEAD(&dentry_hashtable[loop]);
}
  1. d_hash

d_hash函数用来获取dentry在全局dentry_hashtable中的对应位置的头结点:

static inline struct hlist_head *d_hash(struct dentry *parent,
                    unsigned long hash)
{
    hash += ((unsigned long) parent ^ GOLDEN_RATIO_PRIME) / L1_CACHE_BYTES;
    hash = hash ^ ((hash ^ GOLDEN_RATIO_PRIME) >> D_HASHBITS);
    return dentry_hashtable + (hash & D_HASHMASK);
}

d_hash()函数需要两个参数,一个是父dentry地址,一个是文件名的hash(往往存放qstr中)。以_link_path_work() 函数为例,hash值的计算过程如下:

struct qstr this;
unsigned int c;

hash = init_name_hash();
do {
    name++;
    hash = partial_name_hash(c, hash);
    c = *(const unsigned char *)name;
} while (c && (c != '/'));
this.len = name - (const char *) this.name;
this.hash = end_name_hash(hash);

核心逻辑是,通过partial_name_hash,对name中的每个字符,迭代计算hash值。

  1. d_rehash

d_rehash()函数将dentry重新加到dentry_hashtable,并清除dentry->d_flags上的DCACHE_UNHASHED标记。

void d_rehash(struct dentry * entry)
{
    spin_lock(&dcache_lock);
    spin_lock(&entry->d_lock);
    _d_rehash(entry);
    spin_unlock(&entry->d_lock);
    spin_unlock(&dcache_lock);
}

static void __d_rehash(struct dentry * entry, struct hlist_head *list)
{
     entry->d_flags &= ~DCACHE_UNHASHED;
     hlist_add_head_rcu(&entry->d_hash, list);
}

static void _d_rehash(struct dentry * entry)
{
    __d_rehash(entry, d_hash(entry->d_parent, entry->d_name.hash));
}
  1. d_drop

d_drop()函数将dentry从dentry_hashtable中删除,同时给dentry->d_flags打上DCACHE_UNHASHED标记。这样VFS lookup流程就无法从dentry_hashtable中查找到。

static inline void __d_drop(struct dentry *dentry)
{
    if (!(dentry->d_flags & DCACHE_UNHASHED)) {
        dentry->d_flags |= DCACHE_UNHASHED;
        hlist_del_rcu(&dentry->d_hash);
    }
}

static inline void d_drop(struct dentry *dentry)
{
    spin_lock(&dcache_lock);
    spin_lock(&dentry->d_lock);
     __d_drop(dentry);
    spin_unlock(&dentry->d_lock);
    spin_unlock(&dcache_lock);
}

dentry_unused

dentry_unused链表,记录了处于unused状态的dentry。

对于unused状态的dentry,它们被再次访问的可能性比较大,因此,在内存足够的情况下,可以不用立即将它们释放。VFS通过定义dentry_unused链表来保存这些dentry,每一个unused dentry通过其d_lru字段链入全局的dentry_unused链表。

  1. dentry_unused定义

dentry_unused链表定义在dcache.c里面:

static LIST_HEAD(dentry_unused);

LIST_HEAD宏定义在include/linux/list.h里面,实际上就是定义并初始化了一个list_head类型的链表头。

  1. unused dentry的添加

1)dput()

在dput中,如果dentry->d_count减至0,并且dentry在dcache hash表中,则将dentry添加到dentry_unused里面,同时增加unused计数。

2)select_parent()

3)prune_dcache()

  1. unused dentry的删除

当内存紧张时,会触发dcache shrink。这会释放dentry_unused链表中的一些dentry,通常是释放LRU尾部的对象,但也可以根据指定条件来选择释放的dentry对象,因此在这之前要做一个挑选过程,并由这一过程将所选中的dentry对象移到dentry_unused链表的尾部。

1)prune_one_dentry()

该函数实现从LRU链表中释放一个指定的dentry对象。这是一个静态的内部函数,它通常被别的函数调用。注意, prune_one_dentry()函数假定被调用之前,调用者已经将dentry对象从LRU链表中摘除,并且持有自旋锁dcache_lock。因此,它所要做的事情就是:①将这个dentry对象从哈希链表中摘除;②将这个dentry对象从其父目录对象的d_subdirs链表中摘除;③用 dentry_iput()函数释放对相应inode对象的引用;④用d_free()释放这个dentry对象;⑤对父目录dentry对象做一次 dput操作。

2)prune_dcache()

该函数用于实现从LRU链表的尾部开始倒序释放指定个数的dentry对象。它从尾部开始扫描LRU链表,如果被扫描的dentry对象设置了DCACHE_REFERENCED标志,则让其继续留在LRU链表中,否则就将其从LRU链表中摘除,然后调用prune_one_dentry()函数释放该dentry对象。

该函数用于实现从LRU链表的尾部开始倒序释放指定个数的dentry对象。它从尾部开始扫描LRU链表,如果被扫描的dentry对象设置了DCACHE_REFERENCED标志,则让其继续留在LRU链表中,否则就将其从LRU链表中摘除,然后调用prune_one_dentry()函数释放该dentry对象。

上述两个函数prune_one_dentry()和prune_dcache()是dcache的shrink机制的实现基础。在此基础上,Linux实现了根据指定条件压缩dcache的高层接口函数:①shink_dcache_sb()——根据指定的超级块对象,压缩dcache;②shrink_dcache_parent()——根据指定的父目录dentry对象,压缩dcache;③shrink_dcache_memory()——根据优先级压缩dcache。

3)shrink_dcache_sb()

该函数释放dcache的LRU链表中属于某个特定超级块对象的dentry对象。该函数的实现过程主要是两次遍历dentry_unused链表:

①第一次遍历过程将属于指定超级块对象的dentry对象移到dentry_unused链表的首部。

②第二次遍历则将属于指定超级块对象、且d_count=0的dentry对象释放掉(通过prune_one_dentry函数)。

4)shrink_dcache_parent()

该函数释放LRU链表中属于给定父目录对象的子dentry对象。

shrink_dcache_parent()函数首先通过调用 select_parent()函数来从LRU链表中查找父目录parent的子目录对象,并将这些子dentry对象移到LRU链表的尾部,并返回所找到的子dentry对象的个数(这一步是为调用prune_dcache()函数做准备的);然后,调用prune_dcache()函数将LRU链表尾部的子dentry对象释放掉。

函数select_parent()是在dcache.c中实现的内部函数,他根据给定的参数parent,在LRU链表中查找父目录parent的子目录对象,并将这些子dentry对象移到LRU链表的尾部,并返回所找到的子dentry对象的个数。

5)shringk_dcache_memory()

当我们需要内存,但又不知道具体需要多少时,就可以调用这个函数来压缩dcache所占用的内存。该函数通常被kswapd守护进程所调用。

优先级参数priority值越大(优先级越低),表明对内存的需要就越不迫切。因此prune_dcache()函数释放的dentry对象个数就越少。

  1. unused dentry的reuse

// TODO


dentry的状态

dentry的状态有以下三种:

  1. unused

该dentry对象的引用计数d_count为0,但其d_inode仍然指向对应的inode节点。该目录项仍然包含有效信息,只是当前没人引用它。这种dentry在系统内存紧张时可能会被回收释放。

  1. inuse

处于该状态下的dentry对象的引用计数d_count大于0,且其d_inode指针指向对应的inode对象。这种dentry对象不会被释放。

  1. negative

与该状态的dentry相关的inode对象已不复存在(相应的磁盘索引节点可能已经被删除),dentry对象的d_inode指针为NULL。这种dentry对象仍然保存在dcache hashtable中,以便后续对此文件名的查找能够快速完成。这种dentry对象在系统内存紧张时会被优先回收释放。


dentry flags

// TODO

/* d_flags entries */
#define DCACHE_AUTOFS_PENDING 0x0001    /* autofs: "under construction" */
#define DCACHE_NFSFS_RENAMED  0x0002    /* this dentry has been "silly
                     * renamed" and has to be
                     * deleted on the last dput()
                     */
#define    DCACHE_DISCONNECTED 0x0004
     /* This dentry is possibly not currently connected to the dcache tree,
      * in which case its parent will either be itself, or will have this
      * flag as well.  nfsd will not use a dentry with this bit set, but will
      * first endeavour to clear the bit either by discovering that it is
      * connected, or by performing lookup operations.   Any filesystem which
      * supports nfsd_operations MUST have a lookup function which, if it finds
      * a directory inode with a DCACHE_DISCONNECTED dentry, will d_move
      * that dentry into place and return that dentry rather than the passed one,
      * typically using d_splice_alias.
      */

#define DCACHE_REFERENCED    0x0008  /* Recently used, don't discard. */
#define DCACHE_UNHASHED        0x0010    

#define DCACHE_INOTIFY_PARENT_WATCHED    0x0020 /* Parent inode is watched */