关于openharmony:内存之旅如何提升CMA利用率

9次阅读

共计 7750 个字符,预计需要花费 20 分钟才能阅读完成。

本文作者:宋远征和李佳伟

OpenAtom OpenHarmony(以下简称“OpenHarmony”)作为一款面向终端设备的操作系统,多媒体业务是其重要的利用场景。在终端多媒体业务中,CMA 内存的应用对业务性能和性能至关重要。本文将从入门开始,带你深刻了解 CMA 的设计初衷和工作原理。
(备注:在本文形容中,把 CMA 治理的内存区域称为 CMA 区域;把 CMA 的下层使用者称为 CMA 业务。)

为什么须要 CMA?

CMA 全称是 Contiguous Memory Allocator(间断内存分配器)。顾名思义它是一种内存分配器,提供了调配、开释物理间断内存的性能。
你可能会有疑难:为什么须要 CMA?
这个问题等价于两个子问题:
第一,为什么业务须要物理地址间断的内存空间?
第二,为什么 buddy 零碎无奈满足业务的此类间断内存需要?
对于第一个子问题:
最次要起因是性能上须要。很多嵌入式设施没有 IOMMU,也不反对 scatter-getter,因而这类设施的驱动程序须要操作间断的物理内存空间能力提供服务。晚期,这些设施包含相机、硬件音视频解码器和编码器等,它们通常服务于多媒体业务。尽管,随着挪动设施的倒退,手机等终端设备曾经配置了 IOMMU,然而仍然有诸多嵌入式设施没有 IOMMU,须要应用物理地址间断的内存。
此外,物理地址间断的内存 cache 命中率更高,访问速度更优,对业务性能有劣势。
对于第二个子问题:
尽管 buddy 零碎能够通过调配高 order 的页面来调配物理地址间断的内存空间,然而仍旧存在两个有余:
其一,随着零碎运行,内存碎片化越来越重大,高 order 内存调配耗时长,且容易调配失败。
其二,如果业务须要的内存大小远超过最高 order 的大小,则无奈调配。以 ARM64 为例,buddy 零碎中最高 order 是 10,单次反对调配的最大内存大小是 4MB,所以无奈为业务调配超过 4MB 物理地址间断的内存。
鉴于上述起因,咱们须要 CMA 来实现大块间断物理地址内存的调配。

CMA 的设计思路

要实现大块间断物理地址内存的调配,首先面临的问题就是:大块间断的物理内存从哪里找?

这里有两个计划:

计划一:在零碎开机时,从整机内存中划分一块进去作为 CMA 区域的预留。

计划二:当多媒体业务启动时,再清理出一块物理地址间断的闲暇内存。

显然,第一个计划对多媒体业务是最牢靠、最敌对的。然而毛病也很显著,即在非多媒体业务场景,预留的 CMA 区域无奈失去利用,整机内存利用率变低了。第二个计划对整机敌对,因为没有 CMA 预留,整机内存利用率相比计划一更高。然而因为碎片化问题,随着零碎运行获取间断物理内存越来越耗时,影响多媒体业务的性能,甚至当内存调配失败时,会导致业务不可用。

综上所述,问题实质上是整机内存利用率与 CMA 业务性能之间的均衡。那么,CMA 如果解决上述问题呢?它采取了折中的方法:

• 开机时,零碎预留出 CMA 区域。

• 在 CMA 业务不应用时,容许其余业务有条件地应用 CMA 区域,条件是申请页面的属性必须是可迁徙的。

• 当 CMA 业务应用时,把其余业务的页面迁徙出 CMA 区域,以满足 CMA 业务的需要。

CMA 是如何工作的?

理解了 CMA 的产生背景和设计思路后,上面来看看 CMA 具体是如何工作的。

1. 前置常识

在理解 CMA 如何工作之前,咱们须要先理解一点无关迁徙类型和 pageblock 的常识。

在 buddy 零碎中治理着多种迁徙类型(migrate type)的内存页面,不同的迁徙类型有各自的用处。举例来说,MIGRATE_MOVABLE 示意保留在其页面的数据是能够被迁徙的,这种迁徙类型在磁盘缓存、过程页等方面奉献很大。MIGRATE_UNMOVABLE 示意该页面不能被迁徙。

pageblock 是一个物理上间断的内存区域,它蕴含多个页面。例如,在 ARM64 中,一个 pageblock 的大小是 4MB,一个页面的大小为 4KB,每个 pageblock 治理着 1024 个页面。

页面的迁徙类型与其所在的 pageblock 的迁徙类型统一。当 buddy 零碎接管到内存调配申请时,会依据申请标识从对应迁徙类型的 pageblock 中调配页面。

