内存规整揭秘:源码视角下的内存管理艺术

文摘   2024-10-17 15:23   陕西  

作者简介:高怡香,西安邮电大学研究生,师从陈莉君教授,专注于Linux内核及eBPF相关技术研究学习。

一、内存页面水位线

页面分配器按照zone的水位线来管理,zone的水位分为3个等级,分别是High、low、min。最低水位下的内存是系统预留的内存,通常情况下,普通优先级的请求是不能访问系统预留内存的,只有在特殊情况下,这些系统预留的内存才能被使用。如下表所示:

下面是zone水位管理的流程图:

二、从内存分配到内存规整

在前面的学习笔记中提到过,在慢速路径当中分配页面不成功(此时分配水位线为min)的情况下,会唤醒kswapd进行内存回收,但如果通过回收之后依然不能分配成功,会判断是否同时满足以下三个条件,考虑尝试先调用 __alloc_pages_direct_compact() 进行内存规整,以解决页面分配失败的问题。

  • 允许调用直接页面回收机制
  • 高成本的分配需求 costly_order,此时,系统可能有足够的空闲内存,但没有分配满足分配需求的连续页面,调用内存规整机制可能解决这个问题。或者对于请求,分配不可迁移的多个连续物理页面
  • 不能访问系统预留内存,gfp_pfmemalloc_allowed()表示是否允许访问系统预留内存。返回 0 则表示不允许访问预留内存,返回ALLOC_NO_WATERMARKS 表示不用考虑水位,访问全部的预留内存。

如下图所示,展示了从内存分配函数到直接内存规整的函数调用流程:

接下来分析__alloc_pages_direct_compact()函数

2.1 __alloc_pages_direct_compact()

该函数主要调用try_to_compact_pages()函数,进行内存规整,完成之后调用get_page_from_freelist()函数来尝试分配内存。该函数的主要工作如下:

  • 通过调用try_to_compact_pages(),遍历内存节点中的所有区域,并对每个区域进行内存规整,并使用compact_result 记录规整的结果
  • 如果内存规整成功,捕获到了可用的页面,则使用prep_new_page()函数来准备使用该页面
  • 如果通过内存规整没有成功获取页面,则调用get_page_from_freelist()从空闲链表获取页面

以下是该函数的源码注释:

/**
 * @brief 在低水位之下,alloc_pages() 进入慢速路径分配页面
 *
 * @param gfp_mask 调用页面分配器传递的分配掩码,描述页面分配方法的标志
 * @param order 请求分配页面的大小,order必须小于MAX_ORDER
 * @param alloc_flags 表示页面分配器内部使用的分配标志位
 * @param ac 表示页面分配器内部使用的分配上下文描述符
 * @param prio 内存规整优先级
 * @param compact_result 内存规整后返回的结果
 *
 * @return 成功返回分配的page,失败返回NULL
*/

static struct page *
__alloc_pages_direct_compact(gfp_t gfp_maskunsigned int order,
  unsigned int alloc_flagsconst struct alloc_context *ac,
  enum compact_priority prioenum compact_result *compact_result)
{

 struct page *page = NULL;
 unsigned long pflags;
 unsigned int noreclaim_flag;

 if (!order)
  return NULL;

 psi_memstall_enter(&pflags);
 noreclaim_flag = memalloc_noreclaim_save();
    
 //调用try_to_compact_pages(),遍历内存节点当中的所有zone,并对于每个 zone 进行内存规整
 *compact_result = try_to_compact_pages(gfp_mask, order, alloc_flags, ac,
        prio, &page);

 memalloc_noreclaim_restore(noreclaim_flag);
 psi_memstall_leave(&pflags);

 count_vm_event(COMPACTSTALL);

 /* 如果内存规整成功并且有可用的页面,准备使用此页面 */
 if (page)
  prep_new_page(page, order, gfp_mask, alloc_flags);

 /* 否则,调用get_page_from_freelist()来进行分配内存 */
 if (!page)
  page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);

 if (page) {
  struct zone *zone = page_zone(page);

  zone->compact_blockskip_flush = false;
  compaction_defer_reset(zone, order, true);
  count_vm_event(COMPACTSUCCESS);
  return page;
 }
 count_vm_event(COMPACTFAIL);

 cond_resched();

 return NULL;
}

2.2 try_to_compact_pages()

接下来分析内存规整函数中的关键函数try_to_compact_pages()函数,该函数是遍历内存节点中的所有zone,并对每个zone进行内存规整,并执行compact_zone_order()函数, 保存捕获的页面,返回内存规整的结果。

该函数的主要工作如下:

  • 通过调用for_each_zone_zonelist_nodemask()函数遍历内存节点中的各个 zone,尝试对每个 zone 进行内存规整。
  • 对每个zone,都会进行推迟规整检查,如果某个 zone 之前的内存规整被推迟,并且当前的优先级高于最小优先级,则跳过该 zone
  • 对当前 zone 调用 compact_zone_order(),尝试分配连续内存.
  • 对于规整的结果以及规整的类型,进行不同处理。

该函数的源码注释如下:

/**
 * try_to_compact_pages - 直接压缩以满足高阶分配
 * @gfp_mask: 传递给页面分配器的分配掩码
 * @order: 当前请求分配页面的大小
 * @alloc_flags: 页面分配器内部使用的分配标志位
 * @ac: 页面分配器内部使用的分配上下文描述符
 * @prio: 内存规整的优先级,决定了规整的程度
 * @capture: 成功规整后返回捕获的页面指针
 * @return 返回内存规整的结果,枚举值 compact_result 表示操作状态
 * 直接规整的关键点
 */

enum compact_result try_to_compact_pages(gfp_t gfp_mask, unsigned int order,
  unsigned int alloc_flags, const struct alloc_context *ac,
  enum compact_priority prio, struct page **capture)

{
 int may_perform_io = gfp_mask & __GFP_IO;
 struct zoneref *z;
 struct zone *zone;
 enum compact_result rc = COMPACT_SKIPPED;

 /*
  * 若为GFP_NOIO,直接内存规整会跳过
  */

