Linux内核笔记-VFS

Linux 文件系统有着复杂的结构,仅仅概念就有file,dentry,inode,vfsmount,super_block等等,还有他们之间的关联,细节虽然繁多,但只要抓住主干框架,其他的细节通过阅读源码都可顺利成章的理解。这里Linux运行平台版本和分析的源码都以linux-2.6.36.2作为基准,其他相近版本的差别很小。也很容易理解。这里先归纳一下 Linux 文件系统的概念和机制。

概念

之前已经提到,Linux 文件系统的概念繁多,这里做一些简短的介绍,不会清楚解释每一个细节,只会去“感性”的解释,程序虽然是一种很理性化的东西,但想以一个更高的层次去看他,必须对其有所“感觉”。

分层

Linux 秉承了 Unix 的 KISS 原则:Keep It Simple, Stupid!简洁的一个最主要体现就是:万物皆文件。不论是虚拟文件,或是驱动设备,对用户态程序而言都与操作普通文件方式无异(也有少数设备操作例外,比如网络设备)。这种机制的实现得益于VFS,VFS作为一个中间层,抽象底层驱动的差异。万物皆文件的思想与Java中的万物皆对象的思想相同,都是面向对象的体现,所不同的是,万物皆文件是从更高层次进行抽象,VFS仅仅为一个接口层,底层的文件系统或者设备驱动都是接口的实现,这便是多态的思想,只不过C++的类多态机制是细节上的多态,而Linux的多态却是系统级别的多态;类级别的多态容易跌入细节的泥沼,而系统级别的面向对象实现可以合理的为体系分层,使实现更为清晰,虽然面向对象,但仍保持 Simple,Linux 虽然使用纯 C 实现,却是体现面向对象思想的典范。

VFS 作为一个接口层,需要对上层提供统一的调用接口,同时又要为底层提供实现接口,以致VFS需要引入一些概念。

inode

内核使用 inode 表示文件信息数据,最重要的是存储了文件操作的集合 file_operations 和 inode 集合 inode_operations ,这里的操作集合全部是函数指针,作为抽象接口,具体函数由底层设备提供。file_operations 实现文件内容操作,如read、write等等,inode_operations 实现 inode 本身操作,如mkdir, link 等等,另外,inode并非硬盘中的实体,仅存在于内存。

file

inode 对所有进程可见,仍有一套进程相关的结构,files_struct保存打开文件信息,其中包含 file 结构,file 结构包含文件读写偏移。另外进程还保存一套文件系统信息 fs_struct。

dentry

dentry 为目录缓存,包含目录对应 inode,以及 dentry_operations,以及 d_subdirs 子文件子目录列表。

file_system_type

file_system_type 用来表示文件系统,比如 ext2,ext3 结构,所有文件系统组成链表,新文件系统使用 register_filesystem 向内核注册,函数只是简单的把 file_system_type 插入链表,其中最关键的成员就是 get_sb,得到 super_block 结构。

super_block

super_block 储存了文件系统本身和挂载点有关信息,设备块大小,所有inode链表。

vfsmount

对于挂接点,还需保存一个 vfsmount 结构,保存了挂接信息,每一个挂接点都包含一个 vfsmount 结构实体。vfsmount 和 super_block 对与挂接点来说,都是唯一的。

path

path 包含 vfsmount 和 dentry 结构。

nameidata

nameidata 结构用于查找,想查找函数传递参数,并保存结果,包括 dentry 和 vfsmount 结构。

sys_mount 实现

sys_mount 展现了文件系统加载的所有细节,为学习文件系统提供一个很好的切入点,首先来看 sys_mount 的函数原型:

# 源码中的原型
SYSCALL_DEFINE5(mount, char __user *, dev_name, char __user *, dir_name,
		char __user *, type, unsigned long, flags, void __user *, data)

# 根据宏推演开来展现原型
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)

#define SYSCALL_DEFINEx(x, sname, ...)				\
	__SYSCALL_DEFINEx(x, sname, __VA_ARGS__

#define __SYSCALL_DEFINEx(x, name, ...)					\
	asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__)

#define __SC_DECL1(t1, a1)	t1 a1
#define __SC_DECL2(t2, a2, ...) t2 a2, __SC_DECL1(__VA_ARGS__)
#define __SC_DECL3(t3, a3, ...) t3 a3, __SC_DECL2(__VA_ARGS__)
#define __SC_DECL4(t4, a4, ...) t4 a4, __SC_DECL3(__VA_ARGS__)
#define __SC_DECL5(t5, a5, ...) t5 a5, __SC_DECL4(__VA_ARGS__)

# 最终展开宏之后的原型为
asmlinkage long sys_mount(char __user * dev_name, char __user * dir_name,	\
		char __user * type, unsigned long flags, void __user * data)

参数 dev_name 是设备名,如/dev/sda1;dir_name 是目标挂接点路径,如/mnt/target/;type 是文件系统类型,如ext2、ext3等;flags 是mount标志;data是附加选项值。

