Linux文件系统(三)虚拟文件系统

小灰灰 2023-06-08 03:04 150阅读 0赞

Linux文件系统

Linux文件系统(一)文件系统概述

Linux文件系统(二)磁盘文件系统

Linux文件系统(三)虚拟文件系统

Linux文件系统(四)文件缓存

Linux文件系统(三)虚拟文件系统

文章目录

  • Linux文件系统(三)虚拟文件系统
    • 一、文件系统框架
    • 二、挂载文件系统
    • 三、打开文件
    • 四、总结

一、文件系统框架

文件系统需要许多层的组件一起协作,具体怎么协作,下面来看一看

在这里插入图片描述

  • 在应用层,进程操作文件可以通过 open、read、write 等系统调用
  • 在内核,每个进程都需要对其打开的文件,维护一定的数据结构
  • 在内核,整个系统打开的文件,也需要维护一定的数据结构
  • Linux可以支持多大数十种文件系统格式,每种文件系统的实现各不相同,为了统一操作,Linux提供的虚拟文件系统这一接口。它提供了常见的文件系统对象模型,例如 inode、directory entry、mount等,以及这些对象的操作方法,例如 inode operations、directory operations、file operations 等
  • 然后对接的是真正的文件系统,例如 ext4
  • 为了读取 ext4 文件系统,要通过块设备 I/O 层,也即 BIO 层。这是文件系统层和块设备驱动层的接口
  • 为了加快读写速度,还有一个缓存层
  • 最下面的就是块设备驱动程序了

通过系统调用分析内核架构是一种很好的方式,下面将分析这两个系统调用

  • mount:挂载文件系统
  • open:打开文件,如果文件不存在并且指定了 O_CREAT,那么就创建一个文件

二、挂载文件系统

想要操作文件系统,第一步就是挂载文件系统

内核是否支持某种文件系统,那要看内核是否已经注册了某种文件系统。例如 ext4 文件系统,就需要通过 register_filesystem 进行注册,传入的参数是 ext4_fs_type,表示注册的是 ext4 文件系统类型。这里面有一个重要的成员变量 ext4_mount,请记住它,后面会再谈到

  1. register_filesystem(&ext4_fs_type);
  2. static struct file_system_type ext4_fs_type = {
  3. .owner = THIS_MODULE,
  4. .name = "ext4",
  5. .mount = ext4_mount,
  6. .kill_sb = kill_block_super,
  7. .fs_flags = FS_REQUIRES_DEV,
  8. };

挂载文件系统通过系统调用 sys_mount 来操作,定义如下

  1. SYSCALL_DEFINE5(mount, char __user *, dev_name, char __user *, dir_name, char __user *, type, unsigned long, flags, void __user *, data)
  2. {
  3. ......
  4. ret = do_mount(kernel_dev, dir_name, kernel_type, flags, options);
  5. ......
  6. }

接下来的调用链为:do_mount -> do_new_mount -> vfs_kern_mount

  1. struct vfsmount *
  2. vfs_kern_mount(struct file_system_type *type, int flags, const char *name, void *data)
  3. {
  4. ......
  5. mnt = alloc_vfsmnt(name);
  6. ......
  7. root = mount_fs(type, flags, name, data);
  8. ......
  9. mnt->mnt.mnt_root = root;
  10. mnt->mnt.mnt_sb = root->d_sb;
  11. mnt->mnt_mountpoint = mnt->mnt.mnt_root;
  12. mnt->mnt_parent = mnt;
  13. list_add_tail(&mnt->mnt_instance, &root->d_sb->s_mounts);
  14. return &mnt->mnt;
  15. }