当对应迁徙类型的 pageblock 中页面不够调配时,buddy 零碎会从其余的 pageblock 中获取页面。在肯定条件下,这可能会扭转一个 pageblock 的迁徙类型。比方,申请一个不可挪动的页面时,如果零碎从 MIGRATE_MOVABLE 迁徙类型的 pageblock 中调配页面,会导致这个 pageblock 的迁徙类型变成 MIGRATE_UNMOVABLE。这个过程被称为 fallback。

对于 CMA 区域的内存页面,它并不心愿被扭转迁徙类型,因而 CMA 引入了一个新的 MIGRATE_CMA 迁徙类型。这种迁徙类型具备一个重要的性质:不是所有的内存申请都能到该区域分配内存,只有可挪动的页面能力从 MIGRATE_CMA 类型的 pageblock 中调配。

把握了以上常识之后,上面咱们来看看 CMA 的三个次要工作流程:创立 CMA 区域、调配 CMA 内存和开释 CMA 区域内存。

2. 创立 CMA 区域

创立 CMA 区域的流程如下:

(1)在启动阶段,CMA 告诉 memblock 保留一部分内存。

(2)CMA 将这部分内存设置为 MIGRATE_CMA 迁徙类型,并其返还给 buddy 零碎。

零碎中可能存在多个 CMA 区域。这些被预留的 CMA 区域由 buddy 系统管理。buddy 零碎能够利用这部分页面用于满足可挪动页面的调配申请,这样既保证了预留内存是物理间断的,又进步了整机内存利用率。

3. 调配 CMA 区域内存

CMA 业务通过 cma_alloc() 接口申请 CMA 区域内存。CMA 区域内存的调配过程如下:

(1)依据调配申请抉择要调配的页面范畴。

(2)将波及该页面范畴的 pageblock 从 buddy 零碎中隔离。因为 buddy 零碎不会从 MIGRATE_ISOLATE 迁徙类型的 pageblock 调配页面,所以 CMA 将 pageblock 的迁徙类型由 MIGRATE_CMA 变更为 MIGRATE_ISOLATE,从而实现隔离。如图 1 所示。

图 1 调配步骤(1)

(3)在隔离后,将调配范畴内已应用的页面进行迁徙解决。迁徙过程就是将页面内容复制到其余内存区域,并更新对该页面的援用。

(4)迁徙实现后,pageblock 的迁徙类型从 MIGRATE_ISOLATE 复原为 MIGRATE_CMA,最初将这些页面返回给调用者。如图 2 所示。

图 2 调配步骤(2)

4. 开释 CMA 区域内存

CMA 业务通过 cma_release () 接口开释 CMA 区域内存。开释过程就是简略迭代这些页面,将它们一一放回 buddy 零碎。

代码流程解说

下面为大家介绍了 CMA 的工作原理,接下来咱们来看看 CMA 的代码是如何实现的。

(备注:这部分只是解说主流程,并未八面玲珑开展。具体细节,读者能够自行查阅代码。)

1. CMA 治理构造

首先让咱们来看一下 CMA 区域在代码中是怎么示意的。

CMA 区域形象为 struct cma 构造:

struct cma {
        unsigned long   base_pfn;   // 区域的起始页帧号
        unsigned long   count;      // 区域所蕴含的总页面数量
        unsigned long   *bitmap;    // 位图,用于形容区域的调配单元的分配情况
        unsigned int order_per_bit; // 示意位图中一个 bit 所代表的页面数量(2^order_per_bit)char name[CMA_MAX_NAME];    // 区域名称
};

extern struct cma cma_areas[MAX_CMA_AREAS]; // CMA 区域数组
extern unsigned cma_area_count;             // CMA 区域总个数

CMA 区域通过 bitmap 进行治理,每个 bit 示意 2^order_per_bit 个页面。bit 为 0 则示意闲暇,为 1 示意已占用。

零碎中存在多个 CMA 区域,cma_areas[MAX_CMA_AREAS] 示意 CMA 区域数组,cma_area_count 示意 CMA 区域总个数。

2. 创立 CMA 区域

简略介绍完 CMA 的治理构造,上面咱们来看创立过程。

CMA 区域的创立罕用两种形式:

  1. 应用 command line 形式:cma=nn[MG]@start[MG]]
    •cma=nn[MG]:用来指定 CMA 区域大小。
    •@start[MG]]:用来指定可创立 CMA 区域的地址范畴。
    例如:cma=64M@0x00000000-0x20000000
    在内核启动过程中会解析 cmdline 中 CMA 区域配置参数,最初通过 cma_init_reserved_mem() 函数实现 CMA 区域的创立,具体代码流程如下(以 ARM 为例):
