Linux内核设计与实现——读书笔记(13)页高速缓存和页回写

1、缓存方式

1.1、读缓存

  当进程发起一个read()系统调用时,内核开始一个读操作:首先回去检查需要读取的数据是否在页高速缓存中。如果在(缓存命中),则放弃访问磁盘,直接从内存中读取;如果数据没有在页高速缓存中(缓存未命中),内核必须调度块I/O操作从磁盘读取数据。然后内核将读来的数据放入页高速缓存中,后续相同的数据都可以直接命中。

1.2、写缓存

  写缓存有三种策略:

  • (1)、不缓存(nowrite)。高速缓存不进行写缓存操作,CPU直接把写到磁盘;
  • (2)、同步(写透缓存,write-through cache)。写操作将自动更新内存缓存,同时也更新磁盘文件;
  • (3)、回写。执行写操作时之直接写到缓存中,磁盘数据不会立即更新,而是将页高速缓存中被写入的页面标记为“脏”,并且加入脏页链表。然后由一个“回写进程”周期性将脏页链表中的页写回磁盘中。

2、缓存回收

  有时需要对缓存数据清除,比如有更重要的缓存项时、内存不够用时等等。Linux的缓存回收是通过选择干净的页进行简单替换,如果缓存中没有足够的干净的页,内核将强制性进行回写操作,以腾出更多干净的页。
  但是,最难的是如何决定什么页应该回收,最理想的当然是那些回收后很久都没使用的页面。因此有几个缓存的回收策略:

2.1、最近最少使用算法

  最近最少使用算法,简称LRU。LRU回收策略需要跟踪每个页面的访问踪迹 (或者以访问时间为序的链表),以便能回收最老时间戳的页面(或者回收排序链表头指向的页面)。

2.2、双链策略

  LInux实现的是一个修改过的LRU,也成为双向策略。双链策略维护两个链表:活跃链表和非活跃链表。 处于活跃链表上的页面不会被换出,处于非活跃链表上的页面则是可以换出的。
  两个链表都被伪LRU规则维护:从尾部加入,头部移出。两个链表需要维持平衡:如果活跃链表变得过多,超过了非活跃链表,则活跃链表的头页面将被移出到非活跃链表中,以便能再次回收。

3、页高速缓存

  页高速缓存用来缓存内存页面,缓存的页面内容来自对正规文件、块设备文件和内存映射文件的读写。这样一来,页高速缓存就包含了最近被访问过的文件的数据块。在执行一个I/O操作之前,内核可以检查数据是否已经在页高速缓存中来判断是否需要去磁盘读取数据。那么内核具体是如何管理这些高速缓存的呢:

3.1、address_space对象

  在页高速缓存中的页可能包含了多个不连续的物理块,因为一个页大小通常是4KB,而一个块大小通常是512B,所以页面中映射的磁盘块不一定连续,不能用设备名称和块号来做页高速缓存中的数据索引。
  Linux页高速缓存的目标是缓存任何基于页的对象,这些对象包含了各种类型的文件和各种类型的内存映射。Linux为了维护页高速缓存的普遍性,再进行定位时没有将页高速缓存绑定到物理文件或者inode结构体中,而是使用了一个新的对象来管理缓存项和页I/O操作,这个对象就是address_space结构体。address_space结构体是表示虚拟地址的vm_area_struct对应的物理地址的对等体。一个文件可以被多个进程引用并映射,它会被多个vm_area_struct标识,也就是有多个虚拟地址,但是只能有一个物理地址,也就只能有一个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,包含所有页面的radix(基数)树 */
    spinlock_t      		tree_lock;  		/* and lock protecting it,保护基数树的自旋锁 */
    atomic_t       			i_mmap_writable;	/* count VM_SHARED mappings,VM_SHARED 计数 */
    struct rb_root      	i_mmap;     		/* tree of private and shared mappings,私有和共享映射的优先搜索树 */
    struct rw_semaphore 	i_mmap_rwsem;   	/* protect tree, count, list */
    /* Protected by tree_lock together with the radix tree */
    unsigned long       	nrpages;    		/* number of total pages,页总数 */
    /* number of shadow or DAX exceptional entries */
    unsigned long       	nrexceptional;
    pgoff_t         		writeback_index;	/* writeback starts here,写回的起始偏移 */
    const struct address_space_operations *a_ops;   /* methods,操作表 */
    unsigned long       	flags;      		/* error bits,gfp_mask掩码与错误标识 */
    spinlock_t      		private_lock;  	 	/* for use by the address_space */
    gfp_t           		gfp_mask;   		/* implicit gfp mask for allocations */
    struct list_head    	private_list;   	/* ditto */
    void            		*private_data;  	/* ditto */
} __attribute__((aligned(sizeof(long))));

  i_mmap字段表示一个优先搜索树,搜素搜范围包含了address_space中所有共享与私有的映射页面。优先搜索树是一种巧妙地将堆与radix树相结合的快速搜索树。
  address_space通常会和一个索引节点关联,此时host域指向该索引节点。如果关联对象不是一个索引节点的话,host域会被设置为NULL。

