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_path
和 do_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_init
和 path_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_mountpoint
和 follow_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 添加至全局散列表和父文件系统的子文件系统链。