DMA存储映射IO(三)

Linux驱动和用户空间交互时,都是通过file_operations 结构体中的设备操作函数read()和write()以及驱动程序中的copy_from_user/copy_to_user把用户空间和内核空间的数据相互转移,都是通过先将数据拷贝过来,然后再操作。DMA如果传输数据较大,原来的拷贝方式显然效率特别低。

存储映射 I/O是一种基于内存区域的高级 I/O 操作,它能将一个文件映射到进程地址空间中的一块内存区域中,当从这段内存中读数据时,就相当于读文件中的数据(对文件进行 read 操作),将数据写入这段内存时,则相当于将数据直接写入文件中(对文件进行 write 操作)。进程就可以采用指针的方式读写操作这一段内存,这样就可以在不使用基本 I/O 操作函数 read()和 write()的情况下执行 I/O 操作。

在这里插入图片描述

应用层内存操作

mmap映射函数

将一个给定的文件映射到进程地址空间中的一块内存区域中,由系统调用 mmap()来实现:

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

addr: 参数 addr 用于指定映射到内存区域的起始地址。设置为 NULL,这表示由系统选择该映射区的起始地址。
length: 参数 length 指定映射长度。 表示将文件中的多大部分映射到内存区域中,以字节为单位,譬如length=1024 * 4,表示将文件的 4K 字节大小映射到内存区域中。
offset: 文件映射的偏移量。设置为0表示从文件头部开始映射。
addr 和 offset 的值通常被要求是系统页大小的整数倍,可通过 sysconf()函数获取页大小,以字节为单位
fd: 文件描述符。指定要映射到内存区域中的文件。
prot: 参数 prot 指定了映射区的保护要求,可取值如下:

 1. PROT_EXEC: 映射区可执行;
 2. PROT_READ: 映射区可读;
 3. PROT_WRITE: 映射区可写;
 4. PROT_NONE: 映射区不可访问。

可将 prot 指定为为 PROT_NONE,也可将其设置为 PROT_EXEC、PROT_READ、 PROT_WRITE 中一个或多个(通过按位或运算符任意组合)。对指定映射区的保护要求不能超过文件 open()时的访问权限,譬如,文件是以只读权限方式打开的,那么对映射区的不能指定PROT_WRITE
flags: 参数 flags 可影响映射区的多种属性, 参数 flags 必须要指定以下两种标志之一:

  1. MAP_SHARED: 此标志指定当对映射区写入数据时,数据会写入到文件中,也就是会将写入到映射区中的数据更新到文件中,并且允许其它进程共享。
  2. MAP_PRIVATE: 此标志指定当对映射区写入数据时,会创建映射文件的一个私人副本(copy-on-write),对映射区的任何操作都不会更新到文件中,仅仅只是对文件副本进行读写。

返回值: 成功情况下,函数的返回值便是映射区的起始地址;发生错误时,返回(void *)-1, 通常使用MAP_FAILED 来表示, 并且会设置 errno 来指示错误原因。

如果文件大小为 96 个字节,我们调用 mmap()时参数 length 也是设置为 96,假设系统页大小为 4096 字节(4K),则系统通常会提供 4096 个字节的映射区,其中后 4000 个字节会被设置为0,可以修改后面的这 4000 个字节,但是并不会影响到文件。 但如果访问 4000 个字节后面的内存区域,将会导致异常情况发生,产生 SIGBUS 信号。

munmap解除映射

通过 mmap()将文件映射到进程地址空间中的一块内存区域中,当不再需要时,必须解除映射:

#include <sys/mman.h>
int munmap(void *addr, size_t length);

munmap()系统调用解除指定地址范围内的映射, 参数 addr 指定待解除映射地址范围的起始地址,它必须是系统页大小的整数倍;参数 length 是一个非负整数,指定了待解除映射区域的大小(字节数), 被解除映射的区域对应的大小也必须是系统页大小的整数倍,即使参数 length 并不等于系统页大小的整数倍。

当进程终止时也会自动解除映射(如果程序中没有显式调用 munmap()),但调用 close()关闭文件时并不会解除映射。

可使用 fstat()函数获取源文件的大小,调用 ftruncate()函数设置目标文件的大小与源文件大小保持一致。

