linux内核分析笔记----页高速缓存和页回写

本文深入探讨Linux内核中页高速缓存的工作原理,包括其如何通过物理内存减少磁盘I/O操作,以及如何利用优先搜索树进行高效搜索。详细解释了缓存的实现方式、数据结构(如address_space结构体)和关键操作(如readpage()与writepage()方法),同时介绍了脏页管理机制和pdflush后台回写例程。

      说句真的,也缓存我听的都少,虽然看了很多。页高速缓存是linux内核实现的一种主要磁盘缓存,它主要用来减少对磁盘的IO操作,具体地讲,是通过把磁盘中的数据缓存到物理内存中,把对磁盘的访问变为对物理内存的访问。为什么要这么做呢?一,速度;二临时局部原理。有关这两个概念,相信熟悉操作系统的我们不会太陌生。页高速缓存是由RAM中的物理页组成的,缓存中的每一页都对应着磁盘中的多个块。每当内核开始执行一个页IO操作时,就先到高速缓存中找。这样就可以大大减少磁盘操作。
     
一个物理页可能由多个不连续的物理磁盘块组成。也正是由于页面中映射的磁盘块不一定连续,所以在页高速缓存中检测特定数据是否已被缓存就变得不那么容易了。另外linux页高速缓存对被缓存页的范围定义的非常宽。缓存的目标是任何基于页的对象,这包含各种类型的文件和各种类型的内存映射。为了满足普遍性要求,linux使用定义在linux/fs.h中的结构体address_space结构体描述页高速缓存中的页面,如下:

struct address_space {
        struct inode            *host;              /* owning inode */
        struct radix_tree_root  page_tree;          /* radix tree of all pages */
        spinlock_t              tree_lock;          /* page_tree lock */
        unsigned int            i_mmap_writable;    /* VM_SHARED ma count */
        struct prio_tree_root   i_mmap;             /* list of all mappings */
        struct list_head        i_mmap_nonlinear;   /* VM_NONLINEAR ma list */
        spinlock_t              i_mmap_lock;        /* i_mmap lock */
        atomic_t                truncate_count;     /* truncate re count */
        unsigned long           nrpages;            /* total number of pages */
        pgoff_t                 writeback_index;    /* writeback start offset */
        struct address_space_operations   *a_ops;   /* operations table */
        unsigned long           flags;              /* gfp_mask and error flags */
        struct backing_dev_info *backing_dev_info;  /* read-ahead information */
        spinlock_t              private_lock;       /* private lock */
        struct list_head        private_list;       /* private list */
        struct address_space    *assoc_mapping;     /* associated buffers */
};

      其中的i_mmap字段是一个优先搜索树,它的搜索范围包含了在address_sapce中私有的和共享的页面。nrpages反应了address_space空间的大小。address_space结构往往会和某些内核对象关联。通常情况下,会与一个索引节点(inode)关联,这时host域就会指向该索引节点。如果关联对象不是一个索引节点的话,比如address_space和swapper关联时,这是host域会被置为NULL。a_ops域指向地址空间对象中的操作函数表,这与VFS对象及其操作函数表关系类似,操作函数表定义在linux/fs.h中,由address_space_operations表示,如下:

struct address_space_operations {
        int (*writepage)(struct page *, struct writeback_control *);
        int (*readpage) (struct file *, struct page *);
        int (*sync_page) (struct page *);
        int (*writepages) (struct address_space *, struct writeback_control *);
        int (*set_page_dirty) (struct page *);
        int (*readpages) (struct file *, struct address_space *,struct list_head *, unsigned);
        int (*prepare_write) (struct file *, struct page *, unsigned, unsigned);
        int (*commit_write) (struct file *, struct page *, unsigned, unsigned);
        sector_t (*bmap)(struct address_space *, sector_t);
        int (*invalidatepage) (struct page *, unsigned long);
        int (*releasepage) (struct page *, int);
        int (*direct_IO) (int, struct kiocb *, const struct iovec *,loff_t, unsigned long);
};

      这里面最重要的两个就是readpage()与writepage()了。对于readpage()方法而言,首先,一个address_space对象和一个偏移量会被传给该方法,这两个参数用来在页高速缓存中搜素需要的数据:

page = find_get_page(mapping, index);

      mapping是指定的地址空间,index是文件中的指定位置。如果要搜索的页并没在高速缓存中,那么内核将分配一个新页面,然后将其加入到页高速缓存中,如下:

int error;
cached_page = page_cache_alloc_cold(mapping);
if (!cached_page)
        /* error allocating memory */
error = add_to_page_cache_lru(cached_page, mapping, index, GFP_KERNEL);
if (error)
        /* error adding page to page cache */

      最后,需要的数据从磁盘被读入,再被加入页高速缓存,然后返回给用户:error = mapping->a_ops->readpage(file,page);
      写操作和读操作有少许不同。对于文件映射来说,当页被修改了,VM仅仅需要调用:setPageDirty(page);内核晚些时候通过writepage()方法把页写出。对特定文件的写操作会比较复杂----它的代码在文件mm/filemap.c中,通常写操作路径基本上要包含一下各步:

page = __grab_cache_page(mapping, index, &cached_page, &lru_pvec);
status = a_ops->prepare_write(file, page, offset, offset+bytes);
page_fault = filemap_copy_from_user(page, offset, buf, bytes);
status = a_ops->commit_write(file, page, offset, offset+bytes);

      首先,在页高速缓存中搜索需要的页,如果需要的页不在高速缓存中,那么内核在高速缓存中新分配一空闲项;下一步,prepare_write()方法被调用,创建一个写请求;接着数据被从用户空间拷贝到内核缓冲;最后通过commit_write()函数将数据写入磁盘。