由于sys_mount的字符串参数都是 __user 内存,所以在 sys_mount 开始处,首先使用copy_mount_string等函数把用户空间的字串复制到内核空间。之后再去调用 do_mount 完成具体的操作。do_mount 的原型与 sys_mount 相对应,仅是参数全部已经转换成内核空间的地址。

long do_mount(char *dev_name, char *dir_name, char *type_page,
		  unsigned long flags, void *data_page)
{
	/** 省略无关部分 */

	/* ... and get the mountpoint */
	retval = kern_path(dir_name, LOOKUP_FOLLOW, &path);
	if (retval)
		return retval;

	retval = security_sb_mount(dev_name, &path,
				   type_page, flags, data_page);
	if (retval)
		goto dput_out;

	/** 省略flags判断分支 */

		retval = do_new_mount(&path, type_page, flags, mnt_flags,
				      dev_name, data_page);
dput_out:
	path_put(&path);
	return retval;
}

可以看到 do_mount 的主干是非常清晰的,开始省去一些参数校验代码,紧接着调用 kern_path 根据 dir_name 指示的路径名获取其 path 结构,security_sb_mount 做一些安全校验工作,如果没有设置,这个函数为空,否则去调用 security_ops->sb_mount 成员实现。这里不去研究 LSM 安全模块,省去这个函数的分析。紧接着根据flags进行判断,决定是重新加载,或者移动,或者更改文件系统,这里仅跟加载新的 mount 作为分析对象,其他部分类似。最后使用path_put回收path资源。

这里可以看书,do_mount其实也只是一个分发函数,首先获得path,然后调用各分支的mount函数,接下来分析 kern_pathdo_new_mount 函数。

int kern_path(const char *name, unsigned int flags, struct path *path)
{
	struct nameidata nd;
	int res = do_path_lookup(AT_FDCWD, name, flags, &nd);
	if (!res)
		*path = nd.path;
	return res;
}

kern_path 很明显,构建一个 nameidata 对象,使用 do_path_lookup 函数由路径名 name 得到 nameidata 对象,path 仅是 nameidata 对象的一个成员,把这个成员返回给上层。也就是 kern_path 调用了更为广泛的 do_path_lookup

static int do_path_lookup(int dfd, const char *name,
				unsigned int flags, struct nameidata *nd)
{
	int retval = path_init(dfd, name, flags, nd);
	if (!retval)
		retval = path_walk(name, nd);
	if (unlikely(!retval && !audit_dummy_context() && nd->path.dentry &&
				nd->path.dentry->d_inode))
		audit_inode(name, nd->path.dentry);
	if (nd->root.mnt) {
		path_put(&nd->root);
		nd->root.mnt = NULL;
	}
	return retval;
}

可以看到,do_path_lookup 又去调用了 path_initpath_walk 函数实现功能,path_init 代码很易懂,根据路径名 name 获取当前相对 name 的 nameidata 结构,如果 name 是以/开始的绝对路径,nameidata->path 即为根,否则,如果name是相对路径,nameidata->path 为当前路径的path,也就是不管name是相对路径还是绝对的,对此时的 nameidata 来讲都可以认为是相对路径。

path_walk 便是根据 nameidata 找到相对路径 name 的 path 节点。path_walk 本身也没有做这件事,只是把参数又传递给 link_path_walk 去具体操作,link_path_walk 的操作便复杂了许多,需要处理各种奇怪的路径名,这里先无视这段复杂的代码,继续向后分析,至少已经知道 link_path_walk 所实现的功能,不怕他写的复杂,就怕你猜不到他在做什么。

如此便已清楚 kern_path 的功能,接下来看 do_new_mount 的操作:

static int do_new_mount(struct path *path, char *type, int flags,
			int mnt_flags, char *name, void *data)
{
	struct vfsmount *mnt;

	if (!type)
		return -EINVAL;

	/* we need capabilities... */
	if (!capable(CAP_SYS_ADMIN))
		return -EPERM;

	lock_kernel();
	mnt = do_kern_mount(type, flags, name, data);
	unlock_kernel();
	if (IS_ERR(mnt))
		return PTR_ERR(mnt);

	return do_add_mount(mnt, path, mnt_flags, NULL);
}

do_new_mount 也分为两步,第一步调用 do_kern_mount 创建 vfsmount 挂接点结构,之后调用 do_add_mount 挂接 mnt 到文件系统树。再来一步一步分析,从 do_kern_mount 开始:

struct vfsmount *
do_kern_mount(const char *fstype, int flags, const char *name, void *data)
{
	struct file_system_type *type = get_fs_type(fstype);
	struct vfsmount *mnt;
	if (!type)
		return ERR_PTR(-ENODEV);
	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);
	return mnt;
}

