前言

最近工作中遇到需要在一个Linux内核模块中使用大块连续内存的需求(大于512M的那种大块),于是就研究了一下Linux内核获取大内存的方法,主要有:保留内存、memblock和CMA。保留内存对于模块使用是比较方便,但是保留的内存内核是不管理也不可用的,完全由用户决定怎么使用,这样如果用户使用不充分会造成内存的浪费。memblock是内核启动时,使用的预留内存的方法,当内核启动完成就不能再用对应的接口了。最后CMA(Contiguous Memory Allocation)可以比较灵活的使用大块内存。本文尝试了一种在内核模块中使用CMA机制分配和使用大块内存的方法。


1. 什么是CMA?

CMA(Contiguous Memory Allocation),在内存初始化时预留一块连续内存,可以在内存碎片化严重时从预留的那块连续内存中分配大块连续内存。CMA是一个框架,它允许为物理连续内存管理设置特定于计算机的配置。然后根据该配置分配设备的内存。该框架的主要作用不是分配内存,而是解析和管理内存配置,并充当设备驱动程序和可插拔分配器之间的中介。因此,它不依赖于任何内存分配方法或策略。

这样说可能有点抽象,要理解CMA内存分配和内核中常用的slab内存分配或以page的方式分配的关系,可以参考下面的图。
在这里插入图片描述

内核中CMA机制是为设备DMA需要大块连续的内存设计的,所以在驱动模块中使用CMA并不是直接用到的,而是通过DMA API间接使用的。那么我们是否可以在内核模块中直接重CMA分配器中获取大块内存呢?

本文重点探讨CMA内存使用方式,CMA的原理和实现请参考:
Linux内存管理:什么是CMA(contiguous memory allocation)连续内存分配器?可与DMA结合使用

2. CMA使用前的准备

下面我们就探讨一下在一个Linux内核模块中获取和使用CMA分配大块内存的方法。本文测试在CentOS7.8系统,内核升级到kernel-4.9.230-37.el7.x86_64。

2.1 内核配置选项

使用CMA功能,需要在内核编译时开启DMA_CMA选项,确认运行内核是否支持该选项可以使用下面的命令。

# cat /boot/config-$(uname -r) | grep DMA_CMA

如果有如下输出,则表示运行内核支持DMA_CMA选项。否则表示不支持,需要重新配置内核并编译内核。

CONFIG_DMA_CMA=y

2.2 内核启动参数

要使用CMA,还需要在内核启动阶段预留CMA内存。在Linux内核在启动时,会根据启动参数预留CMA内存,cma内存预留的参数的格式如下:

cma=nn[MG]@[start[MG][-end[MG]]] [ARM,X86,KNL]
        Sets the size of kernel global memory area for
        contiguous memory allocations and optionally the
        placement constraint by the physical address range of
        memory allocations. A value of 0 disables CMA
        altogether. For more information, see
        include/linux/dma-contiguous.h

如:需要从内存物理地址5G开始预留1G的连续内存用于CMA,则可以在内核启动选项中添加如下参数:

cma=1G@5G

如果需要检查CMA是否预留成功,可以执行下面的命令:

# cat /proc/meminfo |  grep Cma
CmaTotal:        1048576 kB
CmaFree:         1048576 kB

2.3 CMA操作接口

在内核中定义了CMA的分配和释放的接口,在头文件<linux/cma.h>中。

extern struct page *cma_alloc(struct cma *cma, size_t count, unsigned int align);
extern bool cma_release(struct cma *cma, const struct page *pages, unsigned int count);

接口中的count参数是指申请或释放的page个数,一个page是4k字节。如:要申请512M内存,count就是0x20000。align参数表示申请的内存以多大块对齐。
这两个接口中第一个参数都是 cma结构体指针,在内核中这个cma的指针是放在设备device结构体中的,也就是涉及到设备DMA时才使用的。参考内核代码的dma_alloc_from_contiguous函数实现,在"drivers/base/dma-contiguous.c"中。

struct page *dma_alloc_from_contiguous(struct device *dev, size_t count,
				       unsigned int align)
{
	if (align > CONFIG_CMA_ALIGNMENT)
		align = CONFIG_CMA_ALIGNMENT;

	return cma_alloc(dev_get_cma_area(dev), count, align);
}

但是如果没有一个特定的设备怎样使用cma的接口呢?我们还是需要一个cma的实例。通过对dma-contiguous.c的代码分析,发现在内核初始化阶段,内核在预留CMA内存时会生成一个默认的cma的实例dma_contiguous_default_area,后续设备device结构关联的cma_area也是从这个默认的实例中来的,在<linux/dma-contiguous.h>中定义。

#ifdef CONFIG_DMA_CMA

extern struct cma *dma_contiguous_default_area

3. 在内核模块中使用

了解了CMA内存分配的接口和参数,我们就可以在内核模块中使用CMA内存了。

3.1 模块代码

我们在模块中申请一片512M的连续内存,直接上代码。

#include <linux/dma-contiguous.h>
#include <linux/cma.h>

