linux页缓存

2017-04-25

本节就聊聊页缓存这个东西……

 


 

一、概述

页缓存是一个相对独立的概念,其根本目的是为了加速对后端设备的IO效率,比如文件的读写。页缓存顾名思义是以页为单位的,目前我能想到的在两个地方页缓存的作用比较明显。1、在文件的读写中。2、在普通进程的匿名映射区操作中。在文件的读写中,进程对一个文件发起读请求,如果没哟对应的物理内存页,则内核处理程序首先在页缓存中查找,如果找到直接返回。如果没找到,那么再分配空间,把内容从文件读入内存,插入页缓存,并返回,这里读操作虽然是一个页面发生的未命中,但是很大程度上会读取多个连续页面到内存中,只不过返回调用指定页面,因为实际情况中,很少仅仅需要读取一个页面,这样后续如果在需要访问还是需要进行磁盘IO,磁盘寻道时间往往比读取时间还要长,为了提高IO效率,就有了上述的预读机制。。2、在普通进程的匿名映射中,比如进程申请的比较大的堆空间(大于128kb的都是利用MMAP映射的),这些空间的内容如果在一段时间内没被释放 且在一段时间内没被访问,则有可能被系统内存管理器交换到外存以腾出更多的物理内存空间。下次在进程访问该内容,根据页表查找发现物理页面不在内存,则需要调用缺页异常处理程序。异常处理程序判断该页时被换出到外存,那么首先也是在页缓存中找,如果找到就返回;否则,从交换分区或者交换文件中把对应的文件读入内存。这里所说的页缓存和1中描述的页缓存是一类缓存,但是并不是一个,1中的页缓存是文件系统相关的,在磁盘上对应着具体的文件;在页面回收的时候需要回写到磁盘即同步一下;而2中的页缓存其实是一种交换缓存,在页面回收的时候只能是换出到交换分区或者交换文件,不存在同步的操作。

二、管理结构描述

还记得之前在分析文件系统的时候,发现inode结构中有个address_space字段,每个文件关键一个address_space, 该结构管理文件映射的所有物理页面。在其中有个radix_tree_root字段,表示基数树的根即radix tree.页缓存正是通过这种数据结构管理的。

struct radix_tree_root {
    unsigned int        height;
    gfp_t            gfp_mask;
    struct radix_tree_node    __rcu *rnode;
};

 

结构比较简单,height表示树的高度;gfp_mask记录了从哪个内存域分配内存;rnode表示树的节点,不过树的根和节点是不同的数据结构,节点是radix_tree_node

struct radix_tree_node {
    unsigned int    height;        /* Height from the bottom */
    unsigned int    count;
    union {
        struct radix_tree_node *parent;    /* Used when ascending tree */
        struct rcu_head    rcu_head;    /* Used when freeing node */
    };
    void __rcu    *slots[RADIX_TREE_MAP_SIZE];
    unsigned long    tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS];
};

 

height表示该节点到树根的高度,count表示节点中已经使用的数组项的数目;slots是一个指向一个数组的指针,数组中每一项都可以是叶子节点或者中间节点。叶子节点一般就是page结构,而中间节点仍然是node节点。tags是一个二维数组,表示该节点的可能关联的页的属性,主要有下面几个

#define PAGECACHE_TAG_DIRTY 0  页当前是脏的
#define PAGECACHE_TAG_WRITEBACK 1  页当前正在回写
#define PAGECACHE_TAG_TOWRITE 2   

每个node关联的slots数组有RADIX_TREE_MAP_SIZE个表项,tags二维数组是一个RADIX_TREE_MAX_TAGS行、RADIX_TREE_TAG_LONGS列的数组。

#ifdef __KERNEL__
#define RADIX_TREE_MAP_SHIFT (CONFIG_BASE_SMALL ? 4 : 6)
#else
#define RADIX_TREE_MAP_SHIFT 3 /* For more stressful testing */
#endif

#define RADIX_TREE_MAP_SIZE (1UL << RADIX_TREE_MAP_SHIFT)

#define RADIX_TREE_MAX_TAGS 3

#define RADIX_TREE_TAG_LONGS ((RADIX_TREE_MAP_SIZE + BITS_PER_LONG - 1) / BITS_PER_LONG)

根据上述信息,RADIX_TREE_MAP_SHIFT多数指的4或者6,即RADIX_TREE_MAP_SIZE基本上是16或者64。不过我们这里为了方便介绍,取RADIX_TREE_MAP_SHIFT为3,即RADIX_TREE_MAP_SIZE为8.

这里虽然也是树状结构,但是其相对于二叉树或者三叉树就随意多了,可以认为仅仅是广义上的树。下面以向缓存中添加一个页为例,介绍下整个处理流程。

