[内存管理]连续内存分配器(CMA)概述

时间:2023-03-09 21:20:53
[内存管理]连续内存分配器(CMA)概述

作者:Younger Liu,

本作品采用知识共享署名-非商业性使用-相同方式共享 3.0 未本地化版本许可协议进行许可。

原文地址:http://lwn.net/Articles/396657/

1.  
 简介

连续内存分配器(CMA - Contiguous Memory Allocator)是一个框架,允许建立一个平台无关的配置,用于连续内存的管理。然后,设备所需内存都根据该配置进行分配。

这个框架的主要作用不是分配内存,而是解析和管理内存配置,以及作为在设备驱动程序和可插拔的分配器之间的中间组件。因此,它是与任何内存分配方法和分配策略没有依赖关系的。

2.  
 为什么需要?

在嵌入式设备中,很多设备都没有支持scatter-getter和IO map,都需要连续内存块的操作。如设备:摄像机,硬件视频解码器,编码器等。

这些设备往往需要较大的内存缓冲区(如:一个200万像素的高清帧摄像机,需要超过6M的内存),该kmalloc内存分配机制对于这么大的内存是没有效果的。

一些嵌入式设备对缓冲区有一些额外的要求,比如:在含有多个内存bank的设备中,要求只能在特定的bank中中分配内存;而还有一些要定内存边界对齐的缓存区。

近来,嵌入式设备有了较大的发展(特别是V4L领域),并且这些驱动都有自己的内存分配代码。它们众多的大多数都是采用bootmem分配方法。CMA框架企图采用统一的连续内存分配机制,并为这些设备驱动提供简单的API,而且是可以定制化和模块化的。

3.  
 设计

CMA主要设计目标是提供一个可定制的模块化框架,并且是可以配置的,以适应个别系统的需要。配置指定的内存区域,然后将这些内存分配给制定的设备。这些内存区域可以共享给多个设备驱动,也可以专门分配一个。这是通过以下方式实现的:

1. CMA的核心不是处理内存分配和空闲空间管理。专用分配器是用来处理内存分配和空闲内存管理的。因此,如果现有的解决方案不符合给定的系统,那么可以开发一种新的算法,这种算饭可以很容易地插入到CMA框架中。

所提出的解决方案中包括一个最适算法(best-fit)的一个实现。

2. CMA允许运行时配置即将分配的内存区域。内存区域都是经由命令行给出的,所以可以很容易地改变它,而不需要重新编译内核。

每个地区都有自己的大小,对齐标准,起始地址(物理地址)和对应该内存区域的内存分配算法。

这意味着同一时刻可以有多中机制在运行,如果在一个平台上同时运行多个不同的设备,这些设备具有不同的存储器使用特性,那么局可以匹配最好的算法。

3. 当设备请求内存时,设备必须“自我介绍”,即附带自己的信息以告知CMA。这样CMA可以知道谁分配内存。这允许系统架构师来指定哪个移动设备应该使用哪些存储区。

3a. 设备也可以指定一个“类”内存区域,这使得系统更容易配置,进而一个单一的设备可能使用来自不同内存区域的内存。例如,一个视频解码器驱动程序可能要分配一些共享的缓冲区,那么从第一个bank中分配一些,再从第二个bank中分配一些,可以获得尽可能高的内存吞吐量。

4.  
 使用场景

虚构一个使用了CMA的系统,来观察一下其是如何使用和配置的。

有一个携带硬件视频解码器和摄像机的平台,每个设备在最坏的情况下可能需要20M的内存。在该系统中,这两个设备是不会同时使用的,并且内存是可能共享的。使用下面的两个命令行:

cma=r=20M cma_map=video,camera=r

第一个CMA指令是分配20M的内存,并且内存分配器是有效的;第二个表示,名称为“video”和“camera”的两个驱动从之前定义的内存区域中分配内存。

因为两者共享同一内存区域,相比于每个设备保留20M的内存区域,使得系统节省了20M的内存空进。

但是随着系统的发展和进化,平台上可能同时运行视频解码器和摄像机,那么20M的内存区域就不能满足需要了。那么可以通过命令快速解决:

cma=v=20M,c=20M cma_map=video=v;camera=c

