地址类型
用户虚拟地址 物理地址 总线地址 内核逻辑地址 内核虚拟地址
用户虚拟地址
用户空间程序所看到的常规地址。用户地址是32位或者64位的,这取决于硬件的体系结构。每个进程都有自己的虚拟地址空间。
物理地址
该地址在处理器和系统内存之间使用
内核逻辑地址
内核逻辑地址组成了内核的常规地址空间。该地址映射了部分或者全部内存,并经常被视为物理地址。逻辑地址使用硬件内建的指针大小,因此在安装了大量内存的32位系统中,它无法寻址全部的物理地址。逻辑地址通常保存在unsigned long或者void *这样类型的变量中。Kmalloc返回的内存就是内核逻辑地址。
内核虚拟地址
内核虚拟地址和逻辑地址的相同之处在于,它们都将内核空间的地址映射到物理地址上。内核虚拟地址与物理地址的映射不必是线性的和一对一的,而这是逻辑地址的特点。所有逻辑地址都是内核虚拟地址,但许多内核虚拟地址不是逻辑地址。举例,vmalloc分配的内存具有一个虚拟地址,虚拟地址通常保存在指针变量中。
页:物理地址被分成离散的单元,系统对内存的操作都是基于页。大部分系统使用每页4k。常量PAGE_SIZE给出了页的大小。
页帧数:如果忽略地址偏移量,并将除去偏移量的剩余位移到右端,称该结果为页帧数。内存地址,无论是虚拟的还是物理的,都被分为页号和一个页内的偏移量。举例,如果使用每页4096字节,那么最后的12位是偏移量,剩余的高位则是页号。移动位以在页帧数和地址间进行转换是一个常用操作;宏PAGE_SHIFT将告诉程序员,必须移动多少位才能完成这个转换。
低端内存:存在于内核空间上的逻辑地址内存。几乎所有现在读者遇到的系统,它全部的内存都是低端内存。
高端内存:是指那些不存在逻辑地址的内存,这是因为他们处于内核虚拟地址之上。
内存映射和页结构
page结构和虚拟地址之间的转换
页表
在任何现代的系统中,处理器必须使用某种机制,将虚拟地址转换为相应的物理地址,这种机制被称为页表
虚拟内存区(VMA)
进程内存映射至少包含下面这些区域:
程序的可执行代码(text)区域
多个数据区,包括初始化数据、非初始化数据(BSS)以及程序堆栈。
与每个活动的内存映射对应的区域。
cat /proc/<pid/maps>可以了解到进程的内存区域。
/proc/self/maps是一个特殊的文件,它始终指向当前进程。
/proc/*/maps中的每个成员都与vm_area_struct结构中的一个成员对应
vm_area_struct结构(重要)
mmap设备操作
内存映射可以提供给用户程序直接访问设备内存的能力。
不是所有的设备都能进行mmap抽象的。比如像串口和其他面向流的设备就不能mmap。对mmap的另外一个限制是:必须以PAGE_SIZE为单位进行映射。
mmap()系统调用
mmap(caddr_t addr, size_t len, int prot, int flags, int fd, off_t offset)
两种建立页表的方法:1.使用remap_pfn_range函数一次全部建立;2.或者通过nopage VMA方法每次建立一个页表。
使用remap_pfn_range和io_remap_page_range负责为一段物理地址建立新的页表。
详细参考博客
Linux地址映射--地址转换(mmap,vma)_yiyeguzhou100的专栏-CSDN博客_mmap 地址对齐
DMA
分配DMA缓冲区
使用DMA缓冲区的主要问题是:当大于一页时,它们必须占据连续的物理页,这是因为设备使用ISA或者PCI系统总线传输数据,而这两种方式使用的都是物理地址。
低层分配DMA缓冲区的方法,使用GFP_DMA标志调用kmalloc或者get_free_pages从DMA区间分配内存。
Get_free_pages函数可以分配多达几M字节的内存,但通常会失败,此时系统内存中充满了内存碎片。如果需要为DMA缓冲区分配一大块内存,需考虑是否有替代的方法。比如分散/聚集I/O
通用DMA层
DMA映射:解决缓存一致性问题
Dma_addr_t表示总线地址
根据DMA缓冲区期望保留时间的长短,PCI代码区分两种类型的DMA映射:
一致性DMA映射
这种类型的映射存在于驱动程序生命周期中。一致性映射的缓冲区必须可同时被CPU和外围设备访问。因此一致性映射必须保存在一致性缓存中
流式DMA映射
流失映射接口比一致性映射复杂,因为需要接受指定的内核缓冲区来建立DMA映射,处理它没得选择的内存地址。
上层已经准备好了buffer,交由device driver通过DMA发送出去,那么显然,以流映射方式更为简便,因为只需要刷cache,DMA就能看到实际的buffer数据。那么采用一致性映射可不可以呢? 也是可以的,只是麻烦而已,那就得申请一块一致性DMA内存,然后把上层的buffer拷贝到这里,然后启动DMA去传输。
总而言之:
一致性DMA
dma_alloc_coherent(dev,size, &dma_handle, gfp);
流式DMA
dma_map_single(dev,addr, size, direction);
dma_unmap_single(dev,dma_handle, size, direction);
一致性DMA可以认为是“同步的”,就是DMA和CPU之间看到的物理内存是一致的。流式DMA则不然。
DMA操作和CPU之间的主要隔阂就是cache,因为一般来说DMA只操作物理内存,不会动cache,但CPU却首先看到的是L1L2cache,所以设备驱动就需要调用正确的DMA函数来操作cache。拿网卡收发包为例,假如CPU发包给网卡,那CPU填好skb的数据之后,得先把cache里有关这个skb数据的行给刷到物理内存,否则网卡从物理内存拿到的数据不是真正所要的数据。反之,CPU把skb数据装配好DMA rx descriptor的时候,得先invalid掉这个skb数据在cache里的行。这样DMA把收到的包填到物理内存后再中断告知CPU时,CPU就可以避免从cache拿到关于这个skb的老(脏)数据,而会从物理内存取包而重新建立数据cache。dma_map_singledma_unmap_single做的就是这个事情,它会根据数据的方向来判断该是clean cache还是incalidcache。
那么DMA描述符呢,DMA控制器和CPU都要对DMA描述符做频繁操作,当CPU和DMA需要频繁的操作一块内存区域的时候,一致性DMA映射就比较合适。所以DMA描述符特别适用于一致性DMA。当然,你也可以对DMA描述符用流式操作,但那样开销就比较大了。
顺便说一句,刷cache是比较耗时的,特别是刷的区域比较大的时候。现代的很多处理器,CPU和DMA控制器之间从硬件上就能保证cache一致性,如ARM的ACP功能,这样像dma_map_single只是返回物理地址,而dma_unmap_single什么都不做。极大的提高了系统性能。
详细参考
https://blog.csdn.net/zqixiao_09/article/details/51089088