campact

简介

在出现内存碎片问题时,可以尝试进行内存规整(compact),移动可移动的页面,腾出更多连续空闲内存。

将一个页移动到另一个页的过程叫作页迁移(page migrate),事实上,页迁移是内存管理的独立逻辑, 内核封装了单独接口 migrate_pages()。其中一个应用场景就是 compact,其他类似场景还有 NUMA Balance、Memory hotplug 和 CMA等。

策略场景

内存规整的范围?何时进行内存规整?

对于内存规整范围,内核通常选择以 zone 为单位进行规整,核心接口 compact_zone()

何时触发,属于触发策略和场景问题,内核当前有四种策略场景,这些场景最终都会通过 compact_zone() 进行内存规整,如下:

  1. direct compact

  2. kcompactd

  3. /proc/sys/vm/compact_memory

  4. /proc/sys/vm/compaction_proactiveness

如下所示,内存规整是基于内存迁移实现的功能,内核根据不同策略触发内存规整,用于缓解内存外部碎片问题, 可以分层分析内存规整。

direct compact        kcompactd         compact_memory       compaction_proactiveness
      |                   |                   |                          |
      +-------------------+-------------------+--------------------------+
      |
      v           +---> migrate scanner
compact_zone() ---+
      |           +---> free scanner
      v
migrate_pages()

direct compact

__alloc_pages_slowpath()
    __alloc_pages_direct_compact()

在 page 分配器进入慢速分配路径时,并且内存低于 min 水位线,会触发 direct compact 策略。

kcompactd

内核在启动过程中会调用 kcompactd_init(),为每个 node 启动一个内核线程 kcompactd ,并且 kcompactd 线程会运行在与 node 相对应的 CPU 核上,在合适的时机 kcompactd 将会被唤醒进行内存规整。

本节主要从二个方面说明,分别是 唤醒条件、运行条件。

唤醒条件,内存规整模块向内核提供 wakeup_kcompactd() 用于唤醒 node 对应的 kcompactd 线程, 唤醒 kcompactd 与 kswapd 是强相关,如下场景会被唤醒:

  1. 内存分配失败时,会先唤醒 kswapd, wakeup_kswapd() 将先判断当前内存无法分配的原因,如果 不是低内存导致,此时内存回收可能已经无法解决此问题,所以将会提前调用 wakeup_kcompactd() 唤醒 kcompacted 线程进行内存规整。

  2. kswapd 线程在每次内存回收完毕后,将会调⽤ kswapd_try_to_sleep() 尝试休眠,随后调用 wakeup_kcompactd() 尝试进行内存规整。

运行条件,当 kcompactd 线程运行时,将会调用 compactd_do_work() 遍历当前 node 的所有 zone 进行合法性判断,若符合条件,则调用 compact_zone() 针对 zone 进⾏内存规整。

/proc/sys/vm/compact_memory

通过对 compact_memory 节点进行写 1,触发当前所有 node 以及所有 zone 进行内存规整。

/proc/devices/system/node/node<id>/compact 只有存在 NUMA 系统上,可以只触发某一个 node 进行内存规整。

解析 compact_zone()

compact_zone() 针对单个 zone 内存区进行内存规整,这是内存规整的最小单元。 通过 migrate scanner 从低地址到高地址寻找迁移页, 通过 free scanner 从高地址到低地址寻找空闲页, 最终将迁移页迁移至空闲页,完成内存规整,如下所示:

low                                                high
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|X|X| |X| |X| |X| | |X| | ... | |X| | |X|X|X|X| | |X|X|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+-----------+-----------+     +-----------+-----------+
  pageblock   pageblock         pageblock   pageblock

migrate scanner                          free scanner
-------------->                          <------------

migrate page list                        free page list
   +-+-+-+-+                               +-+-+-+-+
   |X|X|X|X|                               | | | | |
   +-+-+-+-+                               +-+-+-+-+
       |                                       |
       |                                       |
       |                                       |
       |           +---------------+           |
       +---------->| migrate pages |<----------+
                   +---------------+

更细节一些来说,内存规整开始后,先通过 migrate scanner 扫描,并且扫描单位为一个 pageblock, 将当前 pageblock 中可迁移的页隔离后放入到待迁移的链表。随后调用 free scanner 扫描, free scanner 依然以 pageblock为步长,但不再限制扫描一个pageblock,其扫描的目标是 找到大于等于当前迁移页数量的空闲页。上述扫描过程将会产生迁移页和空闲页,用于后续内存迁移,这样 就完成了内存规整的一轮操作,内存规整并非一次性扫描 zone,然后再迁移,而是以这种一步一步的方式 进行迁移,这能平摊内存规整对性能带来的风险,并且每轮处理后都有机会判断当前内存规整是否可以退出。

再次回到 compact_zone(),核心代码逻辑如下:

  1. compact_finished() 用于判断当前内存规整是否结束

  2. isolate_migratepages()migrate scanner 实现,用于查找需要移动的页

  3. isolate_freepages()free scanner 实现,用于查找空闲页

  4. migrate_pages() 是页迁移函数,将上述两个 scanner 扫描出来的页进行迁移处理,完成内存规整

总而言之,内存规整的核心逻辑在于 migrate scannerfree scanner 运作原理。

migrate scanner

主要是 isolate_migratepages(),本质就是将 page 从 LRU 链表中删除。

isolate_migratepages()
      isolate_migratepages_block() break;
            lruvec_del_folio()
                  list_del(page)
            list_add(page, cc->migratepages)

以 pageblock 为步长,检查是否有合适的 pageblock?

如果当前 pageblock 不合适,继续查找下一个 pageblock,但是这里最多查找 32个 pageblock 后, 就需要主动 schedule 出去,睡眠一段时间再继续。

如果有合适的 pageblock,调用一次 isolate_migratepages_block(),查找合适的迁移页。 如果找到,将此迁移页从 LRU 链表中删除,同时加入到 migrate page list。 完成一轮 pageblock 查找后,直接结束查找迁移页。

free scanner

主要是 isolate_freepages(),本质就是将 page 从 page 分配器中取出,不再参与系统内存分配, 仅用于内存规整迁移。

isolate_freepages()
      isolate_freepages_block()
            __isolate_free_page()
                  list_del(page)
            list_add_tail(page, cc->freepages)
      if (cc->nr_freepages >= cc->nr_migratepages) break;

以 pageblock 为步长,调用一次 isolate_freepages_block(),查找合适的空闲页, 如果找到,将此空闲页从 page 分配器中删除,同时加入到 free page list。

完成一轮 pageblock 查找后,判断空闲页数是否大于等于迁移页数?true,结束查找空闲页。 false,继续查找下一个 pageblock,但是这里最多查找 32个 pageblock 后,就需要主动 schedule 出去,睡眠一段时间再继续。

Last updated

Was this helpful?