do_kern_mount 从主干来看,首先调用 get_fs_type 得到文件系统的 file_system_type 结构,之后调用 vfs_kern_mount 生成 vfsmount 结构。get_fs_type 所作工作比较简单,直接调用__get_fs_type函数从当前已加载的 file_systems 查找对应文件系统,如果未找到,便以文件系统名为模块名调用 request_module 加载模块,如果加载成功,对应模块会调用 register_filesystem 把自己的 file_system_type 结构插入 file_systems 链表,get_fs_type会再次进行查找。如果查找失败,便返回 NULL。

在得到 file_system_type 文件系统结构后,调用 vfs_kern_mount 创建 mnt 挂接点。

struct vfsmount *
vfs_kern_mount(struct file_system_type *type, int flags, const char *name, void *data)
{
	/** 省去无关代码 */
	mnt = alloc_vfsmnt(name);
	if (!mnt)
		goto out;

	if (flags & MS_KERNMOUNT)
		mnt->mnt_flags = MNT_INTERNAL;

	/** 省去无关代码 */

	error = type->get_sb(type, flags, name, data, mnt);
	if (error < 0) 		goto out_free_secdata; 	/** 省去 LSM 安全模块代码 */ 	mnt->mnt_mountpoint = mnt->mnt_root;
	mnt->mnt_parent = mnt;
	up_write(&mnt->mnt_sb->s_umount);
	free_secdata(secdata);
	return mnt;

	/** 省去错误清理代码 */

	return ERR_PTR(error);
}

经过抽取函数的骨干,可以清楚看到函数所作操作,首先 alloc_vfsmnt 为 vfsmount 结构分配空间,并初始化成员。再调用对应文件系统的 file_system_type 成员 get_sb 填充 mnt结构。分配空间并初始化代码并无关键之处,而 get_sb 函数却是相应文件系统的实现,比如 ext2 的 ext2_get_sb 函数。

先不去研究文件系统的具体实现,这与VFS无关,do_kern_mount 便以分析完毕,所作功能便是为 vfsmount 分配空间,再去调用文件系统的 get_sb 实现填充 vfsmount 结构。

接下来查看 do_add_mount 的实现:

int do_add_mount(struct vfsmount *newmnt, struct path *path,
		 int mnt_flags, struct list_head *fslist)
{
	int err;

	mnt_flags &= ~(MNT_SHARED | MNT_WRITE_HOLD | MNT_INTERNAL);

	down_write(&namespace_sem);
	/* Something was mounted here while we slept */
	while (d_mountpoint(path->dentry) &&
	       follow_down(path))
		;
	err = -EINVAL;
	if (!(mnt_flags & MNT_SHRINKABLE) && !check_mnt(path->mnt))
		goto unlock;

	/* Refuse the same filesystem on the same mount point */
	err = -EBUSY;
	if (path->mnt->mnt_sb == newmnt->mnt_sb &&
	    path->mnt->mnt_root == path->dentry)
		goto unlock;

	err = -EINVAL;
	if (S_ISLNK(newmnt->mnt_root->d_inode->i_mode))
		goto unlock;

	newmnt->mnt_flags = mnt_flags;
	if ((err = graft_tree(newmnt, path)))
		goto unlock;

	if (fslist) /* add to the specified expiration list */
		list_add_tail(&newmnt->mnt_expire, fslist);

	up_write(&namespace_sem);
	return 0;

unlock:
	up_write(&namespace_sem);
	mntput(newmnt);
	return err;
}

函数开始调用 d_mountpointfollow_down 查看是否已经有 mnt 挂接到 path 位置,之后便调用 graft_tree 把 newmnt 挂接到 path 位置:

static int graft_tree(struct vfsmount *mnt, struct path *path)
{
	int err;
	if (mnt->mnt_sb->s_flags & MS_NOUSER)
		return -EINVAL;

	/** 检测是否文件夹 */
	if (S_ISDIR(path->dentry->d_inode->i_mode) !=
	      S_ISDIR(mnt->mnt_root->d_inode->i_mode))
		return -ENOTDIR;

	err = -ENOENT;
	mutex_lock(&path->dentry->d_inode->i_mutex);

	/** 检测路径的标志,是否允许 mount */
	if (cant_mount(path->dentry))
		goto out_unlock;

	/** 挂接 mnt */
	if (!d_unlinked(path->dentry))
		err = attach_recursive_mnt(mnt, path, NULL);
out_unlock:
	mutex_unlock(&path->dentry->d_inode->i_mutex);
	return err;
}

看来这里的关键点便是 attach_recursive_mnt 函数,它挂接 mnt 到 path 路径上,甚至内核源码本身也为这个函数使用了很大篇幅的注释。此函数调用 mnt_set_mountpoint ,增加目标挂接点 mounted 计数,最后调用 commit_tree 把当前 mnt 添加至全局散列表和父文件系统的子文件系统链。

Built with Hugo
主题 StackJimmy 设计