页高速缓存和页回写[1]

本文介绍了Linux内核中的页高速缓存机制,包括其如何减少磁盘I/O操作、提高数据访问速度,并详细解析了页高速缓存的内部结构如address_space及radix tree的实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

   有种欲哭无泪的感觉。又是一时失手,把原本写的内容刷掉了。呜呜~~
  重新开始!!
  页高速缓存(cache)是Linux内核实现的一种主要磁盘缓存。它主要用来减少对磁盘的IO操作。具体地讲,就是通过把磁盘中的数据缓存到物理内存中,把对磁盘的访问变为对物理内存的访问。
  磁盘缓存的优点在于:1,访问磁盘的速度远远低于访问内存的速度,因此从内存访问数据要比从磁盘访问速度快。2,数据一旦被访问,就很有可能在短期内被再 次访问到。这种在短期内集中访问同一片数据的原理被称作临时局部原理temporal locality。临时局部原理保证:如果在第一次访问数据时缓存它,那就极有可能在短期内再次被高速缓存命中。
   页高速缓存是由RAM中的物理页组成,缓存中每一页都对应着磁盘中的多个块。每当内核开始执行一个页IO操作时(通常对普通文件中页大小的块进行磁盘操作),首先会检查需要的数据是否在高速缓存中,如果在,那么内核就直接使用高速缓存中的数据,从而避免访问磁盘。
   通过块IO缓冲区把独立的磁盘块与页高速缓存联系在一起。在块IO层中,一个缓冲(buffer)就是一个单独物理磁盘块在内存中的表示,缓冲就是内存到 磁盘块的映射描述符。因此通过磁盘块以及缓冲块IO操作,页高速缓存同一可以减少块IO操作期间的磁盘访问量。这种缓存经常被称为“缓冲区高速缓存”,它 实际上并不是一个独立的缓存,而是页高速缓存的一部分。
  每次页IO操作都要处理数据的全部页面,就需要对一个以上的磁盘块进行操作,所以页高速缓存实际缓存的是页面大小的文件块。
  块IO操作每次操作一个单独的磁盘块,比如读写索引节点就是一个典型的IO操作。内核提供bread()底层函数从磁盘读单个块,通过缓冲,这些磁盘块被映射到内存中相应的页面上,这样就被缓存到页高速缓存里了。
  • 页高速缓存
  页高速缓存缓存的是页面。缓存中的页来自对正规文件、块设备文件和内存映射文件的读写。这样,页高速缓存内就包含了最近被访问的文件的全部页面。
  一个物理页可能由多个不连续的物理磁盘块组成。因为文件本身可能分布在磁盘各个位置,所以页面中映射的块也不需要连续。这样的话,在页高速缓存中检测特定数据是否已被缓存是件非常困难的事。因此不能用设备名称和块号来作页高速缓存中数据的索引。
  Linux页高速缓存的目标是缓存任何基于页的对象,这包含各种类型的文件和各种类型的内存映射。Linux页高速缓存使用address_space结构体描述页高速缓存中的页面。
  1. 在include/fs.h中:
  2. struct backing_dev_info;
  3. struct address_space {
  4.     struct inode        *host;      /* owner: inode, block_device */
  5.     struct radix_tree_root  page_tree;  /* radix tree of all pages */
  6.     rwlock_t        tree_lock;  /* and rwlock protecting it */
  7.     unsigned int        i_mmap_writable;/* count VM_SHARED mappings */
  8.     struct prio_tree_root   i_mmap;     /* tree of private and shared mappings */
  9.     struct list_head    i_mmap_nonlinear;/*list VM_NONLINEAR mappings */
  10.     spinlock_t      i_mmap_lock;    /* protect tree, count, list */
  11.     unsigned int        truncate_count; /* Cover race condition with truncate */
  12.     unsigned long       nrpages;    /* number of total pages */
  13.     pgoff_t         writeback_index;/* writeback starts here */
  14.     const struct address_space_operations *a_ops;   /* methods */
  15.     unsigned long       flags;      /* error bits/gfp mask */
  16.     struct backing_dev_info *backing_dev_info; /* device readahead, etc */
  17.     spinlock_t      private_lock;   /* for use by the address_space */
  18.     struct list_head    private_list;   /* ditto */
  19.     struct address_space    *assoc_mapping; /* ditto */
  20. } __attribute__((aligned(sizeof(long))));


  其中i_mmap字段是一个优先搜索树,它的搜索范围包含了在address_space中所有共享的与私有的映射页面。优先搜索树是一种巧妙的将堆和radix树结合的快速检索树。
  1. 在linux/prio_tree.h中
  2. struct prio_tree_node {
  3.     struct prio_tree_node   *left;
  4.     struct prio_tree_node   *right;
  5.     struct prio_tree_node   *parent;
  6.     unsigned long       start;
  7.     unsigned long       last;   /* last location _in_ interval */
  8. };
  9. struct prio_tree_root {
  10.     struct prio_tree_node   *prio_tree_node;
  11.     unsigned short      index_bits;
  12.     unsigned short      raw;
  13.         /*
  14.          * 0: nodes are of type struct prio_tree_node
  15.          * 1: nodes are of type raw_prio_tree_node
  16.          */
  17. };
  优先搜索树的具体实现在lib/prio.c中:
  1. static unsigned long index_bits_to_maxindex[BITS_PER_LONG];
  2. void __init prio_tree_init(void)
  3. {
  4.     unsigned int i;
  5.     for (i = 0; i < ARRAY_SIZE(index_bits_to_maxindex) - 1; i++)
  6.         index_bits_to_maxindex[i] = (1UL << (i + 1)) - 1;
  7.     index_bits_to_maxindex[ARRAY_SIZE(index_bits_to_maxindex) - 1] = ~0UL;
  8. }
  9. /*
  10.  * Insert a prio_tree_node @node into a radix priority search tree @root. The
  11.  * algorithm typically takes O(log n) time where 'log n' is the number of bits
  12.  * required to represent the maximum heap_index. In the worst case, the algo
  13.  * can take O((log n)^2) - check prio_tree_expand.
  14.  *
  15.  * If a prior node with same radix_index and heap_index is already found in
  16.  * the tree, then returns the address of the prior node. Otherwise, inserts
  17.  * @node into the tree and returns @node.
  18.  */
  19. struct prio_tree_node *prio_tree_insert(struct prio_tree_root *root,
  20.         struct prio_tree_node *node)
  21. {
  22.     struct prio_tree_node *cur, *res = node;
  23.     unsigned long radix_index, heap_index;
  24.     unsigned long r_index, h_index, index, mask;
  25.     int size_flag = 0;
  26.     get_index(root, node, &radix_index, &heap_index);
  27.     if (prio_tree_empty(root) ||
  28.             heap_index > prio_tree_maxindex(root->index_bits))
  29.         return prio_tree_expand(root, node, heap_index);
  30.     cur = root->prio_tree_node;
  31.     mask = 1UL << (root->index_bits - 1);
  32.     while (mask) {
  33.         get_index(root, cur, &r_index, &h_index);
  34.         if (r_index == radix_index && h_index == heap_index)
  35.             return cur;
  36.                 if (h_index < heap_index ||
  37.             (h_index == heap_index && r_index > radix_index)) {
  38.             struct prio_tree_node *tmp = node;
  39.             node = prio_tree_replace(root, cur, node);
  40.             cur = tmp;
  41.             /* swap indices */
  42.             index = r_index;
  43.             r_index = radix_index;
  44.             radix_index = index;
  45.             index = h_index;
  46.             h_index = heap_index;
  47.             heap_index = index;
  48.         }
  49.         if (size_flag)
  50.             index = heap_index - radix_index;
  51.         else
  52.             index = radix_index;
  53.         if (index & mask) {
  54.             if (prio_tree_right_empty(cur)) {
  55.                 INIT_PRIO_TREE_NODE(node);
  56.                 cur->right = node;
  57.                 node->parent = cur;
  58.                 return res;
  59.             } else
  60.                 cur = cur->right;
  61.         } else {
  62.             if (prio_tree_left_empty(cur)) {
  63.                 INIT_PRIO_TREE_NODE(node);
  64.                 cur->left = node;
  65.                 node->parent = cur;
  66.                 return res;
  67.             } else
  68.                 cur = cur->left;
  69.         }
  70.         mask >>= 1;
  71.         if (!mask) {
  72.             mask = 1UL << (BITS_PER_LONG - 1);
  73.             size_flag = 1;
  74.         }
  75.     }
  76.     /* Should not reach here */
  77.     BUG();
  78.     return NULL;
  79. }


  address_space结构体往往会和某些内核对象关联。通常会与一个索引节点inode关联,这时host字段就会指向该索引节点,如果关联对象不是索引节点,host就会被设置为NULL。
   a_ops字段指向地址空间对象中的操作函数表,这与VFS对象及其操作表关系类似:
  1. 在include/fs.h中

  2. /*
  3.  * oh the beauties of C type declarations.
  4.  */
  5. struct page;
  6. struct address_space;
  7. struct writeback_control;

  8. struct address_space_operations {
  9.     int (*writepage)(struct page *page, struct writeback_control *wbc);
  10.     int (*readpage)(struct file *, struct page *);
  11.     void (*sync_page)(struct page *);

  12.     /* Write back some dirty pages from this mapping. */
  13.     int (*writepages)(struct address_space *, struct writeback_control *);

  14.     /* Set a page dirty.  Return true if this dirtied it */
  15.     int (*set_page_dirty)(struct page *page);

  16.     int (*readpages)(struct file *filp, struct address_space *mapping,
  17.             struct list_head *pages, unsigned nr_pages);

  18.     /*
  19.      * ext3 requires that a successful prepare_write() call be followed
  20.      * by a commit_write() call - they must be balanced
  21.      */
  22.     int (*prepare_write)(struct file *, struct page *, unsigned, unsigned);
  23.     int (*commit_write)(struct file *, struct page *, unsigned, unsigned);
  24.     /* Unfortunately this kludge is needed for FIBMAP. Don't use it */
  25.     sector_t (*bmap)(struct address_space *, sector_t);
  26.     void (*invalidatepage) (struct page *, unsigned long);
  27.     int (*releasepage) (struct page *, gfp_t);
  28.     ssize_t (*direct_IO)(intstruct kiocb *, const struct iovec *iov,
  29.             loff_t offset, unsigned long nr_segs);
  30.     struct page* (*get_xip_page)(struct address_space *, sector_t,
  31.             int);
  32.     /* migrate the contents of a page to the specified target */
  33.     int (*migratepage) (struct address_space *,
  34.             struct page *, struct page *);
  35.     int (*launder_page) (struct page *);
  36. };

  readpage()和writerpage()两个方法最主要。
  先看读操作中包含的步骤:
