linux下 DMA管理


DMA管理

make menuconfig后,生成新的配置.config。make时候,把新配置文件转化为autoconf.h,使用时候一般应用为include/linux/autoconf.h->mmdebug.h->mm.h。可以直接引用。
kconfig文件为menu的配置选项文件,功能设置比较简单,如下:
config DMA_MEM
 tristate "DMA Reserver Memory for SA600A"
 ---help---
   This option supports the DMA options for SA600A.

config DMA_MEM_MAX_COUNT
    int "Max RAM(Mbytes)"
 default "64"
 depends on DMA_MEM
 help
   The default value is 64M RAM. Change this if you know what you
   are doing.

config DMA_MEM_RESERVER_COUNT
    int "Reserver RAM(Mbytes)"
 default "24"
 depends on DMA_MEM
 help
   The default reserver value is 24M RAM. Change this if you know what you
   are doing.
这样autoconf.h就会生成3个宏:
#define CONFIG_DMA_MEM  1
#define CONFIG_DMA_MEM_MAX_COUNT 64
#define CONFIG_DMA_MEM_RESERVER_COUNT 24

 
linux内核内部提供3个刷新cache函数,使用前需要打开CONFIG_DMA_NONCOHERENT宏,这个宏内嵌在各个芯片默认的config文件中,如linux-2.6.27.28\arch\mips\configs\malta_defconfig
#define dma_cache_wback_inv(start, size) _dma_cache_wback_inv(start, size)
#define dma_cache_wback(start, size)  _dma_cache_wback(start, size)
#define dma_cache_inv(start, size)  _dma_cache_inv(start, size)

对外只通过_sys_sysmips带入FLUSH_CACHE参数,调用__flush_cache_all函数。如有需要可在syscall.c中增加新的用户态接口

系统共享内存的2种方式:
shm_open和open类似,通过一个文件(类似驱动)作为内存共享的中介(mmap)。所以系统必须要有/dev/shm才能使用shm_open。shm_open是posix的实现,可以实现无亲缘关系的进程共享内存。使用上较为常见。Shm_open的第一个参数,默认为在/dev/shm/下的文件,如:”mytemp”=/dev/shm/mytemp,Man帮助文档说明中指出,shm_open的名字必须以/开头,但是中间不能再有/
shmget是system实现,不依赖其他驱动。可以为无亲缘、亲缘进程共享,其特点是需要key。这个key生产可以通过ftok查找一个真实存在文件,通过此路径及进程内共享内存id号,生产特定的key值。也可以是亲缘进程通过IPC_PRIVATE共享(和shm_open不一样的是,这个文件并不是实现的中介,只是使用文件的节点信息)。Shmget使用上没有shm_open常见,但是更普遍的存在各个版本linux中(如各种BSD,只能使用shmget)

ftok原型如下:
key_t ftok( char * fname, int id )fname就时你指定的文件名,id是子序号。
在一般的UNIX实现中,是将文件的索引节点号取出,前面加上子序号得到key_t的返回值。
如指定文件的索引节点号为65538,换算成16进制为0x010002,而你指定的ID值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002。
查询文件索引节点号的方法是: ls -i
当删除重建文件后,索引节点号由操作系统根据当时文件系统的使用情况分配,因此与原来不同,所以得到的索引节点号也不同。如果要确保key_t值不变,要目确保ftok的文件不被删除,要么不用ftok,指定一个固定的key_t值。