普通I/O
普通 I/O 方式一般是通过调用 read()和 write()函数来实现对文件的读写, 使用 read()和 write()读写文件时,函数经过层层的调用后,才能够最终操作到文件,中间涉及到很多的函数调用过程,数据需要在不同的缓存间倒腾,效率会比较低。 同样使用标准 I/O(库函数 fread()、 fwrite())也是如此,本身标准 I/O 就是对
普通 I/O 的一种封装。如果数据量比较小,影响并不大,使用普通的 I/O 方式还是非常方便的。

存储映射I/O
存储映射 I/O 的实质其实是共享, 与 IPC 之内存共享很相似。譬如执行一个文件复制操作来说,对于普通 I/O 方式,首先需要将源文件中的数据读取出来存放在一个应用层缓冲区中,接着再将缓冲区中的数据写入到目标文件中。
应用层与内核层是不能直接进行交互的,必须要通过操作系统提供的系统调用或库函数来与内核进行数据交互,包括操作硬件。通过存储映射 I/O 将文件直接映射到应用程序地址空间中的一块内存区域中,也就是映射区;直接将磁盘文件直接与映射区关联起来,不用调用 read()、 write()系统调用,直接对映射区进行读写操作即可操作磁盘上的文件,而磁盘文件中的数据也可反应到映射区中,这就是一种共享,可以认为映射区就是应用层与内核层之间的共享内存

驱动层内存操作

1、内存映射基础

每个进程拥有3G字节的用户虚存空间。但是,这并不意味着用户进程在这3G的范围内可以任意使用,因为虚存空间最终得映射到某个物理存储空间(内存或磁盘空间),才真正可以使用。
内核将每个进程都作为一个task_struct结构体存在一个双向循环链表中, 结构体在 include/linux/sched.h中定义。其中有个struct mm_struct *mm成员,记录着这个进程的虚拟地址的信息。
在mm_struct中,struct vm_area_struct mmap 链表保存了每一块应用程序虚拟地址(堆空间,栈空间, bss区,data常量空间,text代码0段)的起始位置和结束位置。 当然虚拟地址不可能是凭空产生, 自然是要有一块相应的物理地址来对应,这一块物理地址就保存在pgd_t * pgd *这个成员变量中叫做页目录表,pgd成员记录了对应的物理地址,也记录了如何映射。

将虚拟地址的某一段转换成物理地址的话,就需要在页表pgd中添加一个页表项。
页表项的内容是个32位的数据,如下图:
在这里插入图片描述

ARM架构内存映射:

RM架构支持一级页表映射,也就是说MMU根据CPU发来的虚拟地址可以找到第1个页表,从第1个页表里就可以知道这个虚拟地址对应的物理地址。一级页表里地址映射的最小单位是1M。
ARM架构还支持二级页表映射,也就是说MMU根据CPU发来的虚拟地址先找到第1个页表,从第1个页表里就可以知道第2级页表在哪里;再取出第2级页表,从第2个页表里才能确定这个虚拟地址对应的物理地址。二级页表地址映射的最小单位有4K、1K,Linux使用4K。

一级页表映射过程
一级页表中每一个表项用来设置1M的空间,对于32位的系统,虚拟地址空间有4G,4G/1M=4096。所以一级页表要映射整个4G空间的话,需要4096个页表项。
第0个页表项用来表示虚拟地址第0个1M(虚拟地址为0~0xFFFFF)对应哪一块物理内存,并且有一些权限设置;
第1个页表项用来表示虚拟地址第1个1M(虚拟地址为0x100000~0x1FFFFF)对应哪一块物理内存,并且有一些权限设置

  1. CPU发出虚拟地址vaddr,假设为0x12345678。
  2. MMU根据vaddr[31:20]找到一级页表项:在[1:0]发现是个一级页表,虚拟地址0x12345678是虚拟地址空间里第0x123个1M,所以找到页表里第0x123项,根据此项内容知道它是一个段页表项。段内偏移是0x45678。
  3. 从这个表项里取出物理基地址:Section Base Address,假设是0x81000000。
  4. 物理基地址加上段内偏移得到:0x81045678,所以CPU要访问虚拟地址0x12345678时,实际上访问的是0x81045678的物理地址。

