Linux文件系统
Linux文件系统
Unix操作系统的核心概念之一就是,几乎每个资源都可以表示为一个文件,并且Linux继承了这种观点.因此文件是内核世界中非常重要的成员,文件的表示设计了相当多工作量.一切皆文件的理念反应在Linux文件类型上就是:普通文件,目录文件(即文件夹),设备文件,链接文件,管道文件,套接字文件等等.
文件系统整体的框架结构如下图所示:

文件系统一般分为以下三种:
-
Disk-based Filesystem
基于磁盘的文件系统,是在非易失介质上存储文件的经典方法.用以在多次会话之间保持文件的内容.实际上大多数文件系统都是由此演变而来.比如一些众所周知的文件系统:ext2 FAT iso9660等.所有这些文件系统都是用面向块的介质.从文件系统角度来看.底层设备无非是存储块组成的一个列表,文件系统相当于
-
Virtual Filesystem
虚拟文件系统在内核中生成,是一种使用户应用程序与用户通信的方法.proc文件系统是这一类的最佳示例.他不需要在任何种类的硬件设备上分配存储空间.相反,内核建立了一个层次化的文件结构,其中的项包含了与系统特定部分相关的信息.举例来说,文件/proc/version在ls命令查看时,标称长度为0字节:
[15:17:25 /proc ]$ ls -l version
-r--r--r-- 1 root root 0 6月 28 15:17 version但如果使用cat输出文件内容,内核会产生一个有关系统处理器的信息列表.这列表从内核内存中的数据结构提取而来:
[15:17:36 /proc ]$ cat version
Linux version 4.2.0-27-generic (buildd@lcy01-23) (gcc version 4.8.2 (Ubuntu 4.8.2-19ubuntu1) ) #32~14.04.1-Ubuntu SMP Fri Jan 22 15:32:26 UTC 2016 -
Network Filesystem
网络文件系统是基于磁盘的文件系统和虚拟文件系统之间的折中.这种文件系统允许访问另一台计算机上的数据,该计算机通过网络连接到本地计算机.在这种情况下,数据实际上存储在一个不同系统的硬件设备上.这意味着内核无需关注文件存取/数据组织和硬件通信的细节,这些由远程计算机的内核处理.对此类文件系统中文件的操作都是通过网络链接进行.
由于vfs抽象层的存在,用户空间进程不会看到本地文件系统与网络文件系统之间的区别.
文件系统中的中间商-VFS
想想一下,我们要买水果,不用专门去果园,菜市场就可以.买车,不用去生产汽车的工厂,4s店就可以.菜市场商户/4s店这些都是中间商.虽然我们花了更多的钱被中间商赚去.但是省下了不少麻烦.不得不说的是,中间商虽然黑,但毕竟给我们的生活带来了方便.
文件系统中也有一个"中间商",它就是VFS(Virtual File System)文件系统.有了它,用户进程不用关心底层文件系统格式是f2fs,ext4还是其他.只需要调用标准接口 read() write() chmod()等等来操作文件.VFS会完成标准接口到具体操作的环节.
VFS虚拟文件系统(也称为虚拟文件系统交换机),是内核中的软件层,它为用户空间程序提供文件系统接口。 它还在内核中提供了一个抽象,允许不同的文件系统实现共存.
VFS一方面提供一种操作文件、目录及其他对象的统一方法,使用户进程不必知道文件系统的细节。另一方面,VFS提供的各种方法必须和具体文件系统的实现达成一种妥协.
为此,VFS中定义了一个通用文件模型,以支持文件系统中对象(或文件)的统一视图.
用户进程通过VFS系统调用来操作文件,如 read() write() chmod()...
文件系统数据结构
我们都知道新装的磁盘一般需要分区,分区完毕后还需要进行格式化,之后操作系统才能够使用这个分区。 为什么需要进行『格式化』呢?这是因为每种操作系统所配置的文件属性/权限并不相同, 为了存放这些文件所需的数据,因此就需要将分区进行格式化,以成为操作系统能够利用的文件系统格式(filesystem)。
这个过程我们可以理解成,我新建了2两个仓库(A/B)非常非常大.A库房用来放图书,B仓库用来放黄金,C.... 图书需要书架来存放.而黄金当然不能用书架,需要一个个的保险箱来存储. 所以AB库房需要不同的装修风格(文件系统格式).每个库房只能被装修(格式化)成一种装修风格(文件系统).
那么我们这个仓库是如何运行的呢?
以存放黄金的B库房为例.首先每个保险箱(block)都有相应的权限信息(所有者/时间/大小/存放位置等).这些信息存放在一个叫inode的账本里(账本有好多页,每页对应一个inode).另外,还有一个账本(superblock)用来存放整个仓库的整体信息,包括保险箱总量,使用量,剩余量等.每页账本和相应的保险箱都有对应的编号.
小小总结一下,这三个数据的意义如下:
-
superblock:记录此 filesystem 的整体信息,包括inode/block的总量、使用量、剩余量, 以及文件系统的格式与相关信息等;
-
inode:记录元数据(访问权限,上次修改日期等)和指向文件数据的指针.但是inode并不包括文件名.
一个文件占用一个inode,同时记录此文件的数据所在的 block 号码;
-
block:实际记录文件的内容,若文件太大时,会占用多个 block 。
由于每个 inode 与 block 都有编号,而每个文件都会占用一个 inode ,inode 内则有文件数据放置的 block 号码。 因此,我们可以知道的是,如果能够找到文件的 inode 的话,那么自然就会知道这个文件所放置数据的 block 号码, 当然也就能够读出该文件的实际数据了。
我们将 inode 与 block 区块用图解来说明一下,如下图所示,文件系统先格式化出 inode 与 block 的区块,假设某一个文件的属性与权限数据是放置到 inode 3 号(下图白色方格内),而这个 inode 记录了文件数据的实际放置点为 6, 7, 10, 14 这四个 block 号码,此时我们的操作系统就能够据此来排列磁盘的阅读顺序,可以一口气将四个 block 内容读出来! 那么数据的读取就如同下图中的箭头所指定的模样了。

