内存映射和DMA
内存的概念:

内存是与CPU进行沟通的桥梁,所有程序的运行都是在内存中进行的。
内存的作用是用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。只要计算机在运行中,CPU就会把需要运算的数据调到内存中进行运算,当运算完成后CPU再将结果传送出来,内存的运行也决定了计算机的稳定运行。

一、Linux的内存管理:

1、地址类型:
1)虚拟地址:用户程序使用的常规地址,每个程序都拥有自己的虚拟地址(应用层)。
2)物理地址:用于处理器和系统内存之间使用(物理层)。
3)总线地址:外围总线和内存之间使用(设备层)。
4)内核的逻辑地址:内核的逻辑地址和物理地址之间的关系是存在一个固定的偏移量进行一一对应,通常保存在unsigned long 或者 void * 类型的变量中,kmalloc 返回的就是逻辑地址。
5)内核虚拟地址:内核的虚拟地址与物理地址不是一一对应的,虚拟地址包含逻辑地址,vmalloc 返回的就是内核虚拟地址。

高端与低端内存
低端内存:内核空间的逻辑地址内存。
高端内存:不存在的逻辑地址内存,处于内核的虚拟地址上。

2、Linux 内存管理物理地址结构
DMA内存区:即直接内存访问分区,通常为物理内存的起始16M。主要是供一些外设使用,外设和内存直接访问数据访问,而无需系统CPU的参与。

正常内存区:从16M到896M内存区。

高端内存区:896M以后的内存区。

页框: 将物理内存划分的块。

3、Linux 内存管理虚拟地址结构

页:将进程分配的虚拟地址空间划分的块,对应的大小就叫页面大小。(包含页表号)

页框:将内存物理地址划分的块。

页和页框二者一一对应,一个页放入一个页框,(理论上)页的大小和页框的大小相等。

页表:操作系统通过维护一张表,这张表上记录了每一对页和框的映射关系,这个表即页表。页表被放在物理内存中,由操作系统维护。

4、虚拟地址映射到物理地址的流程:

二、mmap设备操作
1、应用层调用
#include <sys/mman.h>

   void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
   int munmap(void *addr, size_t length);

1) void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);

addr :映射的起始地址,设为NULL由系统指定;
length :映射到内存的文件长度;
prot : 期望的内存保护标志
flags :指定映射对象的类型,映射选项和映射页是否可以共享。
fd :由open返回的文件描述符,代表要映射的文件;
offset :开始映射的文件的偏移。
返回值:成功执行时,mmap()返回被映射区的指针。失败时,mmap()返回MAP_FAILED

功能:负责把文件内容映射到进程的虚拟地址空间,通过对这段内存的读取和修改来实现对文件的读取和修改,而不需要再调用read和write;

2)int munmap(void *addr, size_t length);

addr :映射内存的起始地址。
length :想要取消映射的大小。

返回值:On success, munmap() returns 0, on failure -1, and errno is set (probably to EINVAL)

mmap 机制原理图

2、mmap 设备操作

映射一个设备是指把用户空间的一段地址(虚拟地址区间)关联到设备内存上,当程序读写这段用户空间的地址时,它实际上是在访问设备。
mmap方法是file_operations结构的成员,在mmap系统调用的发出时被调用。在此之前,内核已经完成了很多工作。
mmap设备方法所需要做的就是建立虚拟地址到物理地址的页表(虚拟地址和设备的物理地址的关联通过页表)。

3、建立页表的方式有两种:
一次全部建立页表(简单)

int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr, unsigned long pfn, unsigned long size, pgprot_t prot)

struct file_operations {
………
int (*mmap) (struct file *, struct vm_area_struct *);
………
};

代码例子:

/文件打开函数/
int mem_open(struct inode *inode, struct file *filp)
{

return 0; 

}

/文件释放函数/
int mem_release(struct inode *inode, struct file *filp)
{
return 0;
}

/内存映射mmap/
static int memdev_mmap(struct file*filp, struct vm_area_struct *vma)
{
struct mem_dev *dev = filp->private_data; /获得设备结构体指针/

  vma->vm_flags |= VM_IO;
  vma->vm_flags |= VM_RESERVED;
  if (remap_pfn_range(vma,vma->vm_start,virt_to_phys(dev->data)>>PAGE_SHIFT, vma->vm_end - vma->vm_start, vma->vm_page_prot))
      return  -EAGAIN;
            
  return 0;

}

