注:本文分析基于linux-4.18.0-193.14.2.el8_2内核版本,即CentOS 8.2

1 dentry

在内存中,每个文件除了有inode,同时也会有一个dentry结构。它记录文件的名称,父目录,子目录等信息,形成我们看到的层级树状结构。与inode不同的是,dentry只存在于内存,磁盘上并没有对应的实体文件,因此目录项也就不会涉及回写磁盘。

2 dentry主要成员变量

struct dentry {
	/* RCU lookup touched fields */
	unsigned int d_flags;		/* protected by d_lock */
	seqcount_t d_seq;		/* per dentry seqlock */
	struct hlist_bl_node d_hash; //通过该变量挂载到全局dentry哈希表dentry_hashtable上
	struct dentry *d_parent;	//父目录的目录项对象
	struct qstr d_name;         //目录项名称
	struct inode *d_inode;		//指向目录对应的inode结构
	unsigned char d_iname[DNAME_INLINE_LEN];	//存放短文件名

	struct lockref d_lockref;	//使用计数
	const struct dentry_operations *d_op; //目录项的操作函数
	struct super_block *d_sb;	//目录项对应的超级块
	unsigned long d_time;		/* used by d_revalidate */
	void *d_fsdata;			//文件系统私有数据

	union {
		struct list_head d_lru;		//通过该变量链接到超级块的s_dentry_lru链表
		wait_queue_head_t *d_wait;	/* in-lookup ones only */
	};
	struct list_head d_child;	//通过该变量链接到父目录的d_subdirs中
	struct list_head d_subdirs;	//本目录所有子目录链表

	union {
		//通过该变量链接到inode结构的i_dentry中,
		//一个inode可对应多个dentry,因为存在软链接
		struct hlist_node d_alias;	
		struct hlist_bl_node d_in_lookup_hash;	/* only for in-lookup ones */
	 	struct rcu_head d_rcu;
	} d_u;
};

3 创建一个dentry

我们仍旧以以ext4的创建目录来大概分析下整个过程,
我们从系统调用开始,在上一篇文章——文件系统之inode中,我们分析的是mkdir的过程,其实在此之前需要创建dentry对象。

SYSCALL_DEFINE2(mkdir, const char __user *, pathname, umode_t, mode)
{
	return do_mkdirat(AT_FDCWD, pathname, mode);
}

long do_mkdirat(int dfd, const char __user *pathname, umode_t mode)
{
	...
	//为该目录创建dentry
	dentry = user_path_create(dfd, pathname, &path, lookup_flags);
	...
	if (!error)
		//调用对应文件系统的mkdir为该目录创建inode,对于ext4调用的是ext4_mkdir
		error = vfs_mkdir(path.dentry->d_inode, dentry, mode);
	...
}

user_path_create通过用户传入的文件路径创建对应的dentry,但是需要查看下系统中是否已存在该目录,查询前需要获取父目录信息,

inline struct dentry *user_path_create(int dfd, const char __user *pathname,
				struct path *path, unsigned int lookup_flags)
{
	return filename_create(dfd, getname(pathname), path, lookup_flags);
}

static struct dentry *filename_create(int dfd, struct filename *name,
				struct path *path, unsigned int lookup_flags)
{
	//查找父目录文件对象,因为新建的目录还不存在,我们需要找到其父目录
	//然后将新建的目录关联到父目录,形成树形结构
	name = filename_parentat(dfd, name, lookup_flags, path, &last, &type);
	...
	//分别在dentry哈希表和磁盘中查找目标dentry,没有则创建新的dentry
	//对于新建的操作,此时还没有建立该dentry对应的inode对象
	dentry = __lookup_hash(&last, path->dentry, lookup_flags);
	...
}

目标目录的查询主要通过__lookup_hash,现在全局哈希表中查询,查不到则到磁盘中查,对于新建目录肯定是查不到的,因此返回的只有一个dentry对象,还没有填充对应的inode结构,这个就需要回到最开始的vfs_mkdir,对于ext4也就是ext4_mkdir里创建inode并关联到该dentry。

static struct dentry *__lookup_hash(const struct qstr *name,
		struct dentry *base, unsigned int flags)
{
	//从dentry hash table上查询目标dentry
	struct dentry *dentry = lookup_dcache(name, base, flags);
	struct dentry *old;
	struct inode *dir = base->d_inode;
	//hash table上有,直接返回
	if (dentry)
		return dentry;

	/* Don't create child dentry for a dead directory. */
	if (unlikely(IS_DEADDIR(dir)))
		return ERR_PTR(-ENOENT);
	//哈希表没查到,从dentry slab缓存中获取空闲对象,创建新的dentry
	dentry = d_alloc(base, name);
	if (unlikely(!dentry))
		return ERR_PTR(-ENOMEM);
	//调用对应文件系统的lookup函数,到磁盘中查找目标目录项
	//对于新建的操作,显然磁盘中是查不到的
	old = dir->i_op->lookup(dir, dentry, flags);
	//如果查到了就释放刚才创建的dentry,返回找到的这个
	if (unlikely(old)) {
		dput(dentry);
		dentry = old;
	}
	return dentry;
}

