一步一步学linux操作系统: 26 文件系统_虚拟文件系统、挂载文件系统与打开文件
进程往文件系统读写数据简要流程
-
应用层
进程在进行文件读写操作时,可通过系统调用如 sys_open、sys_read、sys_write 等。 -
内核
每个进程为打开的文件维护一定的数据结构
整个系统打开的文件也需要维护一定的数据结构 -
虚拟文件系统
Linux 可以支持多达数十种不同的文件系统其实现各不相同,为了统一接口Linux 内核向用户空间提供了虚拟文件系统来对文件系统进行操作
常见的文件系统对象模型,例如 inode、directory entry、mount 等
操作这些对象的方法,例如 inode operations、directory operations、file operations 等。 -
真正的文件系统
例如 25 中所介绍的 ext4 文件系统 -
块设备 I/O 层
文件系统层和块设备驱动的接口,用于读写 ext4 文件系统 -
缓存层
可以加快块设备的读写效率 -
块设备驱动程序
图片来自极客时间趣谈linux操作系统
挂载文件系统
想要操作文件系统,第一件事情就是挂载文件系统。
注册文件系统 register_filesystem 后才能挂载
register_filesystem 函数
\linux-4.13.16\fs\filesystems.c
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;
}
ext4_fs_type 结构
\linux-4.13.16\fs\ext4\super.c
static struct file_system_type ext4_fs_type = {
.owner = THIS_MODULE,
.name = "ext4",
.mount = ext4_mount,
.kill_sb = kill_block_super,
.fs_flags = FS_REQUIRES_DEV,
};
mount 系统调用
调用链为:do_mount->do_new_mount->vfs_kern_mount。
vfs_kern_mount 函数
\linux-4.13.16\fs\namespace.c
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;
}
EXPORT_SYMBOL_GPL(vfs_kern_mount);
1、vfs_kern_mount 先是创建 struct mount 结构。每个挂载的文件系统都对应于这样一个结构。
struct mount 结构
struct mount {
struct hlist_node mnt_hash;
struct mount *mnt_parent;
struct dentry *mnt_mountpoint;
struct vfsmount mnt;
union {
struct rcu_head mnt_rcu;
struct llist_node mnt_llist;
};
struct list_head mnt_mounts; /* list of children, anchored here */
struct list_head mnt_child; /* and going through their mnt_child */
struct list_head mnt_instance; /* mount instance on sb->s_mounts */
const char *mnt_devname; /* Name of device e.g. /dev/dsk/hda1 */
struct list_head mnt_list;
......
} __randomize_layout;
struct vfsmount {
struct dentry *mnt_root; /* root of the mounted tree */
struct super_block *mnt_sb; /* pointer to superblock */
int mnt_flags;
} __randomize_layout;
\linux-4.13.16\fs\mount.h
\linux-4.13.16\include\linux\mount.h
- mnt_parent 是装载点所在的父文件系统
- mnt_mountpoint 是装载点在父文件系统中的 dentry
- struct dentry 表示目录,并和目录的 inode 关联
- mnt_root 是当前文件系统根目录的 dentry
- mnt_sb 是指向超级块的指针
2、调用 mount_fs 函数 ,挂载文件系统
mount_fs 函数
\linux-4.13.16\fs\super.c
struct dentry *
mount_fs(struct file_system_type *type, int flags, const char *name, void *data)
{
struct dentry *root;
struct super_block *sb;
......
root = type->mount(type, flags, name, data);
......
sb = root->d_sb;
......
}
ext4 文件系统就会调用 ext4_fs_type 的 mount 函数,也就是ext4_mount,从文件系统里面读取超级块
mount 后数据结构之间的关系
例子:
假设根文件系统下面有一个目录 home,文件系统A挂载在这个home目录下,文件系统 A 的根目录下面有另外一个文件夹 hello,由于A挂载在目录home下面就有了目录 /home/hello。
另外一个文件系统 B 挂载在文件系统 A 的根目录下的hello下面,在文件系统 B 的根目录下面有另外一个文件夹 world,在 world 下面有个文件夹 data。由于文件系统 B 已经挂载到了 /home/hello 下面,所以就有了目录 /home/hello/world/data。
形成如下结构
图片来自极客时间趣谈linux操作系统
三条斜线
-
dentry 斜线
最左边向左斜
每一个文件和文件夹都有 dentry,用于和 inode 关联 -
mount 斜线
最右面向右斜
例子涉及两次文件系统的挂载,再加上启动的时候挂载的根文件系统,一共三个 mount -
file 斜线
中间向右斜
每个打开的文件都有一个 file 结构,它里面有两个变量,一个指向相应的 mount,一个指向相应的 dentry。
从最上面往下看
第一层
- 根目录 / 对应一个 dentry
根目录是在根文件系统上的,根文件系统是系统启动的时候挂载的 - 一次挂载一个 mount 结构
这个 mount 结构的 mount point 指针和 mount root 指针都是指向根目录的 dentry - 根目录对应的 file的两个指针
一个指向根目录的 dentry,一个指向根目录的挂载结构 mount。
第二层
- 下一层目录 home 对应了两个 dentry
home 是根文件系统的一个挂载点
home 是文件系统 A 的根目录
其parent 都指向第一层的 dentry - 一次挂载一个 mount 结构
mount 结构的 mount point 指针指向作为挂载点的那个 dentry
mount root 指针指向作为根目录的那个 dentry
parent 指针指向第一层的 mount 结构 - home 对应的 file 的两个指针
一个指向文件系统 A 根目录的 dentry
一个指向文件系统 A 的挂载结构 mount
第三层
- 目录 hello 又挂载了一个文件系统 B,其结构和第二层几乎一样。
第四层
目录 world 就是一个普通的目录
- 它的 dentry 的 parent 指针指向上一层
- world 对应的 file 结构
由于挂载点不变,还是指向第三层的 mount 结构。
也就是文件系统B挂载时产生的mount结构
第五层
文件 data,是一个普通的文件
- 它的 dentry 的 parent 指向第四层的 dentry
- data 对应的 file 结构
由于挂载点不变,还是指向第三层的 mount 结构。
打开文件
进程里面通过 open 系统调用打开文件,最终调用到内核的系统调用实现 sys_open。
系统调用 参见 https://blog.csdn.net/leacock1991/article/details/106773065
do_sys_open 函数
\linux-4.13.16\fs\open.c
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
......
return do_sys_open(AT_FDCWD, filename, flags, mode);
}
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
......
fd = get_unused_fd_flags(flags);
if (fd >= 0) {
struct file *f = do_filp_open(dfd, tmp, &op);
if (IS_ERR(f)) {
put_unused_fd(fd);
fd = PTR_ERR(f);
} else {
fsnotify_open(f);
fd_install(fd, f);
}
}
putname(tmp);
return fd;
}
1、首先通过 get_unused_fd_flags 得到一个没有用的文件描述符
如何获取这个文件描述符呢?
在每一个进程的 task_struct 中,有一个指针 files,类型是 files_struct
struct files_struct *files;
files_struct 里面最重要的是一个文件描述符列表,每打开一个文件,就会在这个列表中分配一项,下标就是文件描述符。
struct files_struct {
......
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};
对于任何一个进程,默认情况下
- 文件描述符 0 表示 stdin 标准输入
- 文件描述符 1 表示 stdout 标准输出
- 文件描述符 2 表示 stderr 标准错误输出
- 再打开的文件,都会从这个列表中找一个空闲位置分配给它
文件描述符列表的每一项都是一个指向 struct file 的指针,也就是说,每打开一个文件,都会有一个 struct file 对应。
2、再调用 do_filp_open 函数
创建 struct file 结构,然后 fd_install(fd, f) 是将文件描述符和这个结构关联起来
\linux-4.13.16\fs\namei.c
struct file *do_filp_open(int dfd, struct filename *pathname,
const struct open_flags *op)
{
......
set_nameidata(&nd, dfd, pathname);
filp = path_openat(&nd, op, flags | LOOKUP_RCU);
......
restore_nameidata();
return filp;
}
a、首先初始化了 struct nameidata 这个结构
struct nameidata 结构
\linux-4.13.16\fs\namei.c
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;
辅助解析和查找路径
关键的成员变量 struct path
struct path
\linux-4.13.16\include\linux\path.h
struct path {
struct vfsmount *mnt;
struct dentry *dentry;
} __randomize_layout;
struct vfsmount 和文件系统的挂载有关
struct dentry,除了用于标识目录之外,还可以表示文件名,还会建立文件名及其 inode 之间的关联
b、接下来就调用 path_openat
\linux-4.13.16\fs\namei.c
static struct file *path_openat(struct nameidata *nd,
const struct open_flags *op, unsigned flags)
{
......
file = get_empty_filp();
......
s = path_init(nd, flags);
......
while (!(error = link_path_walk(s, nd)) &&
(error = do_last(nd, file, op, &opened)) > 0) {
......
}
terminate_walk(nd);
......
return file;
}
- get_empty_filp 生成一个 struct file 结构
- path_init 初始化 nameidata,准备开始节点路径查找
- link_path_walk 对于路径名逐层进行节点路径查找,这里面有一个大的循环,用“/”分隔逐层处理;
- do_last 获取文件对应的 inode 对象,并且初始化 file 对象
do_last 函数
查找文件路径最后一部分对应的 dentry
Linux 为了提高目录项对象的处理效率,设计与实现了目录项高速缓存 dentry cache,简称 dcache。主要由两个数据结构组成。
哈希表 dentry_hashtable:dcache 中的所有 dentry 对象都通过 d_hash 指针链到相应的 dentry 哈希链表中;
未使用的 dentry 对象链表 s_dentry_lru:dentry 对象通过其 d_lru 指针链入 LRU 链表中。
\linux-4.13.16\fs\namei.c
static int do_last(struct nameidata *nd,
struct file *file, const struct open_flags *op,
int *opened)
{
......
error = lookup_fast(nd, &path, &inode, &seq);
......
error = lookup_open(nd, &path, file, op, got_write, opened);
......
error = vfs_open(&nd->path, file, current_cred());
......
}
先从缓存中查找,调用的是 lookup_fast
如果缓存中没有找到,使用lookup_open到文件系统里面去找
lookup_open 函数
\linux-4.13.16\fs\namei.c
static int lookup_open(struct nameidata *nd, struct path *path,
struct file *file,
const struct open_flags *op,
bool got_write, int *opened)
{
......
dentry = d_alloc_parallel(dir, &nd->last, &wq);
......
struct dentry *res = dir_inode->i_op->lookup(dir_inode, dentry,
nd->flags);
......
path->dentry = dentry;
path->mnt = nd->path.mnt;
}
const struct inode_operations ext4_dir_inode_operations = {
.create = ext4_create,
.lookup = ext4_lookup,
...
lookup_open 会创建一个新的 dentry,调用上一级目录的 inode_operations 的 lookup 函数,对于 ext4 来讲,调用的是 ext4_lookup 会到25中讲的文件系统里面去找 inode,找到后将新生成的 dentry 赋给 path 变量
最后调用 vfs_open 真正打开文件
** vfs_open函数**
\linux-4.13.16\fs\open.c
int vfs_open(const struct path *path, struct file *file,
const struct cred *cred)
{
struct dentry *dentry = d_real(path->dentry, NULL, file->f_flags, 0);
......
file->f_path = *path;
return do_dentry_open(file, d_backing_inode(dentry), NULL, cred);
}
static int do_dentry_open(struct file *f,
struct inode *inode,
int (*open)(struct inode *, struct file *),
const struct cred *cred)
{
......
f->f_mode = OPEN_FMODE(f->f_flags) | FMODE_LSEEK |
FMODE_PREAD | FMODE_PWRITE;
path_get(&f->f_path);
f->f_inode = inode;
f->f_mapping = inode->i_mapping;
......
f->f_op = fops_get(inode->i_fop);
......
open = f->f_op->open;
......
error = open(inode, f);
......
f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);
file_ra_state_init(&f->f_ra, f->f_mapping->host->i_mapping);
return 0;
......
}
const struct file_operations ext4_file_operations = {
......
.open = ext4_file_open,
......
};
vfs_open 做的第一件重要的事情:调用 f_op->open,也就是调用 ext4_file_open
vfs_open 做的第二件重要的事情:将打开文件的所有信息,填写到 struct file 这个结构里面
struct file结构
\linux-4.13.16\include\linux\fs.h
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
spinlock_t f_lock;
enum rw_hint f_write_hint;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
......
struct address_space *f_mapping;
errseq_t f_wb_err;
}
总结
图片来自极客时间趣谈linux操作系统
- 对于每一个进程,打开的文件都有一个文件描述符,在 files_struct 里面会有文件描述符数组。
- 每个一个文件描述符是这个数组的下标,里面的内容指向一个 file 结构,表示打开的文件。
- file结构里面有这个文件对应的 inode,最重要的是这个文件对应的操作 file_operation
- 对于每一个打开的文件,都有一个 dentry 对应,虽然叫作 directory entry,但是不仅仅表示文件夹,也表示文件。指向这个文件对应的 inode
- file 结构是一个文件打开以后才创建的,dentry 是放在一个 dentry cache 里面的,文件关闭了,依然存在,可以更长期地维护内存中的文件的表示和硬盘上文件的表示之间的关系
- inode 结构就表示硬盘上的 inode,包括块设备号等
- 几乎每一种结构都有自己对应的 operation 结构,里面都是一些对本结构的处理方法
参考资料:
趣谈Linux操作系统(极客时间)链接:
http://gk.link/a/10iXZ
欢迎大家来一起交流学习
下一篇: SQL必知必会(三)-进阶篇