Linux内核-内存-伙伴系统算法

来源:互联网 发布:mirna数据库 编辑:程序博客网 时间:2024/05/29 12:31

简介

内核在分配一组连续的页框时,必须解决一个著名的内存管理问题,也就是所谓的外碎片。本质上说,避免外碎片有两种方法:

  • 利用分页单元把一组非连续的空闲页框映射到连续的线性地址区间。
  • 开发一种适当的技术来记录现存的空闲连续页框块的情况,以尽量避免为满足小块的请求而分割大的空闲块。

Linux采用伙伴系统算法来解决外碎片问题,把所有的空闲框分组为11个块链表,每个块链表分别包含大小为1,2,4……1024个连续的页框,每个块的第一个页框的物理地址是该快大小的整数倍。

该算法的原理非常简单,假设请求的一个2^n大小的块,首先会检查链表中是否有空闲块,如果没有,算法会查找下一个更大的页块,即2^(n+1),如果存在这样的块,就把它分割成两等份,一半用作满足请求,另一半插入到2^n个页框的链表中;如果在2^(n+1)的链表中也没有空闲块,就继续找更大的块2^(n+m),将2^n满足请求,其余的2^n,2^(n+1),……,2^(n+m-1)插入对应的链表中。

以上的逆过程为页框块的释放过程,内核试图把大小为b的一对空闲伙伴块合并为一个大小为2b的单独块,满足以下条件的两个块称为伙伴:

  • 两个块具有相同的大小b
  • 它们的物理地址是连续的
  • 第一个块的第一个页框的物理地址是2*b*2^12的倍数

源码分析(Linux2.6)

  1. 数据结构
    Linux2.6为每个管理区使用不同的伙伴系统,每个伙伴系统使用的主要数据结构如下:

    • 每个管理区都关系到mem_map(存放内存所有的页框的数组)的子集,子集中的第一个元素和元素的个数分别由管理区描述符的zone_mem_map和size字段指定。
    • 包含有11个元素、元素类型为free_area的一个数组,每个元素对应一种块大小,存放在管理区描述符的free_area字段中。
  2. 分配块(mm/page_alloc.c)

    static struct page *__rmqueue(struct zone *zone, unsigned int order)    {        struct free_area * area;        unsigned int current_order;        struct page *page;        /**         * 从所请求的order开始,扫描每个可用块链表进行循环搜索。         */        for (current_order = order; current_order < MAX_ORDER; ++current_order) {            area = zone->free_area + current_order;            /**             * 对应的空闲块链表为空,在更大的空闲块链表中进行循环搜索。             */            if (list_empty(&area->free_list))                continue;            /**             * 运行到此,说明有合适的空闲块。             */            page = list_entry(area->free_list.next, struct page, lru);            /**             * 首先在空闲块链表中删除第一个页框描述符。             */            list_del(&page->lru);            rmv_page_order(page);            area->nr_free--;            /**             * 并减少空闲管理区的空闲页数量。             */            zone->free_pages -= 1UL << order;            /**             * 如果2^order空闲块链表中没有合适的空闲块,那么就是从更大的空闲链表中分配的。             * 将剩余的空闲块分散到合适的链表中去。             */            return expand(zone, page, order, current_order, area);        }        /**         * 直到循环结束都没有找到合适的空闲块,就返回NULL。         */        return NULL;    }

    expand函数如下:

    static inline struct page * expand(struct zone *zone, struct page *page, int low, int high, struct free_area *area)    {        unsigned long size = 1 << high;        while (high > low) {            area--;            high--;            size >>= 1;            BUG_ON(bad_range(zone, &page[size]));            /**            * 插入伙伴作为链表中的第一个元素            */            list_add(&page[size].lru, &area->free_list);            area->nr_free++;            set_page_order(&page[size], high);        }        return page;    }

    set_page_order函数如下,其实就是设置空闲块的第一个页框的标志,表明从page开始的连续2^order个页框为空闲块:

        static inline void set_page_order(struct page *page, int order) {        /**        * 块大小为2^order        */        page->private = order;        __SetPagePrivate(page);    }
  3. 释放块(mm/page_alloc.c)

    static inline void __free_pages_bulk (struct page *page, struct page *base, struct zone *zone, unsigned int order)    {        /**         * page_idx包含块中第一个页框的下标。         * 这是相对于管理区中的第一个页框而言的。         */        unsigned long page_idx;        struct page *coalesced;        /**         * order_size用于增加管理区中空闲页框的计数器。         */        int order_size = 1 << order;        if (unlikely(order))            destroy_compound_page(page, order);        /**        * base为管理区第一个页框,即zone_mem_map        */        page_idx = page - base;        /**         * 大小为2^k的块,它的线性地址都是2^k * 2 ^ 12的整数倍。         * 相应的,它在管理区的偏移应该是2^k倍。         */        BUG_ON(page_idx & (order_size - 1));        BUG_ON(bad_range(zone, page));        /**         * 增加管理区的空闲页数         */        zone->free_pages += order_size;        /**         * 最多循环10 - order次。每次都将一个块和它的伙伴进行合并。         * 每次从最小的块开始,向上合并。         */        while (order < MAX_ORDER-1) {            struct free_area *area;            struct page *buddy;            int buddy_idx;            /**             * buddy_idx为要合并的块的伙伴相对管理区第一个页框的下标             *下面的异或操作转换page_idx的第order位的值,因此如果这个位原先是0,buddy_idx就等于page_idx + order_size;             *相反,如果是1,buddy_idx就等于page_idx - order_size。之所以可以这样运算,是因为最上面伙伴块             *的第三个条件约束:第一个块的第一个页框的物理地址是2*b*2^12的倍数             */            buddy_idx = (page_idx ^ (1 << order));            /**             * 通过伙伴的下标找到页描述符的地址。             */            buddy = base + buddy_idx;            if (bad_range(zone, buddy))                break;            /**             * 判断伙伴块是否是大小为order的空闲页框的第一个页。             * 首先,伙伴的第一个页必须是空闲的(_count == -1)             * 同时,必须属于动态内存(PG_reserved被清0,PG_reserved为1表示留给内核或者没有使用)             * 最后,其private字段必须是order             */            if (!page_is_buddy(buddy, order))                break;            /**             * 运行到这里,说明伙伴块可以与当前块合并。             */            /* Move the buddy up one level. */            /**             * 伙伴将被合并,将它从现有链表中取下。             */            list_del(&buddy->lru);            area = zone->free_area + order;            area->nr_free--;            rmv_page_order(buddy);            page_idx &= buddy_idx;            /**             * 将合并了的块再与它的伙伴进行合并。             */            order++;        }        /**         * 伙伴不能与当前块合并。         * 将块插入适当的链表,并以块大小的order更新第一个页框的private字段。         */        coalesced = base + page_idx;        set_page_order(coalesced, order);        list_add(&coalesced->lru, &zone->free_area[order].free_list);        zone->free_area[order].nr_free++;    }
0 0