Linux内存管理--CMA技术原理分析

adtxl
2023-09-18 / 0 评论 / 30 阅读 / 正在检测是否收录...

转载自https://blog.csdn.net/feelabclihu/article/details/129457653?spm=1001.2014.3001.5502


前言

本文介绍CMA(Contiguous Memory Allocator)技术原理,从源码分析CMA的初始化和分配流程,同时讲解涉及到的页面迁移、LRU(Least Rencntly Used)缓存、PCP(per cpu page)缓存等知识。

1. CMA概述

CMA是什么?为什么需要CMA?

Linux伙伴系统(Buddy)使用 Page 粒度来管理内存,每个页面大小为4K。伙伴系统按照空闲内存块的长度,把内存挂载到不同长度的 free_list链表中。free_list 的单位是以 (2^order个Page) 来递增的,即 1 page、2 page、… 2^n,通常情况下最大 order 为10 对应的空闲内存大小为 4M bytes。我们使用伙伴系统来申请连续的物理页面最大的页面最大小4M bytes,且系统内存碎片化严重的时候,也很难分配到高order的页面。

imagedda76fa1af8b7958.png

嵌入式系统上一些外设设备,如GPU、Camera,HDMI等都需要预留大量连续内存才能正常工作,且很多情况下仅4M连续内存是不足以满足设备的需求的,当然我们也可以使用memblock预留内存的方法来保留更大的连续内存,但这部分内存只能被设备所使用而Buddy使用不到,会导致内存浪费。CMA由此而生,我们即要能分配连续的大的内存空间给设备使用,平时设备不用时又要把内存给系统用,最大化利用内存。

CMA连续内存分配器,主要是为了可用于分配连续的大块内存。系统初始化的时候会通过保留一片物理内存区域,平时设备驱动不用时,内存管理系统将该区域用于分配和管理可移动类型页面,提供给APP或者内核movable页面使用。设备驱动使用时,此时已经分配的页面则进行迁移走,该区域用于连续内存分配。

在后续的章节中,我们主要通过阅读源码的方式,来介绍CMA的初始化、分配和页面迁移流程等。

注:后续文中所贴源码为kernel5.4版本,代码截图会省略次要代码,只保留关键代码。

2. CMA主要数据结构和API

2.1 struct cma

使用struct cma来描述一个CMA区域:

imagecf7c07a20d66b101.png

base_pfn:CMA区域物理地址的起始page frame number(页帧号)
count: CMA区域的页面数量
bitmap:描述cma区域页面的分配情况,1表示已分配,0为空闲。
order_per_bit:表示bitmap中一个bit所代表的页面数量(2^order_per_bit)。

2.2 cma_init_reserved_mem

image4a5b9f500f67a927.png

从保留内存块里面获取一块地址为base、大小为size的内存,用来创建和初始化struct cma。

2.3 cma_init_reserved_areas

cma_init_reserved_areas()-->cma_activate_area()-->init_cma_reserved_pageblock()-->set_pageblock_migratetype(page, MIGRATE_CMA)

为了提升内存利用率,该函数用来将这CMA内存标记后归还给 buddy 系统,供 buddy作为可移动页面内存申请。

2.4 cma_alloc

image84efb196af2b65e6.png

用来从指定的CMA 区域上分配count个连续的页面,按照align对齐。

2.5 cma_release

imagedac7e8d4fd1aac48.png

用来释放已经分配count个连续的页面。

3. CMA主要流程分析

3.1 CMA初始化流程

3.1.1 系统初始化:

系统初始化过程需要先创建CMA区域,创建方法有:dts的reserved memory或者通过命令行参数。这里我们看经常使用的通过dts的reserved memory方式,物理内存的描述放置在dts中配置,比如:

imagebe00df69b0aece8e.png

linux,cma 为CMA 区域名称。

compatible须为“shared-dma-pool”。

resuable 表示 cma 内存可被 buddy 系统使用。

size 表示cma区域的大小,单位为字节

alignment指定 CMA 区域的地址对齐大小。

linux,cma-default 属性表示当前 cma 内存将会作为默认的cma pool 用于cma 内存的申请。

在系统启动过程中,内核对上面描述的dtb文件进行解析,从而完成内存信息注册,调用流程为:

setup_arch

       arm64_memblock_init

              early_init_fdt_scan_reserved_mem

                   __reserved_mem_init_node

imagee226c3164bc922b3.png

__reserved_mem_init_node会遍历__reservedmem_of_table section中的内容,检查到dts中有compatible匹配(CMA这里为“shared-dma-pool”)就进一步执行对应的initfn。通过RESERVEDMEM_OF_DECLARE定义的都会被链接到__reservedmem_of_table这个section段中,最终会调到使用RESERVEDMEM_OF_DECLARE定义的函数,如下rmem_cma_setup:

image137f8657f99ed7fd.png

3.1.2 rmem_cma_setup

imagec8d6e7af1fbb80a5.png

  1. cma_init_reserved_mem 从保留内存块里面获取一块地址为base、大小为size的内存,这里用dtb中解析出来的地址信息来初始化CMA,用来创建和初始化struct cma,代码很简单:
  2. 如果dts指定了linux,cma-default,则将dma_contiguous_set_default指向这个CMA区域,使用dma_alloc_contiguous从CMA分配内存时,默认会从该区域分。

执行到此, CMA和其它的保留内存是一样的,都是放在 memblock.reserved 中,这部分保留内存一样没能被 Buddy 系统用到。前面讲过为了提升内存利用率,还需要将CMA这部分内存标记后归还给 Buddy系统,供 Buddy作为可移动页面提供给APP或内核内存申请,由cma_init_reserved_areas来实现。

3.1.3 cma_init_reserved_areas

在内核初始化的后期会调用core_initcall描述的初始化函数:

cma_init_reserved_areas,它直接调用cma_activate_area来实现。cma_activate_area根据cma大小分配bitmap,然后循环调用init_cma_reserved_pageblock来操作CMA区域中所有的页面, 看下源码:

image51dc510546e85494.png

@1 CMA区域由一个bitmap来管理各个page的状态,cma_bitmap_maxno计算Bitmap需要多少内存,i变量表示该CMA eara有多少个pageblock(4M)。

@2 遍历该CM区域中的所有的pageblock

@3 确保CMA区域中的所有page都是在一个zone内

@4 最终调用init_cma_reserved_pageblock,以pageblock为单位进行处理,设置migrate type为MIGRATE_CMA,将页面添加到伙伴系统中并更新zone管理的页面总数。如下:

image7497806d0afae010.png

@1 将页面已经设置的reserved标志位清除掉。

@2 将migratetype设置为MIGRATE_CMA

@3 循环调用__free_pages函数,将CMA区域中所有的页面都释放到buddy系统中。

@4 更新伙伴系统管理的内存数量。

执行到此,后续这部分CMA内存就可以为buddy所申请。在伙伴系统中migratetype为movable并且分配flag带CMA,可以从CMA分配内存:

image9e8e45203b1aeab1.png

3.2 CMA分配流程

在阅读cma分配流程代码前,我们先看下它的函数调用流程,后面将通过源码对流程及各个函数进行分析。

imageb23f7fbfe49af2bd.png

3.2.1 cma_alloc

image1b9062daa2b99a9c.png

@1 bitmap的计算,主要是获取bimap最大的可用bit数(bitmap_maxno),此次分配需要多大的bitmap(bitmap_count)等。

@2 根据上面计算得到的bitmap信息,从bitmap中找到一块空闲的位置。

@3 一些特别情况(在后面会讲到)经常会导致CMA分配失败,当分配返回EBUSY时,需要msleep(100)再retry,默认会retry 5次。

@4 将要分配的页面的对应bitmap先置位为1,表示已经分配了。

@5 使用alloc_config_range来进行内存分配,在后面节详细分析。

@6 分配失败则清除掉bitmap。

3.2.2 内核中的“批处理”:LRU缓存和PCP缓存

在分析alloc_config_range之前,先插讲两个知识点LRU缓存和PCP缓存,在阅读内核源码中,我们会发现内核很喜欢使用一些“批处理”的方法来提升效率,减少一些拿锁开销。

1.LRU 缓存

经典的LRU(Least Rencntly Used)链表算法如下图:

imagecca93c963c2ede05.png

注:详细的LRU算法介绍可以参考内核工匠之前的文章:kswapd介绍

新分配的页面不断地加入ACTIVE LRU链表中,同时ACTIVE LRU链表也不断地取出将页面放入 INACTIVE LRU链表。链表中的锁(pgdat->lru_lock)竞争力度是非常强烈的,如果页面转移是一个一个进行的,那对锁的竞争将会十分严重。

为了改善这种情况,内核加入了一个 PER-CPU的 LRU缓存(用 struct pagevec 表示),页面要加入LRU链表会先放入当前 CPU 的LRU缓存 中,直到LRU缓已经满了(一般为15个页面),再获取lru_lock,一次性将这些页面批量放入LRU链表。

2.PCP(PER-CPU PAGES)缓存

由于内存页面属于公共资源,系统中频繁分配释放页面,会因为获得释放锁(zone->lock),CPU之间的同步操作产生大量消耗。同样为了改善这种情况,内核加入了per cpu page 缓存(struct per_cpu_pages表示),每个CPU都从Buddy批发申请少量的页面存放在本地。当系统需要申请内存时,优先从PCP缓存拿,用完了再从buddy批发。释放时也优先放回该PCP缓存,缓存满了再放回buddy系统。

