在实际遇到的问题中,遇到Android虚机中安装超过1.8G以上的大游戏APK文件时,就会出现安装失败的现象,通过分析,发现在/data/目录下进行大文件拷贝时,就会出现数据不一致,发现时内核打开了CMA和DMA_CMA,解决方法目前主要回退,将CMA和DMA_CMA的配置关闭

现象: 

拷贝5次: 数据不一致

/data/app # cp -raf testdata.tgz t1.tgz         
/data/app # cp -raf testdata.tgz t2.tgz         
/data/app # cp -raf testdata.tgz t3.tgz         
/data/app # cp -raf testdata.tgz t4.tgz         
/data/app # cp -raf testdata.tgz t5.tgz         
/data/app # md5sum *.tgz                                                                                                                                      
56d399c2c7ec1790c6bbd0fc81a08a97  t1.tgz
4d998543350652d4507c5d8383534399  t2.tgz
765c8ed479e98530a914e2459ffa9044  t3.tgz
dd508d8f080df7c8d9898bc5788e86df  t4.tgz
4a26d0bd23f9ecd52d11ccc5fd6cde8b  t5.tgz
c5c9032ba047d8c4c8e0dd1492fe1a35  testdata.tgz
/data/app # sync
/data/app # md5sum *.tgz                                                                                                         
3778398d88bc9ce492de7aa8ca91617c  t1.tgz
5056cd7cb67968777ea0be98427e85d4  t2.tgz
50fd5d9db85acc6d46250db3e971838c  t3.tgz
afaf4797277936d3c88416c07fc75034  t4.tgz
c3d1b91b8529783e38a9faa91d71df8b  t5.tgz
b49da3e10c92d5f189829d9ac0845ad1  testdata.tgz

/data/app # ls -lh
total 6.6G
-rw-rw-rw- 1 root   root   2.1G 2024-09-10 18:53 t1.tgz
-rw-rw-rw- 1 root   root   2.1G 2024-09-10 18:53 t2.tgz
-rw-rw-rw- 1 root   root   2.1G 2024-09-10 18:53 t3.tgz
-rw-rw-rw- 1 root   root   2.1G 2024-09-10 18:53 t4.tgz
-rw-rw-rw- 1 root   root   2.1G 2024-09-10 18:53 t5.tgz
-rw-rw-rw- 1 root   root   2.1G 2024-09-10 18:50 testdata.tgz
 

内核中的配置:

@@ -376,6 +376,7 @@ CONFIG_SPARSEMEM_VMEMMAP_ENABLE=y
 CONFIG_SPARSEMEM_VMEMMAP=y
 CONFIG_HAVE_MEMBLOCK=y
 CONFIG_NO_BOOTMEM=y
+CONFIG_MEMORY_ISOLATION=y
 # CONFIG_HAVE_BOOTMEM_INFO_NODE is not set
 CONFIG_SPLIT_PTLOCK_CPUS=4
 CONFIG_MEMORY_BALLOON=y
@@ -389,7 +390,10 @@ CONFIG_KSM=y
 CONFIG_DEFAULT_MMAP_MIN_ADDR=4096
 # CONFIG_TRANSPARENT_HUGEPAGE is not set
 # CONFIG_CLEANCACHE is not set
-# CONFIG_CMA is not set
+CONFIG_CMA=y
+# CONFIG_CMA_DEBUG is not set
+# CONFIG_CMA_DEBUGFS is not set
+CONFIG_CMA_AREAS=7
 # CONFIG_ZPOOL is not set
 # CONFIG_ZBUD is not set
 # CONFIG_ZSMALLOC is not set
@@ -980,6 +984,17 @@ CONFIG_REGMAP=y
 CONFIG_REGMAP_MMIO=y
 CONFIG_DMA_SHARED_BUFFER=y
 # CONFIG_FENCE_TRACE is not set
+CONFIG_DMA_CMA=y
+
+#
+# Default contiguous memory area size:
+#
+CONFIG_CMA_SIZE_MBYTES=16
+CONFIG_CMA_SIZE_SEL_MBYTES=y
+# CONFIG_CMA_SIZE_SEL_PERCENTAGE is not set
+# CONFIG_CMA_SIZE_SEL_MIN is not set
+# CONFIG_CMA_SIZE_SEL_MAX is not set
+CONFIG_CMA_ALIGNMENT=8
 

CMA和DMA_CMA相关的概念之前没怎么接触过,特此做个笔记: 

一、什么是CMA

