linux中的DMA提速1:各种内存分配验证

前言

        驱动需要优化驱动中的DMA速度。在此记录。使用DMA传输1280*800的数据就是1M字节。

        使用外设的DMA功能,就需要给外设相应的DMA缓存寄存器设置一个地址,这个地址在内核中叫总线地址,而我们使用kmalloc分配的地址是虚拟地址,它是通过物理地址经过MMU转换得来的。内核中的三种地址,虚拟地址、物理地址、总线地址。

        1)mmap时使用的是物理地址。

        2)总线地址(例如:dma_addr_t),是通过虚拟地址得到的。在内核中,用的最多的就是虚拟地址。

        3)设置外设寄存器时,使用的是总线地址,例如本次学习的dma_addr_t类型的地址。

       看下面这段代码,virt_to_phys(p)就是虚拟机地址转换为物理地址。对了,mmap的时候使用的是物理地址。        

virt_to_phys(p) >> PAGE_SHIFT

        当前测试环境PAGE_SIZE为4KB,所以PAGE_SHIFT的值是12,这句代码是为了获得该物理地址的页帧号,由此可知,这个地址需要是页(PAGE)对齐的。       

#define PAGE_SHIFT	12

#ifdef __ASSEMBLY__
#define PAGE_SIZE	(1 << PAGE_SHIFT)
#else
#define PAGE_SIZE	(1UL << PAGE_SHIFT)
#endif

#define PAGE_MASK	(~(PAGE_SIZE-1))

static int csi_dev_mmap(struct file *file, struct vm_area_struct *vma) {
void *p = xxx;
...
    if (remap_pfn_range(vma, vma->vm_start, virt_to_phys(p) >> PAGE_SHIFT,
	        vma->vm_end-vma->vm_start, vma->vm_page_prot)) {
	        printk( "remap_pfn_range error\n");
	        return -EAGAIN;
	}
...
    return 0
}

        相关的宏__phys_to_pfn就是获得物理地址的页帧号,__pfn_to_phys是计算出页帧号对应的物理地址。pfn是page frame number的缩写。        

/*
 * Convert a physical address to a Page Frame Number and back
 */
#define	__phys_to_pfn(paddr)	((unsigned long)((paddr) >> PAGE_SHIFT))
#define	__pfn_to_phys(pfn)	((phys_addr_t)(pfn) << PAGE_SHIFT)

        提到虚拟地址,肯定是有MMU的,MMU是啥,MMU就是月老,负责物理地址与虚拟地址配对,一夫一妻吗?月老出生的年代是一夫一妻吗?没研究过。反正MMU不是一夫一妻的,一个物理地址,可以有多个虚拟地址,当然可以有对应的总线地址,一个虚拟地址同一时间只能映射到一个物理地址。

        本次学习的DMA提速探索,就需要全部使用这三个身份,物理地址<=>虚拟地址<=>总线地址(dma_addr_t)。

        还有两个函数用于虚拟地址和总线地址的转换,暂时用不到,先记录下来。

unsigned long virt_to_bus(volatile void *address);
void *bus_to_virt(unsigned long address);

一 验证的方案

        所以初级方案,并不是说,这种方案不好,而是这种办法适合于大部分场景,在内核中绝大多数代码也是这么写的。这种方案容易理解,在内核中可以很容易的借鉴到。

       方案一:dma_alloc_coherent函数

        这种方案操作简单,dma_alloc_coherent返回虚拟地址,并且给dma_addr赋值为对应的总线地址。这个地址就可以设置到外设寄存器中,外设就可以自动向该地址写入数据,写入完毕后就会触发中断。

char *buf;	
dma_addr_t dma_addr;
buf_size = 1280*800;
buf = dma_alloc_coherent(dev,
                    PAGE_ALIGN(buf_size),
                    &dma_addr,
                    GFP_DMA | GFP_KERNEL);
		if(IS_ERR(buf)){
			DEBUG_CM("kmalloc error");
			return -ENOMEM;
		}

方案二:dma_map_single+kmalloc

        kmalloc赋值分配空间,然后通过dma_map_single找到对应的总线地址,经过测试相较于前一种方案。速度上并没有提升。几乎就是一样的。

char *buf;	
dma_addr_t dma_addr;
buf_size = 1280*800;
buf = kmalloc(buf_size,GFP_KERNEL|GFP_DMA);		
		if(IS_ERR(buf)){
			DEBUG_CM("kmalloc error");
			return -ENOMEM;
		}
		new_node->dma_addr = dma_map_single(cm_dev->dev, 
buf, 
CSI_DEV_IMAGE_SIZE, 
DMA_FROM_DEVICE);

方案三:__get_free_pages+dma_map_single

        8表示2的八次方,就是256个页,页大小是4096.然后就是1048576,速度没有明显变化。我都开始怀疑,我这样测试是不是不对啊。