首先创建一个 struct mount 结构,每一个被挂载的文件系统在内核中都对应这样一个结构,定义如下

  1. struct mount {
  2. struct hlist_node mnt_hash;
  3. struct mount *mnt_parent;
  4. struct dentry *mnt_mountpoint;
  5. struct vfsmount mnt;
  6. union {
  7. struct rcu_head mnt_rcu;
  8. struct llist_node mnt_llist;
  9. };
  10. struct list_head mnt_mounts; /* list of children, anchored here */
  11. struct list_head mnt_child; /* and going through their mnt_child */
  12. struct list_head mnt_instance; /* mount instance on sb->s_mounts */
  13. const char *mnt_devname; /* Name of device e.g. /dev/dsk/hda1 */
  14. struct list_head mnt_list;
  15. ......
  16. } __randomize_layout;
  17. struct vfsmount {
  18. struct dentry *mnt_root; /* root of the mounted tree */
  19. struct super_block *mnt_sb; /* pointer to superblock */
  20. int mnt_flags;
  21. } __randomize_layout;
  • mnt_parent:挂载点所在的文件系统对应的 struct mount
  • mnt_mountpoint:挂载点在父文件系统的 dentry

    struct dentry 表示目录项对象,并于对应的目录或文件的inode关联

  • mnt_root:当前文件系统根目录的dentry
  • mnt_sb:指向超级块

接下来调用 mount_fs 挂载文件系统

  1. struct dentry *
  2. mount_fs(struct file_system_type *type, int flags, const char *name, void *data)
  3. {
  4. struct dentry *root;
  5. struct super_block *sb;
  6. ......
  7. root = type->mount(type, flags, name, data);
  8. ......
  9. sb = root->d_sb;
  10. ......
  11. }

这里调用了 type->mount,如果是ext4,那么就对应上面提到的 ext4_mount,通过这个函数,从文件系统中获得超级块。在文件系统的实现中,每个在磁盘中的结构,在内存中也对应相同形式的结构,当所有的数据结构读到内存中,内核就可以通过操作这些数据结构,来操作文件系统了

下面举一个例子,来解析 mount 之后,各数据结构之间的关系

假设根文件系统下面有一个目录 home,然后将另一个文件系统A挂载到根文件系统的 home 目录下面。在文件系统A的根目录下面,有一个文件夹 hello,所以就有了 /home/hello 目录。然后将文件系统B挂载到 /home/hello 目录下,而文件系统B根目录下面有一个文件夹 work,work 下面有一个文件夹 data,所以就有了目录 /home/hello/work/data

为了维护这些关系,内核创建并维护了这些数据结构,如下图

在这里插入图片描述

黄色部分为 struct mount,每个被挂载的文件系统在内核中都对应一个 struct mount

绿色部分为 struct file,每个打开的文件都有一个 struct file,里面有两个变量,一个指向对应的 struct mount,一个指向对应的 struct dentry

红色部分为 struct dentry,叫做目录项对象,每个目录或者文件都对应一个 struct dentry,用于与 inode 相关联

三、打开文件

打开文件通过系统调用 sys_open,定义如下

  1. SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
  2. {
  3. ......
  4. return do_sys_open(AT_FDCWD, filename, flags, mode);
  5. }
  6. long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
  7. {
  8. ......
  9. fd = get_unused_fd_flags(flags);
  10. if (fd >= 0) {
  11. struct file *f = do_filp_open(dfd, tmp, &op);
  12. if (IS_ERR(f)) {
  13. put_unused_fd(fd);
  14. fd = PTR_ERR(f);
  15. } else {
  16. fsnotify_open(f);
  17. fd_install(fd, f);
  18. }
  19. }
  20. putname(tmp);
  21. return fd;
  22. }

要打开一个文件,首先要通过 get_unused_fd_flags 获取一个没有用的文件描述符,如何获取没有用的文件描述符呢?

每个进程 task_struct 都有一个 files_struct

  1. struct files_struct *files;

files_struct 里面有一个文件描述符数组

  1. struct files_struct {
  2. ......
  3. struct file __rcu * fd_array[NR_OPEN_DEFAULT];
  4. };

每打开一个文件,就会在这个数组中找到第一个未被使用的项,下标就是文件描述符

对于一个进程,默认的文件描述符0就是 stdin,文件描述符1就是 stdout,文件描述符2就是 stderr

文件描述符数组中的每一项都是一个 struct file 指针,也就是说,每一个打开的文件都对应一个 struct file