二级页表映射过程

  1. CPU发出虚拟地址vaddr,假设为0x12345678。
  2. MMU根据vaddr[31:20]找到一级页表项:
    虚拟地址0x12345678是虚拟地址空间里第0x123个1M,所以找到页表里第0x123项。根据此项的[1:0]内容知道它是一个二级页表项。
  3. 从这个表项里取出地址,假设是address,这表示的是二级页表项的物理地址。
  4. vaddr[19:12]表示的是二级页表项中的索引index即0x45,在二级页表项中找到第0x45项。
  5. 二级页表项格式如下:
    在这里插入图片描述里面含有这4K或1K物理空间的基地址page base addr,假设是0x81889000:
    它跟vaddr[11:0]组合得到物理地址:0x81889000 + 0x678 = 0x81889678。
    所以CPU要访问虚拟地址0x12345678时,实际上访问的是0x81889678的物理地址。

2、内存映射-驱动程序的结构体

在驱动file_operations 中声明mmap在驱动中对应的函数。

static struct file_operations dma_lops=
{
.owner = THIS_MODULE,
.read  = dma_read,
.open  = dma_open,
.write = dma_write,
.release = dma_close,
.mmap    =dma_mmap,
.unlocked_ioctl = dma_ioctl,
};
 int (*mmap)(struct file *,struct vm_area_struct *);

APP调用mmap系统函数时,内核就帮我们构造了一个vm_area_stuct结构体。里面含有虚拟地址的地址范围、权限。
在内核中,这样每个区域用一个结构struct vm_area_struct 来表示,它描述的是一段连续的、具有相同访问属性的虚存空间,该虚存空间的大小为物理内存页面的整数倍。
vm_area_struct
为内存映射服务的地址空间处在堆栈之间的空余部分。 linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域,由于每个不同质的虚拟内存区域功能和内部机制都不同,因此一个进程使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。各个vm_area_struct结构使用链表或者树形结构链接,方便进程快速访问。

结构体 vm_area_struct
表示的是一块连续的虚拟地址空间区域,给进程使用的,地址空间范围是0~3G,对应的物理页面都可以是不连续的。

#include <linux/mm_types.h>
 
/* This struct defines a memory VMM memory area. */
 
struct vm_area_struct 
{
struct mm_struct * vm_mm; /* VM area parameters */
unsigned long vm_start;
unsigned long vm_end;
 
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next;
pgprot_t vm_page_prot;
unsigned long vm_flags;
 
/* AVL tree of VM areas per task, sorted by address */
short vm_avl_height;
struct vm_area_struct * vm_avl_left;
struct vm_area_struct * vm_avl_right;
 
/* For areas with an address space and backing store,
vm_area_struct *vm_next_share;
struct vm_area_struct **vm_pprev_share;
struct vm_operations_struct * vm_ops;
unsigned long vm_pgoff; /* offset in PAGE_SIZE units, *not* PAGE_CACHE_SIZE */
struct file * vm_file;
unsigned long vm_raend;
void * vm_private_data; /* was vm_pte (shared mem) */
};

linux查看内存使用情况命令行:

/proc/meminfo 机器的内存使用信息
/proc/pid/maps pid为进程号,显示当前进程所占用的虚拟地址。
/proc/pid/statm 进程所占用的内存
/proc/pid/status  某个进程占用内存信息,还包括进程IDs、信号等信息
/proc/pid/smaps 查看进程的虚拟内存空间的分布情况

cat /proc/[pid]/maps 来查看一个进程的内存使用情况,pid是进程号,其中显示的每一行对应进程的一个vm_area_struct结构.

root@tronlong-virtual-machine:/proc# cat /proc/2309/maps
00400000-0040a000 r-xp 00000000 08:01 1837478                            /usr/lib/x86_64-linux-gnu/gconf/gconfd-2
0060a000-0060b000 r--p 0000a000 08:01 1837478                            /usr/lib/x86_64-linux-gnu/gconf/gconfd-2
0060b000-0060c000 rw-p 0000b000 08:01 1837478                            /usr/lib/x86_64-linux-gnu/gconf/gconfd-2
00bc7000-00ceb000 rw-p 00000000 00:00 0                                  [heap]
7f56f5043000-7f56f5051000 r-xp 00000000 08:01 1966392                    /usr/lib/x86_64-linux-gnu/gconf/2/libgconfbackend-xml.so
7f56f5051000-7f56f5250000 ---p 0000e000 08:01 1966392                    /usr/lib/x86_64-linux-gnu/gconf/2/libgconfbackend-xml.so
7f56f5250000-7f56f5251000 r--p 0000d000 08:01 1966392                    /usr/lib/x86_64-linux-gnu/gconf/2/libgconfbackend-xml.so
7f56f5251000-7f56f5252000 rw-p 0000e000 08:01 1966392                    /usr/lib/x86_64-linux-gnu/gconf/2/libgconfbackend-xml.so