 if (!may_perform_io)
  return COMPACT_SKIPPED;
 // 记录规整操作的跟踪信息
 trace_mm_compaction_try_to_compact_pages(order, gfp_mask, prio);

 /* 遍历指定的zonelist中的所有zone */
 for_each_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx,
        ac->nodemask) {
  enum compact_result status;
  // 检查是否应该推迟压缩。如果区域之前的压缩被推迟且当前优先级不是最低,则跳过该区域,继续检查下一个区域。
  if (prio > MIN_COMPACT_PRIORITY
     && compaction_deferred(zone, order)) {
   rc = max_t(enum compact_result, COMPACT_DEFERRED, rc);
   continue;
  }
  //在每个zone 中调用 compact_zone_order() 来进行内存规整,尝试获取高阶页面
  status = compact_zone_order(zone, order, gfp_mask, prio,
    alloc_flags, ac_classzone_idx(ac), capture);
  rc = max(status, rc);

  /* 如果规整成功,重置该区域的规整推迟标志并退出循环 */
  if (status == COMPACT_SUCCESS) {
   compaction_defer_reset(zone, order, false);

   break;
  }
  // 如果规整是同步的,并且状态是完全完成或部分跳过,推迟该区域的进一步规整
  if (prio != COMPACT_PRIO_ASYNC && (status == COMPACT_COMPLETE ||
     status == COMPACT_PARTIAL_SKIPPED))

   defer_compaction(zone, order);

  /*
   * 对于异步规整,如果需要重新调度或者有致命信号挂起,则终止规整尝试。
   */

  if ((prio == COMPACT_PRIO_ASYNC && need_resched())
     || fatal_signal_pending(current))
   break;
 }

 return rc;
}

2.3  compact_zone_order()

try_to_compact_pages()函数中的关键规整操作由compact_zone_order()函数来完成,该函数是在一个指定的zone中进行内存规整,保存内存规整中捕获的页面,并将内存规整的结果返回。该结果是一个枚举类型,具体在include/linux/compaction.h中定义。

该函数进行的主要工作如下:

  • 初始化 compact_control 结构体,明确了所需的连续页面大小,当前所要规整的区域等信息
  • 初始化 capture_control 结构体,用于在规整该区域的时候捕获所需的页面
  • 根据 compact_control结构体,去调用 compact_zone 函数,在当前区域内按照相关设置执行实际内存规整,并将捕获的页面保存到capture_control 结构体中
  • 返回捕获的页面以及规整的结果
/**
 * compact_zone_order - 初始化内部使用的compact_control数据结构,在调用compact_zone进行内存规整
 * @zone: 指向需要进行内存规整的zone的指针
 * @order: 当前请求分配页面的大小
 * @gfp_mask: 传递给页面分配器的分配掩码
 * @prio: 内存规整的优先级
 * @alloc_flags: 页面分配器内部使用的分配标志位
 * @classzone_idx: 页面分配器根据分配掩码计算出的首选zone的编号
 * @capture: 用于在内存规整过程中捕获一个页面,如果成功,它将指向被分配的页面
 * @return 返回规整的结果
 * 直接规整的关键点
 */

static enum compact_result compact_zone_order(struct zone *zone, int order,
  gfp_t gfp_mask, enum compact_priority prio,
  unsigned int alloc_flags, int classzone_idx,
  struct page **capture)

{
 enum compact_result ret;

 // 初始化 compact_control 结构,用于控制规整的参数
 struct compact_control cc = {
  .order = order,
  .search_order = order,
  .gfp_mask = gfp_mask,
  .zone = zone,
  .mode = (prio == COMPACT_PRIO_ASYNC) ?
     MIGRATE_ASYNC : MIGRATE_SYNC_LIGHT,
  .alloc_flags = alloc_flags,
  .classzone_idx = classzone_idx,
  //设置并检查有关区域内规整的选项
  .direct_compaction = true,
  .whole_zone = (prio == MIN_COMPACT_PRIORITY),
  .ignore_skip_hint = (prio == MIN_COMPACT_PRIORITY),
  .ignore_block_suitable = (prio == MIN_COMPACT_PRIORITY)
 };
 //填充capture_control变量,用于在内存规整中捕获页面
 struct capture_control capc = {
  .cc = &cc,
  .page = NULL,// 初始化时没有捕获页面
 };
 //将 capture_control 变量赋值给当前进程上下文
 if (capture)
  current->capture_control = &capc;

 //接着调用 compact_zone() 进行内存规整
 ret = compact_zone(&cc, &capc);

 // 使用断言来确保释放所有链表中的页面
 VM_BUG_ON(!list_empty(&cc.freepages));
 VM_BUG_ON(!list_empty(&cc.migratepages));

 // 如果有捕获的页面,通过 capture 返回
 *capture = capc.page;

 // 清除当前线程的 capture_control 设置
 current->capture_control = NULL;

 return ret;
}

三、内存碎片

内存碎片是由大量离散且不连续的空闲页面组成,在系统运行时间越长的情况下,内存碎片会越来越多,内存碎片化越严重,最直接的影响就是导致分配大块内存失败。而在内核中,内存页面可分为可回收、可移动和不可移动等类型,其中可移动页面:仅需要修改页面的映射关系,代价低,可回收页面指的是不可移动但可以释放的页面。

这两种类型的页面会通过改变页面的映射关系以及释放页面空间对内存碎片进行规整。经过上述分析可知,内存规整的核心函数为compact_zone,接下来分析该函数。

四、内存规整源码分析

4.1 compact_zone()

compact_zone()是内存规整的核心函数,主要用于“兵分两路”扫描一个 zone,一个从zone头部向尾部开始扫描,找出可以迁移的页面。另一个从zone的尾部向头部开始扫描,查找空闲页面。这两路“兵”会在 zone 的中间汇合,或者已经满足分配大块内存的需求(能满足分配的大块内存并且满足最低水位要求),就退出扫描,接着调用内存迁移的接口进行页面迁移,整理大块内存。

