从零开始学Linux设备驱动--Linux内存管理与DMA(万字长文)

内存管理与DMA

一、概述

  • 我们知道类似于ARM这种结构,操作硬件都是通过特殊功能寄存器(SFR)来进行的,他们和内存统一编址,也叫IO内存
  • 下面我们一起来看一下Linux系统是如何来对内存进行管理的。

二、内存管理

  • 从内存管理的方法来分,可以将计算机分为两种类型,一种是UMA(uniform memory access,一致内存访问)计算机,另一种是NUMA(non-uniform memory access,非一致内存访问)计算机。

  • UMA计算机:每一CPU访问的都是同一块内存,分CPU对内存的访问不存在性能差异。

  • NUMA计算机:各内存和各CPU通过总线联系在一起,每个CPU都有一个本地内存,访问速度较快,CPU也可以访问其他CPU的本地内存,但速度较慢。(找到了一个还不错的画图工具,所以这次画的图应该还能看)

在这里插入图片描述

  • 为了流通和交互,存在两种不同的模式可定是不科学的。Linux为了统一这两种平台,在内存组织中,将最高层次定义为内存节点。如上图所示,UMA含有一个内存节点,NUMA含有两个内存节点。那么UMA是不是就可以看做是只有一个内存节点的NUMA呢?
  • 出于内存分配来考虑,当腰分配一块内存时,应该先考虑从CPU的本地内存对应的节点来分配内存,如果不能满足要求,再考虑从非本地内存节点上分配内存。

Linux内存管理的第二个层次为,每个内存节点可以划分为多个区。可以联想到我们Windows电脑下的为硬盘分区,然后每个分区方不同的文件。当然本质上还是有很大差别的。目前内核中定义了以下几个分区:

  • ZONE_DMA:适合于DMA操作的内存区
  • ZONE_NORMAL:常规内存区域,指的是可以直接映射到内核空间的内存,所谓直接映射是指物理地址和映射后的虚拟地址之间存在着一种简单的关系,那就是物理地址加上一个固定的偏移就可以得到映射之后的虚拟内存地址。假设32位系统上,用户空间为3GB,内核空间为1GB。那么这个偏移就是0xC0000000(该值物理内存的起始地址)。内核空间只有1GB,一般除去用作特殊目的的一段内核内存空间(通常是最高128MB内存空间),常规的内存区域通常指的就是低于896MB的这部分物理内存。也就是在内和空间最频繁使用的一段内存空间。在32位系统上,指的是除去ZONE_DMA而低于896MB的这段物理内存。
  • ZONE_HIGHHEM:高端内存。在32位系统中通常指的是高于896MB的物理内存;而在64位操作系统上,因为内核空间可以很大,所以一般没有高端内存。要将这段物理内存映射到内核空间的话,需要通过单独的映射来完成,而这种映射通常是不能保证物理地址和虚拟地址之间的固定对应关系。
  • ZONE_MOVABLE:一个位内存区域,在防止物理内存碎片时会用到该片区域。

将内存按区域划分主要是为了满足特定的需求。

Linux内存管理的第三个层次为页。对物理内存而言,通常叫做页帧页框。页的大小由CPU的内存管理单元MMU来决定,而通常MMU又支持了不同的页大小。ARM体系结构中,一个页的大小有4KB和1MB。目前常见的为4KB,Linux用struct page来表示一个物理页。

关于内存管理的总结:

  1. Linux内核的内存管理,首先将内存分成若干个大的节点,然后将每个节点又划分成若干个区,而每个区又包含若干页。
  2. Linux是按页来管理内存的,最基本的内存分配和释放都是按页进行的。

三、内存分配