start_kernel()
`-|setup_arch()
  `-|arm_memblock_init()
    `-|dma_contiguous_reserve()
      `-|dma_contiguous_reserve_area()
        `-|cma_declare_contiguous()
          `-|cma_declare_contiguous_nid()
            `-|cma_init_reserved_mem() 
  1. 应用 DTS 形式

这里以共享 CMA 区域(cma-default)创立流程为例:

首先,应用 DTS(Device Tree Source)形容以后要创立的 CMA 区域。

Reserved-memory {
  #address-cells = <1>;
  #size-cells = <1>;
  ranges;

  /* global autoconfigured region for contiguous allocations */
  linux,cma {
    compatible = "shared-dma-pool";
    reusable;
    size = <0x04000000>;
    alignment = <0x2000>;
    alloc-ranges = <0x00000000 0x20000000>;
    linux,cma-default;
  };
};

•“linux,cma”为创立的 CMA 区域名称。
•“compatible”属性必须为“shared-dma-pool”。
•“reusable”属性阐明 CMA 区域的页面是能够映射建设页表,在 CMA 区域页面闲置时可满足可挪动页面申请,进步内存利用率。
•“size”属性示意以后 CMA 区域的大小。
•“alignment”属性指定 CMA 区域的地址对齐大小。
•“alloc-ranges”属性指定可创立 CMA 区域的地址范畴。
•“linux,cma-default”属性指定以后区域为共享 CMA 区域。
而后,通过 rmem_cma_setup() 函数中解析 DTS 中的 CMA 区域配置信息,传递参数给 cma_init_reserved_mem() 函数进行 CMA 区域的创立

RESERVEDMEM_OF_DECLARE(dma, "shared-dma-pool", rmem_dma_setup);

rmem_cma_setup()
`-|cma_init_reserved_mem()

创立实现后,因为临时没有设施驱动应用,为了晋升内存利用率,须要将这部分内存标记后,归还给 buddy 零碎,供 buddy 零碎满足可挪动页面内存申请。
标记和返还 buddy 零碎的过程如下:

core_initcall(cma_init_reserved_areas);

cma_init_reserved_areas()
`-|for (i = 0; i < cma_area_count; i++)
  `-|cma_activate_area()
    `-|for (pfn = base_pfn; pfn < base_pfn + cma->count; pfn += pageblock_nr_pages)
      `-|init_cma_reserved_pageblock()
  `-|set_pageblock_migratetype(page, MIGRATE_CMA);
          |__free_pages(page, pageblock_order);

通过 cma_init_reserved_areas() 迭代所有的 CMA 区域,将 CMA 区域以 pageblock 为单位宰割,设置 pageblock 为 MIGRATE_CMA 迁徙类型,而后通过 _free_pages() 函数将内存页面返还给 buddy 零碎。

3. 调配 CMA 区域内存

当初 CMA 区域属于可用状态,咱们来看一下如何调配 CMA 区域内存。

cma_alloc()
  `-|for (;;)
`-|alloc_contig_range()
  `-|start_isolate_page_range()
    |__alloc_contig_migrate_range()
    |isolate_freepages_range()
    |undo_isolate_page_range()

这部分流程和后面叙述的工作原理是绝对应的。cma_alloc() 函数在 bitmap 中查找满足调配要求的内存区域,应用 alloc_contig_range() 函数进行调配解决。alloc_contig_range() 函数作为分配内存的次要函数,进行如下解决:
(1)通过 start_isolate_page_range() 函数将波及调配区域的 pageblock 迁徙类型更改为 MIGRATE_ISOLATE。
(2)通过 __alloc_contig_migrate_range() 函数将要分配内存范畴内的页面进行迁徙(将已应用的页面迁徙进去)。
(3)isolate_freepages_range() 函数将须要应用的页面从 buddy 零碎摘取进去。
最初,通过 undo_isolate_page_range() 函数,复原相干 pageblock 迁徙类型。

4. 开释 CMA 区域内存
设施驱动在不应用 CMA 区域内存时,能够通过 cma_release () 函数进行开释。开释后的内存又从新回到 buddy 零碎,能够持续为整个零碎服务。