通过分析源码,总结 compact_zone()函数的核心工作如下:

  • 初始化链表 cc->freepagescc->migratepages,它们分别用于存储扫描到的空闲页面和待迁移的页面。
  • 调用compaction_suitable()函数,根据zone水位线判断当前zone是否需要进行内存规整。如果无需规整,则直接返回,否则执行下一步。
  • 根据cc->whole_zone来确定扫描的范围,如果设置该标志,则扫描整个区域,否则根据compact_cached_migrate_pfn[] zone->compact_cached_free_pfn[]成员记录的位置,确定扫描的范围
  • 在while循环当中,通过调用 compact_finished() 函数检查当前规整是否结束,在while循环中执行以下操作:
    • 如果成功隔离到页面 (ISOLATE_SUCCESS),继续迁移。
    • 如果隔离被中止 (ISOLATE_ABORT),释放已经隔离的页面并退出。
    • 通过调用isolate_migratepages() 执行迁移页面的隔离操作,并将可迁移页面加入 cc->migratepages 链表中。
    • 调用migrate_pages()来迁移页面,从cc->migratepages链表中获取已经隔离的页面,然后尝试迁移
    • 迁移成功之后,释放cc->freepages中的空闲页面并记录迁移事件

有关具体的迁移页面代码详见第五节,页面迁移源码分析。

/**
 * @brief 内存规整的核心函数,主要用于“兵分两路”扫描一个 zone
 * 一个从zone头部向尾部开始扫描,找出可以迁移的页面。另一个从zone的尾部向头部开始扫描,查找空闲页面。这两路“兵”会在 zone 的中间汇合,或者已经满足分配大块内存的需求,退出扫描
 * 然后调用页面迁移的接口函数进行页面迁移,最终整理出大块空闲页面
 *
 * @param cc 表示内存规整中内部使用的控制参数
 * @param capc  表示在内存规整过程中捕获的页面 
*/

