打开文件的流程和文件系统的挂载

本文仅作个人知识梳理,所述并不细致。作者对ocfs2文件系统较为熟悉,若涉及到具体的文件系统文中将以ocfs2为例进行描述。

为什么要把文件打开流程和文件系统的挂载放在一起梳理。因为要了解清楚文件打开流程,我们就需要了解如何如何判断一个文件所在的文件系统。而这样我们就知道文件系统挂载流程要达到的目的,从而能带着目的去分析挂载流程。

1. 文件的打开

1.1 打开文件的大致流程

打开文件时,调用open函数传入一个文件路径字符串,返回一个int类型文件描述符fd。

int fd = open(const char *pathname, int flags, mode_t mode);

整体流程大致如下:

  1. 从当前进程的task_struct对象上找到其对应的files_struct对象,从该对象的fdtab(struct fdtable)中分配一个空闲的文件描述符(struct fdtable中的成员fd是一个file类型的数组,表示一个进程打开的多个文件);
  2. 解析文件路径,以“/”将路径拆分,层层查找,每一层目录都创建对应的dentry和inode(需要读取磁盘上的dinode初始化inode)并将他们插入到哈希表dentry_hashtable和inode_hashtable(如果缓存中能找到,就不必再创建)。无缓存场景,要查找当前目录下的子目录和文件,需调用目录inode的lookup()函数读取磁盘上的目录项ocfs2_dentry(包含文件名和inode号),查找到目录子目录或文件,再创建dentry和inode并。
  3. 当最终查找到目标文件,也创建对应dentry对象和inode对象,将dentry和inode关联(inode有个字段可以连接多个dentry,满足硬链接需要;dentry有个字段指向inode);分配file对象,将inode和inode->i_fop赋值给file对象相应的字段(f->f_inode, f->fop)。
  4. 将file指针关联到task_struct中文件描述符所在的fdtab中。

1.2 打开文件的本质是什么

对于内核来说,就是分配一个file对象代表目标文件且file对象包含操作该文件的操作函数集,file对象关联到打开该文件的进程的结构体task_struct。对于用户态而言,就是获取到这个file对象对应的文件描述符fd。这样当用户针对该fd进行读、写时,内核就可以调用file对象操作函数集中相应的write、read函数,执行具体文件系统的读、写函数。

1.3 文件描述符和file的关系

file指针其实就是task_struct结构体中的一个数组项。而用户态的文件描述符其实就是数组的下标。这样通过文件描述符就可以很容易到找到file结构体指针,然后通过其中的函数指针访问数据。(下图引用自 书籍《文件系统技术内幕》)

1.4 解析路径时怎么识别和处理挂载点

假设挂载点/mnt/ocfs2_test挂载了ocfs2文件系统,而/mnt所在文件系统为ext4。如果要打开文件/mnt/ocfs2_test/f2,则解析路径的过程中为目录/mnt/ocfs2_test创建的inode应该用ocfs2根目录dinode信息填充。那么如何识别到/mnt/ocfs2_test是ocfs2文件系统的挂载点,然后再进行特殊处理呢?

在处理路径的过程中,函数follow_managed()进行挂载点处理。函数中判断path->dentry->d_flags如果被设置了DCACHE_MOUNTED,则调用lookup_mnt(path)获取该挂载点上的子文件系统vfsmount对象,并用该对象及其对应的根目录dentry对象更新path。这里有个while循环,也就说需要对上一次获取的子文件系统再判断,其是否还有子系统,直到获取到最后一个挂载的vfsmount对象更新path。于是函数follow_managed将返回指定path上最后一个挂载的文件系统的根目录dentry和vfsmount对象。这个dentry关联了挂载文件系统的根目录inode,所以,之后在查找该目录下的文件时,调用inode的lookup函数,进入到所挂载文件系统中查找子目录和文件。

进入路径
fs/namei.c walk_component->follow_managed
fs/namei.c walk_component->lookup_fast->follow_managed