new_node->buf = (void *)__get_free_pages(GFP_KERNEL|GFP_DMA,8);		
		if(IS_ERR(new_node->buf)){
			kfree(new_node);
			DEBUG_CM("kmalloc error");
			spin_unlock_irqrestore(&cm_dev->slock,flags);
			return -ENOMEM;
		}
		new_node->buf_size = CSI_DEV_IMAGE_SIZE;
		new_node->dma_addr = dma_map_single(cm_dev->dev, new_node->buf, new_node->buf_size, DMA_FROM_DEVICE);

也可以写成下面这样,效果是一样的

        int order;
        new_node->buf_size = CSI_DEV_IMAGE_SIZE;
        order = get_order(CSI_DEV_IMAGE_SIZE);
		new_node->buf = (void *)__get_dma_pages(GFP_KERNEL, order);		
		if(IS_ERR(new_node->buf)){
			kfree(new_node);
			DEBUG_CM("kmalloc error");
			spin_unlock_irqrestore(&cm_dev->slock,flags);
			return -ENOMEM;
		}
		new_node->dma_addr = dma_map_single(cm_dev->dev, new_node->buf, new_node->buf_size, DMA_FROM_DEVICE);

二 后来验证的方案

方案一:pgprot_noncached

        如下所示,加上这个以后,在用户空间memcpy1280*800的数据,时间增加了20毫秒。

static int csi_dev_mmap(struct file *file, struct vm_area_struct *vma) {
...
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
...
}

        pgprot_noncached宏有何神奇之处呢?定义如下所示,L_PTE_MT_MASK, L_PTE_MT_UNCACHED这两个宏是和二级页表,三级页表相关的。

typedef struct {
	unsigned long pgprot;
} pgprot_t;

#define pgprot_val(x)	((x).pgprot)
#define __pgprot(x)	((pgprot_t) { (x) } )

#define __pgprot_modify(prot,mask,bits)		\
	__pgprot((pgprot_val(prot) & ~(mask)) | (bits))

#define pgprot_noncached(prot) \
	__pgprot_modify(prot, L_PTE_MT_MASK, L_PTE_MT_UNCACHED)

        pgprot_noncached()实际禁止了相关页的Cache和写缓冲(Write Buffer),pgprot_writecombine()则没有禁止写缓冲。ARM的写缓冲器是一个非常小的FIFO存储器,位于处理器核与主存之间,其目的在于将处理器核和Cache从较慢的主存写操作中解脱出来。写缓冲区与Cache在存储层次上处于同一层次,但是它只作用于写主存。

方案二:dma_mmap_coherent

相关头文件

#include <linux/dma-mapping.h>
#include <linux/dma-buf.h>

方案三:dma_alloc_writecombine,执行失败

		DEBUG_CSI_INIT("");
		new_node->buf_size = CSI_DEV_IMAGE_SIZE;
		new_node->buf = dma_alloc_writecombine(cm_dev->dev,
					PAGE_ALIGN(new_node->buf_size),
					&new_node->dma_addr,
					GFP_DMA | GFP_KERNEL);
		if(new_node->buf == NULL || IS_ERR(new_node->buf)){
			//kfree(new_node);
			DEBUG_CSI_INIT("new_node->buf = %p",new_node->buf);
			DEBUG_CSI_INIT("dma_alloc_writecombine error");
			//mutex_unlock(&cm_dev->lock);
			return -ENOMEM;
		}

经过测试,如果要使用这个函数,就需要实现struct dma_map_ops*指针,如下所示,如果没有自定义,就使用默认的arm_dma_ops。

static inline struct dma_map_ops *__generic_dma_ops(struct device *dev)
{
	if (dev && dev->archdata.dma_ops)
		return dev->archdata.dma_ops;
	return &arm_dma_ops;
}

       然后代码改成        

        DEBUG_CSI_INIT("");
		new_node->buf_size = CSI_DEV_IMAGE_SIZE;
		cm_dev->dev->archdata.dma_ops = NULL;
		new_node->buf = dma_alloc_writecombine(cm_dev->dev,
					PAGE_ALIGN(new_node->buf_size),
					&new_node->dma_addr,
					GFP_DMA | GFP_KERNEL);
		if(new_node->buf == NULL || IS_ERR(new_node->buf)){
			DEBUG_CSI_INIT("new_node->buf = %p",new_node->buf);
			DEBUG_CSI_INIT("dma_alloc_writecombine error");
			return -ENOMEM;
		}
		DEBUG_CSI_INIT("");

         第一次执行失败,原因是dma_alloc_writecombine的调用两侧加了自旋锁,去掉自旋锁后。就执行成功了,得出结论:dma_alloc_writecombine的不能位于自旋锁区间。

        这个运行速度,几乎和以前一样。

方案四:dmam_alloc_noncoherent,执行失败