static enum compact_result
compact_zone(struct compact_control *cc, struct capture_control *capc)
{
 enum compact_result ret;

 //zone的起始页帧和终止页帧
 unsigned long start_pfn = cc->zone->zone_start_pfn;
 unsigned long end_pfn = zone_end_pfn(cc->zone);

 unsigned long last_migrated_pfn;
 // 表示是否支持同步的迁移模式
 const bool sync = cc->mode != MIGRATE_ASYNC;
 bool update_cached;

 /*
  * These counters track activities during zone compaction.  Initialize
  * them before compacting a new zone.
  */

 cc->total_migrate_scanned = 0;
 cc->total_free_scanned = 0;
 cc->nr_migratepages = 0;
 cc->nr_freepages = 0;
    //初始化空闲页面和待迁移的页面链表
 INIT_LIST_HEAD(&cc->freepages);
 INIT_LIST_HEAD(&cc->migratepages);

 //gfpflags_to_migratetype() 从分配掩码当中来获取页面的迁移类型
 cc->migratetype = gfpflags_to_migratetype(cc->gfp_mask);

 //compaction_suitable() 函数根据当前zone的水位来判断是否要进行内存规整
 ret = compaction_suitable(cc->zone, cc->order, cc->alloc_flags,
       cc->classzone_idx);
 /* 无需规整 */
 if (ret == COMPACT_SUCCESS || ret == COMPACT_SKIPPED)
  return ret;

 /* huh, compaction_suitable is returning something unexpected */
 VM_BUG_ON(ret != COMPACT_CONTINUE);

 /*
  * Clear pageblock skip if there were failures recently and compaction
  * is about to be retried after being deferred.
  */

 if (compaction_restarting(cc->zone, cc->order))
  __reset_isolation_suitable(cc->zone);

 /*
  * Setup to move all movable pages to the end of the zone. Used cached
  * information on where the scanners should start (unless we explicitly
  * want to compact the whole zone), but check that it is initialised
  * by ensuring the values are within zone boundaries.
  */

 cc->fast_start_pfn = 0;
 
 // whole_zone表示要扫描整个zone,其中 free_pfn指的是 zone 最后一个页块的起始地址
 if (cc->whole_zone) {
  cc->migrate_pfn = start_pfn;
  cc->free_pfn = pageblock_start_pfn(end_pfn - 1);
 } else {

  //compact_cached_migrate_pfn[] 成员记录了上一次扫描中可迁移页面的位置,数组分别记录同步和异步模式。zone->compact_cached_free_pfn成员记录了上一次扫描中空闲页面的位置
  cc->migrate_pfn = cc->zone->compact_cached_migrate_pfn[sync];
  cc->free_pfn = cc->zone->compact_cached_free_pfn;

  if (cc->free_pfn < start_pfn || cc->free_pfn >= end_pfn) {
   cc->free_pfn = pageblock_start_pfn(end_pfn - 1);
   cc->zone->compact_cached_free_pfn = cc->free_pfn;
  }
  if (cc->migrate_pfn < start_pfn || cc->migrate_pfn >= end_pfn) {
   cc->migrate_pfn = start_pfn;
   cc->zone->compact_cached_migrate_pfn[0] = cc->migrate_pfn;
   cc->zone->compact_cached_migrate_pfn[1] = cc->migrate_pfn;
  }

  if (cc->migrate_pfn <= cc->zone->compact_init_migrate_pfn)
   cc->whole_zone = true;
 }

 last_migrated_pfn = 0;

 /*
  * Migrate has separate cached PFNs for ASYNC and SYNC* migration on
  * the basis that some migrations will fail in ASYNC mode. However,
  * if the cached PFNs match and pageblocks are skipped due to having
  * no isolation candidates, then the sync state does not matter.
  * Until a pageblock with isolation candidates is found, keep the
  * cached PFNs in sync to avoid revisiting the same blocks.
  */

 update_cached = !sync &&
  cc->zone->compact_cached_migrate_pfn[0] == cc->zone->compact_cached_migrate_pfn[1];
 
 trace_mm_compaction_begin(start_pfn, cc->migrate_pfn,
    cc->free_pfn, end_pfn, sync); //记录开始扫描的事件

 migrate_prep_local();
 //compact_finished() 函数会判断当前规整是否结束,即是否需要扫描,COMPACT_CONTINUE表示需要继续扫描下一个页块
 while ((ret = compact_finished(cc)) == COMPACT_CONTINUE) {
  int err;
  unsigned long start_pfn = cc->migrate_pfn;

  /*
   * Avoid multiple rescans which can happen if a page cannot be
   * isolated (dirty/writeback in async mode) or if the migrated
   * pages are being allocated before the pageblock is cleared.
   * The first rescan will capture the entire pageblock for
   * migration. If it fails, it'll be marked skip and scanning
   * will proceed as normal.
   */

  cc->rescan = false;
  if (pageblock_start_pfn(last_migrated_pfn) ==
      pageblock_start_pfn(start_pfn)) {
   cc->rescan = true;
  }

  //isolate_migratepages 是迁移页扫描器实现,用于查找需要移动的页,并将可移动的页面加入到cc->migratepages链表中

  switch (isolate_migratepages(cc)) {
  case ISOLATE_ABORT:
  //如果隔离过程被中止,通过 putback_movable_pages 将已经隔离的页面重新放回内存,再退出
   ret = COMPACT_CONTENDED;
   putback_movable_pages(&cc->migratepages);
   cc->nr_migratepages = 0;
   last_migrated_pfn = 0;
   goto out;
  case ISOLATE_NONE:
  //如果没有找到任何可以隔离的页面,则检查是否需要刷新之前的迁移操作,并跳转到 check_drain 来清理状态
   if (update_cached) {
    cc->zone->compact_cached_migrate_pfn[1] =
     cc->zone->compact_cached_migrate_pfn[0];
   }

   /*
    * We haven't isolated and migrated anything, but
    * there might still be unflushed migrations from
    * previous cc->order aligned block.
    */

   goto check_drain;
  case ISOLATE_SUCCESS:
  //如果隔离成功,更新缓存信息并继续页面迁移,在这里记录此次迁移的起始地址
   update_cached = false;
   last_migrated_pfn = start_pfn;
   ;
  }
  //migrate_pages()迁移页面,从cc->migratepages链表中获取页面,然后尝试迁移页面
  err = migrate_pages(&cc->migratepages, compaction_alloc,
    compaction_free, (unsigned long)cc, cc->mode,
    MR_COMPACTION);
  //记录迁移的事件
  trace_mm_compaction_migratepages(cc->nr_migratepages, err,
       &cc->migratepages);

  /* 所有的页要么被迁移要么被释放*/
  cc->nr_migratepages = 0;
  if (err) {
   //putback_movable_pages()把以及分离的页面重新添加到LRU链表中
   putback_movable_pages(&cc->migratepages);
   /*
    * 当扫描器之间已经相遇,或者内存不足,退出循环
    */

   if (err == -ENOMEM && !compact_scanners_met(cc)) {
    ret = COMPACT_CONTENDED;
    goto out;
   }
   /*
    * We failed to migrate at least one page in the current
    * order-aligned block, so skip the rest of it.
    */

   if (cc->direct_compaction &&
      (cc->mode == MIGRATE_ASYNC)) {
    cc->migrate_pfn = block_end_pfn(
      cc->migrate_pfn - 1, cc->order);
    /* Draining pcplists is useless in this case */
    last_migrated_pfn = 0;
   }
  }

check_drain:
  /*
   * Has the migration scanner moved away from the previous
   * cc->order aligned block where we migrated from? If yes,
   * flush the pages that were freed, so that they can merge and
   * compact_finished() can detect immediately if allocation
   * would succeed.
   */

  if (cc->order > 0 && last_migrated_pfn) {
   int cpu;
   unsigned long current_block_start =
    block_start_pfn(cc->migrate_pfn, cc->order);

   if (last_migrated_pfn < current_block_start) {
    cpu = get_cpu();
    lru_add_drain_cpu(cpu);
    drain_local_pages(cc->zone);
    put_cpu();
    /* No more flushing until we migrate again */
    last_migrated_pfn = 0;
   }
  }

  /* Stop if a page has been captured */
  if (capc && capc->page) {
   ret = COMPACT_SUCCESS;
   break;
  }
 }

out:
 /*
  * Release free pages and update where the free scanner should restart,
  * so we don't leave any returned pages behind in the next attempt.
  */

 if (cc->nr_freepages > 0) {
  unsigned long free_pfn = release_freepages(&cc->freepages);

  cc->nr_freepages = 0;
  VM_BUG_ON(free_pfn == 0);
  /* The cached pfn is always the first in a pageblock */
  free_pfn = pageblock_start_pfn(free_pfn);
  /*
   * Only go back, not forward. The cached pfn might have been
   * already reset to zone end in compact_finished()
   */

  if (free_pfn > cc->zone->compact_cached_free_pfn)
   cc->zone->compact_cached_free_pfn = free_pfn;
 }

 count_compact_events(COMPACTMIGRATE_SCANNED, cc->total_migrate_scanned);
 count_compact_events(COMPACTFREE_SCANNED, cc->total_free_scanned);

 trace_mm_compaction_end(start_pfn, cc->migrate_pfn,
    cc->free_pfn, end_pfn, sync, ret);

 return ret;
}

4.2 compact_suitable()

compact_zone()函数中,该函数是根据当前的zone水位判断是否需要进行内存规整,其内部是通过__compaction_suitable()函数实现。该函数的核心工作如下:

  • 调用__compaction_suitable()函数,根据zone的空闲页面数量,判断是否满足所需的内存分配要求
  • 根据上述函数的结果,如果不能判断是否进行内存规整且order大于PAGE_ALLOC_COSTLY_ORDER,则使用函数fragmentation_index()进行内存碎片化指数计算,来做进一步判断,当指数小于等于 sysctl_extfrag_threshold(系统设定的碎片化阈值,通常为 500)时,认为当前内存碎片化不严重,不需要内存规整。
