14.分配页之主要步骤(__alloc_pages详解)

如前所述,__alloc_pages是伙伴系统的主函数。我们已经处理了所有的准备工作并描述了所有可能的标志,现在我们把注意力转向相对复杂的部分:该函数的实现,这也是内核中比较冗长的部分之一。特别是在可用内存太少或逐渐用完时,函数就会比较复杂。如果可用内存足够,则必要的工作会很快完成,就像下述代码。
 
mm/page_alloc.c
struct page * fastcall __alloc_pages(gfp_t gfp_mask, unsigned int order,struct zonelist *zonelist)
{
  const gfp_t wait = gfp_mask & __GFP_WAIT;
  struct zone **z;
  struct page *page;
  struct reclaim_state reclaim_state;
  struct task_struct *p = current;
  int do_retry;
  int alloc_flags;
  int did_some_progress;
  might_sleep_if(wait);
restart:
  z = zonelist->zones; /* 适合于gfp_mask的内存域列表 */
  if (unlikely(*z == NULL)) {
    /*
    *如果在没有内存的结点上使用GFP_THISNODE,导致zonelist为空,就会发生这种情况
    */
    return NULL;
  }
  page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, order,zonelist, ALLOC_WMARK_LOW|ALLOC_CPUSET);//尝试在进程允许运行的CPU集合中查找
  if (page)
    goto got_pg;
...

 

在最简单的情形中,分配空闲内存区只涉及调用一次get_page_from_freelist,然后返回所需数目的页(由标号got_pg处的代码处理)。
第一次内存分配尝试不会特别积极。如果在某个内存域中无法找到空闲内存,则意味着内存没剩下多少了,内核需要增加较多的工作量才能找到更多内存(“重型武器”稍后才会出现)。
mm/page_alloc.c
...
  for (z = zonelist->zones; *z; z++)
    wakeup_kswapd(*z, order);
    alloc_flags = ALLOC_WMARK_MIN;
    if ((unlikely(rt_task(p)) && !in_interrupt()) || !wait)
      alloc_flags |= ALLOC_HARDER;
    if (gfp_mask & __GFP_HIGH)
      alloc_flags |= ALLOC_HIGH;
    if (wait)
      alloc_flags |= ALLOC_CPUSET;
    page = get_page_from_freelist(gfp_mask, order, zonelist, alloc_flags);
    if (page)
      goto got_pg;
...
}

 

内核再次遍历备用列表中的所有内存域,每次都调用wakeup_kswapd。读者在这里需要注意的是,空闲内存可以通过缩减内核缓存和页面回收获得,即写回或换出很少使用的页。这两种措施都是由该守护进程发起的。
在交换守护进程唤醒后,内核开始新的尝试,在内存域之一查找适当的内存块。这一次进行的搜索更为积极,对分配标志进行了调整,修改为一些在当前特定情况下更有可能分配成功的标志。同时,将水印降低到最小值。对实时进程和指定了__GFP_WAIT标志因而不能睡眠的调用,会设置ALLOC_HARDER。然后用修改的标志集,再一次调用get_page_from_freelist,试图获得所需的页。
 
如果再次失败,内核会借助于更强有力的措施:
mm/page_alloc.c
rebalance:
  if (((p->flags & PF_MEMALLOC) || unlikely(test_thread_flag(TIF_MEMDIE)))&& !in_interrupt()) {
    if (!(gfp_mask & __GFP_NOMEMALLOC)) {
nofail_alloc:
      /* 再一次遍历zonelist,忽略水印 */
      page = get_page_from_freelist(gfp_mask, order,zonelist, ALLOC_NO_WATERMARKS);
      if (page)
        goto got_pg;
      if (gfp_mask & __GFP_NOFAIL) {
        congestion_wait(WRITE, HZ/50);
        goto nofail_alloc;
      }
    }
  goto nopage;
}

 

...
如果设置了PF_MEMALLOC或进程设置了TIF_MEMDIE标志(在这两种情况下,内核不能处于中断上下文中),会再次调用get_page_from_freelist试图获得所需的页。但这次会完全忽略水印,因为设置了ALLOC_NO_WATERMARKS。通常只有在分配器自身需要更多内存时,才会设置PF_MEMALLOC,而只有在线程刚好被OOM killer机制选中时,才会设置TIF_MEMDIE。
在这里搜索可能因为两个原因结束。
 
(1) 设置了__GFP_NOMEMALLOC。该标志禁止使用紧急分配链表(如果忽略水印,这可能是最佳途径),因此无法在禁用水印的情况下调用get_page_from_freelist。在这种情况下内核最终只能失败,跳转到noopage标号,通过内核消息将失败报告给用户,并将NULL指针返回调用者。
 
(2) 在忽略水印的情况下,get_page_from_freelist仍然失败了。在这种情况下,也会放弃搜索,报告错误消息。但如果设置了__GFP_NOFAIL,内核会进入无限循环(通过跳转到nofail_alloc标号实现)首先等待(通过congestion_wait)块设备层结束“占线”,在回收页时可能出现这种情况(参见第18章)。接下来再次尝试分配,直至成功。
 