super_block
一个文件系统对应一个super_block
**super_block 也有一个全局的链表static LIST_HEAD(super_blocks)在fs/super.c中定义.
struct super_block {
struct list_head s_list; /* Keep this first */
dev_t s_dev; /* search index; _not_ kdev_t */
unsigned char s_blocksize_bits;/* 对s_blocksize取2为底的对数 */
unsigned long s_blocksize; /* 单位字节,执行了文件系统的块长度 */
loff_t s_maxbytes; /* Max file size 单个文件最大大小*/
struct file_system_type *s_type; /* 指向file_system_type实例 */
const struct super_operations *s_op; /* */
const struct dquot_operations *dq_op;
const struct quotactl_ops *s_qcop;
const struct export_operations *s_export_op;
unsigned long s_flags;
unsigned long s_iflags; /* internal SB_I_* flags */
unsigned long s_magic;
/*s_root:
* 将超级块与全局根目录的dentry关联起来,
* 处理文件系统对象的代码进场需要检查文件系统是否已经装载,s_root可用于该目的.
* 如果为NULL,则该文件系统是一个伪文件系统,只在内部可见.否则,该文件系统在用户控件是可见的.
* */
struct dentry *s_root;
struct rw_semaphore s_umount;
int s_count;
atomic_t s_active;
...
} __randomize_layout;
dentry
由于块设备速度较慢,每次去查找inode需要较长时间.linux使用目录项(dentry)缓存来快速访问此前的查找操作结果.这提供了一种非常快速的查找机制,可将路径名(filename)转换为特定的dentry。 Dentries存在于RAM中,永远不会保存到光盘:它们仅用于提高性能。
dentry缓存旨在成为整个文件空间的视图。 由于大多数计算机无法同时在RAM中的生成所有dentries,因此缺少一些缓存。 对于不存在的dentry.系统会创建一个,然后加载inode。 这是通过查找inode来完成的。
struct dentry {
/* RCU lookup touched fields */
unsigned int d_flags; /* protected by d_lock */
seqcount_t d_seq; /* per dentry seqlock */
struct hlist_bl_node d_hash; /* lookup hash list */
struct dentry *d_parent; /* parent directory */
struct qstr d_name;
struct inode *d_inode; /* Where the name belongs to - NULL is
* negative */
unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */
/* Ref lookup also touches following */
struct lockref d_lockref; /* per-dentry lock and refcount */
const struct dentry_operations *d_op;
struct super_block *d_sb; /* The root of the dentry tree */
unsigned long d_time; /* used by d_revalidate */
void *d_fsdata; /* fs-specific data */
union {
struct list_head d_lru; /* LRU list */
wait_queue_head_t *d_wait; /* in-lookup ones only */
};
struct list_head d_child; /* child of parent list */
struct list_head d_subdirs; /* our children */
/*
* d_alias and d_rcu can share memory
*/
union {
struct hlist_node d_alias; /* inode alias list */
struct hlist_bl_node d_in_lookup_hash; /* only for in-lookup ones */
struct rcu_head d_rcu;
} d_u;
} __randomize_layout;
file_system_type
struct file_system_type {
const char *name;
int fs_flags;
/* 文件系统必须在物理设备上*/
#define FS_REQUIRES_DEV 1
/* mount此文件系统时,需要使用二进制数据结构的mount data
* (如每个位域都有固定的位置和意义),常见的nfs使用这种mount data
*/
#define FS_BINARY_MOUNTDATA 2
/* 系统含有子类型,最常见的就是FUSE,FUSE本不是真正的文件系统,
* 所以要通过子文件系统类型来区别,通过FUSE接口实现不同文件系统
*/
#define FS_HAS_SUBTYPE 4
/* Can be mounted by userns root,每次挂载后都是不同的user namespace,如devpts
*/
#define FS_USERNS_MOUNT 8
#define FS_RENAME_DOES_D_MOVE 32768 /* FS will handle d_move() during rename() internally. */
struct dentry *(*mount) (struct file_system_type *, int,
const char *, void *); /* 用户调用sys_mount挂载某一文件系统时,最终会调到该回调函数*/
struct dentry *(*mount2) (struct vfsmount *, struct file_system_type *, int,
const char *, void *);
void *(*alloc_mnt_data) (void);
void (*kill_sb) (struct super_block *); /* 删除内存中的superblock,在卸载文件系统时使用*/
struct module *owner; /* 指向实现这个 文件系统的模块.通常为THIS_MOUDULE宏*/
struct file_system_type * next; /* 指向文件系统链表的下一个文件系统类型*/
struct hlist_head fs_supers; /* 此文件系统类型的文件系统超级块结构串都在这个表头下*/
...
};
每种注册到内核的文件系统以struct file_system_type结构表示,每种文件系统中都有一个链表,指向所有属于该类型的文件系统的超级块.
这里有一个全局变量,用来从存储所有已经注册过的filesystemstatic struct file_system_type *file_systems;
vfsmount
当一个文件系统挂载到内核文件系统的目录树上,会生成一个挂载点.用来管理所挂载的文进系统信息.该挂载点用一个struct vfsmount结构表示.另外,文件系统中还有一个struct mount成员,struct mount代表着一个mount实例(一次真正挂载对应一个mount实例),其中struct vfsmount定义的mnt成员是它最核心的部分。过去没有stuct mount,mount和vfsmount的成员都在vfsmount里,现在linux将vfsmount改作mount结构体,并将mount中mnt_root, mnt_sb, mnt_flags成员移到vfsmount结构体中了。这样使得vfsmount的内容更加精简,在很多情况下只需要传递vfsmount而已。
struct vfsmount {
struct dentry *mnt_root; /* root of the mounted tree */
struct super_block *mnt_sb; /* pointer to superblock */
int mnt_flags;
void *data;
} __randomize_layout;
struct mount {
/*用于链接到全局已挂载文件系统的链表*/
struct hlist_node mnt_hash;
/*指向此文件系统的挂载点所属的文件系统,即父文件系统 */
struct mount *mnt_parent;
/* 指向此文件系统挂载点的dentry*/
struct dentry *mnt_mountpoint;
/*指向此文件系统vfsmount实例*/
struct vfsmount mnt;
union {
struct rcu_head mnt_rcu;
struct llist_node mnt_llist;
};
#ifdef CONFIG_SMP
struct mnt_pcp __percpu *mnt_pcp;
#else
int mnt_count;
int mnt_writers;
#endif
/*挂载在此文件系统下的所有子文件系统的链表的表头,下面的节点都是mnt_child */
struct list_head mnt_mounts; /* list of children, anchored here */
/* 链接到被此文件系统所挂的父文件系统的mnt_mounts上
* 注意与上边 *mnt_parent的区别
* */
struct list_head mnt_child; /* and going through their mnt_child */
/* 链接到sb->s_mounts上的一个mount的实例 */
struct list_head mnt_instance; /* mount instance on sb->s_mounts */
/* 设备名,如/dev/hda1 */
const char *mnt_devname; /* Name of device e.g. /dev/dsk/hda1 */
/* 链接到进程namespace中已挂载的文件系统中,表头为mnt_namespace的list域*/
struct list_head mnt_list;
/* 链接到一些文件系统转悠的过期链表,如NFS CIFS等*/
struct list_head mnt_expire; /* link in fs-specific expiry list */
/* 链接到共享挂载的循环链表*/
struct list_head mnt_share; /* circular list of shared mounts */
/* 此文件系统的的slave mount链表的表头*/
struct list_head mnt_slave_list;/* list of slave mounts */
/* 链接到master文件系统的mnt_slave_list*/
struct list_head mnt_slave; /* slave list entry */
/* 指向此文件系统的master文件系统*/
struct mount *mnt_master; /* slave is on master->mnt_slave_list */
/* 指向包含这个文件系统的namespace*/
struct mnt_namespace *mnt_ns; /* containing namespace */
/* 此文件系统挂载的地点*/
struct mountpoint *mnt_mp; /* where is it mounted */
/* 具有相同挂载点的文件系统*/
struct hlist_node mnt_mp_list; /* list mounts with the same mountpoint */
struct list_head mnt_umounting; /* list entry for umount propagation */
#ifdef CONFIG_FSNOTIFY
struct fsnotify_mark_connector __rcu *mnt_fsnotify_marks;
__u32 mnt_fsnotify_mask;
#endif
int mnt_id; /* mount identifier */
int mnt_group_id; /* peer group identifier */
int mnt_expiry_mark; /* true if marked for expiry */
struct hlist_head mnt_pins;
struct fs_pin mnt_umount;
struct dentry *mnt_ex_mountpoint;
} __randomize_layout;
各个数据结构的概览如下:
其中红色字体的链表为内核中的全局链表
其他成员
path
path.h中定义
path作用
- 保存了文件名和inode之间的关联: dentry
- 文件所在文件系统的有关信息: mnt
struct path {
struct vfsmount *mnt;
struct dentry *dentry;
} __randomize_layout;
nameidata
此结构用来向查找函数传递参数,并保存查找结果
struct nameidata {
struct path path;
struct qstr last;
struct path root;
struct inode *inode; /* path.dentry.d_inode */
unsigned int flags;
unsigned seq, m_seq;
int last_type;
unsigned depth;
int total_link_count;
struct saved {
struct path link;
struct delayed_call done;
const char *name;
unsigned seq;
} *stack, internal[EMBEDDED_LEVELS];
struct filename *name;
struct nameidata *saved;
struct inode *link_inode;
unsigned root_seq;
int dfd;
} __randomize_layout;
filename
struct filename {
const char *name; /* pointer to actual string */
const __user char *uptr; /* original userland pointer */
struct audit_names *aname;
int refcnt;
const char iname[];
};
mountpoint
/* mountpoint 当前文件系统的装载点在其父目录中的dentry结构 */
struct mountpoint {
struct hlist_node m_hash;
struct dentry *m_dentry;
struct hlist_head m_list;
int m_count;
};
task_struct
进程描述符
struct task_struct {
……
/* filesystem information */
struct fs_struct fs;
/ open file information */
struct files_struct files;
/ namespaces */
struct nsproxy *nsproxy;
……
}
fs_struct fs 成员指向当前工作目录的文件系统信息.
file_struct files 成员指向了进程打开的文件的信息。
nsproxy 指向了进程所在的命名空间,其中包含了虚拟文件系统命名空间。
fs_struct
struct fs_struct {
int users;
spinlock_t lock;
seqcount_t seq;
int umask;
int in_exec;
struct path root, pwd;
} __randomize_layout;
成员含义:
- users: 共享同一fs_struct进程数目
- root: 指向整个文件系统的根path
- pwd:指向当前文件夹在当前文件系统的path
TODO: 待确定
files_struct
struct files_struct {
/*
* read mostly part
*/
atomic_t count;
bool resize_in_progress;
wait_queue_head_t resize_wait;
struct fdtable __rcu *fdt;
struct fdtable fdtab;
/*
* written part on a separate cache line in SMP
*/
spinlock_t file_lock ____cacheline_aligned_in_smp;
unsigned int next_fd;
unsigned long close_on_exec_init[1];
unsigned long open_fds_init[1];
unsigned long full_fds_bits_init[1];
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};
fdtable
struct fdtable {
unsigned int max_fds;
struct file __rcu **fd; /* current fd array */
unsigned long *close_on_exec;
unsigned long *open_fds;
unsigned long *full_fds_bits;
struct rcu_head rcu;
};
上述涉及的文件描述符和进程描述符关系图如下