/**
 * @brief 根据zone水位来判断是否需要进行内存规整
 *
 * @param zone 指的是待评估的zone
 * @param order 请求分配页面的大小,order必须小于MAX_ORDER
 * @param alloc_flags 表示页面分配器内部使用的分配标志位
 * @param classzone_idx 表示页面分配器根据分配掩码计算出来的首选zone
 * @return 返回内存规整的结果,具体结果取决于 zone 的当前状态
*/

enum compact_result compaction_suitable(struct zone *zone, int order,
     unsigned int alloc_flags,
     int classzone_idx)

{
 enum compact_result ret;
 int fragindex;
 //__compaction_suitable() 判断水线是否满足当前order阶内存分配要求
 ret = __compaction_suitable(zone, order, alloc_flags, classzone_idx,
        zone_page_state(zone, NR_FREE_PAGES));
 /*
  *extfrag_index可以在内核接口中查看,其值表明了内存分配失败的原因是因为缺少内存还是因为内存碎片化过高。
  当值为正并且越接近1表示因为内存碎片化程度越高,越低表明是内存不足,若为-1,则表明当前内存满足分配需求
  */


 //当__compaction_suitable() 返回的是COMPACT_CONTINUE 且  order>3
 if (ret == COMPACT_CONTINUE && (order > PAGE_ALLOC_COSTLY_ORDER)) {
  //进行反碎片化监测
  fragindex = fragmentation_index(zone, order);//内存碎片化指数,范围在0-1000。并填充了contig_page_info结构体。

  //若实际碎片化指数低于sysctl_extfrag_threshold(500),则认为内存压缩不适合当前区域。
  if (fragindex >= 0 && fragindex <= sysctl_extfrag_threshold)
   ret = COMPACT_NOT_SUITABLE_ZONE;
 }

 trace_mm_compaction_suitable(zone, order, ret);//用于记录和追踪适合内存规整的事件。

 //如果当前zone不适合规整,则跳过
 if (ret == COMPACT_NOT_SUITABLE_ZONE)
  ret = COMPACT_SKIPPED;

 return ret;
}

接下来分析__compaction_suitable() 函数相关源码分析以及主要工作介绍:

  • 使用 is_via_compact_memory() 检查是否由于高阶内存需求导致的内存规整,如果是则返回 COMPACT_CONTINUE
  • 使用 wmark_pages() 函数及 zone_watermark_ok() 函数计算并检查当前 zone 是否已经达到所需的水位线。并根据order调整水位线
  • 检查是否有足够的单页支持规整操作过程的迁移
/**
 * @brief 判断该zone中是否适合内存规整
 *
 * @param zone 指的是待评估的zone
 * @param order 请求分配页面的大小,order必须小于MAX_ORDER
 * @param alloc_flags 表示页面分配器内部使用的分配标志位
 * @param classzone_idx 表示页面分配器根据分配掩码计算出来的首选zone
 * @param wmark_target 目标水位线
*/

static enum compact_result __compaction_suitable(struct zone *zone, int order,
     unsigned int alloc_flags,
     int classzone_idx,
     unsigned long wmark_target)
{
 unsigned long watermark;
 // 检查是否由于高阶内存需求导致必须进行内存规整,如果是,则在下个页块就可以进行内存规整
 if (is_via_compact_memory(order))
  return COMPACT_CONTINUE;

 watermark = wmark_pages(zone, alloc_flags & ALLOC_WMARK_MASK);//计算基于当前分配标志的水位线
 /*
  * zone_watermark_ok() 检查当前区域是否已满足所需的水位线,满足的话,则无需进行规整
  */

 if (zone_watermark_ok(zone, order, watermark, classzone_idx,
        alloc_flags))
  return COMPACT_SUCCESS;


 //当order>3,认为分配成本高,则使用 low 水位为参考,否则以 min 水位为参考
 watermark = (order > PAGE_ALLOC_COSTLY_ORDER) ?
    low_wmark_pages(zone) : min_wmark_pages(zone);
 watermark += compact_gap(order);
 
 //接下来以order = 0来监测该zone中的水位线是否在watermark上,若不在,则表明zone中只有很少的空闲页面,不适合做内存规整,跳过该zone
 //为什么只需要判断0阶?
 // 因为内存规整过程中需要order阶空闲页用于内存迁移。因此第二次水线的判断,仅对0阶内存判断,因为迁移过程中申请空闲页都是单页,确定是否有足够的空闲内存支持迁移过程
 if (!__zone_watermark_ok(zone, 0, watermark, classzone_idx,
      ALLOC_CMA, wmark_target))
  return COMPACT_SKIPPED;
 //否则可以进行内存规整
 return COMPACT_CONTINUE;
}

4.3 compact_finished()

compact_finished()函数是用于判断当前规整是否结束,其内部通过__compact_finished()即是否结束扫描。如果不结束扫描,即需要继续内存规整,则需要去扫描整个zone。

static enum compact_result compact_finished(struct compact_control *cc)
{
 int ret;

 ret = __compact_finished(cc);
 trace_mm_compaction_finished(cc->zone, cc->order, ret);
 if (ret == COMPACT_NO_SUITABLE_PAGE)
  ret = COMPACT_CONTINUE;

 return ret;
}

接下来分析一下__compact_finished()函数的相关工作和源码:

  • 通过调用 compact_scanners_met()函数检查迁移和空闲页块的扫描器是否已经相遇。相遇,则代表扫描结束,内存规整完成。并根据扫描的范围等返回结果
  • 通过is_via_compact_memory()函数判断是否由于高阶内存分配导致内存规整,如果是,则返回 COMPACT_CONTINUE
  • 遍历order,检查当前 order 对应迁移类型的空闲链表是否有可用的页块。如果找到,返回 COMPACT_SUCCESS
  • 如果当前迁移类型没有合适的空闲页块,使用 find_suitable_fallback() 函数从其他迁移类型偷取页块,偷取成功,返回 COMPACT_SUCCESS

函数源码注解如下:

/**
 * @brief 判断内存规整是否完成
 * @param cc compaction_control 结构体,管理内存规整的控制信息
 * @return 返回内存规整的结果状态
 */

static enum compact_result __compact_finished(struct compact_control *cc)
{
 unsigned int order;
 const int migratetype = cc->migratetype;
 int ret;