在分配dentry对象时,使用的是slab缓存,用以提高分配效率。分配后需要将该dentry的d_parent指向刚才查找到的父目录,同时将d_child挂到父目录的d_subdirs链表,这样由父子目录一层层堆叠,就形成了树形结构。

struct dentry *d_alloc(struct dentry * parent, const struct qstr *name)
{
	//从对应的超级块的slab缓存中分配空闲dentry
	struct dentry *dentry = __d_alloc(parent->d_sb, name);
	if (!dentry)
		return NULL;
	dentry->d_flags |= DCACHE_RCUACCESS;
	spin_lock(&parent->d_lock);

	__dget_dlock(parent);
	//对新目录项设置父目录项
	dentry->d_parent = parent;
	//新目录项的d_child挂到父目录的d_subdirs链表
	list_add(&dentry->d_child, &parent->d_subdirs);
	spin_unlock(&parent->d_lock);

	return dentry;
}

struct dentry *__d_alloc(struct super_block *sb, const struct qstr *name)
{
	struct dentry *dentry;
	char *dname;
	int err;
	//从slab缓存中分配dentry对象
	dentry = kmem_cache_alloc(dentry_cache, GFP_KERNEL);
	//对dentry进行各个变量的初始化
	...
	return dentry;
}

4 添加dentry到dentry cache中

我们知道超级块上有用于管理未使用的inode的LRU链表s_inode_lru,同样的,对于dentry,超级块上也有对应的LRU链表s_dentry_lru,同样也是用于管理未使用的dentry,其实也就是使用完的对象,在内存不足时,就会回收这里的对象。

主要的路径就是dput,在使用完dentry后减少对其的引用,如果引用计数大于1,就比较简单,减少引用就可以返回。

void dput(struct dentry *dentry)
{
	while (dentry) {
		might_sleep();

		rcu_read_lock();
		//如果该dentry引用计数大于1,则减少引用计数后直接返回
		if (likely(fast_dput(dentry))) {
			rcu_read_unlock();
			return;
		}

		/* Slow case: now with the dentry lock held */
		rcu_read_unlock();
		//如果该dentry引用计数小于等于1,判断是否要直接回收
		if (likely(retain_dentry(dentry))) {
			spin_unlock(&dentry->d_lock);
			return;
		}
		//走到这说明这个dentry可以回收了
		dentry = dentry_kill(dentry);
	}
}

但对于引用计数小于等于1时,还需要进一步判断,如果dentry不在哈希表,或者该dentry有自己的d_delete方法,此时可以对其进行回收,否则就先将其加入超级块的s_dentry_lru链表,等待内存回收时再将其释放


static inline bool retain_dentry(struct dentry *dentry)
{
	WARN_ON(d_in_lookup(dentry));
	//dentry已不在全局hash表,直接返回,后续调用dentry_kill删除该dentry
	if (unlikely(d_unhashed(dentry)))
		return false;

	if (unlikely(dentry->d_flags & DCACHE_DISCONNECTED))
		return false;
	//该dentry有d_delete操作函数,则调用,然后返回再dentry_kill删除该dentry
	//常见的文件系统ext4、xfs都没有设置该函数
	if (unlikely(dentry->d_flags & DCACHE_OP_DELETE)) {
		if (dentry->d_op->d_delete(dentry))
			return false;
	}
	/* retain; LRU fodder */
	dentry->d_lockref.count--;
	//不属于以上情况,那就先放超级块的s_dentry_lru链表
	if (unlikely(!(dentry->d_flags & DCACHE_LRU_LIST)))
		d_lru_add(dentry);
	else if (unlikely(!(dentry->d_flags & DCACHE_REFERENCED)))
		dentry->d_flags |= DCACHE_REFERENCED;
	return true;
}

static void d_lru_add(struct dentry *dentry)
{
	D_FLAG_VERIFY(dentry, 0);
	dentry->d_flags |= DCACHE_LRU_LIST;
	this_cpu_inc(nr_dentry_unused);
	if (d_is_negative(dentry))
		this_cpu_inc(nr_dentry_negative);
	//将dentry挂到对应超级块的s_dentry_lru链表
	WARN_ON_ONCE(!list_lru_add(&dentry->d_sb->s_dentry_lru, &dentry->d_lru));
}

5 从dentry cache中删除dentry(回收dentry cache)

当内存不足时,除了回收inode cache,dentry cache也会被回收,主要通过prune_dcache_sb操作,

