OpenHarmony 实战开发内存之旅——如何提升CMA利用率?

869 篇文章 5 订阅
608 篇文章 11 订阅

​一、为什么需要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][-end[MG]]]**​

  • cma=nn[MG]:用来指定 CMA 区域大小
  • @[start[MG][-end[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()

2)使用 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 区域的创建:

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 区域分配。

鸿蒙全栈开发全新学习指南

也为了积极培养鸿蒙生态人才,让大家都能学习到鸿蒙开发最新的技术,针对一些在职人员、0基础小白、应届生/计算机专业、鸿蒙爱好者等人群,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线【包含了大厂APP实战项目开发】

本路线共分为四个阶段:

第一阶段:鸿蒙初中级开发必备技能

在这里插入图片描述

第二阶段:鸿蒙南北双向高工技能基础:gitee.com/MNxiaona/733GH

第三阶段:应用开发中高级就业技术

第四阶段:全网首发-工业级南向设备开发就业技术:gitee.com/MNxiaona/733GH

《鸿蒙 (Harmony OS)开发学习手册》(共计892页)

如何快速入门?

1.基本概念
2.构建第一个ArkTS应用
3.……

开发基础知识:gitee.com/MNxiaona/733GH

1.应用基础知识
2.配置文件
3.应用数据管理
4.应用安全管理
5.应用隐私保护
6.三方应用调用管控机制
7.资源分类与访问
8.学习ArkTS语言
9.……

基于ArkTS 开发

1.Ability开发
2.UI开发
3.公共事件与通知
4.窗口管理
5.媒体
6.安全
7.网络与链接
8.电话服务
9.数据管理
10.后台任务(Background Task)管理
11.设备管理
12.设备使用信息统计
13.DFX
14.国际化开发
15.折叠屏系列
16.……

鸿蒙开发面试真题(含参考答案):gitee.com/MNxiaona/733GH

鸿蒙入门教学视频:

美团APP实战开发教学:gitee.com/MNxiaona/733GH

写在最后

  • 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  • 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
  • 想要获取更多完整鸿蒙最新学习资源,请移步前往小编:gitee.com/MNxiaona/733GH

  • 13
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值