 /*如果迁移和空闲页块扫描器相遇,说明内存规整完成 */
 if (compact_scanners_met(cc)) {
  /* 重置缓存位置,为下一次内存规整做准备 */
  reset_cached_positions(cc->zone);

  /*
   * Mark that the PG_migrate_skip information should be cleared
   * by kswapd when it goes to sleep. kcompactd does not set the
   * flag itself as the decision to be clear should be directly
   * based on an allocation request.
   */

  if (cc->direct_compaction)
   cc->zone->compact_blockskip_flush = true;
  //如果整个 zone 都已经扫描过,返回 COMPACT_COMPLETE,否则返回 COMPACT_PARTIAL_SKIPPED
  if (cc->whole_zone)
   return COMPACT_COMPLETE;
  else
   return COMPACT_PARTIAL_SKIPPED;
 }
 //如果是通过高阶内存请求触发的内存规整,继续进行 
 if (is_via_compact_memory(cc->order))
  return COMPACT_CONTINUE;

 /*
  * Always finish scanning a pageblock to reduce the possibility of
  * fallbacks in the future. This is particularly important when
  * migration source is unmovable/reclaimable but it's not worth
  * special casing.
  */

 if (!IS_ALIGNED(cc->migrate_pfn, pageblock_nr_pages))
  return COMPACT_CONTINUE;

 /* 是否有合适的空闲页面? */
 ret = COMPACT_NO_SUITABLE_PAGE;

 // 判断zone 里面order 对应的迁移类型的空闲链表是否存在可用于迁移的页面,通过order++逐步检查高阶内存
 for (order = cc->order; order < MAX_ORDER; order++) {
  struct free_area *area = &cc->zone->free_area[order];
  bool can_steal;

  /* 如果对应的迁移类型有空闲的页块,则内存规整可以结束 */
  if (!free_area_empty(area, migratetype))
   return COMPACT_SUCCESS;

#ifdef CONFIG_CMA
  /* MIGRATE_MOVABLE can fallback on MIGRATE_CMA */
  if (migratetype == MIGRATE_MOVABLE &&
   !free_area_empty(area, MIGRATE_CMA))
   return COMPACT_SUCCESS;
#endif
  /*
   * 如果对应的迁移类型没有空闲的页块,则需要去从其他的迁移类型中偷取空闲页块,具体从哪些迁移类型可以偷取,需要看fallbacks[]定义
   */

  if (find_suitable_fallback(area, order, migratetype,
      true, &can_steal) != -1) {

   /* movable pages are OK in any pageblock */
   if (migratetype == MIGRATE_MOVABLE)
    return COMPACT_SUCCESS;

   /*
    * We are stealing for a non-movable allocation. Make
    * sure we finish compacting the current pageblock
    * first so it is as free as possible and we won't
    * have to steal another one soon. This only applies
    * to sync compaction, as async compaction operates
    * on pageblocks of the same migratetype.
    */

   if (cc->mode == MIGRATE_ASYNC ||
     IS_ALIGNED(cc->migrate_pfn,
       pageblock_nr_pages)) {
    return COMPACT_SUCCESS;
   }

   ret = COMPACT_CONTINUE;
   break;
  }
 }

 if (cc->contended || fatal_signal_pending(current))
  ret = COMPACT_CONTENDED;

 return ret;
}

4.4 isolate_migratepages()

该函数的核心工作如下:

  • 通过 fast_find_migrateblock() 确定起始的页帧号 low_pfn,判断是从上次扫描到的位置(cc->migrate_pfn)继续开始还是从头开始
  • 循环遍历 zone 中的页块,步长为 pageblock_nr_pages
  • 使用 isolate_migratepages_block() 函数对页块中的页面进行隔离。如果隔离失败(返回 0),则返回 ISOLATE_ABORT,表示中止操作。
  • 成功隔离页面后,更新 cc->migrate_pfn,记录本次扫描结束的位置,以便下次扫描从该位置继续。并返回隔离结果。

该函数的源码注解如下:

/**
 * isolate_migratepages - 扫描并且寻觅zone中可迁移的页面,把可迁移的页面添加到 cc->migratepages链表中
 * @cc: 表示内存规整内部使用的控制参数
 * 扫描的步长是页块,pageblock
 */

