重新開始!!
頁高速緩存(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僅僅需要調用
對特定文件的寫操作比較複雜,實現在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中已經不再有獨立的緩衝區高速緩存了,只有唯一的磁盤緩存---頁高速緩存。內核仍然需要在內存中使用緩衝區來表示磁盤塊,緩衝是用頁映射塊的,它正好在頁高速緩存中。