1. 按页分配内存
struct page *alloc_pages(gfp_t gfp_mask,unsigned int order);
/*
1.alloc_pages 分配2的order次方连续的物理页,返回值为起始页的struct page对象地址。
2.Linux是按照伙伴系统来管理物理内存的,所以分配的页数为2的order次方。
*/
alloc_page(gfp_mask);
/*
1.很显然,alloc_page就是用于分配单独的一页物理内存,也就是order为0的alloc_pages函数的封装。
*/
void __free_pages(struct page *page,unsigned int order);
/*
1. __free_pages用于释放这些分配得到的页,page为分配时得到的起始页的struct page对象的地址
*/
  • 上面讲了与页分配相关的几个函数,很显然,这三个函数有一个共同的参数gfp_mask,我没有讲到。因为是想把它单独拎出来讲一下。gfp_mask是用于控制页面分配行为的一个掩码值。下面列出比较常见的一些掩码(位于内核源码include/linux/gfp.h):
#define GFP_ATOMIC	(__GFP_HIGH)
/*
1.告诉分配器以原子的方式分配内存,即在内存分配期间不能够引起进程切换。(如果内存不够,内核会唤醒一些内核进程来尝试回收一些内存)
2.这在某些有特殊要求的情况下非常有用,比如我们前面知道的中断上下文和持*有自旋锁的上下文中,必须使用该掩码来获取内存。
3.该掩码还表明了可以使用紧急情况下的保留内存。
*/
#define GFP_KERNEL	(__GFP_WAIT | __GFP_IO | __GFP_FS)
/*
1.最常用的内存分配掩码,具体来讲就是在内存分配允许进程切换,可以进行IO操作(如为得到空闲页,将页面暂时换出到磁盘上)。允许执行文件系统操作。
*/
#define GFP_USER	(__GFP_HIGH | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
/*
1.用于为用户空间分配内存页,在内存分配过程中允许进程切换。
*/
#define GFP_DMA	(__GFP_DMA)
/*
1.告诉分配器只能在ZONE_DMA区分配内存,用于DMA操作。
*/

可以看到这些宏不过是一些__GFP_xxx的组合
如果指定了__GFP_HIGHMEM,那么分配器优先会在ZONE_HIGHMEM区域内查找空闲内存,如果没有,则退到ZONE_DMA中查找。
  • 上面讲到了页分配函数的一些常见的掩码。上面的页分配函数返回的都是管理物理页面的struct page对象地址,而我们通常需要的是物理内存在内核中虚拟地址。那么怎么办呢?
  • 前面讲到过,对于不是高端内存的物理地址,虚拟地址和它之间有一个偏移。通过这个关系,我们就能够得到其对应的虚拟地址,从而将物理地址与虚拟地址之间一一对应起来。但是对于高端内存的虚拟地址的获取就要麻烦一些,内核需要操作页表来建立映射。相关的函数或宏如下:
void *page_address(const struct page *page);
/*
只用于非高端内存的虚拟地址的获取,参数page是分配页得到的struct page 对象地址,返回该物理页对应的内核空间虚拟地址。
*/
void *kmap(struct page *page);
/*
用于返回分配的高端或者非高端内存的虚拟地址,如果不是高端内存,则内部调用的其实是page_adress,也叫作永久映射。但是内核中用于永久映射的区域非常小,所以在不使用的时候,应该尽快排出映射。该函数可能会引起休眠。                                             
*/
void *kmap_atomic(struct page *page);
/*
和kmap功能类似,但操作是原子性的,也叫临时映射。
*/
void kunmap(struct page *page);
/*
用于解除前面提到的映射
*/
  • 一般我们分配内存分为两步:1.获取物理内存页 2.返回内核的虚拟地址。这可步骤可能略显繁琐,首先我们需要分配物理内存页,然后在将其映射到虚拟地址,最后才能得到我们所需要的可以使用的内存。所以内核又提供了另外一组将上面两个步骤合二为一的函数,常见的函数如下:
unsigned long __get_free_pages(gfp_t gfp_mask,unsigned int order);
//分配2的order次方的页
unsigned long __get_free_page(gfp_t gfp_mask);
//分配单独的一页物理内存
unsigned long get_zeroed_page(gfp_t gfp_mask);
//获取清零页
void free_pages(unsigned long addr,unsigned int order);
//释放2的order次方页
void free_page(unsigned long addr);
//释放一页

