高速缓存的价值在于两个方面:第一,访问磁盘的速度要远远低于访问内存的速度,因为,在内存访问数据比从磁盘访问速度更快。第二,数据一旦被访问,就很有可能在短期内再次被访问到。如果在第一次访问数据时缓存它,那就极有可能在短期内再次被高速缓存命中。
页高速缓存缓存的是页。Linux页高速缓存的目标是缓存任何基于页的对象,这包含各种类型的文件和各种类型的内存映射。为了满足普遍性的要求,Linux页高速缓存使用address_space结构体描述页高速缓存中的页面。该结构体定义在文件linux/fs.h中。
struct address_space {
struct inode *host; /* owner: inode, block_device */
struct radix_tree_root page_tree; /* radix tree of all pages */
spinlock_t tree_lock; /* and spinlock 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 */
atomic_t truncate_count; /* Cover race condition with truncate */
unsigned long nrpages; /* number of total pages */
pgoff_t writeback_index;/* writeback starts here */
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树结合的快速检索树。address space空间大小由nrpages字段描述,表示共有多少页。
address_space结构往往会和某些内核对象关联,通常情况下会与一个索引节点关联,这时host域就会指向该索引节点,如果关联对象不是一个索引节点的话,host就被设置为NULL。
a_ops域指向地址空间对象中的操作函数表,操作函数表定义在linux/fs.h文件中,由address_space_operations结构体表示:
struct address_space_operations {
int (*writepage)(struct page *page, struct writeback_control *wbc);
int (*readpage)(struct file *, struct page *);
int (*sync_page)(struct page *);
/* Write back some dirty pages from this mapping. */
int (*writepages)(struct address_space *, struct writeback_control *);
/* Set a page dirty */
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);
int (*invalidatepage) (struct page *, unsigned long);
int (*releasepage) (struct page *, int);
ssize_t (*direct_IO)(int, struct kiocb *, const struct iovec *iov,
loff_t offset, unsigned long nr_segs);
};
每个address_space对象都有唯一的基树(radix tree),它保存在page_tree结构体中。基树是一个二叉树,只要指定了文件偏移量,就可以在基树中迅速检索到希望的数据,页高速缓存的搜索函数find_get_page()要调用函数radix_tree_loopup(),该函数会在指定基树中搜索指定页面。
基树核心代码的通用形式可以在文件lib/radix-tree.c中找到,要想使用基树,需要包含头文件linux/radix_tree.h
现在的Linux系统中已经不再有独立的缓冲区高速缓存了。但在2.2版本的内核中,存在两个独立的磁盘缓存:页高速缓存和缓冲区高速缓存。前者缓存页,后者缓存缓冲。两种缓存并不同一:一个磁盘块可以在两种缓存中同时存在,因此需要对缓存中的同一拷贝进行很麻烦的同步操作。
在2.4版本的内核开始,统一了这两种缓存,现在Linux只有唯一的页高速缓存。
由于页高速缓存的缓存作用,写操作实际上会被延迟,当页高速缓存中的数据比磁盘存储的数据更新时,这时候页高速缓存中的数据被称为脏数据,脏数据所在的页被称为脏页,这些脏页最终必须被写回磁盘。在以下两种情况发送时,脏页被写回磁盘:
上面两种工作的目的完全不同。在老内核中,这是由两个独立的内核线程分别完成(bdflush和kupdated两个线程)的,但是在2.6内核中,由一群内核线程,pdflush后台回写线程同一执行两种工作。首先,pdflush线程在系统中的空闲内存低于一个特定的阈值时,将脏页刷新回磁盘。此目的是在可用物理内存过低时,释放脏页以重新获得内存。特定的内存阈值可以通过dirty_backgriud_radio sysctl系统调用设置。当空闲内存比阈值dirty_background_ratio低时,内核便会调用wakeup_bdflush()唤醒一个pdflush线程。随后pdflush线程进一步调用函数background_writeout()开始将脏页回写磁盘。函数background_writeout()需要一个长整型参数,该参数指定试图写回的页面数目。函数background_writeout会连续写出数据,直到满足以下两个条件:
上述条件确保了pdflush操作可以减轻系统中内存不足的压力,回写操作不会在达到这两个条件前停止,除非pdflush写回了所有的脏页,没有剩下的脏页可以写回了。
为了满足第二个目标,pdflush后台例程会被周期性唤醒,将那些在内存驻留时间过长的脏页写出,确保内存中不会有长期存在的脏页。在系统启动时,内核初始化一个定时器,让它周期地唤醒pdflush线程,随后使其运行函数wb_kupdate()。该函数将把所有驻留时间超过百分之drity_expire_centisece秒的脏页写回。然后定时器将再次被初始化为百分之drity_expire_centisece秒后唤醒pdflush线程。
pdflush线程的实现代码在文件mm/pdflush.c中,回写机制的实现代码在文件mm/page-writebacke.c和fs/fs-writeback.c中。
膝上型电脑模式是一种特殊的页回写策略,该策略主要目的是将磁盘转动的机械行为最小化,允许磁盘尽可能长时间停滞,以此延长电池供电时间。该模式可通过/proc/sys/vm/laptop_mode文件进行配置,通常,该文件内容为0,膝上电脑模式关闭,如果需要启动,则向配置文件写入1.
在2.6内核版本前,pdflush线程的工作是分别由bdflush和kupdated两个线程共同完成。当可用内存过低时,bdflush内核线程在后台执行脏页回写操作,与pdflush一样,它也有一组阈值参数,当系统中空闲内存消耗到特定内存阈值以下时,bdflush线程就被wakeup_bdflush函数唤醒。
bdflush和pdflush之间主要有两个区别。第一个是系统中只有一个bdflush线程,而pdflush线程的数目可以动态改变;第二个是bdflush线程基于缓冲,它将脏缓冲写回磁盘,pdflush基于页,它将整个脏页写回磁盘。
因为只有在内存过低和缓冲数量过大时,bdflush才刷新缓冲,所以kupdate线程被引入,以便周期性地写回脏页。
bdflush和kupdate内核线程现在完全被pdflush线程取代了。
bdflush仅仅只有一个线程,因此很有可能在页回写任务很重时,造成阻塞,这是因为单一的线程很可能堵塞在某个设备的已阻塞请求队列上,而其他设备的请求队列却没法得到处理。
2.6内核通过使用多个pdflush线程来解决上述问题。每个线程可以相互独立地将脏页刷新回磁盘,而且不同的pdflush线程处理不同的设备队列。
通过一个简单的算法,pdflush线程的数目可以根据系统的运行时间进行调整,如果所有已存在的pdflush线程都已经持续工作1秒以上,内核就会创建一个新的pdflush线程。线程数量最多不能超过MAX_PDFLUSH_THREADS,默认值是8.如果一个pdflush线程睡眠超过1秒,内核就会终止该线程,线程的数量最少不得小于MIN_PDFLUSH_THREADS,默认值是2.pdflush线程数量取决于页回写的数量和阻塞情况,动态调整。
这种方式看起来很理想,但是如果每一个pdflush线程都挂起在同一个阻塞的队列上会怎么样?在这种情况下,多个pdflush线程的性能并不会比单个线程提高多少,反而会造成严重的内存浪费。为了克服这种负面影响,pdflush线程利用阻塞避免策略,它们会积极地试图写回那些不属于阻塞队列的页面。这样一来,pdflush通过分派回写工作,阻止多个线程在同一个忙设备纠缠。所以pdflush线程很忙,此时会有一个新的pdflush线程被创建,它们才是真正的繁忙。