LINUX CMA 详细分析

2019-07-13 08:31发布

CMA是什么

在我们使用ARM等嵌入式Linux系 统的时候,一个头疼的问题是GPU,Camera,HDMI等都需要预留大量连续内存,这部分内存平时不用,但是一般的做法又必须先预留着。通过CMA机制,我们可以做到不预留内存,这些内存平时是可用的,只有当需要的时候才被分配给Camera,HDMI等设备。(本段纯属百度,阅完即可,不必惦记)
如果对CMA的实现过程有兴趣可以百度或者google。我在这里给大家来点实在的!!

CMA段内存到底是reserved 还是 memory的?内核下free统计值是否包含CMA内存?

Cma段内存既是reserved又是memory的。但更像是一段普通的memory。
start_kernel -> setup_arch -> arm_memblock_init -> dma_contiguous_reserve -> dma_declare_contiguous
在dma_declare_contiguous函数中,调用__memblock_alloc_base得到内存。这里只是将内存给扣了出来。
内存扣出来做什么?当然是留出来用哦!但CMA初始化中做了一个有趣的动作! void __init init_cma_reserved_pageblock(struct page *page) { unsigned i = pageblock_nr_pages; struct page *p = page; do { __ClearPageReserved(p); set_page_count(p, 0); } while (++p, --i); set_page_refcounted(page); set_pageblock_migratetype(page, MIGRATE_CMA); __free_pages(page, pageblock_order); totalram_pages += pageblock_nr_pages; #ifdef CONFIG_HIGHMEM if (PageHighMem(page)) totalhigh_pages += pageblock_nr_pages; #endif } #endif 在这个函数中,先将CMA区中的page设置为MIGRATE_CMA,然后放入伙伴系统中,等待用户使用(NOTE:MIGRATE_CMA是伙伴系统中页属性的概念,所以CMA区也只是伙伴系统中的一个概念,不是一个ZONE)。
这样一初始化后,free统计时也会将CMA区的内存统计进去

用户态程序何时分配MIGRATE_CMA页框?

首先让我们看下标志位__GFP_MOVABLE的定义 #define __GFP_MOVABLE ((__force gfp_t)___GFP_MOVABLE) #define ___GFP_MOVABLE 0x08u 这个标志位在很多地方都有使用,如磁盘文件系统分配页缓存、用户态程序分配堆栈空间等。
那我们在来看下,此类标志位是怎么影响页面分配的?
分配页面的其余流程的先不做分析,我们直接从分配物理页面的入口函数__alloc_pages_nodemask来入手。
在__alloc_pages_nodemask函数中,有个非常重要的变量—页分配上下文,具体初始化代码: struct alloc_context ac = { .high_zoneidx = gfp_zone(gfp_mask), .zonelist = zonelist, .nodemask = nodemask, .migratetype = gfpflags_to_migratetype(gfp_mask), }; 在此上下文中,调用gfpflags_to_migratetype函数对页迁移类型进行初始化。 static inline int gfpflags_to_migratetype(const gfp_t gfp_flags) { VM_WARN_ON((gfp_flags & GFP_MOVABLE_MASK) == GFP_MOVABLE_MASK); BUILD_BUG_ON((1UL << GFP_MOVABLE_SHIFT) != ___GFP_MOVABLE); BUILD_BUG_ON((___GFP_MOVABLE >> GFP_MOVABLE_SHIFT) != MIGRATE_MOVABLE); if (unlikely(page_group_by_mobility_disabled)) return MIGRATE_UNMOVABLE; /* Group based on mobility */ return (gfp_flags & GFP_MOVABLE_MASK) >> GFP_MOVABLE_SHIFT; } 此函数gfp标志位转换为页迁移类型。转换关系如下: __GFP_RECLAIMABLE ---> MIGRATE_RECLAIMABLE __GFP_MOVABLE ---> MIGRATE_MOVABLE page_group_by_mobility_disabled 或者以上两种都不是 —> MIGRATE_UNMOVABLE 这里的代码说明很重要的一点,在页面分配时,上层只能明确说明分配到这三种迁移类型的页面,尽管迁移类型远不止三种类型。
上层页面标志位中,只有__GFP_RECLAIMABLE和__GFP_MOVABLE影响分配时的页框迁移属性,也可不做设置,默认为MIGRATE_UNMOVABLE属性。 分配页框的调用链: __alloc_pages_nodemask -> get_page_from_freelist -> buffered_rmqueue -> rmqueue_bulk -> __rmqueue -> __rmqueue_smallest get_page_from_freelish将migratetype传给下层函数,__rmqueue_smallest函数去zone->free-area[order]->free_list[migratetype] 页框链表中分配物理页面。 从上面可知,上层函数分配页框时,不能明确的指定分配MIGRATE_CMA属性的页框(除dma_alloc_from_contiguous函数接口)。 我们都知道内核启动时,会将内存分为几个zone,常见的如DMA,NORMAL zone。
但是我们分配物理内存时,是去伙伴系统上进行“摘”页框的。zone->free_area[order]上一般会存在多种迁移属性类型页框链表。
页框链表的迁移类型: enum { MIGRATE_UNMOVABLE, MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_PCPTYPES, MIGRATE_RESERVE = MIGRATE_PCPTYPES, #ifdef CONFIG_CMA MIGRATE_CMA, #endif #ifdef CONFIG_MEMORY_ISOLATION MIGRATE_ISOLATE, #endif MIGRATE_TYPES }; CMA区域内存初始化完成后,挂载DMA的free_area[order]->free_list[MIGRATE_CMA]链表中。 当用户态程序分配堆时,malloc或者mmap都只是会分配一个虚拟内存,并不直接分配物理内存。当用户态程序访问虚拟内存时,突然发现,哎。。。不能访问。那就整个异常吧。就这样,一个缺页异常就生成了。内核将进入缺页异常处理中。 在缺页异常流程中,内核会为堆分配匿名页,进入do_anonymous_page函数,在此函数中调用alloc_zeroed_user_highpage_movable分配物理内存。 static inline struct page * alloc_zeroed_user_highpage_movable(struct vm_area_struct *vma, unsigned long vaddr) { return __alloc_zeroed_user_highpage(__GFP_MOVABLE, vma, vaddr); } 这里给了分配页函数一个 __GFP_MOVABLE 标志位,分配物理页面时首先分配zone->free-area[order]->free_list[MIGRATE_ MOVABLE]中的页框。这里会有种情况,万一MIGRATE_ MOVABLE属性页框没有了,内核该怎么办?
这时看看伙伴系统的fallback机制;
先看下fallbacks的定义: static int fallbacks[MIGRATE_TYPES][4] = { [MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_RESERVE }, [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_RESERVE }, #ifdef CONFIG_CMA [MIGRATE_MOVABLE] = { MIGRATE_CMA, MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE }, [MIGRATE_CMA] = { MIGRATE_RESERVE }, /* Never used */ #else [MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE }, #endif [MIGRATE_RESERVE] = { MIGRATE_RESERVE }, /* Never used */ #ifdef CONFIG_MEMORY_ISOLATION [MIGRATE_ISOLATE] = { MIGRATE_RESERVE }, /* Never used */ #endif }; fallback大概意思:如果指定链表上的物理页框没有了,那就按照fallbacks定义的顺序依次查找同zone下其他页框链表,直到遍历完所以页框链表或找到物理页框返回。(具体实现见page_alloc.c 中__rmqueue_fallback 函数)
上面可以看到,如果MIGRATE_MOVABLE 页框链表没有页框后,将第一个访问MIGRATE_CMA 页框链表,在MIGRATE_CMA上分配物理页框。从上面可以看出,只有MIGRATE_MOVABLE才会fallback MIGRATE_CMA链表,分配CMA区域内存。

如ext2等磁盘文件系统为什么可以分配到CMA的内存,而ramfs不能?

我们都知道linux上文件都是通过inode节点对文件进行管理。在创建文件的时候,都会为文件分配一个inode节点。以下是ext2文件上的inode节点创建调用链。 ext2_create -> ext2_new_inode -> new_inode -> new_inode_pseudo -> alloc_inode 在alloc_inode函数中通过kmem_cache_alloc函数申请内存,然后调用inode_init_always函数进行和文件系统无关的初始化。
在inode_init_always函数中
mapping_set_gfp_mask(mapping, GFP_HIGHUSER_MOVABLE);
将inode 的页缓存结构的flasg设置为GFP_HIGHUSER_MOVABLE. GFP_HIGHUSER_MOVABLE的定义为: #define GFP_HIGHUSER_MOVABLE (GFP_HIGHUSER | __GFP_MOVABLE) 从ext2文件系统中,读取一个文件大致调用调用链:
(首次读取) do_sync_read -> generic_file_aio_read -> do_generic_file_read -> page_cache_sync_readahead -> ondemand_readahead -> __do_page_cache_readahead -> read_pages -> ext2_readpages ext2_readpages会去磁盘中读取数据,并填充到page中。在此之前,page在 __do_page_cache_readahead函数中进行分配。 //__do_page_cache_readahead -> page_cache_alloc_readahead static inline struct page *page_cache_alloc_readahead(struct address_space *x) { return __page_cache_alloc(mapping_gfp_mask(x) | __GFP_COLD | __GFP_NORETRY | __GFP_NOWARN); } 使用inode的页缓存的flags进行页分配。从inode的创建过程,我们可以看到分配页的过程存在用到__GFP_MOVABLE属性,在MIGRATE_MOVABLE属性伙伴系统链表分配完后,会首先fallback MIGRATE_CMA的,分配页框时,首先分配zone -> free_aera[order] -> free_list[MIGRATE_MOVABLE]的页框,如果此链表上已经无页框可用时,fallback到MIGRATE_CMA链表分配页框。 其他大部分非易失性储存介质文件系统都可以分配MIGRATE_CMA的页框。 下面看看ramfs为什么不能分配MIGRATE_CMA页面?
文件系统创建inode都是调用new_inode 接口实现。
Ramfs创建一个inode过程, ramfs_create -> ramfs_mknod -> ramfs_get_inode -> new_inode 和ext2文件系统不同在于,当new_inode成功返回一个inode后,在ramfs_get_inode函数中,会对inode得页缓存属性进行修改: mapping_set_gfp_mask(inode->i_mapping, GFP_HIGHUSER); /* #define GFP_HIGHUSER (GFP_USER | __GFP_HIGHMEM) */ 不再拥有__GFP_MOVABLE页缓存属性。在__alloc_pages_nodemask中分配页框上下文migratetype初始化为MIGRATE_UNMOVABLE。 在分配物理页面时,不会分配MIGRATE_CMA的页面,ramfs在写数据时,分配物理页框,读取数据时不会分配物理页框。ramfs写数据时分配物理调用链: vfs_write -> do_sync_write -> generic_file_aio_write -> __generic_file_aio_write -> generic_file_buffered_write -> simple_write_begin -> grab_cache_page_write_begin -> grab_cache_page_write_begin -> __page_cache_alloc –> __alloc_pages_nodemask

光说不练,假把戏:

NOTE:
1:都在imx6单板上测试的,imx6单板启动后只有一个DMA区。
2:linux 3.10 中,/proc/meminfo 没有 free_cma段:需自己添加。

实验一:ramfs 不会分配CMA内存

方法:
Ramfs不能分配MIGRATR_CMA区域内存,
dd if=/dev/zero of=/dd.txt bs=1M count=1000
现象:出现oom,并且有meminfo可以看出,还有很多内存(比CMA free 大上几兆)。

实验二:磁盘文件系统如ext2页缓存会在CMA区域中,分配物理内存

方法:
1:使用实验一的方法,将非CMA区域内存,基本消耗完。
2:使用在外部设备上挂载文件系统。
3:在文件系统上,运行fstress。
4:cat /proc/meminfo 看到 cma_free 在变化。
现象:cma_free 和没有运行fstress前有明显变化。 实验三:用户态程序堆可以使用CMA区域内存
方法:
1:使用malloc分配内存,并使用memset分配物理内存。
2:循环分配直到不能分配为。(测试程序较简单,见附件)
现象:单板没有物理内存,出现oom时,可看到DMA free值和水线值相差无几。