@gfp_mask :和前文提到的掩码一样
@addr :分配得到的内核虚拟地址
  • 常见的用法
void *kva;

kva = (void *)__get_free_pages(GFP_KERNEL,2);
...
free_pages((unsigned long)kva,2);
2. slab 分配器
  • 前面我们讲到的内存分配函数一般都是按页来分配的,而Linux中一页一般是4kb,而我们驱动中通常较少会使用到如此巨大的内存,跟多情况下是若干个字节。那么我们就得使用另一种针对小块内存分配的工具了—slab分配器。
  • slab分配器的工作很复杂,但是原理却很简单。主要的设计思想是:首先利用页面分配器预先分配若干个页,然后将这若干个页按照一个特定的对象大小进行切分,要获取一个对象,就从这个对象的高速缓存中来获取,使用完后,再释放到同样的对象高速缓存中。所以slab分配器不仅具有能够管理小块内存的能力,同时也提供类似高速缓存的作用,使对象的分配和释放变得非常迅速。我们内核中经常使用的结构对象通常使用这种方式来进行管理,比如inode、task_struct等。另外对象的高速缓存还能自动的动态伸缩,如果当前系统内存比较紧张,那么内核会尝试回收一部分没有用到的对象高速缓存,如果段时间内需要分配很多的对象而对象的高速缓存不够用时,内核会增加动态高速缓存。
  • slab分配器相关PAI如下:
struct kmem_cache *kmem_cache_create(const char *name,size_t size,size_t align,unsigned long flags,void (*ctor)(void *));
/*
功能:用于创建一个高速缓存
参数:name  : 高速缓存的名字,在proc/slabinfo中可以查到该信息
	 size  : 对象的大小
	 align : slab内的一个对象偏移,也就是对齐设置,通常为0即可
	 flags : 可选的参数设置项,用于进一步控制slab控制器,详细可以参考/include/linux/slab.h。如果没有特殊要求一般可以设为0.
	 ctor  : 追加新的页到高速缓存中用到的构造函数,通常不需要,为NULL即可。函数返回高速缓存的结构对象地址。
*/
void *kmem_cache_alloc(struct kmem_cache *cachep,gfp_t flags);
/*
从对象高速缓存cachep中返回一个对象,flags是分配的掩码。
*/
void kmem_cache_free(struct kmem_cache *cache,void *objp);
/*
释放一个对象到高速缓存cachep中
*/
void kemem(struct kmem_cache *cachep);
/*
销毁对象高速缓存
*/
  • 给出一简答的例子,这样我们就更能理解其用法:
#include <linux/slab.h>

struct test{
    char c;
    int i;
};

static struct kmem_cache *test_cache;
static struct test *t;
...
//创建一个高速缓存,我们可以在/proc/slabinfo中找到"test_cache"
test_cache = kmem_cache_create("test_cache",sizeof(struct test),0,0,NULL);
if(!test_cache)
    return -ENMEM;
...
//从创建的高速缓存中返回一个对象
t = kmem_cache_alloc(test_cache,GFP_KERNEL);
if(!t)
    return -ENOMEM;
...
...
//释放对象到高速缓存
kmem_cache_free(test_cache,t);
...
//销毁高速缓存
kmem_cache_destroy(test_cache);
  • 上面我们看到使用slab分配器来分配一个对象的过程,但是过程却是比较繁琐的,当然了这是相对于应用程序中malloc和free来讲的。内核也专门创建了一些常见大小的对象高速缓存,来简单快速分配和释放若干个字节。我们可以通过如下指令来查看:

    cat /proc/slabinfo | grep kmalloc
    
  • 内核预先创建了很多八字节、十六字节等大小的对象高速缓存,我们可以使用一组简单的函数来分配和释放这些对象。