因为在任何页IO操作前内核都要检查页是否已经在页高速缓存中了,所以这种检查必须迅速,高效。否则得不偿失了。前边已经说过,也高速缓存通过两个参数address_space对象和一个偏移量进行搜索。每个address_space对象都有唯一的基树(radix tree),它保证在page_tree结构体中。基树是一个二叉树,只要指定了文件偏移量,就可以在基树中迅速检索到希望的数据,页高速缓存的搜索函数find_get_
page()要调用函数radix_tree_lookup(),该函数会在指定基树中搜索指定页面。基树核心代码的通用形式可以在文件lib/radix-tree.c中找到,另外想要使用基树,需要包含头文件linux/radix_tree.h.

      在内存中累积起来的脏页必须被写回到磁盘,在一下两种情况下,脏页会被写会到磁盘:

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

      现在你只需知道,2.6内核中,使用pdflush后台回写例程来完成这个工作。那么具体是怎么实现的呢:

首先,pdflush线程在系统中的空闲内存低于一个特定的阈值时,将脏页刷新回磁盘。该后台回写例程的目的在于在可用物理内存过低时,释放脏页以重新获得内存。上面提到的特定的内存阈值可以通过dirty_background_ratio系统调用设置。一旦空闲内存比这个指小时,内核便会调用函数wakeup_bdflush() 唤醒一个pdflush线程,随后pdflush线程进一步调用函数background_writeout()开始将脏页写会到磁盘,函数background_writeout()需要一个长整型参数,该参数指定试图写回的页面数目。函数background_writeout会连续地写会数据,直到满足一下两个条件:

1.已经有指定的最小数目的页被写回到磁盘。
2.空闲内存页已经回升,超过了阈值dirty_background_ration.

      pdflush线程(实现在mm/pdflush.c中,回写机制的实现代码在文件mm/page-writeback.c和fs/fs-writeback.c中)周期地被唤醒并且把超过特定期限的脏页写回磁盘。系统管理员可以在/proc/sys/vm中设置回写相关的参数,也可以通过sysctl系统调用来设置它们。下表给出了可以设置的量:

     image

Linux内核第八章通常涉及内存管理的核心部分,特别是框管理分配机制。在这一章中,会深入探讨物理内存的管理方式,包括如何将内存划分为框(page frame),以及内核如何高效地分配回收这些框[^3]。 ### 框管理 Linux内核将物理内存划分为固定大小的单元,称为框(page frame),通常每个框的大小为4KB。为了管理这些框,内核维护了一个称为“描述符”(page descriptor)的数据结构,即`struct page`,它描述了每个框的状态用途。所有的描述符存储在一个全局数组`mem_map`中,数组的索引对应于框的编号。 框可以处于不同的状态,例如空闲、已分配、保留等。内核使用伙伴系统(buddy system)来管理框的分配与释放。该机制通过将内存划分为大小为2的幂次方的块来优化分配效率,并尽量减少内存碎片。 ### 内存区域划分 Linux将物理内存划分为多个内存区域(zone),以应对不同类型的内存访问需求。常见的内存区域包括: - `ZONE_DMA`:用于DMA(直接内存访问)操作的内存区域,通常位于较低的物理地址范围。 - `ZONE_NORMAL`:常规的内存区域,内核可以直接映射。 - `ZONE_HIGHMEM`:高端内存区域,用于支持超过内核直接映射能力的内存。 每个内存区域都有自己的框分配策略,确保不同类型的内存请求能够得到适当的响应。 ### 框分配器 Linux内核提供了多种框分配器接口,用于满足不同的内存分配需求。主要的分配函数包括: - `alloc_pages()`:用于分配一个或多个连续的框,并返回一个指向`struct page`的指针。 - `__get_free_pages()`:返回一个指向分配框的虚拟地址,适用于需要直接访问内存的情况。 - `free_pages()` `__free_pages()`:用于释放之前分配的框。 这些函数通常用于内核模块或设备驱动程序中,处理对物理内存的低级操作。 ### Slab分配器 为了提高内存分配的效率,Linux引入了Slab分配器。Slab分配器用于管理常用数据结构(如`struct inode`、`struct dentry`等)的内存分配。它通过预分配一组对象,并将它们缓存起来,以减少频繁的框分配释放操作。 Slab分配器的核心接口包括: - `kmem_cache_create()`:创建一个特定类型对象的缓存- `kmem_cache_alloc()`:从缓存中分配一个对象。 - `kmem_cache_free()`:将对象释放回缓存- `kmem_cache_destroy()`:销毁缓存。 Slab分配器显著减少了内存碎片,并提高了对象分配的速度。 ### 框回收机制 当系统内存紧张时,Linux内核会启动框回收机制,尝试释放一些不常用的框。主要的回收策略包括: - **缓存回收**:将文件缓存中的内容写回磁盘,以释放内存。 - **交换(Swapping)**:将匿名(如进程堆栈、堆内存)写入交换分区,以腾出物理内存。 内核通过`kswapd`守护进程直接回收机制来协调框的回收工作。`kswapd`会定期检查系统的内存状态,并在内存不足时触发框回收操作。 ### 示例代码:分配释放框 以下是一个简单的示例,展示如何使用`alloc_pages``__free_pages`函数来分配释放框: ```c #include <linux/gfp.h> #include <linux/mm.h> struct page *my_page; my_page = alloc_pages(GFP_KERNEL, 0); // 分配一个框 if (my_page) { void *addr = page_address(my_page); // 获取框的虚拟地址 // 使用 addr 进行内存操作 __free_pages(my_page, 0); // 释放框 } ``` 此代码片段展示了如何在内核模块中动态分配释放框。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值