mount系统调用剖析
开篇
? 对于mount 系统调用,在linux内核代码中同样也会使用到,特别是在linux内核的启动部分的源码中,她换了一种形式:在linux内核4.1.15版本下,内核的mount操作与用户空间的mount系统调用操作接口函数是一致的:都由sys_mount() 函数完成。在linux内核4.19.4 版本下,linux内核中mount调用的接口则分离成ksys_mount() 。
文本以4.1.15 版本的linux内核作为分析对象展开。
?mount系统调用在linux内核中的定义如下(/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;
unsigned long data_page;
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;
ret = copy_mount_options(data, &data_page);
if (ret < 0)
goto out_data;
ret = do_mount(kernel_dev, dir_name, kernel_type, flags,
(void *) data_page);
free_page(data_page);
out_data:
kfree(kernel_dev);
out_dev:
kfree(kernel_type);
out_type:
return ret;
}
上述第9-12、14-17行代码,使用copy_mount_string() 函数从用户空间复制已经存在的字符串:挂载类型、挂载设备名称。
第19-21行代码,使用copy_mount_options() 函数复制挂载选项参数。
如果代码执行异常,将通过goto语句跳转到对应的位置,从而结束mount系统调用流程。如果没有出现错误或异常,将调用do_mount() 函数执行实际的挂载操作。该函数则是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 retval = 0;
int mnt_flags = 0;
if ((flags & MS_MGC_MSK) == MS_MGC_VAL)
flags &= ~MS_MGC_MSK;
if (data_page)
((char *)data_page)[PAGE_SIZE - 1] = 0;
retval = user_path(dir_name, &path);
if (retval)
return retval;
retval = security_sb_mount(dev_name, &path,
type_page, flags, data_page);
if (!retval && !may_mount())
retval = -EPERM;
if (retval)
goto dput_out;
if (!(flags & MS_NOATIME))
mnt_flags |= MNT_RELATIME;
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_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;
}
flags &= ~(MS_NOSUID | MS_NOEXEC | MS_NODEV | MS_ACTIVE | MS_BORN |
MS_NOATIME | MS_NODIRATIME | MS_RELATIME| MS_KERNMOUNT |
MS_STRICTATIME);
if (flags & MS_REMOUNT)
retval = do_remount(&path, flags & ~MS_REMOUNT, mnt_flags,
data_page);
else if (flags & MS_BIND)
retval = do_loopback(&path, dev_name, flags & MS_REC);
else if (flags & (MS_SHARED | MS_PRIVATE | MS_SLAVE | MS_UNBINDABLE))
retval = do_change_type(&path, flags);
else if (flags & MS_MOVE)
retval = do_move_mount(&path, dev_name);
else
retval = do_new_mount(&path, type_page, flags, mnt_flags,
dev_name, data_page);
dput_out:
path_put(&path);
return retval;
}
从以上代码可见,在之前的59行的代码大部分用于flags标志位的判断和设置。在59-70行代码则是真正执行的挂载逻辑。从代码可知,在do_mount()函数中,将挂载分成了5种情况:
(1)如果是重新挂载:将调用do_remount() 执行挂载操作。
(2)如果是MS_BIND标志:则调用do_loopback() 函数。
(3)如果是MS_SHARED | MS_PRIVATE | MS_SLAVE | MS_UNBINDABLE标志:则调用do_change_type() 函数。
(4)如果是MS_MOVE标志:则调用do_move_mount() 函数。
(5)如果都不是以上四种挂载情况,那么就会调用do_new_mount() 函数创建新的挂载。
在实际挂载中,小生假设运行逻辑中do_new_mount() 函数将被执行,下文将分析该函数。
do_new_mount() 函数功能是:为用户空间创建一个新的挂载和请求,并将其添加到名称空间挂载树上。该函数定义如下:
static int do_new_mount(struct path *path, const char *fstype, int flags,
int mnt_flags, const char *name, void *data)
{
struct file_system_type *type;
struct user_namespace *user_ns = current->nsproxy->mnt_ns->user_ns;
struct vfsmount *mnt;
int err;
if (!fstype)
return -EINVAL;
type = get_fs_type(fstype);
if (!type)
return -ENODEV;
if (user_ns != &init_user_ns) {
if (!(type->fs_flags & FS_USERNS_MOUNT)) {
put_filesystem(type);
return -EPERM;
}
if (!(type->fs_flags & FS_USERNS_DEV_MOUNT)) {
flags |= MS_NODEV;
mnt_flags |= MNT_NODEV | MNT_LOCK_NODEV;
}
if (type->fs_flags & FS_USERNS_VISIBLE) {
if (!fs_fully_visible(type, &mnt_flags))
return -EPERM;
}
}
mnt = vfs_kern_mount(type, flags, name, data);
if (!IS_ERR(mnt) && (type->fs_flags & FS_HAS_SUBTYPE) &&
!mnt->mnt_sb->s_subtype)
mnt = fs_set_subtype(mnt, fstype);
put_filesystem(type);
if (IS_ERR(mnt))
return PTR_ERR(mnt);
err = do_add_mount(real_mount(mnt), path, mnt_flags);
if (err)
mntput(mnt);
return err;
}
从以上代码可知,do_new_mount() 函数会调用vfs_kern_mount() 函数创建文件系统的挂载,然后再调用do_add_mount() 函数将创建的挂载添加到namespace的挂载树上。
vfs_kern_mount() 函数定义如下:
struct vfsmount *
vfs_kern_mount(struct file_system_type *type, int flags, const char *name, void *data)
{
struct mount *mnt;
struct dentry *root;
if (!type)
return ERR_PTR(-ENODEV);
mnt = alloc_vfsmnt(name);
if (!mnt)
return ERR_PTR(-ENOMEM);
if (flags & MS_KERNMOUNT)
mnt->mnt.mnt_flags = MNT_INTERNAL;
root = mount_fs(type, flags, name, data);
if (IS_ERR(root)) {
mnt_free_id(mnt);
free_vfsmnt(mnt);
return ERR_CAST(root);
}
mnt->mnt.mnt_root = root;
mnt->mnt.mnt_sb = root->d_sb;
mnt->mnt_mountpoint = mnt->mnt.mnt_root;
mnt->mnt_parent = mnt;
lock_mount_hash();
list_add_tail(&mnt->mnt_instance, &root->d_sb->s_mounts);
unlock_mount_hash();
return &mnt->mnt;
}
do_add_mount() 函数定义如下:
static int do_add_mount(struct mount *newmnt, struct path *path, int mnt_flags)
{
struct mountpoint *mp;
struct mount *parent;
int err;
mnt_flags &= ~MNT_INTERNAL_FLAGS;
mp = lock_mount(path);
if (IS_ERR(mp))
return PTR_ERR(mp);
parent = real_mount(path->mnt);
err = -EINVAL;
if (unlikely(!check_mnt(parent))) {
if (!(mnt_flags & MNT_SHRINKABLE))
goto unlock;
if (!parent->mnt_ns)
goto unlock;
}
err = -EBUSY;
if (path->mnt->mnt_sb == newmnt->mnt.mnt_sb &&
path->mnt->mnt_root == path->dentry)
goto unlock;
err = -EINVAL;
if (d_is_symlink(newmnt->mnt.mnt_root))
goto unlock;
newmnt->mnt.mnt_flags = mnt_flags;
err = graft_tree(newmnt, parent, mp);
unlock:
unlock_mount(mp);
return err;
}
do_add_mount() 函数的核心操作是:
1、把挂载描述符加入到散列表中。
2、将挂载描述符加入父亲的孩子链表中。如下代码片段(出自/fs/namespace.c文件中commit_tree() 函数):
list_add_tail(&head, &mnt->mnt_list);
list_for_each_entry(m, &head, mnt_list)
m->mnt_ns = n;
结尾
? 总结一下,mount系统调用主要执行的流程如下:
(1)调用user_path() 函数,根据目录名称找到挂载描述符和dentry实例。
(2)调用get_fs_type() 函数,根据文件系统类型的名称查找对应的file_system_type 实例。
(3)调用alloc_vfsmount() 函数,分配挂载描述符内存空间,并填充相关参数。
(4)在mount_fs() 函数中,调用对应文件系统类型下注册的mount回调函数,执行文件系统挂载操作,读取并解析超级块。(该函数与具体文件系统相关)
(5)把挂载描述符添加到超级块的挂载实例链表中。
(6)把挂载描述符加入父亲的孩子链表。
由于小生知识与精力有限,如果文章存在不妥的地方,欢迎批评指正或与我讨论,哈哈!
|