3.2、address_space操作

  address_space结构的操作表定义在 <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 *);

    /* 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);

    int (*write_begin)(struct file *, struct address_space *mapping,
                loff_t pos, unsigned len, unsigned flags,
                struct page **pagep, void **fsdata);
    int (*write_end)(struct file *, struct address_space *mapping,
                loff_t pos, unsigned len, unsigned copied,
                struct page *page, void *fsdata);

    /* Unfortunately this kludge is needed for FIBMAP. Don't use it */
    sector_t (*bmap)(struct address_space *, sector_t);
    void (*invalidatepage) (struct page *, unsigned int, unsigned int);
    int (*releasepage) (struct page *, gfp_t);
    void (*freepage)(struct page *);
    ssize_t (*direct_IO)(struct kiocb *, struct iov_iter *iter);
    /*   
     * migrate the contents of a page to the specified target. If
     * migrate_mode is MIGRATE_ASYNC, it must not block.
     */
    int (*migratepage) (struct address_space *,
            struct page *, struct page *, enum migrate_mode);
    bool (*isolate_page)(struct page *, isolate_mode_t);
    void (*putback_page)(struct page *);
    int (*launder_page) (struct page *);
    int (*is_partially_uptodate) (struct page *, unsigned long,
                    unsigned long);
    void (*is_dirty_writeback) (struct page *, bool *, bool *);
    int (*error_remove_page)(struct address_space *, struct page *);

    /* swapfile support */
    int (*swap_activate)(struct swap_info_struct *sis, struct file *file,
                sector_t *span);
    void (*swap_deactivate)(struct file *file);
};

  这些方法指针指向为指定缓存对象实现的页I/O操作,每个后备存储都通过自己的address_space_operations描述自己如何与页高速缓存交互。比如ext3文件系统会在fs/ext3/inode.c中定义自己的操作表。
  一个页面的读操作会包含以下步骤:
  内核使用以下函数试图在页高速缓存中找到需要的数据:

page = find_get_page(struct address_space *mapping,pgoff_t offset)

  mapping指定一个地址空间,index是文件中指定位置,以页面为单位。如果搜索的页没有在高速缓存中,则返回NULL,并且内核将分配一个新页面,从磁盘中读出数据并加入到页高速缓存中。
  写操作的步骤如下:
  对于文件映射,仅仅调用SetPageDirty() 函数:

SetPageDirty(page)

  内核在晚些时候会通过wirtepage() 函数将页写出。
  对于特定文件的写操作,则要分多步进行,代码定义在mm/filemap.c中:

  • 1、在页高速缓存中搜索需要的页,如果页不在高速缓存中,内核会在高速缓存中分配一个新的空闲项;
  • 2、内核创建一个写请求;
  • 3、数据从用户空间被拷贝到内核高速缓冲;
  • 4、将缓冲数据写入磁盘。

3.3、基树

  每个address_space结构体都有一个唯一的基树,保存在page_tree域中,基树是一个二叉树,只要指定了文件偏移量,就可以在基树上迅速检索到希望的页。页高速缓存的搜索函数find_get_page() 会调用radix_tree_lookup() 进行指定页面搜索。
  基树核心代码的通用形式在文件lib/radix-tree.c中,使用时需要包含 <linu/radix_tree.h>

4、flusher线程

4.1、flusher

  当页高速缓存中的数据比后台存储的数据更新后,会在以下三种情况发生时,脏页被写回磁盘:

  • 当空闲内存低于一个特定的阈值时,内核必须将脏页写回磁盘以便释放内存;
  • 当脏页在内存中驻留时间超过一个阈值时,内核必须将超时的脏页写回磁盘;
  • 当用户进程调用sync()fsync() 时,内核会按要求执行写回。

  在2.6的内核中,这些工作是由flusher线程来执行的(flusher线程不止一个)。
  空闲内存的下限阈值由sysctl系统调用设置dirty_background_ratio来修改。当空闲内存值低于设置的下限阈值时,内核会调用flusher_threads()函数唤醒一个或多个flusher线程,随后flusher线程调用bdi_wirteback_all() 函数开始将脏页写回磁盘,此函数会接受一个参数表示需要写回的页面数量。函数会连续地写出数据,直到以下条件被满足

  • 指定数目的页被写回磁盘;
  • 空闲内存数量回升超过下限阈值;

  另外flusher线程会在后台周期性被唤醒,检测内存中是否有驻留时间过长的脏页。此时flusher线程会调用wb_wirteback() 函数将驻留时间超时的脏页写回。
  可以在 /proc/sys/vm中设置回写相关的参数,也可以通过sysctl系统调用设置他们:
在这里插入图片描述

  flusher线程的实现在mm/page-writeback.cmm/backing-dev.c中。回写机制实现在文件fs/fs-writeback.c中。

4.2、被淘汰的bdflush、kupdated和pdflush

  2.6版本前,flusher线程的工作由bdflush和kupdated共同完成。bdflush负责在空闲内存不足和缓冲数量太大时进行写回操作,kupdated线程负责周期性写回操作。
  bdflush和flusher线程的区别在于:系统中只有一个bdflush后台线程;bdflush线程是基于缓冲的。
  odflysh线程的数量是动态的,默认为2到8个,他们是面向系统所有磁盘的全局任务。虽然简单,但是会因拥塞的磁盘而堵塞。和flusher线程的最大区别在于:flusher线程针对每个磁盘独立执行回写操作
  
  
  
  

5、膝上型计算机模式

  为了将机械硬盘转动的机械行为最小化,提供更好的省电能力。通过往 /proc/sys/vm/laptop_mode写’1’来打开膝上型计算机模式。这样就不会因为特定的写回机制而主动激活磁盘运行。
  此模式下,要求 dirty_expire_intervaldirty_wirteback_interval 两个阈值必须设置更大,例如10分钟。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr_zhangsq

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值