本文贴代码过头了,以后想起来再优化一下吧
目录
概述
数据结构
构建初始化(DTS+CONFIG_DMA_CMA)
页表与物理页初始化
分配器激活
分配器使用
CMA部署
实战
概述
CMA(Contiguous Memory Allocator)是连续内存分配技术,是 Linux Kernel 内存管理系统的扩展,目的在于解决视频播放 (特别对于 4K 视频) 等需要预留大量连续内存导致运行内存紧张的问题。
CMA 框架的主要作用不是分配内存,而是解析和管理内存配置,以及作为在设备驱动程序和可插拔的分配器之间的中间组件。
数据结构
1 .
struct cma 结构用于维护一块 CMA 区域
struct cma {
unsigned long base_pfn; //CMA 区域 起始物理地址对应的物理页帧号
unsigned long count; //描述 CMA 区域总共维护的 page 数量
unsigned long *bitmap; //该 CMA 区域的所有物理页维护在该 bitmap 中,bitmap 中每个 bit 代表一定数量的物理页,至于代表多少物理页与 order_per_bit 有关
unsigned int order_per_bit; //指明该 CMA 区域的 bitmap 中,每个 bit 代表 的 page 数量
struct mutex lock;
const char *name; //CMA 区域的名字
};
cma模块使用bitmap来管理其内存的分配,0表示free,1表示已经分配。具体内存管理的单位和struct cma中的order_per_bit成员相关,如果order_per_bit等于0,表示按照一个一个page来分配和释放,如果order_per_bit等于1,表示按照2个page组成的block来分配和释放,以此类推。struct cma中的bitmap成员就是管理该cma area内存的bit map。count成员说明了该cma area内存有多少个page。它和order_per_bit一起决定了bitmap指针指向内存的大小。base_pfn定义了该CMA area的起始page frame number,base_pfn和count一起定义了该CMA area在内存在的位置。
2 . 维护 CMA 分配器中可用的 CMA 区域。 每个 CMA 区域包含了一段可用的物理内存
#define MAX_RESERVED_REGIONS 32
struct cma cma_areas[MAX_CMA_AREAS];
每一个struct cma抽象了一个CMA area,标识了一个物理地址连续的memory area。调用cma_alloc分配的连续内存就是从CMA area中获得的。默认定义32个cma_areas。
构建初始化(DTS+CONFIG_DMA_CMA)
CMA的分配初始化有三种:一种DTS,一种Kbuild配置,还一种通过cmdline常见。此处分析优先级最高的DTS方式。使用DTS方式时,要打开CONFIG_DMA_CMA宏,否则只会创建预留区,不会将改预留区加入到CMA中。
start_kernel
---->setup_arch
---->arm_memblock_init
--→early_init_fdt_scan_reserved_mem
void __init early_init_fdt_scan_reserved_mem(void) {
int n;
u64 base, size;
early_init_dt_reserve_memory_arch(__pa(initial_boot_params),
fdt_totalsize(initial_boot_params), 0);
//遍历DTS中的节点,然后把节点信息传入__fdt_scan_reserved_mem函数,该函数用于筛选“reserved-memory”节点,然后将信息存储在reserved_mem数组里
of_scan_flat_dt(__fdt_scan_reserved_mem, NULL);
//将预留区中的区域在 MEMBLOCK 分配之后加入到 CMA 和 DMA 区域中
fdt_init_reserved_mem();
}
为 DTS 中的预留区分配内存。 DTS 中预留区分做两类,一类是 DTB 本身需要预留的区域,另一类是 “/reserved-memory” 节点中描述的预留区。在后者中,预留区分配需要的内存之后,还会将这些预留区加入到 CMA 或 DMA 中
void __init fdt_init_reserved_mem(void) {
int i;
for (i = 0; i < reserved_mem_count; i++) {
struct reserved_mem *rmem = &reserved_mem[i];
unsigned long node = rmem->fdt_node;
int len;
int err = 0;
//将系统预留数组 reserved_mem[] 中 size 成员为 0 的成员分配对应长度的物理内存作为预留区。实现连续物理内存的最初分配,并完成将该区域加入到系统的预留区 数组 reserved-mem[] 中。
//注意:当在dts中指定了size大小,那么这里的rmem->size不为0,无需执行alloc size动作
if (rmem->size == 0)
err = __reserved_mem_alloc_size(node, rmem->name, &rmem->base, &rmem->size);
//遍历 __reservedmem_of_table
if (err == 0)
__reserved_mem_init_node(rmem);
}
}
__reserved_mem_init_node函数中一些代码无法追踪,遍历 __reservedmem_of_table section 内的预留区时,函数会调用 rmem_cma_setup() 函数,该函数用于将全局 reserved-mem[] 数组的区域加入到 CMA 分配器中,即添加 一块新的 CMA 区域。在该函数内,涉及从 MEMBLOCK 分配物理内存和加入 新的 CMA 区域,也包含了设置 CMA 分配器使用的默认分配区。
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
将预留区添加到 CMA 子系统
static int __init rmem_cma_setup(struct reserved_mem *rmem) {
phys_addr_t align = PAGE_SIZE << max(MAX_ORDER - 1, pageblock_order);
phys_addr_t mask = align - 1;
unsigned long node = rmem->fdt_node;
struct cma *cma;
int err;
//包含 “no-map” 或者不包含 “reusable” 属性预留区不建立映射关系
if (!of_get_flat_dt_prop(node, "reusable", NULL) || of_get_flat_dt_prop(node, "no-map", NULL))
return -EINVAL;
//对预留区的 基地址和长度进行对齐检测
if ((rmem->base & mask) || (rmem->size & mask)) {
pr_err("Reserved memory: incorrect alignment of CMA region\n");
return -EINVAL;
}
//将预留区 加入到一块可用的 CMA 区域内,并初始化这块 CMA 区域的管理数据结构体成员。【详细见后】
err = cma_init_reserved_mem(rmem->base, rmem->size, 0, rmem->name, &cma);
//将预留区加入到 dma_mmu_remap[] 数组,以供系统初始化 DMA 映射时使用
dma_contiguous_early_fixup(rmem->base, rmem->size);
//设备数节点含有linux,cma-default属性,则将当前 CMA 区域作为系统设备默认 使用的 CMA 区域
if (of_get_flat_dt_prop(node, "linux,cma-default", NULL))
dma_contiguous_set_default(cma);
rmem->ops = &rmem_cma_ops; //操作方法 【详细见后】
rmem->priv = cma;
return 0;
}
RESERVEDMEM_OF_DECLARE(cma, "shared-dma-pool", rmem_cma_setup);
=====================================================================
//cma结构体初始化的同时也指向了当前cma area count的cma_areas全局数组。
int __init cma_init_reserved_mem(phys_addr_t base, phys_addr_t size, int order_per_bit, struct cma **res_cma) {
struct cma *cma;
phys_addr_t alignment;
alignment = PAGE_SIZE << max(MAX_ORDER - 1, pageblock_order);
cma = &cma_areas[cma_area_count];
cma->base_pfn = PFN_DOWN(base);
cma->count = size >> PAGE_SHIFT;
cma->order_per_bit = order_per_bit;
*res_cma = cma;
cma_area_count++;
totalcma_pages += (size / PAGE_SIZE);
return 0;
}
================================================================
rmem_cma_ops 包含 rmem_cma_device_init 和 rmem_cma_device_release
前者设置设备使用的 CMA 区域 dev->cma_area = cma;
后者用于将设备预留区信息设置为 NULL dev->cma_area = NULL;
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
至此连续物理内存并未初始成功,只是做出了内存预留。
页表与物理页初始化
构建完 CMA 区域之后,CMA 需要将每个 CMA 区域的页表进行映射,以及将 CMA 区域内的物理页进行初始化。该阶段初始化完毕之后还不能使用 CMA 分配器。
start_kernel
→setup_arch
-→paging_init
→dma_contiguous_remap
该函数用于创建映射关系。【建立映射的套路都是如下代码】
void __init dma_contiguous_remap(void) {
int i;
for (i = 0; i < dma_mmu_remap_num; i++) {
phys_addr_t start = dma_mmu_remap[i].base;
phys_addr_t end = start + dma_mmu_remap[i].size;
struct map_desc map;
unsigned long addr;
if (end > arm_lowmem_limit) end = arm_lowmem_limit;
if (start >= end) continue;
map.pfn = __phys_to_pfn(start);
map.virtual = __phys_to_virt(start);
map.length = end - start;
map.type = MT_MEMORY_DMA_READY;
for (addr = __phys_to_virt(start); addr < __phys_to_virt(end);
addr += PMD_SIZE)
pmd_clear(pmd_off_k(addr));
flush_tlb_kernel_range(__phys_to_virt(start), __phys_to_virt(end));
//建立映射
iotable_init(&map, 1);
}
}
分配器激活
间接对 CMA 进行激活初始化,激活之后 CMA 就可用供其他模块、设备和子系统使用。
static int __init cma_init_reserved_areas(void) {
int i;
//CMA 的激活入口
for (i = 0; i < cma_area_count; i++) {
int ret = cma_activate_area(&cma_areas[i]); //【详细见后】
if (ret) return ret;
}
return 0;
}
core_initcall(cma_init_reserved_areas);
=============================================================
函数用于将 CMA 区域内的预留页全部释放添加到 Buddy 管理器内,然后激活 CMA 区域供系统使用。此函数将cam结构体剩余成员的初始化。
static int __init cma_activate_area(struct cma *cma) {
//比如CMA为8M,则bitmap size为2^8
int bitmap_size = BITS_TO_LONGS(cma_bitmap_maxno(cma)) * sizeof(long);
unsigned long base_pfn = cma->base_pfn, pfn = base_pfn;
unsigned i = cma->count >> pageblock_order;
struct zone *zone;
cma->bitmap = kzalloc(bitmap_size, GFP_KERNEL);
zone = page_zone(pfn_to_page(pfn));
//检查 CMA 区域的每个 pageblock 内所有页是否有效,并且所有页与起始页是在同一个 ZONE 分区 内
do {
unsigned j;
base_pfn = pfn;
for (j = pageblock_nr_pages; j; --j, pfn++) {
if (page_zone(pfn_to_page(pfn)) != zone)
goto not_in_zone;
}
//将 pageblock 内所有的物理页的 RESERVED 标志清除,让后将这些页都返回 给 Buddy 系统使用 【详细见后】
init_cma_reserved_pageblock(pfn_to_page(base_pfn));
} while (--i);
mutex_init(&cma->lock);
return 0;
not_in_zone:
pr_err("CMA area %s could not be activated\n", cma->name);
kfree(cma->bitmap);
cma->count = 0;
return -EINVAL;
}
========================================================================
void __init init_cma_reserved_pageblock(struct page *page) {
unsigned i = pageblock_nr_pages;
struct page *p = page;
do {
__ClearPageReserved(p); //所有 page 清除 Reserved 标志
set_page_count(p, 0);
} while (++p, --i);
//将cma区域 page的迁移类型设置为 MIGRATE_CMA
set_pageblock_migratetype(page, MIGRATE_CMA);
//内核中默认pageblock_order = MAX_ORDER-1,可以直接从else看起
if (pageblock_order >= MAX_ORDER) {
i = pageblock_nr_pages;
p = page;
do {
set_page_refcounted(p);
__free_pages(p, MAX_ORDER - 1);
p += MAX_ORDER_NR_PAGES;
} while (i -= MAX_ORDER_NR_PAGES);
} else {
set_page_refcounted(page); //page 引用 计数设置为 1
__free_pages(page, pageblock_order); //释放页
}
//将释放的 page 数量全部加到系统里进行维护
adjust_managed_page_count(page, pageblock_nr_pages);
}
cma默认是从reserved memory中分配的,通常情况这块内存是直接分配并预留不做任何使用,无形之中造成了浪费。所以在不用的时候放入伙伴系统,作为普通内存使用。
分配器使用
CMA 激活之后,内核可以使用 CMA API 就可以使用连续物理内存
分配 CMA 里面的连续物理内存,可以使用:
struct page *dma_alloc_from_contiguous(struct device *dev, size_t count, unsigned int align, gfp_t gfp_mask)
指针dev 指向需要分配CMA的设备,
参数 count 指明需要分配的page数,
align 参数 指明对齐的方式,align = CONFIG_CMA_ALIGNMENT;
no_warn 控制警告消息的打印。
核心函数cma_alloc:根据pfn获取page
当使用完 CMA 连续物理内存之后,可以将物理内存归还给 CMA 内存管理器
bool dma_release_from_contiguous(struct device *dev, struct page *pages, int count)
参数 dev 指向一个设备,
pages 指向连续物理内存的起始页,
参数 count 表示分配的page数
核心函数cma_release:根据page找到pfn,然后根据pfn释放当前分配的页
当然还有更高级的接口
cma_allocation_alloc
cma_allocation_free
CMA部署
CMA 问题的本质就是如何规划系统的 物理内存
第一个比较重要的是获得系统物理内存的范围
cat /proc/iomem
找到 “System RAM”, 其代表系统物理内存的起始物理地址和终止物理地址,分配的cma区域不能超过这段范围。
第二个查看当前系统的预留区
系统已经预留的不可使用
cat /sys/kernel/debug/memblock/reserved
通过这个命令可以知道系统已预留的内存信息,这些已预留的内存信息不可使用。但排除这些预留区域,再在RAM范围内找出可用内存,再满足对其需求就可以自己手动找出可用于CMA的区域。
第三个,在DTS中说明cma信息
dts方式部署cma的好处是既可以指定起始地址和长度,还可以命名该cma
linux,cma { //cma 名字
compatible = "shared-dma-pool"; //默认属性
reusable; //默认属性
size = <0x00800000>; /* 8M */ //通常size为8M的倍数
alloc-ranges = <0x69000000 0x00800000>; //起始地址和长度
linux,cma-default; //默认使用这段区域
};
这些属性可以查看rmem_cma_setup函数
实战
1 查看memory范围
2 查看预留区信息
3 dts添加代码
reserved-memory {
#address-cells = <1>;
#size-cells = <1>;
ranges;
//此处为要添加的cma。假设为video预留一块8M内存
video_cma: video_cma@69000000 {
compatible = "shared-dma-pool";
reusable;
reg = <0x69000000 0x800000>;
};
};
添加完dts信息后,系统在启动阶段会去自动读取并解析。
4 编写驱动
#include <linux/module.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/cma.h>
#include <linux/mm.h>
#include <linux/of.h>
#include <linux/dma-contiguous.h>
#include <linux/fs.h>
#include <linux/miscdevice.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/types.h>
#include <linux/uaccess.h>
#include "cma.h"
#define CMA_NAME "video_cma@69000000"
struct cma_allocation {
struct list_head list;
struct page *cma_page;
int count;
unsigned long vaddr;
};
static struct device *cma_dev;
static LIST_HEAD(cma_allocations);
static DEFINE_SPINLOCK(cma_lock);
/*
* struct cma *find_cma_by_name(const char *name) {
* int idx;
* for (idx = 0; idx < MAX_CMA_AREAS; idx++) {
* if (!strcmp(name, cma_areas[idx].name))
* return &cma_areas[idx];
* }
* return NULL;
* }
*/
static ssize_t
cma_test_read(struct file * file, char __user *buf, size_t count, loff_t *ppos)
{
struct cma_allocation *alloc = NULL;
bool ret;
spin_lock(&cma_lock);
if(!list_empty(&cma_allocations)) {
alloc = list_first_entry(&cma_allocations, struct cma_allocation, list);
list_del(&alloc->list);
}
spin_unlock(&cma_lock);
if(!alloc)
return -EIDRM;
ret = dma_release_from_contiguous(cma_dev, alloc->cma_page, alloc->count);
if (ret)
dev_info(cma_dev, "free %d pages. vaddr: 0x%lx paddr: 0x%x\n", alloc->count, alloc->vaddr, __pa(alloc->vaddr));
kfree(alloc);
return 0;
}
static ssize_t
cma_test_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
struct cma_allocation *alloc;
int ret;
alloc = kmalloc(sizeof(struct cma_allocation), GFP_KERNEL);
if(!alloc)
return -ENOMEM;
memset(alloc, 0, sizeof(struct cma_allocation));
ret = kstrtouint_from_user(buf, count, 0, &alloc->count);
if (ret != 0) {
pr_notice("copy_from_user failed\n");
return -EFAULT;
}
alloc->cma_page = dma_alloc_from_contiguous(cma_dev, alloc->count, 8, GFP_KERNEL);
if (!alloc->cma_page) {
dev_info(cma_dev, "alloc cma pages failed!\n");
return -EFAULT;
}
alloc->vaddr = (unsigned long)page_address(alloc->cma_page);
if(alloc->vaddr) {
dev_info(cma_dev, "alloc %d pages, vaddr: 0x%lx paddr: 0x%x \n", alloc->count, alloc->vaddr, __pa(alloc->vaddr));
spin_lock(&cma_lock);
list_add_tail(&alloc->list, &cma_allocations);
spin_unlock(&cma_lock);
return count;
} else {
dev_err(cma_dev, "no mem in CMA area\n");
kfree(alloc);
return -ENOSPC;
}
}
static const struct file_operations cma_test_fops = {
.owner = THIS_MODULE,
.read = cma_test_read,
.write = cma_test_write,
};
static struct miscdevice cma_test_misc = {
.name = "cma_test",
.fops = &cma_test_fops,
};
static int __init cma_test_init(void)
{
struct device_node *np;
struct cma *cma;
int ret;
ret = misc_register(&cma_test_misc);
if(unlikely(ret)) {
pr_err("failed to register cma test misc device!\n");
goto err;
}
cma_dev = cma_test_misc.this_device;
dev_info(cma_dev, "registered.\n");
np = of_find_node_by_path("/reserved-memory/video_cma@69000000");
if (!np) {
dev_info(cma_dev, "find node %s failed!\n", CMA_NAME);
goto err;
}
dev_info(cma_dev, "find node %s ok!\n", CMA_NAME);
cma = find_cma_by_name(CMA_NAME);
if (cma) {
dev_info(cma_dev, "find cma %s success, base pfn 0x%lx\n", cma->name, cma->base_pfn);
cma_dev->cma_area = cma;
} else {
dev_info(cma_dev, "find cma err!\n");
goto err;
}
return 0;
err:
return -EFAULT;
}
static void __exit cma_test_exit(void)
{
misc_deregister(&cma_test_misc);
}
module_init(cma_test_init);
module_exit(cma_test_exit);
MODULE_LICENSE("GPL");
5 测试,基于内核5.0
注:旧的内核中cma结构体不含name成员,使用不是很方便。
首先对比内存信息
未添加video cma
cat /proc/meminfo
CmaTotal: 16384 kB
CmaFree: 14592 kB
free -m
total used free shared buffers cached
Mem: 497 18 478 0 0 1
-/+ buffers/cache: 17 480
添加video cma
cat /proc/meminfo
CmaTotal: 24576kB
CmaFree: 22784kB
free -m
total used free shared buffers cached
Mem: 497 18 478 0 0 1
-/+ buffers/cache: 17 480
对比得到,cmatotal和cmafree都增加了8M,符合添加8M预留区并加入cma系统的预期。
free看到total不变,说明预留区已放入伙伴系统。
使用这段CMA
echo 1024 > /dev/cma_test
misc cma_test: alloc 1024 pages, vaddr: 0xc9000000 paddr: 0x69000000
cat /proc/meminfo
CmaTotal: 24576 kB
CmaFree: 18688 kB
成功使用4M内存
释放这段内存
cat /dev/cma_test
misc cma_test: free 1024 pages. vaddr: 0xc9000000 paddr: 0x69000000
cat /proc/meminfo
CmaTotal: 24576 kB
CmaFree: 22244 kB
可以发现,内存并未完全释放。原因是产生了碎片,无法合并。
cam的碎片问题还是蛮严重的,建议一个需求一块cma。
总结一下:
在内存不断使用的过程中,会产生碎片,日益使用对内存消耗大的设备比如camera,video,将很难分配到足够大的内存。cma的技术提前分配一块专用内存,在不用的时候释放到伙伴系统中,在使用的时候通过页迁移腾出这块内存。
cma也是一种内存调节技术之一。
*部分内容参考网络