页缓存page cache和地址空间address_space

前言

  • 在学习 mmap 的时候,看到物理地址和进程虚拟地址建立–的映射关系的时候,遇到页缓存 page cache 和地址空间 address_space,不清楚,故查阅资料,学习并做此总结

  • 在 Linux 操作系统中,当应用程序需要读取文件中的数据时,操作系统先分配一些内存,将数据从存储设备读入到这些内存中,然后再将数据分发给应用程序;当需要往文件中写数据时,操作系统先分配内存接收用户数据,然后再将数据从内存写到磁盘上。文件 Cache 管理指的就是对这些由操作系统分配,并用来存储文件数据的内存的管理。 Cache 管理的优劣通过两个指标衡量:一是 Cache 命中率,Cache 命中时数据可以直接从内存中获取,不再需要访问低速外设,因而可以显著提高性能;二是有效 Cache 的比率,有效 Cache 是指真正会被访问到的 Cache 项,如果有效 Cache 的比率偏低,则相当部分磁盘带宽会被浪费到读取无用 Cache 上,而且无用 Cache 会间接导致系统内存紧张,最后可能会严重影响性能

  • 在Linux 2.4内核中块缓存 buffer cache 和页缓存 page cache 是并存的,表现的现象是同一份文件的数据,可能即出现在 buffer cache中,又出现在页缓存中,这样就造成了物理内存的浪费。

    • Linux 2.6内核对两个 cache 进行了合并,统一使用页缓存在做缓存,只有极少数的情况下才使用到 buffer cache
    • 每一个 Page Cache 包含若干 Buffer Cache
  • 内存管理系统和 VFS(virtual file system) 只与 Page Cache 交互,内存管理系统负责维护每项 Page Cache 的分配和回收,同时在使用 memory map 方式访问时负责建立映射;VFS 负责 Page Cache 与用户空间的数据交换。而具体文件系统则一般只与 Buffer Cache 交互,它们负责在外围存储设备和 Buffer Cache 之间交换数据

buffer cache和page cache的区别

  • 我们要理解的是不管是buffer cache还是page cache都是为了处理块设备和内存交互时高速访问的问题

  • buffer cache是面向底层块设备的,所以它的粒度是文件系统的块,块设备和系统采用块进行交互。块再转换成磁盘的基本物理结构扇区。扇区的大小是512KB,而文件系统的块一般是2KB, 4KB, 8KB。扇区和块之间是可以快速转换的

    • 随着内核的功能越来越完善,块粒度的缓存已经不能满足性能的需要。内核的内存管理组件采用了比文件系统的块更高级别的抽象,页page,页的大小一般从4KB到2MB,粒度更大,处理的性能更高。所以缓存组件为了和内存管理组件更好地交互,创建了页缓存page cache来代替原来的buffer cache

    • 页缓存是面向文件,面向内存的。通过一系列的数据结构,比如inode, address_space, page,将一个文件映射到页的级别,通过page + offset就可以定位到一个文件的具体位置

  • buffer cache实际操作时按块为基本单位,page cache操作时按页为基本单位,新建了一个BIO的抽象,可以同时处理多个非连续的页的IO操作,也就是所谓的scatter/gather IO

  • buffer cache目前主要用在需要按块传输的场景下,比如超级块的读写等。而 page cache 可以用在所有以文件为单元的场景下,比如网络文件系统等等,缓存组件抽象了地址空间 address_space 这个概念来作为文件系统和页缓存的中间适配器,屏蔽了底层设备的细节

  • buffer cache 可以和 page cache 集成在一起,属于一个 page 的块缓存使用 buffer_head 链表的方式组织,page_cache 维护了一个private 指针指向这个 buffer_head 链表,buffer_head 链表维护了一个指针指向这个页 page。这样只需要在页缓存中存储一份数据即可

  • 文件系统的 inode 实际维护了这个文件所有的块 block 的块号,通过对文件偏移量 offset 取模可以很快定位到这个偏移量所在的文件系统的块号,磁盘的扇区号。同样,通过对文件偏移量 offset 进行取模可以计算出偏移量所在的页的偏移量,地址空间 address_space 通过指针可以方便的获取两端 inode 和 page 的信息,所以可以很方便地定位到一个文件的offset 在各个组件中的位置

  • 文件字节偏移量 –> 页偏移量 –> 文件系统块号 block –> 磁盘扇区号