/文件操作结构体/
static const struct file_operations mem_fops =
{
.owner = THIS_MODULE,
.open = mem_open,
.release = mem_release,
.mmap = memdev_mmap,
};

/设备驱动模块加载函数/
static int memdev_init(void)
{
int result = 0;
return result;
}

/模块卸载函数/
static void memdev_exit(void)
{
/注销设备/
/释放设备结构体内存/
/释放设备号/
}

MODULE_LICENSE(“GPL”);

module_init(memdev_init);
module_exit(memdev_exit);

应用层代码:
#include <stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/mman.h>

int main()
{
int fd;
char *start;
//char buf[100];
char *buf;

/*打开文件*/
fd = open("/dev/memdev0",O_RDWR);
    
buf = (char *)malloc(100);
memset(buf, 0, 100);
start=mmap(NULL,100,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);

/* 读出数据 */
strcpy(buf,start);
sleep (1);
printf("buf 1 = %s\n",buf);    

/* 写入数据 */
strcpy(start,"Buf Is Not Null!");

memset(buf, 0, 100);
strcpy(buf,start);
sleep (1);
printf("buf 2 = %s\n",buf);

   
munmap(start,100); /*解除映射*/
free(buf);
close(fd);  
return 0;    

}

总结:mmap设备方法实现将用户空间的一段内存关联到设备内存上,对用户空间的读写就相当于对字符设备的读写;不是所有的设备都能进行mmap抽象,比如像串口和其他面向流的设备就不能做mmap抽象。

三、I/O 映射

在一个进程中,同一个(一组)寄存器可以多次ioremap,每次ioremap,都会为其建立新的页表,即也会有不同的虚拟地址。但这些虚拟地址都是映射的同一片物理地址,所以无论操纵那一个ioremap返回的虚拟地址,最终都操作能够的是那块物理地址。

四、直接内存访问(DMA)

DMA(Direct Memory Access,直接内存存取) 传输将数据从一个地址空间复制到另外一个地址空间。当CPU 初始化这个传输动作,传输动作本身是由 DMA 控制器来实行和完成。

DMA工作原理

在系统进行DMA数据交换的过程中,cpu都不得获得系统总线的控制权,在此过程当中,cpu只需开始和结束的对DMAC进行响应,其余的时间,cpu可以处理其他的事情。

对DMA内存的使用有两种方式:

1、一致DMA映射
dma_alloc_coherent()和dma_free_coherent()。dma_alloc_coherent()会传一个device结构体指明给哪个设备申请一致性DMA内存,它会产生两个地址,一个是给CPU看的,一个是给DMA看的。CPU需要通过返回的虚拟地址来访问这段内存。
通过这种方式得到的dma内存,不用担心cache的问题,但是要注意在执行DMA操作之前flush write buffer。

void * dma_alloc_coherent(struct device * dev,size_t size,dma_addr_t * dma_handle,int flag);

/**
  * dma_alloc_coherent - 为DMA分配一致的内存
  * @dev:有效的结构设备指针,或ISA和类似EISA的设备的NULL
  * @size:所需的内存大小
  * @handle:特定于总线的DMA地址
  *  为执行DMA的设备分配一些未缓存的无缓冲内存。 此函数分配页面,并返回 * CPU查看的地址,并将@handle设置为设备查看的地址。
 */

作用:
①函数的返回值是一个 void *。代表缓冲区的内核逻辑地址。 
②相关的总线地址(物理地址),保存在dma_handle中

举例应用:

A =dma_alloc_coherent(B,C,D,GFP_KERNEL);

含义:
A: 内存的虚拟起始地址,在内核要用此地址来操作所分配的内存
B: struct device指针。能够平台初始化里指定。
C: 实际分配大小,传入dma_map_size就可以。
D: 返回的内存物理地址,dma就能够用。

Note :A和D是一一相应的,A是虚拟地址,而D是物理地址。

释放DMA内存:
void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t dma_handle )

