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

        

  • 相关阅读:
    204. 计数质数
    236. 二叉树的最近公共祖先
    优先队列和哈夫曼树
    185. 部门工资前三高的所有员工(求组内前几的值)
    部门工资最高的员工(求组内最大值)
    回调函数的案例
    单链表
    动态数组
    一致性哈希算法的基本原理
    只用2GB内存在20亿个整数中找到出现次数最多的数
  • 原文地址:https://www.cnblogs.com/ck1020/p/6764243.html
Copyright © 2011-2022 走看看