内核中alloc_pages系列页框分配函数都是基于伙伴算法实现的,这些函数最终都会调用伙伴算法的入口函数buffered_rmqueue()。
Linux内核管理物理内存有三种方式,其一就是经典的伙伴算法。但是伙伴算法分配物理内存的基本单位是页框,因此内核又引入了slab机制,基于此机制实现的物理内存分配器可以快速有效的分配小于页框的物理内存,并且可以有效避免内部碎片。另外,内核常常会申请单个页框大小的物理内存,因此内核又引入了per-CPU机制,该机制专门用于快速分配单个页框。
1.__rmqueue()
其实buffered_rmqueue()函数仍然没有进行真正的页框分配,该函数首先判断分配阶是否为0,如果是则启用per-CPU机制来分配物理内存,否则调用__rmqueue()。
static struct page *__rmqueue(struct zone *zone, unsigned int order, int migratetype) { struct page *page; retry_reserve: page = __rmqueue_smallest(zone, order, migratetype); if (unlikely(!page) && migratetype != MIGRATE_RESERVE) { page = __rmqueue_fallback(zone, order, migratetype); if (!page) { migratetype = MIGRATE_RESERVE; goto retry_reserve; } } trace_mm_page_alloc_zone_locked(page, order, migratetype); return page; }
传递到此函数中的zone表示伙伴算法将从该内存管理区中分配页框,order即分配阶,migratetype表示迁移类型。该函数首选__rmqueue_smallest()进行内存分配,如果在指定的迁移类型上分配失败后,再选用其他备用的迁移列表进行内存分配,该过程通过__rmqueue_fallback()完成。总之内核总是在竭尽全力保证满足分配内存的请求。
2.__rmqueue_smallest()
该函数的实现比较简单,从当前指定的分配阶到最高分配阶依次进行遍历。在每次遍历的分配阶链表中,根据参数migratetype选择正确的迁移队列。根据以上的限定条件,当选定一个页框块链表后,只要该链表不为空,就说明可以分配该分配阶对应的页框块。
一旦选定在当前遍历的分配阶链表上分配页框,那么就通过list_entry()将该页框块从链表上移除。然后将页框块首页框的PG_buddy标志删除,删除该标志说明当前页框块已经不属于伙伴链表。并且将该首页框描述符中的priveate置0,该字段中本来保存的是其所处页框块的分配阶。以上这个过程通过rmv_page_order()完成。此外,还要更新页框块链表nr_free的值。
static inline struct page *__rmqueue_smallest(struct zone *zone, unsigned int order, int migratetype) { unsigned int current_order; struct free_area * area; struct page *page; 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--; expand(zone, page, order, current_order, area, migratetype); return page; } return NULL; } static inline void rmv_page_order(struct page *page) { __ClearPageBuddy(page); set_page_private(page, 0); }
__rmqueue_smallest()内部还有一个重要的函数expand()。进入该函数的条件是当所申请的分配阶order小于当前选中的分配阶current_order,也就是说指定的分配阶链表中没有空闲的页框块,只能选用较大的页框块。因此,expand()必须按照伙伴算法的分裂原理将比较大的页框块分割成较小的块。
3.expand()
分裂函数的实现也是显而易见的,它完全遵照伙伴算法的分裂原理。这里有两个分配阶,一个是申请页框时指定的low,一个是在上级函数中遍历时所选定的high。该函数从high分配阶开始递减向low遍历,也就是从较大的页框块开始依次分裂。
比如high为4,而low为2。那么第一遍历时,将大小为16(分配阶为4)的页框块一份为2。通过list_add()将后面的8个连续页框块加入下级链表(分配阶为3),下级链表通过将area指针自减即可得到,后8个页框块的指针也通过page+size获得,而page仍然指向最初的页框块首页框。此时还要对分配阶为3的链表更新nr_free,以及通过set_page_order()对后8个页框块设置一些标志。
第二次遍历将前面8个页框块继续一分为二,将后4个页框块加入area所指向的下级链表(分配阶为2)。第三次遍历时,循环条件已经不再满足,因此返回前4个页框块首页框的描述符地址page。
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); } } static inline void set_page_order(struct page *page, int order) { set_page_private(page, order); __SetPageBuddy(page); }