有种欲哭无泪的感觉。又是一时失手,把原本写的内容刷掉了。呜呜~~
重新开始!!
页高速缓存(cache)是Linux内核实现的一种主要磁盘缓存。它主要用来减少对磁盘的IO操作。具体地讲,就是通过把磁盘中的数据缓存到物理内存中,把对磁盘的访问变为对物理内存的访问。
磁盘缓存的优点在于:1,访问磁盘的速度远远低于访问内存的速度,因此从内存访问数据要比从磁盘访问速度快。2,数据一旦被访问,就很有可能在短期内被再 次访问到。这种在短期内集中访问同一片数据的原理被称作临时局部原理temporal locality。临时局部原理保证:如果在第一次访问数据时缓存它,那就极有可能在短期内再次被高速缓存命中。
页高速缓存是由RAM中的物理页组成,缓存中每一页都对应着磁盘中的多个块。每当内核开始执行一个页IO操作时(通常对普通文件中页大小的块进行磁盘操作),首先会检查需要的数据是否在高速缓存中,如果在,那么内核就直接使用高速缓存中的数据,从而避免访问磁盘。
通过块IO缓冲区把独立的磁盘块与页高速缓存联系在一起。在块IO层中,一个缓冲(buffer)就是一个单独物理磁盘块在内存中的表示,缓冲就是内存到 磁盘块的映射描述符。因此通过磁盘块以及缓冲块IO操作,页高速缓存同一可以减少块IO操作期间的磁盘访问量。这种缓存经常被称为“缓冲区高速缓存”,它 实际上并不是一个独立的缓存,而是页高速缓存的一部分。
每次页IO操作都要处理数据的全部页面,就需要对一个以上的磁盘块进行操作,所以页高速缓存实际缓存的是页面大小的文件块。
块IO操作每次操作一个单独的磁盘块,比如读写索引节点就是一个典型的IO操作。内核提供bread()底层函数从磁盘读单个块,通过缓冲,这些磁盘块被映射到内存中相应的页面上,这样就被缓存到页高速缓存里了。
一个物理页可能由多个不连续的物理磁盘块组成。因为文件本身可能分布在磁盘各个位置,所以页面中映射的块也不需要连续。这样的话,在页高速缓存中检测特定数据是否已被缓存是件非常困难的事。因此不能用设备名称和块号来作页高速缓存中数据的索引。
Linux页高速缓存的目标是缓存任何基于页的对象,这包含各种类型的文件和各种类型的内存映射。Linux页高速缓存使用address_space结构体描述页高速缓存中的页面。
其中i_mmap字段是一个优先搜索树,它的搜索范围包含了在address_space中所有共享的与私有的映射页面。优先搜索树是一种巧妙的将堆和radix树结合的快速检索树。
优先搜索树的具体实现在lib/prio.c中:
address_space结构体往往会和某些内核对象关联。通常会与一个索引节点inode关联,这时host字段就会指向该索引节点,如果关联对象不是索引节点,host就会被设置为NULL。
a_ops字段指向地址空间对象中的操作函数表,这与VFS对象及其操作表关系类似:
readpage()和writerpage()两个方法最主要。
先看读操作中包含的步骤:
1. 一个address_space对象和一个偏移量会被传给readpage()方法,这两个参数用来在页高速缓存中搜索需要的数据,调用find_get_page()方法,
2. 如果搜索的页面并没有在高速缓存中,那么内核将分配一个新的页面,调用page_cache_alloc_cold()方法;然后将其加入到页高速缓存中,调用add_to_page_lru()方法
3. 需要的数据从磁盘被读入,在被加入到页高速缓存中,然后返回给用户:调用a_ops->readpage()方法
写操作和读操作有少许不同。对于文件映射来说,当页被修改了,VM仅仅需要调用
对特定文件的写操作比较复杂,实现在mm/filemap.c中,包含以下步骤:
1. 调用__grab_cache_page()方法,在页高速缓存中搜索需要的页,如果需要的页不在告诉缓存中,那么内核在高速缓存分配一个空闲项
2. 调用a_ops->prepare_write()方法创建一个写请求。
3. 调用filemap_copy_from_user()方法将数据从用户空间拷贝到内核缓冲
4. 通过a_ops->commit_write()函数将数据写入磁盘
基树是一个二叉树,只要指定了文件偏移量,就可以在基树中迅速检索到希望的数据。页高速缓存的搜索函数find_get_page()要调用radix_tree_lookup()函数,该函数会在指定基树中搜索到指定的页面。
基树核心代码的通用形式可在文件lib/radix-tree.c中找到。
要想使用基树,需要包含头文件linux/radix_tree.h。
在2.6版本之前,内核页高速缓存不是通过基树检索,而是通过一个维护了系统中所有页的全局散列表进行检索。对于给定的同一个键值,该散列表会返回一个双 向链表。如果需要的页驻留在缓存中,那么链表中的一项会与其对于。否则,页就不在页高速缓存中,散链函数返回NULL。
现在Linux中已经不再有独立的缓冲区高速缓存了,只有唯一的磁盘缓存---页高速缓存。内核仍然需要在内存中使用缓冲区来表示磁盘块,缓冲是用页映射块的,它正好在页高速缓存中。
重新开始!!
页高速缓存(cache)是Linux内核实现的一种主要磁盘缓存。它主要用来减少对磁盘的IO操作。具体地讲,就是通过把磁盘中的数据缓存到物理内存中,把对磁盘的访问变为对物理内存的访问。
磁盘缓存的优点在于:1,访问磁盘的速度远远低于访问内存的速度,因此从内存访问数据要比从磁盘访问速度快。2,数据一旦被访问,就很有可能在短期内被再 次访问到。这种在短期内集中访问同一片数据的原理被称作临时局部原理temporal locality。临时局部原理保证:如果在第一次访问数据时缓存它,那就极有可能在短期内再次被高速缓存命中。
页高速缓存是由RAM中的物理页组成,缓存中每一页都对应着磁盘中的多个块。每当内核开始执行一个页IO操作时(通常对普通文件中页大小的块进行磁盘操作),首先会检查需要的数据是否在高速缓存中,如果在,那么内核就直接使用高速缓存中的数据,从而避免访问磁盘。
通过块IO缓冲区把独立的磁盘块与页高速缓存联系在一起。在块IO层中,一个缓冲(buffer)就是一个单独物理磁盘块在内存中的表示,缓冲就是内存到 磁盘块的映射描述符。因此通过磁盘块以及缓冲块IO操作,页高速缓存同一可以减少块IO操作期间的磁盘访问量。这种缓存经常被称为“缓冲区高速缓存”,它 实际上并不是一个独立的缓存,而是页高速缓存的一部分。
每次页IO操作都要处理数据的全部页面,就需要对一个以上的磁盘块进行操作,所以页高速缓存实际缓存的是页面大小的文件块。
块IO操作每次操作一个单独的磁盘块,比如读写索引节点就是一个典型的IO操作。内核提供bread()底层函数从磁盘读单个块,通过缓冲,这些磁盘块被映射到内存中相应的页面上,这样就被缓存到页高速缓存里了。
- 页高速缓存
一个物理页可能由多个不连续的物理磁盘块组成。因为文件本身可能分布在磁盘各个位置,所以页面中映射的块也不需要连续。这样的话,在页高速缓存中检测特定数据是否已被缓存是件非常困难的事。因此不能用设备名称和块号来作页高速缓存中数据的索引。
Linux页高速缓存的目标是缓存任何基于页的对象,这包含各种类型的文件和各种类型的内存映射。Linux页高速缓存使用address_space结构体描述页高速缓存中的页面。
- 在include/fs.h中:
- struct backing_dev_info;
- struct address_space {
- struct inode *host; /* owner: inode, block_device */
- struct radix_tree_root page_tree; /* radix tree of all pages */
- rwlock_t tree_lock; /* and rwlock protecting it */
- unsigned int i_mmap_writable;/* count VM_SHARED mappings */
- struct prio_tree_root i_mmap; /* tree of private and shared mappings */
- struct list_head i_mmap_nonlinear;/*list VM_NONLINEAR mappings */
- spinlock_t i_mmap_lock; /* protect tree, count, list */
- unsigned int truncate_count; /* Cover race condition with truncate */
- unsigned long nrpages; /* number of total pages */
- pgoff_t writeback_index;/* writeback starts here */
- const struct address_space_operations *a_ops; /* methods */
- unsigned long flags; /* error bits/gfp mask */
- struct backing_dev_info *backing_dev_info; /* device readahead, etc */
- spinlock_t private_lock; /* for use by the address_space */
- struct list_head private_list; /* ditto */
- struct address_space *assoc_mapping; /* ditto */
- } __attribute__((aligned(sizeof(long))));
其中i_mmap字段是一个优先搜索树,它的搜索范围包含了在address_space中所有共享的与私有的映射页面。优先搜索树是一种巧妙的将堆和radix树结合的快速检索树。
- 在linux/prio_tree.h中
- struct prio_tree_node {
- struct prio_tree_node *left;
- struct prio_tree_node *right;
- struct prio_tree_node *parent;
- unsigned long start;
- unsigned long last; /* last location _in_ interval */
- };
- struct prio_tree_root {
- struct prio_tree_node *prio_tree_node;
- unsigned short index_bits;
- unsigned short raw;
- /*
- * 0: nodes are of type struct prio_tree_node
- * 1: nodes are of type raw_prio_tree_node
- */
- };
- static unsigned long index_bits_to_maxindex[BITS_PER_LONG];
- void __init prio_tree_init(void)
- {
- unsigned int i;
- for (i = 0; i < ARRAY_SIZE(index_bits_to_maxindex) - 1; i++)
- index_bits_to_maxindex[i] = (1UL << (i + 1)) - 1;
- index_bits_to_maxindex[ARRAY_SIZE(index_bits_to_maxindex) - 1] = ~0UL;
- }
- /*
- * Insert a prio_tree_node @node into a radix priority search tree @root. The
- * algorithm typically takes O(log n) time where 'log n' is the number of bits
- * required to represent the maximum heap_index. In the worst case, the algo
- * can take O((log n)^2) - check prio_tree_expand.
- *
- * If a prior node with same radix_index and heap_index is already found in
- * the tree, then returns the address of the prior node. Otherwise, inserts
- * @node into the tree and returns @node.
- */
- struct prio_tree_node *prio_tree_insert(struct prio_tree_root *root,
- struct prio_tree_node *node)
- {
- struct prio_tree_node *cur, *res = node;
- unsigned long radix_index, heap_index;
- unsigned long r_index, h_index, index, mask;
- int size_flag = 0;
- get_index(root, node, &radix_index, &heap_index);
- if (prio_tree_empty(root) ||
- heap_index > prio_tree_maxindex(root->index_bits))
- return prio_tree_expand(root, node, heap_index);
- cur = root->prio_tree_node;
- mask = 1UL << (root->index_bits - 1);
- while (mask) {
- get_index(root, cur, &r_index, &h_index);
- if (r_index == radix_index && h_index == heap_index)
- return cur;
- if (h_index < heap_index ||
- (h_index == heap_index && r_index > radix_index)) {
- struct prio_tree_node *tmp = node;
- node = prio_tree_replace(root, cur, node);
- cur = tmp;
- /* swap indices */
- index = r_index;
- r_index = radix_index;
- radix_index = index;
- index = h_index;
- h_index = heap_index;
- heap_index = index;
- }
- if (size_flag)
- index = heap_index - radix_index;
- else
- index = radix_index;
- if (index & mask) {
- if (prio_tree_right_empty(cur)) {
- INIT_PRIO_TREE_NODE(node);
- cur->right = node;
- node->parent = cur;
- return res;
- } else
- cur = cur->right;
- } else {
- if (prio_tree_left_empty(cur)) {
- INIT_PRIO_TREE_NODE(node);
- cur->left = node;
- node->parent = cur;
- return res;
- } else
- cur = cur->left;
- }
- mask >>= 1;
- if (!mask) {
- mask = 1UL << (BITS_PER_LONG - 1);
- size_flag = 1;
- }
- }
- /* Should not reach here */
- BUG();
- return NULL;
- }
address_space结构体往往会和某些内核对象关联。通常会与一个索引节点inode关联,这时host字段就会指向该索引节点,如果关联对象不是索引节点,host就会被设置为NULL。
a_ops字段指向地址空间对象中的操作函数表,这与VFS对象及其操作表关系类似:
- 在include/fs.h中
- /*
- * oh the beauties of C type declarations.
- */
- struct page;
- struct address_space;
- struct writeback_control;
- struct address_space_operations {
- int (*writepage)(struct page *page, struct writeback_control *wbc);
- int (*readpage)(struct file *, struct page *);
- void (*sync_page)(struct page *);
- /* Write back some dirty pages from this mapping. */
- int (*writepages)(struct address_space *, struct writeback_control *);
- /* Set a page dirty. Return true if this dirtied it */
- int (*set_page_dirty)(struct page *page);
- int (*readpages)(struct file *filp, struct address_space *mapping,
- struct list_head *pages, unsigned nr_pages);
- /*
- * ext3 requires that a successful prepare_write() call be followed
- * by a commit_write() call - they must be balanced
- */
- int (*prepare_write)(struct file *, struct page *, unsigned, unsigned);
- int (*commit_write)(struct file *, struct page *, unsigned, unsigned);
- /* Unfortunately this kludge is needed for FIBMAP. Don't use it */
- sector_t (*bmap)(struct address_space *, sector_t);
- void (*invalidatepage) (struct page *, unsigned long);
- int (*releasepage) (struct page *, gfp_t);
- ssize_t (*direct_IO)(int, struct kiocb *, const struct iovec *iov,
- loff_t offset, unsigned long nr_segs);
- struct page* (*get_xip_page)(struct address_space *, sector_t,
- int);
- /* migrate the contents of a page to the specified target */
- int (*migratepage) (struct address_space *,
- struct page *, struct page *);
- int (*launder_page) (struct page *);
- };
先看读操作中包含的步骤:
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()函数将数据写入磁盘
- 基树
基树是一个二叉树,只要指定了文件偏移量,就可以在基树中迅速检索到希望的数据。页高速缓存的搜索函数find_get_page()要调用radix_tree_lookup()函数,该函数会在指定基树中搜索到指定的页面。
基树核心代码的通用形式可在文件lib/radix-tree.c中找到。
- struct radix_tree_node {
- unsigned int height; /* Height from the bottom */
- unsigned int count;
- struct rcu_head rcu_head;
- void *slots[RADIX_TREE_MAP_SIZE];
- unsigned long tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS];
- };
- struct radix_tree_path {
- struct radix_tree_node *node;
- int offset;
- };
- /*
- * Radix tree node cache.
- */
- static struct kmem_cache *radix_tree_node_cachep;
- void __init radix_tree_init(void)
- {
- radix_tree_node_cachep = kmem_cache_create("radix_tree_node",
- sizeof(struct radix_tree_node), 0,
- SLAB_PANIC, radix_tree_node_ctor, NULL);
- radix_tree_init_maxindex();
- hotcpu_notifier(radix_tree_callback, 0);
- }
- /**
- * radix_tree_lookup - perform lookup operation on a radix tree
- * @root: radix tree root
- * @index: index key
- *
- * Lookup the item at the position @index in the radix tree @root.
- *
- * This function can be called under rcu_read_lock, however the caller
- * must manage lifetimes of leaf nodes (eg. RCU may also be used to free
- * them safely). No RCU barriers are required to access or modify the
- * returned item, however.
- */
- void *radix_tree_lookup(struct radix_tree_root *root, unsigned long index)
- {
- unsigned int height, shift;
- struct radix_tree_node *node, **slot;
- node = rcu_dereference(root->rnode);
- if (node == NULL)
- return NULL;
- if (radix_tree_is_direct_ptr(node)) {
- if (index > 0)
- return NULL;
- return radix_tree_direct_to_ptr(node);
- }
- height = node->height;
- if (index > radix_tree_maxindex(height))
- return NULL;
- shift = (height-1) * RADIX_TREE_MAP_SHIFT;
- do {
- slot = (struct radix_tree_node **)
- (node->slots + ((index>>shift) & RADIX_TREE_MAP_MASK));
- node = rcu_dereference(*slot);
- if (node == NULL)
- return NULL;
- shift -= RADIX_TREE_MAP_SHIFT;
- height--;
- } while (height > 0);
- return node;
- }
要想使用基树,需要包含头文件linux/radix_tree.h。
- /*** radix-tree API starts here ***/
- #define RADIX_TREE_MAX_TAGS 2
- /* root tags are stored in gfp_mask, shifted by __GFP_BITS_SHIFT */
- struct radix_tree_root {
- unsigned int height;
- gfp_t gfp_mask;
- struct radix_tree_node *rnode;
- };
- #define RADIX_TREE_INIT(mask) { /
- .height = 0, /
- .gfp_mask = (mask), /
- .rnode = NULL, /
- }
- #define RADIX_TREE(name, mask) /
- struct radix_tree_root name = RADIX_TREE_INIT(mask)
- #define INIT_RADIX_TREE(root, mask) /
- do { /
- (root)->height = 0; /
- (root)->gfp_mask = (mask); /
- (root)->rnode = NULL; /
- } while (0)
在2.6版本之前,内核页高速缓存不是通过基树检索,而是通过一个维护了系统中所有页的全局散列表进行检索。对于给定的同一个键值,该散列表会返回一个双 向链表。如果需要的页驻留在缓存中,那么链表中的一项会与其对于。否则,页就不在页高速缓存中,散链函数返回NULL。
- 缓冲区高速缓存
现在Linux中已经不再有独立的缓冲区高速缓存了,只有唯一的磁盘缓存---页高速缓存。内核仍然需要在内存中使用缓冲区来表示磁盘块,缓冲是用页映射块的,它正好在页高速缓存中。