物理页面的分配
内核代码中申请物理页面的函数是 alloc_pages
页面分配需要解决的问题
NUMA buddy和位图 内存不够时如何处理
NUMA结构的处理逻辑
NUMA的处理是功过宏 CONFIG_NUMA走不同的分支
struct page * alloc_pages(int gfp_mask, unsigned long order) { struct page *ret = 0; pg_data_t *start, *temp; #ifndef CONFIG_NUMA unsigned long flags; static pg_data_t *next = 0; #endif if (order >= MAX_ORDER) return NULL; #ifdef CONFIG_NUMA temp = NODE_DATA(numa_node_id()); #else spin_lock_irqsave(&node_lock, flags); if (!next) next = pgdat_list; temp = next; next = next->node_next; spin_unlock_irqrestore(&node_lock, flags); #endif start = temp; while (temp) { if ((ret = alloc_pages_pgdat(temp, gfp_mask, order))) return(ret); temp = temp->node_next; } temp = pgdat_list; while (temp != start) { if ((ret = alloc_pages_pgdat(temp, gfp_mask, order))) return(ret); temp = temp->node_next; } return(0); }
0) 参数: gfp_mask 是一个整数代表使用哪中分配策略;order是分配物理页的大小。
1) 若果定义了CONFIG_NUMA 就用 NODE_DATA(numa_node_id()) 获取当前node对应的pg_data_t, 否则就用全局的pgdata_list。
2) pgdata_list是node的一个数组。
3) 然后,循环遍历所有的pg_data,也就是循环所有的分配策略,从每一个分配策略中调用 alloc_pages_pgdat分配。
alloc_pages_pgdat
static struct page * alloc_pages_pgdat(pg_data_t *pgdat, int gfp_mask, unsigned long order) { return __alloc_pages(pgdat->node_zonelists + gfp_mask, order); } 1) gfp_mask就是在一个node中的分配策略数组的下标 pgdat->node_zonelists + gfp_mask
开始分配内存__alloc_pages
struct page * __alloc_pages(zonelist_t *zonelist, unsigned long order) { zone_t **zone; int direct_reclaim = 0; unsigned int gfp_mask = zonelist->gfp_mask; struct page * page; memory_pressure++; if (order == 0 && (gfp_mask & __GFP_WAIT) && !(current->flags & PF_MEMALLOC)) direct_reclaim = 1; if (inactive_shortage() > inactive_target / 2 && free_shortage()) wakeup_kswapd(0); else if (free_shortage() && nr_inactive_dirty_pages > free_shortage() && nr_inactive_dirty_pages >= freepages.high) wakeup_bdflush(0);
1) memory_pressure 统计VM子系统的压力
2) gfp_mask 这个 gfp_mask不同于上面那个,代表一个具体的分配策略的控制位
#define __GFP_WAIT 0x01 #define __GFP_HIGH 0x02 #define __GFP_IO 0x04 #define __GFP_DMA 0x08 #ifdef CONFIG_HIGHMEM #define __GFP_HIGHMEM 0x10 #else #define __GFP_HIGHMEM 0x0 /* noop */ #endif
_GFP_WAIT 表示要等待分配完成。 order==0 表示分配的是单个页面 !(current->falgs & PF_MEMALLOC) 表示当前进程不是‘内存分配的管理进程’ 满足上3个条件就允许从相应管理区“不活跃干净页面”的缓冲队列中回收, 设置标记direct_reclaim=1。
3) 当内核缺少页面,并且 inactive_shortage 不活跃的页面的缺口也不多时候,启动 wakeup_kswapd,帮助交换一些页面出去。
4) 当内核缺少页面,并且 nr_inactive_dirty_pages 脏页很多时候,启动 wakeup_bdflush, 帮助刷页面。
从free pages较多的zone中分配
zone = zonelist->zones; for (;;) { zone_t *z = *(zone++); if (!z) break; if (!z->size) BUG(); if (z->free_pages >= z->pages_low) { page = rmqueue(z, order); if (page) return page; } else if (z->free_pages < z->pages_min && waitqueue_active(&kreclaimd_wait)) { wake_up_interruptible(&kreclaimd_wait); } }
1) 遍历当前分配测率中的zone。
2) 如果zone的 free_pages 高于pages_low水位线就从这个zone中分配。
但是不一定保证分配成功,因为申请的是连续的2^order物理页。 free_pages高于low水位线不代表就一定有这么多的连续的物理页。
3) 如果 free_pages 低于pages_min水位线,而且有进程在 kreclaimd_wait队列上等待回收页面,就唤醒这个进程,让它开始干活。
从一个zone中分配连续的物理页面rmqueue
static struct page * rmqueue(zone_t *zone, unsigned long order) { free_area_t * area = zone->free_area + order; unsigned long curr_order = order; struct list_head *head, *curr; unsigned long flags; struct page *page; spin_lock_irqsave(&zone->lock, flags); do { head = &area->free_list; curr = memlist_next(head); if (curr != head) { unsigned int index; page = memlist_entry(curr, struct page, list); if (BAD_RANGE(zone,page)) BUG(); memlist_del(curr); index = (page - mem_map) - zone->offset; MARK_USED(index, curr_order, area); zone->free_pages -= 1 << order; page = expand(zone, page, index, order, curr_order, area); spin_unlock_irqrestore(&zone->lock, flags); set_page_count(page, 1); if (BAD_RANGE(zone,page)) BUG(); DEBUG_ADD_PAGE return page; } curr_order++; area++; } while (curr_order < MAX_ORDER); spin_unlock_irqrestore(&zone->lock, flags); return NULL; }
1) 这个函数很重要,是buddy算法和位图的实现。
2) zone->free_area + order是从order的链表开始寻找空闲页面。
3) 进入循环之前对zone->lock上锁,因为别的进程有可能也正在这个zone上分配内存。
4) head = &area->free_list;
free_list 是一个双向队列,把2^order大小的页面串了起来,现在从头部取出一个。
5) page = memlist_entry(curr, struct page, list);
memlist_entry 是在从cur指针,page结构体,以及list在page结构体中的偏移,找到 *page
6) index = (page - mem_map) - zone->offset;
计算出这个空闲的page在mem_map中的偏移量。设置pgd,pte使用的是一个页面在mem_map的偏移量。
mem_map是指向全局的page大数组的起始地址,而从free_list双向链表通过memlist_entry获取的是指向一个page的指针。这个指针的值就是在mem_map数组中的。
为什么要减去zone->offset? zone->offset 是zone所管理的内存区域在mem_map起始偏移量,所以,这个index是在zone的偏移量。
MARK_USED更新位图,位图是为了在整理buddy时方便把连续的内存结合从更大order时用。参考ULK。
7) zone->free_pages -= 1 << order;
更新当前zone下的空闲内存。
8) expand 整理高order内存。比如,申请4个页面,而只有16个页面的话,就这个16页面拆开。
free_pages不够
page = __alloc_pages_limit(zonelist, order, PAGES_HIGH, direct_reclaim); page = __alloc_pages_limit(zonelist, order, PAGES_LOW, direct_reclaim); wakeup_kswapd(0); if (gfp_mask & __GFP_WAIT) { __set_current_state(TASK_RUNNING); current->policy |= SCHED_YIELD; schedule(); } page = __alloc_pages_limit(zonelist, order, PAGES_MIN, direct_reclaim);
1) 若果free_pages不够多,则把当前zone里面的inactive_clean_page也算上。
2) 尝试3询,每次都到一个zone里,看看这个zone的free_pages+inactive_clean_pages是否高于3个级别的水位线。
3) 第3次查询,说明内存已经很吃紧,唤醒kswapd。把当前进程放弃执行权,让给kswapd。好处有4个。
主动冲洗脏页
if (!(current->flags & PF_MEMALLOC)) { if (order > 0 && (gfp_mask & __GFP_WAIT)) { zone = zonelist->zones; current->flags |= PF_MEMALLOC; page_launder(gfp_mask, 1); current->flags &= ~PF_MEMALLOC; for (;;) { zone_t *z = *(zone++); if (!z) break; if (!z->size) continue; while (z->inactive_clean_pages) { struct page * page; page = reclaim_page(z); if (!page) break; __free_page(page); page = rmqueue(z, order); if (page) return page; } } } if ((gfp_mask & (__GFP_WAIT|__GFP_IO)) == (__GFP_WAIT|__GFP_IO)) { wakeup_kswapd(1); memory_pressure++; if (!order) goto try_again; } else if (gfp_mask & __GFP_WAIT) { try_to_free_pages(gfp_mask); memory_pressure++; if (!order) goto try_again; } }
0) 首先要看当前进程是不是一个‘内存的管理者。防止进入死递归。
1) 不是,如果仍然找不到,并且是大页面(order>0),而且设置了__GFP_WAIT,就开始调用page_launder,刷洗脏页面,从inactive_clean_list回收
2) 是,不能再次调用kswapd,因为当前这个进程是内存的管理者,就是因为某个时机内存不够用被唤醒了。说明这个进程在管理内存(比如kswapd,flushd)时候出现了内存吃紧。不能再调用kswapd。
解决方法是:直接调用kswapd对应的逻辑函数。
最后一招放宽水位线
zone = zonelist->zones; for (;;) { zone_t *z = *(zone++); struct page * page = NULL; if (!z) break; if (!z->size) BUG(); if (direct_reclaim) { page = reclaim_page(z); if (page) return page; } if (z->free_pages < z->pages_min / 4 && !(current->flags & PF_MEMALLOC)) continue; page = rmqueue(z, order); if (page) return page; } printk(KERN_ERR "__alloc_pages: %lu-order allocation failed.\n", order); return NULL;
0) 如果 direct_reclaim
1) if (z->free_pages < z->pages_min / 4 && !(current->flags & PF_MEMALLOC)) continue;
如果进程不是 PF_MEMALLOC进程,直接continue。
只有管理进程才会使用最后一点‘血本’。