结构体 vm_struct
表示一块连续的虚拟地址空间区域。给内核使用,地址空间范围是(3G + 896M + 8M) ~ 4G,对应的物理页面都可以是不连续的。

  1. 3G ~ (3G + 896M)范围的地址是用来映射连续的物理页面的,可以从以下链接: Linux中内存管理基础(一)查看详情,这个范围的虚拟地址和对应的实际物理地址有着简单的对应关系,即对应0~896M的物理地址空间,

  2. (3G + 896M) ~ (3G + 896M + 8M)是安全保护区域(例如,所有指向这8M地址空间的指针都是非法的)

mmap系统调用所完成的工作就是准备这样一段虚存空间,并建立vm_area_struct结构体,将其传给具体的设备驱动程序。

3、申请物理内存

确定物理地址:映射某个内核buffer,你需要得到它的物理地址。可以参考链接: Linux中DMA驱动空间映射(二)

4、建立映射关系

cache和buffer映射属性搭配:

  1. 不使用cache也不使用buffer,读写时都直达硬件,适合寄存器的读写。
  2. 不使用cache但是使用buffer,写数据时会用buffer进行优化,可能会有“写合并”,这适合显存的操作。因为对显存很少有读操作,基本都是写操作,而写操作即使被“合并”也没有关系。
  3. 使用cache不使用buffer,就是“write through”,适用于只读设备:在读数据时用cache加速,基本不需要写。
  4. 既使用cache又使用buffer,适合一般的内存读写。

修改进程页表:

建立页表方式:

  1. 使用remap_pfn_range一次建立所有页表。
 int remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, 
 unsigned long pfn, unsigned long size, pgprot_t prot);

vma :用户进程创建一个vma区域。
virt_addr :重新映射应当开始的用户虚拟地址。这个函数建立页表为这个虚拟地址范围从 virt_addr 到 virt_addr+size。
pfn :页帧号,对应虚拟地址被映射的物理地址。这个页帧号简单的是通过物理地址右移 PAGE_SHIFT 位。
size: 正在被重新映射的区的大小,以字节为单位。
prot :给新 VMA 要求的"protection"。驱动可使用在vma->vm_page_prot 中找到的值。
返回值:成功返回 0, 失败返回一个负的错误值。
局限性:remap_pfn_range不能映射常规内存,只存取保留页和在物理内存顶之上的物理地址。因为保留页和在物理内存顶之上的物理地址内存管理系统的各个子模块管理不到。640 KB 和 1MB 是保留页可能映射,设备I/O内存也可以映射。如果想把kmalloc()申请的内存映射到用户空间,则可以通过mem_map_reserve()把相应的内存设置为保留后就可以。

  1. 使用nopage VMA方法每次建立一个页表项。
struct page *(*nopage)(struct vm_area_struct *vma, 
unsigned long address, int *type);

address :代表从用户空间传过来的用户空间虚拟地址。
返回值:成功则返回一个有效映射页,失败返回NULL。

驱动程序实例:

static int dma_mmap(struct file *file, struct vm_area_struct *vma)
{
     //获得物理地址,kernel_buf是内核使用的虚拟地址用系统分配
	 unsigned long phy = virt_to_phys(bernel_buf);
     //设置属性:cache, buffer。设置属性, 不使用 cache 使用buffer
	 vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);

    //映射
	if(remap_pfn_range(vma, vma->vm_start, phy>>PAGE_SHIFT,
		vma->vm_end - vma->vm_start, vma->vm_page_prot)){
		printk("mmap remap_pfn_range failed\n");
		return -ENOBUFS;
	}
	return 0;
}
  • 35
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值