shmget()是用来开辟/指向一块共享内存的函数。参数定义如下:
key_t shmkey 是这块共享内存的标识符。如果是父子关系的进程间通信的话,这个标识符用IPC_PRIVATE来代替。如两个进程没有任何关系,就用ftok()算出来一个标识符使用。
1、内核提供了remap_page_range函数,可以实现物理、内核高端地址,映射到用户态空间。但需要注意的是,对于内存而言,2.4内核必须设置PG_Reserved标记,防止内存被交换出去。因此2.4版本内核做这样的工作是比较繁琐的,需要一页页的做。到了2.6时代,PG_Reserved已经放弃了,而是使用vm_reserved标记,这是针对一片区域的属性,所以目前的实现可以比较简单。除了通过remap_pfn_range函数外,还可以使用零页方式实现。零页:延后建表功能。不过实现较为复杂。
2、内核提供了/dev/mem的设备,已经实现了部分功能。这里指的部分功能是有限的、不完整的。VM对于内存和寄存器应该有不同的区分。因此VM结构体有对应的VM_IO做区分。remap_page_range需要判断查看所请求的偏移量(保存在vma->vm_pgoff中)是否超出了物理内存。如果是,则设置VMA的VM_IO标志,以标志该区域为I/O内存。非内存的寄存器空间要标示为IO空间,防止core dump。(core dump:如程序运行时当掉,这时操作系统就会把程序当掉 时的内存内容 dump 出来,做为debugg参考。这个动作就叫作 core dump)
LDD2上提到,通过kmalloc和__get_free_pages得到的物理地址,不应该通过mmap进行用户态访问。原因是/dev/mem设备的实现中,并没有做VM_RESERVED和VM_IO的标记。因此对于内存对象而言,并不保证不置换出去(普通读写应无问题,DMA问题可能比较大)。对于IO地址而言,又不保证不做core dump。而此要利用mem设备,必须进行一定的修改。目前所见的显卡驱动中(与需求很类似),它们代码中都明确的写入此2标记,用作显存和显卡寄存器的访问(因均不为物理内存地址之内)。
PS:mem设备应该在bootmem_init函数,通过boot_mem_map数组来指定可访问的各个物理地址空间。对于mips而言是4G大小,而其他芯片有不同的限制。实际直接使用当前mips的编写也不完全正确。因为我们的芯片确实还有许多未定义的物理地址空间,应对此类地址加以检查,或需要上层使用时额外注意,否则可能会导致使用的exception。
3、kmalloc最终通过__get_free_pages函数获取物理连续页,但据说是kmalloc是不产生syscall的,而且申请大小限制在128K。
Kmalloc包含了许多工作,其中有分中断、非中断模式,还有smp相关的内容。标记解释如下:
情形                            相应标志
进程上下文,可以睡眠            使用GFP_KRENEL
进程上下文,不可以睡眠          使用GFP_ATOMIC,在你睡眠之前或之后用GFP_KERNEL执行内存分配
中断处理程序                    使用GFP_ATOMIC
软中断                          使用GFP_ATOMIC
tasklet                         使用GFP_ATOMIC
需要用于DMA的内存,可以调度     使用(GFP_DMA|GFP_KERNALE)
需要用于DMA的内存,不可以调度   使用(GFP_DMA|GFP_ATOMIC),在调度之前执行内存分配完毕
4、 kmalloc根据MAX_ORDER定义,决定一次最多能分配的内存大小。修改MAX_ORDER,保证大块内存的申请
能利用原有的slab分配是比较稳定、快速的方式。其中GFP_DMA指明内存从保留的DMA内存区域中分配,这就很符和当前的需求。kmem_cache_init初始化系统内存时,根据ZONE_DMA的位置,分配给cs_dmacachep。而其他内存则标记在ZONE_NORMAL,分配给cs_cachep。这就能实现系统申请普通内存和DMA内存的区分。
综上所述,目前的策略定为:
1、     使用ZONE_DMA段,作为大片内存的保留。此片内存的申请、释放的细节,通过kmalloc带入(GFP_DMA|GFP_ATOMIC)实现。如新建一个/dev/dmamem设备,并增加一个动态库做申请释放的接口。
2、     申请函数返回物理、用户态虚拟地址,并通过mmap进行用户态地址的映射。
3、     用户态驱动要访问物理寄存器,通过/dev/mem设备获取访问地址,也由动态库的提供的接口实现。mem设备要做一定的修改
4、     Mem设备和dmamem设备功能上有一定的重复,可以考虑合并。
动态库暂提供4个接口,dma_malloc、dma_free、mmap_iospace、munmap_iospace。
1、     dma_malloc返回物理、虚拟地址。
2、     dma_free传入物理、虚拟地址,进行内存unmap和释放动作。
3、     mmap_iospace接口传入寄存器的物理地址,则通过mem设备做IO映射。如传入内存物理地址,做reserved映射。
4、     munmap_iospace做虚拟地址的unmap动作。

再仔细查找了一下内核代码,发现不少驱动默认带入GFP_DMA标记。当系统有ZONE_DMA,则从DMA区域分配。没有则从ZONE_NORMAL分配。因此想利用ZONE_DMA需要排除其他驱动不再使用此标记,这样的可移植性较差。
因而改为保留区域是系统看不见的区域,使用mem设备在用户态mmap映射出3段虚拟地址空间,分别对应write though/write bak/uncache。这三段区域其实都是映射到同一段物理地址上,那么管理模块就可以根据需要,利用地址偏移就可以快速给出申请的虚拟、物理地址。
由于内核只提供了phys_mem_access_prot函数,做uncache内存的建表功能,所以要新增2个函数。参照pgprot_noncached实现,其实也很简单,只是把对应芯片的tag标记加上即可,如:prot = (prot & ~_CACHE_MASK) | _CACHE_WRITEBACK;。当然_CACHE_WRITEBACK没有定义的,查找一下芯片手册就能得出。