void *kmalloc(size_t size,gfp_t flags);
/*
类似于malloc,参数size指定了要分配内存的大小,不一定必须是8,16等.比如size的值为13,那么内核汇分配一个16字节大小的内存。虽然这样做浪费了内存,但是却提高了内存管理的简单性和高效性,这样做是值得的。flags是分配掩码,函数返回内存虚拟地址,返回NULL表示分配失败。
*/
void *kzalloc(size_t size,gfp_t flags);
/*
和kmalloc功能相同,不过使用该函数分配的内存预先会被清零。
*/
void kfree(const void*);
/*
释放由kmalloc分配的内存
*/
  • 可以看到,这三个函数已经变的非常简单好用了。和应用层的malloc和free基本么有差异了。所以一般在写驱动程序的时候,我们若没有其他需要一般推荐使用这几个函数。
3.不连续内存页分配
  • 内存分配函数能保证所分配的内存在物理地址空间是连续的,但是这个特点使得想要分配大块的内存变得困难。因为频繁的分配和释放将会导致碎片的产生,这可能会导致本来有足够多的物理内存可用,但是因为不连续而不能分配的问题。(这是因为伙伴系统的工作方式决定的,关于伙伴系统后面会专门写一篇博文来介绍,这里主要讲基础知识)。为了解决此问题,内核提供了相应的措施来解决。以下几个API用来分配不连续物理地址:
void *vmalloc(unsigned long size);
/*
用于分配指定size大小的内存
*/
void *vzalloc(unsigned long size);
/*
分配size大小的内存前会先将分配的内存清零
*/
void vfree(const void *addr);
/*
释放内存,addr为要释放的内核的虚拟地址。
*/
4. per-CPU变量
  • per-CPU变量就是每一CPU有一个变量副本,一个典型的用法就是统计各个CPU上的一些信息。比如,每一CPU上可以保存一个运行在该CPU上的进程的数量,要统计整个系统的进程数,则可以将每个CPU上的进程数相加。
  • 最新的内核提供了一些较新的方法来定义使用per-CPU变量,主要的宏和函数如下:
DEFINE_FER_CPU(type,name);
//定义一个类型为type、名字为name的per-CPU变量
DELARE_PER_CPU(TYPE,NAME);
//申明在别的地方定义的per-CPU变量
get_cpu_var(var);
//禁止内核抢占,获得当前处理器上的变狼var
put_cpu_var(var);
//重新使能内核抢占
per_cpu(var,cpu);
//获取其他CPU上的变量var
alloc_percpu(type);
//动态分配一个类型为type的per-CPU变量
void free_percpu(void __percpu *__pdata);
//释放动态分配的per-CPU变量
for_each_possible_cpu(cpu);
//遍历每一个可能CPU,cpu是获得的CPU编号
  • 在内核源码"/kernel/fork.c"文件中有一个统计进程数的函数,该函数可以较好的说明per-CPU变量的使用。
//定义一个类型为unsigned long,名字为process_counts的per-CPU变量,并赋初值为0
DEFINE_PER_CPU(unsigned long,process_counts) = 0;
...
int nr_process(void)
{
    int cpu;
    int total = 0;
    
    for_each_possible_cpu(cpu)
    {
        //获取所有CPU上并获取其process_counts变量,并将其相加
        total += per_cpu(process_counts,cpu);
    }
    //返回总的进程数
    return total;
}
5. I/O 内存
  • 文章开头我们曾经提到过,类似于ARM体系结构中,硬件的访问是通过一组特殊功能寄存器(SFR)的操作来实现的。他们统一编址,访问上和内存基本一致,但是有特殊的意义,既可以通过访问他们来控制硬件设备(I/O设备),所以其在Linux内核中其也叫作I/O内存。我们在编写裸机驱动程序的时候知道,我们可以通过芯片手册来查找特定寄存器的地址然后对其进行读写来控制某些设备,比如LED、按键等。
  • 但是在有操作系统的开发板上则不一样,操作系统内核不允许我们直接访问物理地址,而应该使用虚拟地址,所以对这类硬件地址的访问也必须要经过映射才行。为此内核中提供了一组API用于对这部分内存的访问。