CMA,Contiguous Memory Allocator,是内存管理子系统中的一个模块,负责物理地址连续的内存分配。一般系统会在启动过程中,从整个memory中配置一段连续内存用于CMA,然后内核其他的模块可以通过CMA的接口API进行连续内存的分配。CMA的核心并不是设计精巧的算法来管理地址连续的内存块,实际上它的底层还是依赖内核伙伴系统这样的内存管理机制,或者说CMA是处于需要连续内存块的其他内核模块(例如DMA mapping framework)和内存管理模块之间的一个中间层模块,主要功能包括:

1、解析DTS或者命令行中的参数,确定CMA内存的区域,这样的区域我们定义为CMA area。

2、提供cma_alloc和cma_release两个接口函数用于分配和释放CMA pages

3、记录和跟踪CMA area中各个pages的状态

4、调用伙伴系统接口,进行真正的内存分配。

Linux内核中已经提供了各种内存分配的接口,为何还有建立CMA这种连续内存分配的机制呢?

我们先来看看内核哪些模块有物理地址连续的需求。huge page模块需要物理地址连续是显而易见的。大家都熟悉的处理器(不要太古老),例如ARM64,其内存管理单元都可以支持多个页面大小(4k、64K、2M或者更大的page size),但在大多数CPU架构上,Linux内核总是倾向使用最小的page size,即4K page size。Page size大于4K的page统称为“huge page”。对于一个2M的huge page,MMU会把一个连续的2M的虚拟地址mapping到连续的、2M的物理地址上去,当然,这2M size的物理地址段必须是由512个地址连续的4k page frame组成。

当然,更多的连续内存的分配需求来自形形色色的驱动。例如现在大家的手机都有视频功能,camer功能,这类驱动都需要非常大块的内存,而且有DMA用来进行外设和大块内存之间的数据交换。对于嵌入式设备,一般不会有IOMMU,而且DMA也不具备scatter-getter功能,这时候,驱动分配的大块内存(DMA buffer)必须是物理地址连续的。

顺便说一句,huge page的连续内存需求和驱动DMA buffer还是有不同的,例如在对齐要求上,一个2M的huge page,其底层的2M 的物理页面的首地址需要对齐在2M上,一般而言,DMA buffer不会有这么高的对齐要求。因此,我们这里讲的CMA主要是为设备驱动准备的,huge page相关的内容不在本文中描述。

我们来一个实际的例子吧:我的手机,像素是1300W的,一个像素需要3B,那么拍摄一幅图片需要的内存大概是1300W x 3B = 26MB。通过内存管理系统分配26M的内存,压力可是不小。当然,在系统启动之处,伙伴系统中的大块内存比较大,也许分配26M不算什么,但是随着系统的运行,内存不断的分配、释放,大块内存不断的裂解,再裂解,这时候,内存碎片化导致分配地址连续的大块内存变得不是那么的容易了,怎么办?作为驱动工程师,我们有两个选择:其一是在启动时分配用于视频采集的DMA buffer,另外一个方案是当实际使用camer设备的时候分配DMA buffer。前者的选择是可靠的,但它有一个缺点,即当照相机不使用时(大多数时间内camera其实都是空闲的),预留的那些DMA BUFFER的内存实际上是浪费了(特别在内存配置不大的系统上更是如此)。后一种选择不会浪费内存,但是不可靠,随着内存碎片化,大的、连续的内存分配变得越来越困难,一旦内存分配失败,camera功能就会缺失,估计用户不会答应。

这就是驱动工程师面临的困境,为了解决这个问题,各个驱动各出奇招,但是都不能非常完美的解决问题。最终来自Michal Nazarewicz的CMA补丁将可以把各个驱动工程师的烦恼“一洗了之”。对于CMA 内存,当前驱动没有分配使用的时候,这些memory可以内核的被其他的模块使用(当然有一定的要求),而当驱动分配CMA内存后,那些被其他模块使用的内存需要吐出来,形成物理地址连续的大块内存,给具体的驱动来使用。

三、CMA模块的蓝图是怎样的?

实战中学习:CMA和DMA_CMA导致安装大游戏失败的问题_物理地址

虽然用于解决驱动的内存分配问题,但是驱动并不会直接调用CMA模块的接口,而是通过DMA mapping framework来间接使用CMA的服务。一开始,CMA area的概念是全局的,通过内核配置参数和命令行参数,内核可以定位到Global CMA area在内存中的起始地址和大小(注:这里的Global的意思是针对所有的driver而言的)。并在初始化的时候,调用dma_contiguous_reserve函数,将指定的memory region保留给Global CMA area使用。人性是贪婪的,驱动亦然,很快,有些驱动想吃独食,不愿意和其他驱动共享CMA,因此出现两种CMA area:Global CMA area给大家共享,而per device CMA可以给指定的一个或者几个驱动使用。这时候,命令行参数不是那么合适了,因此引入了device tree中的reserved memory node的概念。当然,为了兼容,内核仍然支持CMA的command line参数。

