电子说
分配页帧
分配页帧的具体实现
释放页帧
分配页帧
内核中使用ZONE分配器满足内存分配请求。该分配器必须具有足够的空闲页帧,以便满足各种内存大小请求。为此,ZONE分配器必须能够:
它应该保护预留页帧池;
当内存不足并且允许阻塞当前进程时,能够触发页帧回收机制。一旦某些页帧被释放,ZONE分配器重新分配;
尽可能保留小的、珍贵的ZONE_DMA内存区。如果请求正常内存或高端内存,ZONE分配器不太可能分配ZONE_DMA内存区中的页帧。
对于每次连续页帧的申请,ZONE页帧分配器调用alloc_pages()宏实现。该宏其实是__alloc_pages()的封装,而该函数才是ZONE分配器的核心。它需要三个参数:
gfp_mask
内存分配请求中指定的标志。
order
连续物理页帧的对数。
zonelist
指向zonelist数据结构,按照优先顺序,选择适合内存分配的内存区。
__alloc_pages()扫描zonelist数据结构中每一个内存区,代码大概如下所示:
for (i = 0; (z=zonelist->zones[i]) != NULL; i++) {
if (zone_watermark_ok(z, order, ...)) {
page = buffered_rmqueue(z, order, gfp_mask);
if (page)
return page;
}
}
对于每个内存区域,该函数将空闲页帧的数量与一个阈值进行比较,该阈值取决于内存分配标志、当前进程的类型以及该函数已经检查该区域的次数。实际上,如果可用内存很少,通常会对每个内存区域扫描几次,每次都对分配所需的最小可用内存设置较低的阈值。因此,前面的代码块在__alloc_pages()函数的主体中被复用了几次(只有很小的变化)。buffered_rmqueue()函数已经在前面的“CPU页帧缓存”一节中描述过了:它返回第一个分配的页帧的页描述符,如果内存区域不包含一组请求大小的连续页帧,则返回NULL。
zone_watermark_ok()辅助函数接收几个参数,这些参数决定内存ZONE中可用页帧数量的阈值min。特别是,如果满足以下两个条件,该函数返回值1,也就是具有足够的内存:
/*
* 如果空闲页帧在阈值之上,则返回1.考虑分配的大小(order密数决定)
*/
int zone_watermark_ok(struct zone *z, int order, unsigned long mark,
int classzone_idx, int can_try_harder, int gfp_high)
{
/* free_pages可能会变成负值,但是没有关系 */
long min = mark, free_pages = z->free_pages - (1 << order) + 1;
int o;
/* 如果设置了gfp_high标志,则阈值再减少1/2 */
if (gfp_high)
min -= min / 2;
/* 如果设置了can_try_harder标志,则阈值再减少1/4 */
if (can_try_harder)
min -= min / 4;
/* 除了要分配的页帧之外,该内存`ZONE`还至少包含min个页帧,
* 但是,不包含预留的页帧。
*(ZONE描述符的`low-on-memory`字段表示)。
*/
if (free_pages <= min + z->lowmem_reserve[classzone_idx])
return 0;
/* 除了要分配的页帧,
* 在`1`到`order`之间的空闲页帧列表中的每一个`k`,
* 至少有`min/(2^k)`个空闲页帧。
* 因此,如果`order`大于0,在大小为`2`的内存块列表中,
* 至少有`min/2`个空闲页帧;
* 如果`order`大于0,在大小为`4`的内存块列表中,
* 至少有`min/4`个空闲页帧;以此类推。
*/
for (o = 0; o < order; o++) {
/* At the next order, this order's pages become unavailable */
free_pages -= z->free_area[o].nr_free << o;
/* Require fewer higher order pages to be free */
min >>= 1;
if (free_pages <= min)
return 0;
}
return 1;
阈值min的值由zone_watermark_ok()确定,如下所示:
可以将pages_min,pages_low和pages_high三个内存ZONE区之一作为基本值作为函数的参数(参见本章前面的“预留页帧池”一节)。
如果设置了gfp_high标志,则将基值除以2。通常,如果在gfp_mask中设置了__GFP_HIGHMEM标志,也就是说,如果可以从高端内存中分配页帧的话,则该标志等于1。
如果设置了can_try_harder标志,则阈值将进一步减少四分之一。如果在gfp_mask中设置了__GFP_WAIT标志,或者当前进程是实时进程,并且内存分配是在进程上下文中完成的(在中断处理程序和可延迟函数之外),则该标志通常等于1。
分配页帧的具体实现
__alloc_pages()函数主要执行以下步骤:
struct page * fastcall
__alloc_pages(unsigned int gfp_mask, unsigned int order,
struct zonelist *zonelist)
{
// ...省略
/* 如果调用方不能运行直接回收算法,
* 或者调用方具有实时调度策略,
* 则调用方可能会更多地使用预留页帧
*/
can_try_harder = (unlikely(rt_task(p)) && !in_interrupt()) || !wait;
zones = zonelist->zones; /* 内存ZONE列表 */
if (unlikely(zones[0] == NULL)) {
return NULL; /* 这应该发生吗? */
}
classzone_idx = zone_idx(zones[0]);
restart:
/* 1. 执行内存区域的第一次扫描。
* 在第一次扫描中,min阈值设置为z->pages_low,
* 其中z指向正在分析的zone描述符
* (can_try_harder和gfp_high参数设置为零)。
*/
for (i = 0; (z = zones[i]) != NULL; i++) {
if (!zone_watermark_ok(z, order, z->pages_low,
classzone_idx, 0, 0))
continue;
page = buffered_rmqueue(z, order, gfp_mask);
if (page)
goto got_pg;
}
/* 2. 如果在前一步中没有终止,那么剩余的空闲内存就不多了;
* 应该唤醒kswapd内核线程,开始异步回收页帧。
*/
for (i = 0; (z = zones[i]) != NULL; i++)
wakeup_kswapd(z, order);
/* 3. 对内存区域执行第二次扫描:
* 将值z->pages_min作为基本阈值传递。
* 实际阈值还与can_try_harder和gfp_high标志有关。
* (允许内核和实时任务访问预留页帧池)
* 这一步几乎与步骤1相同,只是函数使用了较低的阈值。
*/
for (i = 0; (z = zones[i]) != NULL; i++) {
if (!zone_watermark_ok(z, order, z->pages_min,
classzone_idx, can_try_harder,
gfp_mask & __GFP_HIGH))
continue;
page = buffered_rmqueue(z, order, gfp_mask);
if (page)
goto got_pg;
}
/* 4. 执行第三次内存区域扫描:
* 如果前面没有分配到内存页帧,则说明系统内存应该非常低了。
* 如果内核代码不是中断处理程序或可延迟函数,
* 且它正在尝试回收页帧(设置了PF_MEMALLOC或PF_MEMDIE标志)。
* 此时应该进行第3次扫描。
* 此时应该忽略低内存阈值,即不调用zone_watermark_ok()。
* 这应该是耗尽低内存预留页帧的唯一情况
* (这些页帧由zone描述符的lowmem_reserve字段指定)。
* 在这种情况下,发送内存请求的内核代码最终通过尝试释放页帧,
* 获得它想要的内存请求。
* 如果没有内存ZONE包含足够的页帧,
* 则函数返回NULL,并通知调用者分配失败。
*/
if (((p->flags & PF_MEMALLOC) ||
unlikely(test_thread_flag(TIF_MEMDIE))) &&
!in_interrupt()) {
/* 再一次遍历zonelist,忽略min */
for (i = 0; (z = zones[i]) != NULL; i++) {
page = buffered_rmqueue(z, order, gfp_mask);
if (page)
goto got_pg;
}
goto nopage;
}
/* 5. 原子分配 - 这种情况我们不能做任何均衡处理
* 这种情况下,该函数返回NULL以通知内核代码内存分配失败:
* 这种情况下,没有办法在不阻塞当前进程的情况下满足请求。
*/
if (!wait)
goto nopage;
rebalance:
/* 6. 在这里,当前进程可以被阻塞:
* 调用cond_resched()来检查其他进程是否需要CPU。
*/
cond_resched();
/* 7. 设置当前的PF_MEMALLOC标志,
* 表示进程已准备好执行异步内存回收。
*/
p->flags |= PF_MEMALLOC;
/* 8. reclaim_state只包含一个字段reclaimed_slab,初始化为0 */
reclaim_state.reclaimed_slab = 0;
p->reclaim_state = &reclaim_state;
/* 9. 寻找一些要回收的页帧。
* 该函数可能会阻塞当前进程。
* 一旦该函数返回,重置当前的PF_MEMALLOC标志,
* 并再次调用cond_resched()。
*/
did_some_progress = try_to_free_pages(zones, gfp_mask, order);
p->reclaim_state = NULL;
p->flags &= ~PF_MEMALLOC;
cond_resched();
if (likely(did_some_progress)) {
/* 10. 说明前一步释放了一些页帧,
* 那么该函数将执行与步骤3中相同的另一次内存区域扫描。
* 如果内存分配请求不能被满足,
* zone_watermark_ok函数决定是否应该继续扫描内存区域。
* 这儿使用高阈值,仅是为了捕获并行的oom kill;
* (也就是说,如果内存压力还是很大,则应该失败)
*/
for (i = 0; (z = zones[i]) != NULL; i++) {
if (!zone_watermark_ok(z, order, z->pages_min,
classzone_idx, can_try_harder,
gfp_mask & __GFP_HIGH))
continue;
page = buffered_rmqueue(z, order, gfp_mask);
if (page)
goto got_pg;
}
}
/* 11. 如果在步骤9中没有释放页帧,那么内核就有大麻烦了,
* 因为可用内存非常低,无法回收任何页帧。
* 也许是时候做出一个关键的决定了:
* 如果此时设置了__GFP_FS标志,且清零了__GFP_NORETRY标志
* 如果内核控制路径允许执行与文件系统相关的操作来终止进程(gfp_mask中的' __GFP_FS '标志已设置),并且' __GFP_NORETRY '标志已清除,则执行以下子步骤:
*/
else if ((gfp_mask & __GFP_FS) && !(gfp_mask & __GFP_NORETRY)) {
/* 11.a zone_watermark_ok函数决定是否应该继续扫描内存区域。
* 这儿使用高阈值z->pages_high,仅是为了捕获并行的oom kill;
* (也就是说,如果内存压力还是很大,则应该失败)
*
* 因为该步使用的阈值比之前的都高,所以大概率会失败。
* 实际上,只有当内核的其他代码已经杀死了一个进程并回收内存后
* 该步才能成功。但是,这一步避免了杀死两个进程的情况。
*/
for (i = 0; (z = zones[i]) != NULL; i++) {
if (!zone_watermark_ok(z, order, z->pages_high,
classzone_idx, 0, 0))
continue;
page = buffered_rmqueue(z, order, gfp_mask);
if (page)
goto got_pg;
}
/* 11.b 杀死一些进程,释放内存 */
out_of_memory(gfp_mask);
/* 11.c 跳转回第1步 */
goto restart;
}
/* 如果__GFP_NORETRY标志是清除的,并且内存分配请求跨越最多8页帧
* 也就是说,尽量不要重复分配大于8个页帧以上的内存。
* 或者__GFP_REPEAT和__GFP_NOFAIL标志之一被设置,
* 函数调用blk_congestion_wait使进程休眠一段时间,
* 然后它跳回步骤6。
* 否则,该函数返回NULL以通知调用者内存分配失败。
*/
do_retry = 0;
if (!(gfp_mask & __GFP_NORETRY)) {
if ((order <= 3) || (gfp_mask & __GFP_REPEAT))
do_retry = 1;
if (gfp_mask & __GFP_NOFAIL)
do_retry = 1;
}
if (do_retry) {
blk_congestion_wait(WRITE, HZ/50);
goto rebalance;
}
nopage:
if (!(gfp_mask & __GFP_NOWARN) && printk_ratelimit()) {
// ...省略
}
return NULL;
got_pg:
zone_statistics(zonelist, z);
return page;
}
释放页帧
zone分配器还负责释放页帧,但要比分配页帧简单。
内核中,所有释放页帧的宏和函数,都是基于__free_pages()函数实现的。该函数的参数是page,待要释放的第一个页帧的页描述符的地址;order,要释放的连续页帧组的对数大小。函数执行以下步骤:
检查第1个页帧是否真的属于动态内存(它的PG_reserved标志被清除);如果不是,则终止。
减少page->_count使用计数器;如果仍然大于等于0,终止。
如果order等于零,该函数调用free_hot_page()将页帧释放到相应内存区域的CPU本地热缓存中。
如果order大于0,它将页帧添加到本地列表中,并调用free_pages_bulk()函数将它们释放到适当内存区域的buddy系统中。
全部0条评论
快来发表一下你的评论吧 !