《Linux4.0设备驱动开发详解》笔记--第十一章:内存与I/O访问
2016-05-20 13:45
459 查看
由于Linux系统提供了复杂的内存管理功能,所以内存的概念在Linux系统中的相对复杂,有常规的内存、高端的内存、虚拟地址、逻辑地址、总线地址、物理地址、I/O内存、设备内存、预留内存等概念
它通过特定的指令in、out来访问
注意:目前大多数嵌入式微控制例如ARM、PowerPC等不提供I/O空间,而仅存在内存空间。 内存空间可以直接通过地址、指针来访问
虚拟内存机制可以让用户感觉好像程序可以使用非常大的内存空间
MMU操作原理:
TLB:转换旁路缓存,是MMU的核心部件,缓存少量虚拟地址和物理地址的转换关系,是转换表的Cache,也成为快表
TTW:转换表漫游,当TLB中没有对应缓冲对应的地址转换关系时,需要通过对内存中转换表(一般为多级页表)的访问来得到虚拟地址和物理地的对应关系。TTW成功后,会将对应的转换关系写入TLB,方便下次转换
若TLB中没有虚拟地址的入口,则转换表遍历硬件从存放于主存储器的转换表中获取地址转换信息和访问权限(也就是获得TTW啦),同时将信息放入TLB,它或者被放在一个没有使用的入口或者替换一个已经存在的端口,以后当再次访问这些地址时,对真是物理地址的访问将在Cache或者在内存中发生
ARM中的TLB条目中的控制信息用于控制对对应地址的访问权限及Cache的操作
C(高速缓存)和B(缓冲)位被用来控制对应地址的高速缓存和写缓冲,并决定是否进行告诉缓存
访问权限和域位作用用来控制读写访问是否被允许,如果不允许将发送一个异常
用户空间地址:0~3GB
内核空间地址:3~4GB
用户进程通常只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址
用户进程只有通过系统调用的方式才能访问到内核空间
内核空间与用户空间的区别:
每个进程的用户空间都是完全独立的用户进程各自拥有不同的页表
内核空间是由内核负责映射的,它不会跟着进程改变,是固定的
内核地址空间有自己独立的页表
Linux内核空间(1GB)的划分:
常规内存:
物理内映射区(896MB):系统物理内存被顺序映射在内核空间的这个区域
高端内存:
虚拟内存分配区(其实地址:VMALLOC_START~VMALLOC_END,用vmalloc()函数分配)
高端页面映射区(起始地址:PKMAP_BASE)
专用页面应设置区(地址为FIXADDR_START~FIXADDR_TOP) 系统保留映射区
物理内存超过4GB的处理办法
此时必须使用CPU的扩展分页(PAE)模式提供的64位页目录才能取到4GB以上的物理内存
释放:free()
kmalloc()和__get_free_pages()申请的内存位于物理内存映射区,并且是连续的,它与真实的物理地址一般只差一个固定的偏移
vmalloc()
vmalloc()在虚拟地址空间给出一块连续的内存区,实质上这段连续的虚拟内存在物理内存中并不一定连续。也没简单的换算关系
参数
size:分配大小
flag:分配标识
GFP_KERNEL(最常用),在内核空间进程中申请内存
GFP_USER,用来为用户空间分配内存,可能阻塞
GFP_HINSTANCE,与GFP_USER类似但是是从高端内存分配
GFP_NOIO,不允许任何IO初始化
GFP_NOFS,不允许任何文件系统调用
__GFP_DMA,要求分配在能够DMA的内存区
__GFP_HINSTANCE,指示分配的内存区可以位于高端内存
注意:kmalloc()其实就是依赖于_get_free_pages()函数实现的
__get_free_pages(unsigned int flags,unsigned int order);
介绍:此宏是Linux内核本质上最底层用于获取空闲内存的方法
因为底层的伙伴算法总是以页的2的n次方为单位管理空闲内存,所以最底层的内存申请总是以页为单位的
参数
order:分配的页数是2^order
flags:同kmalloc
相关宏
get_zeroed_pages()(申请的同时将页清空)
__get_free_page()(申请一页,不清零)
上述两个宏在实现中调用了alloc_pages()函数
alloc_pages()既可以在内核空间分配,也可以在用户空间分配
对应的释放函数:free_page(s)()
vmalloc函数
使用情况
一般只为存在于软件中(没有对应的硬件意义)的较大的顺序缓存区分配内存,其远大于__get_free_pages()的开销
其不能用于原子上下文中
因为其内部实现使用了标志为GFP_KERNEL的kmalloc()
vmalloc()的虚拟地址和物理地址不是一个简单的线性映射
因为vmolloc()在申请内存时,会进行内存的映射,改变页表项
slab与内存池:
引入slab的原因:
完全使用页为单元申请和释放内存容易导致浪费
在操作系统的运行过程中,经常涉及到对大量对象的重复生成、使用和释放问题,此时使用slab可以大大提高效率
实际上kmalloc()就是使用slab机制实现的
使用方法:
创建一个slab缓存:kmem_cache_create()
它可以保留任意数目且全部同样大小的后备缓存
参数
size:要分配的每个数据结构的大小
flags:控制如何进行分配的位掩码
SLAB_HWCACHE_ALIGN:每个数据对象呗对齐到一个缓存行
SLAB_CACHE_DMA:要求数据对象在DMA区域中的分配
分配slab缓存:kmem_cache_alloc();
创建的slab后备缓存中分配一块并返回首地址指针
释放slab缓存:kmem_cache_free();
释放由kmem_cache_alloc()函数分配的缓存
收回slab缓存:kmem_cache_destroy();
获知当前的slab的分配和使用情况:cat /proc/slabinfo
注意:slab不是要替代__get_free_pages(),其在最底层仍然依赖于__get_free_pages(),slab在底层每次申请1页或多页,之后在分隔这些页为更小的单元进行管理,从而节省了内存,也提高了slab缓冲对象的访问效率
内核中,与内存池相关的操作包括以下几种
创建内存池:mempool_create();
参数
min_nr参数是需要预分配对象的数目
alloc_fn和free_fn是指向内存池基础值提供的标准对象分配和回收函数的指针
其原型分别为
mempool_alloc_t();
参数
pool_data:分配和回收函数用到的指针
gfp_mask:是分配标志,只有当__GFP_WAIT标志被指定时,分配函数才会休眠
mempool_free_t();
分配和回收函数
mempool_alloc();
用来分配对象,如果内存池分配器无法提供内存,那么久可以用预分配的池
mempool_free();
回收内存池:mempool_destroy();
由mempool_create()函数创建的内存池需要mempool_destroy()函数释放
丅述个函数中I/O端口号port高度依赖于具体的硬件平台,因此,这里只写出了unsigned
在设备地址被映射到虚拟地址之后,尽管可以直接通过指针访问这些地址,但是Linux内核推荐使用一组标准的API开完成设备内存映射的虚拟地址的读写
I/O内存读写操作:
没有_relaxed后缀的版本与有_relaxed后缀的版本的区别是没有_relaxed后缀的版本包含一个内存屏障
申请:request_region()
这个函数向内核申请n个端口,这些端口从first开始,name参数为设备的名字
返回值
成功:返回非NULL
失败:返回NULL
释放I/O端口:release_region()
内存申请:request_mem_region()
这个函数向内核申请n个内存地址,这个地址从first开始,name是设备参数的名称
返回值
成功:返回值非NULL
失败:返回NULL
内存释放:release_mem_region()
request_region():在设备打开或驱动模块被加载时申请I/O端口区域
inb()/outb():使用inb()、outb()等函数进行访问
release_region():在设备关闭或驱动被卸载时释放I/O端口范围
I/O内存访问步骤
request_mem_region():首先申请资源
ioremap():将寄存器地址通过映射到内核空间虚拟地址
readb/readl/writeb/writel:通过这些函数访问设备的寄存器
iounmap()+release_mem_region():访问完成之后,对ioremap()申请的虚拟地址进行释放,并释放release_mem_region()申请的I/O内存资源
mmap()
函数的实质
将用户空间的一段内存与设备内存关联,当用户访问用户空间的这段地址范围时,实际上会转化为对应的设备的访问。
mmap()函数必须以页为单位进行映射
驱动程序mmap()的实现机制也是建立页表
驱动中的mmap()函数将在用户空间进行mmap()系统调用时被调用
参数
fd:文件描述符
len:是映射到用户空间的字节数
prot:指定访问权限
PROT_READ
PROT_WRITE
PROT_EXEC
PROT_NONE
caddr:指定文件应该被映射的起始地址,一般被指定为NULL,由内核分配
用户调用mmap()时所进行的工作
(1) 在进程的虚拟地址空间查找一块VMA
(2) 将这块VMA进行映射
(3) 如果设备驱动程序或者文件系统的file_operations定义了mmap()操作则调用它
(4) 将这个VMA插入到进程的VMA表中
VMA结构体
虚拟地址的描述通过VMA结构体来实现
VMA结构体描述的虚拟地址位于vm_start ~vm_end之间
vm_operations_structk结构体
此结构体描述了对VMA的相关操作
当用户进行系统调用mmap()后,内核不会调用VMA的open函数,
通常需要在驱动的mmap()函数中显示调用vma->vm_ops->open()
vm_operations_struct操作范例
重要函数
remap_pfn_range()
作用
用来创建页表项
以VMA结构体的成员作为remap_pfn_range()的参数
VMA的数据成员是内核根据用户的请求自己填充的
映射的虚拟地址范围是vma->vm_start至vma->vm_end
映射内存中的保留页、设备I/O、framebuffer、camera等内存
参数
addr:表示内存映射开始处的虚拟地址
pfn:是虚拟地址应该映射到的物理地址的页帧号
实际上就是物理地址右移PAGE_SHIFT位
页帧号:内核地址无论是虚拟的还是物理的,都是由两部分构成,往往是高N位为页号,低M位为页内偏移量。当我们将地址中的低M位偏移量抛弃时,高N位移动到右端得到这个结果称为页帧号,宏PAGE_SHIFT告诉我们要右移多少位才能得到页帧号
port:新页所要求的保护属性
例程:映射kmalloc申请的内存到用户空间
注意:通常I/O内存被映射时需要nocahe的,这个时候需要对vma_page_prot设置nocache标识之后再进行映射
当发生缺页异常时系统做出的响应:
(1).找到缺页的虚拟地址所在的VMA
(2).如果不要,分配中间页目录表和页表
(3).如果也表项不存在,调用VMA的nopage()方法,返回物理页面的描述符
(4).将物理页面的地址填充到页表中
fault()函数使用范例
map_desc结构体
而不是传统的 外设<—-cpu—–>内存
DMA与Cache的一致性问题:
假设DMA针对内存的目的地址与Cache缓存的对象有重叠区域,那么经过DMA操作后,Cache缓存对应的内存的数据就会被修改,而CPU却并不知道,它仍然会认为Cache中的数据就是内存中的数据,此时会产生Cache与内存之间的数据”不一致”错误
DMA与Cache的一致性实质
在采用Cache的系统中,同样一个数据可能存在于Cache中,也可能存在于主存中,当Cache中的数据与主存中的一样时则具有一致性,否则数据具有不一致性
出现Cache的一致性问题的情况
DMA与Cache的一致性问题
Cache使能和关闭的时刻
在具有MMU的ARM中,开启MMU前要先置Cache无效
区域是具备DMA能力的
Linux内核已经把此操作为我们封装好了
__get_dma_page():它在申请时已经添加了GFP_DMA标志
函数是以2^order为大小分配的DMA内存
如果不想用order为参数来申请DMA内存,则可以用下面的函数
注意:大多数嵌入式设备而言,DMA操作可以在整个常规内存区域进行,因此DMA区域就直接覆盖了常规的内存了
总线地址:是从设备的角度上看到的内存地址
物理地址:是从CPU MMU控制器外围角度上看到的内存地址
Linux内核提供如下函数用于简单的虚拟地址/总线地址的转换
在使用IOMMU或反弹缓冲区的情况下,下述函数不会正常工作
这两个函数是不被建议使用的
该API的本质就是修改device结构体的dma_mask成员
dma_mask:设备DMA可以寻址的范围
1.分配一片DMA缓冲区(为这片缓冲区产生设备可以访问的地址)
2.DMA映射必须考虑Cache一致性问题
内核提供如下函数用于分配就一个DMA一致性的内存区域
ama_alloc_coherent()函数
参数handle返回DMA缓冲区的总线地址,其类型为dma_addr_t,表示总线地址
返回值:为申请到的DMA缓冲区地址
与ama_alloc_coherent()函数对应的释放函数
分配一个写合并的DMA缓冲区函数与其释放函数
PCI设备申请DMA缓冲区的函数
注意:dma_alloc_xxx()函数虽然是以dma_alloc_开头的,但是其申请 的区域比一定在DMA区域里面,例如,以32ARM为例,当coherent_dma_mask小于0xfffffff时,才会设置GFP_DMA标志,并从DMA区域去申请内存
CMA机制:不预留内存,这些内存平时是可用的,只有当需要的时候,才被分配给camera、HDMI等设备使用
其对上呈现的接口是标准的DMA,也是一致性缓冲区API
流式DMA的工作步骤
进行流式DMA映射
执行DMA操作
进行流式DMA去映射
流式DMA本质上是进行Cache的使无效或清除操作,以解决Cache的一致性问题
流式DMA映射的接口
实现DMA的映射函数:dma_map_single()
参数
direction:DMA的方向
DMA_TO_DEVICE
DMA_FROM_DEVICE
DMA_BIDIRECTIONAL
DMA_NONE
返回值
成功:返回总线的地址
失败:返回NULL
反映射函数:dma_unmap_single()
在使用DMA前,设备驱动程序需要首相向dmaengine系统申请DMA通道
申请DMA通道的函数:dma_request_slave_channel();
释放申请的通道
例程:利用dmaengine初始化并发起一次DMA操作
11.1 CPU与内存、I/O
11.1.1 内存空间与I/O空间
I/O空间:在X86处理器中存在着I/O空间的概念,I/O空间是相对于内存空间而言的它通过特定的指令in、out来访问
指令格式:IN 累加器,{端口号|DX} OUT {端口号|DX},累加器
注意:目前大多数嵌入式微控制例如ARM、PowerPC等不提供I/O空间,而仅存在内存空间。 内存空间可以直接通过地址、指针来访问
11.1.2 内存管理单元MMU
作用:辅助操作系统进行内存管理,提供虚拟地址和物理地址的映射、内存访问权限保护和Cache缓存控制等硬件支持。虚拟内存机制可以让用户感觉好像程序可以使用非常大的内存空间
MMU操作原理:
TLB:转换旁路缓存,是MMU的核心部件,缓存少量虚拟地址和物理地址的转换关系,是转换表的Cache,也成为快表
TTW:转换表漫游,当TLB中没有对应缓冲对应的地址转换关系时,需要通过对内存中转换表(一般为多级页表)的访问来得到虚拟地址和物理地的对应关系。TTW成功后,会将对应的转换关系写入TLB,方便下次转换
若TLB中没有虚拟地址的入口,则转换表遍历硬件从存放于主存储器的转换表中获取地址转换信息和访问权限(也就是获得TTW啦),同时将信息放入TLB,它或者被放在一个没有使用的入口或者替换一个已经存在的端口,以后当再次访问这些地址时,对真是物理地址的访问将在Cache或者在内存中发生
ARM中的TLB条目中的控制信息用于控制对对应地址的访问权限及Cache的操作
C(高速缓存)和B(缓冲)位被用来控制对应地址的高速缓存和写缓冲,并决定是否进行告诉缓存
访问权限和域位作用用来控制读写访问是否被允许,如果不允许将发送一个异常
11.2 Linux内存管理
Linux系统中,进程4GB的内存空间被分成两个部分:用户空间和内核空间用户空间地址:0~3GB
内核空间地址:3~4GB
用户进程通常只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址
用户进程只有通过系统调用的方式才能访问到内核空间
内核空间与用户空间的区别:
每个进程的用户空间都是完全独立的用户进程各自拥有不同的页表
内核空间是由内核负责映射的,它不会跟着进程改变,是固定的
内核地址空间有自己独立的页表
Linux内核空间(1GB)的划分:
常规内存:
物理内映射区(896MB):系统物理内存被顺序映射在内核空间的这个区域
高端内存:
虚拟内存分配区(其实地址:VMALLOC_START~VMALLOC_END,用vmalloc()函数分配)
高端页面映射区(起始地址:PKMAP_BASE)
专用页面应设置区(地址为FIXADDR_START~FIXADDR_TOP) 系统保留映射区
物理内存超过4GB的处理办法
此时必须使用CPU的扩展分页(PAE)模式提供的64位页目录才能取到4GB以上的物理内存
11.3 内存读取:
11.3.1 用户空间内存动态申请
申请:malloc()释放:free()
11.3.2 内核空间内存动态申请
1、相关函数关系:
kmalloc()与 - __get_free_pages()kmalloc()和__get_free_pages()申请的内存位于物理内存映射区,并且是连续的,它与真实的物理地址一般只差一个固定的偏移
vmalloc()
vmalloc()在虚拟地址空间给出一块连续的内存区,实质上这段连续的虚拟内存在物理内存中并不一定连续。也没简单的换算关系
2、具体函数详解
kmalloc(size_t size,int flags);参数
size:分配大小
flag:分配标识
GFP_KERNEL(最常用),在内核空间进程中申请内存
GFP_USER,用来为用户空间分配内存,可能阻塞
GFP_HINSTANCE,与GFP_USER类似但是是从高端内存分配
GFP_NOIO,不允许任何IO初始化
GFP_NOFS,不允许任何文件系统调用
__GFP_DMA,要求分配在能够DMA的内存区
__GFP_HINSTANCE,指示分配的内存区可以位于高端内存
注意:kmalloc()其实就是依赖于_get_free_pages()函数实现的
__get_free_pages(unsigned int flags,unsigned int order);
介绍:此宏是Linux内核本质上最底层用于获取空闲内存的方法
因为底层的伙伴算法总是以页的2的n次方为单位管理空闲内存,所以最底层的内存申请总是以页为单位的
参数
order:分配的页数是2^order
flags:同kmalloc
相关宏
get_zeroed_pages()(申请的同时将页清空)
__get_free_page()(申请一页,不清零)
上述两个宏在实现中调用了alloc_pages()函数
alloc_pages()既可以在内核空间分配,也可以在用户空间分配
对应的释放函数:free_page(s)()
__get_free_pages(unsigned int flags,unsigned int order); get_zeroed_page(unsigned int flags); __get_free_page(unsigned int flags); struct page*alloc_pages(int gfp_mask,unsigned long order); //__get_free_pages()函数对应的释放函数 void free_page(unsigned long addr); void free_pages(unsigned long addr,unsigned long order);
vmalloc函数
使用情况
一般只为存在于软件中(没有对应的硬件意义)的较大的顺序缓存区分配内存,其远大于__get_free_pages()的开销
其不能用于原子上下文中
因为其内部实现使用了标志为GFP_KERNEL的kmalloc()
vmalloc()的虚拟地址和物理地址不是一个简单的线性映射
因为vmolloc()在申请内存时,会进行内存的映射,改变页表项
void *vmalloc(unsigned long size); void vfree(void *addr);
slab与内存池:
引入slab的原因:
完全使用页为单元申请和释放内存容易导致浪费
在操作系统的运行过程中,经常涉及到对大量对象的重复生成、使用和释放问题,此时使用slab可以大大提高效率
实际上kmalloc()就是使用slab机制实现的
使用方法:
创建一个slab缓存:kmem_cache_create()
它可以保留任意数目且全部同样大小的后备缓存
参数
size:要分配的每个数据结构的大小
flags:控制如何进行分配的位掩码
SLAB_HWCACHE_ALIGN:每个数据对象呗对齐到一个缓存行
SLAB_CACHE_DMA:要求数据对象在DMA区域中的分配
分配slab缓存:kmem_cache_alloc();
创建的slab后备缓存中分配一块并返回首地址指针
释放slab缓存:kmem_cache_free();
释放由kmem_cache_alloc()函数分配的缓存
收回slab缓存:kmem_cache_destroy();
获知当前的slab的分配和使用情况:cat /proc/slabinfo
注意:slab不是要替代__get_free_pages(),其在最底层仍然依赖于__get_free_pages(),slab在底层每次申请1页或多页,之后在分隔这些页为更小的单元进行管理,从而节省了内存,也提高了slab缓冲对象的访问效率
//1.创建slab缓存 struct kmem_cache *kmem_cache_create(const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void*, struct kmem_cache*, unsigned long), void (*dtor)(void*,struct kmem_cache*, unsigned long)); //2.分配slab缓存 void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags); //3.释放slab函数 void kmem_cache_free(struct kmem_cache *cachep, void objp); //4.收回slab缓存 int kmem_cache_destroy(struct kmem_cache *cachep); //slab缓存使用范例: //创建slab缓存 static kmem_cache_t *xxx_cachep; xxx_cachep = kmem_cache_create("xxx", sizeof(struct xxx), 0, SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL, NULL); //分配slab缓存 struct xxx *ctx; ctx = kmem_cache_alloc(xxx_cachep, GFP_KERNEL); ... //释放slab缓存 kmem_cache_free(xxx_cachep, ctx); kmem_cache_destroy(xxx_cachep);
内核中,与内存池相关的操作包括以下几种
创建内存池:mempool_create();
参数
min_nr参数是需要预分配对象的数目
alloc_fn和free_fn是指向内存池基础值提供的标准对象分配和回收函数的指针
其原型分别为
mempool_alloc_t();
参数
pool_data:分配和回收函数用到的指针
gfp_mask:是分配标志,只有当__GFP_WAIT标志被指定时,分配函数才会休眠
mempool_free_t();
分配和回收函数
mempool_alloc();
用来分配对象,如果内存池分配器无法提供内存,那么久可以用预分配的池
mempool_free();
回收内存池:mempool_destroy();
由mempool_create()函数创建的内存池需要mempool_destroy()函数释放
//1.创建内存池 mempool_t *mempool_create(int min_nr, menpool_alloc_t *alloc_fn, menpool_free_t * free_fn, void *pool_data); //内存池机制提供的标准对象分配和回收函数指针的原型分别为 typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data); 和 typedef void (mempool_free_t)(void *element, void *pool_data); //2.分配和回收对象 void *mempool_alloc(mempool_t, int gfp_mask); void mempool_free(void *element, mempool_t *pool); //3.回收内存池 void mempool_destroy(mempool_t *pool);
11.4 设备I/O端口和I/O内存的访问
设备通常提供一组寄存器来控制设备,读写设备和获取设备状态,及控制寄存器、数据寄存器和状态寄存器。这些寄存器可能位于I/O空间中,也可能位于内存空间中。当位于I/O空间时候,通常称为I/O端口;但位于内存空间时,对应的内存空间称为I/O内存11.4.1 Linux I/O端口和I/O内存访问接口
1. I/O端口
Linux内核提供的访问位于I/O空间的端口丅述个函数中I/O端口号port高度依赖于具体的硬件平台,因此,这里只写出了unsigned
//1.读写字节端口(8位) unsigned inb(unsigned port); void outb(unsigned char byte, unsigned port); //2.读写字端口 unsigned inw(unsined port); void outw(unsigned short word, unsigned port); //3.读写长字端口 unsigned inl(unsigned port); void outl(unsigned longword, unsigned port); //4.读写一串字节 void insb(unsigned port, void *addr, unsigned long count); void outsb(unsigned port, void *addr, unsigned long count); //5.insb()从端口port开始读count个字节端口,并将读取结果接入addr指向的内存;outsb()将addr指向的内存中的count个字节连续的写入以port开始的端口 //6.读写一串字 void insw(unsigned port, void *addr unsigned long count); void outse(unsigned port, void *addr, unsignd long count); //7.读写一串长字 void insl(unsigned port, void *addr, unsigned log count); void outsl(unsigned port, void *addr, unsigned long count);
2. I/O内存
I/O内存通常是芯片内部的哥哥I2C、SPI、USB等控制器的寄存器后者外部内存总线的设备。在访问I/O内存之前首先要做的是将设备所处的物理地址映射到虚拟地址。//将物理地址映射到虚拟地址 void *ioremap(unsigned long offset,unsigned long size) //释放 void ionumap(void * addr); //ioremap()函数的变体函数devm_ioremap(),它不需要再驱动退出或者出错的时候进行iounmap(). void __iomem *devm_ioremap(struct device* dev, resource_size_t offset, unsigned long size);
在设备地址被映射到虚拟地址之后,尽管可以直接通过指针访问这些地址,但是Linux内核推荐使用一组标准的API开完成设备内存映射的虚拟地址的读写
I/O内存读写操作:
没有_relaxed后缀的版本与有_relaxed后缀的版本的区别是没有_relaxed后缀的版本包含一个内存屏障
//1.读I/O内存 #define readb(c) ({u8 __v = readb_relaxed(c);__iomb();__v;}) #define readw(c) ({u16 __v = readb_relaxed(c);__iomb();__v;}) #define readl(c) ({u32 __v = readb_relaxed(c);__iomb();__v;}) //2.写I/O内存 #define writeb(v,c) ({u8 __v = readb_relaxed(c);__iomb();__v;}) #define readw(v,c) ({u16 __v = readb_relaxed(c);__iomb();__v;}) #define readl(v,c) ({u32 __v = readb_relaxed(c);__iomb();__v;})
11.4.2 申请和释放设备I/O端口和内存
1. I/O端口申请
linux内核提供一组函数申请和释放I/O端口,表明该驱动要访问这片区域申请:request_region()
这个函数向内核申请n个端口,这些端口从first开始,name参数为设备的名字
返回值
成功:返回非NULL
失败:返回NULL
释放I/O端口:release_region()
struct resource *request_region(unsigned long first,unsigned long n,const char*name); void release_region(unsigned long start,unsigned long n);
2. I/O 内存申请
Linux内核已提供了一组函数以申请和释放I/O内存的范围,此处的“申请”表明,该驱动要访问这片区域,他不会做任何内存映射的动作内存申请:request_mem_region()
这个函数向内核申请n个内存地址,这个地址从first开始,name是设备参数的名称
返回值
成功:返回值非NULL
失败:返回NULL
内存释放:release_mem_region()
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name); void release_mem_region(unsigned long start, unsigned long len);
11.4.3 设备I/O端口和I/O内存访问流程
I/O端口的访问:request_region():在设备打开或驱动模块被加载时申请I/O端口区域
inb()/outb():使用inb()、outb()等函数进行访问
release_region():在设备关闭或驱动被卸载时释放I/O端口范围
I/O内存访问步骤
request_mem_region():首先申请资源
ioremap():将寄存器地址通过映射到内核空间虚拟地址
readb/readl/writeb/writel:通过这些函数访问设备的寄存器
iounmap()+release_mem_region():访问完成之后,对ioremap()申请的虚拟地址进行释放,并释放release_mem_region()申请的I/O内存资源
11.4.4 将设备地址映射到用户空间
1.内存映射与VMA
一般情况下用户空间是不能也不应该直接访问设备但是我们可以通过在设备驱动程序中实现mmap()函数,这个函数可以使得用户空间能直接访问物理设备mmap()
函数的实质
将用户空间的一段内存与设备内存关联,当用户访问用户空间的这段地址范围时,实际上会转化为对应的设备的访问。
mmap()函数必须以页为单位进行映射
驱动程序mmap()的实现机制也是建立页表
驱动中的mmap()函数将在用户空间进行mmap()系统调用时被调用
参数
fd:文件描述符
len:是映射到用户空间的字节数
prot:指定访问权限
PROT_READ
PROT_WRITE
PROT_EXEC
PROT_NONE
caddr:指定文件应该被映射的起始地址,一般被指定为NULL,由内核分配
//驱动中mmap()函数原型: int (*mmap)(struct file *,struct vm_area_struct *); //用户空间的mmap()函数原型: caddr_t mmap(caddr_t addr,size_t len,int prot,int flags,int fd,off_t offset);
用户调用mmap()时所进行的工作
(1) 在进程的虚拟地址空间查找一块VMA
(2) 将这块VMA进行映射
(3) 如果设备驱动程序或者文件系统的file_operations定义了mmap()操作则调用它
(4) 将这个VMA插入到进程的VMA表中
VMA结构体
虚拟地址的描述通过VMA结构体来实现
VMA结构体描述的虚拟地址位于vm_start ~vm_end之间
struct vm_area_struct { struct mm_struct *vm_mm;/*所处的地址空间*/ unsigned long vm_start;/*开始虚拟地址*/ unsigned long vm_end;/*结束虚拟地址*/ pgprot_t vm_page_prot;/*访问权限*/ unsigned long vm_flags;/*标识,VM_READ,WM_WRITE,VM_EXEC,VM_SHARED*/ ... /*VMA的函数的指针*/ struct vm_operations_struct *vm_ops; unsigned long vm_pgoff;/*偏移(页帧号)*/ struct file *vm_file; void *vm_private_data; .... };
vm_operations_structk结构体
此结构体描述了对VMA的相关操作
当用户进行系统调用mmap()后,内核不会调用VMA的open函数,
通常需要在驱动的mmap()函数中显示调用vma->vm_ops->open()
struct vm_operations_struct{ void(*open)(struct vm_area_struct *area);/*打开vma的操作*/ void (*close)(..) struct page*(nopage)(...)/*访问的页不存在时调用*/ ... };
vm_operations_struct操作范例
重要函数
remap_pfn_range()
作用
用来创建页表项
以VMA结构体的成员作为remap_pfn_range()的参数
VMA的数据成员是内核根据用户的请求自己填充的
映射的虚拟地址范围是vma->vm_start至vma->vm_end
映射内存中的保留页、设备I/O、framebuffer、camera等内存
参数
addr:表示内存映射开始处的虚拟地址
pfn:是虚拟地址应该映射到的物理地址的页帧号
实际上就是物理地址右移PAGE_SHIFT位
页帧号:内核地址无论是虚拟的还是物理的,都是由两部分构成,往往是高N位为页号,低M位为页内偏移量。当我们将地址中的低M位偏移量抛弃时,高N位移动到右端得到这个结果称为页帧号,宏PAGE_SHIFT告诉我们要右移多少位才能得到页帧号
port:新页所要求的保护属性
static int xxx_map(struct file*filp,struct vm_area_struct *vma) { /*建立页表*/ if(remap_pfn_range(vma,vma->start,vm->vm_pgoff, vm->vm_end-vma->start,vma->page_prot)) return -EAGAIN; vma->ops = &xxx_remap_vmops; xxx_vma_open(vma); return 0; } /*vma打开函数*/ void xxx_vm_open(struct vm_area_struct *vma) { ... printk(KERNEL "xxx VMA open,virt %1x,phys %1x\n",vma->vm_start, vma->vm_pgoff<<PAGE_SHIFT); } /*vma关闭函数*/ void xxx_vma_close(struct vm_area_struct *vma) { ... printk(KERN_NOTICE "xxx VMA close.\n"); } static struct vm_operations_struct xxx_remap_vm_ops = { /*VMA操作结构体*/ .open = xxx_vm_open, .close = xxx_vma_close, ... };
//remap_pfn_range函数原型 remap_pfn_range(struct vm_area_struct *vma,unsigned long addr,unsigned long pfn,unsigned long size,pgprot_t prot);
例程:映射kmalloc申请的内存到用户空间
注意:通常I/O内存被映射时需要nocahe的,这个时候需要对vma_page_prot设置nocache标识之后再进行映射
/*内核模块加载函数*/ int __init kmalloc_map_init(void) { ... /*申请设备号 添加cdev结构体*/ buffer = kmalloc(BUFSIZE,GFP_KERNEL);//申请buffer /*virt_to_page,获取对应的虚拟页*/ for(page = virt_to_page(buffer); page<vir_to_page(buffer+BUFSIZE); page++) mem_map_reverse(page);/*设置为保留页*/ } /*mmap()函数*/ static int kmalloc_map_mmap(struct file*filp,struct vm_area_struct *vma) { unsigned long page,pos; unsigned long start = (unsigned long)vma->vm_start; unsigned long size = (unsigned long)(vma->vm_end-vma->vm_start); printk(KERNEL_INFO "mmaptest_mmap called\n"); /*用户要映射的区域太大*/ if(size>BUFSIZE) return -EINVAL; pos = (unsigned long)buffer; /*映射buffer中的所有页*/ while(size > 0){ /*每次映射一页*/ page = virt_to_phys((void *)pos);//先将在内核中用malloc分配的空间转换为对应的物理页地址 if(remap_page_range(start,page,PAGE_SIZE,PAGE_SHARED));/*将物理页地址映射到vma,并且每次只映射一页*/ return - EAGAIN; start += PAGE_SIZE; pos +=PAGE_SIZE; size -=PAGE_SIZE; }return 0; }
2. fault()函数
简介:除了remap_pfn_range函数以外,在驱动程序中实现VMA的fault()函数可以为设备提供,更加灵活的映射途径,当访问的页不存在(发生缺页异常)时,fault()会被内核自动调用当发生缺页异常时系统做出的响应:
(1).找到缺页的虚拟地址所在的VMA
(2).如果不要,分配中间页目录表和页表
(3).如果也表项不存在,调用VMA的nopage()方法,返回物理页面的描述符
(4).将物理页面的地址填充到页表中
fault()函数使用范例
static int xxx_fault(struct vm_area_struct *vm, struct vm_fault *vmf) { unsigned log paddr; unsigned long pn; pgoff_t index = vmf->pgoff; struct vma_data *vdata = vma->vm_private_data; ... pfn = paddr >> PAGE_SHIFT; vm_inset_pfn(vam, (unsigned long)vmf->virtual_address, pfn); return VM_FAULT_NOPAGE; }
11.5 I/O内存的静态映射
简介:假如我们已经做好目标电路板,而要将Linux移植到目标电路板,此时通常会建立外I/O内存物理地址到虚拟地址的静态映射,这个映射通过在与电路板对应的map_desc结构体数组中添加新的成员来完成map_desc结构体
struct map_desc{ unsigned long virtual;//虚拟地址 unsigned long pfn; //__phys_to_phn(phy_addr) unsigned long length; //大小 unsigned int type; //类型 };
11.6 DMA(重点)
DMA:是一种无序CPU帮助就可以让外设与系统之间进行双向数据传输的硬件机制简单点说就是这个样子 外设<———->内存而不是传统的 外设<—-cpu—–>内存
DMA与Cache的一致性问题:
假设DMA针对内存的目的地址与Cache缓存的对象有重叠区域,那么经过DMA操作后,Cache缓存对应的内存的数据就会被修改,而CPU却并不知道,它仍然会认为Cache中的数据就是内存中的数据,此时会产生Cache与内存之间的数据”不一致”错误
DMA与Cache的一致性实质
在采用Cache的系统中,同样一个数据可能存在于Cache中,也可能存在于主存中,当Cache中的数据与主存中的一样时则具有一致性,否则数据具有不一致性
出现Cache的一致性问题的情况
DMA与Cache的一致性问题
Cache使能和关闭的时刻
在具有MMU的ARM中,开启MMU前要先置Cache无效
11.6.2 Linux下的DMA编程
内存中用于与外设交互数据的一块区域被称为DMA缓冲区,一般情况下DMA在物理上连续的1. DMA 区域
对于X86系统的ISA设备而言,DMA操作只能在16MB一下的内存中使用,因此在用kmalloc()和__get_free_pages()及类似的函数申请DMA缓冲区时应使用GFP_DMA标志,这样获得的DMA区域是具备DMA能力的
Linux内核已经把此操作为我们封装好了
__get_dma_page():它在申请时已经添加了GFP_DMA标志
函数是以2^order为大小分配的DMA内存
#define __get_dma_pages(gfp_mask,order)\ __get_free_pages((gfp_mask)|GFP_DMA,(order))
如果不想用order为参数来申请DMA内存,则可以用下面的函数
static unsigend long dma_mem_alloc(int size);
注意:大多数嵌入式设备而言,DMA操作可以在整个常规内存区域进行,因此DMA区域就直接覆盖了常规的内存了
2.虚拟地址、物理地址和总线地址
基于DMA的硬件使用的是总线地址而不是物理地址总线地址:是从设备的角度上看到的内存地址
物理地址:是从CPU MMU控制器外围角度上看到的内存地址
Linux内核提供如下函数用于简单的虚拟地址/总线地址的转换
在使用IOMMU或反弹缓冲区的情况下,下述函数不会正常工作
这两个函数是不被建议使用的
unsigned long virt_to_bus(volate void *address); void *bus_to_virt(unsigned long address);
3.DMA地址掩码
设备不一定在所有的内存地址上执行DMA操作,此时应该通过下列函数执行DMA地址掩码int dma_set_mask(struct device*dev,u64 mask);
- 例如:对于只能在24位地址上执行DMA操作的设备,就应该使用如下方法 dma_set_mask(dev,0xffffff)
该API的本质就是修改device结构体的dma_mask成员
dma_mask:设备DMA可以寻址的范围
4.一致性DMA缓冲区
DMA缓冲区包括两个方面的工作:1.分配一片DMA缓冲区(为这片缓冲区产生设备可以访问的地址)
2.DMA映射必须考虑Cache一致性问题
内核提供如下函数用于分配就一个DMA一致性的内存区域
ama_alloc_coherent()函数
参数handle返回DMA缓冲区的总线地址,其类型为dma_addr_t,表示总线地址
返回值:为申请到的DMA缓冲区地址
void *ama_alloc_coherent(struct device *dev,size_t size,dma_addr_t handle,gfp_t gfp)
与ama_alloc_coherent()函数对应的释放函数
void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle);
分配一个写合并的DMA缓冲区函数与其释放函数
void * dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp); #definedma_free_writecombine(dev,size,cpu_addr,handle) dma_free_coherent(dev,size,cpu_addr,handle)
PCI设备申请DMA缓冲区的函数
void * pci_alloc_consistent(struct pci_dev *pdev, size_t size, dma_addr_t *dma_addrp); void pci_free_consistent(struct pci_dev *pdev, size_t size, void *cpu_addr, dma_addr_t dma_addr);
注意:dma_alloc_xxx()函数虽然是以dma_alloc_开头的,但是其申请 的区域比一定在DMA区域里面,例如,以32ARM为例,当coherent_dma_mask小于0xfffffff时,才会设置GFP_DMA标志,并从DMA区域去申请内存
CMA机制:不预留内存,这些内存平时是可用的,只有当需要的时候,才被分配给camera、HDMI等设备使用
其对上呈现的接口是标准的DMA,也是一致性缓冲区API
5. 流式DMA映射
并不是所有的DMA缓冲区的申请都是驱动申请的,如果是驱动申请的,用一致性的DMA缓冲区自然最方便,这直接考虑了DMA的一致性问题。但是,在许多情况下,缓冲区来自内核的上层(如网卡驱动中的网络报文、块设备驱动中要写入的数据等),上层很可能用普通的kmalloc()、__get_free_pages()等方法来申请,这时候就要使用流式DMA映射流式DMA的工作步骤
进行流式DMA映射
执行DMA操作
进行流式DMA去映射
流式DMA本质上是进行Cache的使无效或清除操作,以解决Cache的一致性问题
流式DMA映射的接口
实现DMA的映射函数:dma_map_single()
参数
direction:DMA的方向
DMA_TO_DEVICE
DMA_FROM_DEVICE
DMA_BIDIRECTIONAL
DMA_NONE
返回值
成功:返回总线的地址
失败:返回NULL
反映射函数:dma_unmap_single()
dma_addr_t dma_map_single(struct device *dev,void *bufer, size_t size, enum dma_data_direction direction); void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction direction);
6. dmaengine标准API
Linux内核目前推荐使用dmaengine的驱动来编写DMA控制器的驱动,同时外设的驱动使用标准的dmaengine API进行DMA的准备、发起和完成时的回调工作在使用DMA前,设备驱动程序需要首相向dmaengine系统申请DMA通道
申请DMA通道的函数:dma_request_slave_channel();
struct dma_chan *dma_request_slave_channel(struct device *dev, const char *name); struct dma_chan *__dma_request_channel(const dma_cap_mask_t *mask, dma_filter_fn fn, void *fn_param);
释放申请的通道
void dma_release_chammel(struct dma_chan* chan);
例程:利用dmaengine初始化并发起一次DMA操作
static void xxx_dma_fini_callback(void *data) { struct completion *dma_complete = data; complete(dma_complete); } issue_xxx_dma(...) { rx_desc = dmaengine_prep_slave_single(xxx->rx_chan,xxx->dst_start, t->len, DMA_DEV_TO_MEN, DMA_PREP_INTERRUPT | DMA_CTRL_ACK); rx_desc->callback = xxx_dma_fini_callback; rx_desc-。callback-param = &xxx->rx_done; dmaengine_submit(rx_desc); dma_async_issue_pending(xxx->rx_chan); }
相关文章推荐
- 浅析μC/OS-Ⅱ设备驱动的设计与实现
- linux驱动开发之key
- 零基础Android手机嵌入式开发实战课程(网吧计费系统、多功能播放器、驱动开发)
- 小白探访WINDOWS下的驱动(五) 第一个HELLO驱动(插曲-调试环境设置HOST&TARGET)
- Win7 环境下如何配置驱动开发环境
- USB开发—自上而下(二)
- USB开发—自上而下(四)
- Server2012R2下安装intel 82579v网卡驱动
- [内核驱动]起步
- Android 驱动开发系列
- linux设备驱动开发详解(基于4.0内核)_读书笔记一
- linux设备驱动开发详解(基于4.0内核)_读书笔记二
- FS_S5PC100平台上Linux Camera驱动开发详解(二)
- ubuntu 14.04 无线网卡驱动安装
- Linux设备驱动开发——环境搭建
- MTK平台闪光灯驱动分析
- sys目录与proc目录
- 南桥下GPIO驱动开发。
- 详解Linux2.6内核中基于platform机制的驱动模型
- RTX系统下对PCI设备的驱动开发