long prune_dcache_sb(struct super_block *sb, struct shrink_control *sc)
{
	LIST_HEAD(dispose);
	long freed;
	//遍历s_dentry_lru链表,分离部分dentry到dispose,避免锁竞争
	freed = list_lru_shrink_walk(&sb->s_dentry_lru, sc,
				     dentry_lru_isolate, &dispose);
	//对分离出来的dentry回收
	shrink_dentry_list(&dispose);
	return freed;
}

回收dentry主要是要将dentry从各个链表中摘除,也就是所以引用的地方都要解除,

  • s_dentry_lru链表
  • 全局dentry哈希表
  • 与父目录的关系
  • 关联的inode
  • 关联的inode别名
static void shrink_dentry_list(struct list_head *list)
{
	while (!list_empty(list)) {
		struct dentry *dentry, *parent;

		dentry = list_entry(list->prev, struct dentry, d_lru);
		spin_lock(&dentry->d_lock);
		...
		//将dentry从超级块的s_dentry_lru链表中摘除,
		//并清除DCACHE_SHRINK_LIST和DCACHE_LRU_LIST标志
		d_shrink_del(dentry);
		parent = dentry->d_parent;
		//做进一步清理工作
		__dentry_kill(dentry);
		//如果该dentry是根目录,到此为止
		if (parent == dentry)
			continue;
		//删除dentry后,如果父目录引用计数也小于等于1了,对父目录尝试做回收操作
		dentry = parent;
		while (dentry && !lockref_put_or_lock(&dentry->d_lockref))
			dentry = dentry_kill(dentry);
	}
}

static void __dentry_kill(struct dentry *dentry)
{
	struct dentry *parent = NULL;
	bool can_free = true;
	if (!IS_ROOT(dentry))
		parent = dentry->d_parent;
	//调用dentry的d_prune去unhash dentry
	if (dentry->d_flags & DCACHE_OP_PRUNE)
		dentry->d_op->d_prune(dentry);
	//如果dentry还在s_dentry_lru链表,则摘除
	if (dentry->d_flags & DCACHE_LRU_LIST) {
		if (!(dentry->d_flags & DCACHE_SHRINK_LIST))
			d_lru_del(dentry);
	}
	/* if it was on the hash then remove it */
	//对于没有d_prune函数的dentry,会在这里从哈希表里删除
	__d_drop(dentry);
	//解除和父目录的关系,d_child和d_child之间的关系
	dentry_unlist(dentry, parent);
	if (parent)
		spin_unlock(&parent->d_lock);
	if (dentry->d_inode)
		//解除和对应inode的关联
		dentry_unlink_inode(dentry);
	else
		spin_unlock(&dentry->d_lock);
	this_cpu_dec(nr_dentry);
	//如果有d_release则调用
	if (dentry->d_op && dentry->d_op->d_release)
		dentry->d_op->d_release(dentry);

	spin_lock(&dentry->d_lock);
	if (dentry->d_flags & DCACHE_SHRINK_LIST) {
		dentry->d_flags |= DCACHE_MAY_FREE;
		can_free = false;
	}
	spin_unlock(&dentry->d_lock);
	if (likely(can_free))
		//来到最后一站,就可以解脱了
		dentry_free(dentry);
	cond_resched();
}

static void dentry_free(struct dentry *dentry)
{
	//解除d_alias上的映射,如果它是某个inode的别名
	WARN_ON(!hlist_unhashed(&dentry->d_u.d_alias));
	...
	//该处理的资源都处理了,可以走了
	if (!(dentry->d_flags & DCACHE_RCUACCESS))
		__d_free(&dentry->d_u.d_rcu);
	else
		call_rcu(&dentry->d_u.d_rcu, __d_free);
}

所有地方都抽身后,就可以释放回slab缓存了,

static void __d_free(struct rcu_head *head)
{
	struct dentry *dentry = container_of(head, struct dentry, d_u.d_rcu);
	//秉承着从哪里来回哪里去的原则,dentry被释放回slab缓存
	kmem_cache_free(dentry_cache, dentry); 
}

6 结构关系

一个路径的各个组成部分,不管是目录还是普通的文件,都是一个dentry对象。我们以/home/test.c为例,/,home,test.c这三个都是一个目录项。

  • 超级块的s_root指向当前文件系统根目录“/”
  • 根目录的d_parent指向自己
  • home目录和var目录的d_parent指向根目录
  • test.c文件的d_parent指向home目录
  • home目录和var目录通过d_child链接到根目录的d_subdirs
  • test.c文件通过d_child链接到home目录的d_subdirs,由此形成树形结构
  • 根目录,home目录,var目录以及test.c文件的d_hash都指向全局目录哈希表dentry_hashtable
  • 使用完被删除资源的deleted目录项通过d_lru链接到超级块的s_dentry_lru链表,而根目录,home目录,var目录以及test.c文件在使用中,不会链接到该链表
  • 根目录,home目录,var目录,test.c文件以及deleted目录项d_sb都指向超级块
    在这里插入图片描述
Logo

更多推荐