2、流式DMA映射


kmalloc() 、__get_free_pages()、vmalloc、kzalloc 的比较

kmalloc()和__get_free_pages()函数申请的内存位于物理内存的映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,存在简单的线性关系;(3G+896M)(低端内存);
vmalloc函数申请的虚拟内存与物理内存之间也没有简单的换算关系;(高端内存)(3G+896M以上的内存);

1、kmalloc()
void *kmalloc(size_t size, int flags);
kmalloc第一个参数是要分配块的大小,第二个参数为分配标志,用于控制kmalloc的行为;
最常用的分配标志是GFP_KERNEL,其含义是内核空间的进程中申请内存。kmalloc()的底层依赖__get_free_page()实现,分配标志的前缀GFP正好是底层函数的缩写。
kmalloc申请的是较小的连续的物理内存,内存物理地址上连续,虚拟地址上也是连续的,使用的是内存分配器slab的一小片。申请的内存位于物理内存的映射区域。其真正的物理地址只相差一个固定的偏移。可以用两个宏来简单转换__pa(address) { virt_to_phys()} 和__va(address) {phys_to_virt()}
使用kmalloc函数之后使用kfree函数;
 
2、__get_free_pages()
get_free_page()申请的内存是一整页,一页的大小一般是128K。
 从本质上讲,kmalloc和get_free_page最终调用实现是相同的,只不过在调用最终函数时所传的flag不同而已。
 
3、vmalloc()
vmalloc()一般用在只存在于软件中的较大顺序缓冲区分配内存,vmalloc()远大于__get_free_pages()的开销,为了完成vmalloc(),新的页表需要被建立。所以效率没有kmalloc和__get_free_page效率高;

4、kzalloc函数
kzalloc申请内存的时候, 效果等同于先是用 kmalloc() 申请空间 , 然后用 memset() 来初始化 ,所有申请的元素都被初始化为 0.对应的释放函数也是kfree函数;

理解完之后开始看一下DMA流式映射的一种方式:

先通过kmalloc, get_free_pages等得到一段物理连续的内存空间,然后使用dma_map_single, 将之前分配的内存空间映射,得到总线地址,使之能被device访问。
这种方式不保证cache的一致性,需要开发者手动处理调用dma_sync_single_for_cpu/device函数;

流式DMA映射的几条原则:

1)缓冲区只能用于这样的传送,即其传送方向匹配于映射时给定的方向。
2)一旦缓冲区被映射,它将属于设备,而不是处理器。
3)直到缓冲区被撤销映射前,驱动程序不能以任何方式访问其中的内容。
4)在DMA处于活动期间内,不能撤销对缓冲区映射,否则会严重破坏系统的稳定性。

举个例子过程:

枚举类型dma_data_direction:
DMA_TO_DEVICE 数据发送到设备(如write系统调用)
DMA_FROM_DEVICE 数据被发送到CPU
DMA_BIDIRECTIONAL 数据可双向移动
DMA_NONE 出于调试目的。

//映射流式DMA
dma_addr_t dma_map_single(struct device *dev,void *buf, size_t size, enum dma_datadirection direction);

//驱动获得DMA拥有权,通常驱动不该这么做
void dma_sync_single_for_cpu(struct device *dev,dma_addr_t dma_handle_t bus_addr,size_t size, enum dma_data_direction direction);

//将DMA拥有权还给设备
void dma_sync_single_for_device(struct device *dev,dma_addr_t dma_handle_t bus_addr,size_t size, enum dma_data_direction direction);

//去映射流式DMA
dma_addr_t dma_unmap_single(struct device *dev,void *buf, size_t size, enum dma_datadirection direction);


总结:
一致DMA映射具有更长的生命周期,它在driver的整个生命周期内都有效,且不用关心cache效应。
流式DMA映射则只在driver填充完要传输的内容到device完成传输这段时间内有效凡使用流式DMA映射的内存区域在map之后,就只对device有效,driver在unmap之前cpu不能在读写这一段内存区域。或者使用dma_sync_single_for_cpu由cpu获得读写权利,然后driver可对其进行读写。

更多推荐

Linux 内存映射和DMA 学习总结