static int follow_managed(struct path *path, struct nameidata *nd)
{
    ......
    while (managed = READ_ONCE(path->dentry->d_flags),
	       managed &= DCACHE_MANAGED_DENTRY,
	       unlikely(managed != 0)) {

        ......

        /* Transit to a mounted filesystem. */
		if (managed & DCACHE_MOUNTED) { // 判断dentry是否为挂载点
			struct vfsmount *mounted = lookup_mnt(path);
			if (mounted) {
				dput(path->dentry);
				if (need_mntput)
					mntput(path->mnt);
				path->mnt = mounted;
				path->dentry = dget(mounted->mnt_root);
				need_mntput = true;
				continue;
			}
		}

    ......

    }

    ......

}

2. ocfs2文件系统的挂载

一个最基本的挂载命令包括文件系统类型、设备路径、挂载点,ocfs2基本的命令如下:

mount.ocfs2 /dev/sdc /mnt/ocfs2_test

执行上述命令后,一个ocfs2文件系统就被挂载到ocfs2_test目录。

2.1 挂载关系图

2.2 通用的挂载流程

mount系统调用的函数原型如下:

mount(char *dev_name, char *dir_name, char *type, unsigned long flags, void *data)

目的:使得打开文件解析路径时,能识别到挂载点并进入到挂载的文件系统;对文件系统上文件的操作能调用到文件系统的操作函数。

忽略mount流程中的细枝末节,mount流程的关键操作有以下几步:

1)根据文件系统类型名称获取file_system_type对象(该对象中有文件系统模块的mount函数指针)

调用路径fs.namespace.c ksys_mount->do_mount->do_new_mount

struct file_system_type *type;
type = get_fs_type(fstype);

2)为本次挂载创建mount对象mnt(包含了vfsmount对象)。

调用路径fs.namespace.c ksys_mount->do_mount->do_new_mount->vfs_kern_mount

struct mount *mnt = alloc_vfsmnt(name);

3)调用特定文件系统模块加载到内核时注册的mount函数,根据传入的设备和文件系统类型名称,从磁盘读取超级块信息和根目录inode信息,完成超级块和根目录(dentry和inode)的初始化。

调用路径fs/super.c ksys_mount->do_mount->do_new_mount->vfs_kern_mount->mount_fs

struct dentry *root = type->mount(type, flags, name, data);

4)将待挂载文件系统的根目录dentry和超级块设置到本次挂载的mount对象mnt中。

调用路径fs/namespace.c ksys_mount->do_mount->do_new_mount->vfs_kern_mount

vfs_kern_mount(struct file_system_type *type, int flags, const char *name, void *data)
{
    mnt = alloc_vfsmnt(name); // 创建mount对象(其中包含vfsmount对象)

    ......

    root = mount_fs(type, flags, name, data); // 调用文件系统注册的mount函数,创建文件系统根目录dentry    
    
    /* 将待挂载文件系统的根目录dentry和超级块设置到本次挂载的mount对象和挂载点 */
    mnt->mnt.mnt_root = root;
	mnt->mnt.mnt_sb = root->d_sb;
	mnt->mnt_mountpoint = mnt->mnt.mnt_root; // 后面会更新为父mount的根目录
	mnt->mnt_parent = mnt; // 后面会更新为父mount
    list_add_tail(&mnt->mnt_instance, &root->d_sb->s_mounts);

    return &mnt->mnt;
} 

5)从当前path对应的mount开始往下级mount对象遍历,找到最底层的mount对象及其根目录的dentry对象,用于与待挂载的mount建立父子关系。

调用路径fs/namespace.c ksys_mount->do_mount->do_new_mount->do_add_mount->lock_mount

