• 【Linux操作系统】 虚拟文件系统 | 文件缓存


    虚拟文件系统

    • 核心就是把磁盘文件数据映射到进程中,可以把在磁盘的文件组织看成一种协议,内存中进程中的文件组织形式看成另外一种协议,内核就是这两个协议的中转proxy.
      在这里插入图片描述
    • 多层组件统一完成进行读写文件的任务
      • 系统调用 sys_open, sys_read 等
      • 进程维护打开的文件数据结构, 系统维护所有打开的文件数据结构
      • Linux 提供统一的虚拟文件系统接口; 例如 inode, directory entry, mount, 以及对应操作 inode operations等, 因此可以同时支持数十种不同的文件系统
      • vfs 通过设备 I/O 层在通过块设备驱动程序访问硬盘文件系统
      • 通过缓存层加快块设备读写
    • 通过解析系统调用了解内核架构

    挂载文件系统 mount

    • 注册文件系统 register_filesystem 后才能挂载
      - 调用链 mount->do_mount->do_new_mount→vfs_kern_mount
      - vfs_kern_mount 首先创建 struct mount
      • 其中 mnt_parent 指向父 fs 的 mount; mnt_parentpoint 指向父 fs 的 dentry
      • 用 dentry 表示目录, 并和目录的 inode 关联
      • mnt_root 指向当前 fs 根目录的 dentry; 还有 vfsmount 指向挂载树 root 和超级块 mnt_sb
    • 调用 mount_fs 进行挂载
      - 调用 ext4_fs_type→mount(ext4_mount), 读取超级块到内存
      - 文件和文件夹都有一个 dentry, 用于与 inode 关联, 每个挂载的文件系统都由一个 mount 描述; 每个打开的文件都由 file 结构描述, 其指向 dentry mount.
      - 二层文件系统根目录有两个 dentry, 一个表示挂载点, 另一个是上层 fs 的目录.
      在这里插入图片描述
      A 挂载到 home 目录下 /home/hello
      B 挂载到 A 有 /home/hello/world/data。 维护看上图
      看 dentry、 mount 、file 斜线

    打开文件 sys_open

    • 先获取一个未使用的 fd, 其中 task_struct.files.fd_array[] 中每一项指向打开文件的 struct file, 其中 fd 作为下标. 对于一个进程 : 默认情况下,文件描述符fd= 0 表示 stdin 标准输入,文件描述符 1 表示 stdout 标准输出,文件描述符 2 表示 stderr 标准错误输出。
      - 调用 do_sys_open->do_flip_open
      • 先初始化 nameidata, 解析文件路径名; 接着调用 path_openat
        • 创建 struct file 结构; 初始化 nameidata, 准备查找
        • link_path_walk 根据路径名逐层查找 以 ‘/’ 分隔
        • do_last 获取文件 inode, 初始化 file
        • nameidata 为 路径名的最后的那个

    查找路径最后一部分对应的 dentry

    在这里插入图片描述

    • Linux 通过目录项高速缓存 dentry cache(dentry) 提高效率. 由两个数据结构组成
      - 哈希表: dentry_hashtable; 引用变为 0 后加入 lru 链表; dentry 没找到则从 slub 分配; 无法分配则从 lru 中获取; 文件删除释放 dentry;
      - 未使用的 dentry lru 链表; 再次被引用返回哈希表; dentry 过期返回给 slub 分配器
      - do_last 先从缓存查找 dentry, 若没找到在从文件系统中找并创建 dentry, 再赋给 nameidata 的 path.dentry; 最后调用 vfs_open 真正打开文件
      - vfs_open 会调用 f_op->open 即 ext4_file_open, 还将文件信息存入 struct file 中.
      - 许多结构体中都有自己对应的 operation 结构, 方便调用对应的函数进行处理

    很重要的一张图
    在这里插入图片描述

    文件缓存

    系统调用层和虚拟文件系统层

    -文件系统的读写: 调用 read/write 进行读写 → vfs_read/write → __vfs_read/write
    - 打开文件时创建 struct file, 其中有 file_operations, 虚拟文件系统调用 operations 中的 read/write

    ext4 文件系统层

    • 调用到 generic_file_read/write_iter, 其中判断是否需要使用缓存
      - 缓存, 即内存中一块空间, 内存比硬盘快 , 通常会选择不直接操作硬盘,而是将读写都在内存中,根据是否使用内存做缓存,把文件的 IO 操作分为俩类.
      • 缓存 I/O: 文件IO默认为缓存IO, 读操作先检测缓存区中是否有, 若无则从文件系统读取并缓存; 写操作直接从用户空间赋值到内核缓存中, 再由 OS 决定或用户调用 sync 写回磁盘
      • 直接 I/O: 程序直接访问磁盘, 不经过缓存
    • 直接 I/O 过程:
      • 读: 若设置了 IOCB_DIRECT, 调用 address_space 的 direct_io 直接读取硬盘( 文件与内存页映射) ; 若使用缓存也要调用 address_sapce 进行文件与内存页的映射
      • 写: 若设置了 IOCB_DIRECT, 调用块设备驱动直接写入磁盘

    带缓存写过程

    • 带缓存写入的函数 generic_perform_write,在 while 循环中, 找出写入影响的页, 并依次写入, 完成以下四步
      - 每一页调用 write_begin 做准备
      - 将写入内容从用户态拷贝到内核态
      - 调用 write_end 完成写入
      - 查看脏页 (未写入磁盘的缓存) 是否过多, 是否需要写回磁盘

        +  第一步、 调用 ext4_write_begin。 
          + ext4 是日志文件系统, 通过日志避免断电数据丢失
            - 文件分为元数据和数据, 其操作日志页分开维护 , 它有三种模式 :
                - Journal 模式下: 写入数据前, 元数据及数据日志必须落盘, 安全但性能差
                - Order 模式下: 只记录元数据日志, 写日志前, 数据必须落盘, 折中
                - Writeback 模式下: 仅记录元数据日志, 数据不用先落盘
            - 然后调用grab_cache_page_write_begin 得到应该写入的缓存页
            - 内核中缓存以页为单位, 打开文件的 file 结构中用 radix tree 维护文件的缓存页
        - 第二步 : iov_iter_copy_from_user_atomic 拷贝内容, kmap_atomic 将缓存页映射到内核虚拟地址; 将拥护他数据拷贝到内核态; kunmap_aotmic 解映射
        - 第三步:write_end, 先完成日志写入 并将缓存设置为脏页,其实所谓的完成写入,并没有真正写入硬盘,仅仅是写入缓存后,标记为**脏页**。但是发生宕机数据就没有了,所以需要一种机制,将写入的页面真正写到硬盘中,我们称为回写(**Write Back**)。			
        - 第四步 :调用 balance_dirty_pages_ratelimited 若发先脏页超额, 启动一个线程执行回写.
            - 回写任务 delayed_work 挂在 bdi_wq  队列, 若delay 设为 0, 马上执行回写
            - bdi = backing device info 描述块设备信息, 初始化块设备时回初始化 timer, 到时会执行写回函数
        - 另外其他情况也会回写 : 
        	-调用write 发现缓存的数据太多 
            - 用户调用 sync 或内存紧张时, 回调用 wakeup_flusher_threads 刷回脏页
            - 脏页时间超过 timer, 及时回写
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17

    带缓存的读操作

    • generic_file_buffered_read 从 page cache 中判断是否由缓存页
      - 若没则从文件系统读取并预读并缓存, 再次查找缓存页
      - 若有, 还需判断是否需要预读, 若需要调用 page_cache_async_readahead
      - 最后调用 copy_page_to_user 从内核拷贝到用户空间

    总结

    在系统调用层我们需要仔细学习 read 和 write。在 VFS 层调用的是 vfs_read 和 vfs_write 并且调用 file_operation。在 ext4 层调用的是 ext4_file_read_iter 和 ext4_file_write_iter。
    接下来就是分叉。
    你需要知道缓存 I/O 和直接 I/O。直接 I/O 读写的流程是一样的,调用 ext4_direct_IO,再往下就调用块设备层了。缓存 I/O 读写的流程不一样。对于读,从块设备读取到缓存中,然后从缓存中拷贝到用户态。对于写,从用户态拷贝到缓存,设置缓存页为脏,然后启动一个线程写入块设备。

    在这里插入图片描述

  • 相关阅读:
    DolphinScheduler
    python异常常见处理
    redis事务
    操作系统-死锁,锁
    复现xss绕过一个循环和两个循环
    MySQL 数据库设计范式
    1064 Complete Binary Search Tree
    【附源码】计算机毕业设计JAVA宠物管理系统
    芯科科技第二代平台的所有蓝牙片上系统均可支持蓝牙技术联盟的新功能和新标准
    工业控制系统所面临的安全威胁
  • 原文地址:https://blog.csdn.net/weixin_49486457/article/details/126058049