LInux内核——CMA
CMA
,Contiguous Memory Allocator,是内存管理子系统中的一个模块,预留内存的配置解析和管理内存配置。一般系统会在启动过程中,从整个memory中配置一段连续内存用于CMA,然后内核其他的模块可以通过CMA的接口API进行连续内存的分配。CMA的核心并不是设计精巧的算法来管理地址连续的内存块,实际上它的底层还是依赖内核伙伴系统这样的内存管理机制,或者说CMA是处于需要连续内存块的其他内核模块(例如DMA mapping framework)和内存管理模块之间的一个中间层模块,主要功能包括:
- 解析DTS或者命令行中的参数,确定CMA内存的区域,这样的区域我们定义为CMA area。
- 提供cma_alloc和cma_release两个接口函数用于分配和释放CMA pages
- 记录和跟踪CMA area中各个pages的状态
- 调用伙伴系统接口,进行真正的内存分配。
CMA最初的目的
https://lwn.net/Articles/396657/
连续内存分配器(CMA)是一个框架,它允许为物理连续内存管理设置特定于计算机的配置。然后根据该配置为设备分配内存。
该框架的主要作用不是分配内存,而是解析和管理内存配置,以及充当设备驱动程序和可插入分配器之间的中介。因此,它不依赖于任何内存分配方法或策略。
CMA使用
cma区域可以通过设备树
、cmdline
和menuconfig
指定,并且可以通过设备树的phandle机制和单独的设备绑定,具体的实现和原理说明如下:
设备树指定
在设备树中添加reserved-memory
节点,并且compatible属性指定为shared-dma-pool,并在设备节点中通过memory-region引用该节点,
1 | reserved-memory { |
节点配置属性:
- compatible (optional) ——standard definition
shared-dma-pool
: 表示一个内存区域,用于一组设备的DMA缓冲区共享池。操作系统可以使用它在必要时实例化必要的池管理子系统。- vendor specific, 特定于供应商的字符串,形式为,
,[ -]
- no-map (optional) —— empty property
- 指示操作系统不能创建该区域的虚拟映射作为其系统内存的标准映射的一部分,也不允许在使用该区域的设备驱动程序控制之外的任何情况下对其进行投机性访问。
- reusable (optional) —— empty property
- 操作系统可以使用该区域的内存,但该区域的设备驱动程序需要能够回收它。通常,这意味着操作系统可以使用该区域存储易失性数据或缓存数据,这些数据可以重新生成或迁移到其他地方。
详细参考文档:Documentation/devicetree/bindings/reserved-memory/reserved-memory.txt
cmdline指定
在uboot的bootargs
可以添加cma属性指定cma区域
1 | cma=nn[MG]@[start[MG][-end[MG]]] |
Documentation/admin-guide/kernel-parameters.txt
1 | cma=256M |
menuconfig指定
CMA相关的menuconfig分别在memory management options
和library routines
里面
1 | Memory Management options ---> |
原理
CMA通过在启动阶段预先保留内存。这些内存叫做CMA区域,稍后返回给伙伴系统从而可以被用作正常申请。如果要保留内存,则需要恰好在底层MEMBLOCK
分配器初始化之后,及大量内存被占用之前调用,并在伙伴系统建立之前调用。
- 页迁移:
当从伙伴系统申请内存的时候,需要提供一个gfp_mask参数。不管其他事情,这个参数指定了要申请内存的迁移类型。迁移类型是MIGRATE_MOVABLE,它背后的意思是在可移动页面上的数据可以被迁移(或者移动,因此而命名),这对于磁盘缓存或者进程页面来说很有效。为了使相同迁移类型的页面在一起,伙伴系统把页面组成 “页面块 (pageblock)”,每组都有一个指定的迁移类型。分配器根据请求的类型在不同的页面块上分配页。如果尝试失败,分配器会在其它页面块上分配并甚至修改页面块的迁移类型。这意味着一个不可移动的页可能分配自一个MIGRATE_MOVABLE页面块,并导致该页面块的迁移类型改变。这不是CMA想要的,所以它引入了一个MIGRATE_CMA类型,该类型又一个重要的属性: 只有可移动页可以从MIGRATE_CMA页面块种分配。那么,在启动期间,当dma_congiguous_reserve()和dma_declare_contiguous()方法被调用的时候,CMA在memblock中预留一部分RAM,并在随后将其返还给伙伴系统,仅将其页面块的迁移类型置为MIGRATE_CMA. 最终的结果是所有预留的页都在伙伴系统里,所以它们都可以用于可移动页的分配。
- CMA分配与释放CMA分配,
1
2int alloc_contig_range(unsigned long start, unsigned long end, unsigned migratetype, gfp_t gfp_mask)
void free_contig_range(unsigned long pfn, unsigned nr_pages);dma_alloc_from_contiguous()
选择一个页范围,start和end参数指定了目标内存的页框个数(或PFN范围)。参数migratetype指定了潜在的迁移类型; 在CMA的情况下,这个参数就是MIGRATE_CMA。这个函数所做的第一件事是将包含 (start, end) 范围内的页面块标记为 MIGRATE_ISOLATE。伙伴系统不会去触动这种类型的页面块。改变迁移类型不会魔 法般地释放页面,因此接下来需要调用 __alloc_conting_migrate_range()。它扫 描PFN范围并寻找可以迁移的页面。迁移是将页面复制到系统其它内存部分并更新相 关引用的过程。迁移部份很直接,后面的部分需要内存管理子系统来完成。当数据迁 移完成,旧的页面被释放并回归伙伴系统。这就是为什么之前那些需要包含的页面块 一定要标记为 MIGRATE_ISOLATE 的原因。如果指定了其它的迁移类型,伙伴系统会 毫不犹豫地将它们用于其它类型的申请。
现在所有 alloc_contig_range 关心的页都是空闲的了。该方法将从伙伴系统中取 出它们,并将这些页面块的类型改为 MIGRATE_CMA。然后将这些页返回给调用者。
CMA释放:调用free_contig_range函数迭代所有的页面并将其返还给伙伴系统。
- 当设备驱动不用时,内存管理系统将该区域用于分配和管理可移动类型页面;
- 当设备驱动使用时,此时已经分配的页面需要进行迁移,又用于连续内存分配;
主要数据结构
1 | struct cma { |
系统CMA调试信息
- CMA区域内存统计
1
2
3# cat /proc/meminfo | grep cma -i
CmaTotal: 1310720 kB
CmaFree: 1309472 kB - 各个CMA区域详细信息
1
2
3# ls /sys/kernel/debug/cma/cma-reserved/
alloc bitmap free order_per_bit
base_pfn count maxchunk used1
2
3
4
5
6
7
8# cat /sys/kernel/debug/cma/cma-reserved/count
327680
# cat /sys/kernel/debug/cma/cma-reserved/bitmap
4294967295 4294967295 4294967295 4294967295 16777215 0 0 0 4294967295 4294967295 65535 0 4294967295 4294967295 65535 0 0 0 0 0 0 0
# cat /sys/kernel/debug/cma/cma-reserved/order_per_bit
0 order_per_bit
,为0,表示bitmap中的每一位bit代表一个1(2^0)个物理页。bitmap
: 其中的每一项占32bit,而每一bit代表的一个物理页的状态,0
表示free,1
表示已经分配。(bitmap的总数为count/32, 327680/32=10240)
查看bitmap中的特殊数字
- 4294967295 -> 0xFFFFFFFF: 32位全1
- 16777215 -> 0xFFFFFF: 24位全1
- 65535 -> 0xFFFF: 16为全1
应用——memuconfig指定
内核版本: 5.4.217
1 | # dmesg | grep cma |
- 当前系统内存2G,而cma预留了1280MB,这1280MB内存预留有谁决定??
- 除去CMA预留的1280MB,内存剩余768MB,如果系统占用内存超过768MB事,如何处理???
- 2G系统内存,预留1280MB,为啥free看到的可用内存依然是1866MB,是不是CMA预留内存也可以被系统应用所使用???
- 如果CMA预留内存也可以被系统使用,那么后期内存碎片化严重时,在CMA中无法分配大内存区域时怎么办???
1 | 0.000000] Call trace: |
函数调用栈:
1 | start_kernel |
物理内存范围:
1 | 总大小1920 MB |
CMA预留大小
首先,确定Reserved 1280 MiB
日志输出的位置,再找到其调用关系及cma预留大小的定义。
1 | int __init cma_declare_contiguous(phys_addr_t base, |
mm/cma.c
在dma_contiguous_reserve
接口中指定需要预留的内存大小
1 | void __init dma_contiguous_reserve(phys_addr_t limit) |
menuconfig中指定了CONFIG_CMA_SIZE_SEL_MBYTES
,因此CMA预留内存大小为size_bytes
1 |
|
实际预留大小在menuconfig中进行配置:
1 | # |
在cma_declare_contiguous
函数中的memblock_phys_alloc_range接口申请了CMA内存区域。
1 | + <--------------------------+ 物理内存总大小1920MB +-------------------------> + |
CMA base地址如何确定:随机还是指定?
- cmdline方式中可以指定CMA区域的base地址和大小
- menuconfig方式中指定指定CMA区域的大小,base地址随机指定符合要求的区域。
在cma_declare_contiguous
函数中,通过memblock_phys_alloc_range
接口申请完了CMA区域的内存后,紧接着使用cma_init_reserved_mem
接口将其进行初始化。
初始化的目的指定CMA相应区域的参数,比如基址、大小等
CMA区域的个数可以进行配置,内核默认最多8
个,因为CONFIG_CMA_AREAS
默认值为7
1 | struct cma cma_areas[MAX_CMA_AREAS]; |
每个CMA区域将一定大小的内存申请后将通过cma_init_reserved_mem
进行初始化,并将参数信息保存到cma_areas
数组中。
使用memconfig进行CMA预留区域的指定,只会使用一个cma_areas
数组项,而设计多个的目的是在设备树中可以指定多个不同的CMA预留区域。
1 | /** |
各个CMA预留区域的内存页初始化:
系统启动初期根据各种参数,预留CMA区域的物理内存,将其基地址和大小进行确认,并检查其合法性;系统启动过程中,调用cma_init_reserved_areas
接口对个CMA区域内存进行初始化后, CMA就可用供其他模块、设备和子系统使用。
1 | core_initcall(cma_init_reserved_areas) |
init_cma_reserved_pageblock
接口将释放整个页面块并将其迁移类型设置为MIGRATE_CMA
。
内核初始化过程中,通过core_initcall()函数将该 section 内的初始化 函数遍历执行,其中包括 CMA 的激活入口 cma_init_reserved_areas() 函数, 该函数遍历 CMA 分配的所有 CMA 分区并激活每一个 CMA 分区。在该函数中, 函数首先调用kzalloc()函数为CMA分区的bitmap所需的内存,然后调用init_cma_reserved_pageblock()
函数,在该函数中,内核将CMA区块内的所有物理页都清除RESERVED标志,引用计数设置为0,接着按pageblock的方式设置区域内的页组迁移类型都是MIGRATE_CMA
。函数继续调用set_page_refcounted()函数将引用计数设置为1以及调用__free_pages()
函数将所有的页从CMA分配器中释放并归还给buddy管理器。最后调用adjust_managed_page_count()
更新系统可用物理页总数。至此系统的其他部分可以开始使用CMA分配器分配的连续物理内存。
系统free是否统计CMA内存
结论:free会统计CMA区域内存。
因为CMA区域中的page被设置为MIGRATE_CMA
,然后放入伙伴系统
中,等待用户使用(NOTE:MIGRATE_CMA是伙伴系统中页属性的概念,所以CMA区也只是伙伴系统中的一个概念,不是一个ZONE),这样进行初始化后,free统计时也会将CMA区域的内存统计进去。
CMA预留内存与系统内存关系
结论:CMA区域的内存即是预留内存(reserved),也是系统内存(memory);也就是说CMA区域这部分内存除了设备驱动申请DMA内存使用外,在系统内存不足时可以使用,
系统中何时使用CMA区域内存:
- 设备驱动中主动申请DMA内存时使用,这个每个驱动实现不同由驱动工程师自主控制。
- 系统应用程序的使用,也就是申请内存时如何使用MIGRATE_CMA page?
问题——PFNs busy
1 | [ 136.103382] alloc_contig_range: [22200, 22ef4) PFNs busy |
函数调用栈:
1 | dma_alloc_coherent |
alloc_contig_range
从伙伴系统中分配一定大小的物理页,该接口参数会指定页的起始和结束号。
PFN范围不必是pageblock或MAX_ORDER_NR_PAGES对齐的。PFN范围必须属于一个单独的区域。
这个例程做的第一件事是尝试MIGRATE_ISOLATE范围内的所有页面块。一旦隔离(isolated),页面块不应该被其他人修改。
1 | int alloc_contig_range(unsigned long start, unsigned long end, |
<1>: start_isolate_page_range将pfn范围内的页设置为隔离(MIGRATE_ISOLATE)
- 将页面分配类型设置为
MIGRATE_ISOLATE
意味着将永远不会分配范围内的空闲页面。任何空闲页面和将来释放的页面将不会再次分配。如果指定的范围包括MOVABLE或CMA以外的迁移类型,则会使用-EBUSY失败。为了最终隔离范围内的所有页面,调用者必须释放范围内的全部页面。test_page_isolated()可以用于测试它。 - 请注意,也没有与页面分配器的强同步。当页面块标记为“已隔离”时,页面可能会被释放。在某些情况下,页面可能仍然会出现在pcp列表中,这将允许它们的分配,即使它们实际上已经被隔离。根据调用方需要的保证程度,可能需要drain_all_pages(例如__offline_pages需要在检查隔离范围后调用它,以便下次重试)。
- 一旦页面块被标记为MIGRATE_ISOLATE,我们就从未对齐的范围(即我们感兴趣的页面)迁移页面。这将把范围内的所有页面作为MIGRATE_ISOLATE放回页面分配器。
- 完成此操作后,我们从页面分配器中获取范围内的页面,并将其从伙伴系统中删除。这样,页面分配器将永远不会考虑使用它们。
- 这使我们可以将页面块标记回MIGRATE_CMA/MIGRATE_MOVABLE,以便将对齐范围内的空闲页面(而不是未对齐的原始范围内的)放回页面分配器,以便好友可以使用它们。
- 将页面分配类型设置为
<2>: __alloc_contig_migrate_range申请pfn范围内的页,扫描PFN范围页并寻找可迁移的进行迁移,可能返回BUSY,因为存在页无法迁移。
<3>: lru_add_drain_all对所有CPU实现缓存的刷新,将每CPU中缓存的页面进行释放
<4>: PageBuddy判断一个页是是否在buddy系统中,如果是1,说明还没有分配出去
<5>: test_pages_isolated用于检查确保该pfn范围内的页面已经被隔离
<6>: isolate_freepages_range则是将指定范围的空闲页面隔离出来
<7>: undo_isolate_page_range则是将所有的标记为隔离的页面重新标记为MIGRATE_CMA,至此所需的连续内存页面已经分配到了,无需在乎其迁移属性了,便更改回去。
出现
PFNs busy
是由于在<5>test_pages_isolated中检查到申请pfn范围内的页有没有被隔离出来,才会输出警告信息
而没有被隔离的原因是,这些页可能被系统中的其他应用所占用着,而无法被迁移。
参考
- The Contiguous Memory Allocator
- CMA