或者dma_alloc_noncoherent,都执行失败。

        DEBUG_CSI_INIT("");
		new_node->buf_size = CSI_DEV_IMAGE_SIZE;
		new_node->buf = dmam_alloc_noncoherent(cm_dev->dev,
					PAGE_ALIGN(new_node->buf_size),
					&new_node->dma_addr,
					GFP_DMA | GFP_KERNEL);
		if(new_node->buf == NULL || IS_ERR(new_node->buf)){
			//kfree(new_node);
			DEBUG_CSI_INIT("new_node->buf = %p",new_node->buf);
			DEBUG_CSI_INIT("dmam_alloc_noncoherent error");
			//mutex_unlock(&cm_dev->lock);
			return -ENOMEM;
		}

[   62.372363] init:/big/csi_driver/csi_ov/csi_dev.c:csi_dev_node_array_init:1769: new_node->buf =   (null)
[   62.381852] init:/big/csi_driver/csi_ov/csi_dev.c:csi_dev_node_array_init:1770: dmam_alloc_noncoherent error

三 后来的后来加的

方案1:cma

        Contiguous Memory Allocator, CMA,连续内存分配器,用于分配连续的大块内存。CMA分配器,会Reserve一片物理内存区域:设备驱动不用时,内存管理系统将该区域用于分配和管理可移动类型页面;设备驱动使用时,用于连续内存分配,此时已经分配的页面需要进行迁移;此外,CMA分配器还可以与DMA子系统集成在一起,使用DMA的设备驱动程序无需使用单独的CMA API。

cma的设备树:由内容可知,cma的大小是8X2^{24} = 8X2^{4}X1M = 128M

reserved-memory {
	#address-cells = <1>;
	#size-cells = <1>;
	ranges;

	linux,cma {
		compatible = "shared-dma-pool";
		reusable;
		size = <0x8000000>;
		linux,cma-default;
	};
};

相关的头文件

#include <linux/dma-contiguous.h>

dma_alloc_from_contiguous函数原型

#define CONFIG_CMA_ALIGNMENT 8
/**
 * dma_alloc_from_contiguous() - allocate pages from contiguous area
 * @dev:   Pointer to device for which the allocation is performed.
 * @count: Requested number of pages.
 * @align: Requested alignment of pages (in PAGE_SIZE order).
 *
 * This function allocates memory buffer for specified device. It uses
 * device specific contiguous memory area if available or the default
 * global one. Requires architecture specific dev_get_cma_area() helper
 * function.
 */
struct page *dma_alloc_from_contiguous(struct device *dev, int count,
				       unsigned int align)
{
	if (align > CONFIG_CMA_ALIGNMENT)
		align = CONFIG_CMA_ALIGNMENT;

	return cma_alloc(dev_get_cma_area(dev), count, align);
}
/**
 * dma_alloc_from_contiguous() - allocate pages from contiguous area
 * @dev:   Pointer to device for which the allocation is performed.
 * @count: Requested number of pages.
 * @align: Requested alignment of pages (in PAGE_SIZE order).
 *
 * This function allocates memory buffer for specified device. It uses
 * device specific contiguous memory area if available or the default
 * global one. Requires architecture specific dev_get_cma_area() helper
 * function.
 */
struct page *dma_alloc_from_contiguous(struct device *dev, int count,
				       unsigned int align)
{
	if (align > CONFIG_CMA_ALIGNMENT)
		align = CONFIG_CMA_ALIGNMENT;

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

       使用这个函数时,编译报错,这不是WARNING吗?函数都找不到,就是错误。        

WARNING: "dma_alloc_from_contiguous" [/big/csi_driver/csi_ov/csi_dev.ko] undefined!

         dma_alloc_from_contiguous函数默认情况下并没有导出符号,这是不是意味着内核不想让我们使用它呢?修改文件drivers\base\dma-contiguous.c,添加导出符号。        

EXPORT_SYMBOL(dma_alloc_from_contiguous);

测试代码,测试结果能工作,相比使用__get_dma_pages分配的内存缓冲区,速度没有提升。

DEBUG_CSI_INIT("CONFIG_CMA_ALIGNMENT =%d",CONFIG_CMA_ALIGNMENT);
		new_node->buf_size = CSI_DEV_IMAGE_SIZE;
		order = get_order(new_node->buf_size);
		new_node->page = dma_alloc_from_contiguous(cm_dev->dev,
			new_node->buf_size >> PAGE_SHIFT,
			order);

		if(new_node->page == NULL || IS_ERR(new_node->page)){
			DEBUG_CSI_INIT("new_node->page = %p",new_node->buf);
			DEBUG_CSI_INIT("dma_alloc_from_contiguous error");
			return -ENOMEM;
		}
		new_node->buf = (char*)page_to_phys(new_node->page);
		new_node->dma_addr = dma_map_single(cm_dev->dev, new_node->buf, new_node->buf_size, DMA_FROM_DEVICE);

结束

  • 5
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

千册

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

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

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

打赏作者

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

抵扣说明:

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

余额充值