Linux内核设计与实现---页高速缓存和页回写

页高速缓存和页回写

  • 1 页高速缓存
  • 2 基树
  • 3 缓冲区高速缓存
  • 4 pdflush后台例程
    • 膝上型电脑模式
    • bdflush和kupdated
    • 避免拥塞的方法:使用多线程

页高速缓存(cache)是Linux内核实现的一种主要磁盘缓存,通过把磁盘中的数据缓存到物理内存中,把对磁盘的访问变成对物理内存的访问,主要用来减少对磁盘的I/O操作。它由RAM中的物理页组成,缓存中每一项对应着磁盘中的多个块,每当内核开始执行一个页I/O操作时,首先会检查需要的数据是否在高速缓存中,如果在,那么内核就直接使用高速缓存中的数据,从而避免访问磁盘。

高速缓存的价值在于两个方面:第一,访问磁盘的速度要远远低于访问内存的速度,因为,在内存访问数据比从磁盘访问速度更快。第二,数据一旦被访问,就很有可能在短期内再次被访问到。如果在第一次访问数据时缓存它,那就极有可能在短期内再次被高速缓存命中。

1 页高速缓存

页高速缓存缓存的是页。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);
};

2 基树

每个address_space对象都有唯一的基树(radix tree),它保存在page_tree结构体中。基树是一个二叉树,只要指定了文件偏移量,就可以在基树中迅速检索到希望的数据,页高速缓存的搜索函数find_get_page()要调用函数radix_tree_loopup(),该函数会在指定基树中搜索指定页面。

基树核心代码的通用形式可以在文件lib/radix-tree.c中找到,要想使用基树,需要包含头文件linux/radix_tree.h

3 缓冲区高速缓存

现在的Linux系统中已经不再有独立的缓冲区高速缓存了。但在2.2版本的内核中,存在两个独立的磁盘缓存:页高速缓存和缓冲区高速缓存。前者缓存页,后者缓存缓冲。两种缓存并不同一:一个磁盘块可以在两种缓存中同时存在,因此需要对缓存中的同一拷贝进行很麻烦的同步操作。
在2.4版本的内核开始,统一了这两种缓存,现在Linux只有唯一的页高速缓存。

4 pdflush后台例程

由于页高速缓存的缓存作用,写操作实际上会被延迟,当页高速缓存中的数据比磁盘存储的数据更新时,这时候页高速缓存中的数据被称为脏数据,脏数据所在的页被称为脏页,这些脏页最终必须被写回磁盘。在以下两种情况发送时,脏页被写回磁盘:

  • 当空闲内存低于一个特定的阈值时,内核必须将脏页写回磁盘,以便释放内存。
  • 当脏页在内存中驻留时间超过一个特定的阈值时,内核必须将超时的脏页写回磁盘,以确保脏页不会无限期地驻留在内存中。

上面两种工作的目的完全不同。在老内核中,这是由两个独立的内核线程分别完成(bdflush和kupdated两个线程)的,但是在2.6内核中,由一群内核线程,pdflush后台回写线程同一执行两种工作。首先,pdflush线程在系统中的空闲内存低于一个特定的阈值时,将脏页刷新回磁盘。此目的是在可用物理内存过低时,释放脏页以重新获得内存。特定的内存阈值可以通过dirty_backgriud_radio sysctl系统调用设置。当空闲内存比阈值dirty_background_ratio低时,内核便会调用wakeup_bdflush()唤醒一个pdflush线程。随后pdflush线程进一步调用函数background_writeout()开始将脏页回写磁盘。函数background_writeout()需要一个长整型参数,该参数指定试图写回的页面数目。函数background_writeout会连续写出数据,直到满足以下两个条件:

  • 已经有指定的最小数目的页被写出到磁盘
  • 空闲内存数已经回升,超过了阈值dirty_background_ratio

上述条件确保了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.

bdflush和kupdated

在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线程被创建,它们才是真正的繁忙。

你可能感兴趣的:(Linux内核设计与实现,linux内核,页高速缓存,页回写)