zoukankan      html  css  js  c++  java
  • 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

        

  • 相关阅读:
    leetcode 279. Perfect Squares
    leetcode 546. Remove Boxes
    leetcode 312. Burst Balloons
    leetcode 160. Intersection of Two Linked Lists
    leetcode 55. Jump Game
    剑指offer 滑动窗口的最大值
    剑指offer 剪绳子
    剑指offer 字符流中第一个不重复的字符
    leetcode 673. Number of Longest Increasing Subsequence
    leetcode 75. Sort Colors (荷兰三色旗问题)
  • 原文地址:https://www.cnblogs.com/ck1020/p/6764243.html
Copyright © 2011-2022 走看看