如果没有设置PF_MEMALLOC,内核仍然还有一些选项可以尝试,但这些都需要睡眠。从这里内核进入了一条低速路径(slow path),其中会开始一些耗时的操作。前提是分配掩码中设置了__GFP_WAIT标志,因为随后的操作可能使进程睡眠。
mm/page_alloc.c
  /* 原子分配:我们无法进行“均衡” */
  if (!wait)
    goto nopage;
  cond_schedule();
...

 

回想一下,我们知道如果设置了相应的比特位,那么wait是1,否则是0。如果没有设置该标志,在这里会放弃分配尝试。在做进一步的尝试之前,内核通过cond_resched提供了重调度的时机。这防止了花费过多时间搜索内存,以致于使其他进程处于饥饿状态。
分页机制提供了一个目前尚未使用的选项,将很少使用的页换出到块介质,以便在物理内存中产生更多空间。但该选项非常耗时,还可能导致进程睡眠状态。try_to_free_pages是相应的辅助函数,用于查找当前不急需的页,以便换出。在该分配任务设置了PF_MEMALLOC标志之后,会调用该函数,用于向其余的内核代码表明所有后续的内存分配都需要这样的搜索。
mm/page_alloc.c
  /* 我们现在进入同步回收状态 */
  p->flags |= PF_MEMALLOC;
...
  did_some_progress = try_to_free_pages(zonelist->zones, order, gfp_mask);
...
  p->flags &= ~PF_MEMALLOC;
  cond_resched();
...

 

该调用被设置/清除PF_MEMALLOC标志的代码间隔起来。try_to_free_pages自身可能也需要分配新的内存。由于为获得新内存还需要额外分配一点内存(相当矛盾的情形),该进程当然应该在内
存管理方面享有最高优先级,上述标志的设置即达到了这一目的。
回忆前几行代码,在设置了PF_MEMALLOC标志时,内存分配非常积极
try_to_free_pages非常复杂。目前,只要知道该函数选择最近不十分活跃的页,将其写到交换区,在物理内存中腾出空间,即可。try_to_free_pages会返回增加的空闲页数目。
 
接下来,如果try_to_free_pages释放了一些页,那么内核再次调用get_page_from_freelist尝试分配内存:
mm/page_alloc.c
  if (likely(did_some_progress)) {
    page = get_page_from_freelist(gfp_mask, order,zonelist, alloc_flags);
    if (page)
      goto got_pg;
  } else if ((gfp_mask & __GFP_FS) && !(gfp_mask & __GFP_NORETRY)) {
...

 

如果内核可能执行影响VFS层的调用而又没有设置GFP_NORETRY,那么调用OOM killer(OOM是out of memory的缩写):
mm/page_alloc.c
  /* OOM killer无助于高阶分配,因此失败 */
  if (order > PAGE_ALLOC_COSTLY_ORDER) {
    clear_zonelist_oom(zonelist);
    goto nopage;
  }
  out_of_memory(zonelist, gfp_mask, order);
  goto restart;
}

 

读者请注意,该函数选择一个内核认为犯有分配过多内存“罪行”的进程,并杀死该进程。这有很大几率腾出较多的空闲页,然后跳转到标号restart,重试分配内存的操作。但杀死一个进程未必立即出现多于2 PAGE_COSTLY_ORDER页的连续内存区(其中PAGE_COSTLY_ORDER_PAGES通常设置为3),因此如果当前要分配如此大的内存区,那么内核会饶恕所选择的进程,不执行杀死进程的任务,而是承认失败并跳转到nopage。
 
如果设置了__GFP_NORETRY,或内核不允许使用可能影响VFS层的操作,那么会发生什么?在这种情况下,会判断所需分配的长度,作出不同的决定:
mm/page_alloc.c
...
  do_retry = 0;
  if (!(gfp_mask & __GFP_NORETRY)) {
    if ((order <= PAGE_ALLOC_COSTLY_ORDER) ||(gfp_mask & __GFP_REPEAT))
      do_retry = 1;
    if (gfp_mask & __GFP_NOFAIL)
      do_retry = 1;
  }
  if (do_retry) {
    congestion_wait(WRITE, HZ/50);
    goto rebalance;
  }
nopage:
  if (!(gfp_mask & __GFP_NOWARN) && printk_ratelimit()) {
    printk(KERN_WARNING "%s: page allocation failure."" order:%d, mode:0x%x\n"p->comm, order, gfp_mask);
    dump_stack();
    show_mem();
  }  
got_pg:
  return page;
}

 

如果分配长度小于2 PAGE_ALLOC_COSTLY_ORDER=8页,或设置了__GFP_REPEAT标志,则内核进入无限循环。在这两种情况下,是不能设置GFP_NORETRY的。因为如果调用者不打算重试,那么进入无限循环重试
并没有意义。内核会跳转回rebalance标号,即低速路径的入口,并一直等待,直至找到适当大小的内存块——根据所要分配的内存大小,内核可以假定该无限循环不会持续太长时间。内核在跳转之前会调用congestion_wait,等待块设备层队列释放,这样内核就有机会换出页。在所要求的分配阶大于3但设置了__GFP_NOFAIL标志的情况下,内核也会进入上述无限循环,因为该标志无论如何都不允许失败。
如果情况不是这样,内核只能放弃,并向用户返回NULL指针,并输出一条内存请求无法满足的警告消息。
posted @ 2022-03-20 20:04  while(true);;  阅读(757)  评论(0)    收藏  举报