上次我们讲到page frame是物理内存的基本组成单位。那Kernel就必须要有一套机制来管理空闲的page frames。这一点不难理解。每个县长必须要把本县可用的劳动力登记在册,这样哪天皇帝要征兵了,你才不至于手忙脚乱。


这个问题看似简单,实则不然。因为这里面有一个外碎片的问题。

在物理内存中,连续的物理内存页有时是很重要的。例如在DMA操作中,由于大部分DMA处理器都没有分页机制,它们会直接访问物理内存地址,因此DMA 所用的缓冲区在物理地址空间必须连续;再例如,使用连续的物理内存页,可以保证内核页表中直接映射的页表项始终保持不变,同时连续的物理内存页可以作为一个大页(4MB)使用,这些都可以减少TLB miss,从而增加性能。

然而,系统运行一段时间后,可能会出现以下尴尬的局面:系统想申请一块连续的内存空间,同时系统中也有足够多的空闲内存页,可惜这些内存页都分散开了,导致申请失败。

你领着女朋友去坐地铁,车厢里可能会有好多空座,但却都不挨着,结果你还是没法和女朋友在一块坐下。这才是两个人的情况,假设你再带上小三小四,想同时坐在一块是不是更难了。。


为了解决外碎片的问题,Kernel采用了大名鼎鼎的buddy system算法。这个算法在内存管理中论地位,稳如泰山;论效率,快如闪电;论设计,却是极其简单优雅。

它把所有的空闲内存页按照0 ~ 10的order放在了MAX_ORDER (11)个链表中,每个链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续内存页组成的页块。其中,每个页块的第一个内存页的物理地址是该块大小的整数倍。如果两个页块满足以下几个条件,那他俩就互为伙伴。

  • 这两个页块的大小相同,记作b;

  • 他们的物理地址是连续的;

  • 第一个页块的第一个内存页的物理地址是 2*b*(2^12)的倍数。


注意,buddy system算法是递归的。当两个伙伴b相认后会合并成2b,然后这个2b再接着寻找自己的小伙伴。


说了这么多,让我们来看看它的具体实现吧。


1. 数据结构

在每个memory zone描述符里,有这样一个成员变量:

struct zone {
...

struct free_area    free_area[MAX_ORDER];

...
};


这个就是buddy system使用的主要数据结构了。MAX_ORDER一般为11,struct free_area 定义如下:

struct free_area {
    struct list_head    free_list[MIGRATE_TYPES];
    unsigned long       nr_free;
};


MIGRATE_TYPES是buddy system进一步减少碎片的机制,该机制从2.6.24才开始引入。我们这里暂且忽略,等以后专门讲一下这个机制。所以可以先把MIGRATE_TYPES当作1,即每个free_area结构体只包含一个链表。


我们以free_area数组中第k个元素为例,看看这个数据结构是怎样使用的。

  • 第k个元素标识了所有大小为 2^k 的空闲页块。这些空闲页块以链表的形式连在一起,链表的头部即为第k个元素中的 free_list。实际上,这个链表中链接的是每一个空闲页块的起始内存页的描述符(struct page)。页块之间通过lru字段互连。

  • 第k个元素还包含了一个nr_free字段,它指定了该链表中空闲页块的个数。

  • 对于每一个大小为 2^k 的空闲页块的起始内存页,还会做两个标志:页描述符(struct page)的private字段存放该页块的order,也就是数字k;页描述符flags中的PG_buddy置位。


Kernel那些事儿之内存管理(3) --- 久别重逢_内存管理


2. 分配一个页块

从buddy system中分配一个页块主要是由函数 __rmqueue_smallest 来完成的。

/*
 * Go through the free lists for the given migratetype and remove
 * the smallest available page from the freelists
 */
static struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
                        int migratetype)
{
    unsigned int current_order;
    struct free_area * area;
    struct page *page;

    /* Find a page of the appropriate size in the preferred list */
    for (current_order = order; current_order < MAX_ORDER; ++current_order) {
        area = &(zone->free_area[current_order]);
        if (list_empty(&area->free_list[migratetype]))
            continue;

        page = list_entry(area->free_list[migratetype].next,
                            struct page, lru);
        list_del(&page->lru);
        rmv_page_order(page);
        area->nr_free--;
        __mod_zone_page_state(zone, NR_FREE_PAGES, - (1UL << order));
        expand(zone, page, order, current_order, area, migratetype);
        return page;
    }

    return NULL;
}

该函数从参数指定的 order 开始寻找页块,如果当前current_order对应的链表为空,则继续向下一级寻找。一个 2^(k+1) 页的页块中肯定包含 2^k 页的页块,对吧。

如果找到了一个页块,则把它从current_order链表中摘下来,相应的递减nr_free的值,更新该zone的统计信息中空闲页的计数。

清除该页块的起始内存页中的两个标志:PG_buddy标志和private字段。