VFS对象的操作
文件系统注册
int register_filesystem(struct file_system_type * fs)
{
int res = 0;
struct file_system_type ** p;
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;
}
无论文件系统是编译为模块或者编译到内核,文件系统注册都没有差别.
register_filesystem用来向内核注册文件系统.该函数的结构非常简单.所有文件系统都保存在一个单向链表中,各个文件系统的名称存储为字符串.以f2fs为例,注册流程如下:
static struct file_system_type f2fs_fs_type = {
.owner = THIS_MODULE,
.name = "f2fs",
.mount = f2fs_mount,
.kill_sb = kill_f2fs_super,
.fs_flags = FS_REQUIRES_DEV,
};
MODULE_ALIAS_FS("f2fs");

int register_filesystem(struct file_system_type * fs)
{
int res = 0;
struct file_system_type ** p;
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;
}
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;
}
文件系统挂载
文件系统挂载流程如下图所示:


下图所示可以让我们更好的理解挂载实例和文件系统树结构关系:
为了说明的方便,我们下面以这样的场景为例进行描述:
-
系统中有xfs, ext2和minix等若干文件系统模块
-
现有/dev/sda1和/dev/sdb1上存在xfs文件系统,/dev/sda2上为ext2文件系统,/dev/sdc1上为minix文件系统
-
将/dev/sda1挂载到/mnt/a上,将/dev/sdb1挂载到/mnt/b上,将/dev/sdc1不挂载
-
在第三步之后,将/dev/sda2也挂载到/mnt/a上。再将/dev/sda1同时挂载到/mnt/x上
file_system_type/super block/mount的关系
从file_system_type到super_block到mount实例的角度来看,上述关系大致是这样的:

系统中存在三个文件系统,也就有三种file_system_type被注册。sda1和sdb1都是xfs文件系统,所以xfs的file_system_type的fs_supers把这两个同为xfs文件系统的super_block串连在自己下面。sda2是ext2文件系统,所以它挂在ext2的file_system_type下。sdc1是minix文件系统,在sdc1的设备上存在着super_block信息,但是我们这里说的super_block是指内存中的,由于sdc1没有被挂载使用,所以没有它的super_block信息被读入内存。
从挂载实例上看,sda1和sda2都挂载到/mnt/a上,但是从上述关系中很难表述它们的区别,需要借助mount和dentry的关系来说明,下面再具体说明。sda1同时挂在了/mnt/a和/mnt/x上,所以它有两个挂载实例对应同一个super_block。sdc1没有被挂载,所以没有挂载实例和它对应。
mount_hashtable是一个全局挂载实例的哈希表,系统中除了根挂载点以外所有的挂载实例都记录在它下面,搜索一个mount实例时需要借助这个mount的父mount实例和dentry实例来计算的出。比如说/mnt上挂载着一个文件系统,/mnt/a和b上分别又挂载着文件系统,此时要想检索/mnt/a(或b),需要以/mnt上的挂载实例和/mnt/a(或者b)的dentry结构为依据计算hash数值从mount_hashtable上得到一个头指针,这个头指针下就是所有父文件系统是在/mnt上且挂载点是/mnt/a(或b)的挂载到/mnt/a或b下的mount实例。(这里其实有一个不太容易注意到的地方,到底什么情况下同一个父文件系统下的同一个dentry下会有多个挂载实例呢?我们不在本文讨论,以后有机会再讨论这个问题。)
上图可以看出file_system_type, super_block和mount实例之间的关系,但是不能看出来父子文件系统之间的相互关系。下面让我们看一下当一个文件系统挂载到另一个文件系统的子目录下的情况。
父子挂载点的挂载关系
假设在/mnt上挂载着一个文件系统,根据上面条件我们以/dev/sda1挂载到/mnt/a上为例,来解释一下/mnt/b上这个挂载实例和/mnt的挂载实例的关系,如下图所示:

父文件系统代表/mnt上的文件系统,子文件系统代表/mnt/b上的文件系统(带颜色的地方为重点要注意的地方)。父子文件系统通过mnt_parent, mount_child, mnt_mounts等成员来联系在一起,每个挂载实例的mnt_sb都指向这个挂载实例所属文件系统的super_block。每个挂载实例的mnt_root都指向这个文件系统的根dentry。
根dentry就是一个文件系统的路径的起始,也就是"/"。比如一个路径名/mnt/a/dir/file。在/mnt/b这个文件系统下看这个文件是/dir/file,这个起始的"/"代表/mnt/a下挂载的文件系统的根,也就是如上图红色所示的dentry,它是这一文件系统的起始dentry。当发现到了一个文件系统的根后,如果想继续探寻完整路径应该根据/mnt/b的挂载实例向上找到其父文件系统,也就是/mnt下挂载的文件系统。/dev/sda1挂载在了/mnt/a上,这里的/mnt/a代表/mnt下文件系统的一个子dentry,如图绿色部分所示。注意红色和绿色是两个文件系统下的两个不同的dentry,虽然不是很恰当的说它们从全局来看是一个路径名。那么从/mnt所在的文件系统看/mnt/a就是/a。最后再往上就到了rootfs文件系统,也就是最上层的根"/"。所以我们之前说过,表示一个文件的路径需要<mount, dentry>二元组来共同确定。
子文件系统的mnt_mountpoint就指向了父文件系统的一个dentry,这个dentry也就是子文件系统的真正挂载点。可以说子文件系统在挂载后会新创建一个dentry,并在此构建这个文件系统下的路径结构。
宗上所述,/mnt/b上这个新挂载的文件系统创建了一个新的mount, super_block, 根inode和根dentry。在看懂了一个简单的父子文件系统挂载关系后,我们来看下多个文件系统挂载到同一路径名下时又是什么样子。
多文件系统单挂载点的挂载关系
就像上面所叙述的/dev/sda1挂载到了/mnt/a上,之后/dev/sda2也挂载到/mnt/a上的情况,当两个以上的文件系统先后挂载到同一个路径名下时会是怎样一种情况呢?如下图所示:

