基于 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中。
- 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段中。
- 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]);
}
- 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值。
- 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));
}
- 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链表。
- dentry_unused定义
dentry_unused链表定义在dcache.c里面:
static LIST_HEAD(dentry_unused);
LIST_HEAD宏定义在include/linux/list.h里面,实际上就是定义并初始化了一个list_head类型的链表头。
- unused dentry的添加
1)dput()
在dput中,如果dentry->d_count减至0,并且dentry在dcache hash表中,则将dentry添加到dentry_unused里面,同时增加unused计数。
2)select_parent()
3)prune_dcache()
- 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对象个数就越少。
- unused dentry的reuse
// TODO
dentry的状态
dentry的状态有以下三种:
- unused
该dentry对象的引用计数d_count为0,但其d_inode仍然指向对应的inode节点。该目录项仍然包含有效信息,只是当前没人引用它。这种dentry在系统内存紧张时可能会被回收释放。
- inuse
处于该状态下的dentry对象的引用计数d_count大于0,且其d_inode指针指向对应的inode对象。这种dentry对象不会被释放。
- 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 */