request_mem_region(start,n,name);
/*用于创建一个从start开始的n字节物理内存资源,名字为name,并标记为忙状态,也就是想内核申请一段I/O内存空间的使用权。返回值为创建的资源对象地址,类型为struct resource *,NULL表示失败。
在使用一段IO内存之前,一般要先用该函数进行申请,相当于国家对一块灵图宣誓主权,这样可以阻止其他驱动申请这块IO内存资源。创建的资源可以在"/proc/meminfo"文件中看到
*/
release_mem_region(start,n);
/*
用于释放之前创建(或申请)的IO内存资源
*/

void __iomem *ioremap(phys_addr_t offset,unsigned long size);
/*
映射从offset开始的size字节IO内存,返回值为对应的虚拟地址,NULL表示映射失败。
*/
void iounmap(void __iomap *addr);
/*
解除之前的IO内存映射
*/

u8  readb(const volatible void __iomem *addr);
u16 readw(const volatible void __iomem *addr);
u32 readl(const volatitle void __iomem *addr);
/*
上面三个函数表示分别按1字节、2个字节、4字节读取映射之后地址为addr的IO内存,返回值为读取的IO内存内容。
*/

void writeb(u8 b,volatile void __iomem *addr);
void writew(u16 b,volatile void __iomem *addr);
void writel(u16 b,volatile void __iomem *addr);
/*
上面三个函数表示分别按1字节、2个字节、4字节写入映射之后地址为addr的IO内存。
*/

ioread8(addr);
ioread16(addr);
ioread32(addr);

iowrite8(v,addr);
iowrite16(v,addr);
iowrite32(v,addr);
/*
上面六个函数为IO读写的另一种形式。
*/

ioread8_rep(p,dst,count);
ioread16_rep(p,dst,count);
ioread32_rep(p,dst,count);

iowrite8_rep(p,src,count);
iowrite16_rep(p,src,count);
iowrite32_rep(p,src,count);
/*
上面六个函数为IO读写的另一种形式,加rep表示连续读写count个单元
*/

四、DMA原理及映射

1.DMA原理
  • DMA(Direct Memory Access,直接内存存取),在驱动中有着广泛的应用。其原理图如下所示:

在这里插入图片描述

  • DMAC是DMA控制器,外设是一个可以手法数据的设备,比如网卡或串口。一般情况下,我们要将内存中的一块数据通过外设发送出去,如果没有DMAC参与,那么一般是这样完成的(ARM体系架构下):CPU通过LDR指令将数据读入到CPU的寄存器中,然后在通过STR指令写入到外设的发送FIFO中。因为外设的FIFO不会很大,对于串口而言通常只有几十个字节,如果要发送的数据很多,那么CPU一次只能搬移较少的一部分数据到外设的发送FIFO中。等待外设将FIFO汇总的数据发送完成(或一部分)后,外设产生一个中断,告知CPU继续将剩下的数据搬移到外设的发送FIFO中。整个过程中,CPU需要不停搬移数据,会产生多次中断。这会给CPU带来不小的负担,在数据比较多的时候就会更加明显。
  • 当有DMA时,其工作方式是这样的:CPU只需要告诉DMAC要将某个内存起始处的若干个字节搬移到某个具体外设的发送FIFO中,然后启动DMAC控制器开始数据搬移,CPU就可以去做其他的事情了。DMAC会在存储器总线空闲时搬移数据当所有的数据搬移完成,DMAC就产生一个中断,通过CPU数据搬移完成。这样一来CPU就负担就得到了大大减轻,也提高了其工作效率。这个过程中DMAC就相当于CPU的一个专门负责数据传送的助理一样。
  • 从DAMC的工作过程可以看出DMAC工作需要以下几个重要信息,那就是源地址、目的地址、传输字节数和传输方向。传输方向一般包括以下几种:
    1. 内存到内存
    2. 内存到外设
    3. 外设到内存
    4. 外设到外设
  • CPU发出的地址是物理地址,这是根据硬件设计所决定的地址,可以通过原理图和芯片手册来获得此地址。一般作为驱动开发者,我们使用的地址为虚拟地址,通过对MMU(内存管理单元),可以将这个虚拟地址映射到一个对应的物理地址上。最后DMAC看到的内存或外设的地址,这个叫做总线地址。大多数系统中总线地址和物理地址相同。显然,CPU在告诉DMAC源地址和目的地址时,应该使用的是物理地址,而不是虚拟地址,因为DMAC通常不经过MMU。这样一来,我们在驱动中必须要将虚拟地址转换成对应的物理地址,然后再进行DMA操作
  • 然后是缓存一致性问题,现代CPU都有高速缓存(cache),他可以在很大程度上提高效率。告诉缓存访问速度相比于内存来说快得多,但是容量一般不能很大。比如,我们在读取数据时,CPU要去读取内存中的一段数据,首先要在高速缓存中查找是否有要读取的数据,如果有则成为缓存命中。那么CPU直接从缓存中获取数据,就没必要去访问读写速度要慢很多的内存了。如果高速缓存中没有需要的数据,那么CPU再从内存中获取数据,同时将数据的副本放在高速缓存中,下次再访问同样的数据就可以直接从高速缓存中获取。基于程序的空间局部性事件局部性原理,缓存的命中率通常比较高,这就大大提高了效率。
  • 但是由此也引发了较多的问题,在DMA传输中就存在一个典型的不一致问题。假设DMAC按照CPU的要求将外设接收FIFO中的数据版移到了指定的内存中,但是因为高速缓存的原因,CPU将会从缓存中获取数据而不是从内存中获取数据,而是从高速缓存中获取数据,这就造成了不一致的问题。所以驱动程序中必须要保证用于DMA操作的内存关闭高速缓存这一特性。(同样的,我们在编写驱动程序的时候很多全局变量都用volatile来修饰,也是为了是数据保持一致性)