static struct mountpoint *lock_mount(struct path *path)
{
	struct vfsmount *mnt;
	struct dentry *dentry = path->dentry;
retry: // 一直循环,直到找到mount树中最底层的mount对象
	inode_lock(dentry->d_inode);
	if (unlikely(cant_mount(dentry))) {
		inode_unlock(dentry->d_inode);
		return ERR_PTR(-ENOENT);
	}
	namespace_lock();
	mnt = lookup_mnt(path); // Return the first child mount mounted at path
	if (likely(!mnt)) { // 没有找到mount对象,说明path->mnt已经是叶子mount了
		struct mountpoint *mp = get_mountpoint(dentry); // 获取挂载点对象并将旗标DCACHE_MOUNTED设置给dentry
		if (IS_ERR(mp)) {
			namespace_unlock();
			inode_unlock(dentry->d_inode);
			return mp;
		}
		return mp;
	}
	namespace_unlock();
	inode_unlock(path->dentry->d_inode);
	path_put(path);
	path->mnt = mnt; // 将子mount对象更新到path中,用于继续找孙子mount对象
	dentry = path->dentry = dget(mnt->mnt_root);
	goto retry;
}

6)找到mount树中的叶子mount对象及其根目录的dentry对象后,创建mountpoint对象,将DCACHE_MOUNTED设置到dentry->d_flags,表示这个dentry是一个挂载点;将dentry设置到mountpoint对象中。

调用路径fs/dcache.c ksys_mount->do_mount->do_new_mount->do_add_mount->lock_mount->d_set_mounted

int d_set_mounted(struct dentry *dentry)
{
	struct mountpoint *mp, *new = NULL;
    ......
    new = kmalloc(sizeof(struct mountpoint), GFP_KERNEL);

	if (!d_unlinked(dentry)) {
		ret = -EBUSY;
		if (!d_mountpoint(dentry)) {
			dentry->d_flags |= DCACHE_MOUNTED; // 设置挂载标志
			ret = 0;
		}
	}
    new->m_dentry = dentry;
	new->m_count = 1;
	hlist_add_head(&new->m_hash, mp_hash(dentry)); // 该mountpoint(new)加入到mountpoint_hashtable。
    
    ......
    return new;
 	
}

// namespace.c
static inline struct hlist_head *mp_hash(struct dentry *dentry)
{
	unsigned long tmp = ((unsigned long)dentry / L1_CACHE_BYTES);
	tmp = tmp + (tmp >> mp_hash_shift);
	return &mountpoint_hashtable[tmp & mp_hash_mask];
}

7)建立本次挂载的mount对象与父mount的父子关系,并将本次挂载的mount对象加入到mount_hashtable中。这样在打开文件解析路径时,识别到挂载点dentry就可以层层遍历mount对象,直到找到最后一个挂载的mount。挂载流程结束。

attach_recursive_mnt->attach_mnt

static void attach_mnt(struct mount *mnt,
			struct mount *parent,
			struct mountpoint *mp)
{
	mnt_set_mountpoint(parent, mp, mnt);
	__attach_mnt(mnt, parent);
}


void mnt_set_mountpoint(struct mount *mnt,
			struct mountpoint *mp,
			struct mount *child_mnt)
{
	mp->m_count++;
	mnt_add_count(mnt, 1);	/* essentially, that's mntget */
	child_mnt->mnt_mountpoint = dget(mp->m_dentry);
	child_mnt->mnt_parent = mnt; // 指向父mount
	child_mnt->mnt_mp = mp; // mp->m_dentry为父mount的根目录dentry
	hlist_add_head(&child_mnt->mnt_mp_list, &mp->m_list); // 加入mountpoint对象所拥有的mount对象链表
}

static void __attach_mnt(struct mount *mnt, struct mount *parent)
{
	hlist_add_head_rcu(&mnt->mnt_hash,
			   m_hash(&parent->mnt, mnt->mnt_mountpoint)); // 将本次挂载的mount对象加入到mount_hashtable中
	list_add_tail(&mnt->mnt_child, &parent->mnt_mounts); // 加入到父节点的链表中
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值