do_sys_open 中调用 do_filp_open,就是创建这个 struct file 结构,然后 fd_install(fd, f) 是将文件描述符和这个结构体关联起来

  1. struct file *do_filp_open(int dfd, struct filename *pathname,
  2. const struct open_flags *op)
  3. {
  4. ......
  5. set_nameidata(&nd, dfd, pathname);
  6. filp = path_openat(&nd, op, flags | LOOKUP_RCU);
  7. ......
  8. restore_nameidata();
  9. return filp;
  10. }

do_filp_open 首先调用 set_nameidata 解析路径,我们知道文件就是一串的路径名称,需要逐个解析。这个结构就是解析和查找路径的时候做辅助作用

在 struct nameidata 有一个关键的成员变量 struct path

  1. struct path {
  2. struct vfsmount *mnt;
  3. struct dentry *dentry;
  4. } __randomize_layout;

其中的 struct vfs_mount 与文件系统的挂载有关。另一个是 struct dentry,可以用来表示目录和文件,建立文件名和 inode 之间的关联

接下来是 path_openat,主要做以下几件事

  1. static struct file *path_openat(struct nameidata *nd,
  2. const struct open_flags *op, unsigned flags)
  3. {
  4. ......
  5. file = get_empty_filp();
  6. ......
  7. s = path_init(nd, flags);
  8. ......
  9. while (!(error = link_path_walk(s, nd)) &&
  10. (error = do_last(nd, file, op, &opened)) > 0) {
  11. ......
  12. }
  13. terminate_walk(nd);
  14. ......
  15. return file;
  16. }
  • get_empty_filp:生成一个 struct file 结构
  • path_init:初始化 nameidata,准备开始节点路径查找
  • link_path_walk:对于路径名逐层进行文件节点的查找
  • do_last:获取文件对应的 inode 对象,并且初始化 file 对象

例如,文件 /root/hello/work/data,link_path_walk 会解析前面的路径部分 /root/hello/work,解析完毕后,nameidata 的 dentry 就是路径最后的上一级目录对应的 dentry,这里是 work,而 nameidata 的filename 为路径的最后一部分 data,解析完后之后,交给 do_last 做下一步处理

do_last 的定义如下

  1. static int do_last(struct nameidata *nd,
  2. struct file *file, const struct open_flags *op,
  3. int *opened)
  4. {
  5. ......
  6. error = lookup_fast(nd, &path, &inode, &seq);
  7. ......
  8. error = lookup_open(nd, &path, file, op, got_write, opened);
  9. ......
  10. error = vfs_open(&nd->path, file, current_cred());
  11. ......
  12. }

在这里面,我们需要先查找文件路径最后一部分对应的 dentry,如何查找呢?

Linux 为了提高目录项对象的处理效率,实现了目录项高速缓存 dentry cache,简称 dcache,它主要由两个数据结构组成

  • 哈希表 dentry_hashtable:dcache 中的所有 dentry 对象都通过 d_hash 指针链到相应的 dentry 哈希表中
  • 未使用的 dentry 对象链表 s_dentry_lru:dentry 对象通过其 d_lru 指针链入 LRU 链表中。LRU 的意思是最近最少使用

img

这两个列表会产生复杂的关系:

  • 引用次数为0:一个散列表的 dentry 没有被引用,就会加到LRU链表中
  • 再次被引用:一个在LRU链表中的 dentry 再次被引用了,则从LRU链表中移除
  • 分配:当 dentry 在散列表中没有找到,则从 Slub 分配器中分配
  • 过期归还:当LRU链表中最长时间没有被使用的 dentry 应该释放回 Slub 分配器
  • 文件删除:文件被删除了,相应的 dentry 应该释放回 Slub 分配器
  • 结构复用:当需要分配一个 dentry,但是无法分配新的,就从LRU链表中取出一个来复用

所以,do_last 在查找 dentry 的时候,会先从缓存中查找,调用的是 lookup_fast