2.DMA映射
  • DMA映射的主要工作是找到一块适合用于DMA操作的内存,返回其虚拟地址总线地址关闭高速缓存等。
  • DMA 映射主要由一下几种方式。
  1. 一致性DMA映射
#define dma_alloc_coherent(d,s,h,f) dma_alloc_attrs(d,s,h,f,NULL)
/*
功能:返回可用于DMA操作的内存(DMA缓存区)虚拟地址
参数:d :DMA设备
	 s :需要的DMA缓冲区的大小
	 h :得到的DMA缓冲区的总线地址
	 f :内存分配掩码
补充:一般在ARM体系结构中,内核地址空间中的0xFFC00000到0xFFEFFFFF共计3MB的地址空间专门用于此类映射,它可以保证这段范围内所映射到的物理内存能用于DMA操作并且cache是关闭的。
*/

static inline void *dma_alloc_attrs(struct device *dev,size_t size,dma_addr_t *dma_handle ,gfp_t flag,struct dma_attrs *attrs);
  • 如果DMA缓冲区远小于一页,应该考虑DMA池。这种映射的优点是一旦DMA缓冲区建立,就再也不用担心cache的一致性问题,它通常适合于DMA缓冲区要存在于整个驱动程序的生存周期的情况。但是这个缓冲区只有3MB,多个驱动都长期使用的话最终会被分配完,导致其他驱动没有空间可以建立映射,要释放DMA映射,可以使用以下宏:
#define dma_free_coherent(d,s,c,h) dma_free_attrs(d,s,c,h,NULL)
//这里只需补充一个参数的含义,C: 之前映射得到的虚拟地址9

static inline void *dma_free_attrs(struct device *dev,size_t size,dma_addr_t *dma_handle ,gfp_t flag,struct dma_attrs *attrs);
  1. 流式DMA映射

    如果用于DMA操作的缓冲区不是驱动分配的,而是有内核的其他代码传递过来的,那么就需要进行流式DMA映射。比如,我们写一个SD卡的驱动,上层的内核diamante要通过SPI设备驱动来读取SD卡中的数据,那么上鞥的内核代码会传递一个缓冲区指针,而对于该缓冲区就应该建立流式DMA映射。