/*
* Per-cpu pool of preloaded nodes
*/
struct radix_tree_preload {
int nr;
struct radix_tree_node *nodes[RADIX_TREE_PRELOAD_SIZE];
};

起始函数为static inline int add_to_page_cache(struct page *page,struct address_space *mapping, pgoff_t offset, gfp_t gfp_mask),该函数锁定了页面之后就调用了add_to_page_cache_locked函数。

int add_to_page_cache_locked(struct page *page, struct address_space *mapping,
        pgoff_t offset, gfp_t gfp_mask)
{
    int error;

    VM_BUG_ON(!PageLocked(page));
    VM_BUG_ON(PageSwapBacked(page));

    error = mem_cgroup_cache_charge(page, current->mm,
                    gfp_mask & GFP_RECLAIM_MASK);
    if (error)
        goto out;

    error = radix_tree_preload(gfp_mask & ~__GFP_HIGHMEM);
    if (error == 0) {
        page_cache_get(page);
        page->mapping = mapping;
        page->index = offset;

        spin_lock_irq(&mapping->tree_lock);
        error = radix_tree_insert(&mapping->page_tree, offset, page);
        if (likely(!error)) {
            mapping->nrpages++;
            __inc_zone_page_state(page, NR_FILE_PAGES);
            spin_unlock_irq(&mapping->tree_lock);
            trace_mm_filemap_add_to_page_cache(page);
        } else {
            page->mapping = NULL;
            /* Leave page->index set: truncation relies upon it */
            spin_unlock_irq(&mapping->tree_lock);
            mem_cgroup_uncharge_cache_page(page);
            page_cache_release(page);
        }
        radix_tree_preload_end();
    } else
        mem_cgroup_uncharge_cache_page(page);
out:
    return error;
}

 

该函数中首先进行的是cgroup统计,这点不是本文重点,忽略。然后调用了radix_tree_preload函数,该函数检查CPU的radix_tree_node 缓存池是否充足,如果不充足就填充,正常情况是返回0的。进入if判断,首先对page结构做些设置,然后获取了mapping->tree_lock自旋锁。保证操作的一致性。调用radix_tree_insert函数把page插入到mapping->page_tree指向的radix树中。如果插入成功,返回0。插入成功后增加mapping page数目,释放锁。否则插入失败就对page进行释放。释放其实就是减少引用,等下次内存管理器扫描空闲内存时对其进行回收。发现最后还调用了下page_cache_release函数,该函数仅仅实现了启用抢占的功能,在radix_tree_preload函数中禁用了内核抢占。

现在我们自己说说radix_tree_insert函数

int radix_tree_insert(struct radix_tree_root *root,
            unsigned long index, void *item)
{
    struct radix_tree_node *node = NULL, *slot;
    unsigned int height, shift;
    int offset;
    int error;

    BUG_ON(radix_tree_is_indirect_ptr(item));

    /* Make sure the tree is high enough.  */
    if (index > radix_tree_maxindex(root->height)) {
        /*尝试扩展树,以满足条件*/
        error = radix_tree_extend(root, index);
        if (error)
            return error;
    }
    /*radix_tree_node最低一位表示是间接指针还是直接指针*/
    slot = indirect_to_ptr(root->rnode);

    height = root->height;
    //RADIX_TREE_MAP_SHIFT 3
    shift = (height-1) * RADIX_TREE_MAP_SHIFT;

    offset = 0;            /* uninitialised var warning */
    while (height > 0) {
        if (slot == NULL) {//需要扩充树
        /* Have to add a child node.  */
            if (!(slot = radix_tree_node_alloc(root)))
                return -ENOMEM;
            slot->height = height;
            slot->parent = node;
            if (node) {
                rcu_assign_pointer(node->slots[offset], slot);
                node->count++;
            } else
                rcu_assign_pointer(root->rnode, ptr_to_indirect(slot));
        }

        /* Go a level down */
        offset = (index >> shift) & RADIX_TREE_MAP_MASK;
        node = slot;
        slot = node->slots[offset];
        shift -= RADIX_TREE_MAP_SHIFT;
        height--;
    }

    if (slot != NULL)
        return -EEXIST;

    if (node) {
        node->count++;//增加计数
        rcu_assign_pointer(node->slots[offset], item);//设置指向
        BUG_ON(tag_get(node, 0, offset));
        BUG_ON(tag_get(node, 1, offset));
    } else {
        rcu_assign_pointer(root->rnode, item);
        BUG_ON(root_tag_get(root, 0));
        BUG_ON(root_tag_get(root, 1));
    }

    return 0;
}

 

