【linux kernel】mount系统调用剖析

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;

	/* Discard magic */
	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;
	//根据fstype参数获取文件系统的类型,返回struct file_system_type
	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;
		}
		/* Only in special cases allow devices from mounts
		 * created outside the initial user namespace.
		 */
		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挂载描述符的内存空间
	mnt = alloc_vfsmnt(name);
	if (!mnt)
		return ERR_PTR(-ENOMEM);

	if (flags & MS_KERNMOUNT)
		mnt->mnt.mnt_flags = MNT_INTERNAL;
	
    //在mount_fs()函数中,将调用对应文件系统类型注册的mount函数挂载文件系统,读取并且解析超级块并返回对应的struct dentry。
	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))) {
		/* that's acceptable only for automounts done in private ns */
		if (!(mnt_flags & MNT_SHRINKABLE))
			goto unlock;
		/* ... and for those we'd better have mountpoint still alive */
		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)把挂载描述符加入父亲的孩子链表。


由于小生知识与精力有限,如果文章存在不妥的地方,欢迎批评指正或与我讨论,哈哈!


搜索关注【嵌入式小生】wx公众号获取更多精彩内容>>>>
请添加图片描述

你可能感兴趣的:(小生聊【linux,kernel】,linux,linux,kernel,mount,文件系统,C)