分配页帧
分配页帧的具体实现
释放页帧
分配页帧
内核中使用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) returnpage; } }
对于每个内存区域,该函数将空闲页帧的数量与一个阈值进行比较,该阈值取决于内存分配标志、当前进程的类型以及该函数已经检查该区域的次数。实际上,如果可用内存很少,通常会对每个内存区域扫描几次,每次都对分配所需的最小可用内存设置较低的阈值。因此,前面的代码块在__alloc_pages()函数的主体中被复用了几次(只有很小的变化)。buffered_rmqueue()函数已经在前面的“CPU页帧缓存”一节中描述过了:它返回第一个分配的页帧的页描述符,如果内存区域不包含一组请求大小的连续页帧,则返回NULL。
zone_watermark_ok()辅助函数接收几个参数,这些参数决定内存ZONE中可用页帧数量的阈值min。特别是,如果满足以下两个条件,该函数返回值1,也就是具有足够的内存:
/* *如果空闲页帧在阈值之上,则返回1.考虑分配的大小(order密数决定) */ intzone_watermark_ok(structzone*z,intorder,unsignedlongmark, intclasszone_idx,intcan_try_harder,intgfp_high) { /*free_pages可能会变成负值,但是没有关系*/ longmin=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]) return0; /*除了要分配的页帧, *在`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()函数主要执行以下步骤:
structpage*fastcall __alloc_pages(unsignedintgfp_mask,unsignedintorder, structzonelist*zonelist) { //...省略 /*如果调用方不能运行直接回收算法, *或者调用方具有实时调度策略, *则调用方可能会更多地使用预留页帧 */ can_try_harder=(unlikely(rt_task(p))&&!in_interrupt())||!wait; zones=zonelist->zones;/*内存ZONE列表*/ if(unlikely(zones[0]==NULL)){ returnNULL;/*这应该发生吗?*/ } 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) gotogot_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) gotogot_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) gotogot_pg; } gotonopage; } /*5.原子分配-这种情况我们不能做任何均衡处理 *这种情况下,该函数返回NULL以通知内核代码内存分配失败: *这种情况下,没有办法在不阻塞当前进程的情况下满足请求。 */ if(!wait) gotonopage; 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) gotogot_pg; } } /*11.如果在步骤9中没有释放页帧,那么内核就有大麻烦了, *因为可用内存非常低,无法回收任何页帧。 *也许是时候做出一个关键的决定了: *如果此时设置了__GFP_FS标志,且清零了__GFP_NORETRY标志 *如果内核控制路径允许执行与文件系统相关的操作来终止进程(gfp_mask中的'__GFP_FS'标志已设置),并且'__GFP_NORETRY'标志已清除,则执行以下子步骤: */ elseif((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) gotogot_pg; } /*11.b杀死一些进程,释放内存*/ out_of_memory(gfp_mask); /*11.c跳转回第1步*/ gotorestart; } /*如果__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系统中。
-
内核
+关注
关注
3文章
1372浏览量
40282 -
cpu
+关注
关注
68文章
10855浏览量
211604 -
Linux
+关注
关注
87文章
11296浏览量
209353 -
分配器
+关注
关注
0文章
193浏览量
25747
原文标题:Linux内核8.6-内存管理之ZONE内存分配器
文章出处:【微信号:嵌入式ARM和Linux,微信公众号:嵌入式ARM和Linux】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论