内存管理使用bitmap映射方式,内核提供了一套查找方法find_next_bit和find_next_zero_bit。对应大段、小端机器进行了算法改进。同时如果芯片支持bit检测功能,内核也加入对应的实现了。
值得一提的是,内核还提供了海明距离的优化函数:hweight32/hweight16/hweight8。能快速得出一个字段内有多少个bit是1。
在信息论中,两个等长字符串之间的汉明距离是两个字符串对应位置的不同字符的个数。换句话说,它就是将一个字符串变换成另外一个字符串所需要替换的字符个数。 例如:
1011101 与 1001001 之间的汉明距离是 2。
2143896 与 2233796 之间的汉明距离是 3。
"toned" 与 "roses" 之间的汉明距离是 3。
汉明重量是字符串相对于同样长度的零字符串的汉明距离,也就是说,它是字符串中非零的元素个数:对于二进制字符串来说,就是 1 的个数,所以 11101 的汉明重量是 4。


DMA及cache

这里提到的DMA设备是非PCI设备
在framebuffer的prob函数中,用到了这样一个函数,下面分析下它的作用
/*
*    s3c_fb_map_video_memory():
*   分配DRAM的缓存区给frame buffer。
*   这个缓存区是一个non-cached,non-buffered的。
*   这片内存区域允许调色板和像素在写入时不刷新cache缓存。
*   一旦这片区域重新映射,那么所有用来访问video memory的虚拟内存将会
*   对应另外一片新的区域,即另外一片物理地址
*/
int __init s3c_fb_map_video_memory(struct s3c_fb_info *fbi)
{
......
fbi->map_size_f1 = PAGE_ALIGN(fbi->fb.fix.smem_len + PAGE_SIZE);
fbi->map_cpu_f1 = dma_alloc_writecombine(fbi->dev, fbi->map_size_f1,
&fbi->map_dma_f1, GFP_KERNEL);
......
}


首先说说6410的DMA虚拟地址和物理地址的映射。2.6.29中,比2.6.24有一些出入

2.6.29内核中,在arch/arm/mm/dma-mapping.c 中实现了DMA映射的函数。其中

#define CONSISTENT_END    (0xffe00000)
#define CONSISTENT_BASE    (CONSISTENT_END - CONSISTENT_DMA_SIZE)


CONSISTENT_ENT 是DMA虚拟地址的结束地址
CONSISTENT_ENT 是DMA虚拟地址的起始地址

而CONSISTENT_DMA_SIZE定义在/arch/arm/include/asm/memory.h

/*
* Size of DMA-consistent memory region.  Must be multiple of 2M,
* between 2MB and 14MB inclusive.
*/
#ifndef CONSISTENT_DMA_SIZE
#define CONSISTENT_DMA_SIZE (SZ_8M + SZ_4M) //+ chachi - SZ_2M       
#endif


DMA的大小必须是2M的整数倍。且在2M和14M之间
原来定义的大小是2M,网上看到有些blog说不够用,会报错。考虑到将来如果移植摄像头和音频的话,将会占用不少DMA缓存,因此这里改到12M

由于在编写驱动的时候,出了很多错误,所以走了不少弯路,也学到不少东西。因此在研究
dma_writecombine函数的时,有必要先了解一下cache的一致性问题。

参考书:《Linux设备驱动开发详解》、《Linux设备驱动程序》第三版


cache的一致性
先理解cache的作用
CPU在访问内存时,首先判断所要访问的内容是否在Cache中,如果在,就称为“命中(hit)”,此时CPU直接从Cache中调用该内容;否则,就 称为“ 不命中”,CPU只好去内存中调用所需的子程序或指令了。CPU不但可以直接从Cache中读出内容,也可以直接往其中写入内容。由于Cache的存取速 率相当快,使得CPU的利用率大大提高,进而使整个系统的性能得以提升。

Cache的一致性就是直Cache中的数据,与对应的内存中的数据是一致的。

DMA是直接操作总线地址的,这里先当作物理地址来看待吧(系统总线地址和物理地址只是观察内存的角度不同)。如果cache缓存的内存区域不包括DMA分配到的区域,那么就没有一致性的问题。但是如果cache缓存包括了DMA目的地址的话,会出现什么什么问题呢?

问题出在,经过DMA操作,cache缓存对应的内存数据已经被修改了,而CPU本身不知道(DMA传输是不通过CPU的),它仍然认为cache中的数 据就是内存中的数据,以后访问Cache映射的内存时,它仍然使用旧的Cache数据。这样就发生Cache与内存的数据“不一致性”错误。