从该解决方案中也可以看出CMA是如何为每一个设备分配所需的私有内存池的。

分配机制也能通过一种相似的方式进行替换。在测试中发现,当给定的内存区域大小为40M时,系统运行一段时间后,碎片会成为一个问题。因此,为了满足所需要求的缓存区大小,需要预留一个较大的缓存区。

但是不幸的是,你需要w设置一个新的分配算法——Neat Allocation Algorithm(简写na),这两个设备对于内存有30M的需求:

cma=r=30M:na cma_map=video,camera=r

从上述示例可以看出,当CMA提供的算法不满足要求时,如何配置自己的分配算法,而不需要修改CMA或重编内核。

5.  
 技术细节

5.1           命令行参数

如上图所示,CMA有两个参数“cma”和“cma_map”;其中“cma”指定要为CMA保留的内存大小,参数“cma_map”用于指定该区域分配给为哪一个设备使用。

参数“cma”格式如下:

cma          ::=  "cma=" regions [ ';' ]

regions      ::= region [ ';' regions ]

region       ::= reg-name

'=' size

[ '@' start ]

[ '/' alignment ]

[ ':' [ alloc-name ] [ '(' alloc-params ')' ] ]

reg-name     ::= a sequence of letters and digits

//内存区的名称

size         ::= memsize   //内存区的大小

start        ::= memsize   //期望的内存区的起始地址

alignment    ::= memsize   //起始地址的对齐倍数

alloc-name   ::= a non-empty sequence of letters and digits

//
将要使用的分配器的名称

alloc-params ::= a sequence of chars other then ')' and ';'

//
分配器的参数

memsize      ::= whatever memparse() accepts

参数"cma_map" 的格式如下:

cma-map      ::=  "cma_map=" rules [ ';' ]

rules        ::= rule [ ';' rules ]

rule         ::= patterns '=' regions

patterns     ::= pattern [ ',' patterns ]

regions      ::= reg-name [ ',' regions ]

//
与device相匹配的内存区的名称

pattern      ::= dev-pattern [ '/' kind-pattern ]

| '/' kind-pattern

// pattern request must match for this rule to

// apply to it; the first rule that matches is

// applied; if dev-pattern part is omitted

// value identical to the one used in previous

// pattern is assumed

dev-pattern  ::= pattern-str

// pattern that device name must match for the

// rule to apply.

kind-pattern ::= pattern-str

// pattern that "kind" of memory (provided by

// device) must match for the rule to apply.

pattern-str  ::= a non-empty sequence of characters with '?'

meaning any character and possible '*' at

the end meaning to match the rest of the

string

示例 (whitespace added for better readability):

cma = r1 = 64M         // 64M区域

@512M       // 开始于512M (或尽可能接近)

/1M         // 确保内存区域1M对齐

:foo(bar);  // 使用带参bar的分配器foo

r2 = 64M         // 64M区域

/1M;        // 确保内存区域1M对齐

//
使用第一个有效分配器

r3 = 64M         // 64M区域

@512M       // 开始于512M (或尽可能接近)

:foo;       // 使用不带参的分配器foo

cma_map = foo = r1;    // kind==NULL的foo设备使用区域r1

foo/quaz = r2;  // OR:

/quaz = r2;  // kind == "quaz"的设备foo使用区域r2