如果当前current_order比参数指定的 order 大,则从buddy system中摘下链表的这个页块就要被分成若干小的页块,除去要分配的这一块,其他的还得放回buddy system。这个工作是通过函数 expand 来完成的。

static inline void expand(struct zone *zone, struct page *page,
    int low, int high, struct free_area *area,
    int migratetype)
{
    unsigned long size = 1 << high;

    while (high > low) {
        area--;
        high--;
        size >>= 1;
        VM_BUG_ON(bad_range(zone, &page[size]));
        list_add(&page[size].lru, &area->free_list[migratetype]);
        area->nr_free++;
        set_page_order(&page[size], high);
    }
}

背后的逻辑不难理解:假设一个请求申请分配大小为 2^h 的页块,最后拿到了一个大小为 2^k 的空闲页块 (k > h),则该空闲页块前面 2^h 页会被分配出去,剩下的后面 (2^k - 2^h)页还需要再重新放回buddy system。

假设要分配一个2页的内存,现在取下了一个16页的页块。

16页的页块分成两个8页的页块,后面的8页放回order为3的链表中;

剩下的8页分成两个4页,后面的4页放回到order为2的链表中;

剩下的4页分成两个2页,后面的2页放回到order为1的链表中,前面的2页就是分配出去的页块了。


至此,分配完成,一个满足要求的页块不得不与它的小伙伴分离,离开buddy system。这个页块可能被用作page cache,可能被用作slab,可能被用来存放内核进程或用户进程的代码或数据。。。

从此,天涯沦落,漂泊江湖,每当夜雨潇潇漏尽残灯之时,总会想起当年,春风拂面,与小伙伴一起在盛开的桃李花下举杯畅饮,调笑风生。正所谓:桃李春风一杯酒,江湖夜雨十年灯。


3. 释放一个页块

要释放一个页块,首先要先解决两个问题,给定一个页块,其order为O,其起始内存页的索引为B1,那么:

  • 它的buddy页块的起始内存页索引为: B2 = B1 ^ (1 << O)       (这里是异或操作)

  • 它和它的buddy合并,形成的新的页块的起始内存页索引为:P = B & ~(1 << O)


这两个操作由以下两个函数完成:

static inline struct page *
__page_find_buddy(struct page *page, unsigned long page_idx, unsigned int order)
{
    unsigned long buddy_idx = page_idx ^ (1 << order);

    return page + (buddy_idx - page_idx);
}


static inline unsigned long
__find_combined_index(unsigned long page_idx, unsigned int order)
{
    return (page_idx & ~(1 << order));
}


一旦找到了自己的buddy,如何进行身份验证:

  • 一个页块的buddy不能在memory hole中;

  • 一个页块的buddy必须是在buddy system中;

  • 页块和其buddy有相同的order

  • 页块和其buddy在相同的zone中。


验证工作是由函数 page_is_buddy 来完成的。

static inline int page_is_buddy(struct page *page, struct page *buddy,
                                int order)
{
    if (!pfn_valid_within(page_to_pfn(buddy)))
        return 0;

    if (page_zone_id(page) != page_zone_id(buddy))
        return 0;

    if (PageBuddy(buddy) && page_order(buddy) == order) {
        BUG_ON(page_count(buddy) != 0);
        return 1;
    }
    return 0;
}


好了,万事俱备。释放一个页块是由函数 __free_one_page 来完成的。

static inline void __free_one_page(struct page *page,
        struct zone *zone, unsigned int order)
{
    unsigned long page_idx;
    int order_size = 1 << order;
    int migratetype = get_pageblock_migratetype(page);

    page_idx = page_to_pfn(page) & ((1 << MAX_ORDER) - 1);

    __mod_zone_page_state(zone, NR_FREE_PAGES, order_size);
    while (order < MAX_ORDER-1) {
        unsigned long combined_idx;
        struct page *buddy;

        buddy = __page_find_buddy(page, page_idx, order);
        if (!page_is_buddy(page, buddy, order))
            break;      /* Move the buddy up one level. */

        list_del(&buddy->lru);
        zone->free_area[order].nr_free--;
        rmv_page_order(buddy);
        combined_idx = __find_combined_index(page_idx, order);
        page = page + (combined_idx - page_idx);
        page_idx = combined_idx;
        order++;
    }
    set_page_order(page, order);
    list_add(&page->lru,
        &zone->free_area[order].free_list[migratetype]);
    zone->free_area[order].nr_free++;
}


页块b一旦找到自己的buddy,便会与其合并成页块2b。这个2b再去寻找自己的buddy,合并成4b。。。直到某个页块发现 page_is_buddy 不满足,或是已经合并到了最大页块。

最后,合并完成之后的页块被放回到buddy system中与order对应的链表上。


当年分配页块时不得不分离的伙伴,最终又重新聚在了一起。

电影《一代宗师》中,宫二×××有句台词:世间所有的相遇,都是久别重逢。用这句话来概括Kernel中的buddy system算法,再合适不过了。