内核之前只支持order=0的PCP,社区最新已经有补丁可以支持order>0的per-cpu。

imagec2cb345c152d6363.png

3.2.3 alloc_config_range函数:

继续看cma_alloc流程的alloc_config_range要干哪些事情:

简而言之,目的就是想从一块“脏的”连续内存块(已经被各种类型的内存使用),得到一块干净的连续内存块,要么是回收掉,要么是迁移走,最后将这块干净的连续内存返回给调用者使用,如下图:

image38245239f39474fa.png

走读下代码:

image8c2b8baf1d27d26f.png

@1 start_isolate_page_range:将目标内存块的pageblock 的迁移类型由MIGRATE_CMA 变更为 MIGRATE_ISOLATE。因为buddy系统不会从 MIGRATE_ISOLATE 迁移类型的pageblock 分配页面,可以防止在cma分配过程中,这些页面又被人从Buddy分走。

@2 drain_all_pages:回收per-cpu pages,前面已经有介绍过PCP,回收过程需要先将放在PCP缓存页面归还给Buddy。

@3 __alloc_contig_migrate_range:将目标内存块已使用的页面进行迁移处理,迁移过程就是将页面内容复制到其他内存区域,并更新对该页面的引用。

imagee7c8d10423d557c4.png

@3.1 lru_cache_disable: 因为在LRU缓存的页面是无法迁移的,需要先将pagevec页面刷到LRU,即将准备添加到LRU链表上,却还未加入LRU的页面(还待在LRU缓存)添加到LRU上,并关闭LRU缓存功能。

@3.2 isolate_migratepages_range 隔离要分配区域已经被Buddy使用的page,存放到cc的链表中,返回的是最后扫描并处理的页框号。这里隔离主要是防止后续迁移过程,page被释放或者被LRU回收路径使用。

@3.3 reclaim_clean_pages_from_list:对于干净的文件页,直接回收即可。

@3.4 migrate_pages:该函数是页面迁移在内核态的主要接口,内核中涉及到页面迁移的功能大都会调到,它把可移动的物理页迁移到一个新分配的页面。

在下一节详细介绍它。

@3.5 lru_cache_enable迁移过程完成,重新使能LRU PAGEVEC

@4.undo_isolate_page_range: @1的逆过程pageblock的迁移类型从 MIGRATE_ISOLATE 恢复为 MIGRATE_CMA。

最后将这些页面返回给调用者。

3.3 CMA释放流程

cma_release释放CMA内存的代码很简单,就是把页面重新free给Buddy和清楚到cma的bitmap分配标识,这里直接贴一下代码:

imagea6369ea21c074c02.png

4. 页面迁移

系统要使用CMA区域的内存,内存上的页面必须是可迁移的,这样子当设备要使用CMA时页面才能迁移走,那么哪些页面可以迁移呢?有两种类型:

  1. LRU上的页面,LRU链表上的页面为用户进程地址空间映射的页面,如匿名页和文件页,都是从buddy分配器migrate type为movable的pageblock上分来的。
  2. 非LRU上,但是是movable页面。非LRU的页面通常是为kernel space分配的page,要实现迁移需要驱动实现page->mapping->a_ops中的相关方法。比如我们常见的zsmalloc内存分配器的页面就支持迁移。

migrate_pages()是页面迁移在内核态的主要接口,内核中涉及到页面迁移的功能大都会调到它。如下图,migrate_pages()无非是要分配一个新的页面,断开旧页面的映射关系,重新简历映射到新的页面,并且要拷贝旧页面的内容到新页面、新页面的struct page属性要和旧页面设置得一样,最后释放旧的页面。下面来阅读下它的源码。

image1da55b4ce34778cf.png

4.1 migrate_pages:

migrate_pages函数和参数:

image09fb08c0cb6ad880.png

from: 准备迁移页面的链表

get_new_page:申请新页面函数的指针

putnew_page:释放新页面函数的指针

private:传给get_new_page的参数,CMA这里没有使用到传NULL

mode:迁移模式,CMA的迁移模式会设置为MIGRATE_SYNC。共有下面几种:

image4f9e305e0521bd42.png

reason:迁移原因,记录是什么功能触发了迁移的行为。因为内核许多路径都需要用migrate_pages来迁移比如还有内存规整、热插拔等。CMA传递的为MR_CONTIG_RANG,表示调用alloc_contig_range()分配连续内存。

再看migrate_pages代码,它遍历 from链表,对每个page调用unmap_and_move来实现迁移处理。

image167a18dfcc60ad25.png

4.2 unmap_and_move

