简介

CMA的全称是contiguous memory allocator, 其工作原理是:预留一段的内存给驱动使用,但当驱动不用的时候,memory allocator(buddy system)可以分配给用户进程用作匿名内存或者页缓存。而当驱动需要使用时,就将进程占用的内存通过回收或者迁移的方式将之前占用的预留内存腾出来, 供驱动使用。本文对CMA的初始化,分配和释放做一下源码分析(源码版本v3.10).

初始化

CMA的初始化必须在buddy 物理内存管理初始化之前和memory
  block early allocator分配器初始化之后(可参考dma_contiguous_reserve函数的注释:This function reserves memory from early allocator. It should be called by arch specific code once the early allocator (memblock or bootmem) has been activated and all other subsystems have already allocated/reserved memory.)。
  
在ARM中,初始化CMA的接口是:dma_contiguous_reserve(phys_addr_t limit)。参数limit是指该CMA区域的上限。

setup_arch->arm_memblock_init->dma_contiguous_reserve:

在该函数中,需要弄清楚俩值,分别是selected_size和limit。selectetd_size是声明CMA区域的大小,limit规定了CMA区域在分配时候的上界。

首先介绍下怎样获得selected_size: 若cmdline中定义了cma=”xxx”,那么就用cmdline中规定的(114行)。若cmdline中没有定义,则看有没有在config文件中定义CONFIG_CMA_SIZE_SEL_MBYTES(117行)或者CONFIG_CMA_SIZE_SEL_PERCENTAGE(119行)。如果前面两个配置项都没有定义,则从CONFIG_CMA_SIZE_MBYTES和CONFIG_CMA_SIZE_PERCENTAGE中选择两者的最小值(121行)或者最大值(123行)。
计算好CMA的size并得到了limit后,就进入dma_declare_contiguous中。

setup_arch->arm_memblock_init->dma_contiguous_reserve->dma_declare_contiguous:

在该函数中,首先根据输入的参数size和limit,得到CMA区域的基址和大小。基址若没有指定的话(在该初始化的情境中是0),就需要用early allocator分配了。而大小需要进行一个alignment,这个alignment一般是4MB(250行,MAX_ORDER是11, pageblock_order是10)。用early allocator分配的这个CMA会从物理内存lowmem的高地址开始分配。
得到CMA区域的基址和大小后,会存入到cma_reserved[]全局数组中(280~282行)。全局变量cma_reserved_count来标识在cma_reserved[]数组中,保留了多少个cma区(283行)
在ARM的kernel code中,得到的CMA区域还会保存到dma_mmu_remap数组中(这个dma_mmu_remap数据结构只记录基址和大小,下面396~410行)。

以上,只是将CMA区域reserve下来并记录到相关的数组中。当buddy系统初始化结束后,会对reserved的CMA区域进行进一步的处理:

在之前CMA初始化的时候,看到其base和size都会pageblock_order对齐。pageblock_order的值是10,即一个pageblock_order代表4MB的内存块(2^10 * PAGE_SIZE)。因此,该函数cma_activate_area是对每一个用于CMA的block进行初始化(140行,155行)。由于CMA规定了,其区域内的页面必须在一个zone中,因此149~153行对每一个页面进行甄别是否都在同一个zone中,然后对CMA区域内的每一个pageblock进行初始化。

cma_init_reserved_areas->cma_create_area-> cma_create_area-> init_cma_reserved_pageblock:

进入buddy的空闲页面其page->_count都需要为0.因此在778行设置pageblock 区内的每一个page的使用技术都为0.而781行将一个pageblock块的第一个page的_count设置为1的原因是在783行的__free_pages的时候会put_page_testzero减1. 同时还需要设置pageblock的第一个页面的migratetype为MIGRATE_CMA. 所有页面都有一个migratetype放在zone->pageblock_flags中,每个migratetype占3个bit,但对于buddy system中的pageblock的第一个页面的migratetype才有意义(其他页面设置了也用不上)。
对页面的初始化做完后,就通过__free_pages将其存放在buddy system中(783行)。

由此可见在初始化的时候,所有的CMA都放在order为10的buddy链表中,具体放在相关zone的zone->free_area[10].free_list[MIGRATE_CMA]链表上。

分配

CMA并不直接开放给driver的开发者。开发者只需要在需要分配dma缓冲区的时候,调用dma相关函数就可以了,例如dma_alloc_coherent。最终dma相关的分配函数会到达cma的分配函数:dma_alloc_from_contiguous

301~304行的注释,告诉该函数的目的是从特定的driver(或者系统默认)CMA中分配一段buffer. 310行是获取特定driver的CMA区域,若dev没有对应的CMA,则从系统默认的CMA区中查找。每一个CMA区域都有一个bitmap用来记录对应的page是否已经被使用(struct cma->bitmap)。因此从CMA区域查找一定数量的连续内存页的方法就是在cma->bitmap中查找连续的N个为0的bit,代表连续的N个物理页。若找到的话就返回一个不大于CMA边界的索引(333行)并设置对应的cma->bitmap中的bit位(339行)。
dma_alloc_from_contiguous-> dma_alloc_from_contiguous:

该函数的注释中讲述了调用该函数需要注意的事项:对齐,所分配的页面都在一个zone中。释放时,需要使用free_contig_range。
在5967行,先对始末区间进行对齐,然后通过start_isolate_page_range,先确认该区间内没有unmovable的页,如果有unmovable的页,那unmovable的页占着内存而不能被迁移,导致整个区间就都不能被用作CMA(start_isolate_page_range->set_migratetype_isolate->has_unmovable_pages)。确认没有unmovable页后,将该区间的pageblock标志为MIGRATE_ISOLATE。并将对应的page在buddy中都移到freearea[].free_list[MIGRATE_ISLOATE]的链表上,并调用drain_all_pages(start_isolate_page_range->set_migratetype_isolate),将每处理器上暂存的空闲页都释放给buddy(因为有可能在要分配的CMA区间中有页面还在pcp的pageset中—pageset记录了每cpu暂存的空闲热页)。然后通过5973行的__alloc_contig_migrate_range,将被隔离出的页中,已经被buddy分配出去的页摘出来,然后迁移到其他地方,以腾出物理页给CMA用。腾出连续的物理页后,便会通过6017行的isolate_freepages_range来将这段连续的空闲物理页从buddy
  system取下来。
__alloc_contig_migrate_range的代码如下:
dma_alloc_from_contiguous-> dma_alloc_from_contiguous-> __alloc_contig_migrate_range:

该函数主要进行迁移工作。由于CMA区域的页是允许被buddy system当作movable页分配出去的,所以,如果某些页之前被buddy分配出去了,但在cma->bitmap上仍然记录该页可以被用作CMA,所以这时候就需要将该页迁移到别的地方以将该页腾出来供CMA用。
5882行做的事情是,将被buddy分配出去的页挂到cc->migratepages的链表上。然后通过5894行的reclaim_clean_pages_from_list看是否某些页是clean可以直接回收掉,之后在通过5898行的migrate_pages将暂时不能回收的内存内容迁移到物理内存的其他地方。
隔离需要迁移和回收页的函数isolate_migratepages_range如下:
dma_alloc_from_contiguous-> dma_alloc_from_contiguous-> __alloc_contig_migrate_range-> isolate_migratepages_range:

上面函数的作用是在指定分配到的CMA区域的范围内将被使用到的内存(PageLRU(Page)不为空)隔离出来挂到cc->migratepages链表上,以备以后迁移。要迁移的页分两类,一类是可以直接被回收的(比如页缓存),另一类是暂时不能被回收,内容需要迁移到其他地方。可以被回收的物理页流程如下:
dma_alloc_from_contiguous-> dma_alloc_from_contiguous-> __alloc_contig_migrate_range-> reclaim_clean_pages_from_list:

在该函数中,对于那些用于文件缓存的页,如果是干净的,就进行直接回收(981~989行),下次该内容需要被用到,再次从文件中读取就是了。如果由于一些原因不能被回收掉的,那就挂回cc->migratepages链表上进行迁移(990行)。
迁移页的流程如下:
dma_alloc_from_contiguous-> dma_alloc_from_contiguous-> __alloc_contig_migrate_range-> migrate_pages:

该函数是先进行unmap,然后再进行move(881行),这个move实际上是一个copy动作(__unmap_and_move->move_to_new_page->migrate_page->migrate_page_copy)。随后将迁移后老页释放到buddy系统中(905行)。
至此,CMA分配一段连续空闲物理内存的准备工作已经做完了(已经将连续的空闲物理内存放在一张链表上了cc->freepages)。但这段物理页还在buddy 系统上。因此,需要把它们从buddy 系统上摘除下来。摘除的操作并不通过通用的alloc_pages流程,而是手工进行处理(dma_alloc_from_contiguous-> dma_alloc_from_contiguous –>isolate_freepages_range)。在处理的时候,需要将连续的物理块进行打散(order为N->order为0),并将物理块打头的页的page->lru从buddy的链表上取下。设置连续物理页块中的每一个物理页的struct page结构(split_free_page函数),设置其在zone->pageblock_flags迁移属性为MIGRATE_CMA。

释放

释放的流程比较简单。同分配一样,释放CMA的接口直接给dma。比如,dma_free_coherent。它会最终调用到CMA中的释放接口:free_contig_range。
6035 void free_contig_range(unsigned long pfn, unsigned nr_pages) 6036 { 6037 unsigned int count = 0; 6038 6039 for (; nr_pages–; pfn++) { 6040 struct page *page = pfn_to_page(pfn); 6041 6042 count += page_count(page) != 1; 6043 __free_page(page); 6044 } 6045 WARN(count != 0, “%d pages are still in use!\n”, count); 6046 }
直接遍历每一个物理页将其释放到buddy系统中便是了(6039~6044行)。

小结

CMA的使用避免了因内存预留给指定驱动而减少了系统可用内存的缺点。其CMA内存在驱动不用的时候可以分配给用户进程使用,而当其需要被驱动用作DMA传输时,将之前分配给用户进程的内存通过回收或者迁移的方式腾给驱动使用。 [1]