dma_map_single(d,a,s,r);
/*
功能:建立流式DMA映射
参数:d :DMA设备
	 a :上层传递过来的缓冲区地址
	 s :大小
	 r :DMA传输方向,一般有以下几种
	 	DMA_MEM_TO_MEM
	 	DMA_MEM_TO_DEV
	 	DMA_DEV_TO_MEM
	 	DMA_DEV_TO_DEV
	 	但不是所有的DMAC都支持这些方向,要查看相应的手册
*/
dma_unmap_single(d,a,s,r);
//解除流式DMA映射
  • 流式DMA映射不会长期占用一致性映射的空间,并且开销比较小,所以一般推荐使用这种方式。
  • 但是流式映射也会存在一个问题,那就是上层所给的缓冲区所对应的物理内存不一定可以用作DMA操作。
  • 在ARM体系中,流式映射其实是通过使cache无效和写通操作来实现的,如果是读方向,那么DMA操作完成后,CPU在读内存之前只要操作cache,使这部分内存所对应的cache无效即可,这会导致CPU在cache中查找数据时caches时不被命中,从而强制CPU到内存中去获取数据。这对于写方向,则是设置为写通的方式,即保证CPU的数据会更新到DMA缓冲区中。
3. 分散/聚集映射
  • 磁盘设备通常支持分散/聚集操作,例如readv和write系统调用所产生的集群磁盘IO请求。对于写操作就是把虚拟地址分散的各个缓冲区的数据写入到磁盘,对于读操作就是把磁盘的数据读取到各个分散的缓冲区中。如果流式DMA映射,那就需要依次依次映射每一个缓冲区,DMA操作完成后再映射下一个。这会给驱动编程造成一些麻烦,如果能够依次映射多个分散缓冲区,显然会方便的多。分散聚集就是完成该任务的,主要函数原型如下。
int dma_map_sg(struct device *dev,struct scatterlist *sg,int nents,enum dma_data_direction dir);
/*
功能:分散聚集映射
参数:dev: DMA设备
	 sg: 指向struct scatterlist类型数组的首元素的指针,数组中的每一元素都描述了一个缓冲区,包括缓冲区对应的物理页框信息、缓冲区在物理页框中的偏移、缓冲区的长度和映射后得到的DMA总线地址等。
	 nents: 缓冲区的个数
	 dir: DMA的传输方向。
	 
该函数会遍历sg数组中的每一个元素,然后对每一个缓冲区做流式DMA映射。
*/
void dma_unmap_sg(struct device *dev,struct scatterlist *sg,int nents,enum dma_data_direction dir);
/*
解除分散/聚集映射,各参数的含义同上。
*/
4. DMA池
  • 前面我们说到一致性DMA映射适合映射比较大的缓冲区,通常是页大小的整数倍,而较小的DMA缓冲区则用DMA池更适合。DMA池和slab分配器的工作原理非常相似,就连函数就连函数接口的名字也非常相似。DMA池就是预先分配一个大的DMA缓冲区,然后在这个大的缓冲区中分配和释放较小的缓冲区。
struct dma_pool_create(const char *name,struct device *dev,size_t size,size_t align,size_t boundary);
/*
功能:创建DMA池
参数:name: DMA池的名字
	 dev: DMA设备
	 size: DMA池大小
	 align: 对齐值
	 boundary: 边界值,设为0则由大小和对齐来自动决定边界。
返回值: 函数返回DMA池对象的地址,NULL表示失败。
说明: 该函数不能用于中断上下文。
*/
void *dma_pool_alloc(struct dma_pool *pool,gfp_t mem_flags,dma_addr_t *handle);
/*
功能:从DMA池中分配一块DMA缓冲区
参数:mem_flags: 为分配掩码
	 handle: 是回传的DMA总线地址。
返回值:返回虚拟地址,返回NULL表示失败
*/
void dma_pool_free(struct dma_pool *pool,void *addr,dam_addr_t dma);
/*
功能:释放DMA缓冲区到DMA池pool中。
参数:vaddr: 虚拟地址
	 dma: DMA总线地址
*/
void dma_pool_destory(struct dma_pool *pool);
/*
功能:销毁DMA池pool
*/
5. 回弹缓冲区
  • 上层所传递的缓冲区所对应的物理内存应该能够执行DMA操作才可以。
  • 实现原理:在驱动中分配一块能够用于DMA操作的缓冲区,如果是写操作,那么将上层传递下来的数据先复制到DMA缓冲区中(回弹缓冲区)。然后再用回弹缓冲区来完成DMA操作。如果是读方向,那么就想用回弹缓冲区来完成数据的读操作。然后再把回弹缓冲区的内容复制到上层的缓冲区中。可以发现回弹缓冲更像一个中转站,这基本上抵消了DMA带来的性能提升,所以不到不得不使用的时候一般不用。