imageb8e1d64d8e902bdf.png

unmap_and_move函数的参数同migrate_pages一模一样,它调用get_new_page分配一个新页面,然后使用__unmap_and_move迁移页面到这个新分配的页面中,我们主要看下__unmap_and_move

4.3 __unmap_and_move

image873993b3a6a849cf.png

@1尝试获取old page的页面锁PG_locked,若页面已经被其它进程持有了锁,则这里会尝试获取锁失败,对于MIGRATE_ASYNC模式的为异步迁移拿不到锁就直接跳过此页面。CMA迁移模式为MIGRATE_SYNC,这里一定使用lock_page一定要等到锁。

@2处理正在回写的页面,根据迁移模式判断是否等待页面回写完成。MIGRATE_SYNC_LIGHT和MIGRATE_ASYNC不等待,cma迁移模式为MIGRATE_SYNC,会调用wait_on_page_writeback()函数等待页面回写完成。

@3 对于匿名页,为了防止迁移过程anon_vma数据结构被释放了,需要使用page_get_anon_vma增加anon_vma->refcount引用计数。

@4 获取new page的页面锁PG_locked,正常情况都能获取到。

@5 判断这个页面是否属于非LRU页面,

如果页面为非LRU页面,则通过调用move_to_new_page来处理,该函数会回调驱动的miratepage函数来进行页面迁移。

如果是LRU页面,继续执行@6

image4572abdb00474814.png

@6 通过page_mapped()判断是否有用户PTE映射了改页面。如果有则调用try_to_unmap(),通过反向映射机制解除old page所有相关的PTE。

@7 调用move_to_new_page,拷贝old page的内容和struct page属性数据到new page。对于LRU页面 move_to_new_page是通过调用migrate_page做了两件事:复制struct page的属性和页面内容。

@8对页表进行迁移:remove_migration_ptes通过反向映射机制建立new page到进程的映射关系。

@9 迁移完成释放old、new页面的PG_locked,当然对于匿名页我们也要put_anon_vma减少的anon_vma->refcount引用计数

@10 对于非LRU页面,调用put_page,释放old page引用计数(_refcount减1)

对于传统LRU putback_lru_page把newpage添加到LRU链表中。

4.4 move_to_new_page

在@5和@7中,非LRU和LRU页面都是通过move_to_new_page来复制页面,我们来看下他的实现:

image862b0d8f3dced21e.png

1.对于非LRU页面,该函数会回调驱动的miratepage函数来进行页面迁移

比如在zsmalloc内存分配器会注册迁移回调函数,迁移流程这里会调用到zsmalloc的zs_page_migrate来迁移其申请的页面。zsmalloc内存分配器这里就不展开讲了,有兴趣的读者可以阅读zsmalloc的源码。

image811cf5e7d87d6618.png

2.对于LRU页面,调用migrate_page做了两件事:复制struct page的属性和页面内容。

imageeb534294891306ca.png

@7.1 struct page属性的复制:

migrate_page_move_mapping 要先检查page的refcount是否符合预期,符合后之后会复制页面的映射数据,比如page->index、page->mapping以及PG_swapbacked

image7909db349e95237b.png

这里顺带提一下refcount:refcount是struct page中重的引用计数,用来表示内核中引用改页面的次数。当refcount=0,表示该页面为空闲页面或即将要被释放的页面。当refcount的值>0,表示该页面已经被分配了且内核正在使用,暂时不会被释放。

内核中使用get_page、pin_user_pages、get_user_pages等函数来增加_refcount的引用计数,可以防止在进行某些操作过程(比如添加入LRU)页面被其它路径释放了,同时他也会导致refcount不符合预期,也就是在这里不能迁移。

@7.2 page页面内容的复制:

copy_highpage就很简单了,使用kmap映射两个页面,再将旧页面的内存复制到新的页面。

image07594d028281aacc.png

@7.3 migrate_page_states用来复制页面的flag,如PG_dirty,PG_XXX等标志位,也是属于struct page属性的复制。

4.5 小结:

整个迁移过程已经分析完,画出流程图如下,

image545068332ceaf4b5.png

五、总结

从上面章节分析,我们可以看到CMA的设计都是围绕这两点来做的:

  1. 平时设备驱动不用时,CMA内存交给Buddy管理,这是在初始化流程cma_init_reserved_areas()或cma_release()来实现的。

2.设备驱动要使用时,通过cma_alloc来申请物理连续的CMA内存。对于已经在Buddy被APP或者内核movable分配走了的页面,要通过回收或迁移将这块内存清理“干净”,最后将这块物理连续“干净”的内存返回给设备驱动使用。核心实现在alloc_config_range()和migrate_pages()函数。

0

评论 (0)

取消