foo/* = r3;     // OR:

/* = r3;        // 任何kinde的设备foo均可使用区域r3

bar/* = r1,r2;  // 任何kind的设备bar均可使用区域r1和r2

baz?/a* , baz?/b* = r3;

//
任何名如baz?且kind为a或b的设备都可以区域r3,其中?代表任意字符

5.2           设备与内存类别

设备名称来自device结构体。如果一个驱动没有注册为device,那么它是不能使用CMA的(通常,至少提供一个伪设备)。

无论设备何时申请内存,内存的类别都是一个可选的参数。在很多场景下,这个参数是可以忽略的,但是有时某些设备也可能需要。

比如:当前有两个内存bank,由于性能的原因,这两个内存bank设备都会使用,那么此时,设备驱动需要为这两个不同的buffer定义两个不同的名称。

cma=a=32M@0,b=32M@512M cma_map=foo/a=a;foo/b=b

但是无论何时,驱动分配内存都需要指定为某一个内存区域:

buffer1 = cma_alloc(dev, 1 << 20, 0, "a");

buffer2 = cma_alloc(dev, 1 << 20, 0, "b");

当然,如果需要(比如,当指定的内存已经使用完),需要允许驱动从其他的内存bank中分配。命令行参数可以改为:

cma=a=32M@0,b=32M@512M cma_map=foo/a=a,b;foo/b=b,a

换句话说,如果同一个设备在某一个系统上只使用一个内存bank,命令行参数:

cma=r=64M cma_map=foo/*=r

驱动无需做出任何更改。

5.3           API接口

CMA框架为设备提供了四个接口,分配接口cma_alloc():

unsigned long cma_alloc(const struct device *dev,

const char *kind,

unsigned long size,

unsigned long alignment);

如果需要,设备可能需要指定对齐规格(这个规格是内存chunk需要满足的),必须是2的幂次或0.而chunks一般至少一PAGE对齐的。(page大小一般为4k)。

参数kind指定内存区域名称,如果没有指定,则采用NULL。调用示例:

addr = cma_alloc(dev, NULL, size, 0);

该函数返回的是物理地址,一般需要判断返回值的有效性:

unsigned long addr = cma_alloc(dev, size);

if (IS_ERR_VALUE(addr))

return (int)addr;

/* Allocated */

(Make sure to include <linux/err.h> which contains the definition of the IS_ERR_VALUE() macro.)

释放函数cma_put():

int cma_put(unsigned long addr);

参数为内存块的物理地址,实现机制是递减引用计数,如果引用计数变为0,则释放该内存块。绝大部分时候,用户无需关注引用计数,只需要简单地调用cma_put()就可以了。

当该内存块共享给其他的驱动时,就需要调用cma_get()递增引用计数:

int cma_put(unsigned long addr);

最后一个函数是cma_info(),返回分配给指定(dev, kind)的内存缓存区的描述信息。原型如下:

int cma_info(struct cma_info *info,

const struct device *dev,

const char *kind);

通过这个函数可以获取内存区的边间,大小和对应于(dev, kind)内存区的个数。

5.4           分配操作

为CMA创建一个分配器需要实现四个函数。

前两个是用来初始化和卸载分配器:

int  cma_foo_init(struct cma_region *reg);

void cma_foo_done(struct cma_region *reg);

第一个函数在平台初始化时调用。结构体cma_region记录了缓存区的起始地址、大小,也会记录经由命令行传入的alloc_params域。

当调用函数cma_foo_done()时,均认为改缓存区的内存已经全部释放。

另外两个函数式块分配和释放函数:

struct cma_chunk *cma_foo_alloc(struct cma_region *reg,

unsigned long size,

unsigned long alignment);

void cma_foo_free(struct cma_chunk *chunk);

每一个函数,都是唯一线程访问的。因此,分配器不需要担心并发。

当分配器已经实现后,剩下的就是注册了,在文件"mm/cma-allocators.h"中定义如下语句:

CMA_ALLOCATOR("foo", foo)

第一个foo是命令行使用的分配器的名称,第二个是函数名字。

5.5           平台集成

在平台初始化过程,会调用函数cma_regions_allocate():

void cma_regions_allocate(int (*alloc)(struct cma_region *reg));

会遍历命令行提过所有的内存区并为他们保留内存。只有一个参数cma_region用于保留内存。传入NULL会调用cma_region_alloc()函数来通过bootmem来分配内存。

平台也会提过cma_defaults()提供默认的cma和cma_map参数:

int cma_defaults(const char *cma, const char *cma_map)

6.  
 展望

在将来,CMA机制能够实现:CMA空闲区域能够用于page cache,文件系统buffer或设备交换区。如此,内存将不再会被浪费。

因为CMA的内存是经由CMA框架分配和释放的,所以CMA框架能够知道哪些内存分配了,哪些内存释放了。因此,可以跟踪未使用的内存,使得CMA可以将未使用的内存用于其他目的,如page cache,IO缓存,交换缓存等等。

作者:Younger Liu,

本作品采用知识共享署名-非商业性使用-相同方式共享 3.0 未本地化版本许可协议进行许可。