linux内存调节之CMA

本文贴代码过头了,以后想起来再优化一下吧

目录

概述

数据结构

构建初始化(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也是一种内存调节技术之一。

 

*部分内容参考网络

 

 

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值