DMA其他概念: 一致性缓存

CPU写内存的时候有两种方式:

  1. write through: CPU直接写内存,不经过cache。
  2. write back: CPU只写到cache中。cache的硬件使用LRU算法将cache里面的内容替换到内存。通常是这种方式。

DMA可以完成从内存到外设直接进行数据搬移。但DMA不能访问CPU的cache,CPU在读内存的时候,如果cache命中则只是在cache去读,而不是从内存读,写内存的时候,也可能实际上没有写到内存,而只是直接写到了cache。

实战中学习:CMA和DMA_CMA导致安装大游戏失败的问题_Linux内核_02

这样一来,如果DMA从将数据从外设写到内存,CPU中cache中的数据(如果有的话)就是旧数据了,这时CPU在读内存的时候命中cache了,就是读到了旧数据;CPU写数据到内存时,如果只是先写到了cache,则内存里的数据就是旧数据了。这两种情况(两个方向)都存在cache一致性问题。例如,网卡发包的时候,CPU将数据写到cache,而网卡的DMA从内存里去读数据,就发送了错误的数据。

实战中学习:CMA和DMA_CMA导致安装大游戏失败的问题_android_03

2 如何解决一致性问题
主要靠两类APIs:

2.1 一致性DMA缓存(Coherent DMA buffers)

DMA需要的内存由内核去申请,内核可能需要对这段内存重新做一遍映射,特点是映射的时候标记这些页是不带cache的,这个特性也是存放在页表里面的。
上面说“可能”需要重新做映射,如果内核在highmem映射区申请内存并将这个地址通过vmap映射到vmalloc区域,则需要修改相应页表项并将页面设置为非cache的,而如果内核从lowmem申请内存,我们知道这部分是已经线性映射好了,因此不需要修改页表,只需修改相应页表项为非cache即可。

相关的接口就是dma_alloc_coherent()和dma_free_coherent()。dma_alloc_coherent()会传一个device结构体指明给哪个设备申请一致性DMA内存,它会产生两个地址,一个是给CPU看的,一个是给DMA看的。CPU需要通过返回的虚拟地址来访问这段内存,才是非cache的。至于dma_alloc_coherent()的内部实现可以不关注,它是和体系结构如何实现非cache(如mips的kseg1)相关,也可能与硬件特性(如是否支持CMA)相关。

还有一个接口dma_cache_sync(),可以手动去做cache同步,上面说dma_alloc_coherent()分配的是uncached内存,但有时给DMA用的内存是其他模块已经分配好的,例如协议栈发包时,最终要把skb的地址和长度交给DMA,除了将skb地址转换为物理地址外,还要将CPU cache写回(因为cache里可能是新的,内存里是旧的)。

2.2 流式DMA映射(DMA Streaming Mapping),
相关接口为 dma_map_sg(), dma_unmap_sg(),dma_map_single(),dma_unmap_single()。
一致性缓存的方式是内核专门申请好一块内存给DMA用。而有时驱动并没这样做,而是让DMA引擎直接在上层传下来的内存里做事情。例如从协议栈里发下来的一个包,想通过网卡发送出去。
但是协议栈并不知道这个包要往哪里走,因此分配内存的时候并没有特殊对待,这个包所在的内存通常都是可以cache的。
这时,内存在给DMA使用之前,就要调用一次dma_map_sg()或dma_map_single(),取决于你的DMA引擎是否支持聚集散列(DMA scatter-gather),支持就用dma_map_sg(),不支持就用dma_map_single()。DMA用完之后要调用对应的unmap接口。

由于协议栈下来的包的数据有可能还在cache里面,调用dma_map_single()后,CPU就会做一次cache的flush,将cache的数据刷到内存,这样DMA去读内存就读到新的数据了。

注意,在map的时候要指定一个参数,来指明数据的方向是从外设到内存还是从内存到外设:
从内存到外设:CPU会做cache的flush操作,将cache中新的数据刷到内存。
从外设到内存:CPU将cache置无效,这样CPU读的时候不命中,就会从内存去读新的数据。

还要注意,这几个接口都是一次性的,每次操作数据都要调用一次map和unmap。并且在map期间,CPU不能去操作这段内存,因此如果CPU去写,就又不一致了。
同样的,dma_map_sg()和dma_map_single()的后端实现也都是和硬件特性相关。

参考:

Linux内存管理 —— DMA和一致性缓存(dma_alloc_coherent(),dma_map_sg())_还没写完内存就读-博客

 https://zhuanlan.zhihu.com/p/542008458

http://www.wowotech.net/memory_management/cma.html

https://zhuanlan.zhihu.com/p/575194785