cma_release()
  `-|free_contig_range(pfn, count);
    |cma_clear_bitmap(cma, pfn, count);

cma_release() 函数通过 free_contig_range() 函数将不应用的内存页面偿还到 buddy 零碎,再通过 cma_clear_bitmap() 更新 CMA 区域内存应用状况。

OpenHarmony 对 CMA 的加强

以后,CMA 次要存在两个问题:
•CMA 区域内存利用率低。
•局部 Movable 内存无奈迁徙导致 cma_alloc 失败
上面咱们来具体理解这两个问题,并看看 OpenHarmony 是如何应答这两个问题的。
问题 1:CMA 区域内存利用率低
以后 Linux 内核 CMA 区域应用策略较为激进,CMA 内存区域利用率低。极其状况下可能呈现 CMA 区域内存短缺,但非 Movable 申请不到内存的问题(因为应用 CMA 区域的条件是申请页面的属性必须是可迁徙的)。
CMA 区域的应用策略次要是指图 3 中的 ① 处。

图 3 CMA 和 buddy 零碎的关系

Linux 5.10 内核的策略是:当 #free_cma > 1/2#total_free 时(CMA 区域闲暇量占总内存闲暇量的一半以上),Movable 能够申请到 CMA 区域调配。假如物理内存中 CMA、Movable、其余(Unmovable 和 Reclaimable)的量别离是 X、Y、Z,通常状况下 X < Y 且 X<Z。显然,当用户通过 buddy 零碎先申请 X 数量的 Movable、再申请 Y+Z 数量的非 Movable 时,就会呈现:CMA 区域内存短缺,但非 Movable 内存申请不到内存的问题。
随着挪动设施多媒体业务的倒退,CMA 区域越来越大,因而进步其利用率对系统至关重要。为此,OpenHarmony 将 CMA 应用策略调整为:Movable 申请优先到 CMA 区域调配。这样就能够躲避上述问题了。

问题 2:局部 Movable 内存无奈迁徙导致 cma_alloc 失败

局部 Movable 内存在某些状况下无奈迁徙,如果这类内存处于 CMA 区域,会导致 cma_alloc 失败。文章(https://lwn.net/Articles/636234/)中首先提出该问题,该问题是 CMA 区域的 Movable 内存被长期 pin 住,导致这些内存会变得“不可迁徙”。显然要彻底解决问题,必须从 Movable/Unmovable 内存的定义和划分动手,这曾经不单是 CMA 要探讨的议题了,有趣味的同学能够关注 Linux 社区的探讨。
回到 CMA 自身,针对该问题,Linux 社区有多种长期解决方案,比方:
计划 1:当 Movable 内存须要被 pin 住时,把它迁徙出 CMA 区域。
计划 2:仅容许那些不容易长期被 pin 住的 Movable 申请应用 CMA 区域。
显然这些计划未能从根本上解决 Movable 内存在某些状况下“不可迁徙”的问题,所以它们都未能合入 Linux 主线。
OpenHarmony 采纳了计划 2 的思路躲避该问题,仅将 Movable 申请中的用户态匿名页申请重定向到 CMA 区域调配,因为这类内存比内核态申请的 Movable 迁徙胜利的概率更大。
图 4 从零碎架构的角度展现了 OpenHarmony 中 CMA 内存的应用策略。(图中 C /M/U/ R 别离示意 CMA/Movable/Unmovable/Reclaimable。)

图 4 OpenHarmony 的 CMA 内存应用策略示意图

具体实现上,次要包含如下细节:
•内核态引入 GFP_CMA 标记。触发 page fault 时,对用户态的匿名页内存申请,在 gfp_mask 参数中增加 GFP_CMA 标记。
•在获取内存申请的 migratetype 时,依据 gfp_mask 中是否携带 GFP_CMA,将 Movable 申请划分成 MIGRATE_CMA 申请(带 GFP_CMA 标记)和 MIGRATE_MOVABLE 申请(不带 GFP_CMA 标记)。
•对 order 0 申请,PCP 仅容许 MIGRATE_CMA 申请到 CMA PCP list 中调配,如果调配失败,再到 buddy 零碎申请。非 MIGRATE_CMA 申请禁止到 CMA PCP list 中调配。
•buddy 零碎解决 MIGRATE_CMA 申请时,优先到 CMA 区域调配。如果 CMA 区域闲暇内存不足,再到 Movable 区域调配。非 MIGRATE_CMA 申请禁止到 CMA 区域调配。

结束语

感兴趣的小伙伴能够从以下链接获取 OpenHarmony 源码进行深刻理解:
https://gitee.com/openharmony…
源码中 GFP_CMA 标记的引入采纳了 Chris Goldsworthy 等人的 Linux 社区 Patch(cma: redirect page allocation to CMA),在此对几位作者表示感谢。
Patch 链接:
https://lkml.org/lkml/2020/11…

本文参考资料:

https://lwn.net/Articles/396707/

https://lwn.net/Articles/486301/

https://lwn.net/Articles/447405/

https://lwn.net/Articles/684611/

正文完
 0