首先总体介绍下linux对内存的划分:
一 内存介绍
1. 节点 pg_data_t
在某些体系架构下,同一cpu访问不同部分的内存地址时间是不一样的,为了解决这个问题,将内存划分为不同的节点。同一cpu访问同一节点下的内存,其时间是一样的。当然,不同cpu访问同一节点下的内存,时间不一定一样。
对于x86体系,cpu访问不同地址的内存,其时间是一样的,因此内存只有一个节点。
2. 管理区 zone
节点下又划分不同的管理区。受限于某些硬件设备,或地址空间的大小,物理内存被划分为三(两)块:DMA区,NORMAL区,HIGHMEM区(只有32位系统有)。节点中会维护着该节点下所有管理区的信息,如下图所示:
图1
内核从管理区(DMA管理区,NORMAL管理区等)申请内存时,是以页框为单位的,即申请的内存大小为4K的整数倍。试想如果存在以下的情况。其中白色部分为空闲页框,红色部分为已使用页框。在下面的区域中,存在8K的空闲内存,但无法申请连续的8K的内存,即内存存在碎片。即使系统有足够的空闲内存,也有可能出现申请失败的情况、
图2
下面为看下linux内核是如何处理这种情况的
二 伙伴算法
每个管理区维护了一个free_area结构的数组(结构中包含一个双向链表头),该数据共11个元素,第n个元素链接的是多个页框,每个页框有连续2的n次方个空闲页框,如下图:
图3
第0个元素链接的是单个页框;
第1个元素链接的是连续2(2^1)个空闲的页框;
第2个元素链接的是连续4(2^2)个空闲的页框;
第3个元素链接的是连续8(2^3)个空闲的页框;
以此类推。
2.1 链表非空
内核申请内存时,每次申请的都是2的n次方个连续的页框。
比如内存申请1个页框时,需要从数组0的链表中获取。此时链表不空,取出第1个页框后,如下图4所示:
图4
再获取一个页框后,数组0的链表将成为空链表,如图5所示:
图5
2.2 链表已空
再申请一个页框时,此时数组0链表已空,则从数组1链表中获取。由于数组1指向的都是连续2个空闲的页框,将其中一个页框提供给内核,然后将另一个页框放入数组0链表中,如图6所示:
图6
2.3 下级链表已空
图7
图7中,数组0和数组1的链表都已空,此时申请1个页框,此从数组2链表中获取。将1个页框返回给内核,一个页框添加到数组0链表中,另外2个页框添加到数组1链表中。如图8:
图8
2.4 链表中页框地址
第n个数组中的链表中链接的页框,其起始地址为2的n次方的倍数。
如数组1链表中的页框,起始地址为2(2^1)的倍数;
数组3链表中的页框,起始地址为8(2^3)的倍数;
2.4 返还页框
返还页框,是申请页框的逆向,如向图8返还1个页框后,如图9所示:
图9
再返地址为4的页框后,如图10所示:
图10
右边有两个相临的单个页框(蓝色),其起始地址为 4,符合2.4中所述规则,因此将两个页框视做连续2个空闲的页框,放入数组1链表中,如图11所示:
图11
右上角黄色的页框,又可以合并后放入数组2的链表中,如图12所示:
图12
2.5 不能合并的页框
图 13
图中连续的两个蓝色页框,其起始地址为1,不是2(2^1)的倍数,因此不能合并后,放入数组1的链表。
三 内核代码
从管理区的伙伴系统申请页框的方法是__rmqueue,__rmqueue又调用__rmqueue_smallest
static __always_inline struct page *
__rmqueue(struct zone *zone, unsigned int order, int migratetype)
{
struct page *page;
retry:
page = __rmqueue_smallest(zone, order, migratetype);
if (unlikely(!page)) {
if (migratetype == MIGRATE_MOVABLE)
page = __rmqueue_cma_fallback(zone, order);
if (!page && __rmqueue_fallback(zone, order, migratetype))
goto retry;
}
trace_mm_page_alloc_zone_locked(page, order, migratetype);
return page;
}
/*
zone 目标管理区(DMA, NORMAL, HIGHMEM)
order free_area数组的下标
*/
static __always_inline
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]);
page = list_first_entry_or_null(&area->free_list[migratetype],
struct page, lru);
// 先从指定的数组中查找,如果没找到,则从下一个数组中找(对应2.2 链表已空)
if (!page)
continue;
list_del(&page->lru); // 将页框从链表中删除
rmv_page_order(page);
area->nr_free--;
// 重新整理链表
expand(zone, page, order, current_order, area, migratetype);
set_pcppage_migratetype(page, migratetype);
return page;
}
return NULL;
}
/*
low 请求的order
high 实际获取页框的order
*/
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;
// __rmqueue指定的数组中未找到空闲页框
while (high > low) {
area--;
high--;
size >>= 1;
VM_BUG_ON_PAGE(bad_range(zone, &page[size]), &page[size]);
/*
* Mark as guard pages (or page), that will allow to
* merge back to allocator when buddy will be freed.
* Corresponding page table entries will not be touched,
* pages will stay not present in virtual address space
*/
if (set_page_guard(zone, &page[size], high, migratetype))
continue;
// 插入伙伴,作为链表中第一个元素(将后一半的页框,加到前一个数组中,对应图5到图6)
list_add(&page[size].lru, &area->free_list[migratetype]);
area->nr_free++; // 可用数量+1
set_page_order(&page[size], high);
}
}
归还页框的函数是free_pages,这里就不赘述了。