static isolate_migrate_t isolate_migratepages(struct compact_control *cc)
{
 unsigned long block_start_pfn;
 unsigned long block_end_pfn;
 unsigned long low_pfn;
 struct page *page;

 //表示分离模式,判断是否支持异步分离模式
 const isolate_mode_t isolate_mode =
  (sysctl_compact_unevictable_allowed ? ISOLATE_UNEVICTABLE : 0) |
  (cc->mode != MIGRATE_SYNC ? ISOLATE_ASYNC_MIGRATE : 0);
 bool fast_find_block;

 /*
  * Start at where we last stopped, or beginning of the zone as
  * initialized by compact_zone(). The first failure will use
  * the lowest PFN as the starting point for linear scanning.
  */


 // 确定扫描的内存范围,判断是从上次扫描到的位置(cc->migrate_pfn)继续开始还是从头开始
 low_pfn = fast_find_migrateblock(cc);
 //pageblock_start_pfn()表示向页块起始地址对齐,block_start_pfn是扫描的起始页帧
 block_start_pfn = pageblock_start_pfn(low_pfn);

 if (block_start_pfn < cc->zone->zone_start_pfn)
  block_start_pfn = cc->zone->zone_start_pfn;

 /*
  * fast_find_migrateblock marks a pageblock skipped so to avoid
  * the isolation_suitable check below, check whether the fast
  * search was successful.
  */

 fast_find_block = low_pfn != cc->migrate_pfn && !cc->fast_search_fail;

 /* Only scan within a pageblock boundary */                                                                                                                      
 block_end_pfn = pageblock_end_pfn(low_pfn);

 /*
  * 开始遍历页块,以block_end_pfn为起始页帧号开始扫描,查找的步长为pageblock_nr_pages。
  在内核中,以页块为单位来管理页的迁移属性,内核使用两个函数来管理页的迁移类型:get_pageblock_migratetype()和set_pageblock_migratetype()
  在内核初始化的时候,所有的页都被初始化为可移动页面类型
  */

 for (; block_end_pfn <= cc->free_pfn;
   fast_find_block = false,
   low_pfn = block_end_pfn,
   block_start_pfn = block_end_pfn,
   //pageblock_nr_pages即为步长
   block_end_pfn += pageblock_nr_pages) {

  /*
   * This can potentially iterate a massively long zone with
   * many pageblocks unsuitable, so periodically check if we
   * need to schedule.
   */

  if (!(low_pfn % (SWAP_CLUSTER_MAX * pageblock_nr_pages)))
   cond_resched();
  //pageblock_pfn_to_page()函数返回这个页块的第一个物理页面的page数据结构
  page = pageblock_pfn_to_page(block_start_pfn,
      block_end_pfn, cc->zone);
  if (!page)
   continue;
  if (IS_ALIGNED(low_pfn, pageblock_nr_pages) &&
      !fast_find_block && !isolation_suitable(cc, page))
   continue;
  //suitable_migration_source()判断是否适合页块的迁移类型
  //对于异步类型的内存规整,只支持可移动页块
  //对于同步类型的内存规整,需要判断页块的迁移类型,若迁移类型不一致,则跳过该页块
  
  //如果不适合迁移,则将更新迁移位置到当前页块的结束页帧号,跳过该页块
  if (!suitable_migration_source(cc, page)) {
   update_cached_migrate(cc, block_end_pfn);
   continue;
  }

  /* isolate_migratepages_block()函数对页块里面的页面进行隔离 */
  low_pfn = isolate_migratepages_block(cc, low_pfn,
      block_end_pfn, isolate_mode);

  if (!low_pfn)
   return ISOLATE_ABORT;

  break;
 }

 /* 记录本次扫描结束的页帧号,下一次从该位置继续扫描迁移页块 */
 cc->migrate_pfn = low_pfn;
 
 return cc->nr_migratepages ? ISOLATE_SUCCESS : ISOLATE_NONE;
}

五、页面迁移源码分析

5.1 migrate_pages()

该函数在内部通过调用unmap_and_move()函数实现的,主要功能是将页面从给定的页面链表(from)迁移到通过 get_new_page 函数分配的空闲页面中旨在将指定的页面列表中的页面迁移到空闲的页面上。函数在 10 次尝试后或者当没有可移动页面时结束。

该函数的核心工作如下:

  • 最多尝试 10 次页面迁移。在每一轮迁移过程中,会遍历可迁移页面链表中的每个页面,尝试将其迁移到新的页面。
  • 在每一轮循环当中,对于可迁移页面链表中的页面类型会调用不同的迁移函数:
    • 对于大页:调用 unmap_and_move_huge_page() 函数处理大页迁移。
    • 对于普通页面:调用 unmap_and_move() 函数处理。
  • 根据迁移结果做出不同处理,如果是大页迁移失败,尝试将大页分裂成普通页进行处理,否则仅仅只是计数
/*
 * migrate_pages - migrate the pages specified in a list, to the free pages
 *     supplied as the target for the page migration
 *
 * @from:  将要迁移页面的链表
 * @get_new_page: 申请新内存的页面的函数指针
 * @put_new_page: 迁移失败时释放目标页面的函数指针
 * @private:  传递给get_new_page的参数
 * @mode:  迁移模式
 * @reason:  迁移原因
 * @return 返回未迁移页面的数量,或者错误代码。
 *
 * Returns the number of pages that were not migrated, or an error code.
 */

int migrate_pages(struct list_head *from, new_page_t get_new_page,
  free_page_t put_new_page, unsigned long private,
  enum migrate_mode mode, int reason)

{
 int retry = 1;
 int nr_failed = 0;
 int nr_succeeded = 0;
 int pass = 0;
 struct page *page;
 struct page *page2;
 int swapwrite = current->flags & PF_SWAPWRITE;
 int rc;

 if (!swapwrite)
  current->flags |= PF_SWAPWRITE;
 // 循环最多尝试10次,或直到没有可重试的页面
 for(pass = 0; pass < 10 && retry; pass++) {
  retry = 0;
  // 遍历页面链表中的每个页面
  list_for_each_entry_safe(page, page2, from, lru) {
retry:
   cond_resched();
   // 如果页面是大页,使用专门的迁移函数unmap_and_move_huge_page
   if (PageHuge(page))
    rc = unmap_and_move_huge_page(get_new_page,
      put_new_page, private, page,
      pass > 2, mode, reason);
   else
   // 否则,使用普通页面迁移函数
    rc = unmap_and_move(get_new_page, put_new_page,
      private, page, pass > 2, mode,
      reason);

   switch(rc) {
   case -ENOMEM:
    /*
     *如果 THP(Transparent Huge Pages,透明大页)迁移失败,
     * 尝试将大页分裂为普通页,并重新迁移。头页面立即重试,
     * 尾页则加入链表尾部,待其他页面处理后再处理。
     */

    if (PageTransHuge(page) && !PageHuge(page)) {
     lock_page(page);
     rc = split_huge_page_to_list(page, from);
     unlock_page(page);
     if (!rc) {
      list_safe_reset_next(page, page2, lru);
      goto retry;
     }
    }
    nr_failed++;
    goto out;
    // 迁移暂时失败,标记重试
   case -EAGAIN:
    retry++;
    break;
   case MIGRATEPAGE_SUCCESS:
   // 迁移成功,增加成功页面计数
    nr_succeeded++;
    break;
   default:
    /*
     * Permanent failure (-EBUSY, -ENOSYS, etc.):
     * unlike -EAGAIN case, the failed page is
     * removed from migration page list and not
     * retried in the next outer loop.
     */

    nr_failed++;
    break;
   }
  }
 }
 nr_failed += retry;
 rc = nr_failed;
out:
 if (nr_succeeded)
  count_vm_events(PGMIGRATE_SUCCESS, nr_succeeded);
 if (nr_failed)
  count_vm_events(PGMIGRATE_FAIL, nr_failed);
 trace_mm_migrate_pages(nr_succeeded, nr_failed, mode, reason);