1. 一个address_space对象和一个偏移量会被传给readpage()方法,这两个参数用来在页高速缓存中搜索需要的数据,调用find_get_page()方法,
2. 如果搜索的页面并没有在高速缓存中,那么内核将分配一个新的页面,调用page_cache_alloc_cold()方法;然后将其加入到页高速缓存中,调用add_to_page_lru()方法
3. 需要的数据从磁盘被读入,在被加入到页高速缓存中,然后返回给用户:调用a_ops->readpage()方法
  写操作和读操作有少许不同。对于文件映射来说,当页被修改了,VM仅仅需要调用
SetPageDirty(page);
  内核会在晚些时候通过writepage()方法把页写出。
  对特定文件的写操作比较复杂,实现在mm/filemap.c中,包含以下步骤:
1. 调用__grab_cache_page()方法,在页高速缓存中搜索需要的页,如果需要的页不在告诉缓存中,那么内核在高速缓存分配一个空闲项
2. 调用a_ops->prepare_write()方法创建一个写请求。
3. 调用filemap_copy_from_user()方法将数据从用户空间拷贝到内核缓冲
4. 通过a_ops->commit_write()函数将数据写入磁盘
  • 基树
  在任何页IO操作之前内核都要检查页是否已经在页高速缓存中了,主要通过两个参数(address_space对象和一个偏移量)进行查找。每个address_space对象都有惟一的基树(radix tree),它保存在page_tree结构体中。
  基树是一个二叉树,只要指定了文件偏移量,就可以在基树中迅速检索到希望的数据。页高速缓存的搜索函数find_get_page()要调用radix_tree_lookup()函数,该函数会在指定基树中搜索到指定的页面。
  基树核心代码的通用形式可在文件lib/radix-tree.c中找到。
  1. struct radix_tree_node {
  2.     unsigned int    height;     /* Height from the bottom */
  3.     unsigned int    count;
  4.     struct rcu_head rcu_head;
  5.     void        *slots[RADIX_TREE_MAP_SIZE];
  6.     unsigned long   tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS];
  7. };

  8. struct radix_tree_path {
  9.     struct radix_tree_node *node;
  10.     int offset;
  11. };


  12. /*
  13.  * Radix tree node cache.
  14.  */
  15. static struct kmem_cache *radix_tree_node_cachep;


  16. void __init radix_tree_init(void)
  17. {
  18.     radix_tree_node_cachep = kmem_cache_create("radix_tree_node",
  19.             sizeof(struct radix_tree_node), 0,
  20.             SLAB_PANIC, radix_tree_node_ctor, NULL);
  21.     radix_tree_init_maxindex();
  22.     hotcpu_notifier(radix_tree_callback, 0);
  23. }


  24. /**
  25.  *  radix_tree_lookup    -    perform lookup operation on a radix tree
  26.  *  @root:      radix tree root
  27.  *  @index:     index key
  28.  *
  29.  *  Lookup the item at the position @index in the radix tree @root.
  30.  *
  31.  *  This function can be called under rcu_read_lock, however the caller
  32.  *  must manage lifetimes of leaf nodes (eg. RCU may also be used to free
  33.  *  them safely). No RCU barriers are required to access or modify the
  34.  *  returned item, however.
  35.  */
  36. void *radix_tree_lookup(struct radix_tree_root *root, unsigned long index)
  37. {
  38.     unsigned int height, shift;
  39.     struct radix_tree_node *node, **slot;

  40.     node = rcu_dereference(root->rnode);
  41.     if (node == NULL)
  42.         return NULL;

  43.     if (radix_tree_is_direct_ptr(node)) {
  44.         if (index > 0)
  45.             return NULL;
  46.         return radix_tree_direct_to_ptr(node);
  47.     }

  48.     height = node->height;
  49.     if (index > radix_tree_maxindex(height))
  50.         return NULL;

  51.     shift = (height-1) * RADIX_TREE_MAP_SHIFT;

  52.     do {
  53.         slot = (struct radix_tree_node **)
  54.             (node->slots + ((index>>shift) & RADIX_TREE_MAP_MASK));
  55.         node = rcu_dereference(*slot);
  56.         if (node == NULL)
  57.             return NULL;

  58.         shift -= RADIX_TREE_MAP_SHIFT;
  59.         height--;
  60.     } while (height > 0);

  61.     return node;
  62. }


  要想使用基树,需要包含头文件linux/radix_tree.h。
  1. /*** radix-tree API starts here ***/

  2. #define RADIX_TREE_MAX_TAGS 2

  3. /* root tags are stored in gfp_mask, shifted by __GFP_BITS_SHIFT */
  4. struct radix_tree_root {
  5.     unsigned int        height;
  6.     gfp_t           gfp_mask;
  7.     struct radix_tree_node  *rnode;
  8. };

  9. #define RADIX_TREE_INIT(mask)   {                   /
  10.     .height = 0,                            /
  11.     .gfp_mask = (mask),                     /
  12.     .rnode = NULL,                          /
  13. }

  14. #define RADIX_TREE(name, mask) /
  15.     struct radix_tree_root name = RADIX_TREE_INIT(mask)

  16. #define INIT_RADIX_TREE(root, mask)                 /
  17. do {                                    /
  18.     (root)->height = 0;                     /
  19.     (root)->gfp_mask = (mask);                  /
  20.     (root)->rnode = NULL;                       /
  21. while (0)

  在2.6版本之前,内核页高速缓存不是通过基树检索,而是通过一个维护了系统中所有页的全局散列表进行检索。对于给定的同一个键值,该散列表会返回一个双 向链表。如果需要的页驻留在缓存中,那么链表中的一项会与其对于。否则,页就不在页高速缓存中,散链函数返回NULL。
  • 缓冲区高速缓存
  在2.2版本的内核中,存在两个独立的磁盘缓存:页高速缓存和缓冲区高速缓存。前者缓存页,后者缓冲缓冲。一个磁盘块可以在两种缓存中同时存在,因此需要对两个缓存中的同一拷贝进行很麻烦的同步操作。
  现在Linux中已经不再有独立的缓冲区高速缓存了,只有唯一的磁盘缓存---页高速缓存。内核仍然需要在内存中使用缓冲区来表示磁盘块,缓冲是用页映射块的,它正好在页高速缓存中。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值