
注册&挂载文件系统
本文是笔者在学习如何自己写一个文件系统过程中学习的内核虚拟文件系统VFS对于注册文件系统和挂载文件系统的笔记,如果有讲的不好的地方请见谅,笔者对于这块只能是有一个大致流程思路和映像,对于具体函数间的调度还不是很熟
注册文件系统
注册文件系统操作为:
1.定义并初始化一个file_system_type结构体
static struct file_system_type myfs_type = {
.owner = THIS_MODULE,
.name = "myfs",
.mount = myfs_mount,
.kill_sb = myfs_kill_sb,
};
2.调用register_filesystem函数注册该文件系统到全局中
int ret = register_filesystem(&myfs_type);
if(ret)
{
printk(KERN_ERR "myfs:failed to register filesystem\r\n");
return ret;
}
file_system_type
file_system_type定义如下:
struct file_system_type {
const char *name;
int fs_flags;
#define FS_REQUIRES_DEV 1
#define FS_BINARY_MOUNTDATA 2
#define FS_HAS_SUBTYPE 4
#define FS_USERNS_MOUNT 8 /* Can be mounted by userns root */
#define FS_DISALLOW_NOTIFY_PERM 16 /* Disable fanotify permission events */
#define FS_ALLOW_IDMAP 32 /* FS has been updated to handle vfs idmappings. */
#define FS_RENAME_DOES_D_MOVE 32768 /* FS will handle d_move() during rename() internally. */
int (*init_fs_context)(struct fs_context *);
const struct fs_parameter_spec *parameters;
struct dentry *(*mount) (struct file_system_type *, int,
const char *, void *);
void (*kill_sb) (struct super_block *);
struct module *owner;
struct file_system_type * next;
struct hlist_head fs_supers;
struct lock_class_key s_lock_key;
struct lock_class_key s_umount_key;
struct lock_class_key s_vfs_rename_key;
struct lock_class_key s_writers_key[SB_FREEZE_LEVELS];
struct lock_class_key i_lock_key;
struct lock_class_key i_mutex_key;
struct lock_class_key invalidate_lock_key;
struct lock_class_key i_mutex_dir_key;
};
name保存了文件系统的名称fs_flags是使用标志owner是一个指向module结构的指针,当文件系统以模块形式加载时有效(NULL表示持久编译在内核)next指向下一个可用的文件系统,在系统中用一个单链表维护fs_supers是超级块链表的表头;在同一个文件系统中可能有多个超级块,这些超级块聚集在一个链表中init_fs_context是一个用于初始化fs_context结构体的初始化函数,可选提供(见后文)mount是系统调用sys_mount中会调用的一个回调函数(见下文)
register_filesystem
register_filesystem是内核注册文件系统的系统函数
/*
* register_filesystem - 注册一个新的文件系统
* @fs: 文件系统结构体
*
* 将传入的文件系统添加到内核已知的文件系统列表中,以供挂载及其他系统调用使用。
* 成功时返回0,出错时返回负的错误码。
*
* 传入的 &struct file_system_type 会被链接到内核数据结构中,
* 在文件系统注销之前,不得释放该结构体。
*/
int register_filesystem(struct file_system_type * fs)
{
int res = 0;
struct file_system_type ** p;
if (fs->parameters &&
!fs_validate_description(fs->name, fs->parameters))
return -EINVAL;
BUG_ON(strchr(fs->name, '.'));
if (fs->next)
return -EBUSY;
write_lock(&file_systems_lock);
p = find_filesystem(fs->name, strlen(fs->name));
if (*p)
res = -EBUSY;
else
*p = fs;
write_unlock(&file_systems_lock);
return res;
}
函数首先会去检查当前注册文件系统是否合法,并且设备名正确填写,以及是否已经在存储文件系统的链表中if (fs->next)
然后就上锁,调用find_filesystem寻找一个链表中空闲的,存放当前文件系统
static struct file_system_type **find_filesystem(const char *name, unsigned len)
{
struct file_system_type **p;
for (p = &file_systems; *p; p = &(*p)->next)
if (strncmp((*p)->name, name, len) == 0 &&
!(*p)->name[len])
break;
return p;
}
挂载文件系统
在使用mount系统调用时候,入口函数是sys_mount,在笔者当前使用的版本中函数的实现是在fs/namespace.c
SYSCALL_DEFINE5(mount, char __user *, dev_name, char __user *, dir_name,
char __user *, type, unsigned long, flags, void __user *, data)
{
int ret;
char *kernel_type;
char *kernel_dev;
void *options;
kernel_type = copy_mount_string(type);
ret = PTR_ERR(kernel_type);
if (IS_ERR(kernel_type))
goto out_type;
kernel_dev = copy_mount_string(dev_name);
ret = PTR_ERR(kernel_dev);
if (IS_ERR(kernel_dev))
goto out_dev;
options = copy_mount_options(data);
ret = PTR_ERR(options);
if (IS_ERR(options))
goto out_data;
ret = do_mount(kernel_dev, dir_name, kernel_type, flags, options);
kfree(options);
out_data:
kfree(kernel_dev);
out_dev:
kfree(kernel_type);
out_type:
return ret;
}
这个系统调用函数主要做了两个操作
1.复制挂载选项copy_mount_string
2.执行do_mount函数
挂载选项包含有设备名称,文件系统类型,以及自定义的参数
do_mount函数
在挂载选项已经由系统调用从用户空间复制到内核空间之后,内核将控制转移给do_mount,该函数将分析传递的信息,并且设置相应的标志
long do_mount(const char *dev_name, const char __user *dir_name,
const char *type_page, unsigned long flags, void *data_page)
{
struct path path;
int ret;
ret = user_path_at(AT_FDCWD, dir_name, LOOKUP_FOLLOW, &path);
if (ret)
return ret;
ret = path_mount(dev_name, &path, type_page, flags, data_page);
path_put(&path);
return ret;
}
do_mount首先将要挂载的目录名称解析出来,存放到一个struct path结构体中
struct path {
struct vfsmount *mnt;
struct dentry *dentry;
} __randomize_layout;
每一个挂载的文件系统都对应一个vfsmount结构实例
struct vfsmount {
struct dentry *mnt_root; /* root of the mounted tree */
struct super_block *mnt_sb; /* pointer to superblock */
int mnt_flags;
struct user_namespace *mnt_userns;
} __randomize_layout;
文件系统本身的相对根目录所对应的dentry保存在mnt_root,也就是挂载点的dentry
然后调用path_mount函数进行后续挂载操作
path_mount函数
path_mount充当一个多路分解器,将仍然需要完成的工作为派给与装载类型相关的各个函数
/* Discard magic */
if ((flags & MS_MGC_MSK) == MS_MGC_VAL)
flags &= ~MS_MGC_MSK;
/* Basic sanity checks */
if (data_page)
((char *)data_page)[PAGE_SIZE - 1] = 0;
if (flags & MS_NOUSER)
return -EINVAL;
ret = security_sb_mount(dev_name, path, type_page, flags, data_page);
if (ret)
return ret;
if (!may_mount())
return -EPERM;
if (flags & SB_MANDLOCK)
warn_mandlock();
/* Default to relatime unless overriden */
if (!(flags & MS_NOATIME))
mnt_flags |= MNT_RELATIME;
/* Separate the per-mountpoint flags */
if (flags & MS_NOSUID)
mnt_flags |= MNT_NOSUID;
if (flags & MS_NODEV)
mnt_flags |= MNT_NODEV;
if (flags & MS_NOEXEC)
mnt_flags |= MNT_NOEXEC;
if (flags & MS_NOATIME)
mnt_flags |= MNT_NOATIME;
if (flags & MS_NODIRATIME)
mnt_flags |= MNT_NODIRATIME;
if (flags & MS_STRICTATIME)
mnt_flags &= ~(MNT_RELATIME | MNT_NOATIME);
if (flags & MS_RDONLY)
mnt_flags |= MNT_READONLY;
if (flags & MS_NOSYMFOLLOW)
mnt_flags |= MNT_NOSYMFOLLOW;
/* The default atime for remount is preservation */
if ((flags & MS_REMOUNT) &&
((flags & (MS_NOATIME | MS_NODIRATIME | MS_RELATIME |
MS_STRICTATIME)) == 0)) {
mnt_flags &= ~MNT_ATIME_MASK;
mnt_flags |= path->mnt->mnt_flags & MNT_ATIME_MASK;
}
sb_flags = flags & (SB_RDONLY |
SB_SYNCHRONOUS |
SB_MANDLOCK |
SB_DIRSYNC |
SB_SILENT |
SB_POSIXACL |
SB_LAZYTIME |
SB_I_VERSION);
首先会去根据传入的flags对挂载参数mnt_flags进行设置,并且根据flags参数不同而选择不同的挂载函数
if ((flags & (MS_REMOUNT | MS_BIND)) == (MS_REMOUNT | MS_BIND))
return do_reconfigure_mnt(path, mnt_flags);
if (flags & MS_REMOUNT)
return do_remount(path, flags, sb_flags, mnt_flags, data_page);
if (flags & MS_BIND)
return do_loopback(path, dev_name, flags & MS_REC);
if (flags & (MS_SHARED | MS_PRIVATE | MS_SLAVE | MS_UNBINDABLE))
return do_change_type(path, flags);
if (flags & MS_MOVE)
return do_move_mount_old(path, dev_name);
return do_new_mount(path, type_page, sb_flags, mnt_flags, dev_name,
data_page);
do_remount修改已经挂载的文件系统的选项do_lookback用于通过环回接口挂载一个文件系统do_change_type负责处理共享,从属,不可绑定的挂载,它可以改变挂载标志或涉及的各个vfsmount实例之间建立所需的数据结构的关联do_move_mount_old用来移动一个已挂载的文件系统do_new_mount处理普通的挂载操作,这是默认情况,不需要任何特殊标志
do_new_mount函数
这段代码是旧版 mount() 系统调用(用户空间传入 void *data 的那种)通往新版挂载 API(基于 fs_context)的适配层。你可以把整个过程看作“将杂乱的传统参数打包成一个标准包裹,然后交给新版流程处理”。
第一阶段:准备与查寻(前戏)
if (!fstype) return -EINVAL;
type = get_fs_type(fstype); // 根据 "ext4"、"nfs" 等名字找到内核中的模块
if (!type) return -ENODEV;
这里先根据字符串找到对应的文件系统类型结构体。注意,有些文件系统(如 FUSE)支持子类型(例如 fuse.sshfs 中的 sshfs),代码接下来的 if (type->fs_flags & FS_HAS_SUBTYPE) 就是专门切分出子类型字符串,供后面使用。
第二阶段:创建上下文(核心包装)
fc = fs_context_for_mount(type, sb_flags);
put_filesystem(type); // 模块引用计数减一,因为 fc 已经持有了 type 的引用
if (IS_ERR(fc)) return PTR_ERR(fc);
fs_context_for_mount:这就是fc的出生地。它会在内核中分配一个struct fs_context结构体,并把type和sb_flags(如只读标志)初始化进去。此时,fc里已经包含了文件系统对应的ops(操作函数集)。put_filesystem:因为fc内部已经安全地引用了type,所以这里减少模块计数,防止模块被意外卸载。
第三阶段:填充上下文(解析杂糅的参数)
这是最关键的步骤,所有的用户传入数据都被“塞”进 fc 里:
fc->oldapi = true; // 标记:我是老式 mount 系统调用来的,不是新的 fsopen()
这个标记很关键,某些文件系统会根据它来调整解析行为(兼容老格式的 data 字符串)。
if (subtype)
err = vfs_parse_fs_string(fc, "subtype", subtype, strlen(subtype));
if (!err && name)
err = vfs_parse_fs_string(fc, "source", name, strlen(name));
vfs_parse_fs_string 是通用解析函数,它把键值对(如 subtype=sshfs,source=/dev/sda1)填入 fc 内部的选项中。
if (!err)
err = parse_monolithic_mount_data(fc, data);
这是旧 API 最特殊的地方。data 是用户传过来的 void *(通常是一个以逗号分隔的字符串,比如 "rw,noatime,data=ordered")。parse_monolithic_mount_data 负责把这个 “大块杂烩” 拆解成一个个可识别的参数,并调用 vfs_parse_fs_string 存入 fc,或者直接存入文件系统私有的 fc->fs_private 字段中。
第四阶段:安全检查
if (!err && !mount_capable(fc))
err = -EPERM;
mount_capable 利用 fc 中保存的用户命名空间和凭据(cred),检查当前进程是否有权限在此命名空间下挂载。这一步将权限逻辑也收归到了上下文里。
第五阶段:获取超级块(实质操作)
if (!err)
err = vfs_get_tree(fc);
这是真正干活的地方。vfs_get_tree 会调用 fc->ops->get_tree(具体文件系统实现)。此时,fc 就像是文件系统的“施工图纸”:
- 如果是块设备(ext4),它根据
fc->source去读取磁盘上的超级块。 - 如果是网络文件系统(NFS),它根据参数去连接服务器。
获取成功后,结果(即 struct super_block 和根目录 dentry)会被写回 fc 内部(存储在 fc->root 中)。
第六阶段:执行挂载(安家落户)
if (!err)
err = do_new_mount_fc(fc, path, mnt_flags);
do_new_mount_fc 接手已经填充好的 fc:
- 它从
fc中取出刚刚获取的root(根目录 dentry)和超级块。 - 创建一个新的
struct mount挂载实例。 - 将这个新挂载实例,挂载到我们传入的
path路径(即用户指定的挂载点)下,并将其插入到当前挂载命名空间的树结构中。
第七阶段:善后清理
put_fs_context(fc);
return err;
无论成功还是失败,put_fs_context 都会被调用。它会释放 fc 占用的所有资源:
- 如果挂载失败,它会释放临时创建的超级块或 dentry。
- 如果挂载成功,它会释放
fc本身,但不会释放已经挂载的超级块(因为超级块已由 VFS 全局缓存或挂载树持有)。
vfs_get_tree函数
正如上面所述,vfs_get_tree函数会去获取磁盘的超级块struct super_block 和根目录 dentry
//fs/super.c
int vfs_get_tree(struct fs_context *fc)
{
struct super_block *sb;
int error;
if (fc->root)
return -EBUSY;
/* Get the mountable root in fc->root, with a ref on the root and a ref
* on the superblock.
*/
error = fc->ops->get_tree(fc);
if (error < 0)
return error;
if (!fc->root) {
pr_err("Filesystem %s get_tree() didn't set fc->root\n",
fc->fs_type->name);
/* We don't know what the locking state of the superblock is -
* if there is a superblock.
*/
BUG();
}
sb = fc->root->d_sb;
在里面主要是调用了fs_context结构体中的const struct fs_context_operations *ops;操作方法中的int (*get_tree)(struct fs_context *fc);
而这个fs_context_operations的初始化在fs_context_for_mount初始化函数当中
在这里面会去判断要挂载的文件系统的file_system_type是否提供了int (*init_fs_context)(struct fs_context *);函数,如果没有的话就使用默认的legacy_init_fs_context进行fc的初始化
//fs/fs_context.c
init_fs_context = fc->fs_type->init_fs_context;
if (!init_fs_context)
init_fs_context = legacy_init_fs_context;
ret = init_fs_context(fc);
而legacy_init_fs_context定义如下
//fs/fs_context.c
/*
* Initialise a legacy context for a filesystem that doesn't support
* fs_context.
*/
static int legacy_init_fs_context(struct fs_context *fc)
{
fc->fs_private = kzalloc(sizeof(struct legacy_fs_context), GFP_KERNEL_ACCOUNT);
if (!fc->fs_private)
return -ENOMEM;
fc->ops = &legacy_fs_context_ops;
return 0;
}
所以可以看到在这里给fs_context_operations赋值了初值legacy_fs_context_ops,这个操作集是内核默认提供的一个操作集
//fs/fs_context.c
const struct fs_context_operations legacy_fs_context_ops = {
.free = legacy_fs_context_free,
.dup = legacy_fs_context_dup,
.parse_param = legacy_parse_param,
.parse_monolithic = legacy_parse_monolithic,
.get_tree = legacy_get_tree,
.reconfigure = legacy_reconfigure,
};
其中get_tree函数指针指向的是.get_tree = legacy_get_tree,
legacy_get_tree函数
这个函数可以看作是最终的获取超级块和根目录的函数,在这里面调用的是file_system_type中的mount函数指针
//fs/fs_context.c
/*
* Get a mountable root with the legacy mount command.
*/
static int legacy_get_tree(struct fs_context *fc)
{
struct legacy_fs_context *ctx = fc->fs_private;
struct super_block *sb;
struct dentry *root;
root = fc->fs_type->mount(fc->fs_type, fc->sb_flags,
fc->source, ctx->legacy_data);
if (IS_ERR(root))
return PTR_ERR(root);
sb = root->d_sb;
BUG_ON(!sb);
fc->root = root;
return 0;
}
fc->source:字符串形式的挂载源,对于块设备,它是设备路径(如"/dev/sda1");对于网络文件系统,它是服务器和共享路径(如"192.168.1.100:/nfs_share");对于虚拟文件系统(如tmpfs),它可能为NULL。ctx->legacy_data:未经内核 VFS 解析的、原始的挂载选项二进制/文本块,它直接指向用户空间通过mount系统调用传进来的void *data指针所指向的内存内容(通常是类似"rw,noatime,data=ordered"这样的字符串)。
在通过fs_type->mount后得到了挂载文家系统的根目录对应的dentry后,会把它存储在fc->root供后续使用
调用文件系统定义的fs_type->mount
笔者自己写了一个简易文件系统,其中fs_type->mount的定义如下myfs_mount
static struct dentry *myfs_mount(struct file_system_type *fs_type, int flags,
const char *dev_name, void *data)
{
struct dentry *dentry = mount_nodev(fs_type, flags, data, myfs_fill_super);
if(IS_ERR(dentry))
printk(KERN_ERR "myfs:failed to mount\n");
else
printk(KERN_INFO "myfs:mount succ\n");
return dentry;
}
在这里我调用的mount_nodev函数
mount_nodev函数
mount_nodev 的核心思想是封装,它将挂载流程中繁琐、通用的“获取超级块”等步骤封装起来,让具体的文件系统只需关注其特有的“填充超级块”逻辑。
//fs/super.c
struct dentry *mount_nodev(struct file_system_type *fs_type,
int flags, void *data,
int (*fill_super)(struct super_block *, void *, int))
{
int error;
struct super_block *s = sget(fs_type, NULL, set_anon_super, flags, NULL);
if (IS_ERR(s))
return ERR_CAST(s);
error = fill_super(s, data, flags & SB_SILENT ? 1 : 0);
if (error) {
deactivate_locked_super(s);
return ERR_PTR(error);
}
s->s_flags |= SB_ACTIVE;
return dget(s->s_root);
}
EXPORT_SYMBOL(mount_nodev);
它的核心实现逻辑大致如下:
- 分配超级块:调用
sget()函数,为这个文件系统实例分配并初始化一个struct super_block结构。 - 调用回调:调用由具体文件系统提供的
fill_super回调函数,让后者去填充超级块中的具体信息(如读写函数、inode 操作等)。 - 错误处理:如果
fill_super失败,则销毁已分配的超级块并返回错误。 - 挂载成功:标记超级块为激活状态 (
SB_ACTIVE),并返回根目录的 dentry
do_new_mount_fc函数
在 do_new_mount 函数中,经过 vfs_get_tree(fc) 之后,fc 已经包含了:
- 一个有效的
super_block(超级块) - 一个根目录的
dentry(存储在fc->root中)
do_new_mount_fc 的任务就是:
- 安全检查(最后一道防线)
- 创建内核挂载对象(
struct vfsmount) - 将该对象绑定到用户指定的挂载点路径,并插入当前挂载命名空间的树中。
整体流程如下:
do_new_mount_fc
├─ 1. 安全检查(LSM + revealing)
│ └─ 失败 -> fc_drop_locked(fc) 并返回
├─ 2. up_write(&sb->s_umount) // 释放超级块锁
├─ 3. mnt = vfs_create_mount(fc) // 创建挂载实例
├─ 4. mnt_warn_timestamp_expiry() // 打印时间辍警告(如果有)
├─ 5. lock_mount(mountpoint) // 获取挂载点锁
│ ├─ 失败 -> mntput(mnt) 返回错误
│ └─ 成功
│ ├─ do_add_mount(..., mnt_flags) // 真正加入树
│ └─ unlock_mount(mp) // 释放锁
└─ 6. 成功返回 0,或失败时 mntput(mnt) 返回错误
//fs/namespace.c
/*
* Create a new mount using a superblock configuration and request it
* be added to the namespace tree.
*/
static int do_new_mount_fc(struct fs_context *fc, struct path *mountpoint,
unsigned int mnt_flags)
{
struct vfsmount *mnt;
struct mountpoint *mp;
struct super_block *sb = fc->root->d_sb;
int error;
传入参数中
- 从
fc->root中取出超级块指针sb。 mountpoint是用户传入的挂载点路径(如/mnt)的struct path结构,包含了目录的 dentry 和 vfsmount。mnt_flags是挂载标志(如MS_NODEV、MS_NOEXEC等)。
整体流程大致解析
1. 安全检查阶段
error = security_sb_kern_mount(sb);
if (!error && mount_too_revealing(sb, &mnt_flags))
error = -EPERM;
if (unlikely(error)) {
fc_drop_locked(fc);
return error;
}
security_sb_kern_mount:调用 Linux 安全模块(LSM,如 SELinux)进行权限检查,确认当前进程是否允许挂载这个超级块。mount_too_revealing:检查该挂载是否会暴露过于敏感的信息(比如将整个文件系统树的内部细节暴露给用户命名空间),若发现风险则拒绝挂载,并可能调整mnt_flags增加限制。- 如果上述检查失败,调用
fc_drop_locked(fc)。这个函数的作用是释放超级块的写锁并清理临时状态(实际上它会调用fc->ops->drop或直接释放锁)。因为之前获取超级块时(vfs_get_tree)对s_umount加了写锁,现在发现权限不足,需要解锁并丢弃临时引用,然后返回错误。
2. 释放超级块锁
up_write(&sb->s_umount);
- 在
vfs_get_tree过程中,文件系统通常会通过down_write(&sb->s_umount)获取超级块的写锁,以防止在挂载过程中被其他操作修改。 - 此时,超级块已经完成构建,安全检查也通过了,因此可以释放写锁,允许其他进程读/写该超级块。
3. 创建 vfsmount 对象
mnt = vfs_create_mount(fc);
if (IS_ERR(mnt))
return PTR_ERR(mnt);
vfs_create_mount(fc)根据fc中的信息(超级块、根 dentry、挂载标志等)分配并初始化一个struct vfsmount结构体。- 这个
vfsmount是内核内部表示一个挂载实例的核心数据结构,它关联了超级块、根 dentry、挂载标志以及所属的挂载命名空间等。
4. 时间戳过期警告
mnt_warn_timestamp_expiry(mountpoint, mnt);
- 这是一个辅助函数,检查文件系统的时间戳(如
ctime、mtime)是否可能超出 32 位范围(比如 2038 年问题),若可能过期则打印一条警告日志,提醒用户注意。
5. 锁定挂载点并添加到挂载树
mp = lock_mount(mountpoint);
if (IS_ERR(mp)) {
mntput(mnt);
return PTR_ERR(mp);
}
lock_mount(mountpoint):- 它会对挂载点目录的
dentry和vfsmount进行加锁,以防止在挂载过程中路径发生变化(如被删除或移动)。 - 同时它会返回一个
struct mountpoint指针,该结构代表挂载点对象,包含了挂载点的 dentry 和挂载列表信息。如果路径无效或已被占用,该函数会返回错误。
- 它会对挂载点目录的
error = do_add_mount(real_mount(mnt), mp, mountpoint, mnt_flags);
unlock_mount(mp);
do_add_mount是真正将新创建的vfsmount加入到挂载命名空间树中的核心函数。- 它接收
struct mount *(通过real_mount(mnt)从vfsmount获取内部mount结构)、挂载点对象mp、挂载点路径mountpoint和标志。 - 它会检查是否存在重叠挂载,并将新的挂载插入到挂载点的挂载列表(
mount_hashtable或父挂载的子列表)中。 - 如果成功,则返回 0;否则返回负错误码。
- 它接收
- 最后释放之前获得的锁
unlock_mount(mp)。
6. 错误处理与清理
if (error < 0)
mntput(mnt);
return error;
- 如果
do_add_mount失败,则减少vfsmount的引用计数(mntput),最终会释放该对象及其相关资源。 - 如果成功,
mnt已经由挂载树持有引用,函数返回 0。
vfs_create_mount
该函数为设备的超级块分配一个struct mount结构体,并且将struct mount结构体添加到当前设备超级块内的链表里面(尾部)
首先是分配struct mount实例
/**
* vfs_create_mount - Create a mount for a configured superblock
* @fc: The configuration context with the superblock attached
*
* Create a mount to an already configured superblock. If necessary, the
* caller should invoke vfs_get_tree() before calling this.
*
* Note that this does not attach the mount to anything.
*/
struct vfsmount *vfs_create_mount(struct fs_context *fc)
{
struct mount *mnt;
struct user_namespace *fs_userns;
if (!fc->root)
return ERR_PTR(-EINVAL);
mnt = alloc_vfsmnt(fc->source ?: "none");
if (!mnt)
return ERR_PTR(-ENOMEM);
alloc_vfsmnt
函数传入的参数来自fc->source,也就是上面说的在用户命令中的设备名称,该函数会为mount结构体分配一块内存空间,然后分配一个id,并且设置fc->source传入的设备名,并进行一些初始化如引用计数,链表等
static struct mount *alloc_vfsmnt(const char *name)
{
struct mount *mnt = kmem_cache_zalloc(mnt_cache, GFP_KERNEL);
if (mnt) {
int err;
err = mnt_alloc_id(mnt);
if (err)
goto out_free_cache;
if (name) {
mnt->mnt_devname = kstrdup_const(name,
GFP_KERNEL_ACCOUNT);
if (!mnt->mnt_devname)
goto out_free_id;
}
#ifdef CONFIG_SMP
mnt->mnt_pcp = alloc_percpu(struct mnt_pcp);
if (!mnt->mnt_pcp)
goto out_free_devname;
this_cpu_add(mnt->mnt_pcp->mnt_count, 1);
#else
mnt->mnt_count = 1;
mnt->mnt_writers = 0;
#endif
INIT_HLIST_NODE(&mnt->mnt_hash);
INIT_LIST_HEAD(&mnt->mnt_child);
INIT_LIST_HEAD(&mnt->mnt_mounts);
INIT_LIST_HEAD(&mnt->mnt_list);
INIT_LIST_HEAD(&mnt->mnt_expire);
INIT_LIST_HEAD(&mnt->mnt_share);
INIT_LIST_HEAD(&mnt->mnt_slave_list);
INIT_LIST_HEAD(&mnt->mnt_slave);
INIT_HLIST_NODE(&mnt->mnt_mp_list);
INIT_LIST_HEAD(&mnt->mnt_umounting);
INIT_HLIST_HEAD(&mnt->mnt_stuck_children);
mnt->mnt.mnt_userns = &init_user_ns;
}
return mnt;
#ifdef CONFIG_SMP
out_free_devname:
kfree_const(mnt->mnt_devname);
#endif
out_free_id:
mnt_free_id(mnt);
out_free_cache:
kmem_cache_free(mnt_cache, mnt);
return NULL;
}
初始化挂载参数
atomic_inc(&fc->root->d_sb->s_active);
mnt->mnt.mnt_sb = fc->root->d_sb;
mnt->mnt.mnt_root = dget(fc->root);//dget除了会把传入的dentry赋值给前面的左值,还会将dentry的引用计数+1
mnt->mnt_mountpoint = mnt->mnt.mnt_root;
mnt->mnt_parent = mnt;
分配好mount结构体之后就会为它进行一些初始化挂载参数(mnt->mnt是struct vfsmount结构体):
- 比如将设备的超级块置位激活状态
atomic_inc(&fc->root->d_sb->s_active);
- 然后当前
mnt->mnt超级块指针指向设备的超级块
mnt->mnt.mnt_sb = fc->root->d_sb;
mnt->mnt的挂载dentry指向挂载设备的根目录的dentry(不是被挂载的目录)- 这里的
dget除了把传入的参数dentry赋值给前面变量,还会让dentry的引用计数+1
- 这里的
mnt->mnt.mnt_root = dget(fc->root);//dget除了会把传入的dentry赋值给前面的左值,还会将dent
//linux/dcache.h
static inline struct dentry *dget(struct dentry *dentry)
{
if (dentry)
lockref_get(&dentry->d_lockref);
return dentry;
}
//lib/lockref.c
void lockref_get(struct lockref *lockref)
{
CMPXCHG_LOOP(
new.count++;
,
return;
);
spin_lock(&lockref->lock);
lockref->count++;
spin_unlock(&lockref->lock);
}
EXPORT_SYMBOL(lockref_get);
mnt的被挂载点指向自身设备根目录
mnt->mnt_mountpoint = mnt->mnt.mnt_root;
- 将
mnt在全局挂载中的父对象指向自己
mnt->mnt_parent = mnt;
在 Linux 的挂载命名空间里,所有挂载点形成了一棵庞大的树。
- 假设宿主机根文件系统是
A,你把 U 盘B挂载到了/mnt/usb下。 - 那么
B的mnt_parent就指向A(因为B是附着在A的目录/mnt/usb之上的)。 mnt_mountpoint则指向A中的/mnt/usb这个 dentry。
这两个字段共同决定了“这个挂载实例挂在哪里”。
判断命名空间是否是初值映射
fs_userns = mnt->mnt.mnt_sb->s_user_ns;
if (!initial_idmapping(fs_userns))//判断给定的用户命名空间是否使用的是“初始映射”,用于容器等情况
mnt->mnt.mnt_userns = get_user_ns(fs_userns);//同上在引用计数+1
将实例添加到链表
list_add_tail 是 Linux 内核中标准双向循环链表的操作函数,它的作用非常纯粹:将一个链表节点(new)添加到指定链表头(head)的末尾(tail)。
lock_mount_hash();
list_add_tail(&mnt->mnt_instance, &mnt->mnt.mnt_sb->s_mounts);//将新节点插入到双向循环链表的尾部,修改相关的前后指针。
unlock_mount_hash();
return &mnt->mnt;
将当前挂载实例 挂载到超级块的全局列表末尾 的操作。
注意:这里的超级块的全局列表只是存放当前设备的挂载实例struct mount,而不是其他设备的会被添加进来
struct super_block(超级块)是用来唯一标识一个具体的物理/逻辑设备(或文件系统实例)的。s_mounts 列表只收纳指向这个超级块的挂载点。
为什么要这么做?(目的)
当内核需要遍历某个文件系统下的所有挂载点时(例如执行 umount 卸载、sync 同步数据或统计使用情况),它不需要盲目扫描整个全局挂载树,只需遍历这个超级块的 s_mounts 列表即可。
如果两个挂载点有父子关系(比如 /dev/sdb1 挂载在 /mnt/a 下面),这个父子关系是由 struct mount 结构体中的 mnt_parent(指向父挂载)和 mnt_mountpoint(指向父目录的 dentry)来维护的,而不是通过 s_mounts 列表。
do_add_mount
将新创建的 vfsmount 加入到挂载命名空间树中
//fs/namespace.c
/*
* add a mount into a namespace's mount tree
*/
static int do_add_mount(struct mount *newmnt, struct mountpoint *mp,
const struct path *path, int mnt_flags)
首先提取出 传入path中vfsmount结构体成员mnt所对应的mount结构体指针
struct mount *parent = real_mount(path->mnt);
防止重复装载
然后进行一系列检查工作,比如被挂载目录的mount是否为空,挂载标志是否合法
以及是否是同一个设备挂载到同一个目录,确保一个文件系统不会被重复挂载到同一个目录
mnt_flags &= ~MNT_INTERNAL_FLAGS;
if (unlikely(!check_mnt(parent))) {
/* that's acceptable only for automounts done in private ns */
if (!(mnt_flags & MNT_SHRINKABLE))
return -EINVAL;
/* ... and for those we'd better have mountpoint still alive */
if (!parent->mnt_ns)
return -EINVAL;
}
/* Refuse the same filesystem on the same mount point */
if (path->mnt->mnt_sb == newmnt->mnt.mnt_sb &&
path->mnt->mnt_root == path->dentry)
return -EBUSY;
if (d_is_symlink(newmnt->mnt.mnt_root))//检查挂载文件系统根目录是不是符号连接(必须是目录)
return -EINVAL;
最后执行将当前文件系统挂载到目标目录
newmnt->mnt.mnt_flags = mnt_flags;
return graft_tree(newmnt, parent, mp);
graft_tree
static int graft_tree(struct mount *mnt, struct mount *p, struct mountpoint *mp)
{
if (mnt->mnt.mnt_sb->s_flags & SB_NOUSER)
return -EINVAL;
if (d_is_dir(mp->m_dentry) !=
d_is_dir(mnt->mnt.mnt_root))
return -ENOTDIR;
return attach_recursive_mnt(mnt, p, mp, false);
}
graft_tree内部主要是调用了attach_recursive_mnt函数,新挂载的文件系统通过调用attach_recursive_mnt函数添加到父文件系统的命名空间
attach_recursive_mnt
static int attach_recursive_mnt(struct mount *source_mnt,
struct mount *dest_mnt,
struct mountpoint *dest_mp,
bool moving)
attach_recursive_mnt(挂载核心函数)
核心概念速览
函数头部的大段注释描述了 BIND 和 MOVE 两种操作下,源挂载和目标挂载的传播类型组合所导致的不同行为。
先理解几个关键概念:
| 术语 | 含义 |
|---|---|
| 共享挂载 (shared) | 挂载点会与同一对等组(peer group)内的其他挂载点同步传播挂载/卸载事件。 |
| 私有挂载 (private) | 挂载点不向任何其他挂载点传播事件,也不接收任何事件。 |
| 从属挂载 (slave) | 挂载点接收来自其主挂载(master)的传播事件,但自身不反向传播。 |
| 不可绑定挂载 (unbindable) | 该挂载点不能作为 bind mount 的源。 |
| 传播树 (propagation tree) | 由共享/从属挂载形成的树状传播关系。 |
函数执行流程分步解析
第一阶段:预备工作(分配资源、计数检查)
smp = get_mountpoint(source_mnt->mnt.mnt_root);
if (IS_ERR(smp)) return PTR_ERR(smp);
- 预分配一个
mountpoint结构体,用于在后续传播过程中,如果有新的子挂载需要被“塞”到其他父挂载下时使用。
if (!moving) {
err = count_mounts(ns, source_mnt);
if (err) goto out;
}
如果是 bind 操作(非移动),检查当前命名空间是否有足够的挂载槽位(pending_mounts 限额),防止无限制挂载耗尽资源。
第二阶段:处理共享传播(核心逻辑)
if (IS_MNT_SHARED(dest_mnt)) {
err = invent_group_ids(source_mnt, true);
if (err) goto out;
err = propagate_mnt(dest_mnt, dest_mp, source_mnt, &tree_list);
lock_mount_hash();
if (err) goto out_cleanup_ids;
for (p = source_mnt; p; p = next_mnt(p, source_mnt))
set_mnt_shared(p);
} else {
lock_mount_hash();
}
IS_MNT_SHARED(dest_mnt):检查目标挂载点是否标记为shared。如果是,需要将传播事件扩散到整个传播树。invent_group_ids:为源挂载树中的每一个挂载实例分配一个对等组 ID,用于后续传播时识别共享关系。propagate_mnt:这是传播的核心。它会遍历目标挂载点的传播树,在每一个需要同步接收该挂载的节点上,克隆一份源挂载的副本,并将这些新克隆的挂载实例放入tree_list链表中,等待后续提交。set_mnt_shared:将源挂载树中的所有节点标记为shared,使其加入对应挂载点的对等组。
第三阶段:挂载实际操作(attach / commit)
if (moving) {
unhash_mnt(source_mnt); // 从旧位置摘下
attach_mnt(source_mnt, dest_mnt, dest_mp); // 挂到新位置
touch_mnt_namespace(source_mnt->mnt_ns);
} else {
if (source_mnt->mnt_ns) {
list_del_init(&source_mnt->mnt_ns->list); // 如果是匿名挂载,从列表移除
}
mnt_set_mountpoint(dest_mnt, dest_mp, source_mnt); // 设置挂载点关系
commit_tree(source_mnt); // 将源树提交到全局挂载树
}
- 移动操作 (
moving):先将源从原父挂载下摘除(unhash_mnt),再附加到新位置(attach_mnt)。 - 绑定操作 (非
moving):- 如果源挂载已经属于某个命名空间(非匿名),则从该命名空间的挂载列表中移除(因为源将被克隆,原挂载本身可能被丢弃)。
mnt_set_mountpoint:设置source_mnt->mnt_parent = dest_mnt,并设置source_mnt->mnt_mountpoint = dest_mp->dentry——这就是之前初始化时mnt_parent = mnt被最终覆盖的地方。commit_tree:将整棵挂载树插入到全局挂载哈希表和父挂载的子列表中。
第四阶段:传播传播树中的克隆节点
hlist_for_each_entry_safe(child, n, &tree_list, mnt_hash) {
struct mount *q;
hlist_del_init(&child->mnt_hash);
q = __lookup_mnt(&child->mnt_parent->mnt, child->mnt_mountpoint);
if (q)
mnt_change_mountpoint(child, smp, q);
if (child->mnt_parent->mnt_ns->user_ns != user_ns)
lock_mnt_tree(child);
child->mnt.mnt_flags &= ~MNT_LOCKED;
commit_tree(child);
}
- 遍历
tree_list(在propagate_mnt中生成的克隆挂载列表)。 - 对于每一个克隆节点:
- 如果其挂载点与现有挂载冲突(
__lookup_mnt找到已有挂载),则用预分配的smp替换其挂载点。 - 如果克隆节点跨越了不同的用户命名空间,则对其整棵子树加锁(
lock_mnt_tree),防止在跨命名空间传播时被意外修改。 - 清除
MNT_LOCKED标志(该标志表示挂载被锁定,不可移动/卸载,跨命名空间传播后需要解锁)。 - 提交到全局挂载树(
commit_tree)。
- 如果其挂载点与现有挂载冲突(
第五阶段:清理与错误回滚
out_cleanup_ids:
while (!hlist_empty(&tree_list)) {
child = hlist_entry(...);
child->mnt_parent->mnt_ns->pending_mounts = 0;
umount_tree(child, UMOUNT_SYNC); // 递归卸载失败的克隆树
}
unlock_mount_hash();
cleanup_group_ids(source_mnt, NULL);
out:
ns->pending_mounts = 0;
read_seqlock_excl(&mount_lock);
put_mountpoint(smp);
read_sequnlock_excl(&mount_lock);
return err;
- 如果
propagate_mnt失败,回滚:递归卸载所有已克隆但尚未提交的挂载(umount_tree),清理分配的组 ID,释放预分配的挂载点。 - 无论成功与否,最终都会减少
pending_mounts计数并释放smp的引用。