如果缓存中没有找到,那么就需要到文件系统中查找,通过 lookup_open 实现,定义如下

  1. static int lookup_open(struct nameidata *nd, struct path *path,
  2. struct file *file,
  3. const struct open_flags *op,
  4. bool got_write, int *opened)
  5. {
  6. ......
  7. dentry = d_alloc_parallel(dir, &nd->last, &wq);
  8. ......
  9. struct dentry *res = dir_inode->i_op->lookup(dir_inode, dentry,
  10. nd->flags);
  11. ......
  12. path->dentry = dentry;
  13. path->mnt = nd->path.mnt;
  14. }
  15. const struct inode_operations ext4_dir_inode_operations = {
  16. .create = ext4_create,
  17. .lookup = ext4_lookup,
  18. ...

lookup_open 会创建一个新的 dentry,然后调用上一级目录的 inode 的 inode_operations 的 lookup 函数,对于 ext4 文件系统来说,调用的是 ext4_lookup,会到文件系统中去寻找 inode,找到后将新生成的 dentry 赋给 path 变量

do_last 最后一步调用的是 vfs_open

  1. int vfs_open(const struct path *path, struct file *file,
  2. const struct cred *cred)
  3. {
  4. struct dentry *dentry = d_real(path->dentry, NULL, file->f_flags, 0);
  5. ......
  6. file->f_path = *path;
  7. return do_dentry_open(file, d_backing_inode(dentry), NULL, cred);
  8. }
  9. static int do_dentry_open(struct file *f,
  10. struct inode *inode,
  11. int (*open)(struct inode *, struct file *),
  12. const struct cred *cred)
  13. {
  14. ......
  15. f->f_mode = OPEN_FMODE(f->f_flags) | FMODE_LSEEK |
  16. FMODE_PREAD | FMODE_PWRITE;
  17. path_get(&f->f_path);
  18. f->f_inode = inode;
  19. f->f_mapping = inode->i_mapping;
  20. ......
  21. f->f_op = fops_get(inode->i_fop);
  22. ......
  23. open = f->f_op->open;
  24. ......
  25. error = open(inode, f);
  26. ......
  27. f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);
  28. file_ra_state_init(&f->f_ra, f->f_mapping->host->i_mapping);
  29. return 0;
  30. ......
  31. }
  32. const struct file_operations ext4_file_operations = {
  33. ......
  34. .open = ext4_file_open,
  35. ......
  36. };

vfs_open 最重要的一件事就是,调用 f_open->open,也就是 ext4_file_open。另外还有一件重要的事情就是将打开文件的所有信息天道 struct file 结构中

  1. struct file {
  2. union {
  3. struct llist_node fu_llist;
  4. struct rcu_head fu_rcuhead;
  5. } f_u;
  6. struct path f_path;
  7. struct inode *f_inode; /* cached value */
  8. const struct file_operations *f_op;
  9. spinlock_t f_lock;
  10. enum rw_hint f_write_hint;
  11. atomic_long_t f_count;
  12. unsigned int f_flags;
  13. fmode_t f_mode;
  14. struct mutex f_pos_lock;
  15. loff_t f_pos;
  16. struct fown_struct f_owner;
  17. const struct cred *f_cred;
  18. ......
  19. struct address_space *f_mapping;
  20. errseq_t f_wb_err;
  21. }

四、总结

下面这张图总结了文件系统中每个结构之间的关系

img

对于每一个进程,都有一个文件描述符数组,数组的下标是文件描述符,数组项指向打开文件对应的 struct file 结构

struct file 中有变量指向文件对应的 inode,还有关于文件的操作集 file_operations,这个操作集根据文件系统类型不同而不同

每一个打开的文件都有一个对应的 struct dentry,虽然叫做目录项对象,但是它不仅表示目录,还可以表示文件,它的重要作用就是指向该文件对应的 inode

dentry 是放在 dentry cache 中,如果文件关闭了,它依旧存在,它可以长期维护文件路径和硬盘上文件的关系

发表评论

表情:
评论列表 (有 0 条评论,150人围观)

还没有评论,来说两句吧...

相关阅读

    相关 linux虚拟文件系统

    虚拟文件系统(VFS):抽象层,提供统一接口,支持各种文件系统,屏蔽各种文件系统和硬件的差异 各种文件系统:有的不把目录当做文件,有的没有inode(比如文件控制信息和文件