如上图,子文件系统A代表/dev/sda1,子文件系统B代表/dev/sda2。父文件系统和子文件系统A的挂载在之前一节已经说明过了,当/dev/sda2在sda1之后也挂载到/mnt/a上时,其关系就像在上一节基础上又添加了子文件系统B的关系。实际上子文件系统A就是子文件系统B的父文件系统,而唯一不同的是子文件系统B的mnt_mountpoint指向了子文件系统A的根dentry。而新的子文件系统B还是有自己的mount, super_block, 根dentry和根inode。
两个文件系统挂载在同一路径名下会造成之前挂载的文件系统被隐藏,这种实现我们在后续讲解。其实从上面的关系也应该可以想到一些解决的方式。最后再来看一下一个文件系统被挂载到多个不同的路径下的情况。
单文件系统多挂载点的挂载关系
同一个文件系统被挂载到不同的路经下,就像上面例子中/dev/sda1被挂载到/mnt/a和/mnt/x两个位置一样,如下图所示:

一个文件系统对应一个super_block,所以同一个文件系统当然只有一个super_block。但是因为挂载了两次,所有每一次挂载对应一个挂载实例struct mount,也就是有两个mount实例。此外同一个文件系统只有一个根,也就是两个挂载实例公用一个根dentry。但是因为挂载在两个不同的路经下,所以每个挂载实例的mnt_mountpoint指向不同的dentry。由于/mnt/a和/mnt/x都属于同一文件系统的下的两个子目录,所以两个子mount才指向同一个父mount(这个不是必须的)。
相关函数
mount
函数位置: fs/namespace.c
文件系统挂载由mount系统调用发起.
例如devtmpfs文件系统mount发起
err = sys_mount("devtmpfs", (char *)mntdir, "devtmpfs", MS_SILENT, NULL);
参数意义:
- dev_name: 文件系统所在设备名称
- dirname: 要挂载到的位置
- type:要挂载的文件系统名称
- flags: 文件系统通用挂载选项
- data: 文件系统特用挂载选项
函数原型如下:
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;
/* mount的五个参数中的四个指针参数所对应的内容还处在用户层,
* 所以需要调用下面的函数来拷贝到内核空间,dir_name的拷贝工
* 作在do_mount函数中完成.
* */
kernel_type = copy_mount_string(type);
kernel_dev = copy_mount_string(dev_name);
options = copy_mount_options(data);
ret = do_mount(kernel_dev, dir_name, kernel_type, flags, options);
}
mount系统调用主要完成一下工作:
- 将userspace参数(dev_name,type,data)拷贝到内核空间.
- 调用do_mount()来完成文件系统挂载
do_mount
函数位置: fs/namespace.c
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;
/* flags被分为mnt_flags和sb_flags*/
unsigned int mnt_flags = 0, sb_flags;
int retval = 0;
...
/* ... and get the mountpoint */
/* path是内核做路径名操作的常用结构体.
* 根据dir_name,来填充path变量.
* */
retval = user_path(dir_name, &path);
...
/* Default to relatime unless overriden */
if (!(flags & MS_NOATIME))
mnt_flags |= MNT_RELATIME;
/* 解析flags信息填充到mnt_flags里 */
/* Separate the per-mountpoint flags */
if (flags & MS_NOSUID)
mnt_flags |= MNT_NOSUID;
if (flags & MS_NODEV)
mnt_flags |= MNT_NODEV;
...
/* 解析sb_flags信息 */
sb_flags = flags & (SB_RDONLY |
SB_SYNCHRONOUS |
SB_MANDLOCK |
SB_DIRSYNC |
SB_SILENT |
SB_POSIXACL |
SB_LAZYTIME |
SB_I_VERSION);
/* 根据flags信息来决定是那种mount操作*/
if (flags & MS_REMOUNT)
retval = do_remount(&path, flags, sb_flags, 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, sb_flags, mnt_flags,
dev_name, data_page);
dput_out:
path_put(&path);
return retval;
}
可以看到,do_mount主要完成工作为:
- 将dir_name解析为path格式到内核;
- 一路解析flags位表,将flags拆分位mnt_flags和sb_flags;
- 根据dir_name来构建path实例
- 根据flags中的标记,决定下面做哪一个mount操作。
path路径的处理
上一步do_mount函数中会调用user_path来处理user空间传过来的dir_name变量.
该变量经过解析后,会构建path实例.上边成员说名中我们介绍过,path包含了当前目录所在当前文件系统的dentry和vfsmount信息.这些信息经过解析后,会填充到nameidata实例中.为后边构建super_block和vfsmount实例提供参考信息.
getname_flags
将用户空间传入的dir_name字符串转化为struct filename格式
struct filename {
const char *name; /* pointer to actual string */
const __user char *uptr; /* original userland pointer */
struct audit_names *aname;
int refcnt;
const char iname[];
};
filename_lookup
- 声明一个
nameidata实例,用来缓存查找信息. - 如果
root被指定的话(入参),设置nd.root - 调用
set_nameidata()填充部分nameidata->name字段信息信息 - 调用
path_lookupat()来查找挂载点信息
static int filename_lookup(int dfd, struct filename *name, unsigned flags,
struct path *path, struct path *root)
{
int retval;
/* nd用来存储查找结果 */
struct nameidata nd;
if (IS_ERR(name))
return PTR_ERR(name);
// mount过程中,root为NULL
if (unlikely(root)) {
nd.root = *root;
flags |= LOOKUP_ROOT;
}
/* 填充struct nameidata nd数据*/
set_nameidata(&nd, dfd, name);
retval = path_lookupat(&nd, flags | LOOKUP_RCU, path);
if (unlikely(retval == -ECHILD))
retval = path_lookupat(&nd, flags, path);
if (unlikely(retval == -ESTALE))
retval = path_lookupat(&nd, flags | LOOKUP_REVAL, path);
if (likely(!retval))
audit_inode(name, path->dentry, flags & LOOKUP_PARENT);
restore_nameidata();
putname(name);
return retval;
}
set_nameidata
填充部分nameidata成员信息:
static void set_nameidata(struct nameidata *p, int dfd, struct filename *name)
{
struct nameidata *old = current->nameidata;
p->stack = p->internal;
p->dfd = dfd;
p->name = name;
p->total_link_count = old ? old->total_link_count : 0;
p->saved = old;
current->nameidata = p;
}
path_lookupat
- 调用
path_init填充nameidata->path实例信息 link_path_walk()根据填充的nd实例,来解析路径分量.找到最后的挂载点路径对应的vfsmount和dentry信息
/* Returns 0 and nd will be valid on success; Retuns error, otherwise. */
static int path_lookupat(struct nameidata *nd, unsigned flags, struct path *path)
{
const char *s = path_init(nd, flags);
int err;
if (unlikely(flags & LOOKUP_DOWN)) {
err = handle_lookup_down(nd);
if (unlikely(err < 0)) {
terminate_walk(nd);
return err;
}
}
while (!(err = link_path_walk(s, nd))
&& ((err = lookup_last(nd)) > 0)) {
s = trailing_symlink(nd);
if (IS_ERR(s)) {
err = PTR_ERR(s);
break;
}
}
if (!err)
err = complete_walk(nd);
if (!err && nd->flags & LOOKUP_DIRECTORY)
if (!d_can_lookup(nd->path.dentry))
err = -ENOTDIR;
if (!err) {
*path = nd->path;
nd->path.mnt = NULL;
nd->path.dentry = NULL;
}
terminate_walk(nd);
return err;
}
path_init
前边set_nameidata()依据filename *name填充了部分nameidata->name参数,此处继续根据传入的flags参数,来继续填充nameidata->path数据.
/* 如函数名,填充nd->path实例 */
static const char *path_init(struct nameidata *nd, unsigned flags)
{
const char *s = nd->name->name;
if (!*s)
flags &= ~LOOKUP_RCU;
nd->last_type = LAST_ROOT; /* if there are only slashes(斜杠)... */
nd->flags = flags | LOOKUP_JUMPED | LOOKUP_PARENT;
nd->depth = 0;
if (flags & LOOKUP_ROOT) {
struct dentry *root = nd->root.dentry;
struct inode *inode = root->d_inode;
if (*s && unlikely(!d_can_lookup(root)))
return ERR_PTR(-ENOTDIR);
nd->path = nd->root;
nd->inode = inode;
if (flags & LOOKUP_RCU) {
rcu_read_lock();
nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
nd->root_seq = nd->seq;
nd->m_seq = read_seqbegin(&mount_lock);
} else {
path_get(&nd->path);
}
return s;
}
nd->root.mnt = NULL;
nd->path.mnt = NULL;
nd->path.dentry = NULL;
/* 读取顺序锁*/
nd->m_seq = read_seqbegin(&mount_lock);
/* 如果挂载目录为绝对路径
* 调用set_root将root字段设置为current->fs->root,并增加引用计数.
* 将path设置为nd->root*/
if (*s == '/') {
if (flags & LOOKUP_RCU)
rcu_read_lock();
/* 设置 nd = current->fs->root*/
set_root(nd);
if (likely(!nd_jump_root(nd)))
return s;
/* 当前root节点没有挂载文进行通的话,置空. */
nd->root.mnt = NULL;
rcu_read_unlock();
return ERR_PTR(-ECHILD);
}
/* 如果挂载目录为相对路径,将path字段设置为进程的当前路径 current->fs->pwd*/
else if (nd->dfd == AT_FDCWD) {
if (flags & LOOKUP_RCU) {
struct fs_struct *fs = current->fs;
unsigned seq;
rcu_read_lock();
do {
seq = read_seqcount_begin(&fs->seq);
nd->path = fs->pwd;
nd->inode = nd->path.dentry->d_inode;
nd->seq = __read_seqcount_begin(&nd->path.dentry->d_seq);
} while (read_seqcount_retry(&fs->seq, seq));
} else {
get_fs_pwd(current->fs, &nd->path);
nd->inode = nd->path.dentry->d_inode;
}
return s;
} else {
/* 使用的基目录就是进程文件文件描述符表中的某个文件,
* 而此时nd->dfd参数则正是该目录文件的文件描述符,
* 将path字段设置为该目录文件的路径.
* */
/* Caller must check execute permissions on the starting path component */
struct fd f = fdget_raw(nd->dfd);
struct dentry *dentry;
if (!f.file)
return ERR_PTR(-EBADF);
dentry = f.file->f_path.dentry;
if (*s) {
if (!d_can_lookup(dentry)) {
fdput(f);
return ERR_PTR(-ENOTDIR);
}
}
nd->path = f.file->f_path;
if (flags & LOOKUP_RCU) {
rcu_read_lock();
nd->inode = nd->path.dentry->d_inode;
nd->seq = read_seqcount_begin(&nd->path.dentry->d_seq);
} else {
path_get(&nd->path);
nd->inode = nd->path.dentry->d_inode;
}
fdput(f);
return s;
}
}
link_path_walk
此函数较长.名称在内核内被分解为各个分量,(各分量以/分割),每个分量表示一个目录名,最后一个除外,也可表示一个文件名.
内核遇到一个/或多个/之前是逐字符进行扫描然后得出路径分量的. 例如 /home/mnt/a,只有路径的三个分量home mnt a 是需要关注的.每个for循环中处理一个路径分量.
接下来我们以挂载一个新的文进系统为例进行分析继续分析mount过程
do_new_mount
函数位置: fs/namespace.c
mount->do_mount->do_new_mount
static int do_new_mount(struct path *path, const char *fstype, int sb_flags,
int mnt_flags, const char *name, void *data)
{
struct file_system_type *type;
/*每个装载的文件系统都对应一个vfsmount结构的实例*/
struct vfsmount *mnt;
int err;
if (!fstype)
return -EINVAL;
/* get_fs_type(),它接受一个文件系统的名称作为参数,然后反向找到其对应的file_system_type实例
* 其实就是在file_systems这个全局变量的链表中根据name(对应参数为fstype)和len查找fstype文件类型的实例*/
type = get_fs_type(fstype);
if (!type)
return -ENODEV;
/* 创建vfsmnt和super_block*/
mnt = vfs_kern_mount(type, sb_flags, name, data);
if (!IS_ERR(mnt) && (type->fs_flags & FS_HAS_SUBTYPE) &&
!mnt->mnt_sb->s_subtype)
mnt = fs_set_subtype(mnt, fstype);
/* TODO: 为什么要-1?*/
put_filesystem(type);
if (IS_ERR(mnt))
return PTR_ERR(mnt);
/* 设置该文件系统可见度 */
if (mount_too_revealing(mnt, &mnt_flags)) {
mntput(mnt);
return -EPERM;
}
/* 将得到的mount结构加入全局目录树 */
err = do_add_mount(real_mount(mnt), path, mnt_flags);
if (err)
mntput(mnt);
return err;
}
do_new_mount主要完成的工作:
- 根据文进系统名称参数,找到该file_system_type实例
- 调用vfs_kern_mount构建mount实例和super_block实例
- 调用do_add_mount将mount实例加入全局目录树
vfs_kernel_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);
/* alloc一个新的struct mount结构,并初始化里边的部分成员 */
mnt = alloc_vfsmnt(name);
if (!mnt)
return ERR_PTR(-ENOMEM);
/* 该成员未赋值的话不会调用(devtmpfs并未赋值)*/
if (type->alloc_mnt_data) {
mnt->mnt.data = type->alloc_mnt_data();
if (!mnt->mnt.data) {
mnt_free_id(mnt);
free_vfsmnt(mnt);
return ERR_PTR(-ENOMEM);
}
}
if (flags & SB_KERNMOUNT)
mnt->mnt.mnt_flags = MNT_INTERNAL;
/* 调用file_system_type->mount函数,来继续挂载操作*/
root = mount_fs(type, flags, name, &mnt->mnt, data);
if (IS_ERR(root)) {
mnt_free_id(mnt);
free_vfsmnt(mnt);
return ERR_CAST(root);
}
/* 完成mnt结构最后赋值,返回vfsmount结构*/
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;
}
EXPORT_SYMBOL_GPL(vfs_kern_mount);
vfs_kern_mount主要完成三件事:
- alloc_vfsmnt创造一个新的struct mount实例
2. 在mount_fs函数里调用特定文件系统的mount回调函数构造一个root dentry 实例,
3. 用第二步得到的结果完成对struct mount的构造,返回vfsmnt结构
mount_fs
/*
* blkdev_get_by_path根据设备名得到block_device结构
* sget得到已经存在或者新分配的super_block结构
* 如果是已经存在的sb,就释放第一步得到的bdev结构
* 如果是新的sb,就调用文件系统个别实现的fill_super函数
* 继续处理新的sb,并创建根inode, dentry返回得到的s_root
*/
struct dentry *mount_bdev(struct file_system_type *fs_type,
int flags, const char *dev_name, void *data,
int (*fill_super)(struct super_block *, void *, int))
{
struct block_device *bdev;
struct super_block *s;
fmode_t mode = FMODE_READ | FMODE_EXCL;
int error = 0;
if (!(flags & SB_RDONLY))
mode |= FMODE_WRITE;
/* 通过dev_name设备名得到对应的block_device结构
* dev_name在blkdev_get_by_path中成为path,即设备的路径.
* */
bdev = blkdev_get_by_path(dev_name, mode, fs_type);
if (IS_ERR(bdev))
return ERR_CAST(bdev);
/*
* once the super is inserted into the list by sget, s_umount
* will protect the lockfs code from trying to start a snapshot
* while we are mounting
*/
mutex_lock(&bdev->bd_fsfreeze_mutex);
if (bdev->bd_fsfreeze_count > 0) {
mutex_unlock(&bdev->bd_fsfreeze_mutex);
error = -EBUSY;
goto error_bdev;
}
/* sget在现存的fs_type->fs_super链表中查找已经存在的对应超级块实例
* (因为一个设备可能已经被挂载过了),fs_super是file_system_type成员,
* 它指向一个特定的文件系统下所有超级块实例的链表表头.
*
* 比较过程就是遍历fs_super链表.用每一个super_blokc->s_bdev和sget
* 的bdev参数作比较,比较他们是不是同一个设备,test_bdev_super就是传入
* 的比较函数.
*
* 如果没能找到已经存在的超级块实例,那只能创建一个新的.此时set_bdev_super
* 函数把bdev参数设置到新创建的super_block的s_bdev域中.然后设置下s_type
* 和s_id(s_id先初始化为文件系统名).之后如果发现是磁盘设备,再改为磁盘设备名,
*并把这个新的sb加入到全局的super_block以及此时的file_system_type的
* fs_super链表中.
*
* 到此时,就得到一个已知的或新的super_block实例.后面的工作都是为了填充这个s
* */
s = sget(fs_type, test_bdev_super, set_bdev_super, flags | SB_NOSEC,
bdev);
mutex_unlock(&bdev->bd_fsfreeze_mutex);
if (IS_ERR(s))
goto error_s;
if (s->s_root) {
if ((flags ^ s->s_flags) & SB_RDONLY) {
deactivate_locked_super(s);
error = -EBUSY;
goto error_bdev;
}
/*
* s_umount nests inside bd_mutex during
* __invalidate_device(). blkdev_put() acquires
* bd_mutex and can't be called under s_umount. Drop
* s_umount temporarily. This is safe as we're
* holding an active reference.
*/
up_write(&s->s_umount);
blkdev_put(bdev, mode);
down_write(&s->s_umount);
} else {
s->s_mode = mode;
snprintf(s->s_id, sizeof(s->s_id), "%pg", bdev);
sb_set_blocksize(s, block_size(bdev));
error = fill_super(s, data, flags & SB_SILENT ? 1 : 0);
if (error) {
deactivate_locked_super(s);
goto error;
}
s->s_flags |= SB_ACTIVE;
bdev->bd_super = s;
}
return dget(s->s_root);
}
EXPORT_SYMBOL(mount_bdev);
do_add_mount
主要完成两件事:
- lock_mount确定本次挂载要挂载到哪个父挂载实例parent的哪个挂载点mountpoint上
- 把newmnt挂载到parent的mp下,完成newmnt到全局的安装.
/*
* add a mount into a namespace's mount tree
*/
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;
/* 获取当前目录挂载的第一个mount结构(即最新一个),赋值给path,并返回*/
mp = lock_mount(path);
if (IS_ERR(mp))
return PTR_ERR(mp);
/* 通过vfsmount找到mount */
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;
}
/* 不允许同一个("个"并非"种")文件系统挂载到同一个挂载点,因为这样做没意义 */
/* Refuse the same filesystem on the same mount point */
err = -EBUSY;
if (path->mnt->mnt_sb == newmnt->mnt.mnt_sb &&
path->mnt->mnt_root == path->dentry)
goto unlock;
err = -EINVAL;
/* 确保根inode不是一个符号链接 */
if (d_is_symlink(newmnt->mnt.mnt_root))
goto unlock;
newmnt->mnt.mnt_flags = mnt_flags;
/* 把newmnt加入到全局文件系统树中 */
err = graft_tree(newmnt, parent, mp);
}
lock_mount
功能:
- 确定mountpoint的最终位置
FOLLOW_MNT逻辑
我们就以上述lookup_mnt的注释中的例子来解释说明一下,lookup_mnt是如何被使用的。假如我们就按照上面注释中的例子,执行了如下操作:
# mount /dev/sda1 /mnt
# mount /dev/sda2 /mnt
# mount /dev/sda3 /mnt
现在我要再执行:
# mount /dev/sdb1 /mnt
那么follow_mnt逻辑的执行顺序就是:
- 1、path的初始状态为:
path->mnt = 根文件系统
path->dentry = 根文件系统下的/mnt的dentry
- 2、第一次lookup_mnt(path)返回的mnt的状态是:
mnt为/dev/sda1这个挂载实例
mnt->mnt_root为/dev/sda1这个文件系统的根dentry
- 3、lookup_mnt返回/dev/sda1的挂载实例,然后执行:
path->mnt = mnt; 将第一个挂载在/mnt上的/dev/sda1的挂载实例赋值给path->mnt
path->dentry = mnt->mnt_root; 将/dev/sda1挂载成功后的根dentry给path->dentry
- 4、重返lookup_mnt(path),只是这次path中的mnt和dentry变成了/dev/sda1的。
- 5、lookup_mnt发现/dev/sda1的根dentry上也挂载的文件系统/dev/sda2,于是返回了/dev/sda2的挂载实例。
- 6、又将/dev/sda2的挂载实例和根dentry赋值给path,重新调用lookup_mnt。
- 7、这回lookup_mnt返回/dev/sda3的挂载实例,然后将/dev/sda3的挂载实例和根dentry赋值给path,再次调用lookup_mnt。
- 8、这回lookup_mnt发现/dev/sda3的根dentry上没有挂载文件系统,于是返回NULL。
- 9、lookup_mnt返回了NULL,也就是找到了本次要被挂载的挂载点,用/dev/sda3的根dentry构建挂载点结构,并让lock_mount返回。新的 /dev/sdb1将在返回后的操作中被挂载到/dev/sda3的根dentry上。
lookup_mnt
该函数在路径查找时也会被用到,作用是根据一个父<mount,dentry>二元数组找到挂载在其下面的子文件系统的mount实例,如果没有找到,则返回null.
全局mount_hashtable哈希数组保存着除根文件系统以外所有的文件系统挂载实例,而每个其中元素的索引条件就是其父文件系统的挂载实例和挂载点的dentry,换句话说,每个mount实例咋保存到mount_hashtable里时,是使用其挂载点dentry和所在点所在的父挂载实例为依据计算hash值并存入的.lookup_mnt就是以这个为依据计算hash值并查找响应挂载点实例的.

浙公网安备 33010602011771号