函数包含三个参数,radix树的根,插入的位置索引,插入项目的指针,以插入page来讲,这里就是指向page结构的指针。radix_tree_node地址的最后一位表示该node连接的是node还是item。如果为1表明连接的还是node。潜意识里不管是node还是item,其地址都至少应该是2字节对齐的。函数首先对item进行了验证,看是否是二字节对齐的。然后判断index是否超过了当前树容纳的最大索引。最大值可以由树高确定,如果index超过了最大额度,则调用radix_tree_extend函数尝试扩展树,扩展失败就返回错误,关于这点最后会详细介绍。通过这里的判断,就获取树根指向的首个radix_tree_node。树高height,位置偏移shift。进入while循环,条件是height大于0.循环中首先判断slot==NULL,意味着根据height还没遍历到末尾,已经没有中间节点了,所以这就需要重新分配孩子节点,直到height减小到0;相反,如果一切顺利,就一层一层往下寻找,直到height=0;此时node为最终找到的radix_tree_node,offset为在node数组中的下标,接下来就可以设置指向了。下面以三层的radix tree为例,走下这个流程,如图所示

咱们不考虑根据height定位node遇到中间节点为NULL的情况。图中初始状态,slot为root->rnode;height=3;shift=6;循环里面执行如下图

出了循环,node应该就是最终定位的radix_tree_node节点,根据偏移项offset,可定位具体的项目。注意这里如图中所说,此时slot是L1层的节点指针,如果slot不为NULL,则表示index对应的slot已经有了映射页面,不需要再次添加物理页面了,否则把item设置到node->slots[offset],并增加node的使用项目计数。前思后想还是没想到node为NULL的情况,如果有知道的朋友可以说明下,谢谢!

下面看下验证index是否在树中的函数,系统用一个数组height_to_maxindex记录height对应的maxindex,看下参考代码

static __init void radix_tree_init_maxindex(void)
{
    unsigned int i;

    for (i = 0; i < ARRAY_SIZE(height_to_maxindex); i++)
        height_to_maxindex[i] = __maxindex(i);
}

static __init unsigned long __maxindex(unsigned int height)
{
    unsigned int width = height * RADIX_TREE_MAP_SHIFT;
    int shift = RADIX_TREE_INDEX_BITS - width;

    if (shift < 0)
        return ~0UL;
    if (shift >= BITS_PER_LONG)
        return 0UL;
    return ~0UL >> shift;
}
#define RADIX_TREE_INDEX_BITS  (8 /* CHAR_BIT */ * sizeof(unsigned long))

 

后者是初始化该数组的代码,关键在于前两行,还是按照前面的约定,RADIX_TREE_MAP_SHIFT看做3,后者根体系结构相关,31位下unsigned long是4个字节,RADIX_TREE_INDEX_BITS  为32。最后通过把全1右移得到结果,很是巧妙。还是按照三层来算

 

 

 

 

下面看扩展radix树的函数radix_tree_extend

static int radix_tree_extend(struct radix_tree_root *root, unsigned long index)
{
    struct radix_tree_node *node;
    struct radix_tree_node *slot;
    unsigned int height;
    int tag;

    /* Figure out what the height should be.  */
    height = root->height + 1;
    while (index > radix_tree_maxindex(height))
        height++;

    if (root->rnode == NULL) {
        root->height = height;
        goto out;
    }

    do {
        unsigned int newheight;
        if (!(node = radix_tree_node_alloc(root)))
            return -ENOMEM;

        /* Propagate the aggregated tag info into the new root */
        for (tag = 0; tag < RADIX_TREE_MAX_TAGS; tag++) {
            if (root_tag_get(root, tag))
                tag_set(node, tag, 0);
        }

        /* Increase the height.  */
        newheight = root->height+1;
        node->height = newheight;//从底部开始计算的高度
        node->count = 1;
        node->parent = NULL;
        slot = root->rnode;
        if (newheight > 1) {
            slot = indirect_to_ptr(slot);
            slot->parent = node;
        }
        node->slots[0] = slot;
        node = ptr_to_indirect(node);
        rcu_assign_pointer(root->rnode, node);
        root->height = newheight;
    } while (height > root->height);
out:
    return 0;
}

 该函数实现逻辑挺简单,首先尝试height+1,如果不能还不能满足条件,持续对height++,直到满足条件。接下来就是一个循环,以height>root->height为条件。所以这里实际上是填充指定height到root->height路径上空缺的节点。具体的填充过程没什么好讲的,只是注意这里的插入实际上类似于链表的头插法,即每次都从根节点插入。且节点的height是从下往上计算的。还有一点就是填充虽然达到了指定的高度,但是并不是填充了所有路径的节点,即并没有形成满树,仅仅是单条路径。因此在根据index插入item的时候,很有可能遇见路径上的节点为NULL,那么就需要重新填充、。

radix树的组织方式基本就是这样,其他的操作很类似就不在重复介绍。

 

 

 参考:

linux 3.10.1源码

http://blog.csdn.net/joker0910/article/details/8250085

    

 

posted @ 2017-04-25 20:40  jack.chen  Views(5888)  Comments(0Edit  收藏  举报

以马内利