- 页缓存page cache和地址空间address_space

  • page_cache

    • page cache 是面向内存,面向文件的,这正好说明了页缓存的作用,它位于内存和文件之间,文件IO操作实际上只和页缓存交互,不直接和内存交互

    • Linux内核使用 page 数据结构来描述物理内存页帧,内核创建了mem_map 数组来表示所有的物理页帧,mem_map 的数组项就是 page

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      struct page{
      unsigned long flags;
      atomic_t _count;
      atomic_t _mapcount;
      unsigned long private;
      struct address_space *mapping;
      pgoff_t index;
      struct list_head lru;
      void *virtual;
      };
      • 参数:
        • 一些标志位flags来表示该页是否是脏页,是否正在被写回等等
        • _count, _mapcount表示这个页被多少个进程使用和映射
        • private指针指向了这个页对应的buffer cache的buffer_head链表,建立了页缓存和块缓存的联系
        1. mapping指向了地址空间address_space,表示这个页是一个页缓存中页,和一个文件的地址空间对应
        2. index是这个页在文件中的页偏移量,通过文件的字节偏移量可以计算出文件的页偏移量
    • 页缓存实际上就是采用了一个基数树结构将一个文件的内容组织起来存放在物理内存page中。文件IO操作直接和页缓存交互。采用缓存原理来管理块设备的IO操作

      1
      2
      3
      4
      5
      struct radix_tree_root{
      unsigned int height;
      gfp_t gfp_mask;
      struct radix_tree_root *rnode;
      };
    • 文件的每个数据块最多只能对应一个 Page Cache 项,它通过两个数据结构来管理这些 Cache 项,一个是 radix tree(一种搜索树,来快速定位 Cache 项),另一个是双向链表(active_list 和 inactive_list 两个双向链表,实现物理内存的回收)

    • 一个文件inode对应一个地址空间address_space。而一个address_space对应一个页缓存基数树,这几个组件的关系如下:

  • address_space

  • address_space 是Linux内核中的一个关键抽象,它是页缓存和外部设备中文件系统的桥梁,可以说关联了内存系统和文件系统,文件系统可以理解成数据源

    • inode 指向这个地址空间的宿主,也就是数据源
    • page_tree 指向了这个地址空间对应的页缓存的基数树。这样就可以通过inode --> address_space --> page_tree找打一个文件对应的页缓存页

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      struct address_space{
      struct inode *host; //所有者:inode或块设备
      struct radix_tree_root page_tree; //所有页的基数树
      unsigned int i_mmap_wrutable; //VM_SHAREAD映射的计数
      struct prio_tree_root i_mmap; //私有和共享映射的树
      struct list_head i_mmap_nonlinear; //VM_NONLINEAR映射的链表元素
      unsigned long nrpages; //页的总数
      pgoff_t writeback_index; //回写由此开始
      struct address_space_operations *a_ops; //方法,即地址空间操作
      unsigned long flags; //错误标志位/gfp掩码
      struct backing_dev_info *backing_dev_info;//设备预读
      struct list_head; private_list;
      struct address_space private_list;
      } __attribute__((aligned(sizeof(long))));
    • 读文件时,首先通过要读取的文件内容的偏移量offset计算出要读取的页,然后通过该文件的inode找到这个文件对应的地址空间address_space,然后在address_space中访问该文件的页缓存,如果页缓存命中,那么直接返回文件内容,如果页缓存缺失,那么产生一个页缺失异常,创业一个页缓存页,然后从磁盘中读取相应文件的页填充该缓存页,租后从页缺失异常中恢复,继续往下读

    • 写文件时,首先通过所写内容在文件中的偏移量计算出相应的页,然后还是通过inode找到address_space,通过address_space找到页缓存中页,如果页缓存命中,直接把文件内容修改更新在页缓存的页中。写文件就结束了。这时候文件修改位于页缓存,并没有写回writeback到磁盘文件中去
    • 一个页缓存中的页如果被修改,那么会被标记成脏页。脏页需要写回到磁盘中的文件块。有两种方式可以把脏页写回磁盘,也就是flush
      • 手动调用sync()或者fsync()系统调用把脏页写回
      • pdflush进程会定时把脏页写回到磁盘
    • 脏页不能被置换出内存,如果脏页正在被写回,那么会被设置写回标记,这时候该页就被上锁,其他写请求被阻塞直到锁释放
    • 在某些情况下我们可能需要绕过页缓存机制,比如系统存在大日志的情况,比如数据库系统,日志不会被经常重复读取,如果都缓存在内存中会影响系统的性能。内核提供了直接IO的方式,O_DIRECT,可以绕过页缓存,直接把文件内容从堆中写到磁盘文件

    • 普通文件IO需要复制两次,第一次复制是从磁盘到内存缓冲区,第二次是从内存缓冲区到进程的堆

      • 从磁盘中读取文件相应的页填充页缓存中的页,也就是第一次复制
      • 从页缓存的页复制内容到文件进程的堆空间的内存中,也就是第二次复制
      • 最后物理内存同一个文件的内容存在了两份拷贝,一份是页缓存,一份是用户进程的堆空间对应的物理内存空间

总结

  • 用户进程访问内存只能通过页表结构,内核可以通过虚拟地址直接访问物理内存。
  • 用户进程不能访问内核的地址空间,这里的地址空间指的是虚拟地址空间,这是肯定的,因为用户进程的虚拟地址空间和内核的虚拟地址空间是不重合的,内核虚拟地址空间必须特权访问
  • page结构表示物理内存页帧,同一个物理内存地址可以同时被内核进程和用户进程访问,只要将用户进程的页表项也指向这个物理内存地址。也就是mmap的实现原理

参考文章

-------------本文结束感谢您的阅读-------------
谢谢你请我吃糖果!
0%