在Linux内核模块中使用CMA内存分配


前言

最近工作中遇到需要在一个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

推荐阅读

<think>好的,我现在需要回答用户关于CMA算法的问题,特别是实现原理和示例。首先,我需要明确CMA是什么。根据用户提供的引用[1],CMA是盲均衡算法,用于估计和抵消信道失真。引用[2]提到了CMA库的使用,可能涉及参数初始化和优化步骤。 接下来,我应该详细解释CMA算法的原理。盲均衡意味着不需要训练序列,直接利用信号特性调整滤波器系数。梯度下降法是核心,这里需要结合数学公式,比如代价函数。根据引用[1],代价函数可能是基于信号模值的误差,比如$J = E[(|y(n)|^2 - R_2)^2]$,其中$R_2$是常数。需要正确使用LaTeX格式,确保行内公式用$...$,独立公式用$$...$$。 然后,实现步骤部分需要分点说明,包括初始化、计算输出、误差计算、系数更新和迭代。每个步骤要简明扼要,引用相关的参考文献,比如参数初始化参考引用[2]。 示例代码部分,用户可能需要Python代码,但引用[2]中的代码是初始化部分,可能需要补充完整的CMA实现。需要确保代码结构清晰,使用正确的语法,比如梯度下降的权重更新。同时,代码中的数学表达式要符合格式要求,例如误差项$\mu e(n) y(n) x(n)$。 最后,生成相关问题时要围绕CMA展开,比如应用场景、参数影响、与其他算法的比较,以及复杂度分析。这些问题应帮助用户进一步探索CMA算法。 在检查过程中,要确保所有数学表达式正确,引用正确标注,例如在提到代价函数时添加[^1],在参数初始化时引用[^2]。同时,回答结构要清晰,分原理、步骤、示例和问题几个部分,符合用户要求的层次结构。</think>### CMA算法详解 #### 一、CMA算法原理 CMA(Constant Modulus Algorithm,常模算法)是一种自适应盲均衡算法,主要用于消除通信信道中的失真,无需依赖训练序列。其核心思想是通过调整均衡器系数,使输出信号的模值逼近恒定值(如QPSK信号模值恒定)。 **数学模型**: 代价函数定义为: $$J = E\left[\left(|y(n)|^2 - R_2\right)^2\right]$$ 其中$y(n)$为均衡器输出,$R_2 = \frac{E[|s(n)|^4]}{E[|s(n)|^2]}$为模值常数($s(n)$为原始信号)。通过梯度下降法更新滤波器权重$w$: $$w(n+1) = w(n) - \mu \cdot \nabla J(w)$$ 展开后权重更新公式为: $$w(n+1) = w(n) + \mu \cdot e(n) \cdot y^*(n) \cdot x(n)$$ 其中$\mu$为步长,$e(n)=|y(n)|^2 - R_2$为误差项,$x(n)$为输入信号向量。 --- #### 二、CMA实现步骤 1. **初始化参数** 设置滤波器长度$L$、步长$\mu$、目标模值$R_2$(引用[2]中示例的`sigma`和`penalty_coeff`类似参数调节) 2. **计算均衡器输出** $$y(n) = w^T(n) \cdot x(n)$$ 3. **计算误差信号** $$e(n) = |y(n)|^2 - R_2$$ 4. **更新滤波器系数** $$w(n+1) = w(n) - \mu \cdot e(n) \cdot y^*(n) \cdot x(n)$$ 5. **迭代至收敛** 重复步骤2-4直到误差稳定或达到最大迭代次数 --- #### 三、Python实现示例 ```python import numpy as np def cma_algorithm(input_signal, L=5, mu=0.001, R2=1, max_iters=1000): # 初始化滤波器系数 w = np.random.randn(L) + 1j*np.random.randn(L) w /= np.linalg.norm(w) # 归一化 for n in range(len(input_signal)-L): x = input_signal[n:n+L] # 输入向量 y = np.dot(w.conj(), x) # 计算输出 error = np.abs(y)**2 - R2 # 误差计算 w = w - mu * error * y * x # 系数更新 return w ``` > 注:代码中`R2`对应QPSK信号时为1,实际需根据调制方式调整 --- #### 四、算法特性 | 指标 | 说明 | |-------------|-------------------------------| | 收敛速度 | 与步长$\mu$正相关,但过大导致震荡[^1] | | 计算复杂度 | $O(NL)$,$N$为迭代次数,$L$为滤波器长度 | | 适用场景 | QAM、PSK等恒包络调制信号的均衡[^4] | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Angel老豆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值