电子说
分配页帧
分配页帧的具体实现
释放页帧
分配页帧
内核中使用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条评论
快来发表一下你的评论吧 !