struct page *p = NULL;

    p = cma_alloc(dma_contiguous_default_area, 0x20000, (1<<PAGE_SHIFT));

这里得到的是连续内存的第一个page的实例指针,如果要使用这个内存就将page转换成虚拟地址:

    unsigned char *buf = NULL;
    buf = page_to_virt(p);

释放内存

    cma_release(dma_contiguous_default_area, p, (1<<PAGE_SHIFT));

3.2 编译加载模块

使用make进行模块编译,成功编译出内核模块ko文件,但在编译中提示了Warning:

# make
WARNING: "cma_release" [/root/cma_test/cmatest.ko] undefined!
WARNING: "dma_contiguous_default_area" [/root/cma_test/cmatest.ko] undefined!
WARNING: "cma_alloc" [/root/cma_test/cmatest.ko] undefined!

这三个告警就是我们使用的CMA的接口,没有定义是会影响模块加载。先试着执行一下插入模块操作,果然不能正确加载。

# insmod cmatest.ko
insmod: ERROR: could not insert module cmatest.ko: Unknown symbol in module

再分析内核代码,发现CMA的接口和dma_contiguous_default_area指针,都没有做符号导出(EXPORT_SYMBOL),是不能在内核之外被使用的,只有编译到内核中的代码可以调用。那么是否表示单独的内核模块ko无法使用CMA内存呢?当然我们可以修改内核,将这三个符号导出再使用,但是这样使用并不灵活。我们采用直接引用内核符号表的方式来解决。

3.3 内核符号表

CMA功能默认只提供给内核中的函数调用,CMA的相关接口没有做符号导出(EXPORT_SYMBOL),启动后加载的内核模块要使用CMA功能需要获取对应的接口的符号地址。

cma测试模块需要如下三个内核符号的地址:

  • dma_contiguous_default_area: DMA_CMA管理结构体指针;
  • cma_alloc: cma内存分配函数地址;
  • cma_release: cma内存释放函数地址;

可以在/proc/kallsyms中获取这三个符号地址。

# cat /proc/kallsyms | grep dma_contiguous_default_area

得到运行系统中的dma_contiguous_default_area符号地址:

ffffffff9445ba58 B dma_contiguous_default_area

前面的数值既是符号的地址,同样的方法可以获取:cma_alloc和cma_release的符号地址。

# cat /proc/kallsyms | grep cma_alloc
ffffffff9a255140 T cma_alloc
# cat /proc/kallsyms | grep cma_release
ffffffff9a255350 T cma_release

NOTE: 每次系统重启,这些符号的地址可能会发生变化。

3.4 CMA测试模块调整

获取了内核的这三个符号地址后,在模块中创建三个模块参数来接收这三个地址,再将这三个地址强转成对应的指针就可以使用了。于是我们对代码做一些修改:

  1. 去掉内核头文件
- #include <linux/dma-contiguous.h>
- #include <linux/cma.h>

struct page *p = NULL;
  1. 添加模块参数,用于接收符号地址
static unsigned long area_base = 0;
module_param(area_base, ulong, 0600);
static unsigned long alloc_fn = 0;
module_param(alloc_fn, ulong, 0600);
static unsigned long free_fn = 0;
module_param(free_fn, ulong, 0600);
  1. 添加函数指针和cma实例指针
typedef struct page *(*cma_alloc_t)(struct cma *, size_t, unsigned int);
typedef bool (*cma_release_t)(struct cma *, const struct page *, unsigned int);

cma_alloc_t cma_alloc=NULL;
cma_release_t cma_release=NULL;
struct cma * dma_cma_p = NULL;
  1. 用模块参数初始化函数指针和cma实例指针
    cma_alloc = (cma_alloc_t)alloc_fn;
    cma_release = (cma_release_t)free_fn;
    dma_cma_p = (struct cma *)(*(unsigned long *)area_base);
  1. 修改cma申请和释放调用
    p = cma_alloc(dma_cma_p, 0x20000, (1<<PAGE_SHIFT));

    cma_release(dma_cma_p, p, 0x20000);

3.5 重新加载模块

修改cma测试模块后编译,重新带参数的加载模块:

# insmod cmatest.ko area_base=0xffffffff9445ba58 alloc_fn=0xffffffff9a255140 free_fn=0xffffffff9a255350

本次加载成功,没有报错。为了验证内存的使用我们可以在代码中添加内存操作的语句,也可以使用调试工具查看内存是否操作成功,这里不赘述。至此我们基本验证了CMA的操作接口,可以从内核中分配出连续的大块内存。


总结

本文我们探讨了在Linux内核模块中使用CMA内存分配的方法,基本达到了预留和分配使用大块连续内存的目标。需要补充说明的是CMA预留的内存也是受内核管理的,也依赖内核的page管理机制的影响,预留的内存可能被其他的内核功能使用到,并不能保证完全被你的模块使用,而不是像保留内存那样确定没有其他人使用。是否可能因为其他内核函数使用导致你的模块无法申请成功的问题,有待进一步验证。

本文中的代码可以从下面链接下载:
CMA-test

推荐阅读

Logo

更多推荐