LINUX CMA 详细分析

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值和水线值相差无几。

阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Jinhua_Wei/article/details/79399111
个人分类: C语言 KERNEL
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