本文仅作个人知识梳理,所述并不细致。作者对ocfs2文件系统较为熟悉,若涉及到具体的文件系统文中将以ocfs2为例进行描述。
为什么要把文件打开流程和文件系统的挂载放在一起梳理。因为要了解清楚文件打开流程,我们就需要了解如何如何判断一个文件所在的文件系统。而这样我们就知道文件系统挂载流程要达到的目的,从而能带着目的去分析挂载流程。
1. 文件的打开
1.1 打开文件的大致流程
打开文件时,调用open函数传入一个文件路径字符串,返回一个int类型文件描述符fd。
int fd = open(const char *pathname, int flags, mode_t mode);
整体流程大致如下:
- 从当前进程的task_struct对象上找到其对应的files_struct对象,从该对象的fdtab(struct fdtable)中分配一个空闲的文件描述符(struct fdtable中的成员fd是一个file类型的数组,表示一个进程打开的多个文件);
- 解析文件路径,以“/”将路径拆分,层层查找,每一层目录都创建对应的dentry和inode(需要读取磁盘上的dinode初始化inode)并将他们插入到哈希表dentry_hashtable和inode_hashtable(如果缓存中能找到,就不必再创建)。无缓存场景,要查找当前目录下的子目录和文件,需调用目录inode的lookup()函数读取磁盘上的目录项ocfs2_dentry(包含文件名和inode号),查找到目录子目录或文件,再创建dentry和inode并。
- 当最终查找到目标文件,也创建对应dentry对象和inode对象,将dentry和inode关联(inode有个字段可以连接多个dentry,满足硬链接需要;dentry有个字段指向inode);分配file对象,将inode和inode->i_fop赋值给file对象相应的字段(f->f_inode, f->fop)。
- 将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); // 加入到父节点的链表中
}