题外话:好像2.6.29内核中,6410的总线地址和物理地址是一样的,因为我在查看vir_to_bus函数的时候,发现在/arch/arm/linux/asm/memory.h中这样定义:

#ifndef __virt_to_bus
#define __virt_to_bus    __virt_to_phys
#define __bus_to_virt    __phys_to_virt
#endif

而且用source Insight搜索了一遍,没有发现6410相关的代码中,重新定义__vit_to_bus,因此擅自认为2.6内核中,6410的总线地址就是物理地址。希望高手指点。

顺便提一下,总线地址是从设备角度上看到的内存,物理地址是CPU的角度看到的未经过转换的内存(经过转换的是虚拟地址)

由上面可以看出,DMA如果使用cache,那么一定要考虑cache的一致性。解决DMA导致的一致性的方法最简单的就是禁止DMA目标地址范围内的cache功能。但是这样就会牺牲性能。

因此在DMA是否使用cache的问题上,可以根据DMA缓冲区期望保留的的时间长短来决策。DAM的映射就分为:一致性DMA映射和流式DMA映射。

一致性DMA映射申请的缓存区能够使用cache,并且保持cache一致性。一致性映射具有很长的生命周期,在这段时间内占用的映射寄存器,即使不使用也不会释放。生命周期为该驱动的生命周期

流式DMA映射实现比较复杂,因为没具体了解,就不说明了。只知道种方式的生命周期比较短,而且禁用cache。一些硬件对流式映射有优化。建立流式DMA映射,需要告诉内核数据的流动方向。


因为LCD随时都在使用,因此在Frame buffer驱动中,使用一致性DMA映射
上面的代码中用到
dma_alloc_writecombine函数,另外还有一个一致性DMA映射函数dma_alloc_coherent

两者的区别在于:
查看两者的源代码

/*
* Allocate DMA-coherent memory space and return both the kernel remapped
* virtual and bus address for that space.
*/
void *
dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp)
{
void *memory;

if (dma_alloc_from_coherent(dev, size, handle, &memory))
return memory;

if (arch_is_coherent()) {
void *virt;

virt = kmalloc(size, gfp);
if (!virt)
return NULL;
*handle =  virt_to_dma(dev, virt);

return virt;
}

return __dma_alloc(dev, size, handle, gfp,
pgprot_noncached(pgprot_kernel));
}


/*
* Allocate a writecombining region, in much the same way as
* dma_alloc_coherent above.
*/
void *
dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp)
{
return __dma_alloc(dev, size, handle, gfp,
pgprot_writecombine(pgprot_kernel));
}

#define pgprot_noncached(prot)  __pgprot(pgprot_val(prot) & ~(L_PTE_CACHEABLE | L_PTE_BUFFERABLE))
#define pgprot_writecombine(prot) __pgprot(pgprot_val(prot) & ~L_PTE_CACHEABLE)


再结合网上的资料(不过我感觉那文章写的有些问题,我修改了一下),由上面代码可以看出,两个函数都调用了__dma_alloc函数,区别只在于最后一个参数。


dma_alloc_coherent 在 arm 平台上会禁止页表项中的 C (Cacheable) 域以及 B (Bufferable)域。而 dma_alloc_writecombine 只禁止 C (Cacheable) 域.


 C 代表是否使用高速缓冲存储器, 而 B 代表是否使用写缓冲区。


这样,dma_alloc_writecombine 分配出来的内存不使用缓存,但是会使用写缓冲区。而 dma_alloc_coherent  则二者都不使用。

C B 位的具体含义
0 0 无cache,无写缓冲;任何对memory的读写都反映到总线上。对 memory 的操作过程中CPU需要等待。
0 1 无cache,有写缓冲;读操作直接反映到总线上;写操作,CPU将数据写入到写缓冲后继续运行,由写缓冲进行写回操作。
1 0 有cache,写通模式;读操作首先考虑cache hit;写操作时直接将数据写入写缓冲,如果同时出现cache hit,那么也更新cache。
1 1 有cache,写回模式;读操作首先考虑cache hit;写操作也首先考虑cache hit。

这样,两者的区别就很清楚了。


A = dma_alloc_writecombine(struct device *dev, size_t size ,dma_addr_t *handle, gfp_t gfp);

含义:
A          : 内存的虚拟起始地址,在内核要用此地址来操作所分配的内存
dev      : 可以平台初始化里指定,主要是用到dma_mask之类参数,可参考framebuffer
size      : 实际分配大小,传入dma_map_size即可
handle: 返回的内存物理地址,dma就可以用。

A和hanle是一一对应的,A是虚拟地址,而handle是总线地址。对任意一个操作都将改变写缓冲区内容。




发布了1 篇原创文章 · 获赞 1 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/likeping/article/details/42235111