 if (!swapwrite)
  current->flags &= ~PF_SWAPWRITE;

 return rc;
}

5.2 compaction_alloc()

该函数的主要工作如下:

  • 检查compact_control 结构体中的的 freepages 链表,判断是否有可用的空闲页面。若为空,则调用isolate_freepages(cc)函数去隔离更多的空闲页面
  • 调用list_entry()获取cc->freepages的空闲页面,获取成功后返回该页面
/*
 * 该函数用作内存规整过程中的迁移回调。它通过从隔离的空闲链表中获取页面来分配空闲页面,用于页面迁移.
 */

static struct page *compaction_alloc(struct page *migratepage,
     unsigned long data)

{
 struct compact_control *cc = (struct compact_control *)data;
 struct page *freepage;

 // 如果隔离的空闲页面链表为空,尝试隔离更多的空闲页面。
 if (list_empty(&cc->freepages)) {
  isolate_freepages(cc);
 // 如果仍然没有空闲页面,返回 NULL 表示分配失败
  if (list_empty(&cc->freepages))
   return NULL;
 }
 // 获取链表中的第一个空闲页面
 freepage = list_entry(cc->freepages.next, struct page, lru);

 // 从链表中移除该页面,减少空闲页面计数。
 list_del(&freepage->lru);
 cc->nr_freepages--;

 return freepage;
}

5.3 isolate_freepages()

isolate_freepages函数是空闲页扫描器的核心逻辑,但是compact_zone中对于空闲页扫描器调用并不直接,而是通过migrate_pages间接调用。

该函数的主要工作如下:

  • 使用 fast_isolate_freepages(cc) 尝试从空闲链表快速查找空闲页面
  • 如果未找到空闲页面,则初始化起始页帧号为 cc->free_pfn。然后计算当前页块的起始和结束页帧号。
  • cc->free_pfn 开始反向遍历页块,步长 pageblock_nr_pages ,查找合适的页块。
  • 查找到合适的页块之后,调用 isolate_freepages_block() 函数隔离空闲页,并检查是否已隔离足够的空闲页
  • 更新 cc->free_pfn 为隔离后的空闲页帧号。
/*
 *isolate_freepages是空闲页扫描器实现,用于查找用于页迁移的空闲页;
 */

static void isolate_freepages(struct compact_control *cc)
{
 struct zone *zone = cc->zone;
 struct page *page;
 unsigned long block_start_pfn; /* start of current pageblock */
 unsigned long isolate_start_pfn; /* exact pfn we start at */
 unsigned long block_end_pfn; /* end of current pageblock */
 unsigned long low_pfn;      /* lowest pfn scanner is able to scan */
 struct list_head *freelist = &cc->freepages;// 空闲页面链表
 unsigned int stride;

 /* 尝试从空闲列表中快速查找候选页面 */ 
 isolate_start_pfn = fast_isolate_freepages(cc);
 if (cc->nr_freepages)
  goto splitmap;

 // 初始化扫描范围
 isolate_start_pfn = cc->free_pfn;
 block_start_pfn = pageblock_start_pfn(isolate_start_pfn);
 block_end_pfn = min(block_start_pfn + pageblock_nr_pages,
      zone_end_pfn(zone));
 low_pfn = pageblock_end_pfn(cc->migrate_pfn);
 stride = cc->mode == MIGRATE_ASYNC ? COMPACT_CLUSTER_MAX : 1;

 /*
  * 从cc->free_pfn开始反向遍历寻找一个合适pageblock,随后针对这个pageblock隔离其空闲页,pageblock_nr_pages即为步长
  */

 for (; block_start_pfn >= low_pfn;
    block_end_pfn = block_start_pfn,
    block_start_pfn -= pageblock_nr_pages,
    isolate_start_pfn = block_start_pfn) {
  unsigned long nr_isolated;

  /*
   * This can iterate a massively long zone without finding any
   * suitable migration targets, so periodically check resched.
   */

  if (!(block_start_pfn % (SWAP_CLUSTER_MAX * pageblock_nr_pages)))
   cond_resched();
   //获取当前页块的第一个页面
  page = pageblock_pfn_to_page(block_start_pfn, block_end_pfn,
         zone);
  if (!page)
   continue;

  /* 检查当前页块是否适合迁移 */
  if (!suitable_migration_target(cc, page))
   continue;

  /* 如果最近隔离失败,不再重试 */
  if (!isolation_suitable(cc, page))
   continue;

  /* 找到一个适合隔离空闲页的页块 */
  nr_isolated = isolate_freepages_block(cc, &isolate_start_pfn,
     block_end_pfn, freelist, stride, false);

  /* Update the skip hint if the full pageblock was scanned */
  if (isolate_start_pfn == block_end_pfn)
   update_pageblock_skip(cc, page, block_start_pfn);

  /* 是否隔离了足够的空闲页? */
  if (cc->nr_freepages >= cc->nr_migratepages) {
   if (isolate_start_pfn >= block_end_pfn) {
    /* 如果可以隔离更多的空闲页,下次从前一个页块重启 */
    isolate_start_pfn =
     block_start_pfn - pageblock_nr_pages;
   }
   break;
  } else if (isolate_start_pfn < block_end_pfn) {
   /*
    * If isolation failed early, do not continue
    * needlessly.
    */

   break;
  }

  /* Adjust stride depending on isolation */
  if (nr_isolated) {
   stride = 1;
   continue;
  }
  stride = min_t(unsigned int, COMPACT_CLUSTER_MAX, stride << 1);
 }

 // 更新下一个可用的空闲页帧号
 cc->free_pfn = isolate_start_pfn;

splitmap:
 /* __isolate_free_page() does not map the pages */
 split_map_pages(freelist);
}

本文对内存规整的相关源码进行注解,但由于笔者能力有限,如有纰漏,敬请指正,期待和大家共同进步!

Linux内核之旅
Linux内核之旅
 最新文章