五、统一DMA编程接口

  • 早期的Linux内核源码中,嵌入式处理器的DMA部分代码是不统一的,也就是各SOC都有自己的一套DMA编程接口(某些驱动还保留了原有的接口)。这样就很不利于我们统一编程,每换一种SOC我们就要学习一种编程接口,这样会给驱动开发人员带来很大的困扰。为了解决此问题,Linux内核开发者提供了统一的编程接口,屏蔽了底层不同DMAC的控制细节,大大提高了通用性,也使得上层的DMA操作变得更加容易。dmaengine完成DMA数据传输,基本需要以下几个步骤:
  1. 分配一个DMA通道
  2. 设置一些传输参数
  3. 获取一个传输描述符
  4. 提交传输
  5. 启动所有挂起的传输,传输完成后回调函数被调用。
  • 所涉及的API
struct dma_chan_request_channel(dma_mask_t mask,dma_filter_fn filter_fn,void *filter_param);
/*
功能:申请一个DMA通道
参数:mask: 描述要申请的通道的能力要求掩码,比如指定该通道要满足内		   存传输的能力。
	 filter_fn: 通道匹配过滤函数,用于指定获取某一满足要求的具体				通道。
	 filter_param: 是传给过滤函数的参数
*/
typedef bool(*dma_filter_fn)(struct dma_chan *chan,void *filter_param);
/*
通道过滤函数
*/
int dmaengine_slave_config(struct dma_chan *chan,struct dma_slave_config *config);
/*
功能:对通道进行配置
参数:chan: 要配置的通道
	 config: 具体的配置信息,包括地址、方向和突发长度等。
*/
struct dma_async_tx_descriptor *(8chan->device->device_prep_slave_sg)(
struct dma_chan *chan,struct scatterlist *sgl,unsigned int sg_len,enum dma_data_direction direction,unsigned long flags);
/*
功能:创建一个用于分散/聚集DMA操作的描述符。
参数:chan: 是使用的通道
	 dma_map_sg: 使用dma_map_sg初始化好的struct scatterlist数组
	 sg_len: DMA缓冲区个数
	 direction: 传输方向
	 flags: DMA传输控制的一些标志,比如DMA_PREP_INTERRUPT表示在传输完成后要调用回调函数。
*/
dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc);
/*提交刚才创建的传输描述符desc,即提交传输,但是传输还没有开始。*/
void dma_async_issue_pending(struct dma_chan *chan);
/*
启动通道chan上挂起的传输
*/
ave_sg)(
struct dma_chan *chan,struct scatterlist *sgl,unsigned int sg_len,enum dma_data_direction direction,unsigned long flags);
/*
功能:创建一个用于分散/聚集DMA操作的描述符。
参数:chan: 是使用的通道
	 dma_map_sg: 使用dma_map_sg初始化好的struct scatterlist数组
	 sg_len: DMA缓冲区个数
	 direction: 传输方向
	 flags: DMA传输控制的一些标志,比如DMA_PREP_INTERRUPT表示在传输完成后要调用回调函数。
*/
dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc);
/*提交刚才创建的传输描述符desc,即提交传输,但是传输还没有开始。*/
void dma_async_issue_pending(struct dma_chan *chan);
/*
启动通道chan上挂起的传输
*/
  • 6
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值