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

时间:2022-06-04 23:35:34

简介

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

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

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++;
    }